diff --git a/flow-typed/Comment.js b/flow-typed/Comment.js index c851d297d..0a497a324 100644 --- a/flow-typed/Comment.js +++ b/flow-typed/Comment.js @@ -45,6 +45,7 @@ declare type CommentsState = { isLoading: boolean, isLoadingById: boolean, isLoadingByParentId: { [string]: boolean }, + isCommenting: boolean, myComments: ?Set, isFetchingReacts: boolean, myReactsByCommentId: ?{ [string]: Array }, // {"CommentId:MyChannelId": ["like", "dislike", ...]} diff --git a/package.json b/package.json index d1b3ee739..505fd3041 100644 --- a/package.json +++ b/package.json @@ -56,6 +56,7 @@ "match-sorter": "^6.3.0", "parse-duration": "^1.0.0", "proxy-polyfill": "0.1.6", + "re-reselect": "^4.0.0", "react-datetime-picker": "^3.2.1", "react-plastic": "^1.1.1", "react-top-loading-bar": "^2.0.1", diff --git a/static/app-strings.json b/static/app-strings.json index b36842566..b3527b9b3 100644 --- a/static/app-strings.json +++ b/static/app-strings.json @@ -2220,5 +2220,7 @@ "filtered": "filtered", "View All Playlists": "View All Playlists", "Your wallet is not currently using a cloud sync service. You are in control of backing up your wallet.": "Your wallet is not currently using a cloud sync service. You are in control of backing up your wallet.", + "Sending": "Sending", + "You sent %lbc%": "You sent %lbc%", "--end--": "--end--" } diff --git a/ui/component/channelMentionSuggestions/index.js b/ui/component/channelMentionSuggestions/index.js index d6b4af95b..c1f635011 100644 --- a/ui/component/channelMentionSuggestions/index.js +++ b/ui/component/channelMentionSuggestions/index.js @@ -4,12 +4,12 @@ import { selectSubscriptions } from 'redux/selectors/subscriptions'; import { withRouter } from 'react-router'; import { makeSelectClaimForUri } from 'redux/selectors/claims'; import { doResolveUris } from 'redux/actions/claims'; -import { makeSelectTopLevelCommentsForUri } from 'redux/selectors/comments'; +import { selectTopLevelCommentsForUri } from 'redux/selectors/comments'; import ChannelMentionSuggestions from './view'; const select = (state, props) => { const subscriptionUris = selectSubscriptions(state).map(({ uri }) => uri); - const topLevelComments = makeSelectTopLevelCommentsForUri(props.uri)(state); + const topLevelComments = selectTopLevelCommentsForUri(state, props.uri); const commentorUris = []; // Avoid repeated commentors diff --git a/ui/component/claimPreview/index.js b/ui/component/claimPreview/index.js index b63696f69..25ab46a8b 100644 --- a/ui/component/claimPreview/index.js +++ b/ui/component/claimPreview/index.js @@ -1,6 +1,6 @@ import { connect } from 'react-redux'; import { - makeSelectClaimForUri, + selectClaimForUri, makeSelectIsUriResolving, makeSelectClaimIsMine, makeSelectClaimIsPending, @@ -8,7 +8,7 @@ import { makeSelectReflectingClaimForUri, makeSelectClaimWasPurchased, makeSelectTitleForUri, - makeSelectDateForUri, + selectDateForUri, } from 'redux/selectors/claims'; import { makeSelectStreamingUrlForUri } from 'redux/selectors/file_info'; import { @@ -30,14 +30,14 @@ import ClaimPreview from './view'; import formatMediaDuration from 'util/formatMediaDuration'; const select = (state, props) => { - const claim = props.uri && makeSelectClaimForUri(props.uri)(state); + const claim = props.uri && selectClaimForUri(state, props.uri); const media = claim && claim.value && (claim.value.video || claim.value.audio); const mediaDuration = media && media.duration && formatMediaDuration(media.duration, { screenReader: true }); return { claim, mediaDuration, - date: props.uri && makeSelectDateForUri(props.uri)(state), + date: props.uri && selectDateForUri(state, props.uri), title: props.uri && makeSelectTitleForUri(props.uri)(state), pending: props.uri && makeSelectClaimIsPending(props.uri)(state), reflectingProgress: props.uri && makeSelectReflectingClaimForUri(props.uri)(state), @@ -45,7 +45,6 @@ const select = (state, props) => { claimIsMine: props.uri && makeSelectClaimIsMine(props.uri)(state), isResolvingUri: props.uri && makeSelectIsUriResolving(props.uri)(state), isResolvingRepost: props.uri && makeSelectIsUriResolving(props.repostUrl)(state), - repostClaim: props.uri && makeSelectClaimForUri(props.uri)(state), nsfw: props.uri && makeSelectClaimIsNsfw(props.uri)(state), blackListedOutpoints: selectBlackListedOutpoints(state), filteredOutpoints: selectFilteredOutpoints(state), diff --git a/ui/component/claimPreviewTile/index.js b/ui/component/claimPreviewTile/index.js index b3be8f36d..eaf64a865 100644 --- a/ui/component/claimPreviewTile/index.js +++ b/ui/component/claimPreviewTile/index.js @@ -6,7 +6,7 @@ import { makeSelectTitleForUri, makeSelectChannelForClaimUri, makeSelectClaimIsNsfw, - makeSelectDateForUri, + selectDateForUri, } from 'redux/selectors/claims'; import { doFileGet } from 'redux/actions/file'; import { doResolveUri } from 'redux/actions/claims'; @@ -24,7 +24,7 @@ const select = (state, props) => { return { claim, mediaDuration, - date: props.uri && makeSelectDateForUri(props.uri)(state), + date: props.uri && selectDateForUri(state, props.uri), channel: props.uri && makeSelectChannelForClaimUri(props.uri)(state), isResolvingUri: props.uri && makeSelectIsUriResolving(props.uri)(state), thumbnail: props.uri && makeSelectThumbnailForUri(props.uri)(state), diff --git a/ui/component/claimTags/index.js b/ui/component/claimTags/index.js index a0280f138..385aca981 100644 --- a/ui/component/claimTags/index.js +++ b/ui/component/claimTags/index.js @@ -1,10 +1,10 @@ import { connect } from 'react-redux'; -import { makeSelectTagsForUri } from 'redux/selectors/claims'; +import { selectTagsForUri } from 'redux/selectors/claims'; import { selectFollowedTags } from 'redux/selectors/tags'; import ClaimTags from './view'; const select = (state, props) => ({ - tags: makeSelectTagsForUri(props.uri)(state), + tags: selectTagsForUri(state, props.uri), followedTags: selectFollowedTags(state), }); diff --git a/ui/component/comment/index.js b/ui/component/comment/index.js index 712b0e34b..ffe06dd37 100644 --- a/ui/component/comment/index.js +++ b/ui/component/comment/index.js @@ -12,7 +12,7 @@ import { doSetPlayingUri } from 'redux/actions/content'; import { selectUserVerifiedEmail } from 'redux/selectors/user'; import { selectLinkedCommentAncestors, - makeSelectOthersReactionsForComment, + selectOthersReactsForComment, makeSelectTotalReplyPagesForParentId, } from 'redux/selectors/comments'; import { selectActiveChannelClaim } from 'redux/selectors/app'; @@ -29,7 +29,7 @@ const select = (state, props) => { thumbnail: props.authorUri && makeSelectThumbnailForUri(props.authorUri)(state), channelIsBlocked: props.authorUri && makeSelectChannelIsMuted(props.authorUri)(state), commentingEnabled: IS_WEB ? Boolean(selectUserVerifiedEmail(state)) : true, - othersReacts: makeSelectOthersReactionsForComment(reactionKey)(state), + othersReacts: selectOthersReactsForComment(state, reactionKey), activeChannelClaim, myChannels: selectMyChannelClaims(state), playingUri: selectPlayingUri(state), diff --git a/ui/component/commentReactions/index.js b/ui/component/commentReactions/index.js index 9f640d619..b2de27008 100644 --- a/ui/component/commentReactions/index.js +++ b/ui/component/commentReactions/index.js @@ -3,7 +3,7 @@ import Comment from './view'; import { makeSelectClaimIsMine, makeSelectClaimForUri } from 'redux/selectors/claims'; import { doResolveUri } from 'redux/actions/claims'; import { doToast } from 'redux/actions/notifications'; -import { makeSelectMyReactionsForComment, makeSelectOthersReactionsForComment } from 'redux/selectors/comments'; +import { selectMyReactsForComment, selectOthersReactsForComment } from 'redux/selectors/comments'; import { doCommentReact } from 'redux/actions/comments'; import { selectActiveChannelClaim } from 'redux/selectors/app'; @@ -15,8 +15,8 @@ const select = (state, props) => { return { claim: makeSelectClaimForUri(props.uri)(state), claimIsMine: makeSelectClaimIsMine(props.uri)(state), - myReacts: makeSelectMyReactionsForComment(reactionKey)(state), - othersReacts: makeSelectOthersReactionsForComment(reactionKey)(state), + myReacts: selectMyReactsForComment(state, reactionKey), + othersReacts: selectOthersReactsForComment(state, reactionKey), activeChannelId, }; }; diff --git a/ui/component/commentsList/index.js b/ui/component/commentsList/index.js index d32dc8476..4b09b9b4f 100644 --- a/ui/component/commentsList/index.js +++ b/ui/component/commentsList/index.js @@ -1,4 +1,5 @@ import { connect } from 'react-redux'; +import { doResolveUris } from 'redux/actions/claims'; import { makeSelectClaimForUri, makeSelectClaimIsMine, @@ -6,17 +7,17 @@ import { selectMyChannelClaims, } from 'redux/selectors/claims'; import { - makeSelectTopLevelCommentsForUri, + selectTopLevelCommentsForUri, makeSelectTopLevelTotalPagesForUri, selectIsFetchingComments, selectIsFetchingCommentsById, selectIsFetchingReacts, makeSelectTotalCommentsCountForUri, - selectOthersReactsById, - selectMyReactionsByCommentId, + selectOthersReacts, + selectMyReacts, makeSelectCommentIdsForUri, selectSettingsByChannelId, - makeSelectPinnedCommentsForUri, + selectPinnedCommentsForUri, } from 'redux/selectors/comments'; import { doCommentReset, doCommentList, doCommentById, doCommentReactList } from 'redux/actions/comments'; import { selectActiveChannelClaim } from 'redux/selectors/app'; @@ -24,11 +25,19 @@ import CommentsList from './view'; const select = (state, props) => { const activeChannelClaim = selectActiveChannelClaim(state); + const topLevelComments = selectTopLevelCommentsForUri(state, props.uri); + + const resolvedComments = + topLevelComments && topLevelComments.length > 0 + ? topLevelComments.filter(({ channel_url }) => makeSelectClaimForUri(channel_url)(state) !== undefined) + : []; + return { + topLevelComments, + resolvedComments, myChannels: selectMyChannelClaims(state), allCommentIds: makeSelectCommentIdsForUri(props.uri)(state), - pinnedComments: makeSelectPinnedCommentsForUri(props.uri)(state), - topLevelComments: makeSelectTopLevelCommentsForUri(props.uri)(state), + pinnedComments: selectPinnedCommentsForUri(state, props.uri), topLevelTotalPages: makeSelectTopLevelTotalPagesForUri(props.uri)(state), totalComments: makeSelectTotalCommentsCountForUri(props.uri)(state), claim: makeSelectClaimForUri(props.uri)(state), @@ -38,8 +47,8 @@ const select = (state, props) => { isFetchingReacts: selectIsFetchingReacts(state), fetchingChannels: selectFetchingMyChannels(state), settingsByChannelId: selectSettingsByChannelId(state), - myReactsByCommentId: selectMyReactionsByCommentId(state), - othersReactsById: selectOthersReactsById(state), + myReactsByCommentId: selectMyReacts(state), + othersReactsById: selectOthersReacts(state), activeChannelId: activeChannelClaim && activeChannelClaim.claim_id, }; }; @@ -49,6 +58,7 @@ const perform = (dispatch) => ({ fetchComment: (commentId) => dispatch(doCommentById(commentId)), fetchReacts: (commentIds) => dispatch(doCommentReactList(commentIds)), resetComments: (claimId) => dispatch(doCommentReset(claimId)), + doResolveUris: (uris) => dispatch(doResolveUris(uris, true)), }); export default connect(select, perform)(CommentsList); diff --git a/ui/component/commentsReplies/index.js b/ui/component/commentsReplies/index.js index 2b4a38c20..d7ac34b2e 100644 --- a/ui/component/commentsReplies/index.js +++ b/ui/component/commentsReplies/index.js @@ -1,15 +1,27 @@ import { connect } from 'react-redux'; -import { makeSelectClaimIsMine, selectMyChannelClaims } from 'redux/selectors/claims'; -import { selectIsFetchingCommentsByParentId, makeSelectRepliesForParentId } from 'redux/selectors/comments'; +import { doResolveUris } from 'redux/actions/claims'; +import { makeSelectClaimIsMine, selectMyChannelClaims, makeSelectClaimForUri } from 'redux/selectors/claims'; +import { selectIsFetchingCommentsByParentId, selectRepliesForParentId } from 'redux/selectors/comments'; import { selectUserVerifiedEmail } from 'redux/selectors/user'; import CommentsReplies from './view'; -const select = (state, props) => ({ - fetchedReplies: makeSelectRepliesForParentId(props.parentId)(state), - claimIsMine: makeSelectClaimIsMine(props.uri)(state), - commentingEnabled: IS_WEB ? Boolean(selectUserVerifiedEmail(state)) : true, - myChannels: selectMyChannelClaims(state), - isFetchingByParentId: selectIsFetchingCommentsByParentId(state), -}); +const select = (state, props) => { + const fetchedReplies = selectRepliesForParentId(state, props.parentId); + const resolvedReplies = + fetchedReplies && fetchedReplies.length > 0 + ? fetchedReplies.filter(({ channel_url }) => makeSelectClaimForUri(channel_url)(state) !== undefined) + : []; -export default connect(select)(CommentsReplies); + return { + fetchedReplies, + resolvedReplies, + claimIsMine: makeSelectClaimIsMine(props.uri)(state), + userCanComment: IS_WEB ? Boolean(selectUserVerifiedEmail(state)) : true, + myChannels: selectMyChannelClaims(state), + isFetchingByParentId: selectIsFetchingCommentsByParentId(state), + }; +}; + +const perform = (dispatch) => ({ doResolveUris: (uris) => dispatch(doResolveUris(uris, true)) }); + +export default connect(select, perform)(CommentsReplies); diff --git a/ui/component/dateTime/index.js b/ui/component/dateTime/index.js index 367ac4ab1..24fc98280 100644 --- a/ui/component/dateTime/index.js +++ b/ui/component/dateTime/index.js @@ -1,11 +1,11 @@ import { connect } from 'react-redux'; -import { makeSelectDateForUri } from 'redux/selectors/claims'; +import { selectDateForUri } from 'redux/selectors/claims'; import * as SETTINGS from 'constants/settings'; import { makeSelectClientSetting } from 'redux/selectors/settings'; import DateTime from './view'; const select = (state, props) => ({ - date: props.date || makeSelectDateForUri(props.uri)(state), + date: props.date || selectDateForUri(state, props.uri), clock24h: makeSelectClientSetting(SETTINGS.CLOCK_24H)(state), }); export default connect(select)(DateTime); diff --git a/ui/page/ownComments/index.js b/ui/page/ownComments/index.js index df53ecfa8..4c41717cb 100644 --- a/ui/page/ownComments/index.js +++ b/ui/page/ownComments/index.js @@ -3,7 +3,7 @@ import { doCommentListOwn, doCommentReset } from 'redux/actions/comments'; import { selectActiveChannelClaim } from 'redux/selectors/app'; import { selectIsFetchingComments, - makeSelectCommentsForUri, + selectCommentsForUri, makeSelectTotalCommentsCountForUri, makeSelectTopLevelTotalPagesForUri, } from 'redux/selectors/comments'; @@ -17,7 +17,7 @@ const select = (state) => { return { activeChannelClaim, - allComments: makeSelectCommentsForUri(uri)(state), + allComments: selectCommentsForUri(state, uri), totalComments: makeSelectTotalCommentsCountForUri(uri)(state), topLevelTotalPages: makeSelectTopLevelTotalPagesForUri(uri)(state), isFetchingComments: selectIsFetchingComments(state), diff --git a/ui/redux/actions/comments.js b/ui/redux/actions/comments.js index d93cb6b52..5ab158fa1 100644 --- a/ui/redux/actions/comments.js +++ b/ui/redux/actions/comments.js @@ -9,8 +9,8 @@ import { selectClaimsByUri, selectMyChannelClaims } from 'redux/selectors/claims import { doClaimSearch } from 'redux/actions/claims'; import { doToast, doSeeNotifications } from 'redux/actions/notifications'; import { - makeSelectMyReactionsForComment, - makeSelectOthersReactionsForComment, + selectMyReactsForComment, + selectOthersReactsForComment, selectPendingCommentReacts, selectModerationBlockList, selectModerationDelegatorsById, @@ -466,8 +466,8 @@ export function doCommentReact(commentId: string, type: string) { } const reactKey = `${commentId}:${activeChannelClaim.claim_id}`; - const myReacts = makeSelectMyReactionsForComment(reactKey)(state); - const othersReacts = makeSelectOthersReactionsForComment(reactKey)(state); + const myReacts = selectMyReactsForComment(state, reactKey) || []; + const othersReacts = selectOthersReactsForComment(state, reactKey) || {}; const signatureData = await channelSignName(activeChannelClaim.claim_id, activeChannelClaim.name); if (!signatureData) { diff --git a/ui/redux/reducers/tags.js b/ui/redux/reducers/tags.js index 4edd4fbf7..c3be1baf2 100644 --- a/ui/redux/reducers/tags.js +++ b/ui/redux/reducers/tags.js @@ -65,11 +65,22 @@ export default handleActions( [ACTIONS.USER_STATE_POPULATE]: (state: TagState, action: { data: { tags: ?Array } }) => { const { tags } = action.data; if (Array.isArray(tags)) { + const next = tags && tags.filter((tag) => typeof tag === 'string'); + const prev = state.followedTags; + + if (next && prev && prev.length === next.length && prev.every((value, index) => value === next[index])) { + // No changes + return state; + } + + // New state return { ...state, - followedTags: tags, + followedTags: next || [], }; } + + // Purge 'followedTags' return { ...state, }; diff --git a/ui/redux/selectors/claims.js b/ui/redux/selectors/claims.js index 6cb544820..45f12936e 100644 --- a/ui/redux/selectors/claims.js +++ b/ui/redux/selectors/claims.js @@ -2,20 +2,22 @@ import { normalizeURI, parseURI, isURIValid } from 'util/lbryURI'; import { selectSupportsByOutpoint } from 'redux/selectors/wallet'; import { createSelector } from 'reselect'; +import { createCachedSelector } from 're-reselect'; import { isClaimNsfw, filterClaims } from 'util/claim'; import * as CLAIM from 'constants/claim'; +type State = { claims: any }; + const selectState = (state) => state.claims || {}; -export const selectById = createSelector(selectState, (state) => state.byId || {}); - -export const selectPendingClaimsById = createSelector(selectState, (state) => state.pendingById || {}); +export const selectById = (state: State) => selectState(state).byId || {}; +export const selectPendingClaimsById = (state: State) => selectState(state).pendingById || {}; export const selectClaimsById = createSelector(selectById, selectPendingClaimsById, (byId, pendingById) => { return Object.assign(byId, pendingById); // do I need merged to keep metadata? }); -export const selectClaimIdsByUri = createSelector(selectState, (state) => state.claimsByUri || {}); +export const selectClaimIdsByUri = (state: State) => selectState(state).claimsByUri || {}; export const selectCurrentChannelPage = createSelector(selectState, (state) => state.currentChannelPage || 1); @@ -74,6 +76,43 @@ export const selectReflectingById = createSelector(selectState, (state) => state export const makeSelectClaimForClaimId = (claimId: string) => createSelector(selectClaimsById, (byId) => byId[claimId]); +export const selectClaimForUri = createCachedSelector( + selectClaimIdsByUri, + selectClaimsById, + (state, uri) => uri, + (state, uri, returnRepost = true) => returnRepost, + (byUri, byId, uri, returnRepost) => { + const validUri = isURIValid(uri); + + if (validUri && byUri) { + const claimId = uri && byUri[normalizeURI(uri)]; + const claim = byId[claimId]; + + // Make sure to return the claim as is so apps can check if it's been resolved before (null) or still needs to be resolved (undefined) + if (claimId === null) { + return null; + } else if (claimId === undefined) { + return undefined; + } + + const repostedClaim = claim && claim.reposted_claim; + if (repostedClaim && returnRepost) { + const channelUrl = + claim.signing_channel && (claim.signing_channel.canonical_url || claim.signing_channel.permanent_url); + + return { + ...repostedClaim, + repost_url: normalizeURI(uri), + repost_channel_url: channelUrl, + repost_bid_amount: claim && claim.meta && claim.meta.effective_amount, + }; + } else { + return claim; + } + } + } +)((state, uri, returnRepost = true) => `${uri}:${returnRepost ? '1' : '0'}`); + export const makeSelectClaimForUri = (uri: string, returnRepost: boolean = true) => createSelector(selectClaimIdsByUri, selectClaimsById, (byUri, byId) => { const validUri = isURIValid(uri); @@ -228,6 +267,11 @@ export const makeSelectTotalPagesInChannelSearch = (uri: string) => return byChannel['pageCount']; }); +export const selectMetadataForUri = createCachedSelector(selectClaimForUri, (claim, uri) => { + const metadata = claim && claim.value; + return metadata || (claim === undefined ? undefined : null); +})((state, uri) => uri); + export const makeSelectMetadataForUri = (uri: string) => createSelector(makeSelectClaimForUri(uri), (claim) => { const metadata = claim && claim.value; @@ -242,8 +286,9 @@ export const makeSelectMetadataItemForUri = (uri: string, key: string) => export const makeSelectTitleForUri = (uri: string) => createSelector(makeSelectMetadataForUri(uri), (metadata) => metadata && metadata.title); -export const makeSelectDateForUri = (uri: string) => - createSelector(makeSelectClaimForUri(uri), (claim) => { +export const selectDateForUri = createCachedSelector( + selectClaimForUri, // input: (state, uri, ?returnRepost) + (claim) => { const timestamp = claim && claim.value && @@ -257,7 +302,8 @@ export const makeSelectDateForUri = (uri: string) => } const dateObj = new Date(timestamp); return dateObj; - }); + } +)((state, uri) => uri); export const makeSelectAmountForUri = (uri: string) => createSelector(makeSelectClaimForUri(uri), (claim) => { @@ -493,6 +539,10 @@ export const makeSelectMyChannelPermUrlForName = (name: string) => return matchingClaim ? matchingClaim.permanent_url : null; }); +export const selectTagsForUri = createCachedSelector(selectMetadataForUri, (metadata: ?GenericMetadata) => { + return (metadata && metadata.tags) || []; +})((state, uri) => uri); + export const makeSelectTagsForUri = (uri: string) => createSelector(makeSelectMetadataForUri(uri), (metadata: ?GenericMetadata) => { return (metadata && metadata.tags) || []; diff --git a/ui/redux/selectors/comments.js b/ui/redux/selectors/comments.js index 045bf001d..bc53eb785 100644 --- a/ui/redux/selectors/comments.js +++ b/ui/redux/selectors/comments.js @@ -1,43 +1,79 @@ // @flow import { createSelector } from 'reselect'; +import { createCachedSelector } from 're-reselect'; import { selectMutedChannels } from 'redux/selectors/blocked'; import { selectShowMatureContent } from 'redux/selectors/settings'; import { selectBlacklistedOutpointMap, selectFilteredOutpointMap } from 'lbryinc'; import { selectClaimsById, selectMyActiveClaims } from 'redux/selectors/claims'; import { isClaimNsfw } from 'util/claim'; +type State = { comments: CommentsState }; + const selectState = (state) => state.comments || {}; -export const selectCommentsById = createSelector(selectState, (state) => state.commentById || {}); -export const selectIsFetchingComments = createSelector(selectState, (state) => state.isLoading); -export const selectIsFetchingCommentsById = createSelector(selectState, (state) => state.isLoadingById); -export const selectIsFetchingCommentsByParentId = createSelector(selectState, (state) => state.isLoadingByParentId); -export const selectIsFetchingReacts = createSelector(selectState, (state) => state.isFetchingReacts); -export const selectOthersReactsById = createSelector(selectState, (state) => state.othersReactsByCommentId); +export const selectCommentsById = (state: State) => selectState(state).commentById || {}; +export const selectIsFetchingComments = (state: State) => selectState(state).isLoading; +export const selectIsFetchingCommentsById = (state: State) => selectState(state).isLoadingById; +export const selectIsFetchingCommentsByParentId = (state: State) => selectState(state).isLoadingByParentId; +export const selectIsFetchingReacts = (state: State) => selectState(state).isFetchingReacts; -export const selectPinnedCommentsById = createSelector(selectState, (state) => state.pinnedCommentsById); -export const makeSelectPinnedCommentsForUri = (uri: string) => - createSelector( - selectCommentsByUri, - selectCommentsById, - selectPinnedCommentsById, - (byUri, byId, pinnedCommentsById) => { - const claimId = byUri[uri]; - const pinnedCommentIds = pinnedCommentsById && pinnedCommentsById[claimId]; - const pinnedComments = []; +export const selectMyReacts = (state: State) => state.comments.myReactsByCommentId; +export const selectMyReactsForComment = (state: State, commentIdChannelId: string) => { + // @commentIdChannelId: Format = 'commentId:MyChannelId' + return state.comments.myReactsByCommentId && state.comments.myReactsByCommentId[commentIdChannelId]; +}; - if (pinnedCommentIds) { - pinnedCommentIds.forEach((commentId) => { - pinnedComments.push(byId[commentId]); - }); - } +export const selectOthersReacts = (state: State) => state.comments.othersReactsByCommentId; +export const selectOthersReactsForComment = (state: State, id: string) => { + return state.comments.othersReactsByCommentId && state.comments.othersReactsByCommentId[id]; +}; - return pinnedComments; +// previously this used a mapping from claimId -> Array +/* export const selectCommentsById = createSelector( + selectState, + state => state.byId || {} + ); */ +export const selectCommentsByUri = createSelector(selectState, (state) => { + const byUri = state.commentsByUri || {}; + const comments = {}; + Object.keys(byUri).forEach((uri) => { + const claimId = byUri[uri]; + if (claimId === null) { + comments[uri] = null; + } else { + comments[uri] = claimId; } - ); + }); -export const selectModerationBlockList = createSelector(selectState, (state) => - state.moderationBlockList ? state.moderationBlockList.reverse() : [] + return comments; +}); + +export const selectPinnedCommentsById = (state: State) => selectState(state).pinnedCommentsById; +export const selectPinnedCommentsForUri = createCachedSelector( + selectCommentsByUri, + selectCommentsById, + selectPinnedCommentsById, + (state, uri) => uri, + (byUri, byId, pinnedCommentsById, uri) => { + const claimId = byUri[uri]; + const pinnedCommentIds = pinnedCommentsById && pinnedCommentsById[claimId]; + const pinnedComments = []; + + if (pinnedCommentIds) { + pinnedCommentIds.forEach((commentId) => { + pinnedComments.push(byId[commentId]); + }); + } + + return pinnedComments; + } +)((state, uri) => uri); + +export const selectModerationBlockList = createSelector( + (state) => selectState(state).moderationBlockList, + (moderationBlockList) => { + return moderationBlockList ? moderationBlockList.reverse() : []; + } ); export const selectAdminBlockList = createSelector(selectState, (state) => state.adminBlockList ? state.adminBlockList.reverse() : [] @@ -46,35 +82,19 @@ export const selectModeratorBlockList = createSelector(selectState, (state) => state.moderatorBlockList ? state.moderatorBlockList.reverse() : [] ); -export const selectPersonalTimeoutMap = createSelector(selectState, (state) => state.personalTimeoutMap); -export const selectAdminTimeoutMap = createSelector(selectState, (state) => state.adminTimeoutMap); -export const selectModeratorTimeoutMap = createSelector(selectState, (state) => state.moderatorTimeoutMap); - -export const selectModeratorBlockListDelegatorsMap = createSelector( - selectState, - (state) => state.moderatorBlockListDelegatorsMap -); - -export const selectTogglingForDelegatorMap = createSelector(selectState, (state) => state.togglingForDelegatorMap); - -export const selectBlockingByUri = createSelector(selectState, (state) => state.blockingByUri); -export const selectUnBlockingByUri = createSelector(selectState, (state) => state.unBlockingByUri); -export const selectFetchingModerationBlockList = createSelector( - selectState, - (state) => state.fetchingModerationBlockList -); - -export const selectModerationDelegatesById = createSelector(selectState, (state) => state.moderationDelegatesById); -export const selectIsFetchingModerationDelegates = createSelector( - selectState, - (state) => state.fetchingModerationDelegates -); - -export const selectModerationDelegatorsById = createSelector(selectState, (state) => state.moderationDelegatorsById); -export const selectIsFetchingModerationDelegators = createSelector( - selectState, - (state) => state.fetchingModerationDelegators -); +export const selectPersonalTimeoutMap = (state: State) => selectState(state).personalTimeoutMap; +export const selectAdminTimeoutMap = (state: State) => selectState(state).adminTimeoutMap; +export const selectModeratorTimeoutMap = (state: State) => selectState(state).moderatorTimeoutMap; +export const selectModeratorBlockListDelegatorsMap = (state: State) => + selectState(state).moderatorBlockListDelegatorsMap; +export const selectTogglingForDelegatorMap = (state: State) => selectState(state).togglingForDelegatorMap; +export const selectBlockingByUri = (state: State) => selectState(state).blockingByUri; +export const selectUnBlockingByUri = (state: State) => selectState(state).unBlockingByUri; +export const selectFetchingModerationBlockList = (state: State) => selectState(state).fetchingModerationBlockList; +export const selectModerationDelegatesById = (state: State) => selectState(state).moderationDelegatesById; +export const selectIsFetchingModerationDelegates = (state: State) => selectState(state).fetchingModerationDelegates; +export const selectModerationDelegatorsById = (state: State) => selectState(state).moderationDelegatorsById; +export const selectIsFetchingModerationDelegators = (state: State) => selectState(state).fetchingModerationDelegators; export const selectHasAdminChannel = createSelector(selectState, (state) => { const myChannelIds = Object.keys(state.moderationDelegatorsById); @@ -107,24 +127,29 @@ export const selectCommentsByClaimId = createSelector(selectState, selectComment return comments; }); -export const selectSuperchatsByUri = createSelector(selectState, (state) => state.superChatsByUri); +// no superchats? +export const selectSuperchatsByUri = (state: State) => selectState(state).superChatsByUri; -export const selectTopLevelCommentsByClaimId = createSelector(selectState, selectCommentsById, (state, byId) => { - const byClaimId = state.topLevelCommentsById || {}; - const comments = {}; +export const selectTopLevelCommentsByClaimId = createSelector( + (state) => selectState(state).topLevelCommentsById, + selectCommentsById, + (topLevelCommentsById, byId) => { + const byClaimId = topLevelCommentsById || {}; + const comments = {}; - // replace every comment_id in the list with the actual comment object - Object.keys(byClaimId).forEach((claimId) => { - const commentIds = byClaimId[claimId]; + // replace every comment_id in the list with the actual comment object + Object.keys(byClaimId).forEach((claimId) => { + const commentIds = byClaimId[claimId]; - comments[claimId] = Array(commentIds === null ? 0 : commentIds.length); - for (let i = 0; i < commentIds.length; i++) { - comments[claimId][i] = byId[commentIds[i]]; - } - }); + comments[claimId] = Array(commentIds === null ? 0 : commentIds.length); + for (let i = 0; i < commentIds.length; i++) { + comments[claimId][i] = byId[commentIds[i]]; + } + }); - return comments; -}); + return comments; + } +); export const makeSelectCommentForCommentId = (commentId: string) => createSelector(selectCommentsById, (comments) => comments[commentId]); @@ -146,27 +171,7 @@ export const selectRepliesByParentId = createSelector(selectState, selectComment return comments; }); -// previously this used a mapping from claimId -> Array -/* export const selectCommentsById = createSelector( - selectState, - state => state.byId || {} -); */ -export const selectCommentsByUri = createSelector(selectState, (state) => { - const byUri = state.commentsByUri || {}; - const comments = {}; - Object.keys(byUri).forEach((uri) => { - const claimId = byUri[uri]; - if (claimId === null) { - comments[uri] = null; - } else { - comments[uri] = claimId; - } - }); - - return comments; -}); - -export const selectLinkedCommentAncestors = createSelector(selectState, (state) => state.linkedCommentAncestors); +export const selectLinkedCommentAncestors = (state: State) => selectState(state).linkedCommentAncestors; export const makeSelectCommentIdsForUri = (uri: string) => createSelector(selectState, selectCommentsByUri, selectClaimsById, (state, byUri) => { @@ -174,62 +179,48 @@ export const makeSelectCommentIdsForUri = (uri: string) => return state.byId[claimId]; }); -export const selectMyReactionsByCommentId = createSelector(selectState, (state) => state.myReactsByCommentId); +const filterCommentsDepOnList = { + claimsById: selectClaimsById, + myClaims: selectMyActiveClaims, + mutedChannels: selectMutedChannels, + personalBlockList: selectModerationBlockList, + blacklistedMap: selectBlacklistedOutpointMap, + filteredMap: selectFilteredOutpointMap, + showMatureContent: selectShowMatureContent, +}; -/** - * makeSelectMyReactionsForComment - * - * @param commentIdChannelId Format = "commentId:MyChannelId". - */ -export const makeSelectMyReactionsForComment = (commentIdChannelId: string) => - createSelector(selectState, (state) => { - if (!state.myReactsByCommentId) { - return []; - } +const filterCommentsPropKeys = Object.keys(filterCommentsDepOnList); - return state.myReactsByCommentId[commentIdChannelId] || []; - }); +export const selectPendingCommentReacts = (state: State) => selectState(state).pendingCommentReactions; +export const selectSettingsByChannelId = (state: State) => selectState(state).settingsByChannelId; +export const selectFetchingCreatorSettings = (state: State) => selectState(state).fetchingSettings; +export const selectFetchingBlockedWords = (state: State) => selectState(state).fetchingBlockedWords; -export const makeSelectOthersReactionsForComment = (commentId: string) => - createSelector(selectState, (state) => { - if (!state.othersReactsByCommentId) { - return {}; - } +export const selectCommentsForUri = createCachedSelector( + (state, uri) => uri, + selectCommentsByClaimId, + selectCommentsByUri, + ...Object.values(filterCommentsDepOnList), + (uri, byClaimId, byUri, ...filterInputs) => { + const claimId = byUri[uri]; + const comments = byClaimId && byClaimId[claimId]; + return filterComments(comments, claimId, filterInputs); + } +)((state, uri) => uri); - return state.othersReactsByCommentId[commentId] || {}; - }); - -export const selectPendingCommentReacts = createSelector(selectState, (state) => state.pendingCommentReactions); - -export const selectSettingsByChannelId = createSelector(selectState, (state) => state.settingsByChannelId); - -export const selectFetchingCreatorSettings = createSelector(selectState, (state) => state.fetchingSettings); - -export const selectFetchingBlockedWords = createSelector(selectState, (state) => state.fetchingBlockedWords); - -export const makeSelectCommentsForUri = (uri: string) => - createSelector( - (state) => state, - selectCommentsByClaimId, - selectCommentsByUri, - (state, byClaimId, byUri) => { - const claimId = byUri[uri]; - const comments = byClaimId && byClaimId[claimId]; - return makeSelectFilteredComments(comments, claimId)(state); - } - ); - -export const makeSelectTopLevelCommentsForUri = (uri: string) => - createSelector( - (state) => state, - selectTopLevelCommentsByClaimId, - selectCommentsByUri, - (state, byClaimId, byUri) => { - const claimId = byUri[uri]; - const comments = byClaimId && byClaimId[claimId]; - return makeSelectFilteredComments(comments, claimId)(state); - } - ); +export const selectTopLevelCommentsForUri = createCachedSelector( + (state, uri) => uri, + (state, uri, maxCount) => maxCount, + selectTopLevelCommentsByClaimId, + selectCommentsByUri, + ...Object.values(filterCommentsDepOnList), + (uri, maxCount = -1, byClaimId, byUri, ...filterInputs) => { + const claimId = byUri[uri]; + const comments = byClaimId && byClaimId[claimId]; + const filtered = filterComments(comments, claimId, filterInputs); + return maxCount > 0 ? filtered.slice(0, maxCount) : filtered; + } +)((state, uri, maxCount = -1) => `${uri}:${maxCount}`); export const makeSelectTopLevelTotalCommentsForUri = (uri: string) => createSelector(selectState, selectCommentsByUri, (state, byUri) => { @@ -243,99 +234,93 @@ export const makeSelectTopLevelTotalPagesForUri = (uri: string) => return state.topLevelTotalPagesById[claimId] || 0; }); -export const makeSelectRepliesForParentId = (id: string) => - createSelector( - (state) => state, - selectCommentsById, - (state, commentsById) => { - // const claimId = byUri[uri]; // just parentId (id) - const replyIdsByParentId = state.comments.repliesByParentId; - const replyIdsForParent = replyIdsByParentId[id] || []; - if (!replyIdsForParent.length) return null; +export const selectRepliesForParentId = createCachedSelector( + (state, id) => id, + (state) => selectState(state).repliesByParentId, + selectCommentsById, + ...Object.values(filterCommentsDepOnList), + (id, repliesByParentId, commentsById, ...filterInputs) => { + // const claimId = byUri[uri]; // just parentId (id) + const replyIdsForParent = repliesByParentId[id] || []; + if (!replyIdsForParent.length) return null; - const comments = []; - replyIdsForParent.forEach((cid) => { - comments.push(commentsById[cid]); - }); - // const comments = byParentId && byParentId[id]; + const comments = []; + replyIdsForParent.forEach((cid) => { + comments.push(commentsById[cid]); + }); + // const comments = byParentId && byParentId[id]; - return makeSelectFilteredComments(comments)(state); - } - ); + return filterComments(comments, undefined, filterInputs); + } +)((state, id: string) => id); /** - * makeSelectFilteredComments + * filterComments * * @param comments List of comments to filter. * @param claimId The claim that `comments` reside in. + * @oaram filterInputs Values returned by filterCommentsDepOnList. */ -const makeSelectFilteredComments = (comments: Array, claimId?: string) => - createSelector( - selectClaimsById, - selectMyActiveClaims, - selectMutedChannels, - selectModerationBlockList, - selectAdminBlockList, - selectModeratorBlockList, - selectBlacklistedOutpointMap, - selectFilteredOutpointMap, - selectShowMatureContent, - ( - claimsById, - myClaims, - mutedChannels, - personalBlockList, - adminBlockList, - moderatorBlockList, - blacklistedMap, - filteredMap, - showMatureContent - ) => { - return comments - ? comments.filter((comment) => { - if (!comment) { - // It may have been recently deleted after being blocked +const filterComments = (comments: Array, claimId?: string, filterInputs: any) => { + const filterProps = filterInputs.reduce(function (acc, cur, i) { + acc[filterCommentsPropKeys[i]] = cur; + return acc; + }, {}); + + const { + claimsById, + myClaims, + mutedChannels, + personalBlockList, + blacklistedMap, + filteredMap, + showMatureContent, + } = filterProps; + + return comments + ? comments.filter((comment) => { + if (!comment) { + // It may have been recently deleted after being blocked + return false; + } + + const channelClaim = claimsById[comment.channel_id]; + + // Return comment if `channelClaim` doesn't exist so the component knows to resolve the author + if (channelClaim) { + if (myClaims && myClaims.size > 0) { + const claimIsMine = channelClaim.is_my_output || myClaims.has(channelClaim.claim_id); + if (claimIsMine) { + return true; + } + } + + const outpoint = `${channelClaim.txid}:${channelClaim.nout}`; + if (blacklistedMap[outpoint] || filteredMap[outpoint]) { + return false; + } + + if (!showMatureContent) { + const claimIsMature = isClaimNsfw(channelClaim); + if (claimIsMature) { return false; } + } + } - const channelClaim = claimsById[comment.channel_id]; - - // Return comment if `channelClaim` doesn't exist so the component knows to resolve the author - if (channelClaim) { - if (myClaims && myClaims.size > 0) { - const claimIsMine = channelClaim.is_my_output || myClaims.has(channelClaim.claim_id); - if (claimIsMine) { - return true; - } - } - - const outpoint = `${channelClaim.txid}:${channelClaim.nout}`; - if (blacklistedMap[outpoint] || filteredMap[outpoint]) { - return false; - } - - if (!showMatureContent) { - const claimIsMature = isClaimNsfw(channelClaim); - if (claimIsMature) { - return false; - } - } + if (claimId) { + const claimIdIsMine = myClaims && myClaims.size > 0 && myClaims.has(claimId); + if (!claimIdIsMine) { + if (personalBlockList.includes(comment.channel_url)) { + return false; } + } + } - if (claimId) { - const claimIdIsMine = myClaims && myClaims.size > 0 && myClaims.has(claimId); - if (!claimIdIsMine) { - if (personalBlockList.includes(comment.channel_url) || adminBlockList.includes(comment.channel_url)) { - return false; - } - } - } - - return !mutedChannels.includes(comment.channel_url); - }) - : []; - } - ); + return !mutedChannels.includes(comment.channel_url); + }) + : []; +}; export const makeSelectTotalReplyPagesForParentId = (parentId: string) => createSelector(selectState, (state) => { diff --git a/ui/redux/selectors/tags.js b/ui/redux/selectors/tags.js index c75fbaa39..1651f14d9 100644 --- a/ui/redux/selectors/tags.js +++ b/ui/redux/selectors/tags.js @@ -1,16 +1,16 @@ // @flow import { createSelector } from 'reselect'; -const selectState = (state: { tags: TagState }) => state.tags || {}; +type State = { tags: TagState }; + +const selectState = (state: State) => state.tags || {}; export const selectKnownTagsByName = createSelector(selectState, (state: TagState): KnownTags => state.knownTags); -export const selectFollowedTagsList = createSelector(selectState, (state: TagState): Array => - state.followedTags.filter(tag => typeof tag === 'string') -); +export const selectFollowedTagsList = (state: State) => selectState(state).followedTags; export const selectFollowedTags = createSelector(selectFollowedTagsList, (followedTags: Array): Array => - followedTags.map(tag => ({ name: tag.toLowerCase() })).sort((a, b) => a.name.localeCompare(b.name)) + followedTags.map((tag) => ({ name: tag.toLowerCase() })).sort((a, b) => a.name.localeCompare(b.name)) ); export const selectUnfollowedTags = createSelector( @@ -19,7 +19,7 @@ export const selectUnfollowedTags = createSelector( (tagsByName: KnownTags, followedTags: Array): Array => { const followedTagsSet = new Set(followedTags); let tagsToReturn = []; - Object.keys(tagsByName).forEach(key => { + Object.keys(tagsByName).forEach((key) => { if (!followedTagsSet.has(key)) { const { name } = tagsByName[key]; tagsToReturn.push({ name: name.toLowerCase() }); @@ -31,6 +31,6 @@ export const selectUnfollowedTags = createSelector( ); export const makeSelectIsFollowingTag = (tag: string) => - createSelector(selectFollowedTags, followedTags => { - return followedTags.some(followedTag => followedTag.name === tag.toLowerCase()); + createSelector(selectFollowedTags, (followedTags) => { + return followedTags.some((followedTag) => followedTag.name === tag.toLowerCase()); }); diff --git a/yarn.lock b/yarn.lock index 66c53b697..571bdedf8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -13179,6 +13179,11 @@ rc@^1.0.1, rc@^1.1.6, rc@^1.2.8: minimist "^1.2.0" strip-json-comments "~2.0.1" +re-reselect@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/re-reselect/-/re-reselect-4.0.0.tgz#9ddec4c72c4d952f68caa5aa4b76a9ed38b75cac" + integrity sha512-wuygyq8TXUlSdVXv2kigXxQNOgdb9m7LbIjwfTNGSpaY1riLd5e+VeQjlQMyUtrk0oiyhi1AqIVynworl3qxHA== + react-async-script@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/react-async-script/-/react-async-script-1.1.1.tgz#f481c6c5f094bf4b94a9d52da0d0dda2e1a74bdf"