diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f0a7a621..35500cde0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ 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)) - 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 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)) - Fallback for files with no extension ([#7704](https://github.com/lbryio/lbry-desktop/pull/7704)) diff --git a/static/app-strings.json b/static/app-strings.json index 756121275..2b84ec35a 100644 --- a/static/app-strings.json +++ b/static/app-strings.json @@ -2318,6 +2318,5 @@ "Odysee Connect --[Section in Help Page]--": "Odysee Connect", "Your hub has blocked this content because it subscribes to the following blocking channel:": "Your hub has blocked this content because it subscribes to the following blocking channel:", "Your hub has blocked access to this content do to a complaint received under the US Digital Millennium Copyright Act.": "Your hub has blocked access to this content do to a complaint received under the US Digital Millennium Copyright Act.", - "Autoplay Next is on.": "Autoplay Next is on.", "--end--": "--end--" } diff --git a/ui/analytics.js b/ui/analytics.js index e9f2300fe..132589d64 100644 --- a/ui/analytics.js +++ b/ui/analytics.js @@ -1,11 +1,4 @@ // @flow -/* - Removed Watchman (internal view tracking) code. - This file may eventually implement cantina - Refer to 0cc0e213a5c5bf9e2a76316df5d9da4b250a13c3 for initial integration commit - refer to ___ for removal commit. - */ - import { Lbryio } from 'lbryinc'; import * as Sentry from '@sentry/browser'; import MatomoTracker from '@datapunt/matomo-tracker-js'; @@ -21,6 +14,9 @@ const devInternalApis = process.env.LBRY_API_URL && process.env.LBRY_API_URL.inc export const SHARE_INTERNAL = 'shareInternal'; const SHARE_THIRD_PARTY = 'shareThirdParty'; +const WATCHMAN_BACKEND_ENDPOINT = 'https://watchman.na-backend.odysee.com/reports/playback'; +// const SEND_DATA_TO_WATCHMAN_INTERVAL = 10; // in seconds + if (isProduction) { ElectronCookies.enable({ origin: 'https://lbry.tv', @@ -72,10 +68,114 @@ type LogPublishParams = { let internalAnalyticsEnabled: boolean = false; if (window.localStorage.getItem(SHARE_INTERNAL) === 'true') internalAnalyticsEnabled = true; +/** + * Determine the mobile device type viewing the data + * This function returns one of 'and' (Android), 'ios', or 'web'. + * + * @returns {String} + */ +function getDeviceType() { + return 'dsk'; +} +// variables initialized for watchman +let amountOfBufferEvents = 0; +let amountOfBufferTimeInMS = 0; +let videoType, userId, claimUrl, playerPoweredBy, videoPlayer, bitrateAsBitsPerSecond; +let lastSentTime; + +// calculate data for backend, send them, and reset buffer data for next interval +async function sendAndResetWatchmanData() { + if (!userId) { + return 'Can only be used with a user id'; + } + + if (!videoPlayer) { + return 'Video player not initialized'; + } + + let timeSinceLastIntervalSend = new Date() - lastSentTime; + lastSentTime = new Date(); + + let protocol; + if (videoType === 'application/x-mpegURL') { + protocol = 'hls'; + // get bandwidth if it exists from the texttrack (so it's accurate if user changes quality) + // $FlowFixMe + bitrateAsBitsPerSecond = videoPlayer.textTracks?.().tracks_[0]?.activeCues[0]?.value?.bandwidth; + } else { + protocol = 'stb'; + } + + // current position in video in MS + const positionInVideo = Math.round(videoPlayer.currentTime()) * 1000; + + // get the duration marking the time in the video for relative position calculation + const totalDurationInSeconds = Math.round(videoPlayer.duration()); + + // build object for watchman backend + const objectToSend = { + rebuf_count: amountOfBufferEvents, + rebuf_duration: amountOfBufferTimeInMS, + url: claimUrl.replace('lbry://', ''), + device: getDeviceType(), + duration: timeSinceLastIntervalSend, + protocol, + player: playerPoweredBy, + user_id: userId.toString(), + position: Math.round(positionInVideo), + rel_position: Math.round((positionInVideo / (totalDurationInSeconds * 1000)) * 100), + bitrate: bitrateAsBitsPerSecond, + bandwidth: undefined, + // ...(userDownloadBandwidthInBitsPerSecond && {bandwidth: userDownloadBandwidthInBitsPerSecond}), // add bandwidth if populated + }; + + // post to watchman + await sendWatchmanData(objectToSend); + + // reset buffer data + amountOfBufferEvents = 0; + amountOfBufferTimeInMS = 0; +} + +let watchmanInterval; +// clear watchman interval and mark it as null (when video paused) +function stopWatchmanInterval() { + clearInterval(watchmanInterval); + watchmanInterval = null; +} + +// creates the setInterval that will run send to watchman on recurring basis +function startWatchmanIntervalIfNotRunning() { + if (!watchmanInterval) { + // instantiate the first time to calculate duration from + lastSentTime = new Date(); + } +} + +// post data to the backend +async function sendWatchmanData(body) { + try { + const response = await fetch(WATCHMAN_BACKEND_ENDPOINT, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + }, + body: JSON.stringify(body), + }); + + return response; + } catch (err) {} +} + const analytics: Analytics = { // receive buffer events from tracking plugin and save buffer amounts and times for backend call videoBufferEvent: async (claim, data) => { - // stub + amountOfBufferEvents = amountOfBufferEvents + 1; + amountOfBufferTimeInMS = amountOfBufferTimeInMS + data.bufferDuration; + }, + onDispose: () => { + stopWatchmanInterval(); }, /** * Is told whether video is being started or paused, and adjusts interval accordingly @@ -83,9 +183,40 @@ const analytics: Analytics = { * @param {object} passedPlayer - VideoJS Player object */ videoIsPlaying: (isPlaying, passedPlayer) => { - // stub + let playerIsSeeking = false; + // have to use this because videojs pauses/unpauses during seek + // sometimes the seeking function isn't populated yet so check for it as well + if (passedPlayer && passedPlayer.seeking) { + playerIsSeeking = passedPlayer.seeking(); + } + + // if being paused, and not seeking, send existing data and stop interval + if (!isPlaying && !playerIsSeeking) { + sendAndResetWatchmanData(); + stopWatchmanInterval(); + // if being told to pause, and seeking, send and restart interval + } else if (!isPlaying && playerIsSeeking) { + sendAndResetWatchmanData(); + stopWatchmanInterval(); + startWatchmanIntervalIfNotRunning(); + // is being told to play, and seeking, don't do anything, + // assume it's been started already from pause + } else if (isPlaying && playerIsSeeking) { + // start but not a seek, assuming a start from paused content + } else if (isPlaying && !playerIsSeeking) { + startWatchmanIntervalIfNotRunning(); + } }, videoStartEvent: (claimId, timeToStartVideo, poweredBy, passedUserId, canonicalUrl, passedPlayer, videoBitrate) => { + // populate values for watchman when video starts + userId = passedUserId; + claimUrl = canonicalUrl; + playerPoweredBy = poweredBy; + + videoType = passedPlayer.currentSource().type; + videoPlayer = passedPlayer; + bitrateAsBitsPerSecond = videoBitrate; + // sendPromMetric('time_to_start', duration); sendMatomoEvent('Media', 'TimeToStart', claimId, timeToStartVideo); }, @@ -251,9 +382,24 @@ function sendMatomoEvent(category, action, name, value) { } } +// Prometheus +// function sendPromMetric(name: string, value?: number) { +// if (IS_WEB) { +// let url = new URL(SDK_API_PATH + '/metric/ui'); +// const params = { name: name, value: value ? value.toString() : '' }; +// url.search = new URLSearchParams(params).toString(); +// return fetch(url, { method: 'post' }); +// } +// } + const MatomoInstance = new MatomoTracker({ urlBase: MATOMO_URL, siteId: MATOMO_ID, // optional, default value: `1` + // heartBeat: { // optional, enabled by default + // active: true, // optional, default value: true + // seconds: 10 // optional, default value: `15 + // }, + // linkTracking: false // optional, default value: true }); analytics.pageView(generateInitialUrl(window.location.hash)); diff --git a/ui/component/commentCreate/view.jsx b/ui/component/commentCreate/view.jsx index 8512fa3b1..a29f039f9 100644 --- a/ui/component/commentCreate/view.jsx +++ b/ui/component/commentCreate/view.jsx @@ -28,6 +28,7 @@ import type { ElementRef } from 'react'; import UriIndicator from 'component/uriIndicator'; import usePersistedState from 'effects/use-persisted-state'; import WalletTipAmountSelector from 'component/walletTipAmountSelector'; + import { getStripeEnvironment } from 'util/stripe'; const stripeEnvironment = getStripeEnvironment(); diff --git a/ui/component/commentsList/index.js b/ui/component/commentsList/index.js index 910ced4e3..f8755219d 100644 --- a/ui/component/commentsList/index.js +++ b/ui/component/commentsList/index.js @@ -23,9 +23,6 @@ import { doCommentReset, doCommentList, doCommentById, doCommentReactList } from import { selectActiveChannelClaim } from 'redux/selectors/app'; import { getChannelIdFromClaim } from 'util/claim'; import CommentsList from './view'; -import { makeSelectClientSetting } from 'redux/selectors/settings'; -import * as SETTINGS from 'constants/settings'; -import { doSetClientSetting } from 'redux/actions/settings'; const select = (state, props) => { const { uri } = props; @@ -59,19 +56,15 @@ const select = (state, props) => { myReactsByCommentId: selectMyReacts(state), othersReactsById: selectOthersReacts(state), activeChannelId: activeChannelClaim && activeChannelClaim.claim_id, - customCommentServers: makeSelectClientSetting(SETTINGS.CUSTOM_COMMENTS_SERVERS)(state), - commentServer: makeSelectClientSetting(SETTINGS.CUSTOM_COMMENTS_SERVER_URL)(state), }; }; -const perform = (dispatch, ownProps) => ({ - fetchTopLevelComments: (uri, parentId, page, pageSize, sortBy) => - dispatch(doCommentList(uri, parentId, page, pageSize, sortBy)), - fetchComment: (commentId) => dispatch(doCommentById(commentId)), - fetchReacts: (commentIds) => dispatch(doCommentReactList(commentIds)), - resetComments: (claimId) => dispatch(doCommentReset(claimId)), - doResolveUris: (uris, returnCachedClaims) => dispatch(doResolveUris(uris, returnCachedClaims)), - setCommentServer: (url) => dispatch(doSetClientSetting(SETTINGS.CUSTOM_COMMENTS_SERVER_URL, url, true)), -}); +const perform = { + fetchTopLevelComments: doCommentList, + fetchComment: doCommentById, + fetchReacts: doCommentReactList, + resetComments: doCommentReset, + doResolveUris, +}; export default connect(select, perform)(CommentsList); diff --git a/ui/component/commentsList/view.jsx b/ui/component/commentsList/view.jsx index 8698c5b2f..6f9a6eed8 100644 --- a/ui/component/commentsList/view.jsx +++ b/ui/component/commentsList/view.jsx @@ -1,6 +1,6 @@ // @flow import { COMMENT_PAGE_SIZE_TOP_LEVEL, SORT_BY } from 'constants/comment'; -import { ENABLE_COMMENT_REACTIONS, COMMENT_SERVER_API, COMMENT_SERVER_NAME } from 'config'; +import { ENABLE_COMMENT_REACTIONS } from 'config'; import { useIsMobile, useIsMediumScreen } from 'effects/use-screensize'; import { getCommentsListTitle } from 'util/comments'; import * as ICONS from 'constants/icons'; @@ -15,8 +15,6 @@ import Empty from 'component/common/empty'; import React, { useEffect } from 'react'; import Spinner from 'component/spinner'; import usePersistedState from 'effects/use-persisted-state'; -import { FormField } from 'component/common/form'; -import Comments from 'comments'; const DEBOUNCE_SCROLL_HANDLER_MS = 200; @@ -54,9 +52,6 @@ type Props = { fetchReacts: (commentIds: Array) => Promise, resetComments: (claimId: string) => void, doResolveUris: (uris: Array, returnCachedClaims: boolean) => void, - customCommentServers: Array, - setCommentServer: (string) => void, - commentServer: string, }; export default function CommentList(props: Props) { @@ -85,17 +80,11 @@ export default function CommentList(props: Props) { fetchReacts, resetComments, doResolveUris, - customCommentServers, - setCommentServer, - commentServer, } = props; const isMobile = useIsMobile(); const isMediumScreen = useIsMediumScreen(); - const defaultServer = { name: COMMENT_SERVER_NAME, url: COMMENT_SERVER_API }; - const allServers = [defaultServer, ...(customCommentServers || [])]; - const spinnerRef = React.useRef(); const DEFAULT_SORT = ENABLE_COMMENT_REACTIONS ? SORT_BY.POPULARITY : SORT_BY.NEWEST; const [sort, setSort] = usePersistedState('comment-sort-by', DEFAULT_SORT); @@ -266,16 +255,7 @@ export default function CommentList(props: Props) { }, [alreadyResolved, doResolveUris, topLevelComments]); const commentProps = { isTopLevel: true, threadDepth: 3, uri, claimIsMine, linkedCommentId }; - const actionButtonsProps = { - totalComments, - sort, - changeSort, - setPage, - allServers, - commentServer, - defaultServer, - setCommentServer, - }; + const actionButtonsProps = { totalComments, sort, changeSort, setPage }; return ( void, setPage: (number) => void, - allServers: Array, - commentServer: string, - setCommentServer: (string) => void, - defaultServer: CommentServerDetails, }; const CommentActionButtons = (actionButtonsProps: ActionButtonsProps) => { - const { totalComments, sort, changeSort, setPage, allServers, commentServer, setCommentServer, defaultServer } = - actionButtonsProps; + const { totalComments, sort, changeSort, setPage } = actionButtonsProps; + const sortButtonProps = { activeSort: sort, changeSort }; return ( @@ -379,35 +355,8 @@ const CommentActionButtons = (actionButtonsProps: ActionButtonsProps) => { )} -
-
- {allServers.length >= 2 && ( -
- - {allServers.map(function (server) { - return ( - - ); - })} - -
- )} + +