@ -1,137 +1,241 @@ | |||
/* | |||
`<ComposeAdvancedOptions>` | |||
========================== | |||
> For more information on the contents of this file, please contact: | |||
> | |||
> - surinna [@srn@dev.glitch.social] | |||
This adds an advanced options dropdown to the toot compose box, for | |||
toggles that don't necessarily fit elsewhere. | |||
__Props:__ | |||
- __`values` (`ImmutablePropTypes.contains(…).isRequired`) :__ | |||
An Immutable map with the following values: | |||
- __`do_not_federate` (`PropTypes.bool.isRequired`) :__ | |||
Specifies whether or not to federate the status. | |||
- __`onChange` (`PropTypes.func.isRequired`) :__ | |||
The function to call when a toggle is changed. We pass this from | |||
our container to the toggle. | |||
- __`intl` (`PropTypes.object.isRequired`) :__ | |||
Our internationalization object, inserted by `@injectIntl`. | |||
__State:__ | |||
- __`open` :__ | |||
This tells whether the dropdown is currently open or closed. | |||
*/ | |||
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * | |||
/* | |||
Imports: | |||
-------- | |||
*/ | |||
// Package imports // | |||
import React from 'react'; | |||
import PropTypes from 'prop-types'; | |||
import ImmutablePropTypes from 'react-immutable-proptypes'; | |||
import Toggle from 'react-toggle'; | |||
import { injectIntl, defineMessages } from 'react-intl'; | |||
// Mastodon imports // | |||
import IconButton from '../../../../mastodon/components/icon_button'; | |||
// Our imports // | |||
import ComposeAdvancedOptionsToggle from './toggle'; | |||
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * | |||
/* | |||
Inital setup: | |||
------------- | |||
The `messages` constant is used to define any messages that we need | |||
from inside props. These are the various titles and labels on our | |||
toggles. | |||
`iconStyle` styles the icon used for the dropdown button. | |||
*/ | |||
const messages = defineMessages({ | |||
local_only_short: { id: 'advanced-options.local-only.short', defaultMessage: 'Local-only' }, | |||
local_only_long: { id: 'advanced-options.local-only.long', defaultMessage: 'Do not post to other instances' }, | |||
advanced_options_icon_title: { id: 'advanced_options.icon_title', defaultMessage: 'Advanced options' }, | |||
local_only_short : | |||
{ id: 'advanced-options.local-only.short', defaultMessage: 'Local-only' }, | |||
local_only_long : | |||
{ id: 'advanced-options.local-only.long', defaultMessage: 'Do not post to other instances' }, | |||
advanced_options_icon_title : | |||
{ id: 'advanced_options.icon_title', defaultMessage: 'Advanced options' }, | |||
}); | |||
const iconStyle = { | |||
height: null, | |||
lineHeight: '27px', | |||
height : null, | |||
lineHeight : '27px', | |||
}; | |||
class AdvancedOptionToggle extends React.PureComponent { | |||
static propTypes = { | |||
onChange: PropTypes.func.isRequired, | |||
active: PropTypes.bool.isRequired, | |||
name: PropTypes.string.isRequired, | |||
shortText: PropTypes.string.isRequired, | |||
longText: PropTypes.string.isRequired, | |||
} | |||
onToggle = () => { | |||
this.props.onChange(this.props.name); | |||
} | |||
/* | |||
render() { | |||
const { active, shortText, longText } = this.props; | |||
return ( | |||
<div role='button' tabIndex='0' className='advanced-options-dropdown__option' onClick={this.onToggle}> | |||
<div className='advanced-options-dropdown__option__toggle'> | |||
<Toggle checked={active} onChange={this.onToggle} /> | |||
</div> | |||
<div className='advanced-options-dropdown__option__content'> | |||
<strong>{shortText}</strong> | |||
{longText} | |||
</div> | |||
</div> | |||
); | |||
} | |||
Implementation: | |||
--------------- | |||
} | |||
*/ | |||
@injectIntl | |||
export default class ComposeAdvancedOptions extends React.PureComponent { | |||
static propTypes = { | |||
values: ImmutablePropTypes.contains({ | |||
do_not_federate: PropTypes.bool.isRequired, | |||
values : ImmutablePropTypes.contains({ | |||
do_not_federate : PropTypes.bool.isRequired, | |||
}).isRequired, | |||
onChange: PropTypes.func.isRequired, | |||
intl: PropTypes.object.isRequired, | |||
onChange : PropTypes.func.isRequired, | |||
intl : PropTypes.object.isRequired, | |||
}; | |||
state = { | |||
open: false, | |||
}; | |||
/* | |||
### `onToggleDropdown()` | |||
This function toggles the opening and closing of the advanced options | |||
dropdown. | |||
*/ | |||
onToggleDropdown = () => { | |||
this.setState({ open: !this.state.open }); | |||
}; | |||
/* | |||
### `onGlobalClick(e)` | |||
This function closes the advanced options dropdown if you click | |||
anywhere else on the screen. | |||
*/ | |||
onGlobalClick = (e) => { | |||
if (e.target !== this.node && !this.node.contains(e.target) && this.state.open) { | |||
this.setState({ open: false }); | |||
} | |||
} | |||
/* | |||
### `componentDidMount()`, `componentWillUnmount()` | |||
This function closes the advanced options dropdown if you click | |||
anywhere else on the screen. | |||
*/ | |||
componentDidMount () { | |||
window.addEventListener('click', this.onGlobalClick); | |||
window.addEventListener('touchstart', this.onGlobalClick); | |||
} | |||
componentWillUnmount () { | |||
window.removeEventListener('click', this.onGlobalClick); | |||
window.removeEventListener('touchstart', this.onGlobalClick); | |||
} | |||
state = { | |||
open: false, | |||
}; | |||
/* | |||
handleClick = (e) => { | |||
const option = e.currentTarget.getAttribute('data-index'); | |||
e.preventDefault(); | |||
this.props.onChange(option); | |||
} | |||
### `setRef(c)` | |||
`setRef()` stores a reference to the dropdown's `<div> in `this.node`. | |||
*/ | |||
setRef = (c) => { | |||
this.node = c; | |||
} | |||
/* | |||
### `render()` | |||
`render()` actually puts our component on the screen. | |||
*/ | |||
render () { | |||
const { open } = this.state; | |||
const { intl, values } = this.props; | |||
/* | |||
The `options` array provides all of the available advanced options | |||
alongside their icon, text, and name. | |||
*/ | |||
const options = [ | |||
{ icon: 'wifi', shortText: messages.local_only_short, longText: messages.local_only_long, key: 'do_not_federate' }, | |||
{ icon: 'wifi', shortText: messages.local_only_short, longText: messages.local_only_long, name: 'do_not_federate' }, | |||
]; | |||
/* | |||
`anyEnabled` tells us if any of our advanced options have been enabled. | |||
*/ | |||
const anyEnabled = values.some((enabled) => enabled); | |||
/* | |||
`optionElems` takes our `options` and creates | |||
`<ComposeAdvancedOptionsToggle>`s out of them. We use the `name` of the | |||
toggle as its `key` so that React can keep track of it. | |||
*/ | |||
const optionElems = options.map((option) => { | |||
return ( | |||
<AdvancedOptionToggle | |||
<ComposeAdvancedOptionsToggle | |||
onChange={this.props.onChange} | |||
active={values.get(option.key)} | |||
key={option.key} | |||
name={option.key} | |||
active={values.get(option.name)} | |||
key={option.name} | |||
name={option.name} | |||
shortText={intl.formatMessage(option.shortText)} | |||
longText={intl.formatMessage(option.longText)} | |||
/> | |||
); | |||
}); | |||
return (<div ref={this.setRef} className={`advanced-options-dropdown ${open ? 'open' : ''} ${anyEnabled ? 'active' : ''} `}> | |||
<div className='advanced-options-dropdown__value'> | |||
<IconButton | |||
className='advanced-options-dropdown__value' | |||
title={intl.formatMessage(messages.advanced_options_icon_title)} | |||
icon='ellipsis-h' active={open || anyEnabled} | |||
size={18} | |||
style={iconStyle} | |||
onClick={this.onToggleDropdown} | |||
/> | |||
</div> | |||
<div className='advanced-options-dropdown__dropdown'> | |||
{optionElems} | |||
/* | |||
Finally, we can render our component. | |||
*/ | |||
return ( | |||
<div ref={this.setRef} className={`advanced-options-dropdown ${open ? 'open' : ''} ${anyEnabled ? 'active' : ''} `}> | |||
<div className='advanced-options-dropdown__value'> | |||
<IconButton | |||
className='advanced-options-dropdown__value' | |||
title={intl.formatMessage(messages.advanced_options_icon_title)} | |||
icon='ellipsis-h' active={open || anyEnabled} | |||
size={18} | |||
style={iconStyle} | |||
onClick={this.onToggleDropdown} | |||
/> | |||
</div> | |||
<div className='advanced-options-dropdown__dropdown'> | |||
{optionElems} | |||
</div> | |||
</div> | |||
</div>); | |||
); | |||
} | |||
} |
@ -0,0 +1,103 @@ | |||
/* | |||
`<ComposeAdvancedOptionsToggle>` | |||
================================ | |||
> For more information on the contents of this file, please contact: | |||
> | |||
> - surinna [@srn@dev.glitch.social] | |||
This creates the toggle used by `<ComposeAdvancedOptions>`. | |||
__Props:__ | |||
- __`onChange` (`PropTypes.func`) :__ | |||
This provides the function to call when the toggle is | |||
(de-?)activated. | |||
- __`active` (`PropTypes.bool`) :__ | |||
This prop controls whether the toggle is currently active or not. | |||
- __`name` (`PropTypes.string`) :__ | |||
This identifies the toggle, and is sent to `onChange()` when it is | |||
called. | |||
- __`shortText` (`PropTypes.string`) :__ | |||
This is a short string used as the title of the toggle. | |||
- __`longText` (`PropTypes.string`) :__ | |||
This is a longer string used as a subtitle for the toggle. | |||
*/ | |||
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * | |||
/* | |||
Imports: | |||
-------- | |||
*/ | |||
// Package imports // | |||
import React from 'react'; | |||
import PropTypes from 'prop-types'; | |||
import Toggle from 'react-toggle'; | |||
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * | |||
/* | |||
Implementation: | |||
--------------- | |||
*/ | |||
export default class ComposeAdvancedOptionsToggle extends React.PureComponent { | |||
static propTypes = { | |||
onChange: PropTypes.func.isRequired, | |||
active: PropTypes.bool.isRequired, | |||
name: PropTypes.string.isRequired, | |||
shortText: PropTypes.string.isRequired, | |||
longText: PropTypes.string.isRequired, | |||
} | |||
/* | |||
### `onToggle()` | |||
The `onToggle()` function simply calls the `onChange()` prop with the | |||
toggle's `name`. | |||
*/ | |||
onToggle = () => { | |||
this.props.onChange(this.props.name); | |||
} | |||
/* | |||
### `render()` | |||
The `render()` function is used to render our component. We just render | |||
a `<Toggle>` and place next to it our text. | |||
*/ | |||
render() { | |||
const { active, shortText, longText } = this.props; | |||
return ( | |||
<div role='button' tabIndex='0' className='advanced-options-dropdown__option' onClick={this.onToggle}> | |||
<div className='advanced-options-dropdown__option__toggle'> | |||
<Toggle checked={active} onChange={this.onToggle} /> | |||
</div> | |||
<div className='advanced-options-dropdown__option__content'> | |||
<strong>{shortText}</strong> | |||
{longText} | |||
</div> | |||
</div> | |||
); | |||
} | |||
} |
@ -0,0 +1,171 @@ | |||
/* | |||
`<NotificationFollow>` | |||
====================== | |||
This component renders a follow notification. | |||
__Props:__ | |||
- __`id` (`PropTypes.number.isRequired`) :__ | |||
This is the id of the notification. | |||
- __`onDeleteNotification` (`PropTypes.func.isRequired`) :__ | |||
The function to call when a notification should be | |||
dismissed/deleted. | |||
- __`account` (`PropTypes.object.isRequired`) :__ | |||
The account associated with the follow notification, ie the account | |||
which followed the user. | |||
- __`intl` (`PropTypes.object.isRequired`) :__ | |||
Our internationalization object, inserted by `@injectIntl`. | |||
*/ | |||
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * | |||
/* | |||
Imports: | |||
-------- | |||
*/ | |||
// Package imports // | |||
import React from 'react'; | |||
import ImmutablePropTypes from 'react-immutable-proptypes'; | |||
import PropTypes from 'prop-types'; | |||
import { defineMessages, FormattedMessage, injectIntl } from 'react-intl'; | |||
import escapeTextContentForBrowser from 'escape-html'; | |||
import ImmutablePureComponent from 'react-immutable-pure-component'; | |||
// Mastodon imports // | |||
import emojify from '../../../mastodon/emoji'; | |||
import Permalink from '../../../mastodon/components/permalink'; | |||
import AccountContainer from '../../../mastodon/containers/account_container'; | |||
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * | |||
/* | |||
Inital setup: | |||
------------- | |||
The `messages` constant is used to define any messages that we need | |||
from inside props. | |||
*/ | |||
const messages = defineMessages({ | |||
deleteNotification : | |||
{ id: 'status.dismiss_notification', defaultMessage: 'Dismiss notification' }, | |||
}); | |||
/* | |||
Implementation: | |||
--------------- | |||
*/ | |||
@injectIntl | |||
export default class NotificationFollow extends ImmutablePureComponent { | |||
static propTypes = { | |||
id : PropTypes.number.isRequired, | |||
onDeleteNotification : PropTypes.func.isRequired, | |||
account : ImmutablePropTypes.map.isRequired, | |||
intl : PropTypes.object.isRequired, | |||
}; | |||
/* | |||
### `handleNotificationDeleteClick()` | |||
This function just calls our `onDeleteNotification()` prop with the | |||
notification's `id`. | |||
*/ | |||
handleNotificationDeleteClick = () => { | |||
this.props.onDeleteNotification(this.props.id); | |||
} | |||
/* | |||
### `render()` | |||
This actually renders the component. | |||
*/ | |||
render () { | |||
const { account, intl } = this.props; | |||
/* | |||
`dismiss` creates the notification dismissal button. Its title is given | |||
by `dismissTitle`. | |||
*/ | |||
const dismissTitle = intl.formatMessage(messages.deleteNotification); | |||
const dismiss = ( | |||
<button | |||
aria-label={dismissTitle} | |||
title={dismissTitle} | |||
onClick={this.handleNotificationDeleteClick} | |||
className='status__prepend-dismiss-button' | |||
> | |||
<i className='fa fa-eraser' /> | |||
</button> | |||
); | |||
/* | |||
`link` is a container for the account's `displayName`, which links to | |||
the account timeline using a `<Permalink>`. | |||
*/ | |||
const displayName = account.get('display_name') || account.get('username'); | |||
const displayNameHTML = { __html: emojify(escapeTextContentForBrowser(displayName)) }; | |||
const link = ( | |||
<Permalink | |||
className='notification__display-name' | |||
href={account.get('url')} | |||
title={account.get('acct')} | |||
to={`/accounts/${account.get('id')}`} | |||
dangerouslySetInnerHTML={displayNameHTML} | |||
/> | |||
); | |||
/* | |||
We can now render our component. | |||
*/ | |||
return ( | |||
<div className='notification notification-follow'> | |||
<div className='notification__message'> | |||
<div className='notification__favourite-icon-wrapper'> | |||
<i className='fa fa-fw fa-user-plus' /> | |||
</div> | |||
<FormattedMessage | |||
id='notification.follow' | |||
defaultMessage='{name} followed you' | |||
values={{ name: link }} | |||
/> | |||
{dismiss} | |||
</div> | |||
<AccountContainer id={account.get('id')} withNote={false} /> | |||
</div> | |||
); | |||
} | |||
} |
@ -1,78 +0,0 @@ | |||
// Package imports // | |||
import React from 'react'; | |||
import ImmutablePropTypes from 'react-immutable-proptypes'; | |||
import PropTypes from 'prop-types'; | |||
import { defineMessages, FormattedMessage, injectIntl } from 'react-intl'; | |||
import escapeTextContentForBrowser from 'escape-html'; | |||
import ImmutablePureComponent from 'react-immutable-pure-component'; | |||
// Mastodon imports // | |||
import emojify from '../../../mastodon/emoji'; | |||
import Permalink from '../../../mastodon/components/permalink'; | |||
import AccountContainer from '../../../mastodon/containers/account_container'; | |||
const messages = defineMessages({ | |||
deleteNotification: { id: 'status.dismiss_notification', defaultMessage: 'Dismiss notification' }, | |||
}); | |||
@injectIntl | |||
export default class FollowNotification extends ImmutablePureComponent { | |||
static contextTypes = { | |||
router: PropTypes.object, | |||
}; | |||
static propTypes = { | |||
notificationId: PropTypes.number.isRequired, | |||
onDeleteNotification: PropTypes.func.isRequired, | |||
account: ImmutablePropTypes.map.isRequired, | |||
intl: PropTypes.object.isRequired, | |||
}; | |||
// Avoid checking props that are functions (and whose equality will always | |||
// evaluate to false. See react-immutable-pure-component for usage. | |||
updateOnProps = [ | |||
'account', | |||
] | |||
handleNotificationDeleteClick = () => { | |||
this.props.onDeleteNotification(this.props.notificationId); | |||
} | |||
render () { | |||
const { account, intl } = this.props; | |||
const dismissTitle = intl.formatMessage(messages.deleteNotification); | |||
const dismiss = ( | |||
<button | |||
aria-label={dismissTitle} | |||
title={dismissTitle} | |||
onClick={this.handleNotificationDeleteClick} | |||
className='status__prepend-dismiss-button' | |||
> | |||
<i className='fa fa-eraser' /> | |||
</button> | |||
); | |||
const displayName = account.get('display_name').length > 0 ? account.get('display_name') : account.get('username'); | |||
const displayNameHTML = { __html: emojify(escapeTextContentForBrowser(displayName)) }; | |||
const link = <Permalink className='notification__display-name' href={account.get('url')} title={account.get('acct')} to={`/accounts/${account.get('id')}`} dangerouslySetInnerHTML={displayNameHTML} />; | |||
return ( | |||
<div className='notification notification-follow'> | |||
<div className='notification__message'> | |||
<div className='notification__favourite-icon-wrapper'> | |||
<i className='fa fa-fw fa-user-plus' /> | |||
</div> | |||
<FormattedMessage id='notification.follow' defaultMessage='{name} followed you' values={{ name: link }} /> | |||
{dismiss} | |||
</div> | |||
<AccountContainer id={account.get('id')} withNote={false} /> | |||
</div> | |||
); | |||
} | |||
} |