diff --git a/.env.ody b/.env.ody deleted file mode 100644 index 2d9e1313e..000000000 --- a/.env.ody +++ /dev/null @@ -1,92 +0,0 @@ -# Copy this file to .env to make modifications - -# Base config - -WEBPACK_WEB_PORT=9090 -WEBPACK_ELECTRON_PORT=9091 -WEB_SERVER_PORT=1337 - -WELCOME_VERSION=1.0 - -# Custom Site info -DOMAIN=lbry.tv -URL=https://lbry.tv - -# UI -SITE_TITLE=lbry.tv -SITE_NAME=local.lbry.tv -SITE_DESCRIPTION=Meet LBRY, an open, free, and community-controlled content wonderland. -LOGO_TITLE=local.lbry.tv - -##### ODYSEE SETTINGS ####### - -MATOMO_URL=https://analytics.lbry.com/ -MATOMO_ID=4 - -# Base config -WEBPACK_WEB_PORT=9090 -WEBPACK_ELECTRON_PORT=9091 -WEB_SERVER_PORT=1337 - -## APIS -LBRY_API_URL=https://api.odysee.com -#LBRY_WEB_API=https://api.na-backend.odysee.com -#LBRY_WEB_STREAMING_API=https://cdn.lbryplayer.xyz -# deprecated: -#LBRY_WEB_BUFFER_API=https://collector-service.api.lbry.tv/api/v1/events/video -#COMMENT_SERVER_API=https://comments.lbry.com/api/v2 -WELCOME_VERSION=1.0 - -# STRIPE -STRIPE_PUBLIC_KEY='pk_live_e8M4dRNnCCbmpZzduEUZBgJO' - -## UI - -LOADING_BAR_COLOR=#e50054 - -# IMAGE ASSETS -YRBL_HAPPY_IMG_URL=https://spee.ch/spaceman-happy:a.png -YRBL_SAD_IMG_URL=https://spee.ch/spaceman-sad:d.png -LOGIN_IMG_URL=https://spee.ch/login:b.png -LOGO=https://spee.ch/odysee-logo-png:3.png -LOGO_TEXT_LIGHT=https://spee.ch/odysee-white-png:f.png -LOGO_TEXT_DARK=https://spee.ch/odysee-png:2.png -AVATAR_DEFAULT=https://spee.ch/spaceman-png:2.png -FAVICON=https://spee.ch/favicon-png:c.png - -# LOCALE -DEFAULT_LANGUAGE=en - -## LINKED CONTENT WHITELIST -KNOWN_APP_DOMAINS=open.lbry.com,lbry.tv,lbry.lat,odysee.com - -## CUSTOM CONTENT -# If the following is true, copy custom/homepage.example.js to custom/homepage.js and modify -CUSTOM_HOMEPAGE=true - -# Add channels to auto-follow on firstrun (space delimited) -AUTO_FOLLOW_CHANNELS=lbry://@Odysee#80d2590ad04e36fb1d077a9b9e3a8bba76defdf8 lbry://@OdyseeHelp#b58dfaeab6c70754d792cdd9b56ff59b90aea334 - -## FEATURES AND LIMITS -SIMPLE_SITE=true -BRANDED_SITE=odysee -# SIMPLE_SITE REPLACEMENTS -ENABLE_MATURE=false -ENABLE_UI_NOTIFICATIONS=true -ENABLE_WILD_WEST=true -SHOW_TAGS_INTRO=false - -# CENTRALIZED FEATURES -ENABLE_COMMENT_REACTIONS=true -ENABLE_FILE_REACTIONS=true -ENABLE_CREATOR_REACTIONS=true -ENABLE_NO_SOURCE_CLAIMS=true -ENABLE_PREROLL_ADS=false -SHOW_ADS=true - -CHANNEL_STAKED_LEVEL_VIDEO_COMMENTS=4 -CHANNEL_STAKED_LEVEL_LIVESTREAM=3 -WEB_PUBLISH_SIZE_LIMIT_GB=4 - -#SEARCH TYPES - comma-delimited -LIGHTHOUSE_DEFAULT_TYPES=audio,video diff --git a/.flowconfig b/.flowconfig index 2c6b8839f..0ae16f24e 100644 --- a/.flowconfig +++ b/.flowconfig @@ -43,6 +43,8 @@ module.name_mapper='^web\/page\(.*\)$' -> '/web/page\1' module.name_mapper='^homepage\(.*\)$' -> '/ui/util/homepage\1' module.name_mapper='^scss\/component\(.*\)$' -> '/ui/scss/component/\1' +esproposal.optional_chaining=enable + ; Extensions module.file_ext=.js module.file_ext=.jsx @@ -51,4 +53,5 @@ module.file_ext=.css module.file_ext=.scss + [strict] diff --git a/.gitignore b/.gitignore index cef7245af..f3adf8f8a 100644 --- a/.gitignore +++ b/.gitignore @@ -33,6 +33,6 @@ package-lock.json !/custom/robots.disallowall !/custom/robots.allowall .env -!.env.ody +.env.ody .env.desktop .env.lbrytv diff --git a/flow-typed/File.js b/flow-typed/File.js deleted file mode 100644 index 44dc4f398..000000000 --- a/flow-typed/File.js +++ /dev/null @@ -1,78 +0,0 @@ -// @flow - -declare type FileListItem = { - metadata: StreamMetadata, - added_on: number, - blobs_completed: number, - blobs_in_stream: number, - blobs_remaining: number, - channel_claim_id: string, - channel_name: string, - claim_id: string, - claim_name: string, - completed: false, - content_fee?: { txid: string }, - purchase_receipt?: { txid: string, amount: string }, - download_directory: string, - download_path: string, - file_name: string, - key: string, - mime_type: string, - nout: number, - outpoint: string, - points_paid: number, - protobuf: string, - reflector_progress: number, - sd_hash: string, - status: string, - stopped: false, - stream_hash: string, - stream_name: string, - streaming_url: string, - suggested_file_name: string, - total_bytes: number, - total_bytes_lower_bound: number, - is_fully_reflected: boolean, - // TODO: sdk plans to change `tx` - // It isn't currently used by the apps - tx: {}, - txid: string, - uploading_to_reflector: boolean, - written_bytes: number, -}; - -declare type FileState = { - failedPurchaseUris: Array, - purchasedUris: Array, -}; - -declare type PurchaseUriCompleted = { - type: ACTIONS.PURCHASE_URI_COMPLETED, - data: { - uri: string, - streamingUrl: string, - }, -}; - -declare type PurchaseUriFailed = { - type: ACTIONS.PURCHASE_URI_FAILED, - data: { - uri: string, - error: any, - }, -}; - -declare type PurchaseUriStarted = { - type: ACTIONS.PURCHASE_URI_STARTED, - data: { - uri: string, - streamingUrl: string, - }, -}; - -declare type DeletePurchasedUri = { - type: ACTIONS.CLEAR_PURCHASED_URI_SUCCESS, - data: { - uri: string, - }, -}; diff --git a/package.json b/package.json index f1545afa3..6826b3296 100644 --- a/package.json +++ b/package.json @@ -75,6 +75,7 @@ "@babel/plugin-proposal-class-properties": "^7.0.0", "@babel/plugin-proposal-decorators": "^7.3.0", "@babel/plugin-proposal-object-rest-spread": "^7.6.2", + "@babel/plugin-proposal-optional-chaining": "^7.14.5", "@babel/plugin-syntax-dynamic-import": "^7.2.0", "@babel/plugin-transform-flow-strip-types": "^7.2.3", "@babel/plugin-transform-runtime": "^7.4.3", diff --git a/static/app-strings.json b/static/app-strings.json index 5373770c1..5baa9217f 100644 --- a/static/app-strings.json +++ b/static/app-strings.json @@ -1638,7 +1638,6 @@ "This link leads to an external website.": "This link leads to an external website.", "No Content Found": "No Content Found", "No Lists Found": "No Lists Found", - "No matching playlists": "No matching playlists", "You have no lists! Create one from any playable content.": "You have no lists! Create one from any playable content.", "Pick": "Pick", "You have unpublished lists! %pick% one and publish it!": "You have unpublished lists! %pick% one and publish it!", @@ -2059,8 +2058,6 @@ "MyAwesomeList": "MyAwesomeList", "My Awesome List": "My Awesome List", "This list has no items.": "This list has no items.", - "1 item": "1 item", - "%collectionCount% items": "%collectionCount% items", "Select File": "Select File", "File Selected": "File Selected", "Url": "Url", @@ -2148,8 +2145,11 @@ "%title% by %channelTitle%": "%title% by %channelTitle%", "%title% by %channelTitle% %ariaDate%": "%title% by %channelTitle% %ariaDate%", "%title% by %channelTitle% %ariaDate%, %mediaDuration%": "%title% by %channelTitle% %ariaDate%, %mediaDuration%", + "Collapse Thread": "Collapse Thread", "Collapse": "Collapse", + "Expand Comments": "Expand Comments", "Expand": "Expand", + "Load More": "Load More", "%formattedSubCount% Followers": "%formattedSubCount% Followers", "1 Follower": "1 Follower", "Collection": "Collection", @@ -2183,6 +2183,5 @@ "Creator": "Creator", "From comments": "From comments", "From search": "From search", - "Manage tags": "Manage tags", "--end--": "--end--" } diff --git a/ui/analytics.js b/ui/analytics.js index 495535dcc..feae9a64a 100644 --- a/ui/analytics.js +++ b/ui/analytics.js @@ -10,6 +10,15 @@ import ElectronCookies from '@exponent/electron-cookies'; import { generateInitialUrl } from 'util/url'; // @endif import { MATOMO_ID, MATOMO_URL } from 'config'; +// import getConnectionSpeed from 'util/detect-user-bandwidth'; + +// let userDownloadBandwidthInBitsPerSecond; +// async function getUserBandwidth() { +// userDownloadBandwidthInBitsPerSecond = await getConnectionSpeed(); +// } + +// get user bandwidth every minute, starting after an initial one minute wait +// setInterval(getUserBandwidth, 1000 * 60); const isProduction = process.env.NODE_ENV === 'production'; const devInternalApis = process.env.LBRY_API_URL && process.env.LBRY_API_URL.includes('dev'); @@ -40,7 +49,7 @@ type Analytics = { tagFollowEvent: (string, boolean, ?string) => void, playerLoadedEvent: (?boolean) => void, playerStartedEvent: (?boolean) => void, - videoStartEvent: (string, number, string, number, string, any) => void, + videoStartEvent: (string, number, string, number, string, any, number) => void, videoIsPlaying: (boolean, any) => void, videoBufferEvent: ( StreamClaim, @@ -111,7 +120,7 @@ function getDeviceType() { // variables initialized for watchman let amountOfBufferEvents = 0; let amountOfBufferTimeInMS = 0; -let videoType, userId, claimUrl, playerPoweredBy, videoPlayer; +let videoType, userId, claimUrl, playerPoweredBy, videoPlayer, bitrateAsBitsPerSecond; let lastSentTime; // calculate data for backend, send them, and reset buffer data for next interval @@ -130,6 +139,9 @@ async function sendAndResetWatchmanData() { let protocol; if (videoType === 'application/x-mpegURL') { protocol = 'hls'; + // get bandwidth if it exists from the texttrack (so it's accurate if user changes quality) + // $FlowFixMe + bitrateAsBitsPerSecond = videoPlayer.textTracks?.().tracks_[0]?.activeCues[0]?.value?.bandwidth; } else { protocol = 'stb'; } @@ -152,6 +164,9 @@ async function sendAndResetWatchmanData() { user_id: userId.toString(), position: Math.round(positionInVideo), rel_position: Math.round((positionInVideo / (totalDurationInSeconds * 1000)) * 100), + bitrate: bitrateAsBitsPerSecond, + bandwidth: undefined, + // ...(userDownloadBandwidthInBitsPerSecond && {bandwidth: userDownloadBandwidthInBitsPerSecond}), // add bandwidth if populated }; // post to watchman @@ -202,7 +217,7 @@ async function sendWatchmanData(body) { } const analytics: Analytics = { - // receive buffer events from tracking plugin and jklj + // receive buffer events from tracking plugin and save buffer amounts and times for backend call videoBufferEvent: async (claim, data) => { amountOfBufferEvents = amountOfBufferEvents + 1; amountOfBufferTimeInMS = amountOfBufferTimeInMS + data.bufferDuration; @@ -240,7 +255,7 @@ const analytics: Analytics = { startWatchmanIntervalIfNotRunning(); } }, - videoStartEvent: (claimId, duration, poweredBy, passedUserId, canonicalUrl, passedPlayer) => { + videoStartEvent: (claimId, duration, poweredBy, passedUserId, canonicalUrl, passedPlayer, videoBitrate) => { // populate values for watchman when video starts userId = passedUserId; claimUrl = canonicalUrl; @@ -248,6 +263,7 @@ const analytics: Analytics = { videoType = passedPlayer.currentSource().type; videoPlayer = passedPlayer; + bitrateAsBitsPerSecond = videoBitrate; sendPromMetric('time_to_start', duration); sendMatomoEvent('Media', 'TimeToStart', claimId, duration); diff --git a/ui/component/channelContent/view.jsx b/ui/component/channelContent/view.jsx index 1bbcc8b58..a6981cd15 100644 --- a/ui/component/channelContent/view.jsx +++ b/ui/component/channelContent/view.jsx @@ -56,8 +56,7 @@ function ChannelContent(props: Props) { claimType, empty, } = props; - // const claimsInChannel = (claim && claim.meta.claims_in_channel) || 0; - const claimsInChannel = 9999; + const claimsInChannel = (claim && claim.meta.claims_in_channel) || 0; const [searchQuery, setSearchQuery] = React.useState(''); const [searchResults, setSearchResults] = React.useState(undefined); const { diff --git a/ui/component/channelDiscussion/view.jsx b/ui/component/channelDiscussion/view.jsx index ed1b8d97e..15b215f50 100644 --- a/ui/component/channelDiscussion/view.jsx +++ b/ui/component/channelDiscussion/view.jsx @@ -17,7 +17,7 @@ function ChannelDiscussion(props: Props) { } return (
- +
); } diff --git a/ui/component/claimPreview/index.js b/ui/component/claimPreview/index.js index 2dc96541d..832004474 100644 --- a/ui/component/claimPreview/index.js +++ b/ui/component/claimPreview/index.js @@ -6,6 +6,7 @@ import { makeSelectClaimIsMine, makeSelectClaimIsPending, makeSelectClaimIsNsfw, + doFileGet, makeSelectReflectingClaimForUri, makeSelectClaimWasPurchased, makeSelectStreamingUrlForUri, @@ -24,7 +25,7 @@ import { selectShowMatureContent } from 'redux/selectors/settings'; import { makeSelectHasVisitedUri } from 'redux/selectors/content'; import { makeSelectIsSubscribed } from 'redux/selectors/subscriptions'; import { selectModerationBlockList } from 'redux/selectors/comments'; -import { doFileGet } from 'redux/actions/file'; + import ClaimPreview from './view'; import formatMediaDuration from 'util/formatMediaDuration'; diff --git a/ui/component/claimPreviewTile/index.js b/ui/component/claimPreviewTile/index.js index 59d506718..40b3355c0 100644 --- a/ui/component/claimPreviewTile/index.js +++ b/ui/component/claimPreviewTile/index.js @@ -5,6 +5,7 @@ import { makeSelectIsUriResolving, makeSelectThumbnailForUri, makeSelectTitleForUri, + doFileGet, makeSelectChannelForClaimUri, makeSelectClaimIsNsfw, makeSelectClaimIsStreamPlaceholder, @@ -13,7 +14,6 @@ import { import { selectMutedChannels } from 'redux/selectors/blocked'; import { makeSelectViewCountForUri, selectBlackListedOutpoints, selectFilteredOutpoints } from 'lbryinc'; import { makeSelectIsActiveLivestream } from 'redux/selectors/livestream'; -import { doFileGet } from 'redux/actions/file'; import { selectShowMatureContent } from 'redux/selectors/settings'; import ClaimPreviewTile from './view'; import formatMediaDuration from 'util/formatMediaDuration'; diff --git a/ui/component/collectionContentSidebar/view.jsx b/ui/component/collectionContentSidebar/view.jsx index 786c233c2..0e3573442 100644 --- a/ui/component/collectionContentSidebar/view.jsx +++ b/ui/component/collectionContentSidebar/view.jsx @@ -65,7 +65,7 @@ export default function CollectionContent(props: Props) { titleActions={
{/* TODO: BUTTON TO SAVE COLLECTION - Probably save/copy modal */} -
} body={ diff --git a/ui/component/collectionsListMine/view.jsx b/ui/component/collectionsListMine/view.jsx index 3cbffa9b1..c57273b38 100644 --- a/ui/component/collectionsListMine/view.jsx +++ b/ui/component/collectionsListMine/view.jsx @@ -175,7 +175,7 @@ export default function CollectionsListMine(props: Props) { {filteredCollections && filteredCollections.length > 0 && filteredCollections.map((key) => )} - {!filteredCollections.length &&
{__('No matching playlists')}
} + {!filteredCollections.length &&
{__('No matching collections')}
} )} diff --git a/ui/component/commentMenuList/index.js b/ui/component/commentMenuList/index.js index 44e87e54a..b03992d37 100644 --- a/ui/component/commentMenuList/index.js +++ b/ui/component/commentMenuList/index.js @@ -1,13 +1,13 @@ import { connect } from 'react-redux'; -import { doChannelMute } from 'redux/actions/blocked'; +import { makeSelectChannelPermUrlForClaimUri, makeSelectClaimIsMine, makeSelectClaimForUri } from 'lbry-redux'; import { doCommentPin, doCommentModAddDelegate } from 'redux/actions/comments'; +import { doChannelMute } from 'redux/actions/blocked'; +// import { doSetActiveChannel } from 'redux/actions/app'; import { doOpenModal } from 'redux/actions/app'; import { doSetPlayingUri } from 'redux/actions/content'; -import { doToast } from 'redux/actions/notifications'; -import { makeSelectChannelPermUrlForClaimUri, makeSelectClaimIsMine, makeSelectClaimForUri } from 'lbry-redux'; import { selectActiveChannelClaim } from 'redux/selectors/app'; -import { selectModerationDelegatorsById } from 'redux/selectors/comments'; import { selectPlayingUri } from 'redux/selectors/content'; +import { selectModerationDelegatorsById } from 'redux/selectors/comments'; import CommentMenuList from './view'; const select = (state, props) => ({ @@ -24,9 +24,9 @@ const perform = (dispatch) => ({ clearPlayingUri: () => dispatch(doSetPlayingUri({ uri: null })), muteChannel: (channelUri) => dispatch(doChannelMute(channelUri)), pinComment: (commentId, claimId, remove) => dispatch(doCommentPin(commentId, claimId, remove)), + // setActiveChannel: channelId => dispatch(doSetActiveChannel(channelId)), commentModAddDelegate: (modChanId, modChanName, creatorChannelClaim) => dispatch(doCommentModAddDelegate(modChanId, modChanName, creatorChannelClaim, true)), - doToast: (props) => dispatch(doToast(props)), }); export default connect(select, perform)(CommentMenuList); diff --git a/ui/component/commentMenuList/view.jsx b/ui/component/commentMenuList/view.jsx index b938f1b16..e3f9b2779 100644 --- a/ui/component/commentMenuList/view.jsx +++ b/ui/component/commentMenuList/view.jsx @@ -1,14 +1,12 @@ // @flow -import { getChannelFromClaim } from 'util/claim'; -import { MenuList, MenuItem } from '@reach/menu-button'; -import { parseURI } from 'lbry-redux'; -import { URL } from 'config'; -import { useHistory } from 'react-router'; import * as ICONS from 'constants/icons'; import * as MODALS from 'constants/modal_types'; +import React from 'react'; +import { MenuList, MenuItem } from '@reach/menu-button'; import ChannelThumbnail from 'component/channelThumbnail'; import Icon from 'component/common/icon'; -import React from 'react'; +import { parseURI } from 'lbry-redux'; +import { getChannelFromClaim } from 'util/claim'; type Props = { uri: ?string, @@ -20,6 +18,7 @@ type Props = { disableEdit?: boolean, disableRemove?: boolean, supportAmount?: any, + handleEditComment: () => void, // --- select --- claim: ?Claim, claimIsMine: boolean, @@ -28,8 +27,6 @@ type Props = { playingUri: ?PlayingUri, moderationDelegatorsById: { [string]: { global: boolean, delegators: { name: string, claimId: string } } }, // --- perform --- - doToast: ({ message: string }) => void, - handleEditComment: () => void, openModal: (id: string, {}) => void, clearPlayingUri: () => void, muteChannel: (string) => void, @@ -45,29 +42,24 @@ function CommentMenuList(props: Props) { authorUri, commentIsMine, commentId, + muteChannel, + pinComment, + clearPlayingUri, activeChannelClaim, contentChannelPermanentUrl, isTopLevel, isPinned, + handleEditComment, + commentModAddDelegate, playingUri, moderationDelegatorsById, disableEdit, disableRemove, - supportAmount, - doToast, - handleEditComment, openModal, - clearPlayingUri, - muteChannel, - pinComment, - commentModAddDelegate, + supportAmount, setQuickReply, } = props; - const { - location: { pathname, search }, - } = useHistory(); - const contentChannelClaim = getChannelFromClaim(claim); const activeModeratorInfo = activeChannelClaim && moderationDelegatorsById[activeChannelClaim.claim_id]; const activeChannelIsCreator = activeChannelClaim && activeChannelClaim.permanent_url === contentChannelPermanentUrl; @@ -78,6 +70,10 @@ function CommentMenuList(props: Props) { activeModeratorInfo && Object.values(activeModeratorInfo.delegators).includes(contentChannelClaim.claim_id); + function handlePinComment(commentId, claimId, remove) { + pinComment(commentId, claimId, remove); + } + function handleDeleteComment() { if (playingUri && playingUri.source === 'comment') { clearPlayingUri(); @@ -91,6 +87,14 @@ function CommentMenuList(props: Props) { }); } + function handleCommentBlock() { + openModal(MODALS.BLOCK_CHANNEL, { contentUri: uri, commenterUri: authorUri }); + } + + function handleCommentMute() { + muteChannel(authorUri); + } + function assignAsModerator() { if (activeChannelClaim && authorUri) { const { channelName, channelClaimId } = parseURI(authorUri); @@ -151,15 +155,6 @@ function CommentMenuList(props: Props) { ); } - function handleCopyCommentLink() { - const urlParams = new URLSearchParams(search); - urlParams.delete('lc'); - urlParams.append('lc', commentId); - navigator.clipboard - .writeText(`${URL}${pathname}?${urlParams.toString()}`) - .then(() => doToast({ message: __('Link copied.') })); - } - return ( {activeChannelIsCreator &&
{__('Creator tools')}
} @@ -167,7 +162,7 @@ function CommentMenuList(props: Props) { {activeChannelIsCreator && isTopLevel && ( pinComment(commentId, claim ? claim.claim_id : '', isPinned)} + onSelect={() => handlePinComment(commentId, claim ? claim.claim_id : '', isPinned)} > @@ -210,31 +205,20 @@ function CommentMenuList(props: Props) { )} {!commentIsMine && ( - <> - openModal(MODALS.BLOCK_CHANNEL, { contentUri: uri, commenterUri: authorUri })} - > - {getBlockOptionElem()} - - muteChannel(authorUri)}> -
- - {__('Mute')} -
- {activeChannelIsCreator && ( - {__('Hide this channel for you only.')} - )} -
- + + {getBlockOptionElem()} + )} - {IS_WEB && ( - + {!commentIsMine && ( +
- - {__('Copy Link')} + + {__('Mute')}
+ {activeChannelIsCreator && ( + {__('Hide this channel for you only.')} + )}
)} diff --git a/ui/component/commentsList/view.jsx b/ui/component/commentsList/view.jsx index debff2d0e..35525d19a 100644 --- a/ui/component/commentsList/view.jsx +++ b/ui/component/commentsList/view.jsx @@ -1,22 +1,22 @@ // @flow +import * as REACTION_TYPES from 'constants/reactions'; +import * as ICONS from 'constants/icons'; import { COMMENT_HIGHLIGHTED } from 'constants/classnames'; import { COMMENT_PAGE_SIZE_TOP_LEVEL, SORT_BY } from 'constants/comment'; -import { ENABLE_COMMENT_REACTIONS } from 'config'; -import { getChannelIdFromClaim } from 'util/claim'; -import { useIsMobile, useIsMediumScreen } from 'effects/use-screensize'; -import * as ICONS from 'constants/icons'; -import * as REACTION_TYPES from 'constants/reactions'; +import React, { useEffect } from 'react'; +import classnames from 'classnames'; +import CommentView from 'component/comment'; +import Spinner from 'component/spinner'; import Button from 'component/button'; import Card from 'component/common/card'; -import classnames from 'classnames'; import CommentCreate from 'component/commentCreate'; -import CommentView from 'component/comment'; -import debounce from 'util/debounce'; -import Empty from 'component/common/empty'; -import React, { useEffect } from 'react'; -import Spinner from 'component/spinner'; -import useFetched from 'effects/use-fetched'; import usePersistedState from 'effects/use-persisted-state'; +import { ENABLE_COMMENT_REACTIONS } from 'config'; +import Empty from 'component/common/empty'; +import debounce from 'util/debounce'; +import useFetched from 'effects/use-fetched'; +import { useIsMobile, useIsMediumScreen } from 'effects/use-screensize'; +import { getChannelIdFromClaim } from 'util/claim'; const DEBOUNCE_SCROLL_HANDLER_MS = 200; @@ -33,6 +33,10 @@ type Props = { pinnedComments: Array, topLevelComments: Array, topLevelTotalPages: number, + fetchTopLevelComments: (string, number, number, number) => void, + fetchComment: (string) => void, + fetchReacts: (Array) => Promise, + resetComments: (string) => void, uri: string, claim: ?Claim, claimIsMine: boolean, @@ -47,16 +51,15 @@ type Props = { othersReactsById: ?{ [string]: { [REACTION_TYPES.LIKE | REACTION_TYPES.DISLIKE]: number } }, activeChannelId: ?string, settingsByChannelId: { [channelId: string]: PerChannelSettings }, - fetchReacts: (Array) => Promise, - commentsAreExpanded?: boolean, - fetchTopLevelComments: (string, number, number, number) => void, - fetchComment: (string) => void, - resetComments: (string) => void, }; function CommentList(props: Props) { const { allCommentIds, + fetchTopLevelComments, + fetchComment, + fetchReacts, + resetComments, uri, pinnedComments, topLevelComments, @@ -74,28 +77,21 @@ function CommentList(props: Props) { othersReactsById, activeChannelId, settingsByChannelId, - fetchReacts, - commentsAreExpanded, - fetchTopLevelComments, - fetchComment, - resetComments, } = props; - const isMobile = useIsMobile(); - const isMediumScreen = useIsMediumScreen(); const spinnerRef = React.useRef(); const DEFAULT_SORT = ENABLE_COMMENT_REACTIONS ? SORT_BY.POPULARITY : SORT_BY.NEWEST; const [sort, setSort] = usePersistedState('comment-sort-by', DEFAULT_SORT); const [page, setPage] = React.useState(0); - const fetchedCommentsOnce = useFetched(isFetchingComments); - const fetchedReactsOnce = useFetched(isFetchingReacts); - const fetchedLinkedComment = useFetched(isFetchingCommentsById); - const hasDefaultExpansion = commentsAreExpanded || (!isMobile && !isMediumScreen); - const [expandedComments, setExpandedComments] = React.useState(hasDefaultExpansion); + const isMobile = useIsMobile(); + const isMediumScreen = useIsMediumScreen(); + const [expandedComments, setExpandedComments] = React.useState(!isMobile && !isMediumScreen); const totalFetchedComments = allCommentIds ? allCommentIds.length : 0; const channelId = getChannelIdFromClaim(claim); const channelSettings = channelId ? settingsByChannelId[channelId] : undefined; - const moreBelow = page < topLevelTotalPages; + const fetchedCommentsOnce = useFetched(isFetchingComments); + const fetchedReactsOnce = useFetched(isFetchingReacts); + const fetchedLinkedComment = useFetched(isFetchingCommentsById); // Display comments immediately if not fetching reactions // If not, wait to show comments until reactions are fetched @@ -103,6 +99,20 @@ function CommentList(props: Props) { Boolean(othersReactsById) || !ENABLE_COMMENT_REACTIONS ); + const hasNoComments = !totalComments; + const moreBelow = page < topLevelTotalPages; + + const isMyComment = (channelId: string): boolean => { + if (myChannels != null && channelId != null) { + for (let i = 0; i < myChannels.length; i++) { + if (myChannels[i].claim_id === channelId) { + return true; + } + } + } + return false; + }; + function changeSort(newSort) { if (sort !== newSort) { setSort(newSort); @@ -110,6 +120,34 @@ function CommentList(props: Props) { } } + function getCommentElems(comments) { + return comments.map((comment) => { + return ( + + ); + }); + } + // Reset comments useEffect(() => { if (page === 0) { @@ -130,7 +168,7 @@ function CommentList(props: Props) { fetchTopLevelComments(uri, page, COMMENT_PAGE_SIZE_TOP_LEVEL, sort); } - }, [fetchComment, fetchTopLevelComments, linkedCommentId, page, sort, uri]); + }, [fetchTopLevelComments, uri, page, resetComments, sort, linkedCommentId, fetchComment]); // Fetch reacts useEffect(() => { @@ -155,14 +193,15 @@ function CommentList(props: Props) { } } }, [ - activeChannelId, + totalFetchedComments, allCommentIds, + othersReactsById, + myReactsByCommentId, fetchReacts, + uri, + activeChannelId, fetchingChannels, isFetchingReacts, - myReactsByCommentId, - othersReactsById, - totalFetchedComments, ]); // Scroll to linked-comment @@ -184,7 +223,9 @@ function CommentList(props: Props) { // Infinite scroll useEffect(() => { function shouldFetchNextPage(page, topLevelTotalPages, window, document, yPrefetchPx = 1000) { - if (!spinnerRef || !spinnerRef.current) return false; + if (!spinnerRef || !spinnerRef.current) { + return false; + } const rect = spinnerRef.current.getBoundingClientRect(); // $FlowFixMe const windowH = window.innerHeight || document.documentElement.clientHeight; // $FlowFixMe @@ -206,7 +247,7 @@ function CommentList(props: Props) { } const handleCommentScroll = debounce(() => { - if (hasDefaultExpansion && shouldFetchNextPage(page, topLevelTotalPages, window, document)) { + if (!isMobile && !isMediumScreen && shouldFetchNextPage(page, topLevelTotalPages, window, document)) { setPage(page + 1); } }, DEBOUNCE_SCROLL_HANDLER_MS); @@ -219,76 +260,86 @@ function CommentList(props: Props) { return () => window.removeEventListener('scroll', handleCommentScroll); } } - }, [hasDefaultExpansion, isFetchingComments, moreBelow, page, readyToDisplayComments, topLevelTotalPages]); + }, [ + isMobile, + isMediumScreen, + page, + moreBelow, + spinnerRef, + isFetchingComments, + readyToDisplayComments, + topLevelComments.length, + topLevelTotalPages, + ]); - const getCommentElems = (comments) => { - return comments.map((comment) => ( - claim_id === comment.channel_id) - } - linkedCommentId={linkedCommentId} - isPinned={comment.is_pinned} - supportAmount={comment.support_amount} - numDirectReplies={comment.replies} - isModerator={comment.is_moderator} - isGlobalMod={comment.is_global_mod} - isFiat={comment.is_fiat} - /> - )); - }; - - const sortButton = (label, icon, sortOption) => { - return ( -