embed functionality inside markdown posts

This commit is contained in:
Sean Yesmunt 2020-04-29 16:50:06 -04:00
parent 0738d186a5
commit 1a50e697ce
18 changed files with 343 additions and 186 deletions

View file

@ -1,27 +0,0 @@
// @flow
import React from 'react';
import Button from 'component/button';
import { formatLbryUrlForWeb } from 'util/url';
import { withRouter } from 'react-router';
import { URL } from 'config';
import * as ICONS from 'constants/icons';
type Props = {
uri: string,
title: ?string,
};
function fileViewerEmbeddedTitle(props: Props) {
const { uri, title } = props;
const lbrytvLink = `${URL}${formatLbryUrlForWeb(uri)}?src=embed`;
return (
<div className="file-viewer__embedded-title">
<Button label={title} button="link" href={lbrytvLink} />
<Button className="file-viewer__overlay-logo file-viewer__embedded-title-logo" icon={ICONS.LBRY} href={URL} />
</div>
);
}
export default withRouter(fileViewerEmbeddedTitle);

View file

@ -3,7 +3,7 @@ import type { ElementRef, Node } from 'react';
import React from 'react';
import ReactDOMServer from 'react-dom/server';
import SimpleMDE from 'react-simplemde-editor';
import MarkdownPreview from 'component/common/markdown-preview-internal';
import MarkdownPreview from 'component/common/markdown-preview';
import { openEditorMenu, stopContextMenu } from 'util/context-menu';
import { MAX_CHARACTERS_IN_COMMENT as defaultTextAreaLimit } from 'constants/comments';
import 'easymde/dist/easymde.min.css';

View file

@ -1,121 +0,0 @@
// @flow
import * as React from 'react';
import remark from 'remark';
import remarkAttr from 'remark-attr';
import remarkStrip from 'strip-markdown';
import remarkEmoji from 'remark-emoji';
import reactRenderer from 'remark-react';
import ExternalLink from 'component/externalLink';
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';
type SimpleTextProps = {
children?: React.Node,
};
type SimpleLinkProps = {
href?: string,
title?: string,
children?: React.Node,
};
type MarkdownProps = {
strip?: boolean,
content: ?string,
promptLinks?: boolean,
};
const SimpleText = (props: SimpleTextProps) => {
return <span>{props.children}</span>;
};
const SimpleLink = (props: SimpleLinkProps) => {
const { title, children } = props;
let { href } = props;
if (IS_WEB && href && href.startsWith('lbry://')) {
href = formatLbryUrlForWeb(href);
// using Link after formatLbryUrl to handle "/" vs "#/"
// for web and desktop scenarios respectively
return (
<Link
title={title}
to={href}
onClick={e => {
e.stopPropagation();
}}
>
{children}
</Link>
);
}
return (
<a href={href} title={title}>
{children}
</a>
);
};
// Use github sanitation schema
const schema = { ...defaultSchema };
// Extend sanitation schema to support lbry protocol
schema.protocols.href.push('lbry');
schema.attributes.a.push('embed');
const MarkdownPreview = (props: MarkdownProps) => {
const { content, strip, promptLinks } = props;
const remarkOptions: Object = {
sanitize: schema,
fragment: React.Fragment,
remarkReactComponents: {
a: promptLinks ? ExternalLink : SimpleLink,
// Workaraund of remarkOptions.Fragment
div: React.Fragment,
},
};
const remarkAttrOpts = {
scope: 'extended',
elements: ['link'],
extend: { link: ['embed'] },
defaultValue: true,
};
// Strip all content and just render text
if (strip) {
// Remove new lines and extra space
remarkOptions.remarkReactComponents.p = SimpleText;
return (
<span className="markdown-preview">
{
remark()
.use(remarkStrip)
.use(reactRenderer, remarkOptions)
.processSync(content).contents
}
</span>
);
}
return (
<div className="markdown-preview">
{
remark()
.use(remarkAttr, remarkAttrOpts)
// Remark plugins for lbry urls
// Note: The order is important
.use(formatedLinks)
.use(inlineLinks)
// Emojis
.use(remarkEmoji)
.use(reactRenderer, remarkOptions)
.processSync(content).contents
}
</div>
);
};
export default MarkdownPreview;

View file

@ -1,8 +1,144 @@
import React, { Suspense } from 'react';
import MarkDownPreview from './markdown-preview-internal';
// @flow
import * as React from 'react';
import remark from 'remark';
import remarkAttr from 'remark-attr';
import remarkStrip from 'strip-markdown';
import remarkEmoji from 'remark-emoji';
import reactRenderer from 'remark-react';
import ExternalLink from 'component/externalLink';
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';
const MarkdownPreview = props => {
return <MarkDownPreview {...props} />;
type SimpleTextProps = {
children?: React.Node,
};
type SimpleLinkProps = {
href?: string,
title?: string,
children?: React.Node,
};
type MarkdownProps = {
strip?: boolean,
content: ?string,
promptLinks?: boolean,
};
const SimpleText = (props: SimpleTextProps) => {
return <span>{props.children}</span>;
};
const SimpleLink = (props: SimpleLinkProps) => {
const { title, children } = props;
const { href } = props;
if (!href) {
return children || null;
}
if (!href.startsWith('lbry://')) {
return (
<a href={href} title={title}>
{children}
</a>
);
}
const [uri, search] = href.split('?');
const urlParams = new URLSearchParams(search);
const embed = urlParams.get('embed');
if (embed) {
return <EmbedPlayButton uri={uri} />;
}
const webLink = formatLbryUrlForWeb(uri);
// using Link after formatLbryUrl to handle "/" vs "#/"
// for web and desktop scenarios respectively
return (
<Link
title={title}
to={webLink}
onClick={e => {
e.stopPropagation();
}}
>
{children}
</Link>
);
};
// Use github sanitation schema
const schema = { ...defaultSchema };
// Extend sanitation schema to support lbry protocol
schema.protocols.href.push('lbry');
schema.attributes.a.push('embed');
const REPLACE_REGEX = /(<iframe src=")(.*?(?=))("><\/iframe>)/g;
const MarkdownPreview = (props: MarkdownProps) => {
const { content, strip, promptLinks } = props;
const strippedContent = content
? content.replace(REPLACE_REGEX, (x, y, iframeSrc) => {
return `${iframeSrc}?embed=true`;
})
: '';
const remarkOptions: Object = {
sanitize: schema,
fragment: React.Fragment,
remarkReactComponents: {
a: promptLinks ? ExternalLink : linkProps => <SimpleLink {...linkProps} />,
// Workaraund of remarkOptions.Fragment
div: React.Fragment,
},
};
const remarkAttrOpts = {
scope: 'extended',
elements: ['link'],
extend: { link: ['embed'] },
defaultValue: true,
};
// Strip all content and just render text
if (strip) {
// Remove new lines and extra space
remarkOptions.remarkReactComponents.p = SimpleText;
return (
<span className="markdown-preview">
{
remark()
.use(remarkStrip)
.use(reactRenderer, remarkOptions)
.processSync(content).contents
}
</span>
);
}
return (
<div className="markdown-preview">
{
remark()
.use(remarkAttr, remarkAttrOpts)
// Remark plugins for lbry urls
// Note: The order is important
.use(formatedLinks)
.use(inlineLinks)
// Emojis
.use(remarkEmoji)
.use(reactRenderer, remarkOptions)
.processSync(strippedContent).contents
}
</div>
);
};
export default MarkdownPreview;

View file

@ -0,0 +1,19 @@
import * as SETTINGS from 'constants/settings';
import { connect } from 'react-redux';
import { makeSelectThumbnailForUri, doResolveUri, makeSelectClaimForUri } from 'lbry-redux';
import { doFetchCostInfoForUri } from 'lbryinc';
import { doSetFloatingUri } from 'redux/actions/content';
import { makeSelectClientSetting } from 'redux/selectors/settings';
import ChannelThumbnail from './view';
const select = (state, props) => ({
thumbnail: makeSelectThumbnailForUri(props.uri)(state),
claim: makeSelectClaimForUri(props.uri)(state),
floatingPlayerEnabled: makeSelectClientSetting(SETTINGS.FLOATING_PLAYER)(state),
});
export default connect(select, {
doResolveUri,
doFetchCostInfoForUri,
doSetFloatingUri,
})(ChannelThumbnail);

View file

@ -0,0 +1,60 @@
// @flow
import React, { useEffect } from 'react';
import Button from 'component/button';
import FileViewerEmbeddedTitle from 'component/fileViewerEmbeddedTitle';
import { useHistory } from 'react-router-dom';
import useIsMobile from 'effects/use-is-mobile';
import { formatLbryUrlForWeb } from 'util/url';
type Props = {
uri: string,
thumbnail: string,
claim: ?Claim,
doResolveUri: string => void,
doFetchCostInfoForUri: string => void,
doSetFloatingUri: string => void,
floatingPlayerEnabled: boolean,
};
export default function FileRenderFloating(props: Props) {
const {
uri,
thumbnail = '',
claim,
doResolveUri,
doFetchCostInfoForUri,
doSetFloatingUri,
floatingPlayerEnabled,
} = props;
const { push } = useHistory();
const isMobile = useIsMobile();
const hasResolvedUri = claim !== undefined;
useEffect(() => {
if (!hasResolvedUri) {
doResolveUri(uri);
doFetchCostInfoForUri(uri);
}
}, [uri, hasResolvedUri, doResolveUri, doFetchCostInfoForUri]);
function handleClick() {
if (isMobile || !floatingPlayerEnabled) {
const formattedUrl = formatLbryUrlForWeb(uri);
push(formattedUrl);
} else {
doSetFloatingUri(uri);
}
}
return (
<div
role="button"
className="embed__inline-button"
onClick={handleClick}
style={{ backgroundImage: `url('${thumbnail.replace(/'/g, "\\'")}')` }}
>
<FileViewerEmbeddedTitle uri={uri} isInApp />
<Button onClick={handleClick} iconSize={30} title={__('Play')} className={'button--icon button--play'} />
</div>
);
}

View file

@ -3,17 +3,20 @@ import { connect } from 'react-redux';
import { makeSelectFileInfoForUri, makeSelectTitleForUri } from 'lbry-redux';
import {
makeSelectIsPlayerFloating,
selectFloatingUri,
selectPlayingUri,
makeSelectFileRenderModeForUri,
makeSelectStreamingUrlForUriWebProxy,
} from 'redux/selectors/content';
import { makeSelectClientSetting } from 'redux/selectors/settings';
import { doSetPlayingUri } from 'redux/actions/content';
import { doCloseFloatingPlayer } from 'redux/actions/content';
import { withRouter } from 'react-router';
import FileRenderFloating from './view';
const select = (state, props) => {
const uri = selectPlayingUri(state);
const floatingUri = selectFloatingUri(state);
const playingUri = selectPlayingUri(state);
const uri = floatingUri || playingUri;
return {
uri,
title: makeSelectTitleForUri(uri)(state),
@ -26,7 +29,7 @@ const select = (state, props) => {
};
const perform = dispatch => ({
clearPlayingUri: () => dispatch(doSetPlayingUri(null)),
closeFloatingPlayer: () => dispatch(doCloseFloatingPlayer(null)),
});
export default withRouter(connect(select, perform)(FileRenderFloating));

View file

@ -21,13 +21,21 @@ type Props = {
streamingUrl?: string,
title: ?string,
floatingPlayerEnabled: boolean,
clearPlayingUri: () => void,
closeFloatingPlayer: () => void,
renderMode: string,
};
export default function FileRenderFloating(props: Props) {
const { fileInfo, uri, streamingUrl, title, isFloating, clearPlayingUri, floatingPlayerEnabled, renderMode } = props;
const {
fileInfo,
uri,
streamingUrl,
title,
isFloating,
closeFloatingPlayer,
floatingPlayerEnabled,
renderMode,
} = props;
const isMobile = useIsMobile();
const [fileViewerRect, setFileViewerRect] = usePersistedState('inline-file-viewer:rect');
const [position, setPosition] = usePersistedState('floating-file-viewer:position', {
@ -111,7 +119,7 @@ export default function FileRenderFloating(props: Props) {
<Button navigate={uri} icon={ICONS.VIEW} button="primary" />
</Tooltip>
<Tooltip label={__('Close')}>
<Button onClick={clearPlayingUri} icon={ICONS.REMOVE} button="primary" />
<Button onClick={closeFloatingPlayer} icon={ICONS.REMOVE} button="primary" />
</Tooltip>
</div>
</div>

View file

@ -0,0 +1,38 @@
// @flow
import React from 'react';
import Button from 'component/button';
import { formatLbryUrlForWeb } from 'util/url';
import { withRouter } from 'react-router';
import { URL } from 'config';
import * as ICONS from 'constants/icons';
type Props = {
uri: string,
title: ?string,
isInApp: boolean,
};
function FileViewerEmbeddedTitle(props: Props) {
const { uri, title, isInApp } = props;
let contentLink = `${formatLbryUrlForWeb(uri)}`;
if (!isInApp) {
contentLink = `${contentLink}?src=embed`;
}
const lbryLinkProps = isInApp ? { navigate: '/' } : { href: URL };
return (
<div className="file-viewer__embedded-title">
<Button label={title} button="link" navigate={contentLink} />
<Button
className="file-viewer__overlay-logo file-viewer__embedded-title-logo"
icon={ICONS.LBRY}
{...lbryLinkProps}
/>
</div>
);
}
export default withRouter(FileViewerEmbeddedTitle);

View file

@ -11,7 +11,7 @@ import { FORCE_CONTENT_TYPE_PLAYER } from 'constants/claim';
import AutoplayCountdown from 'component/autoplayCountdown';
import usePrevious from 'effects/use-previous';
import FileViewerEmbeddedEnded from 'lbrytv/component/fileViewerEmbeddedEnded';
import FileViewerEmbeddedTitle from 'lbrytv/component/fileViewerEmbeddedTitle';
import FileViewerEmbeddedTitle from 'component/fileViewerEmbeddedTitle';
import LoadingScreen from 'component/common/loading-screen';
const PLAY_TIMEOUT_ERROR = 'play_timeout_error';

View file

@ -82,6 +82,7 @@ export const PUBLISH_STARTED = 'PUBLISH_STARTED';
export const PUBLISH_COMPLETED = 'PUBLISH_COMPLETED';
export const PUBLISH_FAILED = 'PUBLISH_FAILED';
export const SET_PLAYING_URI = 'SET_PLAYING_URI';
export const SET_FLOATING_URI = 'SET_FLOATING_URI';
export const SET_CONTENT_POSITION = 'SET_CONTENT_POSITION';
export const SET_CONTENT_LAST_VIEWED = 'SET_CONTENT_LAST_VIEWED';
export const CLEAR_CONTENT_HISTORY_URI = 'CLEAR_CONTENT_HISTORY_URI';

View file

@ -1,4 +1,5 @@
// @flow
import * as ACTIONS from 'constants/action_types';
import * as SETTINGS from 'constants/settings';
import * as NOTIFICATION_TYPES from 'constants/subscriptions';
import * as MODALS from 'constants/modal_types';
@ -10,7 +11,6 @@ import { push } from 'connected-react-router';
import { doUpdateUnreadSubscriptions } from 'redux/actions/subscriptions';
import { makeSelectUnreadByChannel } from 'redux/selectors/subscriptions';
import {
ACTIONS,
Lbry,
makeSelectFileInfoForUri,
selectFileInfosByOutpoint,
@ -24,6 +24,7 @@ import {
import { makeSelectCostInfoForUri, Lbryio } from 'lbryinc';
import { makeSelectClientSetting, selectosNotificationsEnabled, selectDaemonSettings } from 'redux/selectors/settings';
import { formatLbryUrlForWeb } from 'util/url';
import { selectFloatingUri } from 'redux/selectors/content';
const DOWNLOAD_POLL_INTERVAL = 250;
@ -134,6 +135,28 @@ export function doSetPlayingUri(uri: ?string) {
};
}
export function doSetFloatingUri(uri: ?string) {
return (dispatch: Dispatch) => {
dispatch({
type: ACTIONS.SET_FLOATING_URI,
data: { uri },
});
};
}
export function doCloseFloatingPlayer() {
return (dispatch: Dispatch, getState: GetState) => {
const state = getState();
const floatingUri = selectFloatingUri(state);
if (floatingUri) {
dispatch(doSetFloatingUri(null));
} else {
dispatch(doSetPlayingUri(null));
}
};
}
export function doPurchaseUriWrapper(uri: string, cost: number, saveFile: boolean, cb: ?() => void) {
return (dispatch: Dispatch, getState: () => any) => {
function onSuccess(fileInfo) {
@ -249,12 +272,3 @@ export function doClearContentHistoryAll() {
dispatch({ type: ACTIONS.CLEAR_CONTENT_HISTORY_ALL });
};
}
export function doSetHistoryPage(page: string) {
return (dispatch: Dispatch) => {
dispatch({
type: ACTIONS.SET_CONTENT_HISTORY_PAGE,
data: { page },
});
};
}

View file

@ -3,6 +3,7 @@ import * as ACTIONS from 'constants/action_types';
const reducers = {};
const defaultState = {
playingUri: null,
floatingUri: null,
channelClaimCounts: {},
positions: {},
history: [],
@ -13,6 +14,11 @@ reducers[ACTIONS.SET_PLAYING_URI] = (state, action) =>
playingUri: action.data.uri,
});
reducers[ACTIONS.SET_FLOATING_URI] = (state, action) =>
Object.assign({}, state, {
floatingUri: action.data.uri,
});
reducers[ACTIONS.SET_CONTENT_POSITION] = (state, action) => {
const { claimId, outpoint, position } = action.data;
return {

View file

@ -12,9 +12,9 @@ import {
selectBalance,
selectBlockedChannels,
parseURI,
buildURI,
makeSelectContentTypeForUri,
makeSelectFileNameForUri,
buildURI,
} from 'lbry-redux';
import { selectAllCostInfoByUri, makeSelectCostInfoForUri } from 'lbryinc';
import { selectShowMatureContent } from 'redux/selectors/settings';
@ -31,17 +31,28 @@ const HISTORY_ITEMS_PER_PAGE = 50;
export const selectState = (state: any) => state.content || {};
export const selectPlayingUri = createSelector(selectState, state => state.playingUri);
export const selectFloatingUri = createSelector(selectState, state => state.floatingUri);
export const makeSelectIsPlaying = (uri: string) => createSelector(selectPlayingUri, playingUri => playingUri === uri);
// below is dumb, some context: https://stackoverflow.com/questions/39622864/access-react-router-state-in-selector
export const makeSelectIsPlayerFloating = location =>
createSelector(selectPlayingUri, playingUri => {
export const makeSelectIsPlayerFloating = (location: UrlLocation) =>
createSelector(selectFloatingUri, selectPlayingUri, (floatingUri, playingUri) => {
if (floatingUri) {
if (!playingUri) {
return true;
} else {
return floatingUri !== playingUri;
}
}
// If there is no floatingPlayer explicitly set, see if the playingUri can float
try {
const { pathname, hash } = location;
const newpath = buildURI(parseURI(pathname.slice(1).replace(/:/g, '#')));
return playingUri && playingUri !== newpath + hash;
} catch (e) {}
return !!playingUri;
});

View file

@ -8,3 +8,20 @@
align-items: center;
background-color: var(--color-black);
}
.embed__inline-button {
@include thumbnail;
position: relative;
background-position: 50% 50%;
background-repeat: no-repeat;
background-size: 100%;
height: 300px;
width: 100%;
display: flex;
justify-content: center;
align-items: center;
@media (max-width: $breakpoint-small) {
height: 200px;
}
}

View file

@ -229,6 +229,10 @@
.button {
color: var(--color-white);
&:hover {
color: var(--color-white);
}
&:first-of-type {
max-width: 90%;
}

View file

@ -46,18 +46,6 @@
font-size: 1em;
}
// Paragraphs
p {
svg {
width: 1rem;
height: 1rem;
margin-left: 0.2rem;
position: relative;
top: 1px;
}
}
p,
blockquote,
dl,