From 297921fce570bfab413bab4e16a4ae694ecc4f28 Mon Sep 17 00:00:00 2001 From: kibigo! Date: Wed, 12 Jul 2017 01:02:51 -0700 Subject: Moved glitch files to their own location ;) --- app/javascript/glitch/components/account/header.js | 112 +++++++++++++++++++++ 1 file changed, 112 insertions(+) create mode 100644 app/javascript/glitch/components/account/header.js (limited to 'app/javascript/glitch/components/account') diff --git a/app/javascript/glitch/components/account/header.js b/app/javascript/glitch/components/account/header.js new file mode 100644 index 000000000..875ee3c54 --- /dev/null +++ b/app/javascript/glitch/components/account/header.js @@ -0,0 +1,112 @@ +// Package imports // +import React from 'react'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import PropTypes from 'prop-types'; +import escapeTextContentForBrowser from 'escape-html'; +import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; +import ImmutablePureComponent from 'react-immutable-pure-component'; + +// Mastodon imports // +import emojify from '../../../mastodon/emoji'; +import IconButton from '../../../mastodon/components/icon_button'; +import Avatar from '../../../mastodon/components/avatar'; + +// Our imports // +import { processBio } from '../../util/bio_metadata'; + +const messages = defineMessages({ + unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' }, + follow: { id: 'account.follow', defaultMessage: 'Follow' }, + requested: { id: 'account.requested', defaultMessage: 'Awaiting approval' }, +}); + +@injectIntl +export default class Header extends ImmutablePureComponent { + + static propTypes = { + account: ImmutablePropTypes.map, + me: PropTypes.number.isRequired, + onFollow: PropTypes.func.isRequired, + intl: PropTypes.object.isRequired, + }; + + render () { + const { account, me, intl } = this.props; + + if (!account) { + return null; + } + + let displayName = account.get('display_name'); + let info = ''; + let actionBtn = ''; + let lockedIcon = ''; + + if (displayName.length === 0) { + displayName = account.get('username'); + } + + 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 displayNameHTML = { __html: emojify(escapeTextContentForBrowser(displayName)) }; + const { text, metadata } = processBio(account.get('note')); + + return ( +
+
+
+ + + + + @{account.get('acct')} {lockedIcon} +
+ + {info} + {actionBtn} +
+
+ + {metadata.length && ( + + {(() => { + let data = []; + for (let i = 0; i < metadata.length; i++) { + data.push( + + + + + ); + } + return data; + })()} +
+ ) || null} +
+ ); + } + +} -- cgit From 21b04af524888fea134cc7dfa04e1203ede0427a Mon Sep 17 00:00:00 2001 From: kibigo! Date: Thu, 13 Jul 2017 14:20:51 -0700 Subject: Fixes lack of in header metadata table --- app/javascript/glitch/components/account/header.js | 26 ++++++++++++---------- 1 file changed, 14 insertions(+), 12 deletions(-) (limited to 'app/javascript/glitch/components/account') diff --git a/app/javascript/glitch/components/account/header.js b/app/javascript/glitch/components/account/header.js index 875ee3c54..e2d961240 100644 --- a/app/javascript/glitch/components/account/header.js +++ b/app/javascript/glitch/components/account/header.js @@ -91,18 +91,20 @@ export default class Header extends ImmutablePureComponent { {metadata.length && ( - {(() => { - let data = []; - for (let i = 0; i < metadata.length; i++) { - data.push( - - - - - ); - } - return data; - })()} + + {(() => { + let data = []; + for (let i = 0; i < metadata.length; i++) { + data.push( + + + + + ); + } + return data; + })()} +
) || null}
-- cgit From d0aad1ac854eaa53f9b7d38cc8dd90e289790629 Mon Sep 17 00:00:00 2001 From: kibigo! Date: Fri, 14 Jul 2017 11:13:02 -0700 Subject: Documentation and cleanup --- app/javascript/glitch/actions/local_settings.js | 18 +- app/javascript/glitch/components/account/header.js | 163 ++++++++++++-- .../compose/advanced_options/container.js | 44 ++++ .../components/compose/advanced_options/index.js | 236 +++++++++++++++------ .../components/compose/advanced_options/toggle.js | 103 +++++++++ .../glitch/components/notification/container.js | 47 +++- .../glitch/components/notification/follow.js | 171 +++++++++++++++ .../components/notification/follow_notification.js | 78 ------- .../glitch/components/notification/index.js | 6 +- app/javascript/glitch/reducers/local_settings.js | 24 +-- app/javascript/glitch/util/bio_metadata.js | 4 +- 11 files changed, 705 insertions(+), 189 deletions(-) create mode 100644 app/javascript/glitch/components/compose/advanced_options/toggle.js create mode 100644 app/javascript/glitch/components/notification/follow.js delete mode 100644 app/javascript/glitch/components/notification/follow_notification.js (limited to 'app/javascript/glitch/components/account') diff --git a/app/javascript/glitch/actions/local_settings.js b/app/javascript/glitch/actions/local_settings.js index 479b5841d..93c5a9a17 100644 --- a/app/javascript/glitch/actions/local_settings.js +++ b/app/javascript/glitch/actions/local_settings.js @@ -21,12 +21,12 @@ consists of the following: */ - /* * * * */ +// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * /* -Constants ---------- +Constants: +---------- We provide the following constants: @@ -39,12 +39,12 @@ We provide the following constants: 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`. `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. `changeLocalSetting()` calls this whenever it changes a setting. We diff --git a/app/javascript/glitch/components/account/header.js b/app/javascript/glitch/components/account/header.js index e2d961240..b79140c02 100644 --- a/app/javascript/glitch/components/account/header.js +++ b/app/javascript/glitch/components/account/header.js @@ -1,3 +1,45 @@ +/* + +`` +================= + +> 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'; @@ -14,25 +56,63 @@ import Avatar from '../../../mastodon/components/avatar'; // 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 Header extends ImmutablePureComponent { +export default class AccountHeader extends ImmutablePureComponent { static propTypes = { - account: ImmutablePropTypes.map, - me: PropTypes.number.isRequired, - onFollow: PropTypes.func.isRequired, - intl: PropTypes.object.isRequired, + account : ImmutablePropTypes.map, + me : PropTypes.number.isRequired, + onFollow : PropTypes.func.isRequired, + intl : PropTypes.object.isRequired, }; +/* + +### `render()` + +The `render()` function is used to render our component. + +*/ + render () { 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) { return null; } @@ -40,17 +120,30 @@ export default class Header extends ImmutablePureComponent { let displayName = account.get('display_name'); let info = ''; let actionBtn = ''; - let lockedIcon = ''; + let following = false; if (displayName.length === 0) { displayName = account.get('username'); } - if (me !== account.get('id') && account.getIn(['relationship', 'followed_by'])) { - info = ; - } +/* + +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 = (
@@ -58,30 +151,64 @@ export default class Header extends ImmutablePureComponent {
); } else if (!account.getIn(['relationship', 'blocking'])) { + following = account.getIn(['relationship', 'following']); actionBtn = (
- +
); } } - if (account.get('locked')) { - lockedIcon = ; - } +/* - 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')); +/* + +Here, we render our component using all the things we've defined above. + +*/ + return (
-
+
- - + + + + - @{account.get('acct')} {lockedIcon} + + @{account.get('acct')} + {account.get('locked') ? : null} +
{info} diff --git a/app/javascript/glitch/components/compose/advanced_options/container.js b/app/javascript/glitch/components/compose/advanced_options/container.js index 10804454a..160f22737 100644 --- a/app/javascript/glitch/components/compose/advanced_options/container.js +++ b/app/javascript/glitch/components/compose/advanced_options/container.js @@ -1,3 +1,21 @@ +/* + +`` +=================================== + +This container connects `` to the Redux store. + +*/ + +// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + +/* + +Imports: +-------- + +*/ + // Package imports // import { connect } from 'react-redux'; @@ -7,10 +25,36 @@ import { toggleComposeAdvancedOption } from '../../../../mastodon/actions/compos // 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) { diff --git a/app/javascript/glitch/components/compose/advanced_options/index.js b/app/javascript/glitch/components/compose/advanced_options/index.js index dabf66095..b745d1cdf 100644 --- a/app/javascript/glitch/components/compose/advanced_options/index.js +++ b/app/javascript/glitch/components/compose/advanced_options/index.js @@ -1,137 +1,241 @@ +/* + +`` +========================== + +> For more information on the contents of this file, please contact: +> +> - surinna [@srn@dev.glitch.social] + +This adds an advanced options dropdown to the toot compose box, for +toggles that don't necessarily fit elsewhere. + +__Props:__ + + - __`values` (`ImmutablePropTypes.contains(…).isRequired`) :__ + An Immutable map with the following values: + + - __`do_not_federate` (`PropTypes.bool.isRequired`) :__ + Specifies whether or not to federate the status. + + - __`onChange` (`PropTypes.func.isRequired`) :__ + The function to call when a toggle is changed. We pass this from + our container to the toggle. + + - __`intl` (`PropTypes.object.isRequired`) :__ + Our internationalization object, inserted by `@injectIntl`. + +__State:__ + + - __`open` :__ + This tells whether the dropdown is currently open or closed. + +*/ + +// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + +/* + +Imports: +-------- + +*/ + // Package imports // import React from 'react'; import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; -import Toggle from 'react-toggle'; import { injectIntl, defineMessages } from 'react-intl'; // Mastodon imports // import IconButton from '../../../../mastodon/components/icon_button'; +// Our imports // +import ComposeAdvancedOptionsToggle from './toggle'; + +// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + +/* + +Inital setup: +------------- + +The `messages` constant is used to define any messages that we need +from inside props. These are the various titles and labels on our +toggles. + +`iconStyle` styles the icon used for the dropdown button. + +*/ + const messages = defineMessages({ - local_only_short: { id: 'advanced-options.local-only.short', defaultMessage: 'Local-only' }, - local_only_long: { id: 'advanced-options.local-only.long', defaultMessage: 'Do not post to other instances' }, - advanced_options_icon_title: { id: 'advanced_options.icon_title', defaultMessage: 'Advanced options' }, + local_only_short : + { id: 'advanced-options.local-only.short', defaultMessage: 'Local-only' }, + local_only_long : + { id: 'advanced-options.local-only.long', defaultMessage: 'Do not post to other instances' }, + advanced_options_icon_title : + { id: 'advanced_options.icon_title', defaultMessage: 'Advanced options' }, }); const iconStyle = { - height: null, - lineHeight: '27px', + height : null, + lineHeight : '27px', }; -class AdvancedOptionToggle extends React.PureComponent { - - static propTypes = { - onChange: PropTypes.func.isRequired, - active: PropTypes.bool.isRequired, - name: PropTypes.string.isRequired, - shortText: PropTypes.string.isRequired, - longText: PropTypes.string.isRequired, - } - - onToggle = () => { - this.props.onChange(this.props.name); - } +/* - render() { - const { active, shortText, longText } = this.props; - return ( -
-
- -
-
- {shortText} - {longText} -
-
- ); - } +Implementation: +--------------- -} +*/ @injectIntl export default class ComposeAdvancedOptions extends React.PureComponent { static propTypes = { - values: ImmutablePropTypes.contains({ - do_not_federate: PropTypes.bool.isRequired, + values : ImmutablePropTypes.contains({ + do_not_federate : PropTypes.bool.isRequired, }).isRequired, - onChange: PropTypes.func.isRequired, - intl: PropTypes.object.isRequired, + onChange : PropTypes.func.isRequired, + intl : PropTypes.object.isRequired, }; + state = { + open: false, + }; + +/* + +### `onToggleDropdown()` + +This function toggles the opening and closing of the advanced options +dropdown. + +*/ + onToggleDropdown = () => { this.setState({ open: !this.state.open }); }; +/* + +### `onGlobalClick(e)` + +This function closes the advanced options dropdown if you click +anywhere else on the screen. + +*/ + onGlobalClick = (e) => { if (e.target !== this.node && !this.node.contains(e.target) && this.state.open) { this.setState({ open: false }); } } +/* + +### `componentDidMount()`, `componentWillUnmount()` + +This function closes the advanced options dropdown if you click +anywhere else on the screen. + +*/ + componentDidMount () { window.addEventListener('click', this.onGlobalClick); window.addEventListener('touchstart', this.onGlobalClick); } - componentWillUnmount () { window.removeEventListener('click', this.onGlobalClick); window.removeEventListener('touchstart', this.onGlobalClick); } - state = { - open: false, - }; +/* - handleClick = (e) => { - const option = e.currentTarget.getAttribute('data-index'); - e.preventDefault(); - this.props.onChange(option); - } +### `setRef(c)` + +`setRef()` stores a reference to the dropdown's `
in `this.node`. + +*/ setRef = (c) => { this.node = c; } +/* + +### `render()` + +`render()` actually puts our component on the screen. + +*/ + render () { const { open } = this.state; const { intl, values } = this.props; +/* + +The `options` array provides all of the available advanced options +alongside their icon, text, and name. + +*/ const options = [ - { icon: 'wifi', shortText: messages.local_only_short, longText: messages.local_only_long, key: 'do_not_federate' }, + { icon: 'wifi', shortText: messages.local_only_short, longText: messages.local_only_long, name: 'do_not_federate' }, ]; +/* + +`anyEnabled` tells us if any of our advanced options have been enabled. + +*/ + const anyEnabled = values.some((enabled) => enabled); + +/* + +`optionElems` takes our `options` and creates +``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 ( - ); }); - return (
-
- -
-
- {optionElems} +/* + +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 new file mode 100644 index 000000000..d6907472a --- /dev/null +++ b/app/javascript/glitch/components/compose/advanced_options/toggle.js @@ -0,0 +1,103 @@ +/* + +`` +================================ + +> 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/notification/container.js b/app/javascript/glitch/components/notification/container.js index 60303537d..bed086172 100644 --- a/app/javascript/glitch/components/notification/container.js +++ b/app/javascript/glitch/components/notification/container.js @@ -1,3 +1,21 @@ +/* + +`` +========================= + +This container connects ``s to the Redux store. + +*/ + +// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + +/* + +Imports: +-------- + +*/ + // Package imports // import { connect } from 'react-redux'; @@ -8,6 +26,20 @@ import { makeGetNotification } from '../../../mastodon/selectors'; import Notification from '.'; 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 getNotification = makeGetNotification(); @@ -19,7 +51,20 @@ const makeMapStateToProps = () => { 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) { dispatch(deleteNotification(id)); }, diff --git a/app/javascript/glitch/components/notification/follow.js b/app/javascript/glitch/components/notification/follow.js new file mode 100644 index 000000000..26396478b --- /dev/null +++ b/app/javascript/glitch/components/notification/follow.js @@ -0,0 +1,171 @@ +/* + +`` +====================== + +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 = ( + + ); + +/* + +`link` is a container for the account's `displayName`, which links to +the account timeline using a ``. + +*/ + + const displayName = account.get('display_name') || account.get('username'); + const displayNameHTML = { __html: emojify(escapeTextContentForBrowser(displayName)) }; + const link = ( + + ); + +/* + +We can now render our component. + +*/ + + return ( +
+
+
+ +
+ + + + {dismiss} +
+ + +
+ ); + } + +} diff --git a/app/javascript/glitch/components/notification/follow_notification.js b/app/javascript/glitch/components/notification/follow_notification.js deleted file mode 100644 index 7cabd91f6..000000000 --- a/app/javascript/glitch/components/notification/follow_notification.js +++ /dev/null @@ -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 = ( - - ); - - const displayName = account.get('display_name').length > 0 ? account.get('display_name') : account.get('username'); - const displayNameHTML = { __html: emojify(escapeTextContentForBrowser(displayName)) }; - const link = ; - return ( -
-
-
- -
- - - - {dismiss} -
- - -
- ); - } - -} diff --git a/app/javascript/glitch/components/notification/index.js b/app/javascript/glitch/components/notification/index.js index 0cdc03cbe..556d5aea8 100644 --- a/app/javascript/glitch/components/notification/index.js +++ b/app/javascript/glitch/components/notification/index.js @@ -8,7 +8,7 @@ import PropTypes from 'prop-types'; // Our imports // import StatusContainer from '../status/container'; -import FollowNotification from './follow_notification'; +import NotificationFollow from './follow'; export default class Notification extends ImmutablePureComponent { @@ -20,8 +20,8 @@ export default class Notification extends ImmutablePureComponent { renderFollow (notification) { return ( - diff --git a/app/javascript/glitch/reducers/local_settings.js b/app/javascript/glitch/reducers/local_settings.js index 776dcead7..35a8e065b 100644 --- a/app/javascript/glitch/reducers/local_settings.js +++ b/app/javascript/glitch/reducers/local_settings.js @@ -18,12 +18,12 @@ associated actions are: */ - /* * * * */ +// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * /* -Imports -------- +Imports: +-------- */ @@ -36,12 +36,12 @@ import { STORE_HYDRATE } from '../../mastodon/actions/store'; // Our imports // import { LOCAL_SETTING_CHANGE } from '../actions/local_settings'; - /* * * * */ +// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * /* -initialState ------------- +initialState: +------------- You can see the default values for all of our local settings here. These are only used if no previously-saved values exist. @@ -71,12 +71,12 @@ const initialState = ImmutableMap({ }), }); - /* * * * */ +// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * /* -Helper functions ----------------- +Helper functions: +----------------- ### `hydrate(state, localSettings)` @@ -89,12 +89,12 @@ from `localStorage`. const hydrate = (state, localSettings) => state.mergeDeep(localSettings); - /* * * * */ +// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * /* -`localSettings(state = initialState, action)` ---------------------------------------------- +`localSettings(state = initialState, action)`: +---------------------------------------------- This function holds our actual reducer. diff --git a/app/javascript/glitch/util/bio_metadata.js b/app/javascript/glitch/util/bio_metadata.js index c5e87f356..0c8195e9d 100644 --- a/app/javascript/glitch/util/bio_metadata.js +++ b/app/javascript/glitch/util/bio_metadata.js @@ -1,7 +1,7 @@ /* `util/bio_metadata` -======================== +=================== > For more information on the contents of this file, please contact: > @@ -26,7 +26,7 @@ functions are: */ - /* * * * */ +// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * /*********************************************************************\ -- cgit From 7de0fa698dcdb6bd237b260fd42b1ce9bfc7191e Mon Sep 17 00:00:00 2001 From: Ondřej Hruška Date: Sun, 6 Aug 2017 20:59:53 +0200 Subject: Updated glitch files to use the new Avatar class correctly --- app/javascript/glitch/components/account/header.js | 6 +----- app/javascript/glitch/components/status/header.js | 11 ++--------- 2 files changed, 3 insertions(+), 14 deletions(-) (limited to 'app/javascript/glitch/components/account') diff --git a/app/javascript/glitch/components/account/header.js b/app/javascript/glitch/components/account/header.js index b79140c02..a1197c4be 100644 --- a/app/javascript/glitch/components/account/header.js +++ b/app/javascript/glitch/components/account/header.js @@ -194,11 +194,7 @@ Here, we render our component using all the things we've defined above.
- + ` if we don't. >
{ friend ? ( - + ) : ( - + ) }
-- cgit From cf24ce7e0300b8280631efddd58ce576e5cccabf Mon Sep 17 00:00:00 2001 From: Ondřej Hruška Date: Thu, 28 Sep 2017 11:03:23 +0200 Subject: remove wrong emojify usage in old glitch components --- app/javascript/glitch/components/account/header.js | 14 ++++---------- app/javascript/glitch/components/notification/follow.js | 7 ++----- app/javascript/glitch/components/status/content.js | 8 ++------ app/javascript/glitch/components/status/prepend.js | 8 +------- 4 files changed, 9 insertions(+), 28 deletions(-) (limited to 'app/javascript/glitch/components/account') diff --git a/app/javascript/glitch/components/account/header.js b/app/javascript/glitch/components/account/header.js index a1197c4be..bc2ce30f6 100644 --- a/app/javascript/glitch/components/account/header.js +++ b/app/javascript/glitch/components/account/header.js @@ -44,7 +44,6 @@ Imports: import React from 'react'; import ImmutablePropTypes from 'react-immutable-proptypes'; import PropTypes from 'prop-types'; -import escapeTextContentForBrowser from 'escape-html'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import ImmutablePureComponent from 'react-immutable-pure-component'; @@ -89,7 +88,7 @@ export default class AccountHeader extends ImmutablePureComponent { static propTypes = { account : ImmutablePropTypes.map, - me : PropTypes.number.isRequired, + me : PropTypes.string.isRequired, onFollow : PropTypes.func.isRequired, intl : PropTypes.object.isRequired, }; @@ -117,7 +116,7 @@ then we set the `displayName` to just be the `username` of the account. return null; } - let displayName = account.get('display_name'); + let displayName = account.get('display_name_html'); let info = ''; let actionBtn = ''; let following = false; @@ -167,16 +166,11 @@ appropriate icon. } /* - -`displayNameHTML` processes the `displayName` and prepares it for -insertion into the document. Meanwhile, we extract the `text` and + 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')); /* @@ -198,7 +192,7 @@ Here, we render our component using all the things we've defined above.
diff --git a/app/javascript/glitch/components/notification/follow.js b/app/javascript/glitch/components/notification/follow.js index 99a5c6219..e2c21bf35 100644 --- a/app/javascript/glitch/components/notification/follow.js +++ b/app/javascript/glitch/components/notification/follow.js @@ -11,11 +11,9 @@ import React from 'react'; import ImmutablePropTypes from 'react-immutable-proptypes'; import PropTypes from 'prop-types'; import { FormattedMessage } 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'; @@ -39,15 +37,14 @@ export default class NotificationFollow extends ImmutablePureComponent { const { account, notification } = this.props; // Links to the display name. - const displayName = account.get('display_name') || account.get('username'); - const displayNameHTML = { __html: emojify(escapeTextContentForBrowser(displayName)) }; + const displayName = account.get('display_name_html') || account.get('username'); const link = ( ); diff --git a/app/javascript/glitch/components/status/content.js b/app/javascript/glitch/components/status/content.js index 9dd34fcb2..06015619b 100644 --- a/app/javascript/glitch/components/status/content.js +++ b/app/javascript/glitch/components/status/content.js @@ -1,13 +1,11 @@ // Package imports // import React from 'react'; import ImmutablePropTypes from 'react-immutable-proptypes'; -import escapeTextContentForBrowser from 'escape-html'; import PropTypes from 'prop-types'; import { FormattedMessage } from 'react-intl'; import classnames from 'classnames'; // Mastodon imports // -import emojify from '../../../mastodon/emoji'; import { isRtl } from '../../../mastodon/rtl'; import Permalink from '../../../mastodon/components/permalink'; @@ -32,7 +30,7 @@ export default class StatusContent extends React.PureComponent { const node = this.node; const links = node.querySelectorAll('a'); - for (var i = 0; i < links.length; ++i) { + for (let i = 0; i < links.length; ++i) { let link = links[i]; let mention = this.props.status.get('mentions').find(item => link.href === item.get('url')); @@ -132,9 +130,7 @@ export default class StatusContent extends React.PureComponent { ); const content = { __html: status.get('contentHtml') }; - const spoilerContent = { - __html: status.get('spoilerHtml'), - }; + const spoilerContent = { __html: status.get('spoilerHtml') }; const directionStyle = { direction: 'ltr' }; const classNames = classnames('status__content', { 'status__content--with-action': parseClick && !disabled, diff --git a/app/javascript/glitch/components/status/prepend.js b/app/javascript/glitch/components/status/prepend.js index 6213e4c8d..8c0aed0f4 100644 --- a/app/javascript/glitch/components/status/prepend.js +++ b/app/javascript/glitch/components/status/prepend.js @@ -22,12 +22,8 @@ Imports: import React from 'react'; import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; -import escapeTextContentForBrowser from 'escape-html'; import { FormattedMessage } from 'react-intl'; -// Mastodon imports // -import emojify from '../../../mastodon/emoji'; - /* * * * */ /* @@ -99,9 +95,7 @@ generate the message. > -- cgit From 35e2cad4eb32649ddc674f209b41e4440de5113b Mon Sep 17 00:00:00 2001 From: kibigo! Date: Wed, 11 Oct 2017 11:00:28 -0700 Subject: Fixed header emojification --- app/javascript/glitch/components/account/header.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'app/javascript/glitch/components/account') diff --git a/app/javascript/glitch/components/account/header.js b/app/javascript/glitch/components/account/header.js index bc2ce30f6..9404ee6a5 100644 --- a/app/javascript/glitch/components/account/header.js +++ b/app/javascript/glitch/components/account/header.js @@ -48,7 +48,7 @@ import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import ImmutablePureComponent from 'react-immutable-pure-component'; // Mastodon imports // -import emojify from '../../../mastodon/emoji'; +import emojify from 'mastodon/features/emoji/emoji'; import IconButton from '../../../mastodon/components/icon_button'; import Avatar from '../../../mastodon/components/avatar'; -- cgit From 3cc6255a7e01645799be7af01de0d7c2ec18832a Mon Sep 17 00:00:00 2001 From: kibigo! Date: Wed, 11 Oct 2017 11:50:11 -0700 Subject: Removed length check which now happens in reducer --- app/javascript/glitch/components/account/header.js | 4 ---- 1 file changed, 4 deletions(-) (limited to 'app/javascript/glitch/components/account') diff --git a/app/javascript/glitch/components/account/header.js b/app/javascript/glitch/components/account/header.js index 9404ee6a5..6359c1775 100644 --- a/app/javascript/glitch/components/account/header.js +++ b/app/javascript/glitch/components/account/header.js @@ -121,10 +121,6 @@ then we set the `displayName` to just be the `username` of the account. let actionBtn = ''; let following = false; - if (displayName.length === 0) { - displayName = account.get('username'); - } - /* Next, we handle the account relationships. If the account follows the -- cgit From 37ff061d9bf0a91da6580960be01e3c6bc5e5c4c Mon Sep 17 00:00:00 2001 From: Ondřej Hruška Date: Mon, 16 Oct 2017 22:24:44 +0200 Subject: satisfy eslint and jest --- app/javascript/glitch/components/account/header.js | 2 +- app/javascript/glitch/components/local_settings/container.js | 4 ++-- app/javascript/glitch/components/local_settings/index.js | 2 +- app/javascript/glitch/components/local_settings/navigation/index.js | 2 +- .../glitch/components/local_settings/navigation/item/index.js | 2 +- app/javascript/glitch/components/local_settings/page/index.js | 2 +- app/javascript/glitch/components/local_settings/page/item/index.js | 2 +- .../mastodon/components/__tests__/__snapshots__/avatar-test.js.snap | 2 ++ .../components/__tests__/__snapshots__/avatar_overlay-test.js.snap | 2 ++ app/javascript/packs/application.js | 3 ++- app/javascript/themes/spin/pack.js | 4 ++-- 11 files changed, 16 insertions(+), 11 deletions(-) (limited to 'app/javascript/glitch/components/account') diff --git a/app/javascript/glitch/components/account/header.js b/app/javascript/glitch/components/account/header.js index 6359c1775..f4a413aa3 100644 --- a/app/javascript/glitch/components/account/header.js +++ b/app/javascript/glitch/components/account/header.js @@ -48,7 +48,7 @@ import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import ImmutablePureComponent from 'react-immutable-pure-component'; // Mastodon imports // -import emojify from 'mastodon/features/emoji/emoji'; +import emojify from '../../../mastodon/features/emoji/emoji'; import IconButton from '../../../mastodon/components/icon_button'; import Avatar from '../../../mastodon/components/avatar'; diff --git a/app/javascript/glitch/components/local_settings/container.js b/app/javascript/glitch/components/local_settings/container.js index 6c202a4e7..4569db99f 100644 --- a/app/javascript/glitch/components/local_settings/container.js +++ b/app/javascript/glitch/components/local_settings/container.js @@ -2,10 +2,10 @@ import { connect } from 'react-redux'; // Mastodon imports // -import { closeModal } from 'mastodon/actions/modal'; +import { closeModal } from '../../../mastodon/actions/modal'; // Our imports // -import { changeLocalSetting } from 'glitch/actions/local_settings'; +import { changeLocalSetting } from '../../../glitch/actions/local_settings'; import LocalSettings from '.'; const mapStateToProps = state => ({ diff --git a/app/javascript/glitch/components/local_settings/index.js b/app/javascript/glitch/components/local_settings/index.js index 7f7b93de4..ef711229a 100644 --- a/app/javascript/glitch/components/local_settings/index.js +++ b/app/javascript/glitch/components/local_settings/index.js @@ -8,7 +8,7 @@ import LocalSettingsPage from './page'; import LocalSettingsNavigation from './navigation'; // Stylesheet imports -import './style'; +import './style.scss'; export default class LocalSettings extends React.PureComponent { diff --git a/app/javascript/glitch/components/local_settings/navigation/index.js b/app/javascript/glitch/components/local_settings/navigation/index.js index 1f72cc824..fa35e83c7 100644 --- a/app/javascript/glitch/components/local_settings/navigation/index.js +++ b/app/javascript/glitch/components/local_settings/navigation/index.js @@ -7,7 +7,7 @@ import { injectIntl, defineMessages } from 'react-intl'; import LocalSettingsNavigationItem from './item'; // Stylesheet imports -import './style'; +import './style.scss'; // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * diff --git a/app/javascript/glitch/components/local_settings/navigation/item/index.js b/app/javascript/glitch/components/local_settings/navigation/item/index.js index 1676aa404..a352d5fb2 100644 --- a/app/javascript/glitch/components/local_settings/navigation/item/index.js +++ b/app/javascript/glitch/components/local_settings/navigation/item/index.js @@ -4,7 +4,7 @@ import PropTypes from 'prop-types'; import classNames from 'classnames'; // Stylesheet imports -import './style'; +import './style.scss'; // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * diff --git a/app/javascript/glitch/components/local_settings/page/index.js b/app/javascript/glitch/components/local_settings/page/index.js index 338d86333..366c113c0 100644 --- a/app/javascript/glitch/components/local_settings/page/index.js +++ b/app/javascript/glitch/components/local_settings/page/index.js @@ -8,7 +8,7 @@ import { defineMessages, FormattedMessage, injectIntl } from 'react-intl'; import LocalSettingsPageItem from './item'; // Stylesheet imports -import './style'; +import './style.scss'; // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * diff --git a/app/javascript/glitch/components/local_settings/page/item/index.js b/app/javascript/glitch/components/local_settings/page/item/index.js index 326c7eeb0..37e28c084 100644 --- a/app/javascript/glitch/components/local_settings/page/item/index.js +++ b/app/javascript/glitch/components/local_settings/page/item/index.js @@ -4,7 +4,7 @@ import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; // Stylesheet imports -import './style'; +import './style.scss'; // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * diff --git a/app/javascript/mastodon/components/__tests__/__snapshots__/avatar-test.js.snap b/app/javascript/mastodon/components/__tests__/__snapshots__/avatar-test.js.snap index 76ab3374a..4005c860f 100644 --- a/app/javascript/mastodon/components/__tests__/__snapshots__/avatar-test.js.snap +++ b/app/javascript/mastodon/components/__tests__/__snapshots__/avatar-test.js.snap @@ -3,6 +3,7 @@ exports[` Autoplay renders a animated avatar 1`] = `
Autoplay renders a animated avatar 1`] = ` exports[` Still renders a still avatar 1`] = `
Date: Thu, 9 Nov 2017 08:41:10 -0600 Subject: "Show reblogs" per-follower UI/database changes TODO: * Tests (particularly for FollowRequests). * Anything to respect the setting when putting reblogs in timelines. --- app/controllers/api/v1/accounts_controller.rb | 6 +++-- app/javascript/glitch/components/account/header.js | 2 +- app/javascript/mastodon/actions/accounts.js | 4 +-- app/javascript/mastodon/components/account.js | 2 +- .../features/account/components/action_bar.js | 12 +++++++++ .../features/account_timeline/components/header.js | 6 +++++ .../containers/header_container.js | 8 ++++++ app/models/concerns/account_interactions.rb | 23 +++++++++++++--- app/models/follow.rb | 1 + app/models/follow_request.rb | 3 ++- app/services/follow_service.rb | 31 ++++++++++++++++------ db/schema.rb | 4 ++- 12 files changed, 82 insertions(+), 20 deletions(-) (limited to 'app/javascript/glitch/components/account') diff --git a/app/controllers/api/v1/accounts_controller.rb b/app/controllers/api/v1/accounts_controller.rb index 4676f60de..afdbf6e2d 100644 --- a/app/controllers/api/v1/accounts_controller.rb +++ b/app/controllers/api/v1/accounts_controller.rb @@ -13,9 +13,11 @@ class Api::V1::AccountsController < Api::BaseController end def follow - FollowService.new.call(current_user.account, @account.acct) + reblogs_arg = { reblogs: params[:reblogs] } + + FollowService.new.call(current_user.account, @account.acct, reblogs_arg) - options = @account.locked? ? {} : { following_map: { @account.id => true }, requested_map: { @account.id => false } } + options = @account.locked? ? {} : { following_map: reblogs_arg, requested_map: { @account.id => false } } render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships(options) end diff --git a/app/javascript/glitch/components/account/header.js b/app/javascript/glitch/components/account/header.js index f4a413aa3..c94fb0851 100644 --- a/app/javascript/glitch/components/account/header.js +++ b/app/javascript/glitch/components/account/header.js @@ -152,7 +152,7 @@ appropriate icon. diff --git a/app/javascript/mastodon/actions/accounts.js b/app/javascript/mastodon/actions/accounts.js index fbaebf786..cabf72bde 100644 --- a/app/javascript/mastodon/actions/accounts.js +++ b/app/javascript/mastodon/actions/accounts.js @@ -105,11 +105,11 @@ export function fetchAccountFail(id, error) { }; }; -export function followAccount(id) { +export function followAccount(id, reblogs = true) { return (dispatch, getState) => { dispatch(followAccountRequest(id)); - api(getState).post(`/api/v1/accounts/${id}/follow`).then(response => { + api(getState).post(`/api/v1/accounts/${id}/follow`, { reblogs }).then(response => { dispatch(followAccountSuccess(response.data)); }).catch(error => { dispatch(followAccountFail(error)); diff --git a/app/javascript/mastodon/components/account.js b/app/javascript/mastodon/components/account.js index 7cdb8c672..376e544fb 100644 --- a/app/javascript/mastodon/components/account.js +++ b/app/javascript/mastodon/components/account.js @@ -93,7 +93,7 @@ export default class Account extends ImmutablePureComponent {
); } else { - buttons = ; + buttons = ; } } diff --git a/app/javascript/mastodon/features/account/components/action_bar.js b/app/javascript/mastodon/features/account/components/action_bar.js index 2819ae252..718e7fbad 100644 --- a/app/javascript/mastodon/features/account/components/action_bar.js +++ b/app/javascript/mastodon/features/account/components/action_bar.js @@ -19,6 +19,8 @@ const messages = defineMessages({ media: { id: 'account.media', defaultMessage: 'Media' }, blockDomain: { id: 'account.block_domain', defaultMessage: 'Hide everything from {domain}' }, unblockDomain: { id: 'account.unblock_domain', defaultMessage: 'Unhide {domain}' }, + hideReblogs: { id: 'account.hide_reblogs', defaultMessage: 'Hide boosts from @{name}' }, + showReblogs: { id: 'account.show_reblogs', defaultMessage: 'Show boosts from @{name}' }, }); @injectIntl @@ -30,6 +32,7 @@ export default class ActionBar extends React.PureComponent { onFollow: PropTypes.func, onBlock: PropTypes.func.isRequired, onMention: PropTypes.func.isRequired, + onReblogToggle: PropTypes.func.isRequired, onReport: PropTypes.func.isRequired, onMute: PropTypes.func.isRequired, onBlockDomain: PropTypes.func.isRequired, @@ -60,6 +63,15 @@ export default class ActionBar extends React.PureComponent { if (account.get('id') === me) { menu.push({ text: intl.formatMessage(messages.edit_profile), href: '/settings/profile' }); } else { + const following = account.getIn(['relationship', 'following']); + if (following) { + if (following.get('reblogs')) { + menu.push({ text: intl.formatMessage(messages.hideReblogs, { name: account.get('username') }), action: this.props.onReblogToggle }); + } else { + menu.push({ text: intl.formatMessage(messages.showReblogs, { name: account.get('username') }), action: this.props.onReblogToggle }); + } + } + if (account.getIn(['relationship', 'muting'])) { menu.push({ text: intl.formatMessage(messages.unmute, { name: account.get('username') }), action: this.props.onMute }); } else { diff --git a/app/javascript/mastodon/features/account_timeline/components/header.js b/app/javascript/mastodon/features/account_timeline/components/header.js index c3cd4e55d..b33df282f 100644 --- a/app/javascript/mastodon/features/account_timeline/components/header.js +++ b/app/javascript/mastodon/features/account_timeline/components/header.js @@ -14,6 +14,7 @@ export default class Header extends ImmutablePureComponent { onFollow: PropTypes.func.isRequired, onBlock: PropTypes.func.isRequired, onMention: PropTypes.func.isRequired, + onReblogToggle: PropTypes.func.isRequired, onReport: PropTypes.func.isRequired, onMute: PropTypes.func.isRequired, onBlockDomain: PropTypes.func.isRequired, @@ -40,6 +41,10 @@ export default class Header extends ImmutablePureComponent { this.props.onReport(this.props.account); } + handleReblogToggle = () => { + this.props.onReblogToggle(this.props.account); + } + handleMute = () => { this.props.onMute(this.props.account); } @@ -80,6 +85,7 @@ export default class Header extends ImmutablePureComponent { me={me} onBlock={this.handleBlock} onMention={this.handleMention} + onReblogToggle={this.handleReblogToggle} onReport={this.handleReport} onMute={this.handleMute} onBlockDomain={this.handleBlockDomain} diff --git a/app/javascript/mastodon/features/account_timeline/containers/header_container.js b/app/javascript/mastodon/features/account_timeline/containers/header_container.js index 9ad13a231..68c037e9b 100644 --- a/app/javascript/mastodon/features/account_timeline/containers/header_container.js +++ b/app/javascript/mastodon/features/account_timeline/containers/header_container.js @@ -68,6 +68,14 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ dispatch(mentionCompose(account, router)); }, + onReblogToggle (account) { + if (account.getIn(['relationship', 'following', 'reblogs'])) { + dispatch(followAccount(account.get('id'), false)); + } else { + dispatch(followAccount(account.get('id'), true)); + } + }, + onReport (account) { dispatch(initReport(account)); }, diff --git a/app/models/concerns/account_interactions.rb b/app/models/concerns/account_interactions.rb index 0afdebf89..088fef4da 100644 --- a/app/models/concerns/account_interactions.rb +++ b/app/models/concerns/account_interactions.rb @@ -5,7 +5,11 @@ module AccountInteractions class_methods do def following_map(target_account_ids, account_id) - follow_mapping(Follow.where(target_account_id: target_account_ids, account_id: account_id), :target_account_id) + Follow.where(target_account_id: target_account_ids, account_id: account_id).each_with_object({}) do |follow, mapping| + mapping[follow.target_account_id] = { + reblogs: follow.show_reblogs? + } + end end def followed_by_map(target_account_ids, account_id) @@ -25,7 +29,11 @@ module AccountInteractions end def requested_map(target_account_ids, account_id) - follow_mapping(FollowRequest.where(target_account_id: target_account_ids, account_id: account_id), :target_account_id) + FollowRequest.where(target_account_id: target_account_ids, account_id: account_id).each_with_object({}) do |follow_request, mapping| + mapping[follow_request.target_account_id] = { + reblogs: follow_request.show_reblogs? + } + end end def domain_blocking_map(target_account_ids, account_id) @@ -66,8 +74,15 @@ module AccountInteractions has_many :domain_blocks, class_name: 'AccountDomainBlock', dependent: :destroy end - def follow!(other_account) - active_relationships.find_or_create_by!(target_account: other_account) + def follow!(other_account, reblogs: nil) + reblogs = true if reblogs.nil? + rel = active_relationships.create_with(show_reblogs: reblogs).find_or_create_by!(target_account: other_account) + if rel.show_reblogs != reblogs + rel.show_reblogs = reblogs + rel.save! + end + + rel end def block!(other_account) diff --git a/app/models/follow.rb b/app/models/follow.rb index 667720a88..a8ddcb7f0 100644 --- a/app/models/follow.rb +++ b/app/models/follow.rb @@ -8,6 +8,7 @@ # account_id :integer not null # id :integer not null, primary key # target_account_id :integer not null +# show_reblogs :boolean default(TRUE), not null # class Follow < ApplicationRecord diff --git a/app/models/follow_request.rb b/app/models/follow_request.rb index 60036d903..0608ffabc 100644 --- a/app/models/follow_request.rb +++ b/app/models/follow_request.rb @@ -8,6 +8,7 @@ # account_id :integer not null # id :integer not null, primary key # target_account_id :integer not null +# show_reblogs :boolean default(TRUE), not null # class FollowRequest < ApplicationRecord @@ -21,7 +22,7 @@ class FollowRequest < ApplicationRecord validates :account_id, uniqueness: { scope: :target_account_id } def authorize! - account.follow!(target_account) + account.follow!(target_account, reblogs: reblogs) MergeWorker.perform_async(target_account.id, account.id) destroy! diff --git a/app/services/follow_service.rb b/app/services/follow_service.rb index 791773f25..70572110d 100644 --- a/app/services/follow_service.rb +++ b/app/services/follow_service.rb @@ -6,25 +6,40 @@ class FollowService < BaseService # Follow a remote user, notify remote user about the follow # @param [Account] source_account From which to follow # @param [String, Account] uri User URI to follow in the form of username@domain (or account record) - def call(source_account, uri) + def call(source_account, uri, reblogs: nil) + reblogs = true if reblogs.nil? target_account = uri.is_a?(Account) ? uri : ResolveRemoteAccountService.new.call(uri) raise ActiveRecord::RecordNotFound if target_account.nil? || target_account.id == source_account.id || target_account.suspended? raise Mastodon::NotPermittedError if target_account.blocking?(source_account) || source_account.blocking?(target_account) - return if source_account.following?(target_account) || source_account.requested?(target_account) + if source_account.following?(target_account) + # We're already following this account, but we'll call follow! again to + # make sure the reblogs status is set correctly. + source_account.follow!(target_account, reblogs: reblogs) + return + elsif source_account.requested?(target_account) + # This isn't managed by a method in AccountInteractions, so we modify it + # ourselves if necessary. + req = follow_requests.find_by(target_account: other_account) + if req.show_reblogs != reblogs + req.show_reblogs = reblogs + req.save! + end + return + end if target_account.locked? || target_account.activitypub? - request_follow(source_account, target_account) + request_follow(source_account, target_account, reblogs: reblogs) else - direct_follow(source_account, target_account) + direct_follow(source_account, target_account, reblogs: reblogs) end end private - def request_follow(source_account, target_account) - follow_request = FollowRequest.create!(account: source_account, target_account: target_account) + def request_follow(source_account, target_account, reblogs: true) + follow_request = FollowRequest.create!(account: source_account, target_account: target_account, reblogs: reblogs) if target_account.local? NotifyService.new.call(target_account, follow_request) @@ -38,8 +53,8 @@ class FollowService < BaseService follow_request end - def direct_follow(source_account, target_account) - follow = source_account.follow!(target_account) + def direct_follow(source_account, target_account, reblogs: true) + follow = source_account.follow!(target_account, reblogs: reblogs) if target_account.local? NotifyService.new.call(target_account, follow) diff --git a/db/schema.rb b/db/schema.rb index f96a5340f..93505f9a0 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20171021191900) do +ActiveRecord::Schema.define(version: 20171028221157) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -145,6 +145,7 @@ ActiveRecord::Schema.define(version: 20171021191900) do t.datetime "updated_at", null: false t.bigint "account_id", null: false t.bigint "target_account_id", null: false + t.boolean "show_reblogs", default: true, null: false t.index ["account_id", "target_account_id"], name: "index_follow_requests_on_account_id_and_target_account_id", unique: true end @@ -153,6 +154,7 @@ ActiveRecord::Schema.define(version: 20171021191900) do t.datetime "updated_at", null: false t.bigint "account_id", null: false t.bigint "target_account_id", null: false + t.boolean "show_reblogs", default: true, null: false t.index ["account_id", "target_account_id"], name: "index_follows_on_account_id_and_target_account_id", unique: true end -- cgit