Merge branch 'master' into accessibility
This commit is contained in:
commit
2f67ca370e
61 changed files with 1785 additions and 793 deletions
|
@ -52,6 +52,7 @@ const config = {
|
||||||
STRIPE_PUBLIC_KEY: process.env.STRIPE_PUBLIC_KEY,
|
STRIPE_PUBLIC_KEY: process.env.STRIPE_PUBLIC_KEY,
|
||||||
ENABLE_UI_NOTIFICATIONS: process.env.ENABLE_UI_NOTIFICATIONS === 'true',
|
ENABLE_UI_NOTIFICATIONS: process.env.ENABLE_UI_NOTIFICATIONS === 'true',
|
||||||
ENABLE_MATURE: process.env.ENABLE_MATURE === 'true',
|
ENABLE_MATURE: process.env.ENABLE_MATURE === 'true',
|
||||||
|
CUSTOM_HOMEPAGE: process.env.CUSTOM_HOMEPAGE === 'true',
|
||||||
};
|
};
|
||||||
|
|
||||||
config.URL_LOCAL = `http://localhost:${config.WEB_SERVER_PORT}`;
|
config.URL_LOCAL = `http://localhost:${config.WEB_SERVER_PORT}`;
|
||||||
|
|
66
flow-typed/Comment.js
vendored
66
flow-typed/Comment.js
vendored
|
@ -13,6 +13,7 @@ declare type Comment = {
|
||||||
parent_id?: number, // comment_id of comment this is in reply to
|
parent_id?: number, // comment_id of comment this is in reply to
|
||||||
is_pinned: boolean,
|
is_pinned: boolean,
|
||||||
support_amount: number,
|
support_amount: number,
|
||||||
|
replies: number, // number of direct replies (i.e. excluding nested replies).
|
||||||
};
|
};
|
||||||
|
|
||||||
declare type PerChannelSettings = {
|
declare type PerChannelSettings = {
|
||||||
|
@ -27,15 +28,21 @@ declare type PerChannelSettings = {
|
||||||
declare type CommentsState = {
|
declare type CommentsState = {
|
||||||
commentsByUri: { [string]: string },
|
commentsByUri: { [string]: string },
|
||||||
superChatsByUri: { [string]: { totalAmount: number, comments: Array<Comment> } },
|
superChatsByUri: { [string]: { totalAmount: number, comments: Array<Comment> } },
|
||||||
byId: { [string]: Array<string> },
|
byId: { [string]: Array<string> }, // ClaimID -> list of fetched comment IDs.
|
||||||
repliesByParentId: { [string]: Array<string> }, // ParentCommentID -> list of reply comments
|
totalCommentsById: {}, // ClaimId -> ultimate total (including replies) in commentron.
|
||||||
topLevelCommentsById: { [string]: Array<string> }, // ClaimID -> list of top level comments
|
repliesByParentId: { [string]: Array<string> }, // ParentCommentID -> list of fetched replies.
|
||||||
|
totalRepliesByParentId: {}, // ParentCommentID -> total replies in commentron.
|
||||||
|
topLevelCommentsById: { [string]: Array<string> }, // 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 },
|
commentById: { [string]: Comment },
|
||||||
|
linkedCommentAncestors: { [string]: Array<string> }, // {"linkedCommentId": ["parentId", "grandParentId", ...]}
|
||||||
isLoading: boolean,
|
isLoading: boolean,
|
||||||
|
isLoadingByParentId: { [string]: boolean },
|
||||||
myComments: ?Set<string>,
|
myComments: ?Set<string>,
|
||||||
isFetchingReacts: boolean,
|
isFetchingReacts: boolean,
|
||||||
myReactsByCommentId: any,
|
myReactsByCommentId: ?{ [string]: Array<string> }, // {"CommentId:MyChannelId": ["like", "dislike", ...]}
|
||||||
othersReactsByCommentId: any,
|
othersReactsByCommentId: ?{ [string]: { [string]: number } }, // {"CommentId:MyChannelId": {"like": 2, "dislike": 2, ...}}
|
||||||
pendingCommentReactions: Array<string>,
|
pendingCommentReactions: Array<string>,
|
||||||
moderationBlockList: ?Array<string>, // @KP rename to "personalBlockList"?
|
moderationBlockList: ?Array<string>, // @KP rename to "personalBlockList"?
|
||||||
adminBlockList: ?Array<string>,
|
adminBlockList: ?Array<string>,
|
||||||
|
@ -64,17 +71,48 @@ declare type CommentReactParams = {
|
||||||
remove?: boolean,
|
remove?: boolean,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
declare type CommentReactListParams = {
|
||||||
|
comment_ids?: string,
|
||||||
|
channel_id?: string,
|
||||||
|
channel_name?: string,
|
||||||
|
wallet_id?: string,
|
||||||
|
react_types?: string,
|
||||||
|
};
|
||||||
|
|
||||||
declare type CommentListParams = {
|
declare type CommentListParams = {
|
||||||
page: number,
|
page: number, // pagination: which page of results
|
||||||
page_size: number,
|
page_size: number, // pagination: nr of comments to show in a page (max 200)
|
||||||
claim_id: string,
|
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 = {
|
declare type CommentListResponse = {
|
||||||
items: Array<Comment>,
|
items: Array<Comment>,
|
||||||
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<Comment>,
|
||||||
|
}
|
||||||
|
|
||||||
declare type CommentAbandonParams = {
|
declare type CommentAbandonParams = {
|
||||||
comment_id: string,
|
comment_id: string,
|
||||||
creator_channel_id?: string,
|
creator_channel_id?: string,
|
||||||
|
@ -94,6 +132,16 @@ declare type CommentCreateParams = {
|
||||||
|
|
||||||
declare type SuperListParams = {};
|
declare type SuperListParams = {};
|
||||||
|
|
||||||
|
declare type SuperListResponse = {
|
||||||
|
page: number,
|
||||||
|
page_size: number,
|
||||||
|
total_pages: number,
|
||||||
|
total_items: number,
|
||||||
|
total_amount: number,
|
||||||
|
items: Array<Comment>,
|
||||||
|
has_hidden_comments: boolean,
|
||||||
|
};
|
||||||
|
|
||||||
declare type ModerationBlockParams = {};
|
declare type ModerationBlockParams = {};
|
||||||
|
|
||||||
declare type ModerationAddDelegateParams = {
|
declare type ModerationAddDelegateParams = {
|
||||||
|
|
2
flow-typed/homepage.js
vendored
2
flow-typed/homepage.js
vendored
|
@ -20,7 +20,7 @@ declare type RowDataItem = {
|
||||||
options?: {
|
options?: {
|
||||||
channelIds?: Array<string>,
|
channelIds?: Array<string>,
|
||||||
limitClaimsPerChannel?: number,
|
limitClaimsPerChannel?: number,
|
||||||
pageSize: number,
|
pageSize?: number,
|
||||||
},
|
},
|
||||||
route?: string,
|
route?: string,
|
||||||
hideForUnauth?: boolean,
|
hideForUnauth?: boolean,
|
||||||
|
|
|
@ -1,38 +1,7 @@
|
||||||
// @flow
|
import * as PAGES from '../ui/constants/pages';
|
||||||
import * as PAGES from 'constants/pages';
|
import * as CS from '../ui/constants/claim_search';
|
||||||
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';
|
|
||||||
|
|
||||||
export type RowDataItem = {
|
const YOUTUBER_CHANNEL_IDS = [
|
||||||
title: string,
|
|
||||||
link?: string,
|
|
||||||
help?: any,
|
|
||||||
options?: {},
|
|
||||||
icon?: string,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function GetHomePageRowData(
|
|
||||||
authenticated: boolean,
|
|
||||||
showPersonalizedChannels: boolean,
|
|
||||||
showPersonalizedTags: boolean,
|
|
||||||
subscribedChannels: Array<Subscription>,
|
|
||||||
followedTags: Array<Tag>,
|
|
||||||
showIndividualTags: boolean,
|
|
||||||
showNsfw: boolean
|
|
||||||
) {
|
|
||||||
const isLargeScreen = useIsLargeScreen();
|
|
||||||
|
|
||||||
function getPageSize(originalSize) {
|
|
||||||
return isLargeScreen ? originalSize * (3 / 2) : originalSize;
|
|
||||||
}
|
|
||||||
|
|
||||||
let rowData: Array<RowDataItem> = [];
|
|
||||||
const individualTagDataItems: Array<RowDataItem> = [];
|
|
||||||
const YOUTUBER_CHANNEL_IDS = [
|
|
||||||
'fb364ef587872515f545a5b4b3182b58073f230f',
|
'fb364ef587872515f545a5b4b3182b58073f230f',
|
||||||
'589276465a23c589801d874f484cc39f307d7ec7',
|
'589276465a23c589801d874f484cc39f307d7ec7',
|
||||||
'ba79c80788a9e1751e49ad401f5692d86f73a2db',
|
'ba79c80788a9e1751e49ad401f5692d86f73a2db',
|
||||||
|
@ -116,142 +85,18 @@ export default function GetHomePageRowData(
|
||||||
'e8f68563d242f6ac9784dcbc41dd86c28a9391d6',
|
'e8f68563d242f6ac9784dcbc41dd86c28a9391d6',
|
||||||
];
|
];
|
||||||
|
|
||||||
const YOUTUBE_CREATOR_ROW = {
|
const YOUTUBERS = {
|
||||||
title: __('CableTube Escape Artists'),
|
ids: YOUTUBER_CHANNEL_IDS,
|
||||||
link: `/$/${PAGES.DISCOVER}?${CS.CLAIM_TYPE}=${CS.CLAIM_STREAM}&${CS.CHANNEL_IDS_KEY}=${YOUTUBER_CHANNEL_IDS.join(
|
link: `/$/${PAGES.DISCOVER}?${CS.CLAIM_TYPE}=${CS.CLAIM_STREAM}&${CS.CHANNEL_IDS_KEY}=${YOUTUBER_CHANNEL_IDS.join(
|
||||||
','
|
','
|
||||||
)}`,
|
)}`,
|
||||||
options: {
|
name: 'general',
|
||||||
claimType: ['stream'],
|
label: 'CableTube Escape Artists',
|
||||||
orderBy: ['release_time'],
|
channelLimit: 1,
|
||||||
pageSize: getPageSize(12),
|
daysOfContent: 30,
|
||||||
channelIds: YOUTUBER_CHANNEL_IDS,
|
pageSize: 24,
|
||||||
limitClaimsPerChannel: 1,
|
//pinnedUrls: [],
|
||||||
releaseTime: `>${Math.floor(moment().subtract(1, 'months').startOf('week').unix())}`,
|
//mixIn: [],
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (followedTags.length) {
|
module.exports = { YOUTUBERS };
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
import * as lbrytv from './homepage';
|
module.exports = {
|
||||||
// import all homepages
|
en: {},
|
||||||
// export object of homepages
|
};
|
||||||
export default {'en': lbrytv};
|
|
||||||
export const NO_ADS_CHANNEL_IDS = [];
|
|
||||||
|
|
|
@ -1445,6 +1445,7 @@
|
||||||
"You loved this": "You loved this",
|
"You loved this": "You loved this",
|
||||||
"Creator loved this": "Creator 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",
|
"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",
|
"Best": "Best",
|
||||||
"Controversial": "Controversial",
|
"Controversial": "Controversial",
|
||||||
"Show Replies": "Show Replies",
|
"Show Replies": "Show Replies",
|
||||||
|
@ -1513,6 +1514,7 @@
|
||||||
"Create A Channel": "Create A Channel",
|
"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!",
|
"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%",
|
"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 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 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.",
|
"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",
|
"Tip Creators": "Tip Creators",
|
||||||
"Only select creators can receive tips at this time": "Only select creators can receive tips at this time",
|
"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",
|
"The payment will be made from your saved card": "The payment will be made from your saved card",
|
||||||
"Trending for #Art": "Trending for #Art",
|
"A channel is required to comment on lbry.tv": "A channel is required to comment on lbry.tv",
|
||||||
"Trending for #Education": "Trending for #Education",
|
"Commenting...": "Commenting...",
|
||||||
"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",
|
|
||||||
"--end--": "--end--"
|
"--end--": "--end--"
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,6 +17,7 @@ const Comments = {
|
||||||
comment_list: (params: CommentListParams) => fetchCommentsApi('comment.List', params),
|
comment_list: (params: CommentListParams) => fetchCommentsApi('comment.List', params),
|
||||||
comment_abandon: (params: CommentAbandonParams) => fetchCommentsApi('comment.Abandon', params),
|
comment_abandon: (params: CommentAbandonParams) => fetchCommentsApi('comment.Abandon', params),
|
||||||
comment_create: (params: CommentCreateParams) => fetchCommentsApi('comment.Create', 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_list: (params: SettingsParams) => fetchCommentsApi('setting.List', params),
|
||||||
setting_block_word: (params: BlockWordParams) => fetchCommentsApi('setting.BlockWord', params),
|
setting_block_word: (params: BlockWordParams) => fetchCommentsApi('setting.BlockWord', params),
|
||||||
setting_unblock_word: (params: BlockWordParams) => fetchCommentsApi('setting.UnBlockWord', params),
|
setting_unblock_word: (params: BlockWordParams) => fetchCommentsApi('setting.UnBlockWord', params),
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { withRouter } from 'react-router';
|
import { withRouter } from 'react-router';
|
||||||
import { makeSelectCommentForCommentId } from 'redux/selectors/comments';
|
|
||||||
import { DISABLE_COMMENTS_TAG } from 'constants/tags';
|
import { DISABLE_COMMENTS_TAG } from 'constants/tags';
|
||||||
import ChannelDiscussion from './view';
|
import ChannelDiscussion from './view';
|
||||||
import { makeSelectTagInClaimOrChannelForUri } from 'lbry-redux';
|
import { makeSelectTagInClaimOrChannelForUri } from 'lbry-redux';
|
||||||
|
@ -8,10 +7,9 @@ import { makeSelectTagInClaimOrChannelForUri } from 'lbry-redux';
|
||||||
const select = (state, props) => {
|
const select = (state, props) => {
|
||||||
const { search } = props.location;
|
const { search } = props.location;
|
||||||
const urlParams = new URLSearchParams(search);
|
const urlParams = new URLSearchParams(search);
|
||||||
const linkedCommentId = urlParams.get('lc');
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
linkedComment: makeSelectCommentForCommentId(linkedCommentId)(state),
|
linkedCommentId: urlParams.get('lc'),
|
||||||
commentsDisabled: makeSelectTagInClaimOrChannelForUri(props.uri, DISABLE_COMMENTS_TAG)(state),
|
commentsDisabled: makeSelectTagInClaimOrChannelForUri(props.uri, DISABLE_COMMENTS_TAG)(state),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -5,19 +5,19 @@ import Empty from 'component/common/empty';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
uri: string,
|
uri: string,
|
||||||
linkedComment: ?any,
|
linkedCommentId?: string,
|
||||||
commentsDisabled: boolean,
|
commentsDisabled: boolean,
|
||||||
};
|
};
|
||||||
|
|
||||||
function ChannelDiscussion(props: Props) {
|
function ChannelDiscussion(props: Props) {
|
||||||
const { uri, linkedComment, commentsDisabled } = props;
|
const { uri, linkedCommentId, commentsDisabled } = props;
|
||||||
|
|
||||||
if (commentsDisabled) {
|
if (commentsDisabled) {
|
||||||
return <Empty text={__('This channel has disabled comments on their page.')} />;
|
return <Empty text={__('This channel has disabled comments on their page.')} />;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<section className="section">
|
<section className="section">
|
||||||
<CommentsList uri={uri} linkedComment={linkedComment} />
|
<CommentsList uri={uri} linkedCommentId={linkedCommentId} />
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,6 +28,7 @@ import { doToast } from 'redux/actions/notifications';
|
||||||
import { makeSelectSigningIsMine } from 'redux/selectors/content';
|
import { makeSelectSigningIsMine } from 'redux/selectors/content';
|
||||||
import { doChannelSubscribe, doChannelUnsubscribe } from 'redux/actions/subscriptions';
|
import { doChannelSubscribe, doChannelUnsubscribe } from 'redux/actions/subscriptions';
|
||||||
import { makeSelectIsSubscribed } from 'redux/selectors/subscriptions';
|
import { makeSelectIsSubscribed } from 'redux/selectors/subscriptions';
|
||||||
|
import { selectUserVerifiedEmail } from 'redux/selectors/user';
|
||||||
import ClaimPreview from './view';
|
import ClaimPreview from './view';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
|
|
||||||
|
@ -38,6 +39,7 @@ const select = (state, props) => {
|
||||||
claim,
|
claim,
|
||||||
claimIsMine: makeSelectSigningIsMine(props.uri)(state),
|
claimIsMine: makeSelectSigningIsMine(props.uri)(state),
|
||||||
hasClaimInWatchLater: makeSelectCollectionForIdHasClaimUrl(COLLECTIONS_CONSTS.WATCH_LATER_ID, permanentUri)(state),
|
hasClaimInWatchLater: makeSelectCollectionForIdHasClaimUrl(COLLECTIONS_CONSTS.WATCH_LATER_ID, permanentUri)(state),
|
||||||
|
hasClaimInCustom: makeSelectCollectionForIdHasClaimUrl(COLLECTIONS_CONSTS.FAVORITES_ID, permanentUri)(state),
|
||||||
channelIsMuted: makeSelectChannelIsMuted(props.uri)(state),
|
channelIsMuted: makeSelectChannelIsMuted(props.uri)(state),
|
||||||
channelIsBlocked: makeSelectChannelIsBlocked(props.uri)(state),
|
channelIsBlocked: makeSelectChannelIsBlocked(props.uri)(state),
|
||||||
fileInfo: makeSelectFileInfoForUri(props.uri)(state),
|
fileInfo: makeSelectFileInfoForUri(props.uri)(state),
|
||||||
|
@ -48,6 +50,7 @@ const select = (state, props) => {
|
||||||
collectionName: makeSelectNameForCollectionId(props.collectionId)(state),
|
collectionName: makeSelectNameForCollectionId(props.collectionId)(state),
|
||||||
isMyCollection: makeSelectCollectionIsMine(props.collectionId)(state),
|
isMyCollection: makeSelectCollectionIsMine(props.collectionId)(state),
|
||||||
editedCollection: makeSelectEditedCollectionForId(props.collectionId)(state),
|
editedCollection: makeSelectEditedCollectionForId(props.collectionId)(state),
|
||||||
|
isAuthenticated: Boolean(selectUserVerifiedEmail(state)),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -40,6 +40,7 @@ type Props = {
|
||||||
isRepost: boolean,
|
isRepost: boolean,
|
||||||
doCollectionEdit: (string, any) => void,
|
doCollectionEdit: (string, any) => void,
|
||||||
hasClaimInWatchLater: boolean,
|
hasClaimInWatchLater: boolean,
|
||||||
|
hasClaimInCustom: boolean,
|
||||||
claimInCollection: boolean,
|
claimInCollection: boolean,
|
||||||
collectionName?: string,
|
collectionName?: string,
|
||||||
collectionId: string,
|
collectionId: string,
|
||||||
|
@ -53,6 +54,7 @@ type Props = {
|
||||||
doChannelUnsubscribe: (SubscriptionArgs) => void,
|
doChannelUnsubscribe: (SubscriptionArgs) => void,
|
||||||
isChannelPage: boolean,
|
isChannelPage: boolean,
|
||||||
editedCollection: Collection,
|
editedCollection: Collection,
|
||||||
|
isAuthenticated: boolean,
|
||||||
};
|
};
|
||||||
|
|
||||||
function ClaimMenuList(props: Props) {
|
function ClaimMenuList(props: Props) {
|
||||||
|
@ -75,6 +77,7 @@ function ClaimMenuList(props: Props) {
|
||||||
doCommentModUnBlockAsAdmin,
|
doCommentModUnBlockAsAdmin,
|
||||||
doCollectionEdit,
|
doCollectionEdit,
|
||||||
hasClaimInWatchLater,
|
hasClaimInWatchLater,
|
||||||
|
hasClaimInCustom,
|
||||||
collectionId,
|
collectionId,
|
||||||
collectionName,
|
collectionName,
|
||||||
isMyCollection,
|
isMyCollection,
|
||||||
|
@ -87,6 +90,7 @@ function ClaimMenuList(props: Props) {
|
||||||
doChannelUnsubscribe,
|
doChannelUnsubscribe,
|
||||||
isChannelPage = false,
|
isChannelPage = false,
|
||||||
editedCollection,
|
editedCollection,
|
||||||
|
isAuthenticated,
|
||||||
} = props;
|
} = props;
|
||||||
const repostedContent = claim && claim.reposted_claim;
|
const repostedContent = claim && claim.reposted_claim;
|
||||||
const contentClaim = repostedContent || claim;
|
const contentClaim = repostedContent || claim;
|
||||||
|
@ -96,6 +100,7 @@ function ClaimMenuList(props: Props) {
|
||||||
const isChannel = !incognitoClaim && signingChannel === claim;
|
const isChannel = !incognitoClaim && signingChannel === claim;
|
||||||
const showDelete = claimIsMine || (fileInfo && (fileInfo.written_bytes > 0 || fileInfo.blobs_completed > 0));
|
const showDelete = claimIsMine || (fileInfo && (fileInfo.written_bytes > 0 || fileInfo.blobs_completed > 0));
|
||||||
const subscriptionLabel = isSubscribed ? __('Unfollow') : __('Follow');
|
const subscriptionLabel = isSubscribed ? __('Unfollow') : __('Follow');
|
||||||
|
const lastCollectionName = 'Favorites';
|
||||||
|
|
||||||
const { push, replace } = useHistory();
|
const { push, replace } = useHistory();
|
||||||
if (!claim) {
|
if (!claim) {
|
||||||
|
@ -219,154 +224,180 @@ function ClaimMenuList(props: Props) {
|
||||||
<Icon size={20} icon={ICONS.MORE_VERTICAL} />
|
<Icon size={20} icon={ICONS.MORE_VERTICAL} />
|
||||||
</MenuButton>
|
</MenuButton>
|
||||||
<MenuList className="menu__list">
|
<MenuList className="menu__list">
|
||||||
{/* WATCH LATER */}
|
{(!IS_WEB || (IS_WEB && isAuthenticated)) && (
|
||||||
<>
|
<>
|
||||||
{isPlayable && !collectionId && (
|
|
||||||
<MenuItem
|
|
||||||
className="comment__menu-option"
|
|
||||||
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',
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className="menu__link">
|
|
||||||
<Icon aria-hidden icon={hasClaimInWatchLater ? ICONS.DELETE : ICONS.TIME} />
|
|
||||||
{hasClaimInWatchLater ? __('In Watch Later') : __('Watch Later')}
|
|
||||||
</div>
|
|
||||||
</MenuItem>
|
|
||||||
)}
|
|
||||||
{/* COLLECTION OPERATIONS */}
|
|
||||||
{collectionId && collectionName && isCollectionClaim && (
|
|
||||||
<>
|
<>
|
||||||
{Boolean(editedCollection) && (
|
{/* WATCH LATER */}
|
||||||
|
{isPlayable && !collectionId && (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
className="comment__menu-option"
|
className="comment__menu-option"
|
||||||
onSelect={() => 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',
|
||||||
|
});
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<div className="menu__link">
|
<div className="menu__link">
|
||||||
<Icon aria-hidden iconColor={'red'} icon={ICONS.PUBLISH} />
|
<Icon aria-hidden icon={hasClaimInWatchLater ? ICONS.DELETE : ICONS.TIME} />
|
||||||
{__('Publish')}
|
{hasClaimInWatchLater ? __('In Watch Later') : __('Watch Later')}
|
||||||
</div>
|
</div>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
)}
|
)}
|
||||||
<MenuItem className="comment__menu-option" onSelect={() => push(`/$/${PAGES.LIST}/${collectionId}`)}>
|
{/* CUSTOM LIST */}
|
||||||
<div className="menu__link">
|
{isPlayable && !collectionId && (
|
||||||
<Icon aria-hidden icon={ICONS.VIEW} />
|
<MenuItem
|
||||||
{__('View List')}
|
className="comment__menu-option"
|
||||||
</div>
|
onSelect={() => {
|
||||||
</MenuItem>
|
doToast({
|
||||||
<MenuItem
|
message: __(`Item %action% ${lastCollectionName}`, {
|
||||||
className="comment__menu-option"
|
action: hasClaimInCustom ? __('removed from') : __('added to'),
|
||||||
onSelect={() => openModal(MODALS.COLLECTION_DELETE, { collectionId })}
|
}),
|
||||||
>
|
});
|
||||||
<div className="menu__link">
|
doCollectionEdit(COLLECTIONS_CONSTS.FAVORITES_ID, {
|
||||||
<Icon aria-hidden icon={ICONS.DELETE} />
|
claims: [contentClaim],
|
||||||
{__('Delete List')}
|
remove: hasClaimInCustom,
|
||||||
</div>
|
type: 'playlist',
|
||||||
</MenuItem>
|
});
|
||||||
</>
|
}}
|
||||||
)}
|
>
|
||||||
{/* CURRENTLY ONLY SUPPORT PLAYLISTS FOR PLAYABLE; LATER DIFFERENT TYPES */}
|
<div className="menu__link">
|
||||||
{isPlayable && (
|
<Icon aria-hidden icon={hasClaimInCustom ? ICONS.DELETE : ICONS.STAR} />
|
||||||
<MenuItem
|
{hasClaimInCustom ? __(`In ${lastCollectionName}`) : __(`${lastCollectionName}`)}
|
||||||
className="comment__menu-option"
|
</div>
|
||||||
onSelect={() => openModal(MODALS.COLLECTION_ADD, { uri, type: 'playlist' })}
|
</MenuItem>
|
||||||
>
|
)}
|
||||||
<div className="menu__link">
|
{/* COLLECTION OPERATIONS */}
|
||||||
<Icon aria-hidden icon={ICONS.STACK} />
|
{collectionId && collectionName && isCollectionClaim && (
|
||||||
{__('Add to Lists')}
|
|
||||||
</div>
|
|
||||||
</MenuItem>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
{!isChannelPage && (
|
|
||||||
<>
|
|
||||||
<hr className="menu__separator" />
|
|
||||||
<MenuItem className="comment__menu-option" onSelect={handleSupport}>
|
|
||||||
<div className="menu__link">
|
|
||||||
<Icon aria-hidden icon={ICONS.LBC} />
|
|
||||||
{__('Support --[button to support a claim]--')}
|
|
||||||
</div>
|
|
||||||
</MenuItem>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!incognitoClaim && !isRepost && !claimIsMine && !isChannelPage && (
|
|
||||||
<>
|
|
||||||
<hr className="menu__separator" />
|
|
||||||
<MenuItem className="comment__menu-option" onSelect={handleFollow}>
|
|
||||||
<div className="menu__link">
|
|
||||||
<Icon aria-hidden icon={ICONS.SUBSCRIBE} />
|
|
||||||
{subscriptionLabel}
|
|
||||||
</div>
|
|
||||||
</MenuItem>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{!isMyCollection && (
|
|
||||||
<>
|
|
||||||
{(!claimIsMine || channelIsBlocked) && channelUri ? (
|
|
||||||
!incognitoClaim &&
|
|
||||||
!isRepost && (
|
|
||||||
<>
|
<>
|
||||||
<hr className="menu__separator" />
|
{Boolean(editedCollection) && (
|
||||||
<MenuItem className="comment__menu-option" onSelect={handleToggleBlock}>
|
<MenuItem
|
||||||
<div className="menu__link">
|
className="comment__menu-option"
|
||||||
<Icon aria-hidden icon={ICONS.BLOCK} />
|
onSelect={() => push(`/$/${PAGES.LIST}/${collectionId}?view=edit`)}
|
||||||
{channelIsBlocked ? __('Unblock Channel') : __('Block Channel')}
|
>
|
||||||
</div>
|
|
||||||
</MenuItem>
|
|
||||||
|
|
||||||
{isAdmin && (
|
|
||||||
<MenuItem className="comment__menu-option" onSelect={handleToggleAdminBlock}>
|
|
||||||
<div className="menu__link">
|
<div className="menu__link">
|
||||||
<Icon aria-hidden icon={ICONS.GLOBE} />
|
<Icon aria-hidden iconColor={'red'} icon={ICONS.PUBLISH} />
|
||||||
{channelIsAdminBlocked ? __('Global Unblock Channel') : __('Global Block Channel')}
|
{__('Publish')}
|
||||||
</div>
|
</div>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
)}
|
)}
|
||||||
|
<MenuItem className="comment__menu-option" onSelect={() => push(`/$/${PAGES.LIST}/${collectionId}`)}>
|
||||||
<MenuItem className="comment__menu-option" onSelect={handleToggleMute}>
|
|
||||||
<div className="menu__link">
|
<div className="menu__link">
|
||||||
<Icon aria-hidden icon={ICONS.MUTE} />
|
<Icon aria-hidden icon={ICONS.VIEW} />
|
||||||
{channelIsMuted ? __('Unmute Channel') : __('Mute Channel')}
|
{__('View List')}
|
||||||
|
</div>
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem
|
||||||
|
className="comment__menu-option"
|
||||||
|
onSelect={() => openModal(MODALS.COLLECTION_DELETE, { collectionId })}
|
||||||
|
>
|
||||||
|
<div className="menu__link">
|
||||||
|
<Icon aria-hidden icon={ICONS.DELETE} />
|
||||||
|
{__('Delete List')}
|
||||||
</div>
|
</div>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
</>
|
</>
|
||||||
)
|
)}
|
||||||
) : (
|
{/* CURRENTLY ONLY SUPPORT PLAYLISTS FOR PLAYABLE; LATER DIFFERENT TYPES */}
|
||||||
|
{isPlayable && (
|
||||||
|
<MenuItem
|
||||||
|
className="comment__menu-option"
|
||||||
|
onSelect={() => openModal(MODALS.COLLECTION_ADD, { uri, type: 'playlist' })}
|
||||||
|
>
|
||||||
|
<div className="menu__link">
|
||||||
|
<Icon aria-hidden icon={ICONS.STACK} />
|
||||||
|
{__('Add to Lists')}
|
||||||
|
</div>
|
||||||
|
</MenuItem>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
{!isChannelPage && (
|
||||||
<>
|
<>
|
||||||
{!isChannelPage && !isRepost && (
|
<hr className="menu__separator" />
|
||||||
<MenuItem className="comment__menu-option" onSelect={handleEdit}>
|
<MenuItem className="comment__menu-option" onSelect={handleSupport}>
|
||||||
<div className="menu__link">
|
<div className="menu__link">
|
||||||
<Icon aria-hidden icon={ICONS.EDIT} />
|
<Icon aria-hidden icon={ICONS.LBC} />
|
||||||
{__('Edit')}
|
{__('Support --[button to support a claim]--')}
|
||||||
</div>
|
</div>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
)}
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
{showDelete && (
|
{!incognitoClaim && !isRepost && !claimIsMine && !isChannelPage && (
|
||||||
<MenuItem className="comment__menu-option" onSelect={handleDelete}>
|
<>
|
||||||
<div className="menu__link">
|
<hr className="menu__separator" />
|
||||||
<Icon aria-hidden icon={ICONS.DELETE} />
|
<MenuItem className="comment__menu-option" onSelect={handleFollow}>
|
||||||
{__('Delete')}
|
<div className="menu__link">
|
||||||
</div>
|
<Icon aria-hidden icon={ICONS.SUBSCRIBE} />
|
||||||
</MenuItem>
|
{subscriptionLabel}
|
||||||
|
</div>
|
||||||
|
</MenuItem>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{!isMyCollection && (
|
||||||
|
<>
|
||||||
|
{(!claimIsMine || channelIsBlocked) && channelUri ? (
|
||||||
|
!incognitoClaim &&
|
||||||
|
!isRepost && (
|
||||||
|
<>
|
||||||
|
<hr className="menu__separator" />
|
||||||
|
<MenuItem className="comment__menu-option" onSelect={handleToggleBlock}>
|
||||||
|
<div className="menu__link">
|
||||||
|
<Icon aria-hidden icon={ICONS.BLOCK} />
|
||||||
|
{channelIsBlocked ? __('Unblock Channel') : __('Block Channel')}
|
||||||
|
</div>
|
||||||
|
</MenuItem>
|
||||||
|
|
||||||
|
{isAdmin && (
|
||||||
|
<MenuItem className="comment__menu-option" onSelect={handleToggleAdminBlock}>
|
||||||
|
<div className="menu__link">
|
||||||
|
<Icon aria-hidden icon={ICONS.GLOBE} />
|
||||||
|
{channelIsAdminBlocked ? __('Global Unblock Channel') : __('Global Block Channel')}
|
||||||
|
</div>
|
||||||
|
</MenuItem>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<MenuItem className="comment__menu-option" onSelect={handleToggleMute}>
|
||||||
|
<div className="menu__link">
|
||||||
|
<Icon aria-hidden icon={ICONS.MUTE} />
|
||||||
|
{channelIsMuted ? __('Unmute Channel') : __('Mute Channel')}
|
||||||
|
</div>
|
||||||
|
</MenuItem>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{!isChannelPage && !isRepost && (
|
||||||
|
<MenuItem className="comment__menu-option" onSelect={handleEdit}>
|
||||||
|
<div className="menu__link">
|
||||||
|
<Icon aria-hidden icon={ICONS.EDIT} />
|
||||||
|
{__('Edit')}
|
||||||
|
</div>
|
||||||
|
</MenuItem>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{showDelete && (
|
||||||
|
<MenuItem className="comment__menu-option" onSelect={handleDelete}>
|
||||||
|
<div className="menu__link">
|
||||||
|
<Icon aria-hidden icon={ICONS.DELETE} />
|
||||||
|
{__('Delete')}
|
||||||
|
</div>
|
||||||
|
</MenuItem>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<hr className="menu__separator" />
|
<hr className="menu__separator" />
|
||||||
|
|
||||||
{isChannelPage && IS_WEB && rssUrl && (
|
{isChannelPage && IS_WEB && rssUrl && (
|
||||||
|
|
|
@ -19,6 +19,7 @@ import ClaimPreviewTitle from 'component/claimPreviewTitle';
|
||||||
import ClaimPreviewSubtitle from 'component/claimPreviewSubtitle';
|
import ClaimPreviewSubtitle from 'component/claimPreviewSubtitle';
|
||||||
import ClaimRepostAuthor from 'component/claimRepostAuthor';
|
import ClaimRepostAuthor from 'component/claimRepostAuthor';
|
||||||
import FileDownloadLink from 'component/fileDownloadLink';
|
import FileDownloadLink from 'component/fileDownloadLink';
|
||||||
|
import FileWatchLaterLink from 'component/fileWatchLaterLink';
|
||||||
import PublishPending from 'component/publishPending';
|
import PublishPending from 'component/publishPending';
|
||||||
import ClaimMenuList from 'component/claimMenuList';
|
import ClaimMenuList from 'component/claimMenuList';
|
||||||
import ClaimPreviewLoading from './claim-preview-loading';
|
import ClaimPreviewLoading from './claim-preview-loading';
|
||||||
|
@ -157,6 +158,15 @@ const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
|
||||||
isValid = false;
|
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 isCollection = claim && claim.value_type === 'collection';
|
||||||
const isChannelUri = isValid ? parseURI(uri).isChannel : false;
|
const isChannelUri = isValid ? parseURI(uri).isChannel : false;
|
||||||
const signingChannel = claim && claim.signing_channel;
|
const signingChannel = claim && claim.signing_channel;
|
||||||
|
@ -318,6 +328,11 @@ const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
|
||||||
<PreviewOverlayProperties uri={uri} small={type === 'small'} properties={liveProperty} />
|
<PreviewOverlayProperties uri={uri} small={type === 'small'} properties={liveProperty} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{isPlayable && (
|
||||||
|
<div className="claim-preview__hover-actions">
|
||||||
|
<FileWatchLaterLink uri={uri} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</FileThumbnail>
|
</FileThumbnail>
|
||||||
</NavLink>
|
</NavLink>
|
||||||
) : (
|
) : (
|
||||||
|
|
|
@ -13,6 +13,7 @@ import { formatLbryUrlForWeb } from 'util/url';
|
||||||
import { parseURI, COLLECTIONS_CONSTS } from 'lbry-redux';
|
import { parseURI, COLLECTIONS_CONSTS } from 'lbry-redux';
|
||||||
import PreviewOverlayProperties from 'component/previewOverlayProperties';
|
import PreviewOverlayProperties from 'component/previewOverlayProperties';
|
||||||
import FileDownloadLink from 'component/fileDownloadLink';
|
import FileDownloadLink from 'component/fileDownloadLink';
|
||||||
|
import FileWatchLaterLink from 'component/fileWatchLaterLink';
|
||||||
import ClaimRepostAuthor from 'component/claimRepostAuthor';
|
import ClaimRepostAuthor from 'component/claimRepostAuthor';
|
||||||
import ClaimMenuList from 'component/claimMenuList';
|
import ClaimMenuList from 'component/claimMenuList';
|
||||||
import CollectionPreviewOverlay from 'component/collectionPreviewOverlay';
|
import CollectionPreviewOverlay from 'component/collectionPreviewOverlay';
|
||||||
|
@ -75,6 +76,15 @@ function ClaimPreviewTile(props: Props) {
|
||||||
const isRepost = claim && claim.repost_channel_url;
|
const isRepost = claim && claim.repost_channel_url;
|
||||||
const isCollection = claim && claim.value_type === 'collection';
|
const isCollection = claim && claim.value_type === 'collection';
|
||||||
const isStream = claim && claim.value_type === 'stream';
|
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 collectionClaimId = isCollection && claim && claim.claim_id;
|
||||||
const shouldFetch = claim === undefined;
|
const shouldFetch = claim === undefined;
|
||||||
const thumbnailUrl = useGetThumbnail(uri, claim, streamingUrl, getFile, placeholder) || thumbnail;
|
const thumbnailUrl = useGetThumbnail(uri, claim, streamingUrl, getFile, placeholder) || thumbnail;
|
||||||
|
@ -195,6 +205,12 @@ function ClaimPreviewTile(props: Props) {
|
||||||
)}
|
)}
|
||||||
{/* @endif */}
|
{/* @endif */}
|
||||||
|
|
||||||
|
{isPlayable && (
|
||||||
|
<div className="claim-preview__hover-actions">
|
||||||
|
<FileWatchLaterLink uri={uri} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="claim-preview__file-property-overlay">
|
<div className="claim-preview__file-property-overlay">
|
||||||
<PreviewOverlayProperties uri={uri} properties={liveProperty || properties} />
|
<PreviewOverlayProperties uri={uri} properties={liveProperty || properties} />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -6,6 +6,7 @@ import Button from 'component/button';
|
||||||
import * as PAGES from 'constants/pages';
|
import * as PAGES from 'constants/pages';
|
||||||
import Icon from 'component/common/icon';
|
import Icon from 'component/common/icon';
|
||||||
import * as ICONS from 'constants/icons';
|
import * as ICONS from 'constants/icons';
|
||||||
|
import { COLLECTIONS_CONSTS } from 'lbry-redux';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
collectionUrls: Array<Claim>,
|
collectionUrls: Array<Claim>,
|
||||||
|
@ -26,7 +27,10 @@ export default function CollectionContent(props: Props) {
|
||||||
className="file-page__recommended"
|
className="file-page__recommended"
|
||||||
title={
|
title={
|
||||||
<span>
|
<span>
|
||||||
<Icon icon={ICONS.STACK} className="icon--margin-right" />
|
<Icon
|
||||||
|
icon={(id === COLLECTIONS_CONSTS.WATCH_LATER_ID && ICONS.TIME) ||
|
||||||
|
(id === COLLECTIONS_CONSTS.FAVORITES_ID && ICONS.STAR) || ICONS.STACK}
|
||||||
|
className="icon--margin-right" />
|
||||||
{collectionName}
|
{collectionName}
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,7 +25,7 @@ function CollectionSelectItem(props: Props) {
|
||||||
let icon;
|
let icon;
|
||||||
switch (category) {
|
switch (category) {
|
||||||
case 'builtin':
|
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;
|
break;
|
||||||
case 'published':
|
case 'published':
|
||||||
icon = ICONS.STACK;
|
icon = ICONS.STACK;
|
||||||
|
|
|
@ -68,7 +68,9 @@ export default function CollectionsListMine(props: Props) {
|
||||||
<span className="claim-grid__title-span">
|
<span className="claim-grid__title-span">
|
||||||
{__(`${list.name}`)}
|
{__(`${list.name}`)}
|
||||||
<div className="claim-grid__title--empty">
|
<div className="claim-grid__title--empty">
|
||||||
<Icon className="icon--margin-right" icon={ICONS.STACK} />
|
<Icon className="icon--margin-right"
|
||||||
|
icon={(list.id === COLLECTIONS_CONSTS.WATCH_LATER_ID && ICONS.TIME) ||
|
||||||
|
(list.id === COLLECTIONS_CONSTS.FAVORITES_ID && ICONS.STAR) || ICONS.STACK} />
|
||||||
{itemUrls.length}
|
{itemUrls.length}
|
||||||
</div>
|
</div>
|
||||||
</span>
|
</span>
|
||||||
|
|
|
@ -1,30 +1,43 @@
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { makeSelectStakedLevelForChannelUri, makeSelectClaimForUri, makeSelectThumbnailForUri, selectMyChannelClaims } from 'lbry-redux';
|
import {
|
||||||
import { doCommentUpdate } from 'redux/actions/comments';
|
makeSelectStakedLevelForChannelUri,
|
||||||
|
makeSelectClaimForUri,
|
||||||
|
makeSelectThumbnailForUri,
|
||||||
|
selectMyChannelClaims,
|
||||||
|
} from 'lbry-redux';
|
||||||
|
import { doCommentUpdate, doCommentList } from 'redux/actions/comments';
|
||||||
import { makeSelectChannelIsMuted } from 'redux/selectors/blocked';
|
import { makeSelectChannelIsMuted } from 'redux/selectors/blocked';
|
||||||
import { doToast } from 'redux/actions/notifications';
|
import { doToast } from 'redux/actions/notifications';
|
||||||
import { doSetPlayingUri } from 'redux/actions/content';
|
import { doSetPlayingUri } from 'redux/actions/content';
|
||||||
import { selectUserVerifiedEmail } from 'redux/selectors/user';
|
import { selectUserVerifiedEmail } from 'redux/selectors/user';
|
||||||
import { makeSelectOthersReactionsForComment } from 'redux/selectors/comments';
|
import { selectLinkedCommentAncestors, makeSelectOthersReactionsForComment } from 'redux/selectors/comments';
|
||||||
import { selectActiveChannelClaim } from 'redux/selectors/app';
|
import { selectActiveChannelId, selectActiveChannelClaim } from 'redux/selectors/app';
|
||||||
import { selectPlayingUri } from 'redux/selectors/content';
|
import { selectPlayingUri } from 'redux/selectors/content';
|
||||||
import Comment from './view';
|
import Comment from './view';
|
||||||
|
|
||||||
const select = (state, props) => ({
|
const select = (state, props) => {
|
||||||
claim: makeSelectClaimForUri(props.uri)(state),
|
const activeChannelId = selectActiveChannelId(state);
|
||||||
thumbnail: props.authorUri && makeSelectThumbnailForUri(props.authorUri)(state),
|
const reactionKey = activeChannelId ? `${props.commentId}:${activeChannelId}` : props.commentId;
|
||||||
channelIsBlocked: props.authorUri && makeSelectChannelIsMuted(props.authorUri)(state),
|
|
||||||
commentingEnabled: IS_WEB ? Boolean(selectUserVerifiedEmail(state)) : true,
|
return {
|
||||||
othersReacts: makeSelectOthersReactionsForComment(props.commentId)(state),
|
claim: makeSelectClaimForUri(props.uri)(state),
|
||||||
activeChannelClaim: selectActiveChannelClaim(state),
|
thumbnail: props.authorUri && makeSelectThumbnailForUri(props.authorUri)(state),
|
||||||
myChannels: selectMyChannelClaims(state),
|
channelIsBlocked: props.authorUri && makeSelectChannelIsMuted(props.authorUri)(state),
|
||||||
playingUri: selectPlayingUri(state),
|
commentingEnabled: IS_WEB ? Boolean(selectUserVerifiedEmail(state)) : true,
|
||||||
stakedLevel: makeSelectStakedLevelForChannelUri(props.authorUri)(state),
|
othersReacts: makeSelectOthersReactionsForComment(reactionKey)(state),
|
||||||
});
|
activeChannelClaim: selectActiveChannelClaim(state),
|
||||||
|
myChannels: selectMyChannelClaims(state),
|
||||||
|
playingUri: selectPlayingUri(state),
|
||||||
|
stakedLevel: makeSelectStakedLevelForChannelUri(props.authorUri)(state),
|
||||||
|
linkedCommentAncestors: selectLinkedCommentAncestors(state),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
const perform = (dispatch) => ({
|
const perform = (dispatch) => ({
|
||||||
clearPlayingUri: () => dispatch(doSetPlayingUri({ uri: null })),
|
clearPlayingUri: () => dispatch(doSetPlayingUri({ uri: null })),
|
||||||
updateComment: (commentId, comment) => dispatch(doCommentUpdate(commentId, comment)),
|
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)),
|
doToast: (options) => dispatch(doToast(options)),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
// @flow
|
// @flow
|
||||||
import * as ICONS from 'constants/icons';
|
import * as ICONS from 'constants/icons';
|
||||||
import * as PAGES from 'constants/pages';
|
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 { FF_MAX_CHARS_IN_COMMENT } from 'constants/form-field';
|
||||||
import { SITE_NAME, SIMPLE_SITE, ENABLE_COMMENT_REACTIONS } from 'config';
|
import { SITE_NAME, SIMPLE_SITE, ENABLE_COMMENT_REACTIONS } from 'config';
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
|
@ -23,6 +24,8 @@ import CommentMenuList from 'component/commentMenuList';
|
||||||
import UriIndicator from 'component/uriIndicator';
|
import UriIndicator from 'component/uriIndicator';
|
||||||
import CreditAmount from 'component/common/credit-amount';
|
import CreditAmount from 'component/common/credit-amount';
|
||||||
|
|
||||||
|
const AUTO_EXPAND_ALL_REPLIES = false;
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
clearPlayingUri: () => void,
|
clearPlayingUri: () => void,
|
||||||
uri: string,
|
uri: string,
|
||||||
|
@ -36,8 +39,10 @@ type Props = {
|
||||||
claimIsMine: boolean, // if you control the claim which this comment was posted on
|
claimIsMine: boolean, // if you control the claim which this comment was posted on
|
||||||
commentIsMine: boolean, // if this comment was signed by an owned channel
|
commentIsMine: boolean, // if this comment was signed by an owned channel
|
||||||
updateComment: (string, string) => void,
|
updateComment: (string, string) => void,
|
||||||
|
fetchReplies: (string, string, number, number, number) => void,
|
||||||
commentModBlock: (string) => void,
|
commentModBlock: (string) => void,
|
||||||
linkedComment?: any,
|
linkedCommentId?: string,
|
||||||
|
linkedCommentAncestors: { [string]: Array<string> },
|
||||||
myChannels: ?Array<ChannelClaim>,
|
myChannels: ?Array<ChannelClaim>,
|
||||||
commentingEnabled: boolean,
|
commentingEnabled: boolean,
|
||||||
doToast: ({ message: string }) => void,
|
doToast: ({ message: string }) => void,
|
||||||
|
@ -53,6 +58,7 @@ type Props = {
|
||||||
playingUri: ?PlayingUri,
|
playingUri: ?PlayingUri,
|
||||||
stakedLevel: number,
|
stakedLevel: number,
|
||||||
supportAmount: number,
|
supportAmount: number,
|
||||||
|
numDirectReplies: number,
|
||||||
};
|
};
|
||||||
|
|
||||||
const LENGTH_TO_COLLAPSE = 300;
|
const LENGTH_TO_COLLAPSE = 300;
|
||||||
|
@ -71,7 +77,9 @@ function Comment(props: Props) {
|
||||||
commentIsMine,
|
commentIsMine,
|
||||||
commentId,
|
commentId,
|
||||||
updateComment,
|
updateComment,
|
||||||
linkedComment,
|
fetchReplies,
|
||||||
|
linkedCommentId,
|
||||||
|
linkedCommentAncestors,
|
||||||
commentingEnabled,
|
commentingEnabled,
|
||||||
myChannels,
|
myChannels,
|
||||||
doToast,
|
doToast,
|
||||||
|
@ -82,18 +90,23 @@ function Comment(props: Props) {
|
||||||
playingUri,
|
playingUri,
|
||||||
stakedLevel,
|
stakedLevel,
|
||||||
supportAmount,
|
supportAmount,
|
||||||
|
numDirectReplies,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
push,
|
push,
|
||||||
replace,
|
replace,
|
||||||
location: { pathname, search },
|
location: { pathname, search },
|
||||||
} = useHistory();
|
} = useHistory();
|
||||||
|
|
||||||
const [isReplying, setReplying] = React.useState(false);
|
const [isReplying, setReplying] = React.useState(false);
|
||||||
const [isEditing, setEditing] = useState(false);
|
const [isEditing, setEditing] = useState(false);
|
||||||
const [editedMessage, setCommentValue] = useState(message);
|
const [editedMessage, setCommentValue] = useState(message);
|
||||||
const [charCount, setCharCount] = useState(editedMessage.length);
|
const [charCount, setCharCount] = useState(editedMessage.length);
|
||||||
// used for controlling the visibility of the menu icon
|
// used for controlling the visibility of the menu icon
|
||||||
const [mouseIsHovering, setMouseHover] = useState(false);
|
const [mouseIsHovering, setMouseHover] = useState(false);
|
||||||
|
const [showReplies, setShowReplies] = useState(false);
|
||||||
|
const [page, setPage] = useState(0);
|
||||||
const [advancedEditor] = usePersistedState('comment-editor-mode', false);
|
const [advancedEditor] = usePersistedState('comment-editor-mode', false);
|
||||||
const [displayDeadComment, setDisplayDeadComment] = React.useState(false);
|
const [displayDeadComment, setDisplayDeadComment] = React.useState(false);
|
||||||
const hasChannels = myChannels && myChannels.length > 0;
|
const hasChannels = myChannels && myChannels.length > 0;
|
||||||
|
@ -111,6 +124,19 @@ function Comment(props: Props) {
|
||||||
}
|
}
|
||||||
} catch (e) {}
|
} 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(() => {
|
useEffect(() => {
|
||||||
if (isEditing) {
|
if (isEditing) {
|
||||||
setCharCount(editedMessage.length);
|
setCharCount(editedMessage.length);
|
||||||
|
@ -131,6 +157,12 @@ function Comment(props: Props) {
|
||||||
}
|
}
|
||||||
}, [author, authorUri, editedMessage, isEditing, setEditing]);
|
}, [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) {
|
function handleEditMessageChanged(event) {
|
||||||
setCommentValue(!SIMPLE_SITE && advancedEditor ? event : event.target.value);
|
setCommentValue(!SIMPLE_SITE && advancedEditor ? event : event.target.value);
|
||||||
}
|
}
|
||||||
|
@ -176,7 +208,7 @@ function Comment(props: Props) {
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={classnames('comment__content', {
|
className={classnames('comment__content', {
|
||||||
'comment--highlighted': linkedComment && linkedComment.comment_id === commentId,
|
'comment--highlighted': linkedCommentId && linkedCommentId === commentId,
|
||||||
'comment--slimed': slimedToDeath && !displayDeadComment,
|
'comment--slimed': slimedToDeath && !displayDeadComment,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
|
@ -302,13 +334,43 @@ function Comment(props: Props) {
|
||||||
{ENABLE_COMMENT_REACTIONS && <CommentReactions uri={uri} commentId={commentId} />}
|
{ENABLE_COMMENT_REACTIONS && <CommentReactions uri={uri} commentId={commentId} />}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{numDirectReplies > 0 && !showReplies && (
|
||||||
|
<div className="comment__actions">
|
||||||
|
<Button
|
||||||
|
label={
|
||||||
|
numDirectReplies < 2
|
||||||
|
? __('Show reply')
|
||||||
|
: __('Show %count% replies', { count: numDirectReplies })
|
||||||
|
}
|
||||||
|
button="link"
|
||||||
|
onClick={() => {
|
||||||
|
setShowReplies(true);
|
||||||
|
if (page === 0) {
|
||||||
|
setPage(1);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{numDirectReplies > 0 && showReplies && (
|
||||||
|
<div className="comment__actions">
|
||||||
|
<Button label={__('Hide replies')} button="link" onClick={() => setShowReplies(false)} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{isReplying && (
|
{isReplying && (
|
||||||
<CommentCreate
|
<CommentCreate
|
||||||
isReply
|
isReply
|
||||||
uri={uri}
|
uri={uri}
|
||||||
parentId={commentId}
|
parentId={commentId}
|
||||||
onDoneReplying={() => setReplying(false)}
|
onDoneReplying={() => {
|
||||||
onCancelReplying={() => setReplying(false)}
|
setShowReplies(true);
|
||||||
|
setReplying(false);
|
||||||
|
}}
|
||||||
|
onCancelReplying={() => {
|
||||||
|
setReplying(false);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
@ -317,7 +379,16 @@ function Comment(props: Props) {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<CommentsReplies threadDepth={threadDepth - 1} uri={uri} parentId={commentId} linkedComment={linkedComment} />
|
{showReplies && (
|
||||||
|
<CommentsReplies
|
||||||
|
threadDepth={threadDepth - 1}
|
||||||
|
uri={uri}
|
||||||
|
parentId={commentId}
|
||||||
|
linkedCommentId={linkedCommentId}
|
||||||
|
numDirectReplies={numDirectReplies}
|
||||||
|
onShowMore={() => setPage(page + 1)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,7 +35,6 @@ type Props = {
|
||||||
toast: (string) => void,
|
toast: (string) => void,
|
||||||
claimIsMine: boolean,
|
claimIsMine: boolean,
|
||||||
sendTip: ({}, (any) => void, (any) => void) => void,
|
sendTip: ({}, (any) => void, (any) => void) => void,
|
||||||
justCommented: Array<string>,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export function CommentCreate(props: Props) {
|
export function CommentCreate(props: Props) {
|
||||||
|
@ -54,7 +53,6 @@ export function CommentCreate(props: Props) {
|
||||||
livestream,
|
livestream,
|
||||||
claimIsMine,
|
claimIsMine,
|
||||||
sendTip,
|
sendTip,
|
||||||
justCommented,
|
|
||||||
} = props;
|
} = props;
|
||||||
const buttonref: ElementRef<any> = React.useRef();
|
const buttonref: ElementRef<any> = React.useRef();
|
||||||
const {
|
const {
|
||||||
|
@ -153,7 +151,6 @@ export function CommentCreate(props: Props) {
|
||||||
setIsReviewingSupportComment(false);
|
setIsReviewingSupportComment(false);
|
||||||
setIsSupportComment(false);
|
setIsSupportComment(false);
|
||||||
setCommentFailure(false);
|
setCommentFailure(false);
|
||||||
justCommented.push(res.comment_id);
|
|
||||||
|
|
||||||
if (onDoneReplying) {
|
if (onDoneReplying) {
|
||||||
onDoneReplying();
|
onDoneReplying();
|
||||||
|
@ -217,7 +214,13 @@ export function CommentCreate(props: Props) {
|
||||||
autoFocus
|
autoFocus
|
||||||
button="primary"
|
button="primary"
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
label={isSubmitting ? __('Sending...') : (commentFailure && tipAmount === successTip.tipAmount) ? __('Re-submit') : __('Send')}
|
label={
|
||||||
|
isSubmitting
|
||||||
|
? __('Sending...')
|
||||||
|
: commentFailure && tipAmount === successTip.tipAmount
|
||||||
|
? __('Re-submit')
|
||||||
|
: __('Send')
|
||||||
|
}
|
||||||
onClick={handleSupportComment}
|
onClick={handleSupportComment}
|
||||||
/>
|
/>
|
||||||
<Button button="link" label={__('Cancel')} onClick={() => setIsReviewingSupportComment(false)} />
|
<Button button="link" label={__('Cancel')} onClick={() => setIsReviewingSupportComment(false)} />
|
||||||
|
|
|
@ -3,7 +3,6 @@ import { makeSelectChannelPermUrlForClaimUri, makeSelectClaimIsMine, makeSelectC
|
||||||
import {
|
import {
|
||||||
doCommentAbandon,
|
doCommentAbandon,
|
||||||
doCommentPin,
|
doCommentPin,
|
||||||
doCommentList,
|
|
||||||
doCommentModBlock,
|
doCommentModBlock,
|
||||||
doCommentModBlockAsAdmin,
|
doCommentModBlockAsAdmin,
|
||||||
doCommentModBlockAsModerator,
|
doCommentModBlockAsModerator,
|
||||||
|
@ -30,8 +29,7 @@ const perform = (dispatch) => ({
|
||||||
clearPlayingUri: () => dispatch(doSetPlayingUri({ uri: null })),
|
clearPlayingUri: () => dispatch(doSetPlayingUri({ uri: null })),
|
||||||
deleteComment: (commentId, creatorChannelUrl) => dispatch(doCommentAbandon(commentId, creatorChannelUrl)),
|
deleteComment: (commentId, creatorChannelUrl) => dispatch(doCommentAbandon(commentId, creatorChannelUrl)),
|
||||||
muteChannel: (channelUri) => dispatch(doChannelMute(channelUri)),
|
muteChannel: (channelUri) => dispatch(doChannelMute(channelUri)),
|
||||||
pinComment: (commentId, remove) => dispatch(doCommentPin(commentId, remove)),
|
pinComment: (commentId, claimId, remove) => dispatch(doCommentPin(commentId, claimId, remove)),
|
||||||
fetchComments: (uri) => dispatch(doCommentList(uri)),
|
|
||||||
// setActiveChannel: channelId => dispatch(doSetActiveChannel(channelId)),
|
// setActiveChannel: channelId => dispatch(doSetActiveChannel(channelId)),
|
||||||
commentModBlock: (commenterUri) => dispatch(doCommentModBlock(commenterUri)),
|
commentModBlock: (commenterUri) => dispatch(doCommentModBlock(commenterUri)),
|
||||||
commentModBlockAsAdmin: (commenterUri, blockerId) => dispatch(doCommentModBlockAsAdmin(commenterUri, blockerId)),
|
commentModBlockAsAdmin: (commenterUri, blockerId) => dispatch(doCommentModBlockAsAdmin(commenterUri, blockerId)),
|
||||||
|
|
|
@ -7,18 +7,15 @@ import Icon from 'component/common/icon';
|
||||||
import { parseURI } from 'lbry-redux';
|
import { parseURI } from 'lbry-redux';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
uri: string,
|
|
||||||
claim: ?Claim,
|
claim: ?Claim,
|
||||||
clearPlayingUri: () => void,
|
clearPlayingUri: () => void,
|
||||||
authorUri: string, // full LBRY Channel URI: lbry://@channel#123...
|
authorUri: string, // full LBRY Channel URI: lbry://@channel#123...
|
||||||
commentId: string, // sha256 digest identifying the comment
|
commentId: string, // sha256 digest identifying the comment
|
||||||
commentIsMine: boolean, // if this comment was signed by an owned channel
|
commentIsMine: boolean, // if this comment was signed by an owned channel
|
||||||
deleteComment: (string, ?string) => void,
|
deleteComment: (string, ?string) => void,
|
||||||
linkedComment?: any,
|
|
||||||
isPinned: boolean,
|
isPinned: boolean,
|
||||||
pinComment: (string, boolean) => Promise<any>,
|
pinComment: (string, string, boolean) => Promise<any>,
|
||||||
muteChannel: (string) => void,
|
muteChannel: (string) => void,
|
||||||
fetchComments: (string) => void,
|
|
||||||
handleEditComment: () => void,
|
handleEditComment: () => void,
|
||||||
contentChannelPermanentUrl: any,
|
contentChannelPermanentUrl: any,
|
||||||
activeChannelClaim: ?ChannelClaim,
|
activeChannelClaim: ?ChannelClaim,
|
||||||
|
@ -35,7 +32,6 @@ type Props = {
|
||||||
|
|
||||||
function CommentMenuList(props: Props) {
|
function CommentMenuList(props: Props) {
|
||||||
const {
|
const {
|
||||||
uri,
|
|
||||||
claim,
|
claim,
|
||||||
authorUri,
|
authorUri,
|
||||||
commentIsMine,
|
commentIsMine,
|
||||||
|
@ -49,7 +45,6 @@ function CommentMenuList(props: Props) {
|
||||||
isTopLevel,
|
isTopLevel,
|
||||||
isPinned,
|
isPinned,
|
||||||
handleEditComment,
|
handleEditComment,
|
||||||
fetchComments,
|
|
||||||
commentModBlock,
|
commentModBlock,
|
||||||
commentModBlockAsAdmin,
|
commentModBlockAsAdmin,
|
||||||
commentModBlockAsModerator,
|
commentModBlockAsModerator,
|
||||||
|
@ -77,8 +72,8 @@ function CommentMenuList(props: Props) {
|
||||||
activeModeratorInfo &&
|
activeModeratorInfo &&
|
||||||
Object.values(activeModeratorInfo.delegators).includes(contentChannelClaim.claim_id);
|
Object.values(activeModeratorInfo.delegators).includes(contentChannelClaim.claim_id);
|
||||||
|
|
||||||
function handlePinComment(commentId, remove) {
|
function handlePinComment(commentId, claimId, remove) {
|
||||||
pinComment(commentId, remove).then(() => fetchComments(uri));
|
pinComment(commentId, claimId, remove);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleDeleteComment() {
|
function handleDeleteComment() {
|
||||||
|
@ -122,7 +117,7 @@ function CommentMenuList(props: Props) {
|
||||||
{activeChannelIsCreator && isTopLevel && (
|
{activeChannelIsCreator && isTopLevel && (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
className="comment__menu-option menu__link"
|
className="comment__menu-option menu__link"
|
||||||
onSelect={isPinned ? () => handlePinComment(commentId, true) : () => handlePinComment(commentId, false)}
|
onSelect={() => handlePinComment(commentId, claim ? claim.claim_id : '', isPinned)}
|
||||||
>
|
>
|
||||||
<span className={'button__content'}>
|
<span className={'button__content'}>
|
||||||
<Icon aria-hidden icon={ICONS.PIN} className={'icon'} />
|
<Icon aria-hidden icon={ICONS.PIN} className={'icon'} />
|
||||||
|
|
|
@ -6,17 +6,22 @@ import { makeSelectMyReactionsForComment, makeSelectOthersReactionsForComment }
|
||||||
import { doCommentReact } from 'redux/actions/comments';
|
import { doCommentReact } from 'redux/actions/comments';
|
||||||
import { selectActiveChannelId } from 'redux/selectors/app';
|
import { selectActiveChannelId } from 'redux/selectors/app';
|
||||||
|
|
||||||
const select = (state, props) => ({
|
const select = (state, props) => {
|
||||||
claim: makeSelectClaimForUri(props.uri)(state),
|
const activeChannelId = selectActiveChannelId(state);
|
||||||
claimIsMine: makeSelectClaimIsMine(props.uri)(state),
|
const reactionKey = activeChannelId ? `${props.commentId}:${activeChannelId}` : props.commentId;
|
||||||
myReacts: makeSelectMyReactionsForComment(props.commentId)(state),
|
|
||||||
othersReacts: makeSelectOthersReactionsForComment(props.commentId)(state),
|
|
||||||
activeChannelId: selectActiveChannelId(state),
|
|
||||||
});
|
|
||||||
|
|
||||||
const perform = dispatch => ({
|
return {
|
||||||
|
claim: makeSelectClaimForUri(props.uri)(state),
|
||||||
|
claimIsMine: makeSelectClaimIsMine(props.uri)(state),
|
||||||
|
myReacts: makeSelectMyReactionsForComment(reactionKey)(state),
|
||||||
|
othersReacts: makeSelectOthersReactionsForComment(reactionKey)(state),
|
||||||
|
activeChannelId,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const perform = (dispatch) => ({
|
||||||
react: (commentId, type) => dispatch(doCommentReact(commentId, type)),
|
react: (commentId, type) => dispatch(doCommentReact(commentId, type)),
|
||||||
doToast: params => dispatch(doToast(params)),
|
doToast: (params) => dispatch(doToast(params)),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(select, perform)(Comment);
|
export default connect(select, perform)(Comment);
|
||||||
|
|
|
@ -2,32 +2,42 @@ import { connect } from 'react-redux';
|
||||||
import { makeSelectClaimIsMine, selectFetchingMyChannels, selectMyChannelClaims } from 'lbry-redux';
|
import { makeSelectClaimIsMine, selectFetchingMyChannels, selectMyChannelClaims } from 'lbry-redux';
|
||||||
import {
|
import {
|
||||||
makeSelectTopLevelCommentsForUri,
|
makeSelectTopLevelCommentsForUri,
|
||||||
|
makeSelectTopLevelTotalPagesForUri,
|
||||||
selectIsFetchingComments,
|
selectIsFetchingComments,
|
||||||
makeSelectTotalCommentsCountForUri,
|
makeSelectTotalCommentsCountForUri,
|
||||||
selectOthersReactsById,
|
selectOthersReactsById,
|
||||||
makeSelectCommentsDisabledForUri,
|
makeSelectCommentsDisabledForUri,
|
||||||
|
selectMyReactionsByCommentId,
|
||||||
|
makeSelectCommentIdsForUri,
|
||||||
} from 'redux/selectors/comments';
|
} from 'redux/selectors/comments';
|
||||||
import { doCommentList, doCommentReactList } from 'redux/actions/comments';
|
import { doCommentReset, doCommentList, doCommentById, doCommentReactList } from 'redux/actions/comments';
|
||||||
import { selectUserVerifiedEmail } from 'redux/selectors/user';
|
import { selectUserVerifiedEmail } from 'redux/selectors/user';
|
||||||
import { selectActiveChannelId } from 'redux/selectors/app';
|
import { selectActiveChannelId } from 'redux/selectors/app';
|
||||||
import CommentsList from './view';
|
import CommentsList from './view';
|
||||||
|
|
||||||
const select = (state, props) => ({
|
const select = (state, props) => {
|
||||||
myChannels: selectMyChannelClaims(state),
|
return {
|
||||||
comments: makeSelectTopLevelCommentsForUri(props.uri)(state),
|
myChannels: selectMyChannelClaims(state),
|
||||||
totalComments: makeSelectTotalCommentsCountForUri(props.uri)(state),
|
allCommentIds: makeSelectCommentIdsForUri(props.uri)(state),
|
||||||
claimIsMine: makeSelectClaimIsMine(props.uri)(state),
|
topLevelComments: makeSelectTopLevelCommentsForUri(props.uri)(state),
|
||||||
isFetchingComments: selectIsFetchingComments(state),
|
topLevelTotalPages: makeSelectTopLevelTotalPagesForUri(props.uri)(state),
|
||||||
commentingEnabled: IS_WEB ? Boolean(selectUserVerifiedEmail(state)) : true,
|
totalComments: makeSelectTotalCommentsCountForUri(props.uri)(state),
|
||||||
commentsDisabledBySettings: makeSelectCommentsDisabledForUri(props.uri)(state),
|
claimIsMine: makeSelectClaimIsMine(props.uri)(state),
|
||||||
fetchingChannels: selectFetchingMyChannels(state),
|
isFetchingComments: selectIsFetchingComments(state),
|
||||||
reactionsById: selectOthersReactsById(state),
|
commentingEnabled: IS_WEB ? Boolean(selectUserVerifiedEmail(state)) : true,
|
||||||
activeChannelId: selectActiveChannelId(state),
|
commentsDisabledBySettings: makeSelectCommentsDisabledForUri(props.uri)(state),
|
||||||
});
|
fetchingChannels: selectFetchingMyChannels(state),
|
||||||
|
myReactsByCommentId: selectMyReactionsByCommentId(state),
|
||||||
|
othersReactsById: selectOthersReactsById(state),
|
||||||
|
activeChannelId: selectActiveChannelId(state),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
const perform = (dispatch) => ({
|
const perform = (dispatch) => ({
|
||||||
fetchComments: (uri) => dispatch(doCommentList(uri)),
|
fetchTopLevelComments: (uri, page, pageSize, sortBy) => dispatch(doCommentList(uri, '', page, pageSize, sortBy)),
|
||||||
fetchReacts: (uri) => dispatch(doCommentReactList(uri)),
|
fetchComment: (commentId) => dispatch(doCommentById(commentId)),
|
||||||
|
fetchReacts: (commentIds) => dispatch(doCommentReactList(commentIds)),
|
||||||
|
resetComments: (uri) => dispatch(doCommentReset(uri)),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(select, perform)(CommentsList);
|
export default connect(select, perform)(CommentsList);
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
// @flow
|
// @flow
|
||||||
import * as REACTION_TYPES from 'constants/reactions';
|
import * as REACTION_TYPES from 'constants/reactions';
|
||||||
import * as ICONS from 'constants/icons';
|
import * as ICONS from 'constants/icons';
|
||||||
import { SORT_COMMENTS_NEW, SORT_COMMENTS_BEST, SORT_COMMENTS_CONTROVERSIAL } from 'constants/comment';
|
import { COMMENT_PAGE_SIZE_TOP_LEVEL, SORT_BY } from 'constants/comment';
|
||||||
import React, { useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import CommentView from 'component/comment';
|
import CommentView from 'component/comment';
|
||||||
|
@ -11,59 +11,78 @@ import Card from 'component/common/card';
|
||||||
import CommentCreate from 'component/commentCreate';
|
import CommentCreate from 'component/commentCreate';
|
||||||
import usePersistedState from 'effects/use-persisted-state';
|
import usePersistedState from 'effects/use-persisted-state';
|
||||||
import { ENABLE_COMMENT_REACTIONS } from 'config';
|
import { ENABLE_COMMENT_REACTIONS } from 'config';
|
||||||
import { sortComments } from 'util/comments';
|
|
||||||
import Empty from 'component/common/empty';
|
import Empty from 'component/common/empty';
|
||||||
|
import debounce from 'util/debounce';
|
||||||
|
|
||||||
|
const DEBOUNCE_SCROLL_HANDLER_MS = 200;
|
||||||
|
|
||||||
|
function scaleToDevicePixelRatio(value) {
|
||||||
|
const devicePixelRatio = window.devicePixelRatio || 1.0;
|
||||||
|
if (devicePixelRatio < 1.0) {
|
||||||
|
return Math.ceil(value / devicePixelRatio);
|
||||||
|
}
|
||||||
|
return Math.ceil(value * devicePixelRatio);
|
||||||
|
}
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
comments: Array<Comment>,
|
allCommentIds: any,
|
||||||
|
topLevelComments: Array<Comment>,
|
||||||
|
topLevelTotalPages: number,
|
||||||
commentsDisabledBySettings: boolean,
|
commentsDisabledBySettings: boolean,
|
||||||
fetchComments: (string) => void,
|
fetchTopLevelComments: (string, number, number, number) => void,
|
||||||
fetchReacts: (string) => Promise<any>,
|
fetchComment: (string) => void,
|
||||||
|
fetchReacts: (Array<string>) => Promise<any>,
|
||||||
|
resetComments: (string) => void,
|
||||||
uri: string,
|
uri: string,
|
||||||
claimIsMine: boolean,
|
claimIsMine: boolean,
|
||||||
myChannels: ?Array<ChannelClaim>,
|
myChannels: ?Array<ChannelClaim>,
|
||||||
isFetchingComments: boolean,
|
isFetchingComments: boolean,
|
||||||
linkedComment: any,
|
linkedCommentId?: string,
|
||||||
totalComments: number,
|
totalComments: number,
|
||||||
fetchingChannels: boolean,
|
fetchingChannels: boolean,
|
||||||
reactionsById: ?{ [string]: { [REACTION_TYPES.LIKE | REACTION_TYPES.DISLIKE]: number } },
|
myReactsByCommentId: ?{ [string]: Array<string> }, // "CommentId:MyChannelId" -> reaction array (note the ID concatenation)
|
||||||
|
othersReactsById: ?{ [string]: { [REACTION_TYPES.LIKE | REACTION_TYPES.DISLIKE]: number } },
|
||||||
activeChannelId: ?string,
|
activeChannelId: ?string,
|
||||||
};
|
};
|
||||||
|
|
||||||
function CommentList(props: Props) {
|
function CommentList(props: Props) {
|
||||||
const {
|
const {
|
||||||
fetchComments,
|
allCommentIds,
|
||||||
|
fetchTopLevelComments,
|
||||||
|
fetchComment,
|
||||||
fetchReacts,
|
fetchReacts,
|
||||||
|
resetComments,
|
||||||
uri,
|
uri,
|
||||||
comments,
|
topLevelComments,
|
||||||
|
topLevelTotalPages,
|
||||||
commentsDisabledBySettings,
|
commentsDisabledBySettings,
|
||||||
claimIsMine,
|
claimIsMine,
|
||||||
myChannels,
|
myChannels,
|
||||||
isFetchingComments,
|
isFetchingComments,
|
||||||
linkedComment,
|
linkedCommentId,
|
||||||
totalComments,
|
totalComments,
|
||||||
fetchingChannels,
|
fetchingChannels,
|
||||||
reactionsById,
|
myReactsByCommentId,
|
||||||
|
othersReactsById,
|
||||||
activeChannelId,
|
activeChannelId,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const commentRef = React.useRef();
|
const commentRef = React.useRef();
|
||||||
const spinnerRef = React.useRef();
|
const spinnerRef = React.useRef();
|
||||||
const [sort, setSort] = usePersistedState(
|
const DEFAULT_SORT = ENABLE_COMMENT_REACTIONS ? SORT_BY.POPULARITY : SORT_BY.NEWEST;
|
||||||
'comment-sort',
|
const [sort, setSort] = usePersistedState('comment-sort-by', DEFAULT_SORT);
|
||||||
ENABLE_COMMENT_REACTIONS ? SORT_COMMENTS_BEST : SORT_COMMENTS_NEW
|
const [page, setPage] = React.useState(0);
|
||||||
);
|
const totalFetchedComments = allCommentIds ? allCommentIds.length : 0;
|
||||||
|
|
||||||
const [start] = React.useState(0);
|
|
||||||
const [end, setEnd] = React.useState(9);
|
|
||||||
// Display comments immediately if not fetching reactions
|
// Display comments immediately if not fetching reactions
|
||||||
// If not, wait to show comments until reactions are fetched
|
// If not, wait to show comments until reactions are fetched
|
||||||
const [readyToDisplayComments, setReadyToDisplayComments] = React.useState(
|
const [readyToDisplayComments, setReadyToDisplayComments] = React.useState(
|
||||||
Boolean(reactionsById) || !ENABLE_COMMENT_REACTIONS
|
Boolean(othersReactsById) || !ENABLE_COMMENT_REACTIONS
|
||||||
);
|
);
|
||||||
const [justCommented] = React.useState([]);
|
|
||||||
const linkedCommentId = linkedComment && linkedComment.comment_id;
|
|
||||||
const hasNoComments = !totalComments;
|
const hasNoComments = !totalComments;
|
||||||
const moreBelow = totalComments - end > 0;
|
const moreBelow = page < topLevelTotalPages;
|
||||||
|
|
||||||
const isMyComment = (channelId: string): boolean => {
|
const isMyComment = (channelId: string): boolean => {
|
||||||
if (myChannels != null && channelId != null) {
|
if (myChannels != null && channelId != null) {
|
||||||
for (let i = 0; i < myChannels.length; i++) {
|
for (let i = 0; i < myChannels.length; i++) {
|
||||||
|
@ -75,86 +94,124 @@ function CommentList(props: Props) {
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleMoreBelow = React.useCallback(() => {
|
function changeSort(newSort) {
|
||||||
if (moreBelow) {
|
if (sort !== newSort) {
|
||||||
setEnd(end + 10);
|
setSort(newSort);
|
||||||
|
setPage(0); // Invalidate existing comments
|
||||||
}
|
}
|
||||||
}, [end, setEnd, moreBelow]);
|
}
|
||||||
|
|
||||||
|
// Reset comments
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchComments(uri);
|
if (page === 0) {
|
||||||
}, [fetchComments, uri]);
|
resetComments(uri);
|
||||||
|
setPage(1);
|
||||||
useEffect(() => {
|
|
||||||
if (totalComments && ENABLE_COMMENT_REACTIONS && !fetchingChannels) {
|
|
||||||
fetchReacts(uri)
|
|
||||||
.then(() => {
|
|
||||||
setReadyToDisplayComments(true);
|
|
||||||
})
|
|
||||||
.catch(() => setReadyToDisplayComments(true));
|
|
||||||
}
|
}
|
||||||
}, [fetchReacts, uri, totalComments, activeChannelId, fetchingChannels]);
|
}, [page, uri, resetComments]);
|
||||||
|
|
||||||
|
// Fetch top-level comments
|
||||||
|
useEffect(() => {
|
||||||
|
if (page !== 0) {
|
||||||
|
if (page === 1 && linkedCommentId) {
|
||||||
|
fetchComment(linkedCommentId);
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchTopLevelComments(uri, page, COMMENT_PAGE_SIZE_TOP_LEVEL, sort);
|
||||||
|
}
|
||||||
|
}, [fetchTopLevelComments, uri, page, resetComments, sort, linkedCommentId, fetchComment]);
|
||||||
|
|
||||||
|
// Fetch reacts
|
||||||
|
useEffect(() => {
|
||||||
|
if (totalFetchedComments > 0 && ENABLE_COMMENT_REACTIONS && !fetchingChannels) {
|
||||||
|
let idsForReactionFetch;
|
||||||
|
|
||||||
|
if (!othersReactsById || !myReactsByCommentId) {
|
||||||
|
idsForReactionFetch = allCommentIds;
|
||||||
|
} else {
|
||||||
|
idsForReactionFetch = allCommentIds.filter((commentId) => {
|
||||||
|
const key = activeChannelId ? `${commentId}:${activeChannelId}` : commentId;
|
||||||
|
return !othersReactsById[key] || !myReactsByCommentId[key];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (idsForReactionFetch.length !== 0) {
|
||||||
|
fetchReacts(idsForReactionFetch)
|
||||||
|
.then(() => {
|
||||||
|
setReadyToDisplayComments(true);
|
||||||
|
})
|
||||||
|
.catch(() => setReadyToDisplayComments(true));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
totalFetchedComments,
|
||||||
|
allCommentIds,
|
||||||
|
othersReactsById,
|
||||||
|
myReactsByCommentId,
|
||||||
|
fetchReacts,
|
||||||
|
uri,
|
||||||
|
activeChannelId,
|
||||||
|
fetchingChannels,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Scroll to linked-comment
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (readyToDisplayComments && linkedCommentId && commentRef && commentRef.current) {
|
if (readyToDisplayComments && linkedCommentId && commentRef && commentRef.current) {
|
||||||
commentRef.current.scrollIntoView({ block: 'start' });
|
commentRef.current.scrollIntoView({ block: 'start' });
|
||||||
window.scrollBy(0, -100);
|
window.scrollBy(0, -125);
|
||||||
}
|
}
|
||||||
}, [readyToDisplayComments, linkedCommentId]);
|
}, [readyToDisplayComments, linkedCommentId]);
|
||||||
|
|
||||||
|
// Infinite scroll
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
function handleCommentScroll(e) {
|
function shouldFetchNextPage(page, topLevelTotalPages, window, document, yPrefetchPx = 1000) {
|
||||||
// $FlowFixMe
|
if (!spinnerRef || !spinnerRef.current) {
|
||||||
const rect = spinnerRef.current.getBoundingClientRect();
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rect = spinnerRef.current.getBoundingClientRect(); // $FlowFixMe
|
||||||
|
const windowH = window.innerHeight || document.documentElement.clientHeight; // $FlowFixMe
|
||||||
|
const windowW = window.innerWidth || document.documentElement.clientWidth; // $FlowFixMe
|
||||||
|
|
||||||
|
const isApproachingViewport = yPrefetchPx !== 0 && rect.top < windowH + scaleToDevicePixelRatio(yPrefetchPx);
|
||||||
|
|
||||||
const isInViewport =
|
const isInViewport =
|
||||||
rect.top >= 0 &&
|
rect.width > 0 &&
|
||||||
rect.left >= 0 &&
|
rect.height > 0 &&
|
||||||
|
rect.bottom >= 0 &&
|
||||||
|
rect.right >= 0 &&
|
||||||
// $FlowFixMe
|
// $FlowFixMe
|
||||||
rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
|
rect.top <= windowH &&
|
||||||
// $FlowFixMe
|
// $FlowFixMe
|
||||||
rect.right <= (window.innerWidth || document.documentElement.clientWidth);
|
rect.left <= windowW;
|
||||||
|
|
||||||
if (isInViewport) {
|
return (isInViewport || isApproachingViewport) && page < topLevelTotalPages;
|
||||||
handleMoreBelow();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleCommentScroll = debounce(() => {
|
||||||
|
if (shouldFetchNextPage(page, topLevelTotalPages, window, document)) {
|
||||||
|
setPage(page + 1);
|
||||||
|
}
|
||||||
|
}, DEBOUNCE_SCROLL_HANDLER_MS);
|
||||||
|
|
||||||
if (!isFetchingComments && readyToDisplayComments && moreBelow && spinnerRef && spinnerRef.current) {
|
if (!isFetchingComments && readyToDisplayComments && moreBelow && spinnerRef && spinnerRef.current) {
|
||||||
window.addEventListener('scroll', handleCommentScroll);
|
if (shouldFetchNextPage(page, topLevelTotalPages, window, document, 0)) {
|
||||||
}
|
setPage(page + 1);
|
||||||
|
|
||||||
return () => window.removeEventListener('scroll', handleCommentScroll);
|
|
||||||
}, [moreBelow, handleMoreBelow, spinnerRef, isFetchingComments, readyToDisplayComments]);
|
|
||||||
|
|
||||||
function prepareComments(arrayOfComments, linkedComment, isFetchingComments) {
|
|
||||||
let orderedComments = [];
|
|
||||||
|
|
||||||
if (linkedComment) {
|
|
||||||
if (!linkedComment.parent_id) {
|
|
||||||
orderedComments = arrayOfComments.filter((c) => c.comment_id !== linkedComment.comment_id);
|
|
||||||
orderedComments.unshift(linkedComment);
|
|
||||||
} else {
|
} else {
|
||||||
const parentComment = arrayOfComments.find((c) => c.comment_id === linkedComment.parent_id);
|
window.addEventListener('scroll', handleCommentScroll);
|
||||||
orderedComments = arrayOfComments.filter((c) => c.comment_id !== linkedComment.parent_id);
|
return () => window.removeEventListener('scroll', handleCommentScroll);
|
||||||
|
|
||||||
if (parentComment) {
|
|
||||||
orderedComments.unshift(parentComment);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
orderedComments = arrayOfComments;
|
|
||||||
}
|
}
|
||||||
return orderedComments;
|
}, [
|
||||||
}
|
page,
|
||||||
|
moreBelow,
|
||||||
|
spinnerRef,
|
||||||
|
isFetchingComments,
|
||||||
|
readyToDisplayComments,
|
||||||
|
topLevelComments.length,
|
||||||
|
topLevelTotalPages,
|
||||||
|
]);
|
||||||
|
|
||||||
// Default to newest first for apps that don't have comment reactions
|
const displayedComments = readyToDisplayComments ? topLevelComments : [];
|
||||||
const sortedComments = reactionsById
|
|
||||||
? sortComments({ comments, reactionsById, sort, isMyComment, justCommented })
|
|
||||||
: [];
|
|
||||||
const displayedComments = readyToDisplayComments
|
|
||||||
? prepareComments(sortedComments, linkedComment).slice(start, end)
|
|
||||||
: [];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
|
@ -174,9 +231,9 @@ function CommentList(props: Props) {
|
||||||
label={__('Best')}
|
label={__('Best')}
|
||||||
icon={ICONS.BEST}
|
icon={ICONS.BEST}
|
||||||
iconSize={18}
|
iconSize={18}
|
||||||
onClick={() => setSort(SORT_COMMENTS_BEST)}
|
onClick={() => changeSort(SORT_BY.POPULARITY)}
|
||||||
className={classnames(`button-toggle`, {
|
className={classnames(`button-toggle`, {
|
||||||
'button-toggle--active': sort === SORT_COMMENTS_BEST,
|
'button-toggle--active': sort === SORT_BY.POPULARITY,
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
|
@ -184,9 +241,9 @@ function CommentList(props: Props) {
|
||||||
label={__('Controversial')}
|
label={__('Controversial')}
|
||||||
icon={ICONS.CONTROVERSIAL}
|
icon={ICONS.CONTROVERSIAL}
|
||||||
iconSize={18}
|
iconSize={18}
|
||||||
onClick={() => setSort(SORT_COMMENTS_CONTROVERSIAL)}
|
onClick={() => changeSort(SORT_BY.CONTROVERSY)}
|
||||||
className={classnames(`button-toggle`, {
|
className={classnames(`button-toggle`, {
|
||||||
'button-toggle--active': sort === SORT_COMMENTS_CONTROVERSIAL,
|
'button-toggle--active': sort === SORT_BY.CONTROVERSY,
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
|
@ -194,9 +251,9 @@ function CommentList(props: Props) {
|
||||||
label={__('New')}
|
label={__('New')}
|
||||||
icon={ICONS.NEW}
|
icon={ICONS.NEW}
|
||||||
iconSize={18}
|
iconSize={18}
|
||||||
onClick={() => setSort(SORT_COMMENTS_NEW)}
|
onClick={() => changeSort(SORT_BY.NEWEST)}
|
||||||
className={classnames(`button-toggle`, {
|
className={classnames(`button-toggle`, {
|
||||||
'button-toggle--active': sort === SORT_COMMENTS_NEW,
|
'button-toggle--active': sort === SORT_BY.NEWEST,
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
|
@ -206,22 +263,21 @@ function CommentList(props: Props) {
|
||||||
icon={ICONS.REFRESH}
|
icon={ICONS.REFRESH}
|
||||||
title={__('Refresh')}
|
title={__('Refresh')}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
fetchComments(uri);
|
setPage(0);
|
||||||
fetchReacts(uri);
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
actions={
|
actions={
|
||||||
<>
|
<>
|
||||||
<CommentCreate uri={uri} justCommented={justCommented} />
|
<CommentCreate uri={uri} />
|
||||||
|
|
||||||
{!commentsDisabledBySettings && !isFetchingComments && hasNoComments && (
|
{!commentsDisabledBySettings && !isFetchingComments && hasNoComments && (
|
||||||
<Empty padded text={__('That was pretty deep. What do you think?')} />
|
<Empty padded text={__('That was pretty deep. What do you think?')} />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<ul className="comments" ref={commentRef}>
|
<ul className="comments" ref={commentRef}>
|
||||||
{comments &&
|
{topLevelComments &&
|
||||||
displayedComments &&
|
displayedComments &&
|
||||||
displayedComments.map((comment) => {
|
displayedComments.map((comment) => {
|
||||||
return (
|
return (
|
||||||
|
@ -238,9 +294,10 @@ function CommentList(props: Props) {
|
||||||
timePosted={comment.timestamp * 1000}
|
timePosted={comment.timestamp * 1000}
|
||||||
claimIsMine={claimIsMine}
|
claimIsMine={claimIsMine}
|
||||||
commentIsMine={comment.channel_id && isMyComment(comment.channel_id)}
|
commentIsMine={comment.channel_id && isMyComment(comment.channel_id)}
|
||||||
linkedComment={linkedComment}
|
linkedCommentId={linkedCommentId}
|
||||||
isPinned={comment.is_pinned}
|
isPinned={comment.is_pinned}
|
||||||
supportAmount={comment.support_amount}
|
supportAmount={comment.support_amount}
|
||||||
|
numDirectReplies={comment.replies}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
|
@ -1,14 +1,20 @@
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { makeSelectClaimIsMine, selectMyChannelClaims } from 'lbry-redux';
|
import { makeSelectClaimIsMine, selectMyChannelClaims } from 'lbry-redux';
|
||||||
import { makeSelectRepliesForParentId } from 'redux/selectors/comments';
|
import {
|
||||||
|
selectIsFetchingCommentsByParentId,
|
||||||
|
makeSelectRepliesForParentId,
|
||||||
|
makeSelectTotalRepliesForParentId,
|
||||||
|
} from 'redux/selectors/comments';
|
||||||
import { selectUserVerifiedEmail } from 'redux/selectors/user';
|
import { selectUserVerifiedEmail } from 'redux/selectors/user';
|
||||||
import CommentsReplies from './view';
|
import CommentsReplies from './view';
|
||||||
|
|
||||||
const select = (state, props) => ({
|
const select = (state, props) => ({
|
||||||
comments: makeSelectRepliesForParentId(props.parentId)(state),
|
fetchedReplies: makeSelectRepliesForParentId(props.parentId)(state),
|
||||||
|
totalReplies: makeSelectTotalRepliesForParentId(props.parentId)(state),
|
||||||
claimIsMine: makeSelectClaimIsMine(props.uri)(state),
|
claimIsMine: makeSelectClaimIsMine(props.uri)(state),
|
||||||
commentingEnabled: IS_WEB ? Boolean(selectUserVerifiedEmail(state)) : true,
|
commentingEnabled: IS_WEB ? Boolean(selectUserVerifiedEmail(state)) : true,
|
||||||
myChannels: selectMyChannelClaims(state),
|
myChannels: selectMyChannelClaims(state),
|
||||||
|
isFetchingByParentId: selectIsFetchingCommentsByParentId(state),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(select)(CommentsReplies);
|
export default connect(select)(CommentsReplies);
|
||||||
|
|
|
@ -3,32 +3,45 @@ import * as ICONS from 'constants/icons';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Comment from 'component/comment';
|
import Comment from 'component/comment';
|
||||||
import Button from 'component/button';
|
import Button from 'component/button';
|
||||||
|
import Spinner from 'component/spinner';
|
||||||
|
import ChannelThumbnail from 'component/channelThumbnail';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
comments: Array<any>,
|
fetchedReplies: Array<any>,
|
||||||
|
totalReplies: number,
|
||||||
uri: string,
|
uri: string,
|
||||||
|
parentId: string,
|
||||||
claimIsMine: boolean,
|
claimIsMine: boolean,
|
||||||
myChannels: ?Array<ChannelClaim>,
|
myChannels: ?Array<ChannelClaim>,
|
||||||
linkedComment?: Comment,
|
linkedCommentId?: string,
|
||||||
commentingEnabled: boolean,
|
commentingEnabled: boolean,
|
||||||
threadDepth: number,
|
threadDepth: number,
|
||||||
|
numDirectReplies: number,
|
||||||
|
isFetchingByParentId: { [string]: boolean },
|
||||||
|
onShowMore?: () => void,
|
||||||
};
|
};
|
||||||
|
|
||||||
function CommentsReplies(props: Props) {
|
function CommentsReplies(props: Props) {
|
||||||
const { uri, comments, claimIsMine, myChannels, linkedComment, commentingEnabled, threadDepth } = props;
|
const {
|
||||||
|
uri,
|
||||||
|
parentId,
|
||||||
|
fetchedReplies,
|
||||||
|
totalReplies,
|
||||||
|
claimIsMine,
|
||||||
|
myChannels,
|
||||||
|
linkedCommentId,
|
||||||
|
commentingEnabled,
|
||||||
|
threadDepth,
|
||||||
|
numDirectReplies,
|
||||||
|
isFetchingByParentId,
|
||||||
|
onShowMore,
|
||||||
|
} = props;
|
||||||
|
|
||||||
const [isExpanded, setExpanded] = React.useState(true);
|
const [isExpanded, setExpanded] = React.useState(true);
|
||||||
const [start, setStart] = React.useState(0);
|
|
||||||
const [end, setEnd] = React.useState(9);
|
|
||||||
const sortedComments = comments ? [...comments].reverse() : [];
|
|
||||||
const numberOfComments = comments ? comments.length : 0;
|
|
||||||
const linkedCommentId = linkedComment ? linkedComment.comment_id : '';
|
|
||||||
const commentsIndexOfLInked = comments && sortedComments.findIndex((e) => e.comment_id === linkedCommentId);
|
|
||||||
|
|
||||||
function showMore() {
|
function showMore() {
|
||||||
if (start > 0) {
|
if (onShowMore) {
|
||||||
setStart(0);
|
onShowMore();
|
||||||
} else {
|
|
||||||
setEnd(numberOfComments);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -44,35 +57,12 @@ function CommentsReplies(props: Props) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleCommentDone() {
|
const displayedComments = fetchedReplies;
|
||||||
if (!isExpanded) {
|
|
||||||
setExpanded(true);
|
|
||||||
setStart(numberOfComments || 0);
|
|
||||||
}
|
|
||||||
setEnd(numberOfComments + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
if (
|
|
||||||
setStart &&
|
|
||||||
setEnd &&
|
|
||||||
setExpanded &&
|
|
||||||
linkedCommentId &&
|
|
||||||
Number.isInteger(commentsIndexOfLInked) &&
|
|
||||||
commentsIndexOfLInked > -1
|
|
||||||
) {
|
|
||||||
setStart(commentsIndexOfLInked);
|
|
||||||
setEnd(commentsIndexOfLInked + 1);
|
|
||||||
setExpanded(true);
|
|
||||||
}
|
|
||||||
}, [setStart, setEnd, setExpanded, linkedCommentId, commentsIndexOfLInked]);
|
|
||||||
|
|
||||||
const displayedComments = sortedComments.slice(start, end);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
Boolean(numberOfComments) && (
|
Boolean(numDirectReplies) && (
|
||||||
<div className="comment__replies-container">
|
<div className="comment__replies-container">
|
||||||
{Boolean(numberOfComments) && !isExpanded && (
|
{Boolean(numDirectReplies) && !isExpanded && (
|
||||||
<div className="comment__actions--nested">
|
<div className="comment__actions--nested">
|
||||||
<Button
|
<Button
|
||||||
className="comment__action"
|
className="comment__action"
|
||||||
|
@ -82,13 +72,13 @@ function CommentsReplies(props: Props) {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{comments && displayedComments && isExpanded && (
|
{fetchedReplies && displayedComments && isExpanded && (
|
||||||
<div>
|
<div>
|
||||||
<div className="comment__replies">
|
<div className="comment__replies">
|
||||||
<Button className="comment__threadline" aria-label="Hide Replies" onClick={() => setExpanded(false)} />
|
<Button className="comment__threadline" aria-label="Hide Replies" onClick={() => setExpanded(false)} />
|
||||||
|
|
||||||
<ul className="comments--replies">
|
<ul className="comments--replies">
|
||||||
{displayedComments.map((comment, index) => {
|
{displayedComments.map((comment) => {
|
||||||
return (
|
return (
|
||||||
<Comment
|
<Comment
|
||||||
threadDepth={threadDepth}
|
threadDepth={threadDepth}
|
||||||
|
@ -102,22 +92,46 @@ function CommentsReplies(props: Props) {
|
||||||
timePosted={comment.timestamp * 1000}
|
timePosted={comment.timestamp * 1000}
|
||||||
claimIsMine={claimIsMine}
|
claimIsMine={claimIsMine}
|
||||||
commentIsMine={comment.channel_id && isMyComment(comment.channel_id)}
|
commentIsMine={comment.channel_id && isMyComment(comment.channel_id)}
|
||||||
linkedComment={linkedComment}
|
linkedCommentId={linkedCommentId}
|
||||||
commentingEnabled={commentingEnabled}
|
commentingEnabled={commentingEnabled}
|
||||||
handleCommentDone={handleCommentDone}
|
|
||||||
supportAmount={comment.support_amount}
|
supportAmount={comment.support_amount}
|
||||||
|
numDirectReplies={comment.replies}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
{totalReplies < numDirectReplies && (
|
||||||
|
<li className="comment comment--reply">
|
||||||
|
<div className="comment__content">
|
||||||
|
<div className="comment__thumbnail-wrapper">
|
||||||
|
<ChannelThumbnail xsmall className="comment__author-thumbnail" />
|
||||||
|
</div>
|
||||||
|
<div className="comment__body-container comment--blocked">
|
||||||
|
<div className="comment__meta">
|
||||||
|
<em>---</em>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<em>{__('Comment(s) blocked.')}</em>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{isExpanded && comments && (end < numberOfComments || start > 0) && (
|
{isExpanded && fetchedReplies && displayedComments.length < totalReplies && (
|
||||||
<div className="comment__actions">
|
<div className="comment__actions--nested">
|
||||||
<Button button="link" label={__('Show more')} onClick={showMore} className="button--uri-indicator" />
|
<Button button="link" label={__('Show more')} onClick={showMore} className="button--uri-indicator" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{isFetchingByParentId[parentId] && (
|
||||||
|
<div className="comment__replies-container">
|
||||||
|
<div className="comment__actions--nested">
|
||||||
|
<Spinner type="small" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
|
@ -15,14 +15,13 @@ import analytics from 'analytics';
|
||||||
type Props = {
|
type Props = {
|
||||||
claim: ?ChannelClaim,
|
claim: ?ChannelClaim,
|
||||||
fetchingChannels: boolean,
|
fetchingChannels: boolean,
|
||||||
prepareEdit: string => void,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const UNAUTHENTICATED_ERROR = 'unauthenticated';
|
const UNAUTHENTICATED_ERROR = 'unauthenticated';
|
||||||
const GENERIC_ERROR = 'error';
|
const GENERIC_ERROR = 'error';
|
||||||
|
|
||||||
export default function CreatorAnalytics(props: Props) {
|
export default function CreatorAnalytics(props: Props) {
|
||||||
const { prepareEdit, claim } = props;
|
const { claim } = props;
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const [stats, setStats] = React.useState();
|
const [stats, setStats] = React.useState();
|
||||||
const [error, setError] = React.useState();
|
const [error, setError] = React.useState();
|
||||||
|
@ -39,11 +38,11 @@ export default function CreatorAnalytics(props: Props) {
|
||||||
if (claimId && channelForEffect && channelHasClaims) {
|
if (claimId && channelForEffect && channelHasClaims) {
|
||||||
setFetchingStats(true);
|
setFetchingStats(true);
|
||||||
Lbryio.call('reports', 'content', { claim_id: claimId })
|
Lbryio.call('reports', 'content', { claim_id: claimId })
|
||||||
.then(res => {
|
.then((res) => {
|
||||||
setFetchingStats(false);
|
setFetchingStats(false);
|
||||||
setStats(res);
|
setStats(res);
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch((error) => {
|
||||||
if (error.response.status === 401) {
|
if (error.response.status === 401) {
|
||||||
setError(UNAUTHENTICATED_ERROR);
|
setError(UNAUTHENTICATED_ERROR);
|
||||||
const channelToSend = JSON.parse(channelForEffect);
|
const channelToSend = JSON.parse(channelForEffect);
|
||||||
|
@ -218,12 +217,7 @@ export default function CreatorAnalytics(props: Props) {
|
||||||
button="primary"
|
button="primary"
|
||||||
icon={ICONS.PUBLISH}
|
icon={ICONS.PUBLISH}
|
||||||
label={__('Upload')}
|
label={__('Upload')}
|
||||||
onClick={() => {
|
onClick={() => history.push(`/$/${PAGES.UPLOAD}`)}
|
||||||
if (claim) {
|
|
||||||
prepareEdit(claim.name);
|
|
||||||
history.push(`/$/${PAGES.UPLOAD}`);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
26
ui/component/fileWatchLaterLink/index.js
Normal file
26
ui/component/fileWatchLaterLink/index.js
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import {
|
||||||
|
makeSelectClaimForUri,
|
||||||
|
COLLECTIONS_CONSTS,
|
||||||
|
makeSelectCollectionForIdHasClaimUrl,
|
||||||
|
doCollectionEdit,
|
||||||
|
} from 'lbry-redux';
|
||||||
|
import FileWatchLaterLink from './view';
|
||||||
|
import { doToast } from 'redux/actions/notifications';
|
||||||
|
|
||||||
|
const select = (state, props) => {
|
||||||
|
const claim = makeSelectClaimForUri(props.uri)(state);
|
||||||
|
const permanentUri = claim && claim.permanent_url;
|
||||||
|
|
||||||
|
return {
|
||||||
|
claim,
|
||||||
|
hasClaimInWatchLater: makeSelectCollectionForIdHasClaimUrl(COLLECTIONS_CONSTS.WATCH_LATER_ID, permanentUri)(state),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const perform = dispatch => ({
|
||||||
|
doToast: (props) => dispatch(doToast(props)),
|
||||||
|
doCollectionEdit: (collection, props) => dispatch(doCollectionEdit(collection, props)),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(select, perform)(FileWatchLaterLink);
|
62
ui/component/fileWatchLaterLink/view.jsx
Normal file
62
ui/component/fileWatchLaterLink/view.jsx
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
// @flow
|
||||||
|
import * as ICONS from 'constants/icons';
|
||||||
|
import React, { useRef } from 'react';
|
||||||
|
import Button from 'component/button';
|
||||||
|
import useHover from 'effects/use-hover';
|
||||||
|
import { COLLECTIONS_CONSTS } from 'lbry-redux';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
uri: string,
|
||||||
|
claim: StreamClaim,
|
||||||
|
hasClaimInWatchLater: boolean,
|
||||||
|
doToast: ({ message: string }) => void,
|
||||||
|
doCollectionEdit: (string, any) => void,
|
||||||
|
};
|
||||||
|
|
||||||
|
function FileWatchLaterLink(props: Props) {
|
||||||
|
const {
|
||||||
|
claim,
|
||||||
|
hasClaimInWatchLater,
|
||||||
|
doToast,
|
||||||
|
doCollectionEdit,
|
||||||
|
} = props;
|
||||||
|
const buttonRef = useRef();
|
||||||
|
let isHovering = useHover(buttonRef);
|
||||||
|
|
||||||
|
if (!claim) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleWatchLater(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
doToast({
|
||||||
|
message: __('Item %action% Watch Later', {
|
||||||
|
action: hasClaimInWatchLater ? __('removed from') : __('added to'),
|
||||||
|
}),
|
||||||
|
linkText: !hasClaimInWatchLater && __('See All'),
|
||||||
|
linkTarget: !hasClaimInWatchLater && '/list/watchlater',
|
||||||
|
});
|
||||||
|
doCollectionEdit(COLLECTIONS_CONSTS.WATCH_LATER_ID, {
|
||||||
|
claims: [claim],
|
||||||
|
remove: hasClaimInWatchLater,
|
||||||
|
type: 'playlist',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const title = hasClaimInWatchLater ? __('Remove from Watch Later') : __('Add to Watch Later');
|
||||||
|
const label = !hasClaimInWatchLater ? __('Add') : __('Added');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
ref={buttonRef}
|
||||||
|
requiresAuth={IS_WEB}
|
||||||
|
title={title}
|
||||||
|
label={label}
|
||||||
|
className="button--file-action"
|
||||||
|
icon={(hasClaimInWatchLater && (isHovering ? ICONS.REMOVE : ICONS.COMPLETED)) || (isHovering ? ICONS.COMPLETED : ICONS.TIME)}
|
||||||
|
onClick={(e) => handleWatchLater(e)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FileWatchLaterLink;
|
|
@ -17,7 +17,7 @@ type Props = {
|
||||||
embed?: boolean,
|
embed?: boolean,
|
||||||
doCommentSocketConnect: (string, string) => void,
|
doCommentSocketConnect: (string, string) => void,
|
||||||
doCommentSocketDisconnect: (string) => void,
|
doCommentSocketDisconnect: (string) => void,
|
||||||
doCommentList: (string, number, number) => void,
|
doCommentList: (string, string, number, number) => void,
|
||||||
comments: Array<Comment>,
|
comments: Array<Comment>,
|
||||||
fetchingComments: boolean,
|
fetchingComments: boolean,
|
||||||
doSuperChatList: (string) => void,
|
doSuperChatList: (string) => void,
|
||||||
|
@ -68,7 +68,7 @@ export default function LivestreamComments(props: Props) {
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (claimId) {
|
if (claimId) {
|
||||||
doCommentList(uri, 1, 75);
|
doCommentList(uri, '', 1, 75);
|
||||||
doSuperChatList(uri);
|
doSuperChatList(uri);
|
||||||
doCommentSocketConnect(uri, claimId);
|
doCommentSocketConnect(uri, claimId);
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,7 +13,7 @@ type Props = {
|
||||||
label: ?string,
|
label: ?string,
|
||||||
reward: Reward,
|
reward: Reward,
|
||||||
button: ?boolean,
|
button: ?boolean,
|
||||||
claimReward: Reward => void,
|
claimReward: (Reward) => void,
|
||||||
};
|
};
|
||||||
|
|
||||||
const RewardLink = (props: Props) => {
|
const RewardLink = (props: Props) => {
|
||||||
|
@ -36,6 +36,7 @@ const RewardLink = (props: Props) => {
|
||||||
button={button ? 'primary' : 'link'}
|
button={button ? 'primary' : 'link'}
|
||||||
disabled={isPending}
|
disabled={isPending}
|
||||||
label={<LbcMessage>{displayLabel}</LbcMessage>}
|
label={<LbcMessage>{displayLabel}</LbcMessage>}
|
||||||
|
aria-label={displayLabel}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
claimReward(reward);
|
claimReward(reward);
|
||||||
}}
|
}}
|
||||||
|
|
|
@ -8,6 +8,7 @@ import { LINKED_COMMENT_QUERY_PARAM } from 'constants/comment';
|
||||||
import { parseURI, isURIValid } from 'lbry-redux';
|
import { parseURI, isURIValid } from 'lbry-redux';
|
||||||
import { SITE_TITLE, WELCOME_VERSION, SIMPLE_SITE } from 'config';
|
import { SITE_TITLE, WELCOME_VERSION, SIMPLE_SITE } from 'config';
|
||||||
import LoadingBarOneOff from 'component/loadingBarOneOff';
|
import LoadingBarOneOff from 'component/loadingBarOneOff';
|
||||||
|
import { GetLinksData } from 'util/buildHomepage';
|
||||||
|
|
||||||
import HomePage from 'page/home';
|
import HomePage from 'page/home';
|
||||||
|
|
||||||
|
@ -21,7 +22,9 @@ const Code2257Page = lazyImport(() => import('web/page/code2257' /* webpackChunk
|
||||||
|
|
||||||
// Chunk: "secondary"
|
// Chunk: "secondary"
|
||||||
const SignInPage = lazyImport(() => import('page/signIn' /* webpackChunkName: "secondary" */));
|
const SignInPage = lazyImport(() => import('page/signIn' /* webpackChunkName: "secondary" */));
|
||||||
const SignInWalletPasswordPage = lazyImport(() => import('page/signInWalletPassword' /* webpackChunkName: "secondary" */));
|
const SignInWalletPasswordPage = lazyImport(() =>
|
||||||
|
import('page/signInWalletPassword' /* webpackChunkName: "secondary" */)
|
||||||
|
);
|
||||||
const SignUpPage = lazyImport(() => import('page/signUp' /* webpackChunkName: "secondary" */));
|
const SignUpPage = lazyImport(() => import('page/signUp' /* webpackChunkName: "secondary" */));
|
||||||
const SignInVerifyPage = lazyImport(() => import('page/signInVerify' /* webpackChunkName: "secondary" */));
|
const SignInVerifyPage = lazyImport(() => import('page/signInVerify' /* webpackChunkName: "secondary" */));
|
||||||
|
|
||||||
|
@ -36,7 +39,9 @@ const WalletPage = lazyImport(() => import('page/wallet' /* webpackChunkName: "s
|
||||||
const NotificationsPage = lazyImport(() => import('page/notifications' /* webpackChunkName: "secondary" */));
|
const NotificationsPage = lazyImport(() => import('page/notifications' /* webpackChunkName: "secondary" */));
|
||||||
const CollectionPage = lazyImport(() => import('page/collection' /* webpackChunkName: "secondary" */));
|
const CollectionPage = lazyImport(() => import('page/collection' /* webpackChunkName: "secondary" */));
|
||||||
const ChannelNew = lazyImport(() => import('page/channelNew' /* webpackChunkName: "secondary" */));
|
const ChannelNew = lazyImport(() => import('page/channelNew' /* webpackChunkName: "secondary" */));
|
||||||
const ChannelsFollowingDiscoverPage = lazyImport(() => import('page/channelsFollowingDiscover' /* webpackChunkName: "secondary" */));
|
const ChannelsFollowingDiscoverPage = lazyImport(() =>
|
||||||
|
import('page/channelsFollowingDiscover' /* webpackChunkName: "secondary" */)
|
||||||
|
);
|
||||||
const ChannelsFollowingPage = lazyImport(() => import('page/channelsFollowing' /* webpackChunkName: "secondary" */));
|
const ChannelsFollowingPage = lazyImport(() => import('page/channelsFollowing' /* webpackChunkName: "secondary" */));
|
||||||
const ChannelsPage = lazyImport(() => import('page/channels' /* webpackChunkName: "secondary" */));
|
const ChannelsPage = lazyImport(() => import('page/channels' /* webpackChunkName: "secondary" */));
|
||||||
const CheckoutPage = lazyImport(() => import('page/checkoutPage' /* webpackChunkName: "checkoutPage" */));
|
const CheckoutPage = lazyImport(() => import('page/checkoutPage' /* webpackChunkName: "checkoutPage" */));
|
||||||
|
@ -65,10 +70,14 @@ const SearchPage = lazyImport(() => import('page/search' /* webpackChunkName: "s
|
||||||
const SettingsAdvancedPage = lazyImport(() => import('page/settingsAdvanced' /* webpackChunkName: "secondary" */));
|
const SettingsAdvancedPage = lazyImport(() => import('page/settingsAdvanced' /* webpackChunkName: "secondary" */));
|
||||||
const SettingsStripeCard = lazyImport(() => import('page/settingsStripeCard' /* webpackChunkName: "secondary" */));
|
const SettingsStripeCard = lazyImport(() => import('page/settingsStripeCard' /* webpackChunkName: "secondary" */));
|
||||||
const SettingsCreatorPage = lazyImport(() => import('page/settingsCreator' /* webpackChunkName: "secondary" */));
|
const SettingsCreatorPage = lazyImport(() => import('page/settingsCreator' /* webpackChunkName: "secondary" */));
|
||||||
const SettingsNotificationsPage = lazyImport(() => import('page/settingsNotifications' /* webpackChunkName: "secondary" */));
|
const SettingsNotificationsPage = lazyImport(() =>
|
||||||
|
import('page/settingsNotifications' /* webpackChunkName: "secondary" */)
|
||||||
|
);
|
||||||
const SettingsPage = lazyImport(() => import('page/settings' /* webpackChunkName: "secondary" */));
|
const SettingsPage = lazyImport(() => import('page/settings' /* webpackChunkName: "secondary" */));
|
||||||
const ShowPage = lazyImport(() => import('page/show' /* webpackChunkName: "secondary" */));
|
const ShowPage = lazyImport(() => import('page/show' /* webpackChunkName: "secondary" */));
|
||||||
const TagsFollowingManagePage = lazyImport(() => import('page/tagsFollowingManage' /* webpackChunkName: "secondary" */));
|
const TagsFollowingManagePage = lazyImport(() =>
|
||||||
|
import('page/tagsFollowingManage' /* webpackChunkName: "secondary" */)
|
||||||
|
);
|
||||||
const TagsFollowingPage = lazyImport(() => import('page/tagsFollowing' /* webpackChunkName: "secondary" */));
|
const TagsFollowingPage = lazyImport(() => import('page/tagsFollowing' /* webpackChunkName: "secondary" */));
|
||||||
const TopPage = lazyImport(() => import('page/top' /* webpackChunkName: "secondary" */));
|
const TopPage = lazyImport(() => import('page/top' /* webpackChunkName: "secondary" */));
|
||||||
const Welcome = lazyImport(() => import('page/welcome' /* webpackChunkName: "secondary" */));
|
const Welcome = lazyImport(() => import('page/welcome' /* webpackChunkName: "secondary" */));
|
||||||
|
@ -150,7 +159,7 @@ function AppRouter(props: Props) {
|
||||||
const resetScroll = urlParams.get('reset_scroll');
|
const resetScroll = urlParams.get('reset_scroll');
|
||||||
const hasLinkedCommentInUrl = urlParams.get(LINKED_COMMENT_QUERY_PARAM);
|
const hasLinkedCommentInUrl = urlParams.get(LINKED_COMMENT_QUERY_PARAM);
|
||||||
|
|
||||||
const dynamicRoutes = Object.values(homepageData).filter(
|
const dynamicRoutes = GetLinksData(homepageData).filter(
|
||||||
(potentialRoute: any) => potentialRoute && potentialRoute.route
|
(potentialRoute: any) => potentialRoute && potentialRoute.route
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -1,14 +1,13 @@
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { doUpdateSearchOptions } from 'redux/actions/search';
|
import { doUpdateSearchOptions } from 'redux/actions/search';
|
||||||
import { selectSearchOptions, makeSelectQueryWithOptions } from 'redux/selectors/search';
|
import { selectSearchOptions } from 'redux/selectors/search';
|
||||||
import { doToggleSearchExpanded } from 'redux/actions/app';
|
import { doToggleSearchExpanded } from 'redux/actions/app';
|
||||||
import { selectSearchOptionsExpanded } from 'redux/selectors/app';
|
import { selectSearchOptionsExpanded } from 'redux/selectors/app';
|
||||||
import SearchOptions from './view';
|
import SearchOptions from './view';
|
||||||
|
|
||||||
const select = state => ({
|
const select = (state) => ({
|
||||||
options: selectSearchOptions(state),
|
options: selectSearchOptions(state),
|
||||||
expanded: selectSearchOptionsExpanded(state),
|
expanded: selectSearchOptionsExpanded(state),
|
||||||
query: makeSelectQueryWithOptions(undefined, {})(state),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const perform = (dispatch, ownProps) => {
|
const perform = (dispatch, ownProps) => {
|
||||||
|
|
|
@ -9,7 +9,8 @@ import Icon from 'component/common/icon';
|
||||||
import NotificationBubble from 'component/notificationBubble';
|
import NotificationBubble from 'component/notificationBubble';
|
||||||
import I18nMessage from 'component/i18nMessage';
|
import I18nMessage from 'component/i18nMessage';
|
||||||
import ChannelThumbnail from 'component/channelThumbnail';
|
import ChannelThumbnail from 'component/channelThumbnail';
|
||||||
import { PINNED_LABEL_1, PINNED_URI_1, PINNED_URI_2, PINNED_LABEL_2, SIMPLE_SITE, DOMAIN, ENABLE_UI_NOTIFICATIONS } from 'config';
|
import { GetLinksData } from 'util/buildHomepage';
|
||||||
|
import { SIMPLE_SITE, DOMAIN, ENABLE_UI_NOTIFICATIONS } from 'config';
|
||||||
// @if TARGET='app'
|
// @if TARGET='app'
|
||||||
import { IS_MAC } from 'component/app/view';
|
import { IS_MAC } from 'component/app/view';
|
||||||
// @endif
|
// @endif
|
||||||
|
@ -76,8 +77,7 @@ function SideNavigation(props: Props) {
|
||||||
followedTags,
|
followedTags,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const { EXTRA_SIDEBAR_LINKS } = homepageData;
|
const EXTRA_SIDEBAR_LINKS = GetLinksData(homepageData);
|
||||||
|
|
||||||
const FULL_LINKS: Array<SideNavLink> = [
|
const FULL_LINKS: Array<SideNavLink> = [
|
||||||
{
|
{
|
||||||
title: 'Your Tags',
|
title: 'Your Tags',
|
||||||
|
@ -197,22 +197,6 @@ function SideNavigation(props: Props) {
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
if (PINNED_URI_1 && PINNED_LABEL_1) {
|
|
||||||
MOBILE_LINKS.push({
|
|
||||||
title: PINNED_LABEL_1,
|
|
||||||
link: PINNED_URI_1,
|
|
||||||
icon: ICONS.PINNED,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (PINNED_URI_2 && PINNED_LABEL_2) {
|
|
||||||
MOBILE_LINKS.push({
|
|
||||||
title: PINNED_LABEL_2,
|
|
||||||
link: PINNED_URI_2,
|
|
||||||
icon: ICONS.PINNED,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const notificationsEnabled = ENABLE_UI_NOTIFICATIONS || (user && user.experimental_ui);
|
const notificationsEnabled = ENABLE_UI_NOTIFICATIONS || (user && user.experimental_ui);
|
||||||
const isAuthenticated = Boolean(email);
|
const isAuthenticated = Boolean(email);
|
||||||
// SIDE LINKS: FOLLOWING, HOME, [FULL,] [EXTRA]
|
// SIDE LINKS: FOLLOWING, HOME, [FULL,] [EXTRA]
|
||||||
|
@ -239,8 +223,17 @@ function SideNavigation(props: Props) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (EXTRA_SIDEBAR_LINKS) {
|
if (SIMPLE_SITE && EXTRA_SIDEBAR_LINKS) {
|
||||||
|
// $FlowFixMe
|
||||||
SIDE_LINKS.push(...EXTRA_SIDEBAR_LINKS);
|
SIDE_LINKS.push(...EXTRA_SIDEBAR_LINKS);
|
||||||
|
|
||||||
|
const WILD_WEST = {
|
||||||
|
title: 'Wild West',
|
||||||
|
link: `/$/${PAGES.WILD_WEST}`,
|
||||||
|
icon: ICONS.WILD_WEST,
|
||||||
|
};
|
||||||
|
|
||||||
|
SIDE_LINKS.push(WILD_WEST);
|
||||||
}
|
}
|
||||||
|
|
||||||
const [pulseLibrary, setPulseLibrary] = React.useState(false);
|
const [pulseLibrary, setPulseLibrary] = React.useState(false);
|
||||||
|
@ -275,7 +268,7 @@ function SideNavigation(props: Props) {
|
||||||
if (e.keyCode === ESCAPE_KEY_CODE && isAbsolute) {
|
if (e.keyCode === ESCAPE_KEY_CODE && isAbsolute) {
|
||||||
setSidebarOpen(false);
|
setSidebarOpen(false);
|
||||||
} else if (e.keyCode === BACKSLASH_KEY_CODE) {
|
} else if (e.keyCode === BACKSLASH_KEY_CODE) {
|
||||||
const hasActiveInput = document.querySelector('input:focus');
|
const hasActiveInput = document.querySelector('input:focus, textarea:focus');
|
||||||
if (!hasActiveInput) {
|
if (!hasActiveInput) {
|
||||||
setSidebarOpen(!sidebarOpen);
|
setSidebarOpen(!sidebarOpen);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,52 @@
|
||||||
// Created by xander on 6/21/2021
|
// Created by xander on 6/21/2021
|
||||||
import videojs from 'video.js';
|
import videojs from 'video.js';
|
||||||
|
import 'videojs-contrib-ads';
|
||||||
import 'videojs-ima';
|
import 'videojs-ima';
|
||||||
const VERSION = '0.0.1';
|
const VERSION = '0.0.1';
|
||||||
|
|
||||||
|
/* Macro provided by aniview
|
||||||
|
* const macroUrl =
|
||||||
|
`https://vast.aniview.com/api/adserver61/vast/` +
|
||||||
|
`?AV_PUBLISHERID=60afcbc58cfdb065440d2426` +
|
||||||
|
`&AV_CHANNELID=60b354389c7adb506d0bd9a4` +
|
||||||
|
`&AV_URL=[URL_MACRO]` +
|
||||||
|
`&cb=[TIMESTAMP_MACRO]` +
|
||||||
|
`&AV_WIDTH=[WIDTH_MACRO]` +
|
||||||
|
`&AV_HEIGHT=[HEIGHT_MACRO]` +
|
||||||
|
`&AV_SCHAIN=[SCHAIN_MACRO]` +
|
||||||
|
`&AV_CCPA=[CCPA_MACRO]` +
|
||||||
|
`&AV_GDPR=[GDPR_MACRO]` +
|
||||||
|
`&AV_CONSENT=[CONSENT_MACRO]` +
|
||||||
|
`&skip=true` +
|
||||||
|
`&skiptimer=5` +
|
||||||
|
`&logo=false` +
|
||||||
|
`&usevslot=true` +
|
||||||
|
`&vastretry=3` +
|
||||||
|
`&hidecontrols=false`;
|
||||||
|
* */
|
||||||
|
|
||||||
|
// TEST PRE-ROLL WITH THIS TAG:
|
||||||
|
// https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/ad_rule_samples&ciu_szs=300x250&ad_rule=1&impl=s&gdfp_req=1&env=vp&output=vmap&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ar%3Dpreonly&cmsid=496&vid=short_onecue&correlator=
|
||||||
|
|
||||||
|
// Modified to work with IMA
|
||||||
const macroUrl =
|
const macroUrl =
|
||||||
'https://vast.aniview.com/api/adserver61/vast/?AV_PUBLISHERID=60afcbc58cfdb065440d2426&AV_CHANNELID=60b354389c7adb506d0bd9a4&AV_URL=[URL_MACRO]&cb=[TIMESTAMP_MACRO]&AV_WIDTH=[WIDTH_MACRO]&AV_HEIGHT=[HEIGHT_MACRO]&AV_SCHAIN=[SCHAIN_MACRO]&AV_CCPA=[CCPA_MACRO]&AV_GDPR=[GDPR_MACRO]&AV_CONSENT=[CONSENT_MACRO]&skip=true&skiptimer=5&usevslot=true&hidecontrols=false';
|
`https://vast.aniview.com/api/adserver61/vast/` +
|
||||||
|
`?AV_PUBLISHERID=60afcbc58cfdb065440d2426` +
|
||||||
|
`&AV_CHANNELID=60b354389c7adb506d0bd9a4` +
|
||||||
|
`&AV_URL=[URL]` +
|
||||||
|
`&cb=[CACHEBUSTING]` +
|
||||||
|
`&AV_WIDTH=[WIDTH]` +
|
||||||
|
`&AV_HEIGHT=[HEIGHT]` +
|
||||||
|
// `&AV_SCHAIN=[SCHAIN_MACRO]` +
|
||||||
|
// `&AV_CCPA=[CCPA_MACRO]` +
|
||||||
|
// `&AV_GDPR=[GDPR_MACRO]` +
|
||||||
|
// `&AV_CONSENT=[CONSENT_MACRO]` +
|
||||||
|
`&skip=true` +
|
||||||
|
`&skiptimer=5` +
|
||||||
|
`&logo=true` +
|
||||||
|
`&usevslot=true` +
|
||||||
|
`&vastretry=5` +
|
||||||
|
`&hidecontrols=false`;
|
||||||
|
|
||||||
const defaults = {
|
const defaults = {
|
||||||
adTagUrl: macroUrl,
|
adTagUrl: macroUrl,
|
||||||
|
@ -19,46 +61,24 @@ class AniviewPlugin extends Component {
|
||||||
super(player, options);
|
super(player, options);
|
||||||
|
|
||||||
// Plugin started
|
// Plugin started
|
||||||
if (options.debug) {
|
if (options.debug) this.log(`Created aniview plugin.`);
|
||||||
this.log(`Created aniview plugin.`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// To help with debugging, we'll add a global vjs object with the video js player
|
// To help with debugging, we'll add a global vjs object with the video js player
|
||||||
window.aniview = player;
|
window.aniview = player;
|
||||||
|
|
||||||
this.player = player;
|
this.player = player;
|
||||||
|
|
||||||
const google = window.google;
|
|
||||||
|
|
||||||
player.ima({
|
|
||||||
// adTagUrl: macroUrl,
|
|
||||||
id: 'ad_content_video',
|
|
||||||
vpaidMode: google.ima.ImaSdkSettings.VpaidMode.INSECURE,
|
|
||||||
adTagUrl:
|
|
||||||
'https://vast.aniview.com/api/adserver61/vast/?AV_PUBLISHERID=60afcbc58cfdb065440d2426&AV_CHANNELID=60b354389c7adb506d0bd9a4',
|
|
||||||
});
|
|
||||||
|
|
||||||
// this.player.ads();
|
|
||||||
// const serverUrl = this.player.ads.adMacroReplacement(macroUrl);
|
|
||||||
// this.log(serverUrl);
|
|
||||||
|
|
||||||
// request ads whenever there's new video content
|
// request ads whenever there's new video content
|
||||||
player.on('contentchanged', () => {
|
/* player.on('contentchanged', () => {
|
||||||
// in a real plugin, you might fetch your ad inventory here
|
// in a real plugin, you might fetch your ad inventory here
|
||||||
player.trigger('adsready');
|
player.trigger('adsready');
|
||||||
});
|
}); */
|
||||||
|
|
||||||
// Plugin event listeners
|
// Plugin event listeners
|
||||||
player.on('readyforpreroll', (event) => this.onReadyForPreroll(event));
|
// player.on('readyforpreroll', (event) => this.onReadyForPreroll(event));
|
||||||
}
|
}
|
||||||
|
|
||||||
onReadyForPreroll(event) {
|
/* onReadyForPreroll(event) {
|
||||||
this.player.ads.startLinearAdMode();
|
|
||||||
|
|
||||||
// play your linear ad content
|
|
||||||
// in this example, we use a static mp4
|
|
||||||
this.player.src('kitteh.mp4');
|
|
||||||
|
|
||||||
// send event when ad is playing to remove loading spinner
|
// send event when ad is playing to remove loading spinner
|
||||||
this.player.one('adplaying', () => {
|
this.player.one('adplaying', () => {
|
||||||
this.player.trigger('ads-ad-started');
|
this.player.trigger('ads-ad-started');
|
||||||
|
@ -68,7 +88,7 @@ class AniviewPlugin extends Component {
|
||||||
this.player.one('adended', () => {
|
this.player.one('adended', () => {
|
||||||
this.player.ads.endLinearAdMode();
|
this.player.ads.endLinearAdMode();
|
||||||
});
|
});
|
||||||
}
|
} */
|
||||||
|
|
||||||
log(...args) {
|
log(...args) {
|
||||||
if (this.options_.debug) {
|
if (this.options_.debug) {
|
||||||
|
@ -90,6 +110,14 @@ const onPlayerReady = (player, options) => {
|
||||||
* @param {Object} [options={}]
|
* @param {Object} [options={}]
|
||||||
*/
|
*/
|
||||||
const plugin = function (options) {
|
const plugin = function (options) {
|
||||||
|
const google = window.google;
|
||||||
|
|
||||||
|
this.ima({
|
||||||
|
// $FlowFixMe
|
||||||
|
vpaidMode: google.ima.ImaSdkSettings.VpaidMode.INSECURE,
|
||||||
|
adTagUrl: macroUrl,
|
||||||
|
});
|
||||||
|
|
||||||
this.ready(() => {
|
this.ready(() => {
|
||||||
onPlayerReady(this, videojs.mergeOptions(defaults, options));
|
onPlayerReady(this, videojs.mergeOptions(defaults, options));
|
||||||
});
|
});
|
||||||
|
|
|
@ -13,29 +13,9 @@ import hlsQualitySelector from './plugins/videojs-hls-quality-selector/plugin';
|
||||||
import recsys from './plugins/videojs-recsys/plugin';
|
import recsys from './plugins/videojs-recsys/plugin';
|
||||||
import qualityLevels from 'videojs-contrib-quality-levels';
|
import qualityLevels from 'videojs-contrib-quality-levels';
|
||||||
import isUserTyping from 'util/detect-typing';
|
import isUserTyping from 'util/detect-typing';
|
||||||
import 'videojs-contrib-ads';
|
import './plugins/videojs-aniview/plugin';
|
||||||
import 'videojs-ima';
|
|
||||||
// import aniview from './plugins/videojs-aniview/plugin';
|
|
||||||
|
|
||||||
const isDev = process.env.NODE_ENV !== 'production';
|
const isDev = process.env.NODE_ENV !== 'production';
|
||||||
const macroUrl =
|
|
||||||
`https://vast.aniview.com/api/adserver61/vast/` +
|
|
||||||
`?AV_PUBLISHERID=60afcbc58cfdb065440d2426` +
|
|
||||||
`&AV_CHANNELID=60b354389c7adb506d0bd9a4` +
|
|
||||||
`&AV_URL=[URL_MACRO]` +
|
|
||||||
`&cb=[TIMESTAMP_MACRO]` +
|
|
||||||
`&AV_WIDTH=[WIDTH_MACRO]` +
|
|
||||||
`&AV_HEIGHT=[HEIGHT_MACRO]` +
|
|
||||||
`&AV_SCHAIN=[SCHAIN_MACRO]` +
|
|
||||||
`&AV_CCPA=[CCPA_MACRO]` +
|
|
||||||
`&AV_GDPR=[GDPR_MACRO]` +
|
|
||||||
`&AV_CONSENT=[CONSENT_MACRO]` +
|
|
||||||
`&skip=true` +
|
|
||||||
`&skiptimer=5` +
|
|
||||||
`&logo=false` +
|
|
||||||
`&usevslot=true` +
|
|
||||||
`&vastretry=3` +
|
|
||||||
`&hidecontrols=false`;
|
|
||||||
|
|
||||||
export type Player = {
|
export type Player = {
|
||||||
on: (string, (any) => void) => void,
|
on: (string, (any) => void) => void,
|
||||||
|
@ -605,14 +585,9 @@ export default React.memo<Props>(function VideoJs(props: Props) {
|
||||||
// pre-roll ads
|
// pre-roll ads
|
||||||
// This must be initialized earlier than everything else
|
// This must be initialized earlier than everything else
|
||||||
// otherwise a race condition occurs if we place this in the onReady call back
|
// otherwise a race condition occurs if we place this in the onReady call back
|
||||||
if (allowPreRoll && SIMPLE_SITE && window.google) {
|
// allow if isDev because otherwise you'll never see ads when basing to master
|
||||||
const google = window.google;
|
if ((allowPreRoll && SIMPLE_SITE) || isDev) {
|
||||||
// player.aniview();
|
vjs.aniview();
|
||||||
vjs.ima({
|
|
||||||
// $FlowFixMe
|
|
||||||
vpaidMode: google.ima.ImaSdkSettings.VpaidMode.INSECURE,
|
|
||||||
adTagUrl: macroUrl,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// fixes #3498 (https://github.com/lbryio/lbry-desktop/issues/3498)
|
// fixes #3498 (https://github.com/lbryio/lbry-desktop/issues/3498)
|
||||||
|
|
|
@ -2,7 +2,6 @@ import * as MODALS from 'constants/modal_types';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { selectLanguage, selectShowMatureContent } from 'redux/selectors/settings';
|
import { selectLanguage, selectShowMatureContent } from 'redux/selectors/settings';
|
||||||
import { doToast } from 'redux/actions/notifications';
|
import { doToast } from 'redux/actions/notifications';
|
||||||
import { doSearch } from 'redux/actions/search';
|
|
||||||
import { doOpenModal, doHideModal } from 'redux/actions/app';
|
import { doOpenModal, doHideModal } from 'redux/actions/app';
|
||||||
import { withRouter } from 'react-router';
|
import { withRouter } from 'react-router';
|
||||||
import { doResolveUris } from 'lbry-redux';
|
import { doResolveUris } from 'lbry-redux';
|
||||||
|
@ -16,7 +15,6 @@ const select = (state, props) => ({
|
||||||
|
|
||||||
const perform = (dispatch, ownProps) => ({
|
const perform = (dispatch, ownProps) => ({
|
||||||
doResolveUris: (uris) => dispatch(doResolveUris(uris)),
|
doResolveUris: (uris) => dispatch(doResolveUris(uris)),
|
||||||
doSearch: (query, options) => dispatch(doSearch(query, options)),
|
|
||||||
navigateToSearchPage: (query) => {
|
navigateToSearchPage: (query) => {
|
||||||
let encodedQuery = encodeURIComponent(query);
|
let encodedQuery = encodeURIComponent(query);
|
||||||
ownProps.history.push({ pathname: `/$/search`, search: `?q=${encodedQuery}` });
|
ownProps.history.push({ pathname: `/$/search`, search: `?q=${encodedQuery}` });
|
||||||
|
|
|
@ -248,6 +248,8 @@ export const DISMISS_ERROR = 'DISMISS_ERROR';
|
||||||
export const COMMENT_LIST_STARTED = 'COMMENT_LIST_STARTED';
|
export const COMMENT_LIST_STARTED = 'COMMENT_LIST_STARTED';
|
||||||
export const COMMENT_LIST_COMPLETED = 'COMMENT_LIST_COMPLETED';
|
export const COMMENT_LIST_COMPLETED = 'COMMENT_LIST_COMPLETED';
|
||||||
export const COMMENT_LIST_FAILED = 'COMMENT_LIST_FAILED';
|
export const COMMENT_LIST_FAILED = 'COMMENT_LIST_FAILED';
|
||||||
|
export const COMMENT_LIST_RESET = 'COMMENT_LIST_RESET';
|
||||||
|
export const COMMENT_BY_ID_COMPLETED = 'COMMENT_BY_ID_COMPLETED';
|
||||||
export const COMMENT_CREATE_STARTED = 'COMMENT_CREATE_STARTED';
|
export const COMMENT_CREATE_STARTED = 'COMMENT_CREATE_STARTED';
|
||||||
export const COMMENT_CREATE_COMPLETED = 'COMMENT_CREATE_COMPLETED';
|
export const COMMENT_CREATE_COMPLETED = 'COMMENT_CREATE_COMPLETED';
|
||||||
export const COMMENT_CREATE_FAILED = 'COMMENT_CREATE_FAILED';
|
export const COMMENT_CREATE_FAILED = 'COMMENT_CREATE_FAILED';
|
||||||
|
|
|
@ -4,8 +4,18 @@ export const SORT_COMMENTS_NEW = 'new';
|
||||||
export const SORT_COMMENTS_BEST = 'best';
|
export const SORT_COMMENTS_BEST = 'best';
|
||||||
export const SORT_COMMENTS_CONTROVERSIAL = 'controversial';
|
export const SORT_COMMENTS_CONTROVERSIAL = 'controversial';
|
||||||
|
|
||||||
|
export const SORT_BY = {
|
||||||
|
NEWEST: 0,
|
||||||
|
OLDEST: 1,
|
||||||
|
CONTROVERSY: 2,
|
||||||
|
POPULARITY: 3,
|
||||||
|
};
|
||||||
|
|
||||||
export const BLOCK_LEVEL = {
|
export const BLOCK_LEVEL = {
|
||||||
SELF: 'self',
|
SELF: 'self',
|
||||||
MODERATOR: 'moderator',
|
MODERATOR: 'moderator',
|
||||||
ADMIN: 'admin',
|
ADMIN: 'admin',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const COMMENT_PAGE_SIZE_TOP_LEVEL = 10;
|
||||||
|
export const COMMENT_PAGE_SIZE_REPLIES = 10;
|
||||||
|
|
|
@ -111,7 +111,10 @@ export default function CollectionPage(props: Props) {
|
||||||
<Card
|
<Card
|
||||||
title={
|
title={
|
||||||
<span>
|
<span>
|
||||||
<Icon icon={ICONS.STACK} className="icon--margin-right" />
|
<Icon
|
||||||
|
icon={(collectionId === COLLECTIONS_CONSTS.WATCH_LATER_ID && ICONS.TIME) ||
|
||||||
|
(collectionId === COLLECTIONS_CONSTS.FAVORITES_ID && ICONS.STAR) || ICONS.STACK}
|
||||||
|
className="icon--margin-right" />
|
||||||
{claim ? claim.value.title || claim.name : collection && collection.name}
|
{claim ? claim.value.title || claim.name : collection && collection.name}
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,7 +16,6 @@ import {
|
||||||
import { makeSelectCostInfoForUri, doFetchCostInfoForUri } from 'lbryinc';
|
import { makeSelectCostInfoForUri, doFetchCostInfoForUri } from 'lbryinc';
|
||||||
import { selectShowMatureContent, makeSelectClientSetting } from 'redux/selectors/settings';
|
import { selectShowMatureContent, makeSelectClientSetting } from 'redux/selectors/settings';
|
||||||
import { makeSelectFileRenderModeForUri } from 'redux/selectors/content';
|
import { makeSelectFileRenderModeForUri } from 'redux/selectors/content';
|
||||||
import { makeSelectCommentForCommentId } from 'redux/selectors/comments';
|
|
||||||
import { DISABLE_COMMENTS_TAG } from 'constants/tags';
|
import { DISABLE_COMMENTS_TAG } from 'constants/tags';
|
||||||
|
|
||||||
import FilePage from './view';
|
import FilePage from './view';
|
||||||
|
@ -24,11 +23,10 @@ import FilePage from './view';
|
||||||
const select = (state, props) => {
|
const select = (state, props) => {
|
||||||
const { search } = props.location;
|
const { search } = props.location;
|
||||||
const urlParams = new URLSearchParams(search);
|
const urlParams = new URLSearchParams(search);
|
||||||
const linkedCommentId = urlParams.get('lc');
|
|
||||||
const collectionId = urlParams.get(COLLECTIONS_CONSTS.COLLECTION_ID);
|
const collectionId = urlParams.get(COLLECTIONS_CONSTS.COLLECTION_ID);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
linkedComment: makeSelectCommentForCommentId(linkedCommentId)(state),
|
linkedCommentId: urlParams.get('lc'),
|
||||||
costInfo: makeSelectCostInfoForUri(props.uri)(state),
|
costInfo: makeSelectCostInfoForUri(props.uri)(state),
|
||||||
metadata: makeSelectMetadataForUri(props.uri)(state),
|
metadata: makeSelectMetadataForUri(props.uri)(state),
|
||||||
obscureNsfw: !selectShowMatureContent(state),
|
obscureNsfw: !selectShowMatureContent(state),
|
||||||
|
|
|
@ -27,7 +27,7 @@ type Props = {
|
||||||
renderMode: string,
|
renderMode: string,
|
||||||
obscureNsfw: boolean,
|
obscureNsfw: boolean,
|
||||||
isMature: boolean,
|
isMature: boolean,
|
||||||
linkedComment: any,
|
linkedCommentId?: string,
|
||||||
setPrimaryUri: (?string) => void,
|
setPrimaryUri: (?string) => void,
|
||||||
collection?: Collection,
|
collection?: Collection,
|
||||||
collectionId: string,
|
collectionId: string,
|
||||||
|
@ -46,7 +46,7 @@ function FilePage(props: Props) {
|
||||||
obscureNsfw,
|
obscureNsfw,
|
||||||
isMature,
|
isMature,
|
||||||
costInfo,
|
costInfo,
|
||||||
linkedComment,
|
linkedCommentId,
|
||||||
setPrimaryUri,
|
setPrimaryUri,
|
||||||
videoTheaterMode,
|
videoTheaterMode,
|
||||||
commentsDisabled,
|
commentsDisabled,
|
||||||
|
@ -146,7 +146,7 @@ function FilePage(props: Props) {
|
||||||
<div>
|
<div>
|
||||||
{RENDER_MODES.FLOATING_MODES.includes(renderMode) && <FileTitleSection uri={uri} />}
|
{RENDER_MODES.FLOATING_MODES.includes(renderMode) && <FileTitleSection uri={uri} />}
|
||||||
{commentsDisabled && <Empty text={__('The creator of this content has disabled comments.')} />}
|
{commentsDisabled && <Empty text={__('The creator of this content has disabled comments.')} />}
|
||||||
{!commentsDisabled && <CommentsList uri={uri} linkedComment={linkedComment} />}
|
{!commentsDisabled && <CommentsList uri={uri} linkedCommentId={linkedCommentId} />}
|
||||||
</div>
|
</div>
|
||||||
{!collection && !isMarkdown && videoTheaterMode && <RecommendedContent uri={uri} />}
|
{!collection && !isMarkdown && videoTheaterMode && <RecommendedContent uri={uri} />}
|
||||||
{collection && !isMarkdown && videoTheaterMode && <CollectionContent id={collectionId} uri={uri} />}
|
{collection && !isMarkdown && videoTheaterMode && <CollectionContent id={collectionId} uri={uri} />}
|
||||||
|
@ -157,7 +157,7 @@ function FilePage(props: Props) {
|
||||||
{!collection && !isMarkdown && !videoTheaterMode && <RecommendedContent uri={uri} />}
|
{!collection && !isMarkdown && !videoTheaterMode && <RecommendedContent uri={uri} />}
|
||||||
{isMarkdown && (
|
{isMarkdown && (
|
||||||
<div className="file-page__post-comments">
|
<div className="file-page__post-comments">
|
||||||
<CommentsList uri={uri} linkedComment={linkedComment} />
|
<CommentsList uri={uri} linkedCommentId={linkedCommentId} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Page>
|
</Page>
|
||||||
|
|
|
@ -12,6 +12,11 @@ import I18nMessage from 'component/i18nMessage';
|
||||||
import LbcSymbol from 'component/common/lbc-symbol';
|
import LbcSymbol from 'component/common/lbc-symbol';
|
||||||
import WaitUntilOnPage from 'component/common/wait-until-on-page';
|
import WaitUntilOnPage from 'component/common/wait-until-on-page';
|
||||||
import useGetLivestreams from 'effects/use-get-livestreams';
|
import useGetLivestreams from 'effects/use-get-livestreams';
|
||||||
|
import { GetLinksData } from 'util/buildHomepage';
|
||||||
|
|
||||||
|
// @if TARGET='web'
|
||||||
|
import Pixel from 'web/component/pixel';
|
||||||
|
// @endif
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
authenticated: boolean,
|
authenticated: boolean,
|
||||||
|
@ -26,10 +31,11 @@ function HomePage(props: Props) {
|
||||||
const showPersonalizedChannels = (authenticated || !IS_WEB) && subscribedChannels && subscribedChannels.length > 0;
|
const showPersonalizedChannels = (authenticated || !IS_WEB) && subscribedChannels && subscribedChannels.length > 0;
|
||||||
const showPersonalizedTags = (authenticated || !IS_WEB) && followedTags && followedTags.length > 0;
|
const showPersonalizedTags = (authenticated || !IS_WEB) && followedTags && followedTags.length > 0;
|
||||||
const showIndividualTags = showPersonalizedTags && followedTags.length < 5;
|
const showIndividualTags = showPersonalizedTags && followedTags.length < 5;
|
||||||
const { default: getHomepage } = homepageData;
|
|
||||||
const { livestreamMap } = useGetLivestreams();
|
const { livestreamMap } = useGetLivestreams();
|
||||||
|
|
||||||
const rowData: Array<RowDataItem> = getHomepage(
|
const rowData: Array<RowDataItem> = GetLinksData(
|
||||||
|
homepageData,
|
||||||
|
true,
|
||||||
authenticated,
|
authenticated,
|
||||||
showPersonalizedChannels,
|
showPersonalizedChannels,
|
||||||
showPersonalizedTags,
|
showPersonalizedTags,
|
||||||
|
@ -54,7 +60,7 @@ function HomePage(props: Props) {
|
||||||
livestreamMap={livestreamMap}
|
livestreamMap={livestreamMap}
|
||||||
showNoSourceClaims={ENABLE_NO_SOURCE_CLAIMS}
|
showNoSourceClaims={ENABLE_NO_SOURCE_CLAIMS}
|
||||||
hasSource
|
hasSource
|
||||||
pin={route === `/$/${PAGES.GENERAL}`}
|
pin={route === `/$/${PAGES.GENERAL}`} // use pinUrls here
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -135,8 +141,12 @@ function HomePage(props: Props) {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{rowData.map(({ title, route, link, icon, help, options = {} }, index) => {
|
{rowData.map(({ title, route, link, icon, help, options = {} }, index) => {
|
||||||
|
// add pins here
|
||||||
return getRowElements(title, route, link, icon, help, options, index);
|
return getRowElements(title, route, link, icon, help, options, index);
|
||||||
})}
|
})}
|
||||||
|
{/* @if TARGET='web' */}
|
||||||
|
<Pixel type={'retargeting'} />
|
||||||
|
{/* @endif */}
|
||||||
</Page>
|
</Page>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,43 +1,42 @@
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { withRouter } from 'react-router';
|
import { withRouter } from 'react-router';
|
||||||
import { doSearch } from 'redux/actions/search';
|
import { doSearch } from 'redux/actions/search';
|
||||||
import { SIMPLE_SITE } from 'config';
|
|
||||||
import {
|
import {
|
||||||
selectIsSearching,
|
selectIsSearching,
|
||||||
makeSelectSearchUris,
|
makeSelectSearchUris,
|
||||||
makeSelectQueryWithOptions,
|
|
||||||
selectSearchOptions,
|
selectSearchOptions,
|
||||||
makeSelectHasReachedMaxResultsLength,
|
makeSelectHasReachedMaxResultsLength,
|
||||||
} from 'redux/selectors/search';
|
} from 'redux/selectors/search';
|
||||||
import { selectShowMatureContent } from 'redux/selectors/settings';
|
import { selectShowMatureContent } from 'redux/selectors/settings';
|
||||||
import { selectUserVerifiedEmail } from 'redux/selectors/user';
|
import { selectUserVerifiedEmail } from 'redux/selectors/user';
|
||||||
|
import { getSearchQueryString } from 'util/query-params';
|
||||||
import SearchPage from './view';
|
import SearchPage from './view';
|
||||||
|
|
||||||
const select = (state, props) => {
|
const select = (state, props) => {
|
||||||
const showMature = selectShowMatureContent(state);
|
const showMature = selectShowMatureContent(state);
|
||||||
const urlParams = new URLSearchParams(props.location.search);
|
const urlParams = new URLSearchParams(props.location.search);
|
||||||
|
|
||||||
let urlQuery = urlParams.get('q') || null;
|
let urlQuery = urlParams.get('q') || null;
|
||||||
if (urlQuery) {
|
if (urlQuery) {
|
||||||
urlQuery = urlQuery.replace(/^lbry:\/\//i, '').replace(/\//, ' ');
|
urlQuery = urlQuery.replace(/^lbry:\/\//i, '').replace(/\//, ' ');
|
||||||
}
|
}
|
||||||
const query = makeSelectQueryWithOptions(
|
|
||||||
urlQuery,
|
|
||||||
SIMPLE_SITE
|
|
||||||
? { nsfw: false, isBackgroundSearch: false }
|
|
||||||
: showMature === false
|
|
||||||
? { nsfw: false, isBackgroundSearch: false }
|
|
||||||
: { isBackgroundSearch: false }
|
|
||||||
)(state);
|
|
||||||
|
|
||||||
|
const searchOptions = {
|
||||||
|
...selectSearchOptions(state),
|
||||||
|
isBackgroundSearch: false,
|
||||||
|
nsfw: showMature,
|
||||||
|
};
|
||||||
|
|
||||||
|
const query = getSearchQueryString(urlQuery, searchOptions);
|
||||||
const uris = makeSelectSearchUris(query)(state);
|
const uris = makeSelectSearchUris(query)(state);
|
||||||
const hasReachedMaxResultsLength = makeSelectHasReachedMaxResultsLength(query)(state);
|
const hasReachedMaxResultsLength = makeSelectHasReachedMaxResultsLength(query)(state);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
urlQuery,
|
||||||
|
searchOptions,
|
||||||
isSearching: selectIsSearching(state),
|
isSearching: selectIsSearching(state),
|
||||||
showNsfw: showMature,
|
|
||||||
uris: uris,
|
uris: uris,
|
||||||
isAuthenticated: selectUserVerifiedEmail(state),
|
isAuthenticated: selectUserVerifiedEmail(state),
|
||||||
searchOptions: selectSearchOptions(state),
|
|
||||||
hasReachedMaxResultsLength: hasReachedMaxResultsLength,
|
hasReachedMaxResultsLength: hasReachedMaxResultsLength,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -11,43 +11,21 @@ import { formatLbryUrlForWeb } from 'util/url';
|
||||||
import { useHistory } from 'react-router';
|
import { useHistory } from 'react-router';
|
||||||
import { SEARCH_PAGE_SIZE } from 'constants/search';
|
import { SEARCH_PAGE_SIZE } from 'constants/search';
|
||||||
|
|
||||||
type AdditionalOptions = {
|
|
||||||
isBackgroundSearch: boolean,
|
|
||||||
nsfw?: boolean,
|
|
||||||
from?: number,
|
|
||||||
};
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
search: (string, AdditionalOptions) => void,
|
urlQuery: string,
|
||||||
searchOptions: {},
|
searchOptions: SearchOptions,
|
||||||
|
search: (string, SearchOptions) => void,
|
||||||
isSearching: boolean,
|
isSearching: boolean,
|
||||||
location: UrlLocation,
|
|
||||||
uris: Array<string>,
|
uris: Array<string>,
|
||||||
showNsfw: boolean,
|
|
||||||
isAuthenticated: boolean,
|
isAuthenticated: boolean,
|
||||||
hasReachedMaxResultsLength: boolean,
|
hasReachedMaxResultsLength: boolean,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function SearchPage(props: Props) {
|
export default function SearchPage(props: Props) {
|
||||||
const {
|
const { urlQuery, searchOptions, search, uris, isSearching, isAuthenticated, hasReachedMaxResultsLength } = props;
|
||||||
search,
|
|
||||||
uris,
|
|
||||||
location,
|
|
||||||
isSearching,
|
|
||||||
showNsfw,
|
|
||||||
isAuthenticated,
|
|
||||||
searchOptions,
|
|
||||||
hasReachedMaxResultsLength,
|
|
||||||
} = props;
|
|
||||||
const { push } = useHistory();
|
const { push } = useHistory();
|
||||||
const urlParams = new URLSearchParams(location.search);
|
|
||||||
const urlQuery = urlParams.get('q') || '';
|
|
||||||
const additionalOptions: AdditionalOptions = { isBackgroundSearch: false };
|
|
||||||
const [from, setFrom] = React.useState(0);
|
const [from, setFrom] = React.useState(0);
|
||||||
|
|
||||||
additionalOptions['nsfw'] = SIMPLE_SITE ? false : showNsfw;
|
|
||||||
additionalOptions['from'] = from;
|
|
||||||
|
|
||||||
const modifiedUrlQuery = urlQuery.trim().replace(/\s+/g, '').replace(/:/g, '#');
|
const modifiedUrlQuery = urlQuery.trim().replace(/\s+/g, '').replace(/:/g, '#');
|
||||||
const uriFromQuery = `lbry://${modifiedUrlQuery}`;
|
const uriFromQuery = `lbry://${modifiedUrlQuery}`;
|
||||||
|
|
||||||
|
@ -78,14 +56,14 @@ export default function SearchPage(props: Props) {
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
const stringifiedOptions = JSON.stringify(additionalOptions);
|
|
||||||
const stringifiedSearchOptions = JSON.stringify(searchOptions);
|
const stringifiedSearchOptions = JSON.stringify(searchOptions);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (urlQuery) {
|
if (urlQuery) {
|
||||||
const jsonOptions = JSON.parse(stringifiedOptions);
|
const searchOptions = JSON.parse(stringifiedSearchOptions);
|
||||||
search(urlQuery, jsonOptions);
|
search(urlQuery, { ...searchOptions, from: from });
|
||||||
}
|
}
|
||||||
}, [search, urlQuery, stringifiedOptions, stringifiedSearchOptions]);
|
}, [search, urlQuery, stringifiedSearchOptions, from]);
|
||||||
|
|
||||||
function loadMore() {
|
function loadMore() {
|
||||||
if (!isSearching && !hasReachedMaxResultsLength) {
|
if (!isSearching && !hasReachedMaxResultsLength) {
|
||||||
|
@ -114,7 +92,7 @@ export default function SearchPage(props: Props) {
|
||||||
header={
|
header={
|
||||||
<SearchOptions
|
<SearchOptions
|
||||||
simple={SIMPLE_SITE}
|
simple={SIMPLE_SITE}
|
||||||
additionalOptions={additionalOptions}
|
additionalOptions={searchOptions}
|
||||||
onSearchOptionsChanged={resetPage}
|
onSearchOptionsChanged={resetPage}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,9 +8,11 @@ import Button from 'component/button';
|
||||||
import { Lbryio } from 'lbryinc';
|
import { Lbryio } from 'lbryinc';
|
||||||
import I18nMessage from 'component/i18nMessage';
|
import I18nMessage from 'component/i18nMessage';
|
||||||
import Card from 'component/common/card';
|
import Card from 'component/common/card';
|
||||||
|
// @if TARGET='web'
|
||||||
|
import Pixel from 'web/component/pixel';
|
||||||
|
// @endif
|
||||||
type Props = {
|
type Props = {
|
||||||
history: { push: string => void, location: { search: string } },
|
history: { push: (string) => void, location: { search: string } },
|
||||||
doToast: ({}) => void,
|
doToast: ({}) => void,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -132,6 +134,9 @@ function SignInVerifyPage(props: Props) {
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
{/* @if TARGET='web' */}
|
||||||
|
<Pixel type={'kill'} />
|
||||||
|
{/* @endif */}
|
||||||
</Page>
|
</Page>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,11 +2,10 @@
|
||||||
import * as ACTIONS from 'constants/action_types';
|
import * as ACTIONS from 'constants/action_types';
|
||||||
import * as REACTION_TYPES from 'constants/reactions';
|
import * as REACTION_TYPES from 'constants/reactions';
|
||||||
import * as PAGES from 'constants/pages';
|
import * as PAGES from 'constants/pages';
|
||||||
import { BLOCK_LEVEL } from 'constants/comment';
|
import { SORT_BY, BLOCK_LEVEL } from 'constants/comment';
|
||||||
import { Lbry, parseURI, buildURI, selectClaimsById, selectClaimsByUri, selectMyChannelClaims } from 'lbry-redux';
|
import { Lbry, parseURI, buildURI, selectClaimsById, selectClaimsByUri, selectMyChannelClaims } from 'lbry-redux';
|
||||||
import { doToast, doSeeNotifications } from 'redux/actions/notifications';
|
import { doToast, doSeeNotifications } from 'redux/actions/notifications';
|
||||||
import {
|
import {
|
||||||
makeSelectCommentIdsForUri,
|
|
||||||
makeSelectMyReactionsForComment,
|
makeSelectMyReactionsForComment,
|
||||||
makeSelectOthersReactionsForComment,
|
makeSelectOthersReactionsForComment,
|
||||||
selectPendingCommentReacts,
|
selectPendingCommentReacts,
|
||||||
|
@ -18,7 +17,22 @@ import { selectActiveChannelClaim } from 'redux/selectors/app';
|
||||||
import { toHex } from 'util/hex';
|
import { toHex } from 'util/hex';
|
||||||
import Comments from 'comments';
|
import Comments from 'comments';
|
||||||
|
|
||||||
export function doCommentList(uri: string, page: number = 1, pageSize: number = 99999) {
|
const isDev = process.env.NODE_ENV !== 'production';
|
||||||
|
|
||||||
|
function devToast(dispatch, msg) {
|
||||||
|
if (isDev) {
|
||||||
|
console.error(msg); // eslint-disable-line
|
||||||
|
dispatch(doToast({ isError: true, message: `DEV: ${msg}` }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function doCommentList(
|
||||||
|
uri: string,
|
||||||
|
parentId: string,
|
||||||
|
page: number = 1,
|
||||||
|
pageSize: number = 99999,
|
||||||
|
sortBy: number = SORT_BY.NEWEST
|
||||||
|
) {
|
||||||
return (dispatch: Dispatch, getState: GetState) => {
|
return (dispatch: Dispatch, getState: GetState) => {
|
||||||
const state = getState();
|
const state = getState();
|
||||||
const claim = selectClaimsByUri(state)[uri];
|
const claim = selectClaimsByUri(state)[uri];
|
||||||
|
@ -35,6 +49,9 @@ export function doCommentList(uri: string, page: number = 1, pageSize: number =
|
||||||
|
|
||||||
dispatch({
|
dispatch({
|
||||||
type: ACTIONS.COMMENT_LIST_STARTED,
|
type: ACTIONS.COMMENT_LIST_STARTED,
|
||||||
|
data: {
|
||||||
|
parentId,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Adding 'channel_id' and 'channel_name' enables "CreatorSettings > commentsEnabled".
|
// Adding 'channel_id' and 'channel_name' enables "CreatorSettings > commentsEnabled".
|
||||||
|
@ -44,20 +61,28 @@ export function doCommentList(uri: string, page: number = 1, pageSize: number =
|
||||||
page,
|
page,
|
||||||
claim_id: claimId,
|
claim_id: claimId,
|
||||||
page_size: pageSize,
|
page_size: pageSize,
|
||||||
|
parent_id: parentId || undefined,
|
||||||
|
top_level: !parentId,
|
||||||
channel_id: authorChannelClaim ? authorChannelClaim.claim_id : undefined,
|
channel_id: authorChannelClaim ? authorChannelClaim.claim_id : undefined,
|
||||||
channel_name: authorChannelClaim ? authorChannelClaim.name : undefined,
|
channel_name: authorChannelClaim ? authorChannelClaim.name : undefined,
|
||||||
|
sort_by: sortBy,
|
||||||
})
|
})
|
||||||
.then((result: CommentListResponse) => {
|
.then((result: CommentListResponse) => {
|
||||||
const { items: comments } = result;
|
const { items: comments, total_items, total_filtered_items, total_pages } = result;
|
||||||
dispatch({
|
dispatch({
|
||||||
type: ACTIONS.COMMENT_LIST_COMPLETED,
|
type: ACTIONS.COMMENT_LIST_COMPLETED,
|
||||||
data: {
|
data: {
|
||||||
comments,
|
comments,
|
||||||
|
parentId,
|
||||||
|
totalItems: total_items,
|
||||||
|
totalFilteredItems: total_filtered_items,
|
||||||
|
totalPages: total_pages,
|
||||||
claimId: claimId,
|
claimId: claimId,
|
||||||
authorClaimId: authorChannelClaim ? authorChannelClaim.claim_id : undefined,
|
authorClaimId: authorChannelClaim ? authorChannelClaim.claim_id : undefined,
|
||||||
uri: uri,
|
uri: uri,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
|
@ -70,6 +95,7 @@ export function doCommentList(uri: string, page: number = 1, pageSize: number =
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
devToast(dispatch, `doCommentList: ${error.message}`);
|
||||||
dispatch({
|
dispatch({
|
||||||
type: ACTIONS.COMMENT_LIST_FAILED,
|
type: ACTIONS.COMMENT_LIST_FAILED,
|
||||||
data: error,
|
data: error,
|
||||||
|
@ -79,6 +105,60 @@ export function doCommentList(uri: string, page: number = 1, pageSize: number =
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function doCommentById(commentId: string, toastIfNotFound: boolean = true) {
|
||||||
|
return (dispatch: Dispatch, getState: GetState) => {
|
||||||
|
return Comments.comment_by_id({ comment_id: commentId, with_ancestors: true })
|
||||||
|
.then((result: CommentByIdResponse) => {
|
||||||
|
const { item, items, ancestors } = result;
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: ACTIONS.COMMENT_BY_ID_COMPLETED,
|
||||||
|
data: {
|
||||||
|
comment: item || items, // Requested a change to rename it to 'item'. This covers both.
|
||||||
|
ancestors: ancestors,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
if (error.message === 'sql: no rows in result set' && toastIfNotFound) {
|
||||||
|
dispatch(
|
||||||
|
doToast({
|
||||||
|
isError: true,
|
||||||
|
message: __('The requested comment is no longer available.'),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
devToast(dispatch, error.message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function doCommentReset(uri: string) {
|
||||||
|
return (dispatch: Dispatch, getState: GetState) => {
|
||||||
|
const state = getState();
|
||||||
|
const claim = selectClaimsByUri(state)[uri];
|
||||||
|
const claimId = claim ? claim.claim_id : null;
|
||||||
|
|
||||||
|
if (!claimId) {
|
||||||
|
dispatch({
|
||||||
|
type: ACTIONS.COMMENT_LIST_FAILED,
|
||||||
|
data: 'unable to find claim for uri',
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: ACTIONS.COMMENT_LIST_RESET,
|
||||||
|
data: {
|
||||||
|
claimId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function doSuperChatList(uri: string) {
|
export function doSuperChatList(uri: string) {
|
||||||
return (dispatch: Dispatch, getState: GetState) => {
|
return (dispatch: Dispatch, getState: GetState) => {
|
||||||
const state = getState();
|
const state = getState();
|
||||||
|
@ -97,7 +177,7 @@ export function doSuperChatList(uri: string) {
|
||||||
return Comments.super_list({
|
return Comments.super_list({
|
||||||
claim_id: claimId,
|
claim_id: claimId,
|
||||||
})
|
})
|
||||||
.then((result: CommentListResponse) => {
|
.then((result: SuperListResponse) => {
|
||||||
const { items: comments, total_amount: totalAmount } = result;
|
const { items: comments, total_amount: totalAmount } = result;
|
||||||
dispatch({
|
dispatch({
|
||||||
type: ACTIONS.COMMENT_SUPER_CHAT_LIST_COMPLETED,
|
type: ACTIONS.COMMENT_SUPER_CHAT_LIST_COMPLETED,
|
||||||
|
@ -117,17 +197,16 @@ export function doSuperChatList(uri: string) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function doCommentReactList(uri: string | null, commentId?: string) {
|
export function doCommentReactList(commentIds: Array<string>) {
|
||||||
return (dispatch: Dispatch, getState: GetState) => {
|
return (dispatch: Dispatch, getState: GetState) => {
|
||||||
const state = getState();
|
const state = getState();
|
||||||
const activeChannelClaim = selectActiveChannelClaim(state);
|
const activeChannelClaim = selectActiveChannelClaim(state);
|
||||||
const commentIds = uri ? makeSelectCommentIdsForUri(uri)(state) : [commentId];
|
|
||||||
|
|
||||||
dispatch({
|
dispatch({
|
||||||
type: ACTIONS.COMMENT_REACTION_LIST_STARTED,
|
type: ACTIONS.COMMENT_REACTION_LIST_STARTED,
|
||||||
});
|
});
|
||||||
|
|
||||||
const params: { comment_ids: string, channel_name?: string, channel_id?: string } = {
|
const params: CommentReactListParams = {
|
||||||
comment_ids: commentIds.join(','),
|
comment_ids: commentIds.join(','),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -144,10 +223,12 @@ export function doCommentReactList(uri: string | null, commentId?: string) {
|
||||||
data: {
|
data: {
|
||||||
myReactions: myReactions || {},
|
myReactions: myReactions || {},
|
||||||
othersReactions,
|
othersReactions,
|
||||||
|
channelId: activeChannelClaim ? activeChannelClaim.claim_id : undefined,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
|
devToast(dispatch, `doCommentReactList: ${error.message}`);
|
||||||
dispatch({
|
dispatch({
|
||||||
type: ACTIONS.COMMENT_REACTION_LIST_FAILED,
|
type: ACTIONS.COMMENT_REACTION_LIST_FAILED,
|
||||||
data: error,
|
data: error,
|
||||||
|
@ -182,8 +263,9 @@ export function doCommentReact(commentId: string, type: string) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let myReacts = makeSelectMyReactionsForComment(commentId)(state);
|
const reactKey = `${commentId}:${activeChannelClaim.claim_id}`;
|
||||||
const othersReacts = makeSelectOthersReactionsForComment(commentId)(state);
|
const myReacts = makeSelectMyReactionsForComment(reactKey)(state);
|
||||||
|
const othersReacts = makeSelectOthersReactionsForComment(reactKey)(state);
|
||||||
const params: CommentReactParams = {
|
const params: CommentReactParams = {
|
||||||
comment_ids: commentId,
|
comment_ids: commentId,
|
||||||
channel_name: activeChannelClaim.name,
|
channel_name: activeChannelClaim.name,
|
||||||
|
@ -217,8 +299,8 @@ export function doCommentReact(commentId: string, type: string) {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: ACTIONS.COMMENT_REACTION_LIST_COMPLETED,
|
type: ACTIONS.COMMENT_REACTION_LIST_COMPLETED,
|
||||||
data: {
|
data: {
|
||||||
myReactions: { [commentId]: myReactsObj },
|
myReactions: { [reactKey]: myReactsObj },
|
||||||
othersReactions: { [commentId]: othersReacts },
|
othersReactions: { [reactKey]: othersReacts },
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -371,7 +453,7 @@ export function doCommentCreate(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function doCommentPin(commentId: string, remove: boolean) {
|
export function doCommentPin(commentId: string, claimId: string, remove: boolean) {
|
||||||
return (dispatch: Dispatch, getState: GetState) => {
|
return (dispatch: Dispatch, getState: GetState) => {
|
||||||
const state = getState();
|
const state = getState();
|
||||||
const activeChannel = selectActiveChannelClaim(state);
|
const activeChannel = selectActiveChannelClaim(state);
|
||||||
|
@ -394,7 +476,11 @@ export function doCommentPin(commentId: string, remove: boolean) {
|
||||||
.then((result: CommentPinResponse) => {
|
.then((result: CommentPinResponse) => {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: ACTIONS.COMMENT_PIN_COMPLETED,
|
type: ACTIONS.COMMENT_PIN_COMPLETED,
|
||||||
data: result,
|
data: {
|
||||||
|
pinnedComment: result.items,
|
||||||
|
claimId,
|
||||||
|
unpin: remove,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
|
|
|
@ -1,13 +1,9 @@
|
||||||
// @flow
|
// @flow
|
||||||
import * as ACTIONS from 'constants/action_types';
|
import * as ACTIONS from 'constants/action_types';
|
||||||
import { buildURI, doResolveUris, batchActions, isURIValid, makeSelectClaimForUri } from 'lbry-redux';
|
import { buildURI, doResolveUris, batchActions, isURIValid, makeSelectClaimForUri } from 'lbry-redux';
|
||||||
import {
|
import { makeSelectSearchUris, selectSearchValue } from 'redux/selectors/search';
|
||||||
makeSelectSearchUris,
|
|
||||||
makeSelectQueryWithOptions,
|
|
||||||
selectSearchValue,
|
|
||||||
selectSearchOptions,
|
|
||||||
} from 'redux/selectors/search';
|
|
||||||
import handleFetchResponse from 'util/handle-fetch';
|
import handleFetchResponse from 'util/handle-fetch';
|
||||||
|
import { getSearchQueryString } from 'util/query-params';
|
||||||
|
|
||||||
type Dispatch = (action: any) => any;
|
type Dispatch = (action: any) => any;
|
||||||
type GetState = () => { search: SearchState };
|
type GetState = () => { search: SearchState };
|
||||||
|
@ -44,10 +40,9 @@ export const doSearch = (rawQuery: string, searchOptions: SearchOptions) => (
|
||||||
|
|
||||||
const state = getState();
|
const state = getState();
|
||||||
|
|
||||||
const mainOptions: any = selectSearchOptions(state);
|
const queryWithOptions = getSearchQueryString(query, searchOptions);
|
||||||
const queryWithOptions = makeSelectQueryWithOptions(query, searchOptions)(state);
|
|
||||||
|
|
||||||
const size = mainOptions.size;
|
const size = searchOptions.size;
|
||||||
const from = searchOptions.from;
|
const from = searchOptions.from;
|
||||||
|
|
||||||
// If we have already searched for something, we don't need to do anything
|
// If we have already searched for something, we don't need to do anything
|
||||||
|
@ -101,7 +96,7 @@ export const doSearch = (rawQuery: string, searchOptions: SearchOptions) => (
|
||||||
});
|
});
|
||||||
dispatch(batchActions(...actions));
|
dispatch(batchActions(...actions));
|
||||||
})
|
})
|
||||||
.catch((e) => {
|
.catch(() => {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: ACTIONS.SEARCH_FAIL,
|
type: ACTIONS.SEARCH_FAIL,
|
||||||
});
|
});
|
||||||
|
|
|
@ -3,17 +3,25 @@ import * as ACTIONS from 'constants/action_types';
|
||||||
import { handleActions } from 'util/redux-utils';
|
import { handleActions } from 'util/redux-utils';
|
||||||
import { BLOCK_LEVEL } from 'constants/comment';
|
import { BLOCK_LEVEL } from 'constants/comment';
|
||||||
|
|
||||||
|
const IS_DEV = process.env.NODE_ENV !== 'production';
|
||||||
|
|
||||||
const defaultState: CommentsState = {
|
const defaultState: CommentsState = {
|
||||||
commentById: {}, // commentId -> Comment
|
commentById: {}, // commentId -> Comment
|
||||||
byId: {}, // ClaimID -> list of comments
|
byId: {}, // ClaimID -> list of fetched comment IDs.
|
||||||
repliesByParentId: {}, // ParentCommentID -> list of reply comments
|
totalCommentsById: {}, // ClaimId -> ultimate total (including replies) in commentron.
|
||||||
topLevelCommentsById: {}, // ClaimID -> list of top level comments
|
repliesByParentId: {}, // ParentCommentID -> list of fetched replies.
|
||||||
|
totalRepliesByParentId: {}, // ParentCommentID -> total replies in commentron.
|
||||||
|
topLevelCommentsById: {}, // ClaimID -> list of fetched top level comments.
|
||||||
|
topLevelTotalPagesById: {}, // ClaimID -> total number of top-level pages in commentron. Based on COMMENT_PAGE_SIZE_TOP_LEVEL.
|
||||||
|
topLevelTotalCommentsById: {}, // ClaimID -> total top level comments in commentron.
|
||||||
// TODO:
|
// TODO:
|
||||||
// Remove commentsByUri
|
// Remove commentsByUri
|
||||||
// It is not needed and doesn't provide anything but confusion
|
// It is not needed and doesn't provide anything but confusion
|
||||||
commentsByUri: {}, // URI -> claimId
|
commentsByUri: {}, // URI -> claimId
|
||||||
|
linkedCommentAncestors: {}, // {"linkedCommentId": ["parentId", "grandParentId", ...]}
|
||||||
superChatsByUri: {},
|
superChatsByUri: {},
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
|
isLoadingByParentId: {},
|
||||||
isCommenting: false,
|
isCommenting: false,
|
||||||
myComments: undefined,
|
myComments: undefined,
|
||||||
isFetchingReacts: false,
|
isFetchingReacts: false,
|
||||||
|
@ -39,6 +47,14 @@ const defaultState: CommentsState = {
|
||||||
fetchingBlockedWords: false,
|
fetchingBlockedWords: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function pushToArrayInObject(obj, key, valueToPush) {
|
||||||
|
if (!obj[key]) {
|
||||||
|
obj[key] = [valueToPush];
|
||||||
|
} else if (!obj[key].includes(valueToPush)) {
|
||||||
|
obj[key].push(valueToPush);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default handleActions(
|
export default handleActions(
|
||||||
{
|
{
|
||||||
[ACTIONS.COMMENT_CREATE_STARTED]: (state: CommentsState, action: any): CommentsState => ({
|
[ACTIONS.COMMENT_CREATE_STARTED]: (state: CommentsState, action: any): CommentsState => ({
|
||||||
|
@ -58,10 +74,13 @@ export default handleActions(
|
||||||
uri,
|
uri,
|
||||||
livestream,
|
livestream,
|
||||||
}: { comment: Comment, claimId: string, uri: string, livestream: boolean } = action.data;
|
}: { comment: Comment, claimId: string, uri: string, livestream: boolean } = action.data;
|
||||||
|
|
||||||
const commentById = Object.assign({}, state.commentById);
|
const commentById = Object.assign({}, state.commentById);
|
||||||
const byId = Object.assign({}, state.byId);
|
const byId = Object.assign({}, state.byId);
|
||||||
|
const totalCommentsById = Object.assign({}, state.totalCommentsById);
|
||||||
const topLevelCommentsById = Object.assign({}, state.topLevelCommentsById); // was byId {ClaimId -> [commentIds...]}
|
const topLevelCommentsById = Object.assign({}, state.topLevelCommentsById); // was byId {ClaimId -> [commentIds...]}
|
||||||
const repliesByParentId = Object.assign({}, state.repliesByParentId); // {ParentCommentID -> [commentIds...] } list of reply comments
|
const repliesByParentId = Object.assign({}, state.repliesByParentId); // {ParentCommentID -> [commentIds...] } list of reply comments
|
||||||
|
const totalRepliesByParentId = Object.assign({}, state.totalRepliesByParentId);
|
||||||
const commentsByUri = Object.assign({}, state.commentsByUri);
|
const commentsByUri = Object.assign({}, state.commentsByUri);
|
||||||
const comments = byId[claimId] || [];
|
const comments = byId[claimId] || [];
|
||||||
const newCommentIds = comments.slice();
|
const newCommentIds = comments.slice();
|
||||||
|
@ -75,12 +94,27 @@ export default handleActions(
|
||||||
newCommentIds.unshift(comment.comment_id);
|
newCommentIds.unshift(comment.comment_id);
|
||||||
byId[claimId] = newCommentIds;
|
byId[claimId] = newCommentIds;
|
||||||
|
|
||||||
|
if (totalCommentsById[claimId]) {
|
||||||
|
totalCommentsById[claimId] += 1;
|
||||||
|
}
|
||||||
|
|
||||||
if (comment['parent_id']) {
|
if (comment['parent_id']) {
|
||||||
if (!repliesByParentId[comment.parent_id]) {
|
if (!repliesByParentId[comment.parent_id]) {
|
||||||
repliesByParentId[comment.parent_id] = [comment.comment_id];
|
repliesByParentId[comment.parent_id] = [comment.comment_id];
|
||||||
} else {
|
} else {
|
||||||
repliesByParentId[comment.parent_id].unshift(comment.comment_id);
|
repliesByParentId[comment.parent_id].unshift(comment.comment_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!totalRepliesByParentId[comment.parent_id]) {
|
||||||
|
totalRepliesByParentId[comment.parent_id] = 1;
|
||||||
|
} else {
|
||||||
|
totalRepliesByParentId[comment.parent_id] += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the parent's "replies" value
|
||||||
|
if (commentById[comment.parent_id]) {
|
||||||
|
commentById[comment.parent_id].replies = (commentById[comment.parent_id].replies || 0) + 1;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
if (!topLevelCommentsById[claimId]) {
|
if (!topLevelCommentsById[claimId]) {
|
||||||
commentsByUri[uri] = claimId;
|
commentsByUri[uri] = claimId;
|
||||||
|
@ -95,8 +129,10 @@ export default handleActions(
|
||||||
...state,
|
...state,
|
||||||
topLevelCommentsById,
|
topLevelCommentsById,
|
||||||
repliesByParentId,
|
repliesByParentId,
|
||||||
|
totalRepliesByParentId,
|
||||||
commentById,
|
commentById,
|
||||||
byId,
|
byId,
|
||||||
|
totalCommentsById,
|
||||||
commentsByUri,
|
commentsByUri,
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
isCommenting: false,
|
isCommenting: false,
|
||||||
|
@ -147,12 +183,14 @@ export default handleActions(
|
||||||
},
|
},
|
||||||
|
|
||||||
[ACTIONS.COMMENT_REACTION_LIST_COMPLETED]: (state: CommentsState, action: any): CommentsState => {
|
[ACTIONS.COMMENT_REACTION_LIST_COMPLETED]: (state: CommentsState, action: any): CommentsState => {
|
||||||
const { myReactions, othersReactions } = action.data;
|
const { myReactions, othersReactions, channelId } = action.data;
|
||||||
const myReacts = Object.assign({}, state.myReactsByCommentId);
|
const myReacts = Object.assign({}, state.myReactsByCommentId);
|
||||||
const othersReacts = Object.assign({}, state.othersReactsByCommentId);
|
const othersReacts = Object.assign({}, state.othersReactsByCommentId);
|
||||||
|
|
||||||
if (myReactions) {
|
if (myReactions) {
|
||||||
Object.entries(myReactions).forEach(([commentId, reactions]) => {
|
Object.entries(myReactions).forEach(([commentId, reactions]) => {
|
||||||
myReacts[commentId] = Object.entries(reactions).reduce((acc, [name, count]) => {
|
const key = channelId ? `${commentId}:${channelId}` : commentId;
|
||||||
|
myReacts[key] = Object.entries(reactions).reduce((acc, [name, count]) => {
|
||||||
if (count === 1) {
|
if (count === 1) {
|
||||||
acc.push(name);
|
acc.push(name);
|
||||||
}
|
}
|
||||||
|
@ -160,9 +198,11 @@ export default handleActions(
|
||||||
}, []);
|
}, []);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (othersReactions) {
|
if (othersReactions) {
|
||||||
Object.entries(othersReactions).forEach(([commentId, reactions]) => {
|
Object.entries(othersReactions).forEach(([commentId, reactions]) => {
|
||||||
othersReacts[commentId] = reactions;
|
const key = channelId ? `${commentId}:${channelId}` : commentId;
|
||||||
|
othersReacts[key] = reactions;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -174,10 +214,32 @@ export default handleActions(
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
[ACTIONS.COMMENT_LIST_STARTED]: (state) => ({ ...state, isLoading: true }),
|
[ACTIONS.COMMENT_LIST_STARTED]: (state, action: any) => {
|
||||||
|
const { parentId } = action.data;
|
||||||
|
const isLoadingByParentId = Object.assign({}, state.isLoadingByParentId);
|
||||||
|
if (parentId) {
|
||||||
|
isLoadingByParentId[parentId] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
isLoading: true,
|
||||||
|
isLoadingByParentId,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
[ACTIONS.COMMENT_LIST_COMPLETED]: (state: CommentsState, action: any) => {
|
[ACTIONS.COMMENT_LIST_COMPLETED]: (state: CommentsState, action: any) => {
|
||||||
const { comments, claimId, uri, disabled, authorClaimId } = action.data;
|
const {
|
||||||
|
comments,
|
||||||
|
parentId,
|
||||||
|
totalItems,
|
||||||
|
totalFilteredItems,
|
||||||
|
totalPages,
|
||||||
|
claimId,
|
||||||
|
uri,
|
||||||
|
disabled,
|
||||||
|
authorClaimId,
|
||||||
|
} = action.data;
|
||||||
const commentsDisabledChannelIds = [...state.commentsDisabledChannelIds];
|
const commentsDisabledChannelIds = [...state.commentsDisabledChannelIds];
|
||||||
|
|
||||||
if (disabled) {
|
if (disabled) {
|
||||||
|
@ -185,10 +247,16 @@ export default handleActions(
|
||||||
commentsDisabledChannelIds.push(authorClaimId);
|
commentsDisabledChannelIds.push(authorClaimId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isLoadingByParentId = Object.assign({}, state.isLoadingByParentId);
|
||||||
|
if (parentId) {
|
||||||
|
isLoadingByParentId[parentId] = false;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
commentsDisabledChannelIds,
|
commentsDisabledChannelIds,
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
|
isLoadingByParentId,
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
const index = commentsDisabledChannelIds.indexOf(authorClaimId);
|
const index = commentsDisabledChannelIds.indexOf(authorClaimId);
|
||||||
|
@ -200,49 +268,135 @@ export default handleActions(
|
||||||
const commentById = Object.assign({}, state.commentById);
|
const commentById = Object.assign({}, state.commentById);
|
||||||
const byId = Object.assign({}, state.byId);
|
const byId = Object.assign({}, state.byId);
|
||||||
const topLevelCommentsById = Object.assign({}, state.topLevelCommentsById); // was byId {ClaimId -> [commentIds...]}
|
const topLevelCommentsById = Object.assign({}, state.topLevelCommentsById); // was byId {ClaimId -> [commentIds...]}
|
||||||
|
const topLevelTotalCommentsById = Object.assign({}, state.topLevelTotalCommentsById);
|
||||||
|
const topLevelTotalPagesById = Object.assign({}, state.topLevelTotalPagesById);
|
||||||
const commentsByUri = Object.assign({}, state.commentsByUri);
|
const commentsByUri = Object.assign({}, state.commentsByUri);
|
||||||
|
const repliesByParentId = Object.assign({}, state.repliesByParentId);
|
||||||
|
const totalCommentsById = Object.assign({}, state.totalCommentsById);
|
||||||
|
const totalRepliesByParentId = Object.assign({}, state.totalRepliesByParentId);
|
||||||
|
const isLoadingByParentId = Object.assign({}, state.isLoadingByParentId);
|
||||||
|
|
||||||
|
const commonUpdateAction = (comment, commentById, commentIds, index) => {
|
||||||
|
// map the comment_ids to the new comments
|
||||||
|
commentById[comment.comment_id] = comment;
|
||||||
|
commentIds[index] = comment.comment_id;
|
||||||
|
};
|
||||||
|
|
||||||
const tempRepliesByParent = {};
|
|
||||||
const topLevelComments = [];
|
|
||||||
if (comments) {
|
if (comments) {
|
||||||
// we use an Array to preserve order of listing
|
// we use an Array to preserve order of listing
|
||||||
// in reality this doesn't matter and we can just
|
// in reality this doesn't matter and we can just
|
||||||
// sort comments by their timestamp
|
// sort comments by their timestamp
|
||||||
const commentIds = Array(comments.length);
|
const commentIds = Array(comments.length);
|
||||||
|
|
||||||
// map the comment_ids to the new comments
|
// totalCommentsById[claimId] = totalItems;
|
||||||
for (let i = 0; i < comments.length; i++) {
|
// --> currently, this value is only correct when done via a top-level query.
|
||||||
const comment = comments[i];
|
// Until this is fixed, I'm moving it downwards to **
|
||||||
if (comment['parent_id']) {
|
|
||||||
if (!tempRepliesByParent[comment.parent_id]) {
|
|
||||||
tempRepliesByParent[comment.parent_id] = [comment.comment_id];
|
|
||||||
} else {
|
|
||||||
tempRepliesByParent[comment.parent_id].push(comment.comment_id);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
commentById[comment.comment_id] = comment;
|
|
||||||
topLevelComments.push(comment.comment_id);
|
|
||||||
}
|
|
||||||
commentIds[i] = comments[i].comment_id;
|
|
||||||
commentById[commentIds[i]] = comments[i];
|
|
||||||
}
|
|
||||||
topLevelCommentsById[claimId] = topLevelComments;
|
|
||||||
|
|
||||||
byId[claimId] = commentIds;
|
// --- Top-level comments ---
|
||||||
|
if (!parentId) {
|
||||||
|
totalCommentsById[claimId] = totalItems; // **
|
||||||
|
|
||||||
|
topLevelTotalCommentsById[claimId] = totalFilteredItems;
|
||||||
|
topLevelTotalPagesById[claimId] = totalPages;
|
||||||
|
|
||||||
|
if (!topLevelCommentsById[claimId]) {
|
||||||
|
topLevelCommentsById[claimId] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const topLevelCommentIds = topLevelCommentsById[claimId];
|
||||||
|
|
||||||
|
for (let i = 0; i < comments.length; ++i) {
|
||||||
|
const comment = comments[i];
|
||||||
|
commonUpdateAction(comment, commentById, commentIds, i);
|
||||||
|
|
||||||
|
if (IS_DEV && comment['parent_id']) console.error('Invalid top-level comment:', comment); // eslint-disable-line
|
||||||
|
|
||||||
|
if (!topLevelCommentIds.includes(comment.comment_id)) {
|
||||||
|
topLevelCommentIds.push(comment.comment_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// --- Replies ---
|
||||||
|
else {
|
||||||
|
totalRepliesByParentId[parentId] = totalFilteredItems;
|
||||||
|
isLoadingByParentId[parentId] = false;
|
||||||
|
|
||||||
|
for (let i = 0; i < comments.length; ++i) {
|
||||||
|
const comment = comments[i];
|
||||||
|
commonUpdateAction(comment, commentById, commentIds, i);
|
||||||
|
|
||||||
|
if (IS_DEV && !comment['parent_id']) console.error('Missing parent_id:', comment); // eslint-disable-line
|
||||||
|
if (IS_DEV && comment.parent_id !== parentId) console.error('Black sheep in the family?:', comment); // eslint-disable-line
|
||||||
|
|
||||||
|
pushToArrayInObject(repliesByParentId, parentId, comment.comment_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
byId[claimId] ? byId[claimId].push(...commentIds) : (byId[claimId] = commentIds);
|
||||||
commentsByUri[uri] = claimId;
|
commentsByUri[uri] = claimId;
|
||||||
}
|
}
|
||||||
|
|
||||||
const repliesByParentId = Object.assign({}, state.repliesByParentId, tempRepliesByParent); // {ParentCommentID -> [commentIds...] } list of reply comments
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
topLevelCommentsById,
|
topLevelCommentsById,
|
||||||
|
topLevelTotalCommentsById,
|
||||||
|
topLevelTotalPagesById,
|
||||||
repliesByParentId,
|
repliesByParentId,
|
||||||
|
totalCommentsById,
|
||||||
|
totalRepliesByParentId,
|
||||||
byId,
|
byId,
|
||||||
commentById,
|
commentById,
|
||||||
commentsByUri,
|
commentsByUri,
|
||||||
commentsDisabledChannelIds,
|
commentsDisabledChannelIds,
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
|
isLoadingByParentId,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
[ACTIONS.COMMENT_BY_ID_COMPLETED]: (state: CommentsState, action: any) => {
|
||||||
|
const { comment, ancestors } = action.data;
|
||||||
|
const claimId = comment.claim_id;
|
||||||
|
|
||||||
|
const commentById = Object.assign({}, state.commentById);
|
||||||
|
const byId = Object.assign({}, state.byId);
|
||||||
|
const topLevelCommentsById = Object.assign({}, state.topLevelCommentsById); // was byId {ClaimId -> [commentIds...]}
|
||||||
|
const topLevelTotalCommentsById = Object.assign({}, state.topLevelTotalCommentsById);
|
||||||
|
const topLevelTotalPagesById = Object.assign({}, state.topLevelTotalPagesById);
|
||||||
|
const repliesByParentId = Object.assign({}, state.repliesByParentId);
|
||||||
|
const linkedCommentAncestors = Object.assign({}, state.linkedCommentAncestors);
|
||||||
|
|
||||||
|
const updateStore = (comment, commentById, byId, repliesByParentId, topLevelCommentsById) => {
|
||||||
|
// 'comment.ByID' doesn't populate 'replies'. We should have at least 1
|
||||||
|
// at the moment, and the correct value will populated by 'comment.List'.
|
||||||
|
commentById[comment.comment_id] = { ...comment, replies: 1 };
|
||||||
|
byId[claimId] ? byId[claimId].unshift(comment.comment_id) : (byId[claimId] = [comment.comment_id]);
|
||||||
|
|
||||||
|
const parentId = comment.parent_id;
|
||||||
|
if (comment.parent_id) {
|
||||||
|
pushToArrayInObject(repliesByParentId, parentId, comment.comment_id);
|
||||||
|
} else {
|
||||||
|
pushToArrayInObject(topLevelCommentsById, claimId, comment.comment_id);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
updateStore(comment, commentById, byId, repliesByParentId, topLevelCommentsById);
|
||||||
|
|
||||||
|
if (ancestors) {
|
||||||
|
ancestors.forEach((ancestor) => {
|
||||||
|
updateStore(ancestor, commentById, byId, repliesByParentId, topLevelCommentsById);
|
||||||
|
pushToArrayInObject(linkedCommentAncestors, comment.comment_id, ancestor.comment_id);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
topLevelCommentsById,
|
||||||
|
topLevelTotalCommentsById,
|
||||||
|
topLevelTotalPagesById,
|
||||||
|
repliesByParentId,
|
||||||
|
byId,
|
||||||
|
commentById,
|
||||||
|
linkedCommentAncestors,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -273,6 +427,51 @@ export default handleActions(
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
[ACTIONS.COMMENT_LIST_RESET]: (state: CommentsState, action: any) => {
|
||||||
|
const { claimId } = action.data;
|
||||||
|
|
||||||
|
const byId = Object.assign({}, state.byId);
|
||||||
|
const totalCommentsById = Object.assign({}, state.totalCommentsById);
|
||||||
|
const topLevelCommentsById = Object.assign({}, state.topLevelCommentsById); // was byId {ClaimId -> [commentIds...]}
|
||||||
|
const topLevelTotalCommentsById = Object.assign({}, state.topLevelTotalCommentsById);
|
||||||
|
const topLevelTotalPagesById = Object.assign({}, state.topLevelTotalPagesById);
|
||||||
|
const myReacts = Object.assign({}, state.myReactsByCommentId);
|
||||||
|
const othersReacts = Object.assign({}, state.othersReactsByCommentId);
|
||||||
|
|
||||||
|
function deleteReacts(reactObj, commentIdsToRemove) {
|
||||||
|
if (commentIdsToRemove && commentIdsToRemove.length > 0) {
|
||||||
|
let reactionKeys = Object.keys(reactObj);
|
||||||
|
reactionKeys.forEach((rk) => {
|
||||||
|
const colonIndex = rk.indexOf(':');
|
||||||
|
const commentId = colonIndex === -1 ? rk : rk.substring(0, colonIndex);
|
||||||
|
if (commentIdsToRemove.includes(commentId)) {
|
||||||
|
delete reactObj[rk];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteReacts(myReacts, byId[claimId]);
|
||||||
|
deleteReacts(othersReacts, byId[claimId]);
|
||||||
|
|
||||||
|
delete byId[claimId];
|
||||||
|
delete totalCommentsById[claimId];
|
||||||
|
delete topLevelCommentsById[claimId];
|
||||||
|
delete topLevelTotalCommentsById[claimId];
|
||||||
|
delete topLevelTotalPagesById[claimId];
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
byId,
|
||||||
|
totalCommentsById,
|
||||||
|
topLevelCommentsById,
|
||||||
|
topLevelTotalCommentsById,
|
||||||
|
topLevelTotalPagesById,
|
||||||
|
myReactsByCommentId: myReacts,
|
||||||
|
othersReactsByCommentId: othersReacts,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
[ACTIONS.COMMENT_RECEIVED]: (state: CommentsState, action: any) => {
|
[ACTIONS.COMMENT_RECEIVED]: (state: CommentsState, action: any) => {
|
||||||
const { uri, claimId, comment } = action.data;
|
const { uri, claimId, comment } = action.data;
|
||||||
const commentsByUri = Object.assign({}, state.commentsByUri);
|
const commentsByUri = Object.assign({}, state.commentsByUri);
|
||||||
|
@ -352,21 +551,50 @@ export default handleActions(
|
||||||
const { comment_id } = action.data;
|
const { comment_id } = action.data;
|
||||||
const commentById = Object.assign({}, state.commentById);
|
const commentById = Object.assign({}, state.commentById);
|
||||||
const byId = Object.assign({}, state.byId);
|
const byId = Object.assign({}, state.byId);
|
||||||
|
const repliesByParentId = Object.assign({}, state.repliesByParentId); // {ParentCommentID -> [commentIds...] } list of reply comments
|
||||||
|
const totalRepliesByParentId = Object.assign({}, state.totalRepliesByParentId);
|
||||||
|
const totalCommentsById = Object.assign({}, state.totalCommentsById);
|
||||||
|
|
||||||
|
const comment = commentById[comment_id];
|
||||||
|
|
||||||
// to remove the comment and its references
|
// to remove the comment and its references
|
||||||
const claimId = commentById[comment_id].claim_id;
|
const claimId = comment.claim_id;
|
||||||
for (let i = 0; i < byId[claimId].length; i++) {
|
for (let i = 0; i < byId[claimId].length; i++) {
|
||||||
if (byId[claimId][i] === comment_id) {
|
if (byId[claimId][i] === comment_id) {
|
||||||
byId[claimId].splice(i, 1);
|
byId[claimId].splice(i, 1);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update replies
|
||||||
|
if (comment['parent_id'] && repliesByParentId[comment.parent_id]) {
|
||||||
|
const index = repliesByParentId[comment.parent_id].indexOf(comment.comment_id);
|
||||||
|
if (index > -1) {
|
||||||
|
repliesByParentId[comment.parent_id].splice(index, 1);
|
||||||
|
|
||||||
|
if (commentById[comment.parent_id]) {
|
||||||
|
commentById[comment.parent_id].replies = Math.max(0, (commentById[comment.parent_id].replies || 0) - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (totalRepliesByParentId[comment.parent_id]) {
|
||||||
|
totalRepliesByParentId[comment.parent_id] = Math.max(0, totalRepliesByParentId[comment.parent_id] - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (totalCommentsById[claimId]) {
|
||||||
|
totalCommentsById[claimId] = Math.max(0, totalCommentsById[claimId] - 1);
|
||||||
|
}
|
||||||
|
|
||||||
delete commentById[comment_id];
|
delete commentById[comment_id];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
commentById,
|
commentById,
|
||||||
byId,
|
byId,
|
||||||
|
totalCommentsById,
|
||||||
|
repliesByParentId,
|
||||||
|
totalRepliesByParentId,
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
@ -390,11 +618,41 @@ export default handleActions(
|
||||||
isCommenting: false,
|
isCommenting: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
[ACTIONS.COMMENT_UPDATE_FAILED]: (state: CommentsState, action: any) => ({
|
[ACTIONS.COMMENT_UPDATE_FAILED]: (state: CommentsState, action: any) => ({
|
||||||
...state,
|
...state,
|
||||||
isCmmenting: false,
|
isCmmenting: false,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
[ACTIONS.COMMENT_PIN_COMPLETED]: (state: CommentsState, action: any) => {
|
||||||
|
const { pinnedComment, claimId, unpin } = action.data;
|
||||||
|
const commentById = Object.assign({}, state.commentById);
|
||||||
|
const topLevelCommentsById = Object.assign({}, state.topLevelCommentsById);
|
||||||
|
|
||||||
|
if (pinnedComment && topLevelCommentsById[claimId]) {
|
||||||
|
const index = topLevelCommentsById[claimId].indexOf(pinnedComment.comment_id);
|
||||||
|
if (index > -1) {
|
||||||
|
topLevelCommentsById[claimId].splice(index, 1);
|
||||||
|
|
||||||
|
if (unpin) {
|
||||||
|
// Without the sort score, I have no idea where to put it. Just
|
||||||
|
// dump it at the bottom. Users can refresh if they want it back to
|
||||||
|
// the correct sorted position.
|
||||||
|
topLevelCommentsById[claimId].push(pinnedComment.comment_id);
|
||||||
|
} else {
|
||||||
|
topLevelCommentsById[claimId].unshift(pinnedComment.comment_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
commentById[pinnedComment.comment_id] = pinnedComment;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
commentById,
|
||||||
|
topLevelCommentsById,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
[ACTIONS.COMMENT_MODERATION_BLOCK_LIST_STARTED]: (state: CommentsState, action: any) => ({
|
[ACTIONS.COMMENT_MODERATION_BLOCK_LIST_STARTED]: (state: CommentsState, action: any) => ({
|
||||||
...state,
|
...state,
|
||||||
fetchingModerationBlockList: true,
|
fetchingModerationBlockList: true,
|
||||||
|
|
|
@ -9,6 +9,7 @@ const selectState = (state) => state.comments || {};
|
||||||
|
|
||||||
export const selectCommentsById = createSelector(selectState, (state) => state.commentById || {});
|
export const selectCommentsById = createSelector(selectState, (state) => state.commentById || {});
|
||||||
export const selectIsFetchingComments = createSelector(selectState, (state) => state.isLoading);
|
export const selectIsFetchingComments = createSelector(selectState, (state) => state.isLoading);
|
||||||
|
export const selectIsFetchingCommentsByParentId = createSelector(selectState, (state) => state.isLoadingByParentId);
|
||||||
export const selectIsPostingComment = createSelector(selectState, (state) => state.isCommenting);
|
export const selectIsPostingComment = createSelector(selectState, (state) => state.isCommenting);
|
||||||
export const selectIsFetchingReacts = createSelector(selectState, (state) => state.isFetchingReacts);
|
export const selectIsFetchingReacts = createSelector(selectState, (state) => state.isFetchingReacts);
|
||||||
export const selectCommentsDisabledChannelIds = createSelector(
|
export const selectCommentsDisabledChannelIds = createSelector(
|
||||||
|
@ -143,19 +144,28 @@ export const selectCommentsByUri = createSelector(selectState, (state) => {
|
||||||
return comments;
|
return comments;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const selectLinkedCommentAncestors = createSelector(selectState, (state) => state.linkedCommentAncestors);
|
||||||
|
|
||||||
export const makeSelectCommentIdsForUri = (uri: string) =>
|
export const makeSelectCommentIdsForUri = (uri: string) =>
|
||||||
createSelector(selectState, selectCommentsByUri, selectClaimsById, (state, byUri) => {
|
createSelector(selectState, selectCommentsByUri, selectClaimsById, (state, byUri) => {
|
||||||
const claimId = byUri[uri];
|
const claimId = byUri[uri];
|
||||||
return state.byId[claimId];
|
return state.byId[claimId];
|
||||||
});
|
});
|
||||||
|
|
||||||
export const makeSelectMyReactionsForComment = (commentId: string) =>
|
export const selectMyReactionsByCommentId = createSelector(selectState, (state) => state.myReactsByCommentId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* makeSelectMyReactionsForComment
|
||||||
|
*
|
||||||
|
* @param commentIdChannelId Format = "commentId:MyChannelId".
|
||||||
|
*/
|
||||||
|
export const makeSelectMyReactionsForComment = (commentIdChannelId: string) =>
|
||||||
createSelector(selectState, (state) => {
|
createSelector(selectState, (state) => {
|
||||||
if (!state.myReactsByCommentId) {
|
if (!state.myReactsByCommentId) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
return state.myReactsByCommentId[commentId] || [];
|
return state.myReactsByCommentId[commentIdChannelId] || [];
|
||||||
});
|
});
|
||||||
|
|
||||||
export const makeSelectOthersReactionsForComment = (commentId: string) =>
|
export const makeSelectOthersReactionsForComment = (commentId: string) =>
|
||||||
|
@ -276,6 +286,18 @@ export const makeSelectTopLevelCommentsForUri = (uri: string) =>
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const makeSelectTopLevelTotalCommentsForUri = (uri: string) =>
|
||||||
|
createSelector(selectState, selectCommentsByUri, (state, byUri) => {
|
||||||
|
const claimId = byUri[uri];
|
||||||
|
return state.topLevelTotalCommentsById[claimId] || 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
export const makeSelectTopLevelTotalPagesForUri = (uri: string) =>
|
||||||
|
createSelector(selectState, selectCommentsByUri, (state, byUri) => {
|
||||||
|
const claimId = byUri[uri];
|
||||||
|
return state.topLevelTotalPagesById[claimId] || 0;
|
||||||
|
});
|
||||||
|
|
||||||
export const makeSelectRepliesForParentId = (id: string) =>
|
export const makeSelectRepliesForParentId = (id: string) =>
|
||||||
createSelector(
|
createSelector(
|
||||||
selectState, // no selectRepliesByParentId
|
selectState, // no selectRepliesByParentId
|
||||||
|
@ -334,9 +356,15 @@ export const makeSelectRepliesForParentId = (id: string) =>
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const makeSelectTotalRepliesForParentId = (parentId: string) =>
|
||||||
|
createSelector(selectState, (state) => {
|
||||||
|
return state.totalRepliesByParentId[parentId] || 0;
|
||||||
|
});
|
||||||
|
|
||||||
export const makeSelectTotalCommentsCountForUri = (uri: string) =>
|
export const makeSelectTotalCommentsCountForUri = (uri: string) =>
|
||||||
createSelector(makeSelectCommentsForUri(uri), (comments) => {
|
createSelector(selectState, selectCommentsByUri, (state, byUri) => {
|
||||||
return comments ? comments.length : 0;
|
const claimId = byUri[uri];
|
||||||
|
return state.totalCommentsById[claimId] || 0;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Personal list
|
// Personal list
|
||||||
|
|
|
@ -57,25 +57,6 @@ export const makeSelectHasReachedMaxResultsLength = (query: string): ((state: St
|
||||||
return hasReachedMaxResultsLength[query];
|
return hasReachedMaxResultsLength[query];
|
||||||
});
|
});
|
||||||
|
|
||||||
// Creates a query string based on the state in the search reducer
|
|
||||||
// Can be overrided by passing in custom sizes/from values for other areas pagination
|
|
||||||
|
|
||||||
type CustomOptions = {
|
|
||||||
isBackgroundSearch?: boolean,
|
|
||||||
size?: number,
|
|
||||||
from?: number,
|
|
||||||
related_to?: string,
|
|
||||||
nsfw?: boolean,
|
|
||||||
};
|
|
||||||
|
|
||||||
export const makeSelectQueryWithOptions = (customQuery: ?string, options: CustomOptions) =>
|
|
||||||
createSelector(selectSearchValue, selectSearchOptions, (query, defaultOptions) => {
|
|
||||||
const searchOptions = { ...defaultOptions, ...options };
|
|
||||||
const queryString = getSearchQueryString(customQuery || query, searchOptions);
|
|
||||||
|
|
||||||
return queryString;
|
|
||||||
});
|
|
||||||
|
|
||||||
export const makeSelectRecommendedContentForUri = (uri: string) =>
|
export const makeSelectRecommendedContentForUri = (uri: string) =>
|
||||||
createSelector(
|
createSelector(
|
||||||
makeSelectClaimForUri(uri),
|
makeSelectClaimForUri(uri),
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import { SETTINGS, DAEMON_SETTINGS } from 'lbry-redux';
|
import { SETTINGS, DAEMON_SETTINGS } from 'lbry-redux';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import homepages from 'homepages';
|
|
||||||
import { ENABLE_MATURE } from 'config';
|
import { ENABLE_MATURE } from 'config';
|
||||||
import { getDefaultHomepageKey, getDefaultLanguage } from 'util/default-languages';
|
import { getDefaultHomepageKey, getDefaultLanguage } from 'util/default-languages';
|
||||||
|
const homepages = require('homepages');
|
||||||
|
|
||||||
const selectState = (state) => state.settings || {};
|
const selectState = (state) => state.settings || {};
|
||||||
|
|
||||||
|
@ -70,7 +70,8 @@ export const selectHomepageData = createSelector(
|
||||||
selectHomepageCode,
|
selectHomepageCode,
|
||||||
(homepageCode) => {
|
(homepageCode) => {
|
||||||
// homepages = { 'en': homepageFile, ... }
|
// homepages = { 'en': homepageFile, ... }
|
||||||
return homepages[homepageCode] || homepages['en'];
|
// mixin Homepages here
|
||||||
|
return homepages[homepageCode] || homepages['en'] || {};
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -677,9 +677,31 @@
|
||||||
|
|
||||||
& > * {
|
& > * {
|
||||||
color: var(--color-black);
|
color: var(--color-black);
|
||||||
background-color: var(--color-white);
|
background-color: var(--color-black);
|
||||||
padding: var(--spacing-xs);
|
|
||||||
border-radius: var(--border-radius);
|
border-radius: var(--border-radius);
|
||||||
|
padding: var(--spacing-xxs);
|
||||||
|
margin-right: 0;
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--color-button-alt-bg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.button--file-action {
|
||||||
|
margin: 0 0;
|
||||||
|
padding: var(--spacing-xxs) var(--spacing-xxs);
|
||||||
|
height: unset;
|
||||||
|
|
||||||
|
.button__label {
|
||||||
|
color: var(--color-text);
|
||||||
|
font-size: var(--font-small);
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
.button__label {
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -441,3 +441,7 @@ $thumbnailWidthSmall: 1rem;
|
||||||
.comment__tip-input {
|
.comment__tip-input {
|
||||||
margin: var(--spacing-s) 0;
|
margin: var(--spacing-s) 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.comment--blocked {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
324
ui/util/buildHomepage.js
Normal file
324
ui/util/buildHomepage.js
Normal file
|
@ -0,0 +1,324 @@
|
||||||
|
// @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 { CUSTOM_HOMEPAGE } from 'config';
|
||||||
|
|
||||||
|
export type RowDataItem = {
|
||||||
|
title: any,
|
||||||
|
link?: string,
|
||||||
|
help?: any,
|
||||||
|
icon?: string,
|
||||||
|
extra?: any,
|
||||||
|
options?: {
|
||||||
|
channelIds?: Array<string>,
|
||||||
|
pageSize?: number,
|
||||||
|
limitClaimsPerChannel?: number,
|
||||||
|
},
|
||||||
|
route?: string,
|
||||||
|
hideForUnauth?: boolean,
|
||||||
|
};
|
||||||
|
|
||||||
|
export type HomepageCat = {
|
||||||
|
name: string,
|
||||||
|
icon: string,
|
||||||
|
label: string,
|
||||||
|
channelIds?: Array<string>,
|
||||||
|
daysOfContent?: number,
|
||||||
|
channelLimit?: string,
|
||||||
|
pageSize?: number,
|
||||||
|
claimType?: string,
|
||||||
|
order?: string,
|
||||||
|
tags?: Array<string>,
|
||||||
|
pinnedUrls?: Array<string>,
|
||||||
|
mixIn?: Array<string>,
|
||||||
|
};
|
||||||
|
|
||||||
|
function getLimitPerChannel(size, isChannel) {
|
||||||
|
if (isChannel) {
|
||||||
|
return 1;
|
||||||
|
} else {
|
||||||
|
return size < 250 ? (size < 150 ? 3 : 2) : 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getHomepageRowForCat = (cat: HomepageCat) => {
|
||||||
|
let orderValue;
|
||||||
|
switch (cat.order) {
|
||||||
|
case 'trending':
|
||||||
|
orderValue = CS.ORDER_BY_TRENDING_VALUE;
|
||||||
|
break;
|
||||||
|
case 'top':
|
||||||
|
orderValue = CS.ORDER_BY_TOP_VALUE;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
orderValue = CS.ORDER_BY_NEW_VALUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
let urlParams = new URLSearchParams();
|
||||||
|
if (cat.claimType) {
|
||||||
|
urlParams.set(CS.CLAIM_TYPE, cat.claimType);
|
||||||
|
}
|
||||||
|
if (cat.channelIds) {
|
||||||
|
urlParams.set(CS.CHANNEL_IDS_KEY, cat.channelIds.join(','));
|
||||||
|
}
|
||||||
|
|
||||||
|
const isChannelType = cat.claimType && cat.claimType === 'channel';
|
||||||
|
|
||||||
|
// can intend no limit, numerica auto limit, specific limit.
|
||||||
|
let limitClaims;
|
||||||
|
if (typeof cat.channelLimit === 'string' && cat.channelIds && cat.channelIds.length) {
|
||||||
|
if (cat.channelLimit === 'auto') {
|
||||||
|
limitClaims = getLimitPerChannel(cat.channelIds.length, isChannelType);
|
||||||
|
} else if (cat.channelLimit) {
|
||||||
|
const limitNumber = Number(cat.channelLimit);
|
||||||
|
// eslint-disable-next-line
|
||||||
|
if (limitNumber === limitNumber && limitNumber !== 0) {
|
||||||
|
// because javascript and NaN !== NaN
|
||||||
|
limitClaims = Math.floor(limitNumber);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
link: `/$/${PAGES.DISCOVER}?${urlParams.toString()}`,
|
||||||
|
route: cat.name ? `/$/${cat.name}` : undefined,
|
||||||
|
icon: cat.icon || '', // some default
|
||||||
|
title: cat.label,
|
||||||
|
options: {
|
||||||
|
claimType: cat.claimType || 'stream',
|
||||||
|
channelIds: cat.channelIds,
|
||||||
|
orderBy: orderValue,
|
||||||
|
pageSize: cat.pageSize || undefined,
|
||||||
|
limitClaimsPerChannel: limitClaims,
|
||||||
|
releaseTime: `>${Math.floor(
|
||||||
|
moment()
|
||||||
|
.subtract(cat.daysOfContent || 30, 'days')
|
||||||
|
.startOf('week')
|
||||||
|
.unix()
|
||||||
|
)}`,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export function GetLinksData(
|
||||||
|
all: any,
|
||||||
|
isHomepage?: boolean = false,
|
||||||
|
authenticated?: boolean,
|
||||||
|
showPersonalizedChannels?: boolean,
|
||||||
|
showPersonalizedTags?: boolean,
|
||||||
|
subscribedChannels?: Array<Subscription>,
|
||||||
|
followedTags?: Array<Tag>,
|
||||||
|
showIndividualTags?: boolean,
|
||||||
|
showNsfw?: boolean
|
||||||
|
) {
|
||||||
|
const isLargeScreen = useIsLargeScreen();
|
||||||
|
|
||||||
|
function getPageSize(originalSize) {
|
||||||
|
return isLargeScreen ? originalSize * (3 / 2) : originalSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
// $FlowFixMe
|
||||||
|
let rowData: Array<RowDataItem> = [];
|
||||||
|
const individualTagDataItems: Array<RowDataItem> = [];
|
||||||
|
|
||||||
|
const YOUTUBER_CHANNEL_IDS = [
|
||||||
|
'fb364ef587872515f545a5b4b3182b58073f230f',
|
||||||
|
'589276465a23c589801d874f484cc39f307d7ec7',
|
||||||
|
'ba79c80788a9e1751e49ad401f5692d86f73a2db',
|
||||||
|
'b6e207c5f8c58e7c8362cd05a1501bf2f5b694f2',
|
||||||
|
'c5724e280283cd985186af9a62494aae377daabd',
|
||||||
|
'243b6f18093ff97c861d0568c7d3379606201a4b',
|
||||||
|
'5b7c7a202201033d99e1be2930d290c127c0f4fe',
|
||||||
|
'c9da929d12afe6066acc89eb044b552f0d63782a',
|
||||||
|
'89985db232ec2a9a31dbd985196de817da223fe6',
|
||||||
|
'187bf3616318b4bfb85223fc40724c307696f0c6',
|
||||||
|
'aa3db8d2145340e26597b88fbb6d0e7ff09786be',
|
||||||
|
'a9d289718f3f14e3d1fa8da7a7fcfdb6f40ae2d7',
|
||||||
|
'9a5dfcb1a4b29c3a1598392d039744b9938b5a26',
|
||||||
|
'0b998b98a2b9a88d9519739f99f2c74c95e3fc22',
|
||||||
|
'46be492ee0f56db11e005991c537c867a8682f77',
|
||||||
|
'c5cd9b63e2ba0abc191feae48238f464baecb147',
|
||||||
|
'4b602d7a3e268abb45951f623a109d2a131ab0ba',
|
||||||
|
'd25ae97a1516f5700fc717152b885f33da47f12b',
|
||||||
|
'8f4fecfc836ea33798ee3e5cef56926fa54e2cf9',
|
||||||
|
'8671dfd2f34302c1a4dcb4dd7361568a0bb23eba',
|
||||||
|
'b9288432bd089c6f332145aab08a56eec155f307',
|
||||||
|
'87b13b074936b1f42a7c6758c7c2995f58c602e7',
|
||||||
|
'25f384bd95e218f6ac37fcaca99ed40f36760d8c',
|
||||||
|
'02c020b2fab7dd1fbd175c3b22947688c0a219e5',
|
||||||
|
'57dbc8fdc4d062e2824d8550861b380203539099',
|
||||||
|
'4e17d248adc0128afe969c2e1327e10afd9cb921',
|
||||||
|
'760da3ba3dd85830a843beaaed543a89b7a367e7',
|
||||||
|
'5a1b164d0a2e7adf1db08d7363ea1cb06c30cd74',
|
||||||
|
'c9da929d12afe6066acc89eb044b552f0d63782a',
|
||||||
|
'113515e893b8186595595e594ecc410bae50c026',
|
||||||
|
'5fbfcf517d3df749bd032a44c1946b2baa738ecb',
|
||||||
|
'74333143a3dcc001a5602aa524583fc75a013d75',
|
||||||
|
'0d4e104ffc0ff0a6c8701e67cf13760f4e0335a8',
|
||||||
|
'b924ac36b7499591f7929d9c4903de79b07b1cb9',
|
||||||
|
'13edd7e7e2fbaf845699cf2f8f0b1c095bacb05f',
|
||||||
|
'7b1c72ba903af4aecdc2595397a9cb91bb7f188d',
|
||||||
|
'6c0bf1fed2705d675da950a94e4af004ec975a06',
|
||||||
|
'f33657a2fcbab2dc3ce555d5d6728f8758af7bc7',
|
||||||
|
'26c9b54d7e47dc8f7dc847821b26fce3009ee1a0',
|
||||||
|
'1516361918bfd02ddd460489f438e153c918521c',
|
||||||
|
'd468989b4668bce7452fc3434a6dc7ba7d799378',
|
||||||
|
'a1c8f84670da9a3371bc5832e86c8d32826b2f2e',
|
||||||
|
'70e56234217f30317c0e67fd0eede6e82b74aea0',
|
||||||
|
'7a88e0eabf60af5ac61240fe60f8f08fa3e48ab4',
|
||||||
|
'2f229d3ac26aa655c5123c29f1f7352403279ca3',
|
||||||
|
'7ea92a937f5755b40ac3d99ed37c53b40359b0a2',
|
||||||
|
'96ede5667bc4533ace8cfcbde4f33aa9fe1ae5f5',
|
||||||
|
'5097b175c4c58c431424ce3b60901de6ae650127',
|
||||||
|
'32de523ba228dd3f3159eb5a6cc07b6fd51f4dc0',
|
||||||
|
'cdb6fe516afe08618b91a754f92412e7f98a8a62',
|
||||||
|
'1e9f582c2df798228e8583fe1101fee551487d4b',
|
||||||
|
'b032695b52a78e0f5251b8d7f2f32183a5985d19',
|
||||||
|
'c042155dfcb5c813345248bff18a62d0f585718e',
|
||||||
|
'294f5c164da5ac9735658b2d58d8fee6745dfc45',
|
||||||
|
'07e4546674268fc0222b2ca22d31d0549dc217ee',
|
||||||
|
'1487afc813124abbeb0629d2172be0f01ccec3bf',
|
||||||
|
'ac471128a5ed05b80365170b29997d860afa33b7',
|
||||||
|
'c101bac49ec048acca169fd6090e70f7488645b1',
|
||||||
|
'c9282bbb89d3f9f5f1d972a02f96a5f0f0f40df8',
|
||||||
|
'9576be30de21b3b755828314d6ccbbaa3334f43a',
|
||||||
|
'b12e255e9f84d8b4ed86343b27676dccbc8b6d8b',
|
||||||
|
'50ebba2b06908f93d7963b1c6826cc0fd6104477',
|
||||||
|
'84342ae85d216d5ffc0ef149a123aae649d5c253',
|
||||||
|
'80f78c4b8591390758b9e6303eaf9087180444ad',
|
||||||
|
'086d2bacf441cef45ff15b5afe163d0b03a9f7ea',
|
||||||
|
'5af39f818f668d8c00943c9326c5201c4fe3c423',
|
||||||
|
'057053dfb657aaa98553e2c544b06e1a2371557e',
|
||||||
|
'fd1aee1d4858ec2ef6ccc3e60504c76e9d774386',
|
||||||
|
'930fc43ca7bae20d4706543e97175d1872b0671f',
|
||||||
|
'e715c457b4a3e51214b62f49f05303bba4ee5be9',
|
||||||
|
'ebf5bc6842638cefcf66904522ac96231ea7a9d8',
|
||||||
|
'1f9bb08bfa2259629f4aaa9ed40f97e9a41b6fa1',
|
||||||
|
'ac415179241e0cd8a14ed71175b759254d381137',
|
||||||
|
'8e098d2042ad9b9074f52cc06b89d6d4db5231dd',
|
||||||
|
'149c4686ff0792b8d68dac1f17b6273a85628d34',
|
||||||
|
'199eba05b6ecccab919e26a0cb7dacd544f25700',
|
||||||
|
'6569758308f12a66001e28f5e6056cb84334e69c',
|
||||||
|
'e50f82e2236274c54af762a9c2b897646477ef62',
|
||||||
|
'7e1a7afadc8734b33a3e219f5668470715fb063d',
|
||||||
|
'ff80e24f41a2d706c70df9779542cba4715216c9',
|
||||||
|
'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 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 LATEST_FROM_LBRY = {
|
||||||
|
title: __('Latest From @lbry'),
|
||||||
|
link: `/@lbry:3f`,
|
||||||
|
options: {
|
||||||
|
orderBy: ['release_time'],
|
||||||
|
pageSize: getPageSize(4),
|
||||||
|
channelIds: ['3fda836a92faaceedfe398225fb9b2ee2ed1f01a'],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isHomepage && showPersonalizedChannels && subscribedChannels) {
|
||||||
|
const RECENT_FROM_FOLLOWING = {
|
||||||
|
title: __('Recent From Following'),
|
||||||
|
link: `/$/${PAGES.CHANNELS_FOLLOWING}`,
|
||||||
|
icon: ICONS.SUBSCRIBE,
|
||||||
|
options: {
|
||||||
|
orderBy: CS.ORDER_BY_NEW_VALUE,
|
||||||
|
releaseTime:
|
||||||
|
subscribedChannels.length > 20
|
||||||
|
? `>${Math.floor(moment().subtract(9, 'months').startOf('week').unix())}`
|
||||||
|
: `>${Math.floor(moment().subtract(1, 'year').startOf('week').unix())}`,
|
||||||
|
pageSize: getPageSize(subscribedChannels.length > 3 ? (subscribedChannels.length > 6 ? 16 : 8) : 4),
|
||||||
|
streamTypes: CS.FILE_TYPES,
|
||||||
|
channelIds: subscribedChannels.map((subscription: Subscription) => {
|
||||||
|
const { channelClaimId } = parseURI(subscription.uri);
|
||||||
|
return channelClaimId;
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
rowData.push(RECENT_FROM_FOLLOWING);
|
||||||
|
}
|
||||||
|
if (isHomepage && !CUSTOM_HOMEPAGE) {
|
||||||
|
if (followedTags) {
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
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'],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
if (showPersonalizedTags && !showIndividualTags) rowData.push(TRENDING_FOR_TAGS);
|
||||||
|
if (showPersonalizedTags && showIndividualTags) {
|
||||||
|
individualTagDataItems.forEach((item: RowDataItem) => {
|
||||||
|
rowData.push(item);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!CUSTOM_HOMEPAGE) {
|
||||||
|
if (!authenticated) {
|
||||||
|
rowData.push(YOUTUBE_CREATOR_ROW);
|
||||||
|
}
|
||||||
|
rowData.push(TOP_CONTENT_TODAY);
|
||||||
|
rowData.push(LATEST_FROM_LBRY);
|
||||||
|
if (!showPersonalizedChannels) rowData.push(TOP_CHANNELS);
|
||||||
|
}
|
||||||
|
(Object.values(all): any).map((row) => rowData.push(getHomepageRowForCat(row)));
|
||||||
|
return rowData;
|
||||||
|
}
|
|
@ -10,11 +10,10 @@ type SortProps = {
|
||||||
reactionsById: {},
|
reactionsById: {},
|
||||||
sort: string,
|
sort: string,
|
||||||
isMyComment: (string) => boolean,
|
isMyComment: (string) => boolean,
|
||||||
justCommented: Array<string>,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export function sortComments(sortProps: SortProps): Array<Comment> {
|
export function sortComments(sortProps: SortProps): Array<Comment> {
|
||||||
const { comments, reactionsById, sort, isMyComment, justCommented } = sortProps;
|
const { comments, reactionsById, sort, isMyComment } = sortProps;
|
||||||
|
|
||||||
if (!comments) return [];
|
if (!comments) return [];
|
||||||
|
|
||||||
|
@ -29,12 +28,10 @@ export function sortComments(sortProps: SortProps): Array<Comment> {
|
||||||
|
|
||||||
const aIsMine = isMyComment(a.channel_id);
|
const aIsMine = isMyComment(a.channel_id);
|
||||||
const bIsMine = isMyComment(b.channel_id);
|
const bIsMine = isMyComment(b.channel_id);
|
||||||
const aIsMyRecent = justCommented.includes(a.comment_id);
|
|
||||||
const bIsMyRecent = justCommented.includes(b.comment_id);
|
|
||||||
|
|
||||||
if (aIsMine && justCommented.length && aIsMyRecent) {
|
if (aIsMine) {
|
||||||
return -1;
|
return -1;
|
||||||
} else if (bIsMine && justCommented.length && bIsMyRecent) {
|
} else if (bIsMine) {
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
9
web/component/pixel/index.js
Normal file
9
web/component/pixel/index.js
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { selectUserVerifiedEmail } from 'redux/selectors/user';
|
||||||
|
import Pixel from './view';
|
||||||
|
|
||||||
|
const select = (state) => ({
|
||||||
|
isAuthenticated: Boolean(selectUserVerifiedEmail(state)),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(select)(Pixel);
|
42
web/component/pixel/view.jsx
Normal file
42
web/component/pixel/view.jsx
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
// @flow
|
||||||
|
import React from 'react';
|
||||||
|
import { SIMPLE_SITE } from 'config';
|
||||||
|
import { useIsMobile } from 'effects/use-screensize';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
type: string,
|
||||||
|
isAuthenticated: boolean,
|
||||||
|
};
|
||||||
|
|
||||||
|
const Pixel = (props: Props) => {
|
||||||
|
const { type, isAuthenticated } = props;
|
||||||
|
const isMobile = useIsMobile();
|
||||||
|
|
||||||
|
// TODO: restrict to country
|
||||||
|
if (!SIMPLE_SITE || isMobile || isAuthenticated) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (type === 'retargeting') {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<img
|
||||||
|
src="https://ctrack.trafficjunky.net/ctrack?action=list&type=add&id=0&context=Odysee&cookiename=RetargetingPixel&age=44000&maxcookiecount=10"
|
||||||
|
alt=""
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
} else if (type === 'kill') {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<img
|
||||||
|
src="https://ctrack.trafficjunky.net/ctrack?action=list&type=add&id=0&context=Odysee&cookiename=KillPixel&age=0&maxcookiecount=10"
|
||||||
|
alt=""
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Pixel;
|
13
web/src/getHomepageJSON.js
Normal file
13
web/src/getHomepageJSON.js
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
const memo = {};
|
||||||
|
// this didn't seem to help.
|
||||||
|
if (!memo.homepageData) {
|
||||||
|
try {
|
||||||
|
memo.homepageData = require('../../custom/homepages/v2');
|
||||||
|
} catch (err) {
|
||||||
|
console.log('homepage data failed');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const getHomepageJSON = () => {
|
||||||
|
return memo.homepageData || {};
|
||||||
|
};
|
||||||
|
module.exports = { getHomepageJSON };
|
|
@ -1,10 +1,10 @@
|
||||||
const { getHtml } = require('./html');
|
const { getHtml } = require('./html');
|
||||||
const { getRss } = require('./rss');
|
const { getRss } = require('./rss');
|
||||||
|
const { getHomepageJSON } = require('./getHomepageJSON');
|
||||||
const { generateStreamUrl } = require('../../ui/util/web');
|
const { generateStreamUrl } = require('../../ui/util/web');
|
||||||
const fetch = require('node-fetch');
|
const fetch = require('node-fetch');
|
||||||
const Router = require('@koa/router');
|
const Router = require('@koa/router');
|
||||||
const fs = require('fs');
|
const { CUSTOM_HOMEPAGE } = require('../../config.js');
|
||||||
const path = require('path');
|
|
||||||
|
|
||||||
// So any code from 'lbry-redux'/'lbryinc' that uses `fetch` can be run on the server
|
// So any code from 'lbry-redux'/'lbryinc' that uses `fetch` can be run on the server
|
||||||
global.fetch = fetch;
|
global.fetch = fetch;
|
||||||
|
@ -24,18 +24,28 @@ const rssMiddleware = async (ctx) => {
|
||||||
ctx.body = xml;
|
ctx.body = xml;
|
||||||
};
|
};
|
||||||
|
|
||||||
router.get(`/$/api/content/get`, async (ctx) => {
|
router.get(`/$/api/content/v1/get`, async (ctx) => {
|
||||||
let content;
|
if (!CUSTOM_HOMEPAGE) {
|
||||||
try {
|
ctx.status = 404;
|
||||||
content = await fs.readFileSync(path.join(__dirname, '../../custom/content/test.json'), 'utf-8');
|
ctx.body = {
|
||||||
} catch (e) {
|
message: 'Not Found',
|
||||||
content = await fs.readFileSync(path.join(__dirname, '../../custom/content/default.json'), 'utf-8');
|
};
|
||||||
|
} else {
|
||||||
|
let content;
|
||||||
|
try {
|
||||||
|
content = getHomepageJSON();
|
||||||
|
ctx.set('Content-Type', 'application/json');
|
||||||
|
ctx.body = {
|
||||||
|
status: 'success',
|
||||||
|
data: content,
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
ctx.status = err.statusCode || err.status || 500;
|
||||||
|
ctx.body = {
|
||||||
|
message: err.message,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.body = {
|
|
||||||
status: 'success',
|
|
||||||
data: content,
|
|
||||||
};
|
|
||||||
});
|
});
|
||||||
|
|
||||||
router.get(`/$/download/:claimName/:claimId`, async (ctx) => {
|
router.get(`/$/download/:claimName/:claimId`, async (ctx) => {
|
||||||
|
|
|
@ -39,17 +39,17 @@ let baseConfig = {
|
||||||
{
|
{
|
||||||
test: /\.s?css$/,
|
test: /\.s?css$/,
|
||||||
use: [
|
use: [
|
||||||
{ loader: 'style-loader' },
|
{ loader: 'style-loader' },
|
||||||
{ loader: 'css-loader' },
|
{ loader: 'css-loader' },
|
||||||
{ loader: 'postcss-loader',
|
{ loader: 'postcss-loader',
|
||||||
options: {
|
options: {
|
||||||
plugins: function () {
|
plugins: function () {
|
||||||
return [ require( 'postcss-rtl' )() ]
|
return [ require('postcss-rtl')() ];
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{ loader: 'sass-loader'},
|
{ loader: 'sass-loader'},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
test: /\.(png|svg|gif)$/,
|
test: /\.(png|svg|gif)$/,
|
||||||
|
@ -76,7 +76,7 @@ let baseConfig = {
|
||||||
alias: {
|
alias: {
|
||||||
config: path.resolve(__dirname, 'config.js'),
|
config: path.resolve(__dirname, 'config.js'),
|
||||||
homepage: 'util/homepage.js',
|
homepage: 'util/homepage.js',
|
||||||
homepages: process.env.CUSTOM_HOMEPAGE === 'true' ? path.resolve(__dirname, 'custom/homepages/index.js') : ('homepages/index.js'),
|
homepages: process.env.CUSTOM_HOMEPAGE === 'true' ? path.resolve(__dirname, 'custom/homepages/v2/index.js') : ('homepages/index.js'),
|
||||||
lbryinc: 'lbryinc/dist/bundle.es.js',
|
lbryinc: 'lbryinc/dist/bundle.es.js',
|
||||||
// Build optimizations for 'redux-persist-transform-filter'
|
// Build optimizations for 'redux-persist-transform-filter'
|
||||||
'redux-persist-transform-filter': 'redux-persist-transform-filter/index.js',
|
'redux-persist-transform-filter': 'redux-persist-transform-filter/index.js',
|
||||||
|
|
Loading…
Add table
Reference in a new issue