Add channel selector on HeaderMenuButton

- Moved from reach/ui to material/ui menu components, because reach ui wouldn't work with 2 menus
- This channel selector stores the default on settings
- setActiveChannelIfNotSet was deprecated, if the account has channels, it will always return a channel even if there is no active channel or stored channel
This commit is contained in:
Rafael 2022-05-04 09:34:31 -03:00 committed by Thomas Zarebczan
parent fa1f3abbdc
commit 7eb5eb9996
11 changed files with 190 additions and 108 deletions

View file

@ -23,7 +23,7 @@ import {
import { selectUploadCount } from 'redux/selectors/publish';
import { doSetLanguage } from 'redux/actions/settings';
import { doSyncLoop } from 'redux/actions/sync';
import { doDownloadUpgradeRequested, doSignIn, doSetActiveChannel, doSetIncognito } from 'redux/actions/app';
import { doDownloadUpgradeRequested, doSignIn, doSetIncognito } from 'redux/actions/app';
import { doFetchModBlockedList, doFetchCommentModAmIList } from 'redux/actions/comments';
import App from './view';
@ -57,7 +57,6 @@ const perform = (dispatch) => ({
requestDownloadUpgrade: () => dispatch(doDownloadUpgradeRequested()),
syncLoop: (noInterval) => dispatch(doSyncLoop(noInterval)),
setReferrer: (referrer, doClaim) => dispatch(doUserSetReferrer(referrer, doClaim)),
setActiveChannelIfNotSet: () => dispatch(doSetActiveChannel()),
setIncognito: () => dispatch(doSetIncognito()),
fetchModBlockedList: () => dispatch(doFetchModBlockedList()),
fetchModAmIList: () => dispatch(doFetchCommentModAmIList()),

View file

@ -86,7 +86,6 @@ type Props = {
activeChannelClaim: ?ChannelClaim,
myChannelClaimIds: ?Array<string>,
hasPremiumPlus: ?boolean,
setActiveChannelIfNotSet: () => void,
setIncognito: (boolean) => void,
fetchModBlockedList: () => void,
fetchModAmIList: () => void,
@ -120,7 +119,6 @@ function App(props: Props) {
syncFatalError,
myChannelClaimIds,
activeChannelClaim,
setActiveChannelIfNotSet,
setIncognito,
fetchModBlockedList,
hasPremiumPlus,
@ -312,9 +310,7 @@ function App(props: Props) {
}, [currentModal]);
useEffect(() => {
if (hasMyChannels && !hasActiveChannelClaim) {
setActiveChannelIfNotSet();
} else if (hasNoChannels) {
if (hasNoChannels) {
setIncognito(true);
}
@ -323,7 +319,7 @@ function App(props: Props) {
fetchModAmIList();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [hasMyChannels, hasNoChannels, hasActiveChannelClaim, setActiveChannelIfNotSet, setIncognito]);
}, [hasMyChannels, hasNoChannels, hasActiveChannelClaim, setIncognito]);
useEffect(() => {
// $FlowFixMe

View file

@ -26,49 +26,10 @@ type Props = {
odyseeMembershipByUri: (uri: string) => string,
storeSelection?: boolean,
doSetClientSetting: (key: string, value: string, pushPrefs: boolean) => void,
isHeaderMenu: boolean,
};
type ListItemProps = {
uri: string,
isSelected?: boolean,
claimsByUri: { [string]: any },
doFetchUserMemberships: (claimIdCsv: string) => void,
odyseeMembershipByUri: (uri: string) => string,
};
function ChannelListItem(props: ListItemProps) {
const { uri, isSelected = false, claimsByUri, doFetchUserMemberships, odyseeMembershipByUri } = props;
const membership = odyseeMembershipByUri(uri);
const shouldFetchUserMemberships = true;
useGetUserMemberships(shouldFetchUserMemberships, [uri], claimsByUri, doFetchUserMemberships, [uri]);
return (
<div className={classnames('channel__list-item', { 'channel__list-item--selected': isSelected })}>
<ChannelThumbnail uri={uri} hideStakedIndicator xsmall noLazyLoad />
<ChannelTitle uri={uri} />
<PremiumBadge membership={membership} />
{isSelected && <Icon icon={ICONS.DOWN} />}
</div>
);
}
type IncognitoSelectorProps = {
isSelected?: boolean,
};
function IncognitoSelector(props: IncognitoSelectorProps) {
return (
<div className={classnames('channel__list-item', { 'channel__list-item--selected': props.isSelected })}>
<Icon sectionIcon icon={ICONS.ANONYMOUS} />
<h2 className="channel__list-text">{__('Anonymous')}</h2>
{props.isSelected && <Icon icon={ICONS.DOWN} />}
</div>
);
}
function ChannelSelector(props: Props) {
export default function ChannelSelector(props: Props) {
const {
channels,
activeChannelClaim,
@ -81,6 +42,7 @@ function ChannelSelector(props: Props) {
doFetchUserMemberships,
storeSelection,
doSetClientSetting,
isHeaderMenu,
} = props;
const {
@ -102,8 +64,14 @@ function ChannelSelector(props: Props) {
return (
<div className="channel__selector">
<Menu>
<MenuButton>
{(incognito && !hideAnon) || !activeChannelUrl ? (
<MenuButton className={isHeaderMenu ? 'menu__link' : undefined}>
{isHeaderMenu ? (
<>
<ChannelThumbnail uri={activeChannelUrl} hideStakedIndicator xxsmall noLazyLoad />
{__('Change Default Channel')}
<Icon icon={ICONS.DOWN} />
</>
) : (incognito && !hideAnon) || !activeChannelUrl ? (
<IncognitoSelector isSelected />
) : (
<ChannelListItem
@ -145,4 +113,42 @@ function ChannelSelector(props: Props) {
);
}
export default ChannelSelector;
type ListItemProps = {
uri: string,
isSelected?: boolean,
claimsByUri: { [string]: any },
doFetchUserMemberships: (claimIdCsv: string) => void,
odyseeMembershipByUri: (uri: string) => string,
};
function ChannelListItem(props: ListItemProps) {
const { uri, isSelected = false, claimsByUri, doFetchUserMemberships, odyseeMembershipByUri } = props;
const membership = odyseeMembershipByUri(uri);
const shouldFetchUserMemberships = true;
useGetUserMemberships(shouldFetchUserMemberships, [uri], claimsByUri, doFetchUserMemberships, [uri]);
return (
<div className={classnames('channel__list-item', { 'channel__list-item--selected': isSelected })}>
<ChannelThumbnail uri={uri} hideStakedIndicator xsmall noLazyLoad />
<ChannelTitle uri={uri} />
<PremiumBadge membership={membership} />
{isSelected && <Icon icon={ICONS.DOWN} />}
</div>
);
}
type IncognitoSelectorProps = {
isSelected?: boolean,
};
function IncognitoSelector(props: IncognitoSelectorProps) {
return (
<div className={classnames('channel__list-item', { 'channel__list-item--selected': props.isSelected })}>
<Icon sectionIcon icon={ICONS.ANONYMOUS} />
<h2 className="channel__list-text">{__('Anonymous')}</h2>
{props.isSelected && <Icon icon={ICONS.DOWN} />}
</div>
);
}

View file

@ -18,6 +18,7 @@ type Props = {
obscure?: boolean,
small?: boolean,
xsmall?: boolean,
xxsmall?: boolean,
allowGifs?: boolean,
claim: ?ChannelClaim,
doResolveUri: (string) => void,
@ -44,6 +45,7 @@ function ChannelThumbnail(props: Props) {
obscure,
small = false,
xsmall = false,
xxsmall,
allowGifs = false,
claim,
doResolveUri,
@ -110,6 +112,7 @@ function ChannelThumbnail(props: Props) {
[colorClassName]: !showThumb,
'channel-thumbnail--small': small,
'channel-thumbnail--xsmall': xsmall,
'channel-thumbnail--xxsmall': xxsmall,
'channel-thumbnail--resolving': isResolving,
})}
>
@ -117,8 +120,8 @@ function ChannelThumbnail(props: Props) {
alt={__('Channel profile picture')}
className={!channelThumbnail ? 'channel-thumbnail__default' : 'channel-thumbnail__custom'}
src={(!thumbLoadError && channelThumbnail) || defaultAvatar}
width={small || xsmall ? 64 : 160}
quality={small || xsmall ? 85 : 95}
width={xxsmall ? 16 : small || xsmall ? 64 : 160}
quality={xxsmall ? 16 : small || xsmall ? 85 : 95}
loading={noLazyLoad ? undefined : 'lazy'}
onError={() => {
if (setThumbUploadError) {

View file

@ -3,6 +3,8 @@ import * as PAGES from 'constants/pages';
import React from 'react';
import { Link, useHistory } from 'react-router-dom';
import { MenuLink, MenuItem } from '@reach/menu-button';
import MuiMenuItem from '@mui/material/MenuItem';
import MuiLink from '@mui/material/Link';
import Icon from 'component/common/icon';
type Props = {
@ -10,13 +12,23 @@ type Props = {
name: string,
page: string,
requiresAuth?: boolean,
useMui?: boolean,
};
export default function HeaderMenuLink(props: Props) {
const { icon, name, page, requiresAuth } = props;
const { icon, name, page, requiresAuth, useMui } = props;
const { push } = useHistory();
if (useMui) {
return (
<MuiMenuItem className="menu__link" component={MuiLink} href={`/$/${page}`}>
<Icon aria-hidden icon={icon} />
{name}
</MuiMenuItem>
);
}
if (requiresAuth) {
return (
<MenuItem className="menu__link" onSelect={() => push(`/$/${PAGES.AUTH}`)}>

View file

@ -1,7 +1,7 @@
// @flow
import 'scss/component/_header.scss';
import { Menu, MenuList, MenuButton, MenuItem } from '@reach/menu-button';
import { Menu as MuiMenu, MenuItem as MuiMenuItem } from '@mui/material';
import * as ICONS from 'constants/icons';
import * as PAGES from 'constants/pages';
import ChannelThumbnail from 'component/channelThumbnail';
@ -10,6 +10,8 @@ import HeaderMenuLink from 'component/common/header-menu-link';
import Icon from 'component/common/icon';
import React from 'react';
import Skeleton from '@mui/material/Skeleton';
import ChannelSelector from 'component/channelSelector';
import Button from 'component/button';
type HeaderMenuButtonProps = {
myChannelClaimIds: ?Array<string>,
@ -22,6 +24,15 @@ type HeaderMenuButtonProps = {
export default function HeaderProfileMenuButton(props: HeaderMenuButtonProps) {
const { myChannelClaimIds, activeChannelClaim, authenticated, email, signOut } = props;
const [anchorEl, setAnchorEl] = React.useState(null);
const open = Boolean(anchorEl);
const handleClick = (event) => {
setAnchorEl(!anchorEl ? event.currentTarget : null);
};
const handleClose = () => {
setAnchorEl(null);
};
const activeChannelUrl = activeChannelClaim && activeChannelClaim.permanent_url;
// activeChannel will be: undefined = fetching, null = nothing, or { channel claim }
const noActiveChannel = activeChannelUrl === null;
@ -29,12 +40,15 @@ export default function HeaderProfileMenuButton(props: HeaderMenuButtonProps) {
return (
<div className="header__buttons">
<Menu>
{pendingChannelFetch ? (
<Skeleton variant="circular" animation="wave" className="header__navigationItem--iconSkeleton" />
) : (
<MenuButton
aria-label={__('Your account')}
<Button
id="basic-button"
aria-controls={open ? 'basic-menu' : undefined}
aria-haspopup="true"
aria-expanded={open ? 'true' : undefined}
onClick={handleClick}
className={classnames('header__navigationItem', {
'header__navigationItem--icon': !activeChannelUrl,
'header__navigationItem--profilePic': activeChannelUrl,
@ -45,37 +59,55 @@ export default function HeaderProfileMenuButton(props: HeaderMenuButtonProps) {
) : (
<Icon size={18} icon={ICONS.ACCOUNT} aria-hidden />
)}
</MenuButton>
</Button>
)}
<MenuList className="menu__list--header">
<MuiMenu
id="basic-menu"
anchorEl={anchorEl}
open={open}
onClose={handleClose}
MenuListProps={{
'aria-labelledby': 'basic-button',
sx: { padding: 'var(--spacing-xs)' },
}}
className="menu__list--header"
sx={{ 'z-index': 2 }}
PaperProps={{ className: 'MuiMenu-list--paper' }}
>
{authenticated ? (
<>
<HeaderMenuLink page={PAGES.UPLOADS} icon={ICONS.PUBLISH} name={__('Uploads')} />
<HeaderMenuLink page={PAGES.CHANNELS} icon={ICONS.CHANNEL} name={__('Channels')} />
<HeaderMenuLink page={PAGES.CREATOR_DASHBOARD} icon={ICONS.ANALYTICS} name={__('Creator Analytics')} />
<HeaderMenuLink page={PAGES.REWARDS} icon={ICONS.REWARDS} name={__('Rewards')} />
<HeaderMenuLink page={PAGES.INVITE} icon={ICONS.INVITE} name={__('Invites')} />
<HeaderMenuLink page={PAGES.ODYSEE_MEMBERSHIP} icon={ICONS.UPGRADE} name={__('Odysee Premium')} />
<HeaderMenuLink useMui page={PAGES.UPLOADS} icon={ICONS.PUBLISH} name={__('Uploads')} />
<HeaderMenuLink useMui page={PAGES.CHANNELS} icon={ICONS.CHANNEL} name={__('Channels')} />
<HeaderMenuLink
useMui
page={PAGES.CREATOR_DASHBOARD}
icon={ICONS.ANALYTICS}
name={__('Creator Analytics')}
/>
<HeaderMenuLink useMui page={PAGES.REWARDS} icon={ICONS.REWARDS} name={__('Rewards')} />
<HeaderMenuLink useMui page={PAGES.INVITE} icon={ICONS.INVITE} name={__('Invites')} />
<HeaderMenuLink useMui page={PAGES.ODYSEE_MEMBERSHIP} icon={ICONS.UPGRADE} name={__('Odysee Premium')} />
<ChannelSelector storeSelection isHeaderMenu />
<MenuItem onSelect={signOut}>
<div className="menu__link">
<MuiMenuItem onClick={signOut} sx={{ padding: '0px' }}>
<div className="menu__link" style={{ 'flex-direction': 'column', 'align-items': 'flex-start' }}>
<div>
<Icon aria-hidden icon={ICONS.SIGN_OUT} />
{__('Sign Out')}
</div>
<span className="menu__link-help">{email}</span>
</MenuItem>
</div>
</MuiMenuItem>
</>
) : (
<>
<HeaderMenuLink page={PAGES.AUTH_SIGNIN} icon={ICONS.SIGN_IN} name={__('Log In')} />
<HeaderMenuLink page={PAGES.AUTH} icon={ICONS.SIGN_UP} name={__('Sign Up')} />
<HeaderMenuLink page={PAGES.SETTINGS} icon={ICONS.SETTINGS} name={__('Settings')} />
<HeaderMenuLink page={PAGES.HELP} icon={ICONS.HELP} name={__('Help')} />
<HeaderMenuLink useMui page={PAGES.AUTH_SIGNIN} icon={ICONS.SIGN_IN} name={__('Log In')} />
<HeaderMenuLink useMui page={PAGES.AUTH} icon={ICONS.SIGN_UP} name={__('Sign Up')} />
<HeaderMenuLink useMui page={PAGES.SETTINGS} icon={ICONS.SETTINGS} name={__('Settings')} />
<HeaderMenuLink useMui page={PAGES.HELP} icon={ICONS.HELP} name={__('Help')} />
</>
)}
</MenuList>
</Menu>
</MuiMenu>
</div>
);
}

View file

@ -8,7 +8,6 @@ import {
selectCurrentUploads,
} from 'redux/selectors/publish';
import { selectActiveChannelClaim, selectIncognito } from 'redux/selectors/app';
import { doSetActiveChannel } from 'redux/actions/app';
import PublishPage from './view';
const select = (state) => ({
@ -26,7 +25,6 @@ const select = (state) => ({
const perform = (dispatch) => ({
updatePublishForm: (value) => dispatch(doUpdatePublishForm(value)),
prepareEdit: (claim, uri) => dispatch(doPrepareEdit(claim, uri)),
setActiveChannel: (claimId) => dispatch(doSetActiveChannel(claimId)),
});
export default connect(select, perform)(PublishPage);

View file

@ -1,7 +1,6 @@
import { connect } from 'react-redux';
import { selectHasChannels, selectFetchingMyChannels } from 'redux/selectors/claims';
import { selectActiveChannelClaim } from 'redux/selectors/app';
import { doSetActiveChannel } from 'redux/actions/app';
import CreatorDashboardPage from './view';
const select = (state) => ({
@ -10,4 +9,4 @@ const select = (state) => ({
activeChannelClaim: selectActiveChannelClaim(state),
});
export default connect(select, { doSetActiveChannel })(CreatorDashboardPage);
export default connect(select)(CreatorDashboardPage);

View file

@ -76,7 +76,7 @@ export const selectActiveChannelClaim = createSelector(
// Null: has none. Undefined: not resolved, default state, could have or not
if (!userEmail || myChannelClaims === null) {
return null;
} else if (!activeChannelClaim || !myChannelClaims || !myChannelClaims.length) {
} else if (!myChannelClaims || !myChannelClaims.length) {
return undefined;
}

View file

@ -267,6 +267,12 @@ $actions-z-index: 2;
margin-right: var(--spacing-xs);
}
.channel-thumbnail--xxsmall {
height: 16px;
width: 16px;
margin-right: var(--spacing-s) !important;
}
.channel-thumbnail--waiting {
background-color: var(--color-gray-5);
border-radius: var(--border-radius);

View file

@ -31,16 +31,17 @@ reach-portal {
max-width: calc(100% - var(--height-button) - var(--spacing-xs));
}
[data-reach-menu-list] {
[data-reach-menu-list],
.MuiMenu-list--paper {
display: block;
white-space: nowrap;
outline: none;
// background-color: var(--color-header-background) !important;
background-color: rgba(var(--color-header-button-base), 0.9) !important;
background-color: rgba(var(--color-header-button-base), 0.5) !important;
border: 2px solid var(--color-header-background) !important;
border-top: none;
-webkit-backdrop-filter: blur(4px);
backdrop-filter: blur(4px);
-webkit-backdrop-filter: blur(4px) !important;
backdrop-filter: blur(4px) !important;
&:focus-visible {
background-color: var(--color-header-background) !important;
@ -85,6 +86,13 @@ reach-portal {
}
}
.menu__link:hover {
color: var(--color-odysee-contrast) !important;
background-color: var(--color-odysee) !important;
border-radius: var(--border-radius);
box-shadow: none;
}
.menu__button {
display: flex;
justify-content: center;
@ -125,6 +133,22 @@ reach-portal {
.menu__list--header {
@extend .menu__list;
.channel__selector {
margin: 0px !important;
display: flex;
.menu__link {
margin: 0px !important;
padding: var(--spacing-xs) var(--spacing-s) !important;
width: 100%;
color: inherit;
}
.icon--ChevronDown {
margin-left: var(--spacing-s);
}
}
[data-reach-menu-item][data-selected] {
.menu__link {
color: var(--color-odysee-contrast) !important;
@ -155,6 +179,12 @@ reach-portal {
@extend .menu__list;
}
.MuiMenuItem-root {
margin-left: 0px !important;
font-size: var(--font-small) !important;
display: block !important;
}
.menu__link {
display: flex;
align-items: center;
@ -162,6 +192,7 @@ reach-portal {
// padding-right: var(--spacing-l);
padding: var(--spacing-xs) var(--spacing-s) var(--spacing-xs) var(--spacing-s);
height: var(--button-height);
color: var(--color-primary-contrast) !important;
.icon {
stroke: var(--color-menu-icon);
@ -186,7 +217,7 @@ reach-portal {
display: block;
color: var(--color-text);
font-size: var(--font-small);
padding-top: 0;
padding: 0px;
white-space: normal;
text-overflow: ellipsis;
overflow-x: hidden;