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);
+}