mirror of
https://github.com/filecoin-project/slate.git
synced 2024-09-11 22:07:08 +03:00
strings: supports username mentions and slate mentions and links in text
This commit is contained in:
parent
448bc104aa
commit
44d5458d3f
@ -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>
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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">
|
||||
|
@ -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};
|
||||
|
@ -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
63
vendor/react-string-replace.js
vendored
Normal 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;
|
||||
})
|
||||
);
|
||||
};
|
Loading…
Reference in New Issue
Block a user