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/status/index.js | 719 +++++++++++++++++++++++ 1 file changed, 719 insertions(+) create mode 100644 app/javascript/glitch/components/status/index.js (limited to 'app/javascript/glitch/components/status/index.js') diff --git a/app/javascript/glitch/components/status/index.js b/app/javascript/glitch/components/status/index.js new file mode 100644 index 000000000..a11cc0b6e --- /dev/null +++ b/app/javascript/glitch/components/status/index.js @@ -0,0 +1,719 @@ +/* + +`` +========== + +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'; + +// Our imports // +import StatusPrepend from './prepend'; +import StatusHeader from './header'; +import StatusContent from './content'; +import StatusActionBar from './action_bar'; +import StatusGallery from './gallery'; +import StatusVideoPlayer from './video_player'; + + /* * * * */ + +/* + +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. + + - __`me` (`PropTypes.number`) :__ + This is the id of the currently-signed-in user. + + - __`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. + + - __`autoPlayGif` (`PropTypes.bool`) :__ + This tells the frontend whether or not to autoplay gifs! + + - __`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.number, + status : ImmutablePropTypes.map, + account : ImmutablePropTypes.map, + settings : ImmutablePropTypes.map, + me : PropTypes.number, + onFavourite : PropTypes.func, + onReblog : PropTypes.func, + onModalReblog : PropTypes.func, + onDelete : PropTypes.func, + onMention : PropTypes.func, + onMute : PropTypes.func, + onMuteConversation : PropTypes.func, + onBlock : PropTypes.func, + onReport : PropTypes.func, + onOpenMedia : PropTypes.func, + onOpenVideo : PropTypes.func, + reblogModal : PropTypes.bool, + deleteModal : PropTypes.bool, + autoPlayGif : PropTypes.bool, + muted : PropTypes.bool, + collapse : PropTypes.bool, + prepend : PropTypes.string, + withDismiss : PropTypes.bool, + intersectionObserverWrapper : PropTypes.object, + }; + + state = { + isExpanded : null, + isIntersecting : true, + isHidden : 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', + 'me', + 'boostModal', + 'autoPlayGif', + 'muted', + 'collapse', + ] + + updateOnStates = [ + 'isExpanded', + ] + +/* + +#### `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'])) { + this.setExpansion(true); + } 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, + } = 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('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 (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, handleRef } = this; + const { + status, + account, + settings, + collapsed, + muted, + prepend, + intersectionObserverWrapper, + onOpenVideo, + onOpenMedia, + autoPlayGif, + ...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']); + } + + +/* + +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} +
+ ); + + } + +} -- cgit From 66b1174d258e4ee0b4a410f2aa3b63f1e6cf280e Mon Sep 17 00:00:00 2001 From: Ondřej Hruška Date: Wed, 12 Jul 2017 19:52:36 +0200 Subject: Fix CW auto-expanding if collapsed toots are disabled --- app/javascript/glitch/components/status/index.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) (limited to 'app/javascript/glitch/components/status/index.js') diff --git a/app/javascript/glitch/components/status/index.js b/app/javascript/glitch/components/status/index.js index a11cc0b6e..12ea0e9aa 100644 --- a/app/javascript/glitch/components/status/index.js +++ b/app/javascript/glitch/components/status/index.js @@ -233,7 +233,9 @@ uncollapse our status accordingly. componentWillReceiveProps (nextProps) { if (!nextProps.settings.getIn(['collapsed', 'enabled'])) { - this.setExpansion(true); + if (this.state.isExpanded === false) { + this.setExpansion(null); + } } else if ( nextProps.collapse !== this.props.collapse && nextProps.collapse !== undefined -- cgit From 5770d461b21cf5b6a8adcaa44d19832e11289960 Mon Sep 17 00:00:00 2001 From: kibigo! Date: Thu, 13 Jul 2017 02:40:16 -0700 Subject: Moved glitch containers and commented unused files --- .../glitch/components/compose/advanced_options.js | 137 ----------- .../compose/advanced_options/container.js | 22 ++ .../components/compose/advanced_options/index.js | 137 +++++++++++ .../glitch/components/notification/container.js | 21 ++ .../glitch/components/notification/index.js | 2 +- .../glitch/components/settings/container.js | 27 +++ .../glitch/components/status/container.js | 252 +++++++++++++++++++++ app/javascript/glitch/components/status/index.js | 4 +- app/javascript/glitch/components/status/player.js | 199 ++++++++++++++++ .../glitch/components/status/video_player.js | 199 ---------------- .../glitch/containers/compose/advanced_options.js | 22 -- .../glitch/containers/notification/index.js | 21 -- app/javascript/glitch/containers/settings/index.js | 27 --- app/javascript/glitch/containers/status/index.js | 252 --------------------- .../mastodon/components/media_gallery.js | 3 + app/javascript/mastodon/components/status.js | 3 + .../mastodon/components/status_action_bar.js | 3 + .../mastodon/components/status_content.js | 3 + app/javascript/mastodon/components/status_list.js | 2 +- app/javascript/mastodon/components/video_player.js | 3 + .../mastodon/containers/status_container.js | 3 + .../mastodon/features/account/components/header.js | 3 + .../features/compose/components/compose_form.js | 2 +- .../features/compose/components/search_results.js | 2 +- .../notifications/components/notification.js | 3 + .../containers/notification_container.js | 3 + .../mastodon/features/notifications/index.js | 2 +- .../features/status/components/detailed_status.js | 4 +- app/javascript/mastodon/features/status/index.js | 2 +- .../mastodon/features/ui/util/async-components.js | 4 +- 30 files changed, 697 insertions(+), 670 deletions(-) delete mode 100644 app/javascript/glitch/components/compose/advanced_options.js create mode 100644 app/javascript/glitch/components/compose/advanced_options/container.js create mode 100644 app/javascript/glitch/components/compose/advanced_options/index.js create mode 100644 app/javascript/glitch/components/notification/container.js create mode 100644 app/javascript/glitch/components/settings/container.js create mode 100644 app/javascript/glitch/components/status/container.js create mode 100644 app/javascript/glitch/components/status/player.js delete mode 100644 app/javascript/glitch/components/status/video_player.js delete mode 100644 app/javascript/glitch/containers/compose/advanced_options.js delete mode 100644 app/javascript/glitch/containers/notification/index.js delete mode 100644 app/javascript/glitch/containers/settings/index.js delete mode 100644 app/javascript/glitch/containers/status/index.js (limited to 'app/javascript/glitch/components/status/index.js') diff --git a/app/javascript/glitch/components/compose/advanced_options.js b/app/javascript/glitch/components/compose/advanced_options.js deleted file mode 100644 index 17fc1d801..000000000 --- a/app/javascript/glitch/components/compose/advanced_options.js +++ /dev/null @@ -1,137 +0,0 @@ -// 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'; - -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' }, -}); - -const iconStyle = { - 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} -
-
- ); - } - -} - -@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, - }; - - onToggleDropdown = () => { - this.setState({ open: !this.state.open }); - }; - - onGlobalClick = (e) => { - if (e.target !== this.node && !this.node.contains(e.target) && this.state.open) { - this.setState({ open: false }); - } - } - - 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) => { - this.node = c; - } - - render () { - const { open } = this.state; - const { intl, values } = this.props; - - const options = [ - { icon: 'wifi', shortText: messages.local_only_short, longText: messages.local_only_long, key: 'do_not_federate' }, - ]; - - const anyEnabled = values.some((enabled) => enabled); - const optionElems = options.map((option) => { - return ( - - ); - }); - - return (
-
- -
-
- {optionElems} -
-
); - } - -} diff --git a/app/javascript/glitch/components/compose/advanced_options/container.js b/app/javascript/glitch/components/compose/advanced_options/container.js new file mode 100644 index 000000000..10804454a --- /dev/null +++ b/app/javascript/glitch/components/compose/advanced_options/container.js @@ -0,0 +1,22 @@ +// Package imports // +import { connect } from 'react-redux'; + +// Mastodon imports // +import { toggleComposeAdvancedOption } from '../../../../mastodon/actions/compose'; + +// Our imports // +import ComposeAdvancedOptions from '.'; + +const mapStateToProps = state => ({ + values: state.getIn(['compose', 'advanced_options']), +}); + +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 new file mode 100644 index 000000000..dabf66095 --- /dev/null +++ b/app/javascript/glitch/components/compose/advanced_options/index.js @@ -0,0 +1,137 @@ +// 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'; + +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' }, +}); + +const iconStyle = { + 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} +
+
+ ); + } + +} + +@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, + }; + + onToggleDropdown = () => { + this.setState({ open: !this.state.open }); + }; + + onGlobalClick = (e) => { + if (e.target !== this.node && !this.node.contains(e.target) && this.state.open) { + this.setState({ open: false }); + } + } + + 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) => { + this.node = c; + } + + render () { + const { open } = this.state; + const { intl, values } = this.props; + + const options = [ + { icon: 'wifi', shortText: messages.local_only_short, longText: messages.local_only_long, key: 'do_not_federate' }, + ]; + + const anyEnabled = values.some((enabled) => enabled); + const optionElems = options.map((option) => { + return ( + + ); + }); + + return (
+
+ +
+
+ {optionElems} +
+
); + } + +} diff --git a/app/javascript/glitch/components/notification/container.js b/app/javascript/glitch/components/notification/container.js new file mode 100644 index 000000000..c58ef4bd2 --- /dev/null +++ b/app/javascript/glitch/components/notification/container.js @@ -0,0 +1,21 @@ +// Package imports // +import { connect } from 'react-redux'; + +// Mastodon imports // +import { makeGetNotification } from '../../../mastodon/selectors'; + +// Our imports // +import Notification from '.'; + +const makeMapStateToProps = () => { + const getNotification = makeGetNotification(); + + const mapStateToProps = (state, props) => ({ + notification: getNotification(state, props.notification, props.accountId), + settings: state.get('local_settings'), + }); + + return mapStateToProps; +}; + +export default connect(makeMapStateToProps)(Notification); diff --git a/app/javascript/glitch/components/notification/index.js b/app/javascript/glitch/components/notification/index.js index 3f424d85d..34e03cdca 100644 --- a/app/javascript/glitch/components/notification/index.js +++ b/app/javascript/glitch/components/notification/index.js @@ -11,7 +11,7 @@ import Permalink from '../../../mastodon/components/permalink'; import emojify from '../../../mastodon/emoji'; // Our imports // -import StatusContainer from '../../containers/status'; +import StatusContainer from '../status'; export default class Notification extends ImmutablePureComponent { diff --git a/app/javascript/glitch/components/settings/container.js b/app/javascript/glitch/components/settings/container.js new file mode 100644 index 000000000..6034935eb --- /dev/null +++ b/app/javascript/glitch/components/settings/container.js @@ -0,0 +1,27 @@ +// Package imports // +import { connect } from 'react-redux'; + +// Mastodon imports // +import { closeModal } from '../../../mastodon/actions/modal'; + +// Our imports // +import { changeLocalSetting } from '../../actions/local_settings'; +import Settings from '../../components/settings'; + +const mapStateToProps = state => ({ + settings: state.get('local_settings'), +}); + +const mapDispatchToProps = dispatch => ({ + toggleSetting (setting, e) { + dispatch(changeLocalSetting(setting, e.target.checked)); + }, + changeSetting (setting, e) { + dispatch(changeLocalSetting(setting, e.target.value)); + }, + onClose () { + dispatch(closeModal()); + }, +}); + +export default connect(mapStateToProps, mapDispatchToProps)(Settings); diff --git a/app/javascript/glitch/components/status/container.js b/app/javascript/glitch/components/status/container.js new file mode 100644 index 000000000..a8aa6efe9 --- /dev/null +++ b/app/javascript/glitch/components/status/container.js @@ -0,0 +1,252 @@ +/* + +`` +=================== + +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, +} from '../../../mastodon/actions/interactions'; +import { + blockAccount, + muteAccount, +} from '../../../mastodon/actions/accounts'; +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', + }, + muteConfirm : { + id : 'confirmations.mute.confirm', + defaultMessage : 'Mute', + }, +}); + + /* * * * */ + +/* + +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); + 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, + me : state.getIn(['meta', 'me']), + settings : state.get('local_settings'), + prepend : prepend || ownProps.prepend, + reblogModal : state.getIn(['meta', 'boost_modal']), + deleteModal : state.getIn(['meta', 'delete_modal']), + autoPlayGif : state.getIn(['meta', 'auto_play_gif']), + }; + }; + + 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)); + } + }, + + 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(openModal('CONFIRM', { + message: @{account.get('acct')} }} />, + confirm: intl.formatMessage(messages.muteConfirm), + onConfirm: () => dispatch(muteAccount(account.get('id'))), + })); + }, + + 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/index.js b/app/javascript/glitch/components/status/index.js index 12ea0e9aa..1d135754a 100644 --- a/app/javascript/glitch/components/status/index.js +++ b/app/javascript/glitch/components/status/index.js @@ -46,7 +46,7 @@ import StatusHeader from './header'; import StatusContent from './content'; import StatusActionBar from './action_bar'; import StatusGallery from './gallery'; -import StatusVideoPlayer from './video_player'; +import StatusPlayer from './player'; /* * * * */ @@ -619,7 +619,7 @@ backgrounds for collapsed statuses are enabled. attachments.getIn([0, 'type']) === 'video' ) { media = ( // Media type is 'video' - { + 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 = ( +
+ +
+ ); + + 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/components/status/video_player.js b/app/javascript/glitch/components/status/video_player.js deleted file mode 100644 index 6583107c8..000000000 --- a/app/javascript/glitch/components/status/video_player.js +++ /dev/null @@ -1,199 +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 StatusVideoPlayer extends React.PureComponent { - - 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 = ( -
- -
- ); - - 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/containers/compose/advanced_options.js b/app/javascript/glitch/containers/compose/advanced_options.js deleted file mode 100644 index 92b989052..000000000 --- a/app/javascript/glitch/containers/compose/advanced_options.js +++ /dev/null @@ -1,22 +0,0 @@ -// Package imports // -import { connect } from 'react-redux'; - -// Mastodon imports // -import { toggleComposeAdvancedOption } from '../../../mastodon/actions/compose'; - -// Our imports // -import ComposeAdvancedOptions from '../../components/compose/advanced_options'; - -const mapStateToProps = state => ({ - values: state.getIn(['compose', 'advanced_options']), -}); - -const mapDispatchToProps = dispatch => ({ - - onChange (option) { - dispatch(toggleComposeAdvancedOption(option)); - }, - -}); - -export default connect(mapStateToProps, mapDispatchToProps)(ComposeAdvancedOptions); diff --git a/app/javascript/glitch/containers/notification/index.js b/app/javascript/glitch/containers/notification/index.js deleted file mode 100644 index 783c838ae..000000000 --- a/app/javascript/glitch/containers/notification/index.js +++ /dev/null @@ -1,21 +0,0 @@ -// Package imports // -import { connect } from 'react-redux'; - -// Mastodon imports // -import { makeGetNotification } from '../../../mastodon/selectors'; - -// Our imports // -import Notification from '../../components/notification'; - -const makeMapStateToProps = () => { - const getNotification = makeGetNotification(); - - const mapStateToProps = (state, props) => ({ - notification: getNotification(state, props.notification, props.accountId), - settings: state.get('local_settings'), - }); - - return mapStateToProps; -}; - -export default connect(makeMapStateToProps)(Notification); diff --git a/app/javascript/glitch/containers/settings/index.js b/app/javascript/glitch/containers/settings/index.js deleted file mode 100644 index 6034935eb..000000000 --- a/app/javascript/glitch/containers/settings/index.js +++ /dev/null @@ -1,27 +0,0 @@ -// Package imports // -import { connect } from 'react-redux'; - -// Mastodon imports // -import { closeModal } from '../../../mastodon/actions/modal'; - -// Our imports // -import { changeLocalSetting } from '../../actions/local_settings'; -import Settings from '../../components/settings'; - -const mapStateToProps = state => ({ - settings: state.get('local_settings'), -}); - -const mapDispatchToProps = dispatch => ({ - toggleSetting (setting, e) { - dispatch(changeLocalSetting(setting, e.target.checked)); - }, - changeSetting (setting, e) { - dispatch(changeLocalSetting(setting, e.target.value)); - }, - onClose () { - dispatch(closeModal()); - }, -}); - -export default connect(mapStateToProps, mapDispatchToProps)(Settings); diff --git a/app/javascript/glitch/containers/status/index.js b/app/javascript/glitch/containers/status/index.js deleted file mode 100644 index baf898e97..000000000 --- a/app/javascript/glitch/containers/status/index.js +++ /dev/null @@ -1,252 +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, -} from '../../../mastodon/actions/interactions'; -import { - blockAccount, - muteAccount, -} from '../../../mastodon/actions/accounts'; -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 '../../components/status'; - - /* * * * */ - -/* - -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', - }, - muteConfirm : { - id : 'confirmations.mute.confirm', - defaultMessage : 'Mute', - }, -}); - - /* * * * */ - -/* - -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); - 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, - me : state.getIn(['meta', 'me']), - settings : state.get('local_settings'), - prepend : prepend || ownProps.prepend, - reblogModal : state.getIn(['meta', 'boost_modal']), - deleteModal : state.getIn(['meta', 'delete_modal']), - autoPlayGif : state.getIn(['meta', 'auto_play_gif']), - }; - }; - - 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)); - } - }, - - 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(openModal('CONFIRM', { - message: @{account.get('acct')} }} />, - confirm: intl.formatMessage(messages.muteConfirm), - onConfirm: () => dispatch(muteAccount(account.get('id'))), - })); - }, - - 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/mastodon/components/media_gallery.js b/app/javascript/mastodon/components/media_gallery.js index 89a358e38..680a4e58a 100644 --- a/app/javascript/mastodon/components/media_gallery.js +++ b/app/javascript/mastodon/components/media_gallery.js @@ -1,3 +1,6 @@ +// THIS FILE EXISTS FOR UPSTREAM COMPATIBILITY & SHOULDN'T BE USED !! +// SEE INSTEAD : glitch/components/status/gallery + import React from 'react'; import ImmutablePropTypes from 'react-immutable-proptypes'; import PropTypes from 'prop-types'; diff --git a/app/javascript/mastodon/components/status.js b/app/javascript/mastodon/components/status.js index 6b9fdd2af..6605457f7 100644 --- a/app/javascript/mastodon/components/status.js +++ b/app/javascript/mastodon/components/status.js @@ -1,3 +1,6 @@ +// 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'; diff --git a/app/javascript/mastodon/components/status_action_bar.js b/app/javascript/mastodon/components/status_action_bar.js index 7bb394e71..2fad0fa5a 100644 --- a/app/javascript/mastodon/components/status_action_bar.js +++ b/app/javascript/mastodon/components/status_action_bar.js @@ -1,3 +1,6 @@ +// THIS FILE EXISTS FOR UPSTREAM COMPATIBILITY & SHOULDN'T BE USED !! +// SEE INSTEAD : glitch/components/status/action_bar + import React from 'react'; import ImmutablePropTypes from 'react-immutable-proptypes'; import PropTypes from 'prop-types'; diff --git a/app/javascript/mastodon/components/status_content.js b/app/javascript/mastodon/components/status_content.js index 1b803a22e..ad925edef 100644 --- a/app/javascript/mastodon/components/status_content.js +++ b/app/javascript/mastodon/components/status_content.js @@ -1,3 +1,6 @@ +// 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 escapeTextContentForBrowser from 'escape-html'; diff --git a/app/javascript/mastodon/components/status_list.js b/app/javascript/mastodon/components/status_list.js index b5c6c2053..94b348f25 100644 --- a/app/javascript/mastodon/components/status_list.js +++ b/app/javascript/mastodon/components/status_list.js @@ -2,7 +2,7 @@ import React from 'react'; import ImmutablePropTypes from 'react-immutable-proptypes'; import { ScrollContainer } from 'react-router-scroll'; import PropTypes from 'prop-types'; -import StatusContainer from '../../glitch/containers/status'; +import StatusContainer from '../../glitch/components/status/container'; import LoadMore from './load_more'; import ImmutablePureComponent from 'react-immutable-pure-component'; import IntersectionObserverWrapper from '../features/ui/util/intersection_observer_wrapper'; diff --git a/app/javascript/mastodon/components/video_player.js b/app/javascript/mastodon/components/video_player.js index 999cf42d9..5f2447c6d 100644 --- a/app/javascript/mastodon/components/video_player.js +++ b/app/javascript/mastodon/components/video_player.js @@ -1,3 +1,6 @@ +// THIS FILE EXISTS FOR UPSTREAM COMPATIBILITY & SHOULDN'T BE USED !! +// SEE INSTEAD : glitch/components/status/player + import React from 'react'; import ImmutablePropTypes from 'react-immutable-proptypes'; import PropTypes from 'prop-types'; diff --git a/app/javascript/mastodon/containers/status_container.js b/app/javascript/mastodon/containers/status_container.js index 438ecfe43..9b7f984e0 100644 --- a/app/javascript/mastodon/containers/status_container.js +++ b/app/javascript/mastodon/containers/status_container.js @@ -1,3 +1,6 @@ +// THIS FILE EXISTS FOR UPSTREAM COMPATIBILITY & SHOULDN'T BE USED !! +// SEE INSTEAD : glitch/components/status/container + import React from 'react'; import { connect } from 'react-redux'; import Status from '../components/status'; diff --git a/app/javascript/mastodon/features/account/components/header.js b/app/javascript/mastodon/features/account/components/header.js index 3239b1085..1133e8a4e 100644 --- a/app/javascript/mastodon/features/account/components/header.js +++ b/app/javascript/mastodon/features/account/components/header.js @@ -1,3 +1,6 @@ +// 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'; diff --git a/app/javascript/mastodon/features/compose/components/compose_form.js b/app/javascript/mastodon/features/compose/components/compose_form.js index 4b14f8c81..de5b09834 100644 --- a/app/javascript/mastodon/features/compose/components/compose_form.js +++ b/app/javascript/mastodon/features/compose/components/compose_form.js @@ -11,7 +11,7 @@ import { defineMessages, injectIntl } from 'react-intl'; import Collapsable from '../../../components/collapsable'; import SpoilerButtonContainer from '../containers/spoiler_button_container'; import PrivacyDropdownContainer from '../containers/privacy_dropdown_container'; -import ComposeAdvancedOptionsContainer from '../../../../glitch/containers/compose/advanced_options'; +import ComposeAdvancedOptionsContainer from '../../../../glitch/components/compose/advanced_options/container'; import SensitiveButtonContainer from '../containers/sensitive_button_container'; import EmojiPickerDropdown from './emoji_picker_dropdown'; import UploadFormContainer from '../containers/upload_form_container'; diff --git a/app/javascript/mastodon/features/compose/components/search_results.js b/app/javascript/mastodon/features/compose/components/search_results.js index 3d1c3ca5e..cae4ca412 100644 --- a/app/javascript/mastodon/features/compose/components/search_results.js +++ b/app/javascript/mastodon/features/compose/components/search_results.js @@ -2,7 +2,7 @@ import React from 'react'; import ImmutablePropTypes from 'react-immutable-proptypes'; import { FormattedMessage } from 'react-intl'; import AccountContainer from '../../../containers/account_container'; -import StatusContainer from '../../../../glitch/containers/status'; +import StatusContainer from '../../../../glitch/components/status/container'; import Link from 'react-router-dom/Link'; import ImmutablePureComponent from 'react-immutable-pure-component'; diff --git a/app/javascript/mastodon/features/notifications/components/notification.js b/app/javascript/mastodon/features/notifications/components/notification.js index 9d631644a..0771849c2 100644 --- a/app/javascript/mastodon/features/notifications/components/notification.js +++ b/app/javascript/mastodon/features/notifications/components/notification.js @@ -1,3 +1,6 @@ +// THIS FILE EXISTS FOR UPSTREAM COMPATIBILITY & SHOULDN'T BE USED !! +// SEE INSTEAD : glitch/components/notification + import React from 'react'; import ImmutablePropTypes from 'react-immutable-proptypes'; import StatusContainer from '../../../containers/status_container'; diff --git a/app/javascript/mastodon/features/notifications/containers/notification_container.js b/app/javascript/mastodon/features/notifications/containers/notification_container.js index 786222967..1f98a66d2 100644 --- a/app/javascript/mastodon/features/notifications/containers/notification_container.js +++ b/app/javascript/mastodon/features/notifications/containers/notification_container.js @@ -1,3 +1,6 @@ +// THIS FILE EXISTS FOR UPSTREAM COMPATIBILITY & SHOULDN'T BE USED !! +// SEE INSTEAD : glitch/components/notification/container + import { connect } from 'react-redux'; import { makeGetNotification } from '../../../selectors'; import Notification from '../components/notification'; diff --git a/app/javascript/mastodon/features/notifications/index.js b/app/javascript/mastodon/features/notifications/index.js index fb8f0d3cc..39fb4b26d 100644 --- a/app/javascript/mastodon/features/notifications/index.js +++ b/app/javascript/mastodon/features/notifications/index.js @@ -6,7 +6,7 @@ import Column from '../../components/column'; import ColumnHeader from '../../components/column_header'; import { expandNotifications, scrollTopNotifications } from '../../actions/notifications'; import { addColumn, removeColumn, moveColumn } from '../../actions/columns'; -import NotificationContainer from '../../../glitch/containers/notification'; +import NotificationContainer from '../../../glitch/components/notification/container'; import { ScrollContainer } from 'react-router-scroll'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import ColumnSettingsContainer from './containers/column_settings_container'; diff --git a/app/javascript/mastodon/features/status/components/detailed_status.js b/app/javascript/mastodon/features/status/components/detailed_status.js index db0181f23..67d1e822d 100644 --- a/app/javascript/mastodon/features/status/components/detailed_status.js +++ b/app/javascript/mastodon/features/status/components/detailed_status.js @@ -5,7 +5,7 @@ import Avatar from '../../../components/avatar'; import DisplayName from '../../../components/display_name'; import StatusContent from '../../../../glitch/components/status/content'; import StatusGallery from '../../../../glitch/components/status/gallery'; -import StatusVideoPlayer from '../../../../glitch/components/status/video_player'; +import StatusPlayer from '../../../../glitch/components/status/player'; import AttachmentList from '../../../components/attachment_list'; import Link from 'react-router-dom/Link'; import { FormattedDate, FormattedNumber } from 'react-intl'; @@ -48,7 +48,7 @@ export default class DetailedStatus extends ImmutablePureComponent { media = ; } else if (status.getIn(['media_attachments', 0, 'type']) === 'video') { media = ( - Date: Fri, 14 Jul 2017 17:03:43 +0200 Subject: Added buttons and menu items to dismiss individual notifications (#76) * Added DELETE verb for notifications * Added notification dismiss button to status dropdown * Added reveal-on-hover notif dismiss button, added FollowNotification component --- app/controllers/api/v1/notifications_controller.rb | 4 ++ .../glitch/components/notification/container.js | 9 ++- .../components/notification/follow_notification.js | 78 ++++++++++++++++++++++ .../glitch/components/notification/index.js | 31 +++------ .../glitch/components/status/action_bar.js | 8 +++ .../glitch/components/status/container.js | 4 ++ app/javascript/glitch/components/status/index.js | 4 ++ app/javascript/glitch/components/status/prepend.js | 29 +++++++- app/javascript/mastodon/actions/notifications.js | 17 +++++ app/javascript/mastodon/locales/en.json | 1 + app/javascript/mastodon/reducers/notifications.js | 7 ++ app/javascript/styles/components.scss | 21 ++++++ config/routes.rb | 2 +- 13 files changed, 192 insertions(+), 23 deletions(-) create mode 100644 app/javascript/glitch/components/notification/follow_notification.js (limited to 'app/javascript/glitch/components/status/index.js') diff --git a/app/controllers/api/v1/notifications_controller.rb b/app/controllers/api/v1/notifications_controller.rb index 8910b77e9..55f35fa4b 100644 --- a/app/controllers/api/v1/notifications_controller.rb +++ b/app/controllers/api/v1/notifications_controller.rb @@ -24,6 +24,10 @@ class Api::V1::NotificationsController < Api::BaseController render_empty end + def destroy + dismiss + end + def dismiss current_account.notifications.find_by!(id: params[:id]).destroy! render_empty diff --git a/app/javascript/glitch/components/notification/container.js b/app/javascript/glitch/components/notification/container.js index c58ef4bd2..60303537d 100644 --- a/app/javascript/glitch/components/notification/container.js +++ b/app/javascript/glitch/components/notification/container.js @@ -6,6 +6,7 @@ import { makeGetNotification } from '../../../mastodon/selectors'; // Our imports // import Notification from '.'; +import { deleteNotification } from '../../../mastodon/actions/notifications'; const makeMapStateToProps = () => { const getNotification = makeGetNotification(); @@ -18,4 +19,10 @@ const makeMapStateToProps = () => { return mapStateToProps; }; -export default connect(makeMapStateToProps)(Notification); +const mapDispatchToProps = (dispatch) => ({ + onDeleteNotification (id) { + dispatch(deleteNotification(id)); + }, +}); + +export default connect(makeMapStateToProps, mapDispatchToProps)(Notification); diff --git a/app/javascript/glitch/components/notification/follow_notification.js b/app/javascript/glitch/components/notification/follow_notification.js new file mode 100644 index 000000000..7cabd91f6 --- /dev/null +++ b/app/javascript/glitch/components/notification/follow_notification.js @@ -0,0 +1,78 @@ +// 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 83ac8dfc1..0cdc03cbe 100644 --- a/app/javascript/glitch/components/notification/index.js +++ b/app/javascript/glitch/components/notification/index.js @@ -1,42 +1,30 @@ // Package imports // import React from 'react'; import ImmutablePropTypes from 'react-immutable-proptypes'; -import { FormattedMessage } from 'react-intl'; -import escapeTextContentForBrowser from 'escape-html'; import ImmutablePureComponent from 'react-immutable-pure-component'; +import PropTypes from 'prop-types'; // Mastodon imports // -import AccountContainer from '../../../mastodon/containers/account_container'; -import Permalink from '../../../mastodon/components/permalink'; -import emojify from '../../../mastodon/emoji'; // Our imports // import StatusContainer from '../status/container'; +import FollowNotification from './follow_notification'; export default class Notification extends ImmutablePureComponent { static propTypes = { notification: ImmutablePropTypes.map.isRequired, settings: ImmutablePropTypes.map.isRequired, + onDeleteNotification: PropTypes.func.isRequired, }; renderFollow (notification) { - const account = notification.get('account'); - const displayName = account.get('display_name').length > 0 ? account.get('display_name') : account.get('username'); - const displayNameHTML = { __html: emojify(escapeTextContentForBrowser(displayName)) }; - const link = ; return ( -
-
-
- -
- - -
- - -
+ ); } @@ -44,6 +32,7 @@ export default class Notification extends ImmutablePureComponent { return ( ); @@ -56,6 +45,7 @@ export default class Notification extends ImmutablePureComponent { account={notification.get('account')} prepend='favourite' muted + notificationId={notification.get('id')} withDismiss /> ); @@ -68,6 +58,7 @@ export default class Notification extends ImmutablePureComponent { account={notification.get('account')} prepend='reblog' muted + notificationId={notification.get('id')} withDismiss /> ); diff --git a/app/javascript/glitch/components/status/action_bar.js b/app/javascript/glitch/components/status/action_bar.js index f298dcaa8..6aa088c04 100644 --- a/app/javascript/glitch/components/status/action_bar.js +++ b/app/javascript/glitch/components/status/action_bar.js @@ -24,6 +24,7 @@ const messages = defineMessages({ report: { id: 'status.report', defaultMessage: 'Report @{name}' }, muteConversation: { id: 'status.mute_conversation', defaultMessage: 'Mute conversation' }, unmuteConversation: { id: 'status.unmute_conversation', defaultMessage: 'Unmute conversation' }, + deleteNotification: { id: 'status.dismiss_notification', defaultMessage: 'Dismiss notification' }, }); @injectIntl @@ -35,6 +36,7 @@ export default class StatusActionBar extends ImmutablePureComponent { static propTypes = { status: ImmutablePropTypes.map.isRequired, + notificationId: PropTypes.number, onReply: PropTypes.func, onFavourite: PropTypes.func, onReblog: PropTypes.func, @@ -44,6 +46,7 @@ export default class StatusActionBar extends ImmutablePureComponent { onBlock: PropTypes.func, onReport: PropTypes.func, onMuteConversation: PropTypes.func, + onDeleteNotification: PropTypes.func, me: PropTypes.number.isRequired, withDismiss: PropTypes.bool, intl: PropTypes.object.isRequired, @@ -97,6 +100,10 @@ export default class StatusActionBar extends ImmutablePureComponent { this.props.onMuteConversation(this.props.status); } + handleNotificationDeleteClick = () => { + this.props.onDeleteNotification(this.props.notificationId); + } + render () { const { status, me, intl, withDismiss } = this.props; const reblogDisabled = status.get('visibility') === 'private' || status.get('visibility') === 'direct'; @@ -112,6 +119,7 @@ export default class StatusActionBar extends ImmutablePureComponent { if (withDismiss) { menu.push({ text: intl.formatMessage(mutingConversation ? messages.unmuteConversation : messages.muteConversation), action: this.handleConversationMuteClick }); + menu.push({ text: intl.formatMessage(messages.deleteNotification), action: this.handleNotificationDeleteClick }); menu.push(null); } diff --git a/app/javascript/glitch/components/status/container.js b/app/javascript/glitch/components/status/container.js index a8aa6efe9..c45b2e0ec 100644 --- a/app/javascript/glitch/components/status/container.js +++ b/app/javascript/glitch/components/status/container.js @@ -50,6 +50,7 @@ import { } from '../../../mastodon/actions/statuses'; import { initReport } from '../../../mastodon/actions/reports'; import { openModal } from '../../../mastodon/actions/modal'; +import { deleteNotification } from '../../../mastodon/actions/notifications'; // Our imports // import Status from '.'; @@ -245,6 +246,9 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ } }, + onDeleteNotification (id) { + dispatch(deleteNotification(id)); + }, }); export default injectIntl( diff --git a/app/javascript/glitch/components/status/index.js b/app/javascript/glitch/components/status/index.js index 1d135754a..314e8b51c 100644 --- a/app/javascript/glitch/components/status/index.js +++ b/app/javascript/glitch/components/status/index.js @@ -170,6 +170,7 @@ export default class Status extends ImmutablePureComponent { onReport : PropTypes.func, onOpenMedia : PropTypes.func, onOpenVideo : PropTypes.func, + onDeleteNotification : PropTypes.func, reblogModal : PropTypes.bool, deleteModal : PropTypes.bool, autoPlayGif : PropTypes.bool, @@ -177,6 +178,7 @@ export default class Status extends ImmutablePureComponent { collapse : PropTypes.bool, prepend : PropTypes.string, withDismiss : PropTypes.bool, + notificationId : PropTypes.number, intersectionObserverWrapper : PropTypes.object, }; @@ -685,6 +687,8 @@ collapsed. type={prepend} account={account} parseClick={parseClick} + notificationId={this.props.notificationId} + onDeleteNotification={this.props.onDeleteNotification} /> ) : null} { + this.props.onDeleteNotification(this.props.notificationId); + } + /* #### ``. @@ -145,7 +159,19 @@ the `` inside of an
+ {dismiss} ); } diff --git a/app/javascript/mastodon/actions/notifications.js b/app/javascript/mastodon/actions/notifications.js index c7d248122..b2a0f7ac3 100644 --- a/app/javascript/mastodon/actions/notifications.js +++ b/app/javascript/mastodon/actions/notifications.js @@ -6,6 +6,8 @@ import { defineMessages } from 'react-intl'; export const NOTIFICATIONS_UPDATE = 'NOTIFICATIONS_UPDATE'; +export const NOTIFICATION_DELETE_SUCCESS = 'NOTIFICATION_DELETE_SUCCESS'; + export const NOTIFICATIONS_REFRESH_REQUEST = 'NOTIFICATIONS_REFRESH_REQUEST'; export const NOTIFICATIONS_REFRESH_SUCCESS = 'NOTIFICATIONS_REFRESH_SUCCESS'; export const NOTIFICATIONS_REFRESH_FAIL = 'NOTIFICATIONS_REFRESH_FAIL'; @@ -187,3 +189,18 @@ export function scrollTopNotifications(top) { top, }; }; + +export function deleteNotification(id) { + return (dispatch, getState) => { + api(getState).delete(`/api/v1/notifications/${id}`).then(() => { + dispatch(deleteNotificationSuccess(id)); + }); + }; +}; + +export function deleteNotificationSuccess(id) { + return { + type: NOTIFICATION_DELETE_SUCCESS, + id: id, + }; +}; diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json index cf29e38da..d2e5f90ea 100644 --- a/app/javascript/mastodon/locales/en.json +++ b/app/javascript/mastodon/locales/en.json @@ -190,6 +190,7 @@ "status.show_more": "Show more", "status.uncollapse": "Uncollapse", "status.unmute_conversation": "Unmute conversation", + "status.dismiss_notification": "Dismiss notification", "tabs_bar.compose": "Compose", "tabs_bar.federated_timeline": "Federated", "tabs_bar.home": "Home", diff --git a/app/javascript/mastodon/reducers/notifications.js b/app/javascript/mastodon/reducers/notifications.js index 0063d24e4..da5fcde84 100644 --- a/app/javascript/mastodon/reducers/notifications.js +++ b/app/javascript/mastodon/reducers/notifications.js @@ -8,6 +8,7 @@ import { NOTIFICATIONS_EXPAND_FAIL, NOTIFICATIONS_CLEAR, NOTIFICATIONS_SCROLL_TOP, + NOTIFICATION_DELETE_SUCCESS, } from '../actions/notifications'; import { ACCOUNT_BLOCK_SUCCESS } from '../actions/accounts'; import { TIMELINE_DELETE } from '../actions/timelines'; @@ -92,6 +93,10 @@ const deleteByStatus = (state, statusId) => { return state.update('items', list => list.filterNot(item => item.get('status') === statusId)); }; +const deleteById = (state, notificationId) => { + return state.update('items', list => list.filterNot(item => item.get('id') === notificationId)); +}; + export default function notifications(state = initialState, action) { switch(action.type) { case NOTIFICATIONS_REFRESH_REQUEST: @@ -113,6 +118,8 @@ export default function notifications(state = initialState, action) { return state.set('items', ImmutableList()).set('next', null); case TIMELINE_DELETE: return deleteByStatus(state, action.id); + case NOTIFICATION_DELETE_SUCCESS: + return deleteById(state, action.id); default: return state; } diff --git a/app/javascript/styles/components.scss b/app/javascript/styles/components.scss index 6cca3666a..b06c99c95 100644 --- a/app/javascript/styles/components.scss +++ b/app/javascript/styles/components.scss @@ -617,6 +617,27 @@ position: absolute; } +.status__prepend-dismiss-button { + border: 0; + background: transparent; + position: absolute; + right: -3px; + opacity: 0; + transition: opacity 0.1s ease-in-out; + + i.fa { + color: crimson; + } + + .notification__message:hover & { + opacity: 1; + } + + .notification-follow & { + right: 6px; + } +} + .status { padding: 8px 10px; padding-left: 68px; diff --git a/config/routes.rb b/config/routes.rb index 963fedcb4..a63fb3ae6 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -175,7 +175,7 @@ Rails.application.routes.draw do end end - resources :notifications, only: [:index, :show] do + resources :notifications, only: [:index, :show, :destroy] do collection do post :clear post :dismiss -- cgit From bc4202d00b2956b630a7609fb74f31c04e9275f3 Mon Sep 17 00:00:00 2001 From: kibigo! Date: Sat, 15 Jul 2017 15:10:06 -0700 Subject: Ported updates from #64 --- .../glitch/components/status/action_bar.js | 11 +++++---- app/javascript/glitch/components/status/content.js | 28 ++++++++++++++-------- app/javascript/glitch/components/status/header.js | 1 + app/javascript/glitch/components/status/index.js | 14 ++++++++--- app/javascript/glitch/components/status/player.js | 6 ++++- app/javascript/glitch/reducers/local_settings.js | 20 ++++++++-------- 6 files changed, 51 insertions(+), 29 deletions(-) (limited to 'app/javascript/glitch/components/status/index.js') diff --git a/app/javascript/glitch/components/status/action_bar.js b/app/javascript/glitch/components/status/action_bar.js index 6aa088c04..df0904a7c 100644 --- a/app/javascript/glitch/components/status/action_bar.js +++ b/app/javascript/glitch/components/status/action_bar.js @@ -47,7 +47,7 @@ export default class StatusActionBar extends ImmutablePureComponent { onReport: PropTypes.func, onMuteConversation: PropTypes.func, onDeleteNotification: PropTypes.func, - me: PropTypes.number.isRequired, + me: PropTypes.number, withDismiss: PropTypes.bool, intl: PropTypes.object.isRequired, }; @@ -108,6 +108,7 @@ export default class StatusActionBar extends ImmutablePureComponent { const { status, me, intl, withDismiss } = this.props; const reblogDisabled = status.get('visibility') === 'private' || status.get('visibility') === 'direct'; const mutingConversation = status.get('muted'); + const anonymousAccess = !me; let menu = []; let reblogIcon = 'retweet'; @@ -151,12 +152,12 @@ export default class StatusActionBar extends ImmutablePureComponent { return (
- - - + + +
- +
diff --git a/app/javascript/glitch/components/status/content.js b/app/javascript/glitch/components/status/content.js index 76f5b765a..06fe04ce0 100644 --- a/app/javascript/glitch/components/status/content.js +++ b/app/javascript/glitch/components/status/content.js @@ -4,6 +4,7 @@ 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'; @@ -12,10 +13,6 @@ import Permalink from '../../../mastodon/components/permalink'; export default class StatusContent extends React.PureComponent { - static contextTypes = { - router: PropTypes.object, - }; - static propTypes = { status: ImmutablePropTypes.map.isRequired, expanded: PropTypes.oneOf([true, false, null]), @@ -24,6 +21,7 @@ export default class StatusContent extends React.PureComponent { media: PropTypes.element, mediaIcon: PropTypes.string, parseClick: PropTypes.func, + disabled: PropTypes.bool, }; state = { @@ -45,10 +43,11 @@ export default class StatusContent extends React.PureComponent { link.addEventListener('click', this.onHashtagClick.bind(this, link.text), false); } else { link.addEventListener('click', this.onLinkClick.bind(this), false); - link.setAttribute('target', '_blank'); - link.setAttribute('rel', 'noopener'); link.setAttribute('title', link.href); } + + link.setAttribute('target', '_blank'); + link.setAttribute('rel', 'noopener'); } } @@ -118,7 +117,13 @@ export default class StatusContent extends React.PureComponent { } render () { - const { status, media, mediaIcon } = this.props; + const { + status, + media, + mediaIcon, + parseClick, + disabled, + } = this.props; const hidden = ( this.props.setExpansion ? @@ -133,6 +138,9 @@ export default class StatusContent extends React.PureComponent { )), }; const directionStyle = { direction: 'ltr' }; + const classNames = classnames('status__content', { + 'status__content--with-action': parseClick && !disabled, + }); if (isRtl(status.get('search_index'))) { directionStyle.direction = 'rtl'; @@ -180,7 +188,7 @@ export default class StatusContent extends React.PureComponent { } return ( -
+
` if we don't. } diff --git a/app/javascript/glitch/components/status/index.js b/app/javascript/glitch/components/status/index.js index 314e8b51c..4a91b5aa3 100644 --- a/app/javascript/glitch/components/status/index.js +++ b/app/javascript/glitch/components/status/index.js @@ -507,6 +507,7 @@ applicable. 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')) @@ -532,7 +533,13 @@ this operation are further explained in the code below. */ render () { - const { parseClick, setExpansion, handleRef } = this; + const { + parseClick, + setExpansion, + saveHeight, + handleRef, + } = this; + const { router } = this.context; const { status, account, @@ -706,9 +713,10 @@ collapsed. media={media} mediaIcon={mediaIcon} expanded={isExpanded} - setExpansion={this.setExpansion} - onHeightUpdate={this.saveHeight} + setExpansion={setExpansion} + onHeightUpdate={saveHeight} parseClick={parseClick} + disabled={!router} /> {isExpanded !== false ? ( ); - let expandButton = ( + let expandButton = !this.context.router ? '' : (
diff --git a/app/javascript/glitch/reducers/local_settings.js b/app/javascript/glitch/reducers/local_settings.js index 79ff96307..776dcead7 100644 --- a/app/javascript/glitch/reducers/local_settings.js +++ b/app/javascript/glitch/reducers/local_settings.js @@ -28,7 +28,7 @@ Imports */ // Package imports // -import Immutable from 'immutable'; +import { Map as ImmutableMap } from 'immutable'; // Mastodon imports // import { STORE_HYDRATE } from '../../mastodon/actions/store'; @@ -48,27 +48,27 @@ These are only used if no previously-saved values exist. */ -const initialState = Immutable.fromJS({ +const initialState = ImmutableMap({ layout : 'auto', stretch : true, - collapsed : { + collapsed : ImmutableMap({ enabled : true, - auto : { + auto : ImmutableMap({ all : false, notifications : true, lengthy : true, replies : false, media : false, - }, - backgrounds : { + }), + backgrounds : ImmutableMap({ user_backgrounds : false, preview_images : false, - }, - }, - media : { + }), + }), + media : ImmutableMap({ letterbox : true, fullwidth : true, - }, + }), }); /* * * * */ -- cgit From 604654ccb417ffdc9b48d876bea76c8bec14f360 Mon Sep 17 00:00:00 2001 From: Ondřej Hruška Date: Fri, 21 Jul 2017 20:33:16 +0200 Subject: New notification cleaning mode (#89) This PR adds a new notification cleaning mode, super perfectly tuned for accessibility, and removes the previous notification cleaning functionality as it's now redundant. * w.i.p. notif clearing mode * Better CSS for selected notification and shorter text if Stretch is off * wip for rebase ~ * all working in notif clearing mode, except the actual removal * bulk delete route for piggo * cleaning + refactor. endpoint gives 422 for some reason * formatting * use the right route * fix broken destroy_multiple * load more notifs after succ cleaning * satisfy eslint * Removed CSS for the old notif delete button * Tabindex=0 is mandatory In order to make it possible to tab to this element you must have tab index = 0. Removing this violates WCAG and makes it impossible to use the interface without good eyesight and a mouse. So nobody with certain mobility impairments, vision impairments, or brain injuries would be able to use this feature if you don't have tabindex=0 * Corrected aria-label Previous label implied a different behavior from what actually happens * aria role localization & made the overlay behave like a checkbox * checkboxes css and better contrast * color tuning for the notif overlay * fanceh checkboxes etc and nice backgrounds * SHUT UP TRAVIS --- app/controllers/api/v1/notifications_controller.rb | 5 ++ .../column/notif_cleaning_widget/container.js | 56 ++++++++++++ .../notification_purge_buttons.js | 100 +++++++++++++++++++++ .../glitch/components/notification/container.js | 20 +---- .../glitch/components/notification/follow.js | 61 ++----------- .../glitch/components/notification/index.js | 10 +-- .../components/notification/overlay/container.js | 49 ++++++++++ .../notification/overlay/notification_overlay.js | 59 ++++++++++++ .../glitch/components/status/action_bar.js | 8 -- .../glitch/components/status/container.js | 5 -- app/javascript/glitch/components/status/index.js | 20 ++++- app/javascript/glitch/components/status/prepend.js | 28 +----- app/javascript/glitch/locales/en.json | 2 +- app/javascript/mastodon/actions/notifications.js | 64 +++++++++++-- app/javascript/mastodon/components/column.js | 7 +- .../mastodon/components/column_header.js | 26 +++++- .../mastodon/features/notifications/index.js | 16 +++- app/javascript/mastodon/reducers/notifications.js | 42 +++++++-- app/javascript/styles/components.scss | 90 +++++++++++++++---- config/routes.rb | 1 + 20 files changed, 513 insertions(+), 156 deletions(-) create mode 100644 app/javascript/glitch/components/column/notif_cleaning_widget/container.js create mode 100644 app/javascript/glitch/components/column/notif_cleaning_widget/notification_purge_buttons.js create mode 100644 app/javascript/glitch/components/notification/overlay/container.js create mode 100644 app/javascript/glitch/components/notification/overlay/notification_overlay.js (limited to 'app/javascript/glitch/components/status/index.js') diff --git a/app/controllers/api/v1/notifications_controller.rb b/app/controllers/api/v1/notifications_controller.rb index 55f35fa4b..a949752fb 100644 --- a/app/controllers/api/v1/notifications_controller.rb +++ b/app/controllers/api/v1/notifications_controller.rb @@ -33,6 +33,11 @@ class Api::V1::NotificationsController < Api::BaseController render_empty end + def destroy_multiple + current_account.notifications.where(id: params[:ids]).destroy_all + render_empty + end + private def load_notifications diff --git a/app/javascript/glitch/components/column/notif_cleaning_widget/container.js b/app/javascript/glitch/components/column/notif_cleaning_widget/container.js new file mode 100644 index 000000000..bf079e3c4 --- /dev/null +++ b/app/javascript/glitch/components/column/notif_cleaning_widget/container.js @@ -0,0 +1,56 @@ +/* + +`` +========================= + +This container connects ``s to the Redux store. + +*/ + +// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + +/* + +Imports: +-------- + +*/ + +// Package imports // +import { connect } from 'react-redux'; + +// Our imports // +import NotificationPurgeButtons from './notification_purge_buttons'; +import { + deleteMarkedNotifications, + enterNotificationClearingMode, +} 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 => ({ + onEnterCleaningMode(yes) { + dispatch(enterNotificationClearingMode(yes)); + }, + + onDeleteMarkedNotifications() { + dispatch(deleteMarkedNotifications()); + }, +}); + +const mapStateToProps = state => ({ + active: state.getIn(['notifications', 'cleaningMode']), +}); + +export default connect(mapStateToProps, mapDispatchToProps)(NotificationPurgeButtons); diff --git a/app/javascript/glitch/components/column/notif_cleaning_widget/notification_purge_buttons.js b/app/javascript/glitch/components/column/notif_cleaning_widget/notification_purge_buttons.js new file mode 100644 index 000000000..e41572256 --- /dev/null +++ b/app/javascript/glitch/components/column/notif_cleaning_widget/notification_purge_buttons.js @@ -0,0 +1,100 @@ +/** + * Buttons widget for controlling the notification clearing mode. + * In idle state, the cleaning mode button is shown. When the mode is active, + * a Confirm and Abort buttons are shown in its place. + */ + + +// Package imports // +import React from 'react'; +import PropTypes from 'prop-types'; +import { defineMessages, injectIntl } from 'react-intl'; +import ImmutablePureComponent from 'react-immutable-pure-component'; + +// Mastodon imports // + +// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + +const messages = defineMessages({ + enter : { id: 'notification_purge.start', defaultMessage: 'Enter notification cleaning mode' }, + accept : { id: 'notification_purge.confirm', defaultMessage: 'Dismiss selected notifications' }, + abort : { id: 'notification_purge.abort', defaultMessage: 'Leave cleaning mode' }, +}); + +@injectIntl +export default class NotificationPurgeButtons extends ImmutablePureComponent { + + static propTypes = { + // Nukes all marked notifications + onDeleteMarkedNotifications : PropTypes.func.isRequired, + // Enables or disables the mode + // and also clears the marked status of all notifications + onEnterCleaningMode : PropTypes.func.isRequired, + // Active state, changed via onStateChange() + active: PropTypes.bool.isRequired, + // i18n + intl: PropTypes.object.isRequired, + }; + + onEnterBtnClick = () => { + this.props.onEnterCleaningMode(true); + } + + onAcceptBtnClick = () => { + this.props.onDeleteMarkedNotifications(); + } + + onAbortBtnClick = () => { + this.props.onEnterCleaningMode(false); + } + + render () { + const { intl, active } = this.props; + + const msgEnter = intl.formatMessage(messages.enter); + const msgAccept = intl.formatMessage(messages.accept); + const msgAbort = intl.formatMessage(messages.abort); + + let enterButton, acceptButton, abortButton; + + if (active) { + acceptButton = ( + + ); + abortButton = ( + + ); + } else { + enterButton = ( + + ); + } + + return ( +
+ {acceptButton}{abortButton}{enterButton} +
+ ); + } + +} diff --git a/app/javascript/glitch/components/notification/container.js b/app/javascript/glitch/components/notification/container.js index bed086172..7d2590684 100644 --- a/app/javascript/glitch/components/notification/container.js +++ b/app/javascript/glitch/components/notification/container.js @@ -24,7 +24,6 @@ import { makeGetNotification } from '../../../mastodon/selectors'; // Our imports // import Notification from '.'; -import { deleteNotification } from '../../../mastodon/actions/notifications'; // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * @@ -53,21 +52,4 @@ const makeMapStateToProps = () => { // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * -/* - -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)); - }, -}); - -export default connect(makeMapStateToProps, mapDispatchToProps)(Notification); +export default connect(makeMapStateToProps)(Notification); diff --git a/app/javascript/glitch/components/notification/follow.js b/app/javascript/glitch/components/notification/follow.js index 26396478b..0e0065eb1 100644 --- a/app/javascript/glitch/components/notification/follow.js +++ b/app/javascript/glitch/components/notification/follow.js @@ -36,7 +36,7 @@ Imports: import React from 'react'; import ImmutablePropTypes from 'react-immutable-proptypes'; import PropTypes from 'prop-types'; -import { defineMessages, FormattedMessage, injectIntl } from 'react-intl'; +import { FormattedMessage } from 'react-intl'; import escapeTextContentForBrowser from 'escape-html'; import ImmutablePureComponent from 'react-immutable-pure-component'; @@ -45,22 +45,10 @@ 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. - -*/ +// Our imports // +import NotificationOverlayContainer from '../notification/overlay/container'; -const messages = defineMessages({ - deleteNotification : - { id: 'status.dismiss_notification', defaultMessage: 'Dismiss notification' }, -}); +// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * /* @@ -69,31 +57,16 @@ 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, + notification : ImmutablePropTypes.map.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. @@ -101,26 +74,7 @@ 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 = ( - - ); + const { account, notification } = this.props; /* @@ -149,6 +103,7 @@ We can now render our component. return (
+
@@ -159,8 +114,6 @@ We can now render our component. defaultMessage='{name} followed you' values={{ name: link }} /> - - {dismiss}
diff --git a/app/javascript/glitch/components/notification/index.js b/app/javascript/glitch/components/notification/index.js index 556d5aea8..b2e55aad5 100644 --- a/app/javascript/glitch/components/notification/index.js +++ b/app/javascript/glitch/components/notification/index.js @@ -2,7 +2,6 @@ import React from 'react'; import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePureComponent from 'react-immutable-pure-component'; -import PropTypes from 'prop-types'; // Mastodon imports // @@ -15,7 +14,6 @@ export default class Notification extends ImmutablePureComponent { static propTypes = { notification: ImmutablePropTypes.map.isRequired, settings: ImmutablePropTypes.map.isRequired, - onDeleteNotification: PropTypes.func.isRequired, }; renderFollow (notification) { @@ -23,7 +21,7 @@ export default class Notification extends ImmutablePureComponent { ); } @@ -32,7 +30,7 @@ export default class Notification extends ImmutablePureComponent { return ( ); @@ -45,7 +43,7 @@ export default class Notification extends ImmutablePureComponent { account={notification.get('account')} prepend='favourite' muted - notificationId={notification.get('id')} + notification={notification} withDismiss /> ); @@ -58,7 +56,7 @@ export default class Notification extends ImmutablePureComponent { account={notification.get('account')} prepend='reblog' muted - notificationId={notification.get('id')} + notification={notification} withDismiss /> ); diff --git a/app/javascript/glitch/components/notification/overlay/container.js b/app/javascript/glitch/components/notification/overlay/container.js new file mode 100644 index 000000000..019b78d0b --- /dev/null +++ b/app/javascript/glitch/components/notification/overlay/container.js @@ -0,0 +1,49 @@ +/* + +`` +========================= + +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 => ({ + revealed: state.getIn(['notifications', 'cleaningMode']), +}); + +export default connect(mapStateToProps, mapDispatchToProps)(NotificationOverlay); diff --git a/app/javascript/glitch/components/notification/overlay/notification_overlay.js b/app/javascript/glitch/components/notification/overlay/notification_overlay.js new file mode 100644 index 000000000..73eda718f --- /dev/null +++ b/app/javascript/glitch/components/notification/overlay/notification_overlay.js @@ -0,0 +1,59 @@ +/** + * Notification overlay + */ + + +// Package imports // +import React from 'react'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import PropTypes from 'prop-types'; +import ImmutablePureComponent from 'react-immutable-pure-component'; +import { defineMessages, injectIntl } from 'react-intl'; + +// Mastodon imports // + +// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + +const messages = defineMessages({ + markForDeletion: { id: 'notification.markForDeletion', defaultMessage: 'Mark for deletion' }, +}); + +@injectIntl +export default class NotificationOverlay extends ImmutablePureComponent { + + static propTypes = { + notification : ImmutablePropTypes.map.isRequired, + onMarkForDelete : PropTypes.func.isRequired, + revealed : PropTypes.bool.isRequired, + intl : PropTypes.object.isRequired, + }; + + onToggleMark = () => { + const mark = !this.props.notification.get('markedForDelete'); + const id = this.props.notification.get('id'); + this.props.onMarkForDelete(id, mark); + } + + render () { + const { notification, revealed, intl } = this.props; + + const active = notification.get('markedForDelete'); + const label = intl.formatMessage(messages.markForDeletion); + + return ( +
+ +
+ ); + } + +} diff --git a/app/javascript/glitch/components/status/action_bar.js b/app/javascript/glitch/components/status/action_bar.js index df0904a7c..7c73002c1 100644 --- a/app/javascript/glitch/components/status/action_bar.js +++ b/app/javascript/glitch/components/status/action_bar.js @@ -24,7 +24,6 @@ const messages = defineMessages({ report: { id: 'status.report', defaultMessage: 'Report @{name}' }, muteConversation: { id: 'status.mute_conversation', defaultMessage: 'Mute conversation' }, unmuteConversation: { id: 'status.unmute_conversation', defaultMessage: 'Unmute conversation' }, - deleteNotification: { id: 'status.dismiss_notification', defaultMessage: 'Dismiss notification' }, }); @injectIntl @@ -36,7 +35,6 @@ export default class StatusActionBar extends ImmutablePureComponent { static propTypes = { status: ImmutablePropTypes.map.isRequired, - notificationId: PropTypes.number, onReply: PropTypes.func, onFavourite: PropTypes.func, onReblog: PropTypes.func, @@ -46,7 +44,6 @@ export default class StatusActionBar extends ImmutablePureComponent { onBlock: PropTypes.func, onReport: PropTypes.func, onMuteConversation: PropTypes.func, - onDeleteNotification: PropTypes.func, me: PropTypes.number, withDismiss: PropTypes.bool, intl: PropTypes.object.isRequired, @@ -100,10 +97,6 @@ export default class StatusActionBar extends ImmutablePureComponent { this.props.onMuteConversation(this.props.status); } - handleNotificationDeleteClick = () => { - this.props.onDeleteNotification(this.props.notificationId); - } - render () { const { status, me, intl, withDismiss } = this.props; const reblogDisabled = status.get('visibility') === 'private' || status.get('visibility') === 'direct'; @@ -120,7 +113,6 @@ export default class StatusActionBar extends ImmutablePureComponent { if (withDismiss) { menu.push({ text: intl.formatMessage(mutingConversation ? messages.unmuteConversation : messages.muteConversation), action: this.handleConversationMuteClick }); - menu.push({ text: intl.formatMessage(messages.deleteNotification), action: this.handleNotificationDeleteClick }); menu.push(null); } diff --git a/app/javascript/glitch/components/status/container.js b/app/javascript/glitch/components/status/container.js index c45b2e0ec..1d572e0e7 100644 --- a/app/javascript/glitch/components/status/container.js +++ b/app/javascript/glitch/components/status/container.js @@ -50,7 +50,6 @@ import { } from '../../../mastodon/actions/statuses'; import { initReport } from '../../../mastodon/actions/reports'; import { openModal } from '../../../mastodon/actions/modal'; -import { deleteNotification } from '../../../mastodon/actions/notifications'; // Our imports // import Status from '.'; @@ -245,10 +244,6 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ dispatch(muteStatus(status.get('id'))); } }, - - onDeleteNotification (id) { - dispatch(deleteNotification(id)); - }, }); export default injectIntl( diff --git a/app/javascript/glitch/components/status/index.js b/app/javascript/glitch/components/status/index.js index 4a91b5aa3..dc06250ec 100644 --- a/app/javascript/glitch/components/status/index.js +++ b/app/javascript/glitch/components/status/index.js @@ -47,6 +47,7 @@ import StatusContent from './content'; import StatusActionBar from './action_bar'; import StatusGallery from './gallery'; import StatusPlayer from './player'; +import NotificationOverlayContainer from '../notification/overlay/container'; /* * * * */ @@ -158,6 +159,7 @@ export default class Status extends ImmutablePureComponent { status : ImmutablePropTypes.map, account : ImmutablePropTypes.map, settings : ImmutablePropTypes.map, + notification : ImmutablePropTypes.map, me : PropTypes.number, onFavourite : PropTypes.func, onReblog : PropTypes.func, @@ -170,7 +172,6 @@ export default class Status extends ImmutablePureComponent { onReport : PropTypes.func, onOpenMedia : PropTypes.func, onOpenVideo : PropTypes.func, - onDeleteNotification : PropTypes.func, reblogModal : PropTypes.bool, deleteModal : PropTypes.bool, autoPlayGif : PropTypes.bool, @@ -178,7 +179,6 @@ export default class Status extends ImmutablePureComponent { collapse : PropTypes.bool, prepend : PropTypes.string, withDismiss : PropTypes.bool, - notificationId : PropTypes.number, intersectionObserverWrapper : PropTypes.object, }; @@ -186,6 +186,7 @@ export default class Status extends ImmutablePureComponent { isExpanded : null, isIntersecting : true, isHidden : false, + markedForDelete : false, } /* @@ -212,10 +213,12 @@ to remember to specify it here. 'autoPlayGif', 'muted', 'collapse', + 'notification', ] updateOnStates = [ 'isExpanded', + 'markedForDelete', ] /* @@ -523,6 +526,10 @@ applicable. } } + markNotifForDelete = () => { + this.setState({ 'markedForDelete' : !this.state.markedForDelete }); + } + /* #### `render()`. @@ -551,6 +558,7 @@ this operation are further explained in the code below. onOpenVideo, onOpenMedia, autoPlayGif, + notification, ...other } = this.props; const { isExpanded, isIntersecting, isHidden } = this.state; @@ -678,6 +686,8 @@ collapsed. isExpanded === false ? ' collapsed' : '' }${ isExpanded === false && background ? ' has-background' : '' + }${ + this.state.markedForDelete ? ' marked-for-delete' : '' }` } style={{ @@ -689,13 +699,17 @@ collapsed. }} ref={handleRef} > + {notification ? ( + + ) : null} {prepend && account ? ( ) : null} { - this.props.onDeleteNotification(this.props.notificationId); - } - /* #### ``. @@ -159,19 +146,7 @@ the `` inside of an
- {dismiss} ); } diff --git a/app/javascript/glitch/locales/en.json b/app/javascript/glitch/locales/en.json index 80fdc3a39..d202d9c33 100644 --- a/app/javascript/glitch/locales/en.json +++ b/app/javascript/glitch/locales/en.json @@ -28,5 +28,5 @@ "settings.wide_view": "Wide view (Desktop mode only)", "status.collapse": "Collapse", "status.uncollapse": "Uncollapse", - "status.dismiss_notification": "Dismiss notification" + "notification.markForDeletion": "Mark for deletion" } diff --git a/app/javascript/mastodon/actions/notifications.js b/app/javascript/mastodon/actions/notifications.js index b2a0f7ac3..fca26516a 100644 --- a/app/javascript/mastodon/actions/notifications.js +++ b/app/javascript/mastodon/actions/notifications.js @@ -6,7 +6,15 @@ import { defineMessages } from 'react-intl'; export const NOTIFICATIONS_UPDATE = 'NOTIFICATIONS_UPDATE'; -export const NOTIFICATION_DELETE_SUCCESS = 'NOTIFICATION_DELETE_SUCCESS'; +// tracking the notif cleaning request +export const NOTIFICATIONS_DELETE_MARKED_REQUEST = 'NOTIFICATIONS_DELETE_MARKED_REQUEST'; +export const NOTIFICATIONS_DELETE_MARKED_SUCCESS = 'NOTIFICATIONS_DELETE_MARKED_SUCCESS'; +export const NOTIFICATIONS_DELETE_MARKED_FAIL = 'NOTIFICATIONS_DELETE_MARKED_FAIL'; +export const NOTIFICATIONS_ENTER_CLEARING_MODE = 'NOTIFICATIONS_ENTER_CLEARING_MODE'; // arg: yes +// Unmark notifications (when the cleaning mode is left) +export const NOTIFICATIONS_UNMARK_ALL_FOR_DELETE = 'NOTIFICATIONS_UNMARK_ALL_FOR_DELETE'; +// Mark one for delete +export const NOTIFICATION_MARK_FOR_DELETE = 'NOTIFICATION_MARK_FOR_DELETE'; export const NOTIFICATIONS_REFRESH_REQUEST = 'NOTIFICATIONS_REFRESH_REQUEST'; export const NOTIFICATIONS_REFRESH_SUCCESS = 'NOTIFICATIONS_REFRESH_SUCCESS'; @@ -190,17 +198,61 @@ export function scrollTopNotifications(top) { }; }; -export function deleteNotification(id) { +export function deleteMarkedNotifications() { return (dispatch, getState) => { - api(getState).delete(`/api/v1/notifications/${id}`).then(() => { - dispatch(deleteNotificationSuccess(id)); + dispatch(deleteMarkedNotificationsRequest()); + + let ids = []; + getState().getIn(['notifications', 'items']).forEach((n) => { + if (n.get('markedForDelete')) { + ids.push(n.get('id')); + } + }); + + if (ids.length === 0) { + dispatch(enterNotificationClearingMode(false)); + return; + } + + api(getState).delete(`/api/v1/notifications/destroy_multiple?ids[]=${ids.join('&ids[]=')}`).then(() => { + dispatch(deleteMarkedNotificationsSuccess()); + dispatch(expandNotifications()); // Load more (to fill the empty space) + }).catch(error => { + console.error(error); + dispatch(deleteMarkedNotificationsFail(error)); }); }; }; -export function deleteNotificationSuccess(id) { +export function enterNotificationClearingMode(yes) { + return { + type: NOTIFICATIONS_ENTER_CLEARING_MODE, + yes: yes, + }; +}; + +export function deleteMarkedNotificationsRequest() { return { - type: NOTIFICATION_DELETE_SUCCESS, + type: NOTIFICATIONS_DELETE_MARKED_REQUEST, + }; +}; + +export function deleteMarkedNotificationsFail() { + return { + type: NOTIFICATIONS_DELETE_MARKED_FAIL, + }; +}; + +export function markNotificationForDelete(id, yes) { + return { + type: NOTIFICATION_MARK_FOR_DELETE, id: id, + yes: yes, + }; +}; + +export function deleteMarkedNotificationsSuccess() { + return { + type: NOTIFICATIONS_DELETE_MARKED_SUCCESS, }; }; diff --git a/app/javascript/mastodon/components/column.js b/app/javascript/mastodon/components/column.js index 3cbb745c5..0dd31e137 100644 --- a/app/javascript/mastodon/components/column.js +++ b/app/javascript/mastodon/components/column.js @@ -34,7 +34,12 @@ export default class Column extends React.PureComponent { const { children } = this.props; return ( -
+
{children}
); diff --git a/app/javascript/mastodon/components/column_header.js b/app/javascript/mastodon/components/column_header.js index e9f041be6..045904206 100644 --- a/app/javascript/mastodon/components/column_header.js +++ b/app/javascript/mastodon/components/column_header.js @@ -1,8 +1,18 @@ import React from 'react'; import PropTypes from 'prop-types'; import classNames from 'classnames'; -import { FormattedMessage } from 'react-intl'; +import { defineMessages, FormattedMessage, injectIntl } from 'react-intl'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +// Glitch imports +import NotificationPurgeButtonsContainer from '../../glitch/components/column/notif_cleaning_widget/container'; + +const messages = defineMessages({ + titleNotifClearing: { id: 'column.notifications_clearing', defaultMessage: 'Dismiss selected notifications:' }, + titleNotifClearingShort: { id: 'column.notifications_clearing_short', defaultMessage: 'Dismiss selected:' }, +}); + +@injectIntl export default class ColumnHeader extends React.PureComponent { static contextTypes = { @@ -13,13 +23,17 @@ export default class ColumnHeader extends React.PureComponent { title: PropTypes.node.isRequired, icon: PropTypes.string.isRequired, active: PropTypes.bool, + localSettings : ImmutablePropTypes.map, multiColumn: PropTypes.bool, showBackButton: PropTypes.bool, + notifCleaning: PropTypes.bool, // true only for the notification column + notifCleaningActive: PropTypes.bool, children: PropTypes.node, pinned: PropTypes.bool, onPin: PropTypes.func, onMove: PropTypes.func, onClick: PropTypes.func, + intl: PropTypes.object.isRequired, }; state = { @@ -58,9 +72,16 @@ export default class ColumnHeader extends React.PureComponent { } render () { - const { title, icon, active, children, pinned, onPin, multiColumn, showBackButton } = this.props; + const { intl, icon, active, children, pinned, onPin, multiColumn, showBackButton, notifCleaning, localSettings } = this.props; const { collapsed, animating } = this.state; + let title = this.props.title; + if (notifCleaning && this.props.notifCleaningActive) { + title = intl.formatMessage(localSettings.getIn(['stretch']) ? + messages.titleNotifClearing : + messages.titleNotifClearingShort); + } + const wrapperClassName = classNames('column-header__wrapper', { 'active': active, }); @@ -130,6 +151,7 @@ export default class ColumnHeader extends React.PureComponent { {title}
+ {notifCleaning ? () : null} {backButton} {collapseButton}
diff --git a/app/javascript/mastodon/features/notifications/index.js b/app/javascript/mastodon/features/notifications/index.js index 39fb4b26d..6a262a59e 100644 --- a/app/javascript/mastodon/features/notifications/index.js +++ b/app/javascript/mastodon/features/notifications/index.js @@ -4,7 +4,10 @@ import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; import Column from '../../components/column'; import ColumnHeader from '../../components/column_header'; -import { expandNotifications, scrollTopNotifications } from '../../actions/notifications'; +import { + expandNotifications, + scrollTopNotifications, +} from '../../actions/notifications'; import { addColumn, removeColumn, moveColumn } from '../../actions/columns'; import NotificationContainer from '../../../glitch/components/notification/container'; import { ScrollContainer } from 'react-router-scroll'; @@ -26,9 +29,11 @@ const getNotifications = createSelector([ const mapStateToProps = state => ({ notifications: getNotifications(state), + localSettings: state.get('local_settings'), isLoading: state.getIn(['notifications', 'isLoading'], true), isUnread: state.getIn(['notifications', 'unread']) > 0, hasMore: !!state.getIn(['notifications', 'next']), + notifCleaningActive: state.getIn(['notifications', 'cleaningMode']), }); @connect(mapStateToProps) @@ -45,6 +50,8 @@ export default class Notifications extends React.PureComponent { isUnread: PropTypes.bool, multiColumn: PropTypes.bool, hasMore: PropTypes.bool, + localSettings: ImmutablePropTypes.map, + notifCleaningActive: PropTypes.bool, }; static defaultProps = { @@ -164,7 +171,9 @@ export default class Notifications extends React.PureComponent { this.scrollableArea = scrollableArea; return ( - + diff --git a/app/javascript/mastodon/reducers/notifications.js b/app/javascript/mastodon/reducers/notifications.js index da5fcde84..dd81653d6 100644 --- a/app/javascript/mastodon/reducers/notifications.js +++ b/app/javascript/mastodon/reducers/notifications.js @@ -8,7 +8,11 @@ import { NOTIFICATIONS_EXPAND_FAIL, NOTIFICATIONS_CLEAR, NOTIFICATIONS_SCROLL_TOP, - NOTIFICATION_DELETE_SUCCESS, + NOTIFICATIONS_DELETE_MARKED_REQUEST, + NOTIFICATIONS_DELETE_MARKED_SUCCESS, + NOTIFICATION_MARK_FOR_DELETE, + NOTIFICATIONS_DELETE_MARKED_FAIL, + NOTIFICATIONS_ENTER_CLEARING_MODE, } from '../actions/notifications'; import { ACCOUNT_BLOCK_SUCCESS } from '../actions/accounts'; import { TIMELINE_DELETE } from '../actions/timelines'; @@ -21,12 +25,14 @@ const initialState = ImmutableMap({ unread: 0, loaded: false, isLoading: true, + cleaningMode: false, }); const notificationToMap = notification => ImmutableMap({ id: notification.id, type: notification.type, account: notification.account.id, + markedForDelete: false, status: notification.status ? notification.status.id : null, }); @@ -93,17 +99,34 @@ const deleteByStatus = (state, statusId) => { return state.update('items', list => list.filterNot(item => item.get('status') === statusId)); }; -const deleteById = (state, notificationId) => { - return state.update('items', list => list.filterNot(item => item.get('id') === notificationId)); +const markForDelete = (state, notificationId, yes) => { + return state.update('items', list => list.map(item => { + if(item.get('id') === notificationId) { + return item.set('markedForDelete', yes); + } else { + return item; + } + })); +}; + +const unmarkAllForDelete = (state) => { + return state.update('items', list => list.map(item => item.set('markedForDelete', false))); +}; + +const deleteMarkedNotifs = (state) => { + return state.update('items', list => list.filterNot(item => item.get('markedForDelete'))); }; export default function notifications(state = initialState, action) { switch(action.type) { case NOTIFICATIONS_REFRESH_REQUEST: case NOTIFICATIONS_EXPAND_REQUEST: + case NOTIFICATIONS_DELETE_MARKED_REQUEST: + return state.set('isLoading', true); + case NOTIFICATIONS_DELETE_MARKED_FAIL: case NOTIFICATIONS_REFRESH_FAIL: case NOTIFICATIONS_EXPAND_FAIL: - return state.set('isLoading', true); + return state.set('isLoading', false); case NOTIFICATIONS_SCROLL_TOP: return updateTop(state, action.top); case NOTIFICATIONS_UPDATE: @@ -118,8 +141,15 @@ export default function notifications(state = initialState, action) { return state.set('items', ImmutableList()).set('next', null); case TIMELINE_DELETE: return deleteByStatus(state, action.id); - case NOTIFICATION_DELETE_SUCCESS: - return deleteById(state, action.id); + case NOTIFICATION_MARK_FOR_DELETE: + return markForDelete(state, action.id, action.yes); + case NOTIFICATIONS_DELETE_MARKED_SUCCESS: + return deleteMarkedNotifs(state).set('isLoading', false).set('cleaningMode', false); + case NOTIFICATIONS_ENTER_CLEARING_MODE: + const st = state.set('cleaningMode', action.yes); + if (!action.yes) + return unmarkAllForDelete(st); + else return st; default: return state; } diff --git a/app/javascript/styles/components.scss b/app/javascript/styles/components.scss index 0cd082985..dbdf286a9 100644 --- a/app/javascript/styles/components.scss +++ b/app/javascript/styles/components.scss @@ -451,6 +451,63 @@ cursor: pointer; } +.notification__dismiss-overlay { + position: absolute; + left: 0; top: 0; right: 0; bottom: 0; + + $c1: #00000A; + $c2: #222228; + background: linear-gradient(to right, + rgba($c1, 0.1), + rgba($c1, 0.2) 60%, + rgba($c2, 1) 90%, + rgba($c2, 1)); + + z-index: 999; + align-items: center; + justify-content: flex-end; + cursor: pointer; + + display: none; + + &.show { + display: flex; + } + + // make it brighter + &.active { + $c: #222931; + background: linear-gradient(to right, + rgba($c, 0.1), + rgba($c, 0.2) 60%, + rgba($c, 1) 90%, + rgba($c, 1)); + } + + &:focus { + outline: 0 !important; + } +} + +.notification__dismiss-overlay__ckbox { + border: 2px solid #9baec8; + border-radius: 2px; + width: 30px; + height: 30px; + margin-right: 20px; + font-size: 20px; + color: #c3dcfd; + text-shadow: 0 0 5px black; + display: flex; + justify-content: center; + align-items: center; + + :focus & { + outline: rgb(77, 144, 254) auto 10px; + outline: -webkit-focus-ring-color auto 10px; + } +} + // --- Extra clickable area in the status gutter --- .ui.wide { @mixin xtraspaces-full { @@ -627,24 +684,14 @@ position: absolute; } -.status__prepend-dismiss-button { - border: 0; - background: transparent; - position: absolute; - right: -3px; - opacity: 0; - transition: opacity 0.1s ease-in-out; - - i.fa { - color: crimson; - } +.notification-follow { + position: relative; - .notification__message:hover & { - opacity: 1; - } + // same like Status + border-bottom: 1px solid lighten($ui-base-color, 8%); - .notification-follow & { - right: 6px; + .account { + border-bottom: 0 none; } } @@ -2408,6 +2455,17 @@ button.icon-button.active i.fa-retweet { } } +.column-header__notif-cleaning-buttons { + display: flex; + align-items: stretch; + + button { + @extend .column-header__button; + padding-left: 12px; + padding-right: 12px; + } +} + .column-header__collapsible { max-height: 70vh; overflow: hidden; diff --git a/config/routes.rb b/config/routes.rb index fb2051aad..ac505edc6 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -182,6 +182,7 @@ Rails.application.routes.draw do collection do post :clear post :dismiss + delete :destroy_multiple end end -- cgit From 0244019ca17288802a144c84b7e0f319f1685695 Mon Sep 17 00:00:00 2001 From: Ondřej Hruška Date: Fri, 21 Jul 2017 21:12:43 +0200 Subject: Fixed horrible outline around notif clearing checkbox & moved the overlay to a more sr-friendly place --- app/javascript/glitch/components/notification/follow.js | 2 +- app/javascript/glitch/components/status/index.js | 14 +++++--------- app/javascript/styles/components.scss | 3 +-- 3 files changed, 7 insertions(+), 12 deletions(-) (limited to 'app/javascript/glitch/components/status/index.js') diff --git a/app/javascript/glitch/components/notification/follow.js b/app/javascript/glitch/components/notification/follow.js index 0e0065eb1..d340e83c8 100644 --- a/app/javascript/glitch/components/notification/follow.js +++ b/app/javascript/glitch/components/notification/follow.js @@ -103,7 +103,6 @@ We can now render our component. return (
-
@@ -117,6 +116,7 @@ We can now render our component.
+
); } diff --git a/app/javascript/glitch/components/status/index.js b/app/javascript/glitch/components/status/index.js index dc06250ec..b7ec8b4ca 100644 --- a/app/javascript/glitch/components/status/index.js +++ b/app/javascript/glitch/components/status/index.js @@ -526,10 +526,6 @@ applicable. } } - markNotifForDelete = () => { - this.setState({ 'markedForDelete' : !this.state.markedForDelete }); - } - /* #### `render()`. @@ -699,11 +695,6 @@ collapsed. }} ref={handleRef} > - {notification ? ( - - ) : null} {prepend && account ? ( ) : null} + {notification ? ( + + ) : null} ); diff --git a/app/javascript/styles/components.scss b/app/javascript/styles/components.scss index dbdf286a9..3e80569a9 100644 --- a/app/javascript/styles/components.scss +++ b/app/javascript/styles/components.scss @@ -503,8 +503,7 @@ align-items: center; :focus & { - outline: rgb(77, 144, 254) auto 10px; - outline: -webkit-focus-ring-color auto 10px; + box-shadow: 0 0 2px 2px #3e6fc1; } } -- cgit From cb69e35b3b13a67df5216b2c5fd657a62b3bda7a Mon Sep 17 00:00:00 2001 From: Ondřej Hruška Date: Thu, 27 Jul 2017 00:41:28 +0200 Subject: Add visibility icon to Detailed status --- app/javascript/glitch/components/status/header.js | 24 ++++------- app/javascript/glitch/components/status/index.js | 3 +- .../glitch/components/status/visibility_icon.js | 48 ++++++++++++++++++++++ .../features/status/components/action_bar.js | 4 +- .../features/status/components/detailed_status.js | 3 +- app/javascript/styles/components.scss | 6 +-- 6 files changed, 63 insertions(+), 25 deletions(-) create mode 100644 app/javascript/glitch/components/status/visibility_icon.js (limited to 'app/javascript/glitch/components/status/index.js') diff --git a/app/javascript/glitch/components/status/header.js b/app/javascript/glitch/components/status/header.js index 3187fa7fb..5ce59fba4 100644 --- a/app/javascript/glitch/components/status/header.js +++ b/app/javascript/glitch/components/status/header.js @@ -29,6 +29,7 @@ import Avatar from '../../../mastodon/components/avatar'; import AvatarOverlay from '../../../mastodon/components/avatar_overlay'; import DisplayName from '../../../mastodon/components/display_name'; import IconButton from '../../../mastodon/components/icon_button'; +import VisibilityIcon from './visibility_icon'; /* * * * */ @@ -94,7 +95,7 @@ icons) into a single `
` element. export default class StatusHeader extends React.PureComponent { static propTypes = { - account: ImmutablePropTypes.map.isRequired, + status: ImmutablePropTypes.map.isRequired, friend: ImmutablePropTypes.map, mediaIcon: PropTypes.string, collapsible: PropTypes.bool, @@ -102,7 +103,6 @@ export default class StatusHeader extends React.PureComponent { parseClick: PropTypes.func.isRequired, setExpansion: PropTypes.func.isRequired, intl: PropTypes.object.isRequired, - visibility: PropTypes.string, }; /* @@ -135,8 +135,8 @@ status. */ handleAccountClick = (e) => { - const { account, parseClick } = this.props; - parseClick(e, `/accounts/${+account.get('id')}`); + const { status, parseClick } = this.props; + parseClick(e, `/accounts/${+status.getIn(['account', 'id'])}`); } /* @@ -150,21 +150,15 @@ has a very straightforward rendering process. render () { const { - account, + status, friend, mediaIcon, collapsible, collapsed, intl, - visibility, } = this.props; - const visibilityClass = { - public: 'globe', - unlisted: 'unlock-alt', - private: 'lock', - direct: 'envelope', - }[visibility]; + const account = status.get('account'); return (
@@ -186,11 +180,7 @@ it is rendered as a float. /> ) : null} {( -
- - + + + {shareButton}
- +
diff --git a/app/javascript/glitch/components/status/container.js b/app/javascript/glitch/components/status/container.js index b4d7fb4cc..da2771c0b 100644 --- a/app/javascript/glitch/components/status/container.js +++ b/app/javascript/glitch/components/status/container.js @@ -38,6 +38,8 @@ import { favourite, unreblog, unfavourite, + pin, + unpin, } from '../../../mastodon/actions/interactions'; import { blockAccount } from '../../../mastodon/actions/accounts'; import { initMuteModal } from '../../../mastodon/actions/mutes'; @@ -187,6 +189,18 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ } }, + 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'))); diff --git a/app/javascript/glitch/components/status/index.js b/app/javascript/glitch/components/status/index.js index 55e6f1876..4a2a0e1d4 100644 --- a/app/javascript/glitch/components/status/index.js +++ b/app/javascript/glitch/components/status/index.js @@ -165,10 +165,13 @@ export default class Status extends ImmutablePureComponent { 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, diff --git a/app/views/layouts/embedded.html.haml b/app/views/layouts/embedded.html.haml index 46dab2d0f..5fc60be17 100644 --- a/app/views/layouts/embedded.html.haml +++ b/app/views/layouts/embedded.html.haml @@ -6,6 +6,7 @@ = stylesheet_pack_tag 'common', media: 'all' = javascript_pack_tag 'common', integrity: true, crossorigin: 'anonymous' + = stylesheet_pack_tag 'application', integrity: true, media: 'all' = javascript_pack_tag "locale_#{I18n.locale}", integrity: true, crossorigin: 'anonymous' = javascript_pack_tag 'public', integrity: true, crossorigin: 'anonymous' %body.embed -- cgit From 9a5418942c03f51176faaeec33d2fdc1acc5c30e Mon Sep 17 00:00:00 2001 From: Ondřej Hruška Date: Thu, 28 Sep 2017 10:04:20 +0200 Subject: fix some js errors due to number->string change of status ID --- app/javascript/glitch/components/notification/follow.js | 2 +- app/javascript/glitch/components/status/action_bar.js | 2 +- app/javascript/glitch/components/status/index.js | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) (limited to 'app/javascript/glitch/components/status/index.js') diff --git a/app/javascript/glitch/components/notification/follow.js b/app/javascript/glitch/components/notification/follow.js index f471307b9..99a5c6219 100644 --- a/app/javascript/glitch/components/notification/follow.js +++ b/app/javascript/glitch/components/notification/follow.js @@ -30,7 +30,7 @@ import NotificationOverlayContainer from '../notification/overlay/container'; export default class NotificationFollow extends ImmutablePureComponent { static propTypes = { - id : PropTypes.number.isRequired, + id : PropTypes.string.isRequired, account : ImmutablePropTypes.map.isRequired, notification : ImmutablePropTypes.map.isRequired, }; diff --git a/app/javascript/glitch/components/status/action_bar.js b/app/javascript/glitch/components/status/action_bar.js index d4d26c62c..f4450d31b 100644 --- a/app/javascript/glitch/components/status/action_bar.js +++ b/app/javascript/glitch/components/status/action_bar.js @@ -50,7 +50,7 @@ export default class StatusActionBar extends ImmutablePureComponent { onEmbed: PropTypes.func, onMuteConversation: PropTypes.func, onPin: PropTypes.func, - me: PropTypes.number, + me: PropTypes.string, withDismiss: PropTypes.bool, intl: PropTypes.object.isRequired, }; diff --git a/app/javascript/glitch/components/status/index.js b/app/javascript/glitch/components/status/index.js index 4a2a0e1d4..9e758793c 100644 --- a/app/javascript/glitch/components/status/index.js +++ b/app/javascript/glitch/components/status/index.js @@ -155,12 +155,12 @@ export default class Status extends ImmutablePureComponent { }; static propTypes = { - id : PropTypes.number, + id : PropTypes.string, status : ImmutablePropTypes.map, account : ImmutablePropTypes.map, settings : ImmutablePropTypes.map, notification : ImmutablePropTypes.map, - me : PropTypes.number, + me : PropTypes.string, onFavourite : PropTypes.func, onReblog : PropTypes.func, onModalReblog : PropTypes.func, -- cgit From e0298d66f8f807efe80cb01097b46e0ef33bd154 Mon Sep 17 00:00:00 2001 From: kibigo! Date: Sun, 5 Nov 2017 15:05:12 -0800 Subject: Autocollapse boosts option --- app/javascript/glitch/components/local_settings/page/index.js | 10 ++++++++++ app/javascript/glitch/components/status/index.js | 4 ++++ app/javascript/glitch/locales/en.json | 1 + app/javascript/glitch/reducers/local_settings.js | 1 + 4 files changed, 16 insertions(+) (limited to 'app/javascript/glitch/components/status/index.js') diff --git a/app/javascript/glitch/components/local_settings/page/index.js b/app/javascript/glitch/components/local_settings/page/index.js index 366c113c0..498230f7b 100644 --- a/app/javascript/glitch/components/local_settings/page/index.js +++ b/app/javascript/glitch/components/local_settings/page/index.js @@ -124,6 +124,16 @@ export default class LocalSettingsPage extends React.PureComponent { > + + + ( 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 diff --git a/app/javascript/glitch/locales/en.json b/app/javascript/glitch/locales/en.json index 18e412356..69aa29108 100644 --- a/app/javascript/glitch/locales/en.json +++ b/app/javascript/glitch/locales/en.json @@ -14,6 +14,7 @@ "settings.auto_collapse_lengthy": "Lengthy toots", "settings.auto_collapse_media": "Toots with media", "settings.auto_collapse_notifications": "Notifications", + "settings.auto_collapse_reblogs": "Boosts", "settings.auto_collapse_replies": "Replies", "settings.close": "Close", "settings.collapsed_statuses": "Collapsed toots", diff --git a/app/javascript/glitch/reducers/local_settings.js b/app/javascript/glitch/reducers/local_settings.js index 813e130ca..03654fbe2 100644 --- a/app/javascript/glitch/reducers/local_settings.js +++ b/app/javascript/glitch/reducers/local_settings.js @@ -59,6 +59,7 @@ const initialState = ImmutableMap({ all : false, notifications : true, lengthy : true, + reblogs : false, replies : false, media : false, }), -- cgit