Add persistent watch time setting. #7547
14 changed files with 144 additions and 28 deletions
|
@ -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--"
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 && (
|
||||
<div className="file-thumbnail__viewed-bar">
|
||||
<div className="file-thumbnail__viewed-bar-progress" style={{ width: (position / duration) * 100 + '%' }} />
|
||||
<div className="file-thumbnail__viewed-bar-progress" style={{ width: `${watchedPercentage}%` }} />
|
||||
</div>
|
||||
);
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -24,6 +24,7 @@ type Props = {
|
|||
autoplayNext: boolean,
|
||||
hideReposts: ?boolean,
|
||||
showNsfw: boolean,
|
||||
persistWatchTime: boolean,
|
||||
myChannelUrls: ?Array<string>,
|
||||
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}
|
||||
/>
|
||||
</SettingsRow>
|
||||
<SettingsRow title={__('Show Video View Progress')} subtitle={__(HELP.PERSIST_WATCH_TIME)}>
|
||||
{/* <div className="settings__persistWatchTimeCheckbox"> */}
|
||||
<FormField
|
||||
type="checkbox"
|
||||
name="persist_watch_time"
|
||||
onChange={() => setClientSetting(SETTINGS.PERSIST_WATCH_TIME, !persistWatchTime)}
|
||||
checked={persistWatchTime}
|
||||
/>
|
||||
{/* </div>
|
||||
Disabled until styling is better
|
||||
<div className="settings__persistWatchTimeClearCache">
|
||||
<Button
|
||||
button="primary"
|
||||
icon={ICONS.ALERT}
|
||||
label={
|
||||
contentCacheCleared
|
||||
? __('Views cleared')
|
||||
: clearingContentCache
|
||||
? __('Clearing...')
|
||||
: __('Clear Views')
|
||||
}
|
||||
onClick={onClearContentCache}
|
||||
disabled={clearingContentCache || contentCacheCleared}
|
||||
/>
|
||||
</div> */}
|
||||
</SettingsRow>
|
||||
<SettingsRow title={__('Show mature content')} subtitle={__(HELP.SHOW_MATURE)}>
|
||||
<FormField
|
||||
type="checkbox"
|
||||
|
@ -188,6 +231,7 @@ const HELP = {
|
|||
AUTOPLAY_MEDIA: 'Autoplay video and audio files when navigating to a file.',
|
||||
AUTOPLAY_NEXT: 'Autoplay the next related item when a file (video or audio) finishes playing.',
|
||||
HIDE_REPOSTS: 'You will not see reposts by people you follow or receive email notifying about them.',
|
||||
PERSIST_WATCH_TIME: 'Display view progress on thumbnail. This setting will not hide any blockchain activity or downloads.',
|
||||
SHOW_MATURE: 'Mature content may include nudity, intense sexuality, profanity, or other adult content. By displaying mature content, you are affirming you are of legal age to view mature content in your country or jurisdiction. ',
|
||||
MAX_PURCHASE_PRICE: 'This will prevent you from purchasing any content over a certain cost, as a safety measure.',
|
||||
ONLY_CONFIRM_OVER_AMOUNT: '', // [feel redundant. Disable for now] "When this option is chosen, LBRY won't ask you to confirm purchases or tips below your chosen amount.",
|
||||
|
|
|
@ -270,7 +270,11 @@ function VideoViewer(props: Props) {
|
|||
}
|
||||
|
||||
function handlePosition(player) {
|
||||
savePosition(uri, player.currentTime());
|
||||
const currTime = player.currentTime();
|
||||
const durationInSeconds = claim.value.video && claim.value.video.duration;
|
||||
if (Number(durationInSeconds) > Number(currTime)) {
|
||||
savePosition(uri, player.currentTime());
|
||||
}
|
||||
}
|
||||
|
||||
function restorePlaybackRate(player) {
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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) =>
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue