diff --git a/config.js b/config.js index 85a18883d..d1cd84164 100644 --- a/config.js +++ b/config.js @@ -52,6 +52,7 @@ const config = { STRIPE_PUBLIC_KEY: process.env.STRIPE_PUBLIC_KEY, ENABLE_UI_NOTIFICATIONS: process.env.ENABLE_UI_NOTIFICATIONS === 'true', ENABLE_MATURE: process.env.ENABLE_MATURE === 'true', + CUSTOM_HOMEPAGE: process.env.CUSTOM_HOMEPAGE === 'true', }; config.URL_LOCAL = `http://localhost:${config.WEB_SERVER_PORT}`; diff --git a/flow-typed/Comment.js b/flow-typed/Comment.js index adbecea26..15c6d3038 100644 --- a/flow-typed/Comment.js +++ b/flow-typed/Comment.js @@ -13,6 +13,7 @@ declare type Comment = { parent_id?: number, // comment_id of comment this is in reply to is_pinned: boolean, support_amount: number, + replies: number, // number of direct replies (i.e. excluding nested replies). }; declare type PerChannelSettings = { @@ -27,15 +28,21 @@ declare type PerChannelSettings = { declare type CommentsState = { commentsByUri: { [string]: string }, superChatsByUri: { [string]: { totalAmount: number, comments: Array } }, - byId: { [string]: Array }, - repliesByParentId: { [string]: Array }, // ParentCommentID -> list of reply comments - topLevelCommentsById: { [string]: Array }, // ClaimID -> list of top level comments + byId: { [string]: Array }, // ClaimID -> list of fetched comment IDs. + totalCommentsById: {}, // ClaimId -> ultimate total (including replies) in commentron. + repliesByParentId: { [string]: Array }, // ParentCommentID -> list of fetched replies. + totalRepliesByParentId: {}, // ParentCommentID -> total replies in commentron. + topLevelCommentsById: { [string]: Array }, // ClaimID -> list of fetched top level comments. + topLevelTotalPagesById: { [string]: number }, // ClaimID -> total number of top-level pages in commentron. Based on COMMENT_PAGE_SIZE_TOP_LEVEL. + topLevelTotalCommentsById: { [string]: number }, // ClaimID -> total top level comments in commentron. commentById: { [string]: Comment }, + linkedCommentAncestors: { [string]: Array }, // {"linkedCommentId": ["parentId", "grandParentId", ...]} isLoading: boolean, + isLoadingByParentId: { [string]: boolean }, myComments: ?Set, isFetchingReacts: boolean, - myReactsByCommentId: any, - othersReactsByCommentId: any, + myReactsByCommentId: ?{ [string]: Array }, // {"CommentId:MyChannelId": ["like", "dislike", ...]} + othersReactsByCommentId: ?{ [string]: { [string]: number } }, // {"CommentId:MyChannelId": {"like": 2, "dislike": 2, ...}} pendingCommentReactions: Array, moderationBlockList: ?Array, // @KP rename to "personalBlockList"? adminBlockList: ?Array, @@ -64,17 +71,48 @@ declare type CommentReactParams = { remove?: boolean, }; +declare type CommentReactListParams = { + comment_ids?: string, + channel_id?: string, + channel_name?: string, + wallet_id?: string, + react_types?: string, +}; + declare type CommentListParams = { - page: number, - page_size: number, - claim_id: string, + page: number, // pagination: which page of results + page_size: number, // pagination: nr of comments to show in a page (max 200) + claim_id: string, // claim id of claim being commented on + channel_name?: string, // signing channel name of claim (enables 'commentsEnabled' check) + channel_id?: string, // signing channel claim id of claim (enables 'commentsEnabled' check) + author_claim_id?: string, // filters comments to just this author + parent_id?: string, // filters comments to those under this thread + top_level?: boolean, // filters to only top level comments + hidden?: boolean, // if true, will show hidden comments as well + sort_by?: number, // NEWEST=0, OLDEST=1, CONTROVERSY=2, POPULARITY=3, }; declare type CommentListResponse = { items: Array, - total_amount: number, + page: number, + page_size: number, + total_items: number, // Grand total for the claim being commented on. + total_filtered_items: number, // Total for filtered queries (e.g. top_level=true, parent_id=xxx, etc.). + total_pages: number, + has_hidden_comments: boolean, }; +declare type CommentByIdParams = { + comment_id: string, + with_ancestors: boolean, +} + +declare type CommentByIdResponse = { + item: Comment, + items: Comment, + ancestors: Array, +} + declare type CommentAbandonParams = { comment_id: string, creator_channel_id?: string, @@ -94,6 +132,16 @@ declare type CommentCreateParams = { declare type SuperListParams = {}; +declare type SuperListResponse = { + page: number, + page_size: number, + total_pages: number, + total_items: number, + total_amount: number, + items: Array, + has_hidden_comments: boolean, +}; + declare type ModerationBlockParams = {}; declare type ModerationAddDelegateParams = { diff --git a/flow-typed/homepage.js b/flow-typed/homepage.js index ee4b1226f..0ea62495e 100644 --- a/flow-typed/homepage.js +++ b/flow-typed/homepage.js @@ -20,7 +20,7 @@ declare type RowDataItem = { options?: { channelIds?: Array, limitClaimsPerChannel?: number, - pageSize: number, + pageSize?: number, }, route?: string, hideForUnauth?: boolean, diff --git a/homepages/homepage.js b/homepages/homepage.js index 48181bcf1..36b893b47 100644 --- a/homepages/homepage.js +++ b/homepages/homepage.js @@ -1,38 +1,7 @@ -// @flow -import * as PAGES from 'constants/pages'; -import * as ICONS from 'constants/icons'; -import * as CS from 'constants/claim_search'; -import { parseURI } from 'lbry-redux'; -import moment from 'moment'; -import { toCapitalCase } from 'util/string'; -import { useIsLargeScreen } from 'effects/use-screensize'; +import * as PAGES from '../ui/constants/pages'; +import * as CS from '../ui/constants/claim_search'; -export type RowDataItem = { - title: string, - link?: string, - help?: any, - options?: {}, - icon?: string, -}; - -export default function GetHomePageRowData( - authenticated: boolean, - showPersonalizedChannels: boolean, - showPersonalizedTags: boolean, - subscribedChannels: Array, - followedTags: Array, - showIndividualTags: boolean, - showNsfw: boolean -) { - const isLargeScreen = useIsLargeScreen(); - - function getPageSize(originalSize) { - return isLargeScreen ? originalSize * (3 / 2) : originalSize; - } - - let rowData: Array = []; - const individualTagDataItems: Array = []; - const YOUTUBER_CHANNEL_IDS = [ +const YOUTUBER_CHANNEL_IDS = [ 'fb364ef587872515f545a5b4b3182b58073f230f', '589276465a23c589801d874f484cc39f307d7ec7', 'ba79c80788a9e1751e49ad401f5692d86f73a2db', @@ -116,142 +85,18 @@ export default function GetHomePageRowData( 'e8f68563d242f6ac9784dcbc41dd86c28a9391d6', ]; - const YOUTUBE_CREATOR_ROW = { - title: __('CableTube Escape Artists'), - link: `/$/${PAGES.DISCOVER}?${CS.CLAIM_TYPE}=${CS.CLAIM_STREAM}&${CS.CHANNEL_IDS_KEY}=${YOUTUBER_CHANNEL_IDS.join( - ',' - )}`, - options: { - claimType: ['stream'], - orderBy: ['release_time'], - pageSize: getPageSize(12), - channelIds: YOUTUBER_CHANNEL_IDS, - limitClaimsPerChannel: 1, - releaseTime: `>${Math.floor(moment().subtract(1, 'months').startOf('week').unix())}`, - }, + const YOUTUBERS = { + ids: YOUTUBER_CHANNEL_IDS, + link: `/$/${PAGES.DISCOVER}?${CS.CLAIM_TYPE}=${CS.CLAIM_STREAM}&${CS.CHANNEL_IDS_KEY}=${YOUTUBER_CHANNEL_IDS.join( + ',' + )}`, + name: 'general', + label: 'CableTube Escape Artists', + channelLimit: 1, + daysOfContent: 30, + pageSize: 24, + //pinnedUrls: [], + //mixIn: [], }; - if (followedTags.length) { - followedTags.forEach((tag: Tag) => { - const tagName = `#${toCapitalCase(tag.name)}`; - individualTagDataItems.push({ - title: __('Trending for %tagName%', { tagName: tagName }), - link: `/$/${PAGES.DISCOVER}?t=${tag.name}`, - options: { - pageSize: 4, - tags: [tag.name], - claimType: ['stream'], - }, - }); - }); - } - - const RECENT_FROM_FOLLOWING = { - title: __('Recent From Following'), - link: `/$/${PAGES.CHANNELS_FOLLOWING}`, - icon: ICONS.SUBSCRIBE, - options: { - streamTypes: null, - orderBy: ['release_time'], - releaseTime: - subscribedChannels.length > 20 - ? `>${Math.floor(moment().subtract(6, 'months').startOf('week').unix())}` - : `>${Math.floor(moment().subtract(1, 'year').startOf('week').unix())}`, - pageSize: getPageSize(subscribedChannels.length > 3 ? (subscribedChannels.length > 6 ? 16 : 8) : 4), - channelIds: subscribedChannels.map((subscription: Subscription) => { - const { channelClaimId } = parseURI(subscription.uri); - return channelClaimId; - }), - }, - }; - - const TOP_CONTENT_TODAY = { - title: __('Top Content from Today'), - link: `/$/${PAGES.DISCOVER}?${CS.ORDER_BY_KEY}=${CS.ORDER_BY_TOP}&${CS.FRESH_KEY}=${CS.FRESH_DAY}`, - options: { - pageSize: getPageSize(showPersonalizedChannels || showPersonalizedTags ? 4 : 8), - orderBy: ['effective_amount'], - claimType: ['stream'], - limitClaimsPerChannel: 2, - releaseTime: `>${Math.floor(moment().subtract(1, 'day').startOf('day').unix())}`, - }, - }; - - const TOP_CHANNELS = { - title: __('Top Channels On LBRY'), - link: `/$/${PAGES.DISCOVER}?claim_type=channel&${CS.ORDER_BY_KEY}=${CS.ORDER_BY_TOP}&${CS.FRESH_KEY}=${CS.FRESH_ALL}`, - options: { - orderBy: ['effective_amount'], - claimType: ['channel'], - }, - }; - - // const TRENDING_CLASSICS = { - // title: __('Trending Classics'), - // link: `/$/${PAGES.DISCOVER}?${CS.ORDER_BY_KEY}=${CS.ORDER_BY_TRENDING}&${CS.FRESH_KEY}=${CS.FRESH_WEEK}`, - // options: { - // pageSize: getPageSize(4), - // claimType: ['stream'], - // limitClaimsPerChannel: 1, - // releaseTime: `<${Math.floor( - // moment() - // .subtract(6, 'month') - // .startOf('day') - // .unix() - // )}`, - // }, - // }; - - // const TRENDING_ON_LBRY = { - // title: __('Trending On LBRY'), - // link: `/$/${PAGES.DISCOVER}`, - // options: { - // pageSize: showPersonalizedChannels || showPersonalizedTags ? 4 : 8, - // }, - // }; - - const TRENDING_FOR_TAGS = { - title: __('Trending For Your Tags'), - link: `/$/${PAGES.TAGS_FOLLOWING}`, - icon: ICONS.TAG, - - options: { - pageSize: getPageSize(4), - tags: followedTags.map((tag) => tag.name), - claimType: ['stream'], - limitClaimsPerChannel: 2, - }, - }; - - const LATEST_FROM_LBRY = { - title: __('Latest From @lbry'), - link: `/@lbry:3f`, - options: { - orderBy: ['release_time'], - pageSize: getPageSize(4), - channelIds: ['3fda836a92faaceedfe398225fb9b2ee2ed1f01a'], - }, - }; - - if (showPersonalizedChannels) rowData.push(RECENT_FROM_FOLLOWING); - if (showPersonalizedTags && !showIndividualTags) rowData.push(TRENDING_FOR_TAGS); - if (showPersonalizedTags && showIndividualTags) { - individualTagDataItems.forEach((item: RowDataItem) => { - rowData.push(item); - }); - } - - if (!authenticated) { - rowData.push(YOUTUBE_CREATOR_ROW); - } - - rowData.push(TOP_CONTENT_TODAY); - - // rowData.push(TRENDING_ON_LBRY); - - rowData.push(LATEST_FROM_LBRY); - - if (!showPersonalizedChannels) rowData.push(TOP_CHANNELS); - - return rowData; -} +module.exports = { YOUTUBERS }; diff --git a/homepages/index.js b/homepages/index.js index 2a89d99bb..53cf98a40 100644 --- a/homepages/index.js +++ b/homepages/index.js @@ -1,5 +1,3 @@ -import * as lbrytv from './homepage'; -// import all homepages -// export object of homepages -export default {'en': lbrytv}; -export const NO_ADS_CHANNEL_IDS = []; +module.exports = { + en: {}, +}; diff --git a/static/app-strings.json b/static/app-strings.json index fb8e5689e..e3849098f 100644 --- a/static/app-strings.json +++ b/static/app-strings.json @@ -1445,6 +1445,7 @@ "You loved this": "You loved this", "Creator loved this": "Creator loved this", "A channel is required to throw fire and slime": "A channel is required to throw fire and slime", + "The requested comment is no longer available.": "The requested comment is no longer available.", "Best": "Best", "Controversial": "Controversial", "Show Replies": "Show Replies", @@ -1513,6 +1514,7 @@ "Create A Channel": "Create A Channel", "At least 10 views are required to earn the reward, consume more!": "At least 10 views are required to earn the reward, consume more!", "Blocked %channel%": "Blocked %channel%", + "Comment(s) blocked.": "Comment(s) blocked.", "You earned %lbc% for streaming your first video.": "You earned %lbc% for streaming your first video.", "You earned %lbc% for successfully completing The Journey L4: Perfect Harmony.": "You earned %lbc% for successfully completing The Journey L4: Perfect Harmony.", "You earned %lbc% for successfully completing The Journey L3: Bliss.": "You earned %lbc% for successfully completing The Journey L3: Bliss.", @@ -2040,11 +2042,7 @@ "Tip Creators": "Tip Creators", "Only select creators can receive tips at this time": "Only select creators can receive tips at this time", "The payment will be made from your saved card": "The payment will be made from your saved card", - "Trending for #Art": "Trending for #Art", - "Trending for #Education": "Trending for #Education", - "Trending for #Technology": "Trending for #Technology", - "Enter a @username or URL": "Enter a @username or URL", - "examples: @channel, @channel#3, https://odysee.com/@Odysee:8, lbry://@Odysee#8": "examples: @channel, @channel#3, https://odysee.com/@Odysee:8, lbry://@Odysee#8", - "Moderators": "Moderators", + "A channel is required to comment on lbry.tv": "A channel is required to comment on lbry.tv", + "Commenting...": "Commenting...", "--end--": "--end--" } diff --git a/ui/comments.js b/ui/comments.js index 912c7266e..885d54715 100644 --- a/ui/comments.js +++ b/ui/comments.js @@ -17,6 +17,7 @@ const Comments = { comment_list: (params: CommentListParams) => fetchCommentsApi('comment.List', params), comment_abandon: (params: CommentAbandonParams) => fetchCommentsApi('comment.Abandon', params), comment_create: (params: CommentCreateParams) => fetchCommentsApi('comment.Create', params), + comment_by_id: (params: CommentByIdParams) => fetchCommentsApi('comment.ByID', params), setting_list: (params: SettingsParams) => fetchCommentsApi('setting.List', params), setting_block_word: (params: BlockWordParams) => fetchCommentsApi('setting.BlockWord', params), setting_unblock_word: (params: BlockWordParams) => fetchCommentsApi('setting.UnBlockWord', params), diff --git a/ui/component/channelDiscussion/index.js b/ui/component/channelDiscussion/index.js index cb0c52704..ff198701f 100644 --- a/ui/component/channelDiscussion/index.js +++ b/ui/component/channelDiscussion/index.js @@ -1,6 +1,5 @@ import { connect } from 'react-redux'; import { withRouter } from 'react-router'; -import { makeSelectCommentForCommentId } from 'redux/selectors/comments'; import { DISABLE_COMMENTS_TAG } from 'constants/tags'; import ChannelDiscussion from './view'; import { makeSelectTagInClaimOrChannelForUri } from 'lbry-redux'; @@ -8,10 +7,9 @@ import { makeSelectTagInClaimOrChannelForUri } from 'lbry-redux'; const select = (state, props) => { const { search } = props.location; const urlParams = new URLSearchParams(search); - const linkedCommentId = urlParams.get('lc'); return { - linkedComment: makeSelectCommentForCommentId(linkedCommentId)(state), + linkedCommentId: urlParams.get('lc'), commentsDisabled: makeSelectTagInClaimOrChannelForUri(props.uri, DISABLE_COMMENTS_TAG)(state), }; }; diff --git a/ui/component/channelDiscussion/view.jsx b/ui/component/channelDiscussion/view.jsx index 50a308a70..15b215f50 100644 --- a/ui/component/channelDiscussion/view.jsx +++ b/ui/component/channelDiscussion/view.jsx @@ -5,19 +5,19 @@ import Empty from 'component/common/empty'; type Props = { uri: string, - linkedComment: ?any, + linkedCommentId?: string, commentsDisabled: boolean, }; function ChannelDiscussion(props: Props) { - const { uri, linkedComment, commentsDisabled } = props; + const { uri, linkedCommentId, commentsDisabled } = props; if (commentsDisabled) { return ; } return (
- +
); } diff --git a/ui/component/claimMenuList/index.js b/ui/component/claimMenuList/index.js index ee8a70625..acf6b99d4 100644 --- a/ui/component/claimMenuList/index.js +++ b/ui/component/claimMenuList/index.js @@ -28,6 +28,7 @@ import { doToast } from 'redux/actions/notifications'; import { makeSelectSigningIsMine } from 'redux/selectors/content'; import { doChannelSubscribe, doChannelUnsubscribe } from 'redux/actions/subscriptions'; import { makeSelectIsSubscribed } from 'redux/selectors/subscriptions'; +import { selectUserVerifiedEmail } from 'redux/selectors/user'; import ClaimPreview from './view'; import fs from 'fs'; @@ -38,6 +39,7 @@ const select = (state, props) => { claim, claimIsMine: makeSelectSigningIsMine(props.uri)(state), hasClaimInWatchLater: makeSelectCollectionForIdHasClaimUrl(COLLECTIONS_CONSTS.WATCH_LATER_ID, permanentUri)(state), + hasClaimInCustom: makeSelectCollectionForIdHasClaimUrl(COLLECTIONS_CONSTS.FAVORITES_ID, permanentUri)(state), channelIsMuted: makeSelectChannelIsMuted(props.uri)(state), channelIsBlocked: makeSelectChannelIsBlocked(props.uri)(state), fileInfo: makeSelectFileInfoForUri(props.uri)(state), @@ -48,6 +50,7 @@ const select = (state, props) => { collectionName: makeSelectNameForCollectionId(props.collectionId)(state), isMyCollection: makeSelectCollectionIsMine(props.collectionId)(state), editedCollection: makeSelectEditedCollectionForId(props.collectionId)(state), + isAuthenticated: Boolean(selectUserVerifiedEmail(state)), }; }; diff --git a/ui/component/claimMenuList/view.jsx b/ui/component/claimMenuList/view.jsx index ce9e4de91..6d11581ed 100644 --- a/ui/component/claimMenuList/view.jsx +++ b/ui/component/claimMenuList/view.jsx @@ -40,6 +40,7 @@ type Props = { isRepost: boolean, doCollectionEdit: (string, any) => void, hasClaimInWatchLater: boolean, + hasClaimInCustom: boolean, claimInCollection: boolean, collectionName?: string, collectionId: string, @@ -53,6 +54,7 @@ type Props = { doChannelUnsubscribe: (SubscriptionArgs) => void, isChannelPage: boolean, editedCollection: Collection, + isAuthenticated: boolean, }; function ClaimMenuList(props: Props) { @@ -75,6 +77,7 @@ function ClaimMenuList(props: Props) { doCommentModUnBlockAsAdmin, doCollectionEdit, hasClaimInWatchLater, + hasClaimInCustom, collectionId, collectionName, isMyCollection, @@ -87,6 +90,7 @@ function ClaimMenuList(props: Props) { doChannelUnsubscribe, isChannelPage = false, editedCollection, + isAuthenticated, } = props; const repostedContent = claim && claim.reposted_claim; const contentClaim = repostedContent || claim; @@ -96,6 +100,7 @@ function ClaimMenuList(props: Props) { const isChannel = !incognitoClaim && signingChannel === claim; const showDelete = claimIsMine || (fileInfo && (fileInfo.written_bytes > 0 || fileInfo.blobs_completed > 0)); const subscriptionLabel = isSubscribed ? __('Unfollow') : __('Follow'); + const lastCollectionName = 'Favorites'; const { push, replace } = useHistory(); if (!claim) { @@ -219,154 +224,180 @@ function ClaimMenuList(props: Props) { - {/* WATCH LATER */} - <> - {isPlayable && !collectionId && ( - { - doToast({ - message: __('Item %action% Watch Later', { - action: hasClaimInWatchLater - ? __('removed from --[substring for "Item %action% Watch Later"]--') - : __('added to --[substring for "Item %action% Watch Later"]--'), - }), - }); - doCollectionEdit(COLLECTIONS_CONSTS.WATCH_LATER_ID, { - claims: [contentClaim], - remove: hasClaimInWatchLater, - type: 'playlist', - }); - }} - > -
- - {hasClaimInWatchLater ? __('In Watch Later') : __('Watch Later')} -
-
- )} - {/* COLLECTION OPERATIONS */} - {collectionId && collectionName && isCollectionClaim && ( + {(!IS_WEB || (IS_WEB && isAuthenticated)) && ( + <> <> - {Boolean(editedCollection) && ( + {/* WATCH LATER */} + {isPlayable && !collectionId && ( push(`/$/${PAGES.LIST}/${collectionId}?view=edit`)} + onSelect={() => { + doToast({ + message: __('Item %action% Watch Later', { + action: hasClaimInWatchLater + ? __('removed from --[substring for "Item %action% Watch Later"]--') + : __('added to --[substring for "Item %action% Watch Later"]--'), + }), + }); + doCollectionEdit(COLLECTIONS_CONSTS.WATCH_LATER_ID, { + claims: [contentClaim], + remove: hasClaimInWatchLater, + type: 'playlist', + }); + }} >
- - {__('Publish')} + + {hasClaimInWatchLater ? __('In Watch Later') : __('Watch Later')}
)} - push(`/$/${PAGES.LIST}/${collectionId}`)}> -
- - {__('View List')} -
-
- openModal(MODALS.COLLECTION_DELETE, { collectionId })} - > -
- - {__('Delete List')} -
-
- - )} - {/* CURRENTLY ONLY SUPPORT PLAYLISTS FOR PLAYABLE; LATER DIFFERENT TYPES */} - {isPlayable && ( - openModal(MODALS.COLLECTION_ADD, { uri, type: 'playlist' })} - > -
- - {__('Add to Lists')} -
-
- )} - - {!isChannelPage && ( - <> -
- -
- - {__('Support --[button to support a claim]--')} -
-
- - )} - - {!incognitoClaim && !isRepost && !claimIsMine && !isChannelPage && ( - <> -
- -
- - {subscriptionLabel} -
-
- - )} - {!isMyCollection && ( - <> - {(!claimIsMine || channelIsBlocked) && channelUri ? ( - !incognitoClaim && - !isRepost && ( + {/* CUSTOM LIST */} + {isPlayable && !collectionId && ( + { + doToast({ + message: __(`Item %action% ${lastCollectionName}`, { + action: hasClaimInCustom ? __('removed from') : __('added to'), + }), + }); + doCollectionEdit(COLLECTIONS_CONSTS.FAVORITES_ID, { + claims: [contentClaim], + remove: hasClaimInCustom, + type: 'playlist', + }); + }} + > +
+ + {hasClaimInCustom ? __(`In ${lastCollectionName}`) : __(`${lastCollectionName}`)} +
+
+ )} + {/* COLLECTION OPERATIONS */} + {collectionId && collectionName && isCollectionClaim && ( <> -
- -
- - {channelIsBlocked ? __('Unblock Channel') : __('Block Channel')} -
-
- - {isAdmin && ( - + {Boolean(editedCollection) && ( + push(`/$/${PAGES.LIST}/${collectionId}?view=edit`)} + >
- - {channelIsAdminBlocked ? __('Global Unblock Channel') : __('Global Block Channel')} + + {__('Publish')}
)} - - + push(`/$/${PAGES.LIST}/${collectionId}`)}>
- - {channelIsMuted ? __('Unmute Channel') : __('Mute Channel')} + + {__('View List')} +
+
+ openModal(MODALS.COLLECTION_DELETE, { collectionId })} + > +
+ + {__('Delete List')}
- ) - ) : ( + )} + {/* CURRENTLY ONLY SUPPORT PLAYLISTS FOR PLAYABLE; LATER DIFFERENT TYPES */} + {isPlayable && ( + openModal(MODALS.COLLECTION_ADD, { uri, type: 'playlist' })} + > +
+ + {__('Add to Lists')} +
+
+ )} + + {!isChannelPage && ( <> - {!isChannelPage && !isRepost && ( - -
- - {__('Edit')} -
-
- )} +
+ +
+ + {__('Support --[button to support a claim]--')} +
+
+ + )} - {showDelete && ( - -
- - {__('Delete')} -
-
+ {!incognitoClaim && !isRepost && !claimIsMine && !isChannelPage && ( + <> +
+ +
+ + {subscriptionLabel} +
+
+ + )} + {!isMyCollection && ( + <> + {(!claimIsMine || channelIsBlocked) && channelUri ? ( + !incognitoClaim && + !isRepost && ( + <> +
+ +
+ + {channelIsBlocked ? __('Unblock Channel') : __('Block Channel')} +
+
+ + {isAdmin && ( + +
+ + {channelIsAdminBlocked ? __('Global Unblock Channel') : __('Global Block Channel')} +
+
+ )} + + +
+ + {channelIsMuted ? __('Unmute Channel') : __('Mute Channel')} +
+
+ + ) + ) : ( + <> + {!isChannelPage && !isRepost && ( + +
+ + {__('Edit')} +
+
+ )} + + {showDelete && ( + +
+ + {__('Delete')} +
+
+ )} + )} )} )} -
{isChannelPage && IS_WEB && rssUrl && ( diff --git a/ui/component/claimPreview/view.jsx b/ui/component/claimPreview/view.jsx index f787ea852..4e04211b2 100644 --- a/ui/component/claimPreview/view.jsx +++ b/ui/component/claimPreview/view.jsx @@ -19,6 +19,7 @@ import ClaimPreviewTitle from 'component/claimPreviewTitle'; import ClaimPreviewSubtitle from 'component/claimPreviewSubtitle'; import ClaimRepostAuthor from 'component/claimRepostAuthor'; import FileDownloadLink from 'component/fileDownloadLink'; +import FileWatchLaterLink from 'component/fileWatchLaterLink'; import PublishPending from 'component/publishPending'; import ClaimMenuList from 'component/claimMenuList'; import ClaimPreviewLoading from './claim-preview-loading'; @@ -157,6 +158,15 @@ const ClaimPreview = forwardRef((props: Props, ref: any) => { isValid = false; } } + // $FlowFixMe + const isPlayable = + claim && + // $FlowFixMe + claim.value && + // $FlowFixMe + claim.value.stream_type && + // $FlowFixMe + (claim.value.stream_type === 'audio' || claim.value.stream_type === 'video'); const isCollection = claim && claim.value_type === 'collection'; const isChannelUri = isValid ? parseURI(uri).isChannel : false; const signingChannel = claim && claim.signing_channel; @@ -318,6 +328,11 @@ const ClaimPreview = forwardRef((props: Props, ref: any) => { )} + {isPlayable && ( +
+ +
+ )} ) : ( diff --git a/ui/component/claimPreviewTile/view.jsx b/ui/component/claimPreviewTile/view.jsx index fdf4c3a1a..6533dbba8 100644 --- a/ui/component/claimPreviewTile/view.jsx +++ b/ui/component/claimPreviewTile/view.jsx @@ -13,6 +13,7 @@ import { formatLbryUrlForWeb } from 'util/url'; import { parseURI, COLLECTIONS_CONSTS } from 'lbry-redux'; import PreviewOverlayProperties from 'component/previewOverlayProperties'; import FileDownloadLink from 'component/fileDownloadLink'; +import FileWatchLaterLink from 'component/fileWatchLaterLink'; import ClaimRepostAuthor from 'component/claimRepostAuthor'; import ClaimMenuList from 'component/claimMenuList'; import CollectionPreviewOverlay from 'component/collectionPreviewOverlay'; @@ -75,6 +76,15 @@ function ClaimPreviewTile(props: Props) { const isRepost = claim && claim.repost_channel_url; const isCollection = claim && claim.value_type === 'collection'; const isStream = claim && claim.value_type === 'stream'; + // $FlowFixMe + const isPlayable = + claim && + // $FlowFixMe + claim.value && + // $FlowFixMe + claim.value.stream_type && + // $FlowFixMe + (claim.value.stream_type === 'audio' || claim.value.stream_type === 'video'); const collectionClaimId = isCollection && claim && claim.claim_id; const shouldFetch = claim === undefined; const thumbnailUrl = useGetThumbnail(uri, claim, streamingUrl, getFile, placeholder) || thumbnail; @@ -195,6 +205,12 @@ function ClaimPreviewTile(props: Props) { )} {/* @endif */} + {isPlayable && ( +
+ +
+ )} +
diff --git a/ui/component/collectionContentSidebar/view.jsx b/ui/component/collectionContentSidebar/view.jsx index 7bdc7eeec..49d932100 100644 --- a/ui/component/collectionContentSidebar/view.jsx +++ b/ui/component/collectionContentSidebar/view.jsx @@ -6,6 +6,7 @@ import Button from 'component/button'; import * as PAGES from 'constants/pages'; import Icon from 'component/common/icon'; import * as ICONS from 'constants/icons'; +import { COLLECTIONS_CONSTS } from 'lbry-redux'; type Props = { collectionUrls: Array, @@ -26,7 +27,10 @@ export default function CollectionContent(props: Props) { className="file-page__recommended" title={ - + {collectionName} } diff --git a/ui/component/collectionSelectItem/view.jsx b/ui/component/collectionSelectItem/view.jsx index 14aab0079..267d0f2f3 100644 --- a/ui/component/collectionSelectItem/view.jsx +++ b/ui/component/collectionSelectItem/view.jsx @@ -25,7 +25,7 @@ function CollectionSelectItem(props: Props) { let icon; switch (category) { case 'builtin': - icon = id === COLLECTIONS_CONSTS.WATCH_LATER_ID ? ICONS.TIME : ICONS.STACK; + icon = (id === COLLECTIONS_CONSTS.WATCH_LATER_ID && ICONS.TIME) || (id === COLLECTIONS_CONSTS.FAVORITES_ID && ICONS.STAR) || ICONS.STACK; break; case 'published': icon = ICONS.STACK; diff --git a/ui/component/collectionsListMine/view.jsx b/ui/component/collectionsListMine/view.jsx index 324cee079..45b3b6c96 100644 --- a/ui/component/collectionsListMine/view.jsx +++ b/ui/component/collectionsListMine/view.jsx @@ -68,7 +68,9 @@ export default function CollectionsListMine(props: Props) { {__(`${list.name}`)}
- + {itemUrls.length}
diff --git a/ui/component/comment/index.js b/ui/component/comment/index.js index 0e92f94f5..9e4ed5825 100644 --- a/ui/component/comment/index.js +++ b/ui/component/comment/index.js @@ -1,30 +1,43 @@ import { connect } from 'react-redux'; -import { makeSelectStakedLevelForChannelUri, makeSelectClaimForUri, makeSelectThumbnailForUri, selectMyChannelClaims } from 'lbry-redux'; -import { doCommentUpdate } from 'redux/actions/comments'; +import { + makeSelectStakedLevelForChannelUri, + makeSelectClaimForUri, + makeSelectThumbnailForUri, + selectMyChannelClaims, +} from 'lbry-redux'; +import { doCommentUpdate, doCommentList } from 'redux/actions/comments'; import { makeSelectChannelIsMuted } from 'redux/selectors/blocked'; import { doToast } from 'redux/actions/notifications'; import { doSetPlayingUri } from 'redux/actions/content'; import { selectUserVerifiedEmail } from 'redux/selectors/user'; -import { makeSelectOthersReactionsForComment } from 'redux/selectors/comments'; -import { selectActiveChannelClaim } from 'redux/selectors/app'; +import { selectLinkedCommentAncestors, makeSelectOthersReactionsForComment } from 'redux/selectors/comments'; +import { selectActiveChannelId, selectActiveChannelClaim } from 'redux/selectors/app'; import { selectPlayingUri } from 'redux/selectors/content'; import Comment from './view'; -const select = (state, props) => ({ - claim: makeSelectClaimForUri(props.uri)(state), - thumbnail: props.authorUri && makeSelectThumbnailForUri(props.authorUri)(state), - channelIsBlocked: props.authorUri && makeSelectChannelIsMuted(props.authorUri)(state), - commentingEnabled: IS_WEB ? Boolean(selectUserVerifiedEmail(state)) : true, - othersReacts: makeSelectOthersReactionsForComment(props.commentId)(state), - activeChannelClaim: selectActiveChannelClaim(state), - myChannels: selectMyChannelClaims(state), - playingUri: selectPlayingUri(state), - stakedLevel: makeSelectStakedLevelForChannelUri(props.authorUri)(state), -}); +const select = (state, props) => { + const activeChannelId = selectActiveChannelId(state); + const reactionKey = activeChannelId ? `${props.commentId}:${activeChannelId}` : props.commentId; + + return { + claim: makeSelectClaimForUri(props.uri)(state), + 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), + activeChannelClaim: selectActiveChannelClaim(state), + myChannels: selectMyChannelClaims(state), + playingUri: selectPlayingUri(state), + stakedLevel: makeSelectStakedLevelForChannelUri(props.authorUri)(state), + linkedCommentAncestors: selectLinkedCommentAncestors(state), + }; +}; const perform = (dispatch) => ({ clearPlayingUri: () => dispatch(doSetPlayingUri({ uri: null })), updateComment: (commentId, comment) => dispatch(doCommentUpdate(commentId, comment)), + fetchReplies: (uri, parentId, page, pageSize, sortBy) => + dispatch(doCommentList(uri, parentId, page, pageSize, sortBy)), doToast: (options) => dispatch(doToast(options)), }); diff --git a/ui/component/comment/view.jsx b/ui/component/comment/view.jsx index 421e4f7b4..19afc945d 100644 --- a/ui/component/comment/view.jsx +++ b/ui/component/comment/view.jsx @@ -1,6 +1,7 @@ // @flow import * as ICONS from 'constants/icons'; import * as PAGES from 'constants/pages'; +import { SORT_BY, COMMENT_PAGE_SIZE_REPLIES } from 'constants/comment'; import { FF_MAX_CHARS_IN_COMMENT } from 'constants/form-field'; import { SITE_NAME, SIMPLE_SITE, ENABLE_COMMENT_REACTIONS } from 'config'; import React, { useEffect, useState } from 'react'; @@ -23,6 +24,8 @@ import CommentMenuList from 'component/commentMenuList'; import UriIndicator from 'component/uriIndicator'; import CreditAmount from 'component/common/credit-amount'; +const AUTO_EXPAND_ALL_REPLIES = false; + type Props = { clearPlayingUri: () => void, uri: string, @@ -36,8 +39,10 @@ type Props = { claimIsMine: boolean, // if you control the claim which this comment was posted on commentIsMine: boolean, // if this comment was signed by an owned channel updateComment: (string, string) => void, + fetchReplies: (string, string, number, number, number) => void, commentModBlock: (string) => void, - linkedComment?: any, + linkedCommentId?: string, + linkedCommentAncestors: { [string]: Array }, myChannels: ?Array, commentingEnabled: boolean, doToast: ({ message: string }) => void, @@ -53,6 +58,7 @@ type Props = { playingUri: ?PlayingUri, stakedLevel: number, supportAmount: number, + numDirectReplies: number, }; const LENGTH_TO_COLLAPSE = 300; @@ -71,7 +77,9 @@ function Comment(props: Props) { commentIsMine, commentId, updateComment, - linkedComment, + fetchReplies, + linkedCommentId, + linkedCommentAncestors, commentingEnabled, myChannels, doToast, @@ -82,18 +90,23 @@ function Comment(props: Props) { playingUri, stakedLevel, supportAmount, + numDirectReplies, } = props; + const { push, replace, location: { pathname, search }, } = useHistory(); + const [isReplying, setReplying] = React.useState(false); const [isEditing, setEditing] = useState(false); const [editedMessage, setCommentValue] = useState(message); const [charCount, setCharCount] = useState(editedMessage.length); // used for controlling the visibility of the menu icon const [mouseIsHovering, setMouseHover] = useState(false); + const [showReplies, setShowReplies] = useState(false); + const [page, setPage] = useState(0); const [advancedEditor] = usePersistedState('comment-editor-mode', false); const [displayDeadComment, setDisplayDeadComment] = React.useState(false); const hasChannels = myChannels && myChannels.length > 0; @@ -111,6 +124,19 @@ function Comment(props: Props) { } } catch (e) {} + // Auto-expand (limited to linked-comments for now, but can be for all) + useEffect(() => { + const isInLinkedCommentChain = + linkedCommentId && + linkedCommentAncestors[linkedCommentId] && + linkedCommentAncestors[linkedCommentId].includes(commentId); + + if (isInLinkedCommentChain || AUTO_EXPAND_ALL_REPLIES) { + setShowReplies(true); + setPage(1); + } + }, []); // eslint-disable-line react-hooks/exhaustive-deps + useEffect(() => { if (isEditing) { setCharCount(editedMessage.length); @@ -131,6 +157,12 @@ function Comment(props: Props) { } }, [author, authorUri, editedMessage, isEditing, setEditing]); + useEffect(() => { + if (page > 0) { + fetchReplies(uri, commentId, page, COMMENT_PAGE_SIZE_REPLIES, SORT_BY.OLDEST); + } + }, [page, uri, commentId, fetchReplies]); + function handleEditMessageChanged(event) { setCommentValue(!SIMPLE_SITE && advancedEditor ? event : event.target.value); } @@ -176,7 +208,7 @@ function Comment(props: Props) { >
@@ -302,13 +334,43 @@ function Comment(props: Props) { {ENABLE_COMMENT_REACTIONS && }
+ {numDirectReplies > 0 && !showReplies && ( +
+
+ )} + + {numDirectReplies > 0 && showReplies && ( +
+
+ )} + {isReplying && ( setReplying(false)} - onCancelReplying={() => setReplying(false)} + onDoneReplying={() => { + setShowReplies(true); + setReplying(false); + }} + onCancelReplying={() => { + setReplying(false); + }} /> )} @@ -317,7 +379,16 @@ function Comment(props: Props) { - + {showReplies && ( + setPage(page + 1)} + /> + )} ); } diff --git a/ui/component/commentCreate/view.jsx b/ui/component/commentCreate/view.jsx index e683d9b20..4c32eca47 100644 --- a/ui/component/commentCreate/view.jsx +++ b/ui/component/commentCreate/view.jsx @@ -35,7 +35,6 @@ type Props = { toast: (string) => void, claimIsMine: boolean, sendTip: ({}, (any) => void, (any) => void) => void, - justCommented: Array, }; export function CommentCreate(props: Props) { @@ -54,7 +53,6 @@ export function CommentCreate(props: Props) { livestream, claimIsMine, sendTip, - justCommented, } = props; const buttonref: ElementRef = React.useRef(); const { @@ -153,7 +151,6 @@ export function CommentCreate(props: Props) { setIsReviewingSupportComment(false); setIsSupportComment(false); setCommentFailure(false); - justCommented.push(res.comment_id); if (onDoneReplying) { onDoneReplying(); @@ -217,7 +214,13 @@ export function CommentCreate(props: Props) { autoFocus button="primary" disabled={disabled} - label={isSubmitting ? __('Sending...') : (commentFailure && tipAmount === successTip.tipAmount) ? __('Re-submit') : __('Send')} + label={ + isSubmitting + ? __('Sending...') + : commentFailure && tipAmount === successTip.tipAmount + ? __('Re-submit') + : __('Send') + } onClick={handleSupportComment} />