strings: supports username mentions and slate mentions and links in text

This commit is contained in:
@wwwjim 2020-08-26 23:03:30 -07:00
parent 448bc104aa
commit 44d5458d3f
6 changed files with 180 additions and 25 deletions

View File

@ -5,6 +5,7 @@ import * as Strings from "~/common/strings";
import { css } from "@emotion/react";
import { TooltipAnchor } from "~/components/system/components/fragments/TooltipAnchor";
import { ProcessedText } from "~/components/system/components/Typography";
const STYLES_ROOT = css`
display: flex;
@ -53,7 +54,9 @@ export default (props) => {
<header css={STYLES_ROOT} style={props.style}>
<div css={STYLES_LEFT}>
<div css={STYLES_HEADER}>{props.title}</div>
<div css={STYLES_DESCRIPTION}>{props.children}</div>
<div css={STYLES_DESCRIPTION}>
<ProcessedText text={props.children} />
</div>
</div>
{props.actions ? <div css={STYLES_RIGHT}>{props.actions}</div> : null}
</header>

View File

@ -1,9 +1,11 @@
import * as React from "react";
import * as Constants from "~/common/constants";
import * as Strings from "~/common/strings";
import * as Actions from "~/common/actions";
import { css } from "@emotion/react";
import { LoaderSpinner } from "~/components/system/components/Loaders";
import { ProcessedText } from "~/components/system/components/Typography";
import TextareaAutoSize from "~/vendor/react-textarea-autosize";
@ -236,7 +238,9 @@ export default class SlateMediaObjectSidebar extends React.Component {
if (hasTitle) {
elements.push(
<div key="sidebar-media-info-title" css={STYLES_SIDEBAR_SECTION}>
<h1 css={STYLES_HEADING}>{this.props.data.title}</h1>
<h1 css={STYLES_HEADING}>
<ProcessedText text={this.props.data.title} />
</h1>
</div>
);
}
@ -244,7 +248,9 @@ export default class SlateMediaObjectSidebar extends React.Component {
if (hasBody || hasTitle || hasSource || hasAuthor) {
elements.push(
<div key="sidebar-media-info-body" css={STYLES_SIDEBAR_CONTENT}>
<p css={STYLES_BODY}>{this.props.data.body}</p>
<p css={STYLES_BODY}>
<ProcessedText text={this.props.data.body} />
</p>
</div>
);
}
@ -258,7 +264,9 @@ export default class SlateMediaObjectSidebar extends React.Component {
>
Source
</div>
<p css={STYLES_BODY}>{this.props.data.source}</p>
<p css={STYLES_BODY}>
<ProcessedText text={this.props.data.source} />
</p>
</div>
);
}
@ -272,7 +280,9 @@ export default class SlateMediaObjectSidebar extends React.Component {
>
Author
</div>
<p css={STYLES_BODY}>{this.props.data.source}</p>
<p css={STYLES_BODY}>
<ProcessedText text={this.props.data.source} />
</p>
</div>
);
}

View File

@ -53,9 +53,17 @@ export default (props) => {
return (
<div css={STYLES_CONTAINER} style={props.style}>
<div css={STYLES_LEFT}>
<a css={STYLES_LINK} href={props.href} style={{ fontFamily: Constants.font.codeBold }}>
{props.children}
</a>
{props.href ? (
<a
css={STYLES_LINK}
href={props.href}
style={{ fontFamily: Constants.font.codeBold }}
>
{props.children}
</a>
) : (
props.children
)}
</div>
<div css={STYLES_RIGHT}>
<a css={STYLES_LINK} href="https://github.com/filecoin-project/slate">

View File

@ -1,27 +1,84 @@
import * as React from "react";
import * as Constants from "~/common/constants";
import * as Actions from "~/common/actions";
import * as StringReplace from "~/vendor/react-string-replace";
import { css } from "@emotion/react";
const ANCHOR = `
a {
font-family: ${Constants.font.text};
font-weight: 400;
text-decoration: none;
color: #6a737d;
transition: 200ms ease color;
const LINK_STYLES = `
font-family: ${Constants.font.text};
font-weight: 400;
text-decoration: none;
color: #6a737d;
cursor: pointer;
transition: 200ms ease color;
:visited {
color: #959da5;
}
:hover {
color: #959da5;
}
:visited {
color: #959da5;
}
:hover {
color: #959da5;
}
`;
const STYLES_LINK = css`
${LINK_STYLES}
`;
const ANCHOR = `
a {
${LINK_STYLES}
}
`;
const onDeepLink = async (object) => {
const response = await Actions.getSlateBySlatename({
query: object.deeplink,
deeplink: true,
});
if (!response.data) {
alert("TODO: Can not find deeplink");
}
if (!response.data.slate) {
alert("TODO: Can not find deeplink");
}
return window.open(
`/${response.data.slate.user.username}/${response.data.slate.slatename}`
);
};
export const ProcessedText = ({ text }) => {
let replacedText;
replacedText = StringReplace(text, /(https?:\/\/\S+)/g, (match, i) => (
<a css={STYLES_LINK} key={match + i} href={match} target="_blank">
{match}
</a>
));
replacedText = StringReplace(replacedText, /@(\w+)/g, (match, i) => (
<a css={STYLES_LINK} key={match + i} target="_blank" href={`/${match}`}>
@{match}
</a>
));
replacedText = StringReplace(replacedText, /#(\w+)/g, (match, i) => (
<span
css={STYLES_LINK}
key={match + i}
onClick={() => onDeepLink({ deeplink: match })}
>
#{match}
</span>
));
return <React.Fragment>{replacedText}</React.Fragment>;
};
const STYLES_H1 = css`
box-sizing: border-box;
font-size: ${Constants.typescale.lvl4};

View File

@ -3,6 +3,7 @@ import * as Constants from "~/common/constants";
import * as System from "~/components/system";
import { css } from "@emotion/react";
import { ProcessedText } from "~/components/system/components/Typography";
import WebsitePrototypeWrapper from "~/components/core/WebsitePrototypeWrapper";
import WebsitePrototypeHeaderGeneric from "~/components/core/WebsitePrototypeHeaderGeneric";
@ -51,6 +52,17 @@ const STYLES_HEADER_LEFT = css`
font-family: ${Constants.font.semiBold};
text-transform: none;
flex-shrink: 0;
color: ${Constants.system.pitchBlack};
text-decoration: none;
transition: 200ms ease color;
:visited {
color: ${Constants.system.black};
}
:hover {
color: ${Constants.system.brand};
}
@media (max-width: ${Constants.sizes.mobile}px) {
text-align: center;
@ -135,12 +147,14 @@ export default class SlatePage extends React.Component {
image={image}
>
<div css={STYLES_ROOT}>
<WebsitePrototypeHeaderGeneric href={url}>
<WebsitePrototypeHeaderGeneric>
<div css={STYLES_HEADER}>
<div css={STYLES_HEADER_LEFT}>
<a css={STYLES_HEADER_LEFT} href={url}>
{this.props.creator.username} / {this.props.slate.slatename}
</a>
<div css={STYLES_HEADER_RIGHT}>
<ProcessedText text={this.props.slate.data.body} />
</div>
<div css={STYLES_HEADER_RIGHT}>{this.props.slate.data.body}</div>
</div>
</WebsitePrototypeHeaderGeneric>
<div css={STYLES_SLATE}>

63
vendor/react-string-replace.js vendored Normal file
View File

@ -0,0 +1,63 @@
const getType = (value) =>
Object.prototype.toString.call(value).match(/^\[object\s(.*)\]$/)[1];
const isRegExp = (value) => getType(value) === "RegExp";
const isString = (value) => getType(value) === "String";
const escapeRegExp = (regex) => {
return regex.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&");
};
const flatten = (source) => {
const length = source.length;
let i = 0;
let flattened = [];
for (; i < length; i++) {
flattened = flattened.concat(
!Array.isArray(source[i]) ? source[i] : flatten(source[i])
);
}
return flattened;
};
function replaceString(str, match, fn) {
var curCharStart = 0;
var curCharLen = 0;
if (str === "") {
return str;
} else if (!str || !isString(str)) {
throw new TypeError(
"First argument to react-string-replace#replaceString must be a string"
);
}
var re = match;
if (!isRegExp(re)) {
re = new RegExp("(" + escapeRegExp(re) + ")", "gi");
}
var result = str.split(re);
for (var i = 1, length = result.length; i < length; i += 2) {
curCharLen = result[i].length;
curCharStart += result[i - 1].length;
result[i] = fn(result[i], i, curCharStart);
curCharStart += curCharLen;
}
return result;
}
module.exports = function reactStringReplace(source, match, fn) {
if (!Array.isArray(source)) source = [source];
return flatten(
source.map(function(x) {
return isString(x) ? replaceString(x, match, fn) : x;
})
);
};