Watch history (#1441)
This commit is contained in:
commit
99383272a8
14 changed files with 177 additions and 6 deletions
|
@ -60,6 +60,7 @@ type Props = {
|
|||
droppableProvided?: any,
|
||||
unavailableUris?: Array<string>,
|
||||
showMemberBadge?: boolean,
|
||||
inWatchHistory?: boolean,
|
||||
};
|
||||
|
||||
export default function ClaimList(props: Props) {
|
||||
|
@ -100,6 +101,7 @@ export default function ClaimList(props: Props) {
|
|||
droppableProvided,
|
||||
unavailableUris,
|
||||
showMemberBadge,
|
||||
inWatchHistory,
|
||||
} = props;
|
||||
|
||||
const [currentSort, setCurrentSort] = usePersistedState(persistedStorageKey, SORT_NEW);
|
||||
|
@ -202,6 +204,7 @@ export default function ClaimList(props: Props) {
|
|||
dragHandleProps={draggableProvided && draggableProvided.dragHandleProps}
|
||||
unavailableUris={unavailableUris}
|
||||
showMemberBadge={showMemberBadge}
|
||||
inWatchHistory={inWatchHistory}
|
||||
/>
|
||||
);
|
||||
|
||||
|
|
|
@ -21,6 +21,7 @@ import { selectIsSubscribedForUri } from 'redux/selectors/subscriptions';
|
|||
import { isClaimNsfw, isStreamPlaceholderClaim } from 'util/claim';
|
||||
import ClaimPreview from './view';
|
||||
import formatMediaDuration from 'util/formatMediaDuration';
|
||||
import { doClearContentHistoryUri } from 'redux/actions/content';
|
||||
|
||||
const select = (state, props) => {
|
||||
const claim = props.uri && selectClaimForUri(state, props.uri);
|
||||
|
@ -55,6 +56,7 @@ const select = (state, props) => {
|
|||
const perform = (dispatch) => ({
|
||||
resolveUri: (uri) => dispatch(doResolveUri(uri)),
|
||||
getFile: (uri) => dispatch(doFileGet(uri, false)),
|
||||
doClearContentHistoryUri: (uri) => dispatch(doClearContentHistoryUri(uri)),
|
||||
});
|
||||
|
||||
export default connect(select, perform)(ClaimPreview);
|
||||
|
|
|
@ -96,6 +96,8 @@ type Props = {
|
|||
dragHandleProps?: any,
|
||||
unavailableUris?: Array<string>,
|
||||
showMemberBadge?: boolean,
|
||||
inWatchHistory?: boolean,
|
||||
doClearContentHistoryUri: (uri: string) => void,
|
||||
};
|
||||
|
||||
const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
|
||||
|
@ -161,6 +163,8 @@ const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
|
|||
dragHandleProps,
|
||||
unavailableUris,
|
||||
showMemberBadge,
|
||||
inWatchHistory,
|
||||
doClearContentHistoryUri,
|
||||
} = props;
|
||||
|
||||
const isMobile = useIsMobile();
|
||||
|
@ -292,6 +296,11 @@ const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
|
|||
}
|
||||
}
|
||||
|
||||
function removeFromHistory(e, uri) {
|
||||
e.stopPropagation();
|
||||
doClearContentHistoryUri(uri);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (isValid && !isResolvingUri && shouldFetch && uri) {
|
||||
resolveUri(uri);
|
||||
|
@ -364,7 +373,6 @@ const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
|
|||
>
|
||||
<>
|
||||
{!hideRepostLabel && <ClaimRepostAuthor uri={uri} />}
|
||||
|
||||
<div
|
||||
className={classnames('claim-preview', {
|
||||
'claim-preview--small': type === 'small' || type === 'tooltip',
|
||||
|
@ -487,7 +495,11 @@ const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
|
|||
)}
|
||||
</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. */}
|
||||
{claim && isLivestream && <ClaimPreviewReset uri={uri} />}
|
||||
|
||||
|
|
|
@ -3297,4 +3297,9 @@ export const icons = {
|
|||
</g>
|
||||
</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>
|
||||
),
|
||||
};
|
||||
|
|
|
@ -70,6 +70,7 @@ const LibraryPage = lazyImport(() => import('page/library' /* webpackChunkName:
|
|||
const ListBlockedPage = lazyImport(() => import('page/listBlocked' /* webpackChunkName: "listBlocked" */));
|
||||
const ListsPage = lazyImport(() => import('page/lists' /* 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 LivestreamCurrentPage = lazyImport(() =>
|
||||
import('page/livestreamCurrent' /* webpackChunkName: "livestreamCurrent" */)
|
||||
|
@ -362,6 +363,7 @@ function AppRouter(props: Props) {
|
|||
<PrivateRoute {...props} path={`/$/${PAGES.LIBRARY}`} component={LibraryPage} />
|
||||
<PrivateRoute {...props} path={`/$/${PAGES.LISTS}`} component={ListsPage} />
|
||||
<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.SETTINGS_BLOCKED_MUTED}`} component={ListBlockedPage} />
|
||||
<PrivateRoute {...props} path={`/$/${PAGES.SETTINGS_CREATOR}`} component={SettingsCreatorPage} />
|
||||
|
|
|
@ -87,6 +87,13 @@ const PLAYLISTS: SideNavLink = {
|
|||
hideForUnauth: true,
|
||||
};
|
||||
|
||||
const WATCH_HISTORY: SideNavLink = {
|
||||
title: 'Watch History',
|
||||
link: `/$/${PAGES.WATCH_HISTORY}`,
|
||||
icon: ICONS.WATCH_HISTORY,
|
||||
hideForUnauth: true,
|
||||
};
|
||||
|
||||
const PREMIUM: SideNavLink = {
|
||||
title: 'Premium',
|
||||
link: `/$/${PAGES.ODYSEE_MEMBERSHIP}`,
|
||||
|
@ -529,6 +536,7 @@ function SideNavigation(props: Props) {
|
|||
{!showMicroMenu && getLink(WATCH_LATER)}
|
||||
{!showMicroMenu && getLink(FAVORITES)}
|
||||
{getLink(PLAYLISTS)}
|
||||
{!showMicroMenu && getLink(WATCH_HISTORY)}
|
||||
</ul>
|
||||
|
||||
<ul
|
||||
|
|
|
@ -16,7 +16,13 @@ import {
|
|||
doAnalyticsView,
|
||||
} from 'redux/actions/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 { selectRecommendedContentForUri } from 'redux/selectors/search';
|
||||
import VideoViewer from './view';
|
||||
|
@ -104,6 +110,7 @@ const perform = (dispatch) => ({
|
|||
doAnalyticsView: (uri, timeToStart) => dispatch(doAnalyticsView(uri, timeToStart)),
|
||||
claimRewards: () => dispatch(doClaimEligiblePurchaseRewards()),
|
||||
doToast: (props) => dispatch(doToast(props)),
|
||||
doSetContentHistoryItem: (uri) => dispatch(doSetContentHistoryItem(uri)),
|
||||
});
|
||||
|
||||
export default withRouter(connect(select, perform)(VideoViewer));
|
||||
|
|
|
@ -72,6 +72,8 @@ type Props = {
|
|||
activeLivestreamForChannel: any,
|
||||
defaultQuality: ?string,
|
||||
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,
|
||||
defaultQuality,
|
||||
doToast,
|
||||
doSetContentHistoryItem,
|
||||
} = props;
|
||||
|
||||
const permanentUrl = claim && claim.permanent_url;
|
||||
|
@ -151,6 +154,12 @@ function VideoViewer(props: Props) {
|
|||
const isFirstRender = React.useRef(true);
|
||||
const playerRef = React.useRef(null);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (isPlaying) {
|
||||
doSetContentHistoryItem(claim.permanent_url);
|
||||
}
|
||||
}, [isPlaying]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isFirstRender.current) {
|
||||
isFirstRender.current = false;
|
||||
|
|
|
@ -25,7 +25,7 @@ export const FEEDBACK = 'MessageSquare';
|
|||
export const SEARCH = 'Search';
|
||||
export const CHANNEL = 'AtSign';
|
||||
export const REFRESH = 'RefreshCw';
|
||||
export const HISTORY = 'Clock';
|
||||
export const WATCH_HISTORY = 'WatchHistory';
|
||||
export const HOME = 'Home';
|
||||
export const OVERVIEW = 'Activity';
|
||||
export const WALLET = 'List';
|
||||
|
|
|
@ -34,6 +34,7 @@ exports.HELP = 'help';
|
|||
exports.LIBRARY = 'library';
|
||||
exports.LISTS = 'lists';
|
||||
exports.PLAYLISTS = 'playlists';
|
||||
exports.WATCH_HISTORY = 'watchhistory';
|
||||
exports.INVITE = 'invite';
|
||||
exports.DEPRECATED__DOWNLOADED = 'downloaded';
|
||||
exports.DEPRECATED__PUBLISH = 'publish';
|
||||
|
|
17
ui/page/watchHistory/index.js
Normal file
17
ui/page/watchHistory/index.js
Normal 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));
|
68
ui/page/watchHistory/view.jsx
Normal file
68
ui/page/watchHistory/view.jsx
Normal 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>
|
||||
);
|
||||
}
|
|
@ -41,7 +41,7 @@ $nag-error-z-index: 999;
|
|||
background-color: var(--color-primary);
|
||||
color: var(--color-white);
|
||||
z-index: $nag-helpful-z-index;
|
||||
border-radius: var(--border-radius);
|
||||
// border-radius: var(--border-radius);
|
||||
margin-top: var(--spacing-s);
|
||||
}
|
||||
|
||||
|
@ -88,7 +88,7 @@ $nag-error-z-index: 999;
|
|||
|
||||
.nag__button--helpful {
|
||||
&:hover {
|
||||
color: var(--color-secondary);
|
||||
color: var(--color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
.membership_title {
|
||||
.comment__badge {
|
||||
|
|
Loading…
Reference in a new issue