Compare commits

..

9 commits

Author SHA1 Message Date
ByronEricPerez
1038181386 change using overflow 2022-10-10 17:05:05 -03:00
ByronEricPerez
d2e147f911 margin removed 2022-10-04 12:23:20 -03:00
ByronEricPerez
b6e71a880e size and location of the refresh btn changed, and added maximum and minimum width for the comment server 2022-09-02 14:57:19 -03:00
ByronEricPerez
5baa12ec3d Aligned input 2022-08-29 16:17:14 -03:00
ByronEricPerez
d724676daf Fixed why it didn't show comments 2022-08-26 17:25:48 -03:00
ByronEricPerez
250e9eeebc btn moved to the correct component and syncs correctly 2022-08-26 15:50:13 -03:00
ByronEricPerez
fd2d817ba7 the problem of synchronizing the two commentServer of the application was solved 2022-08-15 12:27:11 -03:00
ByronEricPerez
ef27cb5a9b added input to select custom and default server when creating comment 2022-08-09 14:33:36 -03:00
ByronEricPerez
1c9c0a4cbf work in progress 2022-07-21 20:01:40 -03:00
109 changed files with 2889 additions and 1804 deletions

View file

@ -38,22 +38,7 @@ jobs:
- uses: maxim-lobanov/setup-xcode@v1 - uses: maxim-lobanov/setup-xcode@v1
if: startsWith(runner.os, 'mac') if: startsWith(runner.os, 'mac')
with: with:
xcode-version: '13.1.0' xcode-version: '12.4.0'
# This is gonna be hacky.
# Github made us upgrade xcode, which would force an upgrade of electron-builder to fix mac.
# But there were bugs with copyfiles / extraFiles that kept seeing duplicates erroring on ln.
# A flag USE_HARD_LINKS=false in electron-builder.json was suggested in comments, but that broke windows builds.
# So for now we'll install python2 on mac and make sure it can find it.
# Remove this after successfully upgrading electron-builder.
# HACK part 1
- uses: Homebrew/actions/setup-homebrew@master
if: startsWith(runner.os, 'mac')
# HACK part 2
- name: Install Python2
if: startsWith(runner.os, 'mac')
run: |
/bin/bash -c "$(curl -fsSL https://github.com/alfredapp/dependency-scripts/raw/main/scripts/install-python2.sh)"
echo "PYTHON_PATH=/usr/local/bin/python" >> $GITHUB_ENV
- name: Download blockchain headers - name: Download blockchain headers
run: | run: |
@ -73,7 +58,7 @@ jobs:
WIN_CSC_KEY_PASSWORD: ${{ secrets.WIN_CSC_KEY_PASSWORD }} WIN_CSC_KEY_PASSWORD: ${{ secrets.WIN_CSC_KEY_PASSWORD }}
CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }} CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
WIN_CSC_LINK: https://raw.githubusercontent.com/lbryio/lbry-desktop/master/build/cert2023.pfx WIN_CSC_LINK: https://raw.githubusercontent.com/lbryio/lbry-desktop/master/build/cert-2021-2022.pfx
CSC_LINK: https://s3.amazonaws.com/files.lbry.io/cert/osx-csc-2021-2022.p12 CSC_LINK: https://s3.amazonaws.com/files.lbry.io/cert/osx-csc-2021-2022.p12
# UI # UI

View file

View file

View file

View file

View file

View file

View file

View file

View file

@ -1,73 +1,22 @@
# Changelog # Changelog
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## [Unreleased for desktop]
## [0.53.9] - [2023-2-8]
### Changed
- Updated lbrynet to [0.113.0](https://github.com/lbryio/lbry-sdk/releases/tag/v0.113.0)
## [0.53.8] - [2022-11-17]
### Fixed
- Selecting a large file in publish no longer crashes ([#7736](https://github.com/lbryio/lbry-desktop/pull/7736))
- Unfollowing unpublished channels ([#7737](https://github.com/lbryio/lbry-desktop/pull/7737))
### Changed
- Updated xcode to 13.1 and hacked a fix for release ([#7736](https://github.com/lbryio/lbry-desktop/pull/7736))
## [0.53.7] - [2022-11-10]
### Added
- 'Collections' to txo filter _community pr!_ ([#7711](https://github.com/lbryio/lbry-desktop/pull/7711))
- Swap comment servers _community pr!_ ([#7670](https://github.com/lbryio/lbry-desktop/pull/7670))
### Fixed
- Thumbnails no longer disable publish ([#7714](https://github.com/lbryio/lbry-desktop/pull/7714))
- Publishing posts were empty ([#7715](https://github.com/lbryio/lbry-desktop/pull/7715))
- Minor layout fixes _community pr!_ ([#7709](https://github.com/lbryio/lbry-desktop/pull/7709))
- Comment section buttons layout ([#7716](https://github.com/lbryio/lbry-desktop/pull/7716))
### Changed
- Removed watchman and its errors ([#7710](https://github.com/lbryio/lbry-desktop/pull/7710))
- Updated lbrynet to [0.112.0](https://github.com/lbryio/lbry-sdk/releases/tag/v0.112.0)
## [0.53.6] - [2022-10-21]
### Fixed
- Make thumbnails optional ([#7690](https://github.com/lbryio/lbry-desktop/pull/7690))
- Show downloads newest first ([#7684](https://github.com/lbryio/lbry-desktop/pull/7684))
- Only allow images in image uploader ([#7672](https://github.com/lbryio/lbry-desktop/pull/7672))
- Fixed bug with csv exports ([#7697](https://github.com/lbryio/lbry-desktop/pull/7697))
- Fixed various upload bugs including transcoding ([#7688](https://github.com/lbryio/lbry-desktop/pull/7688))
- Fallback for files with no extension ([#7704](https://github.com/lbryio/lbry-desktop/pull/7704))
### Changed
- Upgraded Electron to v17.2.0 ([#7703](https://github.com/lbryio/lbry-desktop/pull/7703))
- Upgraded Electron to v17.0.0 ([#7691](https://github.com/lbryio/lbry-desktop/pull/7691))
- Updated lbrynet to [0.111.0](https://github.com/lbryio/lbry-sdk/releases/tag/v0.111.0)
## [0.53.5] - [2022-08-26]
### Added ### Added
- Checkbox to disable background wallpaper ([#7630](https://github.com/lbryio/lbry-desktop/pull/7630)) - Checkbox to disable background wallpaper ([#7630](https://github.com/lbryio/lbry-desktop/pull/7630))
- Handle content blocking from hub ([#7665](https://github.com/lbryio/lbry-desktop/pull/7665))
### Fixed ### Fixed
- Better handle decimals liquidating supports ([#7648](https://github.com/lbryio/lbry-desktop/pull/7648))
- Better handle cover uploads ([#7647](https://github.com/lbryio/lbry-desktop/pull/7647)) - Better handle cover uploads ([#7647](https://github.com/lbryio/lbry-desktop/pull/7647))
- Use default path when first choosing file on windows ([#7625](https://github.com/lbryio/lbry-desktop/pull/7625)) - Use default path when first choosing file on windows ([#7625](https://github.com/lbryio/lbry-desktop/pull/7625))
- Emoji button hover ([#7620](https://github.com/lbryio/lbry-desktop/pull/7620)) - Emoji button hover ([#7620](https://github.com/lbryio/lbry-desktop/pull/7620))
- Prevent infinite retries on thumbs ([#7618](https://github.com/lbryio/lbry-desktop/pull/7618)) - Prevent infinite retries on thumbs ([#7618](https://github.com/lbryio/lbry-desktop/pull/7618))
- Double splash/error on app startup ([#7615](https://github.com/lbryio/lbry-desktop/pull/7615)) - Double splash/error on app startup ([#7615](https://github.com/lbryio/lbry-desktop/pull/7615))
- App updates are now more coherent, also debs work. ([#7502](https://github.com/lbryio/lbry-desktop/pull/7502)) - App updates are now more coherent, also debs work. ([#7502](https://github.com/lbryio/lbry-desktop/pull/7502))
- Better handle many channels moderation calls at startup ([#7674](https://github.com/lbryio/lbry-desktop/pull/7674))
- Fix mobile floating viewer position ([#7677](https://github.com/lbryio/lbry-desktop/pull/7677))
### Changed ### Changed
- Upgraded Electron to v15.5.5 ([#7614](https://github.com/lbryio/lbry-desktop/pull/7614)) - Upgraded Electron to v15.5.5 ([#7614](https://github.com/lbryio/lbry-desktop/pull/7614))
- Upgraded to lbrynet v0.110.0 ([#7680](https://github.com/lbryio/lbry-desktop/pull/7680))
## [0.53.4] - [2022-06-10] ## [0.53.4] - [2022-06-10]

View file

@ -65,8 +65,8 @@ _Note: If coming from a deb install, the directory structure is different and yo
| | Flatpak | Arch | Nixpkgs | ARM/ARM64 | | | 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-desktop-bin/) | [Nixpkgs](https://search.nixos.org/packages?channel=unstable&show=lbry&query=lbry) | [Build Guide](https://lbry.tv/@LBRYarm:5) | | Latest Release | [FlatHub Page](https://flathub.org/apps/details/io.lbry.lbry-app) | [AUR Package](https://aur.archlinux.org/packages/lbry-app-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) | | Maintainers | [@kcSeb](https://keybase.io/kcseb) | [@kcSeb](https://keybase.io/kcseb) | [@Enderger](https://github.com/enderger) | [@Madiator2011](https://github.com/kodxana) |
## Usage ## Usage
@ -77,7 +77,7 @@ Start the installed application to interact with the LBRY network.
#### Prerequisites #### Prerequisites
- [Git](https://git-scm.com/downloads) - [Git](https://git-scm.com/downloads)
- [Node.js](https://nodejs.org/en/download/) (v16 required) - [Node.js](https://nodejs.org/en/download/) (v14 required)
- [Corepack](https://nodejs.org/dist/latest-v17.x/docs/api/corepack.html) `npm i -g corepack` (Included in nodejs 14 LTS, 16 LTS and 17) - [Corepack](https://nodejs.org/dist/latest-v17.x/docs/api/corepack.html) `npm i -g corepack` (Included in nodejs 14 LTS, 16 LTS and 17)
- [Yarn](https://yarnpkg.com/en/docs/install) - [Yarn](https://yarnpkg.com/en/docs/install)

Binary file not shown.

View file

@ -20,12 +20,9 @@ import path from 'path';
import { diskSpaceLinux, diskSpaceWindows, diskSpaceMac } from '../ui/util/diskspace'; import { diskSpaceLinux, diskSpaceWindows, diskSpaceMac } from '../ui/util/diskspace';
const { download } = require('electron-dl'); const { download } = require('electron-dl');
const mime = require('mime');
const remote = require('@electron/remote/main'); const remote = require('@electron/remote/main');
const os = require('os'); const os = require('os');
const sudo = require('sudo-prompt'); const sudo = require('sudo-prompt');
const probe = require('ffmpeg-probe');
const MAX_IPC_SEND_BUFFER_SIZE = 500000000; // large files crash when serialized for ipc message
remote.initialize(); remote.initialize();
const filePath = path.join(process.resourcesPath, 'static', 'upgradeDisabled'); const filePath = path.join(process.resourcesPath, 'static', 'upgradeDisabled');
@ -302,96 +299,6 @@ app.on('before-quit', () => {
appState.isQuitting = true; appState.isQuitting = true;
}); });
// Get the content of a file as a raw buffer of bytes.
// Useful to convert a file path to a File instance.
// Example:
// const result = await ipcMain.invoke('get-file-from-path', 'path/to/file');
// const file = new File([result.buffer], result.name);
// NOTE: if path points to a folder, an empty
// file will be given.
ipcMain.handle('get-file-from-path', (event, path, readContents = true) => {
return new Promise((resolve, reject) => {
fs.stat(path, (error, stats) => {
if (error) {
reject(error);
return;
}
// Separate folders considering "\" and "/"
// as separators (cross platform)
const folders = path.split(/[\\/]/);
const name = folders[folders.length - 1];
if (stats.isDirectory()) {
resolve({
name,
mime: undefined,
path,
buffer: new ArrayBuffer(0),
});
return;
}
if (!readContents) {
resolve({
name,
mime: mime.getType(name) || undefined,
path,
buffer: new ArrayBuffer(0),
});
return;
}
// Encoding null ensures data results in a Buffer.
fs.readFile(path, { encoding: null }, (err, data) => {
if (err) {
reject(err);
return;
}
resolve({
name,
mime: mime.getType(name) || undefined,
path,
buffer: data,
});
});
});
});
});
ipcMain.handle('get-file-details-from-path', async (event, path) => {
const isFfMp4 = (ffprobeResults) => {
return ffprobeResults &&
ffprobeResults.format &&
ffprobeResults.format.format_name &&
ffprobeResults.format.format_name.includes('mp4');
};
const folders = path.split(/[\\/]/);
const name = folders[folders.length - 1];
let duration = 0, size = 0, mimeType;
try {
await fs.promises.stat(path);
let ffprobeResults;
try {
ffprobeResults = await probe(path);
duration = ffprobeResults.format.duration;
size = ffprobeResults.format.size;
} catch (e) {
}
let fileReadResult;
if (size < MAX_IPC_SEND_BUFFER_SIZE) {
try {
fileReadResult = await fs.promises.readFile(path);
} catch (e) {
}
}
// TODO: use mmmagic to inspect file and get mime type
mimeType = isFfMp4(ffprobeResults) ? 'video/mp4' : mime.getType(name);
const fileData = {name, mime: mimeType || undefined, path, duration: duration, size, buffer: fileReadResult };
return fileData;
} catch (e) {
// no stat
return { error: 'no file' };
}
});
ipcMain.on('get-disk-space', async (event) => { ipcMain.on('get-disk-space', async (event) => {
try { try {
const { data_dir } = await Lbry.settings_get(); const { data_dir } = await Lbry.settings_get();

View file

@ -5,7 +5,7 @@
// involve moving it from 'extras' to 'ui' (big change). // involve moving it from 'extras' to 'ui' (big change).
import { createCachedSelector } from 're-reselect'; import { createCachedSelector } from 're-reselect';
import { selectClaimForUri, makeSelectIsBlacklisted } from 'redux/selectors/claims'; import { selectClaimForUri } from 'redux/selectors/claims';
import { selectMutedChannels } from 'redux/selectors/blocked'; import { selectMutedChannels } from 'redux/selectors/blocked';
import { selectModerationBlockList } from 'redux/selectors/comments'; import { selectModerationBlockList } from 'redux/selectors/comments';
import { selectBlacklistedOutpointMap, selectFilteredOutpointMap } from 'lbryinc'; import { selectBlacklistedOutpointMap, selectFilteredOutpointMap } from 'lbryinc';
@ -18,8 +18,7 @@ export const selectBanStateForUri = createCachedSelector(
selectFilteredOutpointMap, selectFilteredOutpointMap,
selectMutedChannels, selectMutedChannels,
selectModerationBlockList, selectModerationBlockList,
(state, uri) => makeSelectIsBlacklisted(uri)(state), (claim, blackListedOutpointMap, filteredOutpointMap, mutedChannelUris, personalBlocklist) => {
(claim, blackListedOutpointMap, filteredOutpointMap, mutedChannelUris, personalBlocklist, isBlacklisted) => {
const banState = {}; const banState = {};
if (!claim) { if (!claim) {
@ -28,10 +27,6 @@ export const selectBanStateForUri = createCachedSelector(
const channelClaim = getChannelFromClaim(claim); const channelClaim = getChannelFromClaim(claim);
if (isBlacklisted) {
banState['blacklisted'] = true;
}
// This will be replaced once blocking is done at the wallet server level. // This will be replaced once blocking is done at the wallet server level.
if (blackListedOutpointMap) { if (blackListedOutpointMap) {
if ( if (

37
flow-typed/Claim.js vendored
View file

@ -145,49 +145,12 @@ declare type PurchaseReceipt = {
type: 'purchase', type: 'purchase',
}; };
declare type ClaimErrorCensor = {
address: string,
amount: string,
canonical_url: string,
claim_id: string,
claim_op: string,
confirmations: number,
has_signing_key: boolean,
height: number,
meta: {
activation_height: number,
claims_in_channel: number,
creation_height: number,
creation_timestamp: number,
effective_amount: string,
expiration_height: number,
is_controlling: boolean,
reposted: number,
support_amount: string,
take_over_height: number,
},
name: string,
normalized_name: string,
nout: number,
permanent_url: string,
short_url: string,
timestamp: number,
txid: string,
type: string,
value: {
public_key: string,
public_key_id: string,
},
value_type: string,
}
declare type ClaimActionResolveInfo = { declare type ClaimActionResolveInfo = {
[string]: { [string]: {
stream: ?StreamClaim, stream: ?StreamClaim,
channel: ?ChannelClaim, channel: ?ChannelClaim,
claimsInChannel: ?number, claimsInChannel: ?number,
collection: ?CollectionClaim, collection: ?CollectionClaim,
errorCensor: ?ClaimErrorCensor,
}, },
} }

View file

@ -1,10 +0,0 @@
// @flow
declare type FileData = {
file?: Blob,
path: string,
duration?: number,
size?: number,
mimeType: string,
error?: string,
}

View file

@ -1,9 +0,0 @@
// @flow
declare type FileWithPath = {
file: File,
// The full path will only be available in
// the application. For browser, the name
// of the file will be used.
path: string,
}

6
flow-typed/web-file.js vendored Normal file
View file

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

View file

@ -1,6 +1,6 @@
{ {
"name": "lbry", "name": "lbry",
"version": "0.53.9", "version": "0.53.4",
"description": "A browser for the LBRY network, a digital marketplace controlled by its users.", "description": "A browser for the LBRY network, a digital marketplace controlled by its users.",
"keywords": [ "keywords": [
"lbry" "lbry"
@ -41,29 +41,32 @@
}, },
"dependencies": { "dependencies": {
"@electron/remote": "^2.0.1", "@electron/remote": "^2.0.1",
"@emotion/react": "^11.10.4", "@emotion/react": "^11.6.0",
"@emotion/styled": "^11.10.4", "@emotion/styled": "^11.6.0",
"@mui/material": "^5.2.1", "@mui/material": "^5.2.1",
"@ungap/from-entries": "^0.2.1", "@ungap/from-entries": "^0.2.1",
"auto-launch": "^5.0.5", "auto-launch": "^5.0.5",
"electron-dl": "^3.2.0", "electron-dl": "^3.2.0",
"electron-log": "^4.4.8", "electron-log": "^2.2.12",
"electron-notarize": "^1.0.0", "electron-notarize": "^1.0.0",
"electron-updater": "^4.2.4", "electron-updater": "^4.2.4",
"express": "^4.17.1", "express": "^4.17.1",
"ffmpeg-probe": "^1.0.6",
"humanize-duration": "^3.27.0", "humanize-duration": "^3.27.0",
"if-env": "^1.0.4",
"match-sorter": "^6.3.0", "match-sorter": "^6.3.0",
"mime": "^3.0.0",
"node-html-parser": "^5.1.0", "node-html-parser": "^5.1.0",
"parse-duration": "^1.0.0", "parse-duration": "^1.0.0",
"proxy-polyfill": "0.1.6", "proxy-polyfill": "0.1.6",
"re-reselect": "^4.0.0", "re-reselect": "^4.0.0",
"react-beautiful-dnd": "^13.1.0", "react-beautiful-dnd": "^13.1.0",
"react-color": "^2.19.3",
"react-datetime-picker": "^3.4.3", "react-datetime-picker": "^3.4.3",
"remove-markdown": "^0.3.0",
"rss": "^1.2.2",
"source-map-explorer": "^2.5.2", "source-map-explorer": "^2.5.2",
"sudo-prompt": "^9.2.1", "sudo-prompt": "^9.2.1",
"tempy": "^0.6.0" "tempy": "^0.6.0",
"videojs-logo": "^2.1.4"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.0.0", "@babel/core": "^7.0.0",
@ -74,7 +77,7 @@
"@babel/plugin-syntax-dynamic-import": "^7.2.0", "@babel/plugin-syntax-dynamic-import": "^7.2.0",
"@babel/plugin-transform-flow-strip-types": "^7.2.3", "@babel/plugin-transform-flow-strip-types": "^7.2.3",
"@babel/plugin-transform-runtime": "^7.4.3", "@babel/plugin-transform-runtime": "^7.4.3",
"@babel/polyfill": "^7.12.1", "@babel/polyfill": "^7.2.5",
"@babel/preset-env": "^7.12.11", "@babel/preset-env": "^7.12.11",
"@babel/preset-flow": "^7.12.1", "@babel/preset-flow": "^7.12.1",
"@babel/preset-react": "^7.0.0", "@babel/preset-react": "^7.0.0",
@ -82,6 +85,7 @@
"@datapunt/matomo-tracker-js": "^0.1.4", "@datapunt/matomo-tracker-js": "^0.1.4",
"@hot-loader/react-dom": "^16.13", "@hot-loader/react-dom": "^16.13",
"@meetfranz/electron-cookies": "^3.0.2", "@meetfranz/electron-cookies": "^3.0.2",
"@reach/auto-id": "^0.13.0",
"@reach/combobox": "^0.12.1", "@reach/combobox": "^0.12.1",
"@reach/menu-button": "0.8.6", "@reach/menu-button": "0.8.6",
"@reach/rect": "^0.16.0", "@reach/rect": "^0.16.0",
@ -92,17 +96,21 @@
"@sentry/webpack-plugin": "^1.10.0", "@sentry/webpack-plugin": "^1.10.0",
"@types/three": "^0.103.2", "@types/three": "^0.103.2",
"adm-zip": "^0.4.13", "adm-zip": "^0.4.13",
"async-exit-hook": "^2.0.1",
"babel-eslint": "^10.0.1", "babel-eslint": "^10.0.1",
"babel-loader": "^8.0.5", "babel-loader": "^8.0.5",
"babel-plugin-add-module-exports": "^1.0.4", "babel-plugin-add-module-exports": "^1.0.4",
"babel-plugin-import-glob": "^2.0.0", "babel-plugin-import-glob": "^2.0.0",
"babel-plugin-transform-imports": "^1.5.1", "babel-plugin-transform-imports": "^1.5.1",
"babel-plugin-transform-object-rest-spread": "^6.26.0", "babel-plugin-transform-object-rest-spread": "^6.26.0",
"bluebird": "^3.5.1",
"chalk": "^4.1.0", "chalk": "^4.1.0",
"classnames": "^2.2.5", "classnames": "^2.2.5",
"codemirror": "^5.39.2", "codemirror": "^5.39.2",
"concurrently": "^4.1.2",
"connected-react-router": "^6.8.0", "connected-react-router": "^6.8.0",
"copy-webpack-plugin": "^6.4.1", "copy-webpack-plugin": "^6.4.1",
"copyfiles": "^2.4.1",
"country-data": "^0.0.31", "country-data": "^0.0.31",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"crypto-js": "^4.0.0", "crypto-js": "^4.0.0",
@ -113,9 +121,10 @@
"decompress": "^4.2.1", "decompress": "^4.2.1",
"del": "^3.0.0", "del": "^3.0.0",
"devtron": "^1.4.0", "devtron": "^1.4.0",
"dom-scroll-into-view": "^1.2.1",
"dotenv-defaults": "^2.0.1", "dotenv-defaults": "^2.0.1",
"dotenv-webpack": "^1.8.0", "dotenv-webpack": "^1.8.0",
"electron": "17.2.0", "electron": "15.5.5",
"electron-builder": "^22.10.5", "electron-builder": "^22.10.5",
"electron-devtools-installer": "^3.1.1", "electron-devtools-installer": "^3.1.1",
"electron-is-dev": "^0.3.0", "electron-is-dev": "^0.3.0",
@ -150,7 +159,10 @@
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"mammoth": "^1.4.16", "mammoth": "^1.4.16",
"moment": "^2.29.2", "moment": "^2.29.2",
"node-abi": "^2.5.1",
"node-fetch": "^2.6.7", "node-fetch": "^2.6.7",
"node-html-parser": "^5.1.0",
"node-libs-browser": "^2.1.0",
"node-loader": "^0.6.0", "node-loader": "^0.6.0",
"node-wget": "^0.4.3", "node-wget": "^0.4.3",
"nodemon": "^1.19.1", "nodemon": "^1.19.1",
@ -165,6 +177,7 @@
"rc-progress": "^2.0.6", "rc-progress": "^2.0.6",
"react": "^16.8.2", "react": "^16.8.2",
"react-awesome-lightbox": "^1.7.3", "react-awesome-lightbox": "^1.7.3",
"react-confetti": "^4.0.1",
"react-dom": "^16.8.2", "react-dom": "^16.8.2",
"react-draggable": "^3.3.0", "react-draggable": "^3.3.0",
"react-google-recaptcha": "^2.0.1", "react-google-recaptcha": "^2.0.1",
@ -175,6 +188,7 @@
"react-router": "^5.1.0", "react-router": "^5.1.0",
"react-router-dom": "^5.1.0", "react-router-dom": "^5.1.0",
"react-simplemde-editor": "^4.1.3", "react-simplemde-editor": "^4.1.3",
"react-spring": "^8.0.20",
"reakit": "^1.0.0-beta.13", "reakit": "^1.0.0-beta.13",
"redux": "^3.6.0", "redux": "^3.6.0",
"redux-persist": "^5.10.0", "redux-persist": "^5.10.0",
@ -191,16 +205,20 @@
"sass": "^1.29.0", "sass": "^1.29.0",
"sass-loader": "^7.1.0", "sass-loader": "^7.1.0",
"semver": "^5.3.0", "semver": "^5.3.0",
"stream-to-blob-url": "^2.1.1",
"strip-markdown": "^3.0.3", "strip-markdown": "^3.0.3",
"style-loader": "^0.23.1", "style-loader": "^0.23.1",
"terser-webpack-plugin": "^4.2.3", "terser-webpack-plugin": "^4.2.3",
"three-full": "^28.0.2", "three-full": "^28.0.2",
"tiny-relative-date": "^1.3.0",
"tree-kill": "^1.1.0",
"unist-util-visit": "^2.0.3", "unist-util-visit": "^2.0.3",
"uuid": "^8.3.2", "uuid": "^8.3.2",
"video.js": "^7.14.3", "video.js": "^7.14.3",
"videojs-contrib-quality-levels": "^2.0.9", "videojs-contrib-quality-levels": "^2.0.9",
"videojs-event-tracking": "^1.0.1", "videojs-event-tracking": "^1.0.1",
"villain-react": "^1.0.9", "villain-react": "^1.0.9",
"wavesurfer.js": "^2.2.1",
"webpack": "^4.44.2", "webpack": "^4.44.2",
"webpack-bundle-analyzer": "^3.1.0", "webpack-bundle-analyzer": "^3.1.0",
"webpack-cli": "^3.3.10", "webpack-cli": "^3.3.10",
@ -210,14 +228,15 @@
"webpack-hot-middleware": "^2.24.3", "webpack-hot-middleware": "^2.24.3",
"webpack-merge": "^4.2.1", "webpack-merge": "^4.2.1",
"webpack-node-externals": "^1.7.2", "webpack-node-externals": "^1.7.2",
"y18n": "^4.0.1",
"yarnhook": "^0.2.0" "yarnhook": "^0.2.0"
}, },
"engines": { "engines": {
"node": ">=16.13", "node": ">=7",
"yarn": "^1.3" "yarn": "^1.3"
}, },
"lbrySettings": { "lbrySettings": {
"lbrynetDaemonVersion": "0.113.0", "lbrynetDaemonVersion": "0.107.2",
"lbrynetDaemonUrlTemplate": "https://github.com/lbryio/lbry/releases/download/vDAEMONVER/lbrynet-OSNAME.zip", "lbrynetDaemonUrlTemplate": "https://github.com/lbryio/lbry/releases/download/vDAEMONVER/lbrynet-OSNAME.zip",
"lbrynetDaemonDir": "static/daemon", "lbrynetDaemonDir": "static/daemon",
"lbrynetDaemonFileName": "lbrynet" "lbrynetDaemonFileName": "lbrynet"

View file

@ -2316,11 +2316,5 @@
"Installing, please wait...": "Installing, please wait...", "Installing, please wait...": "Installing, please wait...",
"There was an error during installation. Please, try again.": "There was an error during installation. Please, try again.", "There was an error during installation. Please, try again.": "There was an error during installation. Please, try again.",
"Odysee Connect --[Section in Help Page]--": "Odysee Connect", "Odysee Connect --[Section in Help Page]--": "Odysee Connect",
"Your hub has blocked this content because it subscribes to the following blocking channel:": "Your hub has blocked this content because it subscribes to the following blocking channel:",
"Your hub has blocked access to this content do to a complaint received under the US Digital Millennium Copyright Act.": "Your hub has blocked access to this content do to a complaint received under the US Digital Millennium Copyright Act.",
"Autoplay Next is on.": "Autoplay Next is on.",
"This will be visible in a few minutes after you submit this form.": "This will be visible in a few minutes after you submit this form.",
"Anon --[used in <%anonymous% Reposted>]--": "Anon",
"Your update is now pending. It will take a few minutes to appear for other users.": "Your update is now pending. It will take a few minutes to appear for other users.",
"--end--": "--end--" "--end--": "--end--"
} }

View file

@ -1,11 +1,4 @@
// @flow // @flow
/*
Removed Watchman (internal view tracking) code.
This file may eventually implement cantina
Refer to 0cc0e213a5c5bf9e2a76316df5d9da4b250a13c3 for initial integration commit
refer to ___ for removal commit.
*/
import { Lbryio } from 'lbryinc'; import { Lbryio } from 'lbryinc';
import * as Sentry from '@sentry/browser'; import * as Sentry from '@sentry/browser';
import MatomoTracker from '@datapunt/matomo-tracker-js'; import MatomoTracker from '@datapunt/matomo-tracker-js';
@ -21,6 +14,9 @@ const devInternalApis = process.env.LBRY_API_URL && process.env.LBRY_API_URL.inc
export const SHARE_INTERNAL = 'shareInternal'; export const SHARE_INTERNAL = 'shareInternal';
const SHARE_THIRD_PARTY = 'shareThirdParty'; const SHARE_THIRD_PARTY = 'shareThirdParty';
const WATCHMAN_BACKEND_ENDPOINT = 'https://watchman.na-backend.odysee.com/reports/playback';
// const SEND_DATA_TO_WATCHMAN_INTERVAL = 10; // in seconds
if (isProduction) { if (isProduction) {
ElectronCookies.enable({ ElectronCookies.enable({
origin: 'https://lbry.tv', origin: 'https://lbry.tv',
@ -72,10 +68,114 @@ type LogPublishParams = {
let internalAnalyticsEnabled: boolean = false; let internalAnalyticsEnabled: boolean = false;
if (window.localStorage.getItem(SHARE_INTERNAL) === 'true') internalAnalyticsEnabled = true; if (window.localStorage.getItem(SHARE_INTERNAL) === 'true') internalAnalyticsEnabled = true;
/**
* Determine the mobile device type viewing the data
* This function returns one of 'and' (Android), 'ios', or 'web'.
*
* @returns {String}
*/
function getDeviceType() {
return 'dsk';
}
// variables initialized for watchman
let amountOfBufferEvents = 0;
let amountOfBufferTimeInMS = 0;
let videoType, userId, claimUrl, playerPoweredBy, videoPlayer, bitrateAsBitsPerSecond;
let lastSentTime;
// calculate data for backend, send them, and reset buffer data for next interval
async function sendAndResetWatchmanData() {
if (!userId) {
return 'Can only be used with a user id';
}
if (!videoPlayer) {
return 'Video player not initialized';
}
let timeSinceLastIntervalSend = new Date() - lastSentTime;
lastSentTime = new Date();
let protocol;
if (videoType === 'application/x-mpegURL') {
protocol = 'hls';
// get bandwidth if it exists from the texttrack (so it's accurate if user changes quality)
// $FlowFixMe
bitrateAsBitsPerSecond = videoPlayer.textTracks?.().tracks_[0]?.activeCues[0]?.value?.bandwidth;
} else {
protocol = 'stb';
}
// current position in video in MS
const positionInVideo = Math.round(videoPlayer.currentTime()) * 1000;
// get the duration marking the time in the video for relative position calculation
const totalDurationInSeconds = Math.round(videoPlayer.duration());
// build object for watchman backend
const objectToSend = {
rebuf_count: amountOfBufferEvents,
rebuf_duration: amountOfBufferTimeInMS,
url: claimUrl.replace('lbry://', ''),
device: getDeviceType(),
duration: timeSinceLastIntervalSend,
protocol,
player: playerPoweredBy,
user_id: userId.toString(),
position: Math.round(positionInVideo),
rel_position: Math.round((positionInVideo / (totalDurationInSeconds * 1000)) * 100),
bitrate: bitrateAsBitsPerSecond,
bandwidth: undefined,
// ...(userDownloadBandwidthInBitsPerSecond && {bandwidth: userDownloadBandwidthInBitsPerSecond}), // add bandwidth if populated
};
// post to watchman
await sendWatchmanData(objectToSend);
// reset buffer data
amountOfBufferEvents = 0;
amountOfBufferTimeInMS = 0;
}
let watchmanInterval;
// clear watchman interval and mark it as null (when video paused)
function stopWatchmanInterval() {
clearInterval(watchmanInterval);
watchmanInterval = null;
}
// creates the setInterval that will run send to watchman on recurring basis
function startWatchmanIntervalIfNotRunning() {
if (!watchmanInterval) {
// instantiate the first time to calculate duration from
lastSentTime = new Date();
}
}
// post data to the backend
async function sendWatchmanData(body) {
try {
const response = await fetch(WATCHMAN_BACKEND_ENDPOINT, {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
});
return response;
} catch (err) {}
}
const analytics: Analytics = { const analytics: Analytics = {
// receive buffer events from tracking plugin and save buffer amounts and times for backend call // receive buffer events from tracking plugin and save buffer amounts and times for backend call
videoBufferEvent: async (claim, data) => { videoBufferEvent: async (claim, data) => {
// stub amountOfBufferEvents = amountOfBufferEvents + 1;
amountOfBufferTimeInMS = amountOfBufferTimeInMS + data.bufferDuration;
},
onDispose: () => {
stopWatchmanInterval();
}, },
/** /**
* Is told whether video is being started or paused, and adjusts interval accordingly * Is told whether video is being started or paused, and adjusts interval accordingly
@ -83,9 +183,40 @@ const analytics: Analytics = {
* @param {object} passedPlayer - VideoJS Player object * @param {object} passedPlayer - VideoJS Player object
*/ */
videoIsPlaying: (isPlaying, passedPlayer) => { videoIsPlaying: (isPlaying, passedPlayer) => {
// stub 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();
}
}, },
videoStartEvent: (claimId, timeToStartVideo, poweredBy, passedUserId, canonicalUrl, passedPlayer, videoBitrate) => { videoStartEvent: (claimId, timeToStartVideo, poweredBy, passedUserId, canonicalUrl, passedPlayer, videoBitrate) => {
// populate values for watchman when video starts
userId = passedUserId;
claimUrl = canonicalUrl;
playerPoweredBy = poweredBy;
videoType = passedPlayer.currentSource().type;
videoPlayer = passedPlayer;
bitrateAsBitsPerSecond = videoBitrate;
// sendPromMetric('time_to_start', duration); // sendPromMetric('time_to_start', duration);
sendMatomoEvent('Media', 'TimeToStart', claimId, timeToStartVideo); sendMatomoEvent('Media', 'TimeToStart', claimId, timeToStartVideo);
}, },
@ -251,9 +382,24 @@ function sendMatomoEvent(category, action, name, value) {
} }
} }
// Prometheus
// function sendPromMetric(name: string, value?: number) {
// if (IS_WEB) {
// let url = new URL(SDK_API_PATH + '/metric/ui');
// const params = { name: name, value: value ? value.toString() : '' };
// url.search = new URLSearchParams(params).toString();
// return fetch(url, { method: 'post' });
// }
// }
const MatomoInstance = new MatomoTracker({ const MatomoInstance = new MatomoTracker({
urlBase: MATOMO_URL, urlBase: MATOMO_URL,
siteId: MATOMO_ID, // optional, default value: `1` siteId: MATOMO_ID, // optional, default value: `1`
// heartBeat: { // optional, enabled by default
// active: true, // optional, default value: true
// seconds: 10 // optional, default value: `15
// },
// linkTracking: false // optional, default value: true
}); });
analytics.pageView(generateInitialUrl(window.location.hash)); analytics.pageView(generateInitialUrl(window.location.hash));

View file

@ -5,6 +5,7 @@ import Icon from 'component/common/icon';
import classnames from 'classnames'; import classnames from 'classnames';
import { NavLink } from 'react-router-dom'; import { NavLink } from 'react-router-dom';
import { formatLbryUrlForWeb } from 'util/url'; import { formatLbryUrlForWeb } from 'util/url';
import * as PAGES from 'constants/pages';
import useCombinedRefs from 'effects/use-combined-refs'; import useCombinedRefs from 'effects/use-combined-refs';
type Props = { type Props = {
@ -33,6 +34,7 @@ type Props = {
onMouseLeave: ?(any) => any, onMouseLeave: ?(any) => any,
pathname: string, pathname: string,
emailVerified: boolean, emailVerified: boolean,
requiresAuth: ?boolean,
myref: any, myref: any,
dispatch: any, dispatch: any,
'aria-label'?: string, 'aria-label'?: string,
@ -64,6 +66,7 @@ const Button = forwardRef<any, {}>((props: Props, ref: any) => {
iconColor, iconColor,
activeClass, activeClass,
emailVerified, emailVerified,
requiresAuth,
myref, myref,
dispatch, // <button> doesn't know what to do with dispatch dispatch, // <button> doesn't know what to do with dispatch
pathname, pathname,
@ -72,7 +75,7 @@ const Button = forwardRef<any, {}>((props: Props, ref: any) => {
...otherProps ...otherProps
} = props; } = props;
const disable = disabled; const disable = disabled || (user === null && requiresAuth);
const combinedClassName = classnames( const combinedClassName = classnames(
'button', 'button',
@ -180,6 +183,31 @@ const Button = forwardRef<any, {}>((props: Props, ref: any) => {
} }
} }
if (requiresAuth && !emailVerified) {
let redirectUrl = `/$/${PAGES.AUTH}?redirect=${pathname}`;
if (authSrc) {
redirectUrl += `&src=${authSrc}`;
}
return (
<NavLink
exact
onClick={(e) => {
e.stopPropagation();
}}
to={redirectUrl}
title={title || defaultTooltip}
disabled={disable}
className={combinedClassName}
activeClassName={activeClass}
aria-label={ariaLabel}
>
{content}
</NavLink>
);
}
return path ? ( return path ? (
<NavLink <NavLink
exact exact

View file

@ -3,7 +3,7 @@ import * as MODALS from 'constants/modal_types';
import * as ICONS from 'constants/icons'; import * as ICONS from 'constants/icons';
import React from 'react'; import React from 'react';
import classnames from 'classnames'; import classnames from 'classnames';
import { FormField, FormFieldAreaAdvanced } from 'component/common/form'; import { FormField } from 'component/common/form';
import Button from 'component/button'; import Button from 'component/button';
import TagsSearch from 'component/tagsSearch'; import TagsSearch from 'component/tagsSearch';
import ErrorText from 'component/common/error-text'; import ErrorText from 'component/common/error-text';
@ -325,6 +325,7 @@ function ChannelForm(props: Props) {
uri={uri} uri={uri}
thumbnailPreview={thumbnailPreview} thumbnailPreview={thumbnailPreview}
allowGifs allowGifs
showDelayedMessage={isUpload.thumbnail}
setThumbUploadError={setThumbError} setThumbUploadError={setThumbError}
thumbUploadError={thumbError} thumbUploadError={thumbError}
/> />
@ -376,7 +377,7 @@ function ChannelForm(props: Props) {
onChange={(e) => setParams({ ...params, title: e.target.value })} onChange={(e) => setParams({ ...params, title: e.target.value })}
maxLength={MAX_TITLE_LEN} maxLength={MAX_TITLE_LEN}
/> />
<FormFieldAreaAdvanced <FormField
type="markdown" type="markdown"
name="content_description2" name="content_description2"
label={__('Description')} label={__('Description')}

View file

@ -17,7 +17,7 @@ import { useHistory } from 'react-router-dom';
import { isNameValid, regexInvalidURI } from 'util/lbryURI'; import { isNameValid, regexInvalidURI } from 'util/lbryURI';
import * as THUMBNAIL_STATUSES from 'constants/thumbnail_upload_statuses'; import * as THUMBNAIL_STATUSES from 'constants/thumbnail_upload_statuses';
import { Tabs, TabList, Tab, TabPanels, TabPanel } from 'component/common/tabs'; import { Tabs, TabList, Tab, TabPanels, TabPanel } from 'component/common/tabs';
import { FormField, FormFieldAreaAdvanced } from 'component/common/form'; import { FormField } from 'component/common/form';
import { handleBidChange } from 'util/publish'; import { handleBidChange } from 'util/publish';
import { FF_MAX_CHARS_IN_DESCRIPTION } from 'constants/form-field'; import { FF_MAX_CHARS_IN_DESCRIPTION } from 'constants/form-field';
import { INVALID_NAME_ERROR } from 'constants/claim'; import { INVALID_NAME_ERROR } from 'constants/claim';
@ -371,7 +371,7 @@ function CollectionForm(props: Props) {
usePublishFormMode usePublishFormMode
/> />
</fieldset-section> </fieldset-section>
<FormFieldAreaAdvanced <FormField
type="markdown" type="markdown"
name="content_description2" name="content_description2"
label={__('Description')} label={__('Description')}

View file

@ -17,7 +17,7 @@ import CommentBadge from 'component/common/comment-badge'; // have this?
import ChannelThumbnail from 'component/channelThumbnail'; import ChannelThumbnail from 'component/channelThumbnail';
import { Menu, MenuButton } from '@reach/menu-button'; import { Menu, MenuButton } from '@reach/menu-button';
import Icon from 'component/common/icon'; import Icon from 'component/common/icon';
import { FormFieldAreaAdvanced, Form } from 'component/common/form'; import { FormField, Form } from 'component/common/form';
import classnames from 'classnames'; import classnames from 'classnames';
import usePersistedState from 'effects/use-persisted-state'; import usePersistedState from 'effects/use-persisted-state';
import CommentReactions from 'component/commentReactions'; import CommentReactions from 'component/commentReactions';
@ -319,7 +319,7 @@ function CommentView(props: Props) {
<div> <div>
{isEditing ? ( {isEditing ? (
<Form onSubmit={handleSubmit}> <Form onSubmit={handleSubmit}>
<FormFieldAreaAdvanced <FormField
className="comment__edit-input" className="comment__edit-input"
type={advancedEditor ? 'markdown' : 'textarea'} type={advancedEditor ? 'markdown' : 'textarea'}
name="editing_comment" name="editing_comment"

View file

@ -1,32 +0,0 @@
// @flow
import React from 'react';
import SelectChannel from 'component/selectChannel';
import Button from 'component/button';
import * as ICONS from 'constants/icons';
type Props = {
isReply: boolean,
advancedHandler: () => void,
advanced: boolean,
};
export default function CommentCreateHeader(props: Props) {
const { isReply, advancedHandler, advanced } = props;
return (
<div className="comment-create__header">
<div className="comment-create__label-wrapper">
<span className="comment-create__label">{(isReply ? __('Replying as') : __('Comment as')) + ' '}</span>
<SelectChannel tiny />
</div>
<div className="form-field__quick-action">
<Button
button="alt"
icon={advanced ? ICONS.SIMPLE_EDITOR : ICONS.ADVANCED_EDITOR}
onClick={advancedHandler}
aria-label={isReply ? undefined : advanced ? __('Simple Editor') : __('Advanced Editor')}
/>
</div>
</div>
);
}

View file

@ -4,7 +4,7 @@ import 'scss/component/_comment-create.scss';
import { buildValidSticker } from 'util/comments'; import { buildValidSticker } from 'util/comments';
import { FF_MAX_CHARS_IN_COMMENT } from 'constants/form-field'; import { FF_MAX_CHARS_IN_COMMENT } from 'constants/form-field';
import { FormFieldAreaAdvanced, Form } from 'component/common/form'; import { FormField, Form } from 'component/common/form';
import { getChannelIdFromClaim } from 'util/claim'; import { getChannelIdFromClaim } from 'util/claim';
import { Lbryio } from 'lbryinc'; import { Lbryio } from 'lbryinc';
import { useHistory } from 'react-router'; import { useHistory } from 'react-router';
@ -22,8 +22,8 @@ import I18nMessage from 'component/i18nMessage';
import Icon from 'component/common/icon'; import Icon from 'component/common/icon';
import OptimizedImage from 'component/optimizedImage'; import OptimizedImage from 'component/optimizedImage';
import React from 'react'; import React from 'react';
import SelectChannel from 'component/selectChannel';
import StickerSelector from './sticker-selector'; import StickerSelector from './sticker-selector';
import CommentCreateHeader from './comment-create-header';
import type { ElementRef } from 'react'; import type { ElementRef } from 'react';
import UriIndicator from 'component/uriIndicator'; import UriIndicator from 'component/uriIndicator';
import usePersistedState from 'effects/use-persisted-state'; import usePersistedState from 'effects/use-persisted-state';
@ -363,6 +363,31 @@ export function CommentCreate(props: Props) {
.catch(() => {}); .catch(() => {});
}, [canReceiveFiatTip, claim.claim_id, claim.name, claim.signing_channel, stickerSelector]); }, [canReceiveFiatTip, claim.claim_id, claim.name, claim.signing_channel, stickerSelector]);
// LIVESTREAM ONLY - REMOVE
// Handle keyboard shortcut comment creation
// React.useEffect(() => {
// function altEnterListener(e: SyntheticKeyboardEvent<*>) {
// const inputRef = formFieldRef && formFieldRef.current && formFieldRef.current.input;
//
// if (inputRef && inputRef.current === document.activeElement) {
// // $FlowFixMe
// const isTyping = e.target.attributes['term'];
//
// if (((isLivestream && !isTyping) || e.ctrlKey || e.metaKey) && e.keyCode === KEYCODES.ENTER) {
// e.preventDefault();
// buttonRef.current.click();
// }
// }
// }
//
// window.addEventListener('keydown', altEnterListener);
//
// // removes the listener so it doesn't cause problems elsewhere in the app
// return () => {
// window.removeEventListener('keydown', altEnterListener);
// };
// }, [isLivestream]);
// ************************************************************************** // **************************************************************************
// Render // Render
// ************************************************************************** // **************************************************************************
@ -384,11 +409,7 @@ export function CommentCreate(props: Props) {
push(pathPlusRedirect); push(pathPlusRedirect);
}} }}
> >
<FormFieldAreaAdvanced <FormField type="textarea" name={'comment_signup_prompt'} placeholder={__('Say something about this...')} />
type="textarea"
name={'comment_signup_prompt'}
placeholder={__('Say something about this...')}
/>
<div className="section__actions--no-margin"> <div className="section__actions--no-margin">
<Button disabled button="primary" label={__('Post --[button to submit something]--')} /> <Button disabled button="primary" label={__('Post --[button to submit something]--')} />
</div> </div>
@ -399,22 +420,22 @@ export function CommentCreate(props: Props) {
return ( return (
<Form <Form
onSubmit={() => {}} onSubmit={() => {}}
className={classnames('comment-create', { className={classnames('commentCreate', {
'comment-create--reply': isReply, 'commentCreate--reply': isReply,
'comment-create--nestedReply': isNested, 'commentCreate--nestedReply': isNested,
'comment-create--bottom': bottom, 'commentCreate--bottom': bottom,
})} })}
> >
{/* Input Box/Preview Box */} {/* Input Box/Preview Box */}
{stickerSelector ? ( {stickerSelector ? (
<StickerSelector onSelect={(sticker) => handleSelectSticker(sticker)} claimIsMine={claimIsMine} /> <StickerSelector onSelect={(sticker) => handleSelectSticker(sticker)} claimIsMine={claimIsMine} />
) : isReviewingStickerComment && activeChannelClaim && selectedSticker ? ( ) : isReviewingStickerComment && activeChannelClaim && selectedSticker ? (
<div className="comment-create__stickerPreview"> <div className="commentCreate__stickerPreview">
<div className="comment-create__stickerPreviewInfo"> <div className="commentCreate__stickerPreviewInfo">
<ChannelThumbnail xsmall uri={activeChannelClaim.canonical_url} /> <ChannelThumbnail xsmall uri={activeChannelClaim.canonical_url} />
<UriIndicator uri={activeChannelClaim.canonical_url} link /> <UriIndicator uri={activeChannelClaim.canonical_url} link />
</div> </div>
<div className="comment-create__stickerPreviewImage"> <div className="commentCreate__stickerPreviewImage">
<OptimizedImage src={selectedSticker && selectedSticker.url} waitLoad loading="lazy" /> <OptimizedImage src={selectedSticker && selectedSticker.url} waitLoad loading="lazy" />
</div> </div>
{/* figure out lbc sticker prices */} {/* figure out lbc sticker prices */}
@ -426,15 +447,15 @@ export function CommentCreate(props: Props) {
)} )}
</div> </div>
) : isReviewingSupportComment && activeChannelClaim ? ( ) : isReviewingSupportComment && activeChannelClaim ? (
<div className="comment-create__supportCommentPreview"> <div className="commentCreate__supportCommentPreview">
<CreditAmount <CreditAmount
amount={tipAmount} amount={tipAmount}
className="comment-create__supportCommentPreviewAmount" className="commentCreate__supportCommentPreviewAmount"
isFiat={activeTab === TAB_FIAT} isFiat={activeTab === TAB_FIAT}
size={activeTab === TAB_LBC ? 18 : 2} size={activeTab === TAB_LBC ? 18 : 2}
/> />
<ChannelThumbnail xsmall uri={activeChannelClaim.canonical_url} /> <ChannelThumbnail xsmall uri={activeChannelClaim.canonical_url} />
<div className="comment-create__supportCommentBody"> <div className="commentCreate__supportCommentBody">
<UriIndicator uri={activeChannelClaim.canonical_url} link /> <UriIndicator uri={activeChannelClaim.canonical_url} link />
<div>{commentValue}</div> <div>{commentValue}</div>
</div> </div>
@ -449,22 +470,23 @@ export function CommentCreate(props: Props) {
/> />
)} )}
<FormFieldAreaAdvanced <FormField
autoFocus={isReply} autoFocus={isReply}
charCount={charCount} charCount={charCount}
className={isReply ? 'content_reply' : 'content_comment'} className={isReply ? 'content_reply' : 'content_comment'}
disabled={isFetchingChannels} disabled={isFetchingChannels}
header={ label={
<CommentCreateHeader <div className="commentCreate__labelWrapper">
isReply={isReply} <span className="commentCreate__label">{(isReply ? __('Replying as') : __('Comment as')) + ' '}</span>
advanced={advancedEditor} <SelectChannel tiny />
advancedHandler={() => setAdvancedEditor(!advancedEditor)} </div>
/>
} }
name={isReply ? 'content_reply' : 'content_description'} name={isReply ? 'content_reply' : 'content_description'}
quickActionLabel={isReply ? undefined : advancedEditor ? __('Simple Editor') : __('Advanced Editor')}
ref={formFieldRef} ref={formFieldRef}
onChange={handleCommentChange} onChange={handleCommentChange}
openEmoteMenu={() => setShowEmotes(!showEmotes)} openEmoteMenu={() => setShowEmotes(!showEmotes)}
quickActionHandler={() => setAdvancedEditor(!advancedEditor)}
onFocus={onTextareaFocus} onFocus={onTextareaFocus}
onBlur={onTextareaBlur} onBlur={onTextareaBlur}
placeholder={__('Say something about this...')} placeholder={__('Say something about this...')}
@ -632,7 +654,7 @@ export function CommentCreate(props: Props) {
{/* Help Text */} {/* Help Text */}
{deletedComment && <div className="error__text">{__('This comment has been deleted.')}</div>} {deletedComment && <div className="error__text">{__('This comment has been deleted.')}</div>}
{!!minAmount && ( {!!minAmount && (
<div className="help--notice comment-create__minAmountNotice"> <div className="help--notice commentCreate__minAmountNotice">
<I18nMessage tokens={{ lbc: <CreditAmount noFormat amount={minAmount} /> }}> <I18nMessage tokens={{ lbc: <CreditAmount noFormat amount={minAmount} /> }}>
{minTip ? 'Comment min: %lbc%' : minSuper ? 'HyperChat min: %lbc%' : ''} {minTip ? 'Comment min: %lbc%' : minSuper ? 'HyperChat min: %lbc%' : ''}
</I18nMessage> </I18nMessage>

View file

@ -366,9 +366,9 @@ const CommentActionButtons = (actionButtonsProps: ActionButtonsProps) => {
const sortButtonProps = { activeSort: sort, changeSort }; const sortButtonProps = { activeSort: sort, changeSort };
return ( return (
<div className={'comment__actions-row'}> <>
{totalComments > 1 && ENABLE_COMMENT_REACTIONS && ( {totalComments > 1 && ENABLE_COMMENT_REACTIONS && (
<div className="comment__sort-group"> <span className="comment__sort">
<SortButton {...sortButtonProps} label={__('Best')} icon={ICONS.BEST} sortOption={SORT_BY.POPULARITY} /> <SortButton {...sortButtonProps} label={__('Best')} icon={ICONS.BEST} sortOption={SORT_BY.POPULARITY} />
<SortButton <SortButton
{...sortButtonProps} {...sortButtonProps}
@ -377,16 +377,17 @@ const CommentActionButtons = (actionButtonsProps: ActionButtonsProps) => {
sortOption={SORT_BY.CONTROVERSY} sortOption={SORT_BY.CONTROVERSY}
/> />
<SortButton {...sortButtonProps} label={__('New')} icon={ICONS.NEW} sortOption={SORT_BY.NEWEST} /> <SortButton {...sortButtonProps} label={__('New')} icon={ICONS.NEW} sortOption={SORT_BY.NEWEST} />
</div> </span>
)} )}
<div className="button_refresh">
<Button button="alt" icon={ICONS.REFRESH} title={__('Refresh')} onClick={() => setPage(0)} />
</div>
{allServers.length >= 2 && ( {allServers.length >= 2 && (
<div className="button__selected-server"> <div className="button_selectedServer">
<FormField <FormField
type="select-tiny" type="select-tiny"
onChange={function (x) { onChange={function (x) {
const selectedServer = x.target.value; const selectedServer = x.target.value;
setPage(0);
setCommentServer(selectedServer); setCommentServer(selectedServer);
if (selectedServer === defaultServer.url) { if (selectedServer === defaultServer.url) {
Comments.setServerUrl(undefined); Comments.setServerUrl(undefined);
@ -406,10 +407,7 @@ const CommentActionButtons = (actionButtonsProps: ActionButtonsProps) => {
</FormField> </FormField>
</div> </div>
)} )}
<div className="button_refresh"> </>
<Button button="alt" icon={ICONS.REFRESH} title={__('Refresh')} onClick={() => setPage(0)} />
</div>
</div>
); );
}; };

View file

@ -3,8 +3,8 @@ import React from 'react';
import { useRadioState, Radio, RadioGroup } from 'reakit/Radio'; import { useRadioState, Radio, RadioGroup } from 'reakit/Radio';
type Props = { type Props = {
files: Array<File>, files: Array<WebFile>,
onChange: (File | void) => void, onChange: (WebFile | void) => void,
}; };
type RadioProps = { type RadioProps = {
@ -26,16 +26,16 @@ function FileList(props: Props) {
const getFile = (value?: string) => { const getFile = (value?: string) => {
if (files && files.length) { if (files && files.length) {
return files.find((file: File) => file.name === value); return files.find((file: WebFile) => file.name === value);
} }
}; };
React.useEffect(() => { React.useEffect(() => {
if (radio.items.length) { if (radio.stops.length) {
if (!radio.currentId) { if (!radio.currentId) {
radio.first(); radio.first();
} else { } else {
const first = radio.items[0].ref.current; const first = radio.stops[0].ref.current;
// First auto-selection // First auto-selection
if (first && first.id === radio.currentId && !radio.state) { if (first && first.id === radio.currentId && !radio.state) {
const file = getFile(first.value); const file = getFile(first.value);
@ -46,12 +46,12 @@ function FileList(props: Props) {
if (radio.state) { if (radio.state) {
// Find selected element // Find selected element
const stop = radio.items.find((item) => item.id === radio.currentId); const stop = radio.stops.find(item => item.id === radio.currentId);
const element = stop && stop.ref.current; const element = stop && stop.ref.current;
// Only update state if new item is selected // Only update state if new item is selected
if (element && element.value !== radio.state) { if (element && element.value !== radio.state) {
const file = getFile(element.value); const file = getFile(element.value);
// Select new file and update state // Sselect new file and update state
onChange(file); onChange(file);
radio.setState(element.value); radio.setState(element.value);
} }

View file

@ -1,29 +1,25 @@
// @flow // @flow
import * as React from 'react'; import * as React from 'react';
import * as remote from '@electron/remote'; import * as remote from '@electron/remote';
import { ipcRenderer } from 'electron';
import Button from 'component/button'; import Button from 'component/button';
import { FormField } from 'component/common/form'; import { FormField } from 'component/common/form';
type Props = { type Props = {
type: string, type: string,
currentPath?: ?string, currentPath?: ?string,
onFileChosen: (FileWithPath) => void, onFileChosen: (WebFile) => void,
label?: string, label?: string,
placeholder?: string, placeholder?: string,
accept?: string, accept?: string,
error?: string, error?: string,
disabled?: boolean, disabled?: boolean,
autoFocus?: boolean, autoFocus?: boolean,
filters?: Array<{ name: string, extension: string[] }>,
readFile?: boolean,
}; };
class FileSelector extends React.PureComponent<Props> { class FileSelector extends React.PureComponent<Props> {
static defaultProps = { static defaultProps = {
autoFocus: false, autoFocus: false,
type: 'file', type: 'file',
readFile: true,
}; };
fileInput: React.ElementRef<any>; fileInput: React.ElementRef<any>;
@ -45,7 +41,7 @@ class FileSelector extends React.PureComponent<Props> {
const file = files[0]; const file = files[0];
if (this.props.onFileChosen) { if (this.props.onFileChosen) {
this.props.onFileChosen({ file, path: file.path || file.name }); this.props.onFileChosen(file);
} }
this.fileInput.current.value = null; // clear the file input this.fileInput.current.value = null; // clear the file input
}; };
@ -68,26 +64,12 @@ class FileSelector extends React.PureComponent<Props> {
properties = ['openDirectory']; properties = ['openDirectory'];
} }
remote.dialog remote.dialog.showOpenDialog({ properties, defaultPath }).then((result) => {
.showOpenDialog({
properties,
defaultPath,
filters: this.props.filters,
})
.then((result) => {
const path = result && result.filePaths[0]; const path = result && result.filePaths[0];
if (path) { if (path) {
return ipcRenderer.invoke('get-file-from-path', path, this.props.readFile); // $FlowFixMe
this.props.onFileChosen({ path });
} }
})
.then((result) => {
if (!result) {
return;
}
const file = new File([result.buffer], result.name, {
type: result.mime,
});
this.props.onFileChosen({ file, path: result.path });
}); });
}; };

View file

@ -1,240 +0,0 @@
// @flow
import 'easymde/dist/easymde.min.css';
import { FF_MAX_CHARS_DEFAULT } from 'constants/form-field';
import { openEditorMenu, stopContextMenu } from 'util/context-menu';
import * as ICONS from 'constants/icons';
import Button from 'component/button';
import MarkdownPreview from 'component/common/markdown-preview';
import React from 'react';
import ReactDOMServer from 'react-dom/server';
import SimpleMDE from 'react-simplemde-editor';
import TextareaWithSuggestions from 'component/textareaWithSuggestions';
import type { ElementRef, Node } from 'react';
type Props = {
autoFocus?: boolean,
blockWrap: boolean,
charCount?: number,
children?: React$Node,
disabled?: boolean,
helper?: string | React$Node,
hideSuggestions?: boolean,
isLivestream?: boolean,
label?: string | Node,
labelOnLeft: boolean,
name: string,
noEmojis?: boolean,
placeholder?: string | number,
quickActionLabel?: string,
textAreaMaxLength?: number,
type?: string,
value?: string | number,
onChange?: (any) => any,
openEmoteMenu?: () => void,
quickActionHandler?: (any) => any,
render?: () => React$Node,
header?: React$Node,
};
export class FormFieldAreaAdvanced extends React.PureComponent<Props> {
static defaultProps = { labelOnLeft: false, blockWrap: true };
input: { current: ElementRef<any> };
constructor(props: Props) {
super(props);
this.input = React.createRef();
}
componentDidMount() {
const { autoFocus } = this.props;
const input = this.input.current;
if (input && autoFocus) input.focus();
}
render() {
const {
autoFocus,
blockWrap,
charCount,
children,
helper,
hideSuggestions,
isLivestream,
label,
header,
labelOnLeft,
name,
noEmojis,
quickActionLabel,
textAreaMaxLength,
type,
openEmoteMenu,
quickActionHandler,
render,
...inputProps
} = this.props;
// Ideally, the character count should (and can) be appended to the
// SimpleMDE's "options::status" bar. However, I couldn't figure out how
// to pass the current value to it's callback, nor query the current
// text length from the callback. So, we'll use our own widget.
const hasCharCount = charCount !== undefined && charCount >= 0;
const countInfo = hasCharCount && textAreaMaxLength !== undefined && (
<span className="comment__char-count-mde">{`${charCount || '0'}/${textAreaMaxLength}`}</span>
);
const quickAction =
quickActionLabel && quickActionHandler ? (
<div className="form-field__quick-action">
<Button button="link" onClick={quickActionHandler} label={quickActionLabel} />
</div>
) : null;
const input = () => {
switch (type) {
case 'markdown':
const handleEvents = { contextmenu: openEditorMenu };
const getInstance = (editor) => {
// SimpleMDE max char check
editor.codemirror.on('beforeChange', (instance, changes) => {
if (textAreaMaxLength && changes.update) {
var str = changes.text.join('\n');
var delta = str.length - (instance.indexFromPos(changes.to) - instance.indexFromPos(changes.from));
if (delta <= 0) return;
delta = instance.getValue().length + delta - textAreaMaxLength;
if (delta > 0) {
str = str.substring(0, str.length - delta);
changes.update(changes.from, changes.to, str.split('\n'));
}
}
});
// "Create Link (Ctrl-K)": highlight URL instead of label:
editor.codemirror.on('changes', (instance, changes) => {
try {
// Grab the last change from the buffered list. I assume the
// buffered one ('changes', instead of 'change') is more efficient,
// and that "Create Link" will always end up last in the list.
const lastChange = changes[changes.length - 1];
if (lastChange.origin === '+input') {
// https://github.com/Ionaru/easy-markdown-editor/blob/8fa54c496f98621d5f45f57577ce630bee8c41ee/src/js/easymde.js#L765
const EASYMDE_URL_PLACEHOLDER = '(https://)';
// The URL placeholder is always placed last, so just look at the
// last text in the array to also cover the multi-line case:
const urlLineText = lastChange.text[lastChange.text.length - 1];
if (urlLineText.endsWith(EASYMDE_URL_PLACEHOLDER) && urlLineText !== '[]' + EASYMDE_URL_PLACEHOLDER) {
const from = lastChange.from;
const to = lastChange.to;
const isSelectionMultiline = lastChange.text.length > 1;
const baseIndex = isSelectionMultiline ? 0 : from.ch;
// Everything works fine for the [Ctrl-K] case, but for the
// [Button] case, this handler happens before the original
// code, thus our change got wiped out.
// Add a small delay to handle that case.
setTimeout(() => {
instance.setSelection(
{ line: to.line, ch: baseIndex + urlLineText.lastIndexOf('(') + 1 },
{ line: to.line, ch: baseIndex + urlLineText.lastIndexOf(')') }
);
}, 25);
}
}
} catch (e) {} // Do nothing (revert to original behavior)
});
};
return (
<div className="form-field--SimpleMDE" onContextMenu={stopContextMenu}>
<fieldset-section>
{!header && (
<div className="form-field__two-column">
<div>
<label htmlFor={name}>{label}</label>
</div>
{quickAction}
</div>
)}
{!!header && <div className="form-field__textarea-header">{header}</div>}
<SimpleMDE
{...inputProps}
id={name}
type="textarea"
events={handleEvents}
getMdeInstance={getInstance}
options={{
spellChecker: true,
hideIcons: ['heading', 'image', 'fullscreen', 'side-by-side'],
previewRender(plainText) {
const preview = <MarkdownPreview content={plainText} noDataStore />;
return ReactDOMServer.renderToString(preview);
},
}}
/>
{countInfo}
</fieldset-section>
</div>
);
case 'textarea':
return (
<fieldset-section>
{!header && (label || quickAction) && (
<div className="form-field__two-column">
<label htmlFor={name}>{label}</label>
{quickAction}
</div>
)}
{!!header && <div className="form-field__textarea-header">{header}</div>}
{hideSuggestions ? (
<textarea
type={type}
id={name}
maxLength={textAreaMaxLength || FF_MAX_CHARS_DEFAULT}
ref={this.input}
{...inputProps}
/>
) : (
<TextareaWithSuggestions
type={type}
id={name}
maxLength={textAreaMaxLength || FF_MAX_CHARS_DEFAULT}
inputRef={this.input}
isLivestream={isLivestream}
{...inputProps}
/>
)}
<div className="form-field__textarea-info">
{!noEmojis && openEmoteMenu && (
<Button
type="alt"
className="button--comment-icons"
title="Emotes"
onClick={openEmoteMenu}
icon={ICONS.EMOJI}
iconSize={20}
/>
)}
{countInfo}
</div>
</fieldset-section>
);
}
};
return (
<>
{type && input()}
{helper && <div className="form-field__help">{helper}</div>}
</>
);
}
}
export default FormFieldAreaAdvanced;

View file

@ -1,7 +1,14 @@
// @flow // @flow
import 'easymde/dist/easymde.min.css'; import 'easymde/dist/easymde.min.css';
import { FF_MAX_CHARS_DEFAULT } from 'constants/form-field'; import { FF_MAX_CHARS_DEFAULT } from 'constants/form-field';
import { openEditorMenu, stopContextMenu } from 'util/context-menu';
import * as ICONS from 'constants/icons';
import Button from 'component/button';
import MarkdownPreview from 'component/common/markdown-preview';
import React from 'react'; import React from 'react';
import ReactDOMServer from 'react-dom/server';
import SimpleMDE from 'react-simplemde-editor';
import TextareaWithSuggestions from 'component/textareaWithSuggestions';
import type { ElementRef, Node } from 'react'; import type { ElementRef, Node } from 'react';
type Props = { type Props = {
@ -14,15 +21,19 @@ type Props = {
disabled?: boolean, disabled?: boolean,
error?: string | boolean, error?: string | boolean,
helper?: string | React$Node, helper?: string | React$Node,
hideSuggestions?: boolean,
inputButton?: React$Node, inputButton?: React$Node,
isLivestream?: boolean,
label?: string | Node, label?: string | Node,
labelOnLeft: boolean, labelOnLeft: boolean,
max?: number, max?: number,
min?: number, min?: number,
name: string, name: string,
noEmojis?: boolean,
placeholder?: string | number, placeholder?: string | number,
postfix?: string, postfix?: string,
prefix?: string, prefix?: string,
quickActionLabel?: string,
range?: number, range?: number,
readOnly?: boolean, readOnly?: boolean,
stretch?: boolean, stretch?: boolean,
@ -30,6 +41,8 @@ type Props = {
type?: string, type?: string,
value?: string | number, value?: string | number,
onChange?: (any) => any, onChange?: (any) => any,
openEmoteMenu?: () => void,
quickActionHandler?: (any) => any,
render?: () => React$Node, render?: () => React$Node,
}; };
@ -59,15 +72,21 @@ export class FormField extends React.PureComponent<Props> {
children, children,
error, error,
helper, helper,
hideSuggestions,
inputButton, inputButton,
isLivestream,
label, label,
labelOnLeft, labelOnLeft,
name, name,
noEmojis,
postfix, postfix,
prefix, prefix,
quickActionLabel,
stretch, stretch,
textAreaMaxLength, textAreaMaxLength,
type, type,
openEmoteMenu,
quickActionHandler,
render, render,
...inputProps ...inputProps
} = this.props; } = this.props;
@ -82,10 +101,18 @@ export class FormField extends React.PureComponent<Props> {
const countInfo = hasCharCount && textAreaMaxLength !== undefined && ( const countInfo = hasCharCount && textAreaMaxLength !== undefined && (
<span className="comment__char-count-mde">{`${charCount || '0'}/${textAreaMaxLength}`}</span> <span className="comment__char-count-mde">{`${charCount || '0'}/${textAreaMaxLength}`}</span>
); );
const Wrapper = blockWrap const Wrapper = blockWrap
? ({ children: innerChildren }) => <fieldset-section class="radio">{innerChildren}</fieldset-section> ? ({ children: innerChildren }) => <fieldset-section class="radio">{innerChildren}</fieldset-section>
: ({ children: innerChildren }) => <span className="radio">{innerChildren}</span>; : ({ children: innerChildren }) => <span className="radio">{innerChildren}</span>;
const quickAction =
quickActionLabel && quickActionHandler ? (
<div className="form-field__quick-action">
<Button button="link" onClick={quickActionHandler} label={quickActionLabel} />
</div>
) : null;
const inputSimple = (type: string) => ( const inputSimple = (type: string) => (
<> <>
<input id={name} type={type} {...inputProps} /> <input id={name} type={type} {...inputProps} />
@ -116,14 +143,102 @@ export class FormField extends React.PureComponent<Props> {
return inputSelect(''); return inputSelect('');
case 'select-tiny': case 'select-tiny':
return inputSelect('select--slim'); return inputSelect('select--slim');
case 'markdown':
const handleEvents = { contextmenu: openEditorMenu };
const getInstance = (editor) => {
// SimpleMDE max char check
editor.codemirror.on('beforeChange', (instance, changes) => {
if (textAreaMaxLength && changes.update) {
var str = changes.text.join('\n');
var delta = str.length - (instance.indexFromPos(changes.to) - instance.indexFromPos(changes.from));
if (delta <= 0) return;
delta = instance.getValue().length + delta - textAreaMaxLength;
if (delta > 0) {
str = str.substring(0, str.length - delta);
changes.update(changes.from, changes.to, str.split('\n'));
}
}
});
// "Create Link (Ctrl-K)": highlight URL instead of label:
editor.codemirror.on('changes', (instance, changes) => {
try {
// Grab the last change from the buffered list. I assume the
// buffered one ('changes', instead of 'change') is more efficient,
// and that "Create Link" will always end up last in the list.
const lastChange = changes[changes.length - 1];
if (lastChange.origin === '+input') {
// https://github.com/Ionaru/easy-markdown-editor/blob/8fa54c496f98621d5f45f57577ce630bee8c41ee/src/js/easymde.js#L765
const EASYMDE_URL_PLACEHOLDER = '(https://)';
// The URL placeholder is always placed last, so just look at the
// last text in the array to also cover the multi-line case:
const urlLineText = lastChange.text[lastChange.text.length - 1];
if (urlLineText.endsWith(EASYMDE_URL_PLACEHOLDER) && urlLineText !== '[]' + EASYMDE_URL_PLACEHOLDER) {
const from = lastChange.from;
const to = lastChange.to;
const isSelectionMultiline = lastChange.text.length > 1;
const baseIndex = isSelectionMultiline ? 0 : from.ch;
// Everything works fine for the [Ctrl-K] case, but for the
// [Button] case, this handler happens before the original
// code, thus our change got wiped out.
// Add a small delay to handle that case.
setTimeout(() => {
instance.setSelection(
{ line: to.line, ch: baseIndex + urlLineText.lastIndexOf('(') + 1 },
{ line: to.line, ch: baseIndex + urlLineText.lastIndexOf(')') }
);
}, 25);
}
}
} catch (e) {} // Do nothing (revert to original behavior)
});
};
return (
<div className="form-field--SimpleMDE" onContextMenu={stopContextMenu}>
<fieldset-section>
<div className="form-field__two-column">
<div>
<label htmlFor={name}>{label}</label>
</div>
{quickAction}
</div>
<SimpleMDE
{...inputProps}
id={name}
type="textarea"
events={handleEvents}
getMdeInstance={getInstance}
options={{
spellChecker: true,
hideIcons: ['heading', 'image', 'fullscreen', 'side-by-side'],
previewRender(plainText) {
const preview = <MarkdownPreview content={plainText} noDataStore />;
return ReactDOMServer.renderToString(preview);
},
}}
/>
{countInfo}
</fieldset-section>
</div>
);
case 'textarea': case 'textarea':
return ( return (
<fieldset-section> <fieldset-section>
{label && ( {(label || quickAction) && (
<div className="form-field__two-column"> <div className="form-field__two-column">
<label htmlFor={name}>{label}</label> <label htmlFor={name}>{label}</label>
{quickAction}
</div> </div>
)} )}
{hideSuggestions ? (
<textarea <textarea
type={type} type={type}
id={name} id={name}
@ -131,7 +246,30 @@ export class FormField extends React.PureComponent<Props> {
ref={this.input} ref={this.input}
{...inputProps} {...inputProps}
/> />
<div className="form-field__textarea-info">{countInfo}</div> ) : (
<TextareaWithSuggestions
type={type}
id={name}
maxLength={textAreaMaxLength || FF_MAX_CHARS_DEFAULT}
inputRef={this.input}
isLivestream={isLivestream}
{...inputProps}
/>
)}
<div className="form-field__textarea-info">
{!noEmojis && openEmoteMenu && (
<Button
type="alt"
className="button--comment-icons"
title="Emotes"
onClick={openEmoteMenu}
icon={ICONS.EMOJI}
iconSize={20}
/>
)}
{countInfo}
</div>
</fieldset-section> </fieldset-section>
); );
default: default:

View file

@ -1,5 +1,4 @@
export { Form } from './form-components/form'; export { Form } from './form-components/form';
export { FormField } from './form-components/form-field'; export { FormField } from './form-components/form-field';
export { FormFieldAreaAdvanced } from './form-components/form-field-area-advanced';
export { FormFieldPrice } from './form-components/form-field-price'; export { FormFieldPrice } from './form-components/form-field-price';
export { Submit } from './form-components/submit'; export { Submit } from './form-components/submit';

View file

@ -2054,15 +2054,4 @@ export const icons = {
<path d="M12.5,23.24v-1A10.74,10.74,0,0,1,23.24,11.52" /> <path d="M12.5,23.24v-1A10.74,10.74,0,0,1,23.24,11.52" />
</g> </g>
), ),
[ICONS.SIMPLE_EDITOR]: buildIcon(
<g>
<path d="M1 18V6c0-1 1-2 2-2h18c1 0 2 1 2 2v12c0 1-1 2-2 2H3c-1 0-2-1-2-2ZM5 7v4" />
</g>
),
[ICONS.ADVANCED_EDITOR]: buildIcon(
<g>
<path d="M1 20V4c0-1 1-2 2-2h18c1 0 2 1 2 2v16c0 1-1 2-2 2H3c-1 0-2-1-2-2ZM1 11h22" />
<path d="M5 8V6h2v2H5ZM11 8V6h2v2h-2ZM17 8V6h2v2h-2ZM5 14v4" />
</g>
),
}; };

View file

@ -11,10 +11,10 @@ import Icon from 'component/common/icon';
type Props = { type Props = {
modal: { id: string, modalProps: {} }, modal: { id: string, modalProps: {} },
filePath: ?string, filePath: string | WebFile,
clearPublish: () => void, clearPublish: () => void,
updatePublishForm: ({}) => void, updatePublishForm: ({}) => void,
openModal: (id: string, { files: Array<File> }) => void, openModal: (id: string, { files: Array<WebFile> }) => void,
// React router // React router
history: { history: {
entities: {}[], entities: {}[],
@ -37,7 +37,7 @@ function FileDrop(props: Props) {
const { drag, dropData } = useDragDrop(); const { drag, dropData } = useDragDrop();
const [files, setFiles] = React.useState([]); const [files, setFiles] = React.useState([]);
const [error, setError] = React.useState(false); const [error, setError] = React.useState(false);
const [target, setTarget] = React.useState<?File>(null); const [target, setTarget] = React.useState<?WebFile>(null);
const hideTimer = React.useRef(null); const hideTimer = React.useRef(null);
const targetTimer = React.useRef(null); const targetTimer = React.useRef(null);
const navigationTimer = React.useRef(null); const navigationTimer = React.useRef(null);
@ -65,26 +65,24 @@ function FileDrop(props: Props) {
} }
}, [history]); }, [history]);
// Handle file selection
const handleFileSelected = React.useCallback(
(selectedFile) => {
// Delay hide and navigation for a smooth transition // Delay hide and navigation for a smooth transition
const hideDropArea = React.useCallback(() => {
hideTimer.current = setTimeout(() => { hideTimer.current = setTimeout(() => {
setFiles([]); setFiles([]);
// Navigate to publish area // Navigate to publish area
navigationTimer.current = setTimeout(() => { navigationTimer.current = setTimeout(() => {
// Navigate first, THEN assign filePath, otherwise
// the file selected will get reset (that's how the
// publish file view works, when the user switches to
// publish a file, the pathFile value gets reset to undefined)
navigateToPublish(); navigateToPublish();
updatePublishForm({
filePath: selectedFile.path || selectedFile.name,
});
}, NAVIGATE_TIME_OUT); }, NAVIGATE_TIME_OUT);
}, HIDE_TIME_OUT); }, HIDE_TIME_OUT);
}, [navigateToPublish]);
// Handle file selection
const handleFileSelected = React.useCallback(
(selectedFile) => {
updatePublishForm({ filePath: selectedFile });
hideDropArea();
}, },
[setFiles, navigateToPublish, updatePublishForm] [updatePublishForm, hideDropArea]
); );
// Clear timers when unmounted // Clear timers when unmounted

View file

@ -12,7 +12,7 @@ import { PRIMARY_PLAYER_WRAPPER_CLASS } from 'page/file/view';
import Draggable from 'react-draggable'; import Draggable from 'react-draggable';
import { onFullscreenChange } from 'util/full-screen'; import { onFullscreenChange } from 'util/full-screen';
import { generateListSearchUrlParams, formatLbryUrlForWeb } from 'util/url'; import { generateListSearchUrlParams, formatLbryUrlForWeb } from 'util/url';
import { useIsMobile, useIsMediumScreen } from 'effects/use-screensize'; import { useIsMobile } from 'effects/use-screensize';
import debounce from 'util/debounce'; import debounce from 'util/debounce';
import { useHistory } from 'react-router'; import { useHistory } from 'react-router';
import { isURIEqual } from 'util/lbryURI'; import { isURIEqual } from 'util/lbryURI';
@ -21,6 +21,7 @@ import AutoplayCountdown from 'component/autoplayCountdown';
// scss/init/vars.scss // scss/init/vars.scss
// --header-height // --header-height
const HEADER_HEIGHT = 60; const HEADER_HEIGHT = 60;
const HEADER_HEIGHT_MOBILE = 60;
const IS_DESKTOP_MAC = typeof process === 'object' ? process.platform === 'darwin' : false; const IS_DESKTOP_MAC = typeof process === 'object' ? process.platform === 'darwin' : false;
const DEBOUNCE_WINDOW_RESIZE_HANDLER_MS = 100; const DEBOUNCE_WINDOW_RESIZE_HANDLER_MS = 100;
@ -132,7 +133,6 @@ export default function FileRenderFloating(props: Props) {
const playingUriSource = playingUri && playingUri.source; const playingUriSource = playingUri && playingUri.source;
const isComment = playingUriSource === 'comment'; const isComment = playingUriSource === 'comment';
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const isMediumScreen = useIsMediumScreen();
const mainFilePlaying = !isFloating && primaryUri && isURIEqual(uri, primaryUri); const mainFilePlaying = !isFloating && primaryUri && isURIEqual(uri, primaryUri);
const [fileViewerRect, setFileViewerRect] = useState(); const [fileViewerRect, setFileViewerRect] = useState();
@ -344,8 +344,7 @@ export default function FileRenderFloating(props: Props) {
'content__viewer--floating': isFloating, 'content__viewer--floating': isFloating,
'content__viewer--inline': !isFloating, 'content__viewer--inline': !isFloating,
'content__viewer--secondary': isComment, 'content__viewer--secondary': isComment,
'content__viewer--theater-mode': 'content__viewer--theater-mode': !isFloating && videoTheaterMode && playingUri?.uri === primaryUri,
!isFloating && videoTheaterMode && !isMediumScreen && playingUri?.uri === primaryUri,
'content__viewer--disable-click': wasDragging, 'content__viewer--disable-click': wasDragging,
})} })}
style={ style={
@ -357,7 +356,7 @@ export default function FileRenderFloating(props: Props) {
top: top:
fileViewerRect.windowOffset + fileViewerRect.windowOffset +
fileViewerRect.top - fileViewerRect.top -
(isMobile ? 0 : HEADER_HEIGHT) - (isMobile ? HEADER_HEIGHT_MOBILE : HEADER_HEIGHT) -
(IS_DESKTOP_MAC ? 24 : 0), (IS_DESKTOP_MAC ? 24 : 0),
} }
: {} : {}

View file

@ -9,7 +9,6 @@ import * as PAGES from 'constants/pages';
import * as RENDER_MODES from 'constants/file_render_modes'; import * as RENDER_MODES from 'constants/file_render_modes';
import * as KEYCODES from 'constants/keycodes'; import * as KEYCODES from 'constants/keycodes';
import Button from 'component/button'; import Button from 'component/button';
import { useIsMediumScreen } from 'effects/use-screensize';
import isUserTyping from 'util/detect-typing'; import isUserTyping from 'util/detect-typing';
import { getThumbnailCdnUrl } from 'util/thumbnail'; import { getThumbnailCdnUrl } from 'util/thumbnail';
import Nag from 'component/common/nag'; import Nag from 'component/common/nag';
@ -64,7 +63,6 @@ export default function FileRenderInitiator(props: Props) {
const fileStatus = fileInfo && fileInfo.status; const fileStatus = fileInfo && fileInfo.status;
const isPlayable = RENDER_MODES.FLOATING_MODES.includes(renderMode); const isPlayable = RENDER_MODES.FLOATING_MODES.includes(renderMode);
const isText = RENDER_MODES.TEXT_MODES.includes(renderMode); const isText = RENDER_MODES.TEXT_MODES.includes(renderMode);
const isMediumScreen = useIsMediumScreen();
const [thumbnail, setThumbnail] = React.useState(FileRenderPlaceholder); const [thumbnail, setThumbnail] = React.useState(FileRenderPlaceholder);
const containerRef = React.useRef<any>(); const containerRef = React.useRef<any>();
@ -153,7 +151,7 @@ export default function FileRenderInitiator(props: Props) {
style={thumbnail && !obscurePreview ? { backgroundImage: `url("${thumbnail}")` } : {}} style={thumbnail && !obscurePreview ? { backgroundImage: `url("${thumbnail}")` } : {}}
className={classnames('content__cover', { className={classnames('content__cover', {
'content__cover--disabled': disabled, 'content__cover--disabled': disabled,
'content__cover--theater-mode': videoTheaterMode && !isMediumScreen, 'content__cover--theater-mode': videoTheaterMode,
'content__cover--text': isText, 'content__cover--text': isText,
'card__media--nsfw': obscurePreview, 'card__media--nsfw': obscurePreview,
})} })}

View file

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

View file

@ -92,7 +92,7 @@ function Page(props: Props) {
<div <div
className={classnames('main-wrapper__inner', { className={classnames('main-wrapper__inner', {
'main-wrapper__inner--filepage': isOnFilePage, 'main-wrapper__inner--filepage': isOnFilePage,
'main-wrapper__inner--theater-mode': isOnFilePage && videoTheaterMode && !isMediumScreen, 'main-wrapper__inner--theater-mode': isOnFilePage && videoTheaterMode,
})} })}
> >
{!authPage && {!authPage &&
@ -124,7 +124,7 @@ function Page(props: Props) {
'main--file-page': filePage, 'main--file-page': filePage,
'main--settings-page': settingsPage, 'main--settings-page': settingsPage,
'main--markdown': isMarkdown, 'main--markdown': isMarkdown,
'main--theater-mode': isOnFilePage && videoTheaterMode && !isMediumScreen && !isMarkdown, 'main--theater-mode': isOnFilePage && videoTheaterMode && !isMarkdown,
})} })}
> >
{children} {children}

View file

@ -1,12 +1,12 @@
// @flow // @flow
import React, { useEffect } from 'react'; import React, { useEffect } from 'react';
import { FormFieldAreaAdvanced } from 'component/common/form'; import { FormField } from 'component/common/form';
type Props = { type Props = {
uri: ?string, uri: ?string,
label: ?string, label: ?string,
disabled: ?boolean, disabled: ?boolean,
filePath: File, filePath: string | WebFile,
fileText: ?string, fileText: ?string,
fileMimeType: ?string, fileMimeType: ?string,
streamingUrl: ?string, streamingUrl: ?string,
@ -99,7 +99,7 @@ function PostEditor(props: Props) {
]); ]);
return ( return (
<FormFieldAreaAdvanced <FormField
type={'markdown'} type={'markdown'}
name="content_post" name="content_post"
label={label} label={label}

View file

@ -1,7 +1,7 @@
// @flow // @flow
import { FF_MAX_CHARS_IN_DESCRIPTION } from 'constants/form-field'; import { FF_MAX_CHARS_IN_DESCRIPTION } from 'constants/form-field';
import React from 'react'; import React from 'react';
import { FormFieldAreaAdvanced } from 'component/common/form'; import { FormField } from 'component/common/form';
import usePersistedState from 'effects/use-persisted-state'; import usePersistedState from 'effects/use-persisted-state';
import Card from 'component/common/card'; import Card from 'component/common/card';
@ -27,7 +27,7 @@ function PublishDescription(props: Props) {
return ( return (
<Card <Card
actions={ actions={
<FormFieldAreaAdvanced <FormField
type={advancedEditor ? 'markdown' : 'textarea'} type={advancedEditor ? 'markdown' : 'textarea'}
name="content_description" name="content_description"
label={__('Description')} label={__('Description')}

View file

@ -2,7 +2,6 @@
import type { Node } from 'react'; import type { Node } from 'react';
import * as ICONS from 'constants/icons'; import * as ICONS from 'constants/icons';
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { ipcRenderer } from 'electron';
import { regexInvalidURI } from 'util/lbryURI'; import { regexInvalidURI } from 'util/lbryURI';
import PostEditor from 'component/postEditor'; import PostEditor from 'component/postEditor';
import FileSelector from 'component/common/file-selector'; import FileSelector from 'component/common/file-selector';
@ -14,13 +13,13 @@ import I18nMessage from 'component/i18nMessage';
import usePersistedState from 'effects/use-persisted-state'; import usePersistedState from 'effects/use-persisted-state';
import * as PUBLISH_MODES from 'constants/publish_types'; import * as PUBLISH_MODES from 'constants/publish_types';
import PublishName from 'component/publishName'; import PublishName from 'component/publishName';
import path from 'path';
type Props = { type Props = {
uri: ?string, uri: ?string,
mode: ?string, mode: ?string,
name: ?string, name: ?string,
title: ?string, title: ?string,
filePath: ?string, filePath: string | WebFile,
fileMimeType: ?string, fileMimeType: ?string,
isStillEditing: boolean, isStillEditing: boolean,
balance: number, balance: number,
@ -78,7 +77,7 @@ function PublishFile(props: Props) {
const sizeInMB = Number(size) / 1000000; const sizeInMB = Number(size) / 1000000;
const secondsToProcess = sizeInMB / PROCESSING_MB_PER_SECOND; const secondsToProcess = sizeInMB / PROCESSING_MB_PER_SECOND;
const ffmpegAvail = ffmpegStatus.available; const ffmpegAvail = ffmpegStatus.available;
const currentFile = filePath; const [currentFile, setCurrentFile] = useState(null);
const [currentFileType, setCurrentFileType] = useState(null); const [currentFileType, setCurrentFileType] = useState(null);
const [optimizeAvail, setOptimizeAvail] = useState(false); const [optimizeAvail, setOptimizeAvail] = useState(false);
const [userOptimize, setUserOptimize] = usePersistedState('publish-file-user-optimize', false); const [userOptimize, setUserOptimize] = usePersistedState('publish-file-user-optimize', false);
@ -92,35 +91,17 @@ function PublishFile(props: Props) {
} }
}, [currentFileType, mode, isStillEditing, updatePublishForm]); }, [currentFileType, mode, isStillEditing, updatePublishForm]);
// Since the filePath can be updated from outside this component
// (for instance, when the user drags & drops a file), we need
// to check for changes in the selected file using an effect.
useEffect(() => { useEffect(() => {
if (!filePath) { if (!filePath || filePath === '') {
return; setCurrentFile('');
updateFileInfo(0, 0, false);
} else if (typeof filePath !== 'string') {
// Update currentFile file
if (filePath.name !== currentFile && filePath.path !== currentFile) {
handleFileChange(filePath);
} }
async function readSelectedFileDetails() {
// Read the file to get the file's duration (if possible)
// and offer transcoding it.
const result = await ipcRenderer.invoke('get-file-details-from-path', filePath);
let file;
if (result.buffer) {
file = new File([result.buffer], result.name, {
type: result.mime,
});
} }
const fileData: FileData = { }, [filePath, currentFile, handleFileChange, updateFileInfo]);
path: result.path,
name: result.name,
mimeType: result.mime || 'application/octet-stream',
size: result.size,
duration: result.duration,
file: file,
};
processSelectedFile(fileData);
}
readSelectedFileDetails();
}, [filePath]);
useEffect(() => { useEffect(() => {
const isOptimizeAvail = currentFile && currentFile !== '' && isVid && ffmpegAvail; const isOptimizeAvail = currentFile && currentFile !== '' && isVid && ffmpegAvail;
@ -228,11 +209,11 @@ function PublishFile(props: Props) {
} }
} }
function processSelectedFile(fileData: FileData, clearName = true) { function handleFileChange(file: WebFile, clearName = true) {
window.URL = window.URL || window.webkitURL; window.URL = window.URL || window.webkitURL;
// select file, start to select a new one, then cancel // select file, start to select a new one, then cancel
if (!fileData || fileData.error) { if (!file) {
if (isStillEditing || !clearName) { if (isStillEditing || !clearName) {
updatePublishForm({ filePath: '' }); updatePublishForm({ filePath: '' });
} else { } else {
@ -241,12 +222,8 @@ function PublishFile(props: Props) {
return; return;
} }
// if video, extract duration so we can warn about bitrate if (typeof file !== 'string') // if video, extract duration so we can warn about bitrateif (typeof file !== 'string') {
const file = fileData.file; const contentType = file.type && file.type.split('/');
// Check to see if it's a video and if mp4
const contentType = fileData.mimeType && fileData.mimeType.split('/'); // get this from electron side
const duration = fileData.duration;
const size = fileData.size;
const isVideo = contentType && contentType[0] === 'video'; const isVideo = contentType && contentType[0] === 'video';
const isMp4 = contentType && contentType[1] === 'mp4'; const isMp4 = contentType && contentType[1] === 'mp4';
@ -254,25 +231,34 @@ function PublishFile(props: Props) {
if (contentType && contentType[0] === 'text') { if (contentType && contentType[0] === 'text') {
isTextPost = contentType[1] === 'plain' || contentType[1] === 'markdown'; isTextPost = contentType[1] === 'plain' || contentType[1] === 'markdown';
setCurrentFileType(contentType.join('/')); setCurrentFileType(contentType);
} else if (path.parse(fileData.path).ext) { } else if (file.name) {
// If user's machine is missing a valid content type registration // If user's machine is missign a valid content type registration
// for markdown content: text/markdown, file extension will be used instead // for markdown content: text/markdown, file extension will be used instead
const extension = path.parse(fileData.path).ext; const extension = file.name.split('.').pop();
isTextPost = MARKDOWN_FILE_EXTENSIONS.includes(extension); isTextPost = MARKDOWN_FILE_EXTENSIONS.includes(extension);
} }
if (isVideo) { if (isVideo) {
if (isMp4) { if (isMp4) {
updateFileInfo(duration || 0, size, isVideo); const video = document.createElement('video');
video.preload = 'metadata';
video.onloadedmetadata = () => {
updateFileInfo(video.duration, file.size, isVideo);
window.URL.revokeObjectURL(video.src);
};
video.onerror = () => {
updateFileInfo(0, file.size, isVideo);
};
video.src = window.URL.createObjectURL(file);
} else { } else {
updateFileInfo(duration || 0, size, isVideo); updateFileInfo(0, file.size, isVideo);
} }
} else { } else {
updateFileInfo(0, size, isVideo); updateFileInfo(0, file.size, isVideo);
} }
if (isTextPost && file) { if (isTextPost) {
// Create reader // Create reader
const reader = new FileReader(); const reader = new FileReader();
// Handler for file reader // Handler for file reader
@ -284,17 +270,21 @@ function PublishFile(props: Props) {
setPublishMode(PUBLISH_MODES.FILE); setPublishMode(PUBLISH_MODES.FILE);
} }
// Strip off extension and replace invalid characters const publishFormParams: { filePath: string | WebFile, name?: string, optimize?: boolean } = {
// if electron, we'll set filePath to the path string because SDK is handling publishing.
// File.path will be undefined from web due to browser security, so it will default to the File Object.
filePath: file.path || file,
};
// Strip off extention and replace invalid characters
let fileName = name || (file.name && file.name.substring(0, file.name.lastIndexOf('.'))) || '';
if (!isStillEditing) { if (!isStillEditing) {
const fileWithoutExtension = path.parse(fileData.path).name; publishFormParams.name = parseName(fileName);
updatePublishForm({ name: parseName(fileWithoutExtension) });
}
} }
function handleFileChange(fileWithPath: FileWithPath) { // File path is not supported on web for security reasons so we use the name instead.
if (fileWithPath) { setCurrentFile(file.path || file.name);
updatePublishForm({ filePath: fileWithPath.path }); updatePublishForm(publishFormParams);
}
} }
const showFileUpload = mode === PUBLISH_MODES.FILE; const showFileUpload = mode === PUBLISH_MODES.FILE;
@ -342,7 +332,6 @@ function PublishFile(props: Props) {
onFileChosen={handleFileChange} onFileChosen={handleFileChange}
// https://stackoverflow.com/questions/19107685/safari-input-type-file-accept-video-ignores-mp4-files // https://stackoverflow.com/questions/19107685/safari-input-type-file-accept-video-ignores-mp4-files
placeholder={__('Select file to upload')} placeholder={__('Select file to upload')}
readFile={false}
/> />
{getUploadMessage()} {getUploadMessage()}
</> </>

View file

@ -35,8 +35,8 @@ import tempy from 'tempy';
type Props = { type Props = {
disabled: boolean, disabled: boolean,
tags: Array<Tag>, tags: Array<Tag>,
publish: (source: ?File, ?boolean) => void, publish: (source?: string | File, ?boolean) => void,
filePath: ?File, filePath: string | File,
fileText: string, fileText: string,
bid: ?number, bid: ?number,
bidError: ?string, bidError: ?string,
@ -208,6 +208,7 @@ function PublishForm(props: Props) {
isNameValid(name) && isNameValid(name) &&
title && title &&
bid && bid &&
thumbnail &&
!bidError && !bidError &&
!emptyPostError && !emptyPostError &&
!(thumbnailError && !thumbnailUploaded) && !(thumbnailError && !thumbnailUploaded) &&
@ -372,6 +373,9 @@ function PublishForm(props: Props) {
if (!output || output === '') { if (!output || output === '') {
// Generate a temporary file: // Generate a temporary file:
output = tempy.file({ name: 'post.md' }); output = tempy.file({ name: 'post.md' });
} else if (typeof filePath === 'string') {
// Use current file
output = filePath;
} }
// Create a temporary file and save file changes // Create a temporary file and save file changes
if (output && output !== '') { if (output && output !== '') {
@ -443,7 +447,7 @@ function PublishForm(props: Props) {
// with other properties such as name, title, etc.) for security reasons. // with other properties such as name, title, etc.) for security reasons.
useEffect(() => { useEffect(() => {
if (mode === PUBLISH_MODES.FILE) { if (mode === PUBLISH_MODES.FILE) {
updatePublishForm({ filePath: undefined, fileDur: 0, fileSize: 0 }); updatePublishForm({ filePath: '', fileDur: 0, fileSize: 0 });
} }
}, [mode, updatePublishForm]); }, [mode, updatePublishForm]);

View file

@ -47,7 +47,11 @@ function PublishFormErrors(props: Props) {
{!bid && <div>{__('A deposit amount is required')}</div>} {!bid && <div>{__('A deposit amount is required')}</div>}
{bidError && <div>{__('Please check your deposit amount.')}</div>} {bidError && <div>{__('Please check your deposit amount.')}</div>}
{isUploadingThumbnail && <div>{__('Please wait for thumbnail to finish uploading')}</div>} {isUploadingThumbnail && <div>{__('Please wait for thumbnail to finish uploading')}</div>}
{thumbnailError && !thumbnailUploaded && <div>{__('Thumbnail is invalid.')}</div>} {!isUploadingThumbnail && !thumbnail ? (
<div>{__('A thumbnail is required. Please upload or provide an image URL above.')}</div>
) : (
thumbnailError && !thumbnailUploaded && <div>{__('Thumbnail is invalid.')}</div>
)}
{editingURI && !isStillEditing && !filePath && ( {editingURI && !isStillEditing && !filePath && (
<div>{__('Please reselect a file after changing the LBRY URL')}</div> <div>{__('Please reselect a file after changing the LBRY URL')}</div>
)} )}

View file

@ -89,6 +89,7 @@ export default function SearchChannelField(props: Props) {
return ( return (
<Button <Button
ref={addTagRef} ref={addTagRef}
requiresAuth
button="primary" button="primary"
label={labelFoundAction} label={labelFoundAction}
onClick={() => handleFoundChannelClick(claim)} onClick={() => handleFoundChannelClick(claim)}

View file

@ -27,14 +27,6 @@ type Props = {
// passed to the onUpdate function after the // passed to the onUpdate function after the
// upload service returns success. // upload service returns success.
buildImagePreview?: boolean, buildImagePreview?: boolean,
// File extension filtering. Files can be filtered
// but the "All Files" options always shows up. To
// avoid that, you can use the filters property.
// For example, to only accept images pass the
// following filter:
// { name: 'Images', extensions: ['jpg', 'png', 'gif'] },
filters?: Array<{ name: string, extension: string[] }>,
type?: string,
}; };
function filePreview(file) { function filePreview(file) {
@ -51,8 +43,7 @@ function filePreview(file) {
} }
function SelectAsset(props: Props) { function SelectAsset(props: Props) {
const { onUpdate, onDone, assetName, currentValue, recommended, title, inline, buildImagePreview, filters, type } = const { onUpdate, onDone, assetName, currentValue, recommended, title, inline, buildImagePreview } = props;
props;
const [pathSelected, setPathSelected] = React.useState(''); const [pathSelected, setPathSelected] = React.useState('');
const [fileSelected, setFileSelected] = React.useState<any>(null); const [fileSelected, setFileSelected] = React.useState<any>(null);
const [uploadStatus, setUploadStatus] = React.useState(SPEECH_READY); const [uploadStatus, setUploadStatus] = React.useState(SPEECH_READY);
@ -130,17 +121,17 @@ function SelectAsset(props: Props) {
/> />
) : ( ) : (
<FileSelector <FileSelector
filters={filters}
type={type}
autoFocus autoFocus
disabled={uploadStatus === SPEECH_UPLOADING} disabled={uploadStatus === SPEECH_UPLOADING}
label={fileSelectorLabel} label={fileSelectorLabel}
name="assetSelector" name="assetSelector"
currentPath={pathSelected} currentPath={pathSelected}
onFileChosen={(fileWithPath) => { onFileChosen={(file) => {
if (fileWithPath.file.name) { if (file.name) {
setFileSelected(fileWithPath.file); setFileSelected(file);
setPathSelected(fileWithPath.path); // what why? why not target=WEB this?
// file.path is undefined in web but available in electron
setPathSelected(file.name || file.path);
} }
}} }}
accept={accept} accept={accept}

View file

@ -106,7 +106,7 @@ function SelectThumbnail(props: Props) {
__('This will be visible in a few minutes after you submit this form.')} __('This will be visible in a few minutes after you submit this form.')}
<img <img
style={{ display: 'none' }} style={{ display: 'none' }}
src={thumbnail || ThumbnailMissingImage} src={thumbnail}
alt={__('Thumbnail Preview')} alt={__('Thumbnail Preview')}
onError={() => { onError={() => {
if (updateThumbnailParams) { if (updateThumbnailParams) {
@ -160,9 +160,9 @@ function SelectThumbnail(props: Props) {
label={__('Thumbnail')} label={__('Thumbnail')}
placeholder={__('Choose an enticing thumbnail')} placeholder={__('Choose an enticing thumbnail')}
accept={accept} accept={accept}
onFileChosen={(fileWithPath) => onFileChosen={(file) =>
openModal(MODALS.CONFIRM_THUMBNAIL_UPLOAD, { openModal(MODALS.CONFIRM_THUMBNAIL_UPLOAD, {
file: fileWithPath, file,
cb: (url) => updateThumbnailParams && updateThumbnailParams({ thumbnail_url: url }), cb: (url) => updateThumbnailParams && updateThumbnailParams({ thumbnail_url: url }),
}) })
} }

View file

@ -130,7 +130,7 @@ export default function SettingSystem(props: Props) {
<FileSelector <FileSelector
type="openDirectory" type="openDirectory"
currentPath={daemonSettings.download_dir} currentPath={daemonSettings.download_dir}
onFileChosen={(newDirectory: FileWithPath) => { onFileChosen={(newDirectory: WebFile) => {
setDaemonSetting('download_dir', newDirectory.path); setDaemonSetting('download_dir', newDirectory.path);
}} }}
/> />
@ -224,7 +224,7 @@ export default function SettingSystem(props: Props) {
type="openDirectory" type="openDirectory"
placeholder={__('A Folder containing FFmpeg')} placeholder={__('A Folder containing FFmpeg')}
currentPath={ffmpegPath || daemonSettings.ffmpeg_path} currentPath={ffmpegPath || daemonSettings.ffmpeg_path}
onFileChosen={(newDirectory: FileWithPath) => { onFileChosen={(newDirectory: WebFile) => {
// $FlowFixMe // $FlowFixMe
setDaemonSetting('ffmpeg_path', newDirectory.path); setDaemonSetting('ffmpeg_path', newDirectory.path);
findFFmpeg(); findFFmpeg();

View file

@ -1,6 +1,6 @@
// @flow // @flow
import * as ICONS from 'constants/icons'; import * as ICONS from 'constants/icons';
import React, { useCallback, useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import CreditAmount from 'component/common/credit-amount'; import CreditAmount from 'component/common/credit-amount';
import Button from 'component/button'; import Button from 'component/button';
import { Form, FormField } from 'component/common/form'; import { Form, FormField } from 'component/common/form';
@ -24,100 +24,67 @@ type Props = {
const SupportsLiquidate = (props: Props) => { const SupportsLiquidate = (props: Props) => {
const { claim, abandonSupportForClaim, handleClose, abandonClaimError } = props; const { claim, abandonSupportForClaim, handleClose, abandonClaimError } = props;
const [previewBalance, setPreviewBalance] = useState(undefined); const [previewBalance, setPreviewBalance] = useState(undefined);
const [defaultValueAssigned, setDefaultValueAssigned] = useState(false); const [amount, setAmount] = useState(-1);
const [unlockTextAmount, setUnlockTextAmount] = useState('');
const [error, setError] = useState(false); const [error, setError] = useState(false);
const initialMessage = __('How much would you like to unlock?'); const initialMessage = __('How much would you like to unlock?');
const [message, setMessage] = useState(initialMessage); const [message, setMessage] = useState(initialMessage);
const amount = Number(unlockTextAmount) || 0;
const defaultValue = previewBalance ? previewBalance * 0.25 : 0;
const keep = const keep =
amount >= 0 amount >= 0
? Boolean(previewBalance) && Number.parseFloat(String(Number(previewBalance) - amount)).toFixed(8) ? Boolean(previewBalance) && Number.parseFloat(String(Number(previewBalance) - Number(amount))).toFixed(8)
: Boolean(previewBalance) && Number.parseFloat(String(defaultValue * 3)).toFixed(8); : Boolean(previewBalance) && Number.parseFloat(String((Number(previewBalance) / 4) * 3)).toFixed(8); // default unlock 25%
const claimId = claim && claim.claim_id; const claimId = claim && claim.claim_id;
const type = claim.value_type; const type = claim.value_type;
useEffect(() => { useEffect(() => {
if (claimId && abandonSupportForClaim) { if (claimId && abandonSupportForClaim) {
abandonSupportForClaim(claimId, type, false, true).then((r) => { abandonSupportForClaim(claimId, type, false, true).then(r => {
setPreviewBalance(r.total_input); setPreviewBalance(r.total_input);
}); });
} }
}, [abandonSupportForClaim, claimId, type, setPreviewBalance]); }, [abandonSupportForClaim, claimId, type, setPreviewBalance]);
function handleSubmit() { function handleSubmit() {
abandonSupportForClaim(claimId, type, keep, false).then((r) => { abandonSupportForClaim(claimId, type, keep, false).then(r => {
if (r) { if (r) {
handleClose(); handleClose();
} }
}); });
} }
const handleRangeChange = useCallback( function handleChange(a) {
(newValue) => { if (a === undefined || isNaN(Number(a))) {
setUnlockTextAmount(String(newValue));
},
[setUnlockTextAmount]
);
const handleChangeUnlockText = useCallback(
(newValue) => {
// Get rid of all characters except digits, commas and periods.
const onlyValidAmount = newValue.replace(/[^0-9.,]+/, '');
setUnlockTextAmount(onlyValidAmount);
},
[setUnlockTextAmount]
);
const handleUnlockTextFocus = useCallback(() => {
// Get rid of empty zero when user starts typing (small ux improvement)
if (Number(unlockTextAmount) === 0) {
setUnlockTextAmount('');
}
}, [unlockTextAmount, setUnlockTextAmount]);
const handleUnlockTextBlur = useCallback(() => {
if (!unlockTextAmount || isNaN(Number(unlockTextAmount))) {
setUnlockTextAmount(previewBalance ? String(defaultValue) : '0');
}
}, [unlockTextAmount, setUnlockTextAmount, previewBalance, defaultValue]);
useEffect(() => {
if (defaultValueAssigned || !previewBalance || unlockTextAmount) {
return;
}
setUnlockTextAmount(String(defaultValue));
setDefaultValueAssigned(true);
}, [defaultValueAssigned, previewBalance, unlockTextAmount, setUnlockTextAmount, setDefaultValueAssigned]);
// Update message & error based on unlock amount.
useEffect(() => {
const unlockAmount = Number(unlockTextAmount);
const previewBalanceNumber = Number(previewBalance);
if (unlockTextAmount && isNaN(unlockAmount)) {
setMessage(__('Amount must be a number')); setMessage(__('Amount must be a number'));
setError(true); setError(true);
} else if (unlockAmount > previewBalanceNumber) { setAmount(0);
} else if (a === '') {
setAmount(0);
setError(true);
setMessage(__('Amount cannot be blank'));
} else if (Number(a) > Number(previewBalance)) {
setMessage(__('Amount cannot be more than available')); setMessage(__('Amount cannot be more than available'));
setError(true); setError(false);
} else if (Math.abs(unlockAmount - previewBalanceNumber) <= Number.EPSILON) { } else if (Number(a) === Number(previewBalance)) {
setMessage(__(`She's about to close up the library!`)); setMessage(__(`She's about to close up the library!`));
setAmount(a);
setError(false); setError(false);
} else if (unlockAmount > previewBalanceNumber / 2) { } else if (Number(a) > Number(previewBalance) / 2) {
setMessage(__('Your content will do better with more staked on it')); setMessage(__('Your content will do better with more staked on it'));
setAmount(a);
setError(false); setError(false);
} else if (unlockAmount === 0) { } else if (Number(a) === 0) {
setMessage(__('Amount cannot be zero')); setMessage(__('Amount cannot be zero'));
setAmount(0);
setError(true); setError(true);
} else if (unlockAmount <= previewBalanceNumber / 2) { } else if (Number(a) <= Number(previewBalance) / 2) {
setMessage(__('A prudent choice')); setMessage(__('A prudent choice'));
setAmount(Number(a));
setError(false); setError(false);
} else { } else {
setMessage(initialMessage); setMessage(initialMessage);
setAmount(a);
setError(false); setError(false);
} }
}, [unlockTextAmount, previewBalance, setMessage, setError]); }
return ( return (
<Card <Card
@ -173,8 +140,8 @@ const SupportsLiquidate = (props: Props) => {
min={0} min={0}
step={0.01} step={0.01}
max={previewBalance} max={previewBalance}
value={amount} value={Number(amount) >= 0 ? amount : previewBalance / 4} // by default, set it to 25% of available
onChange={(e) => handleRangeChange(e.target.value)} onChange={e => handleChange(e.target.value)}
/> />
<label className="range__label"> <label className="range__label">
<span>0</span> <span>0</span>
@ -183,11 +150,9 @@ const SupportsLiquidate = (props: Props) => {
</label> </label>
<FormField <FormField
type="text" type="text"
value={unlockTextAmount} value={amount >= 0 ? amount || '' : previewBalance && previewBalance / 4}
helper={message} helper={message}
onFocus={handleUnlockTextFocus} onChange={e => handleChange(e.target.value)}
onChange={(e) => handleChangeUnlockText(e.target.value)}
onBlur={handleUnlockTextBlur}
/> />
</Form> </Form>
)} )}

View file

@ -41,7 +41,7 @@ function SyncToggle(props: Props) {
{!verifiedEmail && ( {!verifiedEmail && (
<div> <div>
<p className="help">{__('An email address is required to sync your account.')}</p> <p className="help">{__('An email address is required to sync your account.')}</p>
<Button button="primary" label={__('Add Email')} /> <Button requiresAuth button="primary" label={__('Add Email')} />
</div> </div>
)} )}
</SettingsRow> </SettingsRow>

View file

@ -103,8 +103,6 @@ function TxoList(props: Props) {
params[TXO.TX_TYPE] = currentUrlParams.type; params[TXO.TX_TYPE] = currentUrlParams.type;
} else if (currentUrlParams.type === TXO.PUBLISH) { } else if (currentUrlParams.type === TXO.PUBLISH) {
params[TXO.TX_TYPE] = TXO.STREAM; params[TXO.TX_TYPE] = TXO.STREAM;
} else if (currentUrlParams.type === TXO.COLLECTION) {
params[TXO.TX_TYPE] = currentUrlParams.type;
} }
} }
if (currentUrlParams.active) { if (currentUrlParams.active) {

View file

@ -0,0 +1,27 @@
import { connect } from 'react-redux';
import { withRouter } from 'react-router';
import WalletSwap from './view';
import { doOpenModal } from 'redux/actions/app';
import { doAddCoinSwap, doQueryCoinSwapStatus } from 'redux/actions/coinSwap';
import { doToast } from 'redux/actions/notifications';
import { selectCoinSwaps } from 'redux/selectors/coinSwap';
import { selectUserVerifiedEmail } from 'redux/selectors/user';
import { doGetNewAddress, doCheckAddressIsMine } from 'redux/actions/wallet';
import { selectReceiveAddress } from 'redux/selectors/wallet';
const select = (state, props) => ({
receiveAddress: selectReceiveAddress(state),
coinSwaps: selectCoinSwaps(state),
isAuthenticated: selectUserVerifiedEmail(state),
});
const perform = (dispatch) => ({
openModal: (modal, props) => dispatch(doOpenModal(modal, props)),
doToast: (options) => dispatch(doToast(options)),
addCoinSwap: (coinSwap) => dispatch(doAddCoinSwap(coinSwap)),
getNewAddress: () => dispatch(doGetNewAddress()),
checkAddressIsMine: (address) => dispatch(doCheckAddressIsMine(address)),
queryCoinSwapStatus: (sendAddress) => dispatch(doQueryCoinSwapStatus(sendAddress)),
});
export default withRouter(connect(select, perform)(WalletSwap));

View file

@ -0,0 +1,707 @@
// @flow
import React from 'react';
import Button from 'component/button';
import { FormField, Form } from 'component/common/form';
import { Lbryio } from 'lbryinc';
import Card from 'component/common/card';
import LbcSymbol from 'component/common/lbc-symbol';
import Spinner from 'component/spinner';
import Nag from 'component/common/nag';
import CopyableText from 'component/copyableText';
import Icon from 'component/common/icon';
import QRCode from 'component/common/qr-code';
import usePersistedState from 'effects/use-persisted-state';
import * as ICONS from 'constants/icons';
import * as MODALS from 'constants/modal_types';
import * as PAGES from 'constants/pages';
import { clipboard } from 'electron';
import I18nMessage from 'component/i18nMessage';
import { Redirect, useHistory } from 'react-router';
const ENABLE_ALTERNATIVE_COINS = true;
const BTC_SATOSHIS = 100000000;
const LBC_MAX = 21000000;
const LBC_MIN = 1;
const IS_DEV = process.env.NODE_ENV !== 'production';
const DEBOUNCE_BTC_CHANGE_MS = 400;
const INTERNAL_APIS_DOWN = 'internal_apis_down';
const BTC_API_STATUS_PENDING = 'NEW'; // Started swap, waiting for coin.
const BTC_API_STATUS_CONFIRMING = 'PENDING'; // Coin receiving, waiting confirmation.
const BTC_API_STATUS_PROCESSING = 'COMPLETED'; // Coin confirmed. Sending LBC.
const BTC_API_STATUS_UNRESOLVED = 'UNRESOLVED'; // Underpaid, overpaid, etc.
const BTC_API_STATUS_EXPIRED = 'EXPIRED'; // Charge expired (60 minutes).
const BTC_API_STATUS_ERROR = 'Error';
const ACTION_MAIN = 'action_main';
const ACTION_STATUS_PENDING = 'action_pending';
const ACTION_STATUS_CONFIRMING = 'action_confirming';
const ACTION_STATUS_PROCESSING = 'action_processing';
const ACTION_STATUS_SUCCESS = 'action_success';
const ACTION_PAST_SWAPS = 'action_past_swaps';
const NAG_API_STATUS_PENDING = 'Waiting to receive your crypto.';
const NAG_API_STATUS_CONFIRMING = 'Confirming transaction.';
const NAG_API_STATUS_PROCESSING = 'Crypto received. Sending your Credits.';
const NAG_API_STATUS_SUCCESS = 'Credits sent. You should see it in your wallet.';
const NAG_API_STATUS_ERROR = 'An error occurred on the previous swap.';
const NAG_SWAP_CALL_FAILED = 'Failed to initiate swap.';
// const NAG_STATUS_CALL_FAILED = 'Failed to query swap status.';
const NAG_SERVER_DOWN = 'The system is currently down. Come back later.';
const NAG_RATE_CALL_FAILED = 'Unable to obtain exchange rate. Try again later.';
const NAG_EXPIRED = 'Swap expired.';
type Props = {
receiveAddress: string,
coinSwaps: Array<CoinSwapInfo>,
isAuthenticated: boolean,
doToast: ({ message: string }) => void,
addCoinSwap: (CoinSwapInfo) => void,
getNewAddress: () => void,
checkAddressIsMine: (string) => void,
openModal: (string, {}) => void,
queryCoinSwapStatus: (string) => void,
};
function WalletSwap(props: Props) {
const {
receiveAddress,
doToast,
coinSwaps,
isAuthenticated,
addCoinSwap,
getNewAddress,
checkAddressIsMine,
openModal,
queryCoinSwapStatus,
} = props;
const [btc, setBtc] = React.useState(0);
const [lbcError, setLbcError] = React.useState();
const [lbc, setLbc] = usePersistedState('swap-desired-lbc', LBC_MIN);
const [action, setAction] = React.useState(ACTION_MAIN);
const [nag, setNag] = React.useState(null);
const [showQr, setShowQr] = React.useState(false);
const [isFetchingRate, setIsFetchingRate] = React.useState(false);
const [isSwapping, setIsSwapping] = React.useState(false);
const [isRefreshingStatus, setIsRefreshingStatus] = React.useState(false);
const { location } = useHistory();
const [swap, setSwap] = React.useState({});
const [coin, setCoin] = React.useState('bitcoin');
const [lastStatusQuery, setLastStatusQuery] = React.useState();
const { goBack } = useHistory();
function formatCoinAmountString(amount) {
return amount === 0 ? '---' : amount.toLocaleString(undefined, { minimumFractionDigits: 8 });
}
function returnToMainAction() {
setIsSwapping(false);
setAction(ACTION_MAIN);
setSwap(null);
}
function removeCoinSwap(chargeCode) {
openModal(MODALS.CONFIRM_REMOVE_BTC_SWAP_ADDRESS, {
chargeCode: chargeCode,
});
}
// Ensure 'receiveAddress' is populated
React.useEffect(() => {
if (!receiveAddress) {
getNewAddress();
} else {
checkAddressIsMine(receiveAddress);
}
}, [receiveAddress, getNewAddress, checkAddressIsMine]);
// Get 'btc/rate' and calculate required BTC.
React.useEffect(() => {
if (isNaN(lbc) || lbc === 0) {
setBtc(0);
return;
}
setIsFetchingRate(true);
const timer = setTimeout(() => {
Lbryio.call('btc', 'rate', { satoshi: BTC_SATOSHIS })
.then((rate) => {
setIsFetchingRate(false);
setBtc((lbc * Math.round(BTC_SATOSHIS * rate)) / BTC_SATOSHIS);
})
.catch(() => {
setIsFetchingRate(false);
setBtc(0);
setNag({ msg: NAG_RATE_CALL_FAILED, type: 'error' });
});
}, DEBOUNCE_BTC_CHANGE_MS);
return () => clearTimeout(timer);
}, [lbc]);
// Resolve 'swap' with the latest info from 'coinSwaps'
React.useEffect(() => {
const swapInfo = swap && coinSwaps.find((x) => x.chargeCode === swap.chargeCode);
if (!swapInfo) {
return;
}
const jsonSwap = JSON.stringify(swap);
const jsonSwapInfo = JSON.stringify(swapInfo);
if (jsonSwap !== jsonSwapInfo) {
setSwap({ ...swapInfo });
}
if (!swapInfo.status) {
return;
}
switch (swapInfo.status.status) {
case BTC_API_STATUS_PENDING:
setAction(ACTION_STATUS_PENDING);
setNag({ msg: NAG_API_STATUS_PENDING, type: 'helpful' });
break;
case BTC_API_STATUS_CONFIRMING:
setAction(ACTION_STATUS_CONFIRMING);
setNag({ msg: NAG_API_STATUS_CONFIRMING, type: 'helpful' });
break;
case BTC_API_STATUS_PROCESSING:
if (swapInfo.status.lbcTxid) {
setAction(ACTION_STATUS_SUCCESS);
setNag({ msg: NAG_API_STATUS_SUCCESS, type: 'helpful' });
setIsSwapping(false);
} else {
setAction(ACTION_STATUS_PROCESSING);
setNag({ msg: NAG_API_STATUS_PROCESSING, type: 'helpful' });
}
break;
case BTC_API_STATUS_ERROR:
setNag({ msg: NAG_API_STATUS_ERROR, type: 'error' });
break;
case INTERNAL_APIS_DOWN:
setNag({ msg: NAG_SERVER_DOWN, type: 'error' });
break;
case BTC_API_STATUS_EXPIRED:
setNag({ msg: NAG_EXPIRED, type: 'error' });
if (action === ACTION_PAST_SWAPS) {
setAction(ACTION_STATUS_PENDING);
}
break;
case BTC_API_STATUS_UNRESOLVED:
setNag({
msg: __(
'Received amount did not match order code %chargeCode%. Contact hello@lbry.com to resolve the payment.',
{ chargeCode: swapInfo.chargeCode }
),
type: 'error',
});
if (action === ACTION_PAST_SWAPS) {
setAction(ACTION_STATUS_PENDING);
}
break;
default:
setNag({ msg: swapInfo.status.status, type: 'error' });
break;
}
}, [swap, coinSwaps]);
// Validate entered LBC
React.useEffect(() => {
let msg;
if (lbc < LBC_MIN) {
msg = __('The amount needs to be higher');
} else if (lbc > LBC_MAX) {
msg = __('The amount is too high');
}
setLbcError(msg);
}, [lbc]);
// 'Refresh' button feedback
React.useEffect(() => {
let timer;
if (isRefreshingStatus) {
timer = setTimeout(() => {
setIsRefreshingStatus(false);
}, 1000);
}
return () => clearTimeout(timer);
}, [isRefreshingStatus]);
function getCoinAddress(coin) {
if (swap && swap.sendAddresses) {
return swap.sendAddresses[coin];
}
return '';
}
function getCoinSendAmountStr(coin) {
if (swap && swap.sendAmounts && swap.sendAmounts[coin]) {
return `${swap.sendAmounts[coin].amount} ${swap.sendAmounts[coin].currency}`;
}
return '';
}
function currencyToCoin(currency) {
const MAP = {
DAI: 'dai',
USDC: 'usdc',
BTC: 'bitcoin',
ETH: 'ethereum',
LTC: 'litecoin',
BCH: 'bitcoincash',
};
return MAP[currency] || 'bitcoin';
}
function getSentAmountStr(swapInfo) {
if (swapInfo && swapInfo.status) {
const currency = swapInfo.status.receiptCurrency;
const coin = currencyToCoin(currency);
return getCoinSendAmountStr(coin);
}
return '';
}
function getCoinLabel(coin) {
const COIN_LABEL = {
dai: 'Dai',
usdc: 'USD Coin',
bitcoin: 'Bitcoin',
ethereum: 'Ethereum',
litecoin: 'Litecoin',
bitcoincash: 'Bitcoin Cash',
};
return COIN_LABEL[coin] || coin;
}
function getLbcAmountStrForSwap(swap) {
if (swap && swap.lbcAmount) {
return formatCoinAmountString(swap.lbcAmount);
}
return '---';
}
function handleStartSwap() {
setIsSwapping(true);
setSwap(null);
setNag(null);
Lbryio.call('btc', 'swap', {
lbc_satoshi_requested: parseInt(lbc * BTC_SATOSHIS + 0.5),
btc_satoshi_provided: parseInt(btc * BTC_SATOSHIS + 0.5),
pay_to_wallet_address: receiveAddress,
})
.then((response) => {
const btcAmount = response.Charge.data.pricing['bitcoin'].amount;
const rate = response.Exchange.rate;
const timeline = response.Charge.data.timeline;
const lastTimeline = timeline[timeline.length - 1];
const newSwap = {
chargeCode: response.Exchange.charge_code,
coins: Object.keys(response.Charge.data.addresses),
sendAddresses: response.Charge.data.addresses,
sendAmounts: response.Charge.data.pricing,
lbcAmount: (btcAmount * BTC_SATOSHIS) / rate,
status: {
status: lastTimeline.status,
receiptCurrency: lastTimeline.payment.value.currency,
receiptTxid: lastTimeline.payment.transaction_id,
lbcTxid: response.Exchange.lbc_txid || '',
},
};
setSwap({ ...newSwap });
addCoinSwap({ ...newSwap });
})
.catch((err) => {
const translateError = (err) => {
// TODO: https://github.com/lbryio/lbry.go/issues/87
// Translate error codes instead of strings when it is available.
if (err === 'users are currently limited to 4 transactions per month') {
return __('Users are currently limited to 4 completed swaps per month or 5 pending swaps.');
}
return err;
};
setNag({ msg: err === INTERNAL_APIS_DOWN ? NAG_SWAP_CALL_FAILED : translateError(err.message), type: 'error' });
returnToMainAction();
});
}
function handleViewPastSwaps() {
setAction(ACTION_PAST_SWAPS);
setNag(null);
setIsRefreshingStatus(true);
const now = Date.now();
if (!lastStatusQuery || now - lastStatusQuery > 30000) {
// There is a '200/minute' limit in the commerce API. If the history is
// long, or if the user goes trigger-happy, the limit could be reached
// easily. Statuses don't change often, so just limit it to every 30s.
setLastStatusQuery(now);
coinSwaps.forEach((x) => {
queryCoinSwapStatus(x.chargeCode);
});
}
}
function getShortStatusStr(coinSwap: CoinSwapInfo) {
const swapInfo = coinSwaps.find((x) => x.chargeCode === coinSwap.chargeCode);
if (!swapInfo || !swapInfo.status) {
return '---';
}
let msg;
switch (swapInfo.status.status) {
case BTC_API_STATUS_PENDING:
msg = __('Waiting');
break;
case BTC_API_STATUS_CONFIRMING:
msg = __('Confirming');
break;
case BTC_API_STATUS_PROCESSING:
if (swapInfo.status.lbcTxid) {
msg = __('Credits sent');
} else {
msg = __('Sending Credits');
}
break;
case BTC_API_STATUS_ERROR:
msg = __('Failed');
break;
case BTC_API_STATUS_EXPIRED:
msg = __('Expired');
break;
case BTC_API_STATUS_UNRESOLVED:
msg = __('Unresolved');
break;
default:
msg = swapInfo.status.status;
// if (IS_DEV) throw new Error('Unhandled "status": ' + status.Status);
break;
}
return msg;
}
function getViewTransactionElement(swap, isSend) {
if (!swap || !swap.status) {
return '';
}
const explorerUrl = (coin, txid) => {
// It's unclear whether we can link to sites like blockchain.com.
// Don't do it for now.
return '';
};
if (isSend) {
const sendTxId = swap.status.receiptTxid;
const url = explorerUrl(swap.status.receiptCurrency, sendTxId);
return sendTxId ? (
<>
{url && <Button button="link" href={url} label={__('View transaction')} />}
{!url && (
<Button
button="link"
label={__('Copy transaction ID')}
title={sendTxId}
onClick={() => {
clipboard.writeText(sendTxId);
doToast({
message: __('Transaction ID copied.'),
});
}}
/>
)}
</>
) : null;
} else {
const lbcTxId = swap.status.lbcTxid;
return lbcTxId ? (
<Button button="link" href={`https://explorer.lbry.com/tx/${lbcTxId}`} label={__('View transaction')} />
) : null;
}
}
function getCloseButton() {
return (
<>
<Button autoFocus button="primary" label={__('Close')} onClick={() => goBack()} />
<Icon
className="icon--help"
icon={ICONS.HELP}
tooltip
size={16}
customTooltipText={__(
'This page can be closed while the transactions are in progress.\nYou can view the status later from:\n • Wallet » Swap » View Past Swaps'
)}
/>
</>
);
}
function getGap() {
return <div className="confirm__value" />; // better way?
}
function getActionElement() {
switch (action) {
case ACTION_MAIN:
return actionMain;
case ACTION_STATUS_PENDING:
return actionPending;
case ACTION_STATUS_CONFIRMING:
return actionConfirmingSend;
case ACTION_STATUS_PROCESSING: // fall-through
case ACTION_STATUS_SUCCESS:
return actionProcessingAndSuccess;
case ACTION_PAST_SWAPS:
return actionPastSwaps;
default:
if (IS_DEV) throw new Error('Unhandled action: ' + action);
return actionMain;
}
}
const actionMain = (
<>
<div className="section section--padded card--inline confirm__wrapper">
<div className="section">
<FormField
autoFocus
label={
<I18nMessage
tokens={{
lbc: <LbcSymbol size={22} />,
}}
>
Enter desired %lbc%
</I18nMessage>
}
type="number"
name="lbc"
className="form-field--price-amount--auto"
affixClass="form-field--fix-no-height"
max={LBC_MAX}
min={LBC_MIN}
step={1 / BTC_SATOSHIS}
placeholder="12.34"
value={lbc}
error={lbcError}
disabled={isSwapping}
onChange={(event) => setLbc(parseFloat(event.target.value))}
/>
{getGap()}
<div className="confirm__label">{__('Estimated BTC price')}</div>
<div className="confirm__value">
{formatCoinAmountString(btc)} {btc === 0 ? '' : 'BTC'}
{isFetchingRate && <Spinner type="small" />}
</div>
</div>
</div>
<div className="section__actions">
<Button
autoFocus
onClick={handleStartSwap}
button="primary"
disabled={isSwapping || isNaN(btc) || btc === 0 || lbc === 0 || lbcError}
label={isSwapping ? __('Processing...') : __('Start Swap')}
/>
{!isSwapping && coinSwaps.length !== 0 && (
<Button button="link" label={__('View Past Swaps')} onClick={handleViewPastSwaps} />
)}
{isSwapping && <Spinner type="small" />}
</div>
</>
);
const actionPending = (
<>
<div className="section section--padded card--inline confirm__wrapper">
<div className="section">
{swap && swap.coins && ENABLE_ALTERNATIVE_COINS && (
<>
<FormField
type="select"
name="select_coin"
value={coin}
label={__('Alternative coins')}
onChange={(e) => setCoin(e.target.value)}
>
{swap.coins.map((x) => (
<option key={x} value={x}>
{getCoinLabel(x)}
</option>
))}
</FormField>
{getGap()}
</>
)}
<div className="confirm__label">{__('Send')}</div>
<CopyableText
primaryButton
copyable={getCoinSendAmountStr(coin)}
snackMessage={__('Amount copied.')}
onCopy={(inputElem) => {
const inputStr = inputElem.value;
const selectEndIndex = inputStr.lastIndexOf(' ');
if (selectEndIndex > -1 && inputStr.substring(0, selectEndIndex).match(/[\d.]/)) {
inputElem.setSelectionRange(0, selectEndIndex, 'forward');
}
}}
/>
<div className="help">{__('Use the copy button to ensure the EXACT amount is sent!')}</div>
{getGap()}
<div className="confirm__label">{__('To')}</div>
<CopyableText primaryButton copyable={getCoinAddress(coin)} snackMessage={__('Address copied.')} />
<div className="confirm__value--subitem">
<Button
button="link"
label={showQr ? __('Hide QR code') : __('Show QR code')}
onClick={() => setShowQr(!showQr)}
/>
{showQr && getCoinAddress(coin) && <QRCode value={getCoinAddress(coin)} />}
</div>
{getGap()}
<div className="confirm__label">{__('Receive')}</div>
<div className="confirm__value">{<LbcSymbol postfix={getLbcAmountStrForSwap(swap)} size={22} />}</div>
</div>
</div>
<div className="section__actions">{getCloseButton()}</div>
</>
);
const actionConfirmingSend = (
<>
<div className="section section--padded card--inline confirm__wrapper">
<div className="section">
<div className="confirm__label">{__('Confirming')}</div>
<div className="confirm__value confirm__value--no-gap">{getSentAmountStr(swap)}</div>
<div className="confirm__value--subitem">{getViewTransactionElement(swap, true)}</div>
</div>
</div>
<div className="section__actions">{getCloseButton()}</div>
</>
);
const actionProcessingAndSuccess = (
<>
<div className="section section--padded card--inline confirm__wrapper">
<div className="section">
<div className="confirm__label">{__('Sent')}</div>
<div className="confirm__value confirm__value--no-gap">{getSentAmountStr(swap)}</div>
<div className="confirm__value--subitem">{getViewTransactionElement(swap, true)}</div>
{getGap()}
<div className="confirm__label">{action === ACTION_STATUS_SUCCESS ? __('Received') : __('Receiving')}</div>
<div className="confirm__value confirm__value--no-gap">
{<LbcSymbol postfix={getLbcAmountStrForSwap(swap)} size={22} />}
</div>
{action === ACTION_STATUS_SUCCESS && (
<div className="confirm__value--subitem">{getViewTransactionElement(swap, false)}</div>
)}
</div>
</div>
<div className="section__actions">{getCloseButton()}</div>
</>
);
const actionPastSwaps = (
<>
<div className="section section--padded card--inline confirm__wrapper">
<div className="section">
<div className="table__wrapper">
<table className="table table--btc-swap">
<thead>
<tr>
<th>{__('Code')}</th>
<th>{__('Status')}</th>
<th />
</tr>
</thead>
<tbody>
{coinSwaps.length === 0 && (
<tr>
<td>{'---'}</td>
</tr>
)}
{coinSwaps.length !== 0 &&
coinSwaps.map((x) => {
return (
<tr key={x.chargeCode}>
<td>
<Button
button="link"
className="button--hash-id"
title={x.chargeCode}
label={x.chargeCode}
onClick={() => {
setSwap({ ...x });
}}
/>
</td>
<td>{isRefreshingStatus ? '...' : getShortStatusStr(x)}</td>
<td>
<Button
button="link"
icon={ICONS.REMOVE}
title={__('Remove swap')}
onClick={() => removeCoinSwap(x.chargeCode)}
/>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
</div>
</div>
<div className="section__actions">
<Button
autoFocus
button="primary"
label={__('Go Back')}
onClick={() => {
returnToMainAction();
setNag(null);
}}
/>
{coinSwaps.length !== 0 && !isRefreshingStatus && (
<Button button="link" label={__('Refresh')} onClick={handleViewPastSwaps} />
)}
{isRefreshingStatus && <Spinner type="small" />}
</div>
</>
);
if (!isAuthenticated) {
return <Redirect to={`/$/${PAGES.AUTH_SIGNIN}?redirect=${location.pathname}`} />;
}
return (
<Form onSubmit={handleStartSwap}>
<Card
title={<I18nMessage tokens={{ lbc: <LbcSymbol size={22} /> }}>Swap Crypto for %lbc%</I18nMessage>}
subtitle={__(
'Send crypto to the address provided and you will be sent an equivalent amount of Credits. You can pay with BCH, LTC, ETH, USDC or DAI after starting the swap.'
)}
actions={getActionElement()}
nag={nag ? <Nag relative type={nag.type} message={__(nag.msg)} /> : null}
/>
</Form>
);
}
export default WalletSwap;

View file

@ -431,6 +431,11 @@ export const COMMENT_SUPER_CHAT_LIST_FAILED = 'COMMENT_SUPER_CHAT_LIST_FAILED';
// Blocked channels // Blocked channels
export const TOGGLE_BLOCK_CHANNEL = 'TOGGLE_BLOCK_CHANNEL'; export const TOGGLE_BLOCK_CHANNEL = 'TOGGLE_BLOCK_CHANNEL';
// Coin swap
export const ADD_COIN_SWAP = 'ADD_COIN_SWAP';
export const REMOVE_COIN_SWAP = 'REMOVE_COIN_SWAP';
export const COIN_SWAP_STATUS_RECEIVED = 'COIN_SWAP_STATUS_RECEIVED';
// Tags // Tags
export const TOGGLE_TAG_FOLLOW = 'TOGGLE_TAG_FOLLOW'; export const TOGGLE_TAG_FOLLOW = 'TOGGLE_TAG_FOLLOW';
export const TAG_ADD = 'TAG_ADD'; export const TAG_ADD = 'TAG_ADD';

View file

@ -186,5 +186,3 @@ export const MYSTERIES = 'Mysteries';
export const TECHNOLOGY = 'Technology'; export const TECHNOLOGY = 'Technology';
export const EMOJI = 'Emoji'; export const EMOJI = 'Emoji';
export const STICKER = 'Sticker'; export const STICKER = 'Sticker';
export const SIMPLE_EDITOR = 'SimpleEditor';
export const ADVANCED_EDITOR = 'AdvancedEditor';

View file

@ -40,6 +40,7 @@ export const SYNC_ENABLE = 'SYNC_ENABLE';
export const IMAGE_UPLOAD = 'image_upload'; export const IMAGE_UPLOAD = 'image_upload';
export const MOBILE_SEARCH = 'mobile_search'; export const MOBILE_SEARCH = 'mobile_search';
export const VIEW_IMAGE = 'view_image'; export const VIEW_IMAGE = 'view_image';
export const CONFIRM_REMOVE_BTC_SWAP_ADDRESS = 'confirm_remove_btc_swap_address';
export const BLOCK_CHANNEL = 'block_channel'; export const BLOCK_CHANNEL = 'block_channel';
export const COLLECTION_ADD = 'collection_add'; export const COLLECTION_ADD = 'collection_add';
export const COLLECTION_DELETE = 'collection_delete'; export const COLLECTION_DELETE = 'collection_delete';

View file

@ -51,6 +51,7 @@ export const PAGE_TITLE = {
[PAGES.SETTINGS_STRIPE_CARD]: 'Payment Methods', [PAGES.SETTINGS_STRIPE_CARD]: 'Payment Methods',
[PAGES.SETTINGS_UPDATE_PWD]: 'Update password', [PAGES.SETTINGS_UPDATE_PWD]: 'Update password',
[PAGES.SETTINGS_OWN_COMMENTS]: 'Your comments', [PAGES.SETTINGS_OWN_COMMENTS]: 'Your comments',
[PAGES.SWAP]: 'Swap Credits',
[PAGES.TAGS_FOLLOWING]: 'Tags', [PAGES.TAGS_FOLLOWING]: 'Tags',
[PAGES.TAGS_FOLLOWING_MANAGE]: 'Manage tags', [PAGES.TAGS_FOLLOWING_MANAGE]: 'Manage tags',
[PAGES.UPLOAD]: 'Upload', [PAGES.UPLOAD]: 'Upload',

View file

@ -70,6 +70,7 @@ exports.CODE_2257 = '2257';
exports.BUY = 'buy'; exports.BUY = 'buy';
exports.RECEIVE = 'receive'; exports.RECEIVE = 'receive';
exports.SEND = 'send'; exports.SEND = 'send';
exports.SWAP = 'swap';
exports.CHANNEL_NEW = 'channel/new'; exports.CHANNEL_NEW = 'channel/new';
exports.NOTIFICATIONS = 'notifications'; exports.NOTIFICATIONS = 'notifications';
exports.YOUTUBE_SYNC = 'youtube'; exports.YOUTUBE_SYNC = 'youtube';

View file

@ -11,8 +11,7 @@ export const SUPPORT = 'support';
export const CHANNEL = 'channel'; export const CHANNEL = 'channel';
export const PUBLISH = 'publish'; export const PUBLISH = 'publish';
export const REPOST = 'repost'; export const REPOST = 'repost';
export const COLLECTION = 'collection'; export const DROPDOWN_TYPES = [ALL, SENT, RECEIVED, SUPPORT, CHANNEL, PUBLISH, REPOST];
export const DROPDOWN_TYPES = [ALL, SENT, RECEIVED, SUPPORT, CHANNEL, PUBLISH, REPOST, COLLECTION];
// dropdown subtypes // dropdown subtypes
export const TIP = 'tip'; export const TIP = 'tip';
export const PURCHASE = 'purchase'; export const PURCHASE = 'purchase';

View file

@ -1,4 +1,4 @@
import '@babel/polyfill'; import 'babel-polyfill';
import ErrorBoundary from 'component/errorBoundary'; import ErrorBoundary from 'component/errorBoundary';
import App from 'component/app'; import App from 'component/app';
import SnackBar from 'component/snackBar'; import SnackBar from 'component/snackBar';

View file

@ -36,7 +36,7 @@ const Lbry = {
// Returns a human readable media type based on the content type or extension of a file that is returned by the sdk // Returns a human readable media type based on the content type or extension of a file that is returned by the sdk
getMediaType: (contentType: ?string, fileName: ?string) => { getMediaType: (contentType: ?string, fileName: ?string) => {
if (fileName && fileName.split('.').length > 1) { if (fileName) {
const formats = [ const formats = [
[/\.(mp4|m4v|webm|flv|f4v|ogv)$/i, 'video'], [/\.(mp4|m4v|webm|flv|f4v|ogv)$/i, 'video'],
[/\.(mp3|m4a|aac|wav|flac|ogg|opus)$/i, 'audio'], [/\.(mp3|m4a|aac|wav|flac|ogg|opus)$/i, 'audio'],

View file

@ -4,7 +4,7 @@ import { Modal } from 'modal/modal';
import { formatFileSystemPath } from 'util/url'; import { formatFileSystemPath } from 'util/url';
type Props = { type Props = {
upload: (File) => void, upload: WebFile => void,
filePath: string, filePath: string,
closeModal: () => void, closeModal: () => void,
showToast: ({}) => void, showToast: ({}) => void,

View file

@ -5,7 +5,7 @@ import ModalConfirmThumbnailUpload from './view';
const perform = (dispatch) => ({ const perform = (dispatch) => ({
closeModal: () => dispatch(doHideModal()), closeModal: () => dispatch(doHideModal()),
upload: (fileWithPath, cb) => dispatch(doUploadThumbnail(null, fileWithPath.file, null, null, fileWithPath.path, cb)), upload: (file, cb) => dispatch(doUploadThumbnail(null, file, null, null, file.path, cb)),
updatePublishForm: (value) => dispatch(doUpdatePublishForm(value)), updatePublishForm: (value) => dispatch(doUpdatePublishForm(value)),
}); });

View file

@ -4,8 +4,8 @@ import { Modal } from 'modal/modal';
import { DOMAIN } from 'config'; import { DOMAIN } from 'config';
type Props = { type Props = {
file: FileWithPath, file: WebFile,
upload: (FileWithPath, (string) => void) => void, upload: (WebFile, (string) => void) => void,
cb: (string) => void, cb: (string) => void,
closeModal: () => void, closeModal: () => void,
updatePublishForm: ({}) => void, updatePublishForm: ({}) => void,
@ -23,7 +23,7 @@ class ModalConfirmThumbnailUpload extends React.PureComponent<Props> {
render() { render() {
const { closeModal, file } = this.props; const { closeModal, file } = this.props;
const filePath = file && file.path; const filePath = file && (file.path || file.name);
return ( return (
<Modal <Modal

View file

@ -9,12 +9,12 @@ import Button from 'component/button';
import FileList from 'component/common/file-list'; import FileList from 'component/common/file-list';
type Props = { type Props = {
files: Array<File>, files: Array<WebFile>,
hideModal: () => void, hideModal: () => void,
updatePublishForm: ({}) => void, updatePublishForm: ({}) => void,
history: { history: {
location: { pathname: string }, location: { pathname: string },
push: (string) => void, push: string => void,
}, },
}; };
@ -43,7 +43,7 @@ const ModalFileSelection = (props: Props) => {
navigateToPublish(); navigateToPublish();
} }
const handleFileChange = (file?: File) => { const handleFileChange = (file?: WebFile) => {
// $FlowFixMe // $FlowFixMe
setSelectedFile(file); setSelectedFile(file);
}; };

View file

@ -14,13 +14,10 @@ type Props = {
function ModalImageUpload(props: Props) { function ModalImageUpload(props: Props) {
const { closeModal, currentValue, title, assetName, helpText, onUpdate } = props; const { closeModal, currentValue, title, assetName, helpText, onUpdate } = props;
const filters = React.useMemo(() => [{ name: 'Images', extensions: ['jpg', 'jpeg', 'png', 'gif', 'svg'] }]);
return ( return (
<Modal isOpen type="card" onAborted={closeModal} contentLabel={title}> <Modal isOpen type="card" onAborted={closeModal} contentLabel={title}>
<SelectAsset <SelectAsset
filters={filters}
type="openFile"
onUpdate={onUpdate} onUpdate={onUpdate}
currentValue={currentValue} currentValue={currentValue}
assetName={assetName} assetName={assetName}

View file

@ -15,7 +15,7 @@ import Icon from 'component/common/icon';
import { NO_FILE } from 'redux/actions/publish'; import { NO_FILE } from 'redux/actions/publish';
type Props = { type Props = {
filePath: ?File, filePath: string | WebFile,
isMarkdownPost: boolean, isMarkdownPost: boolean,
optimize: boolean, optimize: boolean,
title: ?string, title: ?string,
@ -104,12 +104,17 @@ const ModalPublishPreview = (props: Props) => {
// @endif // @endif
} }
function getFilePathName(filePath: ?File) { function getFilePathName(filePath: string | WebFile) {
if (!filePath) { if (!filePath) {
return NO_FILE; return NO_FILE;
} }
if (typeof filePath === 'string') {
return filePath;
} else {
return filePath.name; return filePath.name;
} }
}
function createRow(label: string, value: any) { function createRow(label: string, value: any) {
return ( return (
@ -122,7 +127,7 @@ const ModalPublishPreview = (props: Props) => {
const txFee = previewResponse ? previewResponse['total_fee'] : null; const txFee = previewResponse ? previewResponse['total_fee'] : null;
// $FlowFixMe add outputs[0] etc to PublishResponse type // $FlowFixMe add outputs[0] etc to PublishResponse type
const isOptimizeAvail = filePath && isVid && ffmpegStatus.available; const isOptimizeAvail = filePath && filePath !== '' && isVid && ffmpegStatus.available;
let modalTitle; let modalTitle;
if (isStillEditing) { if (isStillEditing) {
modalTitle = __('Confirm Edit'); modalTitle = __('Confirm Edit');

View file

@ -0,0 +1,13 @@
import { connect } from 'react-redux';
import { doHideModal } from 'redux/actions/app';
import ModalRemoveBtcSwapAddress from './view';
import { doRemoveCoinSwap } from 'redux/actions/coinSwap';
const select = (state, props) => ({});
const perform = (dispatch) => ({
removeCoinSwap: (chargeCode) => dispatch(doRemoveCoinSwap(chargeCode)),
closeModal: () => dispatch(doHideModal()),
});
export default connect(select, perform)(ModalRemoveBtcSwapAddress);

View file

@ -0,0 +1,43 @@
// @flow
import React from 'react';
import { Modal } from 'modal/modal';
import Button from 'component/button';
import Card from 'component/common/card';
import I18nMessage from 'component/i18nMessage';
type Props = {
chargeCode: string,
removeCoinSwap: (string) => void,
closeModal: () => void,
};
function ModalRemoveBtcSwapAddress(props: Props) {
const { chargeCode, removeCoinSwap, closeModal } = props;
return (
<Modal isOpen contentLabel={__('Confirm Swap Removal')} type="card" onAborted={closeModal}>
<Card
title={__('Remove Swap')}
subtitle={<I18nMessage tokens={{ address: <em>{`${chargeCode}`}</em> }}>Remove %address%?</I18nMessage>}
body={<p className="help--warning">{__('This process cannot be reversed.')}</p>}
actions={
<>
<div className="section__actions">
<Button
button="primary"
label={__('OK')}
onClick={() => {
removeCoinSwap(chargeCode);
closeModal();
}}
/>
<Button button="link" label={__('Cancel')} onClick={closeModal} />
</div>
</>
}
/>
</Modal>
);
}
export default ModalRemoveBtcSwapAddress;

View file

@ -41,6 +41,8 @@ import ModalPasswordUnsave from 'modal/modalPasswordUnsave';
import ModalPublish from 'modal/modalPublish'; import ModalPublish from 'modal/modalPublish';
import ModalPublishPreview from 'modal/modalPublishPreview'; import ModalPublishPreview from 'modal/modalPublishPreview';
import ModalRemoveBtcSwapAddress from 'modal/modalRemoveBtcSwapAddress';
import ModalRemoveCard from 'modal/modalRemoveCard'; import ModalRemoveCard from 'modal/modalRemoveCard';
import ModalRemoveComment from 'modal/modalRemoveComment'; import ModalRemoveComment from 'modal/modalRemoveComment';
@ -135,6 +137,8 @@ function getModal(id) {
return ModalViewImage; return ModalViewImage;
case MODALS.MASS_TIP_UNLOCK: case MODALS.MASS_TIP_UNLOCK:
return ModalMassTipsUnlock; return ModalMassTipsUnlock;
case MODALS.CONFIRM_REMOVE_BTC_SWAP_ADDRESS:
return ModalRemoveBtcSwapAddress;
case MODALS.BLOCK_CHANNEL: case MODALS.BLOCK_CHANNEL:
return ModalBlockChannel; return ModalBlockChannel;
case MODALS.COLLECTION_ADD: case MODALS.COLLECTION_ADD:

View file

@ -1,7 +1,6 @@
// @flow // @flow
import * as React from 'react'; import * as React from 'react';
import classnames from 'classnames'; import classnames from 'classnames';
import { useIsMediumScreen } from 'effects/use-screensize';
import Page from 'component/page'; import Page from 'component/page';
import * as RENDER_MODES from 'constants/file_render_modes'; import * as RENDER_MODES from 'constants/file_render_modes';
import FileTitleSection from 'component/fileTitleSection'; import FileTitleSection from 'component/fileTitleSection';
@ -60,7 +59,6 @@ function FilePage(props: Props) {
} = props; } = props;
const cost = costInfo ? costInfo.cost : null; const cost = costInfo ? costInfo.cost : null;
const hasFileInfo = fileInfo !== undefined; const hasFileInfo = fileInfo !== undefined;
const isMediumScreen = useIsMediumScreen();
const isMarkdown = renderMode === RENDER_MODES.MARKDOWN; const isMarkdown = renderMode === RENDER_MODES.MARKDOWN;
const videoPlayedEnoughToResetPosition = React.useMemo(() => { const videoPlayedEnoughToResetPosition = React.useMemo(() => {
const durationInSecs = const durationInSecs =
@ -171,10 +169,8 @@ function FilePage(props: Props) {
<div className={classnames('section card-stack', `file-page__${renderMode}`)}> <div className={classnames('section card-stack', `file-page__${renderMode}`)}>
<FileTitleSection uri={uri} isNsfwBlocked /> <FileTitleSection uri={uri} isNsfwBlocked />
</div> </div>
{collection && !isMarkdown && !videoTheaterMode && !isMediumScreen && ( {collection && !isMarkdown && !videoTheaterMode && <CollectionContent id={collectionId} uri={uri} />}
<CollectionContent id={collectionId} uri={uri} /> {!collection && !isMarkdown && !videoTheaterMode && <RecommendedContent uri={uri} />}
)}
{!collection && !isMarkdown && !videoTheaterMode && !isMediumScreen && <RecommendedContent uri={uri} />}
</Page> </Page>
); );
} }
@ -191,17 +187,13 @@ function FilePage(props: Props) {
{commentsDisabled && <Empty text={__('The creator of this content has disabled comments.')} />} {commentsDisabled && <Empty text={__('The creator of this content has disabled comments.')} />}
{!commentsDisabled && <CommentsList uri={uri} linkedCommentId={linkedCommentId} />} {!commentsDisabled && <CommentsList uri={uri} linkedCommentId={linkedCommentId} />}
</div> </div>
{!collection && !isMarkdown && videoTheaterMode && !isMediumScreen && <RecommendedContent uri={uri} />} {!collection && !isMarkdown && videoTheaterMode && <RecommendedContent uri={uri} />}
{collection && !isMarkdown && videoTheaterMode && !isMediumScreen && ( {collection && !isMarkdown && videoTheaterMode && <CollectionContent id={collectionId} uri={uri} />}
<CollectionContent id={collectionId} uri={uri} />
)}
</div> </div>
)} )}
</div> </div>
{collection && !isMarkdown && !videoTheaterMode && !isMediumScreen && ( {collection && !isMarkdown && !videoTheaterMode && <CollectionContent id={collectionId} uri={uri} />}
<CollectionContent id={collectionId} uri={uri} /> {!collection && !isMarkdown && !videoTheaterMode && <RecommendedContent uri={uri} />}
)}
{!collection && !isMarkdown && !videoTheaterMode && !isMediumScreen && <RecommendedContent uri={uri} />}
{isMarkdown && ( {isMarkdown && (
<div className="file-page__post-comments"> <div className="file-page__post-comments">
{!commentsDisabled && <CommentsList uri={uri} linkedCommentId={linkedCommentId} commentsAreExpanded />} {!commentsDisabled && <CommentsList uri={uri} linkedCommentId={linkedCommentId} commentsAreExpanded />}

View file

@ -62,7 +62,7 @@ class ReportPage extends React.Component {
name="message" name="message"
stretch stretch
value={this.state.message} value={this.state.message}
onChange={(event) => { onChange={event => {
this.onMessageChange(event); this.onMessageChange(event);
}} }}
placeholder={__('Description of your issue or feature request')} placeholder={__('Description of your issue or feature request')}
@ -71,7 +71,7 @@ class ReportPage extends React.Component {
<div className="section__actions"> <div className="section__actions">
<Button <Button
button="primary" button="primary"
onClick={(event) => { onClick={event => {
this.submitMessage(event); this.submitMessage(event);
}} }}
className={`button-block button-primary ${this.state.submitting ? 'disabled' : ''}`} className={`button-block button-primary ${this.state.submitting ? 'disabled' : ''}`}

View file

@ -313,6 +313,39 @@ export default function SettingsCreatorPage(props: Props) {
/> />
</SettingsRow> </SettingsRow>
<SettingsRow
title={
<I18nMessage tokens={{ lbc: <LbcSymbol /> }}>Minimum %lbc% tip amount for hyperchats</I18nMessage>
}
subtitle={
<>
{__(HELP.MIN_SUPER)}
{minTip !== 0 && (
<p className="help--inline">
<em>{__(HELP.MIN_SUPER_OFF)}</em>
</p>
)}
</>
}
>
<FormField
name="min_tip_amount_super_chat"
className="form-field--price-amount"
min={0}
step="any"
type="number"
placeholder="1"
value={minSuper}
disabled={minTip !== 0}
onChange={(e) => {
const newMinSuper = parseFloat(e.target.value);
setMinSuper(newMinSuper);
pushMinSuperDebounced(newMinSuper, activeChannelClaim);
}}
onBlur={() => setLastUpdated(Date.now())}
/>
</SettingsRow>
<SettingsRow title={__('Moderators')} subtitle={__(HELP.MODERATORS)} multirow> <SettingsRow title={__('Moderators')} subtitle={__(HELP.MODERATORS)} multirow>
<SearchChannelField <SearchChannelField
label={__('Moderators')} label={__('Moderators')}

View file

@ -10,9 +10,6 @@ import {
selectTitleForUri, selectTitleForUri,
selectClaimIsMine, selectClaimIsMine,
makeSelectClaimIsPending, makeSelectClaimIsPending,
makeSelectIsBlacklisted,
makeSelectBlacklistedDueToDMCA,
makeSelectClaimErrorCensor,
} from 'redux/selectors/claims'; } from 'redux/selectors/claims';
import { import {
makeSelectCollectionForId, makeSelectCollectionForId,
@ -26,6 +23,7 @@ import { normalizeURI } from 'util/lbryURI';
import * as COLLECTIONS_CONSTS from 'constants/collections'; import * as COLLECTIONS_CONSTS from 'constants/collections';
import { push } from 'connected-react-router'; import { push } from 'connected-react-router';
import { makeSelectChannelInSubscriptions } from 'redux/selectors/subscriptions'; import { makeSelectChannelInSubscriptions } from 'redux/selectors/subscriptions';
import { selectBlackListedOutpoints } from 'lbryinc';
import ShowPage from './view'; import ShowPage from './view';
const select = (state, props) => { const select = (state, props) => {
@ -74,6 +72,7 @@ const select = (state, props) => {
uri, uri,
claim, claim,
isResolvingUri: selectIsUriResolving(state, uri), isResolvingUri: selectIsUriResolving(state, uri),
blackListedOutpoints: selectBlackListedOutpoints(state),
totalPages: makeSelectTotalPagesForChannel(uri, PAGE_SIZE)(state), totalPages: makeSelectTotalPagesForChannel(uri, PAGE_SIZE)(state),
isSubscribed: makeSelectChannelInSubscriptions(uri)(state), isSubscribed: makeSelectChannelInSubscriptions(uri)(state),
title: selectTitleForUri(state, uri), title: selectTitleForUri(state, uri),
@ -83,9 +82,6 @@ const select = (state, props) => {
collectionId: collectionId, collectionId: collectionId,
collectionUrls: makeSelectUrlsForCollectionId(collectionId)(state), collectionUrls: makeSelectUrlsForCollectionId(collectionId)(state),
isResolvingCollection: makeSelectIsResolvingCollectionForId(collectionId)(state), isResolvingCollection: makeSelectIsResolvingCollectionForId(collectionId)(state),
isBlacklisted: makeSelectIsBlacklisted(uri)(state),
isBlacklistedDueToDMCA: makeSelectBlacklistedDueToDMCA(uri)(state),
errorCensor: makeSelectClaimErrorCensor(uri)(state),
}; };
}; };

View file

@ -22,6 +22,10 @@ type Props = {
uri: string, uri: string,
claim: StreamClaim, claim: StreamClaim,
location: UrlLocation, location: UrlLocation,
blackListedOutpoints: Array<{
txid: string,
nout: number,
}>,
title: string, title: string,
claimIsMine: boolean, claimIsMine: boolean,
claimIsPending: boolean, claimIsPending: boolean,
@ -31,9 +35,6 @@ type Props = {
collectionUrls: Array<string>, collectionUrls: Array<string>,
isResolvingCollection: boolean, isResolvingCollection: boolean,
fetchCollectionItems: (string) => void, fetchCollectionItems: (string) => void,
isBlacklisted: boolean,
isBlacklistedDueToDMCA: boolean,
errorCensor: ?ClaimErrorCensor,
}; };
function ShowPage(props: Props) { function ShowPage(props: Props) {
@ -42,6 +43,7 @@ function ShowPage(props: Props) {
resolveUri, resolveUri,
uri, uri,
claim, claim,
blackListedOutpoints,
location, location,
claimIsMine, claimIsMine,
isSubscribed, isSubscribed,
@ -52,13 +54,11 @@ function ShowPage(props: Props) {
collection, collection,
collectionUrls, collectionUrls,
isResolvingCollection, isResolvingCollection,
isBlacklisted,
isBlacklistedDueToDMCA,
errorCensor,
} = props; } = props;
const { search } = location; const { search } = location;
const signingChannel = claim && claim.signing_channel;
const canonicalUrl = claim && claim.canonical_url; const canonicalUrl = claim && claim.canonical_url;
const claimExists = claim !== null && claim !== undefined; const claimExists = claim !== null && claim !== undefined;
const haventFetchedYet = claim === undefined; const haventFetchedYet = claim === undefined;
@ -103,7 +103,7 @@ function ShowPage(props: Props) {
} }
let innerContent = ''; let innerContent = '';
if ((!claim || (claim && !claim.name)) && !isBlacklisted) { if (!claim || (claim && !claim.name)) {
innerContent = ( innerContent = (
<Page> <Page>
{(claim === undefined || {(claim === undefined ||
@ -142,15 +142,26 @@ function ShowPage(props: Props) {
{!isResolvingUri && isSubscribed && claim === null && <AbandonedChannelPreview uri={uri} type={'large'} />} {!isResolvingUri && isSubscribed && claim === null && <AbandonedChannelPreview uri={uri} type={'large'} />}
</Page> </Page>
); );
} else if (claim && claim.name.length && claim.name[0] === '@') { } else if (claim.name.length && claim.name[0] === '@') {
innerContent = <ChannelPage uri={uri} location={location} />; innerContent = <ChannelPage uri={uri} location={location} />;
} else if (isBlacklisted && !claimIsMine) { } else if (claim) {
innerContent = isBlacklistedDueToDMCA ? ( let isClaimBlackListed = false;
isClaimBlackListed =
blackListedOutpoints &&
blackListedOutpoints.some(
(outpoint) =>
(signingChannel && outpoint.txid === signingChannel.txid && outpoint.nout === signingChannel.nout) ||
(outpoint.txid === claim.txid && outpoint.nout === claim.nout)
);
if (isClaimBlackListed && !claimIsMine) {
innerContent = (
<Page> <Page>
<Card <Card
title={uri} title={uri}
subtitle={__( subtitle={__(
'Your hub has blocked access to this content do to a complaint received under the US Digital Millennium Copyright Act.' 'In response to a complaint we received under the US Digital Millennium Copyright Act, we have blocked access to this content from our applications.'
)} )}
actions={ actions={
<div className="section__actions"> <div className="section__actions">
@ -159,26 +170,11 @@ function ShowPage(props: Props) {
} }
/> />
</Page> </Page>
) : (
<Page>
<Card
title={uri}
subtitle={
<>
{__('Your hub has blocked this content because it subscribes to the following blocking channel:')}{' '}
<Button
button="link"
navigate={errorCensor && errorCensor.canonical_url}
label={errorCensor && errorCensor.name}
/>
</>
}
/>
</Page>
); );
} else if (claim) { } else {
innerContent = <FilePage uri={uri} location={location} />; innerContent = <FilePage uri={uri} location={location} />;
} }
}
return <React.Fragment>{innerContent}</React.Fragment>; return <React.Fragment>{innerContent}</React.Fragment>;
} }

View file

@ -16,6 +16,7 @@ import rewardsReducer from 'redux/reducers/rewards';
import userReducer from 'redux/reducers/user'; import userReducer from 'redux/reducers/user';
import commentsReducer from 'redux/reducers/comments'; import commentsReducer from 'redux/reducers/comments';
import blockedReducer from 'redux/reducers/blocked'; import blockedReducer from 'redux/reducers/blocked';
import coinSwapReducer from 'redux/reducers/coinSwap';
import searchReducer from 'redux/reducers/search'; import searchReducer from 'redux/reducers/search';
import reactionsReducer from 'redux/reducers/reactions'; import reactionsReducer from 'redux/reducers/reactions';
import syncReducer from 'redux/reducers/sync'; import syncReducer from 'redux/reducers/sync';
@ -43,6 +44,7 @@ export default (history) =>
subscriptions: subscriptionsReducer, subscriptions: subscriptionsReducer,
tags: tagsReducer, tags: tagsReducer,
blocked: blockedReducer, blocked: blockedReducer,
coinSwap: coinSwapReducer,
user: userReducer, user: userReducer,
wallet: walletReducer, wallet: walletReducer,
sync: syncReducer, sync: syncReducer,

View file

@ -458,34 +458,32 @@ export function doAnalyticsView(uri, timeToStart) {
} }
export function doAnalyticsBuffer(uri, bufferData) { export function doAnalyticsBuffer(uri, bufferData) {
return () => { return (dispatch, getState) => {
// return (dispatch, getState) => { const state = getState();
// const state = getState(); const claim = selectClaimForUri(state, uri);
// const claim = selectClaimForUri(state, uri); const user = selectUser(state);
// const user = selectUser(state); const {
// const { value: { video, audio, source },
// value: { video, audio, source }, } = claim;
// } = claim; const timeAtBuffer = parseInt(bufferData.currentTime ? bufferData.currentTime * 1000 : 0);
// const timeAtBuffer = parseInt(bufferData.currentTime ? bufferData.currentTime * 1000 : 0); const bufferDuration = parseInt(bufferData.secondsToLoad ? bufferData.secondsToLoad * 1000 : 0);
// const bufferDuration = parseInt(bufferData.secondsToLoad ? bufferData.secondsToLoad * 1000 : 0); const fileDurationInSeconds = (video && video.duration) || (audio && audio.duration);
// const fileDurationInSeconds = (video && video.duration) || (audio && audio.duration); const fileSize = source.size; // size in bytes
// const fileSize = source.size; // size in bytes const fileSizeInBits = fileSize * 8;
// const fileSizeInBits = fileSize * 8; const bitRate = parseInt(fileSizeInBits / fileDurationInSeconds);
// const bitRate = parseInt(fileSizeInBits / fileDurationInSeconds); const userId = user && user.id.toString();
// const userId = user && user.id.toString();
// if there's a logged in user, send buffer event data to watchman // if there's a logged in user, send buffer event data to watchman
// if (<condition>) { if (userId) {
// STUB: any buffer events here analytics.videoBufferEvent(claim, {
// analytics.videoBufferEvent(claim, { timeAtBuffer,
// timeAtBuffer, bufferDuration,
// bufferDuration, bitRate,
// bitRate, userId,
// userId, duration: fileDurationInSeconds,
// duration: fileDurationInSeconds, playerPoweredBy: bufferData.playerPoweredBy,
// playerPoweredBy: bufferData.playerPoweredBy, readyState: bufferData.readyState,
// readyState: bufferData.readyState, });
// }); }
// }
}; };
} }

View file

@ -87,7 +87,7 @@ export function doResolveUris(
if (uriResolveInfo) { if (uriResolveInfo) {
if (uriResolveInfo.error) { if (uriResolveInfo.error) {
// $FlowFixMe // $FlowFixMe
resolveInfo[uri] = { ...fallbackResolveInfo, errorCensor: uriResolveInfo.error.censor }; resolveInfo[uri] = { ...fallbackResolveInfo };
} else { } else {
if (checkReposts) { if (checkReposts) {
if (uriResolveInfo.reposted_claim) { if (uriResolveInfo.reposted_claim) {

View file

@ -0,0 +1,44 @@
// @flow
import * as ACTIONS from 'constants/action_types';
import { selectPrefsReady } from 'redux/selectors/sync';
import { doAlertWaitingForSync } from 'redux/actions/app';
import { Lbryio } from 'lbryinc';
export const doAddCoinSwap = (coinSwapInfo: CoinSwapInfo) => (dispatch: Dispatch, getState: GetState) => {
const state = getState();
const ready = selectPrefsReady(state);
if (!ready) {
return dispatch(doAlertWaitingForSync());
}
dispatch({
type: ACTIONS.ADD_COIN_SWAP,
data: coinSwapInfo,
});
};
export const doRemoveCoinSwap = (chargeCode: string) => (dispatch: Dispatch, getState: GetState) => {
const state = getState();
const ready = selectPrefsReady(state);
if (!ready) {
return dispatch(doAlertWaitingForSync());
}
dispatch({
type: ACTIONS.REMOVE_COIN_SWAP,
data: {
chargeCode,
},
});
};
export const doQueryCoinSwapStatus = (chargeCode: string) => (dispatch: Dispatch, getState: GetState) => {
Lbryio.call('btc', 'status', { charge_code: chargeCode }).then((response) => {
dispatch({
type: ACTIONS.COIN_SWAP_STATUS_RECEIVED,
data: response,
});
});
};

View file

@ -1143,9 +1143,6 @@ export function doCommentModUnBlockAsModerator(commenterUri: string, creatorUri:
export function doFetchModBlockedList() { export function doFetchModBlockedList() {
return async (dispatch: Dispatch, getState: GetState) => { return async (dispatch: Dispatch, getState: GetState) => {
const LOOP_CHUNK_SIZE = 1;
const yieldThread = () => new Promise((resolve) => setTimeout(resolve));
const state = getState(); const state = getState();
const myChannels = selectMyChannelClaims(state); const myChannels = selectMyChannelClaims(state);
if (!myChannels) { if (!myChannels) {
@ -1175,7 +1172,7 @@ export function doFetchModBlockedList() {
}) })
) )
) )
.then(async (res) => { .then((res) => {
let personalBlockList = []; let personalBlockList = [];
let adminBlockList = []; let adminBlockList = [];
let moderatorBlockList = []; let moderatorBlockList = [];
@ -1188,21 +1185,15 @@ export function doFetchModBlockedList() {
const adminTimeoutMap = {}; const adminTimeoutMap = {};
const moderatorTimeoutMap = {}; const moderatorTimeoutMap = {};
const blockListsPerChannel = []; const blockListsPerChannel = res.map((r) => r.value);
for (let i = 0; i < res.length; ++i) { blockListsPerChannel
blockListsPerChannel.push(res[i].value); .sort((a, b) => {
if (i % 2 === 0) { return 1;
await yieldThread(); })
} .forEach((channelBlockLists) => {
} const storeList = (fetchedList, blockedList, timeoutMap, blockedByMap) => {
for (let i = 0; i < blockListsPerChannel.length; ++i) {
const storeList = async (fetchedList, blockedList, timeoutMap, blockedByMap) => {
if (fetchedList) { if (fetchedList) {
for (let j = 0; j < fetchedList.length; ++j) { fetchedList.forEach((blockedChannel) => {
const blockedChannel = fetchedList[j];
if (j > 0 && i % LOOP_CHUNK_SIZE === 0) {
await yieldThread();
}
if (blockedChannel.blocked_channel_name) { if (blockedChannel.blocked_channel_name) {
const channelUri = buildURI({ const channelUri = buildURI({
channelName: blockedChannel.blocked_channel_name, channelName: blockedChannel.blocked_channel_name,
@ -1236,28 +1227,24 @@ export function doFetchModBlockedList() {
} }
} }
} }
} });
} }
}; };
const channelBlockLists = blockListsPerChannel[i];
const blocked_channels = channelBlockLists && channelBlockLists.blocked_channels; const blocked_channels = channelBlockLists && channelBlockLists.blocked_channels;
const globally_blocked_channels = channelBlockLists && channelBlockLists.globally_blocked_channels; const globally_blocked_channels = channelBlockLists && channelBlockLists.globally_blocked_channels;
const delegated_blocked_channels = channelBlockLists && channelBlockLists.delegated_blocked_channels; const delegated_blocked_channels = channelBlockLists && channelBlockLists.delegated_blocked_channels;
if (i > 0 && i % LOOP_CHUNK_SIZE === 0) { storeList(blocked_channels, personalBlockList, personalTimeoutMap);
await yieldThread(); storeList(globally_blocked_channels, adminBlockList, adminTimeoutMap);
} storeList(
await storeList(blocked_channels, personalBlockList, personalTimeoutMap);
await storeList(globally_blocked_channels, adminBlockList, adminTimeoutMap);
await storeList(
delegated_blocked_channels, delegated_blocked_channels,
moderatorBlockList, moderatorBlockList,
moderatorTimeoutMap, moderatorTimeoutMap,
moderatorBlockListDelegatorsMap moderatorBlockListDelegatorsMap
); );
} });
dispatch({ dispatch({
type: ACTIONS.COMMENT_MODERATION_BLOCK_LIST_COMPLETED, type: ACTIONS.COMMENT_MODERATION_BLOCK_LIST_COMPLETED,
data: { data: {

View file

@ -18,7 +18,7 @@ import Lbry from 'lbry';
import { isClaimNsfw } from 'util/claim'; import { isClaimNsfw } from 'util/claim';
export const NO_FILE = '---'; export const NO_FILE = '---';
export const doPublishDesktop = (filePath: ?File, preview?: boolean) => (dispatch: Dispatch, getState: () => {}) => { export const doPublishDesktop = (filePath: string, preview?: boolean) => (dispatch: Dispatch, getState: () => {}) => {
const publishPreview = (previewResponse) => { const publishPreview = (previewResponse) => {
dispatch( dispatch(
doOpenModal(MODALS.PUBLISH_PREVIEW, { doOpenModal(MODALS.PUBLISH_PREVIEW, {
@ -138,9 +138,14 @@ export const doUpdatePublishForm = (publishFormValue: UpdatePublishFormData) =>
data: { ...publishFormValue }, data: { ...publishFormValue },
}); });
export const doUploadThumbnail = export const doUploadThumbnail = (
(filePath?: string, thumbnailBlob?: File, fsAdapter?: any, fs?: any, path?: any, cb?: (string) => void) => filePath?: string,
(dispatch: Dispatch) => { thumbnailBlob?: File,
fsAdapter?: any,
fs?: any,
path?: any,
cb?: (string) => void
) => (dispatch: Dispatch) => {
const downMessage = __('Thumbnail upload service may be down, try again later.'); const downMessage = __('Thumbnail upload service may be down, try again later.');
let thumbnail, fileExt, fileName, fileType; let thumbnail, fileExt, fileName, fileType;
@ -248,8 +253,9 @@ export const doUploadThumbnail =
} }
}; };
export const doPrepareEdit = export const doPrepareEdit = (claim: StreamClaim, uri: string, fileInfo: FileListItem, fs: any) => (
(claim: StreamClaim, uri: string, fileInfo: FileListItem, fs: any) => (dispatch: Dispatch) => { dispatch: Dispatch
) => {
const { name, amount, value = {} } = claim; const { name, amount, value = {} } = claim;
const channelName = (claim && claim.signing_channel && claim.signing_channel.name) || null; const channelName = (claim && claim.signing_channel && claim.signing_channel.name) || null;
const { const {
@ -311,8 +317,10 @@ export const doPrepareEdit =
dispatch({ type: ACTIONS.DO_PREPARE_EDIT, data: publishData }); dispatch({ type: ACTIONS.DO_PREPARE_EDIT, data: publishData });
}; };
export const doPublish = export const doPublish = (success: Function, fail: Function, preview: Function) => (
(success: Function, fail: Function, preview: Function) => (dispatch: Dispatch, getState: () => {}) => { dispatch: Dispatch,
getState: () => {}
) => {
if (!preview) { if (!preview) {
dispatch({ type: ACTIONS.PUBLISH_START }); dispatch({ type: ACTIONS.PUBLISH_START });
} }

View file

@ -98,8 +98,10 @@ export function doSetSync(oldHash: string, newHash: string, data: any) {
}; };
} }
export const doGetSyncDesktop = export const doGetSyncDesktop = (cb?: (any, any) => void, password?: string) => (
(cb?: (any, any) => void, password?: string) => (dispatch: Dispatch, getState: GetState) => { dispatch: Dispatch,
getState: GetState
) => {
const state = getState(); const state = getState();
const syncEnabled = makeSelectClientSetting(SETTINGS.ENABLE_SYNC)(state); const syncEnabled = makeSelectClientSetting(SETTINGS.ENABLE_SYNC)(state);
const getSyncPending = selectGetSyncIsPending(state); const getSyncPending = selectGetSyncIsPending(state);
@ -383,6 +385,7 @@ type SharedData = {
following?: Array<{ uri: string, notificationsDisabled: boolean }>, following?: Array<{ uri: string, notificationsDisabled: boolean }>,
tags?: Array<string>, tags?: Array<string>,
blocked?: Array<string>, blocked?: Array<string>,
coin_swap_codes?: Array<string>,
settings?: any, settings?: any,
app_welcome_version?: number, app_welcome_version?: number,
sharing_3P?: boolean, sharing_3P?: boolean,
@ -400,6 +403,7 @@ function extractUserState(rawObj: SharedData) {
following, following,
tags, tags,
blocked, blocked,
coin_swap_codes,
settings, settings,
app_welcome_version, app_welcome_version,
sharing_3P, sharing_3P,
@ -414,6 +418,7 @@ function extractUserState(rawObj: SharedData) {
...(following ? { following } : {}), ...(following ? { following } : {}),
...(tags ? { tags } : {}), ...(tags ? { tags } : {}),
...(blocked ? { blocked } : {}), ...(blocked ? { blocked } : {}),
...(coin_swap_codes ? { coin_swap_codes } : {}),
...(settings ? { settings } : {}), ...(settings ? { settings } : {}),
...(app_welcome_version ? { app_welcome_version } : {}), ...(app_welcome_version ? { app_welcome_version } : {}),
...(sharing_3P ? { sharing_3P } : {}), ...(sharing_3P ? { sharing_3P } : {}),
@ -440,6 +445,7 @@ export function doPopulateSharedUserState(sharedSettings: any) {
following, following,
tags, tags,
blocked, blocked,
coin_swap_codes,
settings, settings,
app_welcome_version, app_welcome_version,
sharing_3P, sharing_3P,
@ -464,6 +470,7 @@ export function doPopulateSharedUserState(sharedSettings: any) {
following, following,
tags, tags,
blocked, blocked,
coinSwapCodes: coin_swap_codes,
walletPrefSettings: settings, walletPrefSettings: settings,
mergedClientSettings, mergedClientSettings,
welcomeVersion: app_welcome_version, welcomeVersion: app_welcome_version,

View file

@ -82,6 +82,12 @@ export const doNotificationSocketConnect = (enableNotifications) => (dispatch) =
dispatch(doNotificationList()); dispatch(doNotificationList());
} }
break; break;
case 'swap-status':
dispatch({
type: ACTIONS.COIN_SWAP_STATUS_RECEIVED,
data: data.data,
});
break;
} }
}); });
}; };

View file

@ -16,7 +16,6 @@ type State = {
createCollectionError: ?string, createCollectionError: ?string,
channelClaimCounts: { [string]: number }, channelClaimCounts: { [string]: number },
claimsByUri: { [string]: string }, claimsByUri: { [string]: string },
blacklistedByUri: { [string]: ClaimErrorCensor },
byId: { [string]: Claim }, byId: { [string]: Claim },
pendingById: { [string]: Claim }, // keep pending claims pendingById: { [string]: Claim }, // keep pending claims
resolvingUris: Array<string>, resolvingUris: Array<string>,
@ -68,7 +67,6 @@ type State = {
const reducers = {}; const reducers = {};
const defaultState = { const defaultState = {
blacklistedByUri: {},
byId: {}, byId: {},
claimsByUri: {}, claimsByUri: {},
paginatedClaimsByChannel: {}, paginatedClaimsByChannel: {},
@ -120,7 +118,6 @@ const defaultState = {
function handleClaimAction(state: State, action: any): State { function handleClaimAction(state: State, action: any): State {
const { resolveInfo }: ClaimActionResolveInfo = action.data; const { resolveInfo }: ClaimActionResolveInfo = action.data;
const blacklistedByUri = Object.assign({}, state.blacklistedByUri);
const byUri = Object.assign({}, state.claimsByUri); const byUri = Object.assign({}, state.claimsByUri);
const byId = Object.assign({}, state.byId); const byId = Object.assign({}, state.byId);
const channelClaimCounts = Object.assign({}, state.channelClaimCounts); const channelClaimCounts = Object.assign({}, state.channelClaimCounts);
@ -130,11 +127,9 @@ function handleClaimAction(state: State, action: any): State {
Object.entries(resolveInfo).forEach(([url, resolveResponse]) => { Object.entries(resolveInfo).forEach(([url, resolveResponse]) => {
// $FlowFixMe // $FlowFixMe
const { claimsInChannel, stream, channel: channelFromResolve, collection, errorCensor } = resolveResponse; const { claimsInChannel, stream, channel: channelFromResolve, collection } = resolveResponse;
const channel = channelFromResolve || (stream && stream.signing_channel); const channel = channelFromResolve || (stream && stream.signing_channel);
blacklistedByUri[url] = errorCensor;
if (stream) { if (stream) {
if (pendingById[stream.claim_id]) { if (pendingById[stream.claim_id]) {
byId[stream.claim_id] = mergeClaim(stream, byId[stream.claim_id]); byId[stream.claim_id] = mergeClaim(stream, byId[stream.claim_id]);
@ -202,7 +197,6 @@ function handleClaimAction(state: State, action: any): State {
}); });
return Object.assign({}, state, { return Object.assign({}, state, {
blacklistedByUri,
byId, byId,
claimsByUri: byUri, claimsByUri: byUri,
channelClaimCounts, channelClaimCounts,
@ -524,8 +518,10 @@ reducers[ACTIONS.UPDATE_PENDING_CLAIMS] = (state: State, action: any): State =>
}; };
reducers[ACTIONS.UPDATE_CONFIRMED_CLAIMS] = (state: State, action: any): State => { reducers[ACTIONS.UPDATE_CONFIRMED_CLAIMS] = (state: State, action: any): State => {
const { claims: confirmedClaims, pending: pendingClaims }: { claims: Array<Claim>, pending: { [string]: Claim } } = const {
action.data; claims: confirmedClaims,
pending: pendingClaims,
}: { claims: Array<Claim>, pending: { [string]: Claim } } = action.data;
const byId = Object.assign({}, state.byId); const byId = Object.assign({}, state.byId);
const byUri = Object.assign({}, state.claimsByUri); const byUri = Object.assign({}, state.claimsByUri);
// //

View file

@ -0,0 +1,152 @@
// @flow
import * as ACTIONS from 'constants/action_types';
import { handleActions } from 'util/redux-utils';
const SWAP_HISTORY_LENGTH_LIMIT = 10;
function getBottomEntries(array, count) {
const curCount = array.length;
if (curCount < count) {
return array;
} else {
return array.slice(curCount - count);
}
}
const defaultState: CoinSwapState = {
coinSwaps: [],
};
export default handleActions(
{
[ACTIONS.ADD_COIN_SWAP]: (state: CoinSwapState, action: CoinSwapAddAction): CoinSwapState => {
const { coinSwaps } = state;
const { chargeCode } = action.data;
const newCoinSwaps = coinSwaps.slice();
if (!newCoinSwaps.find((x) => x.chargeCode === chargeCode)) {
newCoinSwaps.push({ ...action.data });
}
return {
...state,
coinSwaps: newCoinSwaps,
};
},
[ACTIONS.REMOVE_COIN_SWAP]: (state: CoinSwapState, action: CoinSwapRemoveAction): CoinSwapState => {
const { coinSwaps } = state;
const { chargeCode } = action.data;
let newCoinSwaps = coinSwaps.slice();
newCoinSwaps = newCoinSwaps.filter((x) => x.chargeCode !== chargeCode);
return {
...state,
coinSwaps: newCoinSwaps,
};
},
[ACTIONS.COIN_SWAP_STATUS_RECEIVED]: (state: CoinSwapState, action: any) => {
const { coinSwaps } = state;
const newCoinSwaps = coinSwaps.slice();
let exchange;
let charge;
if (action.data.event_data) {
// Source: Websocket
exchange = { lbc_txid: action.data.lbc_txid };
charge = action.data.event_data;
} else {
// Source: btc/status
exchange = action.data.Exchange;
charge = action.data.Charge.data;
}
const calculateLbcAmount = (pricing, exchange, fallback) => {
if (!exchange || !exchange.rate) {
return fallback || 0;
}
const btcAmount = pricing['bitcoin'].amount;
const SATOSHIS = 100000000;
return (btcAmount * SATOSHIS) / exchange.rate;
};
const timeline = charge.timeline;
const lastTimeline = timeline[timeline.length - 1];
const index = newCoinSwaps.findIndex((x) => x.chargeCode === charge.code);
if (index > -1) {
newCoinSwaps[index] = {
chargeCode: charge.code,
coins: Object.keys(charge.addresses),
sendAddresses: charge.addresses,
sendAmounts: charge.pricing,
lbcAmount: calculateLbcAmount(charge.pricing, exchange, newCoinSwaps[index].lbcAmount),
status: {
status: lastTimeline.status,
receiptCurrency: lastTimeline.payment.value.currency,
receiptTxid: lastTimeline.payment.transaction_id,
lbcTxid: exchange.lbc_txid || '',
},
};
} else {
// If a pending swap is removed, the websocket will return an update
// when it expires, for example, causing the entry to re-appear. This
// might be a good thing (e.g. to get back accidental removals), but it
// actually causes synchronization confusion across multiple instances.
const IGNORED_DELETED_SWAPS = true;
if (!IGNORED_DELETED_SWAPS) {
newCoinSwaps.push({
chargeCode: charge.code,
coins: Object.keys(charge.addresses),
sendAddresses: charge.addresses,
sendAmounts: charge.pricing,
lbcAmount: calculateLbcAmount(charge.pricing, exchange, 0),
status: {
status: lastTimeline.status,
receiptCurrency: lastTimeline.payment.value.currency,
receiptTxid: lastTimeline.payment.transaction_id,
lbcTxid: exchange.lbc_txid || '',
},
});
}
}
return {
...state,
coinSwaps: newCoinSwaps,
};
},
[ACTIONS.SYNC_STATE_POPULATE]: (state: CoinSwapState, action: { data: { coinSwapCodes: ?Array<string> } }) => {
const { coinSwapCodes } = action.data;
const newCoinSwaps = [];
if (coinSwapCodes) {
coinSwapCodes.forEach((chargeCode) => {
if (chargeCode && typeof chargeCode === 'string') {
const existingSwap = state.coinSwaps.find((x) => x.chargeCode === chargeCode);
if (existingSwap) {
newCoinSwaps.push({ ...existingSwap });
} else {
newCoinSwaps.push({
// Just restore the 'chargeCode', and query the other data
// via 'btc/status' later.
chargeCode: chargeCode,
coins: [],
sendAddresses: {},
sendAmounts: {},
lbcAmount: 0,
});
}
}
});
}
return {
...state,
coinSwaps: getBottomEntries(newCoinSwaps, SWAP_HISTORY_LENGTH_LIMIT),
};
},
},
defaultState
);

View file

@ -4,7 +4,6 @@ import { selectSupportsByOutpoint } from 'redux/selectors/wallet';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import { createCachedSelector } from 're-reselect'; import { createCachedSelector } from 're-reselect';
import { isClaimNsfw, filterClaims } from 'util/claim'; import { isClaimNsfw, filterClaims } from 'util/claim';
import { selectBlackListedOutpoints } from 'lbryinc';
import * as CLAIM from 'constants/claim'; import * as CLAIM from 'constants/claim';
type State = { claims: any }; type State = { claims: any };
@ -84,46 +83,6 @@ export const selectClaimIdForUri = (state: State, uri: string) => selectClaimIds
export const selectReflectingById = (state: State) => selectState(state).reflectingById; export const selectReflectingById = (state: State) => selectState(state).reflectingById;
export const makeSelectBlacklistedDueToDMCA = (claimUri: string) =>
createSelector(makeSelectClaimErrorCensor(claimUri), (claimError) => {
if (!claimError) {
return false;
}
return claimError.name === '@LBRY-DMCA';
});
export const makeSelectClaimErrorCensor = (claimUri: string) =>
createSelector(selectState, (state) => state.blacklistedByUri[claimUri]);
export const makeSelectIsBlacklisted = (claimUri: string) =>
createSelector(
makeSelectClaimErrorCensor(claimUri),
selectBlackListedOutpoints,
makeSelectClaimForUri(claimUri),
(errorCensor, legacyBlacklistedList, claim) => {
if (errorCensor) {
return true;
}
// Fallback to legacy just in case.
if (!claim) {
return false;
}
if (!legacyBlacklistedList) {
return false;
}
const signingChannel = claim.signing_channel;
if (!signingChannel) {
return false;
}
const isInLegacyBlacklist = legacyBlacklistedList.some(
(outpoint) =>
(signingChannel && outpoint.txid === signingChannel.txid && outpoint.nout === signingChannel.nout) ||
(outpoint.txid === claim.txid && outpoint.nout === claim.nout)
);
return isInLegacyBlacklist;
}
);
export const makeSelectClaimForClaimId = (claimId: string) => createSelector(selectClaimsById, (byId) => byId[claimId]); export const makeSelectClaimForClaimId = (claimId: string) => createSelector(selectClaimsById, (byId) => byId[claimId]);
export const selectClaimForUri = createCachedSelector( export const selectClaimForUri = createCachedSelector(

View file

@ -0,0 +1,8 @@
// @flow
import { createSelector } from 'reselect';
const selectState = (state) => state.coinSwap || {};
export const selectCoinSwaps = createSelector(selectState, (state: CoinSwapState) => {
return state.coinSwaps;
});

View file

@ -161,24 +161,18 @@ function filterFileInfos(fileInfos, query) {
export const makeSelectSearchDownloadUrlsForPage = (query, page = 1) => export const makeSelectSearchDownloadUrlsForPage = (query, page = 1) =>
createSelector(selectFileInfosDownloaded, (fileInfos) => { createSelector(selectFileInfosDownloaded, (fileInfos) => {
const matchingFileInfos = filterFileInfos(fileInfos, query); const matchingFileInfos = filterFileInfos(fileInfos, query);
if (!matchingFileInfos || !matchingFileInfos.length) {
return [];
}
const start = (Number(page) - 1) * Number(PAGE_SIZE); const start = (Number(page) - 1) * Number(PAGE_SIZE);
const end = Number(page) * Number(PAGE_SIZE); const end = Number(page) * Number(PAGE_SIZE);
// Recently downloaded elements first.
const sortedMatchedFileInfos = matchingFileInfos.sort((a, b) => {
return b.added_on - a.added_on;
});
return sortedMatchedFileInfos.slice(start, end).map((fileInfo) => return matchingFileInfos && matchingFileInfos.length
? matchingFileInfos.slice(start, end).map((fileInfo) =>
buildURI({ buildURI({
streamName: fileInfo.claim_name, streamName: fileInfo.claim_name,
channelName: fileInfo.channel_name, channelName: fileInfo.channel_name,
channelClaimId: fileInfo.channel_claim_id, channelClaimId: fileInfo.channel_claim_id,
}) })
); )
: [];
}); });
export const makeSelectSearchDownloadUrlsCount = (query) => export const makeSelectSearchDownloadUrlsCount = (query) =>

View file

@ -9,7 +9,7 @@ import {
selectClaimForUri, selectClaimForUri,
} from 'redux/selectors/claims'; } from 'redux/selectors/claims';
import { swapKeyAndValue } from 'util/swap-json'; import { swapKeyAndValue } from 'util/swap-json';
import { getChannelFromClaim, isChannelClaim } from 'util/claim'; import { getChannelFromClaim } from 'util/claim';
// Returns the entire subscriptions state // Returns the entire subscriptions state
const selectState = (state) => state.subscriptions || {}; const selectState = (state) => state.subscriptions || {};
@ -114,18 +114,12 @@ export const makeSelectChannelInSubscriptions = (uri) =>
createSelector(selectSubscriptions, (subscriptions) => subscriptions.some((sub) => sub.uri === uri)); createSelector(selectSubscriptions, (subscriptions) => subscriptions.some((sub) => sub.uri === uri));
export const selectIsSubscribedForUri = createCachedSelector( export const selectIsSubscribedForUri = createCachedSelector(
(state, uri) => uri,
selectClaimForUri, selectClaimForUri,
selectSubscriptions, selectSubscriptions,
(uri, claim, subscriptions) => { (claim, subscriptions) => {
const channelClaim = getChannelFromClaim(claim); const channelClaim = getChannelFromClaim(claim);
if (channelClaim) { if (channelClaim) {
const permanentUrl = channelClaim.permanent_url; const uri = channelClaim.permanent_url;
return subscriptions.some((sub) => isURIEqual(sub.uri, permanentUrl));
}
// If it failed, it could be an abandoned channel. Try parseURI:
if (isChannelClaim(claim, uri)) {
return subscriptions.some((sub) => isURIEqual(sub.uri, uri)); return subscriptions.some((sub) => isURIEqual(sub.uri, uri));
} }
return false; return false;

View file

@ -167,7 +167,7 @@ a.button--alt {
.vjs-button--theater-mode.vjs-button { .vjs-button--theater-mode.vjs-button {
display: none; display: none;
@media not all and (max-width: $breakpoint-medium) { @media (min-width: $breakpoint-medium) {
display: block; display: block;
order: 1; order: 1;
background-repeat: no-repeat; background-repeat: no-repeat;

View file

@ -41,7 +41,7 @@
margin: 0px var(--spacing-xxs); margin: 0px var(--spacing-xxs);
} }
.button + .comment-create { .button + .commentCreate {
margin-top: var(--spacing-xxs); margin-top: var(--spacing-xxs);
} }
} }
@ -615,7 +615,7 @@
@media (max-width: $breakpoint-small) { @media (max-width: $breakpoint-small) {
font-size: var(--font-small); font-size: var(--font-small);
border-bottom: none;
.button--link { .button--link {
font-size: var(--font-xsmall); font-size: var(--font-xsmall);
margin: 0px; margin: 0px;
@ -659,7 +659,7 @@
} }
} }
.button__selected-server { .button_selectedServer {
display: inline; display: inline;
float: right; float: right;
select { select {

View file

@ -621,7 +621,7 @@
@media (max-width: $breakpoint-xsmall) { @media (max-width: $breakpoint-xsmall) {
-webkit-line-clamp: 2 !important; -webkit-line-clamp: 2 !important;
} }
@media not all and (max-width: $breakpoint-medium) { @media (min-width: $breakpoint-medium) {
-webkit-line-clamp: 1 !important; -webkit-line-clamp: 1 !important;
} }
} }

View file

@ -7,7 +7,7 @@ $thumbnailWidthSmall: 1rem;
position: relative; position: relative;
} }
.comment-create { .commentCreate {
font-size: var(--font-small); font-size: var(--font-small);
position: relative; position: relative;
@ -135,12 +135,12 @@ $thumbnailWidthSmall: 1rem;
} }
} }
.comment-create--reply { .commentCreate--reply {
margin-top: var(--spacing-m); margin-top: var(--spacing-m);
position: relative; position: relative;
} }
.comment-create--nestedReply { .commentCreate--nestedReply {
margin-top: var(--spacing-s); margin-top: var(--spacing-s);
margin-left: calc((#{$thumbnailWidthSmall} + var(--spacing-xs)) * 2 + var(--spacing-m) + 4px); margin-left: calc((#{$thumbnailWidthSmall} + var(--spacing-xs)) * 2 + var(--spacing-m) + 4px);
@ -149,40 +149,27 @@ $thumbnailWidthSmall: 1rem;
} }
} }
.comment-create--bottom { .commentCreate--bottom {
padding-bottom: 0; padding-bottom: 0;
} }
.comment-create__header {
display: flex;
justify-content: space-between;
align-items: flex-end;
.comment-create__header-button {
display: flex;
justify-content: space-between;
}
.button--alt {
padding: var(--spacing-xs);
height: unset;
margin-bottom: var(--spacing-xxs);
background: unset;
}
}
.comment-create__label-wrapper { .comment-create__label-wrapper {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
justify-content: flex-start; justify-content: flex-start;
align-items: baseline; align-items: baseline;
flex-wrap: wrap; flex-wrap: wrap;
max-width: 50%; width: 100%;
.comment-create__label { .comment-create__label {
white-space: nowrap; white-space: nowrap;
margin-right: var(--spacing-xs); margin-right: var(--spacing-xs);
} }
fieldset-section {
max-width: 10rem;
}
@media (max-width: $breakpoint-small) { @media (max-width: $breakpoint-small) {
fieldset-section { fieldset-section {
font-size: var(--font-xxsmall); font-size: var(--font-xxsmall);
@ -192,14 +179,14 @@ $thumbnailWidthSmall: 1rem;
font-size: var(--font-xxsmall); font-size: var(--font-xxsmall);
} }
//select { select {
// height: 1rem; height: 1rem;
// margin: var(--spacing-xxs) 0px; margin: var(--spacing-xxs) 0px;
//} }
} }
} }
.comment-create__supportCommentPreview { .commentCreate__supportCommentPreview {
display: flex; display: flex;
align-items: center; align-items: center;
border: 1px solid var(--color-border); border: 1px solid var(--color-border);
@ -207,7 +194,7 @@ $thumbnailWidthSmall: 1rem;
padding: var(--spacing-s); padding: var(--spacing-s);
margin: var(--spacing-s) 0; margin: var(--spacing-s) 0;
.comment-create__supportCommentPreviewAmount { .commentCreate__supportCommentPreviewAmount {
margin-right: var(--spacing-m); margin-right: var(--spacing-m);
font-size: var(--font-large); font-size: var(--font-large);
} }
@ -236,8 +223,8 @@ $thumbnailWidthSmall: 1rem;
} }
} }
.comment-create__stickerPreview { .commentCreate__stickerPreview {
@extend .comment-create; @extend .commentCreate;
display: flex; display: flex;
background-color: var(--color-header-background); background-color: var(--color-header-background);
border-radius: var(--border-radius); border-radius: var(--border-radius);
@ -247,12 +234,12 @@ $thumbnailWidthSmall: 1rem;
width: 100%; width: 100%;
height: 10rem; height: 10rem;
.comment-create__stickerPreviewInfo { .commentCreate__stickerPreviewInfo {
display: flex; display: flex;
align-items: flex-start; align-items: flex-start;
} }
.comment-create__stickerPreviewImage { .commentCreate__stickerPreviewImage {
width: 100%; width: 100%;
height: 100%; height: 100%;
margin-left: var(--spacing-m); margin-left: var(--spacing-m);

View file

@ -45,12 +45,6 @@ $thumbnailWidthSmall: 1rem;
} }
} }
.comment__actions-row {
display: flex;
flex-direction: row;
justify-content: flex-end;
}
.comment { .comment {
width: 100%; width: 100%;
display: flex; display: flex;
@ -507,7 +501,7 @@ $thumbnailWidthSmall: 1rem;
min-width: 100%; min-width: 100%;
max-width: 100%; max-width: 100%;
@media not all and (max-width: $breakpoint-medium) { @media (min-width: $breakpoint-medium) {
min-width: 40%; min-width: 40%;
max-width: 40%; max-width: 40%;
} }
@ -549,7 +543,7 @@ $thumbnailWidthSmall: 1rem;
} }
} }
@media not all and (max-width: $breakpoint-medium) { @media (min-width: $breakpoint-medium) {
margin: 0 var(--spacing-xs); margin: 0 var(--spacing-xs);
} }
@ -564,7 +558,7 @@ $thumbnailWidthSmall: 1rem;
padding-left: var(--spacing-m); padding-left: var(--spacing-m);
border-left: 4px solid var(--color-border); border-left: 4px solid var(--color-border);
@media not all and (max-width: $breakpoint-medium) { @media (min-width: $breakpoint-medium) {
margin-top: 0; margin-top: 0;
margin-left: var(--spacing-s); margin-left: var(--spacing-s);
} }

View file

@ -3,10 +3,6 @@
position: absolute; position: absolute;
top: var(--spacing-s); top: var(--spacing-s);
border-radius: var(--border-radius); border-radius: var(--border-radius);
@media (max-width: $breakpoint-small) {
border-radius: 0;
}
} }
.content__viewer--disable-click { .content__viewer--disable-click {
@ -179,10 +175,6 @@
width: 100%; width: 100%;
height: 100%; height: 100%;
} }
@media (max-width: $breakpoint-small) {
border-radius: 0;
}
} }
.content__cover--text { .content__cover--text {

View file

@ -29,12 +29,7 @@ select,
background-color: var(--color-secondary); background-color: var(--color-secondary);
} }
} }
textarea {
height: var(--height-input);
border-radius: var(--border-radius);
color: var(--color-input);
background-color: var(--color-input-bg);
}
@media (min-width: $breakpoint-small) { @media (min-width: $breakpoint-small) {
textarea { textarea {
height: var(--height-input); height: var(--height-input);
@ -537,7 +532,6 @@ fieldset-group {
} }
.form-field__quick-action { .form-field__quick-action {
text-align: right;
font-size: var(--font-xsmall); font-size: var(--font-xsmall);
} }

View file

@ -73,7 +73,7 @@ body {
} }
.sidebar--pusher--open { .sidebar--pusher--open {
@media not all and (max-width: $breakpoint-medium) { @media (min-width: $breakpoint-medium) {
transform: scaleX(0.9) translateX(calc(5.4 * var(--spacing-l))) scaleY(0.9); transform: scaleX(0.9) translateX(calc(5.4 * var(--spacing-l))) scaleY(0.9);
} }
} }
@ -155,7 +155,7 @@ body {
} }
} }
@media not all and (max-width: $breakpoint-medium) { @media (min-width: $breakpoint-medium) {
flex-direction: row; flex-direction: row;
} }
@media (max-width: $breakpoint-medium) { @media (max-width: $breakpoint-medium) {
@ -461,6 +461,7 @@ body {
margin-left: 0; margin-left: 0;
margin-right: 0; margin-right: 0;
margin-top: 0; margin-top: 0;
width: 100vw;
max-width: none; max-width: none;
> :first-child { > :first-child {
@ -803,7 +804,7 @@ body {
} }
} }
@media not all and (max-width: $breakpoint-medium) { @media (min-width: $breakpoint-medium) {
> :first-child { > :first-child {
width: calc(30% - var(--spacing-l)); width: calc(30% - var(--spacing-l));
max-width: 25rem; max-width: 25rem;

View file

@ -114,15 +114,10 @@
padding-right: 0; padding-right: 0;
padding-left: 0; padding-left: 0;
border: transparent; border: transparent;
margin-right: var(--spacing-xxs);
&:hover { &:hover {
color: var(--color-header-link); color: var(--color-header-link-active);
background-color: var(--color-editor-button-hover-bg); }
}
}
button.active {
background: var(--color-editor-button-active-bg);
} }
} }
} }

View file

@ -70,7 +70,7 @@
color: var(--color-brand-contrast) !important; color: var(--color-brand-contrast) !important;
} }
@media not all and (max-width: $breakpoint-medium) { @media (min-width: $breakpoint-medium) {
overflow-y: hidden; overflow-y: hidden;
justify-content: space-between; justify-content: space-between;
@ -235,7 +235,7 @@
@extend .navigation-link--highlighted; @extend .navigation-link--highlighted;
} }
@media not all and (max-width: $breakpoint-medium) { @media (min-width: $breakpoint-medium) {
text-align: left; text-align: left;
margin-bottom: 0; margin-bottom: 0;

View file

@ -32,17 +32,13 @@ $contentMaxWidth: 60rem;
} }
} }
.comment-create { .commentCreate {
border-top: 1px solid var(--color-border); border-top: 1px solid var(--color-border);
padding-top: var(--spacing-s); padding-top: var(--spacing-s);
.comment-create__label { .commentCreate__label {
color: var(--color-text); color: var(--color-text);
} }
.comment-create__header {
display: grid;
grid-template-columns: 3fr 1fr;
}
textarea, textarea,
select, select,
.button:not(.button--file-action) { .button:not(.button--file-action) {
@ -85,7 +81,7 @@ $contentMaxWidth: 60rem;
} }
} }
.comment-create, .commentCreate,
.comment__content { .comment__content {
margin: var(--spacing-m); margin: var(--spacing-m);
margin-bottom: 0; margin-bottom: 0;

Some files were not shown because too many files have changed in this diff Show more