From ee754f0085301a6c740b20ecac56f63b02cc0a6e Mon Sep 17 00:00:00 2001 From: Franco Montenegro Date: Fri, 22 Apr 2022 00:00:57 -0300 Subject: [PATCH] Add persistent watch time setting. (#7547) * Add persistent watch time setting. * floating bugfix --jessopb * Improve how the persist watch time is being stored; add clear cache button. * Add makeSelectContentWatchedPercentageForUri selector and give feedback when clearing cache. * tweaks --jessopb Co-authored-by: zeppi --- static/app-strings.json | 17 +++------ ui/component/fileThumbnail/index.js | 15 +++++--- ui/component/fileThumbnail/view.jsx | 21 +++++++---- ui/component/settingContent/index.js | 4 ++- ui/component/settingContent/view.jsx | 44 +++++++++++++++++++++++ ui/component/viewers/videoViewer/view.jsx | 6 +++- ui/constants/action_types.js | 1 + ui/constants/settings.js | 1 + ui/redux/actions/content.js | 15 ++++++++ ui/redux/reducers/content.js | 7 ++++ ui/redux/reducers/settings.js | 1 + ui/redux/selectors/content.js | 25 +++++++++++++ ui/scss/component/_file-thumbnail.scss | 4 +-- ui/scss/component/_settings.scss | 11 ++++++ 14 files changed, 144 insertions(+), 28 deletions(-) diff --git a/static/app-strings.json b/static/app-strings.json index 0b7f246ca..1a8fc9997 100644 --- a/static/app-strings.json +++ b/static/app-strings.json @@ -2300,7 +2300,6 @@ "In %collection%": "In %collection%", "Add to %collection%": "Add to %collection%", "Show this channel your appreciation by sending a donation of Credits. ": "Show this channel your appreciation by sending a donation of Credits. ", - "%action% %collection%": "%action% %collection%", "You've entered the land of content freedom! Let's make sure everything is ship shape.": "You've entered the land of content freedom! Let's make sure everything is ship shape.", "By continuing, you agree to the %terms%": "By continuing, you agree to the %terms%", "Privacy": "Privacy", @@ -2308,21 +2307,13 @@ "Yes, share with LBRY": "Yes, share with LBRY", "Search Uploads": "Search Uploads", "This refundable boost will improve the discoverability of this %claimTypeText% while active. ": "This refundable boost will improve the discoverability of this %claimTypeText% while active. ", - "%repost_channel_link%": "%repost_channel_link%", "Show less": "Show less", - "Channel \"realporno\" blocked.": "Channel \"realporno\" blocked.", "Elements": "Elements", "Icons": "Icons", - "You followed @MinutePhysics!": "You followed @MinutePhysics!", - "Unfollowed @samtime.": "Unfollowed @samtime.", - "You followed @samtime!": "You followed @samtime!", - "Unfollowed @gatogalactico.": "Unfollowed @gatogalactico.", - "You followed @gatogalactico!": "You followed @gatogalactico!", - "Unfollowed @Odysee.": "Unfollowed @Odysee.", - "Unfollowed @rossmanngroup.": "Unfollowed @rossmanngroup.", - "You followed @rossmanngroup!": "You followed @rossmanngroup!", - "%repost% %publish%": "%repost% %publish%", - "Failed to view lbry://@MicheL-PDF#7/LaDameAuPain#f, please try again. If this problem persists, visit https://lbry.com/faq/support for support.": "Failed to view lbry://@MicheL-PDF#7/LaDameAuPain#f, please try again. If this problem persists, visit https://lbry.com/faq/support for support.", "Go to": "Go to", + "Clearing...": "Clearing...", + "Clear Views": "Clear Views", + "Show Video View Progress": "Show Video View Progress", + "Display view progress on thumbnail. This setting will not hide any blockchain activity or downloads.": "Display view progress on thumbnail. This setting will not hide any blockchain activity or downloads.", "--end--": "--end--" } diff --git a/ui/component/fileThumbnail/index.js b/ui/component/fileThumbnail/index.js index 61122facd..f9f447877 100644 --- a/ui/component/fileThumbnail/index.js +++ b/ui/component/fileThumbnail/index.js @@ -1,12 +1,17 @@ import { connect } from 'react-redux'; import { doResolveUri } from 'redux/actions/claims'; import { makeSelectClaimForUri } from 'redux/selectors/claims'; -import { makeSelectContentPositionForUri } from 'redux/selectors/content'; +import { makeSelectContentWatchedPercentageForUri } from 'redux/selectors/content'; import CardMedia from './view'; +import { makeSelectClientSetting } from 'redux/selectors/settings'; +import * as SETTINGS from 'constants/settings'; -const select = (state, props) => ({ - position: makeSelectContentPositionForUri(props.uri)(state), - claim: makeSelectClaimForUri(props.uri)(state), -}); +const select = (state, props) => { + return { + watchedPercentage: makeSelectContentWatchedPercentageForUri(props.uri)(state), + claim: makeSelectClaimForUri(props.uri)(state), + showPercentage: makeSelectClientSetting(SETTINGS.PERSIST_WATCH_TIME)(state), + }; +}; export default connect(select, { doResolveUri })(CardMedia); diff --git a/ui/component/fileThumbnail/view.jsx b/ui/component/fileThumbnail/view.jsx index b658ac215..8d642c00f 100644 --- a/ui/component/fileThumbnail/view.jsx +++ b/ui/component/fileThumbnail/view.jsx @@ -16,11 +16,22 @@ type Props = { claim: ?StreamClaim, doResolveUri: (string) => void, className?: string, - position?: number, + watchedPercentage: number, + showPercentage: boolean, }; function FileThumbnail(props: Props) { - const { claim, uri, doResolveUri, thumbnail: rawThumbnail, children, allowGifs = false, className, position } = props; + const { + claim, + uri, + doResolveUri, + thumbnail: rawThumbnail, + children, + allowGifs = false, + className, + watchedPercentage, + showPercentage, + } = props; const passedThumbnail = rawThumbnail && rawThumbnail.trim().replace(/^http:\/\//i, 'https://'); const thumbnailFromClaim = @@ -30,11 +41,9 @@ function FileThumbnail(props: Props) { const hasResolvedClaim = claim !== undefined; const isGif = thumbnail && thumbnail.endsWith('gif'); - const media = claim && claim.value && (claim.value.video || claim.value.audio); - const duration = media && media.duration; - const viewedBar = position && duration && ( + const viewedBar = showPercentage && watchedPercentage && (
-
+
); diff --git a/ui/component/settingContent/index.js b/ui/component/settingContent/index.js index c0315f279..a934aa76b 100644 --- a/ui/component/settingContent/index.js +++ b/ui/component/settingContent/index.js @@ -1,7 +1,7 @@ import { connect } from 'react-redux'; import { selectMyChannelUrls } from 'redux/selectors/claims'; import * as SETTINGS from 'constants/settings'; -import { doSetPlayingUri } from 'redux/actions/content'; +import { doSetPlayingUri, clearContentCache } from 'redux/actions/content'; import { doSetClientSetting } from 'redux/actions/settings'; import { selectShowMatureContent, makeSelectClientSetting } from 'redux/selectors/settings'; import { selectUserVerifiedEmail } from 'redux/selectors/user'; @@ -14,6 +14,7 @@ const select = (state) => ({ autoplayMedia: makeSelectClientSetting(SETTINGS.AUTOPLAY_MEDIA)(state), autoplayNext: makeSelectClientSetting(SETTINGS.AUTOPLAY_NEXT)(state), hideReposts: makeSelectClientSetting(SETTINGS.HIDE_REPOSTS)(state), + persistWatchTime: makeSelectClientSetting(SETTINGS.PERSIST_WATCH_TIME)(state), showNsfw: selectShowMatureContent(state), myChannelUrls: selectMyChannelUrls(state), instantPurchaseEnabled: makeSelectClientSetting(SETTINGS.INSTANT_PURCHASE_ENABLED)(state), @@ -24,6 +25,7 @@ const select = (state) => ({ const perform = (dispatch) => ({ setClientSetting: (key, value) => dispatch(doSetClientSetting(key, value)), clearPlayingUri: () => dispatch(doSetPlayingUri({ uri: null })), + clearContentCache: () => dispatch(clearContentCache()), }); export default connect(select, perform)(SettingContent); diff --git a/ui/component/settingContent/view.jsx b/ui/component/settingContent/view.jsx index bce8a4a17..50d0ada0e 100644 --- a/ui/component/settingContent/view.jsx +++ b/ui/component/settingContent/view.jsx @@ -24,6 +24,7 @@ type Props = { autoplayNext: boolean, hideReposts: ?boolean, showNsfw: boolean, + persistWatchTime: boolean, myChannelUrls: ?Array, instantPurchaseEnabled: boolean, instantPurchaseMax: Price, @@ -31,6 +32,7 @@ type Props = { // --- perform --- setClientSetting: (string, boolean | string | number) => void, clearPlayingUri: () => void, + // clearContentCache: () => void, }; export default function SettingContent(props: Props) { @@ -40,6 +42,7 @@ export default function SettingContent(props: Props) { autoplayMedia, autoplayNext, hideReposts, + persistWatchTime, showNsfw, myChannelUrls, instantPurchaseEnabled, @@ -47,7 +50,21 @@ export default function SettingContent(props: Props) { enablePublishPreview, setClientSetting, clearPlayingUri, + // clearContentCache, } = props; + // feature disabled until styling is ironed out + // const [contentCacheCleared, setContentCacheCleared] = React.useState(false); + // const [clearingContentCache, setClearingContentCache] = React.useState(false); + // const onClearContentCache = React.useCallback(() => { + // setClearingContentCache(true); + // clearContentCache(); + // // Just a small timer to give the user a visual effect + // // that the content is being cleared. + // setTimeout(() => { + // setClearingContentCache(false); + // setContentCacheCleared(true); + // }, 2000); + // }, [setClearingContentCache, clearContentCache, setContentCacheCleared]); return ( <> @@ -102,6 +119,32 @@ export default function SettingContent(props: Props) { checked={hideReposts} /> + + {/*
*/} + setClientSetting(SETTINGS.PERSIST_WATCH_TIME, !persistWatchTime)} + checked={persistWatchTime} + /> + {/*
+ Disabled until styling is better +
+
*/} +
Number(currTime)) { + savePosition(uri, player.currentTime()); + } } function restorePlaybackRate(player) { diff --git a/ui/constants/action_types.js b/ui/constants/action_types.js index e12958db6..d44f33de3 100644 --- a/ui/constants/action_types.js +++ b/ui/constants/action_types.js @@ -143,6 +143,7 @@ export const SET_PRIMARY_URI = 'SET_PRIMARY_URI'; export const SET_PLAYING_URI = 'SET_PLAYING_URI'; export const SET_CONTENT_POSITION = 'SET_CONTENT_POSITION'; export const CLEAR_CONTENT_POSITION = 'CLEAR_CONTENT_POSITION'; +export const CLEAR_CONTENT_CACHE = 'CLEAR_CONTENT_CACHE'; export const SET_CONTENT_LAST_VIEWED = 'SET_CONTENT_LAST_VIEWED'; export const CLEAR_CONTENT_HISTORY_URI = 'CLEAR_CONTENT_HISTORY_URI'; export const CLEAR_CONTENT_HISTORY_ALL = 'CLEAR_CONTENT_HISTORY_ALL'; diff --git a/ui/constants/settings.js b/ui/constants/settings.js index a225a6120..d73339b38 100644 --- a/ui/constants/settings.js +++ b/ui/constants/settings.js @@ -35,6 +35,7 @@ export const REWARDS_ACKNOWLEDGED = 'rewards_acknowledged'; export const SEARCH_IN_LANGUAGE = 'search_in_language'; export const HOMEPAGE = 'homepage'; export const HIDE_REPOSTS = 'hide_reposts'; +export const PERSIST_WATCH_TIME = 'persist_watch_time'; export const SUPPORT_OPTION = 'support_option'; export const TILE_LAYOUT = 'tile_layout'; export const VIDEO_THEATER_MODE = 'video_theater_mode'; diff --git a/ui/redux/actions/content.js b/ui/redux/actions/content.js index 7c8a92839..11f0be3fa 100644 --- a/ui/redux/actions/content.js +++ b/ui/redux/actions/content.js @@ -239,9 +239,18 @@ export function clearPosition(uri: string) { return (dispatch: Dispatch, getState: () => any) => { const state = getState(); const claim = makeSelectClaimForUri(uri)(state); + const persistWatchTime = makeSelectClientSetting(SETTINGS.PERSIST_WATCH_TIME)(state); const { claim_id: claimId, txid, nout } = claim; const outpoint = `${txid}:${nout}`; + if (persistWatchTime) { + dispatch({ + type: ACTIONS.SET_CONTENT_POSITION, + data: { claimId, outpoint, position: null }, + }); + return; + } + dispatch({ type: ACTIONS.CLEAR_CONTENT_POSITION, data: { claimId, outpoint }, @@ -249,6 +258,12 @@ export function clearPosition(uri: string) { }; } +export function clearContentCache() { + return { + type: ACTIONS.CLEAR_CONTENT_CACHE, + }; +} + export function doSetContentHistoryItem(uri: string) { return (dispatch: Dispatch) => { dispatch({ diff --git a/ui/redux/reducers/content.js b/ui/redux/reducers/content.js index 315df9b2a..72ef10562 100644 --- a/ui/redux/reducers/content.js +++ b/ui/redux/reducers/content.js @@ -90,6 +90,13 @@ reducers[ACTIONS.CLEAR_CONTENT_POSITION] = (state, action) => { } }; +reducers[ACTIONS.CLEAR_CONTENT_CACHE] = (state, action) => { + return { + ...state, + positions: {}, + }; +}; + reducers[ACTIONS.SET_CONTENT_LAST_VIEWED] = (state, action) => { const { uri, lastViewed } = action.data; const { history } = state; diff --git a/ui/redux/reducers/settings.js b/ui/redux/reducers/settings.js index 3a868b8ab..7ceea70d3 100644 --- a/ui/redux/reducers/settings.js +++ b/ui/redux/reducers/settings.js @@ -67,6 +67,7 @@ const defaultState = { [SETTINGS.FLOATING_PLAYER]: true, [SETTINGS.AUTO_DOWNLOAD]: true, [SETTINGS.HIDE_REPOSTS]: false, + [SETTINGS.PERSIST_WATCH_TIME]: true, // OS [SETTINGS.AUTO_LAUNCH]: true, diff --git a/ui/redux/selectors/content.js b/ui/redux/selectors/content.js index f3d4e9784..6ec055e4a 100644 --- a/ui/redux/selectors/content.js +++ b/ui/redux/selectors/content.js @@ -63,6 +63,31 @@ export const makeSelectContentPositionForUri = (uri: string) => return state.positions[id] ? state.positions[id][outpoint] : null; }); +export const makeSelectContentWatchedPercentageForUri = (uri: string) => + createSelector(selectState, makeSelectClaimForUri(uri), (state, claim) => { + if (!claim) { + return 0; + } + const media = claim.value && (claim.value.video || claim.value.audio); + if (!media) { + return 0; + } + const id = claim.claim_id; + if (!state.positions[id]) { + return 0; + } + const outpoint = `${claim.txid}:${claim.nout}`; + const watched = state.positions[id][outpoint]; + // If the user turns on the persist watch setting, + // clearing the position will set it to null, + // which means the entire video has been watched. + if (watched === null) { + return 100; + } + const duration = media.duration; + return (watched / duration) * 100; + }); + export const selectHistory = createSelector(selectState, (state) => state.history || []); export const selectHistoryPageCount = createSelector(selectHistory, (history) => diff --git a/ui/scss/component/_file-thumbnail.scss b/ui/scss/component/_file-thumbnail.scss index a4824fc07..ebfc4339c 100644 --- a/ui/scss/component/_file-thumbnail.scss +++ b/ui/scss/component/_file-thumbnail.scss @@ -4,10 +4,10 @@ left: 0; width: 100%; height: 5px; - background-color: gray; + background-color: var(--color-gray-7); } .file-thumbnail__viewed-bar-progress { height: 5px; - background-color: red; + background-color: var(--color-primary); } diff --git a/ui/scss/component/_settings.scss b/ui/scss/component/_settings.scss index 631e0e606..63f763ef3 100644 --- a/ui/scss/component/_settings.scss +++ b/ui/scss/component/_settings.scss @@ -18,3 +18,14 @@ margin-left: 2.2rem; } } + +.settings__persistWatchTimeCheckbox { + display: flex; + justify-content: end; + padding-bottom: var(--spacing-m); +} + +.settings__persistWatchTimeCheckbox { + text-align: right; + padding-top: var(--spacing-m); +}