Watch history (#1441)

This commit is contained in:
Raphael Wickihalder 2022-05-05 21:50:30 +08:00 committed by infinite-persistence
commit 99383272a8
No known key found for this signature in database
GPG key ID: B9C3252EDC3D0AA0
14 changed files with 177 additions and 6 deletions

View file

@ -60,6 +60,7 @@ type Props = {
droppableProvided?: any, droppableProvided?: any,
unavailableUris?: Array<string>, unavailableUris?: Array<string>,
showMemberBadge?: boolean, showMemberBadge?: boolean,
inWatchHistory?: boolean,
}; };
export default function ClaimList(props: Props) { export default function ClaimList(props: Props) {
@ -100,6 +101,7 @@ export default function ClaimList(props: Props) {
droppableProvided, droppableProvided,
unavailableUris, unavailableUris,
showMemberBadge, showMemberBadge,
inWatchHistory,
} = props; } = props;
const [currentSort, setCurrentSort] = usePersistedState(persistedStorageKey, SORT_NEW); const [currentSort, setCurrentSort] = usePersistedState(persistedStorageKey, SORT_NEW);
@ -202,6 +204,7 @@ export default function ClaimList(props: Props) {
dragHandleProps={draggableProvided && draggableProvided.dragHandleProps} dragHandleProps={draggableProvided && draggableProvided.dragHandleProps}
unavailableUris={unavailableUris} unavailableUris={unavailableUris}
showMemberBadge={showMemberBadge} showMemberBadge={showMemberBadge}
inWatchHistory={inWatchHistory}
/> />
); );

View file

@ -21,6 +21,7 @@ import { selectIsSubscribedForUri } from 'redux/selectors/subscriptions';
import { isClaimNsfw, isStreamPlaceholderClaim } from 'util/claim'; import { isClaimNsfw, isStreamPlaceholderClaim } from 'util/claim';
import ClaimPreview from './view'; import ClaimPreview from './view';
import formatMediaDuration from 'util/formatMediaDuration'; import formatMediaDuration from 'util/formatMediaDuration';
import { doClearContentHistoryUri } from 'redux/actions/content';
const select = (state, props) => { const select = (state, props) => {
const claim = props.uri && selectClaimForUri(state, props.uri); const claim = props.uri && selectClaimForUri(state, props.uri);
@ -55,6 +56,7 @@ const select = (state, props) => {
const perform = (dispatch) => ({ const perform = (dispatch) => ({
resolveUri: (uri) => dispatch(doResolveUri(uri)), resolveUri: (uri) => dispatch(doResolveUri(uri)),
getFile: (uri) => dispatch(doFileGet(uri, false)), getFile: (uri) => dispatch(doFileGet(uri, false)),
doClearContentHistoryUri: (uri) => dispatch(doClearContentHistoryUri(uri)),
}); });
export default connect(select, perform)(ClaimPreview); export default connect(select, perform)(ClaimPreview);

View file

@ -96,6 +96,8 @@ type Props = {
dragHandleProps?: any, dragHandleProps?: any,
unavailableUris?: Array<string>, unavailableUris?: Array<string>,
showMemberBadge?: boolean, showMemberBadge?: boolean,
inWatchHistory?: boolean,
doClearContentHistoryUri: (uri: string) => void,
}; };
const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => { const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
@ -161,6 +163,8 @@ const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
dragHandleProps, dragHandleProps,
unavailableUris, unavailableUris,
showMemberBadge, showMemberBadge,
inWatchHistory,
doClearContentHistoryUri,
} = props; } = props;
const isMobile = useIsMobile(); const isMobile = useIsMobile();
@ -292,6 +296,11 @@ const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
} }
} }
function removeFromHistory(e, uri) {
e.stopPropagation();
doClearContentHistoryUri(uri);
}
useEffect(() => { useEffect(() => {
if (isValid && !isResolvingUri && shouldFetch && uri) { if (isValid && !isResolvingUri && shouldFetch && uri) {
resolveUri(uri); resolveUri(uri);
@ -364,7 +373,6 @@ const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
> >
<> <>
{!hideRepostLabel && <ClaimRepostAuthor uri={uri} />} {!hideRepostLabel && <ClaimRepostAuthor uri={uri} />}
<div <div
className={classnames('claim-preview', { className={classnames('claim-preview', {
'claim-preview--small': type === 'small' || type === 'tooltip', 'claim-preview--small': type === 'small' || type === 'tooltip',
@ -487,7 +495,11 @@ const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
)} )}
</div> </div>
</div> </div>
{inWatchHistory && (
<div onClick={(e) => removeFromHistory(e, uri)} className="claim-preview__history-remove">
<Icon icon={ICONS.REMOVE} />
</div>
)}
{/* Todo: check isLivestreamActive once we have that data consistently everywhere. */} {/* Todo: check isLivestreamActive once we have that data consistently everywhere. */}
{claim && isLivestream && <ClaimPreviewReset uri={uri} />} {claim && isLivestream && <ClaimPreviewReset uri={uri} />}

View file

@ -3297,4 +3297,9 @@ export const icons = {
</g> </g>
</svg> </svg>
), ),
[ICONS.WATCH_HISTORY]: buildIcon(
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M0.6,11.8c0-6,5-11,11-11 M9.6,7.2v9.5l6.9-4.7L9.6,7.2z M-2.1,9.5l2.9,2.9l3.2-2.7 M11.4,23.2 v-0.9 M5.6,21.5L6,20.6 M2.1,16.4l-0.8,0.4 M17,20.8l0.5,0.8 M20.9,16.7l0.8,0.5 M23.1,11l-0.9,0.1 M21,5.2l-0.7,0.5 M16.2,1.2 L15.8,2" />
</svg>
),
}; };

View file

@ -70,6 +70,7 @@ const LibraryPage = lazyImport(() => import('page/library' /* webpackChunkName:
const ListBlockedPage = lazyImport(() => import('page/listBlocked' /* webpackChunkName: "listBlocked" */)); const ListBlockedPage = lazyImport(() => import('page/listBlocked' /* webpackChunkName: "listBlocked" */));
const ListsPage = lazyImport(() => import('page/lists' /* webpackChunkName: "lists" */)); const ListsPage = lazyImport(() => import('page/lists' /* webpackChunkName: "lists" */));
const PlaylistsPage = lazyImport(() => import('page/playlists' /* webpackChunkName: "lists" */)); const PlaylistsPage = lazyImport(() => import('page/playlists' /* webpackChunkName: "lists" */));
const WatchHistoryPage = lazyImport(() => import('page/watchHistory' /* webpackChunkName: "history" */));
const LiveStreamSetupPage = lazyImport(() => import('page/livestreamSetup' /* webpackChunkName: "livestreamSetup" */)); const LiveStreamSetupPage = lazyImport(() => import('page/livestreamSetup' /* webpackChunkName: "livestreamSetup" */));
const LivestreamCurrentPage = lazyImport(() => const LivestreamCurrentPage = lazyImport(() =>
import('page/livestreamCurrent' /* webpackChunkName: "livestreamCurrent" */) import('page/livestreamCurrent' /* webpackChunkName: "livestreamCurrent" */)
@ -362,6 +363,7 @@ function AppRouter(props: Props) {
<PrivateRoute {...props} path={`/$/${PAGES.LIBRARY}`} component={LibraryPage} /> <PrivateRoute {...props} path={`/$/${PAGES.LIBRARY}`} component={LibraryPage} />
<PrivateRoute {...props} path={`/$/${PAGES.LISTS}`} component={ListsPage} /> <PrivateRoute {...props} path={`/$/${PAGES.LISTS}`} component={ListsPage} />
<PrivateRoute {...props} path={`/$/${PAGES.PLAYLISTS}`} component={PlaylistsPage} /> <PrivateRoute {...props} path={`/$/${PAGES.PLAYLISTS}`} component={PlaylistsPage} />
<PrivateRoute {...props} path={`/$/${PAGES.WATCH_HISTORY}`} component={WatchHistoryPage} />
<PrivateRoute {...props} path={`/$/${PAGES.TAGS_FOLLOWING_MANAGE}`} component={TagsFollowingManagePage} /> <PrivateRoute {...props} path={`/$/${PAGES.TAGS_FOLLOWING_MANAGE}`} component={TagsFollowingManagePage} />
<PrivateRoute {...props} path={`/$/${PAGES.SETTINGS_BLOCKED_MUTED}`} component={ListBlockedPage} /> <PrivateRoute {...props} path={`/$/${PAGES.SETTINGS_BLOCKED_MUTED}`} component={ListBlockedPage} />
<PrivateRoute {...props} path={`/$/${PAGES.SETTINGS_CREATOR}`} component={SettingsCreatorPage} /> <PrivateRoute {...props} path={`/$/${PAGES.SETTINGS_CREATOR}`} component={SettingsCreatorPage} />

View file

@ -87,6 +87,13 @@ const PLAYLISTS: SideNavLink = {
hideForUnauth: true, hideForUnauth: true,
}; };
const WATCH_HISTORY: SideNavLink = {
title: 'Watch History',
link: `/$/${PAGES.WATCH_HISTORY}`,
icon: ICONS.WATCH_HISTORY,
hideForUnauth: true,
};
const PREMIUM: SideNavLink = { const PREMIUM: SideNavLink = {
title: 'Premium', title: 'Premium',
link: `/$/${PAGES.ODYSEE_MEMBERSHIP}`, link: `/$/${PAGES.ODYSEE_MEMBERSHIP}`,
@ -529,6 +536,7 @@ function SideNavigation(props: Props) {
{!showMicroMenu && getLink(WATCH_LATER)} {!showMicroMenu && getLink(WATCH_LATER)}
{!showMicroMenu && getLink(FAVORITES)} {!showMicroMenu && getLink(FAVORITES)}
{getLink(PLAYLISTS)} {getLink(PLAYLISTS)}
{!showMicroMenu && getLink(WATCH_HISTORY)}
</ul> </ul>
<ul <ul

View file

@ -16,7 +16,13 @@ import {
doAnalyticsView, doAnalyticsView,
} from 'redux/actions/app'; } from 'redux/actions/app';
import { selectVolume, selectMute } from 'redux/selectors/app'; import { selectVolume, selectMute } from 'redux/selectors/app';
import { savePosition, clearPosition, doPlayUri, doSetPlayingUri } from 'redux/actions/content'; import {
savePosition,
clearPosition,
doPlayUri,
doSetPlayingUri,
doSetContentHistoryItem,
} from 'redux/actions/content';
import { makeSelectIsPlayerFloating, selectContentPositionForUri, selectPlayingUri } from 'redux/selectors/content'; import { makeSelectIsPlayerFloating, selectContentPositionForUri, selectPlayingUri } from 'redux/selectors/content';
import { selectRecommendedContentForUri } from 'redux/selectors/search'; import { selectRecommendedContentForUri } from 'redux/selectors/search';
import VideoViewer from './view'; import VideoViewer from './view';
@ -104,6 +110,7 @@ const perform = (dispatch) => ({
doAnalyticsView: (uri, timeToStart) => dispatch(doAnalyticsView(uri, timeToStart)), doAnalyticsView: (uri, timeToStart) => dispatch(doAnalyticsView(uri, timeToStart)),
claimRewards: () => dispatch(doClaimEligiblePurchaseRewards()), claimRewards: () => dispatch(doClaimEligiblePurchaseRewards()),
doToast: (props) => dispatch(doToast(props)), doToast: (props) => dispatch(doToast(props)),
doSetContentHistoryItem: (uri) => dispatch(doSetContentHistoryItem(uri)),
}); });
export default withRouter(connect(select, perform)(VideoViewer)); export default withRouter(connect(select, perform)(VideoViewer));

View file

@ -72,6 +72,8 @@ type Props = {
activeLivestreamForChannel: any, activeLivestreamForChannel: any,
defaultQuality: ?string, defaultQuality: ?string,
doToast: ({ message: string, linkText: string, linkTarget: string }) => void, doToast: ({ message: string, linkText: string, linkTarget: string }) => void,
doSetContentHistoryItem: (uri: string) => void,
doClearContentHistoryUri: (uri: string) => void,
}; };
/* /*
@ -118,6 +120,7 @@ function VideoViewer(props: Props) {
activeLivestreamForChannel, activeLivestreamForChannel,
defaultQuality, defaultQuality,
doToast, doToast,
doSetContentHistoryItem,
} = props; } = props;
const permanentUrl = claim && claim.permanent_url; const permanentUrl = claim && claim.permanent_url;
@ -151,6 +154,12 @@ function VideoViewer(props: Props) {
const isFirstRender = React.useRef(true); const isFirstRender = React.useRef(true);
const playerRef = React.useRef(null); const playerRef = React.useRef(null);
React.useEffect(() => {
if (isPlaying) {
doSetContentHistoryItem(claim.permanent_url);
}
}, [isPlaying]);
useEffect(() => { useEffect(() => {
if (isFirstRender.current) { if (isFirstRender.current) {
isFirstRender.current = false; isFirstRender.current = false;

View file

@ -25,7 +25,7 @@ export const FEEDBACK = 'MessageSquare';
export const SEARCH = 'Search'; export const SEARCH = 'Search';
export const CHANNEL = 'AtSign'; export const CHANNEL = 'AtSign';
export const REFRESH = 'RefreshCw'; export const REFRESH = 'RefreshCw';
export const HISTORY = 'Clock'; export const WATCH_HISTORY = 'WatchHistory';
export const HOME = 'Home'; export const HOME = 'Home';
export const OVERVIEW = 'Activity'; export const OVERVIEW = 'Activity';
export const WALLET = 'List'; export const WALLET = 'List';

View file

@ -34,6 +34,7 @@ exports.HELP = 'help';
exports.LIBRARY = 'library'; exports.LIBRARY = 'library';
exports.LISTS = 'lists'; exports.LISTS = 'lists';
exports.PLAYLISTS = 'playlists'; exports.PLAYLISTS = 'playlists';
exports.WATCH_HISTORY = 'watchhistory';
exports.INVITE = 'invite'; exports.INVITE = 'invite';
exports.DEPRECATED__DOWNLOADED = 'downloaded'; exports.DEPRECATED__DOWNLOADED = 'downloaded';
exports.DEPRECATED__PUBLISH = 'publish'; exports.DEPRECATED__PUBLISH = 'publish';

View file

@ -0,0 +1,17 @@
import { connect } from 'react-redux';
import { withRouter } from 'react-router-dom';
import WatchHistoryPage from './view';
import { selectHistory } from 'redux/selectors/content';
import { doClearContentHistoryAll } from 'redux/actions/content';
const select = (state) => {
return {
history: selectHistory(state),
};
};
const perform = (dispatch) => ({
doClearContentHistoryAll: () => dispatch(doClearContentHistoryAll()),
});
export default withRouter(connect(select, perform)(WatchHistoryPage));

View file

@ -0,0 +1,68 @@
// @flow
import React from 'react';
import ClaimList from 'component/claimList';
import Page from 'component/page';
import Button from 'component/button';
import classnames from 'classnames';
import Icon from 'component/common/icon';
import * as ICONS from 'constants/icons';
import { YRBL_SAD_IMG_URL } from 'config';
import Tooltip from 'component/common/tooltip';
export const PAGE_VIEW_QUERY = 'view';
type Props = {
history: Array<any>,
doClearContentHistoryAll: () => void,
};
export default function WatchHistoryPage(props: Props) {
const { history, doClearContentHistoryAll } = props;
const [unavailableUris] = React.useState([]);
const watchHistory = [];
for (let entry of history) {
if (entry.uri.indexOf('@') !== -1) {
watchHistory.push(entry.uri);
}
}
function clearHistory() {
doClearContentHistoryAll();
}
return (
<Page className="historyPage-wrapper">
<div className={classnames('section card-stack')}>
<div className="claim-list__header">
<h1 className="card__title">
<Icon icon={ICONS.WATCH_HISTORY} style={{ marginRight: 'var(--spacing-s)' }} />
{__('Watch History')}
<Tooltip title={__('Currently, your watch history is only saved locally.')}>
<Button className="icon--help" icon={ICONS.HELP} iconSize={14} />
</Tooltip>
</h1>
<div className="claim-list__alt-controls--wrap">
{watchHistory.length > 0 && (
<Button
title={__('Clear History')}
button="primary"
label={__('Clear History')}
onClick={() => clearHistory()}
/>
)}
</div>
</div>
{watchHistory.length > 0 && <ClaimList uris={watchHistory} unavailableUris={unavailableUris} inWatchHistory />}
{watchHistory.length === 0 && (
<div style={{ textAlign: 'center' }}>
<img src={YRBL_SAD_IMG_URL} />
<h2 className="main--empty empty" style={{ marginTop: '0' }}>
{__('Nothing here')}
</h2>
</div>
)}
</div>
</Page>
);
}

View file

@ -41,7 +41,7 @@ $nag-error-z-index: 999;
background-color: var(--color-primary); background-color: var(--color-primary);
color: var(--color-white); color: var(--color-white);
z-index: $nag-helpful-z-index; z-index: $nag-helpful-z-index;
border-radius: var(--border-radius); // border-radius: var(--border-radius);
margin-top: var(--spacing-s); margin-top: var(--spacing-s);
} }
@ -88,7 +88,7 @@ $nag-error-z-index: 999;
.nag__button--helpful { .nag__button--helpful {
&:hover { &:hover {
color: var(--color-secondary); color: var(--color-primary);
} }
} }

View file

@ -1205,6 +1205,43 @@ img {
} }
} }
.historyPage-wrapper {
.claim-preview__wrapper {
.claim-preview__history-remove {
position: absolute;
top: var(--spacing-s);
right: var(--spacing-xs);
opacity: 0;
.icon {
width: 22px;
height: 22px;
}
&:hover {
.icon {
stroke: var(--color-primary);
cursor: pointer;
}
}
@media (max-width: $breakpoint-small) {
opacity: 1;
}
}
.menu__button.claim__menu-button {
top: 2.2rem;
}
&:hover {
.claim-preview__history-remove {
opacity: 1;
}
}
}
}
.premium-wrapper { .premium-wrapper {
.membership_title { .membership_title {
.comment__badge { .comment__badge {