Compare commits

..

12 Commits

Author SHA1 Message Date
kibigo!
276432790a Introducing: Mastodon GO! 2017-11-04 05:48:42 -07:00
Ondřej Hruška
516eeeb43d option to add title to <Button>, use for toot buttons (#197) 2017-10-24 19:08:07 +02:00
David Yip
664c9aa708 Merge pull request #196 from glitch-soc/fix-imports
Added app/javascript for imports
2017-10-23 23:34:43 -05:00
kibigo!
119d477c8b Added app/javascript for imports 2017-10-23 20:22:48 -07:00
David Yip
4f01e6e8d5 Merge remote-tracking branch 'origin/master' into gs-master 2017-10-22 22:57:41 -05:00
Marcin Mikołajczak
fdb0848e08 i18n: Update Polish Translation (#5494) 2017-10-22 08:34:39 +09:00
Ondřej Hruška
d589dd7cd0 Compose buttons bar redesign + generalize dropdown (#194)
* Generalize compose dropdown for re-use

* wip stuffs

* new tootbox look and removed old doodle button files

* use the house icon for ...
2017-10-21 20:24:53 +02:00
beatrix
a7be86e875 hide mentions of muted accounts (in home col) (#190)
* hide mentions of muted accounts (in home col)

also cleans up some old crap

* add test
2017-10-20 10:49:54 -04:00
beatrix
b15dd05514 Merge pull request #191 from glitch-soc/garglamel-yaml
ƔAML update
2017-10-19 19:29:52 -04:00
kibigo!
21bafc6555 Updates to bio metadata script 2017-10-19 16:11:53 -07:00
Nolan Lawson
8392ddbf87 Remove unnecessary translateZ(0) when doing scale() (#5473) 2017-10-19 18:27:55 +02:00
masarakki
049381b284 remove-duplicated-jest-config (#5465) 2017-10-19 13:51:38 +02:00
20 changed files with 368 additions and 352 deletions

View File

@@ -29,6 +29,11 @@ settings:
import/ignore: import/ignore:
- node_modules - node_modules
- \\.(css|scss|json)$ - \\.(css|scss|json)$
import/resolver:
node:
moduleDirectory:
- node_modules
- app/javascript
rules: rules:
brace-style: warn brace-style: warn

3
.gitmodules vendored Normal file
View File

@@ -0,0 +1,3 @@
[submodule "app/javascript/themes/mastodon-go"]
path = app/javascript/themes/mastodon-go
url = https://github.com/marrus-sh/mastodon-go

2
Vagrantfile vendored
View File

@@ -83,7 +83,7 @@ Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
config.vm.provider :virtualbox do |vb| config.vm.provider :virtualbox do |vb|
vb.name = "mastodon" vb.name = "mastodon"
vb.customize ["modifyvm", :id, "--memory", "2048"] vb.customize ["modifyvm", :id, "--memory", "4096"]
# Disable VirtualBox DNS proxy to skip long-delay IPv6 resolutions. # Disable VirtualBox DNS proxy to skip long-delay IPv6 resolutions.
# https://github.com/mitchellh/vagrant/issues/1172 # https://github.com/mitchellh/vagrant/issues/1172

View File

@@ -47,11 +47,9 @@ 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';
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
@@ -77,11 +75,6 @@ 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:
@@ -100,67 +93,6 @@ 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;
}
/* /*
@@ -171,7 +103,6 @@ anywhere else on the screen.
*/ */
render () { render () {
const { open } = this.state;
const { intl, values } = this.props; const { intl, values } = this.props;
/* /*
@@ -218,23 +149,14 @@ 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 (
<div ref={this.setRef} className={`advanced-options-dropdown ${open ? 'open' : ''} ${anyEnabled ? 'active' : ''} `}> <ComposeDropdown
<div className='advanced-options-dropdown__value'> title={intl.formatMessage(messages.advanced_options_icon_title)}
<IconButton icon='home'
className='advanced-options-dropdown__value' highlight={anyEnabled}
title={intl.formatMessage(messages.advanced_options_icon_title)} >
icon='ellipsis-h' active={open || anyEnabled} {optionElems}
size={18} </ComposeDropdown>
style={iconStyle}
onClick={this.onToggleDropdown}
/>
</div>
<div className='advanced-options-dropdown__dropdown'>
{optionElems}
</div>
</div>
); );
} }

View File

@@ -0,0 +1,133 @@
// 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

@@ -0,0 +1,77 @@
// 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,6 +69,10 @@ 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]
@@ -96,10 +100,7 @@ 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 = /[,[\]{}]/;
@@ -121,7 +122,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(ANY_WHITE_SPACE) + rexstr(LINE_BREAK) + ')+' '(?:' + rexstr(NEW_LINE) + ')+'
); );
const POSSIBLE_STARTS = unirex( const POSSIBLE_STARTS = unirex(
rexstr(DOCUMENT_START) + rexstr(/<p[^<>]*>/) + '?' rexstr(DOCUMENT_START) + rexstr(/<p[^<>]*>/) + '?'
@@ -131,22 +132,13 @@ const POSSIBLE_ENDS = unirex(
rexstr(DOCUMENT_END) + '|' + rexstr(DOCUMENT_END) + '|' +
rexstr(/<\/p>/) rexstr(/<\/p>/)
); );
const CHARACTER_ESCAPE = unirex( const QUOTE_CHAR = unirex(
rexstr(/\\/) + '(?=' + rexstr(NOT_LINE_BREAK) + ')[^"]'
'(?:' +
rexstr(ESCAPE_CHAR) + '|' +
rexstr(/x/) + rexstr(HEXADECIMAL_CHARS) + '{2}' + '|' +
rexstr(/u/) + rexstr(HEXADECIMAL_CHARS) + '{4}' + '|' +
rexstr(/U/) + rexstr(HEXADECIMAL_CHARS) + '{8}' +
')'
); );
const ESCAPED_CHAR = unirex( const ANY_QUOTE_CHAR = unirex(
rexstr(/(?!["\\])/) + rexstr(NOT_LINE_BREAK) + '|' + rexstr(QUOTE_CHAR) + '*'
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(/[^']|''/)
); );
@@ -190,120 +182,76 @@ const LATER_VALUE_CHAR = unirex(
/* YAML CONSTRUCTS */ /* YAML CONSTRUCTS */
const YAML_START = unirex( const ƔAML_START = unirex(
rexstr(ANY_WHITE_SPACE) + rexstr(/---/) rexstr(ANY_WHITE_SPACE) + '---'
); );
const YAML_END = unirex( const ƔAML_END = unirex(
rexstr(ANY_WHITE_SPACE) + rexstr(/(?:---|\.\.\.)/) rexstr(ANY_WHITE_SPACE) + '(?:---|\.\.\.)'
); );
const YAML_LOOKAHEAD = unirex( const ƔAML_LOOKAHEAD = unirex(
'(?=' + '(?=' +
rexstr(YAML_START) + rexstr(ƔAML_START) +
rexstr(ANY_ALLOWED_CHARS) + rexstr(NEW_LINE) + rexstr(ANY_ALLOWED_CHARS) + rexstr(NEW_LINE) +
rexstr(YAML_END) + rexstr(POSSIBLE_ENDS) + rexstr(ƔAML_END) + rexstr(POSSIBLE_ENDS) +
')' ')'
); );
const YAML_DOUBLE_QUOTE = unirex( const ƔAML_DOUBLE_QUOTE = unirex(
rexstr(/"/) + rexstr(ANY_ESCAPED_CHARS) + rexstr(/"/) '"' + rexstr(ANY_QUOTE_CHAR) + '"'
); );
const YAML_SINGLE_QUOTE = unirex( const ƔAML_SINGLE_QUOTE = unirex(
rexstr(/'/) + rexstr(ANY_ESCAPED_APOS) + rexstr(/'/) '\'' + rexstr(ANY_ESCAPED_APOS) + '\''
); );
const YAML_SIMPLE_KEY = unirex( const ƔAML_SIMPLE_KEY = unirex(
rexstr(FIRST_KEY_CHAR) + rexstr(LATER_KEY_CHAR) + '*' rexstr(FIRST_KEY_CHAR) + rexstr(LATER_KEY_CHAR) + '*'
); );
const YAML_SIMPLE_VALUE = unirex( const ƔAML_SIMPLE_VALUE = unirex(
rexstr(FIRST_VALUE_CHAR) + rexstr(LATER_VALUE_CHAR) + '*' rexstr(FIRST_VALUE_CHAR) + rexstr(LATER_VALUE_CHAR) + '*'
); );
const YAML_KEY = unirex( const ƔAML_KEY = unirex(
rexstr(YAML_DOUBLE_QUOTE) + '|' + rexstr(ƔAML_DOUBLE_QUOTE) + '|' +
rexstr(YAML_SINGLE_QUOTE) + '|' + rexstr(ƔAML_SINGLE_QUOTE) + '|' +
rexstr(YAML_SIMPLE_KEY) rexstr(ƔAML_SIMPLE_KEY)
); );
const YAML_VALUE = unirex( const ƔAML_VALUE = unirex(
rexstr(YAML_DOUBLE_QUOTE) + '|' + rexstr(ƔAML_DOUBLE_QUOTE) + '|' +
rexstr(YAML_SINGLE_QUOTE) + '|' + rexstr(ƔAML_SINGLE_QUOTE) + '|' +
rexstr(YAML_SIMPLE_VALUE) rexstr(ƔAML_SIMPLE_VALUE)
); );
const YAML_SEPARATOR = unirex( const ƔAML_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 YAML_LINE = unirex( const ƔAML_LINE = unirex(
'(' + rexstr(YAML_KEY) + ')' + '(' + rexstr(ƔAML_KEY) + ')' +
rexstr(YAML_SEPARATOR) + rexstr(ƔAML_SEPARATOR) +
'(' + rexstr(YAML_VALUE) + ')' '(' + rexstr(ƔAML_VALUE) + ')'
); );
/* FRONTMATTER REGEX */ /* FRONTMATTER REGEX */
const YAML_FRONTMATTER = unirex( const ƔAML_FRONTMATTER = unirex(
rexstr(POSSIBLE_STARTS) + rexstr(POSSIBLE_STARTS) +
rexstr(YAML_LOOKAHEAD) + rexstr(ƔAML_LOOKAHEAD) +
rexstr(YAML_START) + rexstr(SOME_NEW_LINES) + rexstr(ƔAML_START) + rexstr(SOME_NEW_LINES) +
'(?:' + '(?:' +
'(' + rexstr(INDENTATION) + ')' + rexstr(ANY_WHITE_SPACE) + rexstr(ƔAML_LINE) + rexstr(SOME_NEW_LINES) +
rexstr(YAML_LINE) + rexstr(SOME_NEW_LINES) + '){0,5}' +
'(?:' + 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_YAML_LINES = unirex( const FIND_ƔAML_LINE = unirex(
rexstr(NEW_LINE) + rexstr(INDENTATION) + rexstr(YAML_LINE) rexstr(NEW_LINE) + rexstr(ANY_WHITE_SPACE) + rexstr(ƔAML_LINE)
); );
/* STRING PROCESSING */ /* STRING PROCESSING */
function processString(str) { function processString (str) {
switch (str.charAt(0)) { switch (str.charAt(0)) {
case '"': case '"':
return str return str.substring(1, str.length - 1);
.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)
@@ -321,15 +269,18 @@ export function processBio(content) {
text: content, text: content,
metadata: [], metadata: [],
}; };
let yaml = content.match(YAML_FRONTMATTER); let ɣaml = content.match(ƔAML_FRONTMATTER);
if (!yaml) return result; if (!ɣaml) {
else yaml = yaml[0]; return result;
let start = content.search(YAML_START); } else {
let end = start + yaml.length - yaml.search(YAML_START); ɣaml = ɣaml[0];
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(FIND_YAML_LINES, 'g'); let query = new RegExp(rexstr(FIND_ƔAML_LINE), 'g'); // Some browsers don't allow flags unless both args are strings
while ((metadata = query.exec(yaml))) { while ((metadata = query.exec(ɣaml))) {
result.metadata.push([ result.metadata.push([
processString(metadata[1]), processString(metadata[1]),
processString(metadata[2]), processString(metadata[2]),
@@ -352,63 +303,23 @@ export function createBio(note, data) {
let val = '' + data[i][1]; let val = '' + data[i][1];
// Key processing // Key processing
if (key === (key.match(YAML_SIMPLE_KEY) || [])[0]) /* do nothing */; if (key === (key.match(ƔAML_SIMPLE_KEY) || [])[0]) /* do nothing */;
else if (key.indexOf('\'') === -1 && key === (key.match(ANY_ESCAPED_APOS) || [])[0]) key = '\'' + key + '\''; else if (key === (key.match(ANY_QUOTE_CHAR) || [])[0]) key = '"' + key + '"';
else { else {
key = key key = key
.replace(/\x00/g, '\\0') .replace(/'/g, '\'\'')
.replace(/\x07/g, '\\a') .replace(new RegExp(rexstr(NOT_ALLOWED_CHAR), compat_mode ? 'g' : 'gu'), '<EFBFBD>');
.replace(/\x08/g, '\\b') key = '\'' + key + '\'';
.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(YAML_SIMPLE_VALUE) || [])[0]) /* do nothing */; if (val === (val.match(ƔAML_SIMPLE_VALUE) || [])[0]) /* do nothing */;
else if (val.indexOf('\'') === -1 && val === (val.match(ANY_ESCAPED_APOS) || [])[0]) val = '\'' + val + '\''; else if (val === (val.match(ANY_QUOTE_CHAR) || [])[0]) val = '"' + val + '"';
else { else {
val = val key = key
.replace(/\x00/g, '\\0') .replace(/'/g, '\'\'')
.replace(/\x07/g, '\\a') .replace(new RegExp(rexstr(NOT_ALLOWED_CHAR), compat_mode ? 'g' : 'gu'), '<EFBFBD>');
.replace(/\x08/g, '\\b') key = '\'' + key + '\'';
.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

@@ -112,3 +112,19 @@ exports[`<Button /> renders the props.text instead of children 1`] = `
foo foo
</button> </button>
`; `;
exports[`<Button /> renders title if props.title is given 1`] = `
<button
className="button"
disabled={undefined}
onClick={[Function]}
style={
Object {
"height": "36px",
"lineHeight": "36px",
"padding": "0 16px",
}
}
title="foo"
/>
`;

View File

@@ -72,4 +72,11 @@ describe('<Button />', () => {
expect(tree).toMatchSnapshot(); expect(tree).toMatchSnapshot();
}); });
it('renders title if props.title is given', () => {
const component = renderer.create(<Button title='foo' />);
const tree = component.toJSON();
expect(tree).toMatchSnapshot();
});
}); });

View File

@@ -14,6 +14,7 @@ export default class Button extends React.PureComponent {
className: PropTypes.string, className: PropTypes.string,
style: PropTypes.object, style: PropTypes.object,
children: PropTypes.node, children: PropTypes.node,
title: PropTypes.string,
}; };
static defaultProps = { static defaultProps = {
@@ -35,26 +36,26 @@ export default class Button extends React.PureComponent {
} }
render () { render () {
const style = { let attrs = {
padding: `0 ${this.props.size / 2.25}px`, className: classNames('button', this.props.className, {
height: `${this.props.size}px`, 'button-secondary': this.props.secondary,
lineHeight: `${this.props.size}px`, 'button--block': this.props.block,
...this.props.style, }),
disabled: this.props.disabled,
onClick: this.handleClick,
ref: this.setRef,
style: {
padding: `0 ${this.props.size / 2.25}px`,
height: `${this.props.size}px`,
lineHeight: `${this.props.size}px`,
...this.props.style,
},
}; };
const className = classNames('button', this.props.className, { if (this.props.title) attrs.title = this.props.title;
'button-secondary': this.props.secondary,
'button--block': this.props.block,
});
return ( return (
<button <button {...attrs}>
className={className}
disabled={this.props.disabled}
onClick={this.handleClick}
ref={this.setRef}
style={style}
>
{this.props.text || this.props.children} {this.props.text || this.props.children}
</button> </button>
); );

View File

@@ -5,8 +5,6 @@ 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';
@@ -20,6 +18,7 @@ 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?' },
@@ -165,6 +164,8 @@ export default class ComposeForm extends ImmutablePureComponent {
let publishText = ''; let publishText = '';
let publishText2 = ''; let publishText2 = '';
let title = '';
let title2 = '';
const privacyIcons = { const privacyIcons = {
none: '', none: '',
@@ -174,7 +175,10 @@ export default class ComposeForm extends ImmutablePureComponent {
direct: 'envelope', direct: 'envelope',
}; };
title = `${intl.formatMessage(messages.publish)}: ${intl.formatMessage({ id: `privacy.${this.props.privacy}.short` })}`;
if (showSideArm) { if (showSideArm) {
// Enhanced behavior with dual toot buttons
publishText = ( publishText = (
<span> <span>
{ {
@@ -186,13 +190,15 @@ export default class ComposeForm extends ImmutablePureComponent {
</span> </span>
); );
title2 = `${intl.formatMessage(messages.publish)}: ${intl.formatMessage({ id: `privacy.${secondaryVisibility}.short` })}`;
publishText2 = ( publishText2 = (
<i <i
className={`fa fa-${privacyIcons[secondaryVisibility]}`} className={`fa fa-${privacyIcons[secondaryVisibility]}`}
aria-label={`${intl.formatMessage(messages.publish)}: ${intl.formatMessage({ id: `privacy.${secondaryVisibility}.short` })}`} aria-label={title2}
/> />
); );
} else { } else {
// Original vanilla behavior - no icon if public or unlisted
if (this.props.privacy === 'private' || this.props.privacy === 'direct') { if (this.props.privacy === 'private' || this.props.privacy === 'direct') {
publishText = <span className='compose-form__publish-private'><i className='fa fa-lock' /> {intl.formatMessage(messages.publish)}</span>; publishText = <span className='compose-form__publish-private'><i className='fa fa-lock' /> {intl.formatMessage(messages.publish)}</span>;
} else { } else {
@@ -241,12 +247,12 @@ export default class ComposeForm extends ImmutablePureComponent {
</div> </div>
<div className='compose-form__buttons'> <div className='compose-form__buttons'>
<UploadButtonContainer /> <ComposeAttachOptions />
<DoodleButtonContainer />
<PrivacyDropdownContainer />
<ComposeAdvancedOptionsContainer />
<SensitiveButtonContainer /> <SensitiveButtonContainer />
<div className='compose-form__buttons-separator' />
<PrivacyDropdownContainer />
<SpoilerButtonContainer /> <SpoilerButtonContainer />
<ComposeAdvancedOptionsContainer />
</div> </div>
<div className='compose-form__publish'> <div className='compose-form__publish'>
@@ -257,6 +263,7 @@ export default class ComposeForm extends ImmutablePureComponent {
<Button <Button
className='compose-form__publish__side-arm' className='compose-form__publish__side-arm'
text={publishText2} text={publishText2}
title={title2}
onClick={this.handleSubmit2} onClick={this.handleSubmit2}
disabled={submitDisabled} disabled={submitDisabled}
/> : '' /> : ''
@@ -264,6 +271,7 @@ export default class ComposeForm extends ImmutablePureComponent {
<Button <Button
className='compose-form__publish__primary' className='compose-form__publish__primary'
text={publishText} text={publishText}
title={title}
onClick={this.handleSubmit} onClick={this.handleSubmit}
disabled={submitDisabled} disabled={submitDisabled}
/> />

View File

@@ -1,41 +0,0 @@
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

@@ -68,7 +68,7 @@ export default class Upload extends ImmutablePureComponent {
<div className='compose-form__upload' onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}> <div className='compose-form__upload' onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}>
<Motion defaultStyle={{ scale: 0.8 }} style={{ scale: spring(1, { stiffness: 180, damping: 12 }) }}> <Motion defaultStyle={{ scale: 0.8 }} style={{ scale: spring(1, { stiffness: 180, damping: 12 }) }}>
{({ scale }) => ( {({ scale }) => (
<div className='compose-form__upload-thumbnail' style={{ transform: `translateZ(0) scale(${scale})`, backgroundImage: `url(${media.get('preview_url')})` }}> <div className='compose-form__upload-thumbnail' style={{ transform: `scale(${scale})`, backgroundImage: `url(${media.get('preview_url')})` }}>
<IconButton icon='times' title={intl.formatMessage(messages.undo)} size={36} onClick={this.handleUndoClick} /> <IconButton icon='times' title={intl.formatMessage(messages.undo)} size={36} onClick={this.handleUndoClick} />
<div className={classNames('compose-form__upload-description', { active })}> <div className={classNames('compose-form__upload-description', { active })}>

View File

@@ -1,15 +0,0 @@
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

@@ -47,7 +47,7 @@ class SensitiveButton extends React.PureComponent {
'compose-form__sensitive-button--visible': visible, 'compose-form__sensitive-button--visible': visible,
}); });
return ( return (
<div className={className} style={{ transform: `translateZ(0) scale(${scale})` }}> <div className={className} style={{ transform: `scale(${scale})` }}>
<IconButton <IconButton
className='compose-form__sensitive-button__icon' className='compose-form__sensitive-button__icon'
title={intl.formatMessage(messages.title)} title={intl.formatMessage(messages.title)}

View File

@@ -40,7 +40,7 @@ export default class UploadArea extends React.PureComponent {
{({ backgroundOpacity, backgroundScale }) => {({ backgroundOpacity, backgroundScale }) =>
<div className='upload-area' style={{ visibility: active ? 'visible' : 'hidden', opacity: backgroundOpacity }}> <div className='upload-area' style={{ visibility: active ? 'visible' : 'hidden', opacity: backgroundOpacity }}>
<div className='upload-area__drop'> <div className='upload-area__drop'>
<div className='upload-area__background' style={{ transform: `translateZ(0) scale(${backgroundScale})` }} /> <div className='upload-area__background' style={{ transform: `scale(${backgroundScale})` }} />
<div className='upload-area__content'><FormattedMessage id='upload_area.title' defaultMessage='Drag & drop to upload' /></div> <div className='upload-area__content'><FormattedMessage id='upload_area.title' defaultMessage='Drag & drop to upload' /></div>
</div> </div>
</div> </div>

View File

@@ -159,11 +159,11 @@
"privacy.public.short": "Publiczny", "privacy.public.short": "Publiczny",
"privacy.unlisted.long": "Niewidoczny na publicznych osiach czasu", "privacy.unlisted.long": "Niewidoczny na publicznych osiach czasu",
"privacy.unlisted.short": "Niewidoczny", "privacy.unlisted.short": "Niewidoczny",
"relative_time.days": "{number}d", "relative_time.days": "{number} dni",
"relative_time.hours": "{number}h", "relative_time.hours": "{number} godz.",
"relative_time.just_now": "now", "relative_time.just_now": "teraz",
"relative_time.minutes": "{number}m", "relative_time.minutes": "{number} min.",
"relative_time.seconds": "{number}s", "relative_time.seconds": "{number} s.",
"reply_indicator.cancel": "Anuluj", "reply_indicator.cancel": "Anuluj",
"report.placeholder": "Dodatkowe komentarze", "report.placeholder": "Dodatkowe komentarze",
"report.submit": "Wyślij", "report.submit": "Wyślij",

View File

@@ -322,6 +322,11 @@
} }
} }
.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;
} }
@@ -3397,21 +3402,21 @@ button.icon-button.active i.fa-retweet {
} }
.fa-search { .fa-search {
transform: translateZ(0) rotate(90deg); transform: rotate(90deg);
&.active { &.active {
pointer-events: none; pointer-events: none;
transform: translateZ(0) rotate(0deg); transform: rotate(0deg);
} }
} }
.fa-times-circle { .fa-times-circle {
top: 11px; top: 11px;
transform: translateZ(0) rotate(0deg); transform: rotate(0deg);
cursor: pointer; cursor: pointer;
&.active { &.active {
transform: translateZ(0) rotate(90deg); transform: rotate(90deg);
} }
&:hover { &:hover {

View File

@@ -135,22 +135,5 @@
}, },
"optionalDependencies": { "optionalDependencies": {
"fsevents": "*" "fsevents": "*"
},
"jest": {
"projects": [
"<rootDir>/app/javascript/mastodon"
],
"testPathIgnorePatterns": [
"<rootDir>/node_modules/",
"<rootDir>/vendor/",
"<rootDir>/config/",
"<rootDir>/log/",
"<rootDir>/public/",
"<rootDir>/tmp/"
],
"setupFiles": [
"raf/polyfill"
],
"setupTestFrameworkScriptFile": "<rootDir>/app/javascript/mastodon/test_setup.js"
} }
} }