diff --git a/app/javascript/glitch/actions/local_settings.js b/app/javascript/glitch/actions/local_settings.js deleted file mode 100644 index 93c5a9a171f..00000000000 --- a/app/javascript/glitch/actions/local_settings.js +++ /dev/null @@ -1,93 +0,0 @@ -/* - -`actions/local_settings` -======================== - -> For more information on the contents of this file, please contact: -> -> - kibigo! [@kibi@glitch.social] - -This file provides our Redux actions related to local settings. It -consists of the following: - - - __`changesLocalSetting(key, value)` :__ - Changes the local setting with the given `key` to the given - `value`. `key` **MUST** be an array of strings, as required by - `Immutable.Map.prototype.getIn()`. - - - __`saveLocalSettings()` :__ - Saves the local settings to `localStorage` as a JSON object. We - shouldn't ever need to call this ourselves. - -*/ - -// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * - -/* - -Constants: ----------- - -We provide the following constants: - - - __`LOCAL_SETTING_CHANGE` :__ - This string constant is used to dispatch a setting change to our - reducer in `reducers/local_settings`, where the setting is - actually changed. - -*/ - -export const LOCAL_SETTING_CHANGE = 'LOCAL_SETTING_CHANGE'; - -// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * - -/* - -`changeLocalSetting(key, value)`: ---------------------------------- - -Changes the local setting with the given `key` to the given `value`. -`key` **MUST** be an array of strings, as required by -`Immutable.Map.prototype.getIn()`. - -To accomplish this, we just dispatch a `LOCAL_SETTING_CHANGE` to our -reducer in `reducers/local_settings`. - -*/ - -export function changeLocalSetting(key, value) { - return dispatch => { - dispatch({ - type: LOCAL_SETTING_CHANGE, - key, - value, - }); - - dispatch(saveLocalSettings()); - }; -}; - -// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * - -/* - -`saveLocalSettings()`: ----------------------- - -Saves the local settings to `localStorage` as a JSON object. -`changeLocalSetting()` calls this whenever it changes a setting. We -shouldn't ever need to call this ourselves. - -> __TODO :__ -> Right now `saveLocalSettings()` doesn't keep track of which user -> is currently signed in, but it might be better to give each user -> their *own* local settings. - -*/ - -export function saveLocalSettings() { - return (_, getState) => { - const localSettings = getState().get('local_settings').toJS(); - localStorage.setItem('mastodon-settings', JSON.stringify(localSettings)); - }; -}; diff --git a/app/javascript/glitch/components/account/header.js b/app/javascript/glitch/components/account/header.js deleted file mode 100644 index 7bc1a2189bb..00000000000 --- a/app/javascript/glitch/components/account/header.js +++ /dev/null @@ -1,227 +0,0 @@ -/* - -`` -================= - -> 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 `` 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 // -import React from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import PropTypes from 'prop-types'; -import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; -import ImmutablePureComponent from 'react-immutable-pure-component'; - -// Mastodon imports // -import emojify from '../../../mastodon/features/emoji/emoji'; -import IconButton from '../../../mastodon/components/icon_button'; -import Avatar from '../../../mastodon/components/avatar'; -import { me } from '../../../mastodon/initial_state'; - -// Our imports // -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({ - unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' }, - follow: { id: 'account.follow', defaultMessage: 'Follow' }, - requested: { id: 'account.requested', defaultMessage: 'Awaiting approval' }, -}); - -// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * - -/* - -Implementation: ---------------- - -*/ - -@injectIntl -export default class AccountHeader extends ImmutablePureComponent { - - static propTypes = { - account : ImmutablePropTypes.map, - onFollow : PropTypes.func.isRequired, - intl : PropTypes.object.isRequired, - }; - -/* - -### `render()` - -The `render()` function is used to render our component. - -*/ - - render () { - const { account, 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) { - return null; - } - - let displayName = account.get('display_name_html'); - let info = ''; - let actionBtn = ''; - let following = false; - -/* - -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 (account.getIn(['relationship', 'followed_by'])) { - info = ( - - - - ); - } - if (account.getIn(['relationship', 'requested'])) { - actionBtn = ( -
- -
- ); - } else if (!account.getIn(['relationship', 'blocking'])) { - following = account.getIn(['relationship', 'following']); - actionBtn = ( -
- -
- ); - } - } - -/* - we extract the `text` and -`metadata` from our account's `note` using `processBio()`. - -*/ - - const { text, metadata } = processBio(account.get('note')); - -/* - -Here, we render our component using all the things we've defined above. - -*/ - - return ( -
-
-
- - - - - - - - @{account.get('acct')} - {account.get('locked') ? : null} - -
- - {info} - {actionBtn} -
-
- - {metadata.length && ( - - - {(() => { - let data = []; - for (let i = 0; i < metadata.length; i++) { - data.push( - - - - - ); - } - return data; - })()} - -
- ) || null} -
- ); - } - -} diff --git a/app/javascript/glitch/components/compose/advanced_options/container.js b/app/javascript/glitch/components/compose/advanced_options/container.js deleted file mode 100644 index 160f2273765..00000000000 --- a/app/javascript/glitch/components/compose/advanced_options/container.js +++ /dev/null @@ -1,66 +0,0 @@ -/* - -`` -=================================== - -This container connects `` to the Redux store. - -*/ - -// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * - -/* - -Imports: --------- - -*/ - -// Package imports // -import { connect } from 'react-redux'; - -// Mastodon imports // -import { toggleComposeAdvancedOption } from '../../../../mastodon/actions/compose'; - -// Our imports // -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 => ({ - 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 => ({ - - onChange (option) { - dispatch(toggleComposeAdvancedOption(option)); - }, - -}); - -export default connect(mapStateToProps, mapDispatchToProps)(ComposeAdvancedOptions); diff --git a/app/javascript/glitch/components/compose/advanced_options/index.js b/app/javascript/glitch/components/compose/advanced_options/index.js deleted file mode 100644 index 8251baf4dd1..00000000000 --- a/app/javascript/glitch/components/compose/advanced_options/index.js +++ /dev/null @@ -1,163 +0,0 @@ -/* - -`` -========================== - -> 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 { injectIntl, defineMessages } from 'react-intl'; - -// Our imports // -import ComposeAdvancedOptionsToggle from './toggle'; -import ComposeDropdown from '../dropdown/index'; - -// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * - -/* - -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' }, -}); - -/* - -Implementation: ---------------- - -*/ - -@injectIntl -export default class ComposeAdvancedOptions extends React.PureComponent { - - static propTypes = { - values : ImmutablePropTypes.contains({ - do_not_federate : PropTypes.bool.isRequired, - }).isRequired, - onChange : PropTypes.func.isRequired, - intl : PropTypes.object.isRequired, - }; - - -/* - -### `render()` - -`render()` actually puts our component on the screen. - -*/ - - render () { - 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, 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 -``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 ( - - ); - }); - -/* - -Finally, we can render our component. - -*/ - return ( - - {optionElems} - - ); - } - -} diff --git a/app/javascript/glitch/components/compose/advanced_options/toggle.js b/app/javascript/glitch/components/compose/advanced_options/toggle.js deleted file mode 100644 index d6907472a4f..00000000000 --- a/app/javascript/glitch/components/compose/advanced_options/toggle.js +++ /dev/null @@ -1,103 +0,0 @@ -/* - -`` -================================ - -> For more information on the contents of this file, please contact: -> -> - surinna [@srn@dev.glitch.social] - -This creates the toggle used by ``. - -__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 `` and place next to it our text. - -*/ - - render() { - const { active, shortText, longText } = this.props; - return ( -
-
- -
-
- {shortText} - {longText} -
-
- ); - } - -} diff --git a/app/javascript/glitch/components/local_settings/container.js b/app/javascript/glitch/components/local_settings/container.js deleted file mode 100644 index 4569db99f49..00000000000 --- a/app/javascript/glitch/components/local_settings/container.js +++ /dev/null @@ -1,24 +0,0 @@ -// Package imports // -import { connect } from 'react-redux'; - -// Mastodon imports // -import { closeModal } from '../../../mastodon/actions/modal'; - -// Our imports // -import { changeLocalSetting } from '../../../glitch/actions/local_settings'; -import LocalSettings from '.'; - -const mapStateToProps = state => ({ - settings: state.get('local_settings'), -}); - -const mapDispatchToProps = dispatch => ({ - onChange (setting, value) { - dispatch(changeLocalSetting(setting, value)); - }, - onClose () { - dispatch(closeModal()); - }, -}); - -export default connect(mapStateToProps, mapDispatchToProps)(LocalSettings); diff --git a/app/javascript/glitch/components/notification/container.js b/app/javascript/glitch/components/notification/container.js deleted file mode 100644 index dc4c2168a97..00000000000 --- a/app/javascript/glitch/components/notification/container.js +++ /dev/null @@ -1,48 +0,0 @@ -/* - -`` -========================= - -This container connects ``s to the Redux store. - -*/ - -// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * - -/* - -Imports: --------- - -*/ - -// Package imports // -import { connect } from 'react-redux'; - -// Our imports // -import Notification from '.'; - -// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * - -const mapStateToProps = (state, props) => { - // replace account id with object - let leNotif = props.notification.set('account', state.getIn(['accounts', props.notification.get('account')])); - - // populate markedForDelete from state - is mysteriously lost somewhere - for (let n of state.getIn(['notifications', 'items'])) { - if (n.get('id') === props.notification.get('id')) { - leNotif = leNotif.set('markedForDelete', n.get('markedForDelete')); - break; - } - } - - return ({ - notification: leNotif, - settings: state.get('local_settings'), - notifCleaning: state.getIn(['notifications', 'cleaningMode']), - }); -}; - -// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * - -export default connect(mapStateToProps)(Notification); diff --git a/app/javascript/glitch/components/notification/follow.js b/app/javascript/glitch/components/notification/follow.js deleted file mode 100644 index e2c21bf3505..00000000000 --- a/app/javascript/glitch/components/notification/follow.js +++ /dev/null @@ -1,72 +0,0 @@ -// `` -// ====================== - -// * * * * * * * // - -// Imports -// ------- - -// Package imports. -import React from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import PropTypes from 'prop-types'; -import { FormattedMessage } from 'react-intl'; -import ImmutablePureComponent from 'react-immutable-pure-component'; - -// Mastodon imports. -import Permalink from '../../../mastodon/components/permalink'; -import AccountContainer from '../../../mastodon/containers/account_container'; - -// Our imports. -import NotificationOverlayContainer from '../notification/overlay/container'; - -// * * * * * * * // - -// Implementation -// -------------- - -export default class NotificationFollow extends ImmutablePureComponent { - - static propTypes = { - id : PropTypes.string.isRequired, - account : ImmutablePropTypes.map.isRequired, - notification : ImmutablePropTypes.map.isRequired, - }; - - render () { - const { account, notification } = this.props; - - // Links to the display name. - const displayName = account.get('display_name_html') || account.get('username'); - const link = ( - - ); - - // Renders. - return ( -
-
-
- -
- - -
- - - -
- ); - } - -} diff --git a/app/javascript/glitch/components/notification/overlay/container.js b/app/javascript/glitch/components/notification/overlay/container.js deleted file mode 100644 index 089f615f088..00000000000 --- a/app/javascript/glitch/components/notification/overlay/container.js +++ /dev/null @@ -1,49 +0,0 @@ -/* - -`` -========================= - -This container connects ``s to the Redux store. - -*/ - -// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * - -/* - -Imports: --------- - -*/ - -// Package imports // -import { connect } from 'react-redux'; - -// Our imports // -import NotificationOverlay from './notification_overlay'; -import { markNotificationForDelete } from '../../../../mastodon/actions/notifications'; - -// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * - -/* - -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 => ({ - onMarkForDelete(id, yes) { - dispatch(markNotificationForDelete(id, yes)); - }, -}); - -const mapStateToProps = state => ({ - show: state.getIn(['notifications', 'cleaningMode']), -}); - -export default connect(mapStateToProps, mapDispatchToProps)(NotificationOverlay); diff --git a/app/javascript/glitch/components/status/action_bar.js b/app/javascript/glitch/components/status/action_bar.js deleted file mode 100644 index 34588b00833..00000000000 --- a/app/javascript/glitch/components/status/action_bar.js +++ /dev/null @@ -1,187 +0,0 @@ -// Package imports // -import React from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import PropTypes from 'prop-types'; -import { defineMessages, injectIntl } from 'react-intl'; -import ImmutablePureComponent from 'react-immutable-pure-component'; - -// Mastodon imports // -import RelativeTimestamp from '../../../mastodon/components/relative_timestamp'; -import IconButton from '../../../mastodon/components/icon_button'; -import DropdownMenuContainer from '../../../mastodon/containers/dropdown_menu_container'; -import { me } from '../../../mastodon/initial_state'; - -const messages = defineMessages({ - delete: { id: 'status.delete', defaultMessage: 'Delete' }, - mention: { id: 'status.mention', defaultMessage: 'Mention @{name}' }, - mute: { id: 'account.mute', defaultMessage: 'Mute @{name}' }, - block: { id: 'account.block', defaultMessage: 'Block @{name}' }, - reply: { id: 'status.reply', defaultMessage: 'Reply' }, - share: { id: 'status.share', defaultMessage: 'Share' }, - replyAll: { id: 'status.replyAll', defaultMessage: 'Reply to thread' }, - reblog: { id: 'status.reblog', defaultMessage: 'Boost' }, - cannot_reblog: { id: 'status.cannot_reblog', defaultMessage: 'This post cannot be boosted' }, - favourite: { id: 'status.favourite', defaultMessage: 'Favourite' }, - open: { id: 'status.open', defaultMessage: 'Expand this status' }, - report: { id: 'status.report', defaultMessage: 'Report @{name}' }, - muteConversation: { id: 'status.mute_conversation', defaultMessage: 'Mute conversation' }, - unmuteConversation: { id: 'status.unmute_conversation', defaultMessage: 'Unmute conversation' }, - pin: { id: 'status.pin', defaultMessage: 'Pin on profile' }, - unpin: { id: 'status.unpin', defaultMessage: 'Unpin from profile' }, - embed: { id: 'status.embed', defaultMessage: 'Embed' }, -}); - -@injectIntl -export default class StatusActionBar extends ImmutablePureComponent { - - static contextTypes = { - router: PropTypes.object, - }; - - static propTypes = { - status: ImmutablePropTypes.map.isRequired, - onReply: PropTypes.func, - onFavourite: PropTypes.func, - onReblog: PropTypes.func, - onDelete: PropTypes.func, - onMention: PropTypes.func, - onMute: PropTypes.func, - onBlock: PropTypes.func, - onReport: PropTypes.func, - onEmbed: PropTypes.func, - onMuteConversation: PropTypes.func, - onPin: PropTypes.func, - withDismiss: PropTypes.bool, - 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 = [ - 'status', - 'withDismiss', - ] - - handleReplyClick = () => { - this.props.onReply(this.props.status, this.context.router.history); - } - - handleShareClick = () => { - navigator.share({ - text: this.props.status.get('search_index'), - url: this.props.status.get('url'), - }); - } - - handleFavouriteClick = () => { - this.props.onFavourite(this.props.status); - } - - handleReblogClick = (e) => { - this.props.onReblog(this.props.status, e); - } - - handleDeleteClick = () => { - this.props.onDelete(this.props.status); - } - - handlePinClick = () => { - this.props.onPin(this.props.status); - } - - handleMentionClick = () => { - this.props.onMention(this.props.status.get('account'), this.context.router.history); - } - - handleMuteClick = () => { - this.props.onMute(this.props.status.get('account')); - } - - handleBlockClick = () => { - this.props.onBlock(this.props.status.get('account')); - } - - handleOpen = () => { - this.context.router.history.push(`/statuses/${this.props.status.get('id')}`); - } - - handleEmbed = () => { - this.props.onEmbed(this.props.status); - } - - handleReport = () => { - this.props.onReport(this.props.status); - } - - handleConversationMuteClick = () => { - this.props.onMuteConversation(this.props.status); - } - - render () { - const { status, intl, withDismiss } = this.props; - - const mutingConversation = status.get('muted'); - const anonymousAccess = !me; - const publicStatus = ['public', 'unlisted'].includes(status.get('visibility')); - - let menu = []; - let reblogIcon = 'retweet'; - let replyIcon; - let replyTitle; - - menu.push({ text: intl.formatMessage(messages.open), action: this.handleOpen }); - - if (publicStatus) { - menu.push({ text: intl.formatMessage(messages.embed), action: this.handleEmbed }); - } - - menu.push(null); - - if (status.getIn(['account', 'id']) === me || withDismiss) { - menu.push({ text: intl.formatMessage(mutingConversation ? messages.unmuteConversation : messages.muteConversation), action: this.handleConversationMuteClick }); - menu.push(null); - } - - if (status.getIn(['account', 'id']) === me) { - if (publicStatus) { - menu.push({ text: intl.formatMessage(status.get('pinned') ? messages.unpin : messages.pin), action: this.handlePinClick }); - } - - menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick }); - } else { - menu.push({ text: intl.formatMessage(messages.mention, { name: status.getIn(['account', 'username']) }), action: this.handleMentionClick }); - menu.push(null); - menu.push({ text: intl.formatMessage(messages.mute, { name: status.getIn(['account', 'username']) }), action: this.handleMuteClick }); - menu.push({ text: intl.formatMessage(messages.block, { name: status.getIn(['account', 'username']) }), action: this.handleBlockClick }); - menu.push({ text: intl.formatMessage(messages.report, { name: status.getIn(['account', 'username']) }), action: this.handleReport }); - } - - if (status.get('in_reply_to_id', null) === null) { - replyIcon = 'reply'; - replyTitle = intl.formatMessage(messages.reply); - } else { - replyIcon = 'reply-all'; - replyTitle = intl.formatMessage(messages.replyAll); - } - - const shareButton = ('share' in navigator) && status.get('visibility') === 'public' && ( - - ); - - return ( -
- - - - {shareButton} - -
- -
- - -
- ); - } - -} diff --git a/app/javascript/glitch/components/status/container.js b/app/javascript/glitch/components/status/container.js deleted file mode 100644 index 0054abd1499..00000000000 --- a/app/javascript/glitch/components/status/container.js +++ /dev/null @@ -1,263 +0,0 @@ -/* - -`` -=================== - -Original file by @gargron@mastodon.social et al as part of -tootsuite/mastodon. Documentation by @kibi@glitch.social. The code -detecting reblogs has been moved here from . - -*/ - - /* * * * */ - -/* - -Imports: --------- - -*/ - -// Package imports // -import React from 'react'; -import { connect } from 'react-redux'; -import { - defineMessages, - injectIntl, - FormattedMessage, -} from 'react-intl'; - -// Mastodon imports // -import { makeGetStatus } from '../../../mastodon/selectors'; -import { - replyCompose, - mentionCompose, -} from '../../../mastodon/actions/compose'; -import { - reblog, - favourite, - unreblog, - unfavourite, - pin, - unpin, -} from '../../../mastodon/actions/interactions'; -import { blockAccount } from '../../../mastodon/actions/accounts'; -import { initMuteModal } from '../../../mastodon/actions/mutes'; -import { - muteStatus, - unmuteStatus, - deleteStatus, -} from '../../../mastodon/actions/statuses'; -import { initReport } from '../../../mastodon/actions/reports'; -import { openModal } from '../../../mastodon/actions/modal'; - -// Our imports // -import Status from '.'; - - /* * * * */ - -/* - -Inital setup: -------------- - -The `messages` constant is used to define any messages that we will -need in our component. In our case, these are the various confirmation -messages used with statuses. - -*/ - -const messages = defineMessages({ - deleteConfirm : { - id : 'confirmations.delete.confirm', - defaultMessage : 'Delete', - }, - deleteMessage : { - id : 'confirmations.delete.message', - defaultMessage : 'Are you sure you want to delete this status?', - }, - blockConfirm : { - id : 'confirmations.block.confirm', - defaultMessage : 'Block', - }, -}); - - /* * * * */ - -/* - -State mapping: --------------- - -The `mapStateToProps()` function maps various state properties to the -props of our component. We wrap this in a `makeMapStateToProps()` -function to give us closure and preserve `getStatus()` across function -calls. - -*/ - -const makeMapStateToProps = () => { - const getStatus = makeGetStatus(); - - const mapStateToProps = (state, ownProps) => { - - let status = getStatus(state, ownProps.id); - - if(status === null) { - console.error(`ERROR! NULL STATUS! ${ownProps.id}`); - // work-around: find first good status - for (let k of state.get('statuses').keys()) { - status = getStatus(state, k); - if (status !== null) break; - } - } - - let reblogStatus = status.get('reblog', null); - let account = undefined; - let prepend = undefined; - -/* - -Here we process reblogs. If our status is a reblog, then we create a -`prependMessage` to pass along to our `` along with the -reblogger's `account`, and set `coreStatus` (the one we will actually -render) to the status which has been reblogged. - -*/ - - if (reblogStatus !== null && typeof reblogStatus === 'object') { - account = status.get('account'); - status = reblogStatus; - prepend = 'reblogged_by'; - } - -/* - -Here are the props we pass to ``. - -*/ - - return { - status : status, - account : account || ownProps.account, - settings : state.get('local_settings'), - prepend : prepend || ownProps.prepend, - reblogModal : state.getIn(['meta', 'boost_modal']), - deleteModal : state.getIn(['meta', 'delete_modal']), - }; - }; - - return mapStateToProps; -}; - - /* * * * */ - -/* - -Dispatch mapping: ------------------ - -The `mapDispatchToProps()` function maps dispatches to our store to the -various props of our component. We need to provide dispatches for all -of the things you can do with a status: reply, reblog, favourite, et -cetera. - -For a few of these dispatches, we open up confirmation modals; the rest -just immediately execute their corresponding actions. - -*/ - -const mapDispatchToProps = (dispatch, { intl }) => ({ - - onReply (status, router) { - dispatch(replyCompose(status, router)); - }, - - onModalReblog (status) { - dispatch(reblog(status)); - }, - - onReblog (status, e) { - if (status.get('reblogged')) { - dispatch(unreblog(status)); - } else { - if (e.shiftKey || !this.reblogModal) { - this.onModalReblog(status); - } else { - dispatch(openModal('BOOST', { status, onReblog: this.onModalReblog })); - } - } - }, - - onFavourite (status) { - if (status.get('favourited')) { - dispatch(unfavourite(status)); - } else { - dispatch(favourite(status)); - } - }, - - onPin (status) { - if (status.get('pinned')) { - dispatch(unpin(status)); - } else { - dispatch(pin(status)); - } - }, - - onEmbed (status) { - dispatch(openModal('EMBED', { url: status.get('url') })); - }, - - onDelete (status) { - if (!this.deleteModal) { - dispatch(deleteStatus(status.get('id'))); - } else { - dispatch(openModal('CONFIRM', { - message: intl.formatMessage(messages.deleteMessage), - confirm: intl.formatMessage(messages.deleteConfirm), - onConfirm: () => dispatch(deleteStatus(status.get('id'))), - })); - } - }, - - onMention (account, router) { - dispatch(mentionCompose(account, router)); - }, - - onOpenMedia (media, index) { - dispatch(openModal('MEDIA', { media, index })); - }, - - onOpenVideo (media, time) { - dispatch(openModal('VIDEO', { media, time })); - }, - - onBlock (account) { - dispatch(openModal('CONFIRM', { - message: @{account.get('acct')} }} />, - confirm: intl.formatMessage(messages.blockConfirm), - onConfirm: () => dispatch(blockAccount(account.get('id'))), - })); - }, - - onReport (status) { - dispatch(initReport(status.get('account'), status)); - }, - - onMute (account) { - dispatch(initMuteModal(account)); - }, - - onMuteConversation (status) { - if (status.get('muted')) { - dispatch(unmuteStatus(status.get('id'))); - } else { - dispatch(muteStatus(status.get('id'))); - } - }, -}); - -export default injectIntl( - connect(makeMapStateToProps, mapDispatchToProps)(Status) -); diff --git a/app/javascript/glitch/components/status/gallery/index.js b/app/javascript/glitch/components/status/gallery/index.js deleted file mode 100644 index ae03dc08d70..00000000000 --- a/app/javascript/glitch/components/status/gallery/index.js +++ /dev/null @@ -1,79 +0,0 @@ -// Package imports // -import React from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import PropTypes from 'prop-types'; -import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; - -// Mastodon imports // -import IconButton from '../../../../mastodon/components/icon_button'; - -// Our imports // -import StatusGalleryItem from './item'; - -const messages = defineMessages({ - toggle_visible: { id: 'media_gallery.toggle_visible', defaultMessage: 'Toggle visibility' }, -}); - -@injectIntl -export default class StatusGallery extends React.PureComponent { - - static propTypes = { - sensitive: PropTypes.bool, - media: ImmutablePropTypes.list.isRequired, - letterbox: PropTypes.bool, - fullwidth: PropTypes.bool, - height: PropTypes.number.isRequired, - onOpenMedia: PropTypes.func.isRequired, - intl: PropTypes.object.isRequired, - autoPlayGif: PropTypes.bool.isRequired, - }; - - state = { - visible: !this.props.sensitive, - }; - - handleOpen = () => { - this.setState({ visible: !this.state.visible }); - } - - handleClick = (index) => { - this.props.onOpenMedia(this.props.media, index); - } - - render () { - const { media, intl, sensitive, letterbox, fullwidth } = this.props; - - let children; - - if (!this.state.visible) { - let warning; - - if (sensitive) { - warning = ; - } else { - warning = ; - } - - children = ( -
- {warning} - -
- ); - } else { - const size = media.take(4).size; - children = media.take(4).map((attachment, i) => ); - } - - return ( -
-
- -
- - {children} -
- ); - } - -} diff --git a/app/javascript/glitch/components/status/gallery/item.js b/app/javascript/glitch/components/status/gallery/item.js deleted file mode 100644 index 7fcc1437777..00000000000 --- a/app/javascript/glitch/components/status/gallery/item.js +++ /dev/null @@ -1,158 +0,0 @@ -// Package imports // -import React from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import PropTypes from 'prop-types'; - -// Mastodon imports // -import { isIOS } from '../../../../mastodon/is_mobile'; - -export default class StatusGalleryItem extends React.PureComponent { - - static propTypes = { - attachment: ImmutablePropTypes.map.isRequired, - index: PropTypes.number.isRequired, - size: PropTypes.number.isRequired, - letterbox: PropTypes.bool, - onClick: PropTypes.func.isRequired, - autoPlayGif: PropTypes.bool.isRequired, - }; - - handleMouseEnter = (e) => { - if (this.hoverToPlay()) { - e.target.play(); - } - } - - handleMouseLeave = (e) => { - if (this.hoverToPlay()) { - e.target.pause(); - e.target.currentTime = 0; - } - } - - hoverToPlay () { - const { attachment, autoPlayGif } = this.props; - return !autoPlayGif && attachment.get('type') === 'gifv'; - } - - handleClick = (e) => { - const { index, onClick } = this.props; - - if (e.button === 0) { - e.preventDefault(); - onClick(index); - } - - e.stopPropagation(); - } - - render () { - const { attachment, index, size, letterbox } = this.props; - - let width = 50; - let height = 100; - let top = 'auto'; - let left = 'auto'; - let bottom = 'auto'; - let right = 'auto'; - - if (size === 1) { - width = 100; - } - - if (size === 4 || (size === 3 && index > 0)) { - height = 50; - } - - if (size === 2) { - if (index === 0) { - right = '2px'; - } else { - left = '2px'; - } - } else if (size === 3) { - if (index === 0) { - right = '2px'; - } else if (index > 0) { - left = '2px'; - } - - if (index === 1) { - bottom = '2px'; - } else if (index > 1) { - top = '2px'; - } - } else if (size === 4) { - if (index === 0 || index === 2) { - right = '2px'; - } - - if (index === 1 || index === 3) { - left = '2px'; - } - - if (index < 2) { - bottom = '2px'; - } else { - top = '2px'; - } - } - - let thumbnail = ''; - - if (attachment.get('type') === 'image') { - const previewUrl = attachment.get('preview_url'); - const previewWidth = attachment.getIn(['meta', 'small', 'width']); - - const originalUrl = attachment.get('url'); - const originalWidth = attachment.getIn(['meta', 'original', 'width']); - - const srcSet = `${originalUrl} ${originalWidth}w, ${previewUrl} ${previewWidth}w`; - const sizes = `(min-width: 1025px) ${320 * (width / 100)}px, ${width}vw`; - - thumbnail = ( - - {attachment.get('description')} - - ); - } else if (attachment.get('type') === 'gifv') { - const autoPlay = !isIOS() && this.props.autoPlayGif; - - thumbnail = ( -
-
- ); - } - - return ( -
- {thumbnail} -
- ); - } - -} diff --git a/app/javascript/glitch/components/status/index.js b/app/javascript/glitch/components/status/index.js deleted file mode 100644 index 33a9730e5ac..00000000000 --- a/app/javascript/glitch/components/status/index.js +++ /dev/null @@ -1,760 +0,0 @@ -/* - -`` -========== - -Original file by @gargron@mastodon.social et al as part of -tootsuite/mastodon. *Heavily* rewritten (and documented!) by -@kibi@glitch.social as a part of glitch-soc/mastodon. The following -features have been added: - - - Better separating the "guts" of statuses from their wrapper(s) - - Collapsing statuses - - Moving images inside of CWs - -A number of aspects of this original file have been split off into -their own components for better maintainance; for these, see: - - - - - - -…And, of course, the other -related components as well. - -*/ - - /* * * * */ - -/* - -Imports: --------- - -*/ - -// Package imports // -import React from 'react'; -import PropTypes from 'prop-types'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import ImmutablePureComponent from 'react-immutable-pure-component'; - -// Mastodon imports // -import scheduleIdleTask from '../../../mastodon/features/ui/util/schedule_idle_task'; -import { autoPlayGif } from '../../../mastodon/initial_state'; - -// Our imports // -import StatusPrepend from './prepend'; -import StatusHeader from './header'; -import StatusContent from './content'; -import StatusActionBar from './action_bar'; -import StatusGallery from './gallery'; -import StatusPlayer from './player'; -import NotificationOverlayContainer from '../notification/overlay/container'; - - /* * * * */ - -/* - -The `` component: -------------------------- - -The `` component is a container for statuses. It consists of a -few parts: - - - The ``, which contains tangential information about - the status, such as who reblogged it. - - The ``, which contains the avatar and username of the - status author, as well as a media icon and the "collapse" toggle. - - The ``, which contains the content of the status. - - The ``, which provides actions to be performed - on statuses, like reblogging or sending a reply. - -### Context - - - __`router` (`PropTypes.object`) :__ - We need to get our router from the surrounding React context. - -### Props - - - __`id` (`PropTypes.number`) :__ - The id of the status. - - - __`status` (`ImmutablePropTypes.map`) :__ - The status object, straight from the store. - - - __`account` (`ImmutablePropTypes.map`) :__ - Don't be confused by this one! This is **not** the account which - posted the status, but the associated account with any further - action (eg, a reblog or a favourite). - - - __`settings` (`ImmutablePropTypes.map`) :__ - These are our local settings, fetched from our store. We need this - to determine how best to collapse our statuses, among other things. - - - __`onFavourite`, `onReblog`, `onModalReblog`, `onDelete`, - `onMention`, `onMute`, `onMuteConversation`, onBlock`, `onReport`, - `onOpenMedia`, `onOpenVideo` (`PropTypes.func`) :__ - These are all functions passed through from the - ``. We don't deal with them directly here. - - - __`reblogModal`, `deleteModal` (`PropTypes.bool`) :__ - These tell whether or not the user has modals activated for - reblogging and deleting statuses. They are used by the `onReblog` - and `onDelete` functions, but we don't deal with them here. - - - __`muted` (`PropTypes.bool`) :__ - This has nothing to do with a user or conversation mute! "Muted" is - what Mastodon internally calls the subdued look of statuses in the - notifications column. This should be `true` for notifications, and - `false` otherwise. - - - __`collapse` (`PropTypes.bool`) :__ - This prop signals a directive from a higher power to (un)collapse - a status. Most of the time it should be `undefined`, in which case - we do nothing. - - - __`prepend` (`PropTypes.string`) :__ - The type of prepend: `'reblogged_by'`, `'reblog'`, or - `'favourite'`. - - - __`withDismiss` (`PropTypes.bool`) :__ - Whether or not the status can be dismissed. Used for notifications. - - - __`intersectionObserverWrapper` (`PropTypes.object`) :__ - This holds our intersection observer. In Mastodon parlance, - an "intersection" is just when the status is viewable onscreen. - -### State - - - __`isExpanded` :__ - Should be either `true`, `false`, or `null`. The meanings of - these values are as follows: - - - __`true` :__ The status contains a CW and the CW is expanded. - - __`false` :__ The status is collapsed. - - __`null` :__ The status is not collapsed or expanded. - - - __`isIntersecting` :__ - This boolean tells us whether or not the status is currently - onscreen. - - - __`isHidden` :__ - This boolean tells us if the status has been unrendered to save - CPUs. - -*/ - -export default class Status extends ImmutablePureComponent { - - static contextTypes = { - router : PropTypes.object, - }; - - static propTypes = { - id : PropTypes.string, - status : ImmutablePropTypes.map, - account : ImmutablePropTypes.map, - settings : ImmutablePropTypes.map, - notification : ImmutablePropTypes.map, - onFavourite : PropTypes.func, - onReblog : PropTypes.func, - onModalReblog : PropTypes.func, - onDelete : PropTypes.func, - onPin : PropTypes.func, - onMention : PropTypes.func, - onMute : PropTypes.func, - onMuteConversation : PropTypes.func, - onBlock : PropTypes.func, - onEmbed : PropTypes.func, - onHeightChange : PropTypes.func, - onReport : PropTypes.func, - onOpenMedia : PropTypes.func, - onOpenVideo : PropTypes.func, - reblogModal : PropTypes.bool, - deleteModal : PropTypes.bool, - muted : PropTypes.bool, - collapse : PropTypes.bool, - prepend : PropTypes.string, - withDismiss : PropTypes.bool, - intersectionObserverWrapper : PropTypes.object, - }; - - state = { - isExpanded : null, - isIntersecting : true, - isHidden : false, - markedForDelete : false, - } - -/* - -### Implementation - -#### `updateOnProps` and `updateOnStates`. - -`updateOnProps` and `updateOnStates` tell the component when to update. -We specify them explicitly because some of our props are dynamically= -generated functions, which would otherwise always trigger an update. -Of course, this means that if we add an important prop, we will need -to remember to specify it here. - -*/ - - updateOnProps = [ - 'status', - 'account', - 'settings', - 'prepend', - 'boostModal', - 'muted', - 'collapse', - 'notification', - ] - - updateOnStates = [ - 'isExpanded', - 'markedForDelete', - ] - -/* - -#### `componentWillReceiveProps()`. - -If our settings have changed to disable collapsed statuses, then we -need to make sure that we uncollapse every one. We do that by watching -for changes to `settings.collapsed.enabled` in -`componentWillReceiveProps()`. - -We also need to watch for changes on the `collapse` prop---if this -changes to anything other than `undefined`, then we need to collapse or -uncollapse our status accordingly. - -*/ - - componentWillReceiveProps (nextProps) { - if (!nextProps.settings.getIn(['collapsed', 'enabled'])) { - if (this.state.isExpanded === false) { - this.setExpansion(null); - } - } else if ( - nextProps.collapse !== this.props.collapse && - nextProps.collapse !== undefined - ) this.setExpansion(nextProps.collapse ? false : null); - } - -/* - -#### `componentDidMount()`. - -When mounting, we just check to see if our status should be collapsed, -and collapse it if so. We don't need to worry about whether collapsing -is enabled here, because `setExpansion()` already takes that into -account. - -The cases where a status should be collapsed are: - - - The `collapse` prop has been set to `true` - - The user has decided in local settings to collapse all statuses. - - The user has decided to collapse all notifications ('muted' - statuses). - - The user has decided to collapse long statuses and the status is - over 400px (without media, or 650px with). - - The status is a reply and the user has decided to collapse all - replies. - - The status contains media and the user has decided to collapse all - statuses with media. - -We also start up our intersection observer to monitor our statuses. -`componentMounted` lets us know that everything has been set up -properly and our intersection observer is good to go. - -*/ - - componentDidMount () { - const { node, handleIntersection } = this; - const { - status, - settings, - collapse, - muted, - id, - intersectionObserverWrapper, - prepend, - } = this.props; - const autoCollapseSettings = settings.getIn(['collapsed', 'auto']); - - if ( - collapse || - autoCollapseSettings.get('all') || ( - autoCollapseSettings.get('notifications') && muted - ) || ( - autoCollapseSettings.get('lengthy') && - node.clientHeight > ( - status.get('media_attachments').size && !muted ? 650 : 400 - ) - ) || ( - autoCollapseSettings.get('reblogs') && - prepend === 'reblogged_by' - ) || ( - autoCollapseSettings.get('replies') && - status.get('in_reply_to_id', null) !== null - ) || ( - autoCollapseSettings.get('media') && - !(status.get('spoiler_text').length) && - status.get('media_attachments').size - ) - ) this.setExpansion(false); - - if (!intersectionObserverWrapper) return; - else intersectionObserverWrapper.observe( - id, - node, - handleIntersection - ); - - this.componentMounted = true; - } - -/* - -#### `shouldComponentUpdate()`. - -If the status is about to be both offscreen (not intersecting) and -hidden, then we only need to update it if it's not that way currently. -If the status is moving from offscreen to onscreen, then we *have* to -re-render, so that we can unhide the element if necessary. - -If neither of these cases are true, we can leave it up to our -`updateOnProps` and `updateOnStates` arrays. - -*/ - - shouldComponentUpdate (nextProps, nextState) { - switch (true) { - case !nextState.isIntersecting && nextState.isHidden: - return this.state.isIntersecting || !this.state.isHidden; - case nextState.isIntersecting && !this.state.isIntersecting: - return true; - default: - return super.shouldComponentUpdate(nextProps, nextState); - } - } - -/* - -#### `componentDidUpdate()`. - -If our component is being rendered for any reason and an update has -triggered, this will save its height. - -This is, frankly, a bit overkill, as the only instance when we -actually *need* to update the height right now should be when the -value of `isExpanded` has changed. But it makes for more readable -code and prevents bugs in the future where the height isn't set -properly after some change. - -*/ - - componentDidUpdate () { - if ( - this.state.isIntersecting || !this.state.isHidden - ) this.saveHeight(); - } - -/* - -#### `componentWillUnmount()`. - -If our component is about to unmount, then we'd better unset -`this.componentMounted`. - -*/ - - componentWillUnmount () { - this.componentMounted = false; - } - -/* - -#### `handleIntersection()`. - -`handleIntersection()` either hides the status (if it is offscreen) or -unhides it (if it is onscreen). It's called by -`intersectionObserverWrapper.observe()`. - -If our status isn't intersecting, we schedule an idle task (using the -aptly-named `scheduleIdleTask()`) to hide the status at the next -available opportunity. - -tootsuite/mastodon left us with the following enlightening comment -regarding this function: - -> Edge 15 doesn't support isIntersecting, but we can infer it - -It then implements a polyfill (intersectionRect.height > 0) which isn't -actually sufficient. The short answer is, this behaviour isn't really -supported on Edge but we can get kinda close. - -*/ - - handleIntersection = (entry) => { - const isIntersecting = ( - typeof entry.isIntersecting === 'boolean' ? - entry.isIntersecting : - entry.intersectionRect.height > 0 - ); - this.setState( - (prevState) => { - if (prevState.isIntersecting && !isIntersecting) { - scheduleIdleTask(this.hideIfNotIntersecting); - } - return { - isIntersecting : isIntersecting, - isHidden : false, - }; - } - ); - } - -/* - -#### `hideIfNotIntersecting()`. - -This function will hide the status if we're still not intersecting. -Hiding the status means that it will just render an empty div instead -of actual content, which saves RAMS and CPUs or some such. - -*/ - - hideIfNotIntersecting = () => { - if (!this.componentMounted) return; - this.setState( - (prevState) => ({ isHidden: !prevState.isIntersecting }) - ); - } - -/* - -#### `saveHeight()`. - -`saveHeight()` saves the height of our status so that when whe hide it -we preserve its dimensions. We only want to store our height, though, -if our status has content (otherwise, it would imply that it is -already hidden). - -*/ - - saveHeight = () => { - if (this.node && this.node.children.length) { - this.height = this.node.getBoundingClientRect().height; - } - } - -/* - -#### `setExpansion()`. - -`setExpansion()` sets the value of `isExpanded` in our state. It takes -one argument, `value`, which gives the desired value for `isExpanded`. -The default for this argument is `null`. - -`setExpansion()` automatically checks for us whether toot collapsing -is enabled, so we don't have to. - -We use a `switch` statement to simplify our code. - -*/ - - setExpansion = (value) => { - switch (true) { - case value === undefined || value === null: - this.setState({ isExpanded: null }); - break; - case !value && this.props.settings.getIn(['collapsed', 'enabled']): - this.setState({ isExpanded: false }); - break; - case !!value: - this.setState({ isExpanded: true }); - break; - } - } - -/* - -#### `handleRef()`. - -`handleRef()` just saves a reference to our status node to `this.node`. -It also saves our height, in case the height of our node has changed. - -*/ - - handleRef = (node) => { - this.node = node; - this.saveHeight(); - } - -/* - -#### `parseClick()`. - -`parseClick()` takes a click event and responds appropriately. -If our status is collapsed, then clicking on it should uncollapse it. -If `Shift` is held, then clicking on it should collapse it. -Otherwise, we open the url handed to us in `destination`, if -applicable. - -*/ - - parseClick = (e, destination) => { - const { router } = this.context; - const { status } = this.props; - const { isExpanded } = this.state; - if (!router) return; - if (destination === undefined) { - destination = `/statuses/${ - status.getIn(['reblog', 'id'], status.get('id')) - }`; - } - if (e.button === 0) { - if (isExpanded === false) this.setExpansion(null); - else if (e.shiftKey) { - this.setExpansion(false); - document.getSelection().removeAllRanges(); - } else router.history.push(destination); - e.preventDefault(); - } - } - -/* - -#### `render()`. - -`render()` actually puts our element on the screen. The particulars of -this operation are further explained in the code below. - -*/ - - render () { - const { - parseClick, - setExpansion, - saveHeight, - handleRef, - } = this; - const { router } = this.context; - const { - status, - account, - settings, - collapsed, - muted, - prepend, - intersectionObserverWrapper, - onOpenVideo, - onOpenMedia, - notification, - ...other - } = this.props; - const { isExpanded, isIntersecting, isHidden } = this.state; - let background = null; - let attachments = null; - let media = null; - let mediaIcon = null; - -/* - -If we don't have a status, then we don't render anything. - -*/ - - if (status === null) { - return null; - } - -/* - -If our status is offscreen and hidden, then we render an empty
in -its place. We fill it with "content" but note that opacity is set to 0. - -*/ - - if (!isIntersecting && isHidden) { - return ( -
- { - status.getIn(['account', 'display_name']) || - status.getIn(['account', 'username']) - } - {status.get('content')} -
- ); - } - -/* - -If user backgrounds for collapsed statuses are enabled, then we -initialize our background accordingly. This will only be rendered if -the status is collapsed. - -*/ - - if ( - settings.getIn(['collapsed', 'backgrounds', 'user_backgrounds']) - ) background = status.getIn(['account', 'header']); - -/* - -This handles our media attachments. Note that we don't show media on -muted (notification) statuses. If the media type is unknown, then we -simply ignore it. - -After we have generated our appropriate media element and stored it in -`media`, we snatch the thumbnail to use as our `background` if media -backgrounds for collapsed statuses are enabled. - -*/ - - attachments = status.get('media_attachments'); - if (attachments.size && !muted) { - if (attachments.some((item) => item.get('type') === 'unknown')) { - - } else if ( - attachments.getIn([0, 'type']) === 'video' - ) { - media = ( // Media type is 'video' - - ); - mediaIcon = 'video-camera'; - } else { // Media type is 'image' or 'gifv' - media = ( - - ); - mediaIcon = 'picture-o'; - } - - if ( - !status.get('sensitive') && - !(status.get('spoiler_text').length > 0) && - settings.getIn(['collapsed', 'backgrounds', 'preview_images']) - ) background = attachments.getIn([0, 'preview_url']); - } - -/* - -Here we prepare extra data-* attributes for CSS selectors. -Users can use those for theming, hiding avatars etc via UserStyle - -*/ - - const selectorAttribs = { - 'data-status-by': `@${status.getIn(['account', 'acct'])}`, - }; - - if (prepend && account) { - const notifKind = { - favourite: 'favourited', - reblog: 'boosted', - reblogged_by: 'boosted', - }[prepend]; - - selectorAttribs[`data-${notifKind}-by`] = `@${account.get('acct')}`; - } - -/* - -Finally, we can render our status. We just put the pieces together -from above. We only render the action bar if the status isn't -collapsed. - -*/ - - return ( -
- {prepend && account ? ( - - ) : null} - - - {isExpanded !== false ? ( - - ) : null} - {notification ? ( - - ) : null} -
- ); - - } - -} diff --git a/app/javascript/glitch/components/status/player.js b/app/javascript/glitch/components/status/player.js deleted file mode 100644 index cc65cd34ef9..00000000000 --- a/app/javascript/glitch/components/status/player.js +++ /dev/null @@ -1,203 +0,0 @@ -// Package imports // -import React from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import PropTypes from 'prop-types'; -import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; - -// Mastodon imports // -import IconButton from '../../../mastodon/components/icon_button'; -import { isIOS } from '../../../mastodon/is_mobile'; - -const messages = defineMessages({ - toggle_sound: { id: 'video_player.toggle_sound', defaultMessage: 'Toggle sound' }, - toggle_visible: { id: 'video_player.toggle_visible', defaultMessage: 'Toggle visibility' }, - expand_video: { id: 'video_player.expand', defaultMessage: 'Expand video' }, -}); - -@injectIntl -export default class StatusPlayer extends React.PureComponent { - - static contextTypes = { - router: PropTypes.object, - }; - - static propTypes = { - media: ImmutablePropTypes.map.isRequired, - letterbox: PropTypes.bool, - fullwidth: PropTypes.bool, - height: PropTypes.number, - sensitive: PropTypes.bool, - intl: PropTypes.object.isRequired, - autoplay: PropTypes.bool, - onOpenVideo: PropTypes.func.isRequired, - }; - - static defaultProps = { - height: 110, - }; - - state = { - visible: !this.props.sensitive, - preview: true, - muted: true, - hasAudio: true, - videoError: false, - }; - - handleClick = () => { - this.setState({ muted: !this.state.muted }); - } - - handleVideoClick = (e) => { - e.stopPropagation(); - - const node = this.video; - - if (node.paused) { - node.play(); - } else { - node.pause(); - } - } - - handleOpen = () => { - this.setState({ preview: !this.state.preview }); - } - - handleVisibility = () => { - this.setState({ - visible: !this.state.visible, - preview: true, - }); - } - - handleExpand = () => { - this.video.pause(); - this.props.onOpenVideo(this.props.media, this.video.currentTime); - } - - setRef = (c) => { - this.video = c; - } - - handleLoadedData = () => { - if (('WebkitAppearance' in document.documentElement.style && this.video.audioTracks.length === 0) || this.video.mozHasAudio === false) { - this.setState({ hasAudio: false }); - } - } - - handleVideoError = () => { - this.setState({ videoError: true }); - } - - componentDidMount () { - if (!this.video) { - return; - } - - this.video.addEventListener('loadeddata', this.handleLoadedData); - this.video.addEventListener('error', this.handleVideoError); - } - - componentDidUpdate () { - if (!this.video) { - return; - } - - this.video.addEventListener('loadeddata', this.handleLoadedData); - this.video.addEventListener('error', this.handleVideoError); - } - - componentWillUnmount () { - if (!this.video) { - return; - } - - this.video.removeEventListener('loadeddata', this.handleLoadedData); - this.video.removeEventListener('error', this.handleVideoError); - } - - render () { - const { media, intl, letterbox, fullwidth, height, sensitive, autoplay } = this.props; - - let spoilerButton = ( -
- -
- ); - - let expandButton = !this.context.router ? '' : ( -
- -
- ); - - let muteButton = ''; - - if (this.state.hasAudio) { - muteButton = ( -
- -
- ); - } - - if (!this.state.visible) { - if (sensitive) { - return ( -
- {spoilerButton} - - -
- ); - } else { - return ( -
- {spoilerButton} - - -
- ); - } - } - - if (this.state.preview && !autoplay) { - return ( -
- {spoilerButton} -
-
- ); - } - - if (this.state.videoError) { - return ( -
- -
- ); - } - - return ( -
- {spoilerButton} - {muteButton} - {expandButton} - -
- ); - } - -} diff --git a/app/javascript/glitch/reducers/local_settings.js b/app/javascript/glitch/reducers/local_settings.js deleted file mode 100644 index 03654fbe22a..00000000000 --- a/app/javascript/glitch/reducers/local_settings.js +++ /dev/null @@ -1,126 +0,0 @@ -/* - -`reducers/local_settings` -======================== - -> For more information on the contents of this file, please contact: -> -> - kibigo! [@kibi@glitch.social] - -This file provides our Redux reducers related to local settings. The -associated actions are: - - - __`STORE_HYDRATE` :__ - Used to hydrate the store with its initial values. - - - __`LOCAL_SETTING_CHANGE` :__ - Used to change the value of a local setting in the store. - -*/ - -// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * - -/* - -Imports: --------- - -*/ - -// Package imports // -import { Map as ImmutableMap } from 'immutable'; - -// Mastodon imports // -import { STORE_HYDRATE } from '../../mastodon/actions/store'; - -// Our imports // -import { LOCAL_SETTING_CHANGE } from '../actions/local_settings'; - -// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * - -/* - -initialState: -------------- - -You can see the default values for all of our local settings here. -These are only used if no previously-saved values exist. - -*/ - -const initialState = ImmutableMap({ - layout : 'auto', - stretch : true, - navbar_under : false, - side_arm : 'none', - collapsed : ImmutableMap({ - enabled : true, - auto : ImmutableMap({ - all : false, - notifications : true, - lengthy : true, - reblogs : false, - replies : false, - media : false, - }), - backgrounds : ImmutableMap({ - user_backgrounds : false, - preview_images : false, - }), - }), - media : ImmutableMap({ - letterbox : true, - fullwidth : true, - }), -}); - -// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * - -/* - -Helper functions: ------------------ - -### `hydrate(state, localSettings)` - -`hydrate()` is used to hydrate the `local_settings` part of our store -with its initial values. The `state` will probably just be the -`initialState`, and the `localSettings` should be whatever we pulled -from `localStorage`. - -*/ - -const hydrate = (state, localSettings) => state.mergeDeep(localSettings); - -// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * - -/* - -`localSettings(state = initialState, action)`: ----------------------------------------------- - -This function holds our actual reducer. - -If our action is `STORE_HYDRATE`, then we call `hydrate()` with the -`local_settings` property of the provided `action.state`. - -If our action is `LOCAL_SETTING_CHANGE`, then we set `action.key` in -our state to the provided `action.value`. Note that `action.key` MUST -be an array, since we use `setIn()`. - -> __Note :__ -> We call this function `localSettings`, but its associated object -> in the store is `local_settings`. - -*/ - -export default function localSettings(state = initialState, action) { - switch(action.type) { - case STORE_HYDRATE: - return hydrate(state, action.state.get('local_settings')); - case LOCAL_SETTING_CHANGE: - return state.setIn(action.key, action.value); - default: - return state; - } -}; diff --git a/app/javascript/mastodon/components/status.js b/app/javascript/mastodon/components/status.js deleted file mode 100644 index 5a01c0cdd55..00000000000 --- a/app/javascript/mastodon/components/status.js +++ /dev/null @@ -1,249 +0,0 @@ -// THIS FILE EXISTS FOR UPSTREAM COMPATIBILITY & SHOULDN'T BE USED !! -// SEE INSTEAD : glitch/components/status - -import React from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import PropTypes from 'prop-types'; -import Avatar from './avatar'; -import AvatarOverlay from './avatar_overlay'; -import RelativeTimestamp from './relative_timestamp'; -import DisplayName from './display_name'; -import StatusContent from './status_content'; -import StatusActionBar from './status_action_bar'; -import { FormattedMessage } from 'react-intl'; -import ImmutablePureComponent from 'react-immutable-pure-component'; -import { MediaGallery, Video } from '../features/ui/util/async-components'; -import { HotKeys } from 'react-hotkeys'; -import classNames from 'classnames'; - -// We use the component (and not the container) since we do not want -// to use the progress bar to show download progress -import Bundle from '../features/ui/components/bundle'; - -export default class Status extends ImmutablePureComponent { - - static contextTypes = { - router: PropTypes.object, - }; - - static propTypes = { - status: ImmutablePropTypes.map, - account: ImmutablePropTypes.map, - onReply: PropTypes.func, - onFavourite: PropTypes.func, - onReblog: PropTypes.func, - onDelete: PropTypes.func, - onPin: PropTypes.func, - onOpenMedia: PropTypes.func, - onOpenVideo: PropTypes.func, - onBlock: PropTypes.func, - onEmbed: PropTypes.func, - onHeightChange: PropTypes.func, - muted: PropTypes.bool, - hidden: PropTypes.bool, - onMoveUp: PropTypes.func, - onMoveDown: PropTypes.func, - }; - - state = { - isExpanded: false, - } - - // Avoid checking props that are functions (and whose equality will always - // evaluate to false. See react-immutable-pure-component for usage. - updateOnProps = [ - 'status', - 'account', - 'muted', - 'hidden', - ] - - updateOnStates = ['isExpanded'] - - handleClick = () => { - if (!this.context.router) { - return; - } - - const { status } = this.props; - this.context.router.history.push(`/statuses/${status.getIn(['reblog', 'id'], status.get('id'))}`); - } - - handleAccountClick = (e) => { - if (this.context.router && e.button === 0) { - const id = e.currentTarget.getAttribute('data-id'); - e.preventDefault(); - this.context.router.history.push(`/accounts/${id}`); - } - } - - handleExpandedToggle = () => { - this.setState({ isExpanded: !this.state.isExpanded }); - }; - - renderLoadingMediaGallery () { - return
; - } - - renderLoadingVideoPlayer () { - return
; - } - - handleOpenVideo = startTime => { - this.props.onOpenVideo(this._properStatus().getIn(['media_attachments', 0]), startTime); - } - - handleHotkeyReply = e => { - e.preventDefault(); - this.props.onReply(this._properStatus(), this.context.router.history); - } - - handleHotkeyFavourite = () => { - this.props.onFavourite(this._properStatus()); - } - - handleHotkeyBoost = e => { - this.props.onReblog(this._properStatus(), e); - } - - handleHotkeyMention = e => { - e.preventDefault(); - this.props.onMention(this._properStatus().get('account'), this.context.router.history); - } - - handleHotkeyOpen = () => { - this.context.router.history.push(`/statuses/${this._properStatus().get('id')}`); - } - - handleHotkeyOpenProfile = () => { - this.context.router.history.push(`/accounts/${this._properStatus().getIn(['account', 'id'])}`); - } - - handleHotkeyMoveUp = () => { - this.props.onMoveUp(this.props.status.get('id')); - } - - handleHotkeyMoveDown = () => { - this.props.onMoveDown(this.props.status.get('id')); - } - - _properStatus () { - const { status } = this.props; - - if (status.get('reblog', null) !== null && typeof status.get('reblog') === 'object') { - return status.get('reblog'); - } else { - return status; - } - } - - render () { - let media = null; - let statusAvatar, prepend; - - const { hidden } = this.props; - const { isExpanded } = this.state; - - let { status, account, ...other } = this.props; - - if (status === null) { - return null; - } - - if (hidden) { - return ( -
- {status.getIn(['account', 'display_name']) || status.getIn(['account', 'username'])} - {status.get('content')} -
- ); - } - - if (status.get('reblog', null) !== null && typeof status.get('reblog') === 'object') { - const display_name_html = { __html: status.getIn(['account', 'display_name_html']) }; - - prepend = ( -
-
- }} /> -
- ); - - account = status.get('account'); - status = status.get('reblog'); - } - - if (status.get('media_attachments').size > 0 && !this.props.muted) { - if (status.get('media_attachments').some(item => item.get('type') === 'unknown')) { - - } else if (status.getIn(['media_attachments', 0, 'type']) === 'video') { - const video = status.getIn(['media_attachments', 0]); - - media = ( - - {Component => } - - ); - } else { - media = ( - - {Component => } - - ); - } - } - - if (account === undefined || account === null) { - statusAvatar = ; - }else{ - statusAvatar = ; - } - - const handlers = this.props.muted ? {} : { - reply: this.handleHotkeyReply, - favourite: this.handleHotkeyFavourite, - boost: this.handleHotkeyBoost, - mention: this.handleHotkeyMention, - open: this.handleHotkeyOpen, - openProfile: this.handleHotkeyOpenProfile, - moveUp: this.handleHotkeyMoveUp, - moveDown: this.handleHotkeyMoveDown, - }; - - return ( - -
- {prepend} - -
- - - - - {media} - - -
-
-
- ); - } - -} diff --git a/app/javascript/mastodon/components/status_content.js b/app/javascript/mastodon/components/status_content.js deleted file mode 100644 index 0f7f15dfc1e..00000000000 --- a/app/javascript/mastodon/components/status_content.js +++ /dev/null @@ -1,188 +0,0 @@ -// THIS FILE EXISTS FOR UPSTREAM COMPATIBILITY & SHOULDN'T BE USED !! -// SEE INSTEAD : glitch/components/status/content - -import React from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import PropTypes from 'prop-types'; -import { isRtl } from '../rtl'; -import { FormattedMessage } from 'react-intl'; -import Permalink from './permalink'; -import classnames from 'classnames'; - -export default class StatusContent extends React.PureComponent { - - static contextTypes = { - router: PropTypes.object, - }; - - static propTypes = { - status: ImmutablePropTypes.map.isRequired, - expanded: PropTypes.bool, - onExpandedToggle: PropTypes.func, - onClick: PropTypes.func, - }; - - state = { - hidden: true, - }; - - _updateStatusLinks () { - const node = this.node; - const links = node.querySelectorAll('a'); - - for (var i = 0; i < links.length; ++i) { - let link = links[i]; - if (link.classList.contains('status-link')) { - continue; - } - link.classList.add('status-link'); - - let mention = this.props.status.get('mentions').find(item => link.href === item.get('url')); - - if (mention) { - link.addEventListener('click', this.onMentionClick.bind(this, mention), false); - link.setAttribute('title', mention.get('acct')); - } else if (link.textContent[0] === '#' || (link.previousSibling && link.previousSibling.textContent && link.previousSibling.textContent[link.previousSibling.textContent.length - 1] === '#')) { - link.addEventListener('click', this.onHashtagClick.bind(this, link.text), false); - } else { - link.setAttribute('title', link.href); - } - - link.setAttribute('target', '_blank'); - link.setAttribute('rel', 'noopener'); - } - } - - componentDidMount () { - this._updateStatusLinks(); - } - - componentDidUpdate () { - this._updateStatusLinks(); - } - - onMentionClick = (mention, e) => { - if (this.context.router && e.button === 0) { - e.preventDefault(); - this.context.router.history.push(`/accounts/${mention.get('id')}`); - } - } - - onHashtagClick = (hashtag, e) => { - hashtag = hashtag.replace(/^#/, '').toLowerCase(); - - if (this.context.router && e.button === 0) { - e.preventDefault(); - this.context.router.history.push(`/timelines/tag/${hashtag}`); - } - } - - handleMouseDown = (e) => { - this.startXY = [e.clientX, e.clientY]; - } - - handleMouseUp = (e) => { - if (!this.startXY) { - return; - } - - const [ startX, startY ] = this.startXY; - const [ deltaX, deltaY ] = [Math.abs(e.clientX - startX), Math.abs(e.clientY - startY)]; - - if (e.target.localName === 'button' || e.target.localName === 'a' || (e.target.parentNode && (e.target.parentNode.localName === 'button' || e.target.parentNode.localName === 'a'))) { - return; - } - - if (deltaX + deltaY < 5 && e.button === 0 && this.props.onClick) { - this.props.onClick(); - } - - this.startXY = null; - } - - handleSpoilerClick = (e) => { - e.preventDefault(); - - if (this.props.onExpandedToggle) { - // The parent manages the state - this.props.onExpandedToggle(); - } else { - this.setState({ hidden: !this.state.hidden }); - } - } - - setRef = (c) => { - this.node = c; - } - - render () { - const { status } = this.props; - - const hidden = this.props.onExpandedToggle ? !this.props.expanded : this.state.hidden; - - const content = { __html: status.get('contentHtml') }; - const spoilerContent = { __html: status.get('spoilerHtml') }; - const directionStyle = { direction: 'ltr' }; - const classNames = classnames('status__content', { - 'status__content--with-action': this.props.onClick && this.context.router, - 'status__content--with-spoiler': status.get('spoiler_text').length > 0, - }); - - if (isRtl(status.get('search_index'))) { - directionStyle.direction = 'rtl'; - } - - if (status.get('spoiler_text').length > 0) { - let mentionsPlaceholder = ''; - - const mentionLinks = status.get('mentions').map(item => ( - - @{item.get('username')} - - )).reduce((aggregate, item) => [...aggregate, item, ' '], []); - - const toggleText = hidden ? : ; - - if (hidden) { - mentionsPlaceholder =
{mentionLinks}
; - } - - return ( -
- - - {mentionsPlaceholder} - -
-
- ); - } else if (this.props.onClick) { - return ( -
- ); - } else { - return ( -
- ); - } - } - -} diff --git a/app/javascript/mastodon/features/account/components/header.js b/app/javascript/mastodon/features/account/components/header.js deleted file mode 100644 index b3a73a59097..00000000000 --- a/app/javascript/mastodon/features/account/components/header.js +++ /dev/null @@ -1,131 +0,0 @@ -// THIS FILE EXISTS FOR UPSTREAM COMPATIBILITY & SHOULDN'T BE USED !! -// SEE INSTEAD : glitch/components/account/header - -import React from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import PropTypes from 'prop-types'; -import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; -import IconButton from '../../../components/icon_button'; -import Motion from '../../ui/util/optional_motion'; -import spring from 'react-motion/lib/spring'; -import ImmutablePureComponent from 'react-immutable-pure-component'; -import { autoPlayGif, me } from '../../../initial_state'; - -const messages = defineMessages({ - unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' }, - follow: { id: 'account.follow', defaultMessage: 'Follow' }, - requested: { id: 'account.requested', defaultMessage: 'Awaiting approval. Click to cancel follow request' }, -}); - -class Avatar extends ImmutablePureComponent { - - static propTypes = { - account: ImmutablePropTypes.map.isRequired, - }; - - state = { - isHovered: false, - }; - - handleMouseOver = () => { - if (this.state.isHovered) return; - this.setState({ isHovered: true }); - } - - handleMouseOut = () => { - if (!this.state.isHovered) return; - this.setState({ isHovered: false }); - } - - render () { - const { account } = this.props; - const { isHovered } = this.state; - - return ( - - {({ radius }) => - - {account.get('acct')} - - } - - ); - } - -} - -@injectIntl -export default class Header extends ImmutablePureComponent { - - static propTypes = { - account: ImmutablePropTypes.map, - onFollow: PropTypes.func.isRequired, - intl: PropTypes.object.isRequired, - }; - - render () { - const { account, intl } = this.props; - - if (!account) { - return null; - } - - let info = ''; - let actionBtn = ''; - let lockedIcon = ''; - - if (me !== account.get('id') && account.getIn(['relationship', 'followed_by'])) { - info = ; - } - - if (me !== account.get('id')) { - if (account.getIn(['relationship', 'requested'])) { - actionBtn = ( -
- -
- ); - } else if (!account.getIn(['relationship', 'blocking'])) { - actionBtn = ( -
- -
- ); - } - } - - if (account.get('locked')) { - lockedIcon = ; - } - - const content = { __html: account.get('note_emojified') }; - const displayNameHtml = { __html: account.get('display_name_html') }; - - return ( -
-
- - - - @{account.get('acct')} {lockedIcon} -
- - {info} - {actionBtn} -
-
- ); - } - -} diff --git a/app/javascript/mastodon/features/notifications/components/notification.js b/app/javascript/mastodon/features/notifications/components/notification.js deleted file mode 100644 index 9035268224a..00000000000 --- a/app/javascript/mastodon/features/notifications/components/notification.js +++ /dev/null @@ -1,155 +0,0 @@ -// THIS FILE EXISTS FOR UPSTREAM COMPATIBILITY & SHOULDN'T BE USED !! -// SEE INSTEAD : glitch/components/notification - -import React from 'react'; -import PropTypes from 'prop-types'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import StatusContainer from '../../../containers/status_container'; -import AccountContainer from '../../../containers/account_container'; -import { FormattedMessage } from 'react-intl'; -import Permalink from '../../../components/permalink'; -import ImmutablePureComponent from 'react-immutable-pure-component'; -import { HotKeys } from 'react-hotkeys'; - -export default class Notification extends ImmutablePureComponent { - - static contextTypes = { - router: PropTypes.object, - }; - - static propTypes = { - notification: ImmutablePropTypes.map.isRequired, - hidden: PropTypes.bool, - onMoveUp: PropTypes.func.isRequired, - onMoveDown: PropTypes.func.isRequired, - onMention: PropTypes.func.isRequired, - }; - - handleMoveUp = () => { - const { notification, onMoveUp } = this.props; - onMoveUp(notification.get('id')); - } - - handleMoveDown = () => { - const { notification, onMoveDown } = this.props; - onMoveDown(notification.get('id')); - } - - handleOpen = () => { - const { notification } = this.props; - - if (notification.get('status')) { - this.context.router.history.push(`/statuses/${notification.get('status')}`); - } else { - this.handleOpenProfile(); - } - } - - handleOpenProfile = () => { - const { notification } = this.props; - this.context.router.history.push(`/accounts/${notification.getIn(['account', 'id'])}`); - } - - handleMention = e => { - e.preventDefault(); - - const { notification, onMention } = this.props; - onMention(notification.get('account'), this.context.router.history); - } - - getHandlers () { - return { - moveUp: this.handleMoveUp, - moveDown: this.handleMoveDown, - open: this.handleOpen, - openProfile: this.handleOpenProfile, - mention: this.handleMention, - reply: this.handleMention, - }; - } - - renderFollow (account, link) { - return ( - -
-
-
- -
- - -
- -
-
- ); - } - - renderMention (notification) { - return ( -