Documentation and cleanup

master
kibigo! 7 years ago
parent 21b04af524
commit d0aad1ac85
  1. 18
      app/javascript/glitch/actions/local_settings.js
  2. 163
      app/javascript/glitch/components/account/header.js
  3. 44
      app/javascript/glitch/components/compose/advanced_options/container.js
  4. 236
      app/javascript/glitch/components/compose/advanced_options/index.js
  5. 103
      app/javascript/glitch/components/compose/advanced_options/toggle.js
  6. 47
      app/javascript/glitch/components/notification/container.js
  7. 171
      app/javascript/glitch/components/notification/follow.js
  8. 78
      app/javascript/glitch/components/notification/follow_notification.js
  9. 6
      app/javascript/glitch/components/notification/index.js
  10. 24
      app/javascript/glitch/reducers/local_settings.js
  11. 4
      app/javascript/glitch/util/bio_metadata.js

@ -21,12 +21,12 @@ consists of the following:
*/ */
/* * * * */ // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
/* /*
Constants Constants:
--------- ----------
We provide the following constants: We provide the following constants:
@ -39,12 +39,12 @@ We provide the following constants:
export const LOCAL_SETTING_CHANGE = 'LOCAL_SETTING_CHANGE'; export const LOCAL_SETTING_CHANGE = 'LOCAL_SETTING_CHANGE';
/* * * * */ // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
/* /*
`changeLocalSetting(key, value)` `changeLocalSetting(key, value)`:
-------------------------------- ---------------------------------
Changes the local setting with the given `key` to the given `value`. Changes the local setting with the given `key` to the given `value`.
`key` **MUST** be an array of strings, as required by `key` **MUST** be an array of strings, as required by
@ -67,12 +67,12 @@ export function changeLocalSetting(key, value) {
}; };
}; };
/* * * * */ // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
/* /*
`saveLocalSettings()` `saveLocalSettings()`:
--------------------- ----------------------
Saves the local settings to `localStorage` as a JSON object. Saves the local settings to `localStorage` as a JSON object.
`changeLocalSetting()` calls this whenever it changes a setting. We `changeLocalSetting()` calls this whenever it changes a setting. We

@ -1,3 +1,45 @@
/*
`<AccountHeader>`
=================
> For more information on the contents of this file, please contact:
>
> - kibigo! [@kibi@glitch.social]
Original file by @gargron@mastodon.social et al as part of
tootsuite/mastodon. We've expanded it in order to handle user bio
frontmatter.
The `<AccountHeader>` component provides the header for account
timelines. It is a fairly simple component which mostly just consists
of a `render()` method.
__Props:__
- __`account` (`ImmutablePropTypes.map`) :__
The account to render a header for.
- __`me` (`PropTypes.number.isRequired`) :__
The id of the currently-signed-in account.
- __`onFollow` (`PropTypes.func.isRequired`) :__
The function to call when the user clicks the "follow" button.
- __`intl` (`PropTypes.object.isRequired`) :__
Our internationalization object, inserted by `@injectIntl`.
*/
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
/*
Imports:
--------
*/
// Package imports // // Package imports //
import React from 'react'; import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
@ -14,25 +56,63 @@ import Avatar from '../../../mastodon/components/avatar';
// Our imports // // Our imports //
import { processBio } from '../../util/bio_metadata'; import { processBio } from '../../util/bio_metadata';
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
/*
Inital setup:
-------------
The `messages` constant is used to define any messages that we need
from inside props. In our case, these are the `unfollow`, `follow`, and
`requested` messages used in the `title` of our buttons.
*/
const messages = defineMessages({ const messages = defineMessages({
unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' }, unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
follow: { id: 'account.follow', defaultMessage: 'Follow' }, follow: { id: 'account.follow', defaultMessage: 'Follow' },
requested: { id: 'account.requested', defaultMessage: 'Awaiting approval' }, requested: { id: 'account.requested', defaultMessage: 'Awaiting approval' },
}); });
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
/*
Implementation:
---------------
*/
@injectIntl @injectIntl
export default class Header extends ImmutablePureComponent { export default class AccountHeader extends ImmutablePureComponent {
static propTypes = { static propTypes = {
account: ImmutablePropTypes.map, account : ImmutablePropTypes.map,
me: PropTypes.number.isRequired, me : PropTypes.number.isRequired,
onFollow: PropTypes.func.isRequired, onFollow : PropTypes.func.isRequired,
intl: PropTypes.object.isRequired, intl : PropTypes.object.isRequired,
}; };
/*
### `render()`
The `render()` function is used to render our component.
*/
render () { render () {
const { account, me, intl } = this.props; const { account, me, intl } = this.props;
/*
If no `account` is provided, then we can't render a header. Otherwise,
we get the `displayName` for the account, if available. If it's blank,
then we set the `displayName` to just be the `username` of the account.
*/
if (!account) { if (!account) {
return null; return null;
} }
@ -40,17 +120,30 @@ export default class Header extends ImmutablePureComponent {
let displayName = account.get('display_name'); let displayName = account.get('display_name');
let info = ''; let info = '';
let actionBtn = ''; let actionBtn = '';
let lockedIcon = ''; let following = false;
if (displayName.length === 0) { if (displayName.length === 0) {
displayName = account.get('username'); displayName = account.get('username');
} }
if (me !== account.get('id') && account.getIn(['relationship', 'followed_by'])) { /*
info = <span className='account--follows-info'><FormattedMessage id='account.follows_you' defaultMessage='Follows you' /></span>;
} Next, we handle the account relationships. If the account follows the
user, then we add an `info` message. If the user has requested a
follow, then we disable the `actionBtn` and display an hourglass.
Otherwise, if the account isn't blocked, we set the `actionBtn` to the
appropriate icon.
*/
if (me !== account.get('id')) { if (me !== account.get('id')) {
if (account.getIn(['relationship', 'followed_by'])) {
info = (
<span className='account--follows-info'>
<FormattedMessage id='account.follows_you' defaultMessage='Follows you' />
</span>
);
}
if (account.getIn(['relationship', 'requested'])) { if (account.getIn(['relationship', 'requested'])) {
actionBtn = ( actionBtn = (
<div className='account--action-button'> <div className='account--action-button'>
@ -58,30 +151,64 @@ export default class Header extends ImmutablePureComponent {
</div> </div>
); );
} else if (!account.getIn(['relationship', 'blocking'])) { } else if (!account.getIn(['relationship', 'blocking'])) {
following = account.getIn(['relationship', 'following']);
actionBtn = ( actionBtn = (
<div className='account--action-button'> <div className='account--action-button'>
<IconButton size={26} icon={account.getIn(['relationship', 'following']) ? 'user-times' : 'user-plus'} active={account.getIn(['relationship', 'following'])} title={intl.formatMessage(account.getIn(['relationship', 'following']) ? messages.unfollow : messages.follow)} onClick={this.props.onFollow} /> <IconButton
size={26}
icon={following ? 'user-times' : 'user-plus'}
active={following}
title={intl.formatMessage(following ? messages.unfollow : messages.follow)}
onClick={this.props.onFollow}
/>
</div> </div>
); );
} }
} }
if (account.get('locked')) { /*
lockedIcon = <i className='fa fa-lock' />;
}
const displayNameHTML = { __html: emojify(escapeTextContentForBrowser(displayName)) }; `displayNameHTML` processes the `displayName` and prepares it for
insertion into the document. Meanwhile, we extract the `text` and
`metadata` from our account's `note` using `processBio()`.
*/
const displayNameHTML = {
__html : emojify(escapeTextContentForBrowser(displayName)),
};
const { text, metadata } = processBio(account.get('note')); const { text, metadata } = processBio(account.get('note'));
/*
Here, we render our component using all the things we've defined above.
*/
return ( return (
<div className='account__header__wrapper'> <div className='account__header__wrapper'>
<div className='account__header' style={{ backgroundImage: `url(${account.get('header')})` }}> <div
className='account__header'
style={{ backgroundImage: `url(${account.get('header')})` }}
>
<div> <div>
<a href={account.get('url')} target='_blank' rel='noopener'> <a href={account.get('url')} target='_blank' rel='noopener'>
<span className='account__header__avatar'><Avatar src={account.get('avatar')} staticSrc={account.get('avatar_static')} size={90} /></span> <span className='account__header__avatar'>
<span className='account__header__display-name' dangerouslySetInnerHTML={displayNameHTML} /> <Avatar
src={account.get('avatar')}
staticSrc={account.get('avatar_static')}
size={90}
/>
</span>
<span
className='account__header__display-name'
dangerouslySetInnerHTML={displayNameHTML}
/>
</a> </a>
<span className='account__header__username'>@{account.get('acct')} {lockedIcon}</span> <span className='account__header__username'>
@{account.get('acct')}
{account.get('locked') ? <i className='fa fa-lock' /> : null}
</span>
<div className='account__header__content' dangerouslySetInnerHTML={{ __html: emojify(text) }} /> <div className='account__header__content' dangerouslySetInnerHTML={{ __html: emojify(text) }} />
{info} {info}

@ -1,3 +1,21 @@
/*
`<ComposeAdvancedOptionsContainer>`
===================================
This container connects `<ComposeAdvancedOptions>` to the Redux store.
*/
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
/*
Imports:
--------
*/
// Package imports // // Package imports //
import { connect } from 'react-redux'; import { connect } from 'react-redux';
@ -7,10 +25,36 @@ import { toggleComposeAdvancedOption } from '../../../../mastodon/actions/compos
// Our imports // // Our imports //
import ComposeAdvancedOptions from '.'; import ComposeAdvancedOptions from '.';
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
/*
State mapping:
--------------
The `mapStateToProps()` function maps various state properties to the
props of our component. The only property we care about is
`compose.advanced_options`.
*/
const mapStateToProps = state => ({ const mapStateToProps = state => ({
values: state.getIn(['compose', 'advanced_options']), values: state.getIn(['compose', 'advanced_options']),
}); });
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
/*
Dispatch mapping:
-----------------
The `mapDispatchToProps()` function maps dispatches to our store to the
various props of our component. We just need to provide a dispatch for
when an advanced option toggle changes.
*/
const mapDispatchToProps = dispatch => ({ const mapDispatchToProps = dispatch => ({
onChange (option) { onChange (option) {

@ -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 // // Package imports //
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import Toggle from 'react-toggle';
import { injectIntl, defineMessages } from 'react-intl'; import { injectIntl, defineMessages } from 'react-intl';
// Mastodon imports // // Mastodon imports //
import IconButton from '../../../../mastodon/components/icon_button'; 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({ const messages = defineMessages({
local_only_short: { id: 'advanced-options.local-only.short', defaultMessage: 'Local-only' }, local_only_short :
local_only_long: { id: 'advanced-options.local-only.long', defaultMessage: 'Do not post to other instances' }, { id: 'advanced-options.local-only.short', defaultMessage: 'Local-only' },
advanced_options_icon_title: { id: 'advanced_options.icon_title', defaultMessage: 'Advanced options' }, 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 = { const iconStyle = {
height: null, height : null,
lineHeight: '27px', 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() { Implementation:
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>
);
}
} */
@injectIntl @injectIntl
export default class ComposeAdvancedOptions extends React.PureComponent { export default class ComposeAdvancedOptions extends React.PureComponent {
static propTypes = { static propTypes = {
values: ImmutablePropTypes.contains({ values : ImmutablePropTypes.contains({
do_not_federate: PropTypes.bool.isRequired, do_not_federate : PropTypes.bool.isRequired,
}).isRequired, }).isRequired,
onChange: PropTypes.func.isRequired, onChange : PropTypes.func.isRequired,
intl: PropTypes.object.isRequired, intl : PropTypes.object.isRequired,
}; };
state = {
open: false,
};
/*
### `onToggleDropdown()`
This function toggles the opening and closing of the advanced options
dropdown.
*/
onToggleDropdown = () => { onToggleDropdown = () => {
this.setState({ open: !this.state.open }); 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) => { onGlobalClick = (e) => {
if (e.target !== this.node && !this.node.contains(e.target) && this.state.open) { if (e.target !== this.node && !this.node.contains(e.target) && this.state.open) {
this.setState({ open: false }); this.setState({ open: false });
} }
} }
/*
### `componentDidMount()`, `componentWillUnmount()`
This function closes the advanced options dropdown if you click
anywhere else on the screen.
*/
componentDidMount () { componentDidMount () {
window.addEventListener('click', this.onGlobalClick); window.addEventListener('click', this.onGlobalClick);
window.addEventListener('touchstart', this.onGlobalClick); window.addEventListener('touchstart', this.onGlobalClick);
} }
componentWillUnmount () { componentWillUnmount () {
window.removeEventListener('click', this.onGlobalClick); window.removeEventListener('click', this.onGlobalClick);
window.removeEventListener('touchstart', this.onGlobalClick); window.removeEventListener('touchstart', this.onGlobalClick);
} }
state = { /*
open: false,
};
handleClick = (e) => { ### `setRef(c)`
const option = e.currentTarget.getAttribute('data-index');
e.preventDefault(); `setRef()` stores a reference to the dropdown's `<div> in `this.node`.
this.props.onChange(option);
} */
setRef = (c) => { setRef = (c) => {
this.node = c; this.node = c;
} }
/*
### `render()`
`render()` actually puts our component on the screen.
*/
render () { render () {
const { open } = this.state; const { open } = this.state;
const { intl, values } = this.props; const { intl, values } = this.props;
/*
The `options` array provides all of the available advanced options
alongside their icon, text, and name.
*/
const options = [ 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); 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) => { const optionElems = options.map((option) => {
return ( return (
<AdvancedOptionToggle <ComposeAdvancedOptionsToggle
onChange={this.props.onChange} onChange={this.props.onChange}
active={values.get(option.key)} active={values.get(option.name)}
key={option.key} key={option.name}
name={option.key} name={option.name}
shortText={intl.formatMessage(option.shortText)} shortText={intl.formatMessage(option.shortText)}
longText={intl.formatMessage(option.longText)} 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 Finally, we can render our component.
className='advanced-options-dropdown__value'
title={intl.formatMessage(messages.advanced_options_icon_title)} */
icon='ellipsis-h' active={open || anyEnabled}
size={18} return (
style={iconStyle} <div ref={this.setRef} className={`advanced-options-dropdown ${open ? 'open' : ''} ${anyEnabled ? 'active' : ''} `}>
onClick={this.onToggleDropdown} <div className='advanced-options-dropdown__value'>
/> <IconButton
</div> className='advanced-options-dropdown__value'
<div className='advanced-options-dropdown__dropdown'> title={intl.formatMessage(messages.advanced_options_icon_title)}
{optionElems} icon='ellipsis-h' active={open || anyEnabled}
size={18}
style={iconStyle}
onClick={this.onToggleDropdown}
/>
</div>
<div className='advanced-options-dropdown__dropdown'>
{optionElems}
</div>
</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>
);
}
}

@ -1,3 +1,21 @@
/*
`<NotificationContainer>`
=========================
This container connects `<Notification>`s to the Redux store.
*/
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
/*
Imports:
--------
*/
// Package imports // // Package imports //
import { connect } from 'react-redux'; import { connect } from 'react-redux';
@ -8,6 +26,20 @@ import { makeGetNotification } from '../../../mastodon/selectors';
import Notification from '.'; import Notification from '.';
import { deleteNotification } from '../../../mastodon/actions/notifications'; import { deleteNotification } from '../../../mastodon/actions/notifications';
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
/*
State mapping:
--------------
The `mapStateToProps()` function maps various state properties to the
props of our component. We wrap this in `makeMapStateToProps()` so that
we only have to call `makeGetNotification()` once instead of every
time.
*/
const makeMapStateToProps = () => { const makeMapStateToProps = () => {
const getNotification = makeGetNotification(); const getNotification = makeGetNotification();
@ -19,7 +51,20 @@ const makeMapStateToProps = () => {
return mapStateToProps; return mapStateToProps;
}; };
const mapDispatchToProps = (dispatch) => ({ // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
/*
Dispatch mapping:
-----------------
The `mapDispatchToProps()` function maps dispatches to our store to the
various props of our component. We only need to provide a dispatch for
deleting notifications.
*/
const mapDispatchToProps = dispatch => ({
onDeleteNotification (id) { onDeleteNotification (id) {
dispatch(deleteNotification(id)); dispatch(deleteNotification(id));
}, },

@ -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>
);
}
}

@ -8,7 +8,7 @@ import PropTypes from 'prop-types';
// Our imports // // Our imports //
import StatusContainer from '../status/container'; import StatusContainer from '../status/container';
import FollowNotification from './follow_notification'; import NotificationFollow from './follow';
export default class Notification extends ImmutablePureComponent { export default class Notification extends ImmutablePureComponent {
@ -20,8 +20,8 @@ export default class Notification extends ImmutablePureComponent {
renderFollow (notification) { renderFollow (notification) {
return ( return (
<FollowNotification <NotificationFollow
notificationId={notification.get('id')} id={notification.get('id')}
account={notification.get('account')} account={notification.get('account')}
onDeleteNotification={this.props.onDeleteNotification} onDeleteNotification={this.props.onDeleteNotification}
/> />

@ -18,12 +18,12 @@ associated actions are:
*/ */
/* * * * */ // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
/* /*
Imports Imports:
------- --------
*/ */
@ -36,12 +36,12 @@ import { STORE_HYDRATE } from '../../mastodon/actions/store';
// Our imports // // Our imports //
import { LOCAL_SETTING_CHANGE } from '../actions/local_settings'; import { LOCAL_SETTING_CHANGE } from '../actions/local_settings';
/* * * * */ // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
/* /*
initialState initialState:
------------ -------------
You can see the default values for all of our local settings here. You can see the default values for all of our local settings here.
These are only used if no previously-saved values exist. These are only used if no previously-saved values exist.
@ -71,12 +71,12 @@ const initialState = ImmutableMap({
}), }),
}); });
/* * * * */ // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
/* /*
Helper functions Helper functions:
---------------- -----------------
### `hydrate(state, localSettings)` ### `hydrate(state, localSettings)`
@ -89,12 +89,12 @@ from `localStorage`.
const hydrate = (state, localSettings) => state.mergeDeep(localSettings); const hydrate = (state, localSettings) => state.mergeDeep(localSettings);
/* * * * */ // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
/* /*
`localSettings(state = initialState, action)` `localSettings(state = initialState, action)`:
--------------------------------------------- ----------------------------------------------
This function holds our actual reducer. This function holds our actual reducer.

@ -1,7 +1,7 @@
/* /*
`util/bio_metadata` `util/bio_metadata`
======================== ===================
> For more information on the contents of this file, please contact: > For more information on the contents of this file, please contact:
> >
@ -26,7 +26,7 @@ functions are:
*/ */
/* * * * */ // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
/*********************************************************************\ /*********************************************************************\

Loading…
Cancel
Save