forked from berserker/microblog
Merge upstream!! #64 <3 <3
commit
79d898ae0a
340 changed files with 4979 additions and 2320 deletions
Binary file not shown.
After Width: | Height: | Size: 4.9 KiB |
After Width: | Height: | Size: 5.7 KiB |
After Width: | Height: | Size: 5.1 KiB |
After Width: | Height: | Size: 40 KiB |
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
@ -0,0 +1,25 @@ |
||||
export const BUNDLE_FETCH_REQUEST = 'BUNDLE_FETCH_REQUEST'; |
||||
export const BUNDLE_FETCH_SUCCESS = 'BUNDLE_FETCH_SUCCESS'; |
||||
export const BUNDLE_FETCH_FAIL = 'BUNDLE_FETCH_FAIL'; |
||||
|
||||
export function fetchBundleRequest(skipLoading) { |
||||
return { |
||||
type: BUNDLE_FETCH_REQUEST, |
||||
skipLoading, |
||||
}; |
||||
} |
||||
|
||||
export function fetchBundleSuccess(skipLoading) { |
||||
return { |
||||
type: BUNDLE_FETCH_SUCCESS, |
||||
skipLoading, |
||||
}; |
||||
} |
||||
|
||||
export function fetchBundleFail(error, skipLoading) { |
||||
return { |
||||
type: BUNDLE_FETCH_FAIL, |
||||
error, |
||||
skipLoading, |
||||
}; |
||||
} |
@ -0,0 +1,39 @@ |
||||
import React from 'react'; |
||||
import { Provider } from 'react-redux'; |
||||
import PropTypes from 'prop-types'; |
||||
import configureStore from '../store/configureStore'; |
||||
import { hydrateStore } from '../actions/store'; |
||||
import { IntlProvider, addLocaleData } from 'react-intl'; |
||||
import { getLocale } from '../locales'; |
||||
import PublicTimeline from '../features/standalone/public_timeline'; |
||||
|
||||
const { localeData, messages } = getLocale(); |
||||
addLocaleData(localeData); |
||||
|
||||
const store = configureStore(); |
||||
const initialStateContainer = document.getElementById('initial-state'); |
||||
|
||||
if (initialStateContainer !== null) { |
||||
const initialState = JSON.parse(initialStateContainer.textContent); |
||||
store.dispatch(hydrateStore(initialState)); |
||||
} |
||||
|
||||
export default class TimelineContainer extends React.PureComponent { |
||||
|
||||
static propTypes = { |
||||
locale: PropTypes.string.isRequired, |
||||
}; |
||||
|
||||
render () { |
||||
const { locale } = this.props; |
||||
|
||||
return ( |
||||
<IntlProvider locale={locale} messages={messages}> |
||||
<Provider store={store}> |
||||
<PublicTimeline /> |
||||
</Provider> |
||||
</IntlProvider> |
||||
); |
||||
} |
||||
|
||||
} |
@ -1,35 +1,55 @@ |
||||
import emojione from 'emojione'; |
||||
|
||||
const toImage = str => shortnameToImage(unicodeToImage(str)); |
||||
|
||||
const unicodeToImage = str => { |
||||
const mappedUnicode = emojione.mapUnicodeToShort(); |
||||
|
||||
return str.replace(emojione.regUnicode, unicodeChar => { |
||||
if (typeof unicodeChar === 'undefined' || unicodeChar === '' || !(unicodeChar in emojione.jsEscapeMap)) { |
||||
return unicodeChar; |
||||
import Trie from 'substring-trie'; |
||||
|
||||
const mappedUnicode = emojione.mapUnicodeToShort(); |
||||
const trie = new Trie(Object.keys(emojione.jsEscapeMap)); |
||||
|
||||
function emojify(str) { |
||||
// This walks through the string from start to end, ignoring any tags (<p>, <br>, etc.)
|
||||
// and replacing valid shortnames like :smile: and :wink: as well as unicode strings
|
||||
// that _aren't_ within tags with an <img> version.
|
||||
// The goal is to be the same as an emojione.regShortNames/regUnicode replacement, but faster.
|
||||
let i = -1; |
||||
let insideTag = false; |
||||
let insideShortname = false; |
||||
let shortnameStartIndex = -1; |
||||
let match; |
||||
while (++i < str.length) { |
||||
const char = str.charAt(i); |
||||
if (insideShortname && char === ':') { |
||||
const shortname = str.substring(shortnameStartIndex, i + 1); |
||||
if (shortname in emojione.emojioneList) { |
||||
const unicode = emojione.emojioneList[shortname].unicode[emojione.emojioneList[shortname].unicode.length - 1]; |
||||
const alt = emojione.convert(unicode.toUpperCase()); |
||||
const replacement = `<img draggable="false" class="emojione" alt="${alt}" title="${shortname}" src="/emoji/${unicode}.svg" />`; |
||||
str = str.substring(0, shortnameStartIndex) + replacement + str.substring(i + 1); |
||||
i += (replacement.length - shortname.length - 1); // jump ahead the length we've added to the string
|
||||
} else { |
||||
i--; // stray colon, try again
|
||||
} |
||||
insideShortname = false; |
||||
} else if (insideTag && char === '>') { |
||||
insideTag = false; |
||||
} else if (char === '<') { |
||||
insideTag = true; |
||||
insideShortname = false; |
||||
} else if (!insideTag && char === ':') { |
||||
insideShortname = true; |
||||
shortnameStartIndex = i; |
||||
} else if (!insideTag && (match = trie.search(str.substring(i)))) { |
||||
const unicodeStr = match; |
||||
if (unicodeStr in emojione.jsEscapeMap) { |
||||
const unicode = emojione.jsEscapeMap[unicodeStr]; |
||||
const short = mappedUnicode[unicode]; |
||||
const filename = emojione.emojioneList[short].fname; |
||||
const alt = emojione.convert(unicode.toUpperCase()); |
||||
const replacement = `<img draggable="false" class="emojione" alt="${alt}" title="${short}" src="/emoji/${filename}.svg" />`; |
||||
str = str.substring(0, i) + replacement + str.substring(i + unicodeStr.length); |
||||
i += (replacement.length - unicodeStr.length); // jump ahead the length we've added to the string
|
||||
} |
||||
} |
||||
|
||||
const unicode = emojione.jsEscapeMap[unicodeChar]; |
||||
const short = mappedUnicode[unicode]; |
||||
const filename = emojione.emojioneList[short].fname; |
||||
const alt = emojione.convert(unicode.toUpperCase()); |
||||
|
||||
return `<img draggable="false" class="emojione" alt="${alt}" title="${short}" src="/emoji/${filename}.svg" />`; |
||||
}); |
||||
}; |
||||
|
||||
const shortnameToImage = str => str.replace(emojione.regShortNames, shortname => { |
||||
if (typeof shortname === 'undefined' || shortname === '' || !(shortname in emojione.emojioneList)) { |
||||
return shortname; |
||||
} |
||||
return str; |
||||
} |
||||
|
||||
const unicode = emojione.emojioneList[shortname].unicode[emojione.emojioneList[shortname].unicode.length - 1]; |
||||
const alt = emojione.convert(unicode.toUpperCase()); |
||||
|
||||
return `<img draggable="false" class="emojione" alt="${alt}" title="${shortname}" src="/emoji/${unicode}.svg" />`; |
||||
}); |
||||
|
||||
export default function emojify(text) { |
||||
return toImage(text); |
||||
}; |
||||
export default emojify; |
||||
|
@ -0,0 +1,76 @@ |
||||
import React from 'react'; |
||||
import { connect } from 'react-redux'; |
||||
import PropTypes from 'prop-types'; |
||||
import StatusListContainer from '../../ui/containers/status_list_container'; |
||||
import { |
||||
refreshPublicTimeline, |
||||
expandPublicTimeline, |
||||
} from '../../../actions/timelines'; |
||||
import Column from '../../../components/column'; |
||||
import ColumnHeader from '../../../components/column_header'; |
||||
import { defineMessages, injectIntl } from 'react-intl'; |
||||
|
||||
const messages = defineMessages({ |
||||
title: { id: 'standalone.public_title', defaultMessage: 'A look inside...' }, |
||||
}); |
||||
|
||||
@connect() |
||||
@injectIntl |
||||
export default class PublicTimeline extends React.PureComponent { |
||||
|
||||
static propTypes = { |
||||
dispatch: PropTypes.func.isRequired, |
||||
intl: PropTypes.object.isRequired, |
||||
}; |
||||
|
||||
handleHeaderClick = () => { |
||||
this.column.scrollTop(); |
||||
} |
||||
|
||||
setRef = c => { |
||||
this.column = c; |
||||
} |
||||
|
||||
componentDidMount () { |
||||
const { dispatch } = this.props; |
||||
|
||||
dispatch(refreshPublicTimeline()); |
||||
|
||||
this.polling = setInterval(() => { |
||||
dispatch(refreshPublicTimeline()); |
||||
}, 3000); |
||||
} |
||||
|
||||
componentWillUnmount () { |
||||
if (typeof this.polling !== 'undefined') { |
||||
clearInterval(this.polling); |
||||
this.polling = null; |
||||
} |
||||
} |
||||
|
||||
handleLoadMore = () => { |
||||
this.props.dispatch(expandPublicTimeline()); |
||||
} |
||||
|
||||
render () { |
||||
const { intl } = this.props; |
||||
|
||||
return ( |
||||
<Column ref={this.setRef}> |
||||
<ColumnHeader |
||||
icon='globe' |
||||
title={intl.formatMessage(messages.title)} |
||||
onClick={this.handleHeaderClick} |
||||
/> |
||||
|
||||
<StatusListContainer |
||||
timelineId='public' |
||||
loadMore={this.handleLoadMore} |
||||
scrollKey='standalone_public_timeline' |
||||
trackScroll={false} |
||||
/> |
||||
</Column> |
||||
); |
||||
} |
||||
|
||||
} |
@ -0,0 +1,101 @@ |
||||
import React from 'react'; |
||||
import PropTypes from 'prop-types'; |
||||
|
||||
const emptyComponent = () => null; |
||||
const noop = () => { }; |
||||
|
||||
class Bundle extends React.Component { |
||||
|
||||
static propTypes = { |
||||
fetchComponent: PropTypes.func.isRequired, |
||||
loading: PropTypes.func, |
||||
error: PropTypes.func, |
||||
children: PropTypes.func.isRequired, |
||||
renderDelay: PropTypes.number, |
||||
onFetch: PropTypes.func, |
||||
onFetchSuccess: PropTypes.func, |
||||
onFetchFail: PropTypes.func, |
||||
} |
||||
|
||||
static defaultProps = { |
||||
loading: emptyComponent, |
||||
error: emptyComponent, |
||||
renderDelay: 0, |
||||
onFetch: noop, |
||||
onFetchSuccess: noop, |
||||
onFetchFail: noop, |
||||
} |
||||
|
||||
static cache = {} |
||||
|
||||
state = { |
||||
mod: undefined, |
||||
forceRender: false, |
||||
} |
||||
|
||||
componentWillMount() { |
||||
this.load(this.props); |
||||
} |
||||
|
||||
componentWillReceiveProps(nextProps) { |
||||
if (nextProps.fetchComponent !== this.props.fetchComponent) { |
||||
this.load(nextProps); |
||||
} |
||||
} |
||||
|
||||
componentWillUnmount () { |
||||
if (this.timeout) { |
||||
clearTimeout(this.timeout); |
||||
} |
||||
} |
||||
|
||||
load = (props) => { |
||||
const { fetchComponent, onFetch, onFetchSuccess, onFetchFail, renderDelay } = props || this.props; |
||||
|
||||
this.setState({ mod: undefined }); |
||||
onFetch(); |
||||
|
||||
if (renderDelay !== 0) { |
||||
this.timestamp = new Date(); |
||||
this.timeout = setTimeout(() => this.setState({ forceRender: true }), renderDelay); |
||||
} |
||||
|
||||
if (Bundle.cache[fetchComponent.name]) { |
||||
const mod = Bundle.cache[fetchComponent.name]; |
||||
|
||||
this.setState({ mod: mod.default }); |
||||
onFetchSuccess(); |
||||
return Promise.resolve(); |
||||
} |
||||
|
||||
return fetchComponent() |
||||
.then((mod) => { |
||||
Bundle.cache[fetchComponent.name] = mod; |
||||
this.setState({ mod: mod.default }); |
||||
onFetchSuccess(); |
||||
}) |
||||
.catch((error) => { |
||||
this.setState({ mod: null }); |
||||
onFetchFail(error); |
||||
}); |
||||
} |
||||
|
||||
render() { |
||||
const { loading: Loading, error: Error, children, renderDelay } = this.props; |
||||
const { mod, forceRender } = this.state; |
||||
const elapsed = this.timestamp ? (new Date() - this.timestamp) : renderDelay; |
||||
|
||||
if (mod === undefined) { |
||||
return (elapsed >= renderDelay || forceRender) ? <Loading /> : null; |
||||
} |
||||
|
||||
if (mod === null) { |
||||
return <Error onRetry={this.load} />; |
||||
} |
||||
|
||||
return children(mod); |
||||
} |
||||
|
||||
} |
||||
|
||||
export default Bundle; |
@ -0,0 +1,44 @@ |
||||
import React from 'react'; |
||||
import PropTypes from 'prop-types'; |
||||
import { defineMessages, injectIntl } from 'react-intl'; |
||||
|
||||
import Column from './column'; |
||||
import ColumnHeader from './column_header'; |
||||
import ColumnBackButtonSlim from '../../../components/column_back_button_slim'; |
||||
import IconButton from '../../../components/icon_button'; |
||||
|
||||
const messages = defineMessages({ |
||||
title: { id: 'bundle_column_error.title', defaultMessage: 'Network error' }, |
||||
body: { id: 'bundle_column_error.body', defaultMessage: 'Something went wrong while loading this component.' }, |
||||
retry: { id: 'bundle_column_error.retry', defaultMessage: 'Try again' }, |
||||
}); |
||||
|
||||
class BundleColumnError extends React.Component { |
||||
|
||||
static propTypes = { |
||||
onRetry: PropTypes.func.isRequired, |
||||
intl: PropTypes.object.isRequired, |
||||
} |
||||
|
||||
handleRetry = () => { |
||||
this.props.onRetry(); |
||||
} |
||||
|
||||
render () { |
||||
const { intl: { formatMessage } } = this.props; |
||||
|
||||
return ( |
||||
<Column> |
||||
<ColumnHeader icon='exclamation-circle' type={formatMessage(messages.title)} /> |
||||
<ColumnBackButtonSlim /> |
||||
<div className='error-column'> |
||||
<IconButton title={formatMessage(messages.retry)} icon='refresh' onClick={this.handleRetry} size={64} /> |
||||
{formatMessage(messages.body)} |
||||
</div> |
||||
</Column> |
||||
); |
||||
} |
||||
|
||||
} |
||||
|
||||
export default injectIntl(BundleColumnError); |
@ -0,0 +1,53 @@ |
||||
import React from 'react'; |
||||
import PropTypes from 'prop-types'; |
||||
import { defineMessages, injectIntl } from 'react-intl'; |
||||
|
||||
import IconButton from '../../../components/icon_button'; |
||||
|
||||
const messages = defineMessages({ |
||||
error: { id: 'bundle_modal_error.message', defaultMessage: 'Something went wrong while loading this component.' }, |
||||
retry: { id: 'bundle_modal_error.retry', defaultMessage: 'Try again' }, |
||||
close: { id: 'bundle_modal_error.close', defaultMessage: 'Close' }, |
||||
}); |
||||
|
||||
class BundleModalError extends React.Component { |
||||
|
||||
static propTypes = { |
||||
onRetry: PropTypes.func.isRequired, |
||||
onClose: PropTypes.func.isRequired, |
||||
intl: PropTypes.object.isRequired, |
||||
} |
||||
|
||||
handleRetry = () => { |
||||
this.props.onRetry(); |
||||
} |
||||
|
||||
render () { |
||||
const { onClose, intl: { formatMessage } } = this.props; |
||||
|
||||
// Keep the markup in sync with <ModalLoading />
|
||||
// (make sure they have the same dimensions)
|
||||
return ( |
||||
<div className='modal-root__modal error-modal'> |
||||
<div className='error-modal__body'> |
||||
<IconButton title={formatMessage(messages.retry)} icon='refresh' onClick={this.handleRetry} size={64} /> |
||||
{formatMessage(messages.error)} |
||||
</div> |
||||
|
||||
<div className='error-modal__footer'> |
||||
<div> |
||||
<button |
||||
onClick={onClose} |
||||
className='error-modal__nav onboarding-modal__skip' |
||||
> |
||||
{formatMessage(messages.close)} |
||||
</button> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
); |
||||
} |
||||
|
||||
} |
||||
|
||||
export default injectIntl(BundleModalError); |
@ -0,0 +1,19 @@ |
||||
import React from 'react'; |
||||
import PropTypes from 'prop-types'; |
||||
|
||||
import Column from '../../../components/column'; |
||||
import ColumnHeader from '../../../components/column_header'; |
||||
|
||||
const ColumnLoading = ({ title = '', icon = ' ' }) => ( |
||||
<Column> |
||||
<ColumnHeader icon={icon} title={title} multiColumn={false} /> |
||||
<div className='scrollable' /> |
||||
</Column> |
||||
); |
||||
|
||||
ColumnLoading.propTypes = { |
||||
title: PropTypes.node, |
||||
icon: PropTypes.string, |
||||
}; |
||||
|
||||
export default ColumnLoading; |
@ -0,0 +1,20 @@ |
||||
import React from 'react'; |
||||
|
||||
import LoadingIndicator from '../../../components/loading_indicator'; |
||||
|
||||
// Keep the markup in sync with <BundleModalError />
|
||||
// (make sure they have the same dimensions)
|
||||
const ModalLoading = () => ( |
||||
<div className='modal-root__modal error-modal'> |
||||
<div className='error-modal__body'> |
||||
<LoadingIndicator /> |
||||
</div> |
||||
<div className='error-modal__footer'> |
||||
<div> |
||||
<button className='error-modal__nav onboarding-modal__skip' /> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
); |
||||
|
||||
export default ModalLoading; |
@ -0,0 +1,19 @@ |
||||
import { connect } from 'react-redux'; |
||||
|
||||
import Bundle from '../components/bundle'; |
||||
|
||||
import { fetchBundleRequest, fetchBundleSuccess, fetchBundleFail } from '../../../actions/bundles'; |
||||
|
||||
const mapDispatchToProps = dispatch => ({ |
||||
onFetch () { |
||||
dispatch(fetchBundleRequest()); |
||||
}, |
||||
onFetchSuccess () { |
||||
dispatch(fetchBundleSuccess()); |
||||
}, |
||||
onFetchFail (error) { |
||||
dispatch(fetchBundleFail(error)); |
||||
}, |
||||
}); |
||||
|
||||
export default connect(null, mapDispatchToProps)(Bundle); |
@ -0,0 +1,21 @@ |
||||
|
||||
// Get the bounding client rect from an IntersectionObserver entry.
|
||||
// This is to work around a bug in Chrome: https://crbug.com/737228
|
||||
|
||||
let hasBoundingRectBug; |
||||
|
||||
function getRectFromEntry(entry) { |
||||
if (typeof hasBoundingRectBug !== 'boolean') { |
||||
const boundingRect = entry.target.getBoundingClientRect(); |
||||
const observerRect = entry.boundingClientRect; |
||||
hasBoundingRectBug = boundingRect.height !== observerRect.height || |
||||
boundingRect.top !== observerRect.top || |
||||
boundingRect.width !== observerRect.width || |
||||
boundingRect.bottom !== observerRect.bottom || |
||||
boundingRect.left !== observerRect.left || |
||||
boundingRect.right !== observerRect.right; |
||||
} |
||||
return hasBoundingRectBug ? entry.target.getBoundingClientRect() : entry.boundingClientRect; |
||||
} |
||||
|
||||
export default getRectFromEntry; |
@ -0,0 +1,57 @@ |
||||
import React from 'react'; |
||||
import PropTypes from 'prop-types'; |
||||
import Switch from 'react-router-dom/Switch'; |
||||
import Route from 'react-router-dom/Route'; |
||||
|
||||
import ColumnLoading from '../components/column_loading'; |
||||
import BundleColumnError from '../components/bundle_column_error'; |
||||
import BundleContainer from '../containers/bundle_container'; |
||||
|
||||
// Small wrapper to pass multiColumn to the route components
|
||||
export const WrappedSwitch = ({ multiColumn, children }) => ( |
||||
<Switch> |
||||
{React.Children.map(children, child => React.cloneElement(child, { multiColumn }))} |
||||
</Switch> |
||||
); |
||||
|
||||
WrappedSwitch.propTypes = { |
||||
multiColumn: PropTypes.bool, |
||||
children: PropTypes.node, |
||||
}; |
||||
|
||||
// Small Wraper to extract the params from the route and pass
|
||||
// them to the rendered component, together with the content to
|
||||
// be rendered inside (the children)
|
||||
export class WrappedRoute extends React.Component { |
||||
|
||||
static propTypes = { |
||||
component: PropTypes.func.isRequired, |
||||
content: PropTypes.node, |
||||
multiColumn: PropTypes.bool, |
||||
} |
||||
|
||||
renderComponent = ({ match }) => { |
||||
const { component, content, multiColumn } = this.props; |
||||
|
||||
return ( |
||||
<BundleContainer fetchComponent={component} loading={this.renderLoading} error={this.renderError}> |
||||
{Component => <Component params={match.params} multiColumn={multiColumn}>{content}</Component>} |
||||
</BundleContainer> |
||||
); |
||||
} |
||||
|
||||
renderLoading = () => { |
||||
return <ColumnLoading />; |
||||
} |
||||
|
||||
renderError = (props) => { |
||||
return <BundleColumnError {...props} />; |
||||
} |
||||
|
||||
render () { |
||||
const { component: Component, content, ...rest } = this.props; |
||||
|
||||
return <Route {...rest} render={this.renderComponent} />; |
||||
} |
||||
|
||||
} |
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue