Compare commits

..

2 Commits

Author SHA1 Message Date
beatrix-bitrot
1ba51a4c35 add test 2017-10-19 22:00:03 +00:00
beatrix-bitrot
b10608299e hide mentions of muted accounts (in home col)
also cleans up some old crap
2017-10-19 21:38:41 +00:00
8 changed files with 305 additions and 296 deletions

View File

@@ -47,9 +47,11 @@ import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import { injectIntl, defineMessages } from 'react-intl'; import { injectIntl, defineMessages } from 'react-intl';
// Mastodon imports //
import IconButton from '../../../../mastodon/components/icon_button';
// Our imports // // Our imports //
import ComposeAdvancedOptionsToggle from './toggle'; import ComposeAdvancedOptionsToggle from './toggle';
import ComposeDropdown from '../dropdown/index';
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
@@ -75,6 +77,11 @@ const messages = defineMessages({
{ id: 'advanced_options.icon_title', defaultMessage: 'Advanced options' }, { id: 'advanced_options.icon_title', defaultMessage: 'Advanced options' },
}); });
const iconStyle = {
height : null,
lineHeight : '27px',
};
/* /*
Implementation: Implementation:
@@ -93,6 +100,67 @@ export default class ComposeAdvancedOptions extends React.PureComponent {
intl : PropTypes.object.isRequired, intl : PropTypes.object.isRequired,
}; };
state = {
open: false,
};
/*
### `onToggleDropdown()`
This function toggles the opening and closing of the advanced options
dropdown.
*/
onToggleDropdown = () => {
this.setState({ open: !this.state.open });
};
/*
### `onGlobalClick(e)`
This function closes the advanced options dropdown if you click
anywhere else on the screen.
*/
onGlobalClick = (e) => {
if (e.target !== this.node && !this.node.contains(e.target) && this.state.open) {
this.setState({ open: false });
}
}
/*
### `componentDidMount()`, `componentWillUnmount()`
This function closes the advanced options dropdown if you click
anywhere else on the screen.
*/
componentDidMount () {
window.addEventListener('click', this.onGlobalClick);
window.addEventListener('touchstart', this.onGlobalClick);
}
componentWillUnmount () {
window.removeEventListener('click', this.onGlobalClick);
window.removeEventListener('touchstart', this.onGlobalClick);
}
/*
### `setRef(c)`
`setRef()` stores a reference to the dropdown's `<div> in `this.node`.
*/
setRef = (c) => {
this.node = c;
}
/* /*
@@ -103,6 +171,7 @@ export default class ComposeAdvancedOptions extends React.PureComponent {
*/ */
render () { render () {
const { open } = this.state;
const { intl, values } = this.props; const { intl, values } = this.props;
/* /*
@@ -149,14 +218,23 @@ toggle as its `key` so that React can keep track of it.
Finally, we can render our component. Finally, we can render our component.
*/ */
return ( return (
<ComposeDropdown <div ref={this.setRef} className={`advanced-options-dropdown ${open ? 'open' : ''} ${anyEnabled ? 'active' : ''} `}>
title={intl.formatMessage(messages.advanced_options_icon_title)} <div className='advanced-options-dropdown__value'>
icon='home' <IconButton
highlight={anyEnabled} className='advanced-options-dropdown__value'
> title={intl.formatMessage(messages.advanced_options_icon_title)}
{optionElems} icon='ellipsis-h' active={open || anyEnabled}
</ComposeDropdown> size={18}
style={iconStyle}
onClick={this.onToggleDropdown}
/>
</div>
<div className='advanced-options-dropdown__dropdown'>
{optionElems}
</div>
</div>
); );
} }

View File

@@ -1,133 +0,0 @@
// Package imports //
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { injectIntl, defineMessages } from 'react-intl';
// Our imports //
import ComposeDropdown from '../dropdown/index';
import { uploadCompose } from '../../../../mastodon/actions/compose';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { openModal } from '../../../../mastodon/actions/modal';
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
const messages = defineMessages({
upload :
{ id: 'compose.attach.upload', defaultMessage: 'Upload a file' },
doodle :
{ id: 'compose.attach.doodle', defaultMessage: 'Draw something' },
attach :
{ id: 'compose.attach', defaultMessage: 'Attach...' },
});
const mapStateToProps = state => ({
// This horrible expression is copied from vanilla upload_button_container
disabled: state.getIn(['compose', 'is_uploading']) || (state.getIn(['compose', 'media_attachments']).size > 3 || state.getIn(['compose', 'media_attachments']).some(m => m.get('type') === 'video')),
resetFileKey: state.getIn(['compose', 'resetFileKey']),
acceptContentTypes: state.getIn(['media_attachments', 'accept_content_types']),
});
const mapDispatchToProps = dispatch => ({
onSelectFile (files) {
dispatch(uploadCompose(files));
},
onOpenDoodle () {
dispatch(openModal('DOODLE', { noEsc: true }));
},
});
@injectIntl
@connect(mapStateToProps, mapDispatchToProps)
export default class ComposeAttachOptions extends ImmutablePureComponent {
static propTypes = {
intl : PropTypes.object.isRequired,
resetFileKey: PropTypes.number,
acceptContentTypes: ImmutablePropTypes.listOf(PropTypes.string).isRequired,
disabled: PropTypes.bool,
onSelectFile: PropTypes.func.isRequired,
onOpenDoodle: PropTypes.func.isRequired,
};
handleItemClick = bt => {
if (bt === 'upload') {
this.fileElement.click();
}
if (bt === 'doodle') {
this.props.onOpenDoodle();
}
this.dropdown.setState({ open: false });
};
handleFileChange = (e) => {
if (e.target.files.length > 0) {
this.props.onSelectFile(e.target.files);
}
}
setFileRef = (c) => {
this.fileElement = c;
}
setDropdownRef = (c) => {
this.dropdown = c;
}
render () {
const { intl, resetFileKey, disabled, acceptContentTypes } = this.props;
const options = [
{ icon: 'cloud-upload', text: messages.upload, name: 'upload' },
{ icon: 'paint-brush', text: messages.doodle, name: 'doodle' },
];
const optionElems = options.map((item) => {
const hdl = () => this.handleItemClick(item.name);
return (
<div
role='button'
tabIndex='0'
key={item.name}
onClick={hdl}
className='privacy-dropdown__option'
>
<div className='privacy-dropdown__option__icon'>
<i className={`fa fa-fw fa-${item.icon}`} />
</div>
<div className='privacy-dropdown__option__content'>
<strong>{intl.formatMessage(item.text)}</strong>
</div>
</div>
);
});
return (
<div>
<ComposeDropdown
title={intl.formatMessage(messages.attach)}
icon='paperclip'
disabled={disabled}
ref={this.setDropdownRef}
>
{optionElems}
</ComposeDropdown>
<input
key={resetFileKey}
ref={this.setFileRef}
type='file'
multiple={false}
accept={acceptContentTypes.toArray().join(',')}
onChange={this.handleFileChange}
disabled={disabled}
style={{ display: 'none' }}
/>
</div>
);
}
}

View File

@@ -1,77 +0,0 @@
// Package imports //
import React from 'react';
import PropTypes from 'prop-types';
// Mastodon imports //
import IconButton from '../../../../mastodon/components/icon_button';
const iconStyle = {
height : null,
lineHeight : '27px',
};
export default class ComposeDropdown extends React.PureComponent {
static propTypes = {
title: PropTypes.string.isRequired,
icon: PropTypes.string,
highlight: PropTypes.bool,
disabled: PropTypes.bool,
children: PropTypes.arrayOf(PropTypes.node).isRequired,
};
state = {
open: false,
};
onGlobalClick = (e) => {
if (e.target !== this.node && !this.node.contains(e.target) && this.state.open) {
this.setState({ open: false });
}
};
componentDidMount () {
window.addEventListener('click', this.onGlobalClick);
window.addEventListener('touchstart', this.onGlobalClick);
}
componentWillUnmount () {
window.removeEventListener('click', this.onGlobalClick);
window.removeEventListener('touchstart', this.onGlobalClick);
}
onToggleDropdown = () => {
if (this.props.disabled) return;
this.setState({ open: !this.state.open });
};
setRef = (c) => {
this.node = c;
};
render () {
const { open } = this.state;
let { highlight, title, icon, disabled } = this.props;
if (!icon) icon = 'ellipsis-h';
return (
<div ref={this.setRef} className={`advanced-options-dropdown ${open ? 'open' : ''} ${highlight ? 'active' : ''} `}>
<div className='advanced-options-dropdown__value'>
<IconButton
className={'inverted'}
title={title}
icon={icon} active={open || highlight}
size={18}
style={iconStyle}
disabled={disabled}
onClick={this.onToggleDropdown}
/>
</div>
<div className='advanced-options-dropdown__dropdown'>
{this.props.children}
</div>
</div>
);
}
}

View File

@@ -69,10 +69,6 @@ functions are:
easier to read and to maintain. I leave it to the future readers of easier to read and to maintain. I leave it to the future readers of
this code to determine the extent of my successes in this endeavor. this code to determine the extent of my successes in this endeavor.
UPDATE 19 Oct 2017: We no longer allow character escapes inside our
double-quoted strings for ease of processing. We now internally use
the name "ƔAML" in our code to clarify that this is Not Quite YAML.
Sending love + warmth eternal, Sending love + warmth eternal,
- kibigo [@kibi@glitch.social] - kibigo [@kibi@glitch.social]
@@ -100,7 +96,10 @@ const ALLOWED_CHAR = unirex( // `c-printable` in the YAML 1.2 spec.
compat_mode ? '[\t\n\r\x20-\x7e\x85\xa0-\ufffd]' : '[\t\n\r\x20-\x7e\x85\xa0-\ud7ff\ue000-\ufffd\u{10000}-\u{10FFFF}]' compat_mode ? '[\t\n\r\x20-\x7e\x85\xa0-\ufffd]' : '[\t\n\r\x20-\x7e\x85\xa0-\ud7ff\ue000-\ufffd\u{10000}-\u{10FFFF}]'
); );
const WHITE_SPACE = /[ \t]/; const WHITE_SPACE = /[ \t]/;
const INDENTATION = / */; // Indentation must be only spaces.
const LINE_BREAK = /\r?\n|\r|<br\s*\/?>/; const LINE_BREAK = /\r?\n|\r|<br\s*\/?>/;
const ESCAPE_CHAR = /[0abt\tnvfre "\/\\N_LP]/;
const HEXADECIMAL_CHARS = /[0-9a-fA-F]/;
const INDICATOR = /[-?:,[\]{}&#*!|>'"%@`]/; const INDICATOR = /[-?:,[\]{}&#*!|>'"%@`]/;
const FLOW_CHAR = /[,[\]{}]/; const FLOW_CHAR = /[,[\]{}]/;
@@ -122,7 +121,7 @@ const NEW_LINE = unirex(
rexstr(ANY_WHITE_SPACE) + rexstr(LINE_BREAK) rexstr(ANY_WHITE_SPACE) + rexstr(LINE_BREAK)
); );
const SOME_NEW_LINES = unirex( const SOME_NEW_LINES = unirex(
'(?:' + rexstr(NEW_LINE) + ')+' '(?:' + rexstr(ANY_WHITE_SPACE) + rexstr(LINE_BREAK) + ')+'
); );
const POSSIBLE_STARTS = unirex( const POSSIBLE_STARTS = unirex(
rexstr(DOCUMENT_START) + rexstr(/<p[^<>]*>/) + '?' rexstr(DOCUMENT_START) + rexstr(/<p[^<>]*>/) + '?'
@@ -132,13 +131,22 @@ const POSSIBLE_ENDS = unirex(
rexstr(DOCUMENT_END) + '|' + rexstr(DOCUMENT_END) + '|' +
rexstr(/<\/p>/) rexstr(/<\/p>/)
); );
const QUOTE_CHAR = unirex( const CHARACTER_ESCAPE = unirex(
'(?=' + rexstr(NOT_LINE_BREAK) + ')[^"]' rexstr(/\\/) +
'(?:' +
rexstr(ESCAPE_CHAR) + '|' +
rexstr(/x/) + rexstr(HEXADECIMAL_CHARS) + '{2}' + '|' +
rexstr(/u/) + rexstr(HEXADECIMAL_CHARS) + '{4}' + '|' +
rexstr(/U/) + rexstr(HEXADECIMAL_CHARS) + '{8}' +
')'
); );
const ANY_QUOTE_CHAR = unirex( const ESCAPED_CHAR = unirex(
rexstr(QUOTE_CHAR) + '*' rexstr(/(?!["\\])/) + rexstr(NOT_LINE_BREAK) + '|' +
rexstr(CHARACTER_ESCAPE)
);
const ANY_ESCAPED_CHARS = unirex(
rexstr(ESCAPED_CHAR) + '*'
); );
const ESCAPED_APOS = unirex( const ESCAPED_APOS = unirex(
'(?=' + rexstr(NOT_LINE_BREAK) + ')' + rexstr(/[^']|''/) '(?=' + rexstr(NOT_LINE_BREAK) + ')' + rexstr(/[^']|''/)
); );
@@ -182,76 +190,120 @@ const LATER_VALUE_CHAR = unirex(
/* YAML CONSTRUCTS */ /* YAML CONSTRUCTS */
const ƔAML_START = unirex( const YAML_START = unirex(
rexstr(ANY_WHITE_SPACE) + '---' rexstr(ANY_WHITE_SPACE) + rexstr(/---/)
); );
const ƔAML_END = unirex( const YAML_END = unirex(
rexstr(ANY_WHITE_SPACE) + '(?:---|\.\.\.)' rexstr(ANY_WHITE_SPACE) + rexstr(/(?:---|\.\.\.)/)
); );
const ƔAML_LOOKAHEAD = unirex( const YAML_LOOKAHEAD = unirex(
'(?=' + '(?=' +
rexstr(ƔAML_START) + rexstr(YAML_START) +
rexstr(ANY_ALLOWED_CHARS) + rexstr(NEW_LINE) + rexstr(ANY_ALLOWED_CHARS) + rexstr(NEW_LINE) +
rexstr(ƔAML_END) + rexstr(POSSIBLE_ENDS) + rexstr(YAML_END) + rexstr(POSSIBLE_ENDS) +
')' ')'
); );
const ƔAML_DOUBLE_QUOTE = unirex( const YAML_DOUBLE_QUOTE = unirex(
'"' + rexstr(ANY_QUOTE_CHAR) + '"' rexstr(/"/) + rexstr(ANY_ESCAPED_CHARS) + rexstr(/"/)
); );
const ƔAML_SINGLE_QUOTE = unirex( const YAML_SINGLE_QUOTE = unirex(
'\'' + rexstr(ANY_ESCAPED_APOS) + '\'' rexstr(/'/) + rexstr(ANY_ESCAPED_APOS) + rexstr(/'/)
); );
const ƔAML_SIMPLE_KEY = unirex( const YAML_SIMPLE_KEY = unirex(
rexstr(FIRST_KEY_CHAR) + rexstr(LATER_KEY_CHAR) + '*' rexstr(FIRST_KEY_CHAR) + rexstr(LATER_KEY_CHAR) + '*'
); );
const ƔAML_SIMPLE_VALUE = unirex( const YAML_SIMPLE_VALUE = unirex(
rexstr(FIRST_VALUE_CHAR) + rexstr(LATER_VALUE_CHAR) + '*' rexstr(FIRST_VALUE_CHAR) + rexstr(LATER_VALUE_CHAR) + '*'
); );
const ƔAML_KEY = unirex( const YAML_KEY = unirex(
rexstr(ƔAML_DOUBLE_QUOTE) + '|' + rexstr(YAML_DOUBLE_QUOTE) + '|' +
rexstr(ƔAML_SINGLE_QUOTE) + '|' + rexstr(YAML_SINGLE_QUOTE) + '|' +
rexstr(ƔAML_SIMPLE_KEY) rexstr(YAML_SIMPLE_KEY)
); );
const ƔAML_VALUE = unirex( const YAML_VALUE = unirex(
rexstr(ƔAML_DOUBLE_QUOTE) + '|' + rexstr(YAML_DOUBLE_QUOTE) + '|' +
rexstr(ƔAML_SINGLE_QUOTE) + '|' + rexstr(YAML_SINGLE_QUOTE) + '|' +
rexstr(ƔAML_SIMPLE_VALUE) rexstr(YAML_SIMPLE_VALUE)
); );
const ƔAML_SEPARATOR = unirex( const YAML_SEPARATOR = unirex(
rexstr(ANY_WHITE_SPACE) + rexstr(ANY_WHITE_SPACE) +
':' + rexstr(WHITE_SPACE) + ':' + rexstr(WHITE_SPACE) +
rexstr(ANY_WHITE_SPACE) rexstr(ANY_WHITE_SPACE)
); );
const ƔAML_LINE = unirex( const YAML_LINE = unirex(
'(' + rexstr(ƔAML_KEY) + ')' + '(' + rexstr(YAML_KEY) + ')' +
rexstr(ƔAML_SEPARATOR) + rexstr(YAML_SEPARATOR) +
'(' + rexstr(ƔAML_VALUE) + ')' '(' + rexstr(YAML_VALUE) + ')'
); );
/* FRONTMATTER REGEX */ /* FRONTMATTER REGEX */
const ƔAML_FRONTMATTER = unirex( const YAML_FRONTMATTER = unirex(
rexstr(POSSIBLE_STARTS) + rexstr(POSSIBLE_STARTS) +
rexstr(ƔAML_LOOKAHEAD) + rexstr(YAML_LOOKAHEAD) +
rexstr(ƔAML_START) + rexstr(SOME_NEW_LINES) + rexstr(YAML_START) + rexstr(SOME_NEW_LINES) +
'(?:' + '(?:' +
rexstr(ANY_WHITE_SPACE) + rexstr(ƔAML_LINE) + rexstr(SOME_NEW_LINES) + '(' + rexstr(INDENTATION) + ')' +
'){0,5}' + rexstr(YAML_LINE) + rexstr(SOME_NEW_LINES) +
rexstr(ƔAML_END) + rexstr(POSSIBLE_ENDS) '(?:' +
'\\1' + rexstr(YAML_LINE) + rexstr(SOME_NEW_LINES) +
'){0,4}' +
')?' +
rexstr(YAML_END) + rexstr(POSSIBLE_ENDS)
); );
/* SEARCHES */ /* SEARCHES */
const FIND_ƔAML_LINE = unirex( const FIND_YAML_LINES = unirex(
rexstr(NEW_LINE) + rexstr(ANY_WHITE_SPACE) + rexstr(ƔAML_LINE) rexstr(NEW_LINE) + rexstr(INDENTATION) + rexstr(YAML_LINE)
); );
/* STRING PROCESSING */ /* STRING PROCESSING */
function processString (str) { function processString(str) {
switch (str.charAt(0)) { switch (str.charAt(0)) {
case '"': case '"':
return str.substring(1, str.length - 1); return str
.substring(1, str.length - 1)
.replace(/\\0/g, '\x00')
.replace(/\\a/g, '\x07')
.replace(/\\b/g, '\x08')
.replace(/\\t/g, '\x09')
.replace(/\\\x09/g, '\x09')
.replace(/\\n/g, '\x0a')
.replace(/\\v/g, '\x0b')
.replace(/\\f/g, '\x0c')
.replace(/\\r/g, '\x0d')
.replace(/\\e/g, '\x1b')
.replace(/\\ /g, '\x20')
.replace(/\\"/g, '\x22')
.replace(/\\\//g, '\x2f')
.replace(/\\\\/g, '\x5c')
.replace(/\\N/g, '\x85')
.replace(/\\_/g, '\xa0')
.replace(/\\L/g, '\u2028')
.replace(/\\P/g, '\u2029')
.replace(
new RegExp(
unirex(
rexstr(/\\x/) + '(' + rexstr(HEXADECIMAL_CHARS) + '{2})'
), 'gu'
), (_, n) => String.fromCodePoint('0x' + n)
)
.replace(
new RegExp(
unirex(
rexstr(/\\u/) + '(' + rexstr(HEXADECIMAL_CHARS) + '{4})'
), 'gu'
), (_, n) => String.fromCodePoint('0x' + n)
)
.replace(
new RegExp(
unirex(
rexstr(/\\U/) + '(' + rexstr(HEXADECIMAL_CHARS) + '{8})'
), 'gu'
), (_, n) => String.fromCodePoint('0x' + n)
);
case '\'': case '\'':
return str return str
.substring(1, str.length - 1) .substring(1, str.length - 1)
@@ -269,18 +321,15 @@ export function processBio(content) {
text: content, text: content,
metadata: [], metadata: [],
}; };
let ɣaml = content.match(ƔAML_FRONTMATTER); let yaml = content.match(YAML_FRONTMATTER);
if (!ɣaml) { if (!yaml) return result;
return result; else yaml = yaml[0];
} else { let start = content.search(YAML_START);
ɣaml = ɣaml[0]; let end = start + yaml.length - yaml.search(YAML_START);
} result.text = content.substr(0, start) + content.substr(end);
const start = content.search(ƔAML_START);
const end = start + ɣaml.length - ɣaml.search(ƔAML_START);
result.text = content.substr(end);
let metadata = null; let metadata = null;
let query = new RegExp(rexstr(FIND_ƔAML_LINE), 'g'); // Some browsers don't allow flags unless both args are strings let query = new RegExp(FIND_YAML_LINES, 'g');
while ((metadata = query.exec(ɣaml))) { while ((metadata = query.exec(yaml))) {
result.metadata.push([ result.metadata.push([
processString(metadata[1]), processString(metadata[1]),
processString(metadata[2]), processString(metadata[2]),
@@ -303,23 +352,63 @@ export function createBio(note, data) {
let val = '' + data[i][1]; let val = '' + data[i][1];
// Key processing // Key processing
if (key === (key.match(ƔAML_SIMPLE_KEY) || [])[0]) /* do nothing */; if (key === (key.match(YAML_SIMPLE_KEY) || [])[0]) /* do nothing */;
else if (key === (key.match(ANY_QUOTE_CHAR) || [])[0]) key = '"' + key + '"'; else if (key.indexOf('\'') === -1 && key === (key.match(ANY_ESCAPED_APOS) || [])[0]) key = '\'' + key + '\'';
else { else {
key = key key = key
.replace(/'/g, '\'\'') .replace(/\x00/g, '\\0')
.replace(new RegExp(rexstr(NOT_ALLOWED_CHAR), compat_mode ? 'g' : 'gu'), '<EFBFBD>'); .replace(/\x07/g, '\\a')
key = '\'' + key + '\''; .replace(/\x08/g, '\\b')
.replace(/\x0a/g, '\\n')
.replace(/\x0b/g, '\\v')
.replace(/\x0c/g, '\\f')
.replace(/\x0d/g, '\\r')
.replace(/\x1b/g, '\\e')
.replace(/\x22/g, '\\"')
.replace(/\x5c/g, '\\\\');
let badchars = key.match(
new RegExp(rexstr(NOT_ALLOWED_CHAR), 'gu')
) || [];
for (let j = 0; j < badchars.length; j++) {
key = key.replace(
badchars[i],
'\\u' + badchars[i].codePointAt(0).toLocaleString('en', {
useGrouping: false,
minimumIntegerDigits: 4,
})
);
}
key = '"' + key + '"';
} }
// Value processing // Value processing
if (val === (val.match(ƔAML_SIMPLE_VALUE) || [])[0]) /* do nothing */; if (val === (val.match(YAML_SIMPLE_VALUE) || [])[0]) /* do nothing */;
else if (val === (val.match(ANY_QUOTE_CHAR) || [])[0]) val = '"' + val + '"'; else if (val.indexOf('\'') === -1 && val === (val.match(ANY_ESCAPED_APOS) || [])[0]) val = '\'' + val + '\'';
else { else {
key = key val = val
.replace(/'/g, '\'\'') .replace(/\x00/g, '\\0')
.replace(new RegExp(rexstr(NOT_ALLOWED_CHAR), compat_mode ? 'g' : 'gu'), '<EFBFBD>'); .replace(/\x07/g, '\\a')
key = '\'' + key + '\''; .replace(/\x08/g, '\\b')
.replace(/\x0a/g, '\\n')
.replace(/\x0b/g, '\\v')
.replace(/\x0c/g, '\\f')
.replace(/\x0d/g, '\\r')
.replace(/\x1b/g, '\\e')
.replace(/\x22/g, '\\"')
.replace(/\x5c/g, '\\\\');
let badchars = val.match(
new RegExp(rexstr(NOT_ALLOWED_CHAR), 'gu')
) || [];
for (let j = 0; j < badchars.length; j++) {
val = val.replace(
badchars[i],
'\\u' + badchars[i].codePointAt(0).toLocaleString('en', {
useGrouping: false,
minimumIntegerDigits: 4,
})
);
}
val = '"' + val + '"';
} }
frontmatter += key + ': ' + val + '\n'; frontmatter += key + ': ' + val + '\n';

View File

@@ -5,6 +5,8 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import ReplyIndicatorContainer from '../containers/reply_indicator_container'; import ReplyIndicatorContainer from '../containers/reply_indicator_container';
import AutosuggestTextarea from '../../../components/autosuggest_textarea'; import AutosuggestTextarea from '../../../components/autosuggest_textarea';
import UploadButtonContainer from '../containers/upload_button_container';
import DoodleButtonContainer from '../containers/doodle_button_container';
import { defineMessages, injectIntl } from 'react-intl'; import { defineMessages, injectIntl } from 'react-intl';
import Collapsable from '../../../components/collapsable'; import Collapsable from '../../../components/collapsable';
import SpoilerButtonContainer from '../containers/spoiler_button_container'; import SpoilerButtonContainer from '../containers/spoiler_button_container';
@@ -18,7 +20,6 @@ import { isMobile } from '../../../is_mobile';
import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePureComponent from 'react-immutable-pure-component';
import { length } from 'stringz'; import { length } from 'stringz';
import { countableText } from '../util/counter'; import { countableText } from '../util/counter';
import ComposeAttachOptions from '../../../../glitch/components/compose/attach_options/index';
const messages = defineMessages({ const messages = defineMessages({
placeholder: { id: 'compose_form.placeholder', defaultMessage: 'What is on your mind?' }, placeholder: { id: 'compose_form.placeholder', defaultMessage: 'What is on your mind?' },
@@ -240,12 +241,12 @@ export default class ComposeForm extends ImmutablePureComponent {
</div> </div>
<div className='compose-form__buttons'> <div className='compose-form__buttons'>
<ComposeAttachOptions /> <UploadButtonContainer />
<SensitiveButtonContainer /> <DoodleButtonContainer />
<div className='compose-form__buttons-separator' />
<PrivacyDropdownContainer /> <PrivacyDropdownContainer />
<SpoilerButtonContainer />
<ComposeAdvancedOptionsContainer /> <ComposeAdvancedOptionsContainer />
<SensitiveButtonContainer />
<SpoilerButtonContainer />
</div> </div>
<div className='compose-form__publish'> <div className='compose-form__publish'>

View File

@@ -0,0 +1,41 @@
import React from 'react';
import IconButton from '../../../components/icon_button';
import PropTypes from 'prop-types';
import { defineMessages, injectIntl } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component';
const messages = defineMessages({
doodle: { id: 'doodle_button.label', defaultMessage: 'Add a drawing' },
});
const iconStyle = {
height: null,
lineHeight: '27px',
};
@injectIntl
export default class UploadButton extends ImmutablePureComponent {
static propTypes = {
disabled: PropTypes.bool,
onOpenCanvas: PropTypes.func.isRequired,
style: PropTypes.object,
intl: PropTypes.object.isRequired,
};
handleClick = () => {
this.props.onOpenCanvas();
}
render () {
const { intl, disabled } = this.props;
return (
<div className='compose-form__upload-button'>
<IconButton icon='pencil' title={intl.formatMessage(messages.doodle)} disabled={disabled} onClick={this.handleClick} className='compose-form__upload-button-icon' size={18} inverted style={iconStyle} />
</div>
);
}
}

View File

@@ -0,0 +1,15 @@
import { connect } from 'react-redux';
import DoodleButton from '../components/doodle_button';
import { openModal } from '../../../actions/modal';
const mapStateToProps = state => ({
disabled: state.getIn(['compose', 'is_uploading']) || (state.getIn(['compose', 'media_attachments']).size > 3 || state.getIn(['compose', 'media_attachments']).some(m => m.get('type') === 'video')),
});
const mapDispatchToProps = dispatch => ({
onOpenCanvas () {
dispatch(openModal('DOODLE', { noEsc: true }));
},
});
export default connect(mapStateToProps, mapDispatchToProps)(DoodleButton);

View File

@@ -322,11 +322,6 @@
} }
} }
.compose-form__buttons-separator {
border-left: 1px solid #c3c3c3;
margin: 0 3px;
}
.compose-form__upload-button-icon { .compose-form__upload-button-icon {
line-height: 27px; line-height: 27px;
} }