diff --git a/.env.defaults b/.env.defaults index 839b54b4b..1a81c5b68 100644 --- a/.env.defaults +++ b/.env.defaults @@ -40,6 +40,7 @@ DEFAULT_LANGUAGE=en # Additional settings for below are found in ui/constants/settings and are for # preventing user settings from applying to custom sites without overwriting them. # UNSYNCED_SETTINGS='theme dark_mode_times automatic_dark_mode_enabled' +KNOWN_APP_DOMAINS=lbry.tv,lbry.lat,odysee.com # Custom Content # If the following is true, copy custom/homepage.example.js to custom/homepage.js and modify diff --git a/config.js b/config.js index e1c0cbcad..1b0d7b399 100644 --- a/config.js +++ b/config.js @@ -37,6 +37,7 @@ const config = { PINNED_LABEL_1: process.env.PINNED_LABEL_1, PINNED_URI_2: process.env.PINNED_URI_2, PINNED_LABEL_2: process.env.PINNED_LABEL_2, + KNOWN_APP_DOMAINS: process.env.KNOWN_APP_DOMAINS }; config.URL_LOCAL = `http://localhost:${config.WEB_SERVER_PORT}`; diff --git a/flow-typed/content.js b/flow-typed/content.js new file mode 100644 index 000000000..6246a7c95 --- /dev/null +++ b/flow-typed/content.js @@ -0,0 +1,8 @@ +// @flow + +declare type PlayingUri = { + uri: string, + pathname: string, + commentId?: string, + source?: string, +}; diff --git a/package.json b/package.json index de516a9c5..824774ee8 100644 --- a/package.json +++ b/package.json @@ -136,7 +136,7 @@ "imagesloaded": "^4.1.4", "json-loader": "^0.5.4", "lbry-format": "https://github.com/lbryio/lbry-format.git", - "lbry-redux": "lbryio/lbry-redux#15737f9b098f3654122a75f93d63584e7ffa17a1", + "lbry-redux": "lbryio/lbry-redux#823197af37da745fd79632b9e82deddf9d7fe033", "lbryinc": "lbryio/lbryinc#db0663fcc4a64cb082b6edc5798fafa67eb4300f", "lint-staged": "^7.0.2", "localforage": "^1.7.1", diff --git a/static/app-strings.json b/static/app-strings.json index 450a80104..70ad22ed7 100644 --- a/static/app-strings.json +++ b/static/app-strings.json @@ -1329,7 +1329,7 @@ "Replying as": "Replying as", "No uploads": "No uploads", "uploads": "uploads", - "Discussion": "Discussion", + "Community": "Community", "Staked LBRY Credits": "Staked LBRY Credits", "1 comment": "1 comment", "%total_comments% comments": "%total_comments% comments", diff --git a/ui/component/claimLink/index.js b/ui/component/claimLink/index.js index 9bc006330..1da8ec075 100644 --- a/ui/component/claimLink/index.js +++ b/ui/component/claimLink/index.js @@ -1,6 +1,8 @@ import { connect } from 'react-redux'; import { doResolveUri, makeSelectTitleForUri, makeSelectClaimForUri, makeSelectIsUriResolving } from 'lbry-redux'; import { selectBlackListedOutpoints } from 'lbryinc'; +import { selectPlayingUri } from 'redux/selectors/content'; +import { doSetPlayingUri } from 'redux/actions/content'; import ClaimLink from './view'; const select = (state, props) => { @@ -10,11 +12,11 @@ const select = (state, props) => { title: makeSelectTitleForUri(props.uri)(state), isResolvingUri: makeSelectIsUriResolving(props.uri)(state), blackListedOutpoints: selectBlackListedOutpoints(state), + playingUri: selectPlayingUri(state), }; }; -const perform = dispatch => ({ - resolveUri: uri => dispatch(doResolveUri(uri)), -}); - -export default connect(select, perform)(ClaimLink); +export default connect(select, { + doResolveUri, + doSetPlayingUri, +})(ClaimLink); diff --git a/ui/component/claimLink/view.jsx b/ui/component/claimLink/view.jsx index 067e4dc29..ca51a1f1a 100644 --- a/ui/component/claimLink/view.jsx +++ b/ui/component/claimLink/view.jsx @@ -1,20 +1,25 @@ // @flow import * as React from 'react'; -import PreviewLink from 'component/previewLink'; -import UriIndicator from 'component/uriIndicator'; +import classnames from 'classnames'; +import ClaimPreview from 'component/claimPreview'; +import EmbedPlayButton from 'component/embedPlayButton'; +import Button from 'component/button'; +import { INLINE_PLAYER_WRAPPER_CLASS } from 'component/fileRenderFloating/view'; type Props = { uri: string, claim: StreamClaim, children: React.Node, - autoEmbed: ?boolean, description: ?string, isResolvingUri: boolean, - resolveUri: string => void, + doResolveUri: string => void, blackListedOutpoints: Array<{ txid: string, nout: number, }>, + playingUri: ?PlayingUri, + parentCommentId?: string, + isMarkdownPost?: boolean, }; class ClaimLink extends React.Component { @@ -22,7 +27,6 @@ class ClaimLink extends React.Component { href: null, link: false, thumbnail: null, - autoEmbed: false, description: null, isResolvingUri: false, }; @@ -51,17 +55,22 @@ class ClaimLink extends React.Component { } resolve = (props: Props) => { - const { isResolvingUri, resolveUri, claim, uri } = props; + const { isResolvingUri, doResolveUri, claim, uri } = props; if (!isResolvingUri && claim === undefined && uri) { - resolveUri(uri); + doResolveUri(uri); } }; render() { - const { uri, claim, autoEmbed, children, isResolvingUri } = this.props; + const { uri, claim, children, isResolvingUri, playingUri, parentCommentId, isMarkdownPost } = this.props; const isUnresolved = (!isResolvingUri && !claim) || !claim; const isBlacklisted = this.isClaimBlackListed(); + const isPlayingInline = + playingUri && + playingUri.uri === uri && + ((playingUri.source === 'comment' && parentCommentId === playingUri.commentId) || + playingUri.source === 'markdown'); if (isBlacklisted || isUnresolved) { return {children}; @@ -69,13 +78,23 @@ class ClaimLink extends React.Component { const { value_type: valueType } = claim; const isChannel = valueType === 'channel'; - const showPreview = autoEmbed === true && !isUnresolved; - if (isChannel) { - return ; - } - - return {showPreview && }; + return isChannel ? ( +
+ +
+ ) : ( +
+
+ +
+
+ ); } } diff --git a/ui/component/claimPreview/view.jsx b/ui/component/claimPreview/view.jsx index 28b91f1d6..83d2c5e24 100644 --- a/ui/component/claimPreview/view.jsx +++ b/ui/component/claimPreview/view.jsx @@ -61,6 +61,7 @@ type Props = { includeSupportAction?: boolean, hideActions?: boolean, renderActions?: Claim => ?Node, + wrapperElement?: string, }; const ClaimPreview = forwardRef((props: Props, ref: any) => { @@ -95,7 +96,9 @@ const ClaimPreview = forwardRef((props: Props, ref: any) => { includeSupportAction, hideActions = false, renderActions, + wrapperElement, } = props; + const WrapperElement = wrapperElement || 'li'; const shouldFetch = claim === undefined || (claim !== null && claim.value_type === 'channel' && isEmpty(claim.meta) && !pending); const abandoned = !isResolvingUri && !claim; @@ -221,7 +224,7 @@ const ClaimPreview = forwardRef((props: Props, ref: any) => { } return ( -
  • ((props: Props, ref: any) => { )} -
  • + ); }); diff --git a/ui/component/comment/view.jsx b/ui/component/comment/view.jsx index 5f4474294..b7f9f4770 100644 --- a/ui/component/comment/view.jsx +++ b/ui/component/comment/view.jsx @@ -188,11 +188,11 @@ function Comment(props: Props) { 'comment--slimed': slimedToDeath && !displayDeadComment, })} > -
    +
    {authorUri ? ( - + ) : ( - + )}
    @@ -298,10 +298,10 @@ function Comment(props: Props) {
    ) : editedMessage.length >= LENGTH_TO_COLLAPSE ? ( - + ) : ( - + )} diff --git a/ui/component/commentsList/index.js b/ui/component/commentsList/index.js index 0b2199e3c..98c2009cc 100644 --- a/ui/component/commentsList/index.js +++ b/ui/component/commentsList/index.js @@ -8,8 +8,8 @@ import { selectCommentChannel, } from 'redux/selectors/comments'; import { doCommentList, doCommentReactList } from 'redux/actions/comments'; -import CommentsList from './view'; import { selectUserVerifiedEmail } from 'redux/selectors/user'; +import CommentsList from './view'; const select = (state, props) => ({ myChannels: selectMyChannelClaims(state), diff --git a/ui/component/commentsList/view.jsx b/ui/component/commentsList/view.jsx index 7b3e31fbb..7217d1054 100644 --- a/ui/component/commentsList/view.jsx +++ b/ui/component/commentsList/view.jsx @@ -53,7 +53,9 @@ function CommentList(props: Props) { const [end, setEnd] = React.useState(9); // Display comments immediately if not fetching reactions // If not, wait to show comments until reactions are fetched - const [readyToDisplayComments, setReadyToDisplayComments] = React.useState(!ENABLE_COMMENT_REACTIONS); + const [readyToDisplayComments, setReadyToDisplayComments] = React.useState( + reactionsById || !ENABLE_COMMENT_REACTIONS + ); const linkedCommentId = linkedComment && linkedComment.comment_id; const hasNoComments = totalComments === 0; const moreBelow = totalComments - end > 0; @@ -203,8 +205,7 @@ function CommentList(props: Props) { {!isFetchingComments && hasNoComments &&
    {__('Be the first to comment!')}
    }
      - {!isFetchingComments && - comments && + {comments && displayedComments && displayedComments.map(comment => { return ( diff --git a/ui/component/common/loading-screen.jsx b/ui/component/common/loading-screen.jsx index 661d311cd..0476b1586 100644 --- a/ui/component/common/loading-screen.jsx +++ b/ui/component/common/loading-screen.jsx @@ -19,8 +19,13 @@ class LoadingScreen extends React.PureComponent { const { status, spinner, isDocument } = this.props; return (
      - {spinner && } - {status && {status}} + {spinner && ( + {status}} + /> + )}
      ); } diff --git a/ui/component/common/markdown-preview.jsx b/ui/component/common/markdown-preview.jsx index 86de3a60e..5582e79bd 100644 --- a/ui/component/common/markdown-preview.jsx +++ b/ui/component/common/markdown-preview.jsx @@ -7,12 +7,9 @@ import remarkStrip from 'strip-markdown'; import remarkEmoji from 'remark-emoji'; import remarkBreaks from 'remark-breaks'; import reactRenderer from 'remark-react'; -import ExternalLink from 'component/externalLink'; +import MarkdownLink from 'component/markdownLink'; import defaultSchema from 'hast-util-sanitize/lib/github.json'; import { formatedLinks, inlineLinks } from 'util/remark-lbry'; -import { Link } from 'react-router-dom'; -import { formatLbryUrlForWeb } from 'util/url'; -import EmbedPlayButton from 'component/embedPlayButton'; type SimpleTextProps = { children?: React.Node, @@ -22,7 +19,6 @@ type SimpleLinkProps = { href?: string, title?: string, children?: React.Node, - noDataStore?: boolean, }; type MarkdownProps = { @@ -31,6 +27,8 @@ type MarkdownProps = { promptLinks?: boolean, noDataStore?: boolean, className?: string, + parentCommentId?: string, + isMarkdownPost?: boolean, }; const SimpleText = (props: SimpleTextProps) => { @@ -38,14 +36,13 @@ const SimpleText = (props: SimpleTextProps) => { }; const SimpleLink = (props: SimpleLinkProps) => { - const { title, children } = props; - const { href, noDataStore } = props; + const { title, children, href } = props; if (!href) { return children || null; } - if (!href.startsWith('lbry://')) { + if (!href.startsWith('lbry:/')) { return ( {children} @@ -60,33 +57,15 @@ const SimpleLink = (props: SimpleLinkProps) => { if (embed) { // Decode this since users might just copy it from the url bar const decodedUri = decodeURI(uri); - return noDataStore ? ( + return (
      {decodedUri}
      - ) : ( - ); } - const webLink = formatLbryUrlForWeb(uri); - // using Link after formatLbryUrl to handle "/" vs "#/" - // for web and desktop scenarios respectively - - return noDataStore ? ( - // Dummy link (no 'href') -
      {children} - ) : ( - { - e.stopPropagation(); - }} - > - {children} - - ); + // Dummy link (no 'href') + return {children}; }; // Use github sanitation schema @@ -99,7 +78,7 @@ schema.attributes.a.push('embed'); const REPLACE_REGEX = /(<\/iframe>)/g; const MarkdownPreview = (props: MarkdownProps) => { - const { content, strip, promptLinks, noDataStore, className } = props; + const { content, strip, promptLinks, noDataStore, className, parentCommentId, isMarkdownPost } = props; const strippedContent = content ? content.replace(REPLACE_REGEX, (iframeHtml, y, iframeSrc) => { // Let the browser try to create an iframe to see if the markup is valid @@ -111,7 +90,7 @@ const MarkdownPreview = (props: MarkdownProps) => { const src = iframe.src; if (src && src.startsWith('lbry://')) { - return `${src}?embed=true`; + return src; } } @@ -123,7 +102,16 @@ const MarkdownPreview = (props: MarkdownProps) => { sanitize: schema, fragment: React.Fragment, remarkReactComponents: { - a: promptLinks ? ExternalLink : linkProps => , + a: noDataStore + ? SimpleLink + : linkProps => ( + + ), // Workaraund of remarkOptions.Fragment div: React.Fragment, }, diff --git a/ui/component/embedPlayButton/index.js b/ui/component/embedPlayButton/index.js index 9fd9331ac..f85f42388 100644 --- a/ui/component/embedPlayButton/index.js +++ b/ui/component/embedPlayButton/index.js @@ -1,9 +1,11 @@ import { connect } from 'react-redux'; import { makeSelectThumbnailForUri, doResolveUri, makeSelectClaimForUri, SETTINGS } from 'lbry-redux'; import { doFetchCostInfoForUri, makeSelectCostInfoForUri } from 'lbryinc'; -import { doSetFloatingUri, doPlayUri } from 'redux/actions/content'; +import { doPlayUri, doSetPlayingUri } from 'redux/actions/content'; import { doAnaltyicsPurchaseEvent } from 'redux/actions/app'; import { makeSelectClientSetting } from 'redux/selectors/settings'; +import { makeSelectFileRenderModeForUri } from 'redux/selectors/content'; + import ChannelThumbnail from './view'; const select = (state, props) => ({ @@ -11,12 +13,13 @@ const select = (state, props) => ({ claim: makeSelectClaimForUri(props.uri)(state), floatingPlayerEnabled: makeSelectClientSetting(SETTINGS.FLOATING_PLAYER)(state), costInfo: makeSelectCostInfoForUri(props.uri)(state), + renderMode: makeSelectFileRenderModeForUri(props.uri)(state), }); export default connect(select, { doResolveUri, doFetchCostInfoForUri, - doSetFloatingUri, doPlayUri, + doSetPlayingUri, doAnaltyicsPurchaseEvent, })(ChannelThumbnail); diff --git a/ui/component/embedPlayButton/view.jsx b/ui/component/embedPlayButton/view.jsx index 4c09d77ef..9a796478d 100644 --- a/ui/component/embedPlayButton/view.jsx +++ b/ui/component/embedPlayButton/view.jsx @@ -1,5 +1,7 @@ // @flow +import * as RENDER_MODES from 'constants/file_render_modes'; import React, { useEffect } from 'react'; +import classnames from 'classnames'; import Button from 'component/button'; import FileViewerEmbeddedTitle from 'component/fileViewerEmbeddedTitle'; import { useHistory } from 'react-router-dom'; @@ -12,11 +14,14 @@ type Props = { claim: ?Claim, doResolveUri: string => void, doFetchCostInfoForUri: string => void, - doSetFloatingUri: string => void, costInfo: ?{ cost: number }, floatingPlayerEnabled: boolean, doPlayUri: (string, ?boolean, ?boolean, (GetResponse) => void) => void, doAnaltyicsPurchaseEvent: GetResponse => void, + parentCommentId?: string, + isMarkdownPost: boolean, + doSetPlayingUri: ({}) => void, + renderMode: string, }; export default function EmbedPlayButton(props: Props) { @@ -26,33 +31,54 @@ export default function EmbedPlayButton(props: Props) { claim, doResolveUri, doFetchCostInfoForUri, - doSetFloatingUri, floatingPlayerEnabled, doPlayUri, + doSetPlayingUri, doAnaltyicsPurchaseEvent, costInfo, + parentCommentId, + isMarkdownPost, + renderMode, } = props; - const { push } = useHistory(); + const { + push, + location: { pathname }, + } = useHistory(); const isMobile = useIsMobile(); const hasResolvedUri = claim !== undefined; + const hasCostInfo = costInfo !== undefined; const disabled = !hasResolvedUri || !costInfo; + const canPlayInline = [RENDER_MODES.AUDIO, RENDER_MODES.VIDEO].includes(renderMode); useEffect(() => { - doResolveUri(uri); - doFetchCostInfoForUri(uri); - }, [uri, doResolveUri, doFetchCostInfoForUri]); + if (!hasResolvedUri) { + doResolveUri(uri); + } + + if (!hasCostInfo) { + doFetchCostInfoForUri(uri); + } + }, [uri, doResolveUri, doFetchCostInfoForUri, hasCostInfo, hasResolvedUri]); function handleClick() { if (disabled) { return; } - if (isMobile || !floatingPlayerEnabled) { + if (isMobile || !floatingPlayerEnabled || !canPlayInline) { const formattedUrl = formatLbryUrlForWeb(uri); push(formattedUrl); } else { doPlayUri(uri, undefined, undefined, fileInfo => { - doSetFloatingUri(uri); + let playingOptions: PlayingUri = { uri, pathname }; + if (parentCommentId) { + playingOptions.source = 'comment'; + playingOptions.commentId = parentCommentId; + } else if (isMarkdownPost) { + playingOptions.source = 'markdown'; + } + + doSetPlayingUri(playingOptions); doAnaltyicsPurchaseEvent(fileInfo); }); } @@ -67,7 +93,16 @@ export default function EmbedPlayButton(props: Props) { style={{ backgroundImage: `url('${thumbnail.replace(/'/g, "\\'")}')` }} > -