Sidebar: show "last active subs" instead of "by name"

1003
This commit is contained in:
infinite-persistence 2022-03-04 15:15:23 +08:00 committed by Thomas Zarebczan
parent 4a5e967628
commit b5bd36839e
11 changed files with 186 additions and 45 deletions

View file

@ -13,6 +13,7 @@ declare type Following = {
declare type SubscriptionState = {
subscriptions: Array<Subscription>,
lastActiveSubscriptions: ?Array<Subscription>,
following: Array<Following>,
loading: boolean,
firstRunCompleted: boolean,

View file

@ -2217,5 +2217,7 @@
"Only Language": "Only Language",
"Only Homepage": "Only Homepage",
"Choose Your Preference": "Choose Your Preference",
"Recently Active": "Recently Active",
"All Channels": "All Channels",
"--end--": "--end--"
}

View file

@ -1,6 +1,7 @@
import { connect } from 'react-redux';
import { doFetchLastActiveSubs } from 'redux/actions/subscriptions';
import { selectActiveChannelStakedLevel } from 'redux/selectors/app';
import { selectSubscriptions } from 'redux/selectors/subscriptions';
import { selectLastActiveSubscriptions, selectSubscriptions } from 'redux/selectors/subscriptions';
import { doClearClaimSearch } from 'redux/actions/claims';
import { doClearPurchasedUriSuccess } from 'redux/actions/file';
import { selectFollowedTags } from 'redux/selectors/tags';
@ -14,6 +15,7 @@ import SideNavigation from './view';
const select = (state) => ({
subscriptions: selectSubscriptions(state),
lastActiveSubs: selectLastActiveSubscriptions(state),
followedTags: selectFollowedTags(state),
email: selectUserVerifiedEmail(state),
purchaseSuccess: selectPurchaseUriSuccess(state),
@ -28,4 +30,5 @@ export default connect(select, {
doClearClaimSearch,
doSignOut,
doClearPurchasedUriSuccess,
doFetchLastActiveSubs,
})(SideNavigation);

View file

@ -104,6 +104,7 @@ const WILD_WEST = {
type Props = {
subscriptions: Array<Subscription>,
lastActiveSubs: ?Array<Subscription>,
followedTags: Array<Tag>,
email: ?string,
uploadCount: number,
@ -120,11 +121,13 @@ type Props = {
activeChannelStakedLevel: number,
wildWestDisabled: boolean,
doClearClaimSearch: () => void,
doFetchLastActiveSubs: (count?: number) => void,
};
function SideNavigation(props: Props) {
const {
subscriptions,
lastActiveSubs,
doSignOut,
email,
purchaseSuccess,
@ -140,6 +143,7 @@ function SideNavigation(props: Props) {
activeChannelStakedLevel,
wildWestDisabled,
doClearClaimSearch,
doFetchLastActiveSubs,
} = props;
const isLargeScreen = useIsLargeScreen();
@ -309,37 +313,35 @@ function SideNavigation(props: Props) {
const filter = subscriptionFilter.toLowerCase();
displayedSubscriptions = subscriptions.filter((sub) => sub.channelName.toLowerCase().includes(filter));
} else {
displayedSubscriptions = subscriptions.slice(0, SIDEBAR_SUBS_DISPLAYED);
displayedSubscriptions = lastActiveSubs || subscriptions.slice(0, SIDEBAR_SUBS_DISPLAYED);
}
return (
<>
<ul className="navigation__secondary navigation-links">
{subscriptions.length > SIDEBAR_SUBS_DISPLAYED && (
<li className="navigation-item">
<DebouncedInput icon={ICONS.SEARCH} placeholder={__('Filter')} onChange={setSubscriptionFilter} />
</li>
)}
{displayedSubscriptions.map((subscription) => (
<SubscriptionListItem key={subscription.uri} subscription={subscription} />
))}
{!!subscriptionFilter && !displayedSubscriptions.length && (
<li>
<div className="navigation-item">
<div className="empty empty--centered">{__('No results')}</div>
</div>
</li>
)}
{!subscriptionFilter && (
<Button
key="showMore"
label={__('Manage')}
className="navigation-link"
navigate={`/$/${PAGES.CHANNELS_FOLLOWING_MANAGE}`}
/>
)}
</ul>
</>
<ul className="navigation__secondary navigation-links">
{subscriptions.length > SIDEBAR_SUBS_DISPLAYED && (
<li className="navigation-item">
<DebouncedInput icon={ICONS.SEARCH} placeholder={__('Filter')} onChange={setSubscriptionFilter} />
</li>
)}
{displayedSubscriptions.map((subscription) => (
<SubscriptionListItem key={subscription.uri} subscription={subscription} />
))}
{!!subscriptionFilter && !displayedSubscriptions.length && (
<li>
<div className="navigation-item">
<div className="empty empty--centered">{__('No results')}</div>
</div>
</li>
)}
{!subscriptionFilter && (
<Button
key="showMore"
label={__('Manage')}
className="navigation-link"
navigate={`/$/${PAGES.CHANNELS_FOLLOWING_MANAGE}`}
/>
)}
</ul>
);
}
return null;
@ -418,6 +420,10 @@ function SideNavigation(props: Props) {
}
}, [sidebarOpen]);
React.useEffect(() => {
doFetchLastActiveSubs();
}, []);
const unAuthNudge =
DOMAIN === 'lbry.tv' ? null : (
<div className="navigation__auth-nudge">

View file

@ -324,6 +324,9 @@ export const CHECK_SUBSCRIPTIONS_SUBSCRIBE = 'CHECK_SUBSCRIPTIONS_SUBSCRIBE';
export const FETCH_SUBSCRIPTIONS_START = 'FETCH_SUBSCRIPTIONS_START';
export const FETCH_SUBSCRIPTIONS_FAIL = 'FETCH_SUBSCRIPTIONS_FAIL';
export const FETCH_SUBSCRIPTIONS_SUCCESS = 'FETCH_SUBSCRIPTIONS_SUCCESS';
export const FETCH_LAST_ACTIVE_SUBS_SKIP = 'FETCH_LAST_ACTIVE_SUBS_SKIP';
export const FETCH_LAST_ACTIVE_SUBS_DONE = 'FETCH_LAST_ACTIVE_SUBS_DONE';
export const FETCH_LAST_ACTIVE_SUBS_FAIL = 'FETCH_LAST_ACTIVE_SUBS_FAIL';
export const SET_VIEW_MODE = 'SET_VIEW_MODE';
// Publishing

View file

@ -1,10 +1,11 @@
import { connect } from 'react-redux';
import { doResolveUris } from 'redux/actions/claims';
import { selectSubscriptionUris } from 'redux/selectors/subscriptions';
import { selectLastActiveSubscriptions, selectSubscriptionUris } from 'redux/selectors/subscriptions';
import ChannelsFollowingManage from './view';
const select = (state) => ({
subscribedChannelUris: selectSubscriptionUris(state),
lastActiveSubs: selectLastActiveSubscriptions(state),
});
const perform = {

View file

@ -1,11 +1,15 @@
// @flow
import React from 'react';
import Button from 'component/button';
import ChannelThumbnail from 'component/channelThumbnail';
import ClaimList from 'component/claimList';
import ClaimPreviewTitle from 'component/claimPreviewTitle';
import DebouncedInput from 'component/common/debounced-input';
import Empty from 'component/common/empty';
import Page from 'component/page';
import Spinner from 'component/spinner';
import * as ICONS from 'constants/icons';
import { SIDEBAR_SUBS_DISPLAYED } from 'constants/subscriptions';
function getFilteredUris(uris, filterQuery) {
if (filterQuery) {
@ -23,11 +27,12 @@ const FOLLOW_PAGE_SIZE = 30;
type Props = {
subscribedChannelUris: Array<string>,
lastActiveSubs: ?Array<Subscription>,
doResolveUris: (uris: Array<string>, returnCachedClaims: boolean, resolveReposts: boolean) => void,
};
export default function ChannelsFollowingManage(props: Props) {
const { subscribedChannelUris, doResolveUris } = props;
const { subscribedChannelUris, lastActiveSubs, doResolveUris } = props;
// The locked-on-mount full set of subscribed uris.
const [uris, setUris] = React.useState([]);
@ -95,15 +100,49 @@ export default function ChannelsFollowingManage(props: Props) {
{filteredUris && <ClaimList uris={filteredUris} />}
{!filteredUris && lastActiveSubs && lastActiveSubs.length === SIDEBAR_SUBS_DISPLAYED && (
<>
<div className="card__title-section">
<div className="card__subtitle"> {__('Recently Active')}</div>
</div>
<div className="followManage-wrapper__activeSubs">
{lastActiveSubs.map((sub) => {
return (
<div key={sub.uri} className="navigation-link__wrapper navigation__subscription">
<Button
navigate={sub.uri}
className="navigation-link navigation-link--with-thumbnail"
activeClass="navigation-link--active"
>
<ChannelThumbnail xsmall uri={sub.uri} hideStakedIndicator />
<div className="navigation__subscription-title">
<ClaimPreviewTitle uri={sub.uri} />
<span dir="auto" className="channel-name">
{sub.channelName}
</span>
</div>
</Button>
</div>
);
})}
</div>
</>
)}
{!filteredUris && uris.length > 0 && (
<ClaimList
uris={uris.slice(0, (page + 1) * FOLLOW_PAGE_SIZE)}
onScrollBottom={bumpPage}
page={page + 1}
pageSize={FOLLOW_PAGE_SIZE}
loading={loadingPage}
useLoadingSpinner
/>
<>
<div className="card__title-section">
<div className="card__subtitle"> {__('All Channels')}</div>
</div>
<ClaimList
uris={uris.slice(0, (page + 1) * FOLLOW_PAGE_SIZE)}
onScrollBottom={bumpPage}
page={page + 1}
pageSize={FOLLOW_PAGE_SIZE}
loading={loadingPage}
useLoadingSpinner
/>
</>
)}
</>
)}

View file

@ -1,11 +1,15 @@
// @flow
import Lbry from 'lbry';
import * as ACTIONS from 'constants/action_types';
import { SIDEBAR_SUBS_DISPLAYED } from 'constants/subscriptions';
import REWARDS from 'rewards';
import { Lbryio } from 'lbryinc';
import { doClaimRewardType } from 'redux/actions/rewards';
import { getChannelFromClaim } from 'util/claim';
import { parseURI } from 'util/lbryURI';
import { doAlertWaitingForSync } from 'redux/actions/app';
import { doToast } from 'redux/actions/notifications';
import { selectSubscriptions } from 'redux/selectors/subscriptions';
type SubscriptionArgs = {
channelName: string,
@ -13,6 +17,9 @@ type SubscriptionArgs = {
notificationsDisabled?: boolean,
};
const FETCH_LAST_ACTIVE_SUBS_MIN_INTERVAL_MS = 5 * 60 * 1000;
let activeSubsLastFetchedTime = 0;
export function doToggleSubscription(
subscription: SubscriptionArgs,
followToast: boolean,
@ -71,6 +78,9 @@ export function doToggleSubscription(
})
);
}
// Reset last-fetch counter
activeSubsLastFetchedTime = 0;
};
}
@ -85,3 +95,48 @@ export function doChannelUnsubscribe(subscription: SubscriptionArgs, followToast
return dispatch(doToggleSubscription(subscription, followToast, true));
};
}
export function doFetchLastActiveSubs(forceFetch: boolean = false, count: number = SIDEBAR_SUBS_DISPLAYED) {
function parseIdFromUri(uri) {
try {
const { channelClaimId } = parseURI(uri);
return channelClaimId;
} catch {
return '';
}
}
return (dispatch: Dispatch, getState: GetState) => {
const now = Date.now();
if (!forceFetch && now - activeSubsLastFetchedTime < FETCH_LAST_ACTIVE_SUBS_MIN_INTERVAL_MS) {
dispatch({ type: ACTIONS.FETCH_LAST_ACTIVE_SUBS_SKIP });
return;
}
const state = getState();
const subscriptions = selectSubscriptions(state);
const channelIds = subscriptions.map((sub) => parseIdFromUri(sub.uri));
activeSubsLastFetchedTime = now;
const searchOptions = {
limit_claims_per_channel: 1,
channel_ids: channelIds,
claim_type: ['stream', 'repost'],
page: 1,
page_size: count,
no_totals: true,
order_by: ['release_time'],
};
Lbry.claim_search(searchOptions)
.then((result) => {
dispatch({
type: ACTIONS.FETCH_LAST_ACTIVE_SUBS_DONE,
data: result.items.map((claim) => getChannelFromClaim(claim)),
});
})
.catch(() => {
dispatch({ type: ACTIONS.FETCH_LAST_ACTIVE_SUBS_FAIL });
});
};
}

View file

@ -5,6 +5,7 @@ import { handleActions } from 'util/redux-utils';
const defaultState: SubscriptionState = {
subscriptions: [], // Deprecated
lastActiveSubscriptions: undefined, // undefined = un-fetched, null = no results;
following: [],
loading: false,
firstRunCompleted: false,
@ -49,13 +50,13 @@ export default handleActions(
[ACTIONS.CHANNEL_UNSUBSCRIBE]: (state: SubscriptionState, action): SubscriptionState => {
const subscriptionToRemove: Subscription = action.data;
const newSubscriptions = state.subscriptions
.slice()
.filter((subscription) => !isURIEqual(subscription.uri, subscriptionToRemove.uri));
const newSubscriptions = state.subscriptions.filter(
(subscription) => !isURIEqual(subscription.uri, subscriptionToRemove.uri)
);
const newFollowing = state.following
.slice()
.filter((subscription) => !isURIEqual(subscription.uri, subscriptionToRemove.uri));
const newFollowing = state.following.filter(
(subscription) => !isURIEqual(subscription.uri, subscriptionToRemove.uri)
);
return {
...state,
@ -76,6 +77,25 @@ export default handleActions(
loading: false,
subscriptions: action.data,
}),
[ACTIONS.FETCH_LAST_ACTIVE_SUBS_DONE]: (state: SubscriptionState, action): SubscriptionState => {
const activeChannelClaims = action.data;
if (activeChannelClaims && activeChannelClaims.length > 0) {
const subs = [];
activeChannelClaims.forEach((claim) => {
const index = state.subscriptions.findIndex((sub) => isURIEqual(sub.uri, claim.permanent_url));
if (index !== -1) {
subs.push(state.subscriptions[index]);
}
});
return { ...state, lastActiveSubscriptions: subs };
}
return { ...state, lastActiveSubscriptions: null };
},
[ACTIONS.FETCH_LAST_ACTIVE_SUBS_FAIL]: (state: SubscriptionState, action): SubscriptionState => ({
...state,
lastActiveSubscriptions: null,
}),
[ACTIONS.SET_VIEW_MODE]: (state: SubscriptionState, action): SubscriptionState => ({
...state,
viewMode: action.data,

View file

@ -25,6 +25,8 @@ export const selectSubscriptionUris = createSelector(
(subscriptions) => subscriptions && subscriptions.map((sub) => sub.uri)
);
export const selectLastActiveSubscriptions = (state) => selectState(state).lastActiveSubscriptions;
export const selectFollowing = createSelector(selectState, (state) => state.following && state.following);
// Fetching list of users subscriptions

View file

@ -664,6 +664,15 @@ img {
}
}
.followManage-wrapper__activeSubs {
-moz-column-count: 2;
-moz-column-gap: var(--spacing-m);
-webkit-column-count: 2;
-webkit-column-gap: var(--spacing-m);
column-count: 2;
column-gap: var(--spacing-m);
}
.uploadPage-wraper {
&.card-stack {
.card:not(:last-of-type) {