From 1ebc8f74c5bfc80941aa4bdfa363194907c82a13 Mon Sep 17 00:00:00 2001 From: Anthony Date: Mon, 13 Sep 2021 23:03:19 +0200 Subject: [PATCH 01/39] adding functionality to detect user download speed --- ui/analytics.js | 11 ++++++++- ui/redux/actions/app.js | 1 + ui/util/detect-user-bandwidth.js | 38 ++++++++++++++++++++++++++++++++ 3 files changed, 49 insertions(+), 1 deletion(-) create mode 100644 ui/util/detect-user-bandwidth.js diff --git a/ui/analytics.js b/ui/analytics.js index 495535dcc..505fb1180 100644 --- a/ui/analytics.js +++ b/ui/analytics.js @@ -10,6 +10,13 @@ import ElectronCookies from '@exponent/electron-cookies'; import { generateInitialUrl } from 'util/url'; // @endif import { MATOMO_ID, MATOMO_URL } from 'config'; +import getConnectionSpeed from 'util/detect-user-bandwidth'; + +let downloadSpeed; +getConnectionSpeed(function(speedInMbps){ + downloadSpeed = speedInMbps; + console.log(downloadSpeed); +}); const isProduction = process.env.NODE_ENV === 'production'; const devInternalApis = process.env.LBRY_API_URL && process.env.LBRY_API_URL.includes('dev'); @@ -202,8 +209,10 @@ async function sendWatchmanData(body) { } const analytics: Analytics = { - // receive buffer events from tracking plugin and jklj + // receive buffer events from tracking plugin and save buffer amounts and times for backend call videoBufferEvent: async (claim, data) => { + console.log('running here!'); + console.log(data); amountOfBufferEvents = amountOfBufferEvents + 1; amountOfBufferTimeInMS = amountOfBufferTimeInMS + data.bufferDuration; }, diff --git a/ui/redux/actions/app.js b/ui/redux/actions/app.js index 67a00c4a0..cb0b81332 100644 --- a/ui/redux/actions/app.js +++ b/ui/redux/actions/app.js @@ -505,6 +505,7 @@ export function doAnalyticsBuffer(uri, bufferData) { const fileSizeInBits = fileSize * 8; const bitRate = parseInt(fileSizeInBits / fileDurationInSeconds); const userId = user && user.id.toString(); + // if there's a logged in user, send buffer event data to watchman if (userId) { analytics.videoBufferEvent(claim, { timeAtBuffer, diff --git a/ui/util/detect-user-bandwidth.js b/ui/util/detect-user-bandwidth.js new file mode 100644 index 000000000..f14c82523 --- /dev/null +++ b/ui/util/detect-user-bandwidth.js @@ -0,0 +1,38 @@ +var startTime; +var endTime; + +var testConnectionSpeed = { + imageAddr: 'https://upload.wikimedia.org/wikipedia/commons/a/a6/Brandenburger_Tor_abends.jpg', // this is just an example, you rather want an image hosted on your server + downloadSize: 2707459, // this must match with the image above + + getConnectionSpeed: function(callback) { + testConnectionSpeed.InitiateSpeedDetection(); + testConnectionSpeed.callback = callback; + }, + InitiateSpeedDetection: function() { + window.setTimeout(testConnectionSpeed.MeasureConnectionSpeed, 1); + }, + result: function() { + var duration = (endTime - startTime) / 1000; + var bitsLoaded = testConnectionSpeed.downloadSize * 8; + var speedBps = (bitsLoaded / duration).toFixed(2); + var speedKbps = (speedBps / 1024).toFixed(2); + var speedMbps = (speedKbps / 1024).toFixed(2); + testConnectionSpeed.callback(speedMbps); + }, + MeasureConnectionSpeed: function() { + var download = new Image(); + download.onload = function() { + endTime = (new Date()).getTime(); + testConnectionSpeed.result(); + }; + startTime = (new Date()).getTime(); + var cacheBuster = '?nnn=' + startTime; + download.src = testConnectionSpeed.imageAddr + cacheBuster; + }, +}; + +// start test immediatly, you could also call this on any event or whenever you want +// testConnectionSpeed.getConnectionSpeed(function(time) { console.log(time) }); + +module.exports = testConnectionSpeed.getConnectionSpeed; -- 2.45.2 From e6e7e7bcbca82f055c7a629d33677eba44d3c27a Mon Sep 17 00:00:00 2001 From: Anthony Date: Tue, 21 Sep 2021 18:42:31 +0300 Subject: [PATCH 02/39] calculating bandwidth speed more intelligently --- ui/util/detect-user-bandwidth.js | 42 ++++++++++++++++++++++++++++++-- 1 file changed, 40 insertions(+), 2 deletions(-) diff --git a/ui/util/detect-user-bandwidth.js b/ui/util/detect-user-bandwidth.js index f14c82523..bc41d3e62 100644 --- a/ui/util/detect-user-bandwidth.js +++ b/ui/util/detect-user-bandwidth.js @@ -1,5 +1,5 @@ -var startTime; -var endTime; +// var startTime; +// var endTime; var testConnectionSpeed = { imageAddr: 'https://upload.wikimedia.org/wikipedia/commons/a/a6/Brandenburger_Tor_abends.jpg', // this is just an example, you rather want an image hosted on your server @@ -12,6 +12,7 @@ var testConnectionSpeed = { InitiateSpeedDetection: function() { window.setTimeout(testConnectionSpeed.MeasureConnectionSpeed, 1); }, + result: function() { var duration = (endTime - startTime) / 1000; var bitsLoaded = testConnectionSpeed.downloadSize * 8; @@ -20,6 +21,7 @@ var testConnectionSpeed = { var speedMbps = (speedKbps / 1024).toFixed(2); testConnectionSpeed.callback(speedMbps); }, + MeasureConnectionSpeed: function() { var download = new Image(); download.onload = function() { @@ -32,6 +34,42 @@ var testConnectionSpeed = { }, }; +function timeout(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); +} + +const imageAddr = 'https://upload.wikimedia.org/wikipedia/commons/a/a6/Brandenburger_Tor_abends.jpg'; +const downloadSize = 2707459; // this must match with the image above + +let startTime, endTime; +function measureConnectionSpeed() { + startTime = (new Date()).getTime(); + var cacheBuster = '?nnn=' + startTime; + + var download = new Image(); + download.src = imageAddr + cacheBuster; + + download.onload = function() { + endTime = (new Date()).getTime(); + var duration = (endTime - startTime) / 1000; + var bitsLoaded = downloadSize * 8; + var speedBps = (bitsLoaded / duration).toFixed(2); + var speedKbps = (speedBps / 1024).toFixed(2); + var speedMbps = (speedKbps / 1024).toFixed(2); + console.log(speedMbps); + return new Promise(resolve => resolve(speedMbps)); + // return speedMbps; + }; + + +} + +async function getDownloadSpeed() { + await timeout(1); + const downloadSpeed = await measureConnectionSpeed(); + console.log(downloadSpeed); +} + // start test immediatly, you could also call this on any event or whenever you want // testConnectionSpeed.getConnectionSpeed(function(time) { console.log(time) }); -- 2.45.2 From 50013d26759ab1e8106b484ba1bd9553d9dbc381 Mon Sep 17 00:00:00 2001 From: Anthony Date: Tue, 21 Sep 2021 19:07:57 +0300 Subject: [PATCH 03/39] saving download speed and updating it every 30s --- ui/analytics.js | 12 +++-- ui/util/detect-user-bandwidth.js | 80 ++++++-------------------------- 2 files changed, 20 insertions(+), 72 deletions(-) diff --git a/ui/analytics.js b/ui/analytics.js index 505fb1180..ec96f4bbf 100644 --- a/ui/analytics.js +++ b/ui/analytics.js @@ -12,11 +12,13 @@ import { generateInitialUrl } from 'util/url'; import { MATOMO_ID, MATOMO_URL } from 'config'; import getConnectionSpeed from 'util/detect-user-bandwidth'; -let downloadSpeed; -getConnectionSpeed(function(speedInMbps){ - downloadSpeed = speedInMbps; - console.log(downloadSpeed); -}); +let userDownloadBandwidth; +async function getUserBandwidth() { + userDownloadBandwidth = await getConnectionSpeed(); +} + +getUserBandwidth(); +setInterval(getUserBandwidth, 1000 * 30); const isProduction = process.env.NODE_ENV === 'production'; const devInternalApis = process.env.LBRY_API_URL && process.env.LBRY_API_URL.includes('dev'); diff --git a/ui/util/detect-user-bandwidth.js b/ui/util/detect-user-bandwidth.js index bc41d3e62..353579ee3 100644 --- a/ui/util/detect-user-bandwidth.js +++ b/ui/util/detect-user-bandwidth.js @@ -1,76 +1,22 @@ -// var startTime; -// var endTime; - -var testConnectionSpeed = { - imageAddr: 'https://upload.wikimedia.org/wikipedia/commons/a/a6/Brandenburger_Tor_abends.jpg', // this is just an example, you rather want an image hosted on your server - downloadSize: 2707459, // this must match with the image above - - getConnectionSpeed: function(callback) { - testConnectionSpeed.InitiateSpeedDetection(); - testConnectionSpeed.callback = callback; - }, - InitiateSpeedDetection: function() { - window.setTimeout(testConnectionSpeed.MeasureConnectionSpeed, 1); - }, - - result: function() { - var duration = (endTime - startTime) / 1000; - var bitsLoaded = testConnectionSpeed.downloadSize * 8; - var speedBps = (bitsLoaded / duration).toFixed(2); - var speedKbps = (speedBps / 1024).toFixed(2); - var speedMbps = (speedKbps / 1024).toFixed(2); - testConnectionSpeed.callback(speedMbps); - }, - - MeasureConnectionSpeed: function() { - var download = new Image(); - download.onload = function() { - endTime = (new Date()).getTime(); - testConnectionSpeed.result(); - }; - startTime = (new Date()).getTime(); - var cacheBuster = '?nnn=' + startTime; - download.src = testConnectionSpeed.imageAddr + cacheBuster; - }, -}; - -function timeout(ms) { - return new Promise(resolve => setTimeout(resolve, ms)); -} - const imageAddr = 'https://upload.wikimedia.org/wikipedia/commons/a/a6/Brandenburger_Tor_abends.jpg'; const downloadSize = 2707459; // this must match with the image above let startTime, endTime; -function measureConnectionSpeed() { +async function measureConnectionSpeed() { startTime = (new Date()).getTime(); - var cacheBuster = '?nnn=' + startTime; + const cacheBuster = '?nnn=' + startTime; - var download = new Image(); + const download = new Image(); download.src = imageAddr + cacheBuster; - - download.onload = function() { - endTime = (new Date()).getTime(); - var duration = (endTime - startTime) / 1000; - var bitsLoaded = downloadSize * 8; - var speedBps = (bitsLoaded / duration).toFixed(2); - var speedKbps = (speedBps / 1024).toFixed(2); - var speedMbps = (speedKbps / 1024).toFixed(2); - console.log(speedMbps); - return new Promise(resolve => resolve(speedMbps)); - // return speedMbps; - }; - - + // this returns when the image is finished downloading + await download.decode(); + endTime = (new Date()).getTime(); + const duration = (endTime - startTime) / 1000; + const bitsLoaded = downloadSize * 8; + const speedBps = (bitsLoaded / duration).toFixed(2); + const speedKbps = (speedBps / 1024).toFixed(2); + const speedMbps = (speedKbps / 1024).toFixed(2); + return speedMbps; } -async function getDownloadSpeed() { - await timeout(1); - const downloadSpeed = await measureConnectionSpeed(); - console.log(downloadSpeed); -} - -// start test immediatly, you could also call this on any event or whenever you want -// testConnectionSpeed.getConnectionSpeed(function(time) { console.log(time) }); - -module.exports = testConnectionSpeed.getConnectionSpeed; +module.exports = measureConnectionSpeed; -- 2.45.2 From a9acadfbc988d4e352b9d234f5ecb88e498a8dfd Mon Sep 17 00:00:00 2001 From: Anthony Date: Tue, 21 Sep 2021 20:36:59 +0300 Subject: [PATCH 04/39] all the functionality should be done needs testing --- ui/analytics.js | 13 +++++++------ ui/component/viewers/videoViewer/view.jsx | 10 +++++++++- ui/util/detect-user-bandwidth.js | 4 +--- 3 files changed, 17 insertions(+), 10 deletions(-) diff --git a/ui/analytics.js b/ui/analytics.js index ec96f4bbf..49cc48653 100644 --- a/ui/analytics.js +++ b/ui/analytics.js @@ -12,9 +12,9 @@ import { generateInitialUrl } from 'util/url'; import { MATOMO_ID, MATOMO_URL } from 'config'; import getConnectionSpeed from 'util/detect-user-bandwidth'; -let userDownloadBandwidth; +let userDownloadBandwidthInBitsPerSecond; async function getUserBandwidth() { - userDownloadBandwidth = await getConnectionSpeed(); + userDownloadBandwidthInBitsPerSecond = await getConnectionSpeed(); } getUserBandwidth(); @@ -120,7 +120,7 @@ function getDeviceType() { // variables initialized for watchman let amountOfBufferEvents = 0; let amountOfBufferTimeInMS = 0; -let videoType, userId, claimUrl, playerPoweredBy, videoPlayer; +let videoType, userId, claimUrl, playerPoweredBy, videoPlayer, bitrateAsBitsPerSecond; let lastSentTime; // calculate data for backend, send them, and reset buffer data for next interval @@ -161,6 +161,8 @@ async function sendAndResetWatchmanData() { user_id: userId.toString(), position: Math.round(positionInVideo), rel_position: Math.round((positionInVideo / (totalDurationInSeconds * 1000)) * 100), + ...(userDownloadBandwidthInBitsPerSecond && {bandwidth: userDownloadBandwidthInBitsPerSecond}), // add bandwidth if populated + ...(bitrateAsBitsPerSecond && {bitrate: bitrateAsBitsPerSecond}), // add bitrate if video (audio doesn't work) }; // post to watchman @@ -213,8 +215,6 @@ async function sendWatchmanData(body) { const analytics: Analytics = { // receive buffer events from tracking plugin and save buffer amounts and times for backend call videoBufferEvent: async (claim, data) => { - console.log('running here!'); - console.log(data); amountOfBufferEvents = amountOfBufferEvents + 1; amountOfBufferTimeInMS = amountOfBufferTimeInMS + data.bufferDuration; }, @@ -251,7 +251,7 @@ const analytics: Analytics = { startWatchmanIntervalIfNotRunning(); } }, - videoStartEvent: (claimId, duration, poweredBy, passedUserId, canonicalUrl, passedPlayer) => { + videoStartEvent: (claimId, duration, poweredBy, passedUserId, canonicalUrl, passedPlayer, videoBitrate) => { // populate values for watchman when video starts userId = passedUserId; claimUrl = canonicalUrl; @@ -259,6 +259,7 @@ const analytics: Analytics = { videoType = passedPlayer.currentSource().type; videoPlayer = passedPlayer; + bitrateAsBitsPerSecond = videoBitrate; sendPromMetric('time_to_start', duration); sendMatomoEvent('Media', 'TimeToStart', claimId, duration); diff --git a/ui/component/viewers/videoViewer/view.jsx b/ui/component/viewers/videoViewer/view.jsx index efe534dd1..5619065f9 100644 --- a/ui/component/viewers/videoViewer/view.jsx +++ b/ui/component/viewers/videoViewer/view.jsx @@ -168,9 +168,17 @@ function VideoViewer(props: Props) { } analytics.playerStartedEvent(embedded); + // convert bytes to bits, and then divide by seconds + const contentInBits = Number(claim.value.source.size) * 8; + const durationInSeconds = claim.value.video && claim.value.video.duration; + let bitrateAsBitsPerSecond; + if (durationInSeconds) { + bitrateAsBitsPerSecond = Math.round(contentInBits / durationInSeconds); + } + fetch(source, { method: 'HEAD', cache: 'no-store' }).then((response) => { let playerPoweredBy = response.headers.get('x-powered-by') || ''; - analytics.videoStartEvent(claimId, timeToStart, playerPoweredBy, userId, claim.canonical_url, this); + analytics.videoStartEvent(claimId, timeToStart, playerPoweredBy, userId, claim.canonical_url, this, bitrateAsBitsPerSecond); }); doAnalyticsView(uri, timeToStart).then(() => { diff --git a/ui/util/detect-user-bandwidth.js b/ui/util/detect-user-bandwidth.js index 353579ee3..0610190fe 100644 --- a/ui/util/detect-user-bandwidth.js +++ b/ui/util/detect-user-bandwidth.js @@ -14,9 +14,7 @@ async function measureConnectionSpeed() { const duration = (endTime - startTime) / 1000; const bitsLoaded = downloadSize * 8; const speedBps = (bitsLoaded / duration).toFixed(2); - const speedKbps = (speedBps / 1024).toFixed(2); - const speedMbps = (speedKbps / 1024).toFixed(2); - return speedMbps; + return Math.round(Number(speedBps)); } module.exports = measureConnectionSpeed; -- 2.45.2 From e8dc4b94f3e1ab0f02a43a57f1a7f3a1454cc4ad Mon Sep 17 00:00:00 2001 From: Anthony Date: Wed, 22 Sep 2021 18:46:49 +0300 Subject: [PATCH 05/39] fix linting --- ui/analytics.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/analytics.js b/ui/analytics.js index 49cc48653..de25c7f5f 100644 --- a/ui/analytics.js +++ b/ui/analytics.js @@ -49,7 +49,7 @@ type Analytics = { tagFollowEvent: (string, boolean, ?string) => void, playerLoadedEvent: (?boolean) => void, playerStartedEvent: (?boolean) => void, - videoStartEvent: (string, number, string, number, string, any) => void, + videoStartEvent: (string, number, string, number, string, any, number) => void, videoIsPlaying: (boolean, any) => void, videoBufferEvent: ( StreamClaim, -- 2.45.2 From 9b5111d86cb6c70517a49e1a68a063889baade43 Mon Sep 17 00:00:00 2001 From: Anthony Date: Mon, 27 Sep 2021 16:43:26 +0300 Subject: [PATCH 06/39] use a 1mb file for calculating bandwidth --- ui/analytics.js | 5 +++-- ui/util/detect-user-bandwidth.js | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/ui/analytics.js b/ui/analytics.js index de25c7f5f..39ec740af 100644 --- a/ui/analytics.js +++ b/ui/analytics.js @@ -17,8 +17,8 @@ async function getUserBandwidth() { userDownloadBandwidthInBitsPerSecond = await getConnectionSpeed(); } -getUserBandwidth(); -setInterval(getUserBandwidth, 1000 * 30); +// get user bandwidth every minute, starting after an initial one minute wait +setInterval(getUserBandwidth, 1000 * 60); const isProduction = process.env.NODE_ENV === 'production'; const devInternalApis = process.env.LBRY_API_URL && process.env.LBRY_API_URL.includes('dev'); @@ -136,6 +136,7 @@ async function sendAndResetWatchmanData() { let timeSinceLastIntervalSend = new Date() - lastSentTime; lastSentTime = new Date(); + // TODO: hook into here to calculate bandwidth let protocol; if (videoType === 'application/x-mpegURL') { protocol = 'hls'; diff --git a/ui/util/detect-user-bandwidth.js b/ui/util/detect-user-bandwidth.js index 0610190fe..fd3f0d187 100644 --- a/ui/util/detect-user-bandwidth.js +++ b/ui/util/detect-user-bandwidth.js @@ -1,5 +1,5 @@ -const imageAddr = 'https://upload.wikimedia.org/wikipedia/commons/a/a6/Brandenburger_Tor_abends.jpg'; -const downloadSize = 2707459; // this must match with the image above +const imageAddr = 'https://upload.wikimedia.org/wikipedia/commons/b/b9/Pizigani_1367_Chart_1MB.jpg'; +const downloadSize = 1093957; // this must match with the image above let startTime, endTime; async function measureConnectionSpeed() { -- 2.45.2 From af416b288d04590bbbdd9f7a9b9b46d1be6a9dee Mon Sep 17 00:00:00 2001 From: Anthony Date: Mon, 27 Sep 2021 17:02:45 +0300 Subject: [PATCH 07/39] add optional chaining plugin to babel and get bitrate from texttrack --- package.json | 1 + ui/analytics.js | 3 ++- web/webpack.config.js | 2 +- yarn.lock | 36 +++++++++++++++++++++++++++++++++++- 4 files changed, 39 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index f1545afa3..6826b3296 100644 --- a/package.json +++ b/package.json @@ -75,6 +75,7 @@ "@babel/plugin-proposal-class-properties": "^7.0.0", "@babel/plugin-proposal-decorators": "^7.3.0", "@babel/plugin-proposal-object-rest-spread": "^7.6.2", + "@babel/plugin-proposal-optional-chaining": "^7.14.5", "@babel/plugin-syntax-dynamic-import": "^7.2.0", "@babel/plugin-transform-flow-strip-types": "^7.2.3", "@babel/plugin-transform-runtime": "^7.4.3", diff --git a/ui/analytics.js b/ui/analytics.js index 39ec740af..47eaa8d12 100644 --- a/ui/analytics.js +++ b/ui/analytics.js @@ -136,10 +136,11 @@ async function sendAndResetWatchmanData() { let timeSinceLastIntervalSend = new Date() - lastSentTime; lastSentTime = new Date(); - // TODO: hook into here to calculate bandwidth 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) + bitrateAsBitsPerSecond = videoPlayer.textTracks?.().tracks_[0]?.activeCues[0]?.value?.bandwidth; } else { protocol = 'stb'; } diff --git a/web/webpack.config.js b/web/webpack.config.js index f483c6217..120747802 100644 --- a/web/webpack.config.js +++ b/web/webpack.config.js @@ -149,7 +149,7 @@ const webConfig = { test: /\.jsx?$/, options: { presets: ['@babel/env', '@babel/react', '@babel/flow'], - plugins: ['@babel/plugin-proposal-object-rest-spread', '@babel/plugin-proposal-class-properties'], + plugins: ['@babel/plugin-proposal-optional-chaining', '@babel/plugin-proposal-object-rest-spread', '@babel/plugin-proposal-class-properties'], }, }, { diff --git a/yarn.lock b/yarn.lock index 96aee291f..0234b1a2c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -329,6 +329,11 @@ resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz#2f75a831269d4f677de49986dff59927533cf375" integrity sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg== +"@babel/helper-plugin-utils@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.14.5.tgz#5ac822ce97eec46741ab70a517971e443a70c5a9" + integrity sha512-/37qQCE3K0vvZKwoK4XU/irIJQdIfCJuhU5eKnNxpFDsOkgFaUAwbv+RYw6eYgsC0E4hS7r5KqGULUogqui0fQ== + "@babel/helper-remap-async-to-generator@^7.12.1": version "7.12.1" resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.12.1.tgz#8c4dbbf916314f6047dc05e6a2217074238347fd" @@ -390,6 +395,13 @@ dependencies: "@babel/types" "^7.12.1" +"@babel/helper-skip-transparent-expression-wrappers@^7.14.5": + version "7.15.4" + resolved "https://registry.yarnpkg.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.15.4.tgz#707dbdba1f4ad0fa34f9114fc8197aec7d5da2eb" + integrity sha512-BMRLsdh+D1/aap19TycS4eD1qELGrCBJwzaY9IE8LrpJtJb+H7rQkPIdsfgnMtLBA6DJls7X9z93Z4U8h7xw0A== + dependencies: + "@babel/types" "^7.15.4" + "@babel/helper-split-export-declaration@^7.10.1": version "7.10.1" resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.10.1.tgz#c6f4be1cbc15e3a868e4c64a17d5d31d754da35f" @@ -426,6 +438,11 @@ resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz#c9a1f021917dcb5ccf0d4e453e399022981fc9ed" integrity sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw== +"@babel/helper-validator-identifier@^7.14.9": + version "7.15.7" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.15.7.tgz#220df993bfe904a4a6b02ab4f3385a5ebf6e2389" + integrity sha512-K4JvCtQqad9OY2+yTU8w+E82ywk/fe+ELNlt1G8z3bVGlZfn/hOcQQsUhGhW/N+tb3fxK800wLtKOE/aM0m72w== + "@babel/helper-validator-option@^7.12.1", "@babel/helper-validator-option@^7.12.11": version "7.12.11" resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.12.11.tgz#d66cb8b7a3e7fe4c6962b32020a131ecf0847f4f" @@ -590,6 +607,15 @@ "@babel/helper-skip-transparent-expression-wrappers" "^7.12.1" "@babel/plugin-syntax-optional-chaining" "^7.8.0" +"@babel/plugin-proposal-optional-chaining@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.14.5.tgz#fa83651e60a360e3f13797eef00b8d519695b603" + integrity sha512-ycz+VOzo2UbWNI1rQXxIuMOzrDdHGrI23fRiz/Si2R4kv2XZQ1BK8ccdHwehMKBlcH/joGW/tzrUmo67gbJHlQ== + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + "@babel/helper-skip-transparent-expression-wrappers" "^7.14.5" + "@babel/plugin-syntax-optional-chaining" "^7.8.3" + "@babel/plugin-proposal-private-methods@^7.12.1": version "7.12.1" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.12.1.tgz#86814f6e7a21374c980c10d38b4493e703f4a389" @@ -694,7 +720,7 @@ dependencies: "@babel/helper-plugin-utils" "^7.8.0" -"@babel/plugin-syntax-optional-chaining@^7.8.0": +"@babel/plugin-syntax-optional-chaining@^7.8.0", "@babel/plugin-syntax-optional-chaining@^7.8.3": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz#4f69c2ab95167e0180cd5336613f8c5788f7d48a" integrity sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg== @@ -1230,6 +1256,14 @@ lodash "^4.17.19" to-fast-properties "^2.0.0" +"@babel/types@^7.15.4": + version "7.15.6" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.15.6.tgz#99abdc48218b2881c058dd0a7ab05b99c9be758f" + integrity sha512-BPU+7QhqNjmWyDO0/vitH/CuhpV8ZmK1wpKva8nuyNF5MJfuRNWMc+hc14+u9xT93kvykMdncrJT19h74uB1Ig== + dependencies: + "@babel/helper-validator-identifier" "^7.14.9" + to-fast-properties "^2.0.0" + "@datapunt/matomo-tracker-js@^0.1.4": version "0.1.4" resolved "https://registry.yarnpkg.com/@datapunt/matomo-tracker-js/-/matomo-tracker-js-0.1.4.tgz#1226f0964d2c062bf9392e9c2fd89838262b10df" -- 2.45.2 From 256a7d3d23125ed06f9562d5ac7374037cae6f0d Mon Sep 17 00:00:00 2001 From: Anthony Date: Mon, 27 Sep 2021 17:59:03 +0300 Subject: [PATCH 08/39] allow optional chaining for flow --- .flowconfig | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.flowconfig b/.flowconfig index 2c6b8839f..0ae16f24e 100644 --- a/.flowconfig +++ b/.flowconfig @@ -43,6 +43,8 @@ module.name_mapper='^web\/page\(.*\)$' -> '/web/page\1' module.name_mapper='^homepage\(.*\)$' -> '/ui/util/homepage\1' module.name_mapper='^scss\/component\(.*\)$' -> '/ui/scss/component/\1' +esproposal.optional_chaining=enable + ; Extensions module.file_ext=.js module.file_ext=.jsx @@ -51,4 +53,5 @@ module.file_ext=.css module.file_ext=.scss + [strict] -- 2.45.2 From 6e1dfda6d311017950e421cf276e114d1af58b37 Mon Sep 17 00:00:00 2001 From: Anthony Date: Fri, 1 Oct 2021 21:56:04 +0300 Subject: [PATCH 09/39] ignore flow error --- ui/analytics.js | 1 + 1 file changed, 1 insertion(+) diff --git a/ui/analytics.js b/ui/analytics.js index 47eaa8d12..c3d46119e 100644 --- a/ui/analytics.js +++ b/ui/analytics.js @@ -140,6 +140,7 @@ async function sendAndResetWatchmanData() { 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'; -- 2.45.2 From 8b3820fb288b3eff3a6255814d294e9785de2503 Mon Sep 17 00:00:00 2001 From: infinite-persistence Date: Sat, 2 Oct 2021 08:15:27 +0800 Subject: [PATCH 10/39] --- tmp revert --- This reverts commit de6c6f9bfd0383e9087a38f1f54e13bb54754945. --- flow-typed/Comment.js | 4 +- static/app-strings.json | 4 - ui/component/claimPreview/view.jsx | 7 - ui/component/comment/view.jsx | 28 ++-- ui/component/commentsList/index.js | 2 +- ui/component/commentsList/view.jsx | 7 +- ui/component/router/view.jsx | 2 - ui/component/settingAccount/index.js | 3 +- ui/component/settingAccount/view.jsx | 14 +- ui/constants/comment.js | 1 - ui/constants/pageTitles.js | 1 - ui/constants/pages.js | 1 - ui/page/ownComments/index.js | 33 ----- ui/page/ownComments/view.jsx | 210 --------------------------- ui/redux/actions/comments.js | 131 +++-------------- ui/redux/reducers/comments.js | 6 +- ui/scss/component/_comments.scss | 57 -------- 17 files changed, 39 insertions(+), 472 deletions(-) delete mode 100644 ui/page/ownComments/index.js delete mode 100644 ui/page/ownComments/view.jsx diff --git a/flow-typed/Comment.js b/flow-typed/Comment.js index 57a666683..8b2ad570c 100644 --- a/flow-typed/Comment.js +++ b/flow-typed/Comment.js @@ -111,14 +111,14 @@ declare type ReactionListResponse = { declare type CommentListParams = { page: number, // pagination: which page of results page_size: number, // pagination: nr of comments to show in a page (max 200) - claim_id?: string, // claim id of claim being commented on + claim_id: string, // claim id of claim being commented on channel_name?: string, // signing channel name of claim (enables 'commentsEnabled' check) channel_id?: string, // signing channel claim id of claim (enables 'commentsEnabled' check) author_claim_id?: string, // filters comments to just this author parent_id?: string, // filters comments to those under this thread top_level?: boolean, // filters to only top level comments hidden?: boolean, // if true, will show hidden comments as well - sort_by?: number, // @see: ui/constants/comments.js::SORT_BY + sort_by?: number, // NEWEST=0, OLDEST=1, CONTROVERSY=2, POPULARITY=3, }; declare type CommentListResponse = { diff --git a/static/app-strings.json b/static/app-strings.json index 5baa9217f..36a82cd1a 100644 --- a/static/app-strings.json +++ b/static/app-strings.json @@ -209,9 +209,6 @@ "Failed to copy.": "Failed to copy.", "The publisher has chosen to charge %lbc% to view this content. Your balance is currently too low to view it. Check out %reward_link% for free %lbc% or send more %lbc% to your wallet. You can also %buy_link% more %lbc%.": "The publisher has chosen to charge %lbc% to view this content. Your balance is currently too low to view it. Check out %reward_link% for free %lbc% or send more %lbc% to your wallet. You can also %buy_link% more %lbc%.", "Connecting...": "Connecting...", - "Your comments": "Your comments", - "View your past comments.": "View your past comments.", - "Content or channel was deleted.": "Content or channel was deleted.", "Comments": "Comments", "Comment": "Comment", "Comment --[button to submit something]--": "Comment", @@ -1463,7 +1460,6 @@ "Staked LBRY Credits": "Staked LBRY Credits", "1 comment": "1 comment", "%total_comments% comments": "%total_comments% comments", - "No comments": "No comments", "Upvote": "Upvote", "Downvote": "Downvote", "You loved this": "You loved this", diff --git a/ui/component/claimPreview/view.jsx b/ui/component/claimPreview/view.jsx index 34e7dc188..343424d5f 100644 --- a/ui/component/claimPreview/view.jsx +++ b/ui/component/claimPreview/view.jsx @@ -70,7 +70,6 @@ type Props = { streamingUrl: ?string, getFile: (string) => void, customShouldHide?: (Claim) => boolean, - searchParams?: { [string]: string }, showUnresolvedClaim?: boolean, showNullPlaceholder?: boolean, includeSupportAction?: boolean, @@ -126,7 +125,6 @@ const ClaimPreview = forwardRef((props: Props, ref: any) => { // modifiers active, customShouldHide, - searchParams, showNullPlaceholder, // value from show mature content user setting // true if the user doesn't wanna see nsfw content @@ -223,11 +221,6 @@ const ClaimPreview = forwardRef((props: Props, ref: any) => { if (listId) { navigateSearch.set(COLLECTIONS_CONSTS.COLLECTION_ID, listId); } - if (searchParams) { - Object.keys(searchParams).forEach((key) => { - navigateSearch.set(key, searchParams[key]); - }); - } const handleNavLinkClick = (e) => { if (onClick) { diff --git a/ui/component/comment/view.jsx b/ui/component/comment/view.jsx index b4816303a..b52003bcf 100644 --- a/ui/component/comment/view.jsx +++ b/ui/component/comment/view.jsx @@ -52,7 +52,6 @@ type Props = { doToast: ({ message: string }) => void, isTopLevel?: boolean, threadDepth: number, - hideActions?: boolean, isPinned: boolean, othersReacts: ?{ like: number, @@ -96,7 +95,6 @@ function Comment(props: Props) { doToast, isTopLevel, threadDepth, - hideActions, isPinned, othersReacts, playingUri, @@ -350,20 +348,18 @@ function Comment(props: Props) { )} - {!hideActions && ( -
- {threadDepth !== 0 && ( -
- )} +
+ {threadDepth !== 0 && ( +
{numDirectReplies > 0 && !showReplies && (
diff --git a/ui/component/commentsList/index.js b/ui/component/commentsList/index.js index c83dae73b..8d15ab707 100644 --- a/ui/component/commentsList/index.js +++ b/ui/component/commentsList/index.js @@ -48,7 +48,7 @@ const perform = (dispatch) => ({ fetchTopLevelComments: (uri, page, pageSize, sortBy) => dispatch(doCommentList(uri, '', page, pageSize, sortBy)), fetchComment: (commentId) => dispatch(doCommentById(commentId)), fetchReacts: (commentIds) => dispatch(doCommentReactList(commentIds)), - resetComments: (claimId) => dispatch(doCommentReset(claimId)), + resetComments: (uri) => dispatch(doCommentReset(uri)), }); export default connect(select, perform)(CommentsList); diff --git a/ui/component/commentsList/view.jsx b/ui/component/commentsList/view.jsx index 35525d19a..6a51fd42c 100644 --- a/ui/component/commentsList/view.jsx +++ b/ui/component/commentsList/view.jsx @@ -151,13 +151,10 @@ function CommentList(props: Props) { // Reset comments useEffect(() => { if (page === 0) { - if (claim) { - resetComments(claim.claim_id); - } + resetComments(uri); setPage(1); } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [page, uri, resetComments]); // 'claim' is derived from 'uri' + }, [page, uri, resetComments]); // Fetch top-level comments useEffect(() => { diff --git a/ui/component/router/view.jsx b/ui/component/router/view.jsx index ad5234265..03db2b538 100644 --- a/ui/component/router/view.jsx +++ b/ui/component/router/view.jsx @@ -59,7 +59,6 @@ const ListBlockedPage = lazyImport(() => import('page/listBlocked' /* webpackChu const ListsPage = lazyImport(() => import('page/lists' /* webpackChunkName: "secondary" */)); const LiveStreamSetupPage = lazyImport(() => import('page/livestreamSetup' /* webpackChunkName: "secondary" */)); const LivestreamCurrentPage = lazyImport(() => import('page/livestreamCurrent' /* webpackChunkName: "secondary" */)); -const OwnComments = lazyImport(() => import('page/ownComments' /* webpackChunkName: "ownComments" */)); const PasswordResetPage = lazyImport(() => import('page/passwordReset' /* webpackChunkName: "secondary" */)); const PasswordSetPage = lazyImport(() => import('page/passwordSet' /* webpackChunkName: "secondary" */)); const PublishPage = lazyImport(() => import('page/publish' /* webpackChunkName: "secondary" */)); @@ -330,7 +329,6 @@ function AppRouter(props: Props) { - diff --git a/ui/component/settingAccount/index.js b/ui/component/settingAccount/index.js index 0644ceaf6..83d77af76 100644 --- a/ui/component/settingAccount/index.js +++ b/ui/component/settingAccount/index.js @@ -1,5 +1,5 @@ import { connect } from 'react-redux'; -import { doWalletStatus, selectMyChannelClaims, selectWalletIsEncrypted } from 'lbry-redux'; +import { doWalletStatus, selectWalletIsEncrypted } from 'lbry-redux'; import { selectUser, selectUserVerifiedEmail } from 'redux/selectors/user'; import { selectLanguage } from 'redux/selectors/settings'; @@ -9,7 +9,6 @@ const select = (state) => ({ isAuthenticated: selectUserVerifiedEmail(state), walletEncrypted: selectWalletIsEncrypted(state), user: selectUser(state), - myChannels: selectMyChannelClaims(state), language: selectLanguage(state), }); diff --git a/ui/component/settingAccount/view.jsx b/ui/component/settingAccount/view.jsx index 812e3818c..f39262379 100644 --- a/ui/component/settingAccount/view.jsx +++ b/ui/component/settingAccount/view.jsx @@ -15,13 +15,12 @@ type Props = { isAuthenticated: boolean, walletEncrypted: boolean, user: User, - myChannels: ?Array, // --- perform --- doWalletStatus: () => void, }; export default function SettingAccount(props: Props) { - const { isAuthenticated, walletEncrypted, user, myChannels, doWalletStatus } = props; + const { isAuthenticated, walletEncrypted, user, doWalletStatus } = props; const [storedPassword, setStoredPassword] = React.useState(false); // Determine if password is stored. @@ -93,17 +92,6 @@ export default function SettingAccount(props: Props) { )} {/* @endif */} - - {myChannels && ( - -
-
- {threadDepth !== 0 && ( -
+ {!hideActions && ( +
+ {threadDepth !== 0 && ( +
+ )} {numDirectReplies > 0 && !showReplies && (
-- 2.45.2 From 37327185b1b53534e9442df8093674e1ef756b84 Mon Sep 17 00:00:00 2001 From: infinite-persistence Date: Mon, 27 Sep 2021 13:42:12 +0800 Subject: [PATCH 16/39] Implement own-comments page --- static/app-strings.json | 4 + ui/component/router/view.jsx | 2 + ui/component/settingAccount/index.js | 3 +- ui/component/settingAccount/view.jsx | 14 +- ui/constants/pageTitles.js | 1 + ui/constants/pages.js | 1 + ui/page/ownComments/index.js | 33 +++++ ui/page/ownComments/view.jsx | 210 +++++++++++++++++++++++++++ ui/scss/component/_comments.scss | 57 ++++++++ 9 files changed, 323 insertions(+), 2 deletions(-) create mode 100644 ui/page/ownComments/index.js create mode 100644 ui/page/ownComments/view.jsx diff --git a/static/app-strings.json b/static/app-strings.json index 36a82cd1a..5baa9217f 100644 --- a/static/app-strings.json +++ b/static/app-strings.json @@ -209,6 +209,9 @@ "Failed to copy.": "Failed to copy.", "The publisher has chosen to charge %lbc% to view this content. Your balance is currently too low to view it. Check out %reward_link% for free %lbc% or send more %lbc% to your wallet. You can also %buy_link% more %lbc%.": "The publisher has chosen to charge %lbc% to view this content. Your balance is currently too low to view it. Check out %reward_link% for free %lbc% or send more %lbc% to your wallet. You can also %buy_link% more %lbc%.", "Connecting...": "Connecting...", + "Your comments": "Your comments", + "View your past comments.": "View your past comments.", + "Content or channel was deleted.": "Content or channel was deleted.", "Comments": "Comments", "Comment": "Comment", "Comment --[button to submit something]--": "Comment", @@ -1460,6 +1463,7 @@ "Staked LBRY Credits": "Staked LBRY Credits", "1 comment": "1 comment", "%total_comments% comments": "%total_comments% comments", + "No comments": "No comments", "Upvote": "Upvote", "Downvote": "Downvote", "You loved this": "You loved this", diff --git a/ui/component/router/view.jsx b/ui/component/router/view.jsx index 03db2b538..ad5234265 100644 --- a/ui/component/router/view.jsx +++ b/ui/component/router/view.jsx @@ -59,6 +59,7 @@ const ListBlockedPage = lazyImport(() => import('page/listBlocked' /* webpackChu const ListsPage = lazyImport(() => import('page/lists' /* webpackChunkName: "secondary" */)); const LiveStreamSetupPage = lazyImport(() => import('page/livestreamSetup' /* webpackChunkName: "secondary" */)); const LivestreamCurrentPage = lazyImport(() => import('page/livestreamCurrent' /* webpackChunkName: "secondary" */)); +const OwnComments = lazyImport(() => import('page/ownComments' /* webpackChunkName: "ownComments" */)); const PasswordResetPage = lazyImport(() => import('page/passwordReset' /* webpackChunkName: "secondary" */)); const PasswordSetPage = lazyImport(() => import('page/passwordSet' /* webpackChunkName: "secondary" */)); const PublishPage = lazyImport(() => import('page/publish' /* webpackChunkName: "secondary" */)); @@ -329,6 +330,7 @@ function AppRouter(props: Props) { + diff --git a/ui/component/settingAccount/index.js b/ui/component/settingAccount/index.js index 83d77af76..0644ceaf6 100644 --- a/ui/component/settingAccount/index.js +++ b/ui/component/settingAccount/index.js @@ -1,5 +1,5 @@ import { connect } from 'react-redux'; -import { doWalletStatus, selectWalletIsEncrypted } from 'lbry-redux'; +import { doWalletStatus, selectMyChannelClaims, selectWalletIsEncrypted } from 'lbry-redux'; import { selectUser, selectUserVerifiedEmail } from 'redux/selectors/user'; import { selectLanguage } from 'redux/selectors/settings'; @@ -9,6 +9,7 @@ const select = (state) => ({ isAuthenticated: selectUserVerifiedEmail(state), walletEncrypted: selectWalletIsEncrypted(state), user: selectUser(state), + myChannels: selectMyChannelClaims(state), language: selectLanguage(state), }); diff --git a/ui/component/settingAccount/view.jsx b/ui/component/settingAccount/view.jsx index f39262379..812e3818c 100644 --- a/ui/component/settingAccount/view.jsx +++ b/ui/component/settingAccount/view.jsx @@ -15,12 +15,13 @@ type Props = { isAuthenticated: boolean, walletEncrypted: boolean, user: User, + myChannels: ?Array, // --- perform --- doWalletStatus: () => void, }; export default function SettingAccount(props: Props) { - const { isAuthenticated, walletEncrypted, user, doWalletStatus } = props; + const { isAuthenticated, walletEncrypted, user, myChannels, doWalletStatus } = props; const [storedPassword, setStoredPassword] = React.useState(false); // Determine if password is stored. @@ -92,6 +93,17 @@ export default function SettingAccount(props: Props) { )} {/* @endif */} + + {myChannels && ( + +
-- 2.45.2 From 256ed6a106c704b1b702355681a43d8607f9abb8 Mon Sep 17 00:00:00 2001 From: saltrafael Date: Mon, 4 Oct 2021 16:25:43 -0300 Subject: [PATCH 30/39] Remove expand/collapse from channel discussion page --- ui/component/channelDiscussion/view.jsx | 2 +- ui/component/commentsList/view.jsx | 13 ++++++++----- ui/page/file/view.jsx | 2 +- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/ui/component/channelDiscussion/view.jsx b/ui/component/channelDiscussion/view.jsx index 15b215f50..ed1b8d97e 100644 --- a/ui/component/channelDiscussion/view.jsx +++ b/ui/component/channelDiscussion/view.jsx @@ -17,7 +17,7 @@ function ChannelDiscussion(props: Props) { } return (
- +
); } diff --git a/ui/component/commentsList/view.jsx b/ui/component/commentsList/view.jsx index 29023e241..debff2d0e 100644 --- a/ui/component/commentsList/view.jsx +++ b/ui/component/commentsList/view.jsx @@ -48,6 +48,7 @@ type Props = { activeChannelId: ?string, settingsByChannelId: { [channelId: string]: PerChannelSettings }, fetchReacts: (Array) => Promise, + commentsAreExpanded?: boolean, fetchTopLevelComments: (string, number, number, number) => void, fetchComment: (string) => void, resetComments: (string) => void, @@ -74,6 +75,7 @@ function CommentList(props: Props) { activeChannelId, settingsByChannelId, fetchReacts, + commentsAreExpanded, fetchTopLevelComments, fetchComment, resetComments, @@ -88,7 +90,8 @@ function CommentList(props: Props) { const fetchedCommentsOnce = useFetched(isFetchingComments); const fetchedReactsOnce = useFetched(isFetchingReacts); const fetchedLinkedComment = useFetched(isFetchingCommentsById); - const [expandedComments, setExpandedComments] = React.useState(!isMobile && !isMediumScreen); + const hasDefaultExpansion = commentsAreExpanded || (!isMobile && !isMediumScreen); + const [expandedComments, setExpandedComments] = React.useState(hasDefaultExpansion); const totalFetchedComments = allCommentIds ? allCommentIds.length : 0; const channelId = getChannelIdFromClaim(claim); const channelSettings = channelId ? settingsByChannelId[channelId] : undefined; @@ -203,7 +206,7 @@ function CommentList(props: Props) { } const handleCommentScroll = debounce(() => { - if (!isMobile && !isMediumScreen && shouldFetchNextPage(page, topLevelTotalPages, window, document)) { + if (hasDefaultExpansion && shouldFetchNextPage(page, topLevelTotalPages, window, document)) { setPage(page + 1); } }, DEBOUNCE_SCROLL_HANDLER_MS); @@ -216,7 +219,7 @@ function CommentList(props: Props) { return () => window.removeEventListener('scroll', handleCommentScroll); } } - }, [isFetchingComments, isMediumScreen, isMobile, moreBelow, page, readyToDisplayComments, topLevelTotalPages]); + }, [hasDefaultExpansion, isFetchingComments, moreBelow, page, readyToDisplayComments, topLevelTotalPages]); const getCommentElems = (comments) => { return comments.map((comment) => ( @@ -299,7 +302,7 @@ function CommentList(props: Props) { {readyToDisplayComments && topLevelComments && getCommentElems(topLevelComments)} - {(isMobile || isMediumScreen) && ( + {!hasDefaultExpansion && (
{(!expandedComments || moreBelow) && (
} body={ diff --git a/ui/component/collectionsListMine/view.jsx b/ui/component/collectionsListMine/view.jsx index c57273b38..3cbffa9b1 100644 --- a/ui/component/collectionsListMine/view.jsx +++ b/ui/component/collectionsListMine/view.jsx @@ -175,7 +175,7 @@ export default function CollectionsListMine(props: Props) { {filteredCollections && filteredCollections.length > 0 && filteredCollections.map((key) => )} - {!filteredCollections.length &&
{__('No matching collections')}
} + {!filteredCollections.length &&
{__('No matching playlists')}
} )} diff --git a/ui/page/collection/view.jsx b/ui/page/collection/view.jsx index 282233663..4befdc9d6 100644 --- a/ui/page/collection/view.jsx +++ b/ui/page/collection/view.jsx @@ -102,7 +102,9 @@ export default function CollectionPage(props: Props) { const subTitle = (
- {collectionCount} items + + {collectionCount === 1 ? __('1 item') : __('%collectionCount% items', { collectionCount })} + {uri && }
); -- 2.45.2 From 95654955b1ef5c6ba55ada50f2419949c56cf10a Mon Sep 17 00:00:00 2001 From: infinite-persistence Date: Wed, 6 Oct 2021 11:41:52 +0800 Subject: [PATCH 34/39] Add sitemap to influence Sitelinks ## Issue Part of `7166 improve search metadata` ## Notes This is an experiment to influence the Sitelinks in our search results. Our current sitemap only consists of claims, so claims appear in Sitelinks more often. We (Julian) want categories to have higher priority, if possible. For now, the sitemap will be defined in Google Console instead of robots.txt. If it works, the file should be uploaded to sitemap.odysee.com, alongside the claim list sitemap. --- static/sitemap.txt | 18 ++++++++++++++++++ web/webpack.config.js | 5 +++++ 2 files changed, 23 insertions(+) create mode 100644 static/sitemap.txt diff --git a/static/sitemap.txt b/static/sitemap.txt new file mode 100644 index 000000000..bc0956c4a --- /dev/null +++ b/static/sitemap.txt @@ -0,0 +1,18 @@ +https://odysee.com +https://odysee.com/$/general +https://odysee.com/$/bighits +https://odysee.com/$/gaming +https://odysee.com/$/music +https://odysee.com/$/universe +https://odysee.com/$/tech +https://odysee.com/$/lab +https://odysee.com/$/movies +https://odysee.com/$/news +https://odysee.com/$/finance +https://odysee.com/$/wildwest +https://odysee.com/$/signup +https://odysee.com/$/signin +https://odysee.com/$/help +https://odysee.com/$/settings +https://odysee.com/@Odysee:8 +https://odysee.com/@OdyseeHelp:b \ No newline at end of file diff --git a/web/webpack.config.js b/web/webpack.config.js index f483c6217..45e2cc907 100644 --- a/web/webpack.config.js +++ b/web/webpack.config.js @@ -45,6 +45,11 @@ const copyWebpackCommands = [ to: `${DIST_ROOT}/robots.txt`, force: true, }, + { + from: `${STATIC_ROOT}/sitemap.txt`, + to: `${DIST_ROOT}/sitemap.txt`, + force: true, + }, { from: `${STATIC_ROOT}/img/favicon.png`, to: `${DIST_ROOT}/public/favicon.png`, -- 2.45.2 From 401f7fec177756cc3a9191ec718a522e99be3b4c Mon Sep 17 00:00:00 2001 From: infinite-persistence Date: Wed, 6 Oct 2021 13:04:26 +0800 Subject: [PATCH 35/39] Revert "Add sitemap to influence Sitelinks" Seems like I messed up robots.txt? This reverts commit 95654955b1ef5c6ba55ada50f2419949c56cf10a. --- static/sitemap.txt | 18 ------------------ web/webpack.config.js | 5 ----- 2 files changed, 23 deletions(-) delete mode 100644 static/sitemap.txt diff --git a/static/sitemap.txt b/static/sitemap.txt deleted file mode 100644 index bc0956c4a..000000000 --- a/static/sitemap.txt +++ /dev/null @@ -1,18 +0,0 @@ -https://odysee.com -https://odysee.com/$/general -https://odysee.com/$/bighits -https://odysee.com/$/gaming -https://odysee.com/$/music -https://odysee.com/$/universe -https://odysee.com/$/tech -https://odysee.com/$/lab -https://odysee.com/$/movies -https://odysee.com/$/news -https://odysee.com/$/finance -https://odysee.com/$/wildwest -https://odysee.com/$/signup -https://odysee.com/$/signin -https://odysee.com/$/help -https://odysee.com/$/settings -https://odysee.com/@Odysee:8 -https://odysee.com/@OdyseeHelp:b \ No newline at end of file diff --git a/web/webpack.config.js b/web/webpack.config.js index 45e2cc907..f483c6217 100644 --- a/web/webpack.config.js +++ b/web/webpack.config.js @@ -45,11 +45,6 @@ const copyWebpackCommands = [ to: `${DIST_ROOT}/robots.txt`, force: true, }, - { - from: `${STATIC_ROOT}/sitemap.txt`, - to: `${DIST_ROOT}/sitemap.txt`, - force: true, - }, { from: `${STATIC_ROOT}/img/favicon.png`, to: `${DIST_ROOT}/public/favicon.png`, -- 2.45.2 From 3a644d7bfcdf4dc8d35bdf79e65689dd5751fe38 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 6 Oct 2021 10:12:51 -0400 Subject: [PATCH 36/39] Bump url-parse from 1.5.1 to 1.5.3 (#7230) Bumps [url-parse](https://github.com/unshiftio/url-parse) from 1.5.1 to 1.5.3. - [Release notes](https://github.com/unshiftio/url-parse/releases) - [Commits](https://github.com/unshiftio/url-parse/compare/1.5.1...1.5.3) --- updated-dependencies: - dependency-name: url-parse dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 96aee291f..154f85f0e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -16391,9 +16391,9 @@ url-parse-lax@^3.0.0: prepend-http "^2.0.0" url-parse@^1.1.1, url-parse@^1.1.8, url-parse@^1.4.3: - version "1.5.1" - resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.5.1.tgz#d5fa9890af8a5e1f274a2c98376510f6425f6e3b" - integrity sha512-HOfCOUJt7iSYzEx/UqgtwKRMC6EU91NFhsCHMv9oM03VJcVo2Qrp8T8kI9D7amFf1cu+/3CEhgb3rF9zL7k85Q== + version "1.5.3" + resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.5.3.tgz#71c1303d38fb6639ade183c2992c8cc0686df862" + integrity sha512-IIORyIQD9rvj0A4CLWsHkBBJuNqWpFQe224b6j9t/ABmquIS0qDU2pY6kl6AuOrL5OkCXHMCFNe1jBcuAggjvQ== dependencies: querystringify "^2.1.1" requires-port "^1.0.0" -- 2.45.2 From 4bc4a965d9cfda25967fed24c88634c9d375af7e Mon Sep 17 00:00:00 2001 From: jessopb <36554050+jessopb@users.noreply.github.com> Date: Wed, 6 Oct 2021 10:13:37 -0400 Subject: [PATCH 37/39] fix notifications page on unauthed app (#7226) --- static/app-strings.json | 1 + ui/page/settingsNotifications/view.jsx | 42 ++++++++++++++------------ 2 files changed, 23 insertions(+), 20 deletions(-) diff --git a/static/app-strings.json b/static/app-strings.json index d8b51bf2a..5373770c1 100644 --- a/static/app-strings.json +++ b/static/app-strings.json @@ -2183,5 +2183,6 @@ "Creator": "Creator", "From comments": "From comments", "From search": "From search", + "Manage tags": "Manage tags", "--end--": "--end--" } diff --git a/ui/page/settingsNotifications/view.jsx b/ui/page/settingsNotifications/view.jsx index 6d5053034..f086a9682 100644 --- a/ui/page/settingsNotifications/view.jsx +++ b/ui/page/settingsNotifications/view.jsx @@ -32,28 +32,30 @@ export default function NotificationSettingsPage(props: Props) { const lbryIoParams = verificationToken ? { auth_token: verificationToken } : undefined; React.useEffect(() => { - Lbryio.call('tag', 'list', lbryIoParams) - .then(setTags) - .catch((e) => { - setError(true); - }); + if (isAuthenticated) { + Lbryio.call('tag', 'list', lbryIoParams) + .then(setTags) + .catch((e) => { + setError(true); + }); - Lbryio.call('user_email', 'status', lbryIoParams) - .then((res) => { - const enabledEmails = - res.emails && - Object.keys(res.emails).reduce((acc, email) => { - const isEnabled = res.emails[email]; - return [...acc, { email, isEnabled }]; - }, []); + Lbryio.call('user_email', 'status', lbryIoParams) + .then((res) => { + const enabledEmails = + res.emails && + Object.keys(res.emails).reduce((acc, email) => { + const isEnabled = res.emails[email]; + return [...acc, { email, isEnabled }]; + }, []); - setTagMap(res.tags); - setEnabledEmails(enabledEmails); - }) - .catch((e) => { - setError(true); - }); - }, []); + setTagMap(res.tags); + setEnabledEmails(enabledEmails); + }) + .catch((e) => { + setError(true); + }); + } + }, [isAuthenticated]); function handleChangeTag(name, newIsEnabled) { const tagParams = newIsEnabled ? { add: name } : { remove: name }; -- 2.45.2 From b44be392523fd5baabba1821dc28f48000dd0fa1 Mon Sep 17 00:00:00 2001 From: zeppi Date: Tue, 5 Oct 2021 16:57:47 -0400 Subject: [PATCH 38/39] move file actions from lbry-redux --- flow-typed/File.js | 78 ++++++++++++++ ui/component/claimPreview/index.js | 3 +- ui/component/claimPreviewTile/index.js | 2 +- ui/component/sideNavigation/index.js | 4 +- ui/constants/action_types.js | 7 ++ ui/redux/actions/content.js | 8 +- ui/redux/actions/file.js | 144 +++++++++++++++++++++++-- 7 files changed, 231 insertions(+), 15 deletions(-) create mode 100644 flow-typed/File.js diff --git a/flow-typed/File.js b/flow-typed/File.js new file mode 100644 index 000000000..44dc4f398 --- /dev/null +++ b/flow-typed/File.js @@ -0,0 +1,78 @@ +// @flow + +declare type FileListItem = { + metadata: StreamMetadata, + added_on: number, + blobs_completed: number, + blobs_in_stream: number, + blobs_remaining: number, + channel_claim_id: string, + channel_name: string, + claim_id: string, + claim_name: string, + completed: false, + content_fee?: { txid: string }, + purchase_receipt?: { txid: string, amount: string }, + download_directory: string, + download_path: string, + file_name: string, + key: string, + mime_type: string, + nout: number, + outpoint: string, + points_paid: number, + protobuf: string, + reflector_progress: number, + sd_hash: string, + status: string, + stopped: false, + stream_hash: string, + stream_name: string, + streaming_url: string, + suggested_file_name: string, + total_bytes: number, + total_bytes_lower_bound: number, + is_fully_reflected: boolean, + // TODO: sdk plans to change `tx` + // It isn't currently used by the apps + tx: {}, + txid: string, + uploading_to_reflector: boolean, + written_bytes: number, +}; + +declare type FileState = { + failedPurchaseUris: Array, + purchasedUris: Array, +}; + +declare type PurchaseUriCompleted = { + type: ACTIONS.PURCHASE_URI_COMPLETED, + data: { + uri: string, + streamingUrl: string, + }, +}; + +declare type PurchaseUriFailed = { + type: ACTIONS.PURCHASE_URI_FAILED, + data: { + uri: string, + error: any, + }, +}; + +declare type PurchaseUriStarted = { + type: ACTIONS.PURCHASE_URI_STARTED, + data: { + uri: string, + streamingUrl: string, + }, +}; + +declare type DeletePurchasedUri = { + type: ACTIONS.CLEAR_PURCHASED_URI_SUCCESS, + data: { + uri: string, + }, +}; diff --git a/ui/component/claimPreview/index.js b/ui/component/claimPreview/index.js index 832004474..2dc96541d 100644 --- a/ui/component/claimPreview/index.js +++ b/ui/component/claimPreview/index.js @@ -6,7 +6,6 @@ import { makeSelectClaimIsMine, makeSelectClaimIsPending, makeSelectClaimIsNsfw, - doFileGet, makeSelectReflectingClaimForUri, makeSelectClaimWasPurchased, makeSelectStreamingUrlForUri, @@ -25,7 +24,7 @@ import { selectShowMatureContent } from 'redux/selectors/settings'; import { makeSelectHasVisitedUri } from 'redux/selectors/content'; import { makeSelectIsSubscribed } from 'redux/selectors/subscriptions'; import { selectModerationBlockList } from 'redux/selectors/comments'; - +import { doFileGet } from 'redux/actions/file'; import ClaimPreview from './view'; import formatMediaDuration from 'util/formatMediaDuration'; diff --git a/ui/component/claimPreviewTile/index.js b/ui/component/claimPreviewTile/index.js index 40b3355c0..59d506718 100644 --- a/ui/component/claimPreviewTile/index.js +++ b/ui/component/claimPreviewTile/index.js @@ -5,7 +5,6 @@ import { makeSelectIsUriResolving, makeSelectThumbnailForUri, makeSelectTitleForUri, - doFileGet, makeSelectChannelForClaimUri, makeSelectClaimIsNsfw, makeSelectClaimIsStreamPlaceholder, @@ -14,6 +13,7 @@ import { import { selectMutedChannels } from 'redux/selectors/blocked'; import { makeSelectViewCountForUri, selectBlackListedOutpoints, selectFilteredOutpoints } from 'lbryinc'; import { makeSelectIsActiveLivestream } from 'redux/selectors/livestream'; +import { doFileGet } from 'redux/actions/file'; import { selectShowMatureContent } from 'redux/selectors/settings'; import ClaimPreviewTile from './view'; import formatMediaDuration from 'util/formatMediaDuration'; diff --git a/ui/component/sideNavigation/index.js b/ui/component/sideNavigation/index.js index 6eda01389..40a13ccc8 100644 --- a/ui/component/sideNavigation/index.js +++ b/ui/component/sideNavigation/index.js @@ -1,10 +1,12 @@ import { connect } from 'react-redux'; import { selectSubscriptions } from 'redux/selectors/subscriptions'; -import { selectPurchaseUriSuccess, doClearPurchasedUriSuccess } from 'lbry-redux'; +import { selectPurchaseUriSuccess } from 'lbry-redux'; import { selectFollowedTags } from 'redux/selectors/tags'; import { selectUserVerifiedEmail, selectUser } from 'redux/selectors/user'; import { selectHomepageData, selectLanguage } from 'redux/selectors/settings'; import { doSignOut } from 'redux/actions/app'; +import { doClearPurchasedUriSuccess } from 'redux/actions/file'; + import { selectUnseenNotificationCount } from 'redux/selectors/notifications'; import SideNavigation from './view'; diff --git a/ui/constants/action_types.js b/ui/constants/action_types.js index 7345e773f..4460028ed 100644 --- a/ui/constants/action_types.js +++ b/ui/constants/action_types.js @@ -120,6 +120,13 @@ export const PLAY_VIDEO_STARTED = 'PLAY_VIDEO_STARTED'; export const FETCH_AVAILABILITY_STARTED = 'FETCH_AVAILABILITY_STARTED'; export const FETCH_AVAILABILITY_COMPLETED = 'FETCH_AVAILABILITY_COMPLETED'; export const FILE_DELETE = 'FILE_DELETE'; +export const FETCH_FILE_INFO_FAILED = 'FETCH_FILE_INFO_FAILED'; +export const DOWNLOADING_CANCELED = 'DOWNLOADING_CANCELED'; +export const SET_FILE_LIST_SORT = 'SET_FILE_LIST_SORT'; +export const PURCHASE_URI_STARTED = 'PURCHASE_URI_STARTED'; +export const PURCHASE_URI_COMPLETED = 'PURCHASE_URI_COMPLETED'; +export const PURCHASE_URI_FAILED = 'PURCHASE_URI_FAILED'; +export const CLEAR_PURCHASED_URI_SUCCESS = 'CLEAR_PURCHASED_URI_SUCCESS'; // Search export const SEARCH_START = 'SEARCH_START'; diff --git a/ui/redux/actions/content.js b/ui/redux/actions/content.js index 7e76e3962..1053b617a 100644 --- a/ui/redux/actions/content.js +++ b/ui/redux/actions/content.js @@ -10,7 +10,6 @@ import { SETTINGS, makeSelectFileInfoForUri, selectFileInfosByOutpoint, - doPurchaseUri, makeSelectUriIsStreamable, selectDownloadingByOutpoint, makeSelectClaimForUri, @@ -19,6 +18,7 @@ import { doToast, makeSelectUrlsForCollectionId, } from 'lbry-redux'; +import { doPurchaseUri } from 'redux/actions/file'; import { makeSelectCostInfoForUri, Lbryio } from 'lbryinc'; import { makeSelectClientSetting, selectosNotificationsEnabled, selectDaemonSettings } from 'redux/selectors/settings'; @@ -117,8 +117,8 @@ export function doSetPlayingUri({ uri: ?string, source?: string, commentId?: string, - pathname: string, - collectionId: string, + pathname?: string, + collectionId?: string, }) { return (dispatch: Dispatch) => { dispatch({ @@ -140,7 +140,7 @@ export function doPurchaseUriWrapper(uri: string, cost: number, saveFile: boolea } } - dispatch(doPurchaseUri(uri, { costInfo: cost }, saveFile, onSuccess)); + dispatch(doPurchaseUri(uri, { cost }, saveFile, onSuccess)); }; } diff --git a/ui/redux/actions/file.js b/ui/redux/actions/file.js index 447fd4f20..73adf0bd4 100644 --- a/ui/redux/actions/file.js +++ b/ui/redux/actions/file.js @@ -1,3 +1,4 @@ +// @flow import * as ACTIONS from 'constants/action_types'; // @if TARGET='app' import { shell } from 'electron'; @@ -7,22 +8,28 @@ import { batchActions, doAbandonClaim, makeSelectFileInfoForUri, + selectDownloadingByOutpoint, + makeSelectStreamingUrlForUri, makeSelectClaimForUri, + selectBalance, ABANDON_STATES, } from 'lbry-redux'; import { doHideModal } from 'redux/actions/app'; import { goBack } from 'connected-react-router'; import { doSetPlayingUri } from 'redux/actions/content'; import { selectPlayingUri } from 'redux/selectors/content'; +import { doToast } from 'redux/actions/notifications'; +type Dispatch = (action: any) => any; +type GetState = () => { file: FileState }; -export function doOpenFileInFolder(path) { +export function doOpenFileInFolder(path: string) { return () => { shell.showItemInFolder(path); }; } -export function doOpenFileInShell(path) { - return (dispatch) => { +export function doOpenFileInShell(path: string) { + return (dispatch: Dispatch) => { const success = shell.openPath(path); if (!success) { dispatch(doOpenFileInFolder(path)); @@ -30,8 +37,8 @@ export function doOpenFileInShell(path) { }; } -export function doDeleteFile(outpoint, deleteFromComputer, abandonClaim, cb) { - return (dispatch) => { +export function doDeleteFile(outpoint: string, deleteFromComputer?: boolean, abandonClaim?: boolean, cb: any) { + return (dispatch: Dispatch) => { if (abandonClaim) { const [txid, nout] = outpoint.split(':'); dispatch(doAbandonClaim(txid, Number(nout), cb)); @@ -53,8 +60,13 @@ export function doDeleteFile(outpoint, deleteFromComputer, abandonClaim, cb) { }; } -export function doDeleteFileAndMaybeGoBack(uri, deleteFromComputer, abandonClaim, doGoBack) { - return (dispatch, getState) => { +export function doDeleteFileAndMaybeGoBack( + uri: string, + deleteFromComputer?: boolean, + abandonClaim?: boolean, + doGoBack: (any) => void +) { + return (dispatch: Dispatch, getState: GetState) => { const state = getState(); const playingUri = selectPlayingUri(state); const { outpoint } = makeSelectFileInfoForUri(uri)(state) || ''; @@ -88,3 +100,121 @@ export function doDeleteFileAndMaybeGoBack(uri, deleteFromComputer, abandonClaim dispatch(batchActions(...actions)); }; } + +export function doFileGet(uri: string, saveFile: boolean = true, onSuccess?: (GetResponse) => any) { + return (dispatch: Dispatch, getState: () => any) => { + const state = getState(); + const { nout, txid } = makeSelectClaimForUri(uri)(state); + const outpoint = `${txid}:${nout}`; + + dispatch({ + type: ACTIONS.FETCH_FILE_INFO_STARTED, + data: { + outpoint, + }, + }); + + // set save_file argument to True to save the file (old behaviour) + Lbry.get({ uri, save_file: saveFile }) + .then((streamInfo: GetResponse) => { + const timeout = streamInfo === null || typeof streamInfo !== 'object' || streamInfo.error === 'Timeout'; + if (timeout) { + dispatch({ + type: ACTIONS.FETCH_FILE_INFO_FAILED, + data: { outpoint }, + }); + + dispatch(doToast({ message: `File timeout for uri ${uri}`, isError: true })); + } else { + if (streamInfo.purchase_receipt || streamInfo.content_fee) { + dispatch({ + type: ACTIONS.PURCHASE_URI_COMPLETED, + data: { uri, purchaseReceipt: streamInfo.purchase_receipt || streamInfo.content_fee }, + }); + } + dispatch({ + type: ACTIONS.FETCH_FILE_INFO_COMPLETED, + data: { + fileInfo: streamInfo, + outpoint: outpoint, + }, + }); + + if (onSuccess) { + onSuccess(streamInfo); + } + } + }) + .catch((error) => { + dispatch({ + type: ACTIONS.PURCHASE_URI_FAILED, + data: { uri, error }, + }); + + dispatch({ + type: ACTIONS.FETCH_FILE_INFO_FAILED, + data: { outpoint }, + }); + + dispatch( + doToast({ + message: `Failed to view ${uri}, please try again. If this problem persists, visit https://lbry.com/faq/support for support.`, + isError: true, + }) + ); + }); + }; +} + +export function doPurchaseUri( + uri: string, + costInfo: { cost: number }, + saveFile: boolean = true, + onSuccess?: (GetResponse) => any +) { + return (dispatch: Dispatch, getState: GetState) => { + dispatch({ + type: ACTIONS.PURCHASE_URI_STARTED, + data: { uri }, + }); + + const state = getState(); + const balance = selectBalance(state); + const fileInfo = makeSelectFileInfoForUri(uri)(state); + const downloadingByOutpoint = selectDownloadingByOutpoint(state); + const alreadyDownloading = fileInfo && !!downloadingByOutpoint[fileInfo.outpoint]; + const alreadyStreaming = makeSelectStreamingUrlForUri(uri)(state); + + if (!saveFile && (alreadyDownloading || alreadyStreaming)) { + dispatch({ + type: ACTIONS.PURCHASE_URI_FAILED, + data: { uri, error: `Already fetching uri: ${uri}` }, + }); + + if (onSuccess) { + onSuccess(fileInfo); + } + + return; + } + + const { cost } = costInfo; + if (parseFloat(cost) > balance) { + dispatch({ + type: ACTIONS.PURCHASE_URI_FAILED, + data: { uri, error: 'Insufficient credits' }, + }); + + Promise.resolve(); + return; + } + + dispatch(doFileGet(uri, saveFile, onSuccess)); + }; +} + +export function doClearPurchasedUriSuccess() { + return { + type: ACTIONS.CLEAR_PURCHASED_URI_SUCCESS, + }; +} -- 2.45.2 From c61fd471369af46657e43e1110ebf766814dfc22 Mon Sep 17 00:00:00 2001 From: Anthony Date: Wed, 6 Oct 2021 20:09:01 +0300 Subject: [PATCH 39/39] fix flow error --- ui/analytics.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ui/analytics.js b/ui/analytics.js index fd8af3415..feae9a64a 100644 --- a/ui/analytics.js +++ b/ui/analytics.js @@ -164,8 +164,9 @@ async function sendAndResetWatchmanData() { 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 - ...(bitrateAsBitsPerSecond && {bitrate: bitrateAsBitsPerSecond}), // add bitrate if video (audio doesn't work) }; // post to watchman -- 2.45.2