Compare commits
112 commits
v0.53.2-al
...
master
Author | SHA1 | Date | |
---|---|---|---|
|
d14c9141db | ||
|
06c350c4db | ||
|
c3a9d9d002 | ||
|
aeada6dc74 | ||
|
6ba985fd28 | ||
|
2a0bc85738 | ||
|
523ea284a2 | ||
|
a66d7534c2 | ||
|
89ec07622f | ||
|
7e6ad31392 | ||
|
4ab23f03fc | ||
|
29cea5cc07 | ||
|
8dd7150d67 | ||
|
f1b1523017 | ||
|
88ac250fee | ||
|
0a5e9e87ed | ||
|
20413d79b6 | ||
|
ff9011e6ac | ||
|
802139d0a4 | ||
|
68d307fa50 | ||
|
d3900e39b6 | ||
|
35769dede6 | ||
|
ae1e20d131 | ||
|
051af8b6ad | ||
|
5d77b115f9 | ||
|
7dbeeac112 | ||
|
de062c4aee | ||
|
18a3336714 | ||
|
ebf35a1df8 | ||
|
28e168d5e5 | ||
|
7ad66b99e7 | ||
|
7eb7c1a5ff | ||
|
b88e704e6b | ||
|
8d85af8064 | ||
|
f9d7340729 | ||
|
d57300f785 | ||
|
09baf1d9b9 | ||
|
ce692d38ea | ||
|
b1ca3b0183 | ||
|
a4c34d89e2 | ||
|
d7b9ca3391 | ||
|
55a5c7b051 | ||
|
0e2a9a1033 | ||
|
3fd38be789 | ||
|
329d434c83 | ||
|
6a2939d9fc | ||
|
65781e33f7 | ||
|
608721c7ac | ||
|
2846dd926b | ||
|
7a8a16cd9c | ||
|
1c17ff5dd9 | ||
|
b9be8d9f3a | ||
|
6b1069f02a | ||
|
b92fb03856 | ||
|
1a9743e639 | ||
|
2773cbbe6e | ||
|
e11fb5d225 | ||
|
38200b9912 | ||
|
c69826a887 | ||
|
ce4fadbdf9 | ||
|
2895e93323 | ||
|
3859124c05 | ||
|
da5ec6edc1 | ||
|
9825bccf4a | ||
|
f065218ff4 | ||
|
f79c622edf | ||
|
c7ab47f54d | ||
|
8c10617259 | ||
|
2e565fd95b | ||
|
68718f32b2 | ||
|
f70bde0639 | ||
|
2be96a25b1 | ||
|
30cbc3f5c5 | ||
|
8d8c1fd58c | ||
|
d8600e286f | ||
|
7d08800836 | ||
|
27ede86996 | ||
|
60e5471f5e | ||
|
a1e52eea4a | ||
|
5a99d9777f | ||
|
fab69450c0 | ||
|
5609b43fc7 | ||
|
cc9f2e62de | ||
|
dd6a156d7c | ||
|
7b0d38eca7 | ||
|
168ae17eb6 | ||
|
0067d5a411 | ||
|
99ceaadf8b | ||
|
743c75df16 | ||
|
c5b018afc3 | ||
|
c7511fc803 | ||
|
17bd0eec30 | ||
|
5c6f7a391b | ||
|
d841835c9d | ||
|
3c3635977e | ||
|
d69eeaa589 | ||
|
8a9af7d354 | ||
|
6108860063 | ||
|
63ce691b90 | ||
|
de825fd4dc | ||
|
3671e855cb | ||
|
efa682ef02 | ||
|
5319232918 | ||
|
c5b7cc5ac4 | ||
|
02e4b651af | ||
|
34ea712874 | ||
|
562e154675 | ||
|
5b4948891e | ||
|
5ed13de5d6 | ||
|
9e48d22d70 | ||
|
9f40680b64 | ||
|
addcd63794 |
273 changed files with 5464 additions and 7270 deletions
|
@ -16,7 +16,7 @@ COMMENT_SERVER_NAME=Odysee
|
||||||
SEARCH_SERVER_API=https://lighthouse.odysee.com/search
|
SEARCH_SERVER_API=https://lighthouse.odysee.com/search
|
||||||
SOCKETY_SERVER_API=wss://sockety.odysee.com/ws
|
SOCKETY_SERVER_API=wss://sockety.odysee.com/ws
|
||||||
THUMBNAIL_CDN_URL=https://image-processor.vanwanet.com/optimize/
|
THUMBNAIL_CDN_URL=https://image-processor.vanwanet.com/optimize/
|
||||||
WELCOME_VERSION=1.1
|
WELCOME_VERSION=1.2
|
||||||
|
|
||||||
# STRIPE
|
# STRIPE
|
||||||
# STRIPE_PUBLIC_KEY='pk_test_NoL1JWL7i1ipfhVId5KfDZgo'
|
# STRIPE_PUBLIC_KEY='pk_test_NoL1JWL7i1ipfhVId5KfDZgo'
|
||||||
|
@ -26,16 +26,15 @@ MATOMO_URL=https://analytics.lbry.com/
|
||||||
MATOMO_ID=4
|
MATOMO_ID=4
|
||||||
|
|
||||||
# OG
|
# OG
|
||||||
OG_TITLE_SUFFIX=| lbry.com
|
OG_TITLE_SUFFIX=| lbry.tv
|
||||||
OG_HOMEPAGE_TITLE=lbry.com
|
OG_HOMEPAGE_TITLE=lbry.tv
|
||||||
OG_IMAGE_URL=
|
OG_IMAGE_URL=
|
||||||
SITE_CANONICAL_URL=https://lbry.com
|
SITE_CANONICAL_URL=https://lbry.tv
|
||||||
|
|
||||||
# UI
|
# UI
|
||||||
## Custom Site info
|
## Custom Site info
|
||||||
DOMAIN=lbry.com
|
DOMAIN=lbry.tv
|
||||||
CLOUD_DOMAIN=odysee.com
|
URL=https://lbry.tv
|
||||||
URL=https://lbry.com
|
|
||||||
SITE_TITLE=LBRY
|
SITE_TITLE=LBRY
|
||||||
SITE_NAME=LBRY
|
SITE_NAME=LBRY
|
||||||
SITE_DESCRIPTION=Meet LBRY, an open, free, and community-controlled content wonderland.
|
SITE_DESCRIPTION=Meet LBRY, an open, free, and community-controlled content wonderland.
|
||||||
|
|
19
.github/workflows/deploy.yml
vendored
19
.github/workflows/deploy.yml
vendored
|
@ -38,7 +38,22 @@ jobs:
|
||||||
- uses: maxim-lobanov/setup-xcode@v1
|
- uses: maxim-lobanov/setup-xcode@v1
|
||||||
if: startsWith(runner.os, 'mac')
|
if: startsWith(runner.os, 'mac')
|
||||||
with:
|
with:
|
||||||
xcode-version: '12.4.0'
|
xcode-version: '13.1.0'
|
||||||
|
# This is gonna be hacky.
|
||||||
|
# Github made us upgrade xcode, which would force an upgrade of electron-builder to fix mac.
|
||||||
|
# But there were bugs with copyfiles / extraFiles that kept seeing duplicates erroring on ln.
|
||||||
|
# A flag USE_HARD_LINKS=false in electron-builder.json was suggested in comments, but that broke windows builds.
|
||||||
|
# So for now we'll install python2 on mac and make sure it can find it.
|
||||||
|
# Remove this after successfully upgrading electron-builder.
|
||||||
|
# HACK part 1
|
||||||
|
- uses: Homebrew/actions/setup-homebrew@master
|
||||||
|
if: startsWith(runner.os, 'mac')
|
||||||
|
# HACK part 2
|
||||||
|
- name: Install Python2
|
||||||
|
if: startsWith(runner.os, 'mac')
|
||||||
|
run: |
|
||||||
|
/bin/bash -c "$(curl -fsSL https://github.com/alfredapp/dependency-scripts/raw/main/scripts/install-python2.sh)"
|
||||||
|
echo "PYTHON_PATH=/usr/local/bin/python" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Download blockchain headers
|
- name: Download blockchain headers
|
||||||
run: |
|
run: |
|
||||||
|
@ -58,7 +73,7 @@ jobs:
|
||||||
WIN_CSC_KEY_PASSWORD: ${{ secrets.WIN_CSC_KEY_PASSWORD }}
|
WIN_CSC_KEY_PASSWORD: ${{ secrets.WIN_CSC_KEY_PASSWORD }}
|
||||||
CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
|
CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
|
||||||
|
|
||||||
WIN_CSC_LINK: https://raw.githubusercontent.com/lbryio/lbry-desktop/master/build/cert-2021-2022.pfx
|
WIN_CSC_LINK: https://raw.githubusercontent.com/lbryio/lbry-desktop/master/build/cert2023.pfx
|
||||||
CSC_LINK: https://s3.amazonaws.com/files.lbry.io/cert/osx-csc-2021-2022.p12
|
CSC_LINK: https://s3.amazonaws.com/files.lbry.io/cert/osx-csc-2021-2022.p12
|
||||||
|
|
||||||
# UI
|
# UI
|
||||||
|
|
0
.yarn/versions/17d7e90d.yml
vendored
Normal file
0
.yarn/versions/17d7e90d.yml
vendored
Normal file
0
.yarn/versions/33178102.yml
vendored
Normal file
0
.yarn/versions/33178102.yml
vendored
Normal file
0
.yarn/versions/35f2125e.yml
vendored
Normal file
0
.yarn/versions/35f2125e.yml
vendored
Normal file
0
.yarn/versions/4f9fb046.yml
vendored
Normal file
0
.yarn/versions/4f9fb046.yml
vendored
Normal file
0
.yarn/versions/5bc94294.yml
vendored
Normal file
0
.yarn/versions/5bc94294.yml
vendored
Normal file
0
.yarn/versions/5f1212ad.yml
vendored
Normal file
0
.yarn/versions/5f1212ad.yml
vendored
Normal file
0
.yarn/versions/5f4cac99.yml
vendored
Normal file
0
.yarn/versions/5f4cac99.yml
vendored
Normal file
0
.yarn/versions/6b35c994.yml
vendored
Normal file
0
.yarn/versions/6b35c994.yml
vendored
Normal file
0
.yarn/versions/6be5ab70.yml
vendored
Normal file
0
.yarn/versions/6be5ab70.yml
vendored
Normal file
0
.yarn/versions/86ac1afd.yml
vendored
Normal file
0
.yarn/versions/86ac1afd.yml
vendored
Normal file
0
.yarn/versions/8e384637.yml
vendored
Normal file
0
.yarn/versions/8e384637.yml
vendored
Normal file
0
.yarn/versions/909c3734.yml
vendored
Normal file
0
.yarn/versions/909c3734.yml
vendored
Normal file
0
.yarn/versions/951a8d12.yml
vendored
Normal file
0
.yarn/versions/951a8d12.yml
vendored
Normal file
0
.yarn/versions/ac69bc5f.yml
vendored
Normal file
0
.yarn/versions/ac69bc5f.yml
vendored
Normal file
0
.yarn/versions/c6e2b914.yml
vendored
Normal file
0
.yarn/versions/c6e2b914.yml
vendored
Normal file
0
.yarn/versions/d1a18cef.yml
vendored
Normal file
0
.yarn/versions/d1a18cef.yml
vendored
Normal file
0
.yarn/versions/ec3a9ddf.yml
vendored
Normal file
0
.yarn/versions/ec3a9ddf.yml
vendored
Normal file
0
.yarn/versions/fc1fde84.yml
vendored
Normal file
0
.yarn/versions/fc1fde84.yml
vendored
Normal file
0
.yarn/versions/fc597c00.yml
vendored
Normal file
0
.yarn/versions/fc597c00.yml
vendored
Normal file
94
CHANGELOG.md
94
CHANGELOG.md
|
@ -1,8 +1,98 @@
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||||
|
|
||||||
|
## [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
|
||||||
|
- Checkbox to disable background wallpaper ([#7630](https://github.com/lbryio/lbry-desktop/pull/7630))
|
||||||
|
- Handle content blocking from hub ([#7665](https://github.com/lbryio/lbry-desktop/pull/7665))
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Better handle decimals liquidating supports ([#7648](https://github.com/lbryio/lbry-desktop/pull/7648))
|
||||||
|
- Better handle cover uploads ([#7647](https://github.com/lbryio/lbry-desktop/pull/7647))
|
||||||
|
- Use default path when first choosing file on windows ([#7625](https://github.com/lbryio/lbry-desktop/pull/7625))
|
||||||
|
- Emoji button hover ([#7620](https://github.com/lbryio/lbry-desktop/pull/7620))
|
||||||
|
- Prevent infinite retries on thumbs ([#7618](https://github.com/lbryio/lbry-desktop/pull/7618))
|
||||||
|
- Double splash/error on app startup ([#7615](https://github.com/lbryio/lbry-desktop/pull/7615))
|
||||||
|
- App updates are now more coherent, also debs work. ([#7502](https://github.com/lbryio/lbry-desktop/pull/7502))
|
||||||
|
- Better handle many channels moderation calls at startup ([#7674](https://github.com/lbryio/lbry-desktop/pull/7674))
|
||||||
|
- Fix mobile floating viewer position ([#7677](https://github.com/lbryio/lbry-desktop/pull/7677))
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Upgraded Electron to v15.5.5 ([#7614](https://github.com/lbryio/lbry-desktop/pull/7614))
|
||||||
|
- Upgraded to lbrynet v0.110.0 ([#7680](https://github.com/lbryio/lbry-desktop/pull/7680))
|
||||||
|
|
||||||
|
|
||||||
|
## [0.53.4] - [2022-06-10]
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Add top in language category for non-english on homepage ([#7585](https://github.com/lbryio/lbry-desktop/pull/7585))
|
||||||
|
- Auto hosting in settings and hosting first run page ([#7598](https://github.com/lbryio/lbry-desktop/pull/7598))
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Updated lbry-sdk to [0.107.2](https://github.com/lbryio/lbry-sdk/releases/tag/v0.107.2)
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Better handle empty collections ([#7571](https://github.com/lbryio/lbry-desktop/pull/7571))
|
||||||
|
- Better handle thumbnails in uploads/collections ([#7574](https://github.com/lbryio/lbry-desktop/pull/7574))
|
||||||
|
- Work towards supporting collections of any claim type ([#7578](https://github.com/lbryio/lbry-desktop/pull/7578))
|
||||||
|
- Improve handling of downed custom servers on startup ([#7593](https://github.com/lbryio/lbry-desktop/pull/7593))
|
||||||
|
- Hide watch progress in related if being played ([#7606](https://github.com/lbryio/lbry-desktop/pull/7606))
|
||||||
|
- IPC disk space calls wait for daemon ready; refresh on vis. component load ([#7610](https://github.com/lbryio/lbry-desktop/pull/7610))
|
||||||
|
|
||||||
|
## [0.53.3] - [2022-04-27]
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Reverted lbry.tv changes that broke production login ([#7569](https://github.com/lbryio/lbry-desktop/pull/7569))
|
||||||
|
- Reverted lbry.tv changes that broke login ([#7570](https://github.com/lbryio/lbry-desktop/pull/7570))
|
||||||
|
|
||||||
## [0.53.2] - [2022-04-26]
|
## [0.53.2] - [2022-04-26]
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
@ -10,7 +100,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||||
- Removed some lbrytv references ([#7560](https://github.com/lbryio/lbry-desktop/pull/7560))
|
- Removed some lbrytv references ([#7560](https://github.com/lbryio/lbry-desktop/pull/7560))
|
||||||
- Removed some lbrytv player references ([#7552](https://github.com/lbryio/lbry-desktop/pull/7552))
|
- Removed some lbrytv player references ([#7552](https://github.com/lbryio/lbry-desktop/pull/7552))
|
||||||
|
|
||||||
## Fixed
|
### Fixed
|
||||||
- Repost style issues ([#7559](https://github.com/lbryio/lbry-desktop/pull/7559))
|
- Repost style issues ([#7559](https://github.com/lbryio/lbry-desktop/pull/7559))
|
||||||
- Disappearing sidebar thumbs ([#7556](https://github.com/lbryio/lbry-desktop/pull/7556))
|
- Disappearing sidebar thumbs ([#7556](https://github.com/lbryio/lbry-desktop/pull/7556))
|
||||||
- Restore tags sidebar link ([#7555](https://github.com/lbryio/lbry-desktop/pull/7555))
|
- Restore tags sidebar link ([#7555](https://github.com/lbryio/lbry-desktop/pull/7555))
|
||||||
|
|
10
README.md
10
README.md
|
@ -65,21 +65,19 @@ _Note: If coming from a deb install, the directory structure is different and yo
|
||||||
|
|
||||||
| | Flatpak | Arch | Nixpkgs | ARM/ARM64 |
|
| | Flatpak | Arch | Nixpkgs | ARM/ARM64 |
|
||||||
| -------------- | ----------------------------------------------------------------- | ----------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- | ------------------------------------------- |
|
| -------------- | ----------------------------------------------------------------- | ----------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- | ------------------------------------------- |
|
||||||
| Latest Release | [FlatHub Page](https://flathub.org/apps/details/io.lbry.lbry-app) | [AUR Package](https://aur.archlinux.org/packages/lbry-app-bin/) | [Nixpkgs](https://search.nixos.org/packages?channel=unstable&show=lbry&query=lbry) | [Build Guide](https://lbry.tv/@LBRYarm:5) |
|
| Latest Release | [FlatHub Page](https://flathub.org/apps/details/io.lbry.lbry-app) | [AUR Package](https://aur.archlinux.org/packages/lbry-desktop-bin/) | [Nixpkgs](https://search.nixos.org/packages?channel=unstable&show=lbry&query=lbry) | [Build Guide](https://lbry.tv/@LBRYarm:5) |
|
||||||
| Maintainers | [@kcSeb](https://keybase.io/kcseb) | [@kcSeb](https://keybase.io/kcseb) | [@Enderger](https://github.com/enderger) | [@Madiator2011](https://github.com/kodxana) |
|
| Maintainers | N/A | [@RubenKelevra](https://github.com/RubenKelevra) | [@Enderger](https://github.com/enderger) | [@Madiator2011](https://github.com/kodxana) |
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
Double click the installed application to interact with the LBRY network.
|
Start the installed application to interact with the LBRY network.
|
||||||
|
|
||||||
## Running from Source
|
## Running from Source
|
||||||
|
|
||||||
You can run the web version (lbry.tv), the electron app, or both at the same time.
|
|
||||||
|
|
||||||
#### Prerequisites
|
#### Prerequisites
|
||||||
|
|
||||||
- [Git](https://git-scm.com/downloads)
|
- [Git](https://git-scm.com/downloads)
|
||||||
- [Node.js](https://nodejs.org/en/download/) (v14 required)
|
- [Node.js](https://nodejs.org/en/download/) (v16 required)
|
||||||
- [Corepack](https://nodejs.org/dist/latest-v17.x/docs/api/corepack.html) `npm i -g corepack` (Included in nodejs 14 LTS, 16 LTS and 17)
|
- [Corepack](https://nodejs.org/dist/latest-v17.x/docs/api/corepack.html) `npm i -g corepack` (Included in nodejs 14 LTS, 16 LTS and 17)
|
||||||
- [Yarn](https://yarnpkg.com/en/docs/install)
|
- [Yarn](https://yarnpkg.com/en/docs/install)
|
||||||
|
|
||||||
|
|
BIN
build/cert2023.pfx
Normal file
BIN
build/cert2023.pfx
Normal file
Binary file not shown.
|
@ -21,7 +21,6 @@ const config = {
|
||||||
WELCOME_VERSION: process.env.WELCOME_VERSION,
|
WELCOME_VERSION: process.env.WELCOME_VERSION,
|
||||||
DOMAIN: process.env.DOMAIN,
|
DOMAIN: process.env.DOMAIN,
|
||||||
SHARE_DOMAIN_URL: process.env.SHARE_DOMAIN_URL,
|
SHARE_DOMAIN_URL: process.env.SHARE_DOMAIN_URL,
|
||||||
CLOUD_DOMAIN: process.env.CLOUD_DOMAIN,
|
|
||||||
URL: process.env.URL,
|
URL: process.env.URL,
|
||||||
THUMBNAIL_CDN_URL: process.env.THUMBNAIL_CDN_URL,
|
THUMBNAIL_CDN_URL: process.env.THUMBNAIL_CDN_URL,
|
||||||
SITE_TITLE: process.env.SITE_TITLE,
|
SITE_TITLE: process.env.SITE_TITLE,
|
||||||
|
|
|
@ -29,6 +29,10 @@
|
||||||
"from": "./static/font",
|
"from": "./static/font",
|
||||||
"to": "static/font",
|
"to": "static/font",
|
||||||
"filter": ["**/*"]
|
"filter": ["**/*"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "./static/app-update.yml",
|
||||||
|
"to": "app-update.yml"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"publish": [
|
"publish": [
|
||||||
|
|
|
@ -20,8 +20,12 @@ import path from 'path';
|
||||||
import { diskSpaceLinux, diskSpaceWindows, diskSpaceMac } from '../ui/util/diskspace';
|
import { diskSpaceLinux, diskSpaceWindows, diskSpaceMac } from '../ui/util/diskspace';
|
||||||
|
|
||||||
const { download } = require('electron-dl');
|
const { download } = require('electron-dl');
|
||||||
|
const mime = require('mime');
|
||||||
const remote = require('@electron/remote/main');
|
const remote = require('@electron/remote/main');
|
||||||
const os = require('os');
|
const os = require('os');
|
||||||
|
const sudo = require('sudo-prompt');
|
||||||
|
const probe = require('ffmpeg-probe');
|
||||||
|
const MAX_IPC_SEND_BUFFER_SIZE = 500000000; // large files crash when serialized for ipc message
|
||||||
|
|
||||||
remote.initialize();
|
remote.initialize();
|
||||||
const filePath = path.join(process.resourcesPath, 'static', 'upgradeDisabled');
|
const filePath = path.join(process.resourcesPath, 'static', 'upgradeDisabled');
|
||||||
|
@ -33,29 +37,23 @@ try {
|
||||||
upgradeDisabled = false;
|
upgradeDisabled = false;
|
||||||
}
|
}
|
||||||
autoUpdater.autoDownload = !upgradeDisabled;
|
autoUpdater.autoDownload = !upgradeDisabled;
|
||||||
|
autoUpdater.allowPrerelease = false;
|
||||||
|
|
||||||
// This is set to true if an auto update has been downloaded through the Electron
|
const UPDATE_STATE_INIT = 0;
|
||||||
// auto-update system and is ready to install. If the user declined an update earlier,
|
const UPDATE_STATE_CHECKING = 1;
|
||||||
// it will still install on shutdown.
|
const UPDATE_STATE_UPDATES_FOUND = 2;
|
||||||
let autoUpdateDownloaded = false;
|
const UPDATE_STATE_NO_UPDATES_FOUND = 3;
|
||||||
|
const UPDATE_STATE_DOWNLOADING = 4;
|
||||||
|
const UPDATE_STATE_DOWNLOADED = 5;
|
||||||
|
let updateState = UPDATE_STATE_INIT;
|
||||||
|
let updateDownloadItem;
|
||||||
|
|
||||||
|
const isAutoUpdateSupported = ['win32', 'darwin'].includes(process.platform) || !!process.env.APPIMAGE;
|
||||||
|
|
||||||
// This is used to keep track of whether we are showing the special dialog
|
// This is used to keep track of whether we are showing the special dialog
|
||||||
// that we show on Windows after you decline an upgrade and close the app later.
|
// that we show on Windows after you decline an upgrade and close the app later.
|
||||||
let showingAutoUpdateCloseAlert = false;
|
let showingAutoUpdateCloseAlert = false;
|
||||||
|
|
||||||
// This is used to prevent downloading updates multiple times when
|
|
||||||
// using the auto updater API.
|
|
||||||
// As read in the documentation:
|
|
||||||
// "Calling autoUpdater.checkForUpdates() twice will download the update two times."
|
|
||||||
// https://www.electronjs.org/docs/latest/api/auto-updater#autoupdatercheckforupdates
|
|
||||||
let keepCheckingForUpdates = true;
|
|
||||||
|
|
||||||
// Auto updater doesn't support Linux installations (only trough AppImages)
|
|
||||||
// this is why, for that case, we download a full executable (.deb package)
|
|
||||||
// as a fallback support. This variable will be used to prevent
|
|
||||||
// multiple downloads when auto updater isn't supported.
|
|
||||||
let downloadUpgradeInProgress = false;
|
|
||||||
|
|
||||||
// Keep a global reference, if you don't, they will be closed automatically when the JavaScript
|
// Keep a global reference, if you don't, they will be closed automatically when the JavaScript
|
||||||
// object is garbage collected.
|
// object is garbage collected.
|
||||||
let rendererWindow;
|
let rendererWindow;
|
||||||
|
@ -243,7 +241,8 @@ app.on('activate', () => {
|
||||||
app.on('will-quit', event => {
|
app.on('will-quit', event => {
|
||||||
if (
|
if (
|
||||||
process.platform === 'win32' &&
|
process.platform === 'win32' &&
|
||||||
autoUpdateDownloaded &&
|
updateState === UPDATE_STATE_DOWNLOADED &&
|
||||||
|
isAutoUpdateSupported &&
|
||||||
!appState.autoUpdateAccepted &&
|
!appState.autoUpdateAccepted &&
|
||||||
!showingAutoUpdateCloseAlert
|
!showingAutoUpdateCloseAlert
|
||||||
) {
|
) {
|
||||||
|
@ -303,6 +302,96 @@ app.on('before-quit', () => {
|
||||||
appState.isQuitting = true;
|
appState.isQuitting = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Get the content of a file as a raw buffer of bytes.
|
||||||
|
// Useful to convert a file path to a File instance.
|
||||||
|
// Example:
|
||||||
|
// const result = await ipcMain.invoke('get-file-from-path', 'path/to/file');
|
||||||
|
// const file = new File([result.buffer], result.name);
|
||||||
|
// NOTE: if path points to a folder, an empty
|
||||||
|
// file will be given.
|
||||||
|
ipcMain.handle('get-file-from-path', (event, path, readContents = true) => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
fs.stat(path, (error, stats) => {
|
||||||
|
if (error) {
|
||||||
|
reject(error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Separate folders considering "\" and "/"
|
||||||
|
// as separators (cross platform)
|
||||||
|
const folders = path.split(/[\\/]/);
|
||||||
|
const name = folders[folders.length - 1];
|
||||||
|
if (stats.isDirectory()) {
|
||||||
|
resolve({
|
||||||
|
name,
|
||||||
|
mime: undefined,
|
||||||
|
path,
|
||||||
|
buffer: new ArrayBuffer(0),
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!readContents) {
|
||||||
|
resolve({
|
||||||
|
name,
|
||||||
|
mime: mime.getType(name) || undefined,
|
||||||
|
path,
|
||||||
|
buffer: new ArrayBuffer(0),
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Encoding null ensures data results in a Buffer.
|
||||||
|
fs.readFile(path, { encoding: null }, (err, data) => {
|
||||||
|
if (err) {
|
||||||
|
reject(err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
resolve({
|
||||||
|
name,
|
||||||
|
mime: mime.getType(name) || undefined,
|
||||||
|
path,
|
||||||
|
buffer: data,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('get-file-details-from-path', async (event, path) => {
|
||||||
|
const isFfMp4 = (ffprobeResults) => {
|
||||||
|
return ffprobeResults &&
|
||||||
|
ffprobeResults.format &&
|
||||||
|
ffprobeResults.format.format_name &&
|
||||||
|
ffprobeResults.format.format_name.includes('mp4');
|
||||||
|
};
|
||||||
|
const folders = path.split(/[\\/]/);
|
||||||
|
const name = folders[folders.length - 1];
|
||||||
|
let duration = 0, size = 0, mimeType;
|
||||||
|
try {
|
||||||
|
await fs.promises.stat(path);
|
||||||
|
let ffprobeResults;
|
||||||
|
try {
|
||||||
|
ffprobeResults = await probe(path);
|
||||||
|
duration = ffprobeResults.format.duration;
|
||||||
|
size = ffprobeResults.format.size;
|
||||||
|
} catch (e) {
|
||||||
|
}
|
||||||
|
let fileReadResult;
|
||||||
|
if (size < MAX_IPC_SEND_BUFFER_SIZE) {
|
||||||
|
try {
|
||||||
|
fileReadResult = await fs.promises.readFile(path);
|
||||||
|
} catch (e) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// TODO: use mmmagic to inspect file and get mime type
|
||||||
|
mimeType = isFfMp4(ffprobeResults) ? 'video/mp4' : mime.getType(name);
|
||||||
|
const fileData = {name, mime: mimeType || undefined, path, duration: duration, size, buffer: fileReadResult };
|
||||||
|
return fileData;
|
||||||
|
} catch (e) {
|
||||||
|
// no stat
|
||||||
|
return { error: 'no file' };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
ipcMain.on('get-disk-space', async (event) => {
|
ipcMain.on('get-disk-space', async (event) => {
|
||||||
try {
|
try {
|
||||||
const { data_dir } = await Lbry.settings_get();
|
const { data_dir } = await Lbry.settings_get();
|
||||||
|
@ -323,91 +412,10 @@ ipcMain.on('get-disk-space', async (event) => {
|
||||||
rendererWindow.webContents.send('send-disk-space', { diskSpace });
|
rendererWindow.webContents.send('send-disk-space', { diskSpace });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
rendererWindow.webContents.send('send-disk-space', { error: e.message || e });
|
rendererWindow.webContents.send('send-disk-space', { error: e.message || e });
|
||||||
console.log('Failed to start LbryFirst', e);
|
console.log('Failed to get disk space', e);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.on('download-upgrade', async (event, params) => {
|
|
||||||
if (downloadUpgradeInProgress) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { url, options } = params;
|
|
||||||
const dir = fs.mkdtempSync(app.getPath('temp') + path.sep);
|
|
||||||
options.onProgress = function(p) {
|
|
||||||
rendererWindow.webContents.send('download-progress-update', p);
|
|
||||||
};
|
|
||||||
options.directory = dir;
|
|
||||||
options.onCompleted = function(c) {
|
|
||||||
downloadUpgradeInProgress = false;
|
|
||||||
rendererWindow.webContents.send('download-update-complete', c);
|
|
||||||
};
|
|
||||||
const win = BrowserWindow.getFocusedWindow();
|
|
||||||
downloadUpgradeInProgress = true;
|
|
||||||
await download(win, url, options).catch(e => console.log('e', e));
|
|
||||||
});
|
|
||||||
|
|
||||||
ipcMain.on('upgrade', (event, installerPath) => {
|
|
||||||
app.on('quit', () => {
|
|
||||||
console.log('Launching upgrade installer at', installerPath);
|
|
||||||
// This gets triggered called after *all* other quit-related events, so
|
|
||||||
// we'll only get here if we're fully prepared and quitting for real.
|
|
||||||
shell.openPath(installerPath);
|
|
||||||
});
|
|
||||||
// what to do if no shutdown in a long time?
|
|
||||||
console.log('Update downloaded to', installerPath);
|
|
||||||
console.log('The app will close and you will be prompted to install the latest version of LBRY.');
|
|
||||||
console.log('After the install is complete, please reopen the app.');
|
|
||||||
app.quit();
|
|
||||||
});
|
|
||||||
|
|
||||||
ipcMain.on('check-for-updates', (event, autoDownload) => {
|
|
||||||
// Prevent downloading the same update multiple times.
|
|
||||||
if (!keepCheckingForUpdates) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
keepCheckingForUpdates = false;
|
|
||||||
autoUpdater.autoDownload = autoDownload;
|
|
||||||
autoUpdater.checkForUpdates();
|
|
||||||
});
|
|
||||||
|
|
||||||
autoUpdater.on('update-downloaded', () => {
|
|
||||||
autoUpdateDownloaded = true;
|
|
||||||
|
|
||||||
// If this download was trigger by
|
|
||||||
// autoUpdateAccepted it means, the user
|
|
||||||
// wants to install the new update but
|
|
||||||
// needed to downloaded the files first.
|
|
||||||
if (appState.autoUpdateAccepted) {
|
|
||||||
autoUpdater.quitAndInstall();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
autoUpdater.on('update-not-available', () => {
|
|
||||||
keepCheckingForUpdates = true;
|
|
||||||
});
|
|
||||||
|
|
||||||
ipcMain.on('autoUpdateAccepted', () => {
|
|
||||||
appState.autoUpdateAccepted = true;
|
|
||||||
|
|
||||||
// quitAndInstall can only be called if the
|
|
||||||
// update has been downloaded. Since the user
|
|
||||||
// can disable auto updates, we have to make
|
|
||||||
// sure it has been downloaded first.
|
|
||||||
if (autoUpdateDownloaded) {
|
|
||||||
autoUpdater.quitAndInstall();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the update hasn't been downloaded,
|
|
||||||
// start downloading it. After it's done, the
|
|
||||||
// event 'update-downloaded' will be triggered,
|
|
||||||
// where we will be able to resume the
|
|
||||||
// update installation.
|
|
||||||
autoUpdater.downloadUpdate();
|
|
||||||
});
|
|
||||||
|
|
||||||
ipcMain.on('version-info-requested', () => {
|
ipcMain.on('version-info-requested', () => {
|
||||||
function formatRc(ver) {
|
function formatRc(ver) {
|
||||||
// Adds dash if needed to make RC suffix SemVer friendly
|
// Adds dash if needed to make RC suffix SemVer friendly
|
||||||
|
@ -500,3 +508,162 @@ process.on('uncaughtException', error => {
|
||||||
if (daemon) daemon.quit();
|
if (daemon) daemon.quit();
|
||||||
app.exit(1);
|
app.exit(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Auto updater
|
||||||
|
autoUpdater.on('download-progress', () => {
|
||||||
|
updateState = UPDATE_STATE_DOWNLOADING;
|
||||||
|
});
|
||||||
|
|
||||||
|
autoUpdater.on('update-downloaded', () => {
|
||||||
|
updateState = UPDATE_STATE_DOWNLOADED;
|
||||||
|
|
||||||
|
// If this download was trigger by
|
||||||
|
// autoUpdateAccepted it means, the user
|
||||||
|
// wants to install the new update but
|
||||||
|
// needed to downloaded the files first.
|
||||||
|
if (appState.autoUpdateAccepted) {
|
||||||
|
autoUpdater.quitAndInstall();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
autoUpdater.on('update-available', () => {
|
||||||
|
if (updateState === UPDATE_STATE_DOWNLOADING) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
updateState = UPDATE_STATE_UPDATES_FOUND;
|
||||||
|
});
|
||||||
|
|
||||||
|
autoUpdater.on('update-not-available', () => {
|
||||||
|
updateState = UPDATE_STATE_NO_UPDATES_FOUND;
|
||||||
|
});
|
||||||
|
|
||||||
|
autoUpdater.on('error', () => {
|
||||||
|
if (updateState === UPDATE_STATE_DOWNLOADING) {
|
||||||
|
updateState = UPDATE_STATE_UPDATES_FOUND;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
updateState = UPDATE_STATE_INIT;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Manual (.deb) update
|
||||||
|
ipcMain.on('cancel-download-upgrade', () => {
|
||||||
|
if (updateDownloadItem) {
|
||||||
|
// Cancel the download and execute the onCancel
|
||||||
|
// callback set in the options.
|
||||||
|
updateDownloadItem.cancel();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.on('download-upgrade', (event, params) => {
|
||||||
|
if (updateState !== UPDATE_STATE_UPDATES_FOUND) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (isAutoUpdateSupported) {
|
||||||
|
updateState = UPDATE_STATE_DOWNLOADING;
|
||||||
|
autoUpdater.downloadUpdate();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { url, options } = params;
|
||||||
|
const dir = fs.mkdtempSync(app.getPath('temp') + path.sep);
|
||||||
|
|
||||||
|
updateState = UPDATE_STATE_DOWNLOADING;
|
||||||
|
|
||||||
|
// Grab the download item's handler to allow
|
||||||
|
// cancelling the operation if required.
|
||||||
|
options.onStarted = function(downloadItem) {
|
||||||
|
updateDownloadItem = downloadItem;
|
||||||
|
};
|
||||||
|
options.onCancel = function() {
|
||||||
|
updateState = UPDATE_STATE_UPDATES_FOUND;
|
||||||
|
updateDownloadItem = undefined;
|
||||||
|
};
|
||||||
|
options.onProgress = function(p) {
|
||||||
|
rendererWindow.webContents.send('download-progress-update', p);
|
||||||
|
};
|
||||||
|
options.onCompleted = function(c) {
|
||||||
|
updateState = UPDATE_STATE_DOWNLOADED;
|
||||||
|
updateDownloadItem = undefined;
|
||||||
|
rendererWindow.webContents.send('download-update-complete', c);
|
||||||
|
};
|
||||||
|
options.directory = dir;
|
||||||
|
const win = BrowserWindow.getFocusedWindow();
|
||||||
|
download(win, url, options).catch(e => {
|
||||||
|
updateState = UPDATE_STATE_UPDATES_FOUND;
|
||||||
|
console.log('e', e);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update behavior
|
||||||
|
ipcMain.on('autoUpdateAccepted', () => {
|
||||||
|
appState.autoUpdateAccepted = true;
|
||||||
|
|
||||||
|
// quitAndInstall can only be called if the
|
||||||
|
// update has been downloaded. Since the user
|
||||||
|
// can disable auto updates, we have to make
|
||||||
|
// sure it has been downloaded first.
|
||||||
|
if (updateState === UPDATE_STATE_DOWNLOADED) {
|
||||||
|
autoUpdater.quitAndInstall();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updateState !== UPDATE_STATE_UPDATES_FOUND) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the update hasn't been downloaded,
|
||||||
|
// start downloading it. After it's done, the
|
||||||
|
// event 'update-downloaded' will be triggered,
|
||||||
|
// where we will be able to resume the
|
||||||
|
// update installation.
|
||||||
|
updateState = UPDATE_STATE_DOWNLOADING;
|
||||||
|
autoUpdater.downloadUpdate();
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.on('check-for-updates', (event, autoDownload) => {
|
||||||
|
if (![UPDATE_STATE_INIT, UPDATE_STATE_NO_UPDATES_FOUND].includes(updateState)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateState = UPDATE_STATE_CHECKING;
|
||||||
|
|
||||||
|
// If autoDownload is true, checkForUpdates will begin the
|
||||||
|
// download automatically.
|
||||||
|
if (autoDownload) {
|
||||||
|
updateState = UPDATE_STATE_DOWNLOADING;
|
||||||
|
}
|
||||||
|
|
||||||
|
autoUpdater.autoDownload = autoDownload;
|
||||||
|
autoUpdater.checkForUpdates();
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.on('upgrade', (event, installerPath) => {
|
||||||
|
// what to do if no shutdown in a long time?
|
||||||
|
console.log('Update downloaded to', installerPath);
|
||||||
|
console.log('The app will close and you will be prompted to install the latest version of LBRY.');
|
||||||
|
console.log('After the install is complete, please reopen the app.');
|
||||||
|
|
||||||
|
// Prevent .deb package from opening with archive manager (Ubuntu >= 20)
|
||||||
|
if (process.platform === 'linux' && !process.env.APPIMAGE) {
|
||||||
|
sudo.exec(`dpkg -i ${installerPath}`, { name: app.name }, (err, stdout, stderr) => {
|
||||||
|
if (err || stderr) {
|
||||||
|
rendererWindow.webContents.send('upgrade-installing-error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-launch the application when the installation finishes.
|
||||||
|
app.relaunch();
|
||||||
|
app.quit();
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
app.on('quit', () => {
|
||||||
|
console.log('Launching upgrade installer at', installerPath);
|
||||||
|
// This gets triggered called after *all* other quit-related events, so
|
||||||
|
// we'll only get here if we're fully prepared and quitting for real.
|
||||||
|
shell.openPath(installerPath);
|
||||||
|
});
|
||||||
|
app.quit();
|
||||||
|
});
|
||||||
|
|
|
@ -155,7 +155,10 @@ Lbryio.authenticate = (domain, language) => {
|
||||||
.then(
|
.then(
|
||||||
status =>
|
status =>
|
||||||
new Promise((res, rej) => {
|
new Promise((res, rej) => {
|
||||||
const appId = status.installation_id;
|
const appId =
|
||||||
|
domain && domain !== 'lbry.tv'
|
||||||
|
? (domain.replace(/[.]/gi, '') + status.installation_id).slice(0, 66)
|
||||||
|
: status.installation_id;
|
||||||
Lbryio.call(
|
Lbryio.call(
|
||||||
'user',
|
'user',
|
||||||
'new',
|
'new',
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
// involve moving it from 'extras' to 'ui' (big change).
|
// involve moving it from 'extras' to 'ui' (big change).
|
||||||
|
|
||||||
import { createCachedSelector } from 're-reselect';
|
import { createCachedSelector } from 're-reselect';
|
||||||
import { selectClaimForUri } from 'redux/selectors/claims';
|
import { selectClaimForUri, makeSelectIsBlacklisted } from 'redux/selectors/claims';
|
||||||
import { selectMutedChannels } from 'redux/selectors/blocked';
|
import { selectMutedChannels } from 'redux/selectors/blocked';
|
||||||
import { selectModerationBlockList } from 'redux/selectors/comments';
|
import { selectModerationBlockList } from 'redux/selectors/comments';
|
||||||
import { selectBlacklistedOutpointMap, selectFilteredOutpointMap } from 'lbryinc';
|
import { selectBlacklistedOutpointMap, selectFilteredOutpointMap } from 'lbryinc';
|
||||||
|
@ -18,7 +18,8 @@ export const selectBanStateForUri = createCachedSelector(
|
||||||
selectFilteredOutpointMap,
|
selectFilteredOutpointMap,
|
||||||
selectMutedChannels,
|
selectMutedChannels,
|
||||||
selectModerationBlockList,
|
selectModerationBlockList,
|
||||||
(claim, blackListedOutpointMap, filteredOutpointMap, mutedChannelUris, personalBlocklist) => {
|
(state, uri) => makeSelectIsBlacklisted(uri)(state),
|
||||||
|
(claim, blackListedOutpointMap, filteredOutpointMap, mutedChannelUris, personalBlocklist, isBlacklisted) => {
|
||||||
const banState = {};
|
const banState = {};
|
||||||
|
|
||||||
if (!claim) {
|
if (!claim) {
|
||||||
|
@ -27,6 +28,10 @@ export const selectBanStateForUri = createCachedSelector(
|
||||||
|
|
||||||
const channelClaim = getChannelFromClaim(claim);
|
const channelClaim = getChannelFromClaim(claim);
|
||||||
|
|
||||||
|
if (isBlacklisted) {
|
||||||
|
banState['blacklisted'] = true;
|
||||||
|
}
|
||||||
|
|
||||||
// This will be replaced once blocking is done at the wallet server level.
|
// This will be replaced once blocking is done at the wallet server level.
|
||||||
if (blackListedOutpointMap) {
|
if (blackListedOutpointMap) {
|
||||||
if (
|
if (
|
||||||
|
|
37
flow-typed/Claim.js
vendored
37
flow-typed/Claim.js
vendored
|
@ -145,12 +145,49 @@ declare type PurchaseReceipt = {
|
||||||
type: 'purchase',
|
type: 'purchase',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
declare type ClaimErrorCensor = {
|
||||||
|
address: string,
|
||||||
|
amount: string,
|
||||||
|
canonical_url: string,
|
||||||
|
claim_id: string,
|
||||||
|
claim_op: string,
|
||||||
|
confirmations: number,
|
||||||
|
has_signing_key: boolean,
|
||||||
|
height: number,
|
||||||
|
meta: {
|
||||||
|
activation_height: number,
|
||||||
|
claims_in_channel: number,
|
||||||
|
creation_height: number,
|
||||||
|
creation_timestamp: number,
|
||||||
|
effective_amount: string,
|
||||||
|
expiration_height: number,
|
||||||
|
is_controlling: boolean,
|
||||||
|
reposted: number,
|
||||||
|
support_amount: string,
|
||||||
|
take_over_height: number,
|
||||||
|
},
|
||||||
|
name: string,
|
||||||
|
normalized_name: string,
|
||||||
|
nout: number,
|
||||||
|
permanent_url: string,
|
||||||
|
short_url: string,
|
||||||
|
timestamp: number,
|
||||||
|
txid: string,
|
||||||
|
type: string,
|
||||||
|
value: {
|
||||||
|
public_key: string,
|
||||||
|
public_key_id: string,
|
||||||
|
},
|
||||||
|
value_type: string,
|
||||||
|
}
|
||||||
|
|
||||||
declare type ClaimActionResolveInfo = {
|
declare type ClaimActionResolveInfo = {
|
||||||
[string]: {
|
[string]: {
|
||||||
stream: ?StreamClaim,
|
stream: ?StreamClaim,
|
||||||
channel: ?ChannelClaim,
|
channel: ?ChannelClaim,
|
||||||
claimsInChannel: ?number,
|
claimsInChannel: ?number,
|
||||||
collection: ?CollectionClaim,
|
collection: ?CollectionClaim,
|
||||||
|
errorCensor: ?ClaimErrorCensor,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
4
flow-typed/Settings.js
vendored
4
flow-typed/Settings.js
vendored
|
@ -8,6 +8,6 @@ declare type WalletServerDetails = {
|
||||||
};
|
};
|
||||||
|
|
||||||
declare type DiskSpace = {
|
declare type DiskSpace = {
|
||||||
total: string,
|
total: number,
|
||||||
free: string,
|
free: number,
|
||||||
};
|
};
|
||||||
|
|
10
flow-typed/file-data.js
vendored
Normal file
10
flow-typed/file-data.js
vendored
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
// @flow
|
||||||
|
|
||||||
|
declare type FileData = {
|
||||||
|
file?: Blob,
|
||||||
|
path: string,
|
||||||
|
duration?: number,
|
||||||
|
size?: number,
|
||||||
|
mimeType: string,
|
||||||
|
error?: string,
|
||||||
|
}
|
9
flow-typed/file-with-path.js
vendored
Normal file
9
flow-typed/file-with-path.js
vendored
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
// @flow
|
||||||
|
|
||||||
|
declare type FileWithPath = {
|
||||||
|
file: File,
|
||||||
|
// The full path will only be available in
|
||||||
|
// the application. For browser, the name
|
||||||
|
// of the file will be used.
|
||||||
|
path: string,
|
||||||
|
}
|
1
flow-typed/homepage.js
vendored
1
flow-typed/homepage.js
vendored
|
@ -22,6 +22,7 @@ declare type RowDataItem = {
|
||||||
channelIds?: Array<string>,
|
channelIds?: Array<string>,
|
||||||
limitClaimsPerChannel?: number,
|
limitClaimsPerChannel?: number,
|
||||||
pageSize?: number,
|
pageSize?: number,
|
||||||
|
languages?: Array<string>,
|
||||||
},
|
},
|
||||||
route?: string,
|
route?: string,
|
||||||
hideForUnauth?: boolean,
|
hideForUnauth?: boolean,
|
||||||
|
|
6
flow-typed/web-file.js
vendored
6
flow-typed/web-file.js
vendored
|
@ -1,6 +0,0 @@
|
||||||
// @flow
|
|
||||||
|
|
||||||
declare type WebFile = File & {
|
|
||||||
path?: string,
|
|
||||||
title?: string,
|
|
||||||
}
|
|
48
package.json
48
package.json
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "lbry",
|
"name": "lbry",
|
||||||
"version": "0.53.2-alpha.1",
|
"version": "0.53.9",
|
||||||
"description": "A browser for the LBRY network, a digital marketplace controlled by its users.",
|
"description": "A browser for the LBRY network, a digital marketplace controlled by its users.",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"lbry"
|
"lbry"
|
||||||
|
@ -23,11 +23,8 @@
|
||||||
"analyze": "source-map-explorer --only-mapped dist/electron/webpack/ui*.js --html dist/sourceMap.html",
|
"analyze": "source-map-explorer --only-mapped dist/electron/webpack/ui*.js --html dist/sourceMap.html",
|
||||||
"compile:electron": "node --max_old_space_size=4096 ./node_modules/webpack/bin/webpack.js --config webpack.electron.config.js",
|
"compile:electron": "node --max_old_space_size=4096 ./node_modules/webpack/bin/webpack.js --config webpack.electron.config.js",
|
||||||
"compile": "cross-env NODE_ENV=production yarn compile:electron",
|
"compile": "cross-env NODE_ENV=production yarn compile:electron",
|
||||||
"copyenv": "copyfiles ./.env* web/",
|
|
||||||
"dev": "yarn dev:electron",
|
"dev": "yarn dev:electron",
|
||||||
"dev:electron": "cross-env NODE_ENV=development node ./electron/devServer.js",
|
"dev:electron": "cross-env NODE_ENV=development node ./electron/devServer.js",
|
||||||
"dev:internal-apis": "LBRY_API_URL='http://localhost:8080' yarn dev:electron",
|
|
||||||
"dev:iatv": "LBRY_API_URL='http://localhost:15400' SDK_API_URL='http://localhost:15100' yarn dev:web",
|
|
||||||
"pack": "electron-builder --dir",
|
"pack": "electron-builder --dir",
|
||||||
"dist": "electron-builder",
|
"dist": "electron-builder",
|
||||||
"build": "cross-env NODE_ENV=production yarn compile:electron && electron-builder build",
|
"build": "cross-env NODE_ENV=production yarn compile:electron && electron-builder build",
|
||||||
|
@ -44,31 +41,29 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@electron/remote": "^2.0.1",
|
"@electron/remote": "^2.0.1",
|
||||||
"@emotion/react": "^11.6.0",
|
"@emotion/react": "^11.10.4",
|
||||||
"@emotion/styled": "^11.6.0",
|
"@emotion/styled": "^11.10.4",
|
||||||
"@mui/material": "^5.2.1",
|
"@mui/material": "^5.2.1",
|
||||||
"@ungap/from-entries": "^0.2.1",
|
"@ungap/from-entries": "^0.2.1",
|
||||||
"auto-launch": "^5.0.5",
|
"auto-launch": "^5.0.5",
|
||||||
"electron-dl": "^3.2.0",
|
"electron-dl": "^3.2.0",
|
||||||
"electron-log": "^2.2.12",
|
"electron-log": "^4.4.8",
|
||||||
"electron-notarize": "^1.0.0",
|
"electron-notarize": "^1.0.0",
|
||||||
"electron-updater": "^4.2.4",
|
"electron-updater": "^4.2.4",
|
||||||
"express": "^4.17.1",
|
"express": "^4.17.1",
|
||||||
|
"ffmpeg-probe": "^1.0.6",
|
||||||
"humanize-duration": "^3.27.0",
|
"humanize-duration": "^3.27.0",
|
||||||
"if-env": "^1.0.4",
|
|
||||||
"match-sorter": "^6.3.0",
|
"match-sorter": "^6.3.0",
|
||||||
|
"mime": "^3.0.0",
|
||||||
"node-html-parser": "^5.1.0",
|
"node-html-parser": "^5.1.0",
|
||||||
"parse-duration": "^1.0.0",
|
"parse-duration": "^1.0.0",
|
||||||
"proxy-polyfill": "0.1.6",
|
"proxy-polyfill": "0.1.6",
|
||||||
"re-reselect": "^4.0.0",
|
"re-reselect": "^4.0.0",
|
||||||
"react-beautiful-dnd": "^13.1.0",
|
"react-beautiful-dnd": "^13.1.0",
|
||||||
"react-color": "^2.19.3",
|
|
||||||
"react-datetime-picker": "^3.4.3",
|
"react-datetime-picker": "^3.4.3",
|
||||||
"remove-markdown": "^0.3.0",
|
|
||||||
"rss": "^1.2.2",
|
|
||||||
"source-map-explorer": "^2.5.2",
|
"source-map-explorer": "^2.5.2",
|
||||||
"tempy": "^0.6.0",
|
"sudo-prompt": "^9.2.1",
|
||||||
"videojs-logo": "^2.1.4"
|
"tempy": "^0.6.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.0.0",
|
"@babel/core": "^7.0.0",
|
||||||
|
@ -79,7 +74,7 @@
|
||||||
"@babel/plugin-syntax-dynamic-import": "^7.2.0",
|
"@babel/plugin-syntax-dynamic-import": "^7.2.0",
|
||||||
"@babel/plugin-transform-flow-strip-types": "^7.2.3",
|
"@babel/plugin-transform-flow-strip-types": "^7.2.3",
|
||||||
"@babel/plugin-transform-runtime": "^7.4.3",
|
"@babel/plugin-transform-runtime": "^7.4.3",
|
||||||
"@babel/polyfill": "^7.2.5",
|
"@babel/polyfill": "^7.12.1",
|
||||||
"@babel/preset-env": "^7.12.11",
|
"@babel/preset-env": "^7.12.11",
|
||||||
"@babel/preset-flow": "^7.12.1",
|
"@babel/preset-flow": "^7.12.1",
|
||||||
"@babel/preset-react": "^7.0.0",
|
"@babel/preset-react": "^7.0.0",
|
||||||
|
@ -87,7 +82,6 @@
|
||||||
"@datapunt/matomo-tracker-js": "^0.1.4",
|
"@datapunt/matomo-tracker-js": "^0.1.4",
|
||||||
"@hot-loader/react-dom": "^16.13",
|
"@hot-loader/react-dom": "^16.13",
|
||||||
"@meetfranz/electron-cookies": "^3.0.2",
|
"@meetfranz/electron-cookies": "^3.0.2",
|
||||||
"@reach/auto-id": "^0.13.0",
|
|
||||||
"@reach/combobox": "^0.12.1",
|
"@reach/combobox": "^0.12.1",
|
||||||
"@reach/menu-button": "0.8.6",
|
"@reach/menu-button": "0.8.6",
|
||||||
"@reach/rect": "^0.16.0",
|
"@reach/rect": "^0.16.0",
|
||||||
|
@ -98,21 +92,17 @@
|
||||||
"@sentry/webpack-plugin": "^1.10.0",
|
"@sentry/webpack-plugin": "^1.10.0",
|
||||||
"@types/three": "^0.103.2",
|
"@types/three": "^0.103.2",
|
||||||
"adm-zip": "^0.4.13",
|
"adm-zip": "^0.4.13",
|
||||||
"async-exit-hook": "^2.0.1",
|
|
||||||
"babel-eslint": "^10.0.1",
|
"babel-eslint": "^10.0.1",
|
||||||
"babel-loader": "^8.0.5",
|
"babel-loader": "^8.0.5",
|
||||||
"babel-plugin-add-module-exports": "^1.0.4",
|
"babel-plugin-add-module-exports": "^1.0.4",
|
||||||
"babel-plugin-import-glob": "^2.0.0",
|
"babel-plugin-import-glob": "^2.0.0",
|
||||||
"babel-plugin-transform-imports": "^1.5.1",
|
"babel-plugin-transform-imports": "^1.5.1",
|
||||||
"babel-plugin-transform-object-rest-spread": "^6.26.0",
|
"babel-plugin-transform-object-rest-spread": "^6.26.0",
|
||||||
"bluebird": "^3.5.1",
|
|
||||||
"chalk": "^4.1.0",
|
"chalk": "^4.1.0",
|
||||||
"classnames": "^2.2.5",
|
"classnames": "^2.2.5",
|
||||||
"codemirror": "^5.39.2",
|
"codemirror": "^5.39.2",
|
||||||
"concurrently": "^4.1.2",
|
|
||||||
"connected-react-router": "^6.8.0",
|
"connected-react-router": "^6.8.0",
|
||||||
"copy-webpack-plugin": "^6.4.1",
|
"copy-webpack-plugin": "^6.4.1",
|
||||||
"copyfiles": "^2.4.1",
|
|
||||||
"country-data": "^0.0.31",
|
"country-data": "^0.0.31",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"crypto-js": "^4.0.0",
|
"crypto-js": "^4.0.0",
|
||||||
|
@ -123,10 +113,9 @@
|
||||||
"decompress": "^4.2.1",
|
"decompress": "^4.2.1",
|
||||||
"del": "^3.0.0",
|
"del": "^3.0.0",
|
||||||
"devtron": "^1.4.0",
|
"devtron": "^1.4.0",
|
||||||
"dom-scroll-into-view": "^1.2.1",
|
|
||||||
"dotenv-defaults": "^2.0.1",
|
"dotenv-defaults": "^2.0.1",
|
||||||
"dotenv-webpack": "^1.8.0",
|
"dotenv-webpack": "^1.8.0",
|
||||||
"electron": "15.4.0",
|
"electron": "17.2.0",
|
||||||
"electron-builder": "^22.10.5",
|
"electron-builder": "^22.10.5",
|
||||||
"electron-devtools-installer": "^3.1.1",
|
"electron-devtools-installer": "^3.1.1",
|
||||||
"electron-is-dev": "^0.3.0",
|
"electron-is-dev": "^0.3.0",
|
||||||
|
@ -161,10 +150,7 @@
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
"mammoth": "^1.4.16",
|
"mammoth": "^1.4.16",
|
||||||
"moment": "^2.29.2",
|
"moment": "^2.29.2",
|
||||||
"node-abi": "^2.5.1",
|
|
||||||
"node-fetch": "^2.6.7",
|
"node-fetch": "^2.6.7",
|
||||||
"node-html-parser": "^5.1.0",
|
|
||||||
"node-libs-browser": "^2.1.0",
|
|
||||||
"node-loader": "^0.6.0",
|
"node-loader": "^0.6.0",
|
||||||
"node-wget": "^0.4.3",
|
"node-wget": "^0.4.3",
|
||||||
"nodemon": "^1.19.1",
|
"nodemon": "^1.19.1",
|
||||||
|
@ -179,7 +165,6 @@
|
||||||
"rc-progress": "^2.0.6",
|
"rc-progress": "^2.0.6",
|
||||||
"react": "^16.8.2",
|
"react": "^16.8.2",
|
||||||
"react-awesome-lightbox": "^1.7.3",
|
"react-awesome-lightbox": "^1.7.3",
|
||||||
"react-confetti": "^4.0.1",
|
|
||||||
"react-dom": "^16.8.2",
|
"react-dom": "^16.8.2",
|
||||||
"react-draggable": "^3.3.0",
|
"react-draggable": "^3.3.0",
|
||||||
"react-google-recaptcha": "^2.0.1",
|
"react-google-recaptcha": "^2.0.1",
|
||||||
|
@ -190,7 +175,6 @@
|
||||||
"react-router": "^5.1.0",
|
"react-router": "^5.1.0",
|
||||||
"react-router-dom": "^5.1.0",
|
"react-router-dom": "^5.1.0",
|
||||||
"react-simplemde-editor": "^4.1.3",
|
"react-simplemde-editor": "^4.1.3",
|
||||||
"react-spring": "^8.0.20",
|
|
||||||
"reakit": "^1.0.0-beta.13",
|
"reakit": "^1.0.0-beta.13",
|
||||||
"redux": "^3.6.0",
|
"redux": "^3.6.0",
|
||||||
"redux-persist": "^5.10.0",
|
"redux-persist": "^5.10.0",
|
||||||
|
@ -207,20 +191,16 @@
|
||||||
"sass": "^1.29.0",
|
"sass": "^1.29.0",
|
||||||
"sass-loader": "^7.1.0",
|
"sass-loader": "^7.1.0",
|
||||||
"semver": "^5.3.0",
|
"semver": "^5.3.0",
|
||||||
"stream-to-blob-url": "^2.1.1",
|
|
||||||
"strip-markdown": "^3.0.3",
|
"strip-markdown": "^3.0.3",
|
||||||
"style-loader": "^0.23.1",
|
"style-loader": "^0.23.1",
|
||||||
"terser-webpack-plugin": "^4.2.3",
|
"terser-webpack-plugin": "^4.2.3",
|
||||||
"three-full": "^28.0.2",
|
"three-full": "^28.0.2",
|
||||||
"tiny-relative-date": "^1.3.0",
|
|
||||||
"tree-kill": "^1.1.0",
|
|
||||||
"unist-util-visit": "^2.0.3",
|
"unist-util-visit": "^2.0.3",
|
||||||
"uuid": "^8.3.2",
|
"uuid": "^8.3.2",
|
||||||
"video.js": "^7.14.3",
|
"video.js": "^7.14.3",
|
||||||
"videojs-contrib-quality-levels": "^2.0.9",
|
"videojs-contrib-quality-levels": "^2.0.9",
|
||||||
"videojs-event-tracking": "^1.0.1",
|
"videojs-event-tracking": "^1.0.1",
|
||||||
"villain-react": "^1.0.9",
|
"villain-react": "^1.0.9",
|
||||||
"wavesurfer.js": "^2.2.1",
|
|
||||||
"webpack": "^4.44.2",
|
"webpack": "^4.44.2",
|
||||||
"webpack-bundle-analyzer": "^3.1.0",
|
"webpack-bundle-analyzer": "^3.1.0",
|
||||||
"webpack-cli": "^3.3.10",
|
"webpack-cli": "^3.3.10",
|
||||||
|
@ -230,19 +210,17 @@
|
||||||
"webpack-hot-middleware": "^2.24.3",
|
"webpack-hot-middleware": "^2.24.3",
|
||||||
"webpack-merge": "^4.2.1",
|
"webpack-merge": "^4.2.1",
|
||||||
"webpack-node-externals": "^1.7.2",
|
"webpack-node-externals": "^1.7.2",
|
||||||
"y18n": "^4.0.1",
|
|
||||||
"yarnhook": "^0.2.0"
|
"yarnhook": "^0.2.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=7",
|
"node": ">=16.13",
|
||||||
"yarn": "^1.3"
|
"yarn": "^1.3"
|
||||||
},
|
},
|
||||||
"lbrySettings": {
|
"lbrySettings": {
|
||||||
"lbrynetDaemonVersion": "0.107.1",
|
"lbrynetDaemonVersion": "0.113.0",
|
||||||
"lbrynetDaemonUrlTemplate": "https://github.com/lbryio/lbry/releases/download/vDAEMONVER/lbrynet-OSNAME.zip",
|
"lbrynetDaemonUrlTemplate": "https://github.com/lbryio/lbry/releases/download/vDAEMONVER/lbrynet-OSNAME.zip",
|
||||||
"lbrynetDaemonDir": "static/daemon",
|
"lbrynetDaemonDir": "static/daemon",
|
||||||
"lbrynetDaemonFileName": "lbrynet"
|
"lbrynetDaemonFileName": "lbrynet"
|
||||||
},
|
},
|
||||||
"packageManager": "yarn@3.2.0",
|
"packageManager": "yarn@3.2.0"
|
||||||
"stableVersion": "0.53.1"
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -2206,13 +2206,6 @@
|
||||||
"Enabling a minimum amount to comment will force all comments to have tips associated with them. This can help prevent spam.": "Enabling a minimum amount to comment will force all comments to have tips associated with them. This can help prevent spam.",
|
"Enabling a minimum amount to comment will force all comments to have tips associated with them. This can help prevent spam.": "Enabling a minimum amount to comment will force all comments to have tips associated with them. This can help prevent spam.",
|
||||||
"Comments containing these words will be blocked.": "Comments containing these words will be blocked.",
|
"Comments containing these words will be blocked.": "Comments containing these words will be blocked.",
|
||||||
"Enter the full channel name or URL to search.\n\nExamples:\n - @channel\n - @channel#3\n - https://odysee.com/@Odysee:8\n - lbry://@Odysee#8": "Enter the full channel name or URL to search.\n\nExamples:\n - @channel\n - @channel#3\n - https://odysee.com/@Odysee:8\n - lbry://@Odysee#8",
|
"Enter the full channel name or URL to search.\n\nExamples:\n - @channel\n - @channel#3\n - https://odysee.com/@Odysee:8\n - lbry://@Odysee#8": "Enter the full channel name or URL to search.\n\nExamples:\n - @channel\n - @channel#3\n - https://odysee.com/@Odysee:8\n - lbry://@Odysee#8",
|
||||||
"Disk Space": "Disk Space",
|
|
||||||
"Data Hosting": "Data Hosting",
|
|
||||||
"Limit": "Limit",
|
|
||||||
"Limit Space Used": "Limit Space Used",
|
|
||||||
"Apply": "Apply",
|
|
||||||
"Limit in GB": "Limit in GB",
|
|
||||||
"If you set a limit, playing videos may exceed your limit until cleanup runs every 30 minutes.": "If you set a limit, playing videos may exceed your limit until cleanup runs every 30 minutes.",
|
|
||||||
"Enable Data Hosting": "Enable Data Hosting",
|
"Enable Data Hosting": "Enable Data Hosting",
|
||||||
"Data over the limit will be deleted within 30 minutes. This will make the Yrbl cry a little bit.": "Data over the limit will be deleted within 30 minutes. This will make the Yrbl cry a little bit.",
|
"Data over the limit will be deleted within 30 minutes. This will make the Yrbl cry a little bit.": "Data over the limit will be deleted within 30 minutes. This will make the Yrbl cry a little bit.",
|
||||||
"Choose %asset%": "Choose %asset%",
|
"Choose %asset%": "Choose %asset%",
|
||||||
|
@ -2228,13 +2221,6 @@
|
||||||
"Enable Prerelease Updates": "Enable Prerelease Updates",
|
"Enable Prerelease Updates": "Enable Prerelease Updates",
|
||||||
"Enable Upgrade to Test Builds": "Enable Upgrade to Test Builds",
|
"Enable Upgrade to Test Builds": "Enable Upgrade to Test Builds",
|
||||||
"Prereleases may break things and we may not be able to fix them for you.": "Prereleases may break things and we may not be able to fix them for you.",
|
"Prereleases may break things and we may not be able to fix them for you.": "Prereleases may break things and we may not be able to fix them for you.",
|
||||||
"Limit (GB)": "Limit (GB)",
|
|
||||||
"Limit Hosting for Content you Use": "Limit Hosting for Content you Use",
|
|
||||||
"Allow (GB)": "Allow (GB)",
|
|
||||||
"Content Data Hosting helps to seed things that you watch and download.": "Content Data Hosting helps to seed things that you watch and download.",
|
|
||||||
"Network Data Hosting allows the p2p network to store blobs unrelated to your browsing.": "Network Data Hosting allows the p2p network to store blobs unrelated to your browsing.",
|
|
||||||
"Content: Limit (GB)": "Content: Limit (GB)",
|
|
||||||
"Network: Allow (GB)": "Network: Allow (GB)",
|
|
||||||
"A channel is required to repost on LBRY": "A channel is required to repost on LBRY",
|
"A channel is required to repost on LBRY": "A channel is required to repost on LBRY",
|
||||||
"Admin": "Admin",
|
"Admin": "Admin",
|
||||||
"Stickers": "Stickers",
|
"Stickers": "Stickers",
|
||||||
|
@ -2256,19 +2242,6 @@
|
||||||
"Move Up": "Move Up",
|
"Move Up": "Move Up",
|
||||||
"Move Down": "Move Down",
|
"Move Down": "Move Down",
|
||||||
"Trending for #Game": "Trending for #Game",
|
"Trending for #Game": "Trending for #Game",
|
||||||
"Help the P2P data network by hosting data.": "Help the P2P data network by hosting data.",
|
|
||||||
"History hosting lets you choose how much storage to use helping content you've consumed.": "History hosting lets you choose how much storage to use helping content you've consumed.",
|
|
||||||
"Automatic hosting lets you delegate some amount of storage for the network to automatically download ad host.": "Automatic hosting lets you delegate some amount of storage for the network to automatically download ad host.",
|
|
||||||
"Playing videos may exceed your history hosting limit until cleanup runs every 30 minutes.": "Playing videos may exceed your history hosting limit until cleanup runs every 30 minutes.",
|
|
||||||
"History: Limit (GB)": "History: Limit (GB)",
|
|
||||||
"Automatic: Allow (GB)": "Automatic: Allow (GB)",
|
|
||||||
"Automatic hosting lets you delegate some amount of storage for the network to automatically download and host.": "Automatic hosting lets you delegate some amount of storage for the network to automatically download and host.",
|
|
||||||
"History Hosting": "History Hosting",
|
|
||||||
"Automatic Hosting": "Automatic Hosting",
|
|
||||||
"History Hosting lets you choose how much storage to use helping content you've consumed.": "History Hosting lets you choose how much storage to use helping content you've consumed.",
|
|
||||||
"Automatic Hosting lets you delegate some amount of storage for the network to automatically download and host.": "Automatic Hosting lets you delegate some amount of storage for the network to automatically download and host.",
|
|
||||||
"Help improve the P2P data network (and make LBRY happy) by hosting data.": "Help improve the P2P data network (and make LBRY happy) by hosting data.",
|
|
||||||
"Limit Hosting of Content History": "Limit Hosting of Content History",
|
|
||||||
"Remove custom comment server": "Remove custom comment server",
|
"Remove custom comment server": "Remove custom comment server",
|
||||||
"Use Https": "Use Https",
|
"Use Https": "Use Https",
|
||||||
"Server URL": "Server URL",
|
"Server URL": "Server URL",
|
||||||
|
@ -2315,7 +2288,39 @@
|
||||||
"Clear Views": "Clear Views",
|
"Clear Views": "Clear Views",
|
||||||
"Show Video View Progress": "Show Video View Progress",
|
"Show Video View Progress": "Show Video View Progress",
|
||||||
"Display view progress on thumbnail. This setting will not hide any blockchain activity or downloads.": "Display view progress on thumbnail. This setting will not hide any blockchain activity or downloads.",
|
"Display view progress on thumbnail. This setting will not hide any blockchain activity or downloads.": "Display view progress on thumbnail. This setting will not hide any blockchain activity or downloads.",
|
||||||
"%anonymous%": "%anonymous%",
|
"Content Hosting": "Content Hosting",
|
||||||
|
"Hosting": "Hosting",
|
||||||
|
"Viewed Hosting": "Viewed Hosting",
|
||||||
|
"Auto Hosting": "Auto Hosting",
|
||||||
|
"Help creators and improve the P2P data network by hosting content.": "Help creators and improve the P2P data network by hosting content.",
|
||||||
|
"I'm happy with my settings": "I'm happy with my settings",
|
||||||
|
"We've noticed you already have some settings.": "We've noticed you already have some settings.",
|
||||||
|
"You choose how much data to host.": "You choose how much data to host.",
|
||||||
|
"Go back": "Go back",
|
||||||
|
"Custom Hosting": "Custom Hosting",
|
||||||
|
"Automatic Hosting (GB)": "Automatic Hosting (GB)",
|
||||||
|
"* Note that as peer-to-peer software, your IP address and potentially other system information can be sent to other users, though this information is not stored permanently.": "* Note that as peer-to-peer software, your IP address and potentially other system information can be sent to other users, though this information is not stored permanently.",
|
||||||
|
"Help improve the P2P data network (and make LBRY users happy) by hosting data.": "Help improve the P2P data network (and make LBRY users happy) by hosting data.",
|
||||||
|
"View History Hosting lets you choose how much storage to use hosting content you've consumed.": "View History Hosting lets you choose how much storage to use hosting content you've consumed.",
|
||||||
|
"Automatic Hosting downloads a small portion of content active on the network.": "Automatic Hosting downloads a small portion of content active on the network.",
|
||||||
|
"Publishes --[legend, storage category]--": "Publishes",
|
||||||
|
"Auto Hosting --[legend, storage category]--": "Auto Hosting",
|
||||||
|
"View Hosting --[legend, storage category]--": "View Hosting",
|
||||||
|
"%spaceUsed% of %limit% GB": "%spaceUsed% of %limit% GB",
|
||||||
|
"%spaceUsed% of %limit% Free GB": "%spaceUsed% of %limit% Free GB",
|
||||||
|
"Disabled": "Disabled",
|
||||||
|
"Free --[legend, unused disk space]--": "Free",
|
||||||
|
"Top content in %language%": "Top content in %language%",
|
||||||
|
"Apply": "Apply",
|
||||||
|
"Disable background": "Disable background",
|
||||||
|
"Installing, please wait...": "Installing, please wait...",
|
||||||
|
"There was an error during installation. Please, try again.": "There was an error during installation. Please, try again.",
|
||||||
|
"Odysee Connect --[Section in Help Page]--": "Odysee Connect",
|
||||||
|
"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",
|
"Anon --[used in <%anonymous% Reposted>]--": "Anon",
|
||||||
|
"Your update is now pending. It will take a few minutes to appear for other users.": "Your update is now pending. It will take a few minutes to appear for other users.",
|
||||||
"--end--": "--end--"
|
"--end--": "--end--"
|
||||||
}
|
}
|
||||||
|
|
3
static/app-update.yml
Normal file
3
static/app-update.yml
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
owner: lbryio
|
||||||
|
repo: lbry-desktop
|
||||||
|
provider: github
|
166
ui/analytics.js
166
ui/analytics.js
|
@ -1,4 +1,11 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
/*
|
||||||
|
Removed Watchman (internal view tracking) code.
|
||||||
|
This file may eventually implement cantina
|
||||||
|
Refer to 0cc0e213a5c5bf9e2a76316df5d9da4b250a13c3 for initial integration commit
|
||||||
|
refer to ___ for removal commit.
|
||||||
|
*/
|
||||||
|
|
||||||
import { Lbryio } from 'lbryinc';
|
import { Lbryio } from 'lbryinc';
|
||||||
import * as Sentry from '@sentry/browser';
|
import * as Sentry from '@sentry/browser';
|
||||||
import MatomoTracker from '@datapunt/matomo-tracker-js';
|
import MatomoTracker from '@datapunt/matomo-tracker-js';
|
||||||
|
@ -14,12 +21,9 @@ const devInternalApis = process.env.LBRY_API_URL && process.env.LBRY_API_URL.inc
|
||||||
export const SHARE_INTERNAL = 'shareInternal';
|
export const SHARE_INTERNAL = 'shareInternal';
|
||||||
const SHARE_THIRD_PARTY = 'shareThirdParty';
|
const SHARE_THIRD_PARTY = 'shareThirdParty';
|
||||||
|
|
||||||
const WATCHMAN_BACKEND_ENDPOINT = 'https://watchman.na-backend.odysee.com/reports/playback';
|
|
||||||
// const SEND_DATA_TO_WATCHMAN_INTERVAL = 10; // in seconds
|
|
||||||
|
|
||||||
if (isProduction) {
|
if (isProduction) {
|
||||||
ElectronCookies.enable({
|
ElectronCookies.enable({
|
||||||
origin: 'https://lbry.com',
|
origin: 'https://lbry.tv',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -68,114 +72,10 @@ type LogPublishParams = {
|
||||||
let internalAnalyticsEnabled: boolean = false;
|
let internalAnalyticsEnabled: boolean = false;
|
||||||
if (window.localStorage.getItem(SHARE_INTERNAL) === 'true') internalAnalyticsEnabled = true;
|
if (window.localStorage.getItem(SHARE_INTERNAL) === 'true') internalAnalyticsEnabled = true;
|
||||||
|
|
||||||
/**
|
|
||||||
* Determine the mobile device type viewing the data
|
|
||||||
* This function returns one of 'and' (Android), 'ios', or 'web'.
|
|
||||||
*
|
|
||||||
* @returns {String}
|
|
||||||
*/
|
|
||||||
function getDeviceType() {
|
|
||||||
return 'dsk';
|
|
||||||
}
|
|
||||||
// variables initialized for watchman
|
|
||||||
let amountOfBufferEvents = 0;
|
|
||||||
let amountOfBufferTimeInMS = 0;
|
|
||||||
let videoType, userId, claimUrl, playerPoweredBy, videoPlayer, bitrateAsBitsPerSecond;
|
|
||||||
let lastSentTime;
|
|
||||||
|
|
||||||
// calculate data for backend, send them, and reset buffer data for next interval
|
|
||||||
async function sendAndResetWatchmanData() {
|
|
||||||
if (!userId) {
|
|
||||||
return 'Can only be used with a user id';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!videoPlayer) {
|
|
||||||
return 'Video player not initialized';
|
|
||||||
}
|
|
||||||
|
|
||||||
let timeSinceLastIntervalSend = new Date() - lastSentTime;
|
|
||||||
lastSentTime = new Date();
|
|
||||||
|
|
||||||
let protocol;
|
|
||||||
if (videoType === 'application/x-mpegURL') {
|
|
||||||
protocol = 'hls';
|
|
||||||
// get bandwidth if it exists from the texttrack (so it's accurate if user changes quality)
|
|
||||||
// $FlowFixMe
|
|
||||||
bitrateAsBitsPerSecond = videoPlayer.textTracks?.().tracks_[0]?.activeCues[0]?.value?.bandwidth;
|
|
||||||
} else {
|
|
||||||
protocol = 'stb';
|
|
||||||
}
|
|
||||||
|
|
||||||
// current position in video in MS
|
|
||||||
const positionInVideo = Math.round(videoPlayer.currentTime()) * 1000;
|
|
||||||
|
|
||||||
// get the duration marking the time in the video for relative position calculation
|
|
||||||
const totalDurationInSeconds = Math.round(videoPlayer.duration());
|
|
||||||
|
|
||||||
// build object for watchman backend
|
|
||||||
const objectToSend = {
|
|
||||||
rebuf_count: amountOfBufferEvents,
|
|
||||||
rebuf_duration: amountOfBufferTimeInMS,
|
|
||||||
url: claimUrl.replace('lbry://', ''),
|
|
||||||
device: getDeviceType(),
|
|
||||||
duration: timeSinceLastIntervalSend,
|
|
||||||
protocol,
|
|
||||||
player: playerPoweredBy,
|
|
||||||
user_id: userId.toString(),
|
|
||||||
position: Math.round(positionInVideo),
|
|
||||||
rel_position: Math.round((positionInVideo / (totalDurationInSeconds * 1000)) * 100),
|
|
||||||
bitrate: bitrateAsBitsPerSecond,
|
|
||||||
bandwidth: undefined,
|
|
||||||
// ...(userDownloadBandwidthInBitsPerSecond && {bandwidth: userDownloadBandwidthInBitsPerSecond}), // add bandwidth if populated
|
|
||||||
};
|
|
||||||
|
|
||||||
// post to watchman
|
|
||||||
await sendWatchmanData(objectToSend);
|
|
||||||
|
|
||||||
// reset buffer data
|
|
||||||
amountOfBufferEvents = 0;
|
|
||||||
amountOfBufferTimeInMS = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
let watchmanInterval;
|
|
||||||
// clear watchman interval and mark it as null (when video paused)
|
|
||||||
function stopWatchmanInterval() {
|
|
||||||
clearInterval(watchmanInterval);
|
|
||||||
watchmanInterval = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// creates the setInterval that will run send to watchman on recurring basis
|
|
||||||
function startWatchmanIntervalIfNotRunning() {
|
|
||||||
if (!watchmanInterval) {
|
|
||||||
// instantiate the first time to calculate duration from
|
|
||||||
lastSentTime = new Date();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// post data to the backend
|
|
||||||
async function sendWatchmanData(body) {
|
|
||||||
try {
|
|
||||||
const response = await fetch(WATCHMAN_BACKEND_ENDPOINT, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
Accept: 'application/json',
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify(body),
|
|
||||||
});
|
|
||||||
|
|
||||||
return response;
|
|
||||||
} catch (err) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
const analytics: Analytics = {
|
const analytics: Analytics = {
|
||||||
// receive buffer events from tracking plugin and save buffer amounts and times for backend call
|
// receive buffer events from tracking plugin and save buffer amounts and times for backend call
|
||||||
videoBufferEvent: async (claim, data) => {
|
videoBufferEvent: async (claim, data) => {
|
||||||
amountOfBufferEvents = amountOfBufferEvents + 1;
|
// stub
|
||||||
amountOfBufferTimeInMS = amountOfBufferTimeInMS + data.bufferDuration;
|
|
||||||
},
|
|
||||||
onDispose: () => {
|
|
||||||
stopWatchmanInterval();
|
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* Is told whether video is being started or paused, and adjusts interval accordingly
|
* Is told whether video is being started or paused, and adjusts interval accordingly
|
||||||
|
@ -183,40 +83,9 @@ const analytics: Analytics = {
|
||||||
* @param {object} passedPlayer - VideoJS Player object
|
* @param {object} passedPlayer - VideoJS Player object
|
||||||
*/
|
*/
|
||||||
videoIsPlaying: (isPlaying, passedPlayer) => {
|
videoIsPlaying: (isPlaying, passedPlayer) => {
|
||||||
let playerIsSeeking = false;
|
// stub
|
||||||
// have to use this because videojs pauses/unpauses during seek
|
|
||||||
// sometimes the seeking function isn't populated yet so check for it as well
|
|
||||||
if (passedPlayer && passedPlayer.seeking) {
|
|
||||||
playerIsSeeking = passedPlayer.seeking();
|
|
||||||
}
|
|
||||||
|
|
||||||
// if being paused, and not seeking, send existing data and stop interval
|
|
||||||
if (!isPlaying && !playerIsSeeking) {
|
|
||||||
sendAndResetWatchmanData();
|
|
||||||
stopWatchmanInterval();
|
|
||||||
// if being told to pause, and seeking, send and restart interval
|
|
||||||
} else if (!isPlaying && playerIsSeeking) {
|
|
||||||
sendAndResetWatchmanData();
|
|
||||||
stopWatchmanInterval();
|
|
||||||
startWatchmanIntervalIfNotRunning();
|
|
||||||
// is being told to play, and seeking, don't do anything,
|
|
||||||
// assume it's been started already from pause
|
|
||||||
} else if (isPlaying && playerIsSeeking) {
|
|
||||||
// start but not a seek, assuming a start from paused content
|
|
||||||
} else if (isPlaying && !playerIsSeeking) {
|
|
||||||
startWatchmanIntervalIfNotRunning();
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
videoStartEvent: (claimId, timeToStartVideo, poweredBy, passedUserId, canonicalUrl, passedPlayer, videoBitrate) => {
|
videoStartEvent: (claimId, timeToStartVideo, poweredBy, passedUserId, canonicalUrl, passedPlayer, videoBitrate) => {
|
||||||
// populate values for watchman when video starts
|
|
||||||
userId = passedUserId;
|
|
||||||
claimUrl = canonicalUrl;
|
|
||||||
playerPoweredBy = poweredBy;
|
|
||||||
|
|
||||||
videoType = passedPlayer.currentSource().type;
|
|
||||||
videoPlayer = passedPlayer;
|
|
||||||
bitrateAsBitsPerSecond = videoBitrate;
|
|
||||||
|
|
||||||
// sendPromMetric('time_to_start', duration);
|
// sendPromMetric('time_to_start', duration);
|
||||||
sendMatomoEvent('Media', 'TimeToStart', claimId, timeToStartVideo);
|
sendMatomoEvent('Media', 'TimeToStart', claimId, timeToStartVideo);
|
||||||
},
|
},
|
||||||
|
@ -382,24 +251,9 @@ function sendMatomoEvent(category, action, name, value) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prometheus
|
|
||||||
// function sendPromMetric(name: string, value?: number) {
|
|
||||||
// if (IS_WEB) {
|
|
||||||
// let url = new URL(SDK_API_PATH + '/metric/ui');
|
|
||||||
// const params = { name: name, value: value ? value.toString() : '' };
|
|
||||||
// url.search = new URLSearchParams(params).toString();
|
|
||||||
// return fetch(url, { method: 'post' });
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
const MatomoInstance = new MatomoTracker({
|
const MatomoInstance = new MatomoTracker({
|
||||||
urlBase: MATOMO_URL,
|
urlBase: MATOMO_URL,
|
||||||
siteId: MATOMO_ID, // optional, default value: `1`
|
siteId: MATOMO_ID, // optional, default value: `1`
|
||||||
// heartBeat: { // optional, enabled by default
|
|
||||||
// active: true, // optional, default value: true
|
|
||||||
// seconds: 10 // optional, default value: `15
|
|
||||||
// },
|
|
||||||
// linkTracking: false // optional, default value: true
|
|
||||||
});
|
});
|
||||||
|
|
||||||
analytics.pageView(generateInitialUrl(window.location.hash));
|
analytics.pageView(generateInitialUrl(window.location.hash));
|
||||||
|
|
|
@ -143,7 +143,6 @@ function App(props: Props) {
|
||||||
const shouldMigrateLanguage = LANGUAGE_MIGRATIONS[language];
|
const shouldMigrateLanguage = LANGUAGE_MIGRATIONS[language];
|
||||||
const hasActiveChannelClaim = activeChannelId !== undefined;
|
const hasActiveChannelClaim = activeChannelId !== undefined;
|
||||||
const isPersonalized = hasVerifiedEmail;
|
const isPersonalized = hasVerifiedEmail;
|
||||||
const renderFiledrop = isAuthenticated;
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (userId) {
|
if (userId) {
|
||||||
|
@ -349,7 +348,7 @@ function App(props: Props) {
|
||||||
>
|
>
|
||||||
<Router />
|
<Router />
|
||||||
<ModalRouter />
|
<ModalRouter />
|
||||||
{renderFiledrop && <FileDrop />}
|
<FileDrop />
|
||||||
<FileRenderFloating />
|
<FileRenderFloating />
|
||||||
{isEnhancedLayout && <Yrbl className="yrbl--enhanced" />}
|
{isEnhancedLayout && <Yrbl className="yrbl--enhanced" />}
|
||||||
|
|
||||||
|
|
21
ui/component/appStorageVisualization/index.js
Normal file
21
ui/component/appStorageVisualization/index.js
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import StorageViz from './view';
|
||||||
|
import {
|
||||||
|
selectViewBlobSpace,
|
||||||
|
selectViewHostingLimit,
|
||||||
|
selectAutoBlobSpace,
|
||||||
|
selectPrivateBlobSpace,
|
||||||
|
selectAutoHostingLimit,
|
||||||
|
} from 'redux/selectors/settings';
|
||||||
|
import { selectDiskSpace } from 'redux/selectors/app';
|
||||||
|
|
||||||
|
const select = (state) => ({
|
||||||
|
diskSpace: selectDiskSpace(state),
|
||||||
|
viewHostingLimit: selectViewHostingLimit(state),
|
||||||
|
autoHostingLimit: selectAutoHostingLimit(state),
|
||||||
|
viewBlobSpace: selectViewBlobSpace(state),
|
||||||
|
autoBlobSpace: selectAutoBlobSpace(state),
|
||||||
|
privateBlobSpace: selectPrivateBlobSpace(state),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(select)(StorageViz);
|
130
ui/component/appStorageVisualization/view.jsx
Normal file
130
ui/component/appStorageVisualization/view.jsx
Normal file
|
@ -0,0 +1,130 @@
|
||||||
|
// @flow
|
||||||
|
import * as React from 'react';
|
||||||
|
import I18nMessage from 'component/i18nMessage';
|
||||||
|
import { ipcRenderer } from 'electron';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
// --- select ---
|
||||||
|
diskSpace: DiskSpace, // KB
|
||||||
|
viewHostingLimit: number, // MB
|
||||||
|
autoHostingLimit: number,
|
||||||
|
viewBlobSpace: number,
|
||||||
|
autoBlobSpace: number,
|
||||||
|
privateBlobSpace: number,
|
||||||
|
};
|
||||||
|
|
||||||
|
function StorageViz(props: Props) {
|
||||||
|
const { diskSpace, viewHostingLimit, autoHostingLimit, viewBlobSpace, autoBlobSpace, privateBlobSpace } = props;
|
||||||
|
React.useEffect(() => {
|
||||||
|
ipcRenderer.send('get-disk-space');
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (!diskSpace || !diskSpace.total) {
|
||||||
|
return (
|
||||||
|
<div className={'storage__wrapper'}>
|
||||||
|
<div className={'storage__bar'}>
|
||||||
|
<div className="help">{__('Cannot get disk space information.')}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalMB = diskSpace && Math.floor(diskSpace.total / 1024);
|
||||||
|
const freeMB = diskSpace && Math.floor(diskSpace.free / 1024);
|
||||||
|
const otherMB = totalMB - (freeMB + viewBlobSpace + autoBlobSpace + privateBlobSpace);
|
||||||
|
const autoFree = autoHostingLimit - autoBlobSpace;
|
||||||
|
const viewFree = viewHostingLimit > 0 ? viewHostingLimit - viewBlobSpace : freeMB - autoFree;
|
||||||
|
const unallocFree = freeMB - viewFree - autoFree;
|
||||||
|
const viewLimit =
|
||||||
|
viewHostingLimit === 0
|
||||||
|
? freeMB - (autoHostingLimit - autoBlobSpace) + viewBlobSpace
|
||||||
|
: viewHostingLimit + viewBlobSpace;
|
||||||
|
|
||||||
|
const getPercent = (val, lim = totalMB) => (val / lim) * 100;
|
||||||
|
const getGB = (val) => (Number(val) / 1024).toFixed(2);
|
||||||
|
|
||||||
|
const otherPercent = getPercent(otherMB);
|
||||||
|
const privatePercent = getPercent(privateBlobSpace);
|
||||||
|
const autoLimitPercent = getPercent(autoHostingLimit);
|
||||||
|
const viewLimitPercent = getPercent(viewLimit);
|
||||||
|
const viewUsedPercentOfLimit = getPercent(viewBlobSpace, viewLimit);
|
||||||
|
const autoUsedPercentOfLimit = getPercent(autoBlobSpace, autoHostingLimit);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={'storage__wrapper'}>
|
||||||
|
<div className={'storage__bar'}>
|
||||||
|
<div className={'storage__other'} style={{ width: `${otherPercent}%` }} />
|
||||||
|
<div className={'storage__private'} style={{ width: `${privatePercent}%` }} />
|
||||||
|
<div className={'storage__auto'} style={{ width: `${autoLimitPercent}%` }}>
|
||||||
|
<div className={'storage__auto--used'} style={{ width: `${autoUsedPercentOfLimit}%` }} />
|
||||||
|
<div className={'storage__auto--free'} />
|
||||||
|
</div>
|
||||||
|
<div className={'storage__viewed'} style={{ width: `${viewLimitPercent}%` }}>
|
||||||
|
<div className={'storage__viewed--used'} style={{ width: `${viewUsedPercentOfLimit}%` }} />
|
||||||
|
<div className={'storage__viewed--free'} />
|
||||||
|
</div>
|
||||||
|
{viewHostingLimit !== 0 && <div style={{ 'background-color': 'unset' }} />}
|
||||||
|
</div>
|
||||||
|
<div className={'storage__legend-wrapper'}>
|
||||||
|
<div className={'storage__legend-item'}>
|
||||||
|
<div className={'storage__legend-item-swatch storage__legend-item-swatch--private'} />
|
||||||
|
<div className={'storage__legend-item-label'}>
|
||||||
|
<label>{__('Publishes --[legend, storage category]--')}</label>
|
||||||
|
<div className={'help'}>{`${getGB(privateBlobSpace)} GB`}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={'storage__legend-item'}>
|
||||||
|
<div className={'storage__legend-item-swatch storage__legend-item-swatch--auto'} />
|
||||||
|
<div className={'storage__legend-item-label'}>
|
||||||
|
<label>{__('Auto Hosting --[legend, storage category]--')}</label>
|
||||||
|
<div className={'help'}>
|
||||||
|
{autoHostingLimit === 0 ? (
|
||||||
|
__('Disabled')
|
||||||
|
) : (
|
||||||
|
<I18nMessage
|
||||||
|
tokens={{
|
||||||
|
spaceUsed: getGB(autoBlobSpace),
|
||||||
|
limit: getGB(autoHostingLimit),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
%spaceUsed% of %limit% GB
|
||||||
|
</I18nMessage>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={'storage__legend-item'}>
|
||||||
|
<div className={'storage__legend-item-swatch storage__legend-item-swatch--viewed'} />
|
||||||
|
<div className={'storage__legend-item-label'}>
|
||||||
|
<label>{__('View Hosting --[legend, storage category]--')}</label>
|
||||||
|
<div className={'help'}>
|
||||||
|
{viewHostingLimit === 1 ? (
|
||||||
|
__('Disabled')
|
||||||
|
) : (
|
||||||
|
<I18nMessage
|
||||||
|
tokens={{
|
||||||
|
spaceUsed: getGB(viewBlobSpace),
|
||||||
|
limit: viewHostingLimit !== 0 ? getGB(viewHostingLimit) : getGB(viewFree),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
%spaceUsed% of %limit% Free GB
|
||||||
|
</I18nMessage>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{viewHostingLimit !== 0 && (
|
||||||
|
<div className={'storage__legend-item'}>
|
||||||
|
<div className={'storage__legend-item-swatch storage__legend-item-swatch--free'} />
|
||||||
|
<div className={'storage__legend-item-label'}>
|
||||||
|
<label>{__('Free --[legend, unused disk space]--')}</label>
|
||||||
|
<div className={'help'}>{`${getGB(unallocFree)} GB`}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default StorageViz;
|
|
@ -5,7 +5,6 @@ import Icon from 'component/common/icon';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import { NavLink } from 'react-router-dom';
|
import { NavLink } from 'react-router-dom';
|
||||||
import { formatLbryUrlForWeb } from 'util/url';
|
import { formatLbryUrlForWeb } from 'util/url';
|
||||||
import * as PAGES from 'constants/pages';
|
|
||||||
import useCombinedRefs from 'effects/use-combined-refs';
|
import useCombinedRefs from 'effects/use-combined-refs';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
@ -34,7 +33,6 @@ type Props = {
|
||||||
onMouseLeave: ?(any) => any,
|
onMouseLeave: ?(any) => any,
|
||||||
pathname: string,
|
pathname: string,
|
||||||
emailVerified: boolean,
|
emailVerified: boolean,
|
||||||
requiresAuth: ?boolean,
|
|
||||||
myref: any,
|
myref: any,
|
||||||
dispatch: any,
|
dispatch: any,
|
||||||
'aria-label'?: string,
|
'aria-label'?: string,
|
||||||
|
@ -66,7 +64,6 @@ const Button = forwardRef<any, {}>((props: Props, ref: any) => {
|
||||||
iconColor,
|
iconColor,
|
||||||
activeClass,
|
activeClass,
|
||||||
emailVerified,
|
emailVerified,
|
||||||
requiresAuth,
|
|
||||||
myref,
|
myref,
|
||||||
dispatch, // <button> doesn't know what to do with dispatch
|
dispatch, // <button> doesn't know what to do with dispatch
|
||||||
pathname,
|
pathname,
|
||||||
|
@ -75,7 +72,7 @@ const Button = forwardRef<any, {}>((props: Props, ref: any) => {
|
||||||
...otherProps
|
...otherProps
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const disable = disabled || (user === null && requiresAuth);
|
const disable = disabled;
|
||||||
|
|
||||||
const combinedClassName = classnames(
|
const combinedClassName = classnames(
|
||||||
'button',
|
'button',
|
||||||
|
@ -183,31 +180,6 @@ const Button = forwardRef<any, {}>((props: Props, ref: any) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (requiresAuth && !emailVerified) {
|
|
||||||
let redirectUrl = `/$/${PAGES.AUTH}?redirect=${pathname}`;
|
|
||||||
|
|
||||||
if (authSrc) {
|
|
||||||
redirectUrl += `&src=${authSrc}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<NavLink
|
|
||||||
exact
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
}}
|
|
||||||
to={redirectUrl}
|
|
||||||
title={title || defaultTooltip}
|
|
||||||
disabled={disable}
|
|
||||||
className={combinedClassName}
|
|
||||||
activeClassName={activeClass}
|
|
||||||
aria-label={ariaLabel}
|
|
||||||
>
|
|
||||||
{content}
|
|
||||||
</NavLink>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return path ? (
|
return path ? (
|
||||||
<NavLink
|
<NavLink
|
||||||
exact
|
exact
|
||||||
|
|
|
@ -6,6 +6,7 @@ import CreditAmount from 'component/common/credit-amount';
|
||||||
import DateTime from 'component/dateTime';
|
import DateTime from 'component/dateTime';
|
||||||
import YoutubeBadge from 'component/youtubeBadge';
|
import YoutubeBadge from 'component/youtubeBadge';
|
||||||
import SUPPORTED_LANGUAGES from 'constants/supported_languages';
|
import SUPPORTED_LANGUAGES from 'constants/supported_languages';
|
||||||
|
import { formatNumber } from 'util/number';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
claim: ChannelClaim,
|
claim: ChannelClaim,
|
||||||
|
@ -74,7 +75,7 @@ function ChannelAbout(props: Props) {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<label>{__('Total Uploads')}</label>
|
<label>{__('Total Uploads')}</label>
|
||||||
<div className="media__info-text">{claim.meta.claims_in_channel}</div>
|
<div className="media__info-text">{formatNumber(claim.meta.claims_in_channel || 0, 2, true)}</div>
|
||||||
|
|
||||||
<label>{__('Last Updated')}</label>
|
<label>{__('Last Updated')}</label>
|
||||||
<div className="media__info-text">
|
<div className="media__info-text">
|
||||||
|
|
|
@ -3,10 +3,9 @@ import * as MODALS from 'constants/modal_types';
|
||||||
import * as ICONS from 'constants/icons';
|
import * as ICONS from 'constants/icons';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import { FormField } from 'component/common/form';
|
import { FormField, FormFieldAreaAdvanced } from 'component/common/form';
|
||||||
import Button from 'component/button';
|
import Button from 'component/button';
|
||||||
import TagsSearch from 'component/tagsSearch';
|
import TagsSearch from 'component/tagsSearch';
|
||||||
import { FF_MAX_CHARS_IN_DESCRIPTION } from 'constants/form-field';
|
|
||||||
import ErrorText from 'component/common/error-text';
|
import ErrorText from 'component/common/error-text';
|
||||||
import ChannelThumbnail from 'component/channelThumbnail';
|
import ChannelThumbnail from 'component/channelThumbnail';
|
||||||
import { isNameValid, parseURI } from 'util/lbryURI';
|
import { isNameValid, parseURI } from 'util/lbryURI';
|
||||||
|
@ -27,6 +26,9 @@ import Gerbil from 'component/channelThumbnail/gerbil.png';
|
||||||
const LANG_NONE = 'none';
|
const LANG_NONE = 'none';
|
||||||
|
|
||||||
const MAX_TAG_SELECT = 5;
|
const MAX_TAG_SELECT = 5;
|
||||||
|
const MAX_NAME_LEN = 128;
|
||||||
|
const MAX_TITLE_LEN = 255;
|
||||||
|
const MAX_DESCRIPTION_LEN = 2056;
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
claim: ChannelClaim,
|
claim: ChannelClaim,
|
||||||
|
@ -92,10 +94,11 @@ function ChannelForm(props: Props) {
|
||||||
const [nameError, setNameError] = React.useState(undefined);
|
const [nameError, setNameError] = React.useState(undefined);
|
||||||
const [bidError, setBidError] = React.useState('');
|
const [bidError, setBidError] = React.useState('');
|
||||||
const [isUpload, setIsUpload] = React.useState({ cover: false, thumbnail: false });
|
const [isUpload, setIsUpload] = React.useState({ cover: false, thumbnail: false });
|
||||||
const [coverError, setCoverError] = React.useState(false);
|
|
||||||
const [thumbError, setThumbError] = React.useState(false);
|
const [thumbError, setThumbError] = React.useState(false);
|
||||||
const { claim_id: claimId } = claim || {};
|
const { claim_id: claimId } = claim || {};
|
||||||
const [params, setParams]: [any, (any) => void] = React.useState(getChannelParams());
|
const [params, setParams]: [any, (any) => void] = React.useState(getChannelParams());
|
||||||
|
const [coverError, setCoverError] = React.useState(false);
|
||||||
|
const [coverPreview, setCoverPreview] = React.useState(params.coverUrl);
|
||||||
const { channelName } = parseURI(uri);
|
const { channelName } = parseURI(uri);
|
||||||
const name = params.name;
|
const name = params.name;
|
||||||
const isNewChannel = !uri;
|
const isNewChannel = !uri;
|
||||||
|
@ -204,7 +207,8 @@ function ChannelForm(props: Props) {
|
||||||
setThumbError(false);
|
setThumbError(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleCoverChange(coverUrl: string, uploadSelected: boolean) {
|
function handleCoverChange(coverUrl: string, uploadSelected: boolean, preview: ?string) {
|
||||||
|
setCoverPreview(preview || '');
|
||||||
setParams({ ...params, coverUrl });
|
setParams({ ...params, coverUrl });
|
||||||
setIsUpload({ ...isUpload, cover: uploadSelected });
|
setIsUpload({ ...isUpload, cover: uploadSelected });
|
||||||
setCoverError(false);
|
setCoverError(false);
|
||||||
|
@ -257,7 +261,7 @@ function ChannelForm(props: Props) {
|
||||||
}
|
}
|
||||||
}, [hasClaimedInitialRewards, claimInitialRewards]);
|
}, [hasClaimedInitialRewards, claimInitialRewards]);
|
||||||
|
|
||||||
const coverSrc = coverError ? ThumbnailBrokenImage : params.coverUrl;
|
const coverSrc = coverError ? ThumbnailBrokenImage : coverPreview;
|
||||||
|
|
||||||
let thumbnailPreview;
|
let thumbnailPreview;
|
||||||
if (!params.thumbnailUrl) {
|
if (!params.thumbnailUrl) {
|
||||||
|
@ -279,7 +283,7 @@ function ChannelForm(props: Props) {
|
||||||
title={__('Cover')}
|
title={__('Cover')}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
openModal(MODALS.IMAGE_UPLOAD, {
|
openModal(MODALS.IMAGE_UPLOAD, {
|
||||||
onUpdate: (coverUrl, isUpload) => handleCoverChange(coverUrl, isUpload),
|
onUpdate: (coverUrl, isUpload, preview) => handleCoverChange(coverUrl, isUpload, preview),
|
||||||
title: __('Edit Cover Image'),
|
title: __('Edit Cover Image'),
|
||||||
helpText: __('(6.25:1)'),
|
helpText: __('(6.25:1)'),
|
||||||
assetName: __('Cover Image'),
|
assetName: __('Cover Image'),
|
||||||
|
@ -321,7 +325,6 @@ function ChannelForm(props: Props) {
|
||||||
uri={uri}
|
uri={uri}
|
||||||
thumbnailPreview={thumbnailPreview}
|
thumbnailPreview={thumbnailPreview}
|
||||||
allowGifs
|
allowGifs
|
||||||
showDelayedMessage={isUpload.thumbnail}
|
|
||||||
setThumbUploadError={setThumbError}
|
setThumbUploadError={setThumbError}
|
||||||
thumbUploadError={thumbError}
|
thumbUploadError={thumbError}
|
||||||
/>
|
/>
|
||||||
|
@ -359,6 +362,7 @@ function ChannelForm(props: Props) {
|
||||||
error={nameError}
|
error={nameError}
|
||||||
disabled={!isNewChannel}
|
disabled={!isNewChannel}
|
||||||
onChange={(e) => setParams({ ...params, name: e.target.value })}
|
onChange={(e) => setParams({ ...params, name: e.target.value })}
|
||||||
|
maxLength={MAX_NAME_LEN}
|
||||||
/>
|
/>
|
||||||
</fieldset-group>
|
</fieldset-group>
|
||||||
{!isNewChannel && <span className="form-field__help">{__('This field cannot be changed.')}</span>}
|
{!isNewChannel && <span className="form-field__help">{__('This field cannot be changed.')}</span>}
|
||||||
|
@ -370,15 +374,16 @@ function ChannelForm(props: Props) {
|
||||||
placeholder={__('My Awesome Channel')}
|
placeholder={__('My Awesome Channel')}
|
||||||
value={params.title}
|
value={params.title}
|
||||||
onChange={(e) => setParams({ ...params, title: e.target.value })}
|
onChange={(e) => setParams({ ...params, title: e.target.value })}
|
||||||
|
maxLength={MAX_TITLE_LEN}
|
||||||
/>
|
/>
|
||||||
<FormField
|
<FormFieldAreaAdvanced
|
||||||
type="markdown"
|
type="markdown"
|
||||||
name="content_description2"
|
name="content_description2"
|
||||||
label={__('Description')}
|
label={__('Description')}
|
||||||
placeholder={__('Description of your content')}
|
placeholder={__('Description of your content')}
|
||||||
value={params.description}
|
value={params.description}
|
||||||
onChange={(text) => setParams({ ...params, description: text })}
|
onChange={(text) => setParams({ ...params, description: text })}
|
||||||
textAreaMaxLength={FF_MAX_CHARS_IN_DESCRIPTION}
|
textAreaMaxLength={MAX_DESCRIPTION_LEN}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
|
|
@ -50,6 +50,7 @@ function ChannelThumbnail(props: Props) {
|
||||||
setThumbUploadError,
|
setThumbUploadError,
|
||||||
ThumbUploadError,
|
ThumbUploadError,
|
||||||
} = props;
|
} = props;
|
||||||
|
const [retries, setRetries] = React.useState(3);
|
||||||
const [thumbLoadError, setThumbLoadError] = React.useState(ThumbUploadError);
|
const [thumbLoadError, setThumbLoadError] = React.useState(ThumbUploadError);
|
||||||
const shouldResolve = !isResolving && claim === undefined;
|
const shouldResolve = !isResolving && claim === undefined;
|
||||||
const thumbnail = rawThumbnail && rawThumbnail.trim().replace(/^http:\/\//i, 'https://');
|
const thumbnail = rawThumbnail && rawThumbnail.trim().replace(/^http:\/\//i, 'https://');
|
||||||
|
@ -58,6 +59,15 @@ function ChannelThumbnail(props: Props) {
|
||||||
const channelThumbnail = thumbnailPreview || thumbnail || defaultAvatar;
|
const channelThumbnail = thumbnailPreview || thumbnail || defaultAvatar;
|
||||||
const isGif = channelThumbnail && channelThumbnail.endsWith('gif');
|
const isGif = channelThumbnail && channelThumbnail.endsWith('gif');
|
||||||
const showThumb = (!obscure && !!thumbnail) || thumbnailPreview;
|
const showThumb = (!obscure && !!thumbnail) || thumbnailPreview;
|
||||||
|
const avatarSrc = React.useMemo(() => {
|
||||||
|
if (retries <= 0) {
|
||||||
|
return defaultAvatar;
|
||||||
|
}
|
||||||
|
if (!thumbLoadError) {
|
||||||
|
return channelThumbnail;
|
||||||
|
}
|
||||||
|
return defaultAvatar;
|
||||||
|
}, [retries, thumbLoadError, channelThumbnail, defaultAvatar]);
|
||||||
|
|
||||||
// Generate a random color class based on the first letter of the channel name
|
// Generate a random color class based on the first letter of the channel name
|
||||||
const { channelName } = parseURI(uri);
|
const { channelName } = parseURI(uri);
|
||||||
|
@ -100,9 +110,10 @@ function ChannelThumbnail(props: Props) {
|
||||||
<OptimizedImage
|
<OptimizedImage
|
||||||
alt={__('Channel profile picture')}
|
alt={__('Channel profile picture')}
|
||||||
className={!channelThumbnail ? 'channel-thumbnail__default' : 'channel-thumbnail__custom'}
|
className={!channelThumbnail ? 'channel-thumbnail__default' : 'channel-thumbnail__custom'}
|
||||||
src={(!thumbLoadError && channelThumbnail) || defaultAvatar}
|
src={avatarSrc}
|
||||||
loading={noLazyLoad ? undefined : 'lazy'}
|
loading={noLazyLoad ? undefined : 'lazy'}
|
||||||
onError={() => {
|
onError={() => {
|
||||||
|
setRetries((retries) => retries - 1);
|
||||||
if (setThumbUploadError) {
|
if (setThumbUploadError) {
|
||||||
setThumbUploadError(true);
|
setThumbUploadError(true);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -110,6 +110,11 @@ function ClaimMenuList(props: Props) {
|
||||||
const [doShuffle, setDoShuffle] = React.useState(false);
|
const [doShuffle, setDoShuffle] = React.useState(false);
|
||||||
const incognitoClaim = contentChannelUri && !contentChannelUri.includes('@');
|
const incognitoClaim = contentChannelUri && !contentChannelUri.includes('@');
|
||||||
const isChannel = !incognitoClaim && !contentSigningChannel;
|
const isChannel = !incognitoClaim && !contentSigningChannel;
|
||||||
|
// $FlowFixMe
|
||||||
|
const claimLength = claim && claim.value && claim.value.claims && claim.value.claims.length;
|
||||||
|
// $FlowFixMe
|
||||||
|
const claimCount = editedCollection ? editedCollection.items.length : claimLength;
|
||||||
|
const isEmptyCollection = (Number(claimCount) || 0) <= 0;
|
||||||
const { channelName } = parseURI(contentChannelUri);
|
const { channelName } = parseURI(contentChannelUri);
|
||||||
const showDelete = claimIsMine || (fileInfo && (fileInfo.written_bytes > 0 || fileInfo.blobs_completed > 0));
|
const showDelete = claimIsMine || (fileInfo && (fileInfo.written_bytes > 0 || fileInfo.blobs_completed > 0));
|
||||||
const subscriptionLabel = repostedClaim
|
const subscriptionLabel = repostedClaim
|
||||||
|
@ -297,6 +302,7 @@ function ClaimMenuList(props: Props) {
|
||||||
{__('View List')}
|
{__('View List')}
|
||||||
</a>
|
</a>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
{!isEmptyCollection && (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
className="comment__menu-option"
|
className="comment__menu-option"
|
||||||
onSelect={() => {
|
onSelect={() => {
|
||||||
|
@ -309,6 +315,7 @@ function ClaimMenuList(props: Props) {
|
||||||
{__('Shuffle Play')}
|
{__('Shuffle Play')}
|
||||||
</div>
|
</div>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
)}
|
||||||
{isMyCollection && (
|
{isMyCollection && (
|
||||||
<>
|
<>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
|
|
|
@ -9,7 +9,7 @@ import * as COLLECTIONS_CONSTS from 'constants/collections';
|
||||||
import { isChannelClaim } from 'util/claim';
|
import { isChannelClaim } from 'util/claim';
|
||||||
import { formatLbryUrlForWeb } from 'util/url';
|
import { formatLbryUrlForWeb } from 'util/url';
|
||||||
import { formatClaimPreviewTitle } from 'util/formatAriaLabel';
|
import { formatClaimPreviewTitle } from 'util/formatAriaLabel';
|
||||||
import { toCompactNotation } from 'util/string';
|
import { formatNumber } from 'util/number';
|
||||||
import Tooltip from 'component/common/tooltip';
|
import Tooltip from 'component/common/tooltip';
|
||||||
import FileThumbnail from 'component/fileThumbnail';
|
import FileThumbnail from 'component/fileThumbnail';
|
||||||
import UriIndicator from 'component/uriIndicator';
|
import UriIndicator from 'component/uriIndicator';
|
||||||
|
@ -138,7 +138,6 @@ const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
|
||||||
indexInContainer,
|
indexInContainer,
|
||||||
channelSubCount,
|
channelSubCount,
|
||||||
swipeLayout = false,
|
swipeLayout = false,
|
||||||
lang,
|
|
||||||
showEdit,
|
showEdit,
|
||||||
dragHandleProps,
|
dragHandleProps,
|
||||||
unavailableUris,
|
unavailableUris,
|
||||||
|
@ -162,8 +161,8 @@ const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
|
||||||
if (channelSubCount === undefined) {
|
if (channelSubCount === undefined) {
|
||||||
return <span />;
|
return <span />;
|
||||||
}
|
}
|
||||||
const formattedSubCount = toCompactNotation(channelSubCount, lang, 10000);
|
const formattedSubCount = formatNumber(channelSubCount, 2, true);
|
||||||
const formattedSubCountLocale = Number(channelSubCount).toLocaleString();
|
const formattedSubCountLocale = formatNumber(channelSubCount, 2, false);
|
||||||
return (
|
return (
|
||||||
<div className="media__subtitle">
|
<div className="media__subtitle">
|
||||||
<Tooltip title={formattedSubCountLocale} followCursor placement="top">
|
<Tooltip title={formattedSubCountLocale} followCursor placement="top">
|
||||||
|
|
|
@ -5,6 +5,7 @@ import DateTime from 'component/dateTime';
|
||||||
import Button from 'component/button';
|
import Button from 'component/button';
|
||||||
import FileViewCountInline from 'component/fileViewCountInline';
|
import FileViewCountInline from 'component/fileViewCountInline';
|
||||||
import { parseURI } from 'util/lbryURI';
|
import { parseURI } from 'util/lbryURI';
|
||||||
|
import { formatNumber } from 'util/number';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
uri: string,
|
uri: string,
|
||||||
|
@ -34,7 +35,7 @@ function ClaimPreviewSubtitle(props: Props) {
|
||||||
<>
|
<>
|
||||||
{isChannel &&
|
{isChannel &&
|
||||||
type !== 'inline' &&
|
type !== 'inline' &&
|
||||||
`${claimsInChannel} ${claimsInChannel === 1 ? __('upload') : __('uploads')}`}
|
`${formatNumber(claimsInChannel, 2, true)} ${claimsInChannel === 1 ? __('upload') : __('uploads')}`}
|
||||||
|
|
||||||
{!isChannel && (
|
{!isChannel && (
|
||||||
<>
|
<>
|
||||||
|
|
|
@ -54,9 +54,13 @@ function CollectionActions(props: Props) {
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
const claimId = claim && claim.claim_id;
|
const claimId = claim && claim.claim_id;
|
||||||
const webShareable = true; // collections have cost?
|
const webShareable = true; // collections have cost?
|
||||||
|
const isEmptyCollection = !firstItem;
|
||||||
|
|
||||||
const doPlay = React.useCallback(
|
const doPlay = React.useCallback(
|
||||||
(playUri) => {
|
(playUri) => {
|
||||||
|
if (!playUri) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const navigateUrl = formatLbryUrlForWeb(playUri);
|
const navigateUrl = formatLbryUrlForWeb(playUri);
|
||||||
push({
|
push({
|
||||||
pathname: navigateUrl,
|
pathname: navigateUrl,
|
||||||
|
@ -81,6 +85,7 @@ function CollectionActions(props: Props) {
|
||||||
icon={ICONS.PLAY}
|
icon={ICONS.PLAY}
|
||||||
label={__('Play')}
|
label={__('Play')}
|
||||||
title={__('Play')}
|
title={__('Play')}
|
||||||
|
disabled={isEmptyCollection}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
doToggleShuffleList(collectionId, false);
|
doToggleShuffleList(collectionId, false);
|
||||||
doPlay(firstItem);
|
doPlay(firstItem);
|
||||||
|
@ -91,6 +96,7 @@ function CollectionActions(props: Props) {
|
||||||
icon={ICONS.SHUFFLE}
|
icon={ICONS.SHUFFLE}
|
||||||
label={__('Shuffle Play')}
|
label={__('Shuffle Play')}
|
||||||
title={__('Shuffle Play')}
|
title={__('Shuffle Play')}
|
||||||
|
disabled={isEmptyCollection}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
doToggleShuffleList(collectionId, true);
|
doToggleShuffleList(collectionId, true);
|
||||||
setDoShuffle(true);
|
setDoShuffle(true);
|
||||||
|
|
|
@ -23,6 +23,7 @@ import CollectionForm from './view';
|
||||||
import { selectActiveChannelClaim, selectIncognito } from 'redux/selectors/app';
|
import { selectActiveChannelClaim, selectIncognito } from 'redux/selectors/app';
|
||||||
import { doSetActiveChannel, doSetIncognito } from 'redux/actions/app';
|
import { doSetActiveChannel, doSetIncognito } from 'redux/actions/app';
|
||||||
import { doCollectionEdit } from 'redux/actions/collections';
|
import { doCollectionEdit } from 'redux/actions/collections';
|
||||||
|
import { doResetThumbnailStatus } from 'redux/actions/publish';
|
||||||
|
|
||||||
const select = (state, props) => ({
|
const select = (state, props) => ({
|
||||||
claim: makeSelectClaimForUri(props.uri)(state),
|
claim: makeSelectClaimForUri(props.uri)(state),
|
||||||
|
@ -52,6 +53,7 @@ const perform = (dispatch, ownProps) => ({
|
||||||
setActiveChannel: (claimId) => dispatch(doSetActiveChannel(claimId)),
|
setActiveChannel: (claimId) => dispatch(doSetActiveChannel(claimId)),
|
||||||
setIncognito: (incognito) => dispatch(doSetIncognito(incognito)),
|
setIncognito: (incognito) => dispatch(doSetIncognito(incognito)),
|
||||||
doCollectionEdit: (params) => dispatch(doCollectionEdit(ownProps.collectionId, params)),
|
doCollectionEdit: (params) => dispatch(doCollectionEdit(ownProps.collectionId, params)),
|
||||||
|
resetThumbnailStatus: () => dispatch(doResetThumbnailStatus()),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(select, perform)(CollectionForm);
|
export default connect(select, perform)(CollectionForm);
|
||||||
|
|
|
@ -17,7 +17,7 @@ import { useHistory } from 'react-router-dom';
|
||||||
import { isNameValid, regexInvalidURI } from 'util/lbryURI';
|
import { isNameValid, regexInvalidURI } from 'util/lbryURI';
|
||||||
import * as THUMBNAIL_STATUSES from 'constants/thumbnail_upload_statuses';
|
import * as THUMBNAIL_STATUSES from 'constants/thumbnail_upload_statuses';
|
||||||
import { Tabs, TabList, Tab, TabPanels, TabPanel } from 'component/common/tabs';
|
import { Tabs, TabList, Tab, TabPanels, TabPanel } from 'component/common/tabs';
|
||||||
import { FormField } from 'component/common/form';
|
import { FormField, FormFieldAreaAdvanced } from 'component/common/form';
|
||||||
import { handleBidChange } from 'util/publish';
|
import { handleBidChange } from 'util/publish';
|
||||||
import { FF_MAX_CHARS_IN_DESCRIPTION } from 'constants/form-field';
|
import { FF_MAX_CHARS_IN_DESCRIPTION } from 'constants/form-field';
|
||||||
import { INVALID_NAME_ERROR } from 'constants/claim';
|
import { INVALID_NAME_ERROR } from 'constants/claim';
|
||||||
|
@ -58,6 +58,7 @@ type Props = {
|
||||||
setActiveChannel: (string) => void,
|
setActiveChannel: (string) => void,
|
||||||
setIncognito: (boolean) => void,
|
setIncognito: (boolean) => void,
|
||||||
doCollectionEdit: (CollectionEditParams) => void,
|
doCollectionEdit: (CollectionEditParams) => void,
|
||||||
|
resetThumbnailStatus: () => void,
|
||||||
};
|
};
|
||||||
|
|
||||||
function CollectionForm(props: Props) {
|
function CollectionForm(props: Props) {
|
||||||
|
@ -92,6 +93,7 @@ function CollectionForm(props: Props) {
|
||||||
setIncognito,
|
setIncognito,
|
||||||
onDone,
|
onDone,
|
||||||
doCollectionEdit,
|
doCollectionEdit,
|
||||||
|
resetThumbnailStatus,
|
||||||
} = props;
|
} = props;
|
||||||
const activeChannelName = activeChannelClaim && activeChannelClaim.name;
|
const activeChannelName = activeChannelClaim && activeChannelClaim.name;
|
||||||
let prefix = 'lbry://';
|
let prefix = 'lbry://';
|
||||||
|
@ -158,7 +160,7 @@ function CollectionForm(props: Props) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleUpdateThumbnail(update: { [string]: string }) {
|
function handleUpdateThumbnail(update: { [string]: string }) {
|
||||||
if (update.thumbnail_url) {
|
if (update.thumbnail_url !== undefined) {
|
||||||
setParam(update);
|
setParam(update);
|
||||||
} else if (update.thumbnail_status) {
|
} else if (update.thumbnail_status) {
|
||||||
setThumbStatus(update.thumbnail_status);
|
setThumbStatus(update.thumbnail_status);
|
||||||
|
@ -309,6 +311,10 @@ function CollectionForm(props: Props) {
|
||||||
}
|
}
|
||||||
}, [uri, hasClaim]);
|
}, [uri, hasClaim]);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
resetThumbnailStatus();
|
||||||
|
}, [resetThumbnailStatus]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className={classnames('main--contained', { 'card--disabled': disabled })}>
|
<div className={classnames('main--contained', { 'card--disabled': disabled })}>
|
||||||
|
@ -358,13 +364,14 @@ function CollectionForm(props: Props) {
|
||||||
/>
|
/>
|
||||||
<fieldset-section>
|
<fieldset-section>
|
||||||
<SelectThumbnail
|
<SelectThumbnail
|
||||||
thumbnailParam={params.thumbnail_url}
|
thumbnail={params.thumbnail_url}
|
||||||
thumbnailParamError={thumbError}
|
thumbnailError={thumbError}
|
||||||
thumbnailParamStatus={thumbStatus}
|
thumbnailParamStatus={thumbStatus}
|
||||||
updateThumbnailParams={handleUpdateThumbnail}
|
updateThumbnailParams={handleUpdateThumbnail}
|
||||||
|
usePublishFormMode
|
||||||
/>
|
/>
|
||||||
</fieldset-section>
|
</fieldset-section>
|
||||||
<FormField
|
<FormFieldAreaAdvanced
|
||||||
type="markdown"
|
type="markdown"
|
||||||
name="content_description2"
|
name="content_description2"
|
||||||
label={__('Description')}
|
label={__('Description')}
|
||||||
|
|
|
@ -33,7 +33,7 @@ function CollectionPreviewOverlay(props: Props) {
|
||||||
collectionItemUrls.map((item, index) => {
|
collectionItemUrls.map((item, index) => {
|
||||||
if (index < 2) {
|
if (index < 2) {
|
||||||
return (
|
return (
|
||||||
<div className="collection-preview__overlay-grid-items">
|
<div key={item} className="collection-preview__overlay-grid-items">
|
||||||
<FileThumbnail uri={item} key={item} />
|
<FileThumbnail uri={item} key={item} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -95,7 +95,7 @@ export default function CollectionsListMine(props: Props) {
|
||||||
{builtin.map((list: Collection) => {
|
{builtin.map((list: Collection) => {
|
||||||
const { items: itemUrls } = list;
|
const { items: itemUrls } = list;
|
||||||
return (
|
return (
|
||||||
<>
|
<React.Fragment key={list.name}>
|
||||||
{Boolean(itemUrls && itemUrls.length) && (
|
{Boolean(itemUrls && itemUrls.length) && (
|
||||||
<div className="claim-grid__wrapper" key={list.name}>
|
<div className="claim-grid__wrapper" key={list.name}>
|
||||||
<h1 className="claim-grid__header">
|
<h1 className="claim-grid__header">
|
||||||
|
@ -124,7 +124,7 @@ export default function CollectionsListMine(props: Props) {
|
||||||
<ClaimList tileLayout key={list.name} uris={itemUrls.slice(0, 6)} collectionId={list.id} />
|
<ClaimList tileLayout key={list.name} uris={itemUrls.slice(0, 6)} collectionId={list.id} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
<div className="claim-grid__wrapper">
|
<div className="claim-grid__wrapper">
|
||||||
|
|
|
@ -8,6 +8,7 @@ import { FF_MAX_CHARS_IN_COMMENT } from 'constants/form-field';
|
||||||
import { SITE_NAME, ENABLE_COMMENT_REACTIONS } from 'config';
|
import { SITE_NAME, ENABLE_COMMENT_REACTIONS } from 'config';
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { parseURI } from 'util/lbryURI';
|
import { parseURI } from 'util/lbryURI';
|
||||||
|
import { formatNumber } from 'util/number';
|
||||||
import DateTime from 'component/dateTime';
|
import DateTime from 'component/dateTime';
|
||||||
import Button from 'component/button';
|
import Button from 'component/button';
|
||||||
import Expandable from 'component/expandable';
|
import Expandable from 'component/expandable';
|
||||||
|
@ -16,7 +17,7 @@ import CommentBadge from 'component/common/comment-badge'; // have this?
|
||||||
import ChannelThumbnail from 'component/channelThumbnail';
|
import ChannelThumbnail from 'component/channelThumbnail';
|
||||||
import { Menu, MenuButton } from '@reach/menu-button';
|
import { Menu, MenuButton } from '@reach/menu-button';
|
||||||
import Icon from 'component/common/icon';
|
import Icon from 'component/common/icon';
|
||||||
import { FormField, Form } from 'component/common/form';
|
import { FormFieldAreaAdvanced, Form } from 'component/common/form';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import usePersistedState from 'effects/use-persisted-state';
|
import usePersistedState from 'effects/use-persisted-state';
|
||||||
import CommentReactions from 'component/commentReactions';
|
import CommentReactions from 'component/commentReactions';
|
||||||
|
@ -318,7 +319,7 @@ function CommentView(props: Props) {
|
||||||
<div>
|
<div>
|
||||||
{isEditing ? (
|
{isEditing ? (
|
||||||
<Form onSubmit={handleSubmit}>
|
<Form onSubmit={handleSubmit}>
|
||||||
<FormField
|
<FormFieldAreaAdvanced
|
||||||
className="comment__edit-input"
|
className="comment__edit-input"
|
||||||
type={advancedEditor ? 'markdown' : 'textarea'}
|
type={advancedEditor ? 'markdown' : 'textarea'}
|
||||||
name="editing_comment"
|
name="editing_comment"
|
||||||
|
@ -384,7 +385,7 @@ function CommentView(props: Props) {
|
||||||
label={
|
label={
|
||||||
numDirectReplies < 2
|
numDirectReplies < 2
|
||||||
? __('Show reply')
|
? __('Show reply')
|
||||||
: __('Show %count% replies', { count: numDirectReplies })
|
: __('Show %count% replies', { count: formatNumber(numDirectReplies, 2, true) })
|
||||||
}
|
}
|
||||||
button="link"
|
button="link"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
|
32
ui/component/commentCreate/comment-create-header.jsx
Normal file
32
ui/component/commentCreate/comment-create-header.jsx
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
// @flow
|
||||||
|
import React from 'react';
|
||||||
|
import SelectChannel from 'component/selectChannel';
|
||||||
|
import Button from 'component/button';
|
||||||
|
import * as ICONS from 'constants/icons';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
isReply: boolean,
|
||||||
|
advancedHandler: () => void,
|
||||||
|
advanced: boolean,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function CommentCreateHeader(props: Props) {
|
||||||
|
const { isReply, advancedHandler, advanced } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="comment-create__header">
|
||||||
|
<div className="comment-create__label-wrapper">
|
||||||
|
<span className="comment-create__label">{(isReply ? __('Replying as') : __('Comment as')) + ' '}</span>
|
||||||
|
<SelectChannel tiny />
|
||||||
|
</div>
|
||||||
|
<div className="form-field__quick-action">
|
||||||
|
<Button
|
||||||
|
button="alt"
|
||||||
|
icon={advanced ? ICONS.SIMPLE_EDITOR : ICONS.ADVANCED_EDITOR}
|
||||||
|
onClick={advancedHandler}
|
||||||
|
aria-label={isReply ? undefined : advanced ? __('Simple Editor') : __('Advanced Editor')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -4,7 +4,7 @@ import 'scss/component/_comment-create.scss';
|
||||||
|
|
||||||
import { buildValidSticker } from 'util/comments';
|
import { buildValidSticker } from 'util/comments';
|
||||||
import { FF_MAX_CHARS_IN_COMMENT } from 'constants/form-field';
|
import { FF_MAX_CHARS_IN_COMMENT } from 'constants/form-field';
|
||||||
import { FormField, Form } from 'component/common/form';
|
import { FormFieldAreaAdvanced, Form } from 'component/common/form';
|
||||||
import { getChannelIdFromClaim } from 'util/claim';
|
import { getChannelIdFromClaim } from 'util/claim';
|
||||||
import { Lbryio } from 'lbryinc';
|
import { Lbryio } from 'lbryinc';
|
||||||
import { useHistory } from 'react-router';
|
import { useHistory } from 'react-router';
|
||||||
|
@ -22,13 +22,12 @@ import I18nMessage from 'component/i18nMessage';
|
||||||
import Icon from 'component/common/icon';
|
import Icon from 'component/common/icon';
|
||||||
import OptimizedImage from 'component/optimizedImage';
|
import OptimizedImage from 'component/optimizedImage';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import SelectChannel from 'component/selectChannel';
|
|
||||||
import StickerSelector from './sticker-selector';
|
import StickerSelector from './sticker-selector';
|
||||||
|
import CommentCreateHeader from './comment-create-header';
|
||||||
import type { ElementRef } from 'react';
|
import type { ElementRef } from 'react';
|
||||||
import UriIndicator from 'component/uriIndicator';
|
import UriIndicator from 'component/uriIndicator';
|
||||||
import usePersistedState from 'effects/use-persisted-state';
|
import usePersistedState from 'effects/use-persisted-state';
|
||||||
import WalletTipAmountSelector from 'component/walletTipAmountSelector';
|
import WalletTipAmountSelector from 'component/walletTipAmountSelector';
|
||||||
|
|
||||||
import { getStripeEnvironment } from 'util/stripe';
|
import { getStripeEnvironment } from 'util/stripe';
|
||||||
const stripeEnvironment = getStripeEnvironment();
|
const stripeEnvironment = getStripeEnvironment();
|
||||||
|
|
||||||
|
@ -364,31 +363,6 @@ export function CommentCreate(props: Props) {
|
||||||
.catch(() => {});
|
.catch(() => {});
|
||||||
}, [canReceiveFiatTip, claim.claim_id, claim.name, claim.signing_channel, stickerSelector]);
|
}, [canReceiveFiatTip, claim.claim_id, claim.name, claim.signing_channel, stickerSelector]);
|
||||||
|
|
||||||
// LIVESTREAM ONLY - REMOVE
|
|
||||||
// Handle keyboard shortcut comment creation
|
|
||||||
// React.useEffect(() => {
|
|
||||||
// function altEnterListener(e: SyntheticKeyboardEvent<*>) {
|
|
||||||
// const inputRef = formFieldRef && formFieldRef.current && formFieldRef.current.input;
|
|
||||||
//
|
|
||||||
// if (inputRef && inputRef.current === document.activeElement) {
|
|
||||||
// // $FlowFixMe
|
|
||||||
// const isTyping = e.target.attributes['term'];
|
|
||||||
//
|
|
||||||
// if (((isLivestream && !isTyping) || e.ctrlKey || e.metaKey) && e.keyCode === KEYCODES.ENTER) {
|
|
||||||
// e.preventDefault();
|
|
||||||
// buttonRef.current.click();
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// window.addEventListener('keydown', altEnterListener);
|
|
||||||
//
|
|
||||||
// // removes the listener so it doesn't cause problems elsewhere in the app
|
|
||||||
// return () => {
|
|
||||||
// window.removeEventListener('keydown', altEnterListener);
|
|
||||||
// };
|
|
||||||
// }, [isLivestream]);
|
|
||||||
|
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
// Render
|
// Render
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
@ -410,7 +384,11 @@ export function CommentCreate(props: Props) {
|
||||||
push(pathPlusRedirect);
|
push(pathPlusRedirect);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<FormField type="textarea" name={'comment_signup_prompt'} placeholder={__('Say something about this...')} />
|
<FormFieldAreaAdvanced
|
||||||
|
type="textarea"
|
||||||
|
name={'comment_signup_prompt'}
|
||||||
|
placeholder={__('Say something about this...')}
|
||||||
|
/>
|
||||||
<div className="section__actions--no-margin">
|
<div className="section__actions--no-margin">
|
||||||
<Button disabled button="primary" label={__('Post --[button to submit something]--')} />
|
<Button disabled button="primary" label={__('Post --[button to submit something]--')} />
|
||||||
</div>
|
</div>
|
||||||
|
@ -421,22 +399,22 @@ export function CommentCreate(props: Props) {
|
||||||
return (
|
return (
|
||||||
<Form
|
<Form
|
||||||
onSubmit={() => {}}
|
onSubmit={() => {}}
|
||||||
className={classnames('commentCreate', {
|
className={classnames('comment-create', {
|
||||||
'commentCreate--reply': isReply,
|
'comment-create--reply': isReply,
|
||||||
'commentCreate--nestedReply': isNested,
|
'comment-create--nestedReply': isNested,
|
||||||
'commentCreate--bottom': bottom,
|
'comment-create--bottom': bottom,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{/* Input Box/Preview Box */}
|
{/* Input Box/Preview Box */}
|
||||||
{stickerSelector ? (
|
{stickerSelector ? (
|
||||||
<StickerSelector onSelect={(sticker) => handleSelectSticker(sticker)} claimIsMine={claimIsMine} />
|
<StickerSelector onSelect={(sticker) => handleSelectSticker(sticker)} claimIsMine={claimIsMine} />
|
||||||
) : isReviewingStickerComment && activeChannelClaim && selectedSticker ? (
|
) : isReviewingStickerComment && activeChannelClaim && selectedSticker ? (
|
||||||
<div className="commentCreate__stickerPreview">
|
<div className="comment-create__stickerPreview">
|
||||||
<div className="commentCreate__stickerPreviewInfo">
|
<div className="comment-create__stickerPreviewInfo">
|
||||||
<ChannelThumbnail xsmall uri={activeChannelClaim.canonical_url} />
|
<ChannelThumbnail xsmall uri={activeChannelClaim.canonical_url} />
|
||||||
<UriIndicator uri={activeChannelClaim.canonical_url} link />
|
<UriIndicator uri={activeChannelClaim.canonical_url} link />
|
||||||
</div>
|
</div>
|
||||||
<div className="commentCreate__stickerPreviewImage">
|
<div className="comment-create__stickerPreviewImage">
|
||||||
<OptimizedImage src={selectedSticker && selectedSticker.url} waitLoad loading="lazy" />
|
<OptimizedImage src={selectedSticker && selectedSticker.url} waitLoad loading="lazy" />
|
||||||
</div>
|
</div>
|
||||||
{/* figure out lbc sticker prices */}
|
{/* figure out lbc sticker prices */}
|
||||||
|
@ -448,15 +426,15 @@ export function CommentCreate(props: Props) {
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : isReviewingSupportComment && activeChannelClaim ? (
|
) : isReviewingSupportComment && activeChannelClaim ? (
|
||||||
<div className="commentCreate__supportCommentPreview">
|
<div className="comment-create__supportCommentPreview">
|
||||||
<CreditAmount
|
<CreditAmount
|
||||||
amount={tipAmount}
|
amount={tipAmount}
|
||||||
className="commentCreate__supportCommentPreviewAmount"
|
className="comment-create__supportCommentPreviewAmount"
|
||||||
isFiat={activeTab === TAB_FIAT}
|
isFiat={activeTab === TAB_FIAT}
|
||||||
size={activeTab === TAB_LBC ? 18 : 2}
|
size={activeTab === TAB_LBC ? 18 : 2}
|
||||||
/>
|
/>
|
||||||
<ChannelThumbnail xsmall uri={activeChannelClaim.canonical_url} />
|
<ChannelThumbnail xsmall uri={activeChannelClaim.canonical_url} />
|
||||||
<div className="commentCreate__supportCommentBody">
|
<div className="comment-create__supportCommentBody">
|
||||||
<UriIndicator uri={activeChannelClaim.canonical_url} link />
|
<UriIndicator uri={activeChannelClaim.canonical_url} link />
|
||||||
<div>{commentValue}</div>
|
<div>{commentValue}</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -471,23 +449,22 @@ export function CommentCreate(props: Props) {
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<FormField
|
<FormFieldAreaAdvanced
|
||||||
autoFocus={isReply}
|
autoFocus={isReply}
|
||||||
charCount={charCount}
|
charCount={charCount}
|
||||||
className={isReply ? 'content_reply' : 'content_comment'}
|
className={isReply ? 'content_reply' : 'content_comment'}
|
||||||
disabled={isFetchingChannels}
|
disabled={isFetchingChannels}
|
||||||
label={
|
header={
|
||||||
<div className="commentCreate__labelWrapper">
|
<CommentCreateHeader
|
||||||
<span className="commentCreate__label">{(isReply ? __('Replying as') : __('Comment as')) + ' '}</span>
|
isReply={isReply}
|
||||||
<SelectChannel tiny />
|
advanced={advancedEditor}
|
||||||
</div>
|
advancedHandler={() => setAdvancedEditor(!advancedEditor)}
|
||||||
|
/>
|
||||||
}
|
}
|
||||||
name={isReply ? 'content_reply' : 'content_description'}
|
name={isReply ? 'content_reply' : 'content_description'}
|
||||||
quickActionLabel={isReply ? undefined : advancedEditor ? __('Simple Editor') : __('Advanced Editor')}
|
|
||||||
ref={formFieldRef}
|
ref={formFieldRef}
|
||||||
onChange={handleCommentChange}
|
onChange={handleCommentChange}
|
||||||
openEmoteMenu={() => setShowEmotes(!showEmotes)}
|
openEmoteMenu={() => setShowEmotes(!showEmotes)}
|
||||||
quickActionHandler={() => setAdvancedEditor(!advancedEditor)}
|
|
||||||
onFocus={onTextareaFocus}
|
onFocus={onTextareaFocus}
|
||||||
onBlur={onTextareaBlur}
|
onBlur={onTextareaBlur}
|
||||||
placeholder={__('Say something about this...')}
|
placeholder={__('Say something about this...')}
|
||||||
|
@ -655,7 +632,7 @@ export function CommentCreate(props: Props) {
|
||||||
{/* Help Text */}
|
{/* Help Text */}
|
||||||
{deletedComment && <div className="error__text">{__('This comment has been deleted.')}</div>}
|
{deletedComment && <div className="error__text">{__('This comment has been deleted.')}</div>}
|
||||||
{!!minAmount && (
|
{!!minAmount && (
|
||||||
<div className="help--notice commentCreate__minAmountNotice">
|
<div className="help--notice comment-create__minAmountNotice">
|
||||||
<I18nMessage tokens={{ lbc: <CreditAmount noFormat amount={minAmount} /> }}>
|
<I18nMessage tokens={{ lbc: <CreditAmount noFormat amount={minAmount} /> }}>
|
||||||
{minTip ? 'Comment min: %lbc%' : minSuper ? 'HyperChat min: %lbc%' : ''}
|
{minTip ? 'Comment min: %lbc%' : minSuper ? 'HyperChat min: %lbc%' : ''}
|
||||||
</I18nMessage>
|
</I18nMessage>
|
||||||
|
|
|
@ -9,6 +9,7 @@ import Button from 'component/button';
|
||||||
import ChannelThumbnail from 'component/channelThumbnail';
|
import ChannelThumbnail from 'component/channelThumbnail';
|
||||||
import { useHistory } from 'react-router';
|
import { useHistory } from 'react-router';
|
||||||
import { useIsMobile } from 'effects/use-screensize';
|
import { useIsMobile } from 'effects/use-screensize';
|
||||||
|
import { formatNumber } from 'util/number';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
myReacts: Array<string>,
|
myReacts: Array<string>,
|
||||||
|
@ -109,7 +110,11 @@ export default function CommentReactions(props: Props) {
|
||||||
'comment__action--active': myReacts && myReacts.includes(REACTION_TYPES.LIKE),
|
'comment__action--active': myReacts && myReacts.includes(REACTION_TYPES.LIKE),
|
||||||
})}
|
})}
|
||||||
onClick={handleCommentLike}
|
onClick={handleCommentLike}
|
||||||
label={<span className="comment__reaction-count">{getCountForReact(REACTION_TYPES.LIKE)}</span>}
|
label={
|
||||||
|
<span className="comment__reaction-count">
|
||||||
|
{formatNumber(getCountForReact(REACTION_TYPES.LIKE), 2, true)}
|
||||||
|
</span>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
title={__('Downvote')}
|
title={__('Downvote')}
|
||||||
|
@ -119,7 +124,11 @@ export default function CommentReactions(props: Props) {
|
||||||
'comment__action--active': myReacts && myReacts.includes(REACTION_TYPES.DISLIKE),
|
'comment__action--active': myReacts && myReacts.includes(REACTION_TYPES.DISLIKE),
|
||||||
})}
|
})}
|
||||||
onClick={handleCommentDislike}
|
onClick={handleCommentDislike}
|
||||||
label={<span className="comment__reaction-count">{getCountForReact(REACTION_TYPES.DISLIKE)}</span>}
|
label={
|
||||||
|
<span className="comment__reaction-count">
|
||||||
|
{formatNumber(getCountForReact(REACTION_TYPES.DISLIKE), 2, true)}
|
||||||
|
</span>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{!shouldHide && ENABLE_CREATOR_REACTIONS && (canCreatorReact || creatorLiked) && (
|
{!shouldHide && ENABLE_CREATOR_REACTIONS && (canCreatorReact || creatorLiked) && (
|
||||||
|
|
|
@ -23,6 +23,9 @@ import { doCommentReset, doCommentList, doCommentById, doCommentReactList } from
|
||||||
import { selectActiveChannelClaim } from 'redux/selectors/app';
|
import { selectActiveChannelClaim } from 'redux/selectors/app';
|
||||||
import { getChannelIdFromClaim } from 'util/claim';
|
import { getChannelIdFromClaim } from 'util/claim';
|
||||||
import CommentsList from './view';
|
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 select = (state, props) => {
|
||||||
const { uri } = props;
|
const { uri } = props;
|
||||||
|
@ -56,15 +59,19 @@ const select = (state, props) => {
|
||||||
myReactsByCommentId: selectMyReacts(state),
|
myReactsByCommentId: selectMyReacts(state),
|
||||||
othersReactsById: selectOthersReacts(state),
|
othersReactsById: selectOthersReacts(state),
|
||||||
activeChannelId: activeChannelClaim && activeChannelClaim.claim_id,
|
activeChannelId: activeChannelClaim && activeChannelClaim.claim_id,
|
||||||
|
customCommentServers: makeSelectClientSetting(SETTINGS.CUSTOM_COMMENTS_SERVERS)(state),
|
||||||
|
commentServer: makeSelectClientSetting(SETTINGS.CUSTOM_COMMENTS_SERVER_URL)(state),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const perform = {
|
const perform = (dispatch, ownProps) => ({
|
||||||
fetchTopLevelComments: doCommentList,
|
fetchTopLevelComments: (uri, parentId, page, pageSize, sortBy) =>
|
||||||
fetchComment: doCommentById,
|
dispatch(doCommentList(uri, parentId, page, pageSize, sortBy)),
|
||||||
fetchReacts: doCommentReactList,
|
fetchComment: (commentId) => dispatch(doCommentById(commentId)),
|
||||||
resetComments: doCommentReset,
|
fetchReacts: (commentIds) => dispatch(doCommentReactList(commentIds)),
|
||||||
doResolveUris,
|
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);
|
export default connect(select, perform)(CommentsList);
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
// @flow
|
// @flow
|
||||||
import { COMMENT_PAGE_SIZE_TOP_LEVEL, SORT_BY } from 'constants/comment';
|
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 { useIsMobile, useIsMediumScreen } from 'effects/use-screensize';
|
||||||
import { getCommentsListTitle } from 'util/comments';
|
import { getCommentsListTitle } from 'util/comments';
|
||||||
import * as ICONS from 'constants/icons';
|
import * as ICONS from 'constants/icons';
|
||||||
|
@ -15,6 +15,8 @@ import Empty from 'component/common/empty';
|
||||||
import React, { useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import Spinner from 'component/spinner';
|
import Spinner from 'component/spinner';
|
||||||
import usePersistedState from 'effects/use-persisted-state';
|
import usePersistedState from 'effects/use-persisted-state';
|
||||||
|
import { FormField } from 'component/common/form';
|
||||||
|
import Comments from 'comments';
|
||||||
|
|
||||||
const DEBOUNCE_SCROLL_HANDLER_MS = 200;
|
const DEBOUNCE_SCROLL_HANDLER_MS = 200;
|
||||||
|
|
||||||
|
@ -52,6 +54,9 @@ type Props = {
|
||||||
fetchReacts: (commentIds: Array<string>) => Promise<any>,
|
fetchReacts: (commentIds: Array<string>) => Promise<any>,
|
||||||
resetComments: (claimId: string) => void,
|
resetComments: (claimId: string) => void,
|
||||||
doResolveUris: (uris: Array<string>, returnCachedClaims: boolean) => void,
|
doResolveUris: (uris: Array<string>, returnCachedClaims: boolean) => void,
|
||||||
|
customCommentServers: Array<CommentServerDetails>,
|
||||||
|
setCommentServer: (string) => void,
|
||||||
|
commentServer: string,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function CommentList(props: Props) {
|
export default function CommentList(props: Props) {
|
||||||
|
@ -80,11 +85,17 @@ export default function CommentList(props: Props) {
|
||||||
fetchReacts,
|
fetchReacts,
|
||||||
resetComments,
|
resetComments,
|
||||||
doResolveUris,
|
doResolveUris,
|
||||||
|
customCommentServers,
|
||||||
|
setCommentServer,
|
||||||
|
commentServer,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
const isMediumScreen = useIsMediumScreen();
|
const isMediumScreen = useIsMediumScreen();
|
||||||
|
|
||||||
|
const defaultServer = { name: COMMENT_SERVER_NAME, url: COMMENT_SERVER_API };
|
||||||
|
const allServers = [defaultServer, ...(customCommentServers || [])];
|
||||||
|
|
||||||
const spinnerRef = React.useRef();
|
const spinnerRef = React.useRef();
|
||||||
const DEFAULT_SORT = ENABLE_COMMENT_REACTIONS ? SORT_BY.POPULARITY : SORT_BY.NEWEST;
|
const DEFAULT_SORT = ENABLE_COMMENT_REACTIONS ? SORT_BY.POPULARITY : SORT_BY.NEWEST;
|
||||||
const [sort, setSort] = usePersistedState('comment-sort-by', DEFAULT_SORT);
|
const [sort, setSort] = usePersistedState('comment-sort-by', DEFAULT_SORT);
|
||||||
|
@ -255,7 +266,16 @@ export default function CommentList(props: Props) {
|
||||||
}, [alreadyResolved, doResolveUris, topLevelComments]);
|
}, [alreadyResolved, doResolveUris, topLevelComments]);
|
||||||
|
|
||||||
const commentProps = { isTopLevel: true, threadDepth: 3, uri, claimIsMine, linkedCommentId };
|
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 (
|
return (
|
||||||
<Card
|
<Card
|
||||||
|
@ -334,17 +354,21 @@ type ActionButtonsProps = {
|
||||||
sort: string,
|
sort: string,
|
||||||
changeSort: (string) => void,
|
changeSort: (string) => void,
|
||||||
setPage: (number) => void,
|
setPage: (number) => void,
|
||||||
|
allServers: Array<CommentServerDetails>,
|
||||||
|
commentServer: string,
|
||||||
|
setCommentServer: (string) => void,
|
||||||
|
defaultServer: CommentServerDetails,
|
||||||
};
|
};
|
||||||
|
|
||||||
const CommentActionButtons = (actionButtonsProps: ActionButtonsProps) => {
|
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 };
|
const sortButtonProps = { activeSort: sort, changeSort };
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className={'comment__actions-row'}>
|
||||||
{totalComments > 1 && ENABLE_COMMENT_REACTIONS && (
|
{totalComments > 1 && ENABLE_COMMENT_REACTIONS && (
|
||||||
<span className="comment__sort">
|
<div className="comment__sort-group">
|
||||||
<SortButton {...sortButtonProps} label={__('Best')} icon={ICONS.BEST} sortOption={SORT_BY.POPULARITY} />
|
<SortButton {...sortButtonProps} label={__('Best')} icon={ICONS.BEST} sortOption={SORT_BY.POPULARITY} />
|
||||||
<SortButton
|
<SortButton
|
||||||
{...sortButtonProps}
|
{...sortButtonProps}
|
||||||
|
@ -353,11 +377,39 @@ const CommentActionButtons = (actionButtonsProps: ActionButtonsProps) => {
|
||||||
sortOption={SORT_BY.CONTROVERSY}
|
sortOption={SORT_BY.CONTROVERSY}
|
||||||
/>
|
/>
|
||||||
<SortButton {...sortButtonProps} label={__('New')} icon={ICONS.NEW} sortOption={SORT_BY.NEWEST} />
|
<SortButton {...sortButtonProps} label={__('New')} icon={ICONS.NEW} sortOption={SORT_BY.NEWEST} />
|
||||||
</span>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{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)} />
|
<Button button="alt" icon={ICONS.REFRESH} title={__('Refresh')} onClick={() => setPage(0)} />
|
||||||
</>
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -3,8 +3,8 @@ import React from 'react';
|
||||||
import { useRadioState, Radio, RadioGroup } from 'reakit/Radio';
|
import { useRadioState, Radio, RadioGroup } from 'reakit/Radio';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
files: Array<WebFile>,
|
files: Array<File>,
|
||||||
onChange: (WebFile | void) => void,
|
onChange: (File | void) => void,
|
||||||
};
|
};
|
||||||
|
|
||||||
type RadioProps = {
|
type RadioProps = {
|
||||||
|
@ -26,16 +26,16 @@ function FileList(props: Props) {
|
||||||
|
|
||||||
const getFile = (value?: string) => {
|
const getFile = (value?: string) => {
|
||||||
if (files && files.length) {
|
if (files && files.length) {
|
||||||
return files.find((file: WebFile) => file.name === value);
|
return files.find((file: File) => file.name === value);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (radio.stops.length) {
|
if (radio.items.length) {
|
||||||
if (!radio.currentId) {
|
if (!radio.currentId) {
|
||||||
radio.first();
|
radio.first();
|
||||||
} else {
|
} else {
|
||||||
const first = radio.stops[0].ref.current;
|
const first = radio.items[0].ref.current;
|
||||||
// First auto-selection
|
// First auto-selection
|
||||||
if (first && first.id === radio.currentId && !radio.state) {
|
if (first && first.id === radio.currentId && !radio.state) {
|
||||||
const file = getFile(first.value);
|
const file = getFile(first.value);
|
||||||
|
@ -46,12 +46,12 @@ function FileList(props: Props) {
|
||||||
|
|
||||||
if (radio.state) {
|
if (radio.state) {
|
||||||
// Find selected element
|
// Find selected element
|
||||||
const stop = radio.stops.find(item => item.id === radio.currentId);
|
const stop = radio.items.find((item) => item.id === radio.currentId);
|
||||||
const element = stop && stop.ref.current;
|
const element = stop && stop.ref.current;
|
||||||
// Only update state if new item is selected
|
// Only update state if new item is selected
|
||||||
if (element && element.value !== radio.state) {
|
if (element && element.value !== radio.state) {
|
||||||
const file = getFile(element.value);
|
const file = getFile(element.value);
|
||||||
// Sselect new file and update state
|
// Select new file and update state
|
||||||
onChange(file);
|
onChange(file);
|
||||||
radio.setState(element.value);
|
radio.setState(element.value);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,25 +1,29 @@
|
||||||
// @flow
|
// @flow
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import * as remote from '@electron/remote';
|
import * as remote from '@electron/remote';
|
||||||
|
import { ipcRenderer } from 'electron';
|
||||||
import Button from 'component/button';
|
import Button from 'component/button';
|
||||||
import { FormField } from 'component/common/form';
|
import { FormField } from 'component/common/form';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
type: string,
|
type: string,
|
||||||
currentPath?: ?string,
|
currentPath?: ?string,
|
||||||
onFileChosen: (WebFile) => void,
|
onFileChosen: (FileWithPath) => void,
|
||||||
label?: string,
|
label?: string,
|
||||||
placeholder?: string,
|
placeholder?: string,
|
||||||
accept?: string,
|
accept?: string,
|
||||||
error?: string,
|
error?: string,
|
||||||
disabled?: boolean,
|
disabled?: boolean,
|
||||||
autoFocus?: boolean,
|
autoFocus?: boolean,
|
||||||
|
filters?: Array<{ name: string, extension: string[] }>,
|
||||||
|
readFile?: boolean,
|
||||||
};
|
};
|
||||||
|
|
||||||
class FileSelector extends React.PureComponent<Props> {
|
class FileSelector extends React.PureComponent<Props> {
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
autoFocus: false,
|
autoFocus: false,
|
||||||
type: 'file',
|
type: 'file',
|
||||||
|
readFile: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
fileInput: React.ElementRef<any>;
|
fileInput: React.ElementRef<any>;
|
||||||
|
@ -41,18 +45,49 @@ class FileSelector extends React.PureComponent<Props> {
|
||||||
const file = files[0];
|
const file = files[0];
|
||||||
|
|
||||||
if (this.props.onFileChosen) {
|
if (this.props.onFileChosen) {
|
||||||
this.props.onFileChosen(file);
|
this.props.onFileChosen({ file, path: file.path || file.name });
|
||||||
}
|
}
|
||||||
this.fileInput.current.value = null; // clear the file input
|
this.fileInput.current.value = null; // clear the file input
|
||||||
};
|
};
|
||||||
|
|
||||||
handleDirectoryInputSelection = () => {
|
handleDirectoryInputSelection = () => {
|
||||||
remote.dialog.showOpenDialog({ properties: ['openDirectory'] }).then((result) => {
|
let defaultPath;
|
||||||
|
let properties;
|
||||||
|
let isWin = process.platform === 'win32';
|
||||||
|
let type = this.props.type;
|
||||||
|
|
||||||
|
if (isWin === true) {
|
||||||
|
defaultPath = process.env.HOME || process.env.HOMEPATH || process.env.USERPROFILE;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === 'openFile') {
|
||||||
|
properties = ['openFile'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === 'openDirectory') {
|
||||||
|
properties = ['openDirectory'];
|
||||||
|
}
|
||||||
|
|
||||||
|
remote.dialog
|
||||||
|
.showOpenDialog({
|
||||||
|
properties,
|
||||||
|
defaultPath,
|
||||||
|
filters: this.props.filters,
|
||||||
|
})
|
||||||
|
.then((result) => {
|
||||||
const path = result && result.filePaths[0];
|
const path = result && result.filePaths[0];
|
||||||
if (path) {
|
if (path) {
|
||||||
// $FlowFixMe
|
return ipcRenderer.invoke('get-file-from-path', path, this.props.readFile);
|
||||||
this.props.onFileChosen({ path });
|
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
.then((result) => {
|
||||||
|
if (!result) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const file = new File([result.buffer], result.name, {
|
||||||
|
type: result.mime,
|
||||||
|
});
|
||||||
|
this.props.onFileChosen({ file, path: result.path });
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -82,7 +117,11 @@ class FileSelector extends React.PureComponent<Props> {
|
||||||
autoFocus={autoFocus}
|
autoFocus={autoFocus}
|
||||||
button="primary"
|
button="primary"
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
onClick={type === 'openDirectory' ? this.handleDirectoryInputSelection : this.fileInputButton}
|
onClick={
|
||||||
|
type === 'openDirectory' || type === 'openFile'
|
||||||
|
? this.handleDirectoryInputSelection
|
||||||
|
: this.fileInputButton
|
||||||
|
}
|
||||||
label={__('Browse')}
|
label={__('Browse')}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
|
240
ui/component/common/form-components/form-field-area-advanced.jsx
Normal file
240
ui/component/common/form-components/form-field-area-advanced.jsx
Normal file
|
@ -0,0 +1,240 @@
|
||||||
|
// @flow
|
||||||
|
import 'easymde/dist/easymde.min.css';
|
||||||
|
import { FF_MAX_CHARS_DEFAULT } from 'constants/form-field';
|
||||||
|
import { openEditorMenu, stopContextMenu } from 'util/context-menu';
|
||||||
|
import * as ICONS from 'constants/icons';
|
||||||
|
import Button from 'component/button';
|
||||||
|
import MarkdownPreview from 'component/common/markdown-preview';
|
||||||
|
import React from 'react';
|
||||||
|
import ReactDOMServer from 'react-dom/server';
|
||||||
|
import SimpleMDE from 'react-simplemde-editor';
|
||||||
|
import TextareaWithSuggestions from 'component/textareaWithSuggestions';
|
||||||
|
import type { ElementRef, Node } from 'react';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
autoFocus?: boolean,
|
||||||
|
blockWrap: boolean,
|
||||||
|
charCount?: number,
|
||||||
|
children?: React$Node,
|
||||||
|
disabled?: boolean,
|
||||||
|
helper?: string | React$Node,
|
||||||
|
hideSuggestions?: boolean,
|
||||||
|
isLivestream?: boolean,
|
||||||
|
label?: string | Node,
|
||||||
|
labelOnLeft: boolean,
|
||||||
|
name: string,
|
||||||
|
noEmojis?: boolean,
|
||||||
|
placeholder?: string | number,
|
||||||
|
quickActionLabel?: string,
|
||||||
|
textAreaMaxLength?: number,
|
||||||
|
type?: string,
|
||||||
|
value?: string | number,
|
||||||
|
onChange?: (any) => any,
|
||||||
|
openEmoteMenu?: () => void,
|
||||||
|
quickActionHandler?: (any) => any,
|
||||||
|
render?: () => React$Node,
|
||||||
|
header?: React$Node,
|
||||||
|
};
|
||||||
|
|
||||||
|
export class FormFieldAreaAdvanced extends React.PureComponent<Props> {
|
||||||
|
static defaultProps = { labelOnLeft: false, blockWrap: true };
|
||||||
|
|
||||||
|
input: { current: ElementRef<any> };
|
||||||
|
|
||||||
|
constructor(props: Props) {
|
||||||
|
super(props);
|
||||||
|
this.input = React.createRef();
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
const { autoFocus } = this.props;
|
||||||
|
const input = this.input.current;
|
||||||
|
|
||||||
|
if (input && autoFocus) input.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
autoFocus,
|
||||||
|
blockWrap,
|
||||||
|
charCount,
|
||||||
|
children,
|
||||||
|
helper,
|
||||||
|
hideSuggestions,
|
||||||
|
isLivestream,
|
||||||
|
label,
|
||||||
|
header,
|
||||||
|
labelOnLeft,
|
||||||
|
name,
|
||||||
|
noEmojis,
|
||||||
|
quickActionLabel,
|
||||||
|
textAreaMaxLength,
|
||||||
|
type,
|
||||||
|
openEmoteMenu,
|
||||||
|
quickActionHandler,
|
||||||
|
render,
|
||||||
|
...inputProps
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
// Ideally, the character count should (and can) be appended to the
|
||||||
|
// SimpleMDE's "options::status" bar. However, I couldn't figure out how
|
||||||
|
// to pass the current value to it's callback, nor query the current
|
||||||
|
// text length from the callback. So, we'll use our own widget.
|
||||||
|
const hasCharCount = charCount !== undefined && charCount >= 0;
|
||||||
|
const countInfo = hasCharCount && textAreaMaxLength !== undefined && (
|
||||||
|
<span className="comment__char-count-mde">{`${charCount || '0'}/${textAreaMaxLength}`}</span>
|
||||||
|
);
|
||||||
|
|
||||||
|
const quickAction =
|
||||||
|
quickActionLabel && quickActionHandler ? (
|
||||||
|
<div className="form-field__quick-action">
|
||||||
|
<Button button="link" onClick={quickActionHandler} label={quickActionLabel} />
|
||||||
|
</div>
|
||||||
|
) : null;
|
||||||
|
|
||||||
|
const input = () => {
|
||||||
|
switch (type) {
|
||||||
|
case 'markdown':
|
||||||
|
const handleEvents = { contextmenu: openEditorMenu };
|
||||||
|
|
||||||
|
const getInstance = (editor) => {
|
||||||
|
// SimpleMDE max char check
|
||||||
|
editor.codemirror.on('beforeChange', (instance, changes) => {
|
||||||
|
if (textAreaMaxLength && changes.update) {
|
||||||
|
var str = changes.text.join('\n');
|
||||||
|
var delta = str.length - (instance.indexFromPos(changes.to) - instance.indexFromPos(changes.from));
|
||||||
|
|
||||||
|
if (delta <= 0) return;
|
||||||
|
|
||||||
|
delta = instance.getValue().length + delta - textAreaMaxLength;
|
||||||
|
if (delta > 0) {
|
||||||
|
str = str.substring(0, str.length - delta);
|
||||||
|
changes.update(changes.from, changes.to, str.split('\n'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// "Create Link (Ctrl-K)": highlight URL instead of label:
|
||||||
|
editor.codemirror.on('changes', (instance, changes) => {
|
||||||
|
try {
|
||||||
|
// Grab the last change from the buffered list. I assume the
|
||||||
|
// buffered one ('changes', instead of 'change') is more efficient,
|
||||||
|
// and that "Create Link" will always end up last in the list.
|
||||||
|
const lastChange = changes[changes.length - 1];
|
||||||
|
if (lastChange.origin === '+input') {
|
||||||
|
// https://github.com/Ionaru/easy-markdown-editor/blob/8fa54c496f98621d5f45f57577ce630bee8c41ee/src/js/easymde.js#L765
|
||||||
|
const EASYMDE_URL_PLACEHOLDER = '(https://)';
|
||||||
|
|
||||||
|
// The URL placeholder is always placed last, so just look at the
|
||||||
|
// last text in the array to also cover the multi-line case:
|
||||||
|
const urlLineText = lastChange.text[lastChange.text.length - 1];
|
||||||
|
|
||||||
|
if (urlLineText.endsWith(EASYMDE_URL_PLACEHOLDER) && urlLineText !== '[]' + EASYMDE_URL_PLACEHOLDER) {
|
||||||
|
const from = lastChange.from;
|
||||||
|
const to = lastChange.to;
|
||||||
|
const isSelectionMultiline = lastChange.text.length > 1;
|
||||||
|
const baseIndex = isSelectionMultiline ? 0 : from.ch;
|
||||||
|
|
||||||
|
// Everything works fine for the [Ctrl-K] case, but for the
|
||||||
|
// [Button] case, this handler happens before the original
|
||||||
|
// code, thus our change got wiped out.
|
||||||
|
// Add a small delay to handle that case.
|
||||||
|
setTimeout(() => {
|
||||||
|
instance.setSelection(
|
||||||
|
{ line: to.line, ch: baseIndex + urlLineText.lastIndexOf('(') + 1 },
|
||||||
|
{ line: to.line, ch: baseIndex + urlLineText.lastIndexOf(')') }
|
||||||
|
);
|
||||||
|
}, 25);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {} // Do nothing (revert to original behavior)
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="form-field--SimpleMDE" onContextMenu={stopContextMenu}>
|
||||||
|
<fieldset-section>
|
||||||
|
{!header && (
|
||||||
|
<div className="form-field__two-column">
|
||||||
|
<div>
|
||||||
|
<label htmlFor={name}>{label}</label>
|
||||||
|
</div>
|
||||||
|
{quickAction}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{!!header && <div className="form-field__textarea-header">{header}</div>}
|
||||||
|
<SimpleMDE
|
||||||
|
{...inputProps}
|
||||||
|
id={name}
|
||||||
|
type="textarea"
|
||||||
|
events={handleEvents}
|
||||||
|
getMdeInstance={getInstance}
|
||||||
|
options={{
|
||||||
|
spellChecker: true,
|
||||||
|
hideIcons: ['heading', 'image', 'fullscreen', 'side-by-side'],
|
||||||
|
previewRender(plainText) {
|
||||||
|
const preview = <MarkdownPreview content={plainText} noDataStore />;
|
||||||
|
return ReactDOMServer.renderToString(preview);
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{countInfo}
|
||||||
|
</fieldset-section>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
case 'textarea':
|
||||||
|
return (
|
||||||
|
<fieldset-section>
|
||||||
|
{!header && (label || quickAction) && (
|
||||||
|
<div className="form-field__two-column">
|
||||||
|
<label htmlFor={name}>{label}</label>
|
||||||
|
{quickAction}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{!!header && <div className="form-field__textarea-header">{header}</div>}
|
||||||
|
{hideSuggestions ? (
|
||||||
|
<textarea
|
||||||
|
type={type}
|
||||||
|
id={name}
|
||||||
|
maxLength={textAreaMaxLength || FF_MAX_CHARS_DEFAULT}
|
||||||
|
ref={this.input}
|
||||||
|
{...inputProps}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<TextareaWithSuggestions
|
||||||
|
type={type}
|
||||||
|
id={name}
|
||||||
|
maxLength={textAreaMaxLength || FF_MAX_CHARS_DEFAULT}
|
||||||
|
inputRef={this.input}
|
||||||
|
isLivestream={isLivestream}
|
||||||
|
{...inputProps}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<div className="form-field__textarea-info">
|
||||||
|
{!noEmojis && openEmoteMenu && (
|
||||||
|
<Button
|
||||||
|
type="alt"
|
||||||
|
className="button--comment-icons"
|
||||||
|
title="Emotes"
|
||||||
|
onClick={openEmoteMenu}
|
||||||
|
icon={ICONS.EMOJI}
|
||||||
|
iconSize={20}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{countInfo}
|
||||||
|
</div>
|
||||||
|
</fieldset-section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{type && input()}
|
||||||
|
{helper && <div className="form-field__help">{helper}</div>}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FormFieldAreaAdvanced;
|
|
@ -1,14 +1,7 @@
|
||||||
// @flow
|
// @flow
|
||||||
import 'easymde/dist/easymde.min.css';
|
import 'easymde/dist/easymde.min.css';
|
||||||
import { FF_MAX_CHARS_DEFAULT } from 'constants/form-field';
|
import { FF_MAX_CHARS_DEFAULT } from 'constants/form-field';
|
||||||
import { openEditorMenu, stopContextMenu } from 'util/context-menu';
|
|
||||||
import * as ICONS from 'constants/icons';
|
|
||||||
import Button from 'component/button';
|
|
||||||
import MarkdownPreview from 'component/common/markdown-preview';
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ReactDOMServer from 'react-dom/server';
|
|
||||||
import SimpleMDE from 'react-simplemde-editor';
|
|
||||||
import TextareaWithSuggestions from 'component/textareaWithSuggestions';
|
|
||||||
import type { ElementRef, Node } from 'react';
|
import type { ElementRef, Node } from 'react';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
@ -21,19 +14,15 @@ type Props = {
|
||||||
disabled?: boolean,
|
disabled?: boolean,
|
||||||
error?: string | boolean,
|
error?: string | boolean,
|
||||||
helper?: string | React$Node,
|
helper?: string | React$Node,
|
||||||
hideSuggestions?: boolean,
|
|
||||||
inputButton?: React$Node,
|
inputButton?: React$Node,
|
||||||
isLivestream?: boolean,
|
|
||||||
label?: string | Node,
|
label?: string | Node,
|
||||||
labelOnLeft: boolean,
|
labelOnLeft: boolean,
|
||||||
max?: number,
|
max?: number,
|
||||||
min?: number,
|
min?: number,
|
||||||
name: string,
|
name: string,
|
||||||
noEmojis?: boolean,
|
|
||||||
placeholder?: string | number,
|
placeholder?: string | number,
|
||||||
postfix?: string,
|
postfix?: string,
|
||||||
prefix?: string,
|
prefix?: string,
|
||||||
quickActionLabel?: string,
|
|
||||||
range?: number,
|
range?: number,
|
||||||
readOnly?: boolean,
|
readOnly?: boolean,
|
||||||
stretch?: boolean,
|
stretch?: boolean,
|
||||||
|
@ -41,8 +30,6 @@ type Props = {
|
||||||
type?: string,
|
type?: string,
|
||||||
value?: string | number,
|
value?: string | number,
|
||||||
onChange?: (any) => any,
|
onChange?: (any) => any,
|
||||||
openEmoteMenu?: () => void,
|
|
||||||
quickActionHandler?: (any) => any,
|
|
||||||
render?: () => React$Node,
|
render?: () => React$Node,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -72,21 +59,15 @@ export class FormField extends React.PureComponent<Props> {
|
||||||
children,
|
children,
|
||||||
error,
|
error,
|
||||||
helper,
|
helper,
|
||||||
hideSuggestions,
|
|
||||||
inputButton,
|
inputButton,
|
||||||
isLivestream,
|
|
||||||
label,
|
label,
|
||||||
labelOnLeft,
|
labelOnLeft,
|
||||||
name,
|
name,
|
||||||
noEmojis,
|
|
||||||
postfix,
|
postfix,
|
||||||
prefix,
|
prefix,
|
||||||
quickActionLabel,
|
|
||||||
stretch,
|
stretch,
|
||||||
textAreaMaxLength,
|
textAreaMaxLength,
|
||||||
type,
|
type,
|
||||||
openEmoteMenu,
|
|
||||||
quickActionHandler,
|
|
||||||
render,
|
render,
|
||||||
...inputProps
|
...inputProps
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
@ -101,18 +82,10 @@ export class FormField extends React.PureComponent<Props> {
|
||||||
const countInfo = hasCharCount && textAreaMaxLength !== undefined && (
|
const countInfo = hasCharCount && textAreaMaxLength !== undefined && (
|
||||||
<span className="comment__char-count-mde">{`${charCount || '0'}/${textAreaMaxLength}`}</span>
|
<span className="comment__char-count-mde">{`${charCount || '0'}/${textAreaMaxLength}`}</span>
|
||||||
);
|
);
|
||||||
|
|
||||||
const Wrapper = blockWrap
|
const Wrapper = blockWrap
|
||||||
? ({ children: innerChildren }) => <fieldset-section class="radio">{innerChildren}</fieldset-section>
|
? ({ children: innerChildren }) => <fieldset-section class="radio">{innerChildren}</fieldset-section>
|
||||||
: ({ children: innerChildren }) => <span className="radio">{innerChildren}</span>;
|
: ({ children: innerChildren }) => <span className="radio">{innerChildren}</span>;
|
||||||
|
|
||||||
const quickAction =
|
|
||||||
quickActionLabel && quickActionHandler ? (
|
|
||||||
<div className="form-field__quick-action">
|
|
||||||
<Button button="link" onClick={quickActionHandler} label={quickActionLabel} />
|
|
||||||
</div>
|
|
||||||
) : null;
|
|
||||||
|
|
||||||
const inputSimple = (type: string) => (
|
const inputSimple = (type: string) => (
|
||||||
<>
|
<>
|
||||||
<input id={name} type={type} {...inputProps} />
|
<input id={name} type={type} {...inputProps} />
|
||||||
|
@ -143,102 +116,14 @@ export class FormField extends React.PureComponent<Props> {
|
||||||
return inputSelect('');
|
return inputSelect('');
|
||||||
case 'select-tiny':
|
case 'select-tiny':
|
||||||
return inputSelect('select--slim');
|
return inputSelect('select--slim');
|
||||||
case 'markdown':
|
|
||||||
const handleEvents = { contextmenu: openEditorMenu };
|
|
||||||
|
|
||||||
const getInstance = (editor) => {
|
|
||||||
// SimpleMDE max char check
|
|
||||||
editor.codemirror.on('beforeChange', (instance, changes) => {
|
|
||||||
if (textAreaMaxLength && changes.update) {
|
|
||||||
var str = changes.text.join('\n');
|
|
||||||
var delta = str.length - (instance.indexFromPos(changes.to) - instance.indexFromPos(changes.from));
|
|
||||||
|
|
||||||
if (delta <= 0) return;
|
|
||||||
|
|
||||||
delta = instance.getValue().length + delta - textAreaMaxLength;
|
|
||||||
if (delta > 0) {
|
|
||||||
str = str.substring(0, str.length - delta);
|
|
||||||
changes.update(changes.from, changes.to, str.split('\n'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// "Create Link (Ctrl-K)": highlight URL instead of label:
|
|
||||||
editor.codemirror.on('changes', (instance, changes) => {
|
|
||||||
try {
|
|
||||||
// Grab the last change from the buffered list. I assume the
|
|
||||||
// buffered one ('changes', instead of 'change') is more efficient,
|
|
||||||
// and that "Create Link" will always end up last in the list.
|
|
||||||
const lastChange = changes[changes.length - 1];
|
|
||||||
if (lastChange.origin === '+input') {
|
|
||||||
// https://github.com/Ionaru/easy-markdown-editor/blob/8fa54c496f98621d5f45f57577ce630bee8c41ee/src/js/easymde.js#L765
|
|
||||||
const EASYMDE_URL_PLACEHOLDER = '(https://)';
|
|
||||||
|
|
||||||
// The URL placeholder is always placed last, so just look at the
|
|
||||||
// last text in the array to also cover the multi-line case:
|
|
||||||
const urlLineText = lastChange.text[lastChange.text.length - 1];
|
|
||||||
|
|
||||||
if (urlLineText.endsWith(EASYMDE_URL_PLACEHOLDER) && urlLineText !== '[]' + EASYMDE_URL_PLACEHOLDER) {
|
|
||||||
const from = lastChange.from;
|
|
||||||
const to = lastChange.to;
|
|
||||||
const isSelectionMultiline = lastChange.text.length > 1;
|
|
||||||
const baseIndex = isSelectionMultiline ? 0 : from.ch;
|
|
||||||
|
|
||||||
// Everything works fine for the [Ctrl-K] case, but for the
|
|
||||||
// [Button] case, this handler happens before the original
|
|
||||||
// code, thus our change got wiped out.
|
|
||||||
// Add a small delay to handle that case.
|
|
||||||
setTimeout(() => {
|
|
||||||
instance.setSelection(
|
|
||||||
{ line: to.line, ch: baseIndex + urlLineText.lastIndexOf('(') + 1 },
|
|
||||||
{ line: to.line, ch: baseIndex + urlLineText.lastIndexOf(')') }
|
|
||||||
);
|
|
||||||
}, 25);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {} // Do nothing (revert to original behavior)
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="form-field--SimpleMDE" onContextMenu={stopContextMenu}>
|
|
||||||
<fieldset-section>
|
|
||||||
<div className="form-field__two-column">
|
|
||||||
<div>
|
|
||||||
<label htmlFor={name}>{label}</label>
|
|
||||||
</div>
|
|
||||||
{quickAction}
|
|
||||||
</div>
|
|
||||||
<SimpleMDE
|
|
||||||
{...inputProps}
|
|
||||||
id={name}
|
|
||||||
type="textarea"
|
|
||||||
events={handleEvents}
|
|
||||||
getMdeInstance={getInstance}
|
|
||||||
options={{
|
|
||||||
spellChecker: true,
|
|
||||||
hideIcons: ['heading', 'image', 'fullscreen', 'side-by-side'],
|
|
||||||
previewRender(plainText) {
|
|
||||||
const preview = <MarkdownPreview content={plainText} noDataStore />;
|
|
||||||
return ReactDOMServer.renderToString(preview);
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{countInfo}
|
|
||||||
</fieldset-section>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
case 'textarea':
|
case 'textarea':
|
||||||
return (
|
return (
|
||||||
<fieldset-section>
|
<fieldset-section>
|
||||||
{(label || quickAction) && (
|
{label && (
|
||||||
<div className="form-field__two-column">
|
<div className="form-field__two-column">
|
||||||
<label htmlFor={name}>{label}</label>
|
<label htmlFor={name}>{label}</label>
|
||||||
{quickAction}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{hideSuggestions ? (
|
|
||||||
<textarea
|
<textarea
|
||||||
type={type}
|
type={type}
|
||||||
id={name}
|
id={name}
|
||||||
|
@ -246,30 +131,7 @@ export class FormField extends React.PureComponent<Props> {
|
||||||
ref={this.input}
|
ref={this.input}
|
||||||
{...inputProps}
|
{...inputProps}
|
||||||
/>
|
/>
|
||||||
) : (
|
<div className="form-field__textarea-info">{countInfo}</div>
|
||||||
<TextareaWithSuggestions
|
|
||||||
type={type}
|
|
||||||
id={name}
|
|
||||||
maxLength={textAreaMaxLength || FF_MAX_CHARS_DEFAULT}
|
|
||||||
inputRef={this.input}
|
|
||||||
isLivestream={isLivestream}
|
|
||||||
{...inputProps}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="form-field__textarea-info">
|
|
||||||
{!noEmojis && openEmoteMenu && (
|
|
||||||
<Button
|
|
||||||
type="alt"
|
|
||||||
className="button--file-action"
|
|
||||||
title="Emotes"
|
|
||||||
onClick={openEmoteMenu}
|
|
||||||
icon={ICONS.EMOJI}
|
|
||||||
iconSize={20}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{countInfo}
|
|
||||||
</div>
|
|
||||||
</fieldset-section>
|
</fieldset-section>
|
||||||
);
|
);
|
||||||
default:
|
default:
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
export { Form } from './form-components/form';
|
export { Form } from './form-components/form';
|
||||||
export { FormField } from './form-components/form-field';
|
export { FormField } from './form-components/form-field';
|
||||||
|
export { FormFieldAreaAdvanced } from './form-components/form-field-area-advanced';
|
||||||
export { FormFieldPrice } from './form-components/form-field-price';
|
export { FormFieldPrice } from './form-components/form-field-price';
|
||||||
export { Submit } from './form-components/submit';
|
export { Submit } from './form-components/submit';
|
||||||
|
|
|
@ -2054,4 +2054,15 @@ export const icons = {
|
||||||
<path d="M12.5,23.24v-1A10.74,10.74,0,0,1,23.24,11.52" />
|
<path d="M12.5,23.24v-1A10.74,10.74,0,0,1,23.24,11.52" />
|
||||||
</g>
|
</g>
|
||||||
),
|
),
|
||||||
|
[ICONS.SIMPLE_EDITOR]: buildIcon(
|
||||||
|
<g>
|
||||||
|
<path d="M1 18V6c0-1 1-2 2-2h18c1 0 2 1 2 2v12c0 1-1 2-2 2H3c-1 0-2-1-2-2ZM5 7v4" />
|
||||||
|
</g>
|
||||||
|
),
|
||||||
|
[ICONS.ADVANCED_EDITOR]: buildIcon(
|
||||||
|
<g>
|
||||||
|
<path d="M1 20V4c0-1 1-2 2-2h18c1 0 2 1 2 2v16c0 1-1 2-2 2H3c-1 0-2-1-2-2ZM1 11h22" />
|
||||||
|
<path d="M5 8V6h2v2H5ZM11 8V6h2v2h-2ZM17 8V6h2v2h-2ZM5 14v4" />
|
||||||
|
</g>
|
||||||
|
),
|
||||||
};
|
};
|
||||||
|
|
|
@ -7,7 +7,6 @@ import {
|
||||||
} from 'redux/selectors/claims';
|
} from 'redux/selectors/claims';
|
||||||
import { makeSelectPendingAmountByUri } from 'redux/selectors/wallet';
|
import { makeSelectPendingAmountByUri } from 'redux/selectors/wallet';
|
||||||
import { doOpenModal } from 'redux/actions/app';
|
import { doOpenModal } from 'redux/actions/app';
|
||||||
import { selectUser } from 'redux/selectors/user';
|
|
||||||
import FileDescription from './view';
|
import FileDescription from './view';
|
||||||
|
|
||||||
const select = (state, props) => {
|
const select = (state, props) => {
|
||||||
|
@ -17,7 +16,6 @@ const select = (state, props) => {
|
||||||
claim,
|
claim,
|
||||||
claimIsMine: selectClaimIsMine(state, claim),
|
claimIsMine: selectClaimIsMine(state, claim),
|
||||||
metadata: makeSelectMetadataForUri(props.uri)(state),
|
metadata: makeSelectMetadataForUri(props.uri)(state),
|
||||||
user: selectUser(state),
|
|
||||||
pendingAmount: makeSelectPendingAmountByUri(props.uri)(state),
|
pendingAmount: makeSelectPendingAmountByUri(props.uri)(state),
|
||||||
tags: makeSelectTagsForUri(props.uri)(state),
|
tags: makeSelectTagsForUri(props.uri)(state),
|
||||||
};
|
};
|
||||||
|
|
|
@ -15,7 +15,6 @@ type Props = {
|
||||||
uri: string,
|
uri: string,
|
||||||
claim: StreamClaim,
|
claim: StreamClaim,
|
||||||
metadata: StreamMetadata,
|
metadata: StreamMetadata,
|
||||||
user: ?any,
|
|
||||||
tags: any,
|
tags: any,
|
||||||
pendingAmount: number,
|
pendingAmount: number,
|
||||||
doOpenModal: (id: string, {}) => void,
|
doOpenModal: (id: string, {}) => void,
|
||||||
|
|
|
@ -11,10 +11,10 @@ import Icon from 'component/common/icon';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
modal: { id: string, modalProps: {} },
|
modal: { id: string, modalProps: {} },
|
||||||
filePath: string | WebFile,
|
filePath: ?string,
|
||||||
clearPublish: () => void,
|
clearPublish: () => void,
|
||||||
updatePublishForm: ({}) => void,
|
updatePublishForm: ({}) => void,
|
||||||
openModal: (id: string, { files: Array<WebFile> }) => void,
|
openModal: (id: string, { files: Array<File> }) => void,
|
||||||
// React router
|
// React router
|
||||||
history: {
|
history: {
|
||||||
entities: {}[],
|
entities: {}[],
|
||||||
|
@ -37,7 +37,7 @@ function FileDrop(props: Props) {
|
||||||
const { drag, dropData } = useDragDrop();
|
const { drag, dropData } = useDragDrop();
|
||||||
const [files, setFiles] = React.useState([]);
|
const [files, setFiles] = React.useState([]);
|
||||||
const [error, setError] = React.useState(false);
|
const [error, setError] = React.useState(false);
|
||||||
const [target, setTarget] = React.useState<?WebFile>(null);
|
const [target, setTarget] = React.useState<?File>(null);
|
||||||
const hideTimer = React.useRef(null);
|
const hideTimer = React.useRef(null);
|
||||||
const targetTimer = React.useRef(null);
|
const targetTimer = React.useRef(null);
|
||||||
const navigationTimer = React.useRef(null);
|
const navigationTimer = React.useRef(null);
|
||||||
|
@ -65,24 +65,26 @@ function FileDrop(props: Props) {
|
||||||
}
|
}
|
||||||
}, [history]);
|
}, [history]);
|
||||||
|
|
||||||
|
// Handle file selection
|
||||||
|
const handleFileSelected = React.useCallback(
|
||||||
|
(selectedFile) => {
|
||||||
// Delay hide and navigation for a smooth transition
|
// Delay hide and navigation for a smooth transition
|
||||||
const hideDropArea = React.useCallback(() => {
|
|
||||||
hideTimer.current = setTimeout(() => {
|
hideTimer.current = setTimeout(() => {
|
||||||
setFiles([]);
|
setFiles([]);
|
||||||
// Navigate to publish area
|
// Navigate to publish area
|
||||||
navigationTimer.current = setTimeout(() => {
|
navigationTimer.current = setTimeout(() => {
|
||||||
|
// Navigate first, THEN assign filePath, otherwise
|
||||||
|
// the file selected will get reset (that's how the
|
||||||
|
// publish file view works, when the user switches to
|
||||||
|
// publish a file, the pathFile value gets reset to undefined)
|
||||||
navigateToPublish();
|
navigateToPublish();
|
||||||
|
updatePublishForm({
|
||||||
|
filePath: selectedFile.path || selectedFile.name,
|
||||||
|
});
|
||||||
}, NAVIGATE_TIME_OUT);
|
}, NAVIGATE_TIME_OUT);
|
||||||
}, HIDE_TIME_OUT);
|
}, HIDE_TIME_OUT);
|
||||||
}, [navigateToPublish]);
|
|
||||||
|
|
||||||
// Handle file selection
|
|
||||||
const handleFileSelected = React.useCallback(
|
|
||||||
(selectedFile) => {
|
|
||||||
updatePublishForm({ filePath: selectedFile });
|
|
||||||
hideDropArea();
|
|
||||||
},
|
},
|
||||||
[updatePublishForm, hideDropArea]
|
[setFiles, navigateToPublish, updatePublishForm]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Clear timers when unmounted
|
// Clear timers when unmounted
|
||||||
|
|
|
@ -4,7 +4,7 @@ import * as ICONS from 'constants/icons';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import Button from 'component/button';
|
import Button from 'component/button';
|
||||||
import { formatNumberWithCommas } from 'util/number';
|
import { formatNumber } from 'util/number';
|
||||||
import NudgeFloating from 'component/nudgeFloating';
|
import NudgeFloating from 'component/nudgeFloating';
|
||||||
type Props = {
|
type Props = {
|
||||||
claim: StreamClaim,
|
claim: StreamClaim,
|
||||||
|
@ -18,16 +18,8 @@ type Props = {
|
||||||
};
|
};
|
||||||
|
|
||||||
function FileReactions(props: Props) {
|
function FileReactions(props: Props) {
|
||||||
const {
|
const { claim, uri, doFetchReactions, doReactionLike, doReactionDislike, myReaction, likeCount, dislikeCount } =
|
||||||
claim,
|
props;
|
||||||
uri,
|
|
||||||
doFetchReactions,
|
|
||||||
doReactionLike,
|
|
||||||
doReactionDislike,
|
|
||||||
myReaction,
|
|
||||||
likeCount,
|
|
||||||
dislikeCount,
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
const claimId = claim && claim.claim_id;
|
const claimId = claim && claim.claim_id;
|
||||||
const channel = claim && claim.signing_channel && claim.signing_channel.name;
|
const channel = claim && claim.signing_channel && claim.signing_channel.name;
|
||||||
|
@ -59,7 +51,7 @@ function FileReactions(props: Props) {
|
||||||
className={classnames('button--file-action button-like', {
|
className={classnames('button--file-action button-like', {
|
||||||
'button--file-action-active': myReaction === REACTION_TYPES.LIKE,
|
'button--file-action-active': myReaction === REACTION_TYPES.LIKE,
|
||||||
})}
|
})}
|
||||||
label={<>{formatNumberWithCommas(likeCount, 0)}</>}
|
label={<>{formatNumber(likeCount, 2, true)}</>}
|
||||||
iconSize={18}
|
iconSize={18}
|
||||||
icon={likeIcon}
|
icon={likeIcon}
|
||||||
onClick={() => doReactionLike(uri)}
|
onClick={() => doReactionLike(uri)}
|
||||||
|
@ -70,7 +62,7 @@ function FileReactions(props: Props) {
|
||||||
className={classnames('button--file-action button-dislike', {
|
className={classnames('button--file-action button-dislike', {
|
||||||
'button--file-action-active': myReaction === REACTION_TYPES.DISLIKE,
|
'button--file-action-active': myReaction === REACTION_TYPES.DISLIKE,
|
||||||
})}
|
})}
|
||||||
label={<>{formatNumberWithCommas(dislikeCount, 0)}</>}
|
label={<>{formatNumber(dislikeCount, 2, true)}</>}
|
||||||
iconSize={18}
|
iconSize={18}
|
||||||
icon={dislikeIcon}
|
icon={dislikeIcon}
|
||||||
onClick={() => doReactionDislike(uri)}
|
onClick={() => doReactionDislike(uri)}
|
||||||
|
|
|
@ -12,7 +12,7 @@ import { PRIMARY_PLAYER_WRAPPER_CLASS } from 'page/file/view';
|
||||||
import Draggable from 'react-draggable';
|
import Draggable from 'react-draggable';
|
||||||
import { onFullscreenChange } from 'util/full-screen';
|
import { onFullscreenChange } from 'util/full-screen';
|
||||||
import { generateListSearchUrlParams, formatLbryUrlForWeb } from 'util/url';
|
import { generateListSearchUrlParams, formatLbryUrlForWeb } from 'util/url';
|
||||||
import { useIsMobile } from 'effects/use-screensize';
|
import { useIsMobile, useIsMediumScreen } from 'effects/use-screensize';
|
||||||
import debounce from 'util/debounce';
|
import debounce from 'util/debounce';
|
||||||
import { useHistory } from 'react-router';
|
import { useHistory } from 'react-router';
|
||||||
import { isURIEqual } from 'util/lbryURI';
|
import { isURIEqual } from 'util/lbryURI';
|
||||||
|
@ -21,7 +21,6 @@ import AutoplayCountdown from 'component/autoplayCountdown';
|
||||||
// scss/init/vars.scss
|
// scss/init/vars.scss
|
||||||
// --header-height
|
// --header-height
|
||||||
const HEADER_HEIGHT = 60;
|
const HEADER_HEIGHT = 60;
|
||||||
const HEADER_HEIGHT_MOBILE = 60;
|
|
||||||
|
|
||||||
const IS_DESKTOP_MAC = typeof process === 'object' ? process.platform === 'darwin' : false;
|
const IS_DESKTOP_MAC = typeof process === 'object' ? process.platform === 'darwin' : false;
|
||||||
const DEBOUNCE_WINDOW_RESIZE_HANDLER_MS = 100;
|
const DEBOUNCE_WINDOW_RESIZE_HANDLER_MS = 100;
|
||||||
|
@ -133,6 +132,7 @@ export default function FileRenderFloating(props: Props) {
|
||||||
const playingUriSource = playingUri && playingUri.source;
|
const playingUriSource = playingUri && playingUri.source;
|
||||||
const isComment = playingUriSource === 'comment';
|
const isComment = playingUriSource === 'comment';
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
|
const isMediumScreen = useIsMediumScreen();
|
||||||
const mainFilePlaying = !isFloating && primaryUri && isURIEqual(uri, primaryUri);
|
const mainFilePlaying = !isFloating && primaryUri && isURIEqual(uri, primaryUri);
|
||||||
|
|
||||||
const [fileViewerRect, setFileViewerRect] = useState();
|
const [fileViewerRect, setFileViewerRect] = useState();
|
||||||
|
@ -344,7 +344,8 @@ export default function FileRenderFloating(props: Props) {
|
||||||
'content__viewer--floating': isFloating,
|
'content__viewer--floating': isFloating,
|
||||||
'content__viewer--inline': !isFloating,
|
'content__viewer--inline': !isFloating,
|
||||||
'content__viewer--secondary': isComment,
|
'content__viewer--secondary': isComment,
|
||||||
'content__viewer--theater-mode': !isFloating && videoTheaterMode && playingUri?.uri === primaryUri,
|
'content__viewer--theater-mode':
|
||||||
|
!isFloating && videoTheaterMode && !isMediumScreen && playingUri?.uri === primaryUri,
|
||||||
'content__viewer--disable-click': wasDragging,
|
'content__viewer--disable-click': wasDragging,
|
||||||
})}
|
})}
|
||||||
style={
|
style={
|
||||||
|
@ -356,7 +357,7 @@ export default function FileRenderFloating(props: Props) {
|
||||||
top:
|
top:
|
||||||
fileViewerRect.windowOffset +
|
fileViewerRect.windowOffset +
|
||||||
fileViewerRect.top -
|
fileViewerRect.top -
|
||||||
(isMobile ? HEADER_HEIGHT_MOBILE : HEADER_HEIGHT) -
|
(isMobile ? 0 : HEADER_HEIGHT) -
|
||||||
(IS_DESKTOP_MAC ? 24 : 0),
|
(IS_DESKTOP_MAC ? 24 : 0),
|
||||||
}
|
}
|
||||||
: {}
|
: {}
|
||||||
|
|
|
@ -9,6 +9,7 @@ import * as PAGES from 'constants/pages';
|
||||||
import * as RENDER_MODES from 'constants/file_render_modes';
|
import * as RENDER_MODES from 'constants/file_render_modes';
|
||||||
import * as KEYCODES from 'constants/keycodes';
|
import * as KEYCODES from 'constants/keycodes';
|
||||||
import Button from 'component/button';
|
import Button from 'component/button';
|
||||||
|
import { useIsMediumScreen } from 'effects/use-screensize';
|
||||||
import isUserTyping from 'util/detect-typing';
|
import isUserTyping from 'util/detect-typing';
|
||||||
import { getThumbnailCdnUrl } from 'util/thumbnail';
|
import { getThumbnailCdnUrl } from 'util/thumbnail';
|
||||||
import Nag from 'component/common/nag';
|
import Nag from 'component/common/nag';
|
||||||
|
@ -63,6 +64,7 @@ export default function FileRenderInitiator(props: Props) {
|
||||||
const fileStatus = fileInfo && fileInfo.status;
|
const fileStatus = fileInfo && fileInfo.status;
|
||||||
const isPlayable = RENDER_MODES.FLOATING_MODES.includes(renderMode);
|
const isPlayable = RENDER_MODES.FLOATING_MODES.includes(renderMode);
|
||||||
const isText = RENDER_MODES.TEXT_MODES.includes(renderMode);
|
const isText = RENDER_MODES.TEXT_MODES.includes(renderMode);
|
||||||
|
const isMediumScreen = useIsMediumScreen();
|
||||||
const [thumbnail, setThumbnail] = React.useState(FileRenderPlaceholder);
|
const [thumbnail, setThumbnail] = React.useState(FileRenderPlaceholder);
|
||||||
const containerRef = React.useRef<any>();
|
const containerRef = React.useRef<any>();
|
||||||
|
|
||||||
|
@ -151,7 +153,7 @@ export default function FileRenderInitiator(props: Props) {
|
||||||
style={thumbnail && !obscurePreview ? { backgroundImage: `url("${thumbnail}")` } : {}}
|
style={thumbnail && !obscurePreview ? { backgroundImage: `url("${thumbnail}")` } : {}}
|
||||||
className={classnames('content__cover', {
|
className={classnames('content__cover', {
|
||||||
'content__cover--disabled': disabled,
|
'content__cover--disabled': disabled,
|
||||||
'content__cover--theater-mode': videoTheaterMode,
|
'content__cover--theater-mode': videoTheaterMode && !isMediumScreen,
|
||||||
'content__cover--text': isText,
|
'content__cover--text': isText,
|
||||||
'card__media--nsfw': obscurePreview,
|
'card__media--nsfw': obscurePreview,
|
||||||
})}
|
})}
|
||||||
|
|
|
@ -1,16 +1,25 @@
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { doResolveUri } from 'redux/actions/claims';
|
import { doResolveUri } from 'redux/actions/claims';
|
||||||
import { makeSelectClaimForUri } from 'redux/selectors/claims';
|
import { makeSelectClaimForUri } from 'redux/selectors/claims';
|
||||||
import { makeSelectContentWatchedPercentageForUri } from 'redux/selectors/content';
|
import { selectPrimaryUri, makeSelectContentWatchedPercentageForUri } from 'redux/selectors/content';
|
||||||
import CardMedia from './view';
|
import CardMedia from './view';
|
||||||
import { makeSelectClientSetting } from 'redux/selectors/settings';
|
import { makeSelectClientSetting } from 'redux/selectors/settings';
|
||||||
import * as SETTINGS from 'constants/settings';
|
import * as SETTINGS from 'constants/settings';
|
||||||
|
|
||||||
const select = (state, props) => {
|
const select = (state, props) => {
|
||||||
|
const claimUriBeingPlayed = selectPrimaryUri(state);
|
||||||
|
let isBeingPlayed = false;
|
||||||
|
|
||||||
|
if (claimUriBeingPlayed) {
|
||||||
|
const claim = makeSelectClaimForUri(props.uri)(state);
|
||||||
|
const claimBeingPlayed = makeSelectClaimForUri(claimUriBeingPlayed)(state);
|
||||||
|
isBeingPlayed = claim && claim.claim_id === claimBeingPlayed.claim_id;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
watchedPercentage: makeSelectContentWatchedPercentageForUri(props.uri)(state),
|
watchedPercentage: makeSelectContentWatchedPercentageForUri(props.uri)(state),
|
||||||
claim: makeSelectClaimForUri(props.uri)(state),
|
claim: makeSelectClaimForUri(props.uri)(state),
|
||||||
showPercentage: makeSelectClientSetting(SETTINGS.PERSIST_WATCH_TIME)(state),
|
showPercentage: !isBeingPlayed && makeSelectClientSetting(SETTINGS.PERSIST_WATCH_TIME)(state),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,6 @@ import {
|
||||||
} from 'redux/selectors/claims';
|
} from 'redux/selectors/claims';
|
||||||
import { makeSelectPendingAmountByUri } from 'redux/selectors/wallet';
|
import { makeSelectPendingAmountByUri } from 'redux/selectors/wallet';
|
||||||
import { makeSelectFileInfoForUri } from 'redux/selectors/file_info';
|
import { makeSelectFileInfoForUri } from 'redux/selectors/file_info';
|
||||||
import { selectUser } from 'redux/selectors/user';
|
|
||||||
import { doOpenModal } from 'redux/actions/app';
|
import { doOpenModal } from 'redux/actions/app';
|
||||||
|
|
||||||
import FileValues from './view';
|
import FileValues from './view';
|
||||||
|
@ -20,7 +19,6 @@ const select = (state, props) => {
|
||||||
contentType: makeSelectContentTypeForUri(props.uri)(state),
|
contentType: makeSelectContentTypeForUri(props.uri)(state),
|
||||||
fileInfo: makeSelectFileInfoForUri(props.uri)(state),
|
fileInfo: makeSelectFileInfoForUri(props.uri)(state),
|
||||||
metadata: makeSelectMetadataForUri(props.uri)(state),
|
metadata: makeSelectMetadataForUri(props.uri)(state),
|
||||||
user: selectUser(state),
|
|
||||||
pendingAmount: makeSelectPendingAmountByUri(props.uri)(state),
|
pendingAmount: makeSelectPendingAmountByUri(props.uri)(state),
|
||||||
claimIsMine: selectClaimIsMine(state, claim),
|
claimIsMine: selectClaimIsMine(state, claim),
|
||||||
};
|
};
|
||||||
|
|
|
@ -15,7 +15,6 @@ type Props = {
|
||||||
metadata: StreamMetadata,
|
metadata: StreamMetadata,
|
||||||
openFolder: (string) => void,
|
openFolder: (string) => void,
|
||||||
contentType: string,
|
contentType: string,
|
||||||
user: ?any,
|
|
||||||
pendingAmount: string,
|
pendingAmount: string,
|
||||||
openModal: (id: string, { uri: string }) => void,
|
openModal: (id: string, { uri: string }) => void,
|
||||||
claimIsMine: boolean,
|
claimIsMine: boolean,
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import HelpLink from 'component/common/help-link';
|
import HelpLink from 'component/common/help-link';
|
||||||
import Tooltip from 'component/common/tooltip';
|
import Tooltip from 'component/common/tooltip';
|
||||||
import { toCompactNotation } from 'util/string';
|
import { formatNumber } from 'util/number';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
claimId: ?string, // this
|
claimId: ?string, // this
|
||||||
|
@ -14,8 +14,8 @@ type Props = {
|
||||||
|
|
||||||
function FileViewCount(props: Props) {
|
function FileViewCount(props: Props) {
|
||||||
const { claimId, uri, fetchViewCount, viewCount } = props; // claimId
|
const { claimId, uri, fetchViewCount, viewCount } = props; // claimId
|
||||||
const countCompact = toCompactNotation(viewCount);
|
const countCompact = formatNumber(Number(viewCount), 2, true);
|
||||||
const countFullResolution = Number(viewCount).toLocaleString();
|
const countFullResolution = formatNumber(Number(viewCount), 2, false);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (claimId) {
|
if (claimId) {
|
||||||
|
@ -24,7 +24,7 @@ function FileViewCount(props: Props) {
|
||||||
}, [fetchViewCount, uri, claimId]);
|
}, [fetchViewCount, uri, claimId]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip label={countFullResolution}>
|
<Tooltip title={`${countFullResolution}`}>
|
||||||
<span className="media__subtitle--centered">
|
<span className="media__subtitle--centered">
|
||||||
{viewCount !== 1 ? __('%view_count% views', { view_count: countCompact }) : __('1 view')}
|
{viewCount !== 1 ? __('%view_count% views', { view_count: countCompact }) : __('1 view')}
|
||||||
{<HelpLink href="https://lbry.com/faq/views" />}
|
{<HelpLink href="https://lbry.com/faq/views" />}
|
||||||
|
|
|
@ -6,12 +6,11 @@ import { selectClientSetting } from 'redux/selectors/settings';
|
||||||
import { selectGetSyncErrorMessage } from 'redux/selectors/sync';
|
import { selectGetSyncErrorMessage } from 'redux/selectors/sync';
|
||||||
import { selectHasNavigated } from 'redux/selectors/app';
|
import { selectHasNavigated } from 'redux/selectors/app';
|
||||||
import { selectTotalBalance, selectBalance } from 'redux/selectors/wallet';
|
import { selectTotalBalance, selectBalance } from 'redux/selectors/wallet';
|
||||||
import { selectUserVerifiedEmail, selectEmailToVerify, selectUser } from 'redux/selectors/user';
|
import { selectEmailToVerify, selectUser } from 'redux/selectors/user';
|
||||||
import * as SETTINGS from 'constants/settings';
|
import * as SETTINGS from 'constants/settings';
|
||||||
import Header from './view';
|
import Header from './view';
|
||||||
|
|
||||||
const select = (state) => ({
|
const select = (state) => ({
|
||||||
authenticated: selectUserVerifiedEmail(state),
|
|
||||||
balance: selectBalance(state),
|
balance: selectBalance(state),
|
||||||
emailToVerify: selectEmailToVerify(state),
|
emailToVerify: selectEmailToVerify(state),
|
||||||
hasNavigated: selectHasNavigated(state),
|
hasNavigated: selectHasNavigated(state),
|
||||||
|
|
|
@ -4,11 +4,10 @@ import { selectActiveChannelStakedLevel } from 'redux/selectors/app';
|
||||||
import { selectClientSetting } from 'redux/selectors/settings';
|
import { selectClientSetting } from 'redux/selectors/settings';
|
||||||
import * as SETTINGS from 'constants/settings';
|
import * as SETTINGS from 'constants/settings';
|
||||||
import HeaderMenuButtons from './view';
|
import HeaderMenuButtons from './view';
|
||||||
import { selectUserVerifiedEmail, selectUser } from 'redux/selectors/user';
|
import { selectUser } from 'redux/selectors/user';
|
||||||
|
|
||||||
const select = (state) => ({
|
const select = (state) => ({
|
||||||
activeChannelStakedLevel: selectActiveChannelStakedLevel(state),
|
activeChannelStakedLevel: selectActiveChannelStakedLevel(state),
|
||||||
authenticated: selectUserVerifiedEmail(state),
|
|
||||||
automaticDarkModeEnabled: selectClientSetting(state, SETTINGS.AUTOMATIC_DARK_MODE_ENABLED),
|
automaticDarkModeEnabled: selectClientSetting(state, SETTINGS.AUTOMATIC_DARK_MODE_ENABLED),
|
||||||
currentTheme: selectClientSetting(state, SETTINGS.THEME),
|
currentTheme: selectClientSetting(state, SETTINGS.THEME),
|
||||||
user: selectUser(state),
|
user: selectUser(state),
|
||||||
|
|
|
@ -12,7 +12,6 @@ import React from 'react';
|
||||||
import Tooltip from 'component/common/tooltip';
|
import Tooltip from 'component/common/tooltip';
|
||||||
|
|
||||||
type HeaderMenuButtonProps = {
|
type HeaderMenuButtonProps = {
|
||||||
authenticated: boolean,
|
|
||||||
automaticDarkModeEnabled: boolean,
|
automaticDarkModeEnabled: boolean,
|
||||||
currentTheme: string,
|
currentTheme: string,
|
||||||
user: ?User,
|
user: ?User,
|
||||||
|
|
27
ui/component/hostingSplash/index.js
Normal file
27
ui/component/hostingSplash/index.js
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
|
import HostingSplash from './view';
|
||||||
|
import {
|
||||||
|
selectViewBlobSpace,
|
||||||
|
selectViewHostingLimit,
|
||||||
|
selectAutoBlobSpace,
|
||||||
|
selectAutoHostingLimit,
|
||||||
|
selectSaveBlobs,
|
||||||
|
} from 'redux/selectors/settings';
|
||||||
|
import { doSetDaemonSetting } from 'redux/actions/settings';
|
||||||
|
import { selectDiskSpace } from 'redux/selectors/app';
|
||||||
|
|
||||||
|
const select = (state) => ({
|
||||||
|
diskSpace: selectDiskSpace(state),
|
||||||
|
viewHostingLimit: selectViewHostingLimit(state),
|
||||||
|
autoHostingLimit: selectAutoHostingLimit(state),
|
||||||
|
viewBlobSpace: selectViewBlobSpace(state),
|
||||||
|
autoBlobSpace: selectAutoBlobSpace(state),
|
||||||
|
saveBlobs: selectSaveBlobs(state),
|
||||||
|
});
|
||||||
|
|
||||||
|
const perform = (dispatch) => ({
|
||||||
|
setDaemonSetting: (key, value) => dispatch(doSetDaemonSetting(key, value)),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(select, perform)(HostingSplash);
|
156
ui/component/hostingSplash/view.jsx
Normal file
156
ui/component/hostingSplash/view.jsx
Normal file
|
@ -0,0 +1,156 @@
|
||||||
|
// @flow
|
||||||
|
import React from 'react';
|
||||||
|
import Button from 'component/button';
|
||||||
|
import { FormField } from 'component/common/form-components/form-field';
|
||||||
|
import { Form } from 'component/common/form-components/form';
|
||||||
|
import { withRouter } from 'react-router-dom';
|
||||||
|
|
||||||
|
// $FlowFixMe cannot resolve ...
|
||||||
|
import image from 'static/img/yrblhappy.svg';
|
||||||
|
import * as DAEMON_SETTINGS from 'constants/daemon_settings';
|
||||||
|
|
||||||
|
type SetDaemonSettingArg = boolean | string | number;
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
handleNextPage: () => void,
|
||||||
|
handleDone: () => void,
|
||||||
|
setDaemonSetting: (string, ?SetDaemonSettingArg) => void,
|
||||||
|
// --- select ---
|
||||||
|
diskSpace: DiskSpace, // KB
|
||||||
|
viewHostingLimit: number, // MB
|
||||||
|
autoHostingLimit: number,
|
||||||
|
viewBlobSpace: number,
|
||||||
|
autoBlobSpace: number,
|
||||||
|
privateBlobSpace: number,
|
||||||
|
saveBlobs: boolean,
|
||||||
|
};
|
||||||
|
|
||||||
|
const TWENTY_PERCENT = 0.2;
|
||||||
|
const TEN_PRECNT = 0.1;
|
||||||
|
|
||||||
|
function HostingSplash(props: Props) {
|
||||||
|
const {
|
||||||
|
handleNextPage,
|
||||||
|
diskSpace,
|
||||||
|
viewHostingLimit,
|
||||||
|
autoHostingLimit,
|
||||||
|
viewBlobSpace,
|
||||||
|
autoBlobSpace,
|
||||||
|
saveBlobs,
|
||||||
|
setDaemonSetting,
|
||||||
|
handleDone,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const totalMB = diskSpace && Math.floor(diskSpace.total / 1024);
|
||||||
|
const freeMB = diskSpace && Math.floor(diskSpace.free / 1024);
|
||||||
|
const blobSpaceUsed = viewBlobSpace + autoBlobSpace;
|
||||||
|
|
||||||
|
const [hostingChoice, setHostingChoice] = React.useState('MANAGED');
|
||||||
|
function handleSubmit() {
|
||||||
|
if (hostingChoice === 'CUSTOM') {
|
||||||
|
handleNextPage();
|
||||||
|
} else {
|
||||||
|
handleAuto();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getManagedLimitMB() {
|
||||||
|
const value =
|
||||||
|
freeMB > totalMB * TWENTY_PERCENT // lots of free space?
|
||||||
|
? blobSpaceUsed > totalMB * TEN_PRECNT // using more than 10%?
|
||||||
|
? (freeMB + blobSpaceUsed) / 2 // e.g. 40g used plus 30g free, knock back to 35g limit, freeing to 35g
|
||||||
|
: totalMB * TEN_PRECNT // let it go up to 10%
|
||||||
|
: (freeMB + blobSpaceUsed) / 2; // e.g. 40g used plus 10g free, knock back to 25g limit, freeing to 25g
|
||||||
|
return value > 10240 ? Math.floor(value / 1024) * 1024 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAutoLimit() {
|
||||||
|
// return floor of 10% of total
|
||||||
|
const totalGB = Math.floor(getManagedLimitMB() / 1024); // eg, 25GB
|
||||||
|
return Math.floor(totalGB / 10) * 1024; // eg, 2 GB -> 2048MB
|
||||||
|
}
|
||||||
|
|
||||||
|
function getViewedLimit() {
|
||||||
|
return getManagedLimitMB() - getAutoLimit();
|
||||||
|
}
|
||||||
|
|
||||||
|
function getManagedCopy() {
|
||||||
|
if (viewHostingLimit || autoHostingLimit || !saveBlobs) {
|
||||||
|
return __("I'm happy with my settings");
|
||||||
|
} else if (getManagedLimitMB() > 0) {
|
||||||
|
return __(`Host up to %percent% of my drive (%limit% GB)`, {
|
||||||
|
percent: `${Math.round((Math.floor(getManagedLimitMB() / 1024) / Math.floor(totalMB / 1024)) * 100)}%`,
|
||||||
|
limit: Math.floor(getManagedLimitMB() / 1024),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return __(`Not now, my disk is almost full.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getManagedHelper() {
|
||||||
|
if (viewHostingLimit || autoHostingLimit || !saveBlobs) {
|
||||||
|
return __(`We've noticed you already have some settings.`);
|
||||||
|
} else if (getManagedLimitMB() > 0) {
|
||||||
|
return __(`Donate space without filling up your drive.`);
|
||||||
|
} else {
|
||||||
|
return __(`You can clear some space and check hosting settings later.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleAuto() {
|
||||||
|
if (viewHostingLimit || autoHostingLimit || !saveBlobs) {
|
||||||
|
handleDone();
|
||||||
|
} else if (getManagedLimitMB() > 0) {
|
||||||
|
// limit to used // maybe move this to a single action function that doesn't live inside the component.
|
||||||
|
await setDaemonSetting(DAEMON_SETTINGS.BLOB_STORAGE_LIMIT_MB, getViewedLimit());
|
||||||
|
await setDaemonSetting(DAEMON_SETTINGS.NETWORK_STORAGE_LIMIT_MB, getAutoLimit());
|
||||||
|
handleDone();
|
||||||
|
} else {
|
||||||
|
// running low on space
|
||||||
|
handleDone();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="main--contained">
|
||||||
|
<div className={'columns first-run__wrapper'}>
|
||||||
|
<div className={'first-run__left'}>
|
||||||
|
<div>
|
||||||
|
<h1 className="section__title--large">{__('Hosting')}</h1>
|
||||||
|
<h3 className="section__subtitle">
|
||||||
|
{__('Help creators and improve the P2P data network by hosting content.')}
|
||||||
|
</h3>
|
||||||
|
<fieldset>
|
||||||
|
<FormField
|
||||||
|
name={'managedhosting'}
|
||||||
|
type="radio"
|
||||||
|
checked={hostingChoice === 'MANAGED'}
|
||||||
|
label={getManagedCopy()}
|
||||||
|
helper={getManagedHelper()}
|
||||||
|
onChange={(e) => setHostingChoice('MANAGED')}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
name={'customhosting'}
|
||||||
|
type="radio"
|
||||||
|
checked={hostingChoice === 'CUSTOM'}
|
||||||
|
label={<>{__('Custom')}</>}
|
||||||
|
helper={__(`You choose how much data to host.`)}
|
||||||
|
onChange={(e) => setHostingChoice('CUSTOM')}
|
||||||
|
/>
|
||||||
|
</fieldset>
|
||||||
|
</div>
|
||||||
|
<Form onSubmit={handleSubmit} className="section__body">
|
||||||
|
<div className={'card__actions'}>
|
||||||
|
<Button button="primary" label={hostingChoice === 'CUSTOM' ? __('Next') : __(`Let's go`)} type="submit" />
|
||||||
|
</div>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
<div className={'first-run__image-wrapper'}>
|
||||||
|
<img src={image} className="privacy-img" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withRouter(HostingSplash);
|
3
ui/component/hostingSplashCustom/index.js
Normal file
3
ui/component/hostingSplashCustom/index.js
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
import HostingSplashCustom from './view';
|
||||||
|
|
||||||
|
export default HostingSplashCustom;
|
31
ui/component/hostingSplashCustom/view.jsx
Normal file
31
ui/component/hostingSplashCustom/view.jsx
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
// @flow
|
||||||
|
import React from 'react';
|
||||||
|
import Button from 'component/button';
|
||||||
|
import { Form } from 'component/common/form-components/form';
|
||||||
|
import SettingStorage from 'component/settingStorage';
|
||||||
|
import { withRouter } from 'react-router-dom';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
handleNextPage: () => void,
|
||||||
|
handleGoBack: () => void,
|
||||||
|
};
|
||||||
|
|
||||||
|
function HostingSplashCustom(props: Props) {
|
||||||
|
const { handleNextPage, handleGoBack } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="main--contained">
|
||||||
|
<div className={'first-run__wrapper'}>
|
||||||
|
<SettingStorage isWelcome />
|
||||||
|
<Form onSubmit={handleNextPage} className="section__body">
|
||||||
|
<div className={'card__actions'}>
|
||||||
|
<Button button="primary" label={__(`Let's go`)} type="submit" />
|
||||||
|
<Button button="link" label={__(`Go back`)} onClick={handleGoBack} />
|
||||||
|
</div>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withRouter(HostingSplashCustom);
|
|
@ -1,14 +0,0 @@
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { selectReferralReward } from 'redux/selectors/rewards';
|
|
||||||
import { selectUserInvitees, selectUserInviteStatusIsPending } from 'redux/selectors/user';
|
|
||||||
import InviteList from './view';
|
|
||||||
|
|
||||||
const select = state => ({
|
|
||||||
invitees: selectUserInvitees(state),
|
|
||||||
isPending: selectUserInviteStatusIsPending(state),
|
|
||||||
referralReward: selectReferralReward(state),
|
|
||||||
});
|
|
||||||
|
|
||||||
const perform = () => ({});
|
|
||||||
|
|
||||||
export default connect(select, perform)(InviteList);
|
|
|
@ -1,99 +0,0 @@
|
||||||
// @flow
|
|
||||||
import React from 'react';
|
|
||||||
import RewardLink from 'component/rewardLink';
|
|
||||||
import Icon from 'component/common/icon';
|
|
||||||
import * as ICONS from 'constants/icons';
|
|
||||||
import Card from 'component/common/card';
|
|
||||||
import LbcMessage from 'component/common/lbc-message';
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
invitees: ?Array<{
|
|
||||||
email: string,
|
|
||||||
invite_accepted: boolean,
|
|
||||||
invite_reward_claimed: boolean,
|
|
||||||
invite_reward_claimable: boolean,
|
|
||||||
}>,
|
|
||||||
referralReward: ?Reward,
|
|
||||||
};
|
|
||||||
|
|
||||||
class InviteList extends React.PureComponent<Props> {
|
|
||||||
render() {
|
|
||||||
const { invitees, referralReward } = this.props;
|
|
||||||
|
|
||||||
if (!invitees || !invitees.length) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
let rewardAmount = 0;
|
|
||||||
let rewardHelp = __(
|
|
||||||
"Woah, you have a lot of friends! You've claimed the maximum amount of invite rewards. Email %email% if you'd like to be whitelisted for more invites.",
|
|
||||||
{ email: 'hello@lbry.com' }
|
|
||||||
);
|
|
||||||
|
|
||||||
if (referralReward) {
|
|
||||||
rewardAmount = referralReward.reward_amount;
|
|
||||||
rewardHelp = referralReward.reward_description;
|
|
||||||
}
|
|
||||||
const showClaimable = invitees.some(invite => invite.invite_reward_claimable && !invite.invite_reward_claimed);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card
|
|
||||||
title={<div className="table__header-text">{__('Invite History')}</div>}
|
|
||||||
subtitle={
|
|
||||||
<div className="table__header-text">
|
|
||||||
<LbcMessage>{rewardHelp}</LbcMessage>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
titleActions={
|
|
||||||
referralReward &&
|
|
||||||
showClaimable && (
|
|
||||||
<div className="card__actions--inline">
|
|
||||||
<RewardLink
|
|
||||||
button
|
|
||||||
label={__(`Claim Your %reward_amount% Credit Invite Reward`, { reward_amount: rewardAmount })}
|
|
||||||
claim_code={referralReward.claim_code}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
isBodyList
|
|
||||||
body={
|
|
||||||
<div className="table__wrapper">
|
|
||||||
<table className="table section">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>{__('Invitee Email')}</th>
|
|
||||||
<th>{__('Invite Status')}</th>
|
|
||||||
<th>{__('Reward')}</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{invitees.map(invitee => (
|
|
||||||
<tr key={invitee.email}>
|
|
||||||
<td>{invitee.email}</td>
|
|
||||||
<td>
|
|
||||||
<span>{invitee.invite_accepted ? __('Accepted') : __('Not Accepted')}</span>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
{invitee.invite_reward_claimed && (
|
|
||||||
<React.Fragment>
|
|
||||||
<span>{__('Claimed')}</span>
|
|
||||||
<Icon icon={ICONS.COMPLETE} />
|
|
||||||
</React.Fragment>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!invitee.invite_reward_claimed &&
|
|
||||||
(invitee.invite_reward_claimable ? <span>{__('Claimable')}</span> : __('Unclaimable'))}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default InviteList;
|
|
|
@ -1,29 +0,0 @@
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import {
|
|
||||||
selectUserInvitesRemaining,
|
|
||||||
selectUserInviteNewIsPending,
|
|
||||||
selectUserInviteNewErrorMessage,
|
|
||||||
selectUserInviteReferralLink,
|
|
||||||
selectUserInviteReferralCode,
|
|
||||||
} from 'redux/selectors/user';
|
|
||||||
// import { doUserInviteNew } from 'redux/actions/user';
|
|
||||||
import { selectMyChannelClaims, selectFetchingMyChannels } from 'redux/selectors/claims';
|
|
||||||
import { doFetchChannelListMine } from 'redux/actions/claims';
|
|
||||||
import InviteNew from './view';
|
|
||||||
|
|
||||||
const select = (state) => ({
|
|
||||||
errorMessage: selectUserInviteNewErrorMessage(state),
|
|
||||||
invitesRemaining: selectUserInvitesRemaining(state),
|
|
||||||
referralLink: selectUserInviteReferralLink(state),
|
|
||||||
referralCode: selectUserInviteReferralCode(state),
|
|
||||||
isPending: selectUserInviteNewIsPending(state),
|
|
||||||
channels: selectMyChannelClaims(state),
|
|
||||||
fetchingChannels: selectFetchingMyChannels(state),
|
|
||||||
});
|
|
||||||
|
|
||||||
const perform = (dispatch) => ({
|
|
||||||
// inviteNew: (email) => dispatch(doUserInviteNew(email)),
|
|
||||||
fetchChannelListMine: () => dispatch(doFetchChannelListMine()),
|
|
||||||
});
|
|
||||||
|
|
||||||
export default connect(select, perform)(InviteNew);
|
|
|
@ -1,154 +0,0 @@
|
||||||
// @flow
|
|
||||||
import { URL, SITE_NAME } from 'config';
|
|
||||||
import React, { useEffect, useState } from 'react';
|
|
||||||
import Button from 'component/button';
|
|
||||||
import { Form, FormField } from 'component/common/form';
|
|
||||||
import CopyableText from 'component/copyableText';
|
|
||||||
import Card from 'component/common/card';
|
|
||||||
import analytics from 'analytics';
|
|
||||||
import I18nMessage from 'component/i18nMessage';
|
|
||||||
import LbcSymbol from 'component/common/lbc-symbol';
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
errorMessage: ?string,
|
|
||||||
inviteNew: (string) => void,
|
|
||||||
isPending: boolean,
|
|
||||||
referralLink: string,
|
|
||||||
referralCode: string,
|
|
||||||
channels: ?Array<ChannelClaim>,
|
|
||||||
};
|
|
||||||
|
|
||||||
function InviteNew(props: Props) {
|
|
||||||
const { inviteNew, errorMessage, isPending, referralCode = '', channels } = props;
|
|
||||||
|
|
||||||
// Email
|
|
||||||
const [email, setEmail] = useState('');
|
|
||||||
function handleSubmit() {
|
|
||||||
inviteNew(email);
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleEmailChanged(event: any) {
|
|
||||||
setEmail(event.target.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Referral link
|
|
||||||
const [referralSource, setReferralSource] = useState(referralCode);
|
|
||||||
|
|
||||||
const handleReferralChange = React.useCallback(
|
|
||||||
(code) => {
|
|
||||||
setReferralSource(code);
|
|
||||||
// TODO: keep track of this in an array?
|
|
||||||
const matchingChannel = channels && channels.find((ch) => ch.name === code);
|
|
||||||
if (matchingChannel) {
|
|
||||||
analytics.apiLogPublish(matchingChannel);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[setReferralSource]
|
|
||||||
);
|
|
||||||
|
|
||||||
const topChannel =
|
|
||||||
channels &&
|
|
||||||
channels.reduce((top, channel) => {
|
|
||||||
const topClaimCount = (top && top.meta && top.meta.claims_in_channel) || 0;
|
|
||||||
const currentClaimCount = (channel && channel.meta && channel.meta.claims_in_channel) || 0;
|
|
||||||
return topClaimCount >= currentClaimCount ? top : channel;
|
|
||||||
});
|
|
||||||
const referralString =
|
|
||||||
channels && channels.length && referralSource !== referralCode
|
|
||||||
? lookupUrlByClaimName(referralSource, channels)
|
|
||||||
: referralSource;
|
|
||||||
|
|
||||||
const referral = `${URL}/$/invite/${referralString.replace('#', ':')}`;
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// set default channel
|
|
||||||
if (topChannel) {
|
|
||||||
handleReferralChange(topChannel.name);
|
|
||||||
}
|
|
||||||
}, [topChannel, handleReferralChange]);
|
|
||||||
|
|
||||||
function lookupUrlByClaimName(name, channels) {
|
|
||||||
const claim = channels.find((channel) => channel.name === name);
|
|
||||||
return claim && claim.canonical_url ? claim.canonical_url.replace('lbry://', '') : name;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={'columns'}>
|
|
||||||
<div className="column">
|
|
||||||
<Card
|
|
||||||
title={__('Invites')}
|
|
||||||
subtitle={
|
|
||||||
<I18nMessage tokens={{ SITE_NAME, lbc: <LbcSymbol /> }}>
|
|
||||||
Earn %lbc% for inviting subscribers, followers, fans, friends, etc. to join and follow you on %SITE_NAME%.
|
|
||||||
You can use invites just like affiliate links.
|
|
||||||
</I18nMessage>
|
|
||||||
}
|
|
||||||
actions={
|
|
||||||
<React.Fragment>
|
|
||||||
<CopyableText label={__('Your invite link')} copyable={referral} />
|
|
||||||
{channels && channels.length > 0 && (
|
|
||||||
<FormField
|
|
||||||
type="select"
|
|
||||||
label={__('Customize link')}
|
|
||||||
value={referralSource}
|
|
||||||
onChange={(e) => handleReferralChange(e.target.value)}
|
|
||||||
>
|
|
||||||
{channels.map((channel) => (
|
|
||||||
<option key={channel.claim_id} value={channel.name}>
|
|
||||||
{channel.name}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
<option value={referralCode}>{referralCode}</option>
|
|
||||||
</FormField>
|
|
||||||
)}
|
|
||||||
</React.Fragment>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="column">
|
|
||||||
<Card
|
|
||||||
title={__('Invite by email')}
|
|
||||||
subtitle={
|
|
||||||
<I18nMessage tokens={{ SITE_NAME, lbc: <LbcSymbol /> }}>
|
|
||||||
Invite someone you know by email and earn %lbc% when they join %SITE_NAME%.
|
|
||||||
</I18nMessage>
|
|
||||||
}
|
|
||||||
actions={
|
|
||||||
<React.Fragment>
|
|
||||||
<Form onSubmit={handleSubmit}>
|
|
||||||
<FormField
|
|
||||||
type="text"
|
|
||||||
label={__('Email')}
|
|
||||||
placeholder="youremail@example.org"
|
|
||||||
name="email"
|
|
||||||
value={email}
|
|
||||||
error={errorMessage}
|
|
||||||
inputButton={
|
|
||||||
<Button button="secondary" type="submit" label={__('Invite')} disabled={isPending || !email} />
|
|
||||||
}
|
|
||||||
onChange={(event) => {
|
|
||||||
handleEmailChanged(event);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<p className="help">
|
|
||||||
<I18nMessage
|
|
||||||
tokens={{
|
|
||||||
rewards_link: <Button button="link" navigate="/$/rewards" label={__('rewards')} />,
|
|
||||||
referral_faq_link: (
|
|
||||||
<Button button="link" label={__('FAQ')} href="https://lbry.com/faq/referrals" />
|
|
||||||
),
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Read our %referral_faq_link% to learn more about rewards.
|
|
||||||
</I18nMessage>
|
|
||||||
</p>
|
|
||||||
</Form>
|
|
||||||
</React.Fragment>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default InviteNew;
|
|
|
@ -1,30 +0,0 @@
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { withRouter } from 'react-router';
|
|
||||||
import REWARDS from 'rewards';
|
|
||||||
import { selectUser, selectSetReferrerPending, selectSetReferrerError } from 'redux/selectors/user';
|
|
||||||
import { doClaimRewardType } from 'redux/actions/rewards';
|
|
||||||
import { selectUnclaimedRewards } from 'redux/selectors/rewards';
|
|
||||||
import { doUserSetReferrer } from 'redux/actions/user';
|
|
||||||
import { selectIsSubscribedForUri } from 'redux/selectors/subscriptions';
|
|
||||||
import { doChannelSubscribe } from 'redux/actions/subscriptions';
|
|
||||||
import Invited from './view';
|
|
||||||
|
|
||||||
const select = (state, props) => {
|
|
||||||
return {
|
|
||||||
user: selectUser(state),
|
|
||||||
referrerSetPending: selectSetReferrerPending(state),
|
|
||||||
referrerSetError: selectSetReferrerError(state),
|
|
||||||
rewards: selectUnclaimedRewards(state),
|
|
||||||
isSubscribed: selectIsSubscribedForUri(state, props.fullUri),
|
|
||||||
fullUri: props.fullUri,
|
|
||||||
referrer: props.referrer,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const perform = (dispatch) => ({
|
|
||||||
claimReward: () => dispatch(doClaimRewardType(REWARDS.TYPE_REFEREE)),
|
|
||||||
setReferrer: (referrer) => dispatch(doUserSetReferrer(referrer)),
|
|
||||||
channelSubscribe: (uri) => dispatch(doChannelSubscribe(uri)),
|
|
||||||
});
|
|
||||||
|
|
||||||
export default withRouter(connect(select, perform)(Invited));
|
|
|
@ -1,225 +0,0 @@
|
||||||
// @flow
|
|
||||||
import { SITE_NAME } from 'config';
|
|
||||||
import * as PAGES from 'constants/pages';
|
|
||||||
import React, { useEffect } from 'react';
|
|
||||||
import Button from 'component/button';
|
|
||||||
import ClaimPreview from 'component/claimPreview';
|
|
||||||
import Card from 'component/common/card';
|
|
||||||
import { buildURI, parseURI } from 'util/lbryURI';
|
|
||||||
import { ERRORS } from 'lbryinc';
|
|
||||||
import REWARDS from 'rewards';
|
|
||||||
import { formatLbryUrlForWeb } from 'util/url';
|
|
||||||
import ChannelContent from 'component/channelContent';
|
|
||||||
import I18nMessage from 'component/i18nMessage';
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
user: any,
|
|
||||||
claimReward: () => void,
|
|
||||||
setReferrer: (string) => void,
|
|
||||||
referrerSetPending: boolean,
|
|
||||||
referrerSetError: string,
|
|
||||||
channelSubscribe: (sub: Subscription) => void,
|
|
||||||
history: { push: (string) => void },
|
|
||||||
rewards: Array<Reward>,
|
|
||||||
referrer: string,
|
|
||||||
fullUri: string,
|
|
||||||
isSubscribed: boolean,
|
|
||||||
};
|
|
||||||
|
|
||||||
function Invited(props: Props) {
|
|
||||||
const {
|
|
||||||
user,
|
|
||||||
claimReward,
|
|
||||||
setReferrer,
|
|
||||||
referrerSetPending,
|
|
||||||
referrerSetError,
|
|
||||||
channelSubscribe,
|
|
||||||
history,
|
|
||||||
rewards,
|
|
||||||
fullUri,
|
|
||||||
referrer,
|
|
||||||
isSubscribed,
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
const refUri = referrer && 'lbry://' + referrer.replace(':', '#');
|
|
||||||
const {
|
|
||||||
isChannel: referrerIsChannel,
|
|
||||||
claimName: referrerChannelName,
|
|
||||||
channelClaimId: referrerChannelClaimId,
|
|
||||||
} = parseURI(refUri);
|
|
||||||
const channelUri =
|
|
||||||
referrerIsChannel &&
|
|
||||||
formatLbryUrlForWeb(buildURI({ channelName: referrerChannelName, channelClaimId: referrerChannelClaimId }));
|
|
||||||
const rewardsApproved = user && user.is_reward_approved;
|
|
||||||
const hasVerifiedEmail = user && user.has_verified_email;
|
|
||||||
const referredRewardAvailable = rewards && rewards.some((reward) => reward.reward_type === REWARDS.TYPE_REFEREE);
|
|
||||||
const redirect = channelUri || `/`;
|
|
||||||
|
|
||||||
// always follow if it's a channel
|
|
||||||
useEffect(() => {
|
|
||||||
if (fullUri && !isSubscribed && fullUri) {
|
|
||||||
let channelName;
|
|
||||||
try {
|
|
||||||
const { claimName } = parseURI(fullUri);
|
|
||||||
channelName = claimName;
|
|
||||||
} catch (e) {}
|
|
||||||
if (channelName) {
|
|
||||||
channelSubscribe({
|
|
||||||
channelName: channelName,
|
|
||||||
uri: fullUri,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [fullUri, isSubscribed, channelSubscribe]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!referrerSetPending && hasVerifiedEmail) {
|
|
||||||
claimReward();
|
|
||||||
}
|
|
||||||
}, [referrerSetPending, hasVerifiedEmail, claimReward]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (referrer) {
|
|
||||||
setReferrer(referrer.replace(':', '#'));
|
|
||||||
}
|
|
||||||
}, [referrer, setReferrer]);
|
|
||||||
|
|
||||||
function handleDone() {
|
|
||||||
history.push(redirect);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (referrerSetError === ERRORS.ALREADY_CLAIMED) {
|
|
||||||
return (
|
|
||||||
<Card
|
|
||||||
title={__(`Whoa`)}
|
|
||||||
subtitle={
|
|
||||||
referrerIsChannel
|
|
||||||
? __(`You've already claimed your referrer, but we've followed this channel for you.`)
|
|
||||||
: __(`You've already claimed your referrer.`)
|
|
||||||
}
|
|
||||||
body={
|
|
||||||
referrerIsChannel && (
|
|
||||||
<div className="claim-preview--channel">
|
|
||||||
<ClaimPreview key={refUri} uri={refUri} actions={''} type={'small'} />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
actions={
|
|
||||||
<div className="card__actions">
|
|
||||||
<Button button="primary" label={__('Done!')} onClick={handleDone} />
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (referrerSetError && referredRewardAvailable) {
|
|
||||||
return (
|
|
||||||
<Card
|
|
||||||
title={__(`Welcome!`)}
|
|
||||||
subtitle={__(
|
|
||||||
`Something went wrong with your invite link. You can set and claim your invite reward after signing in.`
|
|
||||||
)}
|
|
||||||
actions={
|
|
||||||
<>
|
|
||||||
<p className="error__text">{__('Not a valid invite')}</p>
|
|
||||||
<div className="card__actions">
|
|
||||||
<Button
|
|
||||||
button="primary"
|
|
||||||
label={hasVerifiedEmail ? __('Verify') : __('Create Account')}
|
|
||||||
navigate={`/$/${PAGES.AUTH}?redirect=/$/${PAGES.REWARDS}`}
|
|
||||||
/>
|
|
||||||
<Button button="link" label={__('Explore')} onClick={handleDone} />
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!rewardsApproved) {
|
|
||||||
const signUpButton = (
|
|
||||||
<Button
|
|
||||||
button="link"
|
|
||||||
label={hasVerifiedEmail ? __(`Finish verification `) : __(`Create an account `)}
|
|
||||||
navigate={`/$/${PAGES.AUTH}?redirect=/$/${PAGES.INVITE}/${referrer}`}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card
|
|
||||||
title={
|
|
||||||
referrerIsChannel
|
|
||||||
? __('%channel_name% invites you to the party!', { channel_name: referrerChannelName })
|
|
||||||
: __(`You're invited!`)
|
|
||||||
}
|
|
||||||
subtitle={
|
|
||||||
<p>
|
|
||||||
{referrerIsChannel ? (
|
|
||||||
<I18nMessage
|
|
||||||
tokens={{
|
|
||||||
channel_name: referrerChannelName,
|
|
||||||
signup_link: signUpButton,
|
|
||||||
SITE_NAME,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
%channel_name% is waiting for you on %SITE_NAME%. Create your account now.
|
|
||||||
</I18nMessage>
|
|
||||||
) : (
|
|
||||||
<I18nMessage
|
|
||||||
tokens={{
|
|
||||||
signup_link: signUpButton,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Content freedom and a present are waiting for you. %signup_link% to claim it.
|
|
||||||
</I18nMessage>
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
}
|
|
||||||
body={
|
|
||||||
referrerIsChannel && (
|
|
||||||
<div className="claim-preview--channel">
|
|
||||||
<div className="section">
|
|
||||||
<ClaimPreview key={refUri} uri={refUri} actions={''} type={'small'} />
|
|
||||||
</div>
|
|
||||||
<div className="section">
|
|
||||||
<ChannelContent uri={fullUri} defaultPageSize={3} defaultInfiniteScroll={false} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
actions={
|
|
||||||
<div className="section__actions">
|
|
||||||
<Button
|
|
||||||
button="primary"
|
|
||||||
label={hasVerifiedEmail ? __('Finish Account') : __('Create Account')}
|
|
||||||
navigate={`/$/${PAGES.AUTH}?redirect=/$/${PAGES.INVITE}/${referrer}`}
|
|
||||||
/>
|
|
||||||
<Button button="link" label={__('Skip')} onClick={handleDone} />
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card
|
|
||||||
title={__(`Welcome!`)}
|
|
||||||
subtitle={referrerIsChannel ? __(`We've followed your invitee for you. Check them out!`) : __(`Congrats!`)}
|
|
||||||
body={
|
|
||||||
referrerIsChannel && (
|
|
||||||
<div className="claim-preview--channel">
|
|
||||||
<ClaimPreview key={refUri} uri={refUri} actions={''} type={'small'} />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
actions={
|
|
||||||
<div className="section__actions">
|
|
||||||
<Button button="primary" label={__('Done')} onClick={handleDone} />
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Invited;
|
|
|
@ -1,3 +1,11 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { selectRemoteVersion } from 'redux/selectors/app';
|
||||||
import LastReleaseChanges from './view';
|
import LastReleaseChanges from './view';
|
||||||
|
|
||||||
export default LastReleaseChanges;
|
const select = (state) => ({
|
||||||
|
releaseVersion: selectRemoteVersion(state),
|
||||||
|
});
|
||||||
|
|
||||||
|
const perform = {};
|
||||||
|
|
||||||
|
export default connect(select, perform)(LastReleaseChanges);
|
||||||
|
|
|
@ -5,11 +5,12 @@ import Button from 'component/button';
|
||||||
import I18nMessage from 'component/i18nMessage';
|
import I18nMessage from 'component/i18nMessage';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
releaseVersion: string,
|
||||||
hideReleaseVersion?: boolean,
|
hideReleaseVersion?: boolean,
|
||||||
};
|
};
|
||||||
|
|
||||||
const LastReleaseChanges = (props: Props) => {
|
const LastReleaseChanges = (props: Props) => {
|
||||||
const { hideReleaseVersion } = props;
|
const { hideReleaseVersion, releaseVersion } = props;
|
||||||
const [releaseTag, setReleaseTag] = useState('');
|
const [releaseTag, setReleaseTag] = useState('');
|
||||||
const [releaseChanges, setReleaseChanges] = useState('');
|
const [releaseChanges, setReleaseChanges] = useState('');
|
||||||
const [fetchingReleaseChanges, setFetchingReleaseChanges] = useState(false);
|
const [fetchingReleaseChanges, setFetchingReleaseChanges] = useState(false);
|
||||||
|
@ -35,7 +36,7 @@ const LastReleaseChanges = (props: Props) => {
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const lastReleaseUrl = 'https://api.github.com/repos/lbryio/lbry-desktop/releases/latest';
|
const lastReleaseUrl = `https://api.github.com/repos/lbryio/lbry-desktop/releases/tags/${releaseVersion}`;
|
||||||
const options = {
|
const options = {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: { Accept: 'application/vnd.github.v3+json' },
|
headers: { Accept: 'application/vnd.github.v3+json' },
|
||||||
|
@ -54,7 +55,7 @@ const LastReleaseChanges = (props: Props) => {
|
||||||
setFetchingReleaseChanges(false);
|
setFetchingReleaseChanges(false);
|
||||||
setFetchReleaseFailed(true);
|
setFetchReleaseFailed(true);
|
||||||
});
|
});
|
||||||
}, []);
|
}, [releaseVersion, setFetchingReleaseChanges, setReleaseTag, setReleaseChanges, setFetchReleaseFailed]);
|
||||||
|
|
||||||
if (fetchingReleaseChanges) {
|
if (fetchingReleaseChanges) {
|
||||||
return <p>{__('Loading...')}</p>;
|
return <p>{__('Loading...')}</p>;
|
||||||
|
|
|
@ -92,7 +92,7 @@ function Page(props: Props) {
|
||||||
<div
|
<div
|
||||||
className={classnames('main-wrapper__inner', {
|
className={classnames('main-wrapper__inner', {
|
||||||
'main-wrapper__inner--filepage': isOnFilePage,
|
'main-wrapper__inner--filepage': isOnFilePage,
|
||||||
'main-wrapper__inner--theater-mode': isOnFilePage && videoTheaterMode,
|
'main-wrapper__inner--theater-mode': isOnFilePage && videoTheaterMode && !isMediumScreen,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{!authPage &&
|
{!authPage &&
|
||||||
|
@ -124,7 +124,7 @@ function Page(props: Props) {
|
||||||
'main--file-page': filePage,
|
'main--file-page': filePage,
|
||||||
'main--settings-page': settingsPage,
|
'main--settings-page': settingsPage,
|
||||||
'main--markdown': isMarkdown,
|
'main--markdown': isMarkdown,
|
||||||
'main--theater-mode': isOnFilePage && videoTheaterMode && !isMarkdown,
|
'main--theater-mode': isOnFilePage && videoTheaterMode && !isMediumScreen && !isMarkdown,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
// @flow
|
// @flow
|
||||||
import React, { useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import { FormField } from 'component/common/form';
|
import { FormFieldAreaAdvanced } from 'component/common/form';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
uri: ?string,
|
uri: ?string,
|
||||||
label: ?string,
|
label: ?string,
|
||||||
disabled: ?boolean,
|
disabled: ?boolean,
|
||||||
filePath: string | WebFile,
|
filePath: File,
|
||||||
fileText: ?string,
|
fileText: ?string,
|
||||||
fileMimeType: ?string,
|
fileMimeType: ?string,
|
||||||
streamingUrl: ?string,
|
streamingUrl: ?string,
|
||||||
|
@ -99,7 +99,7 @@ function PostEditor(props: Props) {
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormField
|
<FormFieldAreaAdvanced
|
||||||
type={'markdown'}
|
type={'markdown'}
|
||||||
name="content_post"
|
name="content_post"
|
||||||
label={label}
|
label={label}
|
||||||
|
|
|
@ -72,9 +72,9 @@ function PrivacyAgreement(props: Props) {
|
||||||
{__('No')} <span>😢</span>
|
{__('No')} <span>😢</span>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
helper={__(`* Note that as
|
helper={__(
|
||||||
peer-to-peer software, your IP address and potentially other system information can be sent to other
|
`* Note that as peer-to-peer software, your IP address and potentially other system information can be sent to other users, though this information is not stored permanently.`
|
||||||
users, though this information is not stored permanently.`)}
|
)}
|
||||||
onChange={(e) => setShare(NONE)}
|
onChange={(e) => setShare(NONE)}
|
||||||
/>
|
/>
|
||||||
{authenticated && (
|
{authenticated && (
|
||||||
|
@ -92,7 +92,7 @@ function PrivacyAgreement(props: Props) {
|
||||||
)}
|
)}
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<div className={'card__actions'}>
|
<div className={'card__actions'}>
|
||||||
<Button button="primary" label={__(`Let's go`)} disabled={!share} type="submit" />
|
<Button button="primary" label={__(`Next`)} disabled={!share} type="submit" />
|
||||||
</div>
|
</div>
|
||||||
{share === NONE && (
|
{share === NONE && (
|
||||||
<p className="help">
|
<p className="help">
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
import { formatCredits } from 'util/format-credits';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
uri: ?string,
|
uri: ?string,
|
||||||
isResolvingUri: boolean,
|
isResolvingUri: boolean,
|
||||||
|
@ -23,7 +25,7 @@ function BidHelpText(props: Props) {
|
||||||
bidHelpText = __(
|
bidHelpText = __(
|
||||||
'If you bid more than %amount% LBRY Credits, when someone navigates to %uri%, it will load your published content. However, you can get a longer version of this URL for any bid.',
|
'If you bid more than %amount% LBRY Credits, when someone navigates to %uri%, it will load your published content. However, you can get a longer version of this URL for any bid.',
|
||||||
{
|
{
|
||||||
amount: amountNeededForTakeover,
|
amount: formatCredits(amountNeededForTakeover, 2, true),
|
||||||
uri: uri,
|
uri: uri,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
// @flow
|
// @flow
|
||||||
import { FF_MAX_CHARS_IN_DESCRIPTION } from 'constants/form-field';
|
import { FF_MAX_CHARS_IN_DESCRIPTION } from 'constants/form-field';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { FormField } from 'component/common/form';
|
import { FormFieldAreaAdvanced } from 'component/common/form';
|
||||||
import usePersistedState from 'effects/use-persisted-state';
|
import usePersistedState from 'effects/use-persisted-state';
|
||||||
import Card from 'component/common/card';
|
import Card from 'component/common/card';
|
||||||
|
|
||||||
|
@ -27,7 +27,7 @@ function PublishDescription(props: Props) {
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
actions={
|
actions={
|
||||||
<FormField
|
<FormFieldAreaAdvanced
|
||||||
type={advancedEditor ? 'markdown' : 'textarea'}
|
type={advancedEditor ? 'markdown' : 'textarea'}
|
||||||
name="content_description"
|
name="content_description"
|
||||||
label={__('Description')}
|
label={__('Description')}
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
import type { Node } from 'react';
|
import type { Node } from 'react';
|
||||||
import * as ICONS from 'constants/icons';
|
import * as ICONS from 'constants/icons';
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { ipcRenderer } from 'electron';
|
||||||
import { regexInvalidURI } from 'util/lbryURI';
|
import { regexInvalidURI } from 'util/lbryURI';
|
||||||
import PostEditor from 'component/postEditor';
|
import PostEditor from 'component/postEditor';
|
||||||
import FileSelector from 'component/common/file-selector';
|
import FileSelector from 'component/common/file-selector';
|
||||||
|
@ -13,13 +14,13 @@ import I18nMessage from 'component/i18nMessage';
|
||||||
import usePersistedState from 'effects/use-persisted-state';
|
import usePersistedState from 'effects/use-persisted-state';
|
||||||
import * as PUBLISH_MODES from 'constants/publish_types';
|
import * as PUBLISH_MODES from 'constants/publish_types';
|
||||||
import PublishName from 'component/publishName';
|
import PublishName from 'component/publishName';
|
||||||
|
import path from 'path';
|
||||||
type Props = {
|
type Props = {
|
||||||
uri: ?string,
|
uri: ?string,
|
||||||
mode: ?string,
|
mode: ?string,
|
||||||
name: ?string,
|
name: ?string,
|
||||||
title: ?string,
|
title: ?string,
|
||||||
filePath: string | WebFile,
|
filePath: ?string,
|
||||||
fileMimeType: ?string,
|
fileMimeType: ?string,
|
||||||
isStillEditing: boolean,
|
isStillEditing: boolean,
|
||||||
balance: number,
|
balance: number,
|
||||||
|
@ -77,7 +78,7 @@ function PublishFile(props: Props) {
|
||||||
const sizeInMB = Number(size) / 1000000;
|
const sizeInMB = Number(size) / 1000000;
|
||||||
const secondsToProcess = sizeInMB / PROCESSING_MB_PER_SECOND;
|
const secondsToProcess = sizeInMB / PROCESSING_MB_PER_SECOND;
|
||||||
const ffmpegAvail = ffmpegStatus.available;
|
const ffmpegAvail = ffmpegStatus.available;
|
||||||
const [currentFile, setCurrentFile] = useState(null);
|
const currentFile = filePath;
|
||||||
const [currentFileType, setCurrentFileType] = useState(null);
|
const [currentFileType, setCurrentFileType] = useState(null);
|
||||||
const [optimizeAvail, setOptimizeAvail] = useState(false);
|
const [optimizeAvail, setOptimizeAvail] = useState(false);
|
||||||
const [userOptimize, setUserOptimize] = usePersistedState('publish-file-user-optimize', false);
|
const [userOptimize, setUserOptimize] = usePersistedState('publish-file-user-optimize', false);
|
||||||
|
@ -91,17 +92,35 @@ function PublishFile(props: Props) {
|
||||||
}
|
}
|
||||||
}, [currentFileType, mode, isStillEditing, updatePublishForm]);
|
}, [currentFileType, mode, isStillEditing, updatePublishForm]);
|
||||||
|
|
||||||
|
// Since the filePath can be updated from outside this component
|
||||||
|
// (for instance, when the user drags & drops a file), we need
|
||||||
|
// to check for changes in the selected file using an effect.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!filePath || filePath === '') {
|
if (!filePath) {
|
||||||
setCurrentFile('');
|
return;
|
||||||
updateFileInfo(0, 0, false);
|
|
||||||
} else if (typeof filePath !== 'string') {
|
|
||||||
// Update currentFile file
|
|
||||||
if (filePath.name !== currentFile && filePath.path !== currentFile) {
|
|
||||||
handleFileChange(filePath);
|
|
||||||
}
|
}
|
||||||
|
async function readSelectedFileDetails() {
|
||||||
|
// Read the file to get the file's duration (if possible)
|
||||||
|
// and offer transcoding it.
|
||||||
|
const result = await ipcRenderer.invoke('get-file-details-from-path', filePath);
|
||||||
|
let file;
|
||||||
|
if (result.buffer) {
|
||||||
|
file = new File([result.buffer], result.name, {
|
||||||
|
type: result.mime,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}, [filePath, currentFile, handleFileChange, updateFileInfo]);
|
const fileData: FileData = {
|
||||||
|
path: result.path,
|
||||||
|
name: result.name,
|
||||||
|
mimeType: result.mime || 'application/octet-stream',
|
||||||
|
size: result.size,
|
||||||
|
duration: result.duration,
|
||||||
|
file: file,
|
||||||
|
};
|
||||||
|
processSelectedFile(fileData);
|
||||||
|
}
|
||||||
|
readSelectedFileDetails();
|
||||||
|
}, [filePath]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const isOptimizeAvail = currentFile && currentFile !== '' && isVid && ffmpegAvail;
|
const isOptimizeAvail = currentFile && currentFile !== '' && isVid && ffmpegAvail;
|
||||||
|
@ -209,11 +228,11 @@ function PublishFile(props: Props) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleFileChange(file: WebFile, clearName = true) {
|
function processSelectedFile(fileData: FileData, clearName = true) {
|
||||||
window.URL = window.URL || window.webkitURL;
|
window.URL = window.URL || window.webkitURL;
|
||||||
|
|
||||||
// select file, start to select a new one, then cancel
|
// select file, start to select a new one, then cancel
|
||||||
if (!file) {
|
if (!fileData || fileData.error) {
|
||||||
if (isStillEditing || !clearName) {
|
if (isStillEditing || !clearName) {
|
||||||
updatePublishForm({ filePath: '' });
|
updatePublishForm({ filePath: '' });
|
||||||
} else {
|
} else {
|
||||||
|
@ -222,8 +241,12 @@ function PublishFile(props: Props) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// if video, extract duration so we can warn about bitrateif (typeof file !== 'string') {
|
// if video, extract duration so we can warn about bitrate if (typeof file !== 'string')
|
||||||
const contentType = file.type && file.type.split('/');
|
const file = fileData.file;
|
||||||
|
// Check to see if it's a video and if mp4
|
||||||
|
const contentType = fileData.mimeType && fileData.mimeType.split('/'); // get this from electron side
|
||||||
|
const duration = fileData.duration;
|
||||||
|
const size = fileData.size;
|
||||||
const isVideo = contentType && contentType[0] === 'video';
|
const isVideo = contentType && contentType[0] === 'video';
|
||||||
const isMp4 = contentType && contentType[1] === 'mp4';
|
const isMp4 = contentType && contentType[1] === 'mp4';
|
||||||
|
|
||||||
|
@ -231,34 +254,25 @@ function PublishFile(props: Props) {
|
||||||
|
|
||||||
if (contentType && contentType[0] === 'text') {
|
if (contentType && contentType[0] === 'text') {
|
||||||
isTextPost = contentType[1] === 'plain' || contentType[1] === 'markdown';
|
isTextPost = contentType[1] === 'plain' || contentType[1] === 'markdown';
|
||||||
setCurrentFileType(contentType);
|
setCurrentFileType(contentType.join('/'));
|
||||||
} else if (file.name) {
|
} else if (path.parse(fileData.path).ext) {
|
||||||
// If user's machine is missign a valid content type registration
|
// If user's machine is missing a valid content type registration
|
||||||
// for markdown content: text/markdown, file extension will be used instead
|
// for markdown content: text/markdown, file extension will be used instead
|
||||||
const extension = file.name.split('.').pop();
|
const extension = path.parse(fileData.path).ext;
|
||||||
isTextPost = MARKDOWN_FILE_EXTENSIONS.includes(extension);
|
isTextPost = MARKDOWN_FILE_EXTENSIONS.includes(extension);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isVideo) {
|
if (isVideo) {
|
||||||
if (isMp4) {
|
if (isMp4) {
|
||||||
const video = document.createElement('video');
|
updateFileInfo(duration || 0, size, isVideo);
|
||||||
video.preload = 'metadata';
|
|
||||||
video.onloadedmetadata = () => {
|
|
||||||
updateFileInfo(video.duration, file.size, isVideo);
|
|
||||||
window.URL.revokeObjectURL(video.src);
|
|
||||||
};
|
|
||||||
video.onerror = () => {
|
|
||||||
updateFileInfo(0, file.size, isVideo);
|
|
||||||
};
|
|
||||||
video.src = window.URL.createObjectURL(file);
|
|
||||||
} else {
|
} else {
|
||||||
updateFileInfo(0, file.size, isVideo);
|
updateFileInfo(duration || 0, size, isVideo);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
updateFileInfo(0, file.size, isVideo);
|
updateFileInfo(0, size, isVideo);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isTextPost) {
|
if (isTextPost && file) {
|
||||||
// Create reader
|
// Create reader
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
// Handler for file reader
|
// Handler for file reader
|
||||||
|
@ -270,21 +284,17 @@ function PublishFile(props: Props) {
|
||||||
setPublishMode(PUBLISH_MODES.FILE);
|
setPublishMode(PUBLISH_MODES.FILE);
|
||||||
}
|
}
|
||||||
|
|
||||||
const publishFormParams: { filePath: string | WebFile, name?: string, optimize?: boolean } = {
|
// Strip off extension and replace invalid characters
|
||||||
// if electron, we'll set filePath to the path string because SDK is handling publishing.
|
|
||||||
// File.path will be undefined from web due to browser security, so it will default to the File Object.
|
|
||||||
filePath: file.path || file,
|
|
||||||
};
|
|
||||||
// Strip off extention and replace invalid characters
|
|
||||||
let fileName = name || (file.name && file.name.substring(0, file.name.lastIndexOf('.'))) || '';
|
|
||||||
|
|
||||||
if (!isStillEditing) {
|
if (!isStillEditing) {
|
||||||
publishFormParams.name = parseName(fileName);
|
const fileWithoutExtension = path.parse(fileData.path).name;
|
||||||
|
updatePublishForm({ name: parseName(fileWithoutExtension) });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// File path is not supported on web for security reasons so we use the name instead.
|
function handleFileChange(fileWithPath: FileWithPath) {
|
||||||
setCurrentFile(file.path || file.name);
|
if (fileWithPath) {
|
||||||
updatePublishForm(publishFormParams);
|
updatePublishForm({ filePath: fileWithPath.path });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const showFileUpload = mode === PUBLISH_MODES.FILE;
|
const showFileUpload = mode === PUBLISH_MODES.FILE;
|
||||||
|
@ -325,12 +335,14 @@ function PublishFile(props: Props) {
|
||||||
{showFileUpload && (
|
{showFileUpload && (
|
||||||
<>
|
<>
|
||||||
<FileSelector
|
<FileSelector
|
||||||
|
type="openFile"
|
||||||
label={__('File')}
|
label={__('File')}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
currentPath={currentFile}
|
currentPath={currentFile}
|
||||||
onFileChosen={handleFileChange}
|
onFileChosen={handleFileChange}
|
||||||
// https://stackoverflow.com/questions/19107685/safari-input-type-file-accept-video-ignores-mp4-files
|
// https://stackoverflow.com/questions/19107685/safari-input-type-file-accept-video-ignores-mp4-files
|
||||||
placeholder={__('Select file to upload')}
|
placeholder={__('Select file to upload')}
|
||||||
|
readFile={false}
|
||||||
/>
|
/>
|
||||||
{getUploadMessage()}
|
{getUploadMessage()}
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -35,8 +35,8 @@ import tempy from 'tempy';
|
||||||
type Props = {
|
type Props = {
|
||||||
disabled: boolean,
|
disabled: boolean,
|
||||||
tags: Array<Tag>,
|
tags: Array<Tag>,
|
||||||
publish: (source?: string | File, ?boolean) => void,
|
publish: (source: ?File, ?boolean) => void,
|
||||||
filePath: string | File,
|
filePath: ?File,
|
||||||
fileText: string,
|
fileText: string,
|
||||||
bid: ?number,
|
bid: ?number,
|
||||||
bidError: ?string,
|
bidError: ?string,
|
||||||
|
@ -208,7 +208,6 @@ function PublishForm(props: Props) {
|
||||||
isNameValid(name) &&
|
isNameValid(name) &&
|
||||||
title &&
|
title &&
|
||||||
bid &&
|
bid &&
|
||||||
thumbnail &&
|
|
||||||
!bidError &&
|
!bidError &&
|
||||||
!emptyPostError &&
|
!emptyPostError &&
|
||||||
!(thumbnailError && !thumbnailUploaded) &&
|
!(thumbnailError && !thumbnailUploaded) &&
|
||||||
|
@ -373,9 +372,6 @@ function PublishForm(props: Props) {
|
||||||
if (!output || output === '') {
|
if (!output || output === '') {
|
||||||
// Generate a temporary file:
|
// Generate a temporary file:
|
||||||
output = tempy.file({ name: 'post.md' });
|
output = tempy.file({ name: 'post.md' });
|
||||||
} else if (typeof filePath === 'string') {
|
|
||||||
// Use current file
|
|
||||||
output = filePath;
|
|
||||||
}
|
}
|
||||||
// Create a temporary file and save file changes
|
// Create a temporary file and save file changes
|
||||||
if (output && output !== '') {
|
if (output && output !== '') {
|
||||||
|
@ -447,7 +443,7 @@ function PublishForm(props: Props) {
|
||||||
// with other properties such as name, title, etc.) for security reasons.
|
// with other properties such as name, title, etc.) for security reasons.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (mode === PUBLISH_MODES.FILE) {
|
if (mode === PUBLISH_MODES.FILE) {
|
||||||
updatePublishForm({ filePath: '', fileDur: 0, fileSize: 0 });
|
updatePublishForm({ filePath: undefined, fileDur: 0, fileSize: 0 });
|
||||||
}
|
}
|
||||||
}, [mode, updatePublishForm]);
|
}, [mode, updatePublishForm]);
|
||||||
|
|
||||||
|
|
|
@ -47,11 +47,7 @@ function PublishFormErrors(props: Props) {
|
||||||
{!bid && <div>{__('A deposit amount is required')}</div>}
|
{!bid && <div>{__('A deposit amount is required')}</div>}
|
||||||
{bidError && <div>{__('Please check your deposit amount.')}</div>}
|
{bidError && <div>{__('Please check your deposit amount.')}</div>}
|
||||||
{isUploadingThumbnail && <div>{__('Please wait for thumbnail to finish uploading')}</div>}
|
{isUploadingThumbnail && <div>{__('Please wait for thumbnail to finish uploading')}</div>}
|
||||||
{!isUploadingThumbnail && !thumbnail ? (
|
{thumbnailError && !thumbnailUploaded && <div>{__('Thumbnail is invalid.')}</div>}
|
||||||
<div>{__('A thumbnail is required. Please upload or provide an image URL above.')}</div>
|
|
||||||
) : (
|
|
||||||
thumbnailError && !thumbnailUploaded && <div>{__('Thumbnail is invalid.')}</div>
|
|
||||||
)}
|
|
||||||
{editingURI && !isStillEditing && !filePath && (
|
{editingURI && !isStillEditing && !filePath && (
|
||||||
<div>{__('Please reselect a file after changing the LBRY URL')}</div>
|
<div>{__('Please reselect a file after changing the LBRY URL')}</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -1,9 +0,0 @@
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { selectUnclaimedRewardValue } from 'redux/selectors/rewards';
|
|
||||||
import RewardAuthIntro from './view';
|
|
||||||
|
|
||||||
const select = state => ({
|
|
||||||
totalRewardValue: selectUnclaimedRewardValue(state),
|
|
||||||
});
|
|
||||||
|
|
||||||
export default connect(select, null)(RewardAuthIntro);
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue