Change design of compose form in web UI (#28119)
Co-authored-by: Claire <claire.github-309c@sitedethib.com>
@ -165,7 +165,7 @@ module.exports = defineConfig({
|
|||||||
// },
|
// },
|
||||||
// ],
|
// ],
|
||||||
'jsx-a11y/no-noninteractive-tabindex': 'off',
|
'jsx-a11y/no-noninteractive-tabindex': 'off',
|
||||||
'jsx-a11y/no-onchange': 'warn',
|
'jsx-a11y/no-onchange': 'off',
|
||||||
// recommended is full 'error'
|
// recommended is full 'error'
|
||||||
'jsx-a11y/no-static-element-interactions': [
|
'jsx-a11y/no-static-element-interactions': [
|
||||||
'warn',
|
'warn',
|
||||||
|
25
app/javascript/images/warning-stripes.svg
Executable file
@ -0,0 +1,25 @@
|
|||||||
|
<svg width="5" height="80" viewBox="0 0 5 80" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g clip-path="url(#clip0_253_1286)">
|
||||||
|
<rect width="5" height="80" fill="url(#paint0_linear_253_1286)"/>
|
||||||
|
<line x1="-0.860365" y1="6.80136" x2="10.6078" y2="-1.22871" stroke="black" stroke-width="3"/>
|
||||||
|
<line x1="-0.860365" y1="14.8314" x2="10.6078" y2="6.80132" stroke="black" stroke-width="3"/>
|
||||||
|
<line x1="-0.860365" y1="22.8615" x2="10.6078" y2="14.8314" stroke="black" stroke-width="3"/>
|
||||||
|
<line x1="-0.860365" y1="30.8916" x2="10.6078" y2="22.8615" stroke="black" stroke-width="3"/>
|
||||||
|
<line x1="-0.860365" y1="38.9216" x2="10.6078" y2="30.8915" stroke="black" stroke-width="3"/>
|
||||||
|
<line x1="-0.860365" y1="46.9517" x2="10.6078" y2="38.9216" stroke="black" stroke-width="3"/>
|
||||||
|
<line x1="-0.860365" y1="54.9818" x2="10.6078" y2="46.9517" stroke="black" stroke-width="3"/>
|
||||||
|
<line x1="-0.860365" y1="63.0118" x2="10.6078" y2="54.9817" stroke="black" stroke-width="3"/>
|
||||||
|
<line x1="-0.860365" y1="71.0419" x2="10.6078" y2="63.0118" stroke="black" stroke-width="3"/>
|
||||||
|
<line x1="-0.860365" y1="79.072" x2="10.6078" y2="71.0419" stroke="black" stroke-width="3"/>
|
||||||
|
<line x1="-0.860365" y1="87.102" x2="10.6078" y2="79.072" stroke="black" stroke-width="3"/>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="paint0_linear_253_1286" x1="2.5" y1="0" x2="2.5" y2="80" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stop-color="#FEC84B"/>
|
||||||
|
<stop offset="1" stop-color="#F79009"/>
|
||||||
|
</linearGradient>
|
||||||
|
<clipPath id="clip0_253_1286">
|
||||||
|
<rect width="5" height="80" fill="white"/>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.5 KiB |
@ -9,7 +9,11 @@ exports[`<AutosuggestEmoji /> renders emoji with custom url 1`] = `
|
|||||||
className="emojione"
|
className="emojione"
|
||||||
src="http://example.com/emoji.png"
|
src="http://example.com/emoji.png"
|
||||||
/>
|
/>
|
||||||
:foobar:
|
<div
|
||||||
|
className="autosuggest-emoji__name"
|
||||||
|
>
|
||||||
|
:foobar:
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@ -22,6 +26,10 @@ exports[`<AutosuggestEmoji /> renders native emoji 1`] = `
|
|||||||
className="emojione"
|
className="emojione"
|
||||||
src="/emoji/1f499.svg"
|
src="/emoji/1f499.svg"
|
||||||
/>
|
/>
|
||||||
:foobar:
|
<div
|
||||||
|
className="autosuggest-emoji__name"
|
||||||
|
>
|
||||||
|
:foobar:
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
@ -37,10 +37,10 @@ class Account extends ImmutablePureComponent {
|
|||||||
static propTypes = {
|
static propTypes = {
|
||||||
size: PropTypes.number,
|
size: PropTypes.number,
|
||||||
account: ImmutablePropTypes.record,
|
account: ImmutablePropTypes.record,
|
||||||
onFollow: PropTypes.func.isRequired,
|
onFollow: PropTypes.func,
|
||||||
onBlock: PropTypes.func.isRequired,
|
onBlock: PropTypes.func,
|
||||||
onMute: PropTypes.func.isRequired,
|
onMute: PropTypes.func,
|
||||||
onMuteNotifications: PropTypes.func.isRequired,
|
onMuteNotifications: PropTypes.func,
|
||||||
intl: PropTypes.object.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
hidden: PropTypes.bool,
|
hidden: PropTypes.bool,
|
||||||
minimal: PropTypes.bool,
|
minimal: PropTypes.bool,
|
||||||
|
@ -35,7 +35,7 @@ export default class AutosuggestEmoji extends PureComponent {
|
|||||||
alt={emoji.native || emoji.colons}
|
alt={emoji.native || emoji.colons}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{emoji.colons}
|
<div className='autosuggest-emoji__name'>{emoji.colons}</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
import { FormattedMessage } from 'react-intl';
|
|
||||||
|
|
||||||
import { ShortNumber } from 'mastodon/components/short_number';
|
import { ShortNumber } from 'mastodon/components/short_number';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@ -16,27 +14,18 @@ interface Props {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AutosuggestHashtag: React.FC<Props> = ({ tag }) => {
|
export const AutosuggestHashtag: React.FC<Props> = ({ tag }) => (
|
||||||
const weeklyUses = tag.history && (
|
<div className='autosuggest-hashtag'>
|
||||||
<ShortNumber
|
<div className='autosuggest-hashtag__name'>
|
||||||
value={tag.history.reduce((total, day) => total + day.uses * 1, 0)}
|
#<strong>{tag.name}</strong>
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='autosuggest-hashtag'>
|
|
||||||
<div className='autosuggest-hashtag__name'>
|
|
||||||
#<strong>{tag.name}</strong>
|
|
||||||
</div>
|
|
||||||
{tag.history !== undefined && (
|
|
||||||
<div className='autosuggest-hashtag__uses'>
|
|
||||||
<FormattedMessage
|
|
||||||
id='autosuggest_hashtag.per_week'
|
|
||||||
defaultMessage='{count} per week'
|
|
||||||
values={{ count: weeklyUses }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
|
||||||
};
|
{tag.history !== undefined && (
|
||||||
|
<div className='autosuggest-hashtag__uses'>
|
||||||
|
<ShortNumber
|
||||||
|
value={tag.history.reduce((total, day) => total + day.uses * 1, 0)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
@ -5,6 +5,8 @@ import classNames from 'classnames';
|
|||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
|
|
||||||
|
import Overlay from 'react-overlays/Overlay';
|
||||||
|
|
||||||
import AutosuggestAccountContainer from '../features/compose/containers/autosuggest_account_container';
|
import AutosuggestAccountContainer from '../features/compose/containers/autosuggest_account_container';
|
||||||
|
|
||||||
import AutosuggestEmoji from './autosuggest_emoji';
|
import AutosuggestEmoji from './autosuggest_emoji';
|
||||||
@ -195,34 +197,37 @@ export default class AutosuggestInput extends ImmutablePureComponent {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='autosuggest-input'>
|
<div className='autosuggest-input'>
|
||||||
<label>
|
<input
|
||||||
<span style={{ display: 'none' }}>{placeholder}</span>
|
type='text'
|
||||||
|
ref={this.setInput}
|
||||||
|
disabled={disabled}
|
||||||
|
placeholder={placeholder}
|
||||||
|
autoFocus={autoFocus}
|
||||||
|
value={value}
|
||||||
|
onChange={this.onChange}
|
||||||
|
onKeyDown={this.onKeyDown}
|
||||||
|
onKeyUp={onKeyUp}
|
||||||
|
onFocus={this.onFocus}
|
||||||
|
onBlur={this.onBlur}
|
||||||
|
dir='auto'
|
||||||
|
aria-autocomplete='list'
|
||||||
|
aria-label={placeholder}
|
||||||
|
id={id}
|
||||||
|
className={className}
|
||||||
|
maxLength={maxLength}
|
||||||
|
lang={lang}
|
||||||
|
spellCheck={spellCheck}
|
||||||
|
/>
|
||||||
|
|
||||||
<input
|
<Overlay show={!(suggestionsHidden || suggestions.isEmpty())} offset={[0, 0]} placement='bottom' target={this.input} popperConfig={{ strategy: 'fixed' }}>
|
||||||
type='text'
|
{({ props }) => (
|
||||||
ref={this.setInput}
|
<div {...props}>
|
||||||
disabled={disabled}
|
<div className='autosuggest-textarea__suggestions' style={{ width: this.input?.clientWidth }}>
|
||||||
placeholder={placeholder}
|
{suggestions.map(this.renderSuggestion)}
|
||||||
autoFocus={autoFocus}
|
</div>
|
||||||
value={value}
|
</div>
|
||||||
onChange={this.onChange}
|
)}
|
||||||
onKeyDown={this.onKeyDown}
|
</Overlay>
|
||||||
onKeyUp={onKeyUp}
|
|
||||||
onFocus={this.onFocus}
|
|
||||||
onBlur={this.onBlur}
|
|
||||||
dir='auto'
|
|
||||||
aria-autocomplete='list'
|
|
||||||
id={id}
|
|
||||||
className={className}
|
|
||||||
maxLength={maxLength}
|
|
||||||
lang={lang}
|
|
||||||
spellCheck={spellCheck}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<div className={`autosuggest-textarea__suggestions ${suggestionsHidden || suggestions.isEmpty() ? '' : 'autosuggest-textarea__suggestions--visible'}`}>
|
|
||||||
{suggestions.map(this.renderSuggestion)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,7 @@ import classNames from 'classnames';
|
|||||||
|
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
|
|
||||||
|
import Overlay from 'react-overlays/Overlay';
|
||||||
import Textarea from 'react-textarea-autosize';
|
import Textarea from 'react-textarea-autosize';
|
||||||
|
|
||||||
import AutosuggestAccountContainer from '../features/compose/containers/autosuggest_account_container';
|
import AutosuggestAccountContainer from '../features/compose/containers/autosuggest_account_container';
|
||||||
@ -52,7 +53,6 @@ const AutosuggestTextarea = forwardRef(({
|
|||||||
onFocus,
|
onFocus,
|
||||||
autoFocus = true,
|
autoFocus = true,
|
||||||
lang,
|
lang,
|
||||||
children,
|
|
||||||
}, textareaRef) => {
|
}, textareaRef) => {
|
||||||
|
|
||||||
const [suggestionsHidden, setSuggestionsHidden] = useState(true);
|
const [suggestionsHidden, setSuggestionsHidden] = useState(true);
|
||||||
@ -183,40 +183,38 @@ const AutosuggestTextarea = forwardRef(({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
return [
|
return (
|
||||||
<div className='compose-form__autosuggest-wrapper' key='autosuggest-wrapper'>
|
<div className='autosuggest-textarea'>
|
||||||
<div className='autosuggest-textarea'>
|
<Textarea
|
||||||
<label>
|
ref={textareaRef}
|
||||||
<span style={{ display: 'none' }}>{placeholder}</span>
|
className='autosuggest-textarea__textarea'
|
||||||
|
disabled={disabled}
|
||||||
|
placeholder={placeholder}
|
||||||
|
autoFocus={autoFocus}
|
||||||
|
value={value}
|
||||||
|
onChange={handleChange}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
onKeyUp={onKeyUp}
|
||||||
|
onFocus={handleFocus}
|
||||||
|
onBlur={handleBlur}
|
||||||
|
onPaste={handlePaste}
|
||||||
|
dir='auto'
|
||||||
|
aria-autocomplete='list'
|
||||||
|
aria-label={placeholder}
|
||||||
|
lang={lang}
|
||||||
|
/>
|
||||||
|
|
||||||
<Textarea
|
<Overlay show={!(suggestionsHidden || suggestions.isEmpty())} offset={[0, 0]} placement='bottom' target={textareaRef} popperConfig={{ strategy: 'fixed' }}>
|
||||||
ref={textareaRef}
|
{({ props }) => (
|
||||||
className='autosuggest-textarea__textarea'
|
<div {...props}>
|
||||||
disabled={disabled}
|
<div className='autosuggest-textarea__suggestions' style={{ width: textareaRef.current?.clientWidth }}>
|
||||||
placeholder={placeholder}
|
{suggestions.map(renderSuggestion)}
|
||||||
autoFocus={autoFocus}
|
</div>
|
||||||
value={value}
|
</div>
|
||||||
onChange={handleChange}
|
)}
|
||||||
onKeyDown={handleKeyDown}
|
</Overlay>
|
||||||
onKeyUp={onKeyUp}
|
</div>
|
||||||
onFocus={handleFocus}
|
);
|
||||||
onBlur={handleBlur}
|
|
||||||
onPaste={handlePaste}
|
|
||||||
dir='auto'
|
|
||||||
aria-autocomplete='list'
|
|
||||||
lang={lang}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
{children}
|
|
||||||
</div>,
|
|
||||||
|
|
||||||
<div className='autosuggest-textarea__suggestions-wrapper' key='suggestions-wrapper'>
|
|
||||||
<div className={`autosuggest-textarea__suggestions ${suggestionsHidden || suggestions.isEmpty() ? '' : 'autosuggest-textarea__suggestions--visible'}`}>
|
|
||||||
{suggestions.map(renderSuggestion)}
|
|
||||||
</div>
|
|
||||||
</div>,
|
|
||||||
];
|
|
||||||
});
|
});
|
||||||
|
|
||||||
AutosuggestTextarea.propTypes = {
|
AutosuggestTextarea.propTypes = {
|
||||||
@ -232,7 +230,6 @@ AutosuggestTextarea.propTypes = {
|
|||||||
onKeyDown: PropTypes.func,
|
onKeyDown: PropTypes.func,
|
||||||
onPaste: PropTypes.func.isRequired,
|
onPaste: PropTypes.func.isRequired,
|
||||||
onFocus:PropTypes.func,
|
onFocus:PropTypes.func,
|
||||||
children: PropTypes.node,
|
|
||||||
autoFocus: PropTypes.bool,
|
autoFocus: PropTypes.bool,
|
||||||
lang: PropTypes.string,
|
lang: PropTypes.string,
|
||||||
};
|
};
|
||||||
|
@ -165,7 +165,7 @@ class Dropdown extends PureComponent {
|
|||||||
children: PropTypes.node,
|
children: PropTypes.node,
|
||||||
icon: PropTypes.string,
|
icon: PropTypes.string,
|
||||||
iconComponent: PropTypes.func,
|
iconComponent: PropTypes.func,
|
||||||
items: PropTypes.oneOfType([PropTypes.array, ImmutablePropTypes.list]).isRequired,
|
items: PropTypes.oneOfType([PropTypes.array, ImmutablePropTypes.list]),
|
||||||
loading: PropTypes.bool,
|
loading: PropTypes.bool,
|
||||||
size: PropTypes.number,
|
size: PropTypes.number,
|
||||||
title: PropTypes.string,
|
title: PropTypes.string,
|
||||||
|
@ -70,9 +70,9 @@ export const defaultMediaVisibility = (status) => {
|
|||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
public_short: { id: 'privacy.public.short', defaultMessage: 'Public' },
|
public_short: { id: 'privacy.public.short', defaultMessage: 'Public' },
|
||||||
unlisted_short: { id: 'privacy.unlisted.short', defaultMessage: 'Unlisted' },
|
unlisted_short: { id: 'privacy.unlisted.short', defaultMessage: 'Quiet public' },
|
||||||
private_short: { id: 'privacy.private.short', defaultMessage: 'Followers only' },
|
private_short: { id: 'privacy.private.short', defaultMessage: 'Followers' },
|
||||||
direct_short: { id: 'privacy.direct.short', defaultMessage: 'Mentioned people only' },
|
direct_short: { id: 'privacy.direct.short', defaultMessage: 'Specific people' },
|
||||||
edited: { id: 'status.edited', defaultMessage: 'Edited {date}' },
|
edited: { id: 'status.edited', defaultMessage: 'Edited {date}' },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -2,8 +2,8 @@ import { defineMessages, useIntl } from 'react-intl';
|
|||||||
|
|
||||||
import AlternateEmailIcon from '@/material-icons/400-24px/alternate_email.svg?react';
|
import AlternateEmailIcon from '@/material-icons/400-24px/alternate_email.svg?react';
|
||||||
import LockIcon from '@/material-icons/400-24px/lock.svg?react';
|
import LockIcon from '@/material-icons/400-24px/lock.svg?react';
|
||||||
import LockOpenIcon from '@/material-icons/400-24px/lock_open.svg?react';
|
|
||||||
import PublicIcon from '@/material-icons/400-24px/public.svg?react';
|
import PublicIcon from '@/material-icons/400-24px/public.svg?react';
|
||||||
|
import QuietTimeIcon from '@/material-icons/400-24px/quiet_time.svg?react';
|
||||||
|
|
||||||
import { Icon } from './icon';
|
import { Icon } from './icon';
|
||||||
|
|
||||||
@ -11,14 +11,17 @@ type Visibility = 'public' | 'unlisted' | 'private' | 'direct';
|
|||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
public_short: { id: 'privacy.public.short', defaultMessage: 'Public' },
|
public_short: { id: 'privacy.public.short', defaultMessage: 'Public' },
|
||||||
unlisted_short: { id: 'privacy.unlisted.short', defaultMessage: 'Unlisted' },
|
unlisted_short: {
|
||||||
|
id: 'privacy.unlisted.short',
|
||||||
|
defaultMessage: 'Quiet public',
|
||||||
|
},
|
||||||
private_short: {
|
private_short: {
|
||||||
id: 'privacy.private.short',
|
id: 'privacy.private.short',
|
||||||
defaultMessage: 'Followers only',
|
defaultMessage: 'Followers',
|
||||||
},
|
},
|
||||||
direct_short: {
|
direct_short: {
|
||||||
id: 'privacy.direct.short',
|
id: 'privacy.direct.short',
|
||||||
defaultMessage: 'Mentioned people only',
|
defaultMessage: 'Specific people',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -35,7 +38,7 @@ export const VisibilityIcon: React.FC<{ visibility: Visibility }> = ({
|
|||||||
},
|
},
|
||||||
unlisted: {
|
unlisted: {
|
||||||
icon: 'unlock',
|
icon: 'unlock',
|
||||||
iconComponent: LockOpenIcon,
|
iconComponent: QuietTimeIcon,
|
||||||
text: intl.formatMessage(messages.unlisted_short),
|
text: intl.formatMessage(messages.unlisted_short),
|
||||||
},
|
},
|
||||||
private: {
|
private: {
|
||||||
|
@ -1,14 +1,12 @@
|
|||||||
import { PureComponent } from 'react';
|
|
||||||
|
|
||||||
import { Provider } from 'react-redux';
|
import { Provider } from 'react-redux';
|
||||||
|
|
||||||
import { fetchCustomEmojis } from '../actions/custom_emojis';
|
import { fetchCustomEmojis } from 'mastodon/actions/custom_emojis';
|
||||||
import { hydrateStore } from '../actions/store';
|
import { hydrateStore } from 'mastodon/actions/store';
|
||||||
import Compose from '../features/standalone/compose';
|
import { Router } from 'mastodon/components/router';
|
||||||
import initialState from '../initial_state';
|
import Compose from 'mastodon/features/standalone/compose';
|
||||||
import { IntlProvider } from '../locales';
|
import initialState from 'mastodon/initial_state';
|
||||||
import { store } from '../store';
|
import { IntlProvider } from 'mastodon/locales';
|
||||||
|
import { store } from 'mastodon/store';
|
||||||
|
|
||||||
if (initialState) {
|
if (initialState) {
|
||||||
store.dispatch(hydrateStore(initialState));
|
store.dispatch(hydrateStore(initialState));
|
||||||
@ -16,16 +14,14 @@ if (initialState) {
|
|||||||
|
|
||||||
store.dispatch(fetchCustomEmojis());
|
store.dispatch(fetchCustomEmojis());
|
||||||
|
|
||||||
export default class ComposeContainer extends PureComponent {
|
const ComposeContainer = () => (
|
||||||
|
<IntlProvider>
|
||||||
|
<Provider store={store}>
|
||||||
|
<Router>
|
||||||
|
<Compose />
|
||||||
|
</Router>
|
||||||
|
</Provider>
|
||||||
|
</IntlProvider>
|
||||||
|
);
|
||||||
|
|
||||||
render () {
|
export default ComposeContainer;
|
||||||
return (
|
|
||||||
<IntlProvider>
|
|
||||||
<Provider store={store}>
|
|
||||||
<Compose />
|
|
||||||
</Provider>
|
|
||||||
</IntlProvider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
import PropTypes from 'prop-types';
|
import { useCallback } from 'react';
|
||||||
import { PureComponent } from 'react';
|
|
||||||
|
|
||||||
import { defineMessages, injectIntl } from 'react-intl';
|
import { defineMessages, useIntl } from 'react-intl';
|
||||||
|
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import { useDispatch } from 'react-redux';
|
||||||
|
|
||||||
import MenuIcon from '@/material-icons/400-24px/menu.svg?react';
|
import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react';
|
||||||
|
import { openModal } from 'mastodon/actions/modal';
|
||||||
import DropdownMenuContainer from '../../../containers/dropdown_menu_container';
|
import DropdownMenuContainer from 'mastodon/containers/dropdown_menu_container';
|
||||||
|
import { logOut } from 'mastodon/utils/log_out';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
edit_profile: { id: 'account.edit_profile', defaultMessage: 'Edit profile' },
|
edit_profile: { id: 'account.edit_profile', defaultMessage: 'Edit profile' },
|
||||||
@ -23,51 +23,52 @@ const messages = defineMessages({
|
|||||||
filters: { id: 'navigation_bar.filters', defaultMessage: 'Muted words' },
|
filters: { id: 'navigation_bar.filters', defaultMessage: 'Muted words' },
|
||||||
logout: { id: 'navigation_bar.logout', defaultMessage: 'Logout' },
|
logout: { id: 'navigation_bar.logout', defaultMessage: 'Logout' },
|
||||||
bookmarks: { id: 'navigation_bar.bookmarks', defaultMessage: 'Bookmarks' },
|
bookmarks: { id: 'navigation_bar.bookmarks', defaultMessage: 'Bookmarks' },
|
||||||
|
logoutMessage: { id: 'confirmations.logout.message', defaultMessage: 'Are you sure you want to log out?' },
|
||||||
|
logoutConfirm: { id: 'confirmations.logout.confirm', defaultMessage: 'Log out' },
|
||||||
});
|
});
|
||||||
|
|
||||||
class ActionBar extends PureComponent {
|
export const ActionBar = () => {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const intl = useIntl();
|
||||||
|
|
||||||
static propTypes = {
|
const handleLogoutClick = useCallback(() => {
|
||||||
account: ImmutablePropTypes.record.isRequired,
|
dispatch(openModal({
|
||||||
onLogout: PropTypes.func.isRequired,
|
modalType: 'CONFIRM',
|
||||||
intl: PropTypes.object.isRequired,
|
modalProps: {
|
||||||
};
|
message: intl.formatMessage(messages.logoutMessage),
|
||||||
|
confirm: intl.formatMessage(messages.logoutConfirm),
|
||||||
|
closeWhenConfirm: false,
|
||||||
|
onConfirm: () => logOut(),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
}, [dispatch, intl]);
|
||||||
|
|
||||||
handleLogout = () => {
|
let menu = [];
|
||||||
this.props.onLogout();
|
|
||||||
};
|
|
||||||
|
|
||||||
render () {
|
menu.push({ text: intl.formatMessage(messages.edit_profile), href: '/settings/profile' });
|
||||||
const { intl } = this.props;
|
menu.push({ text: intl.formatMessage(messages.preferences), href: '/settings/preferences' });
|
||||||
|
menu.push({ text: intl.formatMessage(messages.pins), to: '/pinned' });
|
||||||
|
menu.push(null);
|
||||||
|
menu.push({ text: intl.formatMessage(messages.follow_requests), to: '/follow_requests' });
|
||||||
|
menu.push({ text: intl.formatMessage(messages.favourites), to: '/favourites' });
|
||||||
|
menu.push({ text: intl.formatMessage(messages.bookmarks), to: '/bookmarks' });
|
||||||
|
menu.push({ text: intl.formatMessage(messages.lists), to: '/lists' });
|
||||||
|
menu.push({ text: intl.formatMessage(messages.followed_tags), to: '/followed_tags' });
|
||||||
|
menu.push(null);
|
||||||
|
menu.push({ text: intl.formatMessage(messages.mutes), to: '/mutes' });
|
||||||
|
menu.push({ text: intl.formatMessage(messages.blocks), to: '/blocks' });
|
||||||
|
menu.push({ text: intl.formatMessage(messages.domain_blocks), to: '/domain_blocks' });
|
||||||
|
menu.push({ text: intl.formatMessage(messages.filters), href: '/filters' });
|
||||||
|
menu.push(null);
|
||||||
|
menu.push({ text: intl.formatMessage(messages.logout), action: handleLogoutClick });
|
||||||
|
|
||||||
let menu = [];
|
return (
|
||||||
|
<DropdownMenuContainer
|
||||||
menu.push({ text: intl.formatMessage(messages.edit_profile), href: '/settings/profile' });
|
items={menu}
|
||||||
menu.push({ text: intl.formatMessage(messages.preferences), href: '/settings/preferences' });
|
icon='bars'
|
||||||
menu.push({ text: intl.formatMessage(messages.pins), to: '/pinned' });
|
iconComponent={MoreHorizIcon}
|
||||||
menu.push(null);
|
size={24}
|
||||||
menu.push({ text: intl.formatMessage(messages.follow_requests), to: '/follow_requests' });
|
direction='right'
|
||||||
menu.push({ text: intl.formatMessage(messages.favourites), to: '/favourites' });
|
/>
|
||||||
menu.push({ text: intl.formatMessage(messages.bookmarks), to: '/bookmarks' });
|
);
|
||||||
menu.push({ text: intl.formatMessage(messages.lists), to: '/lists' });
|
};
|
||||||
menu.push({ text: intl.formatMessage(messages.followed_tags), to: '/followed_tags' });
|
|
||||||
menu.push(null);
|
|
||||||
menu.push({ text: intl.formatMessage(messages.mutes), to: '/mutes' });
|
|
||||||
menu.push({ text: intl.formatMessage(messages.blocks), to: '/blocks' });
|
|
||||||
menu.push({ text: intl.formatMessage(messages.domain_blocks), to: '/domain_blocks' });
|
|
||||||
menu.push({ text: intl.formatMessage(messages.filters), href: '/filters' });
|
|
||||||
menu.push(null);
|
|
||||||
menu.push({ text: intl.formatMessage(messages.logout), action: this.handleLogout });
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='compose__action-bar'>
|
|
||||||
<div className='compose__action-bar-dropdown'>
|
|
||||||
<DropdownMenuContainer items={menu} icon='bars' iconComponent={MenuIcon} size={24} direction='right' />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
export default injectIntl(ActionBar);
|
|
||||||
|
@ -15,7 +15,7 @@ export default class AutosuggestAccount extends ImmutablePureComponent {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='autosuggest-account' title={account.get('acct')}>
|
<div className='autosuggest-account' title={account.get('acct')}>
|
||||||
<div className='autosuggest-account-icon'><Avatar account={account} size={18} /></div>
|
<Avatar account={account} size={24} />
|
||||||
<DisplayName account={account} />
|
<DisplayName account={account} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -1,26 +1,18 @@
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { PureComponent } from 'react';
|
|
||||||
|
|
||||||
import { length } from 'stringz';
|
import { length } from 'stringz';
|
||||||
|
|
||||||
export default class CharacterCounter extends PureComponent {
|
export const CharacterCounter = ({ text, max }) => {
|
||||||
|
const diff = max - length(text);
|
||||||
|
|
||||||
static propTypes = {
|
if (diff < 0) {
|
||||||
text: PropTypes.string.isRequired,
|
return <span className='character-counter character-counter--over'>{diff}</span>;
|
||||||
max: PropTypes.number.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
checkRemainingText (diff) {
|
|
||||||
if (diff < 0) {
|
|
||||||
return <span className='character-counter character-counter--over'>{diff}</span>;
|
|
||||||
}
|
|
||||||
|
|
||||||
return <span className='character-counter'>{diff}</span>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
return <span className='character-counter'>{diff}</span>;
|
||||||
const diff = this.props.max - length(this.props.text);
|
};
|
||||||
return this.checkRemainingText(diff);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
CharacterCounter.propTypes = {
|
||||||
|
text: PropTypes.string.isRequired,
|
||||||
|
max: PropTypes.number.isRequired,
|
||||||
|
};
|
||||||
|
@ -10,8 +10,6 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
|
|||||||
|
|
||||||
import { length } from 'stringz';
|
import { length } from 'stringz';
|
||||||
|
|
||||||
import LockIcon from '@/material-icons/400-24px/lock.svg?react';
|
|
||||||
import { Icon } from 'mastodon/components/icon';
|
|
||||||
import { WithOptionalRouterPropTypes, withOptionalRouter } from 'mastodon/utils/react_router';
|
import { WithOptionalRouterPropTypes, withOptionalRouter } from 'mastodon/utils/react_router';
|
||||||
|
|
||||||
import AutosuggestInput from '../../../components/autosuggest_input';
|
import AutosuggestInput from '../../../components/autosuggest_input';
|
||||||
@ -20,25 +18,27 @@ import { Button } from '../../../components/button';
|
|||||||
import EmojiPickerDropdown from '../containers/emoji_picker_dropdown_container';
|
import EmojiPickerDropdown from '../containers/emoji_picker_dropdown_container';
|
||||||
import LanguageDropdown from '../containers/language_dropdown_container';
|
import LanguageDropdown from '../containers/language_dropdown_container';
|
||||||
import PollButtonContainer from '../containers/poll_button_container';
|
import PollButtonContainer from '../containers/poll_button_container';
|
||||||
import PollFormContainer from '../containers/poll_form_container';
|
|
||||||
import PrivacyDropdownContainer from '../containers/privacy_dropdown_container';
|
import PrivacyDropdownContainer from '../containers/privacy_dropdown_container';
|
||||||
import ReplyIndicatorContainer from '../containers/reply_indicator_container';
|
|
||||||
import SpoilerButtonContainer from '../containers/spoiler_button_container';
|
import SpoilerButtonContainer from '../containers/spoiler_button_container';
|
||||||
import UploadButtonContainer from '../containers/upload_button_container';
|
import UploadButtonContainer from '../containers/upload_button_container';
|
||||||
import UploadFormContainer from '../containers/upload_form_container';
|
import UploadFormContainer from '../containers/upload_form_container';
|
||||||
import WarningContainer from '../containers/warning_container';
|
import WarningContainer from '../containers/warning_container';
|
||||||
import { countableText } from '../util/counter';
|
import { countableText } from '../util/counter';
|
||||||
|
|
||||||
import CharacterCounter from './character_counter';
|
import { CharacterCounter } from './character_counter';
|
||||||
|
import { EditIndicator } from './edit_indicator';
|
||||||
|
import { NavigationBar } from './navigation_bar';
|
||||||
|
import { PollForm } from "./poll_form";
|
||||||
|
import { ReplyIndicator } from './reply_indicator';
|
||||||
|
|
||||||
const allowedAroundShortCode = '><\u0085\u0020\u00a0\u1680\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028\u2029\u0009\u000a\u000b\u000c\u000d';
|
const allowedAroundShortCode = '><\u0085\u0020\u00a0\u1680\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028\u2029\u0009\u000a\u000b\u000c\u000d';
|
||||||
|
|
||||||
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?' },
|
||||||
spoiler_placeholder: { id: 'compose_form.spoiler_placeholder', defaultMessage: 'Write your warning here' },
|
spoiler_placeholder: { id: 'compose_form.spoiler_placeholder', defaultMessage: 'Content warning (optional)' },
|
||||||
publish: { id: 'compose_form.publish', defaultMessage: 'Publish' },
|
publish: { id: 'compose_form.publish', defaultMessage: 'Post' },
|
||||||
publishLoud: { id: 'compose_form.publish_loud', defaultMessage: '{publish}!' },
|
saveChanges: { id: 'compose_form.save_changes', defaultMessage: 'Update' },
|
||||||
saveChanges: { id: 'compose_form.save_changes', defaultMessage: 'Save changes' },
|
reply: { id: 'compose_form.reply', defaultMessage: 'Reply' },
|
||||||
});
|
});
|
||||||
|
|
||||||
class ComposeForm extends ImmutablePureComponent {
|
class ComposeForm extends ImmutablePureComponent {
|
||||||
@ -65,6 +65,7 @@ class ComposeForm extends ImmutablePureComponent {
|
|||||||
onPaste: PropTypes.func.isRequired,
|
onPaste: PropTypes.func.isRequired,
|
||||||
onPickEmoji: PropTypes.func.isRequired,
|
onPickEmoji: PropTypes.func.isRequired,
|
||||||
autoFocus: PropTypes.bool,
|
autoFocus: PropTypes.bool,
|
||||||
|
withoutNavigation: PropTypes.bool,
|
||||||
anyMedia: PropTypes.bool,
|
anyMedia: PropTypes.bool,
|
||||||
isInReply: PropTypes.bool,
|
isInReply: PropTypes.bool,
|
||||||
singleColumn: PropTypes.bool,
|
singleColumn: PropTypes.bool,
|
||||||
@ -223,93 +224,90 @@ class ComposeForm extends ImmutablePureComponent {
|
|||||||
};
|
};
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { intl, onPaste, autoFocus } = this.props;
|
const { intl, onPaste, autoFocus, withoutNavigation } = this.props;
|
||||||
const { highlighted } = this.state;
|
const { highlighted } = this.state;
|
||||||
const disabled = this.props.isSubmitting;
|
const disabled = this.props.isSubmitting;
|
||||||
|
|
||||||
let publishText = '';
|
|
||||||
|
|
||||||
if (this.props.isEditing) {
|
|
||||||
publishText = intl.formatMessage(messages.saveChanges);
|
|
||||||
} else if (this.props.privacy === 'private' || this.props.privacy === 'direct') {
|
|
||||||
publishText = <><Icon id='lock' icon={LockIcon} /> {intl.formatMessage(messages.publish)}</>;
|
|
||||||
} else {
|
|
||||||
publishText = this.props.privacy !== 'unlisted' ? intl.formatMessage(messages.publishLoud, { publish: intl.formatMessage(messages.publish) }) : intl.formatMessage(messages.publish);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form className='compose-form' onSubmit={this.handleSubmit}>
|
<form className='compose-form' onSubmit={this.handleSubmit}>
|
||||||
|
<ReplyIndicator />
|
||||||
|
{!withoutNavigation && <NavigationBar />}
|
||||||
<WarningContainer />
|
<WarningContainer />
|
||||||
|
|
||||||
<ReplyIndicatorContainer />
|
<div className={classNames('compose-form__highlightable', { active: highlighted })} ref={this.setRef}>
|
||||||
|
<div className='compose-form__scrollable'>
|
||||||
|
<EditIndicator />
|
||||||
|
|
||||||
<div className={`spoiler-input ${this.props.spoiler ? 'spoiler-input--visible' : ''}`} ref={this.setRef} aria-hidden={!this.props.spoiler}>
|
{this.props.spoiler && (
|
||||||
<AutosuggestInput
|
<div className='spoiler-input'>
|
||||||
placeholder={intl.formatMessage(messages.spoiler_placeholder)}
|
<div className='spoiler-input__border' />
|
||||||
value={this.props.spoilerText}
|
|
||||||
onChange={this.handleChangeSpoilerText}
|
|
||||||
onKeyDown={this.handleKeyDown}
|
|
||||||
disabled={!this.props.spoiler}
|
|
||||||
ref={this.setSpoilerText}
|
|
||||||
suggestions={this.props.suggestions}
|
|
||||||
onSuggestionsFetchRequested={this.onSuggestionsFetchRequested}
|
|
||||||
onSuggestionsClearRequested={this.onSuggestionsClearRequested}
|
|
||||||
onSuggestionSelected={this.onSpoilerSuggestionSelected}
|
|
||||||
searchTokens={[':']}
|
|
||||||
id='cw-spoiler-input'
|
|
||||||
className='spoiler-input__input'
|
|
||||||
lang={this.props.lang}
|
|
||||||
spellCheck
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={classNames('compose-form__highlightable', { active: highlighted })}>
|
<AutosuggestInput
|
||||||
<AutosuggestTextarea
|
placeholder={intl.formatMessage(messages.spoiler_placeholder)}
|
||||||
ref={this.textareaRef}
|
value={this.props.spoilerText}
|
||||||
placeholder={intl.formatMessage(messages.placeholder)}
|
disabled={disabled}
|
||||||
disabled={disabled}
|
onChange={this.handleChangeSpoilerText}
|
||||||
value={this.props.text}
|
onKeyDown={this.handleKeyDown}
|
||||||
onChange={this.handleChange}
|
ref={this.setSpoilerText}
|
||||||
suggestions={this.props.suggestions}
|
suggestions={this.props.suggestions}
|
||||||
onFocus={this.handleFocus}
|
onSuggestionsFetchRequested={this.onSuggestionsFetchRequested}
|
||||||
onKeyDown={this.handleKeyDown}
|
onSuggestionsClearRequested={this.onSuggestionsClearRequested}
|
||||||
onSuggestionsFetchRequested={this.onSuggestionsFetchRequested}
|
onSuggestionSelected={this.onSpoilerSuggestionSelected}
|
||||||
onSuggestionsClearRequested={this.onSuggestionsClearRequested}
|
searchTokens={[':']}
|
||||||
onSuggestionSelected={this.onSuggestionSelected}
|
id='cw-spoiler-input'
|
||||||
onPaste={onPaste}
|
className='spoiler-input__input'
|
||||||
autoFocus={autoFocus}
|
lang={this.props.lang}
|
||||||
lang={this.props.lang}
|
spellCheck
|
||||||
>
|
/>
|
||||||
<div className='compose-form__modifiers'>
|
|
||||||
<UploadFormContainer />
|
|
||||||
<PollFormContainer />
|
|
||||||
</div>
|
|
||||||
</AutosuggestTextarea>
|
|
||||||
<EmojiPickerDropdown onPickEmoji={this.handleEmojiPick} />
|
|
||||||
|
|
||||||
<div className='compose-form__buttons-wrapper'>
|
<div className='spoiler-input__border' />
|
||||||
<div className='compose-form__buttons'>
|
</div>
|
||||||
<UploadButtonContainer />
|
)}
|
||||||
<PollButtonContainer />
|
|
||||||
|
<AutosuggestTextarea
|
||||||
|
ref={this.textareaRef}
|
||||||
|
placeholder={intl.formatMessage(messages.placeholder)}
|
||||||
|
disabled={disabled}
|
||||||
|
value={this.props.text}
|
||||||
|
onChange={this.handleChange}
|
||||||
|
suggestions={this.props.suggestions}
|
||||||
|
onFocus={this.handleFocus}
|
||||||
|
onKeyDown={this.handleKeyDown}
|
||||||
|
onSuggestionsFetchRequested={this.onSuggestionsFetchRequested}
|
||||||
|
onSuggestionsClearRequested={this.onSuggestionsClearRequested}
|
||||||
|
onSuggestionSelected={this.onSuggestionSelected}
|
||||||
|
onPaste={onPaste}
|
||||||
|
autoFocus={autoFocus}
|
||||||
|
lang={this.props.lang}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<UploadFormContainer />
|
||||||
|
<PollForm />
|
||||||
|
|
||||||
|
<div className='compose-form__footer'>
|
||||||
|
<div className='compose-form__dropdowns'>
|
||||||
<PrivacyDropdownContainer disabled={this.props.isEditing} />
|
<PrivacyDropdownContainer disabled={this.props.isEditing} />
|
||||||
<SpoilerButtonContainer />
|
|
||||||
<LanguageDropdown />
|
<LanguageDropdown />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='character-counter__wrapper'>
|
<div className='compose-form__actions'>
|
||||||
<CharacterCounter max={500} text={this.getFulltextForCharacterCounting()} />
|
<div className='compose-form__buttons'>
|
||||||
</div>
|
<UploadButtonContainer />
|
||||||
</div>
|
<PollButtonContainer />
|
||||||
</div>
|
<SpoilerButtonContainer />
|
||||||
|
<EmojiPickerDropdown onPickEmoji={this.handleEmojiPick} />
|
||||||
|
<CharacterCounter max={500} text={this.getFulltextForCharacterCounting()} />
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className='compose-form__publish'>
|
<div className='compose-form__submit'>
|
||||||
<div className='compose-form__publish-button-wrapper'>
|
<Button
|
||||||
<Button
|
type='submit'
|
||||||
type='submit'
|
text={intl.formatMessage(this.props.isEditing ? messages.saveChanges : (this.props.isInReply ? messages.reply : messages.publish))}
|
||||||
text={publishText}
|
disabled={!this.canSubmit()}
|
||||||
disabled={!this.canSubmit()}
|
/>
|
||||||
block
|
</div>
|
||||||
/>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
@ -0,0 +1,62 @@
|
|||||||
|
import { useCallback } from 'react';
|
||||||
|
|
||||||
|
import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
|
||||||
|
import BarChart4BarsIcon from 'mastodon/../material-icons/400-24px/bar_chart_4_bars.svg?react';
|
||||||
|
import CloseIcon from 'mastodon/../material-icons/400-24px/close.svg?react';
|
||||||
|
import PhotoLibraryIcon from 'mastodon/../material-icons/400-24px/photo_library.svg?react';
|
||||||
|
import { cancelReplyCompose } from 'mastodon/actions/compose';
|
||||||
|
import { Icon } from 'mastodon/components/icon';
|
||||||
|
import { IconButton } from 'mastodon/components/icon_button';
|
||||||
|
import { RelativeTimestamp } from 'mastodon/components/relative_timestamp';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
cancel: { id: 'reply_indicator.cancel', defaultMessage: 'Cancel' },
|
||||||
|
});
|
||||||
|
|
||||||
|
export const EditIndicator = () => {
|
||||||
|
const intl = useIntl();
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const id = useSelector(state => state.getIn(['compose', 'id']));
|
||||||
|
const status = useSelector(state => state.getIn(['statuses', id]));
|
||||||
|
const account = useSelector(state => state.getIn(['accounts', status?.get('account')]));
|
||||||
|
|
||||||
|
const handleCancelClick = useCallback(() => {
|
||||||
|
dispatch(cancelReplyCompose());
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
if (!status) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const content = { __html: status.get('contentHtml') };
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='edit-indicator'>
|
||||||
|
<div className='edit-indicator__header'>
|
||||||
|
<div className='edit-indicator__display-name'>
|
||||||
|
<Link to={`/@${account.get('acct')}`}>@{account.get('acct')}</Link>
|
||||||
|
·
|
||||||
|
<Link to={`/@${account.get('acct')}/${status.get('id')}`}><RelativeTimestamp timestamp={status.get('created_at')} /></Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='edit-indicator__cancel'>
|
||||||
|
<IconButton title={intl.formatMessage(messages.cancel)} icon='times' iconComponent={CloseIcon} onClick={handleCancelClick} inverted />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='edit-indicator__content translate' dangerouslySetInnerHTML={content} />
|
||||||
|
|
||||||
|
{(status.get('poll') || status.get('media_attachments').size > 0) && (
|
||||||
|
<div className='edit-indicator__attachments'>
|
||||||
|
{status.get('poll') && <><Icon icon={BarChart4BarsIcon} /><FormattedMessage id='reply_indicator.poll' defaultMessage='Poll' /></>}
|
||||||
|
{status.get('media_attachments').size > 0 && <><Icon icon={PhotoLibraryIcon} /><FormattedMessage id='reply_indicator.attachments' defaultMessage='{count, plural, one {# attachment} other {# attachments}}' values={{ count: status.get('media_attachments').size }} /></>}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -10,6 +10,8 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
|
|||||||
import { supportsPassiveEvents } from 'detect-passive-events';
|
import { supportsPassiveEvents } from 'detect-passive-events';
|
||||||
import Overlay from 'react-overlays/Overlay';
|
import Overlay from 'react-overlays/Overlay';
|
||||||
|
|
||||||
|
import MoodIcon from 'mastodon/../material-icons/400-24px/mood.svg?react';
|
||||||
|
import { IconButton } from 'mastodon/components/icon_button';
|
||||||
import { assetHost } from 'mastodon/utils/config';
|
import { assetHost } from 'mastodon/utils/config';
|
||||||
|
|
||||||
import { buildCustomEmojis, categoriesFromEmojis } from '../../emoji/emoji';
|
import { buildCustomEmojis, categoriesFromEmojis } from '../../emoji/emoji';
|
||||||
@ -321,7 +323,6 @@ class EmojiPickerDropdown extends PureComponent {
|
|||||||
onPickEmoji: PropTypes.func.isRequired,
|
onPickEmoji: PropTypes.func.isRequired,
|
||||||
onSkinTone: PropTypes.func.isRequired,
|
onSkinTone: PropTypes.func.isRequired,
|
||||||
skinTone: PropTypes.number.isRequired,
|
skinTone: PropTypes.number.isRequired,
|
||||||
button: PropTypes.node,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
state = {
|
state = {
|
||||||
@ -379,23 +380,24 @@ class EmojiPickerDropdown extends PureComponent {
|
|||||||
};
|
};
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { intl, onPickEmoji, onSkinTone, skinTone, frequentlyUsedEmojis, button } = this.props;
|
const { intl, onPickEmoji, onSkinTone, skinTone, frequentlyUsedEmojis } = this.props;
|
||||||
const title = intl.formatMessage(messages.emoji);
|
const title = intl.formatMessage(messages.emoji);
|
||||||
const { active, loading } = this.state;
|
const { active, loading } = this.state;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='emoji-picker-dropdown' onKeyDown={this.handleKeyDown}>
|
<div className='emoji-picker-dropdown' onKeyDown={this.handleKeyDown} ref={this.setTargetRef}>
|
||||||
<div ref={this.setTargetRef} className='emoji-button' title={title} aria-label={title} aria-expanded={active} role='button' onClick={this.onToggle} onKeyDown={this.onToggle} tabIndex={0}>
|
<IconButton
|
||||||
{button || <img
|
title={title}
|
||||||
className={classNames('emojione', { 'pulse-loading': active && loading })}
|
aria-expanded={active}
|
||||||
alt='🙂'
|
active={active}
|
||||||
src={`${assetHost}/emoji/1f642.svg`}
|
iconComponent={MoodIcon}
|
||||||
/>}
|
onClick={this.onToggle}
|
||||||
</div>
|
inverted
|
||||||
|
/>
|
||||||
|
|
||||||
<Overlay show={active} placement={'bottom'} target={this.findTarget} popperConfig={{ strategy: 'fixed' }}>
|
<Overlay show={active} placement={'bottom'} target={this.findTarget} popperConfig={{ strategy: 'fixed' }}>
|
||||||
{({ props, placement })=> (
|
{({ props, placement })=> (
|
||||||
<div {...props} style={{ ...props.style, width: 299 }}>
|
<div {...props} style={{ ...props.style }}>
|
||||||
<div className={`dropdown-animation ${placement}`}>
|
<div className={`dropdown-animation ${placement}`}>
|
||||||
<EmojiPickerMenu
|
<EmojiPickerMenu
|
||||||
custom_emojis={this.props.custom_emojis}
|
custom_emojis={this.props.custom_emojis}
|
||||||
|
@ -9,10 +9,11 @@ import { supportsPassiveEvents } from 'detect-passive-events';
|
|||||||
import fuzzysort from 'fuzzysort';
|
import fuzzysort from 'fuzzysort';
|
||||||
import Overlay from 'react-overlays/Overlay';
|
import Overlay from 'react-overlays/Overlay';
|
||||||
|
|
||||||
|
import CancelIcon from 'mastodon/../material-icons/400-24px/cancel-fill.svg?react';
|
||||||
|
import SearchIcon from 'mastodon/../material-icons/400-24px/search.svg?react';
|
||||||
|
import TranslateIcon from 'mastodon/../material-icons/400-24px/translate.svg?react';
|
||||||
|
import { Icon } from 'mastodon/components/icon';
|
||||||
import { languages as preloadedLanguages } from 'mastodon/initial_state';
|
import { languages as preloadedLanguages } from 'mastodon/initial_state';
|
||||||
import { loupeIcon, deleteIcon } from 'mastodon/utils/icons';
|
|
||||||
|
|
||||||
import TextIconButton from './text_icon_button';
|
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
changeLanguage: { id: 'compose.language.change', defaultMessage: 'Change language' },
|
changeLanguage: { id: 'compose.language.change', defaultMessage: 'Change language' },
|
||||||
@ -231,7 +232,7 @@ class LanguageDropdownMenu extends PureComponent {
|
|||||||
<div ref={this.setRef}>
|
<div ref={this.setRef}>
|
||||||
<div className='emoji-mart-search'>
|
<div className='emoji-mart-search'>
|
||||||
<input type='search' value={searchValue} onChange={this.handleSearchChange} onKeyDown={this.handleSearchKeyDown} placeholder={intl.formatMessage(messages.search)} />
|
<input type='search' value={searchValue} onChange={this.handleSearchChange} onKeyDown={this.handleSearchKeyDown} placeholder={intl.formatMessage(messages.search)} />
|
||||||
<button type='button' className='emoji-mart-search-icon' disabled={!isSearching} aria-label={intl.formatMessage(messages.clear)} onClick={this.handleClear}>{!isSearching ? loupeIcon : deleteIcon}</button>
|
<button type='button' className='emoji-mart-search-icon' disabled={!isSearching} aria-label={intl.formatMessage(messages.clear)} onClick={this.handleClear}><Icon icon={!isSearching ? SearchIcon : CancelIcon} /></button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='language-dropdown__dropdown__results emoji-mart-scroll' role='listbox' ref={this.setListRef}>
|
<div className='language-dropdown__dropdown__results emoji-mart-scroll' role='listbox' ref={this.setListRef}>
|
||||||
@ -297,20 +298,24 @@ class LanguageDropdown extends PureComponent {
|
|||||||
render () {
|
render () {
|
||||||
const { value, intl, frequentlyUsedLanguages } = this.props;
|
const { value, intl, frequentlyUsedLanguages } = this.props;
|
||||||
const { open, placement } = this.state;
|
const { open, placement } = this.state;
|
||||||
|
const current = preloadedLanguages.find(lang => lang[0] === value) ?? [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classNames('privacy-dropdown', placement, { active: open })}>
|
<div ref={this.setTargetRef} onKeyDown={this.handleKeyDown}>
|
||||||
<div className='privacy-dropdown__value' ref={this.setTargetRef} >
|
<button
|
||||||
<TextIconButton
|
type='button'
|
||||||
className='privacy-dropdown__value-icon'
|
title={intl.formatMessage(messages.changeLanguage)}
|
||||||
label={value && value.toUpperCase()}
|
aria-expanded={open}
|
||||||
title={intl.formatMessage(messages.changeLanguage)}
|
onClick={this.handleToggle}
|
||||||
active={open}
|
onMouseDown={this.handleMouseDown}
|
||||||
onClick={this.handleToggle}
|
onKeyDown={this.handleButtonKeyDown}
|
||||||
/>
|
className={classNames('dropdown-button', { active: open })}
|
||||||
</div>
|
>
|
||||||
|
<Icon icon={TranslateIcon} />
|
||||||
|
<span className='dropdown-button__label'>{current[2] ?? value}</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
<Overlay show={open} placement={'bottom'} flip target={this.findTarget} popperConfig={{ strategy: 'fixed', onFirstUpdate: this.handleOverlayEnter }}>
|
<Overlay show={open} offset={[5, 5]} placement={placement} flip target={this.findTarget} popperConfig={{ strategy: 'fixed', onFirstUpdate: this.handleOverlayEnter }}>
|
||||||
{({ props, placement }) => (
|
{({ props, placement }) => (
|
||||||
<div {...props}>
|
<div {...props}>
|
||||||
<div className={`dropdown-animation language-dropdown__dropdown ${placement}`} >
|
<div className={`dropdown-animation language-dropdown__dropdown ${placement}`} >
|
||||||
|
@ -1,50 +1,36 @@
|
|||||||
import PropTypes from 'prop-types';
|
import { useCallback } from 'react';
|
||||||
|
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { useIntl, defineMessages } from 'react-intl';
|
||||||
|
|
||||||
import { Link } from 'react-router-dom';
|
import { useSelector, useDispatch } from 'react-redux';
|
||||||
|
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import CloseIcon from 'mastodon/../material-icons/400-24px/close.svg?react';
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
import { cancelReplyCompose } from 'mastodon/actions/compose';
|
||||||
|
import Account from 'mastodon/components/account';
|
||||||
|
import { IconButton } from 'mastodon/components/icon_button';
|
||||||
|
import { me } from 'mastodon/initial_state';
|
||||||
|
|
||||||
import { Avatar } from '../../../components/avatar';
|
import { ActionBar } from './action_bar';
|
||||||
|
|
||||||
import ActionBar from './action_bar';
|
|
||||||
|
|
||||||
export default class NavigationBar extends ImmutablePureComponent {
|
const messages = defineMessages({
|
||||||
|
cancel: { id: 'reply_indicator.cancel', defaultMessage: 'Cancel' },
|
||||||
|
});
|
||||||
|
|
||||||
static propTypes = {
|
export const NavigationBar = () => {
|
||||||
account: ImmutablePropTypes.record.isRequired,
|
const dispatch = useDispatch();
|
||||||
onLogout: PropTypes.func.isRequired,
|
const intl = useIntl();
|
||||||
onClose: PropTypes.func,
|
const account = useSelector(state => state.getIn(['accounts', me]));
|
||||||
};
|
const isReplying = useSelector(state => !!state.getIn(['compose', 'in_reply_to']));
|
||||||
|
|
||||||
render () {
|
const handleCancelClick = useCallback(() => {
|
||||||
const username = this.props.account.get('acct');
|
dispatch(cancelReplyCompose());
|
||||||
return (
|
}, [dispatch]);
|
||||||
<div className='navigation-bar'>
|
|
||||||
<Link to={`/@${username}`}>
|
|
||||||
<span style={{ display: 'none' }}>{username}</span>
|
|
||||||
<Avatar account={this.props.account} size={46} />
|
|
||||||
</Link>
|
|
||||||
|
|
||||||
<div className='navigation-bar__profile'>
|
return (
|
||||||
<span>
|
<div className='navigation-bar'>
|
||||||
<Link to={`/@${username}`}>
|
<Account account={account} minimal />
|
||||||
<strong className='navigation-bar__profile-account'>@{username}</strong>
|
{isReplying ? <IconButton title={intl.formatMessage(messages.cancel)} iconComponent={CloseIcon} onClick={handleCancelClick} /> : <ActionBar />}
|
||||||
</Link>
|
</div>
|
||||||
</span>
|
);
|
||||||
|
};
|
||||||
<span>
|
|
||||||
<a href='/settings/profile' className='navigation-bar__profile-edit'><FormattedMessage id='navigation_bar.edit_profile' defaultMessage='Edit profile' /></a>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='navigation-bar__actions'>
|
|
||||||
<ActionBar account={this.props.account} onLogout={this.props.onLogout} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
@ -3,11 +3,10 @@ import { PureComponent } from 'react';
|
|||||||
|
|
||||||
import { defineMessages, injectIntl } from 'react-intl';
|
import { defineMessages, injectIntl } from 'react-intl';
|
||||||
|
|
||||||
import InsertChartIcon from '@/material-icons/400-24px/insert_chart.svg?react';
|
import BarChart4BarsIcon from '@/material-icons/400-24px/bar_chart_4_bars.svg?react';
|
||||||
|
|
||||||
import { IconButton } from '../../../components/icon_button';
|
import { IconButton } from '../../../components/icon_button';
|
||||||
|
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
add_poll: { id: 'poll_button.add_poll', defaultMessage: 'Add a poll' },
|
add_poll: { id: 'poll_button.add_poll', defaultMessage: 'Add a poll' },
|
||||||
remove_poll: { id: 'poll_button.remove_poll', defaultMessage: 'Remove poll' },
|
remove_poll: { id: 'poll_button.remove_poll', defaultMessage: 'Remove poll' },
|
||||||
@ -22,7 +21,6 @@ class PollButton extends PureComponent {
|
|||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
disabled: PropTypes.bool,
|
disabled: PropTypes.bool,
|
||||||
unavailable: PropTypes.bool,
|
|
||||||
active: PropTypes.bool,
|
active: PropTypes.bool,
|
||||||
onClick: PropTypes.func.isRequired,
|
onClick: PropTypes.func.isRequired,
|
||||||
intl: PropTypes.object.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
@ -33,17 +31,13 @@ class PollButton extends PureComponent {
|
|||||||
};
|
};
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { intl, active, unavailable, disabled } = this.props;
|
const { intl, active, disabled } = this.props;
|
||||||
|
|
||||||
if (unavailable) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='compose-form__poll-button'>
|
<div className='compose-form__poll-button'>
|
||||||
<IconButton
|
<IconButton
|
||||||
icon='tasks'
|
icon='tasks'
|
||||||
iconComponent={InsertChartIcon}
|
iconComponent={BarChart4BarsIcon}
|
||||||
title={intl.formatMessage(active ? messages.remove_poll : messages.add_poll)}
|
title={intl.formatMessage(active ? messages.remove_poll : messages.add_poll)}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
onClick={this.handleClick}
|
onClick={this.handleClick}
|
||||||
|
@ -1,189 +1,162 @@
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { PureComponent } from 'react';
|
import { useCallback } from 'react';
|
||||||
|
|
||||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
import { defineMessages, useIntl } from 'react-intl';
|
||||||
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
|
||||||
|
|
||||||
import AddIcon from '@/material-icons/400-24px/add.svg?react';
|
import {
|
||||||
import CloseIcon from '@/material-icons/400-24px/close.svg?react';
|
changePollSettings,
|
||||||
|
changePollOption,
|
||||||
|
clearComposeSuggestions,
|
||||||
|
fetchComposeSuggestions,
|
||||||
|
selectComposeSuggestion,
|
||||||
|
} from 'mastodon/actions/compose';
|
||||||
import AutosuggestInput from 'mastodon/components/autosuggest_input';
|
import AutosuggestInput from 'mastodon/components/autosuggest_input';
|
||||||
import { Icon } from 'mastodon/components/icon';
|
|
||||||
import { IconButton } from 'mastodon/components/icon_button';
|
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
option_placeholder: { id: 'compose_form.poll.option_placeholder', defaultMessage: 'Choice {number}' },
|
option_placeholder: { id: 'compose_form.poll.option_placeholder', defaultMessage: 'Option {number}' },
|
||||||
add_option: { id: 'compose_form.poll.add_option', defaultMessage: 'Add a choice' },
|
add_option: { id: 'compose_form.poll.add_option', defaultMessage: 'Add option' },
|
||||||
remove_option: { id: 'compose_form.poll.remove_option', defaultMessage: 'Remove this choice' },
|
remove_option: { id: 'compose_form.poll.remove_option', defaultMessage: 'Remove this option' },
|
||||||
poll_duration: { id: 'compose_form.poll.duration', defaultMessage: 'Poll duration' },
|
duration: { id: 'compose_form.poll.duration', defaultMessage: 'Poll length' },
|
||||||
|
type: { id: 'compose_form.poll.type', defaultMessage: 'Style' },
|
||||||
switchToMultiple: { id: 'compose_form.poll.switch_to_multiple', defaultMessage: 'Change poll to allow multiple choices' },
|
switchToMultiple: { id: 'compose_form.poll.switch_to_multiple', defaultMessage: 'Change poll to allow multiple choices' },
|
||||||
switchToSingle: { id: 'compose_form.poll.switch_to_single', defaultMessage: 'Change poll to allow for a single choice' },
|
switchToSingle: { id: 'compose_form.poll.switch_to_single', defaultMessage: 'Change poll to allow for a single choice' },
|
||||||
minutes: { id: 'intervals.full.minutes', defaultMessage: '{number, plural, one {# minute} other {# minutes}}' },
|
minutes: { id: 'intervals.full.minutes', defaultMessage: '{number, plural, one {# minute} other {# minutes}}' },
|
||||||
hours: { id: 'intervals.full.hours', defaultMessage: '{number, plural, one {# hour} other {# hours}}' },
|
hours: { id: 'intervals.full.hours', defaultMessage: '{number, plural, one {# hour} other {# hours}}' },
|
||||||
days: { id: 'intervals.full.days', defaultMessage: '{number, plural, one {# day} other {# days}}' },
|
days: { id: 'intervals.full.days', defaultMessage: '{number, plural, one {# day} other {# days}}' },
|
||||||
|
singleChoice: { id: 'compose_form.poll.single', defaultMessage: 'Pick one' },
|
||||||
|
multipleChoice: { id: 'compose_form.poll.multiple', defaultMessage: 'Multiple choice' },
|
||||||
});
|
});
|
||||||
|
|
||||||
class OptionIntl extends PureComponent {
|
const Select = ({ label, options, value, onChange }) => {
|
||||||
|
return (
|
||||||
|
<label className='compose-form__poll__select'>
|
||||||
|
<span className='compose-form__poll__select__label'>{label}</span>
|
||||||
|
|
||||||
static propTypes = {
|
<select className='compose-form__poll__select__value' value={value} onChange={onChange}>
|
||||||
title: PropTypes.string.isRequired,
|
{options.map((option, i) => (
|
||||||
lang: PropTypes.string,
|
<option key={i} value={option.value}>{option.label}</option>
|
||||||
index: PropTypes.number.isRequired,
|
))}
|
||||||
isPollMultiple: PropTypes.bool,
|
</select>
|
||||||
autoFocus: PropTypes.bool,
|
</label>
|
||||||
onChange: PropTypes.func.isRequired,
|
);
|
||||||
onRemove: PropTypes.func.isRequired,
|
};
|
||||||
onToggleMultiple: PropTypes.func.isRequired,
|
|
||||||
suggestions: ImmutablePropTypes.list,
|
|
||||||
onClearSuggestions: PropTypes.func.isRequired,
|
|
||||||
onFetchSuggestions: PropTypes.func.isRequired,
|
|
||||||
onSuggestionSelected: PropTypes.func.isRequired,
|
|
||||||
intl: PropTypes.object.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
handleOptionTitleChange = e => {
|
Select.propTypes = {
|
||||||
this.props.onChange(this.props.index, e.target.value);
|
label: PropTypes.node,
|
||||||
};
|
value: PropTypes.any,
|
||||||
|
onChange: PropTypes.func,
|
||||||
|
options: PropTypes.arrayOf(PropTypes.shape({
|
||||||
|
label: PropTypes.node,
|
||||||
|
value: PropTypes.any,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
|
||||||
handleOptionRemove = () => {
|
const Option = ({ multipleChoice, index, title, autoFocus }) => {
|
||||||
this.props.onRemove(this.props.index);
|
const intl = useIntl();
|
||||||
};
|
const dispatch = useDispatch();
|
||||||
|
const suggestions = useSelector(state => state.getIn(['compose', 'suggestions']));
|
||||||
|
const lang = useSelector(state => state.getIn(['compose', 'language']));
|
||||||
|
|
||||||
|
const handleChange = useCallback(({ target: { value } }) => {
|
||||||
|
dispatch(changePollOption(index, value));
|
||||||
|
}, [dispatch, index]);
|
||||||
|
|
||||||
handleToggleMultiple = e => {
|
const handleSuggestionsFetchRequested = useCallback(token => {
|
||||||
this.props.onToggleMultiple();
|
dispatch(fetchComposeSuggestions(token));
|
||||||
e.preventDefault();
|
}, [dispatch]);
|
||||||
e.stopPropagation();
|
|
||||||
};
|
|
||||||
|
|
||||||
handleCheckboxKeypress = e => {
|
const handleSuggestionsClearRequested = useCallback(() => {
|
||||||
if (e.key === 'Enter' || e.key === ' ') {
|
dispatch(clearComposeSuggestions());
|
||||||
this.handleToggleMultiple(e);
|
}, [dispatch]);
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
onSuggestionsClearRequested = () => {
|
const handleSuggestionSelected = useCallback((tokenStart, token, value) => {
|
||||||
this.props.onClearSuggestions();
|
dispatch(selectComposeSuggestion(tokenStart, token, value, ['poll', 'options', index]));
|
||||||
};
|
}, [dispatch, index]);
|
||||||
|
|
||||||
onSuggestionsFetchRequested = (token) => {
|
return (
|
||||||
this.props.onFetchSuggestions(token);
|
<label className={classNames('poll__option editable', { empty: index > 1 && title.length === 0 })}>
|
||||||
};
|
<span className={classNames('poll__input', { checkbox: multipleChoice })} />
|
||||||
|
|
||||||
onSuggestionSelected = (tokenStart, token, value) => {
|
<AutosuggestInput
|
||||||
this.props.onSuggestionSelected(tokenStart, token, value, ['poll', 'options', this.props.index]);
|
placeholder={intl.formatMessage(messages.option_placeholder, { number: index + 1 })}
|
||||||
};
|
maxLength={50}
|
||||||
|
value={title}
|
||||||
|
lang={lang}
|
||||||
|
spellCheck
|
||||||
|
onChange={handleChange}
|
||||||
|
suggestions={suggestions}
|
||||||
|
onSuggestionsFetchRequested={handleSuggestionsFetchRequested}
|
||||||
|
onSuggestionsClearRequested={handleSuggestionsClearRequested}
|
||||||
|
onSuggestionSelected={handleSuggestionSelected}
|
||||||
|
searchTokens={[':']}
|
||||||
|
autoFocus={autoFocus}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
render () {
|
Option.propTypes = {
|
||||||
const { isPollMultiple, title, lang, index, autoFocus, intl } = this.props;
|
title: PropTypes.string.isRequired,
|
||||||
|
index: PropTypes.number.isRequired,
|
||||||
|
multipleChoice: PropTypes.bool,
|
||||||
|
autoFocus: PropTypes.bool,
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
export const PollForm = () => {
|
||||||
<li>
|
const intl = useIntl();
|
||||||
<label className='poll__option editable'>
|
const dispatch = useDispatch();
|
||||||
<span
|
const poll = useSelector(state => state.getIn(['compose', 'poll']));
|
||||||
className={classNames('poll__input', { checkbox: isPollMultiple })}
|
const options = poll?.get('options');
|
||||||
onClick={this.handleToggleMultiple}
|
const expiresIn = poll?.get('expires_in');
|
||||||
onKeyPress={this.handleCheckboxKeypress}
|
const isMultiple = poll?.get('multiple');
|
||||||
role='button'
|
|
||||||
tabIndex={0}
|
|
||||||
title={intl.formatMessage(isPollMultiple ? messages.switchToSingle : messages.switchToMultiple)}
|
|
||||||
aria-label={intl.formatMessage(isPollMultiple ? messages.switchToSingle : messages.switchToMultiple)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<AutosuggestInput
|
const handleDurationChange = useCallback(({ target: { value } }) => {
|
||||||
placeholder={intl.formatMessage(messages.option_placeholder, { number: index + 1 })}
|
dispatch(changePollSettings(value, isMultiple));
|
||||||
maxLength={50}
|
}, [dispatch, isMultiple]);
|
||||||
value={title}
|
|
||||||
lang={lang}
|
|
||||||
spellCheck
|
|
||||||
onChange={this.handleOptionTitleChange}
|
|
||||||
suggestions={this.props.suggestions}
|
|
||||||
onSuggestionsFetchRequested={this.onSuggestionsFetchRequested}
|
|
||||||
onSuggestionsClearRequested={this.onSuggestionsClearRequested}
|
|
||||||
onSuggestionSelected={this.onSuggestionSelected}
|
|
||||||
searchTokens={[':']}
|
|
||||||
autoFocus={autoFocus}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<div className='poll__cancel'>
|
const handleTypeChange = useCallback(({ target: { value } }) => {
|
||||||
<IconButton disabled={index <= 1} title={intl.formatMessage(messages.remove_option)} icon='times' iconComponent={CloseIcon} onClick={this.handleOptionRemove} />
|
dispatch(changePollSettings(expiresIn, value === 'true'));
|
||||||
</div>
|
}, [dispatch, expiresIn]);
|
||||||
</li>
|
|
||||||
);
|
if (poll === null) {
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
return (
|
||||||
|
<div className='compose-form__poll'>
|
||||||
|
{options.map((title, i) => (
|
||||||
|
<Option
|
||||||
|
title={title}
|
||||||
|
key={i}
|
||||||
|
index={i}
|
||||||
|
multipleChoice={isMultiple}
|
||||||
|
autoFocus={i === 0}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
|
||||||
const Option = injectIntl(OptionIntl);
|
<div className='compose-form__poll__footer'>
|
||||||
|
<Select label={intl.formatMessage(messages.duration)} options={[
|
||||||
|
{ value: 300, label: intl.formatMessage(messages.minutes, { number: 5 })},
|
||||||
|
{ value: 1800, label: intl.formatMessage(messages.minutes, { number: 30 })},
|
||||||
|
{ value: 3600, label: intl.formatMessage(messages.hours, { number: 1 })},
|
||||||
|
{ value: 21600, label: intl.formatMessage(messages.hours, { number: 6 })},
|
||||||
|
{ value: 43200, label: intl.formatMessage(messages.hours, { number: 12 })},
|
||||||
|
{ value: 86400, label: intl.formatMessage(messages.days, { number: 1 })},
|
||||||
|
{ value: 259200, label: intl.formatMessage(messages.days, { number: 3 })},
|
||||||
|
{ value: 604800, label: intl.formatMessage(messages.days, { number: 7 })},
|
||||||
|
]} value={expiresIn} onChange={handleDurationChange} />
|
||||||
|
|
||||||
class PollForm extends ImmutablePureComponent {
|
<div className='compose-form__poll__footer__sep' />
|
||||||
|
|
||||||
static propTypes = {
|
<Select label={intl.formatMessage(messages.type)} options={[
|
||||||
options: ImmutablePropTypes.list,
|
{ value: false, label: intl.formatMessage(messages.singleChoice) },
|
||||||
lang: PropTypes.string,
|
{ value: true, label: intl.formatMessage(messages.multipleChoice) },
|
||||||
expiresIn: PropTypes.number,
|
]} value={isMultiple} onChange={handleTypeChange} />
|
||||||
isMultiple: PropTypes.bool,
|
|
||||||
onChangeOption: PropTypes.func.isRequired,
|
|
||||||
onAddOption: PropTypes.func.isRequired,
|
|
||||||
onRemoveOption: PropTypes.func.isRequired,
|
|
||||||
onChangeSettings: PropTypes.func.isRequired,
|
|
||||||
suggestions: ImmutablePropTypes.list,
|
|
||||||
onClearSuggestions: PropTypes.func.isRequired,
|
|
||||||
onFetchSuggestions: PropTypes.func.isRequired,
|
|
||||||
onSuggestionSelected: PropTypes.func.isRequired,
|
|
||||||
intl: PropTypes.object.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
handleAddOption = () => {
|
|
||||||
this.props.onAddOption('');
|
|
||||||
};
|
|
||||||
|
|
||||||
handleSelectDuration = e => {
|
|
||||||
this.props.onChangeSettings(e.target.value, this.props.isMultiple);
|
|
||||||
};
|
|
||||||
|
|
||||||
handleToggleMultiple = () => {
|
|
||||||
this.props.onChangeSettings(this.props.expiresIn, !this.props.isMultiple);
|
|
||||||
};
|
|
||||||
|
|
||||||
render () {
|
|
||||||
const { options, lang, expiresIn, isMultiple, onChangeOption, onRemoveOption, intl, ...other } = this.props;
|
|
||||||
|
|
||||||
if (!options) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const autoFocusIndex = options.indexOf('');
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='compose-form__poll-wrapper'>
|
|
||||||
<ul>
|
|
||||||
{options.map((title, i) => <Option title={title} lang={lang} key={i} index={i} onChange={onChangeOption} onRemove={onRemoveOption} isPollMultiple={isMultiple} onToggleMultiple={this.handleToggleMultiple} autoFocus={i === autoFocusIndex} {...other} />)}
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<div className='poll__footer'>
|
|
||||||
<button type='button' disabled={options.size >= 4} className='button button-secondary' onClick={this.handleAddOption}><Icon id='plus' icon={AddIcon} /> <FormattedMessage {...messages.add_option} /></button>
|
|
||||||
|
|
||||||
{/* eslint-disable-next-line jsx-a11y/no-onchange */}
|
|
||||||
<select value={expiresIn} onChange={this.handleSelectDuration}>
|
|
||||||
<option value={300}>{intl.formatMessage(messages.minutes, { number: 5 })}</option>
|
|
||||||
<option value={1800}>{intl.formatMessage(messages.minutes, { number: 30 })}</option>
|
|
||||||
<option value={3600}>{intl.formatMessage(messages.hours, { number: 1 })}</option>
|
|
||||||
<option value={21600}>{intl.formatMessage(messages.hours, { number: 6 })}</option>
|
|
||||||
<option value={43200}>{intl.formatMessage(messages.hours, { number: 12 })}</option>
|
|
||||||
<option value={86400}>{intl.formatMessage(messages.days, { number: 1 })}</option>
|
|
||||||
<option value={259200}>{intl.formatMessage(messages.days, { number: 3 })}</option>
|
|
||||||
<option value={604800}>{intl.formatMessage(messages.days, { number: 7 })}</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
</div>
|
||||||
}
|
);
|
||||||
|
};
|
||||||
}
|
|
||||||
|
|
||||||
export default injectIntl(PollForm);
|
|
||||||
|
@ -5,28 +5,27 @@ import { injectIntl, defineMessages } from 'react-intl';
|
|||||||
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
|
||||||
import { supportsPassiveEvents } from 'detect-passive-events';
|
import { supportsPassiveEvents } from 'detect-passive-events';
|
||||||
import Overlay from 'react-overlays/Overlay';
|
import Overlay from 'react-overlays/Overlay';
|
||||||
|
|
||||||
import AlternateEmailIcon from '@/material-icons/400-24px/alternate_email.svg?react';
|
import AlternateEmailIcon from '@/material-icons/400-24px/alternate_email.svg?react';
|
||||||
|
import InfoIcon from '@/material-icons/400-24px/info.svg?react';
|
||||||
import LockIcon from '@/material-icons/400-24px/lock.svg?react';
|
import LockIcon from '@/material-icons/400-24px/lock.svg?react';
|
||||||
import LockOpenIcon from '@/material-icons/400-24px/lock_open.svg?react';
|
|
||||||
import PublicIcon from '@/material-icons/400-24px/public.svg?react';
|
import PublicIcon from '@/material-icons/400-24px/public.svg?react';
|
||||||
|
import QuietTimeIcon from '@/material-icons/400-24px/quiet_time.svg?react';
|
||||||
import { Icon } from 'mastodon/components/icon';
|
import { Icon } from 'mastodon/components/icon';
|
||||||
|
|
||||||
import { IconButton } from '../../../components/icon_button';
|
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
public_short: { id: 'privacy.public.short', defaultMessage: 'Public' },
|
public_short: { id: 'privacy.public.short', defaultMessage: 'Public' },
|
||||||
public_long: { id: 'privacy.public.long', defaultMessage: 'Visible for all' },
|
public_long: { id: 'privacy.public.long', defaultMessage: 'Anyone on and off Mastodon' },
|
||||||
unlisted_short: { id: 'privacy.unlisted.short', defaultMessage: 'Unlisted' },
|
unlisted_short: { id: 'privacy.unlisted.short', defaultMessage: 'Quiet public' },
|
||||||
unlisted_long: { id: 'privacy.unlisted.long', defaultMessage: 'Visible for all, but opted-out of discovery features' },
|
unlisted_long: { id: 'privacy.unlisted.long', defaultMessage: 'Fewer algorithmic fanfares' },
|
||||||
private_short: { id: 'privacy.private.short', defaultMessage: 'Followers only' },
|
private_short: { id: 'privacy.private.short', defaultMessage: 'Followers' },
|
||||||
private_long: { id: 'privacy.private.long', defaultMessage: 'Visible for followers only' },
|
private_long: { id: 'privacy.private.long', defaultMessage: 'Only your followers' },
|
||||||
direct_short: { id: 'privacy.direct.short', defaultMessage: 'Mentioned people only' },
|
direct_short: { id: 'privacy.direct.short', defaultMessage: 'Specific people' },
|
||||||
direct_long: { id: 'privacy.direct.long', defaultMessage: 'Visible for mentioned users only' },
|
direct_long: { id: 'privacy.direct.long', defaultMessage: 'Everyone mentioned in the post' },
|
||||||
change_privacy: { id: 'privacy.change', defaultMessage: 'Adjust status privacy' },
|
change_privacy: { id: 'privacy.change', defaultMessage: 'Change post privacy' },
|
||||||
|
unlisted_extra: { id: 'privacy.unlisted.additional', defaultMessage: 'This behaves exactly like public, except the post will not appear in live feeds or hashtags, explore, or Mastodon search, even if you are opted-in account-wide.' },
|
||||||
});
|
});
|
||||||
|
|
||||||
const listenerOptions = supportsPassiveEvents ? { passive: true, capture: true } : true;
|
const listenerOptions = supportsPassiveEvents ? { passive: true, capture: true } : true;
|
||||||
@ -135,6 +134,12 @@ class PrivacyDropdownMenu extends PureComponent {
|
|||||||
<strong>{item.text}</strong>
|
<strong>{item.text}</strong>
|
||||||
{item.meta}
|
{item.meta}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{item.extra && (
|
||||||
|
<div className='privacy-dropdown__option__additional' title={item.extra}>
|
||||||
|
<Icon id='info-circle' icon={InfoIcon} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@ -163,30 +168,11 @@ class PrivacyDropdown extends PureComponent {
|
|||||||
};
|
};
|
||||||
|
|
||||||
handleToggle = () => {
|
handleToggle = () => {
|
||||||
if (this.props.isUserTouching && this.props.isUserTouching()) {
|
if (this.state.open && this.activeElement) {
|
||||||
if (this.state.open) {
|
this.activeElement.focus({ preventScroll: true });
|
||||||
this.props.onModalClose();
|
|
||||||
} else {
|
|
||||||
this.props.onModalOpen({
|
|
||||||
actions: this.options.map(option => ({ ...option, active: option.value === this.props.value })),
|
|
||||||
onClick: this.handleModalActionClick,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (this.state.open && this.activeElement) {
|
|
||||||
this.activeElement.focus({ preventScroll: true });
|
|
||||||
}
|
|
||||||
this.setState({ open: !this.state.open });
|
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
handleModalActionClick = (e) => {
|
this.setState({ open: !this.state.open });
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
const { value } = this.options[e.currentTarget.getAttribute('data-index')];
|
|
||||||
|
|
||||||
this.props.onModalClose();
|
|
||||||
this.props.onChange(value);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
handleKeyDown = e => {
|
handleKeyDown = e => {
|
||||||
@ -228,7 +214,7 @@ class PrivacyDropdown extends PureComponent {
|
|||||||
|
|
||||||
this.options = [
|
this.options = [
|
||||||
{ icon: 'globe', iconComponent: PublicIcon, value: 'public', text: formatMessage(messages.public_short), meta: formatMessage(messages.public_long) },
|
{ icon: 'globe', iconComponent: PublicIcon, value: 'public', text: formatMessage(messages.public_short), meta: formatMessage(messages.public_long) },
|
||||||
{ icon: 'unlock', iconComponent: LockOpenIcon, value: 'unlisted', text: formatMessage(messages.unlisted_short), meta: formatMessage(messages.unlisted_long) },
|
{ icon: 'unlock', iconComponent: QuietTimeIcon, value: 'unlisted', text: formatMessage(messages.unlisted_short), meta: formatMessage(messages.unlisted_long), extra: formatMessage(messages.unlisted_extra) },
|
||||||
{ icon: 'lock', iconComponent: LockIcon, value: 'private', text: formatMessage(messages.private_short), meta: formatMessage(messages.private_long) },
|
{ icon: 'lock', iconComponent: LockIcon, value: 'private', text: formatMessage(messages.private_short), meta: formatMessage(messages.private_long) },
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -259,23 +245,21 @@ class PrivacyDropdown extends PureComponent {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={this.setTargetRef} onKeyDown={this.handleKeyDown}>
|
<div ref={this.setTargetRef} onKeyDown={this.handleKeyDown}>
|
||||||
<IconButton
|
<button
|
||||||
className='privacy-dropdown__value-icon'
|
type='button'
|
||||||
icon={valueOption.icon}
|
|
||||||
iconComponent={valueOption.iconComponent}
|
|
||||||
title={intl.formatMessage(messages.change_privacy)}
|
title={intl.formatMessage(messages.change_privacy)}
|
||||||
size={18}
|
aria-expanded={open}
|
||||||
expanded={open}
|
|
||||||
active={open}
|
|
||||||
inverted
|
|
||||||
onClick={this.handleToggle}
|
onClick={this.handleToggle}
|
||||||
onMouseDown={this.handleMouseDown}
|
onMouseDown={this.handleMouseDown}
|
||||||
onKeyDown={this.handleButtonKeyDown}
|
onKeyDown={this.handleButtonKeyDown}
|
||||||
style={{ height: null, lineHeight: '27px' }}
|
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
/>
|
className={classNames('dropdown-button', { active: open })}
|
||||||
|
>
|
||||||
|
<Icon id={valueOption.icon} icon={valueOption.iconComponent} />
|
||||||
|
<span className='dropdown-button__label'>{valueOption.text}</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
<Overlay show={open} placement={placement} flip target={this.findTarget} container={container} popperConfig={{ strategy: 'fixed', onFirstUpdate: this.handleOverlayEnter }}>
|
<Overlay show={open} offset={[5, 5]} placement={placement} flip target={this.findTarget} container={container} popperConfig={{ strategy: 'fixed', onFirstUpdate: this.handleOverlayEnter }}>
|
||||||
{({ props, placement }) => (
|
{({ props, placement }) => (
|
||||||
<div {...props}>
|
<div {...props}>
|
||||||
<div className={`dropdown-animation privacy-dropdown__dropdown ${placement}`}>
|
<div className={`dropdown-animation privacy-dropdown__dropdown ${placement}`}>
|
||||||
|
@ -1,74 +1,48 @@
|
|||||||
import PropTypes from 'prop-types';
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
import { defineMessages, injectIntl } from 'react-intl';
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import { useSelector } from 'react-redux';
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
|
||||||
|
|
||||||
import CloseIcon from '@/material-icons/400-24px/close.svg?react';
|
import BarChart4BarsIcon from '@/material-icons/400-24px/bar_chart_4_bars.svg?react';
|
||||||
import AttachmentList from 'mastodon/components/attachment_list';
|
import PhotoLibraryIcon from '@/material-icons/400-24px/photo_library.svg?react';
|
||||||
import { WithOptionalRouterPropTypes, withOptionalRouter } from 'mastodon/utils/react_router';
|
import { Avatar } from 'mastodon/components/avatar';
|
||||||
|
import { DisplayName } from 'mastodon/components/display_name';
|
||||||
|
import { Icon } from 'mastodon/components/icon';
|
||||||
|
|
||||||
import { Avatar } from '../../../components/avatar';
|
export const ReplyIndicator = () => {
|
||||||
import { DisplayName } from '../../../components/display_name';
|
const inReplyToId = useSelector(state => state.getIn(['compose', 'in_reply_to']));
|
||||||
import { IconButton } from '../../../components/icon_button';
|
const status = useSelector(state => state.getIn(['statuses', inReplyToId]));
|
||||||
|
const account = useSelector(state => state.getIn(['accounts', status?.get('account')]));
|
||||||
|
|
||||||
const messages = defineMessages({
|
if (!status) {
|
||||||
cancel: { id: 'reply_indicator.cancel', defaultMessage: 'Cancel' },
|
return null;
|
||||||
});
|
}
|
||||||
|
|
||||||
class ReplyIndicator extends ImmutablePureComponent {
|
const content = { __html: status.get('contentHtml') };
|
||||||
|
|
||||||
static propTypes = {
|
return (
|
||||||
status: ImmutablePropTypes.map,
|
<div className='reply-indicator'>
|
||||||
onCancel: PropTypes.func.isRequired,
|
<div className='reply-indicator__line' />
|
||||||
intl: PropTypes.object.isRequired,
|
|
||||||
...WithOptionalRouterPropTypes,
|
|
||||||
};
|
|
||||||
|
|
||||||
handleClick = () => {
|
<Link to={`/@${account.get('acct')}`} className='detailed-status__display-avatar'>
|
||||||
this.props.onCancel();
|
<Avatar account={account} size={46} />
|
||||||
};
|
</Link>
|
||||||
|
|
||||||
handleAccountClick = (e) => {
|
<div className='reply-indicator__main'>
|
||||||
if (e.button === 0 && !(e.ctrlKey || e.metaKey)) {
|
<Link to={`/@${account.get('acct')}`} className='detailed-status__display-name'>
|
||||||
e.preventDefault();
|
<DisplayName account={account} />
|
||||||
this.props.history?.push(`/@${this.props.status.getIn(['account', 'acct'])}`);
|
</Link>
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
render () {
|
|
||||||
const { status, intl } = this.props;
|
|
||||||
|
|
||||||
if (!status) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const content = { __html: status.get('contentHtml') };
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='reply-indicator'>
|
|
||||||
<div className='reply-indicator__header'>
|
|
||||||
<div className='reply-indicator__cancel'><IconButton title={intl.formatMessage(messages.cancel)} icon='times' iconComponent={CloseIcon} onClick={this.handleClick} inverted /></div>
|
|
||||||
|
|
||||||
<a href={`/@${status.getIn(['account', 'acct'])}`} onClick={this.handleAccountClick} className='reply-indicator__display-name'>
|
|
||||||
<div className='reply-indicator__display-avatar'><Avatar account={status.get('account')} size={24} /></div>
|
|
||||||
<DisplayName account={status.get('account')} />
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='reply-indicator__content translate' dangerouslySetInnerHTML={content} />
|
<div className='reply-indicator__content translate' dangerouslySetInnerHTML={content} />
|
||||||
|
|
||||||
{status.get('media_attachments').size > 0 && (
|
{(status.get('poll') || status.get('media_attachments').size > 0) && (
|
||||||
<AttachmentList
|
<div className='reply-indicator__attachments'>
|
||||||
compact
|
{status.get('poll') && <><Icon icon={BarChart4BarsIcon} /><FormattedMessage id='reply_indicator.poll' defaultMessage='Poll' /></>}
|
||||||
media={status.get('media_attachments')}
|
{status.get('media_attachments').size > 0 && <><Icon icon={PhotoLibraryIcon} /><FormattedMessage id='reply_indicator.attachments' defaultMessage='{count, plural, one {# attachment} other {# attachments}}' values={{ count: status.get('media_attachments').size }} /></>}
|
||||||
/>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
</div>
|
||||||
}
|
);
|
||||||
|
};
|
||||||
}
|
|
||||||
|
|
||||||
export default withOptionalRouter(injectIntl(ReplyIndicator));
|
|
||||||
|
@ -2,6 +2,8 @@ import PropTypes from 'prop-types';
|
|||||||
|
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
|
|
||||||
@ -9,7 +11,8 @@ import spring from 'react-motion/lib/spring';
|
|||||||
|
|
||||||
import CloseIcon from '@/material-icons/400-24px/close.svg?react';
|
import CloseIcon from '@/material-icons/400-24px/close.svg?react';
|
||||||
import EditIcon from '@/material-icons/400-24px/edit.svg?react';
|
import EditIcon from '@/material-icons/400-24px/edit.svg?react';
|
||||||
import InfoIcon from '@/material-icons/400-24px/info.svg?react';
|
import WarningIcon from '@/material-icons/400-24px/warning.svg?react';
|
||||||
|
import { Blurhash } from 'mastodon/components/blurhash';
|
||||||
import { Icon } from 'mastodon/components/icon';
|
import { Icon } from 'mastodon/components/icon';
|
||||||
|
|
||||||
import Motion from '../../ui/util/optional_motion';
|
import Motion from '../../ui/util/optional_motion';
|
||||||
@ -18,6 +21,7 @@ export default class Upload extends ImmutablePureComponent {
|
|||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
media: ImmutablePropTypes.map.isRequired,
|
media: ImmutablePropTypes.map.isRequired,
|
||||||
|
sensitive: PropTypes.bool,
|
||||||
onUndo: PropTypes.func.isRequired,
|
onUndo: PropTypes.func.isRequired,
|
||||||
onOpenFocalPoint: PropTypes.func.isRequired,
|
onOpenFocalPoint: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
@ -33,7 +37,7 @@ export default class Upload extends ImmutablePureComponent {
|
|||||||
};
|
};
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { media } = this.props;
|
const { media, sensitive } = this.props;
|
||||||
|
|
||||||
if (!media) {
|
if (!media) {
|
||||||
return null;
|
return null;
|
||||||
@ -43,22 +47,26 @@ export default class Upload extends ImmutablePureComponent {
|
|||||||
const focusY = media.getIn(['meta', 'focus', 'y']);
|
const focusY = media.getIn(['meta', 'focus', 'y']);
|
||||||
const x = ((focusX / 2) + .5) * 100;
|
const x = ((focusX / 2) + .5) * 100;
|
||||||
const y = ((focusY / -2) + .5) * 100;
|
const y = ((focusY / -2) + .5) * 100;
|
||||||
|
const missingDescription = (media.get('description') || '').length === 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='compose-form__upload'>
|
<div className='compose-form__upload'>
|
||||||
<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: `scale(${scale})`, backgroundImage: `url(${media.get('preview_url')})`, backgroundPosition: `${x}% ${y}%` }}>
|
<div className='compose-form__upload__thumbnail' style={{ transform: `scale(${scale})`, backgroundImage: !sensitive ? `url(${media.get('preview_url')})` : null, backgroundPosition: `${x}% ${y}%` }}>
|
||||||
|
{sensitive && <Blurhash
|
||||||
|
hash={media.get('blurhash')}
|
||||||
|
className='compose-form__upload__preview'
|
||||||
|
/>}
|
||||||
|
|
||||||
<div className='compose-form__upload__actions'>
|
<div className='compose-form__upload__actions'>
|
||||||
<button type='button' className='icon-button' onClick={this.handleUndoClick}><Icon id='times' icon={CloseIcon} /> <FormattedMessage id='upload_form.undo' defaultMessage='Delete' /></button>
|
<button type='button' className='icon-button compose-form__upload__delete' onClick={this.handleUndoClick}><Icon icon={CloseIcon} /></button>
|
||||||
<button type='button' className='icon-button' onClick={this.handleFocalPointClick}><Icon id='pencil' icon={EditIcon} /> <FormattedMessage id='upload_form.edit' defaultMessage='Edit' /></button>
|
<button type='button' className='icon-button' onClick={this.handleFocalPointClick}><Icon icon={EditIcon} /> <FormattedMessage id='upload_form.edit' defaultMessage='Edit' /></button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{(media.get('description') || '').length === 0 && (
|
<div className='compose-form__upload__warning'>
|
||||||
<div className='compose-form__upload__warning'>
|
<button type='button' className={classNames('icon-button', { active: missingDescription })} onClick={this.handleFocalPointClick}>{missingDescription && <Icon icon={WarningIcon} />} ALT</button>
|
||||||
<button type='button' className='icon-button' onClick={this.handleFocalPointClick}><Icon id='info-circle' icon={InfoIcon} /> <FormattedMessage id='upload_form.description_missing' defaultMessage='No description added' /></button>
|
</div>
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Motion>
|
</Motion>
|
||||||
|
@ -6,9 +6,8 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
|
|||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
import AddPhotoAlternateIcon from '@/material-icons/400-24px/add_photo_alternate.svg?react';
|
import PhotoLibraryIcon from '@/material-icons/400-24px/photo_library.svg?react';
|
||||||
|
import { IconButton } from 'mastodon/components/icon_button';
|
||||||
import { IconButton } from '../../../components/icon_button';
|
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
upload: { id: 'upload_button.label', defaultMessage: 'Add images, a video or an audio file' },
|
upload: { id: 'upload_button.label', defaultMessage: 'Add images, a video or an audio file' },
|
||||||
@ -31,7 +30,6 @@ class UploadButton extends ImmutablePureComponent {
|
|||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
disabled: PropTypes.bool,
|
disabled: PropTypes.bool,
|
||||||
unavailable: PropTypes.bool,
|
|
||||||
onSelectFile: PropTypes.func.isRequired,
|
onSelectFile: PropTypes.func.isRequired,
|
||||||
style: PropTypes.object,
|
style: PropTypes.object,
|
||||||
resetFileKey: PropTypes.number,
|
resetFileKey: PropTypes.number,
|
||||||
@ -54,17 +52,13 @@ class UploadButton extends ImmutablePureComponent {
|
|||||||
};
|
};
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { intl, resetFileKey, unavailable, disabled, acceptContentTypes } = this.props;
|
const { intl, resetFileKey, disabled, acceptContentTypes } = this.props;
|
||||||
|
|
||||||
if (unavailable) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const message = intl.formatMessage(messages.upload);
|
const message = intl.formatMessage(messages.upload);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='compose-form__upload-button'>
|
<div className='compose-form__upload-button'>
|
||||||
<IconButton icon='paperclip' iconComponent={AddPhotoAlternateIcon} title={message} disabled={disabled} onClick={this.handleClick} className='compose-form__upload-button-icon' size={18} inverted style={iconStyle} />
|
<IconButton icon='paperclip' iconComponent={PhotoLibraryIcon} title={message} disabled={disabled} onClick={this.handleClick} className='compose-form__upload-button-icon' size={18} inverted style={iconStyle} />
|
||||||
<label>
|
<label>
|
||||||
<span style={{ display: 'none' }}>{message}</span>
|
<span style={{ display: 'none' }}>{message}</span>
|
||||||
<input
|
<input
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
|
|
||||||
import SensitiveButtonContainer from '../containers/sensitive_button_container';
|
|
||||||
import UploadContainer from '../containers/upload_container';
|
import UploadContainer from '../containers/upload_container';
|
||||||
import UploadProgressContainer from '../containers/upload_progress_container';
|
import UploadProgressContainer from '../containers/upload_progress_container';
|
||||||
|
|
||||||
@ -15,17 +14,17 @@ export default class UploadForm extends ImmutablePureComponent {
|
|||||||
const { mediaIds } = this.props;
|
const { mediaIds } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='compose-form__upload-wrapper'>
|
<>
|
||||||
<UploadProgressContainer />
|
<UploadProgressContainer />
|
||||||
|
|
||||||
<div className='compose-form__uploads-wrapper'>
|
{mediaIds.size > 0 && (
|
||||||
{mediaIds.map(id => (
|
<div className='compose-form__uploads'>
|
||||||
<UploadContainer id={id} key={id} />
|
{mediaIds.map(id => (
|
||||||
))}
|
<UploadContainer id={id} key={id} />
|
||||||
</div>
|
))}
|
||||||
|
</div>
|
||||||
{!mediaIds.isEmpty() && <SensitiveButtonContainer />}
|
)}
|
||||||
</div>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -35,9 +35,7 @@ export default class UploadProgress extends PureComponent {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='upload-progress'>
|
<div className='upload-progress'>
|
||||||
<div className='upload-progress__icon'>
|
<Icon id='upload' icon={UploadFileIcon} />
|
||||||
<Icon id='upload' icon={UploadFileIcon} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='upload-progress__message'>
|
<div className='upload-progress__message'>
|
||||||
{message}
|
{message}
|
||||||
|
@ -1,36 +0,0 @@
|
|||||||
import { defineMessages, injectIntl } from 'react-intl';
|
|
||||||
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
|
|
||||||
import { openModal } from 'mastodon/actions/modal';
|
|
||||||
import { logOut } from 'mastodon/utils/log_out';
|
|
||||||
|
|
||||||
import { me } from '../../../initial_state';
|
|
||||||
import NavigationBar from '../components/navigation_bar';
|
|
||||||
|
|
||||||
const messages = defineMessages({
|
|
||||||
logoutMessage: { id: 'confirmations.logout.message', defaultMessage: 'Are you sure you want to log out?' },
|
|
||||||
logoutConfirm: { id: 'confirmations.logout.confirm', defaultMessage: 'Log out' },
|
|
||||||
});
|
|
||||||
|
|
||||||
const mapStateToProps = state => {
|
|
||||||
return {
|
|
||||||
account: state.getIn(['accounts', me]),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch, { intl }) => ({
|
|
||||||
onLogout () {
|
|
||||||
dispatch(openModal({
|
|
||||||
modalType: 'CONFIRM',
|
|
||||||
modalProps: {
|
|
||||||
message: intl.formatMessage(messages.logoutMessage),
|
|
||||||
confirm: intl.formatMessage(messages.logoutConfirm),
|
|
||||||
closeWhenConfirm: false,
|
|
||||||
onConfirm: () => logOut(),
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(NavigationBar));
|
|
@ -4,7 +4,7 @@ import { addPoll, removePoll } from '../../../actions/compose';
|
|||||||
import PollButton from '../components/poll_button';
|
import PollButton from '../components/poll_button';
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => ({
|
||||||
unavailable: state.getIn(['compose', 'is_uploading']) || (state.getIn(['compose', 'media_attachments']).size > 0),
|
disabled: state.getIn(['compose', 'is_uploading']) || (state.getIn(['compose', 'media_attachments']).size > 0),
|
||||||
active: state.getIn(['compose', 'poll']) !== null,
|
active: state.getIn(['compose', 'poll']) !== null,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1,53 +0,0 @@
|
|||||||
import { connect } from 'react-redux';
|
|
||||||
|
|
||||||
import {
|
|
||||||
addPollOption,
|
|
||||||
removePollOption,
|
|
||||||
changePollOption,
|
|
||||||
changePollSettings,
|
|
||||||
clearComposeSuggestions,
|
|
||||||
fetchComposeSuggestions,
|
|
||||||
selectComposeSuggestion,
|
|
||||||
} from '../../../actions/compose';
|
|
||||||
import PollForm from '../components/poll_form';
|
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
|
||||||
suggestions: state.getIn(['compose', 'suggestions']),
|
|
||||||
options: state.getIn(['compose', 'poll', 'options']),
|
|
||||||
lang: state.getIn(['compose', 'language']),
|
|
||||||
expiresIn: state.getIn(['compose', 'poll', 'expires_in']),
|
|
||||||
isMultiple: state.getIn(['compose', 'poll', 'multiple']),
|
|
||||||
});
|
|
||||||
|
|
||||||
const mapDispatchToProps = dispatch => ({
|
|
||||||
onAddOption(title) {
|
|
||||||
dispatch(addPollOption(title));
|
|
||||||
},
|
|
||||||
|
|
||||||
onRemoveOption(index) {
|
|
||||||
dispatch(removePollOption(index));
|
|
||||||
},
|
|
||||||
|
|
||||||
onChangeOption(index, title) {
|
|
||||||
dispatch(changePollOption(index, title));
|
|
||||||
},
|
|
||||||
|
|
||||||
onChangeSettings(expiresIn, isMultiple) {
|
|
||||||
dispatch(changePollSettings(expiresIn, isMultiple));
|
|
||||||
},
|
|
||||||
|
|
||||||
onClearSuggestions () {
|
|
||||||
dispatch(clearComposeSuggestions());
|
|
||||||
},
|
|
||||||
|
|
||||||
onFetchSuggestions (token) {
|
|
||||||
dispatch(fetchComposeSuggestions(token));
|
|
||||||
},
|
|
||||||
|
|
||||||
onSuggestionSelected (position, token, accountId, path) {
|
|
||||||
dispatch(selectComposeSuggestion(position, token, accountId, path));
|
|
||||||
},
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(PollForm);
|
|
@ -1,36 +0,0 @@
|
|||||||
import { connect } from 'react-redux';
|
|
||||||
|
|
||||||
import { cancelReplyCompose } from '../../../actions/compose';
|
|
||||||
import { makeGetStatus } from '../../../selectors';
|
|
||||||
import ReplyIndicator from '../components/reply_indicator';
|
|
||||||
|
|
||||||
const makeMapStateToProps = () => {
|
|
||||||
const getStatus = makeGetStatus();
|
|
||||||
|
|
||||||
const mapStateToProps = state => {
|
|
||||||
let statusId = state.getIn(['compose', 'id'], null);
|
|
||||||
let editing = true;
|
|
||||||
|
|
||||||
if (statusId === null) {
|
|
||||||
statusId = state.getIn(['compose', 'in_reply_to']);
|
|
||||||
editing = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
status: getStatus(state, { id: statusId }),
|
|
||||||
editing,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
return mapStateToProps;
|
|
||||||
};
|
|
||||||
|
|
||||||
const mapDispatchToProps = dispatch => ({
|
|
||||||
|
|
||||||
onCancel () {
|
|
||||||
dispatch(cancelReplyCompose());
|
|
||||||
},
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
export default connect(makeMapStateToProps, mapDispatchToProps)(ReplyIndicator);
|
|
@ -1,73 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import { PureComponent } from 'react';
|
|
||||||
|
|
||||||
import { injectIntl, defineMessages, FormattedMessage } from 'react-intl';
|
|
||||||
|
|
||||||
import classNames from 'classnames';
|
|
||||||
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
|
|
||||||
import { changeComposeSensitivity } from 'mastodon/actions/compose';
|
|
||||||
|
|
||||||
const messages = defineMessages({
|
|
||||||
marked: {
|
|
||||||
id: 'compose_form.sensitive.marked',
|
|
||||||
defaultMessage: '{count, plural, one {Media is marked as sensitive} other {Media is marked as sensitive}}',
|
|
||||||
},
|
|
||||||
unmarked: {
|
|
||||||
id: 'compose_form.sensitive.unmarked',
|
|
||||||
defaultMessage: '{count, plural, one {Media is not marked as sensitive} other {Media is not marked as sensitive}}',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
|
||||||
active: state.getIn(['compose', 'sensitive']),
|
|
||||||
disabled: state.getIn(['compose', 'spoiler']),
|
|
||||||
mediaCount: state.getIn(['compose', 'media_attachments']).size,
|
|
||||||
});
|
|
||||||
|
|
||||||
const mapDispatchToProps = dispatch => ({
|
|
||||||
|
|
||||||
onClick () {
|
|
||||||
dispatch(changeComposeSensitivity());
|
|
||||||
},
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
class SensitiveButton extends PureComponent {
|
|
||||||
|
|
||||||
static propTypes = {
|
|
||||||
active: PropTypes.bool,
|
|
||||||
disabled: PropTypes.bool,
|
|
||||||
mediaCount: PropTypes.number,
|
|
||||||
onClick: PropTypes.func.isRequired,
|
|
||||||
intl: PropTypes.object.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
render () {
|
|
||||||
const { active, disabled, mediaCount, onClick, intl } = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='compose-form__sensitive-button'>
|
|
||||||
<label className={classNames('icon-button', { active })} title={intl.formatMessage(active ? messages.marked : messages.unmarked, { count: mediaCount })}>
|
|
||||||
<input
|
|
||||||
name='mark-sensitive'
|
|
||||||
type='checkbox'
|
|
||||||
checked={active}
|
|
||||||
onChange={onClick}
|
|
||||||
disabled={disabled}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormattedMessage
|
|
||||||
id='compose_form.sensitive.hide'
|
|
||||||
defaultMessage='{count, plural, one {Mark media as sensitive} other {Mark media as sensitive}}'
|
|
||||||
values={{ count: mediaCount }}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(injectIntl(SensitiveButton));
|
|
@ -2,8 +2,10 @@ import { injectIntl, defineMessages } from 'react-intl';
|
|||||||
|
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
|
import WarningIcon from 'mastodon/../material-icons/400-24px/warning.svg?react';
|
||||||
|
import { IconButton } from 'mastodon/components/icon_button';
|
||||||
|
|
||||||
import { changeComposeSpoilerness } from '../../../actions/compose';
|
import { changeComposeSpoilerness } from '../../../actions/compose';
|
||||||
import TextIconButton from '../components/text_icon_button';
|
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
marked: { id: 'compose_form.spoiler.marked', defaultMessage: 'Text is hidden behind warning' },
|
marked: { id: 'compose_form.spoiler.marked', defaultMessage: 'Text is hidden behind warning' },
|
||||||
@ -11,10 +13,12 @@ const messages = defineMessages({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const mapStateToProps = (state, { intl }) => ({
|
const mapStateToProps = (state, { intl }) => ({
|
||||||
label: 'CW',
|
iconComponent: WarningIcon,
|
||||||
title: intl.formatMessage(state.getIn(['compose', 'spoiler']) ? messages.marked : messages.unmarked),
|
title: intl.formatMessage(state.getIn(['compose', 'spoiler']) ? messages.marked : messages.unmarked),
|
||||||
active: state.getIn(['compose', 'spoiler']),
|
active: state.getIn(['compose', 'spoiler']),
|
||||||
ariaControls: 'cw-spoiler-input',
|
ariaControls: 'cw-spoiler-input',
|
||||||
|
size: 18,
|
||||||
|
inverted: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapDispatchToProps = dispatch => ({
|
const mapDispatchToProps = dispatch => ({
|
||||||
@ -25,4 +29,4 @@ const mapDispatchToProps = dispatch => ({
|
|||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(TextIconButton));
|
export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(IconButton));
|
||||||
|
@ -4,8 +4,7 @@ import { uploadCompose } from '../../../actions/compose';
|
|||||||
import UploadButton from '../components/upload_button';
|
import UploadButton from '../components/upload_button';
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => ({
|
||||||
disabled: state.getIn(['compose', 'is_uploading']) || (state.getIn(['compose', 'media_attachments']).size + state.getIn(['compose', 'pending_media_attachments']) > 3 || state.getIn(['compose', 'media_attachments']).some(m => ['video', 'audio'].includes(m.get('type')))),
|
disabled: state.getIn(['compose', 'poll']) !== null || state.getIn(['compose', 'is_uploading']) || (state.getIn(['compose', 'media_attachments']).size + state.getIn(['compose', 'pending_media_attachments']) > 3 || state.getIn(['compose', 'media_attachments']).some(m => ['video', 'audio'].includes(m.get('type')))),
|
||||||
unavailable: state.getIn(['compose', 'poll']) !== null,
|
|
||||||
resetFileKey: state.getIn(['compose', 'resetFileKey']),
|
resetFileKey: state.getIn(['compose', 'resetFileKey']),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -5,6 +5,7 @@ import Upload from '../components/upload';
|
|||||||
|
|
||||||
const mapStateToProps = (state, { id }) => ({
|
const mapStateToProps = (state, { id }) => ({
|
||||||
media: state.getIn(['compose', 'media_attachments']).find(item => item.get('id') === id),
|
media: state.getIn(['compose', 'media_attachments']).find(item => item.get('id') === id),
|
||||||
|
sensitive: state.getIn(['compose', 'spoiler']),
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapDispatchToProps = dispatch => ({
|
const mapDispatchToProps = dispatch => ({
|
||||||
|
@ -30,7 +30,6 @@ import { isMobile } from '../../is_mobile';
|
|||||||
import Motion from '../ui/util/optional_motion';
|
import Motion from '../ui/util/optional_motion';
|
||||||
|
|
||||||
import ComposeFormContainer from './containers/compose_form_container';
|
import ComposeFormContainer from './containers/compose_form_container';
|
||||||
import NavigationContainer from './containers/navigation_container';
|
|
||||||
import SearchContainer from './containers/search_container';
|
import SearchContainer from './containers/search_container';
|
||||||
import SearchResultsContainer from './containers/search_results_container';
|
import SearchResultsContainer from './containers/search_results_container';
|
||||||
|
|
||||||
@ -129,8 +128,6 @@ class Compose extends PureComponent {
|
|||||||
|
|
||||||
<div className='drawer__pager'>
|
<div className='drawer__pager'>
|
||||||
<div className='drawer__inner' onFocus={this.onFocus}>
|
<div className='drawer__inner' onFocus={this.onFocus}>
|
||||||
<NavigationContainer onClose={this.onBlur} />
|
|
||||||
|
|
||||||
<ComposeFormContainer autoFocus={!isMobile(window.innerWidth)} />
|
<ComposeFormContainer autoFocus={!isMobile(window.innerWidth)} />
|
||||||
|
|
||||||
<div className='drawer__inner__mastodon'>
|
<div className='drawer__inner__mastodon'>
|
||||||
@ -152,7 +149,6 @@ class Compose extends PureComponent {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Column onFocus={this.onFocus}>
|
<Column onFocus={this.onFocus}>
|
||||||
<NavigationContainer onClose={this.onBlur} />
|
|
||||||
<ComposeFormContainer />
|
<ComposeFormContainer />
|
||||||
|
|
||||||
<Helmet>
|
<Helmet>
|
||||||
|
@ -26,7 +26,7 @@ import ColumnHeader from 'mastodon/components/column_header';
|
|||||||
import LinkFooter from 'mastodon/features/ui/components/link_footer';
|
import LinkFooter from 'mastodon/features/ui/components/link_footer';
|
||||||
|
|
||||||
import { me, showTrends } from '../../initial_state';
|
import { me, showTrends } from '../../initial_state';
|
||||||
import NavigationContainer from '../compose/containers/navigation_container';
|
import { NavigationBar } from '../compose/components/navigation_bar';
|
||||||
import ColumnLink from '../ui/components/column_link';
|
import ColumnLink from '../ui/components/column_link';
|
||||||
import ColumnSubheading from '../ui/components/column_subheading';
|
import ColumnSubheading from '../ui/components/column_subheading';
|
||||||
|
|
||||||
@ -143,7 +143,7 @@ class GettingStarted extends ImmutablePureComponent {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Column>
|
<Column>
|
||||||
{(signedIn && !multiColumn) ? <NavigationContainer /> : <ColumnHeader title={intl.formatMessage(messages.menu)} icon='bars' iconComponent={MenuIcon} multiColumn={multiColumn} />}
|
{(signedIn && !multiColumn) ? <NavigationBar /> : <ColumnHeader title={intl.formatMessage(messages.menu)} icon='bars' iconComponent={MenuIcon} multiColumn={multiColumn} />}
|
||||||
|
|
||||||
<div className='getting-started scrollable scrollable--flex'>
|
<div className='getting-started scrollable scrollable--flex'>
|
||||||
<div className='getting-started__wrapper'>
|
<div className='getting-started__wrapper'>
|
||||||
|
@ -1,21 +1,15 @@
|
|||||||
import { PureComponent } from 'react';
|
import ComposeFormContainer from 'mastodon/features/compose/containers/compose_form_container';
|
||||||
|
import LoadingBarContainer from 'mastodon/features/ui/containers/loading_bar_container';
|
||||||
|
import ModalContainer from 'mastodon/features/ui/containers/modal_container';
|
||||||
|
import NotificationsContainer from 'mastodon/features/ui/containers/notifications_container';
|
||||||
|
|
||||||
import ComposeFormContainer from '../../compose/containers/compose_form_container';
|
const Compose = () => (
|
||||||
import LoadingBarContainer from '../../ui/containers/loading_bar_container';
|
<>
|
||||||
import ModalContainer from '../../ui/containers/modal_container';
|
<ComposeFormContainer autoFocus withoutNavigation />
|
||||||
import NotificationsContainer from '../../ui/containers/notifications_container';
|
<NotificationsContainer />
|
||||||
|
<ModalContainer />
|
||||||
|
<LoadingBarContainer className='loading-bar' />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
export default class Compose extends PureComponent {
|
export default Compose;
|
||||||
|
|
||||||
render () {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<ComposeFormContainer autoFocus />
|
|
||||||
<NotificationsContainer />
|
|
||||||
<ModalContainer />
|
|
||||||
<LoadingBarContainer className='loading-bar' />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
@ -6,7 +6,6 @@ import { connect } from 'react-redux';
|
|||||||
import { changeComposing, mountCompose, unmountCompose } from 'mastodon/actions/compose';
|
import { changeComposing, mountCompose, unmountCompose } from 'mastodon/actions/compose';
|
||||||
import ServerBanner from 'mastodon/components/server_banner';
|
import ServerBanner from 'mastodon/components/server_banner';
|
||||||
import ComposeFormContainer from 'mastodon/features/compose/containers/compose_form_container';
|
import ComposeFormContainer from 'mastodon/features/compose/containers/compose_form_container';
|
||||||
import NavigationContainer from 'mastodon/features/compose/containers/navigation_container';
|
|
||||||
import SearchContainer from 'mastodon/features/compose/containers/search_container';
|
import SearchContainer from 'mastodon/features/compose/containers/search_container';
|
||||||
|
|
||||||
import LinkFooter from './link_footer';
|
import LinkFooter from './link_footer';
|
||||||
@ -56,10 +55,7 @@ class ComposePanel extends PureComponent {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{signedIn && (
|
{signedIn && (
|
||||||
<>
|
<ComposeFormContainer singleColumn />
|
||||||
<NavigationContainer onClose={this.onBlur} />
|
|
||||||
<ComposeFormContainer singleColumn />
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<LinkFooter />
|
<LinkFooter />
|
||||||
|
@ -21,7 +21,7 @@ import { Button } from 'mastodon/components/button';
|
|||||||
import { GIFV } from 'mastodon/components/gifv';
|
import { GIFV } from 'mastodon/components/gifv';
|
||||||
import { IconButton } from 'mastodon/components/icon_button';
|
import { IconButton } from 'mastodon/components/icon_button';
|
||||||
import Audio from 'mastodon/features/audio';
|
import Audio from 'mastodon/features/audio';
|
||||||
import CharacterCounter from 'mastodon/features/compose/components/character_counter';
|
import { CharacterCounter } from 'mastodon/features/compose/components/character_counter';
|
||||||
import UploadProgress from 'mastodon/features/compose/components/upload_progress';
|
import UploadProgress from 'mastodon/features/compose/components/upload_progress';
|
||||||
import { Tesseract as fetchTesseract } from 'mastodon/features/ui/util/async-components';
|
import { Tesseract as fetchTesseract } from 'mastodon/features/ui/util/async-components';
|
||||||
import { me } from 'mastodon/initial_state';
|
import { me } from 'mastodon/initial_state';
|
||||||
|
@ -108,7 +108,6 @@ class MuteModal extends PureComponent {
|
|||||||
<div>
|
<div>
|
||||||
<span><FormattedMessage id='mute_modal.duration' defaultMessage='Duration' />: </span>
|
<span><FormattedMessage id='mute_modal.duration' defaultMessage='Duration' />: </span>
|
||||||
|
|
||||||
{/* eslint-disable-next-line jsx-a11y/no-onchange */}
|
|
||||||
<select value={muteDuration} onChange={this.changeMuteDuration}>
|
<select value={muteDuration} onChange={this.changeMuteDuration}>
|
||||||
<option value={0}>{intl.formatMessage(messages.indefinite)}</option>
|
<option value={0}>{intl.formatMessage(messages.indefinite)}</option>
|
||||||
<option value={300}>{intl.formatMessage(messages.minutes, { number: 5 })}</option>
|
<option value={300}>{intl.formatMessage(messages.minutes, { number: 5 })}</option>
|
||||||
|
@ -77,7 +77,6 @@ class NavigationPanel extends Component {
|
|||||||
<div className='navigation-panel'>
|
<div className='navigation-panel'>
|
||||||
<div className='navigation-panel__logo'>
|
<div className='navigation-panel__logo'>
|
||||||
<Link to='/' className='column-link column-link--logo'><WordmarkLogo /></Link>
|
<Link to='/' className='column-link column-link--logo'><WordmarkLogo /></Link>
|
||||||
{!banner && <hr />}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{banner &&
|
{banner &&
|
||||||
|
@ -89,7 +89,6 @@
|
|||||||
"announcement.announcement": "Announcement",
|
"announcement.announcement": "Announcement",
|
||||||
"attachments_list.unprocessed": "(unprocessed)",
|
"attachments_list.unprocessed": "(unprocessed)",
|
||||||
"audio.hide": "Hide audio",
|
"audio.hide": "Hide audio",
|
||||||
"autosuggest_hashtag.per_week": "{count} per week",
|
|
||||||
"boost_modal.combo": "You can press {combo} to skip this next time",
|
"boost_modal.combo": "You can press {combo} to skip this next time",
|
||||||
"bundle_column_error.copy_stacktrace": "Copy error report",
|
"bundle_column_error.copy_stacktrace": "Copy error report",
|
||||||
"bundle_column_error.error.body": "The requested page could not be rendered. It could be due to a bug in our code, or a browser compatibility issue.",
|
"bundle_column_error.error.body": "The requested page could not be rendered. It could be due to a bug in our code, or a browser compatibility issue.",
|
||||||
@ -146,22 +145,22 @@
|
|||||||
"compose_form.lock_disclaimer": "Your account is not {locked}. Anyone can follow you to view your follower-only posts.",
|
"compose_form.lock_disclaimer": "Your account is not {locked}. Anyone can follow you to view your follower-only posts.",
|
||||||
"compose_form.lock_disclaimer.lock": "locked",
|
"compose_form.lock_disclaimer.lock": "locked",
|
||||||
"compose_form.placeholder": "What's on your mind?",
|
"compose_form.placeholder": "What's on your mind?",
|
||||||
"compose_form.poll.add_option": "Add a choice",
|
"compose_form.poll.add_option": "Add option",
|
||||||
"compose_form.poll.duration": "Poll duration",
|
"compose_form.poll.duration": "Poll duration",
|
||||||
"compose_form.poll.option_placeholder": "Choice {number}",
|
"compose_form.poll.multiple": "Multiple choice",
|
||||||
"compose_form.poll.remove_option": "Remove this choice",
|
"compose_form.poll.option_placeholder": "Option {number}",
|
||||||
|
"compose_form.poll.remove_option": "Remove this option",
|
||||||
|
"compose_form.poll.single": "Pick one",
|
||||||
"compose_form.poll.switch_to_multiple": "Change poll to allow multiple choices",
|
"compose_form.poll.switch_to_multiple": "Change poll to allow multiple choices",
|
||||||
"compose_form.poll.switch_to_single": "Change poll to allow for a single choice",
|
"compose_form.poll.switch_to_single": "Change poll to allow for a single choice",
|
||||||
"compose_form.publish": "Publish",
|
"compose_form.poll.type": "Style",
|
||||||
|
"compose_form.publish": "Post",
|
||||||
"compose_form.publish_form": "New post",
|
"compose_form.publish_form": "New post",
|
||||||
"compose_form.publish_loud": "{publish}!",
|
"compose_form.reply": "Reply",
|
||||||
"compose_form.save_changes": "Save changes",
|
"compose_form.save_changes": "Update",
|
||||||
"compose_form.sensitive.hide": "{count, plural, one {Mark media as sensitive} other {Mark media as sensitive}}",
|
|
||||||
"compose_form.sensitive.marked": "{count, plural, one {Media is marked as sensitive} other {Media is marked as sensitive}}",
|
|
||||||
"compose_form.sensitive.unmarked": "{count, plural, one {Media is not marked as sensitive} other {Media is not marked as sensitive}}",
|
|
||||||
"compose_form.spoiler.marked": "Remove content warning",
|
"compose_form.spoiler.marked": "Remove content warning",
|
||||||
"compose_form.spoiler.unmarked": "Add content warning",
|
"compose_form.spoiler.unmarked": "Add content warning",
|
||||||
"compose_form.spoiler_placeholder": "Write your warning here",
|
"compose_form.spoiler_placeholder": "Content warning (optional)",
|
||||||
"confirmation_modal.cancel": "Cancel",
|
"confirmation_modal.cancel": "Cancel",
|
||||||
"confirmations.block.block_and_report": "Block & Report",
|
"confirmations.block.block_and_report": "Block & Report",
|
||||||
"confirmations.block.confirm": "Block",
|
"confirmations.block.confirm": "Block",
|
||||||
@ -408,7 +407,6 @@
|
|||||||
"navigation_bar.direct": "Private mentions",
|
"navigation_bar.direct": "Private mentions",
|
||||||
"navigation_bar.discover": "Discover",
|
"navigation_bar.discover": "Discover",
|
||||||
"navigation_bar.domain_blocks": "Blocked domains",
|
"navigation_bar.domain_blocks": "Blocked domains",
|
||||||
"navigation_bar.edit_profile": "Edit profile",
|
|
||||||
"navigation_bar.explore": "Explore",
|
"navigation_bar.explore": "Explore",
|
||||||
"navigation_bar.favourites": "Favorites",
|
"navigation_bar.favourites": "Favorites",
|
||||||
"navigation_bar.filters": "Muted words",
|
"navigation_bar.filters": "Muted words",
|
||||||
@ -526,14 +524,15 @@
|
|||||||
"poll_button.add_poll": "Add a poll",
|
"poll_button.add_poll": "Add a poll",
|
||||||
"poll_button.remove_poll": "Remove poll",
|
"poll_button.remove_poll": "Remove poll",
|
||||||
"privacy.change": "Change post privacy",
|
"privacy.change": "Change post privacy",
|
||||||
"privacy.direct.long": "Visible for mentioned users only",
|
"privacy.direct.long": "Everyone mentioned in the post",
|
||||||
"privacy.direct.short": "Mentioned people only",
|
"privacy.direct.short": "Specific people",
|
||||||
"privacy.private.long": "Visible for followers only",
|
"privacy.private.long": "Only your followers",
|
||||||
"privacy.private.short": "Followers only",
|
"privacy.private.short": "Followers",
|
||||||
"privacy.public.long": "Visible for all",
|
"privacy.public.long": "Anyone on and off Mastodon",
|
||||||
"privacy.public.short": "Public",
|
"privacy.public.short": "Public",
|
||||||
"privacy.unlisted.long": "Visible for all, but opted-out of discovery features",
|
"privacy.unlisted.additional": "This behaves exactly like public, except the post will not appear in live feeds or hashtags, explore, or Mastodon search, even if you are opted-in account-wide.",
|
||||||
"privacy.unlisted.short": "Unlisted",
|
"privacy.unlisted.long": "Fewer algorithmic fanfares",
|
||||||
|
"privacy.unlisted.short": "Quiet public",
|
||||||
"privacy_policy.last_updated": "Last updated {date}",
|
"privacy_policy.last_updated": "Last updated {date}",
|
||||||
"privacy_policy.title": "Privacy Policy",
|
"privacy_policy.title": "Privacy Policy",
|
||||||
"recommended": "Recommended",
|
"recommended": "Recommended",
|
||||||
@ -551,7 +550,9 @@
|
|||||||
"relative_time.minutes": "{number}m",
|
"relative_time.minutes": "{number}m",
|
||||||
"relative_time.seconds": "{number}s",
|
"relative_time.seconds": "{number}s",
|
||||||
"relative_time.today": "today",
|
"relative_time.today": "today",
|
||||||
|
"reply_indicator.attachments": "{count, plural, one {# attachment} other {# attachments}}",
|
||||||
"reply_indicator.cancel": "Cancel",
|
"reply_indicator.cancel": "Cancel",
|
||||||
|
"reply_indicator.poll": "Poll",
|
||||||
"report.block": "Block",
|
"report.block": "Block",
|
||||||
"report.block_explanation": "You will not see their posts. They will not be able to see your posts or follow you. They will be able to tell that they are blocked.",
|
"report.block_explanation": "You will not see their posts. They will not be able to see your posts or follow you. They will be able to tell that they are blocked.",
|
||||||
"report.categories.legal": "Legal",
|
"report.categories.legal": "Legal",
|
||||||
@ -715,10 +716,8 @@
|
|||||||
"upload_error.poll": "File upload not allowed with polls.",
|
"upload_error.poll": "File upload not allowed with polls.",
|
||||||
"upload_form.audio_description": "Describe for people who are deaf or hard of hearing",
|
"upload_form.audio_description": "Describe for people who are deaf or hard of hearing",
|
||||||
"upload_form.description": "Describe for people who are blind or have low vision",
|
"upload_form.description": "Describe for people who are blind or have low vision",
|
||||||
"upload_form.description_missing": "No description added",
|
|
||||||
"upload_form.edit": "Edit",
|
"upload_form.edit": "Edit",
|
||||||
"upload_form.thumbnail": "Change thumbnail",
|
"upload_form.thumbnail": "Change thumbnail",
|
||||||
"upload_form.undo": "Delete",
|
|
||||||
"upload_form.video_description": "Describe for people who are deaf, hard of hearing, blind or have low vision",
|
"upload_form.video_description": "Describe for people who are deaf, hard of hearing, blind or have low vision",
|
||||||
"upload_modal.analyzing_picture": "Analyzing picture…",
|
"upload_modal.analyzing_picture": "Analyzing picture…",
|
||||||
"upload_modal.apply": "Apply",
|
"upload_modal.apply": "Apply",
|
||||||
|
@ -40,9 +40,7 @@ import {
|
|||||||
COMPOSE_RESET,
|
COMPOSE_RESET,
|
||||||
COMPOSE_POLL_ADD,
|
COMPOSE_POLL_ADD,
|
||||||
COMPOSE_POLL_REMOVE,
|
COMPOSE_POLL_REMOVE,
|
||||||
COMPOSE_POLL_OPTION_ADD,
|
|
||||||
COMPOSE_POLL_OPTION_CHANGE,
|
COMPOSE_POLL_OPTION_CHANGE,
|
||||||
COMPOSE_POLL_OPTION_REMOVE,
|
|
||||||
COMPOSE_POLL_SETTINGS_CHANGE,
|
COMPOSE_POLL_SETTINGS_CHANGE,
|
||||||
INIT_MEDIA_EDIT_MODAL,
|
INIT_MEDIA_EDIT_MODAL,
|
||||||
COMPOSE_CHANGE_MEDIA_DESCRIPTION,
|
COMPOSE_CHANGE_MEDIA_DESCRIPTION,
|
||||||
@ -282,6 +280,18 @@ const updateSuggestionTags = (state, token) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const updatePoll = (state, index, value) => state.updateIn(['poll', 'options'], options => {
|
||||||
|
const tmp = options.set(index, value).filterNot(x => x.trim().length === 0);
|
||||||
|
|
||||||
|
if (tmp.size === 0) {
|
||||||
|
return tmp.push('').push('');
|
||||||
|
} else if (tmp.size < 4) {
|
||||||
|
return tmp.push('');
|
||||||
|
}
|
||||||
|
|
||||||
|
return tmp;
|
||||||
|
});
|
||||||
|
|
||||||
export default function compose(state = initialState, action) {
|
export default function compose(state = initialState, action) {
|
||||||
switch(action.type) {
|
switch(action.type) {
|
||||||
case STORE_HYDRATE:
|
case STORE_HYDRATE:
|
||||||
@ -518,12 +528,8 @@ export default function compose(state = initialState, action) {
|
|||||||
return state.set('poll', initialPoll);
|
return state.set('poll', initialPoll);
|
||||||
case COMPOSE_POLL_REMOVE:
|
case COMPOSE_POLL_REMOVE:
|
||||||
return state.set('poll', null);
|
return state.set('poll', null);
|
||||||
case COMPOSE_POLL_OPTION_ADD:
|
|
||||||
return state.updateIn(['poll', 'options'], options => options.push(action.title));
|
|
||||||
case COMPOSE_POLL_OPTION_CHANGE:
|
case COMPOSE_POLL_OPTION_CHANGE:
|
||||||
return state.setIn(['poll', 'options', action.index], action.title);
|
return updatePoll(state, action.index, action.title);
|
||||||
case COMPOSE_POLL_OPTION_REMOVE:
|
|
||||||
return state.updateIn(['poll', 'options'], options => options.delete(action.index));
|
|
||||||
case COMPOSE_POLL_SETTINGS_CHANGE:
|
case COMPOSE_POLL_SETTINGS_CHANGE:
|
||||||
return state.update('poll', poll => poll.set('expires_in', action.expiresIn).set('multiple', action.isMultiple));
|
return state.update('poll', poll => poll.set('expires_in', action.expiresIn).set('multiple', action.isMultiple));
|
||||||
case COMPOSE_LANGUAGE_CHANGE:
|
case COMPOSE_LANGUAGE_CHANGE:
|
||||||
|
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M80-120v-80h800v80H80Zm40-120v-280h120v280H120Zm200 0v-480h120v480H320Zm200 0v-360h120v360H520Zm200 0v-600h120v600H720Z"/></svg>
|
After Width: | Height: | Size: 225 B |
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M80-120v-80h800v80H80Zm40-120v-280h120v280H120Zm200 0v-480h120v480H320Zm200 0v-360h120v360H520Zm200 0v-600h120v600H720Z"/></svg>
|
After Width: | Height: | Size: 225 B |
1
app/javascript/material-icons/400-24px/mood-fill.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M620-520q25 0 42.5-17.5T680-580q0-25-17.5-42.5T620-640q-25 0-42.5 17.5T560-580q0 25 17.5 42.5T620-520Zm-280 0q25 0 42.5-17.5T400-580q0-25-17.5-42.5T340-640q-25 0-42.5 17.5T280-580q0 25 17.5 42.5T340-520Zm140 260q68 0 123.5-38.5T684-400H276q25 63 80.5 101.5T480-260Zm0 180q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Z"/></svg>
|
After Width: | Height: | Size: 559 B |
1
app/javascript/material-icons/400-24px/mood.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M620-520q25 0 42.5-17.5T680-580q0-25-17.5-42.5T620-640q-25 0-42.5 17.5T560-580q0 25 17.5 42.5T620-520Zm-280 0q25 0 42.5-17.5T400-580q0-25-17.5-42.5T340-640q-25 0-42.5 17.5T280-580q0 25 17.5 42.5T340-520Zm140 260q68 0 123.5-38.5T684-400H276q25 63 80.5 101.5T480-260Zm0 180q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Zm0-400Zm0 320q134 0 227-93t93-227q0-134-93-227t-227-93q-134 0-227 93t-93 227q0 134 93 227t227 93Z"/></svg>
|
After Width: | Height: | Size: 656 B |
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M360-400h400L622-580l-92 120-62-80-108 140Zm-40 160q-33 0-56.5-23.5T240-320v-480q0-33 23.5-56.5T320-880h480q33 0 56.5 23.5T880-800v480q0 33-23.5 56.5T800-240H320ZM160-80q-33 0-56.5-23.5T80-160v-560h80v560h560v80H160Z"/></svg>
|
After Width: | Height: | Size: 322 B |
1
app/javascript/material-icons/400-24px/photo_library.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M360-400h400L622-580l-92 120-62-80-108 140Zm-40 160q-33 0-56.5-23.5T240-320v-480q0-33 23.5-56.5T320-880h480q33 0 56.5 23.5T880-800v480q0 33-23.5 56.5T800-240H320Zm0-80h480v-480H320v480ZM160-80q-33 0-56.5-23.5T80-160v-560h80v560h560v80H160Zm160-720v480-480Z"/></svg>
|
After Width: | Height: | Size: 362 B |
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M524-40q-84 0-157.5-32t-128-86.5Q184-213 152-286.5T120-444q0-146 93-257.5T450-840q-18 99 11 193.5T561-481q71 71 165.5 100T920-370q-26 144-138 237T524-40Z"/></svg>
|
After Width: | Height: | Size: 259 B |
1
app/javascript/material-icons/400-24px/quiet_time.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M524-40q-84 0-157.5-32t-128-86.5Q184-213 152-286.5T120-444q0-146 93-257.5T450-840q-18 99 11 193.5T561-481q71 71 165.5 100T920-370q-26 144-138 237T524-40Zm0-80q88 0 163-44t118-121q-86-8-163-43.5T504-425q-61-61-97-138t-43-163q-77 43-120.5 118.5T200-444q0 135 94.5 229.5T524-120Zm-20-305Z"/></svg>
|
After Width: | Height: | Size: 391 B |
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="m476-80 182-480h84L924-80h-84l-43-122H603L560-80h-84ZM160-200l-56-56 202-202q-35-35-63.5-80T190-640h84q20 39 40 68t48 58q33-33 68.5-92.5T484-720H40v-80h280v-80h80v80h280v80H564q-21 72-63 148t-83 116l96 98-30 82-122-125-202 201Zm468-72h144l-72-204-72 204Z"/></svg>
|
After Width: | Height: | Size: 360 B |
1
app/javascript/material-icons/400-24px/translate.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="m476-80 182-480h84L924-80h-84l-43-122H603L560-80h-84ZM160-200l-56-56 202-202q-35-35-63.5-80T190-640h84q20 39 40 68t48 58q33-33 68.5-92.5T484-720H40v-80h280v-80h80v80h280v80H564q-21 72-63 148t-83 116l96 98-30 82-122-125-202 201Zm468-72h144l-72-204-72 204Z"/></svg>
|
After Width: | Height: | Size: 360 B |
1
app/javascript/material-icons/400-24px/warning-fill.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="m40-120 440-760 440 760H40Zm440-120q17 0 28.5-11.5T520-280q0-17-11.5-28.5T480-320q-17 0-28.5 11.5T440-280q0 17 11.5 28.5T480-240Zm-40-120h80v-200h-80v200Z"/></svg>
|
After Width: | Height: | Size: 260 B |
1
app/javascript/material-icons/400-24px/warning.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="m40-120 440-760 440 760H40Zm138-80h604L480-720 178-200Zm302-40q17 0 28.5-11.5T520-280q0-17-11.5-28.5T480-320q-17 0-28.5 11.5T440-280q0 17 11.5 28.5T480-240Zm-40-120h80v-200h-80v200Zm40-100Z"/></svg>
|
After Width: | Height: | Size: 295 B |
@ -13,10 +13,12 @@ function loaded() {
|
|||||||
|
|
||||||
if (mountNode) {
|
if (mountNode) {
|
||||||
const attr = mountNode.getAttribute('data-props');
|
const attr = mountNode.getAttribute('data-props');
|
||||||
if(!attr) return;
|
|
||||||
|
if (!attr) return;
|
||||||
|
|
||||||
const props = JSON.parse(attr);
|
const props = JSON.parse(attr);
|
||||||
const root = createRoot(mountNode);
|
const root = createRoot(mountNode);
|
||||||
|
|
||||||
root.render(<ComposeContainer {...props} />);
|
root.render(<ComposeContainer {...props} />);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,20 +1,7 @@
|
|||||||
.compose-form {
|
|
||||||
.compose-form__modifiers {
|
|
||||||
.compose-form__upload {
|
|
||||||
&-description {
|
|
||||||
input {
|
|
||||||
&::placeholder {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.status__content a,
|
.status__content a,
|
||||||
.link-footer a,
|
|
||||||
.reply-indicator__content a,
|
.reply-indicator__content a,
|
||||||
|
.edit-indicator__content a,
|
||||||
|
.link-footer a,
|
||||||
.status__content__read-more-button,
|
.status__content__read-more-button,
|
||||||
.status__content__translate-button {
|
.status__content__translate-button {
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
@ -42,7 +29,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.status__content a {
|
.status__content a,
|
||||||
|
.reply-indicator__content a,
|
||||||
|
.edit-indicator__content a {
|
||||||
color: $highlight-text-color;
|
color: $highlight-text-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -50,24 +39,10 @@
|
|||||||
color: $darker-text-color;
|
color: $darker-text-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
.compose-form__poll-wrapper .button.button-secondary,
|
.report-dialog-modal__textarea::placeholder {
|
||||||
.compose-form .autosuggest-textarea__textarea::placeholder,
|
|
||||||
.compose-form .spoiler-input__input::placeholder,
|
|
||||||
.report-dialog-modal__textarea::placeholder,
|
|
||||||
.language-dropdown__dropdown__results__item__common-name,
|
|
||||||
.compose-form .icon-button {
|
|
||||||
color: $inverted-text-color;
|
color: $inverted-text-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
.text-icon-button.active {
|
|
||||||
color: $ui-highlight-color;
|
|
||||||
}
|
|
||||||
|
|
||||||
.language-dropdown__dropdown__results__item.active {
|
|
||||||
background: $ui-highlight-color;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.link-button:disabled {
|
.link-button:disabled {
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
|
|
||||||
|
@ -145,10 +145,6 @@ html {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.compose-form__autosuggest-wrapper,
|
|
||||||
.poll__option input[type='text'],
|
|
||||||
.compose-form .spoiler-input__input,
|
|
||||||
.compose-form__poll-wrapper select,
|
|
||||||
.search__input,
|
.search__input,
|
||||||
.setting-text,
|
.setting-text,
|
||||||
.report-dialog-modal__textarea,
|
.report-dialog-modal__textarea,
|
||||||
@ -172,28 +168,11 @@ html {
|
|||||||
border-bottom: 0;
|
border-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.compose-form__poll-wrapper select {
|
|
||||||
background: $simple-background-color
|
|
||||||
url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 14.933 18.467' height='19.698' width='15.929'><path d='M3.467 14.967l-3.393-3.5H14.86l-3.392 3.5c-1.866 1.925-3.666 3.5-4 3.5-.335 0-2.135-1.575-4-3.5zm.266-11.234L7.467 0 11.2 3.733l3.733 3.734H0l3.733-3.734z' fill='#{hex-color(lighten($ui-base-color, 8%))}'/></svg>")
|
|
||||||
no-repeat right 8px center / auto 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.compose-form__poll-wrapper,
|
|
||||||
.compose-form__poll-wrapper .poll__footer {
|
|
||||||
border-top-color: lighten($ui-base-color, 8%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.notification__filter-bar {
|
.notification__filter-bar {
|
||||||
border: 1px solid lighten($ui-base-color, 8%);
|
border: 1px solid lighten($ui-base-color, 8%);
|
||||||
border-top: 0;
|
border-top: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.compose-form .compose-form__buttons-wrapper {
|
|
||||||
background: $ui-base-color;
|
|
||||||
border: 1px solid lighten($ui-base-color, 8%);
|
|
||||||
border-top: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.drawer__header,
|
.drawer__header,
|
||||||
.drawer__inner {
|
.drawer__inner {
|
||||||
background: $white;
|
background: $white;
|
||||||
@ -206,52 +185,6 @@ html {
|
|||||||
no-repeat bottom / 100% auto;
|
no-repeat bottom / 100% auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Change the colors used in compose-form
|
|
||||||
.compose-form {
|
|
||||||
.compose-form__modifiers {
|
|
||||||
.compose-form__upload__actions .icon-button,
|
|
||||||
.compose-form__upload__warning .icon-button {
|
|
||||||
color: lighten($white, 7%);
|
|
||||||
|
|
||||||
&:active,
|
|
||||||
&:focus,
|
|
||||||
&:hover {
|
|
||||||
color: $white;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.compose-form__buttons-wrapper {
|
|
||||||
background: darken($ui-base-color, 6%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.autosuggest-textarea__suggestions {
|
|
||||||
background: darken($ui-base-color, 6%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.autosuggest-textarea__suggestions__item {
|
|
||||||
&:hover,
|
|
||||||
&:focus,
|
|
||||||
&:active,
|
|
||||||
&.selected {
|
|
||||||
background: lighten($ui-base-color, 4%);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.emoji-mart-bar {
|
|
||||||
border-color: lighten($ui-base-color, 4%);
|
|
||||||
|
|
||||||
&:first-child {
|
|
||||||
background: darken($ui-base-color, 6%);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.emoji-mart-search input {
|
|
||||||
background: rgba($ui-base-color, 0.3);
|
|
||||||
border-color: $ui-base-color;
|
|
||||||
}
|
|
||||||
|
|
||||||
.upload-progress__backdrop {
|
.upload-progress__backdrop {
|
||||||
background: $ui-base-color;
|
background: $ui-base-color;
|
||||||
}
|
}
|
||||||
@ -283,46 +216,11 @@ html {
|
|||||||
background: $ui-base-color;
|
background: $ui-base-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
.privacy-dropdown.active .privacy-dropdown__value.active .icon-button {
|
|
||||||
color: $white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.account-gallery__item a {
|
.account-gallery__item a {
|
||||||
background-color: $ui-base-color;
|
background-color: $ui-base-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Change the colors used in the dropdown menu
|
|
||||||
.dropdown-menu {
|
|
||||||
background: $white;
|
|
||||||
|
|
||||||
&__arrow::before {
|
|
||||||
background-color: $white;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__item {
|
|
||||||
color: $darker-text-color;
|
|
||||||
|
|
||||||
&--dangerous {
|
|
||||||
color: $error-value-color;
|
|
||||||
}
|
|
||||||
|
|
||||||
a,
|
|
||||||
button {
|
|
||||||
background: $white;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Change the text colors on inverted background
|
// Change the text colors on inverted background
|
||||||
.privacy-dropdown__option.active,
|
|
||||||
.privacy-dropdown__option:hover,
|
|
||||||
.privacy-dropdown__option.active .privacy-dropdown__option__content,
|
|
||||||
.privacy-dropdown__option.active .privacy-dropdown__option__content strong,
|
|
||||||
.privacy-dropdown__option:hover .privacy-dropdown__option__content,
|
|
||||||
.privacy-dropdown__option:hover .privacy-dropdown__option__content strong,
|
|
||||||
.dropdown-menu__item:not(.dropdown-menu__item--dangerous) a:active,
|
|
||||||
.dropdown-menu__item:not(.dropdown-menu__item--dangerous) a:focus,
|
|
||||||
.dropdown-menu__item:not(.dropdown-menu__item--dangerous) a:hover,
|
|
||||||
.actions-modal ul li:not(:empty) a.active,
|
.actions-modal ul li:not(:empty) a.active,
|
||||||
.actions-modal ul li:not(:empty) a.active button,
|
.actions-modal ul li:not(:empty) a.active button,
|
||||||
.actions-modal ul li:not(:empty) a:active,
|
.actions-modal ul li:not(:empty) a:active,
|
||||||
@ -331,7 +229,6 @@ html {
|
|||||||
.actions-modal ul li:not(:empty) a:focus button,
|
.actions-modal ul li:not(:empty) a:focus button,
|
||||||
.actions-modal ul li:not(:empty) a:hover,
|
.actions-modal ul li:not(:empty) a:hover,
|
||||||
.actions-modal ul li:not(:empty) a:hover button,
|
.actions-modal ul li:not(:empty) a:hover button,
|
||||||
.language-dropdown__dropdown__results__item.active,
|
|
||||||
.admin-wrapper .sidebar ul .simple-navigation-active-leaf a,
|
.admin-wrapper .sidebar ul .simple-navigation-active-leaf a,
|
||||||
.simple_form .block-button,
|
.simple_form .block-button,
|
||||||
.simple_form .button,
|
.simple_form .button,
|
||||||
@ -339,19 +236,6 @@ html {
|
|||||||
color: $white;
|
color: $white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.language-dropdown__dropdown__results__item
|
|
||||||
.language-dropdown__dropdown__results__item__common-name {
|
|
||||||
color: lighten($ui-base-color, 8%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.language-dropdown__dropdown__results__item.active
|
|
||||||
.language-dropdown__dropdown__results__item__common-name {
|
|
||||||
color: darken($ui-base-color, 12%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdown-menu__separator,
|
|
||||||
.dropdown-menu__item.edited-timestamp__history__item,
|
|
||||||
.dropdown-menu__container__header,
|
|
||||||
.compare-history-modal .report-modal__target,
|
.compare-history-modal .report-modal__target,
|
||||||
.report-dialog-modal .poll__option.dialog-option {
|
.report-dialog-modal .poll__option.dialog-option {
|
||||||
border-bottom-color: lighten($ui-base-color, 4%);
|
border-bottom-color: lighten($ui-base-color, 4%);
|
||||||
@ -385,10 +269,7 @@ html {
|
|||||||
|
|
||||||
.reactions-bar__item:hover,
|
.reactions-bar__item:hover,
|
||||||
.reactions-bar__item:focus,
|
.reactions-bar__item:focus,
|
||||||
.reactions-bar__item:active,
|
.reactions-bar__item:active {
|
||||||
.language-dropdown__dropdown__results__item:hover,
|
|
||||||
.language-dropdown__dropdown__results__item:focus,
|
|
||||||
.language-dropdown__dropdown__results__item:active {
|
|
||||||
background-color: $ui-base-color;
|
background-color: $ui-base-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -631,11 +512,6 @@ html {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.reply-indicator {
|
|
||||||
background: transparent;
|
|
||||||
border: 1px solid lighten($ui-base-color, 8%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.status__content,
|
.status__content,
|
||||||
.reply-indicator__content {
|
.reply-indicator__content {
|
||||||
a {
|
a {
|
||||||
@ -675,3 +551,30 @@ html {
|
|||||||
background-color: rgba($ui-highlight-color, 0.15);
|
background-color: rgba($ui-highlight-color, 0.15);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.compose-form__actions .icon-button.active,
|
||||||
|
.dropdown-button.active,
|
||||||
|
.privacy-dropdown__option.active,
|
||||||
|
.privacy-dropdown__option:focus,
|
||||||
|
.language-dropdown__dropdown__results__item:focus,
|
||||||
|
.language-dropdown__dropdown__results__item.active,
|
||||||
|
.privacy-dropdown__option:focus .privacy-dropdown__option__content,
|
||||||
|
.privacy-dropdown__option:focus .privacy-dropdown__option__content strong,
|
||||||
|
.privacy-dropdown__option.active .privacy-dropdown__option__content,
|
||||||
|
.privacy-dropdown__option.active .privacy-dropdown__option__content strong,
|
||||||
|
.language-dropdown__dropdown__results__item:focus
|
||||||
|
.language-dropdown__dropdown__results__item__common-name,
|
||||||
|
.language-dropdown__dropdown__results__item.active
|
||||||
|
.language-dropdown__dropdown__results__item__common-name {
|
||||||
|
color: $white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.compose-form .spoiler-input__input {
|
||||||
|
color: lighten($ui-highlight-color, 8%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.compose-form .autosuggest-textarea__textarea,
|
||||||
|
.compose-form__highlightable,
|
||||||
|
.poll__option input[type='text'] {
|
||||||
|
background: darken($ui-base-color, 10%);
|
||||||
|
}
|
||||||
|
@ -5,7 +5,7 @@ $white: #ffffff;
|
|||||||
$classic-base-color: #282c37;
|
$classic-base-color: #282c37;
|
||||||
$classic-primary-color: #9baec8;
|
$classic-primary-color: #9baec8;
|
||||||
$classic-secondary-color: #d9e1e8;
|
$classic-secondary-color: #d9e1e8;
|
||||||
$classic-highlight-color: #858afa;
|
$classic-highlight-color: #6364ff;
|
||||||
|
|
||||||
$blurple-600: #563acc; // Iris
|
$blurple-600: #563acc; // Iris
|
||||||
$blurple-500: #6364ff; // Brand purple
|
$blurple-500: #6364ff; // Brand purple
|
||||||
@ -34,7 +34,7 @@ $ui-button-tertiary-border-color: $blurple-500 !default;
|
|||||||
|
|
||||||
$primary-text-color: $black !default;
|
$primary-text-color: $black !default;
|
||||||
$darker-text-color: $classic-base-color !default;
|
$darker-text-color: $classic-base-color !default;
|
||||||
$highlight-text-color: darken($ui-highlight-color, 8%) !default;
|
$highlight-text-color: $ui-highlight-color !default;
|
||||||
$dark-text-color: #444b5d;
|
$dark-text-color: #444b5d;
|
||||||
$action-button-color: #606984;
|
$action-button-color: #606984;
|
||||||
|
|
||||||
@ -55,3 +55,8 @@ $account-background-color: $white !default;
|
|||||||
}
|
}
|
||||||
|
|
||||||
$emojis-requiring-inversion: 'chains';
|
$emojis-requiring-inversion: 'chains';
|
||||||
|
|
||||||
|
.theme-mastodon-light {
|
||||||
|
--dropdown-border-color: #d9e1e8;
|
||||||
|
--dropdown-background-color: #fff;
|
||||||
|
}
|
||||||
|
@ -15,13 +15,14 @@
|
|||||||
outline: 0;
|
outline: 0;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border: 0;
|
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
background: $ui-base-color;
|
background: $ui-base-color;
|
||||||
color: $darker-text-color;
|
color: $darker-text-color;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
font-size: 14px;
|
border: 1px solid lighten($ui-base-color, 8%);
|
||||||
|
font-size: 17px;
|
||||||
|
line-height: normal;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1314,6 +1314,9 @@ a.sparkline {
|
|||||||
|
|
||||||
&__label {
|
&__label {
|
||||||
padding: 15px;
|
padding: 15px;
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__rules {
|
&__rules {
|
||||||
@ -1324,6 +1327,9 @@ a.sparkline {
|
|||||||
&__rule {
|
&__rule {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
padding: 15px;
|
padding: 15px;
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
body {
|
body {
|
||||||
font-family: $font-sans-serif, sans-serif;
|
font-family: $font-sans-serif, sans-serif;
|
||||||
background: darken($ui-base-color, 7%);
|
background: darken($ui-base-color, 8%);
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
line-height: 18px;
|
line-height: 18px;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
|
@ -40,13 +40,12 @@
|
|||||||
.compose-form {
|
.compose-form {
|
||||||
width: 400px;
|
width: 400px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: 20px 0;
|
padding: 10px 0;
|
||||||
margin-top: 40px;
|
padding-bottom: 20px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
|
||||||
@media screen and (width <= 400px) {
|
@media screen and (width <= 400px) {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin-top: 0;
|
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -56,13 +55,15 @@
|
|||||||
width: 400px;
|
width: 400px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
display: flex;
|
display: flex;
|
||||||
font-size: 13px;
|
align-items: center;
|
||||||
line-height: 18px;
|
gap: 10px;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 20px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
padding: 20px 0;
|
padding: 20px 0;
|
||||||
margin-top: 40px;
|
margin-top: 40px;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
border-bottom: 1px solid $ui-base-color;
|
border-bottom: 1px solid lighten($ui-base-color, 8%);
|
||||||
|
|
||||||
@media screen and (width <= 440px) {
|
@media screen and (width <= 440px) {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@ -71,9 +72,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.avatar {
|
.avatar {
|
||||||
width: 40px;
|
width: 48px;
|
||||||
height: 40px;
|
height: 48px;
|
||||||
margin-inline-end: 10px;
|
flex: 0 0 auto;
|
||||||
|
|
||||||
img {
|
img {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@ -87,13 +88,14 @@
|
|||||||
.name {
|
.name {
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
color: $secondary-text-color;
|
color: $secondary-text-color;
|
||||||
width: calc(100% - 90px);
|
|
||||||
|
|
||||||
.username {
|
.username {
|
||||||
display: block;
|
display: block;
|
||||||
font-weight: 500;
|
font-size: 16px;
|
||||||
|
line-height: 24px;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
color: $primary-text-color;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -101,7 +103,7 @@
|
|||||||
display: block;
|
display: block;
|
||||||
font-size: 32px;
|
font-size: 32px;
|
||||||
line-height: 40px;
|
line-height: 40px;
|
||||||
margin-inline-start: 10px;
|
flex: 0 0 auto;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
.emoji-mart {
|
.emoji-mart {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
color: $inverted-text-color;
|
|
||||||
|
|
||||||
&,
|
&,
|
||||||
* {
|
* {
|
||||||
@ -15,13 +14,13 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.emoji-mart-bar {
|
.emoji-mart-bar {
|
||||||
border: 0 solid darken($ui-secondary-color, 8%);
|
border: 0 solid var(--dropdown-border-color);
|
||||||
|
|
||||||
&:first-child {
|
&:first-child {
|
||||||
border-bottom-width: 1px;
|
border-bottom-width: 1px;
|
||||||
border-top-left-radius: 5px;
|
border-top-left-radius: 5px;
|
||||||
border-top-right-radius: 5px;
|
border-top-right-radius: 5px;
|
||||||
background: $ui-secondary-color;
|
background: var(--dropdown-border-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
&:last-child {
|
&:last-child {
|
||||||
@ -36,7 +35,6 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
padding: 0 6px;
|
padding: 0 6px;
|
||||||
color: $lighter-text-color;
|
|
||||||
line-height: 0;
|
line-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -50,9 +48,10 @@
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border: 0;
|
border: 0;
|
||||||
|
color: $darker-text-color;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: darken($lighter-text-color, 4%);
|
color: lighten($darker-text-color, 4%);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -60,7 +59,7 @@
|
|||||||
color: $highlight-text-color;
|
color: $highlight-text-color;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: darken($highlight-text-color, 4%);
|
color: lighten($highlight-text-color, 4%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.emoji-mart-anchor-bar {
|
.emoji-mart-anchor-bar {
|
||||||
@ -95,7 +94,7 @@
|
|||||||
height: 270px;
|
height: 270px;
|
||||||
max-height: 35vh;
|
max-height: 35vh;
|
||||||
padding: 0 6px 6px;
|
padding: 0 6px 6px;
|
||||||
background: $simple-background-color;
|
background: var(--dropdown-background-color);
|
||||||
will-change: transform;
|
will-change: transform;
|
||||||
|
|
||||||
&::-webkit-scrollbar-track:hover,
|
&::-webkit-scrollbar-track:hover,
|
||||||
@ -107,7 +106,7 @@
|
|||||||
.emoji-mart-search {
|
.emoji-mart-search {
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
padding-inline-end: 45px;
|
padding-inline-end: 45px;
|
||||||
background: $simple-background-color;
|
background: var(--dropdown-background-color);
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
input {
|
input {
|
||||||
@ -118,9 +117,9 @@
|
|||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
display: block;
|
display: block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background: rgba($ui-secondary-color, 0.3);
|
background: $ui-base-color;
|
||||||
color: $inverted-text-color;
|
color: $darker-text-color;
|
||||||
border: 1px solid $ui-secondary-color;
|
border: 1px solid lighten($ui-base-color, 8%);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
|
|
||||||
&::-moz-focus-inner {
|
&::-moz-focus-inner {
|
||||||
@ -155,11 +154,10 @@
|
|||||||
&:disabled {
|
&:disabled {
|
||||||
cursor: default;
|
cursor: default;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
opacity: 0.3;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
svg {
|
svg {
|
||||||
fill: $action-button-color;
|
fill: $darker-text-color;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -180,7 +178,7 @@
|
|||||||
inset-inline-start: 0;
|
inset-inline-start: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background-color: rgba($ui-secondary-color, 0.7);
|
background-color: var(--dropdown-border-color);
|
||||||
border-radius: 100%;
|
border-radius: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -197,7 +195,7 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
padding: 5px 6px;
|
padding: 5px 6px;
|
||||||
background: $simple-background-color;
|
background: var(--dropdown-background-color);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -241,7 +239,7 @@
|
|||||||
|
|
||||||
.emoji-mart-no-results {
|
.emoji-mart-no-results {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: $light-text-color;
|
color: $dark-text-color;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 5px 6px;
|
padding: 5px 6px;
|
||||||
padding-top: 70px;
|
padding-top: 70px;
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
.modal-layout {
|
.modal-layout {
|
||||||
background: $ui-base-color
|
background: darken($ui-base-color, 4%)
|
||||||
url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 234.80078 31.757813" width="234.80078" height="31.757812"><path d="M19.599609 0c-1.05 0-2.10039.375-2.90039 1.125L0 16.925781v14.832031h234.80078V17.025391l-16.5-15.900391c-1.6-1.5-4.20078-1.5-5.80078 0l-13.80078 13.099609c-1.6 1.5-4.19883 1.5-5.79883 0L179.09961 1.125c-1.6-1.5-4.19883-1.5-5.79883 0L159.5 14.224609c-1.6 1.5-4.20078 1.5-5.80078 0L139.90039 1.125c-1.6-1.5-4.20078-1.5-5.80078 0l-13.79883 13.099609c-1.6 1.5-4.20078 1.5-5.80078 0L100.69922 1.125c-1.600001-1.5-4.198829-1.5-5.798829 0l-13.59961 13.099609c-1.6 1.5-4.200781 1.5-5.800781 0L61.699219 1.125c-1.6-1.5-4.198828-1.5-5.798828 0L42.099609 14.224609c-1.6 1.5-4.198828 1.5-5.798828 0L22.5 1.125C21.7.375 20.649609 0 19.599609 0z" fill="#{hex-color($ui-base-lighter-color)}33"/></svg>')
|
url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 234.80078 31.757813" width="234.80078" height="31.757812"><path d="M19.599609 0c-1.05 0-2.10039.375-2.90039 1.125L0 16.925781v14.832031h234.80078V17.025391l-16.5-15.900391c-1.6-1.5-4.20078-1.5-5.80078 0l-13.80078 13.099609c-1.6 1.5-4.19883 1.5-5.79883 0L179.09961 1.125c-1.6-1.5-4.19883-1.5-5.79883 0L159.5 14.224609c-1.6 1.5-4.20078 1.5-5.80078 0L139.90039 1.125c-1.6-1.5-4.20078-1.5-5.80078 0l-13.79883 13.099609c-1.6 1.5-4.20078 1.5-5.80078 0L100.69922 1.125c-1.600001-1.5-4.198829-1.5-5.798829 0l-13.59961 13.099609c-1.6 1.5-4.200781 1.5-5.800781 0L61.699219 1.125c-1.6-1.5-4.198828-1.5-5.798828 0L42.099609 14.224609c-1.6 1.5-4.198828 1.5-5.798828 0L22.5 1.125C21.7.375 20.649609 0 19.599609 0z" fill="#{hex-color($ui-base-lighter-color)}33"/></svg>')
|
||||||
repeat-x bottom fixed;
|
repeat-x bottom fixed;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -52,6 +52,8 @@
|
|||||||
&__option {
|
&__option {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 8px;
|
||||||
padding: 6px 0;
|
padding: 6px 0;
|
||||||
line-height: 18px;
|
line-height: 18px;
|
||||||
cursor: default;
|
cursor: default;
|
||||||
@ -78,16 +80,22 @@
|
|||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: $inverted-text-color;
|
color: $secondary-text-color;
|
||||||
outline: 0;
|
outline: 0;
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
background: $simple-background-color;
|
background: $ui-base-color;
|
||||||
border: 1px solid darken($simple-background-color, 14%);
|
border: 1px solid $darker-text-color;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
padding: 6px 10px;
|
padding: 8px 12px;
|
||||||
|
|
||||||
&:focus {
|
&:focus {
|
||||||
border-color: $highlight-text-color;
|
border-color: $ui-highlight-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (width <= 600px) {
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 24px;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -96,26 +104,20 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&.editable {
|
&.editable {
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__input {
|
&__input {
|
||||||
display: inline-block;
|
display: block;
|
||||||
position: relative;
|
position: relative;
|
||||||
border: 1px solid $ui-primary-color;
|
border: 1px solid $ui-primary-color;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
width: 18px;
|
width: 17px;
|
||||||
height: 18px;
|
height: 17px;
|
||||||
margin-inline-end: 10px;
|
|
||||||
top: -1px;
|
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
vertical-align: middle;
|
flex: 0 0 auto;
|
||||||
margin-top: auto;
|
|
||||||
margin-bottom: auto;
|
|
||||||
flex: 0 0 18px;
|
|
||||||
|
|
||||||
&.checkbox {
|
&.checkbox {
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
@ -159,6 +161,15 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__option.editable &__input {
|
||||||
|
&:active,
|
||||||
|
&:focus,
|
||||||
|
&:hover {
|
||||||
|
border-color: $ui-primary-color;
|
||||||
|
border-width: 1px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&__number {
|
&__number {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
width: 45px;
|
width: 45px;
|
||||||
@ -209,90 +220,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.compose-form__poll-wrapper {
|
|
||||||
border-top: 1px solid darken($simple-background-color, 8%);
|
|
||||||
|
|
||||||
ul {
|
|
||||||
padding: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.poll__input {
|
|
||||||
&:active,
|
|
||||||
&:focus,
|
|
||||||
&:hover {
|
|
||||||
border-color: $ui-button-focus-background-color;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.poll__footer {
|
|
||||||
border-top: 1px solid darken($simple-background-color, 8%);
|
|
||||||
padding: 10px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
button,
|
|
||||||
select {
|
|
||||||
flex: 1 1 50%;
|
|
||||||
|
|
||||||
&:focus {
|
|
||||||
border-color: $highlight-text-color;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.button.button-secondary {
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 400;
|
|
||||||
padding: 6px 10px;
|
|
||||||
height: auto;
|
|
||||||
line-height: inherit;
|
|
||||||
color: $action-button-color;
|
|
||||||
border-color: $action-button-color;
|
|
||||||
margin-inline-end: 5px;
|
|
||||||
|
|
||||||
&:hover,
|
|
||||||
&:focus,
|
|
||||||
&.active {
|
|
||||||
border-color: $action-button-color;
|
|
||||||
background-color: $action-button-color;
|
|
||||||
color: $ui-button-color;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
li {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
.poll__option {
|
|
||||||
flex: 0 0 auto;
|
|
||||||
width: calc(100% - (23px + 6px));
|
|
||||||
margin-inline-end: 6px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
select {
|
|
||||||
appearance: none;
|
|
||||||
box-sizing: border-box;
|
|
||||||
font-size: 14px;
|
|
||||||
color: $inverted-text-color;
|
|
||||||
display: inline-block;
|
|
||||||
width: auto;
|
|
||||||
outline: 0;
|
|
||||||
font-family: inherit;
|
|
||||||
background: $simple-background-color
|
|
||||||
url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 14.933 18.467' height='19.698' width='15.929'><path d='M3.467 14.967l-3.393-3.5H14.86l-3.392 3.5c-1.866 1.925-3.666 3.5-4 3.5-.335 0-2.135-1.575-4-3.5zm.266-11.234L7.467 0 11.2 3.733l3.733 3.734H0l3.733-3.734z' fill='#{hex-color(darken($simple-background-color, 14%))}'/></svg>")
|
|
||||||
no-repeat right 8px center / auto 16px;
|
|
||||||
border: 1px solid darken($simple-background-color, 14%);
|
|
||||||
border-radius: 4px;
|
|
||||||
padding: 6px 10px;
|
|
||||||
padding-inline-end: 30px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-button.disabled {
|
|
||||||
color: darken($simple-background-color, 14%);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.muted .poll {
|
.muted .poll {
|
||||||
color: $dark-text-color;
|
color: $dark-text-color;
|
||||||
|
|
||||||
|
@ -24,7 +24,7 @@ describe 'NewStatuses', :sidekiq_inline do
|
|||||||
|
|
||||||
within('.compose-form') do
|
within('.compose-form') do
|
||||||
fill_in "What's on your mind?", with: status_text
|
fill_in "What's on your mind?", with: status_text
|
||||||
click_on 'Publish!'
|
click_on 'Post'
|
||||||
end
|
end
|
||||||
|
|
||||||
expect(subject).to have_css('.status__content__text', text: status_text)
|
expect(subject).to have_css('.status__content__text', text: status_text)
|
||||||
@ -37,7 +37,7 @@ describe 'NewStatuses', :sidekiq_inline do
|
|||||||
|
|
||||||
within('.compose-form') do
|
within('.compose-form') do
|
||||||
fill_in "What's on your mind?", with: status_text
|
fill_in "What's on your mind?", with: status_text
|
||||||
click_on 'Publish!'
|
click_on 'Post'
|
||||||
end
|
end
|
||||||
|
|
||||||
expect(subject).to have_css('.status__content__text', text: status_text)
|
expect(subject).to have_css('.status__content__text', text: status_text)
|
||||||
|
@ -19,13 +19,13 @@ describe 'ShareEntrypoint' do
|
|||||||
|
|
||||||
it 'can be used to post a new status' do
|
it 'can be used to post a new status' do
|
||||||
expect(subject).to have_css('div#mastodon-compose')
|
expect(subject).to have_css('div#mastodon-compose')
|
||||||
expect(subject).to have_css('.compose-form__publish-button-wrapper > button')
|
expect(subject).to have_css('.compose-form__submit')
|
||||||
|
|
||||||
status_text = 'This is a new status!'
|
status_text = 'This is a new status!'
|
||||||
|
|
||||||
within('.compose-form') do
|
within('.compose-form') do
|
||||||
fill_in "What's on your mind?", with: status_text
|
fill_in "What's on your mind?", with: status_text
|
||||||
click_on 'Publish!'
|
click_on 'Post'
|
||||||
end
|
end
|
||||||
|
|
||||||
expect(subject).to have_css('.notification-bar-message', text: 'Post published.')
|
expect(subject).to have_css('.notification-bar-message', text: 'Post published.')
|
||||||
|