From a5460bb97eece7da11d8335568a91c1233c7148e Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 11 Aug 2021 18:23:42 -0500 Subject: [PATCH 1/4] pinnedHosts: actions --- app/soapbox/actions/remote_timeline.js | 24 ++++++++++++++++++++++++ app/soapbox/actions/settings.js | 6 +++++- 2 files changed, 29 insertions(+), 1 deletion(-) create mode 100644 app/soapbox/actions/remote_timeline.js diff --git a/app/soapbox/actions/remote_timeline.js b/app/soapbox/actions/remote_timeline.js new file mode 100644 index 000000000..65ec69e82 --- /dev/null +++ b/app/soapbox/actions/remote_timeline.js @@ -0,0 +1,24 @@ +import { getSettings, changeSetting } from 'soapbox/actions/settings'; + +const getPinnedPosts = state => { + const settings = getSettings(state); + return settings.getIn(['remote_timeline', 'pinnedHosts']); +}; + +export function pinHost(host) { + return (dispatch, getState) => { + const state = getState(); + const pinnedHosts = getPinnedPosts(state); + + return dispatch(changeSetting(['remote_timeline', 'pinnedHosts'], pinnedHosts.add(host))); + }; +} + +export function unpinHost(host) { + return (dispatch, getState) => { + const state = getState(); + const pinnedHosts = getPinnedPosts(state); + + return dispatch(changeSetting(['remote_timeline', 'pinnedHosts'], pinnedHosts.delete(host))); + }; +} diff --git a/app/soapbox/actions/settings.js b/app/soapbox/actions/settings.js index 5beba817a..47b153ee7 100644 --- a/app/soapbox/actions/settings.js +++ b/app/soapbox/actions/settings.js @@ -1,7 +1,7 @@ import { debounce } from 'lodash'; import { showAlertForError } from './alerts'; import { patchMe } from 'soapbox/actions/me'; -import { Map as ImmutableMap, List as ImmutableList } from 'immutable'; +import { Map as ImmutableMap, List as ImmutableList, OrderedSet as ImmutableOrderedSet } from 'immutable'; import { isLoggedIn } from 'soapbox/utils/auth'; import uuid from '../uuid'; import { createSelector } from 'reselect'; @@ -143,6 +143,10 @@ export const defaultSettings = ImmutableMap({ ImmutableMap({ id: 'HOME', uuid: uuid(), params: {} }), ImmutableMap({ id: 'NOTIFICATIONS', uuid: uuid(), params: {} }), ]), + + remote_timeline: ImmutableMap({ + pinnedHosts: ImmutableOrderedSet(), + }), }); export const getSettings = createSelector([ From 9e12e978d8d025a216db320c13c1d31652e34247 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 11 Aug 2021 18:35:31 -0500 Subject: [PATCH 2/4] RemoteTimeline: rename InstanceInfoPanel --- .../components/instance_moderation_panel.js | 80 +++++++++++++++++++ app/soapbox/pages/remote_instance_page.js | 2 + 2 files changed, 82 insertions(+) create mode 100644 app/soapbox/features/ui/components/instance_moderation_panel.js diff --git a/app/soapbox/features/ui/components/instance_moderation_panel.js b/app/soapbox/features/ui/components/instance_moderation_panel.js new file mode 100644 index 000000000..693940540 --- /dev/null +++ b/app/soapbox/features/ui/components/instance_moderation_panel.js @@ -0,0 +1,80 @@ +'use strict'; + +import React from 'react'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import PropTypes from 'prop-types'; +import { connect } from 'react-redux'; +import { injectIntl, defineMessages, FormattedMessage } from 'react-intl'; +import ImmutablePureComponent from 'react-immutable-pure-component'; +import { makeGetRemoteInstance } from 'soapbox/selectors'; +import InstanceRestrictions from 'soapbox/features/federation_restrictions/components/instance_restrictions'; +import DropdownMenu from 'soapbox/containers/dropdown_menu_container'; +import { openModal } from 'soapbox/actions/modal'; +import { isAdmin } from 'soapbox/utils/accounts'; + +const getRemoteInstance = makeGetRemoteInstance(); + +const messages = defineMessages({ + editFederation: { id: 'remote_instance.edit_federation', defaultMessage: 'Edit federation' }, +}); + +const mapStateToProps = (state, { host }) => { + const me = state.get('me'); + const account = state.getIn(['accounts', me]); + + return { + instance: state.get('instance'), + remoteInstance: getRemoteInstance(state, host), + isAdmin: isAdmin(account), + }; +}; + +export default @connect(mapStateToProps, null, null, { forwardRef: true }) +@injectIntl +class InstanceModerationPanel extends ImmutablePureComponent { + + static propTypes = { + intl: PropTypes.object.isRequired, + host: PropTypes.string.isRequired, + instance: ImmutablePropTypes.map, + remoteInstance: ImmutablePropTypes.map, + isAdmin: PropTypes.bool, + }; + + handleEditFederation = e => { + const { dispatch, host } = this.props; + dispatch(openModal('EDIT_FEDERATION', { host })); + } + + makeMenu = () => { + const { intl } = this.props; + + return [{ + text: intl.formatMessage(messages.editFederation), + action: this.handleEditFederation, + }]; + } + + render() { + const { remoteInstance, isAdmin } = this.props; + const menu = this.makeMenu(); + + return ( +
+
+ + + + + {isAdmin &&
+ +
} +
+
+ +
+
+ ); + } + +} diff --git a/app/soapbox/pages/remote_instance_page.js b/app/soapbox/pages/remote_instance_page.js index 218dbcf72..4620c7a40 100644 --- a/app/soapbox/pages/remote_instance_page.js +++ b/app/soapbox/pages/remote_instance_page.js @@ -8,6 +8,7 @@ import FeaturesPanel from 'soapbox/features/ui/components/features_panel'; import LinkFooter from 'soapbox/features/ui/components/link_footer'; import { getFeatures } from 'soapbox/utils/features'; import InstanceInfoPanel from 'soapbox/features/ui/components/instance_info_panel'; +import InstanceModerationPanel from 'soapbox/features/ui/components/instance_moderation_panel'; import { federationRestrictionsDisclosed } from 'soapbox/utils/state'; import { isAdmin } from 'soapbox/utils/accounts'; @@ -38,6 +39,7 @@ class RemoteInstancePage extends ImmutablePureComponent {
+ {(disclosed || isAdmin) && }
From 6961309b8563661cdd7cd3d3544400619c155715 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 11 Aug 2021 18:55:10 -0500 Subject: [PATCH 3/4] RemoteTimeline: allow pinning hosts --- .../ui/components/instance_info_panel.js | 48 ++++++++++--------- app/soapbox/pages/remote_instance_page.js | 4 +- 2 files changed, 27 insertions(+), 25 deletions(-) diff --git a/app/soapbox/features/ui/components/instance_info_panel.js b/app/soapbox/features/ui/components/instance_info_panel.js index 7c677ca4c..8a42fbb9b 100644 --- a/app/soapbox/features/ui/components/instance_info_panel.js +++ b/app/soapbox/features/ui/components/instance_info_panel.js @@ -4,28 +4,27 @@ import React from 'react'; import ImmutablePropTypes from 'react-immutable-proptypes'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; -import { injectIntl, defineMessages, FormattedMessage } from 'react-intl'; +import { injectIntl, defineMessages } from 'react-intl'; import ImmutablePureComponent from 'react-immutable-pure-component'; import { makeGetRemoteInstance } from 'soapbox/selectors'; -import InstanceRestrictions from 'soapbox/features/federation_restrictions/components/instance_restrictions'; import DropdownMenu from 'soapbox/containers/dropdown_menu_container'; -import { openModal } from 'soapbox/actions/modal'; -import { isAdmin } from 'soapbox/utils/accounts'; +import { pinHost, unpinHost } from 'soapbox/actions/remote_timeline'; +import { getSettings } from 'soapbox/actions/settings'; const getRemoteInstance = makeGetRemoteInstance(); const messages = defineMessages({ - editFederation: { id: 'remote_instance.edit_federation', defaultMessage: 'Edit federation' }, + pinHost: { id: 'remote_instance.pin_host', defaultMessage: 'Pin {host}' }, + unpinHost: { id: 'remote_instance.unpin_host', defaultMessage: 'Unpin {host}' }, }); const mapStateToProps = (state, { host }) => { - const me = state.get('me'); - const account = state.getIn(['accounts', me]); + const settings = getSettings(state); return { instance: state.get('instance'), remoteInstance: getRemoteInstance(state, host), - isAdmin: isAdmin(account), + pinned: settings.getIn(['remote_timeline', 'pinnedHosts']).includes(host), }; }; @@ -38,40 +37,43 @@ class InstanceInfoPanel extends ImmutablePureComponent { host: PropTypes.string.isRequired, instance: ImmutablePropTypes.map, remoteInstance: ImmutablePropTypes.map, - isAdmin: PropTypes.bool, + pinned: PropTypes.bool, }; - handleEditFederation = e => { - const { dispatch, host } = this.props; - dispatch(openModal('EDIT_FEDERATION', { host })); + handlePinHost = e => { + const { dispatch, host, pinned } = this.props; + + if (!pinned) { + dispatch(pinHost(host)); + } else { + dispatch(unpinHost(host)); + } } makeMenu = () => { - const { intl } = this.props; + const { intl, host, pinned } = this.props; return [{ - text: intl.formatMessage(messages.editFederation), - action: this.handleEditFederation, + text: intl.formatMessage(pinned ? messages.unpinHost : messages.pinHost, { host }), + action: this.handlePinHost, }]; } render() { - const { remoteInstance, isAdmin } = this.props; + const { remoteInstance, pinned } = this.props; const menu = this.makeMenu(); + const icon = pinned ? 'thumb-tack' : 'globe-w'; return (
- + - + {remoteInstance.get('host')} - {isAdmin &&
+
-
} -
-
- +
); diff --git a/app/soapbox/pages/remote_instance_page.js b/app/soapbox/pages/remote_instance_page.js index 4620c7a40..bf2b9f04c 100644 --- a/app/soapbox/pages/remote_instance_page.js +++ b/app/soapbox/pages/remote_instance_page.js @@ -39,8 +39,8 @@ class RemoteInstancePage extends ImmutablePureComponent {
- - {(disclosed || isAdmin) && } + + {(disclosed || isAdmin) && }
From 4e2f12eb187441abfe18235edac63e8e9247811f Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 11 Aug 2021 19:38:25 -0500 Subject: [PATCH 4/4] pinnedHosts: add host picker to public timelines --- app/soapbox/actions/remote_timeline.js | 6 +-- app/soapbox/features/public_timeline/index.js | 2 + .../components/pinned_hosts_picker.js | 51 +++++++++++++++++++ app/soapbox/features/remote_timeline/index.js | 2 + app/styles/components/remote-timeline.scss | 45 ++++++++++++++++ 5 files changed, 103 insertions(+), 3 deletions(-) create mode 100644 app/soapbox/features/remote_timeline/components/pinned_hosts_picker.js diff --git a/app/soapbox/actions/remote_timeline.js b/app/soapbox/actions/remote_timeline.js index 65ec69e82..38249c009 100644 --- a/app/soapbox/actions/remote_timeline.js +++ b/app/soapbox/actions/remote_timeline.js @@ -1,6 +1,6 @@ import { getSettings, changeSetting } from 'soapbox/actions/settings'; -const getPinnedPosts = state => { +const getPinnedHosts = state => { const settings = getSettings(state); return settings.getIn(['remote_timeline', 'pinnedHosts']); }; @@ -8,7 +8,7 @@ const getPinnedPosts = state => { export function pinHost(host) { return (dispatch, getState) => { const state = getState(); - const pinnedHosts = getPinnedPosts(state); + const pinnedHosts = getPinnedHosts(state); return dispatch(changeSetting(['remote_timeline', 'pinnedHosts'], pinnedHosts.add(host))); }; @@ -17,7 +17,7 @@ export function pinHost(host) { export function unpinHost(host) { return (dispatch, getState) => { const state = getState(); - const pinnedHosts = getPinnedPosts(state); + const pinnedHosts = getPinnedHosts(state); return dispatch(changeSetting(['remote_timeline', 'pinnedHosts'], pinnedHosts.delete(host))); }; diff --git a/app/soapbox/features/public_timeline/index.js b/app/soapbox/features/public_timeline/index.js index dc204f0eb..a424ec988 100644 --- a/app/soapbox/features/public_timeline/index.js +++ b/app/soapbox/features/public_timeline/index.js @@ -7,6 +7,7 @@ import Column from '../../components/column'; import ColumnSettingsContainer from './containers/column_settings_container'; import HomeColumnHeader from '../../components/home_column_header'; import Accordion from 'soapbox/features/ui/components/accordion'; +import PinnedHostsPicker from '../remote_timeline/components/pinned_hosts_picker'; import { expandPublicTimeline } from '../../actions/timelines'; import { connectPublicStream } from '../../actions/streaming'; import { Link } from 'react-router-dom'; @@ -101,6 +102,7 @@ class CommunityTimeline extends React.PureComponent { + {showExplanationBox &&
} diff --git a/app/soapbox/features/remote_timeline/components/pinned_hosts_picker.js b/app/soapbox/features/remote_timeline/components/pinned_hosts_picker.js new file mode 100644 index 000000000..0305268b0 --- /dev/null +++ b/app/soapbox/features/remote_timeline/components/pinned_hosts_picker.js @@ -0,0 +1,51 @@ +'use strict'; + +import React from 'react'; +import { connect } from 'react-redux'; +import PropTypes from 'prop-types'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import classNames from 'classnames'; +import { Link } from 'react-router-dom'; +import { getSettings } from 'soapbox/actions/settings'; + +const mapStateToProps = state => { + const settings = getSettings(state); + + return { + pinnedHosts: settings.getIn(['remote_timeline', 'pinnedHosts']), + }; +}; + +class PinnedHostPicker extends React.PureComponent { + + static contextTypes = { + router: PropTypes.object, + }; + + static propTypes = { + dispatch: PropTypes.func.isRequired, + pinnedHosts: ImmutablePropTypes.orderedSet, + host: PropTypes.string, + }; + + render() { + const { pinnedHosts, host: activeHost } = this.props; + + if (!pinnedHosts || pinnedHosts.isEmpty()) return null; + + return ( +
+
+ {pinnedHosts.map(host => ( +
+ {host} +
+ ))} +
+
+ ); + } + +} + +export default connect(mapStateToProps)(PinnedHostPicker); diff --git a/app/soapbox/features/remote_timeline/index.js b/app/soapbox/features/remote_timeline/index.js index ee08192ff..5f22c4930 100644 --- a/app/soapbox/features/remote_timeline/index.js +++ b/app/soapbox/features/remote_timeline/index.js @@ -5,6 +5,7 @@ import PropTypes from 'prop-types'; import StatusListContainer from '../ui/containers/status_list_container'; import Column from '../../components/column'; import HomeColumnHeader from '../../components/home_column_header'; +import PinnedHostsPicker from './components/pinned_hosts_picker'; import IconButton from 'soapbox/components/icon_button'; import { expandRemoteTimeline } from '../../actions/timelines'; import { connectRemoteStream } from '../../actions/streaming'; @@ -84,6 +85,7 @@ class RemoteTimeline extends React.PureComponent { return ( +