Compare commits
176 commits
Author | SHA1 | Date | |
---|---|---|---|
|
d14c9141db | ||
|
06c350c4db | ||
|
c3a9d9d002 | ||
|
aeada6dc74 | ||
|
6ba985fd28 | ||
|
2a0bc85738 | ||
|
523ea284a2 | ||
|
a66d7534c2 | ||
|
89ec07622f | ||
|
7e6ad31392 | ||
|
4ab23f03fc | ||
|
29cea5cc07 | ||
|
8dd7150d67 | ||
|
f1b1523017 | ||
|
88ac250fee | ||
|
0a5e9e87ed | ||
|
20413d79b6 | ||
|
ff9011e6ac | ||
|
802139d0a4 | ||
|
68d307fa50 | ||
|
d3900e39b6 | ||
|
35769dede6 | ||
|
ae1e20d131 | ||
|
051af8b6ad | ||
|
5d77b115f9 | ||
|
7dbeeac112 | ||
|
de062c4aee | ||
|
18a3336714 | ||
|
ebf35a1df8 | ||
|
28e168d5e5 | ||
|
7ad66b99e7 | ||
|
7eb7c1a5ff | ||
|
b88e704e6b | ||
|
8d85af8064 | ||
|
f9d7340729 | ||
|
d57300f785 | ||
|
09baf1d9b9 | ||
|
ce692d38ea | ||
|
b1ca3b0183 | ||
|
a4c34d89e2 | ||
|
d7b9ca3391 | ||
|
55a5c7b051 | ||
|
0e2a9a1033 | ||
|
3fd38be789 | ||
|
329d434c83 | ||
|
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 | ||
|
c11235c70e | ||
|
d8bedba43d | ||
|
befcf9fd55 | ||
|
feb37a17a6 | ||
|
fcff90e78c | ||
|
6acdfc9623 | ||
|
15ad30d509 | ||
|
3980d0f51e | ||
|
43c45c1f62 | ||
|
673ab85bea | ||
|
3932ecf7c0 | ||
|
9386cc678f | ||
|
fe2af64b90 | ||
|
7c2e4eb3e3 | ||
|
0c990ba276 | ||
|
f6fd061dd6 | ||
|
150f280c33 | ||
|
3bd97984c0 | ||
|
c31e40089c | ||
|
465d204e69 | ||
|
6742bc373c | ||
|
75d2464443 | ||
|
6a26771339 | ||
|
5a2e4ae49a | ||
|
0d44213f78 | ||
|
0bb13331d5 | ||
|
6bffc8a993 | ||
|
811bb0cf30 | ||
|
d64da2a8b2 | ||
|
a99f6d4bf2 | ||
|
0101bc0533 | ||
|
83faa7bba0 | ||
|
a18aceed1f | ||
|
2eb0219ad1 | ||
|
c58b3c752e | ||
|
ee98531c00 | ||
|
ee754f0085 | ||
|
3854bf6fd4 | ||
|
3ca62704d5 | ||
|
34d9b05de0 | ||
|
87835451f3 | ||
|
3b7b9e69c7 | ||
|
febbb4f82e | ||
|
e29e982ffb | ||
|
d84746d395 | ||
|
c133d5b53a | ||
|
845f33b4f2 | ||
|
9b26a65be4 | ||
|
2193c61628 | ||
|
aef3eccfbd | ||
|
3034f4ce6c | ||
|
50ae6e2869 | ||
|
21204321c0 | ||
|
daab8a28ed | ||
|
79f05a831f | ||
|
243cd0dffd | ||
|
8e536e7020 | ||
|
b0ab2daf39 | ||
|
3fd3a548ec | ||
|
d1c82c9af0 | ||
|
5ef1baff3b | ||
|
da564452f7 | ||
|
c3d836bca7 | ||
|
745a3ca889 |
400 changed files with 32457 additions and 22484 deletions
|
@ -16,7 +16,7 @@ COMMENT_SERVER_NAME=Odysee
|
|||
SEARCH_SERVER_API=https://lighthouse.odysee.com/search
|
||||
SOCKETY_SERVER_API=wss://sockety.odysee.com/ws
|
||||
THUMBNAIL_CDN_URL=https://image-processor.vanwanet.com/optimize/
|
||||
WELCOME_VERSION=1.1
|
||||
WELCOME_VERSION=1.2
|
||||
|
||||
# STRIPE
|
||||
# STRIPE_PUBLIC_KEY='pk_test_NoL1JWL7i1ipfhVId5KfDZgo'
|
||||
|
@ -45,16 +45,9 @@ CLOUD_CONNECT_SITE_NAME=Odysee
|
|||
TWITTER_ACCOUNT=LBRYcom
|
||||
BRANDED_SITE=odysee
|
||||
|
||||
## IMAGE ASSETS
|
||||
YRBL_HAPPY_IMG_URL=https://cdn.lbryplayer.xyz/api/v3/streams/free/yrbl-happy/7aa50a7e5adaf48691935d55e45d697547392929/839d9a
|
||||
YRBL_SAD_IMG_URL=https://cdn.lbryplayer.xyz/api/v3/streams/free/yrbl-sad/c2d9649633d974e5ffb503925e1f17d951f1bd0f/f262dd
|
||||
#LOGIN_IMG_URL=https://cdn.lbryplayer.xyz/api/v3/streams/free/login/b671946e911c66c5fa7233afb35de2badd9eceb8/0e1d81
|
||||
#LOGO=https://cdn.lbryplayer.xyz/api/v3/streams/free/yrbl-sad/c2d9649633d974e5ffb503925e1f17d951f1bd0f/f262dd
|
||||
#LOGO_TEXT_LIGHT=https://cdn.lbryplayer.xyz/api/v3/streams/free/yrbl-sad/c2d9649633d974e5ffb503925e1f17d951f1bd0f/f262dd
|
||||
#LOGO_TEXT_DARK=https://cdn.lbryplayer.xyz/api/v3/streams/free/yrbl-sad/c2d9649633d974e5ffb503925e1f17d951f1bd0f/f262dd
|
||||
#AVATAR_DEFAULT=
|
||||
#MISSING_THUMB_DEFAULT=
|
||||
#FAVICON=
|
||||
## OLD IMAGE ASSETS
|
||||
#YRBL_HAPPY_IMG_URL=https://player.odysee.com/api/v3/streams/free/yrbl-happy/7aa50a7e5adaf48691935d55e45d697547392929/839d9a
|
||||
#YRBL_SAD_IMG_URL=https://player.odysee.com/api/v3/streams/free/yrbl-sad/c2d9649633d974e5ffb503925e1f17d951f1bd0f/f262dd
|
||||
|
||||
# LOCALE
|
||||
DEFAULT_LANGUAGE=en
|
||||
|
|
92
.env.ody
92
.env.ody
|
@ -1,92 +0,0 @@
|
|||
# Copy this file to .env to make modifications
|
||||
|
||||
# Base config
|
||||
|
||||
WEBPACK_WEB_PORT=9090
|
||||
WEBPACK_ELECTRON_PORT=9091
|
||||
WEB_SERVER_PORT=1337
|
||||
|
||||
WELCOME_VERSION=1.0
|
||||
|
||||
# Custom Site info
|
||||
DOMAIN=lbry.tv
|
||||
URL=https://lbry.tv
|
||||
|
||||
# UI
|
||||
SITE_TITLE=lbry.tv
|
||||
SITE_NAME=local.lbry.tv
|
||||
SITE_DESCRIPTION=Meet LBRY, an open, free, and community-controlled content wonderland.
|
||||
LOGO_TITLE=local.lbry.tv
|
||||
|
||||
##### ODYSEE SETTINGS #######
|
||||
|
||||
MATOMO_URL=https://analytics.lbry.com/
|
||||
MATOMO_ID=4
|
||||
|
||||
# Base config
|
||||
WEBPACK_WEB_PORT=9090
|
||||
WEBPACK_ELECTRON_PORT=9091
|
||||
WEB_SERVER_PORT=1337
|
||||
|
||||
## APIS
|
||||
LBRY_API_URL=https://api.odysee.com
|
||||
#LBRY_WEB_API=https://api.na-backend.odysee.com
|
||||
#LBRY_WEB_STREAMING_API=https://cdn.lbryplayer.xyz
|
||||
# deprecated:
|
||||
#LBRY_WEB_BUFFER_API=https://collector-service.api.lbry.tv/api/v1/events/video
|
||||
#COMMENT_SERVER_API=https://comments.lbry.com/api/v2
|
||||
WELCOME_VERSION=1.0
|
||||
|
||||
# STRIPE
|
||||
STRIPE_PUBLIC_KEY='pk_live_e8M4dRNnCCbmpZzduEUZBgJO'
|
||||
|
||||
## UI
|
||||
|
||||
LOADING_BAR_COLOR=#e50054
|
||||
|
||||
# IMAGE ASSETS
|
||||
YRBL_HAPPY_IMG_URL=https://spee.ch/spaceman-happy:a.png
|
||||
YRBL_SAD_IMG_URL=https://spee.ch/spaceman-sad:d.png
|
||||
LOGIN_IMG_URL=https://spee.ch/login:b.png
|
||||
LOGO=https://spee.ch/odysee-logo-png:3.png
|
||||
LOGO_TEXT_LIGHT=https://spee.ch/odysee-white-png:f.png
|
||||
LOGO_TEXT_DARK=https://spee.ch/odysee-png:2.png
|
||||
AVATAR_DEFAULT=https://spee.ch/spaceman-png:2.png
|
||||
FAVICON=https://spee.ch/favicon-png:c.png
|
||||
|
||||
# LOCALE
|
||||
DEFAULT_LANGUAGE=en
|
||||
|
||||
## LINKED CONTENT WHITELIST
|
||||
KNOWN_APP_DOMAINS=open.lbry.com,lbry.tv,lbry.lat,odysee.com
|
||||
|
||||
## CUSTOM CONTENT
|
||||
# If the following is true, copy custom/homepage.example.js to custom/homepage.js and modify
|
||||
CUSTOM_HOMEPAGE=true
|
||||
|
||||
# Add channels to auto-follow on firstrun (space delimited)
|
||||
AUTO_FOLLOW_CHANNELS=lbry://@Odysee#80d2590ad04e36fb1d077a9b9e3a8bba76defdf8 lbry://@OdyseeHelp#b58dfaeab6c70754d792cdd9b56ff59b90aea334
|
||||
|
||||
## FEATURES AND LIMITS
|
||||
SIMPLE_SITE=true
|
||||
BRANDED_SITE=odysee
|
||||
# SIMPLE_SITE REPLACEMENTS
|
||||
ENABLE_MATURE=false
|
||||
ENABLE_UI_NOTIFICATIONS=true
|
||||
ENABLE_WILD_WEST=true
|
||||
SHOW_TAGS_INTRO=false
|
||||
|
||||
# CENTRALIZED FEATURES
|
||||
ENABLE_COMMENT_REACTIONS=true
|
||||
ENABLE_FILE_REACTIONS=true
|
||||
ENABLE_CREATOR_REACTIONS=true
|
||||
ENABLE_NO_SOURCE_CLAIMS=true
|
||||
ENABLE_PREROLL_ADS=false
|
||||
SHOW_ADS=true
|
||||
|
||||
CHANNEL_STAKED_LEVEL_VIDEO_COMMENTS=4
|
||||
CHANNEL_STAKED_LEVEL_LIVESTREAM=3
|
||||
WEB_PUBLISH_SIZE_LIMIT_GB=4
|
||||
|
||||
#SEARCH TYPES - comma-delimited
|
||||
LIGHTHOUSE_DEFAULT_TYPES=audio,video
|
|
@ -4,6 +4,7 @@
|
|||
.*/node_modules/react-plastic/.*
|
||||
.*/node_modules/raf-schd/.*
|
||||
.*/node_modules/react-beautiful-dnd/.*
|
||||
.*/node_modules/resolve/test/.*
|
||||
|
||||
[include]
|
||||
|
||||
|
|
37
.github/workflows/deploy.yml
vendored
37
.github/workflows/deploy.yml
vendored
|
@ -11,8 +11,9 @@ jobs:
|
|||
name: lint
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: Borales/actions-yarn@v2.3.0
|
||||
- uses: actions/checkout@v3
|
||||
- run: corepack enable
|
||||
- run: yarn
|
||||
- run: yarn lint
|
||||
|
||||
build:
|
||||
|
@ -20,7 +21,7 @@ jobs:
|
|||
name: 'build'
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [14.x]
|
||||
node-version: [16.x]
|
||||
os:
|
||||
- ubuntu-latest
|
||||
- macos-latest
|
||||
|
@ -28,15 +29,31 @@ jobs:
|
|||
runs-on: ${{ matrix.os }}
|
||||
continue-on-error: true
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2-beta
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- run: corepack enable
|
||||
|
||||
- uses: maxim-lobanov/setup-xcode@v1
|
||||
if: startsWith(runner.os, 'mac')
|
||||
with:
|
||||
xcode-version: '12.4.0'
|
||||
xcode-version: '13.1.0'
|
||||
# This is gonna be hacky.
|
||||
# Github made us upgrade xcode, which would force an upgrade of electron-builder to fix mac.
|
||||
# But there were bugs with copyfiles / extraFiles that kept seeing duplicates erroring on ln.
|
||||
# A flag USE_HARD_LINKS=false in electron-builder.json was suggested in comments, but that broke windows builds.
|
||||
# So for now we'll install python2 on mac and make sure it can find it.
|
||||
# Remove this after successfully upgrading electron-builder.
|
||||
# HACK part 1
|
||||
- uses: Homebrew/actions/setup-homebrew@master
|
||||
if: startsWith(runner.os, 'mac')
|
||||
# HACK part 2
|
||||
- name: Install Python2
|
||||
if: startsWith(runner.os, 'mac')
|
||||
run: |
|
||||
/bin/bash -c "$(curl -fsSL https://github.com/alfredapp/dependency-scripts/raw/main/scripts/install-python2.sh)"
|
||||
echo "PYTHON_PATH=/usr/local/bin/python" >> $GITHUB_ENV
|
||||
|
||||
- name: Download blockchain headers
|
||||
run: |
|
||||
|
@ -46,8 +63,8 @@ jobs:
|
|||
|
||||
- name: Build
|
||||
run: |
|
||||
yarn global add cross-env
|
||||
yarn install --network-timeout 600000
|
||||
yarn dlx cross-env
|
||||
yarn --network-timeout 600000
|
||||
yarn build
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GH_TOKEN_NEW }}
|
||||
|
@ -56,7 +73,7 @@ jobs:
|
|||
WIN_CSC_KEY_PASSWORD: ${{ secrets.WIN_CSC_KEY_PASSWORD }}
|
||||
CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
|
||||
|
||||
WIN_CSC_LINK: https://raw.githubusercontent.com/lbryio/lbry-desktop/master/build/cert-2021-2022.pfx
|
||||
WIN_CSC_LINK: https://raw.githubusercontent.com/lbryio/lbry-desktop/master/build/cert2023.pfx
|
||||
CSC_LINK: https://s3.amazonaws.com/files.lbry.io/cert/osx-csc-2021-2022.p12
|
||||
|
||||
# UI
|
||||
|
@ -69,8 +86,6 @@ jobs:
|
|||
SITE_TITLE: lbry.tv
|
||||
SITE_NAME: lbry.tv
|
||||
SHOW_ADS: false
|
||||
YRBL_HAPPY_IMG_URL: https://cdn.lbryplayer.xyz/api/v3/streams/free/yrbl-happy/7aa50a7e5adaf48691935d55e45d697547392929/839d9a
|
||||
YRBL_SAD_IMG_URL: https://cdn.lbryplayer.xyz/api/v3/streams/free/yrbl-sad/c2d9649633d974e5ffb503925e1f17d951f1bd0f/f262dd
|
||||
ENABLE_COMMENT_REACTIONS: true
|
||||
ENABLE_NO_SOURCE_CLAIMS: false
|
||||
|
||||
|
|
6
.gitignore
vendored
6
.gitignore
vendored
|
@ -36,3 +36,9 @@ package-lock.json
|
|||
!.env.ody
|
||||
.env.desktop
|
||||
.env.lbrytv
|
||||
.yarn/*
|
||||
!.yarn/patches
|
||||
!.yarn/plugins
|
||||
!.yarn/sdks
|
||||
!.yarn/versions
|
||||
!.yarn/releases
|
||||
|
|
550
.yarn/plugins/@yarnpkg/plugin-version.cjs
vendored
Normal file
550
.yarn/plugins/@yarnpkg/plugin-version.cjs
vendored
Normal file
File diff suppressed because one or more lines are too long
785
.yarn/releases/yarn-3.2.0.cjs
vendored
Executable file
785
.yarn/releases/yarn-3.2.0.cjs
vendored
Executable file
File diff suppressed because one or more lines are too long
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/97e7141a.yml
vendored
Normal file
0
.yarn/versions/97e7141a.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
7
.yarnrc.yml
Normal file
7
.yarnrc.yml
Normal file
|
@ -0,0 +1,7 @@
|
|||
nodeLinker: node-modules
|
||||
|
||||
plugins:
|
||||
- path: .yarn/plugins/@yarnpkg/plugin-version.cjs
|
||||
spec: "@yarnpkg/plugin-version"
|
||||
|
||||
yarnPath: .yarn/releases/yarn-3.2.0.cjs
|
124
CHANGELOG.md
124
CHANGELOG.md
|
@ -1,9 +1,127 @@
|
|||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||
|
||||
## [Unreleased for Desktop]
|
||||
## [0.53.9] - [2023-2-8]
|
||||
|
||||
### Changed
|
||||
- Updated lbrynet to [0.113.0](https://github.com/lbryio/lbry-sdk/releases/tag/v0.113.0)
|
||||
|
||||
## [0.53.8] - [2022-11-17]
|
||||
|
||||
### Fixed
|
||||
- Selecting a large file in publish no longer crashes ([#7736](https://github.com/lbryio/lbry-desktop/pull/7736))
|
||||
- Unfollowing unpublished channels ([#7737](https://github.com/lbryio/lbry-desktop/pull/7737))
|
||||
|
||||
|
||||
### Changed
|
||||
- Updated xcode to 13.1 and hacked a fix for release ([#7736](https://github.com/lbryio/lbry-desktop/pull/7736))
|
||||
|
||||
## [0.53.7] - [2022-11-10]
|
||||
|
||||
### Added
|
||||
- 'Collections' to txo filter _community pr!_ ([#7711](https://github.com/lbryio/lbry-desktop/pull/7711))
|
||||
- Swap comment servers _community pr!_ ([#7670](https://github.com/lbryio/lbry-desktop/pull/7670))
|
||||
|
||||
### Fixed
|
||||
- Thumbnails no longer disable publish ([#7714](https://github.com/lbryio/lbry-desktop/pull/7714))
|
||||
- Publishing posts were empty ([#7715](https://github.com/lbryio/lbry-desktop/pull/7715))
|
||||
- Minor layout fixes _community pr!_ ([#7709](https://github.com/lbryio/lbry-desktop/pull/7709))
|
||||
- Comment section buttons layout ([#7716](https://github.com/lbryio/lbry-desktop/pull/7716))
|
||||
|
||||
### Changed
|
||||
- Removed watchman and its errors ([#7710](https://github.com/lbryio/lbry-desktop/pull/7710))
|
||||
- Updated lbrynet to [0.112.0](https://github.com/lbryio/lbry-sdk/releases/tag/v0.112.0)
|
||||
|
||||
## [0.53.6] - [2022-10-21]
|
||||
|
||||
### Fixed
|
||||
- Make thumbnails optional ([#7690](https://github.com/lbryio/lbry-desktop/pull/7690))
|
||||
- Show downloads newest first ([#7684](https://github.com/lbryio/lbry-desktop/pull/7684))
|
||||
- Only allow images in image uploader ([#7672](https://github.com/lbryio/lbry-desktop/pull/7672))
|
||||
- Fixed bug with csv exports ([#7697](https://github.com/lbryio/lbry-desktop/pull/7697))
|
||||
- Fixed various upload bugs including transcoding ([#7688](https://github.com/lbryio/lbry-desktop/pull/7688))
|
||||
- Fallback for files with no extension ([#7704](https://github.com/lbryio/lbry-desktop/pull/7704))
|
||||
|
||||
### Changed
|
||||
- Upgraded Electron to v17.2.0 ([#7703](https://github.com/lbryio/lbry-desktop/pull/7703))
|
||||
- Upgraded Electron to v17.0.0 ([#7691](https://github.com/lbryio/lbry-desktop/pull/7691))
|
||||
- Updated lbrynet to [0.111.0](https://github.com/lbryio/lbry-sdk/releases/tag/v0.111.0)
|
||||
|
||||
## [0.53.5] - [2022-08-26]
|
||||
|
||||
### Added
|
||||
- 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]
|
||||
|
||||
### Changed
|
||||
- Upgraded Yarn to Berry branch ([#7530](https://github.com/lbryio/lbry-desktop/pull/7530))
|
||||
- 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))
|
||||
|
||||
### Fixed
|
||||
- Repost style issues ([#7559](https://github.com/lbryio/lbry-desktop/pull/7559))
|
||||
- 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))
|
||||
- Playlist view link no longer crashes ([#7552](https://github.com/lbryio/lbry-desktop/pull/7552))
|
||||
|
||||
## [0.53.1] - [2022-04-22]
|
||||
|
||||
### Added
|
||||
- Uploads: show placeholder when loading page _community pr!_ ([#7531](https://github.com/lbryio/lbry-desktop/pull/7531))
|
||||
- Sidebar channel search _styles pr_ ([#7542](https://github.com/lbryio/lbry-desktop/pull/7542))
|
||||
- Viewed content progress indicator on thumbnail part 1 ([#7541](https://github.com/lbryio/lbry-desktop/pull/7541))
|
||||
- Viewed content progress indicator on thumbnail part 2 ([#7547](https://github.com/lbryio/lbry-desktop/pull/7547))
|
||||
- Ability to search through publishes ([#7535](https://github.com/lbryio/lbry-desktop/pull/7535))
|
||||
|
||||
### Changed
|
||||
- Large styles revamp following odysee _styles pr_ ([#7542](https://github.com/lbryio/lbry-desktop/pull/7542))
|
||||
|
||||
### Fixed
|
||||
- Fix bad rerender on homepage _styles pr_ ([#7542](https://github.com/lbryio/lbry-desktop/pull/7542))
|
||||
- Fix post-editor preview mode _community pr!_ ([#7532](https://github.com/lbryio/lbry-desktop/pull/7532))
|
||||
- Fix send-tip default tab ([#7533](https://github.com/lbryio/lbry-desktop/pull/7533))
|
||||
|
||||
## [0.52.6] - [2022-04-04]
|
||||
|
||||
|
@ -54,7 +172,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|||
- Reenabled generating thumbs from video ([#7384](https://github.com/lbryio/lbry-desktop/pull/7409))
|
||||
- Brought in playlist drag and drop playlist reordering _odysee team!_ ([#7442](https://github.com/lbryio/lbry-desktop/pull/7442))
|
||||
- Added duration overlays to ClaimPreview component ([#7420](https://github.com/lbryio/lbry-desktop/pull/7420))
|
||||
- Some Horizontal Scroll groundwork from _odysee team!_
|
||||
- Some Horizontal Scroll groundwork from _odysee team!_
|
||||
- Comment Emotes and Stickers and Mentions refactors from _odysee team!_ ([#7435](https://github.com/lbryio/lbry-desktop/pull/7435))
|
||||
- Seek forward and back from _odysee team!_ () ([#7460](https://github.com/lbryio/lbry-desktop/pull/7460))
|
||||
|
||||
|
|
14
README.md
14
README.md
|
@ -65,26 +65,26 @@ _Note: If coming from a deb install, the directory structure is different and yo
|
|||
|
||||
| | Flatpak | Arch | Nixpkgs | ARM/ARM64 |
|
||||
| -------------- | ----------------------------------------------------------------- | ----------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- | ------------------------------------------- |
|
||||
| Latest Release | [FlatHub Page](https://flathub.org/apps/details/io.lbry.lbry-app) | [AUR Package](https://aur.archlinux.org/packages/lbry-app-bin/) | [Nixpkgs](https://search.nixos.org/packages?channel=unstable&show=lbry&query=lbry) | [Build Guide](https://lbry.tv/@LBRYarm:5) |
|
||||
| Maintainers | [@kcSeb](https://keybase.io/kcseb) | [@kcSeb](https://keybase.io/kcseb) | [@Enderger](https://github.com/enderger) | [@Madiator2011](https://github.com/kodxana) |
|
||||
| Latest Release | [FlatHub Page](https://flathub.org/apps/details/io.lbry.lbry-app) | [AUR Package](https://aur.archlinux.org/packages/lbry-desktop-bin/) | [Nixpkgs](https://search.nixos.org/packages?channel=unstable&show=lbry&query=lbry) | [Build Guide](https://lbry.tv/@LBRYarm:5) |
|
||||
| Maintainers | N/A | [@RubenKelevra](https://github.com/RubenKelevra) | [@Enderger](https://github.com/enderger) | [@Madiator2011](https://github.com/kodxana) |
|
||||
|
||||
## Usage
|
||||
|
||||
Double click the installed application to interact with the LBRY network.
|
||||
Start the installed application to interact with the LBRY network.
|
||||
|
||||
## Running from Source
|
||||
|
||||
You can run the web version (lbry.tv), the electron app, or both at the same time.
|
||||
|
||||
#### Prerequisites
|
||||
|
||||
- [Git](https://git-scm.com/downloads)
|
||||
- [Node.js](https://nodejs.org/en/download/) (v14 required)
|
||||
- [Node.js](https://nodejs.org/en/download/) (v16 required)
|
||||
- [Corepack](https://nodejs.org/dist/latest-v17.x/docs/api/corepack.html) `npm i -g corepack` (Included in nodejs 14 LTS, 16 LTS and 17)
|
||||
- [Yarn](https://yarnpkg.com/en/docs/install)
|
||||
|
||||
1. Clone (or [fork](https://help.github.com/articles/fork-a-repo/)) this repository: `git clone https://github.com/lbryio/lbry-desktop`
|
||||
2. Change directory into the cloned repository: `cd lbry-desktop`
|
||||
3. Install the dependencies: `yarn`
|
||||
3. If corepack is not enabled, run `sudo corepack enable` (the sudo is necessary for system-wide installation, if you use container, nvm etc... you might not be forced to use it)
|
||||
4. Install the dependencies: `yarn`
|
||||
|
||||
#### Run the electron app
|
||||
|
||||
|
|
|
@ -7,6 +7,8 @@ module.exports = api => {
|
|||
'import-glob',
|
||||
'@babel/plugin-transform-runtime',
|
||||
['@babel/plugin-proposal-decorators', { decoratorsBeforeExport: true }],
|
||||
['@babel/plugin-proposal-private-methods', { 'loose': false }],
|
||||
['@babel/plugin-proposal-private-property-in-object', { 'loose': false }],
|
||||
'@babel/plugin-transform-flow-strip-types',
|
||||
'@babel/plugin-proposal-class-properties',
|
||||
'react-hot-loader/babel',
|
||||
|
|
BIN
build/cert2023.pfx
Normal file
BIN
build/cert2023.pfx
Normal file
Binary file not shown.
|
@ -4,5 +4,11 @@
|
|||
<dict>
|
||||
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
|
||||
<true/>
|
||||
<key>com.apple.security.network.client</key>
|
||||
<true/>
|
||||
<key>com.apple.security.network.server</key>
|
||||
<true/>
|
||||
<key>com.apple.security.cs.disable-library-validation</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
|
@ -11,7 +11,7 @@ const config = {
|
|||
LBRY_WEB_API: process.env.LBRY_WEB_API, //api.na-backend.odysee.com',
|
||||
LBRY_WEB_PUBLISH_API: process.env.LBRY_WEB_PUBLISH_API,
|
||||
LBRY_API_URL: process.env.LBRY_API_URL, //api.lbry.com',
|
||||
LBRY_WEB_STREAMING_API: process.env.LBRY_WEB_STREAMING_API, //cdn.lbryplayer.xyz',
|
||||
LBRY_WEB_STREAMING_API: process.env.LBRY_WEB_STREAMING_API, //player.odysee.com
|
||||
LBRY_WEB_BUFFER_API: process.env.LBRY_WEB_BUFFER_API,
|
||||
SEARCH_SERVER_API: process.env.SEARCH_SERVER_API,
|
||||
CLOUD_CONNECT_SITE_NAME: process.env.CLOUD_CONNECT_SITE_NAME,
|
||||
|
|
|
@ -29,6 +29,10 @@
|
|||
"from": "./static/font",
|
||||
"to": "static/font",
|
||||
"filter": ["**/*"]
|
||||
},
|
||||
{
|
||||
"from": "./static/app-update.yml",
|
||||
"to": "app-update.yml"
|
||||
}
|
||||
],
|
||||
"publish": [
|
||||
|
|
|
@ -20,8 +20,12 @@ import path from 'path';
|
|||
import { diskSpaceLinux, diskSpaceWindows, diskSpaceMac } from '../ui/util/diskspace';
|
||||
|
||||
const { download } = require('electron-dl');
|
||||
const mime = require('mime');
|
||||
const remote = require('@electron/remote/main');
|
||||
const os = require('os');
|
||||
const sudo = require('sudo-prompt');
|
||||
const probe = require('ffmpeg-probe');
|
||||
const MAX_IPC_SEND_BUFFER_SIZE = 500000000; // large files crash when serialized for ipc message
|
||||
|
||||
remote.initialize();
|
||||
const filePath = path.join(process.resourcesPath, 'static', 'upgradeDisabled');
|
||||
|
@ -33,29 +37,23 @@ try {
|
|||
upgradeDisabled = false;
|
||||
}
|
||||
autoUpdater.autoDownload = !upgradeDisabled;
|
||||
autoUpdater.allowPrerelease = false;
|
||||
|
||||
// This is set to true if an auto update has been downloaded through the Electron
|
||||
// auto-update system and is ready to install. If the user declined an update earlier,
|
||||
// it will still install on shutdown.
|
||||
let autoUpdateDownloaded = false;
|
||||
const UPDATE_STATE_INIT = 0;
|
||||
const UPDATE_STATE_CHECKING = 1;
|
||||
const UPDATE_STATE_UPDATES_FOUND = 2;
|
||||
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
|
||||
// that we show on Windows after you decline an upgrade and close the app later.
|
||||
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
|
||||
// object is garbage collected.
|
||||
let rendererWindow;
|
||||
|
@ -243,7 +241,8 @@ app.on('activate', () => {
|
|||
app.on('will-quit', event => {
|
||||
if (
|
||||
process.platform === 'win32' &&
|
||||
autoUpdateDownloaded &&
|
||||
updateState === UPDATE_STATE_DOWNLOADED &&
|
||||
isAutoUpdateSupported &&
|
||||
!appState.autoUpdateAccepted &&
|
||||
!showingAutoUpdateCloseAlert
|
||||
) {
|
||||
|
@ -303,6 +302,96 @@ app.on('before-quit', () => {
|
|||
appState.isQuitting = true;
|
||||
});
|
||||
|
||||
// Get the content of a file as a raw buffer of bytes.
|
||||
// Useful to convert a file path to a File instance.
|
||||
// Example:
|
||||
// const result = await ipcMain.invoke('get-file-from-path', 'path/to/file');
|
||||
// const file = new File([result.buffer], result.name);
|
||||
// NOTE: if path points to a folder, an empty
|
||||
// file will be given.
|
||||
ipcMain.handle('get-file-from-path', (event, path, readContents = true) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
fs.stat(path, (error, stats) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
// Separate folders considering "\" and "/"
|
||||
// as separators (cross platform)
|
||||
const folders = path.split(/[\\/]/);
|
||||
const name = folders[folders.length - 1];
|
||||
if (stats.isDirectory()) {
|
||||
resolve({
|
||||
name,
|
||||
mime: undefined,
|
||||
path,
|
||||
buffer: new ArrayBuffer(0),
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (!readContents) {
|
||||
resolve({
|
||||
name,
|
||||
mime: mime.getType(name) || undefined,
|
||||
path,
|
||||
buffer: new ArrayBuffer(0),
|
||||
});
|
||||
return;
|
||||
}
|
||||
// Encoding null ensures data results in a Buffer.
|
||||
fs.readFile(path, { encoding: null }, (err, data) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
resolve({
|
||||
name,
|
||||
mime: mime.getType(name) || undefined,
|
||||
path,
|
||||
buffer: data,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
ipcMain.handle('get-file-details-from-path', async (event, path) => {
|
||||
const isFfMp4 = (ffprobeResults) => {
|
||||
return ffprobeResults &&
|
||||
ffprobeResults.format &&
|
||||
ffprobeResults.format.format_name &&
|
||||
ffprobeResults.format.format_name.includes('mp4');
|
||||
};
|
||||
const folders = path.split(/[\\/]/);
|
||||
const name = folders[folders.length - 1];
|
||||
let duration = 0, size = 0, mimeType;
|
||||
try {
|
||||
await fs.promises.stat(path);
|
||||
let ffprobeResults;
|
||||
try {
|
||||
ffprobeResults = await probe(path);
|
||||
duration = ffprobeResults.format.duration;
|
||||
size = ffprobeResults.format.size;
|
||||
} catch (e) {
|
||||
}
|
||||
let fileReadResult;
|
||||
if (size < MAX_IPC_SEND_BUFFER_SIZE) {
|
||||
try {
|
||||
fileReadResult = await fs.promises.readFile(path);
|
||||
} catch (e) {
|
||||
|
||||
}
|
||||
}
|
||||
// TODO: use mmmagic to inspect file and get mime type
|
||||
mimeType = isFfMp4(ffprobeResults) ? 'video/mp4' : mime.getType(name);
|
||||
const fileData = {name, mime: mimeType || undefined, path, duration: duration, size, buffer: fileReadResult };
|
||||
return fileData;
|
||||
} catch (e) {
|
||||
// no stat
|
||||
return { error: 'no file' };
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.on('get-disk-space', async (event) => {
|
||||
try {
|
||||
const { data_dir } = await Lbry.settings_get();
|
||||
|
@ -323,91 +412,10 @@ ipcMain.on('get-disk-space', async (event) => {
|
|||
rendererWindow.webContents.send('send-disk-space', { diskSpace });
|
||||
} catch (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', () => {
|
||||
function formatRc(ver) {
|
||||
// Adds dash if needed to make RC suffix SemVer friendly
|
||||
|
@ -500,3 +508,162 @@ process.on('uncaughtException', error => {
|
|||
if (daemon) daemon.quit();
|
||||
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();
|
||||
});
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import express from 'express';
|
||||
import unpackByOutpoint from './unpackByOutpoint';
|
||||
// import express from 'express';
|
||||
|
||||
// Polyfills and `lbry-redux`
|
||||
global.fetch = require('node-fetch');
|
||||
|
@ -8,31 +7,31 @@ if (typeof global.fetch === 'object') {
|
|||
global.fetch = global.fetch.default;
|
||||
}
|
||||
|
||||
const Lbry = require('lbry');
|
||||
// const Lbry = require('lbry');
|
||||
|
||||
delete global.window;
|
||||
|
||||
export default async function startSandbox() {
|
||||
const port = 5278;
|
||||
const sandbox = express();
|
||||
// const port = 5278;
|
||||
// const sandbox = express();
|
||||
|
||||
sandbox.get('/set/:outpoint', async (req, res) => {
|
||||
const { outpoint } = req.params;
|
||||
|
||||
const resolvedPath = await unpackByOutpoint(Lbry, outpoint);
|
||||
|
||||
sandbox.use(`/sandbox/${outpoint}/`, express.static(resolvedPath));
|
||||
|
||||
res.send(`/sandbox/${outpoint}/`);
|
||||
});
|
||||
|
||||
sandbox
|
||||
.listen(port, 'localhost', () => console.log(`Sandbox listening on port ${port}.`))
|
||||
.on('error', err => {
|
||||
if (err.code === 'EADDRINUSE') {
|
||||
console.log(
|
||||
`Server already listening at localhost:${port}. This is probably another LBRY app running. If not, games in the app will not work.`
|
||||
);
|
||||
}
|
||||
});
|
||||
// sandbox.get('/set/:outpoint', async (req, res) => {
|
||||
// const { outpoint } = req.params;
|
||||
//
|
||||
// const resolvedPath = await unpackByOutpoint(Lbry, outpoint);
|
||||
//
|
||||
// sandbox.use(`/sandbox/${outpoint}/`, express.static(resolvedPath));
|
||||
//
|
||||
// res.send(`/sandbox/${outpoint}/`);
|
||||
// });
|
||||
//
|
||||
// sandbox
|
||||
// .listen(port, 'localhost', () => console.log(`Sandbox listening on port ${port}.`))
|
||||
// .on('error', err => {
|
||||
// if (err.code === 'EADDRINUSE') {
|
||||
// console.log(
|
||||
// `Server already listening at localhost:${port}. This is probably another LBRY app running. If not, games in the app will not work.`
|
||||
// );
|
||||
// }
|
||||
// });
|
||||
}
|
||||
|
|
|
@ -1,23 +0,0 @@
|
|||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { unpackDirectory } from 'lbry-format';
|
||||
|
||||
async function unpackByOutpoint(lbry, outpoint) {
|
||||
const { items: claimFiles } = await lbry.file_list({ outpoint, full_status: true, page: 1, page_size: 1 });
|
||||
|
||||
if (claimFiles && claimFiles.length) {
|
||||
const claimFileInfo = claimFiles[0];
|
||||
const packFilePath = path.resolve(claimFileInfo.download_path);
|
||||
const unpackPath = path.normalize(path.join(claimFileInfo.download_directory, claimFileInfo.claim_name));
|
||||
|
||||
if (!fs.existsSync(unpackPath)) {
|
||||
await unpackDirectory(unpackPath, {
|
||||
fileName: packFilePath,
|
||||
});
|
||||
}
|
||||
|
||||
return unpackPath;
|
||||
}
|
||||
}
|
||||
|
||||
export default unpackByOutpoint;
|
|
@ -5,7 +5,7 @@
|
|||
// involve moving it from 'extras' to 'ui' (big change).
|
||||
|
||||
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 { selectModerationBlockList } from 'redux/selectors/comments';
|
||||
import { selectBlacklistedOutpointMap, selectFilteredOutpointMap } from 'lbryinc';
|
||||
|
@ -18,7 +18,8 @@ export const selectBanStateForUri = createCachedSelector(
|
|||
selectFilteredOutpointMap,
|
||||
selectMutedChannels,
|
||||
selectModerationBlockList,
|
||||
(claim, blackListedOutpointMap, filteredOutpointMap, mutedChannelUris, personalBlocklist) => {
|
||||
(state, uri) => makeSelectIsBlacklisted(uri)(state),
|
||||
(claim, blackListedOutpointMap, filteredOutpointMap, mutedChannelUris, personalBlocklist, isBlacklisted) => {
|
||||
const banState = {};
|
||||
|
||||
if (!claim) {
|
||||
|
@ -27,6 +28,10 @@ export const selectBanStateForUri = createCachedSelector(
|
|||
|
||||
const channelClaim = getChannelFromClaim(claim);
|
||||
|
||||
if (isBlacklisted) {
|
||||
banState['blacklisted'] = true;
|
||||
}
|
||||
|
||||
// This will be replaced once blocking is done at the wallet server level.
|
||||
if (blackListedOutpointMap) {
|
||||
if (
|
||||
|
|
37
flow-typed/Claim.js
vendored
37
flow-typed/Claim.js
vendored
|
@ -145,12 +145,49 @@ declare type PurchaseReceipt = {
|
|||
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 = {
|
||||
[string]: {
|
||||
stream: ?StreamClaim,
|
||||
channel: ?ChannelClaim,
|
||||
claimsInChannel: ?number,
|
||||
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 = {
|
||||
total: string,
|
||||
free: string,
|
||||
total: number,
|
||||
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>,
|
||||
limitClaimsPerChannel?: number,
|
||||
pageSize?: number,
|
||||
languages?: Array<string>,
|
||||
},
|
||||
route?: string,
|
||||
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,
|
||||
}
|
57
package.json
57
package.json
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "lbry",
|
||||
"version": "0.52.6",
|
||||
"version": "0.53.9",
|
||||
"description": "A browser for the LBRY network, a digital marketplace controlled by its users.",
|
||||
"keywords": [
|
||||
"lbry"
|
||||
|
@ -23,52 +23,47 @@
|
|||
"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": "cross-env NODE_ENV=production yarn compile:electron",
|
||||
"copyenv": "copyfiles ./.env* web/",
|
||||
"dev": "yarn dev:electron",
|
||||
"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",
|
||||
"dist": "electron-builder",
|
||||
"build": "cross-env NODE_ENV=production yarn compile:electron && electron-builder build",
|
||||
"build:dir": "yarn build -- --dir -c.compression=store -c.mac.identity=null",
|
||||
"crossenv": "./node_modules/cross-env/dist/bin/cross-env",
|
||||
"crossenv": "cross-env",
|
||||
"flow": "flow",
|
||||
"lint": "eslint 'ui/**/*.{js,jsx}' && eslint 'electron/**/*.js' && flow",
|
||||
"lint-fix": "eslint --fix --quiet 'ui/**/*.{js,jsx}' && eslint --fix --quiet 'electron/**/*.js'",
|
||||
"format": "prettier 'src/**/*.{js,jsx,scss,json}' --write",
|
||||
"flow-defs": "flow-typed install",
|
||||
"precommit": "lint-staged",
|
||||
"preinstall": "yarn cache clean lbry-redux && yarn cache clean lbryinc",
|
||||
"postinstall": "electron-builder install-app-deps && node ./build/downloadDaemon.js",
|
||||
"postinstall:warning": "echo '\n\nWARNING\n\nNot all node modules were installed because NODE_ENV is set to \"production\".\nThis should only be set after installing dependencies with \"yarn\". The app will not work.\n\n'"
|
||||
},
|
||||
"dependencies": {
|
||||
"@electron/remote": "^2.0.1",
|
||||
"@emotion/react": "^11.6.0",
|
||||
"@emotion/styled": "^11.6.0",
|
||||
"@emotion/react": "^11.10.4",
|
||||
"@emotion/styled": "^11.10.4",
|
||||
"@mui/material": "^5.2.1",
|
||||
"@ungap/from-entries": "^0.2.1",
|
||||
"auto-launch": "^5.0.5",
|
||||
"electron-dl": "^3.2.0",
|
||||
"electron-log": "^2.2.12",
|
||||
"electron-log": "^4.4.8",
|
||||
"electron-notarize": "^1.0.0",
|
||||
"electron-updater": "^4.2.4",
|
||||
"express": "^4.17.1",
|
||||
"ffmpeg-probe": "^1.0.6",
|
||||
"humanize-duration": "^3.27.0",
|
||||
"if-env": "^1.0.4",
|
||||
"match-sorter": "^6.3.0",
|
||||
"mime": "^3.0.0",
|
||||
"node-html-parser": "^5.1.0",
|
||||
"parse-duration": "^1.0.0",
|
||||
"proxy-polyfill": "0.1.6",
|
||||
"re-reselect": "^4.0.0",
|
||||
"react-beautiful-dnd": "^13.1.0",
|
||||
"react-datetime-picker": "^3.4.3",
|
||||
"remove-markdown": "^0.3.0",
|
||||
"rss": "^1.2.2",
|
||||
"source-map-explorer": "^2.5.2",
|
||||
"tempy": "^0.6.0",
|
||||
"videojs-logo": "^2.1.4"
|
||||
"sudo-prompt": "^9.2.1",
|
||||
"tempy": "^0.6.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.0.0",
|
||||
|
@ -79,7 +74,7 @@
|
|||
"@babel/plugin-syntax-dynamic-import": "^7.2.0",
|
||||
"@babel/plugin-transform-flow-strip-types": "^7.2.3",
|
||||
"@babel/plugin-transform-runtime": "^7.4.3",
|
||||
"@babel/polyfill": "^7.2.5",
|
||||
"@babel/polyfill": "^7.12.1",
|
||||
"@babel/preset-env": "^7.12.11",
|
||||
"@babel/preset-flow": "^7.12.1",
|
||||
"@babel/preset-react": "^7.0.0",
|
||||
|
@ -87,9 +82,8 @@
|
|||
"@datapunt/matomo-tracker-js": "^0.1.4",
|
||||
"@hot-loader/react-dom": "^16.13",
|
||||
"@meetfranz/electron-cookies": "^3.0.2",
|
||||
"@reach/auto-id": "^0.13.0",
|
||||
"@reach/combobox": "^0.12.1",
|
||||
"@reach/menu-button": "0.7.4",
|
||||
"@reach/menu-button": "0.8.6",
|
||||
"@reach/rect": "^0.16.0",
|
||||
"@reach/tabs": "^0.1.5",
|
||||
"@reach/tooltip": "^0.12.1",
|
||||
|
@ -98,21 +92,17 @@
|
|||
"@sentry/webpack-plugin": "^1.10.0",
|
||||
"@types/three": "^0.103.2",
|
||||
"adm-zip": "^0.4.13",
|
||||
"async-exit-hook": "^2.0.1",
|
||||
"babel-eslint": "^10.0.1",
|
||||
"babel-loader": "^8.0.5",
|
||||
"babel-plugin-add-module-exports": "^1.0.4",
|
||||
"babel-plugin-import-glob": "^2.0.0",
|
||||
"babel-plugin-transform-imports": "^1.5.1",
|
||||
"babel-plugin-transform-object-rest-spread": "^6.26.0",
|
||||
"bluebird": "^3.5.1",
|
||||
"chalk": "^4.1.0",
|
||||
"classnames": "^2.2.5",
|
||||
"codemirror": "^5.39.2",
|
||||
"concurrently": "^4.1.2",
|
||||
"connected-react-router": "^6.8.0",
|
||||
"copy-webpack-plugin": "^6.4.1",
|
||||
"copyfiles": "^2.4.1",
|
||||
"country-data": "^0.0.31",
|
||||
"cross-env": "^7.0.3",
|
||||
"crypto-js": "^4.0.0",
|
||||
|
@ -123,10 +113,9 @@
|
|||
"decompress": "^4.2.1",
|
||||
"del": "^3.0.0",
|
||||
"devtron": "^1.4.0",
|
||||
"dom-scroll-into-view": "^1.2.1",
|
||||
"dotenv-defaults": "^2.0.1",
|
||||
"dotenv-webpack": "^1.8.0",
|
||||
"electron": "15.4.0",
|
||||
"electron": "17.2.0",
|
||||
"electron-builder": "^22.10.5",
|
||||
"electron-devtools-installer": "^3.1.1",
|
||||
"electron-is-dev": "^0.3.0",
|
||||
|
@ -156,16 +145,12 @@
|
|||
"husky": "^3.1.0",
|
||||
"imagesloaded": "^4.1.4",
|
||||
"json-loader": "^0.5.4",
|
||||
"lbry-format": "https://github.com/lbryio/lbry-format.git",
|
||||
"lint-staged": "^7.0.2",
|
||||
"localforage": "^1.7.1",
|
||||
"lodash-es": "^4.17.14",
|
||||
"lodash-es": "^4.17.21",
|
||||
"mammoth": "^1.4.16",
|
||||
"moment": "^2.22.0",
|
||||
"node-abi": "^2.5.1",
|
||||
"moment": "^2.29.2",
|
||||
"node-fetch": "^2.6.7",
|
||||
"node-html-parser": "^5.1.0",
|
||||
"node-libs-browser": "^2.1.0",
|
||||
"node-loader": "^0.6.0",
|
||||
"node-wget": "^0.4.3",
|
||||
"nodemon": "^1.19.1",
|
||||
|
@ -180,7 +165,6 @@
|
|||
"rc-progress": "^2.0.6",
|
||||
"react": "^16.8.2",
|
||||
"react-awesome-lightbox": "^1.7.3",
|
||||
"react-confetti": "^4.0.1",
|
||||
"react-dom": "^16.8.2",
|
||||
"react-draggable": "^3.3.0",
|
||||
"react-google-recaptcha": "^2.0.1",
|
||||
|
@ -191,7 +175,6 @@
|
|||
"react-router": "^5.1.0",
|
||||
"react-router-dom": "^5.1.0",
|
||||
"react-simplemde-editor": "^4.1.3",
|
||||
"react-spring": "^8.0.20",
|
||||
"reakit": "^1.0.0-beta.13",
|
||||
"redux": "^3.6.0",
|
||||
"redux-persist": "^5.10.0",
|
||||
|
@ -208,20 +191,16 @@
|
|||
"sass": "^1.29.0",
|
||||
"sass-loader": "^7.1.0",
|
||||
"semver": "^5.3.0",
|
||||
"stream-to-blob-url": "^2.1.1",
|
||||
"strip-markdown": "^3.0.3",
|
||||
"style-loader": "^0.23.1",
|
||||
"terser-webpack-plugin": "^4.2.3",
|
||||
"three-full": "^28.0.2",
|
||||
"tiny-relative-date": "^1.3.0",
|
||||
"tree-kill": "^1.1.0",
|
||||
"unist-util-visit": "^2.0.3",
|
||||
"uuid": "^8.3.2",
|
||||
"video.js": "^7.14.3",
|
||||
"videojs-contrib-quality-levels": "^2.0.9",
|
||||
"videojs-event-tracking": "^1.0.1",
|
||||
"villain-react": "^1.0.9",
|
||||
"wavesurfer.js": "^2.2.1",
|
||||
"webpack": "^4.44.2",
|
||||
"webpack-bundle-analyzer": "^3.1.0",
|
||||
"webpack-cli": "^3.3.10",
|
||||
|
@ -231,17 +210,17 @@
|
|||
"webpack-hot-middleware": "^2.24.3",
|
||||
"webpack-merge": "^4.2.1",
|
||||
"webpack-node-externals": "^1.7.2",
|
||||
"y18n": "^4.0.1",
|
||||
"yarnhook": "^0.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=7",
|
||||
"node": ">=16.13",
|
||||
"yarn": "^1.3"
|
||||
},
|
||||
"lbrySettings": {
|
||||
"lbrynetDaemonVersion": "0.107.1",
|
||||
"lbrynetDaemonVersion": "0.113.0",
|
||||
"lbrynetDaemonUrlTemplate": "https://github.com/lbryio/lbry/releases/download/vDAEMONVER/lbrynet-OSNAME.zip",
|
||||
"lbrynetDaemonDir": "static/daemon",
|
||||
"lbrynetDaemonFileName": "lbrynet"
|
||||
}
|
||||
},
|
||||
"packageManager": "yarn@3.2.0"
|
||||
}
|
||||
|
|
|
@ -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.",
|
||||
"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",
|
||||
"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",
|
||||
"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%",
|
||||
|
@ -2228,13 +2221,6 @@
|
|||
"Enable Prerelease Updates": "Enable Prerelease Updates",
|
||||
"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.",
|
||||
"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",
|
||||
"Admin": "Admin",
|
||||
"Stickers": "Stickers",
|
||||
|
@ -2256,19 +2242,6 @@
|
|||
"Move Up": "Move Up",
|
||||
"Move Down": "Move Down",
|
||||
"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",
|
||||
"Use Https": "Use Https",
|
||||
"Server URL": "Server URL",
|
||||
|
@ -2300,11 +2273,54 @@
|
|||
"In %collection%": "In %collection%",
|
||||
"Add to %collection%": "Add to %collection%",
|
||||
"Show this channel your appreciation by sending a donation of Credits. ": "Show this channel your appreciation by sending a donation of Credits. ",
|
||||
"%action% %collection%": "%action% %collection%",
|
||||
"You've entered the land of content freedom! Let's make sure everything is ship shape.": "You've entered the land of content freedom! Let's make sure everything is ship shape.",
|
||||
"By continuing, you agree to the %terms%": "By continuing, you agree to the %terms%",
|
||||
"Privacy": "Privacy",
|
||||
"LBRY takes privacy and choice seriously. Is it ok if we monitor performance and help creators track their views?": "LBRY takes privacy and choice seriously. Is it ok if we monitor performance and help creators track their views?",
|
||||
"Yes, share with LBRY": "Yes, share with LBRY",
|
||||
"Search Uploads": "Search Uploads",
|
||||
"This refundable boost will improve the discoverability of this %claimTypeText% while active. ": "This refundable boost will improve the discoverability of this %claimTypeText% while active. ",
|
||||
"Show less": "Show less",
|
||||
"Elements": "Elements",
|
||||
"Icons": "Icons",
|
||||
"Go to": "Go to",
|
||||
"Clearing...": "Clearing...",
|
||||
"Clear Views": "Clear Views",
|
||||
"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.",
|
||||
"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",
|
||||
"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--"
|
||||
}
|
||||
|
|
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
|
BIN
static/img/freespch.png
Normal file
BIN
static/img/freespch.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 13 KiB |
164
ui/analytics.js
164
ui/analytics.js
|
@ -1,4 +1,11 @@
|
|||
// @flow
|
||||
/*
|
||||
Removed Watchman (internal view tracking) code.
|
||||
This file may eventually implement cantina
|
||||
Refer to 0cc0e213a5c5bf9e2a76316df5d9da4b250a13c3 for initial integration commit
|
||||
refer to ___ for removal commit.
|
||||
*/
|
||||
|
||||
import { Lbryio } from 'lbryinc';
|
||||
import * as Sentry from '@sentry/browser';
|
||||
import MatomoTracker from '@datapunt/matomo-tracker-js';
|
||||
|
@ -14,9 +21,6 @@ const devInternalApis = process.env.LBRY_API_URL && process.env.LBRY_API_URL.inc
|
|||
export const SHARE_INTERNAL = 'shareInternal';
|
||||
const SHARE_THIRD_PARTY = 'shareThirdParty';
|
||||
|
||||
const WATCHMAN_BACKEND_ENDPOINT = 'https://watchman.na-backend.odysee.com/reports/playback';
|
||||
// const SEND_DATA_TO_WATCHMAN_INTERVAL = 10; // in seconds
|
||||
|
||||
if (isProduction) {
|
||||
ElectronCookies.enable({
|
||||
origin: 'https://lbry.tv',
|
||||
|
@ -68,114 +72,10 @@ type LogPublishParams = {
|
|||
let internalAnalyticsEnabled: boolean = false;
|
||||
if (window.localStorage.getItem(SHARE_INTERNAL) === 'true') internalAnalyticsEnabled = true;
|
||||
|
||||
/**
|
||||
* Determine the mobile device type viewing the data
|
||||
* This function returns one of 'and' (Android), 'ios', or 'web'.
|
||||
*
|
||||
* @returns {String}
|
||||
*/
|
||||
function getDeviceType() {
|
||||
return 'dsk';
|
||||
}
|
||||
// variables initialized for watchman
|
||||
let amountOfBufferEvents = 0;
|
||||
let amountOfBufferTimeInMS = 0;
|
||||
let videoType, userId, claimUrl, playerPoweredBy, videoPlayer, bitrateAsBitsPerSecond;
|
||||
let lastSentTime;
|
||||
|
||||
// calculate data for backend, send them, and reset buffer data for next interval
|
||||
async function sendAndResetWatchmanData() {
|
||||
if (!userId) {
|
||||
return 'Can only be used with a user id';
|
||||
}
|
||||
|
||||
if (!videoPlayer) {
|
||||
return 'Video player not initialized';
|
||||
}
|
||||
|
||||
let timeSinceLastIntervalSend = new Date() - lastSentTime;
|
||||
lastSentTime = new Date();
|
||||
|
||||
let protocol;
|
||||
if (videoType === 'application/x-mpegURL') {
|
||||
protocol = 'hls';
|
||||
// get bandwidth if it exists from the texttrack (so it's accurate if user changes quality)
|
||||
// $FlowFixMe
|
||||
bitrateAsBitsPerSecond = videoPlayer.textTracks?.().tracks_[0]?.activeCues[0]?.value?.bandwidth;
|
||||
} else {
|
||||
protocol = 'stb';
|
||||
}
|
||||
|
||||
// current position in video in MS
|
||||
const positionInVideo = Math.round(videoPlayer.currentTime()) * 1000;
|
||||
|
||||
// get the duration marking the time in the video for relative position calculation
|
||||
const totalDurationInSeconds = Math.round(videoPlayer.duration());
|
||||
|
||||
// build object for watchman backend
|
||||
const objectToSend = {
|
||||
rebuf_count: amountOfBufferEvents,
|
||||
rebuf_duration: amountOfBufferTimeInMS,
|
||||
url: claimUrl.replace('lbry://', ''),
|
||||
device: getDeviceType(),
|
||||
duration: timeSinceLastIntervalSend,
|
||||
protocol,
|
||||
player: playerPoweredBy,
|
||||
user_id: userId.toString(),
|
||||
position: Math.round(positionInVideo),
|
||||
rel_position: Math.round((positionInVideo / (totalDurationInSeconds * 1000)) * 100),
|
||||
bitrate: bitrateAsBitsPerSecond,
|
||||
bandwidth: undefined,
|
||||
// ...(userDownloadBandwidthInBitsPerSecond && {bandwidth: userDownloadBandwidthInBitsPerSecond}), // add bandwidth if populated
|
||||
};
|
||||
|
||||
// post to watchman
|
||||
await sendWatchmanData(objectToSend);
|
||||
|
||||
// reset buffer data
|
||||
amountOfBufferEvents = 0;
|
||||
amountOfBufferTimeInMS = 0;
|
||||
}
|
||||
|
||||
let watchmanInterval;
|
||||
// clear watchman interval and mark it as null (when video paused)
|
||||
function stopWatchmanInterval() {
|
||||
clearInterval(watchmanInterval);
|
||||
watchmanInterval = null;
|
||||
}
|
||||
|
||||
// creates the setInterval that will run send to watchman on recurring basis
|
||||
function startWatchmanIntervalIfNotRunning() {
|
||||
if (!watchmanInterval) {
|
||||
// instantiate the first time to calculate duration from
|
||||
lastSentTime = new Date();
|
||||
}
|
||||
}
|
||||
|
||||
// post data to the backend
|
||||
async function sendWatchmanData(body) {
|
||||
try {
|
||||
const response = await fetch(WATCHMAN_BACKEND_ENDPOINT, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
|
||||
return response;
|
||||
} catch (err) {}
|
||||
}
|
||||
|
||||
const analytics: Analytics = {
|
||||
// receive buffer events from tracking plugin and save buffer amounts and times for backend call
|
||||
videoBufferEvent: async (claim, data) => {
|
||||
amountOfBufferEvents = amountOfBufferEvents + 1;
|
||||
amountOfBufferTimeInMS = amountOfBufferTimeInMS + data.bufferDuration;
|
||||
},
|
||||
onDispose: () => {
|
||||
stopWatchmanInterval();
|
||||
// stub
|
||||
},
|
||||
/**
|
||||
* Is told whether video is being started or paused, and adjusts interval accordingly
|
||||
|
@ -183,40 +83,9 @@ const analytics: Analytics = {
|
|||
* @param {object} passedPlayer - VideoJS Player object
|
||||
*/
|
||||
videoIsPlaying: (isPlaying, passedPlayer) => {
|
||||
let playerIsSeeking = false;
|
||||
// have to use this because videojs pauses/unpauses during seek
|
||||
// sometimes the seeking function isn't populated yet so check for it as well
|
||||
if (passedPlayer && passedPlayer.seeking) {
|
||||
playerIsSeeking = passedPlayer.seeking();
|
||||
}
|
||||
|
||||
// if being paused, and not seeking, send existing data and stop interval
|
||||
if (!isPlaying && !playerIsSeeking) {
|
||||
sendAndResetWatchmanData();
|
||||
stopWatchmanInterval();
|
||||
// if being told to pause, and seeking, send and restart interval
|
||||
} else if (!isPlaying && playerIsSeeking) {
|
||||
sendAndResetWatchmanData();
|
||||
stopWatchmanInterval();
|
||||
startWatchmanIntervalIfNotRunning();
|
||||
// is being told to play, and seeking, don't do anything,
|
||||
// assume it's been started already from pause
|
||||
} else if (isPlaying && playerIsSeeking) {
|
||||
// start but not a seek, assuming a start from paused content
|
||||
} else if (isPlaying && !playerIsSeeking) {
|
||||
startWatchmanIntervalIfNotRunning();
|
||||
}
|
||||
// stub
|
||||
},
|
||||
videoStartEvent: (claimId, timeToStartVideo, poweredBy, passedUserId, canonicalUrl, passedPlayer, videoBitrate) => {
|
||||
// populate values for watchman when video starts
|
||||
userId = passedUserId;
|
||||
claimUrl = canonicalUrl;
|
||||
playerPoweredBy = poweredBy;
|
||||
|
||||
videoType = passedPlayer.currentSource().type;
|
||||
videoPlayer = passedPlayer;
|
||||
bitrateAsBitsPerSecond = videoBitrate;
|
||||
|
||||
// sendPromMetric('time_to_start', duration);
|
||||
sendMatomoEvent('Media', 'TimeToStart', claimId, timeToStartVideo);
|
||||
},
|
||||
|
@ -382,24 +251,9 @@ function sendMatomoEvent(category, action, name, value) {
|
|||
}
|
||||
}
|
||||
|
||||
// Prometheus
|
||||
// function sendPromMetric(name: string, value?: number) {
|
||||
// if (IS_WEB) {
|
||||
// let url = new URL(SDK_API_PATH + '/metric/ui');
|
||||
// const params = { name: name, value: value ? value.toString() : '' };
|
||||
// url.search = new URLSearchParams(params).toString();
|
||||
// return fetch(url, { method: 'post' });
|
||||
// }
|
||||
// }
|
||||
|
||||
const MatomoInstance = new MatomoTracker({
|
||||
urlBase: MATOMO_URL,
|
||||
siteId: MATOMO_ID, // optional, default value: `1`
|
||||
// heartBeat: { // optional, enabled by default
|
||||
// active: true, // optional, default value: true
|
||||
// seconds: 10 // optional, default value: `15
|
||||
// },
|
||||
// linkTracking: false // optional, default value: true
|
||||
});
|
||||
|
||||
analytics.pageView(generateInitialUrl(window.location.hash));
|
||||
|
|
|
@ -143,7 +143,6 @@ function App(props: Props) {
|
|||
const shouldMigrateLanguage = LANGUAGE_MIGRATIONS[language];
|
||||
const hasActiveChannelClaim = activeChannelId !== undefined;
|
||||
const isPersonalized = hasVerifiedEmail;
|
||||
const renderFiledrop = isAuthenticated;
|
||||
|
||||
useEffect(() => {
|
||||
if (userId) {
|
||||
|
@ -349,7 +348,7 @@ function App(props: Props) {
|
|||
>
|
||||
<Router />
|
||||
<ModalRouter />
|
||||
{renderFiledrop && <FileDrop />}
|
||||
<FileDrop />
|
||||
<FileRenderFloating />
|
||||
{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;
|
|
@ -1,6 +1,5 @@
|
|||
// @flow
|
||||
import { Combobox, ComboboxInput, ComboboxPopover, ComboboxList, ComboboxOption } from '@reach/combobox';
|
||||
// import '@reach/combobox/styles.css'; --> 'scss/third-party.scss'
|
||||
import { matchSorter } from 'match-sorter';
|
||||
import React from 'react';
|
||||
import classnames from 'classnames';
|
||||
|
@ -118,7 +117,7 @@ export default function BlockList(props: Props) {
|
|||
return (
|
||||
<>
|
||||
<div className="help--notice">{help}</div>
|
||||
<div className="section">
|
||||
<div className="section" style={{ zIndex: '4' }}>
|
||||
<SearchList
|
||||
list={localList}
|
||||
placeholder={__('e.g. odysee')}
|
||||
|
|
|
@ -5,7 +5,6 @@ import Icon from 'component/common/icon';
|
|||
import classnames from 'classnames';
|
||||
import { NavLink } from 'react-router-dom';
|
||||
import { formatLbryUrlForWeb } from 'util/url';
|
||||
import * as PAGES from 'constants/pages';
|
||||
import useCombinedRefs from 'effects/use-combined-refs';
|
||||
|
||||
type Props = {
|
||||
|
@ -34,7 +33,6 @@ type Props = {
|
|||
onMouseLeave: ?(any) => any,
|
||||
pathname: string,
|
||||
emailVerified: boolean,
|
||||
requiresAuth: ?boolean,
|
||||
myref: any,
|
||||
dispatch: any,
|
||||
'aria-label'?: string,
|
||||
|
@ -66,7 +64,6 @@ const Button = forwardRef<any, {}>((props: Props, ref: any) => {
|
|||
iconColor,
|
||||
activeClass,
|
||||
emailVerified,
|
||||
requiresAuth,
|
||||
myref,
|
||||
dispatch, // <button> doesn't know what to do with dispatch
|
||||
pathname,
|
||||
|
@ -75,7 +72,7 @@ const Button = forwardRef<any, {}>((props: Props, ref: any) => {
|
|||
...otherProps
|
||||
} = props;
|
||||
|
||||
const disable = disabled || (user === null && requiresAuth);
|
||||
const disable = disabled;
|
||||
|
||||
const combinedClassName = classnames(
|
||||
'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 ? (
|
||||
<NavLink
|
||||
exact
|
||||
|
|
|
@ -6,6 +6,7 @@ import CreditAmount from 'component/common/credit-amount';
|
|||
import DateTime from 'component/dateTime';
|
||||
import YoutubeBadge from 'component/youtubeBadge';
|
||||
import SUPPORTED_LANGUAGES from 'constants/supported_languages';
|
||||
import { formatNumber } from 'util/number';
|
||||
|
||||
type Props = {
|
||||
claim: ChannelClaim,
|
||||
|
@ -74,7 +75,7 @@ function ChannelAbout(props: Props) {
|
|||
</div>
|
||||
|
||||
<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>
|
||||
<div className="media__info-text">
|
||||
|
|
|
@ -3,10 +3,9 @@ import * as MODALS from 'constants/modal_types';
|
|||
import * as ICONS from 'constants/icons';
|
||||
import React from 'react';
|
||||
import classnames from 'classnames';
|
||||
import { FormField } from 'component/common/form';
|
||||
import { FormField, FormFieldAreaAdvanced } from 'component/common/form';
|
||||
import Button from 'component/button';
|
||||
import TagsSearch from 'component/tagsSearch';
|
||||
import { FF_MAX_CHARS_IN_DESCRIPTION } from 'constants/form-field';
|
||||
import ErrorText from 'component/common/error-text';
|
||||
import ChannelThumbnail from 'component/channelThumbnail';
|
||||
import { isNameValid, parseURI } from 'util/lbryURI';
|
||||
|
@ -27,6 +26,9 @@ import Gerbil from 'component/channelThumbnail/gerbil.png';
|
|||
const LANG_NONE = 'none';
|
||||
|
||||
const MAX_TAG_SELECT = 5;
|
||||
const MAX_NAME_LEN = 128;
|
||||
const MAX_TITLE_LEN = 255;
|
||||
const MAX_DESCRIPTION_LEN = 2056;
|
||||
|
||||
type Props = {
|
||||
claim: ChannelClaim,
|
||||
|
@ -92,10 +94,11 @@ function ChannelForm(props: Props) {
|
|||
const [nameError, setNameError] = React.useState(undefined);
|
||||
const [bidError, setBidError] = React.useState('');
|
||||
const [isUpload, setIsUpload] = React.useState({ cover: false, thumbnail: false });
|
||||
const [coverError, setCoverError] = React.useState(false);
|
||||
const [thumbError, setThumbError] = React.useState(false);
|
||||
const { claim_id: claimId } = claim || {};
|
||||
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 name = params.name;
|
||||
const isNewChannel = !uri;
|
||||
|
@ -204,7 +207,8 @@ function ChannelForm(props: Props) {
|
|||
setThumbError(false);
|
||||
}
|
||||
|
||||
function handleCoverChange(coverUrl: string, uploadSelected: boolean) {
|
||||
function handleCoverChange(coverUrl: string, uploadSelected: boolean, preview: ?string) {
|
||||
setCoverPreview(preview || '');
|
||||
setParams({ ...params, coverUrl });
|
||||
setIsUpload({ ...isUpload, cover: uploadSelected });
|
||||
setCoverError(false);
|
||||
|
@ -257,7 +261,7 @@ function ChannelForm(props: Props) {
|
|||
}
|
||||
}, [hasClaimedInitialRewards, claimInitialRewards]);
|
||||
|
||||
const coverSrc = coverError ? ThumbnailBrokenImage : params.coverUrl;
|
||||
const coverSrc = coverError ? ThumbnailBrokenImage : coverPreview;
|
||||
|
||||
let thumbnailPreview;
|
||||
if (!params.thumbnailUrl) {
|
||||
|
@ -271,7 +275,7 @@ function ChannelForm(props: Props) {
|
|||
// TODO clear and bail after submit
|
||||
return (
|
||||
<>
|
||||
<div className={classnames('main--contained', { 'card--disabled': disabled })}>
|
||||
<div className={classnames({ 'card--disabled': disabled })}>
|
||||
<header className="channel-cover">
|
||||
<div className="channel__quick-actions">
|
||||
<Button
|
||||
|
@ -279,7 +283,7 @@ function ChannelForm(props: Props) {
|
|||
title={__('Cover')}
|
||||
onClick={() =>
|
||||
openModal(MODALS.IMAGE_UPLOAD, {
|
||||
onUpdate: (coverUrl, isUpload) => handleCoverChange(coverUrl, isUpload),
|
||||
onUpdate: (coverUrl, isUpload, preview) => handleCoverChange(coverUrl, isUpload, preview),
|
||||
title: __('Edit Cover Image'),
|
||||
helpText: __('(6.25:1)'),
|
||||
assetName: __('Cover Image'),
|
||||
|
@ -321,7 +325,6 @@ function ChannelForm(props: Props) {
|
|||
uri={uri}
|
||||
thumbnailPreview={thumbnailPreview}
|
||||
allowGifs
|
||||
showDelayedMessage={isUpload.thumbnail}
|
||||
setThumbUploadError={setThumbError}
|
||||
thumbUploadError={thumbError}
|
||||
/>
|
||||
|
@ -332,7 +335,7 @@ function ChannelForm(props: Props) {
|
|||
<div className="channel-cover__gradient" />
|
||||
</header>
|
||||
|
||||
<Tabs>
|
||||
<Tabs className="channelPage-wrapper">
|
||||
<TabList className="tabs__list--channel-page">
|
||||
<Tab>{__('General')}</Tab>
|
||||
<Tab>{__('Credit Details')}</Tab>
|
||||
|
@ -359,6 +362,7 @@ function ChannelForm(props: Props) {
|
|||
error={nameError}
|
||||
disabled={!isNewChannel}
|
||||
onChange={(e) => setParams({ ...params, name: e.target.value })}
|
||||
maxLength={MAX_NAME_LEN}
|
||||
/>
|
||||
</fieldset-group>
|
||||
{!isNewChannel && <span className="form-field__help">{__('This field cannot be changed.')}</span>}
|
||||
|
@ -370,15 +374,16 @@ function ChannelForm(props: Props) {
|
|||
placeholder={__('My Awesome Channel')}
|
||||
value={params.title}
|
||||
onChange={(e) => setParams({ ...params, title: e.target.value })}
|
||||
maxLength={MAX_TITLE_LEN}
|
||||
/>
|
||||
<FormField
|
||||
<FormFieldAreaAdvanced
|
||||
type="markdown"
|
||||
name="content_description2"
|
||||
label={__('Description')}
|
||||
placeholder={__('Description of your content')}
|
||||
value={params.description}
|
||||
onChange={(text) => setParams({ ...params, description: text })}
|
||||
textAreaMaxLength={FF_MAX_CHARS_IN_DESCRIPTION}
|
||||
textAreaMaxLength={MAX_DESCRIPTION_LEN}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@ type Props = {
|
|||
level: number,
|
||||
large?: boolean,
|
||||
inline?: boolean,
|
||||
hideTooltip?: Boolean,
|
||||
};
|
||||
|
||||
function getChannelIcon(level: number): string {
|
||||
|
@ -28,7 +29,7 @@ function getChannelIcon(level: number): string {
|
|||
}
|
||||
|
||||
function ChannelStakedIndicator(props: Props) {
|
||||
const { channelClaim, amount, level, large = false, inline = false } = props;
|
||||
const { channelClaim, amount, level, large = false, inline = false, hideTooltip } = props;
|
||||
|
||||
if (!channelClaim || !channelClaim.meta) {
|
||||
return null;
|
||||
|
@ -37,23 +38,36 @@ function ChannelStakedIndicator(props: Props) {
|
|||
const isControlling = channelClaim && channelClaim.meta.is_controlling;
|
||||
const icon = getChannelIcon(level);
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
title={
|
||||
<div className="channel-staked__tooltip">
|
||||
<div className="channel-staked__tooltip-icons">
|
||||
<LevelIcon icon={icon} isControlling={isControlling} size={isControlling ? 14 : 10} />
|
||||
</div>
|
||||
if (!hideTooltip) {
|
||||
return (
|
||||
<Tooltip
|
||||
title={
|
||||
<div className="channel-staked__tooltip">
|
||||
<div className="channel-staked__tooltip-icons">
|
||||
<LevelIcon icon={icon} isControlling={isControlling} size={isControlling ? 14 : 10} />
|
||||
</div>
|
||||
|
||||
<div className="channel-staked__tooltip-text">
|
||||
<span>{__('Level %current_level%', { current_level: level })}</span>
|
||||
<div className="channel-staked__amount">
|
||||
<LbcSymbol postfix={<CreditAmount amount={amount} showLBC={false} />} size={14} />
|
||||
<div className="channel-staked__tooltip-text">
|
||||
<span>{__('Level %current_level%', { current_level: level })}</span>
|
||||
<div className="channel-staked__amount">
|
||||
<LbcSymbol postfix={<CreditAmount amount={amount} showLBC={false} />} size={14} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div
|
||||
className={classnames('channel-staked__wrapper', {
|
||||
'channel-staked__wrapper--large': large,
|
||||
'channel-staked__wrapper--inline': inline,
|
||||
})}
|
||||
>
|
||||
<LevelIcon icon={icon} large={large} isControlling={isControlling} />
|
||||
</div>
|
||||
}
|
||||
>
|
||||
</Tooltip>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<div
|
||||
className={classnames('channel-staked__wrapper', {
|
||||
'channel-staked__wrapper--large': large,
|
||||
|
@ -62,8 +76,8 @@ function ChannelStakedIndicator(props: Props) {
|
|||
>
|
||||
<LevelIcon icon={icon} large={large} isControlling={isControlling} />
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
type LevelIconProps = {
|
||||
|
|
|
@ -23,6 +23,7 @@ type Props = {
|
|||
showDelayedMessage?: boolean,
|
||||
noLazyLoad?: boolean,
|
||||
hideStakedIndicator?: boolean,
|
||||
hideTooltip?: boolean,
|
||||
xsmall?: boolean,
|
||||
noOptimization?: boolean,
|
||||
setThumbUploadError: (boolean) => void,
|
||||
|
@ -45,17 +46,28 @@ function ChannelThumbnail(props: Props) {
|
|||
showDelayedMessage = false,
|
||||
noLazyLoad,
|
||||
hideStakedIndicator = false,
|
||||
hideTooltip,
|
||||
setThumbUploadError,
|
||||
ThumbUploadError,
|
||||
} = props;
|
||||
const [retries, setRetries] = React.useState(3);
|
||||
const [thumbLoadError, setThumbLoadError] = React.useState(ThumbUploadError);
|
||||
const shouldResolve = claim === undefined;
|
||||
const shouldResolve = !isResolving && claim === undefined;
|
||||
const thumbnail = rawThumbnail && rawThumbnail.trim().replace(/^http:\/\//i, 'https://');
|
||||
const thumbnailPreview = rawThumbnailPreview && rawThumbnailPreview.trim().replace(/^http:\/\//i, 'https://');
|
||||
const defaultAvatar = AVATAR_DEFAULT || Gerbil;
|
||||
const channelThumbnail = thumbnailPreview || thumbnail || defaultAvatar;
|
||||
const isGif = channelThumbnail && channelThumbnail.endsWith('gif');
|
||||
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
|
||||
const { channelName } = parseURI(uri);
|
||||
|
@ -77,7 +89,7 @@ function ChannelThumbnail(props: Props) {
|
|||
if (isGif && !allowGifs) {
|
||||
return (
|
||||
<FreezeframeWrapper src={channelThumbnail} className={classnames('channel-thumbnail', className)}>
|
||||
{!hideStakedIndicator && <ChannelStakedIndicator uri={uri} claim={claim} />}
|
||||
{!hideStakedIndicator && <ChannelStakedIndicator uri={uri} claim={claim} hideTooltip={hideTooltip} />}
|
||||
</FreezeframeWrapper>
|
||||
);
|
||||
}
|
||||
|
@ -91,21 +103,30 @@ function ChannelThumbnail(props: Props) {
|
|||
'channel-thumbnail--resolving': isResolving,
|
||||
})}
|
||||
>
|
||||
{/* show delay necessary? */}
|
||||
{showDelayedMessage ? (
|
||||
<div className="channel-thumbnail--waiting">{__('This will be visible in a few minutes.')}</div>
|
||||
) : (
|
||||
<OptimizedImage
|
||||
alt={__('Channel profile picture')}
|
||||
className={!channelThumbnail ? 'channel-thumbnail__default' : 'channel-thumbnail__custom'}
|
||||
src={(!thumbLoadError && channelThumbnail) || defaultAvatar}
|
||||
src={avatarSrc}
|
||||
loading={noLazyLoad ? undefined : 'lazy'}
|
||||
onError={() => {
|
||||
setRetries((retries) => retries - 1);
|
||||
if (setThumbUploadError) {
|
||||
setThumbUploadError(true);
|
||||
} else {
|
||||
setThumbLoadError(true);
|
||||
}
|
||||
}}
|
||||
onLoad={() => {
|
||||
if (setThumbUploadError) {
|
||||
setThumbUploadError(false);
|
||||
} else {
|
||||
setThumbLoadError(false);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{!hideStakedIndicator && <ChannelStakedIndicator uri={uri} claim={claim} />}
|
||||
|
|
|
@ -115,19 +115,13 @@ const ClaimCollectionAdd = (props: Props) => {
|
|||
inputButton={
|
||||
<>
|
||||
<Button
|
||||
button={'alt'}
|
||||
icon={ICONS.ADD}
|
||||
className={'button-toggle'}
|
||||
disabled={!newCollectionName.length}
|
||||
onClick={() => handleAddCollection()}
|
||||
ref={buttonref}
|
||||
/>
|
||||
<Button
|
||||
button={'alt'}
|
||||
className={'button-toggle'}
|
||||
icon={ICONS.REMOVE}
|
||||
onClick={() => handleClearNew()}
|
||||
/>
|
||||
<Button className={'button-toggle'} icon={ICONS.REMOVE} onClick={() => handleClearNew()} />
|
||||
</>
|
||||
}
|
||||
onChange={handleNameInput}
|
||||
|
|
|
@ -102,6 +102,7 @@ export default function ClaimList(props: Props) {
|
|||
|
||||
let tileUris = (prefixUris || []).concat(uris || []);
|
||||
tileUris = tileUris.filter((uri) => !excludeUris.includes(uri));
|
||||
if (prefixUris && prefixUris.length) tileUris.splice(prefixUris.length * -1, prefixUris.length);
|
||||
|
||||
const totalLength = tileUris.length;
|
||||
|
||||
|
@ -143,7 +144,7 @@ export default function ClaimList(props: Props) {
|
|||
const mainEl = document.querySelector(`.${MAIN_CLASS}`);
|
||||
|
||||
if (mainEl && !loading && urisLength >= pageSize) {
|
||||
const contentWrapperAtBottomOfPage = mainEl.getBoundingClientRect().bottom - 0.5 <= window.innerHeight;
|
||||
const contentWrapperAtBottomOfPage = mainEl.getBoundingClientRect().bottom - 1.5 <= window.innerHeight;
|
||||
|
||||
if (contentWrapperAtBottomOfPage) {
|
||||
onScrollBottom();
|
||||
|
|
|
@ -230,7 +230,6 @@ function ClaimListHeader(props: Props) {
|
|||
{CS.ORDER_BY_TYPES.map((type) => (
|
||||
<Button
|
||||
key={type}
|
||||
button="alt"
|
||||
onClick={(e) =>
|
||||
handleChange({
|
||||
key: CS.ORDER_BY_KEY,
|
||||
|
@ -251,7 +250,6 @@ function ClaimListHeader(props: Props) {
|
|||
<div className="claim-search__menu-group">
|
||||
{!hideAdvancedFilter && (
|
||||
<Button
|
||||
button="alt"
|
||||
aria-label={__('More')}
|
||||
className={classnames(`button-toggle button-toggle--top button-toggle--more`, {
|
||||
'button-toggle--custom': isFiltered(),
|
||||
|
@ -261,7 +259,8 @@ function ClaimListHeader(props: Props) {
|
|||
onClick={() => setExpanded(!expanded)}
|
||||
/>
|
||||
)}
|
||||
|
||||
</div>
|
||||
<div className="claim-search__menu-group">
|
||||
{tileLayout !== undefined && (
|
||||
<Button
|
||||
onClick={() => {
|
||||
|
|
|
@ -39,7 +39,7 @@ type Props = {
|
|||
doChannelUnmute: (string) => void,
|
||||
doCommentModBlock: (string) => void,
|
||||
doCommentModUnBlock: (string) => void,
|
||||
doCommentModBlockAsAdmin: (string, string) => void,
|
||||
doCommentModBlockAsAdmin: (string, ?string, ?string) => void,
|
||||
doCommentModUnBlockAsAdmin: (string, string) => void,
|
||||
doCollectionEdit: (string, any) => void,
|
||||
hasClaimInWatchLater: boolean,
|
||||
|
@ -110,6 +110,11 @@ function ClaimMenuList(props: Props) {
|
|||
const [doShuffle, setDoShuffle] = React.useState(false);
|
||||
const incognitoClaim = contentChannelUri && !contentChannelUri.includes('@');
|
||||
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 showDelete = claimIsMine || (fileInfo && (fileInfo.written_bytes > 0 || fileInfo.blobs_completed > 0));
|
||||
const subscriptionLabel = repostedClaim
|
||||
|
@ -232,7 +237,7 @@ function ClaimMenuList(props: Props) {
|
|||
if (channelIsAdminBlocked) {
|
||||
doCommentModUnBlockAsAdmin(contentChannelUri, '');
|
||||
} else {
|
||||
doCommentModBlockAsAdmin(contentChannelUri, '');
|
||||
doCommentModBlockAsAdmin(contentChannelUri, undefined, undefined);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -297,18 +302,20 @@ function ClaimMenuList(props: Props) {
|
|||
{__('View List')}
|
||||
</a>
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
className="comment__menu-option"
|
||||
onSelect={() => {
|
||||
if (!resolvedList) fetchItems();
|
||||
setDoShuffle(true);
|
||||
}}
|
||||
>
|
||||
<div className="menu__link">
|
||||
<Icon aria-hidden icon={ICONS.SHUFFLE} />
|
||||
{__('Shuffle Play')}
|
||||
</div>
|
||||
</MenuItem>
|
||||
{!isEmptyCollection && (
|
||||
<MenuItem
|
||||
className="comment__menu-option"
|
||||
onSelect={() => {
|
||||
if (!resolvedList) fetchItems();
|
||||
setDoShuffle(true);
|
||||
}}
|
||||
>
|
||||
<div className="menu__link">
|
||||
<Icon aria-hidden icon={ICONS.SHUFFLE} />
|
||||
{__('Shuffle Play')}
|
||||
</div>
|
||||
</MenuItem>
|
||||
)}
|
||||
{isMyCollection && (
|
||||
<>
|
||||
<MenuItem
|
||||
|
|
|
@ -11,17 +11,24 @@ function ClaimPreviewLoading(props: Props) {
|
|||
const { isChannel, type } = props;
|
||||
return (
|
||||
<li
|
||||
className={classnames('claim-preview__wrapper', {
|
||||
className={classnames('placeholder claim-preview__wrapper', {
|
||||
'claim-preview__wrapper--channel': isChannel && type !== 'inline',
|
||||
'claim-preview__wrapper--inline': type === 'inline',
|
||||
'claim-preview__wrapper--small': type === 'small',
|
||||
})}
|
||||
>
|
||||
<div className={classnames('claim-preview', { 'claim-preview--large': type === 'large' })}>
|
||||
<div className="placeholder media__thumb" />
|
||||
<div className="media__thumb" />
|
||||
<div className="placeholder__wrapper">
|
||||
<div className="placeholder claim-preview__title" />
|
||||
<div className="placeholder media__subtitle" />
|
||||
<div className="claim-preview__title" />
|
||||
<div className="claim-preview__title_b" />
|
||||
<div className="claim-tile__info">
|
||||
<div className="channel-thumbnail" />
|
||||
<div className="claim-tile__about">
|
||||
<div className="media__subtitle" />
|
||||
<div className="media__subtitle_b" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
|
|
|
@ -9,7 +9,7 @@ import * as COLLECTIONS_CONSTS from 'constants/collections';
|
|||
import { isChannelClaim } from 'util/claim';
|
||||
import { formatLbryUrlForWeb } from 'util/url';
|
||||
import { formatClaimPreviewTitle } from 'util/formatAriaLabel';
|
||||
import { toCompactNotation } from 'util/string';
|
||||
import { formatNumber } from 'util/number';
|
||||
import Tooltip from 'component/common/tooltip';
|
||||
import FileThumbnail from 'component/fileThumbnail';
|
||||
import UriIndicator from 'component/uriIndicator';
|
||||
|
@ -30,6 +30,7 @@ import ClaimPreviewLoading from './claim-preview-loading';
|
|||
import ClaimPreviewHidden from './claim-preview-no-mature';
|
||||
import ClaimPreviewNoContent from './claim-preview-no-content';
|
||||
import CollectionEditButtons from 'component/collectionEditButtons';
|
||||
import { useIsMobile } from 'effects/use-screensize';
|
||||
import AbandonedChannelPreview from 'component/abandonedChannelPreview';
|
||||
|
||||
// preview images used on the landing page and on the channel page
|
||||
|
@ -137,12 +138,13 @@ const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
|
|||
indexInContainer,
|
||||
channelSubCount,
|
||||
swipeLayout = false,
|
||||
lang,
|
||||
showEdit,
|
||||
dragHandleProps,
|
||||
unavailableUris,
|
||||
} = props;
|
||||
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
const isCollection = claim && claim.value_type === 'collection';
|
||||
const collectionClaimId = isCollection && claim && claim.claim_id;
|
||||
const listId = collectionId || collectionClaimId;
|
||||
|
@ -159,16 +161,19 @@ const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
|
|||
if (channelSubCount === undefined) {
|
||||
return <span />;
|
||||
}
|
||||
const formattedSubCount = toCompactNotation(channelSubCount, lang, 10000);
|
||||
const formattedSubCount = formatNumber(channelSubCount, 2, true);
|
||||
const formattedSubCountLocale = formatNumber(channelSubCount, 2, false);
|
||||
return (
|
||||
<Tooltip title={channelSubCount} followCursor placement="top">
|
||||
<span className="claim-preview__channel-sub-count">
|
||||
{channelSubCount === 1 ? __('1 Follower') : __('%formattedSubCount% Followers', { formattedSubCount })}
|
||||
</span>
|
||||
</Tooltip>
|
||||
<div className="media__subtitle">
|
||||
<Tooltip title={formattedSubCountLocale} followCursor placement="top">
|
||||
<span className="claim-preview__channel-sub-count">
|
||||
{channelSubCount === 1 ? __('1 Follower') : __('%formattedSubCount% Followers', { formattedSubCount })}
|
||||
</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
}, [channelSubCount]);
|
||||
const isValid = uri && isURIValid(uri);
|
||||
const isValid = uri && isURIValid(uri, false);
|
||||
|
||||
// $FlowFixMe
|
||||
const isPlayable =
|
||||
|
@ -347,7 +352,7 @@ const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
|
|||
<>
|
||||
{!pending ? (
|
||||
<NavLink aria-hidden tabIndex={-1} {...navLinkProps}>
|
||||
<FileThumbnail thumbnail={thumbnailUrl}>
|
||||
<FileThumbnail uri={uri} thumbnail={thumbnailUrl}>
|
||||
<div className="claim-preview__hover-actions">
|
||||
{isPlayable && <FileWatchLaterLink focusable={false} uri={repostedContentUri} />}
|
||||
</div>
|
||||
|
@ -359,12 +364,12 @@ const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
|
|||
</div>
|
||||
{/* @endif */}
|
||||
<div className="claim-preview__file-property-overlay">
|
||||
<PreviewOverlayProperties uri={uri} />
|
||||
<PreviewOverlayProperties uri={uri} small={type === 'small'} />
|
||||
</div>
|
||||
</FileThumbnail>
|
||||
</NavLink>
|
||||
) : (
|
||||
<FileThumbnail thumbnail={thumbnailUrl} />
|
||||
<FileThumbnail uri={uri} thumbnail={thumbnailUrl} />
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
@ -380,9 +385,18 @@ const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
|
|||
</NavLink>
|
||||
)}
|
||||
</div>
|
||||
<ClaimPreviewSubtitle uri={uri} type={type} />
|
||||
{(pending || !!reflectingProgress) && <PublishPending uri={uri} />}
|
||||
{channelSubscribers}
|
||||
<div className="claim-tile__info" uri={uri}>
|
||||
{!isChannelUri && signingChannel && (
|
||||
<div className="claim-preview__channel-staked">
|
||||
<UriIndicator focusable={false} uri={uri} link hideAnonymous>
|
||||
<ChannelThumbnail uri={signingChannel.permanent_url} xsmall />
|
||||
</UriIndicator>
|
||||
</div>
|
||||
)}
|
||||
<ClaimPreviewSubtitle uri={uri} type={type} />
|
||||
{(pending || !!reflectingProgress) && <PublishPending uri={uri} />}
|
||||
{channelSubscribers}
|
||||
</div>
|
||||
</div>
|
||||
{type !== 'small' && (
|
||||
<div className="claim-preview__actions">
|
||||
|
@ -393,11 +407,6 @@ const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
|
|||
actions
|
||||
) : (
|
||||
<div className="claim-preview__primary-actions">
|
||||
{!isChannelUri && signingChannel && (
|
||||
<div className="claim-preview__channel-staked">
|
||||
<ChannelThumbnail uri={signingChannel.permanent_url} xsmall />
|
||||
</div>
|
||||
)}
|
||||
{isChannelUri && !banState.muted && !claimIsMine && (
|
||||
<SubscribeButton
|
||||
uri={repostedChannelUri || (uri.startsWith('lbry://') ? uri : `lbry://${uri}`)}
|
||||
|
@ -411,13 +420,11 @@ const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
|
|||
)}
|
||||
{claim && (
|
||||
<React.Fragment>
|
||||
{typeof properties === 'function' ? (
|
||||
properties(claim)
|
||||
) : properties !== undefined ? (
|
||||
properties
|
||||
) : (
|
||||
<ClaimTags uri={uri} type={type} />
|
||||
)}
|
||||
{typeof properties === 'function'
|
||||
? properties(claim)
|
||||
: properties !== undefined
|
||||
? properties
|
||||
: !isMobile && <ClaimTags uri={uri} type={type} />}
|
||||
</React.Fragment>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
@ -5,6 +5,7 @@ import DateTime from 'component/dateTime';
|
|||
import Button from 'component/button';
|
||||
import FileViewCountInline from 'component/fileViewCountInline';
|
||||
import { parseURI } from 'util/lbryURI';
|
||||
import { formatNumber } from 'util/number';
|
||||
|
||||
type Props = {
|
||||
uri: string,
|
||||
|
@ -34,7 +35,7 @@ function ClaimPreviewSubtitle(props: Props) {
|
|||
<>
|
||||
{isChannel &&
|
||||
type !== 'inline' &&
|
||||
`${claimsInChannel} ${claimsInChannel === 1 ? __('upload') : __('uploads')}`}
|
||||
`${formatNumber(claimsInChannel, 2, true)} ${claimsInChannel === 1 ? __('upload') : __('uploads')}`}
|
||||
|
||||
{!isChannel && (
|
||||
<>
|
||||
|
|
|
@ -8,7 +8,6 @@ import TruncatedText from 'component/common/truncated-text';
|
|||
import DateTime from 'component/dateTime';
|
||||
import ChannelThumbnail from 'component/channelThumbnail';
|
||||
import FileViewCountInline from 'component/fileViewCountInline';
|
||||
import SubscribeButton from 'component/subscribeButton';
|
||||
import useGetThumbnail from 'effects/use-get-thumbnail';
|
||||
import { formatLbryUrlForWeb, generateListSearchUrlParams } from 'util/url';
|
||||
import { formatClaimPreviewTitle } from 'util/formatAriaLabel';
|
||||
|
@ -85,7 +84,6 @@ function ClaimPreviewTile(props: Props) {
|
|||
const shouldFetch = claim === undefined;
|
||||
const thumbnailUrl = useGetThumbnail(uri, claim, streamingUrl, getFile, placeholder) || thumbnail;
|
||||
const canonicalUrl = claim && claim.canonical_url;
|
||||
const permanentUrl = claim && claim.permanent_url;
|
||||
const repostedContentUri = claim && (claim.reposted_claim ? claim.reposted_claim.permanent_url : claim.permanent_url);
|
||||
const listId = collectionId || collectionClaimId;
|
||||
const navigateUrl =
|
||||
|
@ -110,7 +108,6 @@ function ClaimPreviewTile(props: Props) {
|
|||
const isChannel = claim && claim.value_type === 'channel';
|
||||
const channelUri = !isChannel ? signingChannel && signingChannel.permanent_url : claim && claim.permanent_url;
|
||||
const channelTitle = signingChannel && ((signingChannel.value && signingChannel.value.title) || signingChannel.name);
|
||||
const repostedChannelUri = isRepost && isChannel ? permanentUrl || canonicalUrl : undefined;
|
||||
|
||||
// Aria-label value for claim preview
|
||||
let ariaLabelData = isChannel ? title : formatClaimPreviewTitle(title, channelTitle, date, mediaDuration);
|
||||
|
@ -148,17 +145,24 @@ function ClaimPreviewTile(props: Props) {
|
|||
|
||||
if (placeholder || (!claim && isResolvingUri)) {
|
||||
return (
|
||||
<li className={classnames('claim-preview--tile', {})}>
|
||||
<div className="placeholder media__thumb">
|
||||
<li className={classnames('placeholder claim-preview--tile', {})}>
|
||||
<div className="media__thumb">
|
||||
<img src={PlaceholderTx} alt="Placeholder" />
|
||||
</div>
|
||||
<div className="placeholder__wrapper">
|
||||
<div className="placeholder claim-tile__title" />
|
||||
<div className="claim-tile__title" />
|
||||
<div className="claim-tile__title_b" />
|
||||
<div
|
||||
className={classnames('claim-tile__info placeholder', {
|
||||
className={classnames('claim-tile__info', {
|
||||
contains_view_count: shouldShowViewCount,
|
||||
})}
|
||||
/>
|
||||
>
|
||||
<div className="channel-thumbnail" />
|
||||
<div className="claim-tile__about">
|
||||
<div className="button__content" />
|
||||
<div className="claim-tile__about--counts" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
|
@ -175,7 +179,7 @@ function ClaimPreviewTile(props: Props) {
|
|||
})}
|
||||
>
|
||||
<NavLink {...navLinkProps} role="none" tabIndex={-1} aria-hidden>
|
||||
<FileThumbnail thumbnail={thumbnailUrl} allowGifs>
|
||||
<FileThumbnail uri={uri} thumbnail={thumbnailUrl} allowGifs>
|
||||
{!isChannel && (
|
||||
<React.Fragment>
|
||||
<div className="claim-preview__hover-actions">
|
||||
|
@ -220,11 +224,7 @@ function ClaimPreviewTile(props: Props) {
|
|||
contains_view_count: shouldShowViewCount,
|
||||
})}
|
||||
>
|
||||
{isChannel ? (
|
||||
<div className="claim-tile__about--channel">
|
||||
<SubscribeButton uri={repostedChannelUri || uri} />
|
||||
</div>
|
||||
) : (
|
||||
{!isChannel && (
|
||||
<React.Fragment>
|
||||
<UriIndicator focusable={false} uri={uri} link hideAnonymous>
|
||||
<ChannelThumbnail uri={channelUri} xsmall />
|
||||
|
|
|
@ -19,8 +19,11 @@ function ClaimRepostAuthor(props: Props) {
|
|||
if (short && repostUrl) {
|
||||
return (
|
||||
<span className="claim-preview__repost-author">
|
||||
<Icon icon={ICONS.REPOST} size={12} />
|
||||
<span>{repostUrl}</span>
|
||||
<div className="claim-preview__repost-ribbon">
|
||||
<Icon icon={ICONS.REPOST} size={12} />
|
||||
<br />
|
||||
<span>{repostUrl}</span>
|
||||
</div>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
@ -28,16 +31,18 @@ function ClaimRepostAuthor(props: Props) {
|
|||
if (repostUrl && !repostChannelUrl) {
|
||||
return (
|
||||
<div className="claim-preview__repost-author">
|
||||
<Icon icon={ICONS.REPOST} size={10} />
|
||||
<span>
|
||||
<I18nMessage
|
||||
tokens={{
|
||||
anonymous: <strong>{__('Anonymous --[used in <%anonymous% Reposted>]--')}</strong>,
|
||||
}}
|
||||
>
|
||||
%anonymous% Reposted
|
||||
</I18nMessage>
|
||||
</span>
|
||||
<div className="claim-preview__repost-ribbon claim-preview__repost-ribbon--anon">
|
||||
<Icon icon={ICONS.REPOST} size={10} />
|
||||
<span>
|
||||
<I18nMessage
|
||||
tokens={{
|
||||
anonymous: <strong>{__('Anon --[used in <%anonymous% Reposted>]--')}</strong>,
|
||||
}}
|
||||
>
|
||||
%anonymous%
|
||||
</I18nMessage>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -47,10 +52,11 @@ function ClaimRepostAuthor(props: Props) {
|
|||
|
||||
return (
|
||||
<div className="claim-preview__repost-author">
|
||||
<Icon icon={ICONS.REPOST} size={10} className="claim-preview__repost-icon" />
|
||||
<I18nMessage tokens={{ repost_channel_link: <UriIndicator link uri={repostChannelUrl} /> }}>
|
||||
%repost_channel_link% reposted
|
||||
</I18nMessage>
|
||||
<div className="claim-preview__repost-ribbon">
|
||||
<Icon icon={ICONS.REPOST} size={10} className="claim-preview__repost-icon" />
|
||||
<br />
|
||||
<UriIndicator link uri={repostChannelUrl} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -23,7 +23,7 @@ export default function ClaimSupportButton(props: Props) {
|
|||
|
||||
return (
|
||||
<Button
|
||||
button={fileAction ? undefined : 'alt'}
|
||||
button={fileAction ? undefined : 'secondary'}
|
||||
className={classnames({ 'button--file-action': fileAction })}
|
||||
icon={ICONS.LBC}
|
||||
iconSize={fileAction ? 22 : undefined}
|
||||
|
|
|
@ -9,6 +9,7 @@ import { doToggleTagFollowDesktop } from 'redux/actions/tags';
|
|||
import { makeSelectClientSetting, selectShowMatureContent } from 'redux/selectors/settings';
|
||||
import { selectMutedAndBlockedChannelIds } from 'redux/selectors/blocked';
|
||||
import { ENABLE_NO_SOURCE_CLAIMS } from 'config';
|
||||
import { createNormalizedClaimSearchKey } from 'util/claim';
|
||||
|
||||
import ClaimListDiscover from './view';
|
||||
|
||||
|
@ -17,13 +18,22 @@ const select = (state, props) => {
|
|||
const hideReposts = makeSelectClientSetting(SETTINGS.HIDE_REPOSTS)(state);
|
||||
const mutedAndBlockedChannelIds = selectMutedAndBlockedChannelIds(state);
|
||||
|
||||
return {
|
||||
claimSearchByQuery: selectClaimSearchByQuery(state),
|
||||
claimsByUri: selectClaimsByUri(state),
|
||||
fetchingClaimSearchByQuery: selectFetchingClaimSearchByQuery(state),
|
||||
const options = resolveSearchOptions({
|
||||
showNsfw,
|
||||
hideReposts,
|
||||
options: resolveSearchOptions({ showNsfw, hideReposts, mutedAndBlockedChannelIds, pageSize: 8, ...props }),
|
||||
mutedAndBlockedChannelIds,
|
||||
pageSize: 8,
|
||||
...props,
|
||||
});
|
||||
const searchKey = createNormalizedClaimSearchKey(options);
|
||||
|
||||
return {
|
||||
claimSearchResults: selectClaimSearchByQuery(state)[searchKey],
|
||||
claimsByUri: selectClaimsByUri(state),
|
||||
fetchingClaimSearch: selectFetchingClaimSearchByQuery(state)[searchKey],
|
||||
showNsfw,
|
||||
hideReposts,
|
||||
optionsStringified: JSON.stringify(options),
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -1,31 +1,8 @@
|
|||
// @flow
|
||||
import type { Node } from 'react';
|
||||
import React from 'react';
|
||||
import { createNormalizedClaimSearchKey } from 'util/claim';
|
||||
import ClaimPreviewTile from 'component/claimPreviewTile';
|
||||
import useFetchViewCount from 'effects/use-fetch-view-count';
|
||||
import usePrevious from 'effects/use-previous';
|
||||
|
||||
type SearchOptions = {
|
||||
page_size: number,
|
||||
page: number,
|
||||
no_totals: boolean,
|
||||
any_tags: Array<string>,
|
||||
channel_ids: Array<string>,
|
||||
claim_ids?: Array<string>,
|
||||
not_channel_ids: Array<string>,
|
||||
not_tags: Array<string>,
|
||||
order_by: Array<string>,
|
||||
languages?: Array<string>,
|
||||
release_time?: string,
|
||||
claim_type?: string | Array<string>,
|
||||
timestamp?: string,
|
||||
fee_amount?: string,
|
||||
limit_claims_per_channel?: number,
|
||||
stream_types?: Array<string>,
|
||||
has_source?: boolean,
|
||||
has_no_source?: boolean,
|
||||
};
|
||||
|
||||
function urisEqual(prev: ?Array<string>, next: ?Array<string>) {
|
||||
if (!prev || !next) {
|
||||
|
@ -68,12 +45,12 @@ type Props = {
|
|||
hasNoSource?: boolean,
|
||||
// --- select ---
|
||||
location: { search: string },
|
||||
claimSearchByQuery: { [string]: Array<string> },
|
||||
claimSearchResults: Array<string>,
|
||||
claimsByUri: { [string]: any },
|
||||
fetchingClaimSearchByQuery: { [string]: boolean },
|
||||
fetchingClaimSearch: boolean,
|
||||
showNsfw: boolean,
|
||||
hideReposts: boolean,
|
||||
options: SearchOptions,
|
||||
optionsStringified: string,
|
||||
// --- perform ---
|
||||
doClaimSearch: ({}) => void,
|
||||
doFetchViewCount: (claimIdCsv: string) => void,
|
||||
|
@ -82,10 +59,10 @@ type Props = {
|
|||
function ClaimTilesDiscover(props: Props) {
|
||||
const {
|
||||
doClaimSearch,
|
||||
claimSearchByQuery,
|
||||
claimSearchResults,
|
||||
claimsByUri,
|
||||
fetchViewCount,
|
||||
fetchingClaimSearchByQuery,
|
||||
fetchingClaimSearch,
|
||||
hasNoSource,
|
||||
renderProperties,
|
||||
// pinUrls,
|
||||
|
@ -93,16 +70,15 @@ function ClaimTilesDiscover(props: Props) {
|
|||
showNoSourceClaims,
|
||||
doFetchViewCount,
|
||||
pageSize = 8,
|
||||
options,
|
||||
optionsStringified,
|
||||
} = props;
|
||||
|
||||
const searchKey = createNormalizedClaimSearchKey(options);
|
||||
const fetchingClaimSearch = fetchingClaimSearchByQuery[searchKey];
|
||||
const claimSearchUris = claimSearchByQuery[searchKey] || [];
|
||||
const isUnfetchedClaimSearch = claimSearchByQuery[searchKey] === undefined;
|
||||
const prevUris = React.useRef();
|
||||
const claimSearchUris = claimSearchResults || [];
|
||||
|
||||
const isUnfetchedClaimSearch = claimSearchResults === undefined;
|
||||
|
||||
// Don't use the query from createNormalizedClaimSearchKey for the effect since that doesn't include page & release_time
|
||||
const optionsStringForEffect = JSON.stringify(options);
|
||||
const shouldPerformSearch = !fetchingClaimSearch && claimSearchUris.length === 0;
|
||||
|
||||
const uris = (prefixUris || []).concat(claimSearchUris);
|
||||
|
@ -124,21 +100,19 @@ function ClaimTilesDiscover(props: Props) {
|
|||
uris.push(...Array(pageSize - uris.length).fill(''));
|
||||
}
|
||||
|
||||
const prevUris = usePrevious(uris);
|
||||
|
||||
useFetchViewCount(fetchViewCount, uris, claimsByUri, doFetchViewCount);
|
||||
|
||||
const finalUris = isUnfetchedClaimSearch && prevUris.current ? prevUris.current : uris;
|
||||
prevUris.current = finalUris;
|
||||
// Run `doClaimSearch`
|
||||
React.useEffect(() => {
|
||||
if (shouldPerformSearch) {
|
||||
const searchOptions = JSON.parse(optionsStringForEffect);
|
||||
const searchOptions = JSON.parse(optionsStringified);
|
||||
doClaimSearch(searchOptions);
|
||||
}
|
||||
}, [doClaimSearch, shouldPerformSearch, optionsStringForEffect]);
|
||||
}, [doClaimSearch, shouldPerformSearch, optionsStringified]);
|
||||
|
||||
// Show previous results while we fetch to avoid blinkies and poor CLS.
|
||||
const finalUris = isUnfetchedClaimSearch && prevUris ? prevUris : uris;
|
||||
|
||||
return (
|
||||
<ul className="claim-grid">
|
||||
{finalUris && finalUris.length
|
||||
|
@ -167,33 +141,33 @@ function ClaimTilesDiscover(props: Props) {
|
|||
|
||||
export default React.memo<Props>(ClaimTilesDiscover, areEqual);
|
||||
|
||||
function debug_trace(val) {
|
||||
if (process.env.DEBUG_TRACE) console.log(`Render due to: ${val}`);
|
||||
// ****************************************************************************
|
||||
// ****************************************************************************
|
||||
|
||||
function trace(key, value) {
|
||||
// @if process.env.DEBUG_TILE_RENDER
|
||||
// $FlowFixMe "cannot coerce certain types".
|
||||
console.log(`[claimTilesDiscover] ${key}: ${value}`); // eslint-disable-line no-console
|
||||
// @endif
|
||||
}
|
||||
|
||||
function areEqual(prev: Props, next: Props) {
|
||||
const prevOptions: SearchOptions = prev.options;
|
||||
const nextOptions: SearchOptions = next.options;
|
||||
|
||||
const prevSearchKey = createNormalizedClaimSearchKey(prevOptions);
|
||||
const nextSearchKey = createNormalizedClaimSearchKey(nextOptions);
|
||||
|
||||
if (prevSearchKey !== nextSearchKey) {
|
||||
debug_trace('search key');
|
||||
return false;
|
||||
}
|
||||
|
||||
// --- Deep-compare ---
|
||||
if (!urisEqual(prev.claimSearchByQuery[prevSearchKey], next.claimSearchByQuery[nextSearchKey])) {
|
||||
debug_trace('claimSearchByQuery');
|
||||
return false;
|
||||
// These are props that are hard to memoize from where it is passed.
|
||||
|
||||
if (prev.claimType !== next.claimType) {
|
||||
// Array<string>: confirm the contents are actually different.
|
||||
if (prev.claimType && next.claimType && JSON.stringify(prev.claimType) !== JSON.stringify(next.claimType)) {
|
||||
trace('claimType', next.claimType);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const ARRAY_KEYS = ['prefixUris', 'channelIds'];
|
||||
for (let i = 0; i < ARRAY_KEYS.length; ++i) {
|
||||
const key = ARRAY_KEYS[i];
|
||||
if (!urisEqual(prev[key], next[key])) {
|
||||
debug_trace(`${key}`);
|
||||
trace(key, next[key]);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
@ -203,22 +177,19 @@ function areEqual(prev: Props, next: Props) {
|
|||
// to update this function. Better to render more than miss an important one.
|
||||
const KEYS_TO_IGNORE = [
|
||||
...ARRAY_KEYS,
|
||||
'claimSearchByQuery',
|
||||
'fetchingClaimSearchByQuery', // We are showing previous results while fetching.
|
||||
'options', // Covered by search-key comparison.
|
||||
'claimType', // Handled above.
|
||||
'claimsByUri', // Used for view-count. Just ignore it for now.
|
||||
'location',
|
||||
'history',
|
||||
'match',
|
||||
'claimsByUri',
|
||||
'doClaimSearch',
|
||||
'doToggleTagFollowDesktop',
|
||||
];
|
||||
|
||||
const propKeys = Object.keys(next);
|
||||
for (let i = 0; i < propKeys.length; ++i) {
|
||||
const pk = propKeys[i];
|
||||
if (!KEYS_TO_IGNORE.includes(pk) && prev[pk] !== next[pk]) {
|
||||
debug_trace(`${pk}`);
|
||||
trace(pk, next[pk]);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -54,9 +54,13 @@ function CollectionActions(props: Props) {
|
|||
const isMobile = useIsMobile();
|
||||
const claimId = claim && claim.claim_id;
|
||||
const webShareable = true; // collections have cost?
|
||||
const isEmptyCollection = !firstItem;
|
||||
|
||||
const doPlay = React.useCallback(
|
||||
(playUri) => {
|
||||
if (!playUri) {
|
||||
return;
|
||||
}
|
||||
const navigateUrl = formatLbryUrlForWeb(playUri);
|
||||
push({
|
||||
pathname: navigateUrl,
|
||||
|
@ -81,6 +85,7 @@ function CollectionActions(props: Props) {
|
|||
icon={ICONS.PLAY}
|
||||
label={__('Play')}
|
||||
title={__('Play')}
|
||||
disabled={isEmptyCollection}
|
||||
onClick={() => {
|
||||
doToggleShuffleList(collectionId, false);
|
||||
doPlay(firstItem);
|
||||
|
@ -91,6 +96,7 @@ function CollectionActions(props: Props) {
|
|||
icon={ICONS.SHUFFLE}
|
||||
label={__('Shuffle Play')}
|
||||
title={__('Shuffle Play')}
|
||||
disabled={isEmptyCollection}
|
||||
onClick={() => {
|
||||
doToggleShuffleList(collectionId, true);
|
||||
setDoShuffle(true);
|
||||
|
@ -177,7 +183,7 @@ function CollectionActions(props: Props) {
|
|||
|
||||
if (isMobile) {
|
||||
return (
|
||||
<div className="media__actions">
|
||||
<div className="media__actions stretch">
|
||||
{lhsSection}
|
||||
{rhsSection}
|
||||
{infoButtons}
|
||||
|
|
|
@ -10,7 +10,6 @@ import Card from 'component/common/card';
|
|||
import Button from 'component/button';
|
||||
import * as PAGES from 'constants/pages';
|
||||
import * as COLLECTIONS_CONSTS from 'constants/collections';
|
||||
import Icon from 'component/common/icon';
|
||||
import * as ICONS from 'constants/icons';
|
||||
|
||||
type Props = {
|
||||
|
@ -58,21 +57,21 @@ export default function CollectionContent(props: Props) {
|
|||
return (
|
||||
<Card
|
||||
isBodyList
|
||||
className="file-page__recommended-collection"
|
||||
className="file-page__playlist-collection"
|
||||
title={
|
||||
<>
|
||||
<span className="file-page__recommended-collection__row">
|
||||
<Icon
|
||||
icon={
|
||||
(id === COLLECTIONS_CONSTS.WATCH_LATER_ID && ICONS.TIME) ||
|
||||
(id === COLLECTIONS_CONSTS.FAVORITES_ID && ICONS.STAR) ||
|
||||
ICONS.STACK
|
||||
}
|
||||
className="icon--margin-right"
|
||||
/>
|
||||
{collectionName}
|
||||
</span>
|
||||
<span className="file-page__recommended-collection__row">
|
||||
<Button
|
||||
button="link"
|
||||
className="file-page__playlist-collection__row"
|
||||
navigate={`/$/${PAGES.LIST}/${id}`}
|
||||
icon={
|
||||
(id === COLLECTIONS_CONSTS.WATCH_LATER_ID && ICONS.TIME) ||
|
||||
(id === COLLECTIONS_CONSTS.FAVORITES_ID && ICONS.STAR) ||
|
||||
ICONS.STACK
|
||||
}
|
||||
label={collectionName}
|
||||
/>
|
||||
<span className="file-page__playlist-collection__row">
|
||||
<Button
|
||||
button="alt"
|
||||
title={__('Loop')}
|
||||
|
@ -93,21 +92,14 @@ export default function CollectionContent(props: Props) {
|
|||
</>
|
||||
}
|
||||
titleActions={
|
||||
<>
|
||||
<div className="card__title-actions--link">
|
||||
{/* TODO: BUTTON TO SAVE COLLECTION - Probably save/copy modal */}
|
||||
<Button label={__('View List')} button="link" navigate={`/$/${PAGES.LIST}/${id}`} />
|
||||
</div>
|
||||
|
||||
{isMyCollection && (
|
||||
<Button
|
||||
title={__('Edit')}
|
||||
className={classnames('button-toggle', { 'button-toggle--active': showEdit })}
|
||||
icon={ICONS.EDIT}
|
||||
onClick={() => setShowEdit(!showEdit)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
isMyCollection && (
|
||||
<Button
|
||||
title={__('Edit')}
|
||||
className={classnames('button-toggle', { 'button-toggle--active': showEdit })}
|
||||
icon={ICONS.EDIT}
|
||||
onClick={() => setShowEdit(!showEdit)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
body={
|
||||
<DragDropContext onDragEnd={handleOnDragEnd}>
|
||||
|
|
|
@ -23,6 +23,7 @@ import CollectionForm from './view';
|
|||
import { selectActiveChannelClaim, selectIncognito } from 'redux/selectors/app';
|
||||
import { doSetActiveChannel, doSetIncognito } from 'redux/actions/app';
|
||||
import { doCollectionEdit } from 'redux/actions/collections';
|
||||
import { doResetThumbnailStatus } from 'redux/actions/publish';
|
||||
|
||||
const select = (state, props) => ({
|
||||
claim: makeSelectClaimForUri(props.uri)(state),
|
||||
|
@ -52,6 +53,7 @@ const perform = (dispatch, ownProps) => ({
|
|||
setActiveChannel: (claimId) => dispatch(doSetActiveChannel(claimId)),
|
||||
setIncognito: (incognito) => dispatch(doSetIncognito(incognito)),
|
||||
doCollectionEdit: (params) => dispatch(doCollectionEdit(ownProps.collectionId, params)),
|
||||
resetThumbnailStatus: () => dispatch(doResetThumbnailStatus()),
|
||||
});
|
||||
|
||||
export default connect(select, perform)(CollectionForm);
|
||||
|
|
|
@ -17,7 +17,7 @@ import { useHistory } from 'react-router-dom';
|
|||
import { isNameValid, regexInvalidURI } from 'util/lbryURI';
|
||||
import * as THUMBNAIL_STATUSES from 'constants/thumbnail_upload_statuses';
|
||||
import { Tabs, TabList, Tab, TabPanels, TabPanel } from 'component/common/tabs';
|
||||
import { FormField } from 'component/common/form';
|
||||
import { FormField, FormFieldAreaAdvanced } from 'component/common/form';
|
||||
import { handleBidChange } from 'util/publish';
|
||||
import { FF_MAX_CHARS_IN_DESCRIPTION } from 'constants/form-field';
|
||||
import { INVALID_NAME_ERROR } from 'constants/claim';
|
||||
|
@ -58,6 +58,7 @@ type Props = {
|
|||
setActiveChannel: (string) => void,
|
||||
setIncognito: (boolean) => void,
|
||||
doCollectionEdit: (CollectionEditParams) => void,
|
||||
resetThumbnailStatus: () => void,
|
||||
};
|
||||
|
||||
function CollectionForm(props: Props) {
|
||||
|
@ -92,6 +93,7 @@ function CollectionForm(props: Props) {
|
|||
setIncognito,
|
||||
onDone,
|
||||
doCollectionEdit,
|
||||
resetThumbnailStatus,
|
||||
} = props;
|
||||
const activeChannelName = activeChannelClaim && activeChannelClaim.name;
|
||||
let prefix = 'lbry://';
|
||||
|
@ -158,7 +160,7 @@ function CollectionForm(props: Props) {
|
|||
}
|
||||
|
||||
function handleUpdateThumbnail(update: { [string]: string }) {
|
||||
if (update.thumbnail_url) {
|
||||
if (update.thumbnail_url !== undefined) {
|
||||
setParam(update);
|
||||
} else if (update.thumbnail_status) {
|
||||
setThumbStatus(update.thumbnail_status);
|
||||
|
@ -309,6 +311,10 @@ function CollectionForm(props: Props) {
|
|||
}
|
||||
}, [uri, hasClaim]);
|
||||
|
||||
React.useEffect(() => {
|
||||
resetThumbnailStatus();
|
||||
}, [resetThumbnailStatus]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={classnames('main--contained', { 'card--disabled': disabled })}>
|
||||
|
@ -358,13 +364,14 @@ function CollectionForm(props: Props) {
|
|||
/>
|
||||
<fieldset-section>
|
||||
<SelectThumbnail
|
||||
thumbnailParam={params.thumbnail_url}
|
||||
thumbnailParamError={thumbError}
|
||||
thumbnail={params.thumbnail_url}
|
||||
thumbnailError={thumbError}
|
||||
thumbnailParamStatus={thumbStatus}
|
||||
updateThumbnailParams={handleUpdateThumbnail}
|
||||
usePublishFormMode
|
||||
/>
|
||||
</fieldset-section>
|
||||
<FormField
|
||||
<FormFieldAreaAdvanced
|
||||
type="markdown"
|
||||
name="content_description2"
|
||||
label={__('Description')}
|
||||
|
|
|
@ -33,7 +33,7 @@ function CollectionPreviewOverlay(props: Props) {
|
|||
collectionItemUrls.map((item, index) => {
|
||||
if (index < 2) {
|
||||
return (
|
||||
<div className="collection-preview__overlay-grid-items">
|
||||
<div key={item} className="collection-preview__overlay-grid-items">
|
||||
<FileThumbnail uri={item} key={item} />
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -95,7 +95,7 @@ export default function CollectionsListMine(props: Props) {
|
|||
{builtin.map((list: Collection) => {
|
||||
const { items: itemUrls } = list;
|
||||
return (
|
||||
<>
|
||||
<React.Fragment key={list.name}>
|
||||
{Boolean(itemUrls && itemUrls.length) && (
|
||||
<div className="claim-grid__wrapper" key={list.name}>
|
||||
<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} />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
<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 React, { useEffect, useState } from 'react';
|
||||
import { parseURI } from 'util/lbryURI';
|
||||
import { formatNumber } from 'util/number';
|
||||
import DateTime from 'component/dateTime';
|
||||
import Button from 'component/button';
|
||||
import Expandable from 'component/expandable';
|
||||
|
@ -16,7 +17,7 @@ import CommentBadge from 'component/common/comment-badge'; // have this?
|
|||
import ChannelThumbnail from 'component/channelThumbnail';
|
||||
import { Menu, MenuButton } from '@reach/menu-button';
|
||||
import Icon from 'component/common/icon';
|
||||
import { FormField, Form } from 'component/common/form';
|
||||
import { FormFieldAreaAdvanced, Form } from 'component/common/form';
|
||||
import classnames from 'classnames';
|
||||
import usePersistedState from 'effects/use-persisted-state';
|
||||
import CommentReactions from 'component/commentReactions';
|
||||
|
@ -318,7 +319,7 @@ function CommentView(props: Props) {
|
|||
<div>
|
||||
{isEditing ? (
|
||||
<Form onSubmit={handleSubmit}>
|
||||
<FormField
|
||||
<FormFieldAreaAdvanced
|
||||
className="comment__edit-input"
|
||||
type={advancedEditor ? 'markdown' : 'textarea'}
|
||||
name="editing_comment"
|
||||
|
@ -384,7 +385,7 @@ function CommentView(props: Props) {
|
|||
label={
|
||||
numDirectReplies < 2
|
||||
? __('Show reply')
|
||||
: __('Show %count% replies', { count: numDirectReplies })
|
||||
: __('Show %count% replies', { count: formatNumber(numDirectReplies, 2, true) })
|
||||
}
|
||||
button="link"
|
||||
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 { FF_MAX_CHARS_IN_COMMENT } from 'constants/form-field';
|
||||
import { FormField, Form } from 'component/common/form';
|
||||
import { FormFieldAreaAdvanced, Form } from 'component/common/form';
|
||||
import { getChannelIdFromClaim } from 'util/claim';
|
||||
import { Lbryio } from 'lbryinc';
|
||||
import { useHistory } from 'react-router';
|
||||
|
@ -22,13 +22,12 @@ import I18nMessage from 'component/i18nMessage';
|
|||
import Icon from 'component/common/icon';
|
||||
import OptimizedImage from 'component/optimizedImage';
|
||||
import React from 'react';
|
||||
import SelectChannel from 'component/selectChannel';
|
||||
import StickerSelector from './sticker-selector';
|
||||
import CommentCreateHeader from './comment-create-header';
|
||||
import type { ElementRef } from 'react';
|
||||
import UriIndicator from 'component/uriIndicator';
|
||||
import usePersistedState from 'effects/use-persisted-state';
|
||||
import WalletTipAmountSelector from 'component/walletTipAmountSelector';
|
||||
|
||||
import { getStripeEnvironment } from 'util/stripe';
|
||||
const stripeEnvironment = getStripeEnvironment();
|
||||
|
||||
|
@ -364,31 +363,6 @@ export function CommentCreate(props: Props) {
|
|||
.catch(() => {});
|
||||
}, [canReceiveFiatTip, claim.claim_id, claim.name, claim.signing_channel, stickerSelector]);
|
||||
|
||||
// LIVESTREAM ONLY - REMOVE
|
||||
// Handle keyboard shortcut comment creation
|
||||
// React.useEffect(() => {
|
||||
// function altEnterListener(e: SyntheticKeyboardEvent<*>) {
|
||||
// const inputRef = formFieldRef && formFieldRef.current && formFieldRef.current.input;
|
||||
//
|
||||
// if (inputRef && inputRef.current === document.activeElement) {
|
||||
// // $FlowFixMe
|
||||
// const isTyping = e.target.attributes['term'];
|
||||
//
|
||||
// if (((isLivestream && !isTyping) || e.ctrlKey || e.metaKey) && e.keyCode === KEYCODES.ENTER) {
|
||||
// e.preventDefault();
|
||||
// buttonRef.current.click();
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// window.addEventListener('keydown', altEnterListener);
|
||||
//
|
||||
// // removes the listener so it doesn't cause problems elsewhere in the app
|
||||
// return () => {
|
||||
// window.removeEventListener('keydown', altEnterListener);
|
||||
// };
|
||||
// }, [isLivestream]);
|
||||
|
||||
// **************************************************************************
|
||||
// Render
|
||||
// **************************************************************************
|
||||
|
@ -410,7 +384,11 @@ export function CommentCreate(props: Props) {
|
|||
push(pathPlusRedirect);
|
||||
}}
|
||||
>
|
||||
<FormField type="textarea" name={'comment_signup_prompt'} placeholder={__('Say something about this...')} />
|
||||
<FormFieldAreaAdvanced
|
||||
type="textarea"
|
||||
name={'comment_signup_prompt'}
|
||||
placeholder={__('Say something about this...')}
|
||||
/>
|
||||
<div className="section__actions--no-margin">
|
||||
<Button disabled button="primary" label={__('Post --[button to submit something]--')} />
|
||||
</div>
|
||||
|
@ -421,22 +399,22 @@ export function CommentCreate(props: Props) {
|
|||
return (
|
||||
<Form
|
||||
onSubmit={() => {}}
|
||||
className={classnames('commentCreate', {
|
||||
'commentCreate--reply': isReply,
|
||||
'commentCreate--nestedReply': isNested,
|
||||
'commentCreate--bottom': bottom,
|
||||
className={classnames('comment-create', {
|
||||
'comment-create--reply': isReply,
|
||||
'comment-create--nestedReply': isNested,
|
||||
'comment-create--bottom': bottom,
|
||||
})}
|
||||
>
|
||||
{/* Input Box/Preview Box */}
|
||||
{stickerSelector ? (
|
||||
<StickerSelector onSelect={(sticker) => handleSelectSticker(sticker)} claimIsMine={claimIsMine} />
|
||||
) : isReviewingStickerComment && activeChannelClaim && selectedSticker ? (
|
||||
<div className="commentCreate__stickerPreview">
|
||||
<div className="commentCreate__stickerPreviewInfo">
|
||||
<div className="comment-create__stickerPreview">
|
||||
<div className="comment-create__stickerPreviewInfo">
|
||||
<ChannelThumbnail xsmall uri={activeChannelClaim.canonical_url} />
|
||||
<UriIndicator uri={activeChannelClaim.canonical_url} link />
|
||||
</div>
|
||||
<div className="commentCreate__stickerPreviewImage">
|
||||
<div className="comment-create__stickerPreviewImage">
|
||||
<OptimizedImage src={selectedSticker && selectedSticker.url} waitLoad loading="lazy" />
|
||||
</div>
|
||||
{/* figure out lbc sticker prices */}
|
||||
|
@ -448,15 +426,15 @@ export function CommentCreate(props: Props) {
|
|||
)}
|
||||
</div>
|
||||
) : isReviewingSupportComment && activeChannelClaim ? (
|
||||
<div className="commentCreate__supportCommentPreview">
|
||||
<div className="comment-create__supportCommentPreview">
|
||||
<CreditAmount
|
||||
amount={tipAmount}
|
||||
className="commentCreate__supportCommentPreviewAmount"
|
||||
className="comment-create__supportCommentPreviewAmount"
|
||||
isFiat={activeTab === TAB_FIAT}
|
||||
size={activeTab === TAB_LBC ? 18 : 2}
|
||||
/>
|
||||
<ChannelThumbnail xsmall uri={activeChannelClaim.canonical_url} />
|
||||
<div className="commentCreate__supportCommentBody">
|
||||
<div className="comment-create__supportCommentBody">
|
||||
<UriIndicator uri={activeChannelClaim.canonical_url} link />
|
||||
<div>{commentValue}</div>
|
||||
</div>
|
||||
|
@ -471,23 +449,22 @@ export function CommentCreate(props: Props) {
|
|||
/>
|
||||
)}
|
||||
|
||||
<FormField
|
||||
<FormFieldAreaAdvanced
|
||||
autoFocus={isReply}
|
||||
charCount={charCount}
|
||||
className={isReply ? 'content_reply' : 'content_comment'}
|
||||
disabled={isFetchingChannels}
|
||||
label={
|
||||
<div className="commentCreate__labelWrapper">
|
||||
<span className="commentCreate__label">{(isReply ? __('Replying as') : __('Comment as')) + ' '}</span>
|
||||
<SelectChannel tiny />
|
||||
</div>
|
||||
header={
|
||||
<CommentCreateHeader
|
||||
isReply={isReply}
|
||||
advanced={advancedEditor}
|
||||
advancedHandler={() => setAdvancedEditor(!advancedEditor)}
|
||||
/>
|
||||
}
|
||||
name={isReply ? 'content_reply' : 'content_description'}
|
||||
quickActionLabel={isReply ? undefined : advancedEditor ? __('Simple Editor') : __('Advanced Editor')}
|
||||
ref={formFieldRef}
|
||||
onChange={handleCommentChange}
|
||||
openEmoteMenu={() => setShowEmotes(!showEmotes)}
|
||||
quickActionHandler={() => setAdvancedEditor(!advancedEditor)}
|
||||
onFocus={onTextareaFocus}
|
||||
onBlur={onTextareaBlur}
|
||||
placeholder={__('Say something about this...')}
|
||||
|
@ -655,7 +632,7 @@ export function CommentCreate(props: Props) {
|
|||
{/* Help Text */}
|
||||
{deletedComment && <div className="error__text">{__('This comment has been deleted.')}</div>}
|
||||
{!!minAmount && (
|
||||
<div className="help--notice commentCreate__minAmountNotice">
|
||||
<div className="help--notice comment-create__minAmountNotice">
|
||||
<I18nMessage tokens={{ lbc: <CreditAmount noFormat amount={minAmount} /> }}>
|
||||
{minTip ? 'Comment min: %lbc%' : minSuper ? 'HyperChat min: %lbc%' : ''}
|
||||
</I18nMessage>
|
||||
|
|
|
@ -8,6 +8,8 @@ import classnames from 'classnames';
|
|||
import Button from 'component/button';
|
||||
import ChannelThumbnail from 'component/channelThumbnail';
|
||||
import { useHistory } from 'react-router';
|
||||
import { useIsMobile } from 'effects/use-screensize';
|
||||
import { formatNumber } from 'util/number';
|
||||
|
||||
type Props = {
|
||||
myReacts: Array<string>,
|
||||
|
@ -43,6 +45,8 @@ export default function CommentReactions(props: Props) {
|
|||
location: { pathname },
|
||||
} = useHistory();
|
||||
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!claim) {
|
||||
resolve(uri);
|
||||
|
@ -101,20 +105,30 @@ export default function CommentReactions(props: Props) {
|
|||
<Button
|
||||
title={__('Upvote')}
|
||||
icon={likeIcon}
|
||||
className={classnames('comment__action', {
|
||||
iconSize={isMobile && 12}
|
||||
className={classnames('comment__action button-like', {
|
||||
'comment__action--active': myReacts && myReacts.includes(REACTION_TYPES.LIKE),
|
||||
})}
|
||||
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
|
||||
title={__('Downvote')}
|
||||
icon={dislikeIcon}
|
||||
className={classnames('comment__action', {
|
||||
iconSize={isMobile && 12}
|
||||
className={classnames('comment__action button-dislike', {
|
||||
'comment__action--active': myReacts && myReacts.includes(REACTION_TYPES.DISLIKE),
|
||||
})}
|
||||
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) && (
|
||||
|
|
|
@ -23,6 +23,9 @@ import { doCommentReset, doCommentList, doCommentById, doCommentReactList } from
|
|||
import { selectActiveChannelClaim } from 'redux/selectors/app';
|
||||
import { getChannelIdFromClaim } from 'util/claim';
|
||||
import CommentsList from './view';
|
||||
import { makeSelectClientSetting } from 'redux/selectors/settings';
|
||||
import * as SETTINGS from 'constants/settings';
|
||||
import { doSetClientSetting } from 'redux/actions/settings';
|
||||
|
||||
const select = (state, props) => {
|
||||
const { uri } = props;
|
||||
|
@ -56,15 +59,19 @@ const select = (state, props) => {
|
|||
myReactsByCommentId: selectMyReacts(state),
|
||||
othersReactsById: selectOthersReacts(state),
|
||||
activeChannelId: activeChannelClaim && activeChannelClaim.claim_id,
|
||||
customCommentServers: makeSelectClientSetting(SETTINGS.CUSTOM_COMMENTS_SERVERS)(state),
|
||||
commentServer: makeSelectClientSetting(SETTINGS.CUSTOM_COMMENTS_SERVER_URL)(state),
|
||||
};
|
||||
};
|
||||
|
||||
const perform = {
|
||||
fetchTopLevelComments: doCommentList,
|
||||
fetchComment: doCommentById,
|
||||
fetchReacts: doCommentReactList,
|
||||
resetComments: doCommentReset,
|
||||
doResolveUris,
|
||||
};
|
||||
const perform = (dispatch, ownProps) => ({
|
||||
fetchTopLevelComments: (uri, parentId, page, pageSize, sortBy) =>
|
||||
dispatch(doCommentList(uri, parentId, page, pageSize, sortBy)),
|
||||
fetchComment: (commentId) => dispatch(doCommentById(commentId)),
|
||||
fetchReacts: (commentIds) => dispatch(doCommentReactList(commentIds)),
|
||||
resetComments: (claimId) => dispatch(doCommentReset(claimId)),
|
||||
doResolveUris: (uris, returnCachedClaims) => dispatch(doResolveUris(uris, returnCachedClaims)),
|
||||
setCommentServer: (url) => dispatch(doSetClientSetting(SETTINGS.CUSTOM_COMMENTS_SERVER_URL, url, true)),
|
||||
});
|
||||
|
||||
export default connect(select, perform)(CommentsList);
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
// @flow
|
||||
import { COMMENT_PAGE_SIZE_TOP_LEVEL, SORT_BY } from 'constants/comment';
|
||||
import { ENABLE_COMMENT_REACTIONS } from 'config';
|
||||
import { ENABLE_COMMENT_REACTIONS, COMMENT_SERVER_API, COMMENT_SERVER_NAME } from 'config';
|
||||
import { useIsMobile, useIsMediumScreen } from 'effects/use-screensize';
|
||||
import { getCommentsListTitle } from 'util/comments';
|
||||
import * as ICONS from 'constants/icons';
|
||||
|
@ -15,6 +15,8 @@ import Empty from 'component/common/empty';
|
|||
import React, { useEffect } from 'react';
|
||||
import Spinner from 'component/spinner';
|
||||
import usePersistedState from 'effects/use-persisted-state';
|
||||
import { FormField } from 'component/common/form';
|
||||
import Comments from 'comments';
|
||||
|
||||
const DEBOUNCE_SCROLL_HANDLER_MS = 200;
|
||||
|
||||
|
@ -52,6 +54,9 @@ type Props = {
|
|||
fetchReacts: (commentIds: Array<string>) => Promise<any>,
|
||||
resetComments: (claimId: string) => void,
|
||||
doResolveUris: (uris: Array<string>, returnCachedClaims: boolean) => void,
|
||||
customCommentServers: Array<CommentServerDetails>,
|
||||
setCommentServer: (string) => void,
|
||||
commentServer: string,
|
||||
};
|
||||
|
||||
export default function CommentList(props: Props) {
|
||||
|
@ -80,11 +85,17 @@ export default function CommentList(props: Props) {
|
|||
fetchReacts,
|
||||
resetComments,
|
||||
doResolveUris,
|
||||
customCommentServers,
|
||||
setCommentServer,
|
||||
commentServer,
|
||||
} = props;
|
||||
|
||||
const isMobile = useIsMobile();
|
||||
const isMediumScreen = useIsMediumScreen();
|
||||
|
||||
const defaultServer = { name: COMMENT_SERVER_NAME, url: COMMENT_SERVER_API };
|
||||
const allServers = [defaultServer, ...(customCommentServers || [])];
|
||||
|
||||
const spinnerRef = React.useRef();
|
||||
const DEFAULT_SORT = ENABLE_COMMENT_REACTIONS ? SORT_BY.POPULARITY : SORT_BY.NEWEST;
|
||||
const [sort, setSort] = usePersistedState('comment-sort-by', DEFAULT_SORT);
|
||||
|
@ -255,7 +266,16 @@ export default function CommentList(props: Props) {
|
|||
}, [alreadyResolved, doResolveUris, topLevelComments]);
|
||||
|
||||
const commentProps = { isTopLevel: true, threadDepth: 3, uri, claimIsMine, linkedCommentId };
|
||||
const actionButtonsProps = { totalComments, sort, changeSort, setPage };
|
||||
const actionButtonsProps = {
|
||||
totalComments,
|
||||
sort,
|
||||
changeSort,
|
||||
setPage,
|
||||
allServers,
|
||||
commentServer,
|
||||
defaultServer,
|
||||
setCommentServer,
|
||||
};
|
||||
|
||||
return (
|
||||
<Card
|
||||
|
@ -334,17 +354,21 @@ type ActionButtonsProps = {
|
|||
sort: string,
|
||||
changeSort: (string) => void,
|
||||
setPage: (number) => void,
|
||||
allServers: Array<CommentServerDetails>,
|
||||
commentServer: string,
|
||||
setCommentServer: (string) => void,
|
||||
defaultServer: CommentServerDetails,
|
||||
};
|
||||
|
||||
const CommentActionButtons = (actionButtonsProps: ActionButtonsProps) => {
|
||||
const { totalComments, sort, changeSort, setPage } = actionButtonsProps;
|
||||
|
||||
const { totalComments, sort, changeSort, setPage, allServers, commentServer, setCommentServer, defaultServer } =
|
||||
actionButtonsProps;
|
||||
const sortButtonProps = { activeSort: sort, changeSort };
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={'comment__actions-row'}>
|
||||
{totalComments > 1 && ENABLE_COMMENT_REACTIONS && (
|
||||
<span className="comment__sort">
|
||||
<div className="comment__sort-group">
|
||||
<SortButton {...sortButtonProps} label={__('Best')} icon={ICONS.BEST} sortOption={SORT_BY.POPULARITY} />
|
||||
<SortButton
|
||||
{...sortButtonProps}
|
||||
|
@ -353,11 +377,39 @@ const CommentActionButtons = (actionButtonsProps: ActionButtonsProps) => {
|
|||
sortOption={SORT_BY.CONTROVERSY}
|
||||
/>
|
||||
<SortButton {...sortButtonProps} label={__('New')} icon={ICONS.NEW} sortOption={SORT_BY.NEWEST} />
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Button button="alt" icon={ICONS.REFRESH} title={__('Refresh')} onClick={() => setPage(0)} />
|
||||
</>
|
||||
{allServers.length >= 2 && (
|
||||
<div className="button__selected-server">
|
||||
<FormField
|
||||
type="select-tiny"
|
||||
onChange={function (x) {
|
||||
const selectedServer = x.target.value;
|
||||
setPage(0);
|
||||
setCommentServer(selectedServer);
|
||||
if (selectedServer === defaultServer.url) {
|
||||
Comments.setServerUrl(undefined);
|
||||
} else {
|
||||
Comments.setServerUrl(selectedServer);
|
||||
}
|
||||
}}
|
||||
value={commentServer}
|
||||
>
|
||||
{allServers.map(function (server) {
|
||||
return (
|
||||
<option key={server.url} value={server.url}>
|
||||
{server.name}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</FormField>
|
||||
</div>
|
||||
)}
|
||||
<div className="button_refresh">
|
||||
<Button button="alt" icon={ICONS.REFRESH} title={__('Refresh')} onClick={() => setPage(0)} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -63,7 +63,7 @@ export default function Card(props: Props) {
|
|||
}
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<div className="card__first-pane">
|
||||
{(title || subtitle) && (
|
||||
<div
|
||||
className={classnames('card__header--between', {
|
||||
|
@ -73,7 +73,7 @@ export default function Card(props: Props) {
|
|||
<div
|
||||
className={classnames('card__title-section', {
|
||||
'card__title-section--body-list': isBodyList,
|
||||
'card__title-section--small': smallTitle,
|
||||
'card__title-section--smallx': smallTitle,
|
||||
})}
|
||||
>
|
||||
{icon && <Icon sectionIcon icon={icon} />}
|
||||
|
|
41
ui/component/common/debounced-input.jsx
Normal file
41
ui/component/common/debounced-input.jsx
Normal file
|
@ -0,0 +1,41 @@
|
|||
// @flow
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { FormField } from 'component/common/form';
|
||||
import Icon from 'component/common/icon';
|
||||
import useDebounce from 'effects/use-debounce';
|
||||
import classnames from 'classnames';
|
||||
|
||||
const FILTER_DEBOUNCE_MS = 300;
|
||||
|
||||
interface Props {
|
||||
defaultValue?: string;
|
||||
icon?: string;
|
||||
placeholder?: string;
|
||||
inline?: boolean;
|
||||
onChange: (newValue: string) => any;
|
||||
}
|
||||
|
||||
export default function DebouncedInput(props: Props) {
|
||||
const { defaultValue = '', icon, placeholder = '', inline, onChange } = props;
|
||||
const [rawValue, setRawValue] = useState(defaultValue);
|
||||
const debouncedValue: string = useDebounce(rawValue, FILTER_DEBOUNCE_MS);
|
||||
|
||||
useEffect(() => {
|
||||
onChange(debouncedValue);
|
||||
}, [onChange, debouncedValue]);
|
||||
|
||||
return (
|
||||
<div className={classnames({ wunderbar: !inline, 'wunderbar--inline': inline })}>
|
||||
{icon && <Icon icon={icon} />}
|
||||
<FormField
|
||||
className={classnames({ wunderbar__input: !inline, 'wunderbar__input--inline': inline })}
|
||||
type="text"
|
||||
name="debounced_search"
|
||||
spellCheck={false}
|
||||
placeholder={placeholder}
|
||||
value={rawValue}
|
||||
onChange={(e) => setRawValue(e.target.value.trim())}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -3,8 +3,8 @@ import React from 'react';
|
|||
import { useRadioState, Radio, RadioGroup } from 'reakit/Radio';
|
||||
|
||||
type Props = {
|
||||
files: Array<WebFile>,
|
||||
onChange: (WebFile | void) => void,
|
||||
files: Array<File>,
|
||||
onChange: (File | void) => void,
|
||||
};
|
||||
|
||||
type RadioProps = {
|
||||
|
@ -26,16 +26,16 @@ function FileList(props: Props) {
|
|||
|
||||
const getFile = (value?: string) => {
|
||||
if (files && files.length) {
|
||||
return files.find((file: WebFile) => file.name === value);
|
||||
return files.find((file: File) => file.name === value);
|
||||
}
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
if (radio.stops.length) {
|
||||
if (radio.items.length) {
|
||||
if (!radio.currentId) {
|
||||
radio.first();
|
||||
} else {
|
||||
const first = radio.stops[0].ref.current;
|
||||
const first = radio.items[0].ref.current;
|
||||
// First auto-selection
|
||||
if (first && first.id === radio.currentId && !radio.state) {
|
||||
const file = getFile(first.value);
|
||||
|
@ -46,12 +46,12 @@ function FileList(props: Props) {
|
|||
|
||||
if (radio.state) {
|
||||
// Find selected element
|
||||
const stop = radio.stops.find(item => item.id === radio.currentId);
|
||||
const stop = radio.items.find((item) => item.id === radio.currentId);
|
||||
const element = stop && stop.ref.current;
|
||||
// Only update state if new item is selected
|
||||
if (element && element.value !== radio.state) {
|
||||
const file = getFile(element.value);
|
||||
// Sselect new file and update state
|
||||
// Select new file and update state
|
||||
onChange(file);
|
||||
radio.setState(element.value);
|
||||
}
|
||||
|
|
|
@ -1,25 +1,29 @@
|
|||
// @flow
|
||||
import * as React from 'react';
|
||||
import * as remote from '@electron/remote';
|
||||
import { ipcRenderer } from 'electron';
|
||||
import Button from 'component/button';
|
||||
import { FormField } from 'component/common/form';
|
||||
|
||||
type Props = {
|
||||
type: string,
|
||||
currentPath?: ?string,
|
||||
onFileChosen: (WebFile) => void,
|
||||
onFileChosen: (FileWithPath) => void,
|
||||
label?: string,
|
||||
placeholder?: string,
|
||||
accept?: string,
|
||||
error?: string,
|
||||
disabled?: boolean,
|
||||
autoFocus?: boolean,
|
||||
filters?: Array<{ name: string, extension: string[] }>,
|
||||
readFile?: boolean,
|
||||
};
|
||||
|
||||
class FileSelector extends React.PureComponent<Props> {
|
||||
static defaultProps = {
|
||||
autoFocus: false,
|
||||
type: 'file',
|
||||
readFile: true,
|
||||
};
|
||||
|
||||
fileInput: React.ElementRef<any>;
|
||||
|
@ -41,19 +45,50 @@ class FileSelector extends React.PureComponent<Props> {
|
|||
const file = files[0];
|
||||
|
||||
if (this.props.onFileChosen) {
|
||||
this.props.onFileChosen(file);
|
||||
this.props.onFileChosen({ file, path: file.path || file.name });
|
||||
}
|
||||
this.fileInput.current.value = null; // clear the file input
|
||||
};
|
||||
|
||||
handleDirectoryInputSelection = () => {
|
||||
remote.dialog.showOpenDialog({ properties: ['openDirectory'] }).then((result) => {
|
||||
const path = result && result.filePaths[0];
|
||||
if (path) {
|
||||
// $FlowFixMe
|
||||
this.props.onFileChosen({ path });
|
||||
}
|
||||
});
|
||||
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];
|
||||
if (path) {
|
||||
return ipcRenderer.invoke('get-file-from-path', path, this.props.readFile);
|
||||
}
|
||||
})
|
||||
.then((result) => {
|
||||
if (!result) {
|
||||
return;
|
||||
}
|
||||
const file = new File([result.buffer], result.name, {
|
||||
type: result.mime,
|
||||
});
|
||||
this.props.onFileChosen({ file, path: result.path });
|
||||
});
|
||||
};
|
||||
|
||||
fileInputButton = () => {
|
||||
|
@ -71,7 +106,7 @@ class FileSelector extends React.PureComponent<Props> {
|
|||
<FormField
|
||||
label={label}
|
||||
webkitdirectory="true"
|
||||
className="form-field--copyable"
|
||||
className="form-field--with-button"
|
||||
error={error}
|
||||
disabled={disabled}
|
||||
type="text"
|
||||
|
@ -80,9 +115,13 @@ class FileSelector extends React.PureComponent<Props> {
|
|||
inputButton={
|
||||
<Button
|
||||
autoFocus={autoFocus}
|
||||
button="secondary"
|
||||
button="primary"
|
||||
disabled={disabled}
|
||||
onClick={type === 'openDirectory' ? this.handleDirectoryInputSelection : this.fileInputButton}
|
||||
onClick={
|
||||
type === 'openDirectory' || type === 'openFile'
|
||||
? this.handleDirectoryInputSelection
|
||||
: this.fileInputButton
|
||||
}
|
||||
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
|
||||
import 'easymde/dist/easymde.min.css';
|
||||
import { FF_MAX_CHARS_DEFAULT } from 'constants/form-field';
|
||||
import { openEditorMenu, stopContextMenu } from 'util/context-menu';
|
||||
import * as ICONS from 'constants/icons';
|
||||
import Button from 'component/button';
|
||||
import MarkdownPreview from 'component/common/markdown-preview';
|
||||
import React from 'react';
|
||||
import ReactDOMServer from 'react-dom/server';
|
||||
import SimpleMDE from 'react-simplemde-editor';
|
||||
import TextareaWithSuggestions from 'component/textareaWithSuggestions';
|
||||
import type { ElementRef, Node } from 'react';
|
||||
|
||||
type Props = {
|
||||
|
@ -21,19 +14,15 @@ type Props = {
|
|||
disabled?: boolean,
|
||||
error?: string | boolean,
|
||||
helper?: string | React$Node,
|
||||
hideSuggestions?: boolean,
|
||||
inputButton?: React$Node,
|
||||
isLivestream?: boolean,
|
||||
label?: string | Node,
|
||||
labelOnLeft: boolean,
|
||||
max?: number,
|
||||
min?: number,
|
||||
name: string,
|
||||
noEmojis?: boolean,
|
||||
placeholder?: string | number,
|
||||
postfix?: string,
|
||||
prefix?: string,
|
||||
quickActionLabel?: string,
|
||||
range?: number,
|
||||
readOnly?: boolean,
|
||||
stretch?: boolean,
|
||||
|
@ -41,8 +30,6 @@ type Props = {
|
|||
type?: string,
|
||||
value?: string | number,
|
||||
onChange?: (any) => any,
|
||||
openEmoteMenu?: () => void,
|
||||
quickActionHandler?: (any) => any,
|
||||
render?: () => React$Node,
|
||||
};
|
||||
|
||||
|
@ -72,21 +59,15 @@ export class FormField extends React.PureComponent<Props> {
|
|||
children,
|
||||
error,
|
||||
helper,
|
||||
hideSuggestions,
|
||||
inputButton,
|
||||
isLivestream,
|
||||
label,
|
||||
labelOnLeft,
|
||||
name,
|
||||
noEmojis,
|
||||
postfix,
|
||||
prefix,
|
||||
quickActionLabel,
|
||||
stretch,
|
||||
textAreaMaxLength,
|
||||
type,
|
||||
openEmoteMenu,
|
||||
quickActionHandler,
|
||||
render,
|
||||
...inputProps
|
||||
} = this.props;
|
||||
|
@ -101,18 +82,10 @@ export class FormField extends React.PureComponent<Props> {
|
|||
const countInfo = hasCharCount && textAreaMaxLength !== undefined && (
|
||||
<span className="comment__char-count-mde">{`${charCount || '0'}/${textAreaMaxLength}`}</span>
|
||||
);
|
||||
|
||||
const Wrapper = blockWrap
|
||||
? ({ children: innerChildren }) => <fieldset-section class="radio">{innerChildren}</fieldset-section>
|
||||
: ({ children: innerChildren }) => <span className="radio">{innerChildren}</span>;
|
||||
|
||||
const quickAction =
|
||||
quickActionLabel && quickActionHandler ? (
|
||||
<div className="form-field__quick-action">
|
||||
<Button button="link" onClick={quickActionHandler} label={quickActionLabel} />
|
||||
</div>
|
||||
) : null;
|
||||
|
||||
const inputSimple = (type: string) => (
|
||||
<>
|
||||
<input id={name} type={type} {...inputProps} />
|
||||
|
@ -143,133 +116,22 @@ export class FormField extends React.PureComponent<Props> {
|
|||
return inputSelect('');
|
||||
case 'select-tiny':
|
||||
return inputSelect('select--slim');
|
||||
case 'markdown':
|
||||
const handleEvents = { contextmenu: openEditorMenu };
|
||||
|
||||
const getInstance = (editor) => {
|
||||
// SimpleMDE max char check
|
||||
editor.codemirror.on('beforeChange', (instance, changes) => {
|
||||
if (textAreaMaxLength && changes.update) {
|
||||
var str = changes.text.join('\n');
|
||||
var delta = str.length - (instance.indexFromPos(changes.to) - instance.indexFromPos(changes.from));
|
||||
|
||||
if (delta <= 0) return;
|
||||
|
||||
delta = instance.getValue().length + delta - textAreaMaxLength;
|
||||
if (delta > 0) {
|
||||
str = str.substring(0, str.length - delta);
|
||||
changes.update(changes.from, changes.to, str.split('\n'));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// "Create Link (Ctrl-K)": highlight URL instead of label:
|
||||
editor.codemirror.on('changes', (instance, changes) => {
|
||||
try {
|
||||
// Grab the last change from the buffered list. I assume the
|
||||
// buffered one ('changes', instead of 'change') is more efficient,
|
||||
// and that "Create Link" will always end up last in the list.
|
||||
const lastChange = changes[changes.length - 1];
|
||||
if (lastChange.origin === '+input') {
|
||||
// https://github.com/Ionaru/easy-markdown-editor/blob/8fa54c496f98621d5f45f57577ce630bee8c41ee/src/js/easymde.js#L765
|
||||
const EASYMDE_URL_PLACEHOLDER = '(https://)';
|
||||
|
||||
// The URL placeholder is always placed last, so just look at the
|
||||
// last text in the array to also cover the multi-line case:
|
||||
const urlLineText = lastChange.text[lastChange.text.length - 1];
|
||||
|
||||
if (urlLineText.endsWith(EASYMDE_URL_PLACEHOLDER) && urlLineText !== '[]' + EASYMDE_URL_PLACEHOLDER) {
|
||||
const from = lastChange.from;
|
||||
const to = lastChange.to;
|
||||
const isSelectionMultiline = lastChange.text.length > 1;
|
||||
const baseIndex = isSelectionMultiline ? 0 : from.ch;
|
||||
|
||||
// Everything works fine for the [Ctrl-K] case, but for the
|
||||
// [Button] case, this handler happens before the original
|
||||
// code, thus our change got wiped out.
|
||||
// Add a small delay to handle that case.
|
||||
setTimeout(() => {
|
||||
instance.setSelection(
|
||||
{ line: to.line, ch: baseIndex + urlLineText.lastIndexOf('(') + 1 },
|
||||
{ line: to.line, ch: baseIndex + urlLineText.lastIndexOf(')') }
|
||||
);
|
||||
}, 25);
|
||||
}
|
||||
}
|
||||
} catch (e) {} // Do nothing (revert to original behavior)
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="form-field--SimpleMDE" onContextMenu={stopContextMenu}>
|
||||
<fieldset-section>
|
||||
<div className="form-field__two-column">
|
||||
<div>
|
||||
<label htmlFor={name}>{label}</label>
|
||||
</div>
|
||||
{quickAction}
|
||||
</div>
|
||||
<SimpleMDE
|
||||
{...inputProps}
|
||||
id={name}
|
||||
type="textarea"
|
||||
events={handleEvents}
|
||||
getMdeInstance={getInstance}
|
||||
options={{
|
||||
spellChecker: true,
|
||||
hideIcons: ['heading', 'image', 'fullscreen', 'side-by-side'],
|
||||
previewRender(plainText) {
|
||||
const preview = <MarkdownPreview content={plainText} noDataStore />;
|
||||
return ReactDOMServer.renderToString(preview);
|
||||
},
|
||||
}}
|
||||
/>
|
||||
{countInfo}
|
||||
</fieldset-section>
|
||||
</div>
|
||||
);
|
||||
case 'textarea':
|
||||
return (
|
||||
<fieldset-section>
|
||||
{(label || quickAction) && (
|
||||
{label && (
|
||||
<div className="form-field__two-column">
|
||||
<label htmlFor={name}>{label}</label>
|
||||
{quickAction}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{hideSuggestions ? (
|
||||
<textarea
|
||||
type={type}
|
||||
id={name}
|
||||
maxLength={textAreaMaxLength || FF_MAX_CHARS_DEFAULT}
|
||||
ref={this.input}
|
||||
{...inputProps}
|
||||
/>
|
||||
) : (
|
||||
<TextareaWithSuggestions
|
||||
type={type}
|
||||
id={name}
|
||||
maxLength={textAreaMaxLength || FF_MAX_CHARS_DEFAULT}
|
||||
inputRef={this.input}
|
||||
isLivestream={isLivestream}
|
||||
{...inputProps}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="form-field__textarea-info">
|
||||
{!noEmojis && openEmoteMenu && (
|
||||
<Button
|
||||
type="alt"
|
||||
className="button--file-action"
|
||||
title="Emotes"
|
||||
onClick={openEmoteMenu}
|
||||
icon={ICONS.EMOJI}
|
||||
iconSize={20}
|
||||
/>
|
||||
)}
|
||||
{countInfo}
|
||||
</div>
|
||||
<textarea
|
||||
type={type}
|
||||
id={name}
|
||||
maxLength={textAreaMaxLength || FF_MAX_CHARS_DEFAULT}
|
||||
ref={this.input}
|
||||
{...inputProps}
|
||||
/>
|
||||
<div className="form-field__textarea-info">{countInfo}</div>
|
||||
</fieldset-section>
|
||||
);
|
||||
default:
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
export { Form } from './form-components/form';
|
||||
export { FormField } from './form-components/form-field';
|
||||
export { FormFieldAreaAdvanced } from './form-components/form-field-area-advanced';
|
||||
export { FormFieldPrice } from './form-components/form-field-price';
|
||||
export { Submit } from './form-components/submit';
|
||||
|
|
|
@ -67,19 +67,16 @@ export const icons = {
|
|||
viewBox="0 0 24 24"
|
||||
width={size}
|
||||
height={size}
|
||||
fill="none"
|
||||
fill="black"
|
||||
stroke={color}
|
||||
strokeWidth="0"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
{...rest}
|
||||
>
|
||||
<path d="M1.03125 14.1562V9.84375L12 0L22.9688 9.84375V14.1562L12 24L1.03125 14.1562Z" fill="black" />
|
||||
<path d="M8.925 10.3688L3.99375 14.8125L7.70625 18.15L12.6375 13.7063L8.925 10.3688Z" fill="black" />
|
||||
<path
|
||||
d="M8.925 10.3688L15.1312 4.80005L12 1.98755L2.60625 10.425V13.575L3.99375 14.8125L8.925 10.3688Z"
|
||||
fill="black"
|
||||
/>
|
||||
<path d="M1.03125 14.1562V9.84375L12 0L22.9688 9.84375V14.1562L12 24L1.03125 14.1562Z" />
|
||||
<path d="M8.925 10.3688L3.99375 14.8125L7.70625 18.15L12.6375 13.7063L8.925 10.3688Z" />
|
||||
<path d="M8.925 10.3688L15.1312 4.80005L12 1.98755L2.60625 10.425V13.575L3.99375 14.8125L8.925 10.3688Z" />
|
||||
<path
|
||||
d="M8.925 10.3688L3.99375 14.8125L7.70625 18.15L12.6375 13.7063L8.925 10.3688Z"
|
||||
fill={`url(#paint0_linear${randomId})`}
|
||||
|
@ -172,7 +169,7 @@ export const icons = {
|
|||
</g>
|
||||
),
|
||||
[ICONS.HOME]: buildIcon(
|
||||
<g strokeWidth="2" fill="none" fillRule="evenodd" strokeLinecap="round" strokeLinejoin="round">
|
||||
<g fill="none" fillRule="evenodd" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M1, 11 L12, 2 C12, 2 22.9999989, 11.0000005 23, 11" />
|
||||
<path d="M3, 10 C3, 10 3, 10.4453982 3, 10.9968336 L3, 20.0170446 C3, 20.5675806 3.43788135, 21.0138782 4.00292933, 21.0138781 L8.99707067, 21.0138779 C9.55097324, 21.0138779 10, 20.5751284 10, 20.0089602 L10, 15.0049177 C10, 14.449917 10.4433532, 14 11.0093689, 14 L12.9906311, 14 C13.5480902, 14 14, 14.4387495 14, 15.0049177 L14, 20.0089602 C14, 20.5639609 14.4378817, 21.0138779 15.0029302, 21.0138779 L19.9970758, 21.0138781 C20.5509789, 21.0138782 21.000006, 20.56848 21.000006, 20.0170446 L21.0000057, 10" />
|
||||
</g>
|
||||
|
@ -198,9 +195,25 @@ export const icons = {
|
|||
<path d="M20.88 18.09A5 5 0 0 0 18 9h-1.26A8 8 0 1 0 3 16.29" />
|
||||
</g>
|
||||
),
|
||||
[ICONS.SUBSCRIBE]: buildIcon(
|
||||
<path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z" />
|
||||
),
|
||||
[ICONS.SUBSCRIBE]: (props: IconProps) => {
|
||||
const { size = 24, color = 'currentColor', ...otherProps } = props;
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
width={size}
|
||||
height={size}
|
||||
fill={color}
|
||||
stroke={'#FFFFFF'}
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
{...otherProps}
|
||||
>
|
||||
<path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z" />
|
||||
</svg>
|
||||
);
|
||||
},
|
||||
[ICONS.SUBSCRIBED]: (props: IconProps) => {
|
||||
const { size = 24, color = 'currentColor', ...otherProps } = props;
|
||||
return (
|
||||
|
@ -210,8 +223,8 @@ export const icons = {
|
|||
width={size}
|
||||
height={size}
|
||||
fill={color}
|
||||
stroke={color}
|
||||
strokeWidth="1"
|
||||
stroke={'#FFFFFF'}
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
{...otherProps}
|
||||
|
@ -221,9 +234,25 @@ export const icons = {
|
|||
);
|
||||
},
|
||||
|
||||
[ICONS.UNSUBSCRIBE]: buildIcon(
|
||||
<path d="M 12,5.67 10.94,4.61 C 5.7533356,-0.57666427 -2.0266644,7.2033357 3.16,12.39 l 1.06,1.06 7.78,7.78 7.78,-7.78 1.06,-1.06 c 2.149101,-2.148092 2.149101,-5.6319078 0,-7.78 -2.148092,-2.1491008 -5.631908,-2.1491008 -7.78,0 L 9.4481298,8.2303201 15.320603,9.2419066 11.772427,13.723825" />
|
||||
),
|
||||
[ICONS.UNSUBSCRIBE]: (props: IconProps) => {
|
||||
const { size = 24, color = 'currentColor', ...otherProps } = props;
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
width={size}
|
||||
height={size}
|
||||
fill={color}
|
||||
stroke={'#FFFFFF'}
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
{...otherProps}
|
||||
>
|
||||
<path d="M 12,5.67 10.94,4.61 C 5.7533356,-0.57666427 -2.0266644,7.2033357 3.16,12.39 l 1.06,1.06 7.78,7.78 7.78,-7.78 1.06,-1.06 c 2.149101,-2.148092 2.149101,-5.6319078 0,-7.78 -2.148092,-2.1491008 -5.631908,-2.1491008 -7.78,0 L 9.4481298,8.2303201 15.320603,9.2419066 11.772427,13.723825" />
|
||||
</svg>
|
||||
);
|
||||
},
|
||||
[ICONS.SETTINGS]: buildIcon(
|
||||
<g>
|
||||
<circle cx="12" cy="12" r="3" />
|
||||
|
@ -586,12 +615,24 @@ export const icons = {
|
|||
<path d="M21 13v2a4 4 0 0 1-4 4H3" />
|
||||
</g>
|
||||
),
|
||||
[ICONS.MORE_VERTICAL]: buildIcon(
|
||||
<g>
|
||||
<circle cx="12" cy="5" r="1" />
|
||||
<circle cx="12" cy="12" r="1" />
|
||||
<circle cx="12" cy="19" r="1" />
|
||||
</g>
|
||||
[ICONS.MORE_VERTICAL]: (props: CustomProps) => (
|
||||
<svg
|
||||
{...props}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
width="20"
|
||||
height="20"
|
||||
fill="none"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<g>
|
||||
<circle cx="12" cy="5" r="1" />
|
||||
<circle cx="12" cy="12" r="1" />
|
||||
<circle cx="12" cy="19" r="1" />
|
||||
</g>
|
||||
</svg>
|
||||
),
|
||||
[ICONS.MORE]: buildIcon(
|
||||
<g transform="rotate(90 12 12)">
|
||||
|
@ -974,7 +1015,6 @@ export const icons = {
|
|||
width={props.size || '16'}
|
||||
height={props.size || '18'}
|
||||
fill="none"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
|
@ -1124,13 +1164,6 @@ export const icons = {
|
|||
[ICONS.DOWNVOTE]: buildIcon(
|
||||
<path d="M10 15v4a3 3 0 0 0 3 3l4-9V2H5.72a2 2 0 0 0-2 1.7l-1.38 9a2 2 0 0 0 2 2.3zm7-13h2.67A2.31 2.31 0 0 1 22 4v7a2.31 2.31 0 0 1-2.33 2H17" />
|
||||
),
|
||||
[ICONS.FIRE_ACTIVE]: buildIcon(
|
||||
<path
|
||||
d="M11.3969 23.04C11.3969 23.04 18.4903 21.8396 18.9753 16.2795C19.3997 9.89148 14.2161 7.86333 13.2915 4.56586C13.1861 4.2261 13.1051 3.88045 13.049 3.53109C12.9174 2.68094 12.8516 1.82342 12.852 0.964865C12.852 0.964865 5.607 0.426785 4.87947 10.6227C4.34858 10.1469 3.92655 9.57999 3.63777 8.9548C3.349 8.32962 3.19921 7.65853 3.19706 6.98033C3.19706 6.98033 -4.32074 18.7767 8.45649 23.04C7.94555 22.1623 7.67841 21.1842 7.67841 20.1909C7.67841 19.1976 7.94555 18.2195 8.45649 17.3418C9.54778 15.0653 9.97218 13.8788 9.97218 13.8788C9.97218 13.8788 15.5044 18.6525 11.3969 23.04Z"
|
||||
fill="#FF6635"
|
||||
strokeWidth="0"
|
||||
/>
|
||||
),
|
||||
[ICONS.SLIME_ACTIVE]: buildIcon(
|
||||
<path
|
||||
d="M13.065 4.18508C12.5638 4.47334 11.9699 4.5547 11.4096 4.41183C10.8494 4.26896 10.367 3.91315 10.065 3.42008C9.70126 2.96799 9.52899 2.39146 9.58506 1.81392C9.64113 1.23639 9.92109 0.703759 10.365 0.330081C10.8662 0.0418164 11.4601 -0.0395341 12.0204 0.103332C12.5806 0.246199 13.063 0.602008 13.365 1.09508C13.7287 1.54717 13.901 2.12371 13.8449 2.70124C13.7889 3.27877 13.5089 3.8114 13.065 4.18508ZM2.565 6.76508C1.98518 6.6732 1.39241 6.81157 0.913189 7.15066C0.433971 7.48976 0.106262 8.00272 0 8.58008C0.0118186 9.17159 0.256137 9.73464 0.680058 10.1473C1.10398 10.56 1.67339 10.7891 2.265 10.7851C2.84509 10.8863 3.44175 10.7561 3.92691 10.4224C4.41207 10.0886 4.74707 9.57801 4.86 9.00008C4.85804 8.7046 4.79789 8.41241 4.683 8.14018C4.56811 7.86794 4.40072 7.62101 4.1904 7.41347C3.98007 7.20593 3.73093 7.04185 3.45719 6.9306C3.18345 6.81935 2.89048 6.7631 2.595 6.76508H2.565ZM22.2 15.1951C21.9286 15.0703 21.635 15.0008 21.3364 14.9907C21.0379 14.9806 20.7403 15.0301 20.461 15.1362C20.1818 15.2423 19.9264 15.403 19.7099 15.6088C19.4934 15.8146 19.3201 16.0615 19.2 16.3351C19.1369 16.6299 19.1337 16.9345 19.1906 17.2306C19.2475 17.5267 19.3634 17.8084 19.5313 18.0588C19.6992 18.3093 19.9157 18.5235 20.168 18.6886C20.4203 18.8537 20.7033 18.9665 21 19.0201C21.2714 19.1449 21.565 19.2143 21.8636 19.2244C22.1621 19.2346 22.4597 19.1851 22.739 19.079C23.0182 18.9729 23.2736 18.8122 23.4901 18.6064C23.7066 18.4005 23.8799 18.1536 24 17.8801C24.0634 17.5873 24.0677 17.2849 24.0127 16.9904C23.9577 16.696 23.8444 16.4155 23.6795 16.1654C23.5147 15.9153 23.3015 15.7007 23.0526 15.5341C22.8037 15.3674 22.524 15.2522 22.23 15.1951H22.2ZM20.34 10.2451C20.0073 9.99542 19.6009 9.86349 19.185 9.87008C18.4572 9.93018 17.7485 10.1341 17.1 10.4701C16.7447 10.6341 16.3789 10.7744 16.005 10.8901H15.69C15.5961 10.9108 15.4989 10.9108 15.405 10.8901C15 9.97508 16.5 9.00008 18.285 7.93508C18.8914 7.60883 19.4599 7.21644 19.98 6.76508C20.3961 6.30667 20.646 5.72169 20.6895 5.10413C20.733 4.48658 20.5677 3.87232 20.22 3.36008C19.9329 2.89588 19.5307 2.51381 19.0523 2.25098C18.574 1.98815 18.0358 1.85349 17.49 1.86008C17.2067 1.85969 16.9245 1.89496 16.65 1.96508C16.1585 2.08101 15.7042 2.31914 15.3293 2.65739C14.9543 2.99565 14.6708 3.42308 14.505 3.90008C14.16 4.75508 13.14 7.30508 12.135 7.71008C12.0359 7.72949 11.9341 7.72949 11.835 7.71008C11.6138 7.70259 11.3956 7.65692 11.19 7.57508C9.96 7.12508 9.6 5.62508 9.225 4.03508C9.06457 3.15891 8.79234 2.30695 8.415 1.50008C8.17043 1.04181 7.80465 0.659541 7.3576 0.395014C6.91055 0.130487 6.39941 -0.00612938 5.88 8.05856e-05C5.30686 0.011692 4.74338 0.149999 4.23 0.405081C3.872 0.589131 3.5547 0.843345 3.297 1.15258C3.03931 1.46182 2.84648 1.81976 2.73 2.20508C2.58357 2.66415 2.532 3.1482 2.57841 3.62781C2.62483 4.10743 2.76826 4.57261 3 4.99508C3.63898 5.99088 4.39988 6.90294 5.265 7.71008C5.59239 8.0233 5.90283 8.35377 6.195 8.70008C6.41249 8.94283 6.57687 9.22833 6.67761 9.5383C6.77835 9.84826 6.81322 10.1759 6.78 10.5001C6.68279 10.762 6.52008 10.9947 6.30737 11.1759C6.09467 11.3571 5.83908 11.4808 5.565 11.5351H5.19C4.89755 11.5247 4.60651 11.4896 4.32 11.4301C3.94485 11.3508 3.56329 11.3056 3.18 11.2951H3C2.50224 11.3269 2.02675 11.513 1.63964 11.8275C1.25253 12.142 0.973032 12.5694 0.84 13.0501C0.685221 13.5092 0.678705 14.0053 0.821373 14.4683C0.964041 14.9313 1.24867 15.3377 1.635 15.6301C1.97288 15.8809 2.38429 16.0127 2.805 16.0051C3.4891 15.9504 4.15377 15.751 4.755 15.4201C5.18104 15.1991 5.64344 15.0568 6.12 15.0001H6.285C6.32317 15.0086 6.35846 15.0269 6.38739 15.0532C6.41632 15.0795 6.4379 15.1129 6.45 15.1501C6.52858 15.4213 6.49621 15.7127 6.36 15.9601C5.80418 16.8088 4.95508 17.4229 3.975 17.6851C3.38444 17.8608 2.85799 18.205 2.46025 18.6756C2.06252 19.1462 1.81078 19.7226 1.73592 20.3342C1.66107 20.9458 1.76635 21.5659 2.03886 22.1185C2.31136 22.6711 2.73924 23.1321 3.27 23.4451C3.81477 23.8292 4.46349 24.0384 5.13 24.0451C6.1389 23.9485 7.08103 23.4979 7.7894 22.773C8.49778 22.0482 8.92665 21.0959 9 20.0851V19.9501C9.135 19.0351 9.33 17.7751 10.05 17.3401C10.2442 17.2216 10.4675 17.1593 10.695 17.1601C11.0828 17.1781 11.4558 17.3142 11.7641 17.5501C12.0724 17.786 12.3012 18.1105 12.42 18.4801C13.155 21.2251 13.725 23.4001 16.14 23.4001C16.4527 23.3961 16.7643 23.361 17.07 23.2951C17.8256 23.2158 18.5231 22.8527 19.0214 22.2792C19.5198 21.7057 19.7819 20.9644 19.755 20.2051C19.6664 19.6213 19.4389 19.0673 19.0918 18.5896C18.7446 18.112 18.2879 17.7246 17.76 17.4601C17.4534 17.2574 17.1625 17.0317 16.89 16.7851C16.005 15.9301 15.855 15.4051 15.885 15.1051C15.9198 14.8698 16.0313 14.6526 16.2021 14.4871C16.373 14.3217 16.5937 14.2173 16.83 14.1901H17.055C17.31 14.1901 17.61 14.1901 17.895 14.1901C18.18 14.1901 18.57 14.1901 18.84 14.1901H19.14C19.6172 14.1642 20.0748 13.9919 20.4505 13.6967C20.8263 13.4014 21.102 12.9976 21.24 12.5401C21.3316 12.1166 21.2981 11.6757 21.1436 11.2709C20.9892 10.8661 20.7204 10.5149 20.37 10.2601L20.34 10.2451Z"
|
||||
|
@ -2021,4 +2054,15 @@ export const icons = {
|
|||
<path d="M12.5,23.24v-1A10.74,10.74,0,0,1,23.24,11.52" />
|
||||
</g>
|
||||
),
|
||||
[ICONS.SIMPLE_EDITOR]: buildIcon(
|
||||
<g>
|
||||
<path d="M1 18V6c0-1 1-2 2-2h18c1 0 2 1 2 2v12c0 1-1 2-2 2H3c-1 0-2-1-2-2ZM5 7v4" />
|
||||
</g>
|
||||
),
|
||||
[ICONS.ADVANCED_EDITOR]: buildIcon(
|
||||
<g>
|
||||
<path d="M1 20V4c0-1 1-2 2-2h18c1 0 2 1 2 2v16c0 1-1 2-2 2H3c-1 0-2-1-2-2ZM1 11h22" />
|
||||
<path d="M5 8V6h2v2H5ZM11 8V6h2v2h-2ZM17 8V6h2v2h-2ZM5 14v4" />
|
||||
</g>
|
||||
),
|
||||
};
|
||||
|
|
|
@ -33,6 +33,7 @@ type SimpleTextProps = {
|
|||
type SimpleLinkProps = {
|
||||
href?: string,
|
||||
title?: string,
|
||||
embed?: boolean,
|
||||
children?: React.Node,
|
||||
};
|
||||
|
||||
|
@ -66,7 +67,7 @@ const SimpleText = (props: SimpleTextProps) => {
|
|||
// ****************************************************************************
|
||||
|
||||
const SimpleLink = (props: SimpleLinkProps) => {
|
||||
const { title, children, href } = props;
|
||||
const { title, children, href, embed } = props;
|
||||
|
||||
if (!href) {
|
||||
return children || null;
|
||||
|
@ -82,13 +83,13 @@ const SimpleLink = (props: SimpleLinkProps) => {
|
|||
|
||||
const [uri, search] = href.split('?');
|
||||
const urlParams = new URLSearchParams(search);
|
||||
const embed = urlParams.get('embed');
|
||||
const embedParam = urlParams.get('embed');
|
||||
|
||||
if (embed) {
|
||||
if (embed || embedParam) {
|
||||
// Decode this since users might just copy it from the url bar
|
||||
const decodedUri = decodeURI(uri);
|
||||
return (
|
||||
<div className="embed__inline-button-preview">
|
||||
<div className="embed__inline-button embed__inline-button--preview">
|
||||
<pre>{decodedUri}</pre>
|
||||
</div>
|
||||
);
|
||||
|
@ -195,7 +196,11 @@ export default React.memo<MarkdownProps>(function MarkdownPreview(props: Markdow
|
|||
// Workaraund of remarkOptions.Fragment
|
||||
div: React.Fragment,
|
||||
img: (imgProps) =>
|
||||
isStakeEnoughForPreview(stakedLevel) && !isEmote(imgProps.title, imgProps.src) ? (
|
||||
noDataStore ? (
|
||||
<div className="file-viewer file-viewer--document">
|
||||
<img {...imgProps} />
|
||||
</div>
|
||||
) : isStakeEnoughForPreview(stakedLevel) && !isEmote(imgProps.title, imgProps.src) ? (
|
||||
<ZoomableImage {...imgProps} />
|
||||
) : (
|
||||
<SimpleImageLink src={imgProps.src} alt={imgProps.alt} title={imgProps.title} />
|
||||
|
|
|
@ -81,8 +81,9 @@ function Paginate(props: Props) {
|
|||
<FormField
|
||||
value={textValue}
|
||||
onChange={(e) => setTextValue(e.target.value)}
|
||||
className="paginate-channel"
|
||||
label={__('Go to page:')}
|
||||
className="paginate-goto"
|
||||
aria-label={__('Go to page:')}
|
||||
placeholder={__('Go to')}
|
||||
type="text"
|
||||
name="paginate-file"
|
||||
/>
|
||||
|
|
|
@ -118,7 +118,7 @@ function FileActions(props: Props) {
|
|||
{!claimIsMine && (
|
||||
<Menu>
|
||||
<MenuButton
|
||||
className="button--file-action"
|
||||
className="button--file-action--menu"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
|
|
@ -7,7 +7,6 @@ import {
|
|||
} from 'redux/selectors/claims';
|
||||
import { makeSelectPendingAmountByUri } from 'redux/selectors/wallet';
|
||||
import { doOpenModal } from 'redux/actions/app';
|
||||
import { selectUser } from 'redux/selectors/user';
|
||||
import FileDescription from './view';
|
||||
|
||||
const select = (state, props) => {
|
||||
|
@ -17,7 +16,6 @@ const select = (state, props) => {
|
|||
claim,
|
||||
claimIsMine: selectClaimIsMine(state, claim),
|
||||
metadata: makeSelectMetadataForUri(props.uri)(state),
|
||||
user: selectUser(state),
|
||||
pendingAmount: makeSelectPendingAmountByUri(props.uri)(state),
|
||||
tags: makeSelectTagsForUri(props.uri)(state),
|
||||
};
|
||||
|
|
|
@ -15,7 +15,6 @@ type Props = {
|
|||
uri: string,
|
||||
claim: StreamClaim,
|
||||
metadata: StreamMetadata,
|
||||
user: ?any,
|
||||
tags: any,
|
||||
pendingAmount: number,
|
||||
doOpenModal: (id: string, {}) => void,
|
||||
|
|
|
@ -11,10 +11,10 @@ import Icon from 'component/common/icon';
|
|||
|
||||
type Props = {
|
||||
modal: { id: string, modalProps: {} },
|
||||
filePath: string | WebFile,
|
||||
filePath: ?string,
|
||||
clearPublish: () => void,
|
||||
updatePublishForm: ({}) => void,
|
||||
openModal: (id: string, { files: Array<WebFile> }) => void,
|
||||
openModal: (id: string, { files: Array<File> }) => void,
|
||||
// React router
|
||||
history: {
|
||||
entities: {}[],
|
||||
|
@ -37,7 +37,7 @@ function FileDrop(props: Props) {
|
|||
const { drag, dropData } = useDragDrop();
|
||||
const [files, setFiles] = React.useState([]);
|
||||
const [error, setError] = React.useState(false);
|
||||
const [target, setTarget] = React.useState<?WebFile>(null);
|
||||
const [target, setTarget] = React.useState<?File>(null);
|
||||
const hideTimer = React.useRef(null);
|
||||
const targetTimer = React.useRef(null);
|
||||
const navigationTimer = React.useRef(null);
|
||||
|
@ -65,24 +65,26 @@ function FileDrop(props: Props) {
|
|||
}
|
||||
}, [history]);
|
||||
|
||||
// Delay hide and navigation for a smooth transition
|
||||
const hideDropArea = React.useCallback(() => {
|
||||
hideTimer.current = setTimeout(() => {
|
||||
setFiles([]);
|
||||
// Navigate to publish area
|
||||
navigationTimer.current = setTimeout(() => {
|
||||
navigateToPublish();
|
||||
}, NAVIGATE_TIME_OUT);
|
||||
}, HIDE_TIME_OUT);
|
||||
}, [navigateToPublish]);
|
||||
|
||||
// Handle file selection
|
||||
const handleFileSelected = React.useCallback(
|
||||
(selectedFile) => {
|
||||
updatePublishForm({ filePath: selectedFile });
|
||||
hideDropArea();
|
||||
// Delay hide and navigation for a smooth transition
|
||||
hideTimer.current = setTimeout(() => {
|
||||
setFiles([]);
|
||||
// Navigate to publish area
|
||||
navigationTimer.current = setTimeout(() => {
|
||||
// Navigate first, THEN assign filePath, otherwise
|
||||
// the file selected will get reset (that's how the
|
||||
// publish file view works, when the user switches to
|
||||
// publish a file, the pathFile value gets reset to undefined)
|
||||
navigateToPublish();
|
||||
updatePublishForm({
|
||||
filePath: selectedFile.path || selectedFile.name,
|
||||
});
|
||||
}, NAVIGATE_TIME_OUT);
|
||||
}, HIDE_TIME_OUT);
|
||||
},
|
||||
[updatePublishForm, hideDropArea]
|
||||
[setFiles, navigateToPublish, updatePublishForm]
|
||||
);
|
||||
|
||||
// Clear timers when unmounted
|
||||
|
|
|
@ -4,9 +4,8 @@ import * as ICONS from 'constants/icons';
|
|||
import React from 'react';
|
||||
import classnames from 'classnames';
|
||||
import Button from 'component/button';
|
||||
import { formatNumberWithCommas } from 'util/number';
|
||||
import { formatNumber } from 'util/number';
|
||||
import NudgeFloating from 'component/nudgeFloating';
|
||||
|
||||
type Props = {
|
||||
claim: StreamClaim,
|
||||
doFetchReactions: (string) => void,
|
||||
|
@ -19,16 +18,8 @@ type Props = {
|
|||
};
|
||||
|
||||
function FileReactions(props: Props) {
|
||||
const {
|
||||
claim,
|
||||
uri,
|
||||
doFetchReactions,
|
||||
doReactionLike,
|
||||
doReactionDislike,
|
||||
myReaction,
|
||||
likeCount,
|
||||
dislikeCount,
|
||||
} = props;
|
||||
const { claim, uri, doFetchReactions, doReactionLike, doReactionDislike, myReaction, likeCount, dislikeCount } =
|
||||
props;
|
||||
|
||||
const claimId = claim && claim.claim_id;
|
||||
const channel = claim && claim.signing_channel && claim.signing_channel.name;
|
||||
|
@ -57,26 +48,10 @@ function FileReactions(props: Props) {
|
|||
<Button
|
||||
title={__('I like this')}
|
||||
authSrc="filereaction_like"
|
||||
className={classnames('button--file-action', {
|
||||
className={classnames('button--file-action button-like', {
|
||||
'button--file-action-active': myReaction === REACTION_TYPES.LIKE,
|
||||
})}
|
||||
label={
|
||||
<>
|
||||
{/* Be nice to have animated Likes */}
|
||||
{/* {myReaction === REACTION_TYPES.LIKE && SIMPLE_SITE && ( */}
|
||||
{/* <> */}
|
||||
{/* <div className="button__fire-glow" /> */}
|
||||
{/* <div className="button__fire-particle1" /> */}
|
||||
{/* <div className="button__fire-particle2" /> */}
|
||||
{/* <div className="button__fire-particle3" /> */}
|
||||
{/* <div className="button__fire-particle4" /> */}
|
||||
{/* <div className="button__fire-particle5" /> */}
|
||||
{/* <div className="button__fire-particle6" /> */}
|
||||
{/* </> */}
|
||||
{/* )} */}
|
||||
{formatNumberWithCommas(likeCount, 0)}
|
||||
</>
|
||||
}
|
||||
label={<>{formatNumber(likeCount, 2, true)}</>}
|
||||
iconSize={18}
|
||||
icon={likeIcon}
|
||||
onClick={() => doReactionLike(uri)}
|
||||
|
@ -84,10 +59,10 @@ function FileReactions(props: Props) {
|
|||
<Button
|
||||
authSrc={'filereaction_dislike'}
|
||||
title={__('I dislike this')}
|
||||
className={classnames('button--file-action', {
|
||||
className={classnames('button--file-action button-dislike', {
|
||||
'button--file-action-active': myReaction === REACTION_TYPES.DISLIKE,
|
||||
})}
|
||||
label={<>{formatNumberWithCommas(dislikeCount, 0)}</>}
|
||||
label={<>{formatNumber(dislikeCount, 2, true)}</>}
|
||||
iconSize={18}
|
||||
icon={dislikeIcon}
|
||||
onClick={() => doReactionDislike(uri)}
|
||||
|
|
|
@ -12,6 +12,7 @@ import { PRIMARY_PLAYER_WRAPPER_CLASS } from 'page/file/view';
|
|||
import Draggable from 'react-draggable';
|
||||
import { onFullscreenChange } from 'util/full-screen';
|
||||
import { generateListSearchUrlParams, formatLbryUrlForWeb } from 'util/url';
|
||||
import { useIsMobile, useIsMediumScreen } from 'effects/use-screensize';
|
||||
import debounce from 'util/debounce';
|
||||
import { useHistory } from 'react-router';
|
||||
import { isURIEqual } from 'util/lbryURI';
|
||||
|
@ -19,7 +20,7 @@ import AutoplayCountdown from 'component/autoplayCountdown';
|
|||
|
||||
// scss/init/vars.scss
|
||||
// --header-height
|
||||
const HEADER_HEIGHT = 64;
|
||||
const HEADER_HEIGHT = 60;
|
||||
|
||||
const IS_DESKTOP_MAC = typeof process === 'object' ? process.platform === 'darwin' : false;
|
||||
const DEBOUNCE_WINDOW_RESIZE_HANDLER_MS = 100;
|
||||
|
@ -130,6 +131,8 @@ export default function FileRenderFloating(props: Props) {
|
|||
const hideFloatingPlayer = location.state && location.state.hideFloatingPlayer;
|
||||
const playingUriSource = playingUri && playingUri.source;
|
||||
const isComment = playingUriSource === 'comment';
|
||||
const isMobile = useIsMobile();
|
||||
const isMediumScreen = useIsMediumScreen();
|
||||
const mainFilePlaying = !isFloating && primaryUri && isURIEqual(uri, primaryUri);
|
||||
|
||||
const [fileViewerRect, setFileViewerRect] = useState();
|
||||
|
@ -245,14 +248,10 @@ export default function FileRenderFloating(props: Props) {
|
|||
}, [handleResize]);
|
||||
|
||||
useEffect(() => {
|
||||
// @if TARGET='app'
|
||||
setDesktopPlayStartTime(Date.now());
|
||||
// @endif
|
||||
|
||||
return () => {
|
||||
// @if TARGET='app'
|
||||
setDesktopPlayStartTime(undefined);
|
||||
// @endif
|
||||
};
|
||||
}, [uri]);
|
||||
|
||||
|
@ -293,7 +292,7 @@ export default function FileRenderFloating(props: Props) {
|
|||
if (
|
||||
!isPlayable ||
|
||||
!uri ||
|
||||
(isFloating && (!floatingPlayerEnabled || hideFloatingPlayer)) ||
|
||||
(isFloating && (isMobile || !floatingPlayerEnabled || hideFloatingPlayer)) ||
|
||||
(collectionId && !isFloating && ((!canViewFile && !nextListUri) || countdownCanceled))
|
||||
) {
|
||||
return null;
|
||||
|
@ -317,7 +316,6 @@ export default function FileRenderFloating(props: Props) {
|
|||
|
||||
function handleDragStop(e, ui) {
|
||||
if (wasDragging) {
|
||||
// e.stopPropagation();
|
||||
setWasDragging(false);
|
||||
}
|
||||
|
||||
|
@ -346,7 +344,8 @@ export default function FileRenderFloating(props: Props) {
|
|||
'content__viewer--floating': isFloating,
|
||||
'content__viewer--inline': !isFloating,
|
||||
'content__viewer--secondary': isComment,
|
||||
'content__viewer--theater-mode': !isFloating && videoTheaterMode,
|
||||
'content__viewer--theater-mode':
|
||||
!isFloating && videoTheaterMode && !isMediumScreen && playingUri?.uri === primaryUri,
|
||||
'content__viewer--disable-click': wasDragging,
|
||||
})}
|
||||
style={
|
||||
|
@ -355,7 +354,11 @@ export default function FileRenderFloating(props: Props) {
|
|||
width: fileViewerRect.width,
|
||||
height: fileViewerRect.height,
|
||||
left: fileViewerRect.x,
|
||||
top: fileViewerRect.windowOffset + fileViewerRect.top - HEADER_HEIGHT - (IS_DESKTOP_MAC ? 24 : 0),
|
||||
top:
|
||||
fileViewerRect.windowOffset +
|
||||
fileViewerRect.top -
|
||||
(isMobile ? 0 : HEADER_HEIGHT) -
|
||||
(IS_DESKTOP_MAC ? 24 : 0),
|
||||
}
|
||||
: {}
|
||||
}
|
||||
|
@ -370,7 +373,7 @@ export default function FileRenderFloating(props: Props) {
|
|||
title={__('Close')}
|
||||
onClick={closeFloatingPlayer}
|
||||
icon={ICONS.REMOVE}
|
||||
button="primary"
|
||||
button="alt"
|
||||
className="content__floating-close"
|
||||
/>
|
||||
)}
|
||||
|
|
|
@ -9,6 +9,7 @@ import * as PAGES from 'constants/pages';
|
|||
import * as RENDER_MODES from 'constants/file_render_modes';
|
||||
import * as KEYCODES from 'constants/keycodes';
|
||||
import Button from 'component/button';
|
||||
import { useIsMediumScreen } from 'effects/use-screensize';
|
||||
import isUserTyping from 'util/detect-typing';
|
||||
import { getThumbnailCdnUrl } from 'util/thumbnail';
|
||||
import Nag from 'component/common/nag';
|
||||
|
@ -63,6 +64,7 @@ export default function FileRenderInitiator(props: Props) {
|
|||
const fileStatus = fileInfo && fileInfo.status;
|
||||
const isPlayable = RENDER_MODES.FLOATING_MODES.includes(renderMode);
|
||||
const isText = RENDER_MODES.TEXT_MODES.includes(renderMode);
|
||||
const isMediumScreen = useIsMediumScreen();
|
||||
const [thumbnail, setThumbnail] = React.useState(FileRenderPlaceholder);
|
||||
const containerRef = React.useRef<any>();
|
||||
|
||||
|
@ -151,7 +153,7 @@ export default function FileRenderInitiator(props: Props) {
|
|||
style={thumbnail && !obscurePreview ? { backgroundImage: `url("${thumbnail}")` } : {}}
|
||||
className={classnames('content__cover', {
|
||||
'content__cover--disabled': disabled,
|
||||
'content__cover--theater-mode': videoTheaterMode,
|
||||
'content__cover--theater-mode': videoTheaterMode && !isMediumScreen,
|
||||
'content__cover--text': isText,
|
||||
'card__media--nsfw': obscurePreview,
|
||||
})}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue