Compare commits
32 commits
Author | SHA1 | Date | |
---|---|---|---|
d14c9141db | |||
06c350c4db | |||
c3a9d9d002 | |||
aeada6dc74 | |||
6ba985fd28 | |||
2a0bc85738 | |||
523ea284a2 | |||
a66d7534c2 | |||
89ec07622f | |||
7e6ad31392 | |||
4ab23f03fc | |||
29cea5cc07 | |||
8dd7150d67 | |||
f1b1523017 | |||
88ac250fee | |||
0a5e9e87ed | |||
20413d79b6 | |||
ff9011e6ac | |||
802139d0a4 | |||
68d307fa50 | |||
d3900e39b6 | |||
35769dede6 | |||
ae1e20d131 | |||
051af8b6ad | |||
5d77b115f9 | |||
7dbeeac112 | |||
de062c4aee | |||
18a3336714 | |||
ebf35a1df8 | |||
28e168d5e5 | |||
7ad66b99e7 | |||
7eb7c1a5ff |
19
.github/workflows/deploy.yml
vendored
19
.github/workflows/deploy.yml
vendored
|
@ -38,7 +38,22 @@ jobs:
|
||||||
- uses: maxim-lobanov/setup-xcode@v1
|
- uses: maxim-lobanov/setup-xcode@v1
|
||||||
if: startsWith(runner.os, 'mac')
|
if: startsWith(runner.os, 'mac')
|
||||||
with:
|
with:
|
||||||
xcode-version: '12.4.0'
|
xcode-version: '13.1.0'
|
||||||
|
# This is gonna be hacky.
|
||||||
|
# Github made us upgrade xcode, which would force an upgrade of electron-builder to fix mac.
|
||||||
|
# But there were bugs with copyfiles / extraFiles that kept seeing duplicates erroring on ln.
|
||||||
|
# A flag USE_HARD_LINKS=false in electron-builder.json was suggested in comments, but that broke windows builds.
|
||||||
|
# So for now we'll install python2 on mac and make sure it can find it.
|
||||||
|
# Remove this after successfully upgrading electron-builder.
|
||||||
|
# HACK part 1
|
||||||
|
- uses: Homebrew/actions/setup-homebrew@master
|
||||||
|
if: startsWith(runner.os, 'mac')
|
||||||
|
# HACK part 2
|
||||||
|
- name: Install Python2
|
||||||
|
if: startsWith(runner.os, 'mac')
|
||||||
|
run: |
|
||||||
|
/bin/bash -c "$(curl -fsSL https://github.com/alfredapp/dependency-scripts/raw/main/scripts/install-python2.sh)"
|
||||||
|
echo "PYTHON_PATH=/usr/local/bin/python" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Download blockchain headers
|
- name: Download blockchain headers
|
||||||
run: |
|
run: |
|
||||||
|
@ -58,7 +73,7 @@ jobs:
|
||||||
WIN_CSC_KEY_PASSWORD: ${{ secrets.WIN_CSC_KEY_PASSWORD }}
|
WIN_CSC_KEY_PASSWORD: ${{ secrets.WIN_CSC_KEY_PASSWORD }}
|
||||||
CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
|
CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
|
||||||
|
|
||||||
WIN_CSC_LINK: https://raw.githubusercontent.com/lbryio/lbry-desktop/master/build/cert-2021-2022.pfx
|
WIN_CSC_LINK: https://raw.githubusercontent.com/lbryio/lbry-desktop/master/build/cert2023.pfx
|
||||||
CSC_LINK: https://s3.amazonaws.com/files.lbry.io/cert/osx-csc-2021-2022.p12
|
CSC_LINK: https://s3.amazonaws.com/files.lbry.io/cert/osx-csc-2021-2022.p12
|
||||||
|
|
||||||
# UI
|
# UI
|
||||||
|
|
0
.yarn/versions/5bc94294.yml
vendored
Normal file
0
.yarn/versions/5bc94294.yml
vendored
Normal file
0
.yarn/versions/6b35c994.yml
vendored
Normal file
0
.yarn/versions/6b35c994.yml
vendored
Normal file
0
.yarn/versions/6be5ab70.yml
vendored
Normal file
0
.yarn/versions/6be5ab70.yml
vendored
Normal file
0
.yarn/versions/d1a18cef.yml
vendored
Normal file
0
.yarn/versions/d1a18cef.yml
vendored
Normal file
0
.yarn/versions/ec3a9ddf.yml
vendored
Normal file
0
.yarn/versions/ec3a9ddf.yml
vendored
Normal file
32
CHANGELOG.md
32
CHANGELOG.md
|
@ -2,6 +2,37 @@
|
||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||||
|
|
||||||
|
## [0.53.9] - [2023-2-8]
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Updated lbrynet to [0.113.0](https://github.com/lbryio/lbry-sdk/releases/tag/v0.113.0)
|
||||||
|
|
||||||
|
## [0.53.8] - [2022-11-17]
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Selecting a large file in publish no longer crashes ([#7736](https://github.com/lbryio/lbry-desktop/pull/7736))
|
||||||
|
- Unfollowing unpublished channels ([#7737](https://github.com/lbryio/lbry-desktop/pull/7737))
|
||||||
|
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Updated xcode to 13.1 and hacked a fix for release ([#7736](https://github.com/lbryio/lbry-desktop/pull/7736))
|
||||||
|
|
||||||
|
## [0.53.7] - [2022-11-10]
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- 'Collections' to txo filter _community pr!_ ([#7711](https://github.com/lbryio/lbry-desktop/pull/7711))
|
||||||
|
- Swap comment servers _community pr!_ ([#7670](https://github.com/lbryio/lbry-desktop/pull/7670))
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Thumbnails no longer disable publish ([#7714](https://github.com/lbryio/lbry-desktop/pull/7714))
|
||||||
|
- Publishing posts were empty ([#7715](https://github.com/lbryio/lbry-desktop/pull/7715))
|
||||||
|
- Minor layout fixes _community pr!_ ([#7709](https://github.com/lbryio/lbry-desktop/pull/7709))
|
||||||
|
- Comment section buttons layout ([#7716](https://github.com/lbryio/lbry-desktop/pull/7716))
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Removed watchman and its errors ([#7710](https://github.com/lbryio/lbry-desktop/pull/7710))
|
||||||
|
- Updated lbrynet to [0.112.0](https://github.com/lbryio/lbry-sdk/releases/tag/v0.112.0)
|
||||||
|
|
||||||
## [0.53.6] - [2022-10-21]
|
## [0.53.6] - [2022-10-21]
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
@ -9,7 +40,6 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||||
- Show downloads newest first ([#7684](https://github.com/lbryio/lbry-desktop/pull/7684))
|
- 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))
|
- 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 bug with csv exports ([#7697](https://github.com/lbryio/lbry-desktop/pull/7697))
|
||||||
- Fixed small screen viewer position ([#7677](https://github.com/lbryio/lbry-desktop/pull/7677))
|
|
||||||
- Fixed various upload bugs including transcoding ([#7688](https://github.com/lbryio/lbry-desktop/pull/7688))
|
- 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))
|
- Fallback for files with no extension ([#7704](https://github.com/lbryio/lbry-desktop/pull/7704))
|
||||||
|
|
||||||
|
|
|
@ -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-app-bin/) | [Nixpkgs](https://search.nixos.org/packages?channel=unstable&show=lbry&query=lbry) | [Build Guide](https://lbry.tv/@LBRYarm:5) |
|
| Latest Release | [FlatHub Page](https://flathub.org/apps/details/io.lbry.lbry-app) | [AUR Package](https://aur.archlinux.org/packages/lbry-desktop-bin/) | [Nixpkgs](https://search.nixos.org/packages?channel=unstable&show=lbry&query=lbry) | [Build Guide](https://lbry.tv/@LBRYarm:5) |
|
||||||
| Maintainers | [@kcSeb](https://keybase.io/kcseb) | [@kcSeb](https://keybase.io/kcseb) | [@Enderger](https://github.com/enderger) | [@Madiator2011](https://github.com/kodxana) |
|
| Maintainers | N/A | [@RubenKelevra](https://github.com/RubenKelevra) | [@Enderger](https://github.com/enderger) | [@Madiator2011](https://github.com/kodxana) |
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
|
|
BIN
build/cert2023.pfx
Normal file
BIN
build/cert2023.pfx
Normal file
Binary file not shown.
|
@ -24,6 +24,8 @@ 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');
|
||||||
|
@ -353,6 +355,43 @@ ipcMain.handle('get-file-from-path', (event, path, readContents = true) => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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();
|
||||||
|
|
10
flow-typed/file-data.js
vendored
Normal file
10
flow-typed/file-data.js
vendored
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
// @flow
|
||||||
|
|
||||||
|
declare type FileData = {
|
||||||
|
file?: Blob,
|
||||||
|
path: string,
|
||||||
|
duration?: number,
|
||||||
|
size?: number,
|
||||||
|
mimeType: string,
|
||||||
|
error?: string,
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "lbry",
|
"name": "lbry",
|
||||||
"version": "0.53.6",
|
"version": "0.53.9",
|
||||||
"description": "A browser for the LBRY network, a digital marketplace controlled by its users.",
|
"description": "A browser for the LBRY network, a digital marketplace controlled by its users.",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"lbry"
|
"lbry"
|
||||||
|
@ -51,6 +51,7 @@
|
||||||
"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",
|
||||||
"match-sorter": "^6.3.0",
|
"match-sorter": "^6.3.0",
|
||||||
"mime": "^3.0.0",
|
"mime": "^3.0.0",
|
||||||
|
@ -216,7 +217,7 @@
|
||||||
"yarn": "^1.3"
|
"yarn": "^1.3"
|
||||||
},
|
},
|
||||||
"lbrySettings": {
|
"lbrySettings": {
|
||||||
"lbrynetDaemonVersion": "0.111.0",
|
"lbrynetDaemonVersion": "0.113.0",
|
||||||
"lbrynetDaemonUrlTemplate": "https://github.com/lbryio/lbry/releases/download/vDAEMONVER/lbrynet-OSNAME.zip",
|
"lbrynetDaemonUrlTemplate": "https://github.com/lbryio/lbry/releases/download/vDAEMONVER/lbrynet-OSNAME.zip",
|
||||||
"lbrynetDaemonDir": "static/daemon",
|
"lbrynetDaemonDir": "static/daemon",
|
||||||
"lbrynetDaemonFileName": "lbrynet"
|
"lbrynetDaemonFileName": "lbrynet"
|
||||||
|
|
|
@ -2318,5 +2318,9 @@
|
||||||
"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 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.",
|
"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--"
|
||||||
}
|
}
|
||||||
|
|
164
ui/analytics.js
164
ui/analytics.js
|
@ -1,4 +1,11 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
/*
|
||||||
|
Removed Watchman (internal view tracking) code.
|
||||||
|
This file may eventually implement cantina
|
||||||
|
Refer to 0cc0e213a5c5bf9e2a76316df5d9da4b250a13c3 for initial integration commit
|
||||||
|
refer to ___ for removal commit.
|
||||||
|
*/
|
||||||
|
|
||||||
import { Lbryio } from 'lbryinc';
|
import { Lbryio } from 'lbryinc';
|
||||||
import * as Sentry from '@sentry/browser';
|
import * as Sentry from '@sentry/browser';
|
||||||
import MatomoTracker from '@datapunt/matomo-tracker-js';
|
import MatomoTracker from '@datapunt/matomo-tracker-js';
|
||||||
|
@ -14,9 +21,6 @@ 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',
|
||||||
|
@ -68,114 +72,10 @@ type LogPublishParams = {
|
||||||
let internalAnalyticsEnabled: boolean = false;
|
let internalAnalyticsEnabled: boolean = false;
|
||||||
if (window.localStorage.getItem(SHARE_INTERNAL) === 'true') internalAnalyticsEnabled = true;
|
if (window.localStorage.getItem(SHARE_INTERNAL) === 'true') internalAnalyticsEnabled = true;
|
||||||
|
|
||||||
/**
|
|
||||||
* Determine the mobile device type viewing the data
|
|
||||||
* This function returns one of 'and' (Android), 'ios', or 'web'.
|
|
||||||
*
|
|
||||||
* @returns {String}
|
|
||||||
*/
|
|
||||||
function getDeviceType() {
|
|
||||||
return 'dsk';
|
|
||||||
}
|
|
||||||
// variables initialized for watchman
|
|
||||||
let amountOfBufferEvents = 0;
|
|
||||||
let amountOfBufferTimeInMS = 0;
|
|
||||||
let videoType, userId, claimUrl, playerPoweredBy, videoPlayer, bitrateAsBitsPerSecond;
|
|
||||||
let lastSentTime;
|
|
||||||
|
|
||||||
// calculate data for backend, send them, and reset buffer data for next interval
|
|
||||||
async function sendAndResetWatchmanData() {
|
|
||||||
if (!userId) {
|
|
||||||
return 'Can only be used with a user id';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!videoPlayer) {
|
|
||||||
return 'Video player not initialized';
|
|
||||||
}
|
|
||||||
|
|
||||||
let timeSinceLastIntervalSend = new Date() - lastSentTime;
|
|
||||||
lastSentTime = new Date();
|
|
||||||
|
|
||||||
let protocol;
|
|
||||||
if (videoType === 'application/x-mpegURL') {
|
|
||||||
protocol = 'hls';
|
|
||||||
// get bandwidth if it exists from the texttrack (so it's accurate if user changes quality)
|
|
||||||
// $FlowFixMe
|
|
||||||
bitrateAsBitsPerSecond = videoPlayer.textTracks?.().tracks_[0]?.activeCues[0]?.value?.bandwidth;
|
|
||||||
} else {
|
|
||||||
protocol = 'stb';
|
|
||||||
}
|
|
||||||
|
|
||||||
// current position in video in MS
|
|
||||||
const positionInVideo = Math.round(videoPlayer.currentTime()) * 1000;
|
|
||||||
|
|
||||||
// get the duration marking the time in the video for relative position calculation
|
|
||||||
const totalDurationInSeconds = Math.round(videoPlayer.duration());
|
|
||||||
|
|
||||||
// build object for watchman backend
|
|
||||||
const objectToSend = {
|
|
||||||
rebuf_count: amountOfBufferEvents,
|
|
||||||
rebuf_duration: amountOfBufferTimeInMS,
|
|
||||||
url: claimUrl.replace('lbry://', ''),
|
|
||||||
device: getDeviceType(),
|
|
||||||
duration: timeSinceLastIntervalSend,
|
|
||||||
protocol,
|
|
||||||
player: playerPoweredBy,
|
|
||||||
user_id: userId.toString(),
|
|
||||||
position: Math.round(positionInVideo),
|
|
||||||
rel_position: Math.round((positionInVideo / (totalDurationInSeconds * 1000)) * 100),
|
|
||||||
bitrate: bitrateAsBitsPerSecond,
|
|
||||||
bandwidth: undefined,
|
|
||||||
// ...(userDownloadBandwidthInBitsPerSecond && {bandwidth: userDownloadBandwidthInBitsPerSecond}), // add bandwidth if populated
|
|
||||||
};
|
|
||||||
|
|
||||||
// post to watchman
|
|
||||||
await sendWatchmanData(objectToSend);
|
|
||||||
|
|
||||||
// reset buffer data
|
|
||||||
amountOfBufferEvents = 0;
|
|
||||||
amountOfBufferTimeInMS = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
let watchmanInterval;
|
|
||||||
// clear watchman interval and mark it as null (when video paused)
|
|
||||||
function stopWatchmanInterval() {
|
|
||||||
clearInterval(watchmanInterval);
|
|
||||||
watchmanInterval = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// creates the setInterval that will run send to watchman on recurring basis
|
|
||||||
function startWatchmanIntervalIfNotRunning() {
|
|
||||||
if (!watchmanInterval) {
|
|
||||||
// instantiate the first time to calculate duration from
|
|
||||||
lastSentTime = new Date();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// post data to the backend
|
|
||||||
async function sendWatchmanData(body) {
|
|
||||||
try {
|
|
||||||
const response = await fetch(WATCHMAN_BACKEND_ENDPOINT, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
Accept: 'application/json',
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify(body),
|
|
||||||
});
|
|
||||||
|
|
||||||
return response;
|
|
||||||
} catch (err) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
const analytics: Analytics = {
|
const analytics: Analytics = {
|
||||||
// receive buffer events from tracking plugin and save buffer amounts and times for backend call
|
// receive buffer events from tracking plugin and save buffer amounts and times for backend call
|
||||||
videoBufferEvent: async (claim, data) => {
|
videoBufferEvent: async (claim, data) => {
|
||||||
amountOfBufferEvents = amountOfBufferEvents + 1;
|
// stub
|
||||||
amountOfBufferTimeInMS = amountOfBufferTimeInMS + data.bufferDuration;
|
|
||||||
},
|
|
||||||
onDispose: () => {
|
|
||||||
stopWatchmanInterval();
|
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* Is told whether video is being started or paused, and adjusts interval accordingly
|
* Is told whether video is being started or paused, and adjusts interval accordingly
|
||||||
|
@ -183,40 +83,9 @@ const analytics: Analytics = {
|
||||||
* @param {object} passedPlayer - VideoJS Player object
|
* @param {object} passedPlayer - VideoJS Player object
|
||||||
*/
|
*/
|
||||||
videoIsPlaying: (isPlaying, passedPlayer) => {
|
videoIsPlaying: (isPlaying, passedPlayer) => {
|
||||||
let playerIsSeeking = false;
|
// stub
|
||||||
// have to use this because videojs pauses/unpauses during seek
|
|
||||||
// sometimes the seeking function isn't populated yet so check for it as well
|
|
||||||
if (passedPlayer && passedPlayer.seeking) {
|
|
||||||
playerIsSeeking = passedPlayer.seeking();
|
|
||||||
}
|
|
||||||
|
|
||||||
// if being paused, and not seeking, send existing data and stop interval
|
|
||||||
if (!isPlaying && !playerIsSeeking) {
|
|
||||||
sendAndResetWatchmanData();
|
|
||||||
stopWatchmanInterval();
|
|
||||||
// if being told to pause, and seeking, send and restart interval
|
|
||||||
} else if (!isPlaying && playerIsSeeking) {
|
|
||||||
sendAndResetWatchmanData();
|
|
||||||
stopWatchmanInterval();
|
|
||||||
startWatchmanIntervalIfNotRunning();
|
|
||||||
// is being told to play, and seeking, don't do anything,
|
|
||||||
// assume it's been started already from pause
|
|
||||||
} else if (isPlaying && playerIsSeeking) {
|
|
||||||
// start but not a seek, assuming a start from paused content
|
|
||||||
} else if (isPlaying && !playerIsSeeking) {
|
|
||||||
startWatchmanIntervalIfNotRunning();
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
videoStartEvent: (claimId, timeToStartVideo, poweredBy, passedUserId, canonicalUrl, passedPlayer, videoBitrate) => {
|
videoStartEvent: (claimId, timeToStartVideo, poweredBy, passedUserId, canonicalUrl, passedPlayer, videoBitrate) => {
|
||||||
// populate values for watchman when video starts
|
|
||||||
userId = passedUserId;
|
|
||||||
claimUrl = canonicalUrl;
|
|
||||||
playerPoweredBy = poweredBy;
|
|
||||||
|
|
||||||
videoType = passedPlayer.currentSource().type;
|
|
||||||
videoPlayer = passedPlayer;
|
|
||||||
bitrateAsBitsPerSecond = videoBitrate;
|
|
||||||
|
|
||||||
// sendPromMetric('time_to_start', duration);
|
// sendPromMetric('time_to_start', duration);
|
||||||
sendMatomoEvent('Media', 'TimeToStart', claimId, timeToStartVideo);
|
sendMatomoEvent('Media', 'TimeToStart', claimId, timeToStartVideo);
|
||||||
},
|
},
|
||||||
|
@ -382,24 +251,9 @@ function sendMatomoEvent(category, action, name, value) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prometheus
|
|
||||||
// function sendPromMetric(name: string, value?: number) {
|
|
||||||
// if (IS_WEB) {
|
|
||||||
// let url = new URL(SDK_API_PATH + '/metric/ui');
|
|
||||||
// const params = { name: name, value: value ? value.toString() : '' };
|
|
||||||
// url.search = new URLSearchParams(params).toString();
|
|
||||||
// return fetch(url, { method: 'post' });
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
const MatomoInstance = new MatomoTracker({
|
const MatomoInstance = new MatomoTracker({
|
||||||
urlBase: MATOMO_URL,
|
urlBase: MATOMO_URL,
|
||||||
siteId: MATOMO_ID, // optional, default value: `1`
|
siteId: MATOMO_ID, // optional, default value: `1`
|
||||||
// heartBeat: { // optional, enabled by default
|
|
||||||
// active: true, // optional, default value: true
|
|
||||||
// seconds: 10 // optional, default value: `15
|
|
||||||
// },
|
|
||||||
// linkTracking: false // optional, default value: true
|
|
||||||
});
|
});
|
||||||
|
|
||||||
analytics.pageView(generateInitialUrl(window.location.hash));
|
analytics.pageView(generateInitialUrl(window.location.hash));
|
||||||
|
|
|
@ -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 } from 'component/common/form';
|
import { FormField, FormFieldAreaAdvanced } from 'component/common/form';
|
||||||
import Button from 'component/button';
|
import Button from 'component/button';
|
||||||
import TagsSearch from 'component/tagsSearch';
|
import TagsSearch from 'component/tagsSearch';
|
||||||
import ErrorText from 'component/common/error-text';
|
import ErrorText from 'component/common/error-text';
|
||||||
|
@ -376,7 +376,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}
|
||||||
/>
|
/>
|
||||||
<FormField
|
<FormFieldAreaAdvanced
|
||||||
type="markdown"
|
type="markdown"
|
||||||
name="content_description2"
|
name="content_description2"
|
||||||
label={__('Description')}
|
label={__('Description')}
|
||||||
|
|
|
@ -17,7 +17,7 @@ import { useHistory } from 'react-router-dom';
|
||||||
import { isNameValid, regexInvalidURI } from 'util/lbryURI';
|
import { isNameValid, regexInvalidURI } from 'util/lbryURI';
|
||||||
import * as THUMBNAIL_STATUSES from 'constants/thumbnail_upload_statuses';
|
import * as THUMBNAIL_STATUSES from 'constants/thumbnail_upload_statuses';
|
||||||
import { Tabs, TabList, Tab, TabPanels, TabPanel } from 'component/common/tabs';
|
import { Tabs, TabList, Tab, TabPanels, TabPanel } from 'component/common/tabs';
|
||||||
import { FormField } from 'component/common/form';
|
import { FormField, FormFieldAreaAdvanced } from 'component/common/form';
|
||||||
import { handleBidChange } from 'util/publish';
|
import { handleBidChange } from 'util/publish';
|
||||||
import { FF_MAX_CHARS_IN_DESCRIPTION } from 'constants/form-field';
|
import { FF_MAX_CHARS_IN_DESCRIPTION } from 'constants/form-field';
|
||||||
import { INVALID_NAME_ERROR } from 'constants/claim';
|
import { INVALID_NAME_ERROR } from 'constants/claim';
|
||||||
|
@ -371,7 +371,7 @@ function CollectionForm(props: Props) {
|
||||||
usePublishFormMode
|
usePublishFormMode
|
||||||
/>
|
/>
|
||||||
</fieldset-section>
|
</fieldset-section>
|
||||||
<FormField
|
<FormFieldAreaAdvanced
|
||||||
type="markdown"
|
type="markdown"
|
||||||
name="content_description2"
|
name="content_description2"
|
||||||
label={__('Description')}
|
label={__('Description')}
|
||||||
|
|
|
@ -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 { FormField, Form } from 'component/common/form';
|
import { FormFieldAreaAdvanced, Form } from 'component/common/form';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import usePersistedState from 'effects/use-persisted-state';
|
import usePersistedState from 'effects/use-persisted-state';
|
||||||
import CommentReactions from 'component/commentReactions';
|
import CommentReactions from 'component/commentReactions';
|
||||||
|
@ -319,7 +319,7 @@ function CommentView(props: Props) {
|
||||||
<div>
|
<div>
|
||||||
{isEditing ? (
|
{isEditing ? (
|
||||||
<Form onSubmit={handleSubmit}>
|
<Form onSubmit={handleSubmit}>
|
||||||
<FormField
|
<FormFieldAreaAdvanced
|
||||||
className="comment__edit-input"
|
className="comment__edit-input"
|
||||||
type={advancedEditor ? 'markdown' : 'textarea'}
|
type={advancedEditor ? 'markdown' : 'textarea'}
|
||||||
name="editing_comment"
|
name="editing_comment"
|
||||||
|
|
32
ui/component/commentCreate/comment-create-header.jsx
Normal file
32
ui/component/commentCreate/comment-create-header.jsx
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
// @flow
|
||||||
|
import React from 'react';
|
||||||
|
import SelectChannel from 'component/selectChannel';
|
||||||
|
import Button from 'component/button';
|
||||||
|
import * as ICONS from 'constants/icons';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
isReply: boolean,
|
||||||
|
advancedHandler: () => void,
|
||||||
|
advanced: boolean,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function CommentCreateHeader(props: Props) {
|
||||||
|
const { isReply, advancedHandler, advanced } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="comment-create__header">
|
||||||
|
<div className="comment-create__label-wrapper">
|
||||||
|
<span className="comment-create__label">{(isReply ? __('Replying as') : __('Comment as')) + ' '}</span>
|
||||||
|
<SelectChannel tiny />
|
||||||
|
</div>
|
||||||
|
<div className="form-field__quick-action">
|
||||||
|
<Button
|
||||||
|
button="alt"
|
||||||
|
icon={advanced ? ICONS.SIMPLE_EDITOR : ICONS.ADVANCED_EDITOR}
|
||||||
|
onClick={advancedHandler}
|
||||||
|
aria-label={isReply ? undefined : advanced ? __('Simple Editor') : __('Advanced Editor')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -4,7 +4,7 @@ import 'scss/component/_comment-create.scss';
|
||||||
|
|
||||||
import { buildValidSticker } from 'util/comments';
|
import { buildValidSticker } from 'util/comments';
|
||||||
import { FF_MAX_CHARS_IN_COMMENT } from 'constants/form-field';
|
import { FF_MAX_CHARS_IN_COMMENT } from 'constants/form-field';
|
||||||
import { FormField, Form } from 'component/common/form';
|
import { FormFieldAreaAdvanced, Form } from 'component/common/form';
|
||||||
import { getChannelIdFromClaim } from 'util/claim';
|
import { getChannelIdFromClaim } from 'util/claim';
|
||||||
import { Lbryio } from 'lbryinc';
|
import { Lbryio } from 'lbryinc';
|
||||||
import { useHistory } from 'react-router';
|
import { useHistory } from 'react-router';
|
||||||
|
@ -22,13 +22,12 @@ import I18nMessage from 'component/i18nMessage';
|
||||||
import Icon from 'component/common/icon';
|
import Icon from 'component/common/icon';
|
||||||
import OptimizedImage from 'component/optimizedImage';
|
import OptimizedImage from 'component/optimizedImage';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import SelectChannel from 'component/selectChannel';
|
|
||||||
import StickerSelector from './sticker-selector';
|
import StickerSelector from './sticker-selector';
|
||||||
|
import CommentCreateHeader from './comment-create-header';
|
||||||
import type { ElementRef } from 'react';
|
import type { ElementRef } from 'react';
|
||||||
import UriIndicator from 'component/uriIndicator';
|
import UriIndicator from 'component/uriIndicator';
|
||||||
import usePersistedState from 'effects/use-persisted-state';
|
import usePersistedState from 'effects/use-persisted-state';
|
||||||
import WalletTipAmountSelector from 'component/walletTipAmountSelector';
|
import WalletTipAmountSelector from 'component/walletTipAmountSelector';
|
||||||
|
|
||||||
import { getStripeEnvironment } from 'util/stripe';
|
import { getStripeEnvironment } from 'util/stripe';
|
||||||
const stripeEnvironment = getStripeEnvironment();
|
const stripeEnvironment = getStripeEnvironment();
|
||||||
|
|
||||||
|
@ -364,31 +363,6 @@ export function CommentCreate(props: Props) {
|
||||||
.catch(() => {});
|
.catch(() => {});
|
||||||
}, [canReceiveFiatTip, claim.claim_id, claim.name, claim.signing_channel, stickerSelector]);
|
}, [canReceiveFiatTip, claim.claim_id, claim.name, claim.signing_channel, stickerSelector]);
|
||||||
|
|
||||||
// LIVESTREAM ONLY - REMOVE
|
|
||||||
// Handle keyboard shortcut comment creation
|
|
||||||
// React.useEffect(() => {
|
|
||||||
// function altEnterListener(e: SyntheticKeyboardEvent<*>) {
|
|
||||||
// const inputRef = formFieldRef && formFieldRef.current && formFieldRef.current.input;
|
|
||||||
//
|
|
||||||
// if (inputRef && inputRef.current === document.activeElement) {
|
|
||||||
// // $FlowFixMe
|
|
||||||
// const isTyping = e.target.attributes['term'];
|
|
||||||
//
|
|
||||||
// if (((isLivestream && !isTyping) || e.ctrlKey || e.metaKey) && e.keyCode === KEYCODES.ENTER) {
|
|
||||||
// e.preventDefault();
|
|
||||||
// buttonRef.current.click();
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// window.addEventListener('keydown', altEnterListener);
|
|
||||||
//
|
|
||||||
// // removes the listener so it doesn't cause problems elsewhere in the app
|
|
||||||
// return () => {
|
|
||||||
// window.removeEventListener('keydown', altEnterListener);
|
|
||||||
// };
|
|
||||||
// }, [isLivestream]);
|
|
||||||
|
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
// Render
|
// Render
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
@ -410,7 +384,11 @@ export function CommentCreate(props: Props) {
|
||||||
push(pathPlusRedirect);
|
push(pathPlusRedirect);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<FormField type="textarea" name={'comment_signup_prompt'} placeholder={__('Say something about this...')} />
|
<FormFieldAreaAdvanced
|
||||||
|
type="textarea"
|
||||||
|
name={'comment_signup_prompt'}
|
||||||
|
placeholder={__('Say something about this...')}
|
||||||
|
/>
|
||||||
<div className="section__actions--no-margin">
|
<div className="section__actions--no-margin">
|
||||||
<Button disabled button="primary" label={__('Post --[button to submit something]--')} />
|
<Button disabled button="primary" label={__('Post --[button to submit something]--')} />
|
||||||
</div>
|
</div>
|
||||||
|
@ -421,22 +399,22 @@ export function CommentCreate(props: Props) {
|
||||||
return (
|
return (
|
||||||
<Form
|
<Form
|
||||||
onSubmit={() => {}}
|
onSubmit={() => {}}
|
||||||
className={classnames('commentCreate', {
|
className={classnames('comment-create', {
|
||||||
'commentCreate--reply': isReply,
|
'comment-create--reply': isReply,
|
||||||
'commentCreate--nestedReply': isNested,
|
'comment-create--nestedReply': isNested,
|
||||||
'commentCreate--bottom': bottom,
|
'comment-create--bottom': bottom,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{/* Input Box/Preview Box */}
|
{/* Input Box/Preview Box */}
|
||||||
{stickerSelector ? (
|
{stickerSelector ? (
|
||||||
<StickerSelector onSelect={(sticker) => handleSelectSticker(sticker)} claimIsMine={claimIsMine} />
|
<StickerSelector onSelect={(sticker) => handleSelectSticker(sticker)} claimIsMine={claimIsMine} />
|
||||||
) : isReviewingStickerComment && activeChannelClaim && selectedSticker ? (
|
) : isReviewingStickerComment && activeChannelClaim && selectedSticker ? (
|
||||||
<div className="commentCreate__stickerPreview">
|
<div className="comment-create__stickerPreview">
|
||||||
<div className="commentCreate__stickerPreviewInfo">
|
<div className="comment-create__stickerPreviewInfo">
|
||||||
<ChannelThumbnail xsmall uri={activeChannelClaim.canonical_url} />
|
<ChannelThumbnail xsmall uri={activeChannelClaim.canonical_url} />
|
||||||
<UriIndicator uri={activeChannelClaim.canonical_url} link />
|
<UriIndicator uri={activeChannelClaim.canonical_url} link />
|
||||||
</div>
|
</div>
|
||||||
<div className="commentCreate__stickerPreviewImage">
|
<div className="comment-create__stickerPreviewImage">
|
||||||
<OptimizedImage src={selectedSticker && selectedSticker.url} waitLoad loading="lazy" />
|
<OptimizedImage src={selectedSticker && selectedSticker.url} waitLoad loading="lazy" />
|
||||||
</div>
|
</div>
|
||||||
{/* figure out lbc sticker prices */}
|
{/* figure out lbc sticker prices */}
|
||||||
|
@ -448,15 +426,15 @@ export function CommentCreate(props: Props) {
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : isReviewingSupportComment && activeChannelClaim ? (
|
) : isReviewingSupportComment && activeChannelClaim ? (
|
||||||
<div className="commentCreate__supportCommentPreview">
|
<div className="comment-create__supportCommentPreview">
|
||||||
<CreditAmount
|
<CreditAmount
|
||||||
amount={tipAmount}
|
amount={tipAmount}
|
||||||
className="commentCreate__supportCommentPreviewAmount"
|
className="comment-create__supportCommentPreviewAmount"
|
||||||
isFiat={activeTab === TAB_FIAT}
|
isFiat={activeTab === TAB_FIAT}
|
||||||
size={activeTab === TAB_LBC ? 18 : 2}
|
size={activeTab === TAB_LBC ? 18 : 2}
|
||||||
/>
|
/>
|
||||||
<ChannelThumbnail xsmall uri={activeChannelClaim.canonical_url} />
|
<ChannelThumbnail xsmall uri={activeChannelClaim.canonical_url} />
|
||||||
<div className="commentCreate__supportCommentBody">
|
<div className="comment-create__supportCommentBody">
|
||||||
<UriIndicator uri={activeChannelClaim.canonical_url} link />
|
<UriIndicator uri={activeChannelClaim.canonical_url} link />
|
||||||
<div>{commentValue}</div>
|
<div>{commentValue}</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -471,23 +449,22 @@ export function CommentCreate(props: Props) {
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<FormField
|
<FormFieldAreaAdvanced
|
||||||
autoFocus={isReply}
|
autoFocus={isReply}
|
||||||
charCount={charCount}
|
charCount={charCount}
|
||||||
className={isReply ? 'content_reply' : 'content_comment'}
|
className={isReply ? 'content_reply' : 'content_comment'}
|
||||||
disabled={isFetchingChannels}
|
disabled={isFetchingChannels}
|
||||||
label={
|
header={
|
||||||
<div className="commentCreate__labelWrapper">
|
<CommentCreateHeader
|
||||||
<span className="commentCreate__label">{(isReply ? __('Replying as') : __('Comment as')) + ' '}</span>
|
isReply={isReply}
|
||||||
<SelectChannel tiny />
|
advanced={advancedEditor}
|
||||||
</div>
|
advancedHandler={() => setAdvancedEditor(!advancedEditor)}
|
||||||
|
/>
|
||||||
}
|
}
|
||||||
name={isReply ? 'content_reply' : 'content_description'}
|
name={isReply ? 'content_reply' : 'content_description'}
|
||||||
quickActionLabel={isReply ? undefined : advancedEditor ? __('Simple Editor') : __('Advanced Editor')}
|
|
||||||
ref={formFieldRef}
|
ref={formFieldRef}
|
||||||
onChange={handleCommentChange}
|
onChange={handleCommentChange}
|
||||||
openEmoteMenu={() => setShowEmotes(!showEmotes)}
|
openEmoteMenu={() => setShowEmotes(!showEmotes)}
|
||||||
quickActionHandler={() => setAdvancedEditor(!advancedEditor)}
|
|
||||||
onFocus={onTextareaFocus}
|
onFocus={onTextareaFocus}
|
||||||
onBlur={onTextareaBlur}
|
onBlur={onTextareaBlur}
|
||||||
placeholder={__('Say something about this...')}
|
placeholder={__('Say something about this...')}
|
||||||
|
@ -655,7 +632,7 @@ export function CommentCreate(props: Props) {
|
||||||
{/* Help Text */}
|
{/* Help Text */}
|
||||||
{deletedComment && <div className="error__text">{__('This comment has been deleted.')}</div>}
|
{deletedComment && <div className="error__text">{__('This comment has been deleted.')}</div>}
|
||||||
{!!minAmount && (
|
{!!minAmount && (
|
||||||
<div className="help--notice commentCreate__minAmountNotice">
|
<div className="help--notice comment-create__minAmountNotice">
|
||||||
<I18nMessage tokens={{ lbc: <CreditAmount noFormat amount={minAmount} /> }}>
|
<I18nMessage tokens={{ lbc: <CreditAmount noFormat amount={minAmount} /> }}>
|
||||||
{minTip ? 'Comment min: %lbc%' : minSuper ? 'HyperChat min: %lbc%' : ''}
|
{minTip ? 'Comment min: %lbc%' : minSuper ? 'HyperChat min: %lbc%' : ''}
|
||||||
</I18nMessage>
|
</I18nMessage>
|
||||||
|
|
|
@ -23,6 +23,9 @@ import { doCommentReset, doCommentList, doCommentById, doCommentReactList } from
|
||||||
import { selectActiveChannelClaim } from 'redux/selectors/app';
|
import { selectActiveChannelClaim } from 'redux/selectors/app';
|
||||||
import { getChannelIdFromClaim } from 'util/claim';
|
import { getChannelIdFromClaim } from 'util/claim';
|
||||||
import CommentsList from './view';
|
import CommentsList from './view';
|
||||||
|
import { makeSelectClientSetting } from 'redux/selectors/settings';
|
||||||
|
import * as SETTINGS from 'constants/settings';
|
||||||
|
import { doSetClientSetting } from 'redux/actions/settings';
|
||||||
|
|
||||||
const select = (state, props) => {
|
const select = (state, props) => {
|
||||||
const { uri } = props;
|
const { uri } = props;
|
||||||
|
@ -56,15 +59,19 @@ const select = (state, props) => {
|
||||||
myReactsByCommentId: selectMyReacts(state),
|
myReactsByCommentId: selectMyReacts(state),
|
||||||
othersReactsById: selectOthersReacts(state),
|
othersReactsById: selectOthersReacts(state),
|
||||||
activeChannelId: activeChannelClaim && activeChannelClaim.claim_id,
|
activeChannelId: activeChannelClaim && activeChannelClaim.claim_id,
|
||||||
|
customCommentServers: makeSelectClientSetting(SETTINGS.CUSTOM_COMMENTS_SERVERS)(state),
|
||||||
|
commentServer: makeSelectClientSetting(SETTINGS.CUSTOM_COMMENTS_SERVER_URL)(state),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const perform = {
|
const perform = (dispatch, ownProps) => ({
|
||||||
fetchTopLevelComments: doCommentList,
|
fetchTopLevelComments: (uri, parentId, page, pageSize, sortBy) =>
|
||||||
fetchComment: doCommentById,
|
dispatch(doCommentList(uri, parentId, page, pageSize, sortBy)),
|
||||||
fetchReacts: doCommentReactList,
|
fetchComment: (commentId) => dispatch(doCommentById(commentId)),
|
||||||
resetComments: doCommentReset,
|
fetchReacts: (commentIds) => dispatch(doCommentReactList(commentIds)),
|
||||||
doResolveUris,
|
resetComments: (claimId) => dispatch(doCommentReset(claimId)),
|
||||||
};
|
doResolveUris: (uris, returnCachedClaims) => dispatch(doResolveUris(uris, returnCachedClaims)),
|
||||||
|
setCommentServer: (url) => dispatch(doSetClientSetting(SETTINGS.CUSTOM_COMMENTS_SERVER_URL, url, true)),
|
||||||
|
});
|
||||||
|
|
||||||
export default connect(select, perform)(CommentsList);
|
export default connect(select, perform)(CommentsList);
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
// @flow
|
// @flow
|
||||||
import { COMMENT_PAGE_SIZE_TOP_LEVEL, SORT_BY } from 'constants/comment';
|
import { COMMENT_PAGE_SIZE_TOP_LEVEL, SORT_BY } from 'constants/comment';
|
||||||
import { ENABLE_COMMENT_REACTIONS } from 'config';
|
import { ENABLE_COMMENT_REACTIONS, COMMENT_SERVER_API, COMMENT_SERVER_NAME } from 'config';
|
||||||
import { useIsMobile, useIsMediumScreen } from 'effects/use-screensize';
|
import { useIsMobile, useIsMediumScreen } from 'effects/use-screensize';
|
||||||
import { getCommentsListTitle } from 'util/comments';
|
import { getCommentsListTitle } from 'util/comments';
|
||||||
import * as ICONS from 'constants/icons';
|
import * as ICONS from 'constants/icons';
|
||||||
|
@ -15,6 +15,8 @@ import Empty from 'component/common/empty';
|
||||||
import React, { useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import Spinner from 'component/spinner';
|
import Spinner from 'component/spinner';
|
||||||
import usePersistedState from 'effects/use-persisted-state';
|
import usePersistedState from 'effects/use-persisted-state';
|
||||||
|
import { FormField } from 'component/common/form';
|
||||||
|
import Comments from 'comments';
|
||||||
|
|
||||||
const DEBOUNCE_SCROLL_HANDLER_MS = 200;
|
const DEBOUNCE_SCROLL_HANDLER_MS = 200;
|
||||||
|
|
||||||
|
@ -52,6 +54,9 @@ type Props = {
|
||||||
fetchReacts: (commentIds: Array<string>) => Promise<any>,
|
fetchReacts: (commentIds: Array<string>) => Promise<any>,
|
||||||
resetComments: (claimId: string) => void,
|
resetComments: (claimId: string) => void,
|
||||||
doResolveUris: (uris: Array<string>, returnCachedClaims: boolean) => void,
|
doResolveUris: (uris: Array<string>, returnCachedClaims: boolean) => void,
|
||||||
|
customCommentServers: Array<CommentServerDetails>,
|
||||||
|
setCommentServer: (string) => void,
|
||||||
|
commentServer: string,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function CommentList(props: Props) {
|
export default function CommentList(props: Props) {
|
||||||
|
@ -80,11 +85,17 @@ export default function CommentList(props: Props) {
|
||||||
fetchReacts,
|
fetchReacts,
|
||||||
resetComments,
|
resetComments,
|
||||||
doResolveUris,
|
doResolveUris,
|
||||||
|
customCommentServers,
|
||||||
|
setCommentServer,
|
||||||
|
commentServer,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
const isMediumScreen = useIsMediumScreen();
|
const isMediumScreen = useIsMediumScreen();
|
||||||
|
|
||||||
|
const defaultServer = { name: COMMENT_SERVER_NAME, url: COMMENT_SERVER_API };
|
||||||
|
const allServers = [defaultServer, ...(customCommentServers || [])];
|
||||||
|
|
||||||
const spinnerRef = React.useRef();
|
const spinnerRef = React.useRef();
|
||||||
const DEFAULT_SORT = ENABLE_COMMENT_REACTIONS ? SORT_BY.POPULARITY : SORT_BY.NEWEST;
|
const DEFAULT_SORT = ENABLE_COMMENT_REACTIONS ? SORT_BY.POPULARITY : SORT_BY.NEWEST;
|
||||||
const [sort, setSort] = usePersistedState('comment-sort-by', DEFAULT_SORT);
|
const [sort, setSort] = usePersistedState('comment-sort-by', DEFAULT_SORT);
|
||||||
|
@ -255,7 +266,16 @@ export default function CommentList(props: Props) {
|
||||||
}, [alreadyResolved, doResolveUris, topLevelComments]);
|
}, [alreadyResolved, doResolveUris, topLevelComments]);
|
||||||
|
|
||||||
const commentProps = { isTopLevel: true, threadDepth: 3, uri, claimIsMine, linkedCommentId };
|
const commentProps = { isTopLevel: true, threadDepth: 3, uri, claimIsMine, linkedCommentId };
|
||||||
const actionButtonsProps = { totalComments, sort, changeSort, setPage };
|
const actionButtonsProps = {
|
||||||
|
totalComments,
|
||||||
|
sort,
|
||||||
|
changeSort,
|
||||||
|
setPage,
|
||||||
|
allServers,
|
||||||
|
commentServer,
|
||||||
|
defaultServer,
|
||||||
|
setCommentServer,
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
|
@ -334,17 +354,21 @@ type ActionButtonsProps = {
|
||||||
sort: string,
|
sort: string,
|
||||||
changeSort: (string) => void,
|
changeSort: (string) => void,
|
||||||
setPage: (number) => void,
|
setPage: (number) => void,
|
||||||
|
allServers: Array<CommentServerDetails>,
|
||||||
|
commentServer: string,
|
||||||
|
setCommentServer: (string) => void,
|
||||||
|
defaultServer: CommentServerDetails,
|
||||||
};
|
};
|
||||||
|
|
||||||
const CommentActionButtons = (actionButtonsProps: ActionButtonsProps) => {
|
const CommentActionButtons = (actionButtonsProps: ActionButtonsProps) => {
|
||||||
const { totalComments, sort, changeSort, setPage } = actionButtonsProps;
|
const { totalComments, sort, changeSort, setPage, allServers, commentServer, setCommentServer, defaultServer } =
|
||||||
|
actionButtonsProps;
|
||||||
const sortButtonProps = { activeSort: sort, changeSort };
|
const sortButtonProps = { activeSort: sort, changeSort };
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className={'comment__actions-row'}>
|
||||||
{totalComments > 1 && ENABLE_COMMENT_REACTIONS && (
|
{totalComments > 1 && ENABLE_COMMENT_REACTIONS && (
|
||||||
<span className="comment__sort">
|
<div className="comment__sort-group">
|
||||||
<SortButton {...sortButtonProps} label={__('Best')} icon={ICONS.BEST} sortOption={SORT_BY.POPULARITY} />
|
<SortButton {...sortButtonProps} label={__('Best')} icon={ICONS.BEST} sortOption={SORT_BY.POPULARITY} />
|
||||||
<SortButton
|
<SortButton
|
||||||
{...sortButtonProps}
|
{...sortButtonProps}
|
||||||
|
@ -353,11 +377,39 @@ const CommentActionButtons = (actionButtonsProps: ActionButtonsProps) => {
|
||||||
sortOption={SORT_BY.CONTROVERSY}
|
sortOption={SORT_BY.CONTROVERSY}
|
||||||
/>
|
/>
|
||||||
<SortButton {...sortButtonProps} label={__('New')} icon={ICONS.NEW} sortOption={SORT_BY.NEWEST} />
|
<SortButton {...sortButtonProps} label={__('New')} icon={ICONS.NEW} sortOption={SORT_BY.NEWEST} />
|
||||||
</span>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{allServers.length >= 2 && (
|
||||||
|
<div className="button__selected-server">
|
||||||
|
<FormField
|
||||||
|
type="select-tiny"
|
||||||
|
onChange={function (x) {
|
||||||
|
const selectedServer = x.target.value;
|
||||||
|
setPage(0);
|
||||||
|
setCommentServer(selectedServer);
|
||||||
|
if (selectedServer === defaultServer.url) {
|
||||||
|
Comments.setServerUrl(undefined);
|
||||||
|
} else {
|
||||||
|
Comments.setServerUrl(selectedServer);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
value={commentServer}
|
||||||
|
>
|
||||||
|
{allServers.map(function (server) {
|
||||||
|
return (
|
||||||
|
<option key={server.url} value={server.url}>
|
||||||
|
{server.name}
|
||||||
|
</option>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</FormField>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="button_refresh">
|
||||||
<Button button="alt" icon={ICONS.REFRESH} title={__('Refresh')} onClick={() => setPage(0)} />
|
<Button button="alt" icon={ICONS.REFRESH} title={__('Refresh')} onClick={() => setPage(0)} />
|
||||||
</>
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
240
ui/component/common/form-components/form-field-area-advanced.jsx
Normal file
240
ui/component/common/form-components/form-field-area-advanced.jsx
Normal file
|
@ -0,0 +1,240 @@
|
||||||
|
// @flow
|
||||||
|
import 'easymde/dist/easymde.min.css';
|
||||||
|
import { FF_MAX_CHARS_DEFAULT } from 'constants/form-field';
|
||||||
|
import { openEditorMenu, stopContextMenu } from 'util/context-menu';
|
||||||
|
import * as ICONS from 'constants/icons';
|
||||||
|
import Button from 'component/button';
|
||||||
|
import MarkdownPreview from 'component/common/markdown-preview';
|
||||||
|
import React from 'react';
|
||||||
|
import ReactDOMServer from 'react-dom/server';
|
||||||
|
import SimpleMDE from 'react-simplemde-editor';
|
||||||
|
import TextareaWithSuggestions from 'component/textareaWithSuggestions';
|
||||||
|
import type { ElementRef, Node } from 'react';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
autoFocus?: boolean,
|
||||||
|
blockWrap: boolean,
|
||||||
|
charCount?: number,
|
||||||
|
children?: React$Node,
|
||||||
|
disabled?: boolean,
|
||||||
|
helper?: string | React$Node,
|
||||||
|
hideSuggestions?: boolean,
|
||||||
|
isLivestream?: boolean,
|
||||||
|
label?: string | Node,
|
||||||
|
labelOnLeft: boolean,
|
||||||
|
name: string,
|
||||||
|
noEmojis?: boolean,
|
||||||
|
placeholder?: string | number,
|
||||||
|
quickActionLabel?: string,
|
||||||
|
textAreaMaxLength?: number,
|
||||||
|
type?: string,
|
||||||
|
value?: string | number,
|
||||||
|
onChange?: (any) => any,
|
||||||
|
openEmoteMenu?: () => void,
|
||||||
|
quickActionHandler?: (any) => any,
|
||||||
|
render?: () => React$Node,
|
||||||
|
header?: React$Node,
|
||||||
|
};
|
||||||
|
|
||||||
|
export class FormFieldAreaAdvanced extends React.PureComponent<Props> {
|
||||||
|
static defaultProps = { labelOnLeft: false, blockWrap: true };
|
||||||
|
|
||||||
|
input: { current: ElementRef<any> };
|
||||||
|
|
||||||
|
constructor(props: Props) {
|
||||||
|
super(props);
|
||||||
|
this.input = React.createRef();
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
const { autoFocus } = this.props;
|
||||||
|
const input = this.input.current;
|
||||||
|
|
||||||
|
if (input && autoFocus) input.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
autoFocus,
|
||||||
|
blockWrap,
|
||||||
|
charCount,
|
||||||
|
children,
|
||||||
|
helper,
|
||||||
|
hideSuggestions,
|
||||||
|
isLivestream,
|
||||||
|
label,
|
||||||
|
header,
|
||||||
|
labelOnLeft,
|
||||||
|
name,
|
||||||
|
noEmojis,
|
||||||
|
quickActionLabel,
|
||||||
|
textAreaMaxLength,
|
||||||
|
type,
|
||||||
|
openEmoteMenu,
|
||||||
|
quickActionHandler,
|
||||||
|
render,
|
||||||
|
...inputProps
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
// Ideally, the character count should (and can) be appended to the
|
||||||
|
// SimpleMDE's "options::status" bar. However, I couldn't figure out how
|
||||||
|
// to pass the current value to it's callback, nor query the current
|
||||||
|
// text length from the callback. So, we'll use our own widget.
|
||||||
|
const hasCharCount = charCount !== undefined && charCount >= 0;
|
||||||
|
const countInfo = hasCharCount && textAreaMaxLength !== undefined && (
|
||||||
|
<span className="comment__char-count-mde">{`${charCount || '0'}/${textAreaMaxLength}`}</span>
|
||||||
|
);
|
||||||
|
|
||||||
|
const quickAction =
|
||||||
|
quickActionLabel && quickActionHandler ? (
|
||||||
|
<div className="form-field__quick-action">
|
||||||
|
<Button button="link" onClick={quickActionHandler} label={quickActionLabel} />
|
||||||
|
</div>
|
||||||
|
) : null;
|
||||||
|
|
||||||
|
const input = () => {
|
||||||
|
switch (type) {
|
||||||
|
case 'markdown':
|
||||||
|
const handleEvents = { contextmenu: openEditorMenu };
|
||||||
|
|
||||||
|
const getInstance = (editor) => {
|
||||||
|
// SimpleMDE max char check
|
||||||
|
editor.codemirror.on('beforeChange', (instance, changes) => {
|
||||||
|
if (textAreaMaxLength && changes.update) {
|
||||||
|
var str = changes.text.join('\n');
|
||||||
|
var delta = str.length - (instance.indexFromPos(changes.to) - instance.indexFromPos(changes.from));
|
||||||
|
|
||||||
|
if (delta <= 0) return;
|
||||||
|
|
||||||
|
delta = instance.getValue().length + delta - textAreaMaxLength;
|
||||||
|
if (delta > 0) {
|
||||||
|
str = str.substring(0, str.length - delta);
|
||||||
|
changes.update(changes.from, changes.to, str.split('\n'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// "Create Link (Ctrl-K)": highlight URL instead of label:
|
||||||
|
editor.codemirror.on('changes', (instance, changes) => {
|
||||||
|
try {
|
||||||
|
// Grab the last change from the buffered list. I assume the
|
||||||
|
// buffered one ('changes', instead of 'change') is more efficient,
|
||||||
|
// and that "Create Link" will always end up last in the list.
|
||||||
|
const lastChange = changes[changes.length - 1];
|
||||||
|
if (lastChange.origin === '+input') {
|
||||||
|
// https://github.com/Ionaru/easy-markdown-editor/blob/8fa54c496f98621d5f45f57577ce630bee8c41ee/src/js/easymde.js#L765
|
||||||
|
const EASYMDE_URL_PLACEHOLDER = '(https://)';
|
||||||
|
|
||||||
|
// The URL placeholder is always placed last, so just look at the
|
||||||
|
// last text in the array to also cover the multi-line case:
|
||||||
|
const urlLineText = lastChange.text[lastChange.text.length - 1];
|
||||||
|
|
||||||
|
if (urlLineText.endsWith(EASYMDE_URL_PLACEHOLDER) && urlLineText !== '[]' + EASYMDE_URL_PLACEHOLDER) {
|
||||||
|
const from = lastChange.from;
|
||||||
|
const to = lastChange.to;
|
||||||
|
const isSelectionMultiline = lastChange.text.length > 1;
|
||||||
|
const baseIndex = isSelectionMultiline ? 0 : from.ch;
|
||||||
|
|
||||||
|
// Everything works fine for the [Ctrl-K] case, but for the
|
||||||
|
// [Button] case, this handler happens before the original
|
||||||
|
// code, thus our change got wiped out.
|
||||||
|
// Add a small delay to handle that case.
|
||||||
|
setTimeout(() => {
|
||||||
|
instance.setSelection(
|
||||||
|
{ line: to.line, ch: baseIndex + urlLineText.lastIndexOf('(') + 1 },
|
||||||
|
{ line: to.line, ch: baseIndex + urlLineText.lastIndexOf(')') }
|
||||||
|
);
|
||||||
|
}, 25);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {} // Do nothing (revert to original behavior)
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="form-field--SimpleMDE" onContextMenu={stopContextMenu}>
|
||||||
|
<fieldset-section>
|
||||||
|
{!header && (
|
||||||
|
<div className="form-field__two-column">
|
||||||
|
<div>
|
||||||
|
<label htmlFor={name}>{label}</label>
|
||||||
|
</div>
|
||||||
|
{quickAction}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{!!header && <div className="form-field__textarea-header">{header}</div>}
|
||||||
|
<SimpleMDE
|
||||||
|
{...inputProps}
|
||||||
|
id={name}
|
||||||
|
type="textarea"
|
||||||
|
events={handleEvents}
|
||||||
|
getMdeInstance={getInstance}
|
||||||
|
options={{
|
||||||
|
spellChecker: true,
|
||||||
|
hideIcons: ['heading', 'image', 'fullscreen', 'side-by-side'],
|
||||||
|
previewRender(plainText) {
|
||||||
|
const preview = <MarkdownPreview content={plainText} noDataStore />;
|
||||||
|
return ReactDOMServer.renderToString(preview);
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{countInfo}
|
||||||
|
</fieldset-section>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
case 'textarea':
|
||||||
|
return (
|
||||||
|
<fieldset-section>
|
||||||
|
{!header && (label || quickAction) && (
|
||||||
|
<div className="form-field__two-column">
|
||||||
|
<label htmlFor={name}>{label}</label>
|
||||||
|
{quickAction}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{!!header && <div className="form-field__textarea-header">{header}</div>}
|
||||||
|
{hideSuggestions ? (
|
||||||
|
<textarea
|
||||||
|
type={type}
|
||||||
|
id={name}
|
||||||
|
maxLength={textAreaMaxLength || FF_MAX_CHARS_DEFAULT}
|
||||||
|
ref={this.input}
|
||||||
|
{...inputProps}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<TextareaWithSuggestions
|
||||||
|
type={type}
|
||||||
|
id={name}
|
||||||
|
maxLength={textAreaMaxLength || FF_MAX_CHARS_DEFAULT}
|
||||||
|
inputRef={this.input}
|
||||||
|
isLivestream={isLivestream}
|
||||||
|
{...inputProps}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<div className="form-field__textarea-info">
|
||||||
|
{!noEmojis && openEmoteMenu && (
|
||||||
|
<Button
|
||||||
|
type="alt"
|
||||||
|
className="button--comment-icons"
|
||||||
|
title="Emotes"
|
||||||
|
onClick={openEmoteMenu}
|
||||||
|
icon={ICONS.EMOJI}
|
||||||
|
iconSize={20}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{countInfo}
|
||||||
|
</div>
|
||||||
|
</fieldset-section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{type && input()}
|
||||||
|
{helper && <div className="form-field__help">{helper}</div>}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FormFieldAreaAdvanced;
|
|
@ -1,14 +1,7 @@
|
||||||
// @flow
|
// @flow
|
||||||
import 'easymde/dist/easymde.min.css';
|
import 'easymde/dist/easymde.min.css';
|
||||||
import { FF_MAX_CHARS_DEFAULT } from 'constants/form-field';
|
import { FF_MAX_CHARS_DEFAULT } from 'constants/form-field';
|
||||||
import { openEditorMenu, stopContextMenu } from 'util/context-menu';
|
|
||||||
import * as ICONS from 'constants/icons';
|
|
||||||
import Button from 'component/button';
|
|
||||||
import MarkdownPreview from 'component/common/markdown-preview';
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ReactDOMServer from 'react-dom/server';
|
|
||||||
import SimpleMDE from 'react-simplemde-editor';
|
|
||||||
import TextareaWithSuggestions from 'component/textareaWithSuggestions';
|
|
||||||
import type { ElementRef, Node } from 'react';
|
import type { ElementRef, Node } from 'react';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
@ -21,19 +14,15 @@ type Props = {
|
||||||
disabled?: boolean,
|
disabled?: boolean,
|
||||||
error?: string | boolean,
|
error?: string | boolean,
|
||||||
helper?: string | React$Node,
|
helper?: string | React$Node,
|
||||||
hideSuggestions?: boolean,
|
|
||||||
inputButton?: React$Node,
|
inputButton?: React$Node,
|
||||||
isLivestream?: boolean,
|
|
||||||
label?: string | Node,
|
label?: string | Node,
|
||||||
labelOnLeft: boolean,
|
labelOnLeft: boolean,
|
||||||
max?: number,
|
max?: number,
|
||||||
min?: number,
|
min?: number,
|
||||||
name: string,
|
name: string,
|
||||||
noEmojis?: boolean,
|
|
||||||
placeholder?: string | number,
|
placeholder?: string | number,
|
||||||
postfix?: string,
|
postfix?: string,
|
||||||
prefix?: string,
|
prefix?: string,
|
||||||
quickActionLabel?: string,
|
|
||||||
range?: number,
|
range?: number,
|
||||||
readOnly?: boolean,
|
readOnly?: boolean,
|
||||||
stretch?: boolean,
|
stretch?: boolean,
|
||||||
|
@ -41,8 +30,6 @@ type Props = {
|
||||||
type?: string,
|
type?: string,
|
||||||
value?: string | number,
|
value?: string | number,
|
||||||
onChange?: (any) => any,
|
onChange?: (any) => any,
|
||||||
openEmoteMenu?: () => void,
|
|
||||||
quickActionHandler?: (any) => any,
|
|
||||||
render?: () => React$Node,
|
render?: () => React$Node,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -72,21 +59,15 @@ export class FormField extends React.PureComponent<Props> {
|
||||||
children,
|
children,
|
||||||
error,
|
error,
|
||||||
helper,
|
helper,
|
||||||
hideSuggestions,
|
|
||||||
inputButton,
|
inputButton,
|
||||||
isLivestream,
|
|
||||||
label,
|
label,
|
||||||
labelOnLeft,
|
labelOnLeft,
|
||||||
name,
|
name,
|
||||||
noEmojis,
|
|
||||||
postfix,
|
postfix,
|
||||||
prefix,
|
prefix,
|
||||||
quickActionLabel,
|
|
||||||
stretch,
|
stretch,
|
||||||
textAreaMaxLength,
|
textAreaMaxLength,
|
||||||
type,
|
type,
|
||||||
openEmoteMenu,
|
|
||||||
quickActionHandler,
|
|
||||||
render,
|
render,
|
||||||
...inputProps
|
...inputProps
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
@ -101,18 +82,10 @@ export class FormField extends React.PureComponent<Props> {
|
||||||
const countInfo = hasCharCount && textAreaMaxLength !== undefined && (
|
const countInfo = hasCharCount && textAreaMaxLength !== undefined && (
|
||||||
<span className="comment__char-count-mde">{`${charCount || '0'}/${textAreaMaxLength}`}</span>
|
<span className="comment__char-count-mde">{`${charCount || '0'}/${textAreaMaxLength}`}</span>
|
||||||
);
|
);
|
||||||
|
|
||||||
const Wrapper = blockWrap
|
const Wrapper = blockWrap
|
||||||
? ({ children: innerChildren }) => <fieldset-section class="radio">{innerChildren}</fieldset-section>
|
? ({ children: innerChildren }) => <fieldset-section class="radio">{innerChildren}</fieldset-section>
|
||||||
: ({ children: innerChildren }) => <span className="radio">{innerChildren}</span>;
|
: ({ children: innerChildren }) => <span className="radio">{innerChildren}</span>;
|
||||||
|
|
||||||
const quickAction =
|
|
||||||
quickActionLabel && quickActionHandler ? (
|
|
||||||
<div className="form-field__quick-action">
|
|
||||||
<Button button="link" onClick={quickActionHandler} label={quickActionLabel} />
|
|
||||||
</div>
|
|
||||||
) : null;
|
|
||||||
|
|
||||||
const inputSimple = (type: string) => (
|
const inputSimple = (type: string) => (
|
||||||
<>
|
<>
|
||||||
<input id={name} type={type} {...inputProps} />
|
<input id={name} type={type} {...inputProps} />
|
||||||
|
@ -143,102 +116,14 @@ export class FormField extends React.PureComponent<Props> {
|
||||||
return inputSelect('');
|
return inputSelect('');
|
||||||
case 'select-tiny':
|
case 'select-tiny':
|
||||||
return inputSelect('select--slim');
|
return inputSelect('select--slim');
|
||||||
case 'markdown':
|
|
||||||
const handleEvents = { contextmenu: openEditorMenu };
|
|
||||||
|
|
||||||
const getInstance = (editor) => {
|
|
||||||
// SimpleMDE max char check
|
|
||||||
editor.codemirror.on('beforeChange', (instance, changes) => {
|
|
||||||
if (textAreaMaxLength && changes.update) {
|
|
||||||
var str = changes.text.join('\n');
|
|
||||||
var delta = str.length - (instance.indexFromPos(changes.to) - instance.indexFromPos(changes.from));
|
|
||||||
|
|
||||||
if (delta <= 0) return;
|
|
||||||
|
|
||||||
delta = instance.getValue().length + delta - textAreaMaxLength;
|
|
||||||
if (delta > 0) {
|
|
||||||
str = str.substring(0, str.length - delta);
|
|
||||||
changes.update(changes.from, changes.to, str.split('\n'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// "Create Link (Ctrl-K)": highlight URL instead of label:
|
|
||||||
editor.codemirror.on('changes', (instance, changes) => {
|
|
||||||
try {
|
|
||||||
// Grab the last change from the buffered list. I assume the
|
|
||||||
// buffered one ('changes', instead of 'change') is more efficient,
|
|
||||||
// and that "Create Link" will always end up last in the list.
|
|
||||||
const lastChange = changes[changes.length - 1];
|
|
||||||
if (lastChange.origin === '+input') {
|
|
||||||
// https://github.com/Ionaru/easy-markdown-editor/blob/8fa54c496f98621d5f45f57577ce630bee8c41ee/src/js/easymde.js#L765
|
|
||||||
const EASYMDE_URL_PLACEHOLDER = '(https://)';
|
|
||||||
|
|
||||||
// The URL placeholder is always placed last, so just look at the
|
|
||||||
// last text in the array to also cover the multi-line case:
|
|
||||||
const urlLineText = lastChange.text[lastChange.text.length - 1];
|
|
||||||
|
|
||||||
if (urlLineText.endsWith(EASYMDE_URL_PLACEHOLDER) && urlLineText !== '[]' + EASYMDE_URL_PLACEHOLDER) {
|
|
||||||
const from = lastChange.from;
|
|
||||||
const to = lastChange.to;
|
|
||||||
const isSelectionMultiline = lastChange.text.length > 1;
|
|
||||||
const baseIndex = isSelectionMultiline ? 0 : from.ch;
|
|
||||||
|
|
||||||
// Everything works fine for the [Ctrl-K] case, but for the
|
|
||||||
// [Button] case, this handler happens before the original
|
|
||||||
// code, thus our change got wiped out.
|
|
||||||
// Add a small delay to handle that case.
|
|
||||||
setTimeout(() => {
|
|
||||||
instance.setSelection(
|
|
||||||
{ line: to.line, ch: baseIndex + urlLineText.lastIndexOf('(') + 1 },
|
|
||||||
{ line: to.line, ch: baseIndex + urlLineText.lastIndexOf(')') }
|
|
||||||
);
|
|
||||||
}, 25);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {} // Do nothing (revert to original behavior)
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="form-field--SimpleMDE" onContextMenu={stopContextMenu}>
|
|
||||||
<fieldset-section>
|
|
||||||
<div className="form-field__two-column">
|
|
||||||
<div>
|
|
||||||
<label htmlFor={name}>{label}</label>
|
|
||||||
</div>
|
|
||||||
{quickAction}
|
|
||||||
</div>
|
|
||||||
<SimpleMDE
|
|
||||||
{...inputProps}
|
|
||||||
id={name}
|
|
||||||
type="textarea"
|
|
||||||
events={handleEvents}
|
|
||||||
getMdeInstance={getInstance}
|
|
||||||
options={{
|
|
||||||
spellChecker: true,
|
|
||||||
hideIcons: ['heading', 'image', 'fullscreen', 'side-by-side'],
|
|
||||||
previewRender(plainText) {
|
|
||||||
const preview = <MarkdownPreview content={plainText} noDataStore />;
|
|
||||||
return ReactDOMServer.renderToString(preview);
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{countInfo}
|
|
||||||
</fieldset-section>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
case 'textarea':
|
case 'textarea':
|
||||||
return (
|
return (
|
||||||
<fieldset-section>
|
<fieldset-section>
|
||||||
{(label || quickAction) && (
|
{label && (
|
||||||
<div className="form-field__two-column">
|
<div className="form-field__two-column">
|
||||||
<label htmlFor={name}>{label}</label>
|
<label htmlFor={name}>{label}</label>
|
||||||
{quickAction}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{hideSuggestions ? (
|
|
||||||
<textarea
|
<textarea
|
||||||
type={type}
|
type={type}
|
||||||
id={name}
|
id={name}
|
||||||
|
@ -246,30 +131,7 @@ export class FormField extends React.PureComponent<Props> {
|
||||||
ref={this.input}
|
ref={this.input}
|
||||||
{...inputProps}
|
{...inputProps}
|
||||||
/>
|
/>
|
||||||
) : (
|
<div className="form-field__textarea-info">{countInfo}</div>
|
||||||
<TextareaWithSuggestions
|
|
||||||
type={type}
|
|
||||||
id={name}
|
|
||||||
maxLength={textAreaMaxLength || FF_MAX_CHARS_DEFAULT}
|
|
||||||
inputRef={this.input}
|
|
||||||
isLivestream={isLivestream}
|
|
||||||
{...inputProps}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="form-field__textarea-info">
|
|
||||||
{!noEmojis && openEmoteMenu && (
|
|
||||||
<Button
|
|
||||||
type="alt"
|
|
||||||
className="button--comment-icons"
|
|
||||||
title="Emotes"
|
|
||||||
onClick={openEmoteMenu}
|
|
||||||
icon={ICONS.EMOJI}
|
|
||||||
iconSize={20}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{countInfo}
|
|
||||||
</div>
|
|
||||||
</fieldset-section>
|
</fieldset-section>
|
||||||
);
|
);
|
||||||
default:
|
default:
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
export { Form } from './form-components/form';
|
export { Form } from './form-components/form';
|
||||||
export { FormField } from './form-components/form-field';
|
export { FormField } from './form-components/form-field';
|
||||||
|
export { FormFieldAreaAdvanced } from './form-components/form-field-area-advanced';
|
||||||
export { FormFieldPrice } from './form-components/form-field-price';
|
export { FormFieldPrice } from './form-components/form-field-price';
|
||||||
export { Submit } from './form-components/submit';
|
export { Submit } from './form-components/submit';
|
||||||
|
|
|
@ -2054,4 +2054,15 @@ export const icons = {
|
||||||
<path d="M12.5,23.24v-1A10.74,10.74,0,0,1,23.24,11.52" />
|
<path d="M12.5,23.24v-1A10.74,10.74,0,0,1,23.24,11.52" />
|
||||||
</g>
|
</g>
|
||||||
),
|
),
|
||||||
|
[ICONS.SIMPLE_EDITOR]: buildIcon(
|
||||||
|
<g>
|
||||||
|
<path d="M1 18V6c0-1 1-2 2-2h18c1 0 2 1 2 2v12c0 1-1 2-2 2H3c-1 0-2-1-2-2ZM5 7v4" />
|
||||||
|
</g>
|
||||||
|
),
|
||||||
|
[ICONS.ADVANCED_EDITOR]: buildIcon(
|
||||||
|
<g>
|
||||||
|
<path d="M1 20V4c0-1 1-2 2-2h18c1 0 2 1 2 2v16c0 1-1 2-2 2H3c-1 0-2-1-2-2ZM1 11h22" />
|
||||||
|
<path d="M5 8V6h2v2H5ZM11 8V6h2v2h-2ZM17 8V6h2v2h-2ZM5 14v4" />
|
||||||
|
</g>
|
||||||
|
),
|
||||||
};
|
};
|
||||||
|
|
|
@ -12,7 +12,7 @@ import { PRIMARY_PLAYER_WRAPPER_CLASS } from 'page/file/view';
|
||||||
import Draggable from 'react-draggable';
|
import Draggable from 'react-draggable';
|
||||||
import { onFullscreenChange } from 'util/full-screen';
|
import { onFullscreenChange } from 'util/full-screen';
|
||||||
import { generateListSearchUrlParams, formatLbryUrlForWeb } from 'util/url';
|
import { generateListSearchUrlParams, formatLbryUrlForWeb } from 'util/url';
|
||||||
import { useIsMobile } from 'effects/use-screensize';
|
import { useIsMobile, useIsMediumScreen } from 'effects/use-screensize';
|
||||||
import debounce from 'util/debounce';
|
import debounce from 'util/debounce';
|
||||||
import { useHistory } from 'react-router';
|
import { useHistory } from 'react-router';
|
||||||
import { isURIEqual } from 'util/lbryURI';
|
import { isURIEqual } from 'util/lbryURI';
|
||||||
|
@ -132,6 +132,7 @@ export default function FileRenderFloating(props: Props) {
|
||||||
const playingUriSource = playingUri && playingUri.source;
|
const playingUriSource = playingUri && playingUri.source;
|
||||||
const isComment = playingUriSource === 'comment';
|
const isComment = playingUriSource === 'comment';
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
|
const isMediumScreen = useIsMediumScreen();
|
||||||
const mainFilePlaying = !isFloating && primaryUri && isURIEqual(uri, primaryUri);
|
const mainFilePlaying = !isFloating && primaryUri && isURIEqual(uri, primaryUri);
|
||||||
|
|
||||||
const [fileViewerRect, setFileViewerRect] = useState();
|
const [fileViewerRect, setFileViewerRect] = useState();
|
||||||
|
@ -343,7 +344,8 @@ export default function FileRenderFloating(props: Props) {
|
||||||
'content__viewer--floating': isFloating,
|
'content__viewer--floating': isFloating,
|
||||||
'content__viewer--inline': !isFloating,
|
'content__viewer--inline': !isFloating,
|
||||||
'content__viewer--secondary': isComment,
|
'content__viewer--secondary': isComment,
|
||||||
'content__viewer--theater-mode': !isFloating && videoTheaterMode && playingUri?.uri === primaryUri,
|
'content__viewer--theater-mode':
|
||||||
|
!isFloating && videoTheaterMode && !isMediumScreen && playingUri?.uri === primaryUri,
|
||||||
'content__viewer--disable-click': wasDragging,
|
'content__viewer--disable-click': wasDragging,
|
||||||
})}
|
})}
|
||||||
style={
|
style={
|
||||||
|
|
|
@ -9,6 +9,7 @@ import * as PAGES from 'constants/pages';
|
||||||
import * as RENDER_MODES from 'constants/file_render_modes';
|
import * as RENDER_MODES from 'constants/file_render_modes';
|
||||||
import * as KEYCODES from 'constants/keycodes';
|
import * as KEYCODES from 'constants/keycodes';
|
||||||
import Button from 'component/button';
|
import Button from 'component/button';
|
||||||
|
import { useIsMediumScreen } from 'effects/use-screensize';
|
||||||
import isUserTyping from 'util/detect-typing';
|
import isUserTyping from 'util/detect-typing';
|
||||||
import { getThumbnailCdnUrl } from 'util/thumbnail';
|
import { getThumbnailCdnUrl } from 'util/thumbnail';
|
||||||
import Nag from 'component/common/nag';
|
import Nag from 'component/common/nag';
|
||||||
|
@ -63,6 +64,7 @@ export default function FileRenderInitiator(props: Props) {
|
||||||
const fileStatus = fileInfo && fileInfo.status;
|
const fileStatus = fileInfo && fileInfo.status;
|
||||||
const isPlayable = RENDER_MODES.FLOATING_MODES.includes(renderMode);
|
const isPlayable = RENDER_MODES.FLOATING_MODES.includes(renderMode);
|
||||||
const isText = RENDER_MODES.TEXT_MODES.includes(renderMode);
|
const isText = RENDER_MODES.TEXT_MODES.includes(renderMode);
|
||||||
|
const isMediumScreen = useIsMediumScreen();
|
||||||
const [thumbnail, setThumbnail] = React.useState(FileRenderPlaceholder);
|
const [thumbnail, setThumbnail] = React.useState(FileRenderPlaceholder);
|
||||||
const containerRef = React.useRef<any>();
|
const containerRef = React.useRef<any>();
|
||||||
|
|
||||||
|
@ -151,7 +153,7 @@ export default function FileRenderInitiator(props: Props) {
|
||||||
style={thumbnail && !obscurePreview ? { backgroundImage: `url("${thumbnail}")` } : {}}
|
style={thumbnail && !obscurePreview ? { backgroundImage: `url("${thumbnail}")` } : {}}
|
||||||
className={classnames('content__cover', {
|
className={classnames('content__cover', {
|
||||||
'content__cover--disabled': disabled,
|
'content__cover--disabled': disabled,
|
||||||
'content__cover--theater-mode': videoTheaterMode,
|
'content__cover--theater-mode': videoTheaterMode && !isMediumScreen,
|
||||||
'content__cover--text': isText,
|
'content__cover--text': isText,
|
||||||
'card__media--nsfw': obscurePreview,
|
'card__media--nsfw': obscurePreview,
|
||||||
})}
|
})}
|
||||||
|
|
|
@ -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_id === claimBeingPlayed.claim_id;
|
isBeingPlayed = claim && claim.claim_id === claimBeingPlayed.claim_id;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -92,7 +92,7 @@ function Page(props: Props) {
|
||||||
<div
|
<div
|
||||||
className={classnames('main-wrapper__inner', {
|
className={classnames('main-wrapper__inner', {
|
||||||
'main-wrapper__inner--filepage': isOnFilePage,
|
'main-wrapper__inner--filepage': isOnFilePage,
|
||||||
'main-wrapper__inner--theater-mode': isOnFilePage && videoTheaterMode,
|
'main-wrapper__inner--theater-mode': isOnFilePage && videoTheaterMode && !isMediumScreen,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{!authPage &&
|
{!authPage &&
|
||||||
|
@ -124,7 +124,7 @@ function Page(props: Props) {
|
||||||
'main--file-page': filePage,
|
'main--file-page': filePage,
|
||||||
'main--settings-page': settingsPage,
|
'main--settings-page': settingsPage,
|
||||||
'main--markdown': isMarkdown,
|
'main--markdown': isMarkdown,
|
||||||
'main--theater-mode': isOnFilePage && videoTheaterMode && !isMarkdown,
|
'main--theater-mode': isOnFilePage && videoTheaterMode && !isMediumScreen && !isMarkdown,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
// @flow
|
// @flow
|
||||||
import React, { useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import { FormField } from 'component/common/form';
|
import { FormFieldAreaAdvanced } from 'component/common/form';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
uri: ?string,
|
uri: ?string,
|
||||||
|
@ -99,7 +99,7 @@ function PostEditor(props: Props) {
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormField
|
<FormFieldAreaAdvanced
|
||||||
type={'markdown'}
|
type={'markdown'}
|
||||||
name="content_post"
|
name="content_post"
|
||||||
label={label}
|
label={label}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
// @flow
|
// @flow
|
||||||
import { FF_MAX_CHARS_IN_DESCRIPTION } from 'constants/form-field';
|
import { FF_MAX_CHARS_IN_DESCRIPTION } from 'constants/form-field';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { FormField } from 'component/common/form';
|
import { FormFieldAreaAdvanced } from 'component/common/form';
|
||||||
import usePersistedState from 'effects/use-persisted-state';
|
import usePersistedState from 'effects/use-persisted-state';
|
||||||
import Card from 'component/common/card';
|
import Card from 'component/common/card';
|
||||||
|
|
||||||
|
@ -27,7 +27,7 @@ function PublishDescription(props: Props) {
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
actions={
|
actions={
|
||||||
<FormField
|
<FormFieldAreaAdvanced
|
||||||
type={advancedEditor ? 'markdown' : 'textarea'}
|
type={advancedEditor ? 'markdown' : 'textarea'}
|
||||||
name="content_description"
|
name="content_description"
|
||||||
label={__('Description')}
|
label={__('Description')}
|
||||||
|
|
|
@ -14,7 +14,7 @@ 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,
|
||||||
|
@ -99,18 +99,27 @@ function PublishFile(props: Props) {
|
||||||
if (!filePath) {
|
if (!filePath) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
async function readSelectedFile() {
|
async function readSelectedFileDetails() {
|
||||||
// Read the file to get the file's duration (if possible)
|
// Read the file to get the file's duration (if possible)
|
||||||
// and offer transcoding it.
|
// and offer transcoding it.
|
||||||
const readFileContents = true;
|
const result = await ipcRenderer.invoke('get-file-details-from-path', filePath);
|
||||||
const result = await ipcRenderer.invoke('get-file-from-path', filePath, readFileContents);
|
let file;
|
||||||
const file = new File([result.buffer], result.name, {
|
if (result.buffer) {
|
||||||
|
file = new File([result.buffer], result.name, {
|
||||||
type: result.mime,
|
type: result.mime,
|
||||||
});
|
});
|
||||||
const fileWithPath = { file, path: result.path };
|
|
||||||
processSelectedFile(fileWithPath);
|
|
||||||
}
|
}
|
||||||
readSelectedFile();
|
const fileData: FileData = {
|
||||||
|
path: result.path,
|
||||||
|
name: result.name,
|
||||||
|
mimeType: result.mime || 'application/octet-stream',
|
||||||
|
size: result.size,
|
||||||
|
duration: result.duration,
|
||||||
|
file: file,
|
||||||
|
};
|
||||||
|
processSelectedFile(fileData);
|
||||||
|
}
|
||||||
|
readSelectedFileDetails();
|
||||||
}, [filePath]);
|
}, [filePath]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -219,11 +228,11 @@ function PublishFile(props: Props) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function processSelectedFile(fileWithPath: FileWithPath, clearName = true) {
|
function processSelectedFile(fileData: FileData, clearName = true) {
|
||||||
window.URL = window.URL || window.webkitURL;
|
window.URL = window.URL || window.webkitURL;
|
||||||
|
|
||||||
// select file, start to select a new one, then cancel
|
// select file, start to select a new one, then cancel
|
||||||
if (!fileWithPath) {
|
if (!fileData || fileData.error) {
|
||||||
if (isStillEditing || !clearName) {
|
if (isStillEditing || !clearName) {
|
||||||
updatePublishForm({ filePath: '' });
|
updatePublishForm({ filePath: '' });
|
||||||
} else {
|
} else {
|
||||||
|
@ -233,8 +242,11 @@ function PublishFile(props: Props) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// if video, extract duration so we can warn about bitrate if (typeof file !== 'string')
|
// if video, extract duration so we can warn about bitrate if (typeof file !== 'string')
|
||||||
const file = fileWithPath.file;
|
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';
|
||||||
|
|
||||||
|
@ -242,34 +254,25 @@ function PublishFile(props: Props) {
|
||||||
|
|
||||||
if (contentType && contentType[0] === 'text') {
|
if (contentType && contentType[0] === 'text') {
|
||||||
isTextPost = contentType[1] === 'plain' || contentType[1] === 'markdown';
|
isTextPost = contentType[1] === 'plain' || contentType[1] === 'markdown';
|
||||||
setCurrentFileType(contentType);
|
setCurrentFileType(contentType.join('/'));
|
||||||
} else if (file.name) {
|
} else if (path.parse(fileData.path).ext) {
|
||||||
// If user's machine is missing a valid content type registration
|
// If user's machine is missing a valid content type registration
|
||||||
// for markdown content: text/markdown, file extension will be used instead
|
// for markdown content: text/markdown, file extension will be used instead
|
||||||
const extension = file.name.split('.').pop();
|
const extension = path.parse(fileData.path).ext;
|
||||||
isTextPost = MARKDOWN_FILE_EXTENSIONS.includes(extension);
|
isTextPost = MARKDOWN_FILE_EXTENSIONS.includes(extension);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isVideo) {
|
if (isVideo) {
|
||||||
if (isMp4) {
|
if (isMp4) {
|
||||||
const video = document.createElement('video');
|
updateFileInfo(duration || 0, size, isVideo);
|
||||||
video.preload = 'metadata';
|
|
||||||
video.onloadedmetadata = () => {
|
|
||||||
updateFileInfo(video.duration, file.size, isVideo);
|
|
||||||
window.URL.revokeObjectURL(video.src);
|
|
||||||
};
|
|
||||||
video.onerror = () => {
|
|
||||||
updateFileInfo(0, file.size, isVideo);
|
|
||||||
};
|
|
||||||
video.src = window.URL.createObjectURL(file);
|
|
||||||
} else {
|
} else {
|
||||||
updateFileInfo(0, file.size, isVideo);
|
updateFileInfo(duration || 0, size, isVideo);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
updateFileInfo(0, file.size, isVideo);
|
updateFileInfo(0, size, isVideo);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isTextPost) {
|
if (isTextPost && file) {
|
||||||
// Create reader
|
// Create reader
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
// Handler for file reader
|
// Handler for file reader
|
||||||
|
@ -283,7 +286,7 @@ function PublishFile(props: Props) {
|
||||||
|
|
||||||
// Strip off extension and replace invalid characters
|
// Strip off extension and replace invalid characters
|
||||||
if (!isStillEditing) {
|
if (!isStillEditing) {
|
||||||
const fileWithoutExtension = name || (file.name && file.name.substring(0, file.name.lastIndexOf('.'))) || '';
|
const fileWithoutExtension = path.parse(fileData.path).name;
|
||||||
updatePublishForm({ name: parseName(fileWithoutExtension) });
|
updatePublishForm({ name: parseName(fileWithoutExtension) });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -208,7 +208,6 @@ function PublishForm(props: Props) {
|
||||||
isNameValid(name) &&
|
isNameValid(name) &&
|
||||||
title &&
|
title &&
|
||||||
bid &&
|
bid &&
|
||||||
thumbnail &&
|
|
||||||
!bidError &&
|
!bidError &&
|
||||||
!emptyPostError &&
|
!emptyPostError &&
|
||||||
!(thumbnailError && !thumbnailUploaded) &&
|
!(thumbnailError && !thumbnailUploaded) &&
|
||||||
|
|
|
@ -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}
|
src={thumbnail || ThumbnailMissingImage}
|
||||||
alt={__('Thumbnail Preview')}
|
alt={__('Thumbnail Preview')}
|
||||||
onError={() => {
|
onError={() => {
|
||||||
if (updateThumbnailParams) {
|
if (updateThumbnailParams) {
|
||||||
|
|
|
@ -103,6 +103,8 @@ 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) {
|
||||||
|
|
|
@ -186,3 +186,5 @@ 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';
|
||||||
|
|
|
@ -11,7 +11,8 @@ 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 DROPDOWN_TYPES = [ALL, SENT, RECEIVED, SUPPORT, CHANNEL, PUBLISH, REPOST];
|
export const COLLECTION = 'collection';
|
||||||
|
export const DROPDOWN_TYPES = [ALL, SENT, RECEIVED, SUPPORT, CHANNEL, PUBLISH, REPOST, COLLECTION];
|
||||||
// dropdown subtypes
|
// dropdown subtypes
|
||||||
export const TIP = 'tip';
|
export const TIP = 'tip';
|
||||||
export const PURCHASE = 'purchase';
|
export const PURCHASE = 'purchase';
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
// @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';
|
||||||
|
@ -59,6 +60,7 @@ 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 =
|
||||||
|
@ -169,8 +171,10 @@ 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 && <CollectionContent id={collectionId} uri={uri} />}
|
{collection && !isMarkdown && !videoTheaterMode && !isMediumScreen && (
|
||||||
{!collection && !isMarkdown && !videoTheaterMode && <RecommendedContent uri={uri} />}
|
<CollectionContent id={collectionId} uri={uri} />
|
||||||
|
)}
|
||||||
|
{!collection && !isMarkdown && !videoTheaterMode && !isMediumScreen && <RecommendedContent uri={uri} />}
|
||||||
</Page>
|
</Page>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -187,13 +191,17 @@ 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 && <RecommendedContent uri={uri} />}
|
{!collection && !isMarkdown && videoTheaterMode && !isMediumScreen && <RecommendedContent uri={uri} />}
|
||||||
{collection && !isMarkdown && videoTheaterMode && <CollectionContent id={collectionId} uri={uri} />}
|
{collection && !isMarkdown && videoTheaterMode && !isMediumScreen && (
|
||||||
|
<CollectionContent id={collectionId} uri={uri} />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{collection && !isMarkdown && !videoTheaterMode && <CollectionContent id={collectionId} uri={uri} />}
|
{collection && !isMarkdown && !videoTheaterMode && !isMediumScreen && (
|
||||||
{!collection && !isMarkdown && !videoTheaterMode && <RecommendedContent uri={uri} />}
|
<CollectionContent id={collectionId} 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 />}
|
||||||
|
|
|
@ -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' : ''}`}
|
||||||
|
|
|
@ -458,32 +458,34 @@ export function doAnalyticsView(uri, timeToStart) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function doAnalyticsBuffer(uri, bufferData) {
|
export function doAnalyticsBuffer(uri, bufferData) {
|
||||||
return (dispatch, getState) => {
|
return () => {
|
||||||
const state = getState();
|
// return (dispatch, getState) => {
|
||||||
const claim = selectClaimForUri(state, uri);
|
// const state = getState();
|
||||||
const user = selectUser(state);
|
// const claim = selectClaimForUri(state, uri);
|
||||||
const {
|
// const user = selectUser(state);
|
||||||
value: { video, audio, source },
|
// const {
|
||||||
} = claim;
|
// value: { video, audio, source },
|
||||||
const timeAtBuffer = parseInt(bufferData.currentTime ? bufferData.currentTime * 1000 : 0);
|
// } = claim;
|
||||||
const bufferDuration = parseInt(bufferData.secondsToLoad ? bufferData.secondsToLoad * 1000 : 0);
|
// const timeAtBuffer = parseInt(bufferData.currentTime ? bufferData.currentTime * 1000 : 0);
|
||||||
const fileDurationInSeconds = (video && video.duration) || (audio && audio.duration);
|
// const bufferDuration = parseInt(bufferData.secondsToLoad ? bufferData.secondsToLoad * 1000 : 0);
|
||||||
const fileSize = source.size; // size in bytes
|
// const fileDurationInSeconds = (video && video.duration) || (audio && audio.duration);
|
||||||
const fileSizeInBits = fileSize * 8;
|
// const fileSize = source.size; // size in bytes
|
||||||
const bitRate = parseInt(fileSizeInBits / fileDurationInSeconds);
|
// const fileSizeInBits = fileSize * 8;
|
||||||
const userId = user && user.id.toString();
|
// const bitRate = parseInt(fileSizeInBits / fileDurationInSeconds);
|
||||||
|
// const userId = user && user.id.toString();
|
||||||
// if there's a logged in user, send buffer event data to watchman
|
// if there's a logged in user, send buffer event data to watchman
|
||||||
if (userId) {
|
// if (<condition>) {
|
||||||
analytics.videoBufferEvent(claim, {
|
// STUB: any buffer events here
|
||||||
timeAtBuffer,
|
// analytics.videoBufferEvent(claim, {
|
||||||
bufferDuration,
|
// timeAtBuffer,
|
||||||
bitRate,
|
// bufferDuration,
|
||||||
userId,
|
// bitRate,
|
||||||
duration: fileDurationInSeconds,
|
// userId,
|
||||||
playerPoweredBy: bufferData.playerPoweredBy,
|
// duration: fileDurationInSeconds,
|
||||||
readyState: bufferData.readyState,
|
// playerPoweredBy: bufferData.playerPoweredBy,
|
||||||
});
|
// readyState: bufferData.readyState,
|
||||||
}
|
// });
|
||||||
|
// }
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 } from 'util/claim';
|
import { getChannelFromClaim, isChannelClaim } from 'util/claim';
|
||||||
|
|
||||||
// Returns the entire subscriptions state
|
// Returns the entire subscriptions state
|
||||||
const selectState = (state) => state.subscriptions || {};
|
const selectState = (state) => state.subscriptions || {};
|
||||||
|
@ -114,12 +114,18 @@ 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,
|
||||||
(claim, subscriptions) => {
|
(uri, claim, subscriptions) => {
|
||||||
const channelClaim = getChannelFromClaim(claim);
|
const channelClaim = getChannelFromClaim(claim);
|
||||||
if (channelClaim) {
|
if (channelClaim) {
|
||||||
const uri = channelClaim.permanent_url;
|
const permanentUrl = channelClaim.permanent_url;
|
||||||
|
return subscriptions.some((sub) => isURIEqual(sub.uri, permanentUrl));
|
||||||
|
}
|
||||||
|
|
||||||
|
// If it failed, it could be an abandoned channel. Try parseURI:
|
||||||
|
if (isChannelClaim(claim, uri)) {
|
||||||
return subscriptions.some((sub) => isURIEqual(sub.uri, uri));
|
return subscriptions.some((sub) => isURIEqual(sub.uri, uri));
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
|
|
|
@ -167,7 +167,7 @@ a.button--alt {
|
||||||
.vjs-button--theater-mode.vjs-button {
|
.vjs-button--theater-mode.vjs-button {
|
||||||
display: none;
|
display: none;
|
||||||
|
|
||||||
@media (min-width: $breakpoint-medium) {
|
@media not all and (max-width: $breakpoint-medium) {
|
||||||
display: block;
|
display: block;
|
||||||
order: 1;
|
order: 1;
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
|
|
|
@ -41,7 +41,7 @@
|
||||||
margin: 0px var(--spacing-xxs);
|
margin: 0px var(--spacing-xxs);
|
||||||
}
|
}
|
||||||
|
|
||||||
.button + .commentCreate {
|
.button + .comment-create {
|
||||||
margin-top: var(--spacing-xxs);
|
margin-top: var(--spacing-xxs);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -274,6 +274,21 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
|
.select--slim {
|
||||||
|
display: flex;
|
||||||
|
margin-left: var(--spacing-s);
|
||||||
|
margin-bottom: var(--spacing-s);
|
||||||
|
|
||||||
|
@media (min-width: $breakpoint-small) {
|
||||||
|
max-width: none;
|
||||||
|
|
||||||
|
select {
|
||||||
|
padding: 0 var(--spacing-xs);
|
||||||
|
padding-right: var(--spacing-l);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.card--enable-overflow {
|
.card--enable-overflow {
|
||||||
|
@ -318,6 +333,7 @@
|
||||||
align-self: flex-start;
|
align-self: flex-start;
|
||||||
.button--alt {
|
.button--alt {
|
||||||
padding-top: 2px;
|
padding-top: 2px;
|
||||||
|
padding: 0 var(--spacing-s);
|
||||||
}
|
}
|
||||||
.comment__sort {
|
.comment__sort {
|
||||||
.button--alt {
|
.button--alt {
|
||||||
|
@ -599,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;
|
||||||
|
@ -642,3 +658,25 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.button__selected-server {
|
||||||
|
display: inline;
|
||||||
|
float: right;
|
||||||
|
select {
|
||||||
|
width: 12rem;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
@media (max-width: $breakpoint-small) {
|
||||||
|
select {
|
||||||
|
width: 8rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.button_refresh {
|
||||||
|
display: inline;
|
||||||
|
float: right;
|
||||||
|
margin-left: var(--spacing-s);
|
||||||
|
}
|
||||||
|
|
|
@ -621,7 +621,7 @@
|
||||||
@media (max-width: $breakpoint-xsmall) {
|
@media (max-width: $breakpoint-xsmall) {
|
||||||
-webkit-line-clamp: 2 !important;
|
-webkit-line-clamp: 2 !important;
|
||||||
}
|
}
|
||||||
@media (min-width: $breakpoint-medium) {
|
@media not all and (max-width: $breakpoint-medium) {
|
||||||
-webkit-line-clamp: 1 !important;
|
-webkit-line-clamp: 1 !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,7 +7,7 @@ $thumbnailWidthSmall: 1rem;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.commentCreate {
|
.comment-create {
|
||||||
font-size: var(--font-small);
|
font-size: var(--font-small);
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
|
@ -135,12 +135,12 @@ $thumbnailWidthSmall: 1rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.commentCreate--reply {
|
.comment-create--reply {
|
||||||
margin-top: var(--spacing-m);
|
margin-top: var(--spacing-m);
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.commentCreate--nestedReply {
|
.comment-create--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,27 +149,40 @@ $thumbnailWidthSmall: 1rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.commentCreate--bottom {
|
.comment-create--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;
|
||||||
width: 100%;
|
max-width: 50%;
|
||||||
|
|
||||||
.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);
|
||||||
|
@ -179,14 +192,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;
|
||||||
}
|
//}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.commentCreate__supportCommentPreview {
|
.comment-create__supportCommentPreview {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
border: 1px solid var(--color-border);
|
border: 1px solid var(--color-border);
|
||||||
|
@ -194,7 +207,7 @@ $thumbnailWidthSmall: 1rem;
|
||||||
padding: var(--spacing-s);
|
padding: var(--spacing-s);
|
||||||
margin: var(--spacing-s) 0;
|
margin: var(--spacing-s) 0;
|
||||||
|
|
||||||
.commentCreate__supportCommentPreviewAmount {
|
.comment-create__supportCommentPreviewAmount {
|
||||||
margin-right: var(--spacing-m);
|
margin-right: var(--spacing-m);
|
||||||
font-size: var(--font-large);
|
font-size: var(--font-large);
|
||||||
}
|
}
|
||||||
|
@ -223,8 +236,8 @@ $thumbnailWidthSmall: 1rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.commentCreate__stickerPreview {
|
.comment-create__stickerPreview {
|
||||||
@extend .commentCreate;
|
@extend .comment-create;
|
||||||
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);
|
||||||
|
@ -234,12 +247,12 @@ $thumbnailWidthSmall: 1rem;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 10rem;
|
height: 10rem;
|
||||||
|
|
||||||
.commentCreate__stickerPreviewInfo {
|
.comment-create__stickerPreviewInfo {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
}
|
}
|
||||||
|
|
||||||
.commentCreate__stickerPreviewImage {
|
.comment-create__stickerPreviewImage {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
margin-left: var(--spacing-m);
|
margin-left: var(--spacing-m);
|
||||||
|
|
|
@ -37,16 +37,18 @@ $thumbnailWidthSmall: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.comment__sort {
|
.comment__sort {
|
||||||
margin-right: var(--spacing-s);
|
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
|
||||||
@media (min-width: $breakpoint-small) {
|
@media (min-width: $breakpoint-small) {
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
display: inline;
|
display: inline;
|
||||||
}
|
}
|
||||||
@media (max-width: $breakpoint-small) {
|
}
|
||||||
margin-right: 0;
|
|
||||||
}
|
.comment__actions-row {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: flex-end;
|
||||||
}
|
}
|
||||||
|
|
||||||
.comment {
|
.comment {
|
||||||
|
@ -505,7 +507,7 @@ $thumbnailWidthSmall: 1rem;
|
||||||
min-width: 100%;
|
min-width: 100%;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
|
|
||||||
@media (min-width: $breakpoint-medium) {
|
@media not all and (max-width: $breakpoint-medium) {
|
||||||
min-width: 40%;
|
min-width: 40%;
|
||||||
max-width: 40%;
|
max-width: 40%;
|
||||||
}
|
}
|
||||||
|
@ -547,7 +549,7 @@ $thumbnailWidthSmall: 1rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: $breakpoint-medium) {
|
@media not all and (max-width: $breakpoint-medium) {
|
||||||
margin: 0 var(--spacing-xs);
|
margin: 0 var(--spacing-xs);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -562,7 +564,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 (min-width: $breakpoint-medium) {
|
@media not all and (max-width: $breakpoint-medium) {
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
margin-left: var(--spacing-s);
|
margin-left: var(--spacing-s);
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,7 +29,12 @@ 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);
|
||||||
|
@ -532,6 +537,7 @@ fieldset-group {
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-field__quick-action {
|
.form-field__quick-action {
|
||||||
|
text-align: right;
|
||||||
font-size: var(--font-xsmall);
|
font-size: var(--font-xsmall);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -73,7 +73,7 @@ body {
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar--pusher--open {
|
.sidebar--pusher--open {
|
||||||
@media (min-width: $breakpoint-medium) {
|
@media not all and (max-width: $breakpoint-medium) {
|
||||||
transform: scaleX(0.9) translateX(calc(5.4 * var(--spacing-l))) scaleY(0.9);
|
transform: scaleX(0.9) translateX(calc(5.4 * var(--spacing-l))) scaleY(0.9);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -155,7 +155,7 @@ body {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: $breakpoint-medium) {
|
@media not all and (max-width: $breakpoint-medium) {
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
}
|
}
|
||||||
@media (max-width: $breakpoint-medium) {
|
@media (max-width: $breakpoint-medium) {
|
||||||
|
@ -461,7 +461,6 @@ 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 {
|
||||||
|
@ -804,7 +803,7 @@ body {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: $breakpoint-medium) {
|
@media not all and (max-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;
|
||||||
|
|
|
@ -70,7 +70,7 @@
|
||||||
color: var(--color-brand-contrast) !important;
|
color: var(--color-brand-contrast) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: $breakpoint-medium) {
|
@media not all and (max-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 (min-width: $breakpoint-medium) {
|
@media not all and (max-width: $breakpoint-medium) {
|
||||||
text-align: left;
|
text-align: left;
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
|
|
||||||
|
|
|
@ -32,13 +32,17 @@ $contentMaxWidth: 60rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.commentCreate {
|
.comment-create {
|
||||||
border-top: 1px solid var(--color-border);
|
border-top: 1px solid var(--color-border);
|
||||||
padding-top: var(--spacing-s);
|
padding-top: var(--spacing-s);
|
||||||
|
|
||||||
.commentCreate__label {
|
.comment-create__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) {
|
||||||
|
@ -81,7 +85,7 @@ $contentMaxWidth: 60rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.commentCreate,
|
.comment-create,
|
||||||
.comment__content {
|
.comment__content {
|
||||||
margin: var(--spacing-m);
|
margin: var(--spacing-m);
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
|
|
|
@ -85,7 +85,7 @@
|
||||||
border-radius: 0 !important;
|
border-radius: 0 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card__main-actions .commentCreate .MuiOutlinedInput-notchedOutline {
|
.card__main-actions .comment-create .MuiOutlinedInput-notchedOutline {
|
||||||
border: 1px solid var(--color-border) !important;
|
border: 1px solid var(--color-border) !important;
|
||||||
border-radius: var(--border-radius) !important;
|
border-radius: var(--border-radius) !important;
|
||||||
}
|
}
|
||||||
|
@ -104,7 +104,7 @@
|
||||||
|
|
||||||
textarea {
|
textarea {
|
||||||
border: none;
|
border: none;
|
||||||
margin: 9px 0px;
|
padding: var(--spacing-xxs) var(--spacing-xxs);
|
||||||
}
|
}
|
||||||
|
|
||||||
button {
|
button {
|
||||||
|
@ -320,7 +320,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: $breakpoint-small) {
|
@media (max-width: $breakpoint-small) {
|
||||||
.commentCreate {
|
.comment-create {
|
||||||
.section__actions {
|
.section__actions {
|
||||||
.button {
|
.button {
|
||||||
background-color: var(--color-header-button);
|
background-color: var(--color-header-button);
|
||||||
|
|
|
@ -358,7 +358,7 @@
|
||||||
max-width: unset;
|
max-width: unset;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: $breakpoint-medium) {
|
@media not all and (max-width: $breakpoint-medium) {
|
||||||
width: 40%;
|
width: 40%;
|
||||||
|
|
||||||
.button,
|
.button,
|
||||||
|
@ -375,7 +375,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings__row--value--multirow {
|
.settings__row--value--multirow {
|
||||||
@media (min-width: $breakpoint-medium) {
|
@media not all and (max-width: $breakpoint-medium) {
|
||||||
width: 80%;
|
width: 80%;
|
||||||
margin-top: var(--spacing-l);
|
margin-top: var(--spacing-l);
|
||||||
|
|
||||||
|
@ -389,7 +389,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings__row--value--vertical-separator {
|
.settings__row--value--vertical-separator {
|
||||||
@media (min-width: $breakpoint-medium) {
|
@media not all and (max-width: $breakpoint-medium) {
|
||||||
border-left: 1px solid var(--color-border);
|
border-left: 1px solid var(--color-border);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -62,6 +62,9 @@ $spacing-width: 36px;
|
||||||
--floating-viewer-container-height: calc(var(--floating-viewer-height) + var(--floating-viewer-info-height));
|
--floating-viewer-container-height: calc(var(--floating-viewer-height) + var(--floating-viewer-info-height));
|
||||||
--option-select-width: 8rem;
|
--option-select-width: 8rem;
|
||||||
|
|
||||||
|
--input-select-server-min-width: 100px;
|
||||||
|
--input-select-server-max-width: 250px;
|
||||||
|
|
||||||
// Text
|
// Text
|
||||||
--text-max-width: 660px;
|
--text-max-width: 660px;
|
||||||
--text-link-padding: 4px;
|
--text-link-padding: 4px;
|
||||||
|
|
|
@ -95,7 +95,7 @@ let baseConfig = {
|
||||||
},
|
},
|
||||||
|
|
||||||
plugins: [
|
plugins: [
|
||||||
new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/),
|
new webpack.IgnorePlugin({resourceRegExp: /^\.\/locale$/, contextRegExp: /moment$/}),
|
||||||
new webpack.EnvironmentPlugin(['NODE_ENV']),
|
new webpack.EnvironmentPlugin(['NODE_ENV']),
|
||||||
new DefinePlugin({
|
new DefinePlugin({
|
||||||
__static: `"${path.join(__dirname, 'static').replace(/\\/g, '\\\\')}"`,
|
__static: `"${path.join(__dirname, 'static').replace(/\\/g, '\\\\')}"`,
|
||||||
|
|
175
yarn.lock
175
yarn.lock
|
@ -3080,6 +3080,24 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"@videojs/http-streaming@npm:2.15.0":
|
||||||
|
version: 2.15.0
|
||||||
|
resolution: "@videojs/http-streaming@npm:2.15.0"
|
||||||
|
dependencies:
|
||||||
|
"@babel/runtime": ^7.12.5
|
||||||
|
"@videojs/vhs-utils": 3.0.5
|
||||||
|
aes-decrypter: 3.1.3
|
||||||
|
global: ^4.4.0
|
||||||
|
m3u8-parser: 4.8.0
|
||||||
|
mpd-parser: 0.22.0
|
||||||
|
mux.js: 6.0.1
|
||||||
|
video.js: ^6 || ^7
|
||||||
|
peerDependencies:
|
||||||
|
video.js: ^6 || ^7
|
||||||
|
checksum: 3b04c78c42532419216abb0eae685bfa873aeea77a015875d1cfcc0e5f4eda7da3868e6362320b2843562ba735f728a9b7895384d01c673bc3845e3ef7ac3452
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@videojs/vhs-utils@npm:3.0.5, @videojs/vhs-utils@npm:^3.0.4, @videojs/vhs-utils@npm:^3.0.5":
|
"@videojs/vhs-utils@npm:3.0.5, @videojs/vhs-utils@npm:^3.0.4, @videojs/vhs-utils@npm:^3.0.5":
|
||||||
version: 3.0.5
|
version: 3.0.5
|
||||||
resolution: "@videojs/vhs-utils@npm:3.0.5"
|
resolution: "@videojs/vhs-utils@npm:3.0.5"
|
||||||
|
@ -3291,9 +3309,9 @@ __metadata:
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@xmldom/xmldom@npm:^0.7.2":
|
"@xmldom/xmldom@npm:^0.7.2":
|
||||||
version: 0.7.5
|
version: 0.7.6
|
||||||
resolution: "@xmldom/xmldom@npm:0.7.5"
|
resolution: "@xmldom/xmldom@npm:0.7.6"
|
||||||
checksum: 8d7ec35c1ef6183b4f621df08e01d7e61f244fb964a4719025e65fe6ac06fac418919be64fb40fe5908e69158ef728f2d936daa082db326fe04603012b5f2a84
|
checksum: 3c31dcd909aaefd65090033bd45e0aca42d636a9ae43c7313f4be87de570d046abba28362810d63c0718d6f08a9ce5f8da27b87437afc24d2290b6d4e75a6eee
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
@ -3839,8 +3857,8 @@ __metadata:
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"asar@npm:^3.0.3":
|
"asar@npm:^3.0.3":
|
||||||
version: 3.1.0
|
version: 3.2.0
|
||||||
resolution: "asar@npm:3.1.0"
|
resolution: "asar@npm:3.2.0"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@types/glob": ^7.1.1
|
"@types/glob": ^7.1.1
|
||||||
chromium-pickle-js: ^0.2.0
|
chromium-pickle-js: ^0.2.0
|
||||||
|
@ -3852,7 +3870,7 @@ __metadata:
|
||||||
optional: true
|
optional: true
|
||||||
bin:
|
bin:
|
||||||
asar: bin/asar.js
|
asar: bin/asar.js
|
||||||
checksum: facc80845639fa4f9e1d1aa40b96adbd1e8b6fee0725d287e8c8e30a69b235cd5b7131b7b09ff700da06c919dd0595b373e372c55722808f983fdb71ef0d5399
|
checksum: f7d30b45970b053252ac124230bf319459d0728d7f6dedbe2f765cd2a83792d5a716d2c3f2861ceda69372b401f335e1f46460335169eadd0e91a0904a4f5a15
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
@ -5297,6 +5315,17 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"cliui@npm:^8.0.1":
|
||||||
|
version: 8.0.1
|
||||||
|
resolution: "cliui@npm:8.0.1"
|
||||||
|
dependencies:
|
||||||
|
string-width: ^4.2.0
|
||||||
|
strip-ansi: ^6.0.1
|
||||||
|
wrap-ansi: ^7.0.0
|
||||||
|
checksum: 79648b3b0045f2e285b76fb2e24e207c6db44323581e421c3acbd0e86454cba1b37aea976ab50195a49e7384b871e6dfb2247ad7dec53c02454ac6497394cb56
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"clone-deep@npm:^4.0.1":
|
"clone-deep@npm:^4.0.1":
|
||||||
version: 4.0.1
|
version: 4.0.1
|
||||||
resolution: "clone-deep@npm:4.0.1"
|
resolution: "clone-deep@npm:4.0.1"
|
||||||
|
@ -7976,6 +8005,21 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"execa@npm:^0.10.0":
|
||||||
|
version: 0.10.0
|
||||||
|
resolution: "execa@npm:0.10.0"
|
||||||
|
dependencies:
|
||||||
|
cross-spawn: ^6.0.0
|
||||||
|
get-stream: ^3.0.0
|
||||||
|
is-stream: ^1.1.0
|
||||||
|
npm-run-path: ^2.0.0
|
||||||
|
p-finally: ^1.0.0
|
||||||
|
signal-exit: ^3.0.0
|
||||||
|
strip-eof: ^1.0.0
|
||||||
|
checksum: da132af2b209e69d79f91751ac6d15ddbb8d9414f9e5f7a53405232679a3dca00fe11eb14e0cd5c2c374a749061410a7717fcc3094f6dd779cf4d259faa58d9a
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"execa@npm:^0.7.0":
|
"execa@npm:^0.7.0":
|
||||||
version: 0.7.0
|
version: 0.7.0
|
||||||
resolution: "execa@npm:0.7.0"
|
resolution: "execa@npm:0.7.0"
|
||||||
|
@ -8290,6 +8334,15 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"ffmpeg-probe@npm:^1.0.6":
|
||||||
|
version: 1.0.6
|
||||||
|
resolution: "ffmpeg-probe@npm:1.0.6"
|
||||||
|
dependencies:
|
||||||
|
execa: ^0.10.0
|
||||||
|
checksum: fe649b2ca41bd48b521d7cc5741663d4c608d7bc596033ee9c76d4c3f5e739881a4d421bdcfa3ea60e28301eae7a85b72cd74d6266e661bccf9aea6578fcfe3c
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"figgy-pudding@npm:^3.5.1":
|
"figgy-pudding@npm:^3.5.1":
|
||||||
version: 3.5.2
|
version: 3.5.2
|
||||||
resolution: "figgy-pudding@npm:3.5.2"
|
resolution: "figgy-pudding@npm:3.5.2"
|
||||||
|
@ -9056,11 +9109,11 @@ __metadata:
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"global-dirs@npm:^3.0.0":
|
"global-dirs@npm:^3.0.0":
|
||||||
version: 3.0.0
|
version: 3.0.1
|
||||||
resolution: "global-dirs@npm:3.0.0"
|
resolution: "global-dirs@npm:3.0.1"
|
||||||
dependencies:
|
dependencies:
|
||||||
ini: 2.0.0
|
ini: 2.0.0
|
||||||
checksum: 953c17cf14bf6ee0e2100ae82a0d779934eed8a3ec5c94a7a4f37c5b3b592c31ea015fb9a15cf32484de13c79f4a814f3015152f3e1d65976cfbe47c1bfe4a88
|
checksum: 70147b80261601fd40ac02a104581432325c1c47329706acd773f3a6ce99bb36d1d996038c85ccacd482ad22258ec233c586b6a91535b1a116b89663d49d6438
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
@ -11432,6 +11485,7 @@ __metadata:
|
||||||
eslint-plugin-react-hooks: ^1.6.0
|
eslint-plugin-react-hooks: ^1.6.0
|
||||||
eslint-plugin-standard: ^4.0.1
|
eslint-plugin-standard: ^4.0.1
|
||||||
express: ^4.17.1
|
express: ^4.17.1
|
||||||
|
ffmpeg-probe: ^1.0.6
|
||||||
file-loader: ^4.2.0
|
file-loader: ^4.2.0
|
||||||
flow-bin: ^0.97.0
|
flow-bin: ^0.97.0
|
||||||
flow-typed: ^3.7.0
|
flow-typed: ^3.7.0
|
||||||
|
@ -12014,6 +12068,17 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"m3u8-parser@npm:4.8.0":
|
||||||
|
version: 4.8.0
|
||||||
|
resolution: "m3u8-parser@npm:4.8.0"
|
||||||
|
dependencies:
|
||||||
|
"@babel/runtime": ^7.12.5
|
||||||
|
"@videojs/vhs-utils": ^3.0.5
|
||||||
|
global: ^4.4.0
|
||||||
|
checksum: 850a8c5cacdbdc154d4fa18c79af77fdf45ef4afcb3d8a0507d04e8d9ddb5c0a9d18cb48c8e23be707d007c063038cdeab0d3de23100e5c02050a7aff6e2b2b8
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"make-dir@npm:^1.0.0":
|
"make-dir@npm:^1.0.0":
|
||||||
version: 1.3.0
|
version: 1.3.0
|
||||||
resolution: "make-dir@npm:1.3.0"
|
resolution: "make-dir@npm:1.3.0"
|
||||||
|
@ -12686,6 +12751,20 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"mpd-parser@npm:0.22.0":
|
||||||
|
version: 0.22.0
|
||||||
|
resolution: "mpd-parser@npm:0.22.0"
|
||||||
|
dependencies:
|
||||||
|
"@babel/runtime": ^7.12.5
|
||||||
|
"@videojs/vhs-utils": ^3.0.5
|
||||||
|
"@xmldom/xmldom": ^0.7.2
|
||||||
|
global: ^4.4.0
|
||||||
|
bin:
|
||||||
|
mpd-to-m3u8-json: bin/parse.js
|
||||||
|
checksum: 6266f03ec5eb9501e315ed567e70d9e1c8d114f84a8ec27b31ec5de877ab0cd80592dfb058e9abd7975734374f6f10cec1751ab8af90b577e902f0f38f868b6c
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"ms@npm:2.0.0":
|
"ms@npm:2.0.0":
|
||||||
version: 2.0.0
|
version: 2.0.0
|
||||||
resolution: "ms@npm:2.0.0"
|
resolution: "ms@npm:2.0.0"
|
||||||
|
@ -16347,7 +16426,7 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"semver@npm:^7.1.3, semver@npm:^7.2.1, semver@npm:^7.3.2, semver@npm:^7.3.4, semver@npm:^7.3.5":
|
"semver@npm:^7.1.3, semver@npm:^7.2.1, semver@npm:^7.3.2, semver@npm:^7.3.5":
|
||||||
version: 7.3.7
|
version: 7.3.7
|
||||||
resolution: "semver@npm:7.3.7"
|
resolution: "semver@npm:7.3.7"
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -16358,6 +16437,17 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"semver@npm:^7.3.4":
|
||||||
|
version: 7.3.8
|
||||||
|
resolution: "semver@npm:7.3.8"
|
||||||
|
dependencies:
|
||||||
|
lru-cache: ^6.0.0
|
||||||
|
bin:
|
||||||
|
semver: bin/semver.js
|
||||||
|
checksum: ba9c7cbbf2b7884696523450a61fee1a09930d888b7a8d7579025ad93d459b2d1949ee5bbfeb188b2be5f4ac163544c5e98491ad6152df34154feebc2cc337c1
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"send@npm:0.18.0":
|
"send@npm:0.18.0":
|
||||||
version: 0.18.0
|
version: 0.18.0
|
||||||
resolution: "send@npm:0.18.0"
|
resolution: "send@npm:0.18.0"
|
||||||
|
@ -17598,7 +17688,20 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"terser@npm:^4.1.2, terser@npm:^4.6.12, terser@npm:^4.6.3":
|
"terser@npm:^4.1.2":
|
||||||
|
version: 4.8.1
|
||||||
|
resolution: "terser@npm:4.8.1"
|
||||||
|
dependencies:
|
||||||
|
commander: ^2.20.0
|
||||||
|
source-map: ~0.6.1
|
||||||
|
source-map-support: ~0.5.12
|
||||||
|
bin:
|
||||||
|
terser: bin/terser
|
||||||
|
checksum: b342819bf7e82283059aaa3f22bb74deb1862d07573ba5a8947882190ad525fd9b44a15074986be083fd379c58b9a879457a330b66dcdb77b485c44267f9a55a
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
|
"terser@npm:^4.6.12, terser@npm:^4.6.3":
|
||||||
version: 4.8.0
|
version: 4.8.0
|
||||||
resolution: "terser@npm:4.8.0"
|
resolution: "terser@npm:4.8.0"
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -18713,7 +18816,28 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"video.js@npm:>=5.20.5, video.js@npm:^6 || ^7, video.js@npm:^7.14.3":
|
"video.js@npm:>=5.20.5":
|
||||||
|
version: 7.21.0
|
||||||
|
resolution: "video.js@npm:7.21.0"
|
||||||
|
dependencies:
|
||||||
|
"@babel/runtime": ^7.12.5
|
||||||
|
"@videojs/http-streaming": 2.15.0
|
||||||
|
"@videojs/vhs-utils": ^3.0.4
|
||||||
|
"@videojs/xhr": 2.6.0
|
||||||
|
aes-decrypter: 3.1.3
|
||||||
|
global: ^4.4.0
|
||||||
|
keycode: ^2.2.0
|
||||||
|
m3u8-parser: 4.8.0
|
||||||
|
mpd-parser: 0.22.0
|
||||||
|
mux.js: 6.0.1
|
||||||
|
safe-json-parse: 4.0.0
|
||||||
|
videojs-font: 3.2.0
|
||||||
|
videojs-vtt.js: ^0.15.4
|
||||||
|
checksum: 9eea846bb610daca8b926ff9d0662bedf2d36063e312222c3af7e95494de09b8ce3fd0592082853e7aaaf9c92b8e37e958a41332e241376d88c4970ee717e3ab
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
|
"video.js@npm:^6 || ^7, video.js@npm:^7.14.3":
|
||||||
version: 7.19.2
|
version: 7.19.2
|
||||||
resolution: "video.js@npm:7.19.2"
|
resolution: "video.js@npm:7.19.2"
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -18771,6 +18895,15 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"videojs-vtt.js@npm:^0.15.4":
|
||||||
|
version: 0.15.4
|
||||||
|
resolution: "videojs-vtt.js@npm:0.15.4"
|
||||||
|
dependencies:
|
||||||
|
global: ^4.3.1
|
||||||
|
checksum: 10c6c861621d4314e7d4b60b7bef1afc60f1ac438879f6b3f22e8944d694c8e9dfc809a8187ed72f44e06c39a159044d8fa15e80695b9bf7b9bef99ea2740b70
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"villain-react@npm:^1.0.9":
|
"villain-react@npm:^1.0.9":
|
||||||
version: 1.0.9
|
version: 1.0.9
|
||||||
resolution: "villain-react@npm:1.0.9"
|
resolution: "villain-react@npm:1.0.9"
|
||||||
|
@ -19428,10 +19561,10 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"yargs-parser@npm:^21.0.0":
|
"yargs-parser@npm:^21.1.1":
|
||||||
version: 21.0.1
|
version: 21.1.1
|
||||||
resolution: "yargs-parser@npm:21.0.1"
|
resolution: "yargs-parser@npm:21.1.1"
|
||||||
checksum: c3ea2ed12cad0377ce3096b3f138df8267edf7b1aa7d710cd502fe16af417bafe4443dd71b28158c22fcd1be5dfd0e86319597e47badf42ff83815485887323a
|
checksum: ed2d96a616a9e3e1cc7d204c62ecc61f7aaab633dcbfab2c6df50f7f87b393993fe6640d017759fe112d0cb1e0119f2b4150a87305cc873fd90831c6a58ccf1c
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
@ -19488,17 +19621,17 @@ __metadata:
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"yargs@npm:^17.0.1":
|
"yargs@npm:^17.0.1":
|
||||||
version: 17.5.1
|
version: 17.6.2
|
||||||
resolution: "yargs@npm:17.5.1"
|
resolution: "yargs@npm:17.6.2"
|
||||||
dependencies:
|
dependencies:
|
||||||
cliui: ^7.0.2
|
cliui: ^8.0.1
|
||||||
escalade: ^3.1.1
|
escalade: ^3.1.1
|
||||||
get-caller-file: ^2.0.5
|
get-caller-file: ^2.0.5
|
||||||
require-directory: ^2.1.1
|
require-directory: ^2.1.1
|
||||||
string-width: ^4.2.3
|
string-width: ^4.2.3
|
||||||
y18n: ^5.0.5
|
y18n: ^5.0.5
|
||||||
yargs-parser: ^21.0.0
|
yargs-parser: ^21.1.1
|
||||||
checksum: 00d58a2c052937fa044834313f07910fd0a115dec5ee35919e857eeee3736b21a4eafa8264535800ba8bac312991ce785ecb8a51f4d2cc8c4676d865af1cfbde
|
checksum: 47da1b0d854fa16d45a3ded57b716b013b2179022352a5f7467409da5a04a1eef5b3b3d97a2dfc13e8bbe5f2ffc0afe3bc6a4a72f8254e60f5a4bd7947138643
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue