From 4f60c6845ddf0e602078829e32c0910f0dfeb96f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Wed, 6 Oct 2021 18:15:32 +0200 Subject: [PATCH 001/278] Improve account headers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- .../features/account/components/header.js | 61 ++++++++++-- app/soapbox/pages/profile_page.js | 11 ++- app/styles/components/account-header.scss | 99 ++++++++++++++++++- app/styles/ui.scss | 4 + 4 files changed, 163 insertions(+), 12 deletions(-) diff --git a/app/soapbox/features/account/components/header.js b/app/soapbox/features/account/components/header.js index 2b8110d95..8f7d86d75 100644 --- a/app/soapbox/features/account/components/header.js +++ b/app/soapbox/features/account/components/header.js @@ -5,6 +5,7 @@ import { connect } from 'react-redux'; import ImmutablePropTypes from 'react-immutable-proptypes'; import PropTypes from 'prop-types'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; +import Icon from 'soapbox/components/icon'; import ImmutablePureComponent from 'react-immutable-pure-component'; import { isStaff, @@ -17,14 +18,18 @@ import { } from 'soapbox/utils/accounts'; import classNames from 'classnames'; import Avatar from 'soapbox/components/avatar'; +import { getAcct } from 'soapbox/utils/accounts'; +import { displayFqn } from 'soapbox/utils/state'; import DropdownMenuContainer from 'soapbox/containers/dropdown_menu_container'; import BundleContainer from 'soapbox/features/ui/containers/bundle_container'; import { ProfileInfoPanel } from 'soapbox/features/ui/util/async-components'; -import { debounce } from 'lodash'; +import { debounce, throttle } from 'lodash'; import StillImage from 'soapbox/components/still_image'; import ActionButton from 'soapbox/features/ui/components/action_button'; import SubscriptionButton from 'soapbox/features/ui/components/subscription_button'; import { openModal } from 'soapbox/actions/modal'; +import VerificationBadge from 'soapbox/components/verification_badge'; +import Badge from 'soapbox/components/badge'; import { List as ImmutableList, Map as ImmutableMap } from 'immutable'; import { getFeatures } from 'soapbox/utils/features'; @@ -65,6 +70,8 @@ const messages = defineMessages({ demoteToUser: { id: 'admin.users.actions.demote_to_user', defaultMessage: 'Demote @{name} to a regular user' }, subscribe: { id: 'account.subscribe', defaultMessage: 'Subscribe to notifications from @{name}' }, unsubscribe: { id: 'account.unsubscribe', defaultMessage: 'Unsubscribe to notifications from @{name}' }, + deactivated: { id: 'account.deactivated', defaultMessage: 'Deactivated' }, + bot: { id: 'account.badges.bot', defaultMessage: 'Bot' }, }); const mapStateToProps = state => { @@ -77,6 +84,8 @@ const mapStateToProps = state => { me, meAccount: account, features, + displayFqn: displayFqn(state), + }; }; @@ -91,10 +100,12 @@ class Header extends ImmutablePureComponent { intl: PropTypes.object.isRequired, username: PropTypes.string, features: PropTypes.object, + displayFqn: PropTypes.bool, }; state = { isSmallScreen: (window.innerWidth <= 895), + isLocked: false, } isStatusesPageActive = (match, location) => { @@ -106,19 +117,34 @@ class Header extends ImmutablePureComponent { } componentDidMount() { + window.addEventListener('scroll', this.handleScroll); window.addEventListener('resize', this.handleResize, { passive: true }); } componentWillUnmount() { + window.removeEventListener('scroll', this.handleScroll); window.removeEventListener('resize', this.handleResize); } + setRef = (c) => { + this.node = c; + } + handleResize = debounce(() => { this.setState({ isSmallScreen: (window.innerWidth <= 895) }); }, 5, { trailing: true, }); + handleScroll = throttle(() => { + const { top } = this.node.getBoundingClientRect(); + const isLocked = top <= 60; + + if (this.state.isLocked !== isLocked) { + this.setState({ isLocked }); + } + }, 100, { trailing: true }); + onAvatarClick = () => { const avatar_url = this.props.account.get('avatar'); const avatar = ImmutableMap({ @@ -295,16 +321,18 @@ class Header extends ImmutablePureComponent { } render() { - const { account, username, me, features } = this.props; - const { isSmallScreen } = this.state; + const { account, displayFqn, intl, username, me, features } = this.props; + const { isSmallScreen, isLocked } = this.state; if (!account) { return (
-
+
-
+
+
+
{isSmallScreen && (
@@ -326,6 +354,10 @@ class Header extends ImmutablePureComponent { const avatarSize = isSmallScreen ? 90 : 200; const deactivated = !account.getIn(['pleroma', 'is_active'], true); + const lockedIcon = account.get('locked') ? () : ''; + const displayNameHtml = deactivated ? { __html: intl.formatMessage(messages.deactivated) } : { __html: account.get('display_name_html') }; + const verified = account.getIn(['pleroma', 'tags'], ImmutableList()).includes('verified'); + return (
@@ -342,12 +374,23 @@ class Header extends ImmutablePureComponent {
}
-
+
- - - +
+ + + +
+ +
+ + {verified && } + {account.get('bot') && } + { @{getAcct(account, displayFqn)} {lockedIcon} } +
+
+
{isSmallScreen && (
diff --git a/app/soapbox/pages/profile_page.js b/app/soapbox/pages/profile_page.js index 251bf7190..82657b1a2 100644 --- a/app/soapbox/pages/profile_page.js +++ b/app/soapbox/pages/profile_page.js @@ -19,6 +19,8 @@ import { displayFqn } from 'soapbox/utils/state'; import { getFeatures } from 'soapbox/utils/features'; import { makeGetAccount } from '../selectors'; import { Redirect } from 'react-router-dom'; +import classNames from 'classnames'; + const mapStateToProps = (state, { params, withReplies = false }) => { const username = params.username || ''; @@ -75,13 +77,20 @@ class ProfilePage extends ImmutablePureComponent { return ; } + let headerMissing; + const header = account ? account.get('header', '') : undefined; + + if (header) { + headerMissing = !header || ['/images/banner.png', '/headers/original/missing.png'].some(path => header.endsWith(path)) || !account.getIn(['pleroma', 'is_active'], true); + } + return (
{account && @{getAcct(account, displayFqn)} } -
+
diff --git a/app/styles/components/account-header.scss b/app/styles/components/account-header.scss index 12ca0146c..7b1ff4376 100644 --- a/app/styles/components/account-header.scss +++ b/app/styles/components/account-header.scss @@ -114,15 +114,67 @@ } } - &__avatar { - display: block; + @keyframes fadeIn { + 1% { + visibility: visible; + } + + 100% { + visibility: visible; + } + } + + @keyframes fadeOut { + 1% { + visibility: visible; + } + + 100% { + visibility: hidden; + } + } + + &__card { + display: flex; + flex-direction: column; position: absolute; left: 0; top: -90px; + + &.is-locked { + .account__header__avatar { + top: -130px; + opacity: 0; + animation: 0.3s fadeOut; + animation-fill-mode: forwards; + } + + .account__header__name { + top: 90px; + opacity: 1; + animation: 0.3s fadeIn; + animation-fill-mode: forwards; + } + } + + @media screen and (max-width: 895px) { + top: -45px; + left: 10px; + } + } + + &__avatar { + display: block; + position: absolute; + top: 0; border-radius: 50%; height: 200px; width: 200px; background-color: var(--foreground-color); + opacity: 1; + animation: 0.3s fadeIn; + animation-fill-mode: forwards; + transition: top 0.3s, opacity 0.15s; // NOTE - patch fix for avatar size. Wrapper may not be needed when I do polish up on the page .account__avatar { @@ -163,6 +215,49 @@ } } + &__name { + display: flex; + align-items: center; + column-gap: 10px; + width: 265px; + height: 74px; + position: absolute; + top: 220px; + opacity: 0; + animation: 0.3s fadeOut; + animation-fill-mode: forwards; + transition: top 0.3s, opacity 0.15s; + + div:nth-child(2) { + width: calc(100% - 50px); + color: var(--primary-text-color); + + span:first-of-type { + display: inline-block; + max-width: 100%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-size: 18px; + line-height: 1.25; + font-weight: 600; + + &.with-badge { + max-width: calc(100% - 20px); + } + } + + small { + display: block; + font-size: 14px; + line-height: 1.5; + font-weight: 400; + overflow: hidden; + text-overflow: ellipsis; + } + } + } + &__extra { display: flex; flex-direction: row; diff --git a/app/styles/ui.scss b/app/styles/ui.scss index 926900dea..dbad80cc7 100644 --- a/app/styles/ui.scss +++ b/app/styles/ui.scss @@ -351,6 +351,10 @@ @media (min-width: 896px) { top: -290px; position: sticky; + + &__no-header { + top: -75px; + } } } From 2fe8eee31aa6fcc1b234a5a1f4796d9c01d652a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Wed, 6 Oct 2021 20:46:03 +0200 Subject: [PATCH 002/278] fix breakpoint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- app/styles/components/account-header.scss | 1 - 1 file changed, 1 deletion(-) diff --git a/app/styles/components/account-header.scss b/app/styles/components/account-header.scss index 7b1ff4376..9234ca7a0 100644 --- a/app/styles/components/account-header.scss +++ b/app/styles/components/account-header.scss @@ -201,7 +201,6 @@ } @media screen and (max-width: 895px) { - top: -45px; left: 20px; left: max(20px + env(safe-area-inset-left)); height: 90px; From 416310d302bb0ce9a9067b2b10daca4f19b09b23 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 19 Oct 2021 14:30:14 -0500 Subject: [PATCH 003/278] AccountHeader: make avatar transition less dramatic --- app/soapbox/features/account/components/header.js | 8 ++++++-- app/styles/components/account-header.scss | 8 ++++---- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/app/soapbox/features/account/components/header.js b/app/soapbox/features/account/components/header.js index 7c1e26954..ba8828809 100644 --- a/app/soapbox/features/account/components/header.js +++ b/app/soapbox/features/account/components/header.js @@ -397,7 +397,6 @@ class Header extends ImmutablePureComponent { const avatarSize = isSmallScreen ? 90 : 200; const deactivated = !account.getIn(['pleroma', 'is_active'], true); - const lockedIcon = account.get('locked') ? () : ''; const displayNameHtml = deactivated ? { __html: intl.formatMessage(messages.deactivated) } : { __html: account.get('display_name_html') }; const verified = account.getIn(['pleroma', 'tags'], ImmutableList()).includes('verified'); @@ -430,7 +429,12 @@ class Header extends ImmutablePureComponent { {verified && } {account.get('bot') && } - { @{getAcct(account, displayFqn)} {lockedIcon} } + + @{getAcct(account, displayFqn)} + {account.get('locked') && ( + + )} +
diff --git a/app/styles/components/account-header.scss b/app/styles/components/account-header.scss index db6efa887..731f0c19b 100644 --- a/app/styles/components/account-header.scss +++ b/app/styles/components/account-header.scss @@ -143,7 +143,7 @@ &.is-locked { .account__header__avatar { - top: -130px; + top: -20px; opacity: 0; animation: 0.3s fadeOut; animation-fill-mode: forwards; @@ -221,7 +221,7 @@ width: 265px; height: 74px; position: absolute; - top: 220px; + top: 100px; opacity: 0; animation: 0.3s fadeOut; animation-fill-mode: forwards; @@ -247,9 +247,9 @@ } small { - display: block; + display: flex; font-size: 14px; - line-height: 1.5; + color: var(--primary-text-color--faint); font-weight: 400; overflow: hidden; text-overflow: ellipsis; From 4bc3a0c7dc08b2fa6ad2f7cb699065d29f986163 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Tue, 26 Oct 2021 11:26:58 +0200 Subject: [PATCH 004/278] Styles, set focus to sidebar on open MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- app/soapbox/components/sidebar_menu.js | 4 ++++ app/styles/components/account-header.scss | 3 ++- app/styles/loading.scss | 3 ++- app/styles/navigation.scss | 5 +++++ 4 files changed, 13 insertions(+), 2 deletions(-) diff --git a/app/soapbox/components/sidebar_menu.js b/app/soapbox/components/sidebar_menu.js index 86d8af697..b5b90fdd1 100644 --- a/app/soapbox/components/sidebar_menu.js +++ b/app/soapbox/components/sidebar_menu.js @@ -141,6 +141,10 @@ class SidebarMenu extends ImmutablePureComponent { if (accountChanged || otherAccountsChanged) { this.fetchOwnAccounts(); } + + if (this.props.sidebarOpen && !prevProps.sidebarOpen) { + document.querySelector('.sidebar-menu__close').focus(); + } } renderAccount = account => { diff --git a/app/styles/components/account-header.scss b/app/styles/components/account-header.scss index 69902227c..b09ec1c65 100644 --- a/app/styles/components/account-header.scss +++ b/app/styles/components/account-header.scss @@ -202,7 +202,8 @@ padding: 7px; opacity: 0.6; - &:hover { + &:hover, + &:focus { opacity: 1; } diff --git a/app/styles/loading.scss b/app/styles/loading.scss index be819b892..32000e89e 100644 --- a/app/styles/loading.scss +++ b/app/styles/loading.scss @@ -173,7 +173,8 @@ clear: both; text-decoration: none; - &:hover { + &:hover, + &:focus { background: var(--brand-color--faint); } } diff --git a/app/styles/navigation.scss b/app/styles/navigation.scss index d4042d052..623f59de2 100644 --- a/app/styles/navigation.scss +++ b/app/styles/navigation.scss @@ -154,6 +154,11 @@ justify-content: center; color: var(--primary-text-color--faint); + &:hover, + &:focus { + color: var(--primary-text-color); + } + .svg-icon { margin-right: 7px; width: 22px; From 1796a35951f2bd7ca2a391d016ad5e706288dff2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Tue, 26 Oct 2021 17:38:49 +0200 Subject: [PATCH 005/278] Partially fix post navigation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- app/soapbox/components/column.js | 5 + app/soapbox/components/material_status.js | 6 +- app/soapbox/components/status.js | 135 +++++++++++----------- app/soapbox/components/status_list.js | 9 +- app/soapbox/features/status/index.js | 10 +- 5 files changed, 90 insertions(+), 75 deletions(-) diff --git a/app/soapbox/components/column.js b/app/soapbox/components/column.js index a8aec1542..1b5fabb05 100644 --- a/app/soapbox/components/column.js +++ b/app/soapbox/components/column.js @@ -11,6 +11,10 @@ export default class Column extends React.PureComponent { label: PropTypes.string, }; + setRef = c => { + this.node = c; + } + render() { const { className, label, children, transparent, ...rest } = this.props; @@ -20,6 +24,7 @@ export default class Column extends React.PureComponent { aria-label={label} className={classNames('column', className, { 'column--transparent': transparent })} {...rest} + ref={this.setRef} > {children}
diff --git a/app/soapbox/components/material_status.js b/app/soapbox/components/material_status.js index 6454497c5..9ff242507 100644 --- a/app/soapbox/components/material_status.js +++ b/app/soapbox/components/material_status.js @@ -4,24 +4,24 @@ import React from 'react'; import PropTypes from 'prop-types'; -import StatusContainer from 'soapbox/containers/status_container'; export default class MaterialStatus extends React.Component { static propTypes = { + children: PropTypes.node, hidden: PropTypes.bool, } render() { // Performance: when hidden, don't render the wrapper divs if (this.props.hidden) { - return ; + return this.props.children; } return (
- + {this.props.children}
); diff --git a/app/soapbox/components/status.js b/app/soapbox/components/status.js index de7a330ca..c13a4d6a1 100644 --- a/app/soapbox/components/status.js +++ b/app/soapbox/components/status.js @@ -95,6 +95,7 @@ class Status extends ImmutablePureComponent { displayMedia: PropTypes.string, allowedEmoji: ImmutablePropTypes.list, focusable: PropTypes.bool, + component: PropTypes.func, }; static defaultProps = { @@ -316,7 +317,7 @@ class Status extends ImmutablePureComponent { const poll = null; let statusAvatar, prepend, rebloggedByText, reblogContent; - const { intl, hidden, featured, otherAccounts, unread, showThread, group } = this.props; + const { intl, hidden, featured, otherAccounts, unread, showThread, group, wrapperComponent: WrapperComponent } = this.props; // FIXME: why does this need to reassign status and account?? let { status, account, ...other } = this.props; // eslint-disable-line prefer-const @@ -494,72 +495,76 @@ class Status extends ImmutablePureComponent { const favicon = status.getIn(['account', 'pleroma', 'favicon']); const domain = getDomain(status.get('account')); + const wrappedStatus = ( +
+ {prepend} + +
+
+
+ + + + + {favicon && +
+ + + +
} + +
+
+ + + {statusAvatar} + + +
+ + + +
+
+ + {!group && status.get('group') && ( +
+ Posted in {status.getIn(['group', 'title'])} +
+ )} + + + + {media} + {poll} + + {showThread && status.get('in_reply_to_id') && status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) && ( + + )} + + +
+
+ ); + return ( -
- {prepend} - -
-
-
- - - - - {favicon && -
- - - -
} - -
-
- - - {statusAvatar} - - -
- - - -
-
- - {!group && status.get('group') && ( -
- Posted in {status.getIn(['group', 'title'])} -
- )} - - - - {media} - {poll} - - {showThread && status.get('in_reply_to_id') && status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) && ( - - )} - - -
-
+ {WrapperComponent ? {wrappedStatus} : wrappedStatus} ); } diff --git a/app/soapbox/components/status_list.js b/app/soapbox/components/status_list.js index a186a8073..a48f63e74 100644 --- a/app/soapbox/components/status_list.js +++ b/app/soapbox/components/status_list.js @@ -3,6 +3,7 @@ import React from 'react'; import { FormattedMessage, defineMessages } from 'react-intl'; import ImmutablePropTypes from 'react-immutable-proptypes'; import PropTypes from 'prop-types'; +import StatusContainer from 'soapbox/containers/status_container'; import MaterialStatus from 'soapbox/components/material_status'; import PendingStatus from 'soapbox/features/ui/components/pending_status'; import ImmutablePureComponent from 'react-immutable-pure-component'; @@ -110,7 +111,7 @@ export default class StatusList extends ImmutablePureComponent { const { timelineId, withGroupAdmin, group } = this.props; return ( - ); } @@ -150,7 +153,7 @@ export default class StatusList extends ImmutablePureComponent { if (!featuredStatusIds) return null; return featuredStatusIds.map(statusId => ( - )); } diff --git a/app/soapbox/features/status/index.js b/app/soapbox/features/status/index.js index 79c5d32d1..9a3bcbb5c 100644 --- a/app/soapbox/features/status/index.js +++ b/app/soapbox/features/status/index.js @@ -1,4 +1,4 @@ -import { OrderedSet as ImmutableOrderedSet } from 'immutable'; +import { List as ImmutableList, OrderedSet as ImmutableOrderedSet } from 'immutable'; import React from 'react'; import { connect } from 'react-redux'; import PropTypes from 'prop-types'; @@ -410,10 +410,10 @@ class Status extends ImmutablePureComponent { if (id === status.get('id')) { this._selectChild(ancestorsIds.size - 1, true); } else { - let index = ancestorsIds.indexOf(id); + let index = ImmutableList(ancestorsIds).indexOf(id); if (index === -1) { - index = descendantsIds.indexOf(id); + index = ImmutableList(descendantsIds).indexOf(id); this._selectChild(ancestorsIds.size + index, true); } else { this._selectChild(index - 1, true); @@ -427,10 +427,10 @@ class Status extends ImmutablePureComponent { if (id === status.get('id')) { this._selectChild(ancestorsIds.size + 1, false); } else { - let index = ancestorsIds.indexOf(id); + let index = ImmutableList(ancestorsIds).indexOf(id); if (index === -1) { - index = descendantsIds.indexOf(id); + index = ImmutableList(descendantsIds).indexOf(id); this._selectChild(ancestorsIds.size + index + 2, false); } else { this._selectChild(index + 1, false); From cb98c160119f91ff56767159c20a221a8667155a Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 5 Nov 2021 23:29:35 -0500 Subject: [PATCH 006/278] Poll: fix crash when voting --- app/soapbox/components/poll.js | 8 ++++++-- app/soapbox/containers/poll_container.js | 9 +-------- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/app/soapbox/components/poll.js b/app/soapbox/components/poll.js index 3f10cfee1..7962ee779 100644 --- a/app/soapbox/components/poll.js +++ b/app/soapbox/components/poll.js @@ -12,6 +12,7 @@ import escapeTextContentForBrowser from 'escape-html'; import emojify from 'soapbox/features/emoji/emoji'; import RelativeTimestamp from './relative_timestamp'; import Icon from 'soapbox/components/icon'; +import { openModal } from 'soapbox/actions/modal'; const messages = defineMessages({ closed: { id: 'poll.closed', defaultMessage: 'Closed' }, @@ -33,7 +34,6 @@ class Poll extends ImmutablePureComponent { dispatch: PropTypes.func, disabled: PropTypes.bool, me: SoapboxPropTypes.me, - onOpenUnauthorizedModal: PropTypes.func.isRequired, }; state = { @@ -56,7 +56,7 @@ class Poll extends ImmutablePureComponent { this.setState({ selected: tmp }); } } else { - this.props.onOpenUnauthorizedModal(); + this.openUnauthorizedModal(); } } @@ -80,6 +80,10 @@ class Poll extends ImmutablePureComponent { this.props.dispatch(vote(this.props.poll.get('id'), Object.keys(this.state.selected))); }; + openUnauthorizedModal = () => { + this.props.dispatch(openModal('UNAUTHORIZED')); + } + handleRefresh = () => { if (this.props.disabled) { return; diff --git a/app/soapbox/containers/poll_container.js b/app/soapbox/containers/poll_container.js index 55f14e1b2..dc35964f5 100644 --- a/app/soapbox/containers/poll_container.js +++ b/app/soapbox/containers/poll_container.js @@ -1,5 +1,4 @@ import { connect } from 'react-redux'; -import { openModal } from 'soapbox/actions/modal'; import Poll from 'soapbox/components/poll'; const mapStateToProps = (state, { pollId }) => ({ @@ -7,10 +6,4 @@ const mapStateToProps = (state, { pollId }) => ({ me: state.get('me'), }); -const mapDispatchToProps = (dispatch) => ({ - onOpenUnauthorizedModal() { - dispatch(openModal('UNAUTHORIZED')); - }, -}); - -export default connect(mapStateToProps, mapDispatchToProps)(Poll); +export default connect(mapStateToProps)(Poll); From 187d579be0f4f4d84083ab7bc9c81bc124d5e11d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Sat, 6 Nov 2021 11:26:50 +0100 Subject: [PATCH 007/278] Key navigation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- app/soapbox/components/scrollable_list.js | 2 +- .../notifications/components/notification.js | 2 +- app/styles/components/status.scss | 15 +++++++-------- 3 files changed, 9 insertions(+), 10 deletions(-) diff --git a/app/soapbox/components/scrollable_list.js b/app/soapbox/components/scrollable_list.js index 04ceab234..9c7da2f31 100644 --- a/app/soapbox/components/scrollable_list.js +++ b/app/soapbox/components/scrollable_list.js @@ -20,7 +20,7 @@ const mapStateToProps = state => { }; }; -export default @connect(mapStateToProps) +export default @connect(mapStateToProps, null, null, { forwardRef: true }) class ScrollableList extends PureComponent { static contextTypes = { diff --git a/app/soapbox/features/notifications/components/notification.js b/app/soapbox/features/notifications/components/notification.js index 18d24cd04..9fc3300a8 100644 --- a/app/soapbox/features/notifications/components/notification.js +++ b/app/soapbox/features/notifications/components/notification.js @@ -174,7 +174,7 @@ class Notification extends ImmutablePureComponent { renderMention(notification) { return ( -
+
Date: Sun, 7 Nov 2021 10:22:08 +0100 Subject: [PATCH 008/278] Use warning color for delete actions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- app/soapbox/components/dropdown_menu.js | 5 +- app/soapbox/components/status_action_bar.js | 6 ++ .../admin/components/report_status.js | 1 + .../chats/components/chat_message_list.js | 1 + .../groups/timeline/components/header.js | 1 + .../features/status/components/action_bar.js | 4 ++ .../features/ui/components/actions_modal.js | 4 +- app/styles/components/dropdown-menu.scss | 67 +++++++++++-------- app/styles/components/modal.scss | 5 ++ 9 files changed, 61 insertions(+), 33 deletions(-) diff --git a/app/soapbox/components/dropdown_menu.js b/app/soapbox/components/dropdown_menu.js index 43670621b..456ce8858 100644 --- a/app/soapbox/components/dropdown_menu.js +++ b/app/soapbox/components/dropdown_menu.js @@ -1,4 +1,5 @@ import React from 'react'; +import classNames from 'classnames'; import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; import IconButton from './icon_button'; @@ -147,10 +148,10 @@ class DropdownMenu extends React.PureComponent { return
  • ; } - const { text, href, to, newTab, isLogout, icon } = option; + const { text, href, to, newTab, isLogout, icon, type } = option; return ( -
  • +
  • ; } - const { icon = null, text, meta = null, active = false, href = '#', isLogout } = action; + const { icon = null, text, meta = null, active = false, href = '#', isLogout, type } = action; return (
  • @@ -37,7 +37,7 @@ class ActionsModal extends ImmutablePureComponent { rel='noopener' onClick={this.props.onClick} data-index={i} - className={classNames({ active })} + className={classNames({ active, warning: type === 'warning' })} data-method={isLogout ? 'delete' : null} > {icon && } diff --git a/app/styles/components/dropdown-menu.scss b/app/styles/components/dropdown-menu.scss index 39dd1772e..fb07186cf 100644 --- a/app/styles/components/dropdown-menu.scss +++ b/app/styles/components/dropdown-menu.scss @@ -55,39 +55,48 @@ padding: 6px 0; } - &__item a { - display: flex; - align-items: center; - box-sizing: border-box; - overflow: hidden; - padding: 4px 10px; - font-size: 15px; - text-decoration: none; - text-overflow: ellipsis; - white-space: nowrap; - color: var(--primary-text-color); - - &:focus, - &:hover, - &:active { - outline: 0; - color: #fff; - background: var(--brand-color) !important; - - * { - color: #fff; - } + &__item { + a { + display: flex; + align-items: center; + box-sizing: border-box; + overflow: hidden; + padding: 4px 10px; + font-size: 15px; + text-decoration: none; + text-overflow: ellipsis; + white-space: nowrap; + color: var(--primary-text-color); } - .svg-icon:first-child { - height: 20px; - width: 20px; - margin-right: 10px; - transition: none; + &.warning a { + color: var(--warning-color); + } - svg { - stroke-width: 1.5; + a { + &:focus, + &:hover, + &:active { + outline: 0; + color: #fff; + background: var(--brand-color) !important; + + * { + color: #fff; + } + } + + .svg-icon:first-child { + height: 20px; + width: 20px; + min-width: 20px; + margin-right: 10px; transition: none; + + svg { + stroke-width: 1.5; + transition: none; + } } } } diff --git a/app/styles/components/modal.scss b/app/styles/components/modal.scss index cf77960c5..c1019701f 100644 --- a/app/styles/components/modal.scss +++ b/app/styles/components/modal.scss @@ -581,6 +581,11 @@ transition: none; } + &.warning { + color: var(--warning-color); + opacity: 1; + } + &.active, &:hover, &:focus { From 0549c365e5a8a63c1ead03ae19e1cbefad222a4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Sun, 7 Nov 2021 10:34:02 +0100 Subject: [PATCH 009/278] Use high contrast color MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- app/styles/components/dropdown-menu.scss | 2 +- app/styles/components/modal.scss | 2 +- app/styles/themes.scss | 10 ++++++++++ 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/app/styles/components/dropdown-menu.scss b/app/styles/components/dropdown-menu.scss index fb07186cf..c36cdef12 100644 --- a/app/styles/components/dropdown-menu.scss +++ b/app/styles/components/dropdown-menu.scss @@ -70,7 +70,7 @@ } &.warning a { - color: var(--warning-color); + color: var(--warning-color--hicontrast); } a { diff --git a/app/styles/components/modal.scss b/app/styles/components/modal.scss index c1019701f..e0a11c71b 100644 --- a/app/styles/components/modal.scss +++ b/app/styles/components/modal.scss @@ -582,7 +582,7 @@ } &.warning { - color: var(--warning-color); + color: var(--warning-color--hicontrast); opacity: 1; } diff --git a/app/styles/themes.scss b/app/styles/themes.scss index e1d328b4f..032f8ce4d 100644 --- a/app/styles/themes.scss +++ b/app/styles/themes.scss @@ -88,6 +88,11 @@ body, var(--brand-color_s), calc(var(--brand-color_l) - 12%) ); + --warning-color--hicontrast: hsl( + var(--warning-color_h), + var(--warning-color_s), + calc(var(--warning-color_l) - 12%) + ); } .theme-mode-dark { @@ -119,4 +124,9 @@ body, var(--brand-color_s), calc(var(--brand-color_l) + 12%) ); + --warning-color--hicontrast: hsl( + var(--warning-color_h), + var(--warning-color_s), + calc(var(--warning-color_l) + 12%) + ); } From d25354013a7160cc4baefb3ea47c27162cd3d1d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Mon, 8 Nov 2021 17:21:33 +0100 Subject: [PATCH 010/278] Use .destructive MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- app/soapbox/components/dropdown_menu.js | 4 ++-- app/soapbox/components/status_action_bar.js | 12 ++++++------ .../features/admin/components/report_status.js | 2 +- .../features/chats/components/chat_message_list.js | 2 +- .../features/groups/timeline/components/header.js | 2 +- app/soapbox/features/status/components/action_bar.js | 8 ++++---- app/soapbox/features/ui/components/actions_modal.js | 4 ++-- app/styles/components/dropdown-menu.scss | 2 +- app/styles/components/modal.scss | 2 +- 9 files changed, 19 insertions(+), 19 deletions(-) diff --git a/app/soapbox/components/dropdown_menu.js b/app/soapbox/components/dropdown_menu.js index 456ce8858..868e42066 100644 --- a/app/soapbox/components/dropdown_menu.js +++ b/app/soapbox/components/dropdown_menu.js @@ -148,10 +148,10 @@ class DropdownMenu extends React.PureComponent { return
  • ; } - const { text, href, to, newTab, isLogout, icon, type } = option; + const { text, href, to, newTab, isLogout, icon, destructive } = option; return ( -
  • +
  • ; } - const { icon = null, text, meta = null, active = false, href = '#', isLogout, type } = action; + const { icon = null, text, meta = null, active = false, href = '#', isLogout, destructive } = action; return (
  • @@ -37,7 +37,7 @@ class ActionsModal extends ImmutablePureComponent { rel='noopener' onClick={this.props.onClick} data-index={i} - className={classNames({ active, warning: type === 'warning' })} + className={classNames({ active, destructive })} data-method={isLogout ? 'delete' : null} > {icon && } diff --git a/app/styles/components/dropdown-menu.scss b/app/styles/components/dropdown-menu.scss index c36cdef12..7f2dd870c 100644 --- a/app/styles/components/dropdown-menu.scss +++ b/app/styles/components/dropdown-menu.scss @@ -69,7 +69,7 @@ color: var(--primary-text-color); } - &.warning a { + &.destructive a { color: var(--warning-color--hicontrast); } diff --git a/app/styles/components/modal.scss b/app/styles/components/modal.scss index e0a11c71b..56e8194ca 100644 --- a/app/styles/components/modal.scss +++ b/app/styles/components/modal.scss @@ -581,7 +581,7 @@ transition: none; } - &.warning { + &.destructive { color: var(--warning-color--hicontrast); opacity: 1; } From 109046eef82a3556d02def1da3976d381afb337a Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Mon, 8 Nov 2021 15:57:37 -0600 Subject: [PATCH 011/278] RegistrationForm: validate password mismatch --- .../components/registration_form.js | 59 +++++++++++++++++-- app/soapbox/features/forms/index.js | 7 ++- app/styles/forms.scss | 12 ++-- 3 files changed, 66 insertions(+), 12 deletions(-) diff --git a/app/soapbox/features/auth_login/components/registration_form.js b/app/soapbox/features/auth_login/components/registration_form.js index f8da31be1..86e5812ff 100644 --- a/app/soapbox/features/auth_login/components/registration_form.js +++ b/app/soapbox/features/auth_login/components/registration_form.js @@ -63,6 +63,8 @@ class RegistrationForm extends ImmutablePureComponent { submissionLoading: false, params: ImmutableMap(), captchaIdempotencyKey: uuidv4(), + passwordConfirmation: '', + passwordMismatch: false, } setParams = map => { @@ -77,6 +79,30 @@ class RegistrationForm extends ImmutablePureComponent { this.setParams({ [e.target.name]: e.target.checked }); } + onPasswordChange = e => { + const password = e.target.value; + const { passwordConfirmation } = this.state; + this.onInputChange(e); + + if (password === passwordConfirmation) { + this.setState({ passwordMismatch: false }); + } + } + + onPasswordConfirmChange = e => { + const password = this.state.params.get('password'); + const passwordConfirmation = e.target.value; + this.setState({ passwordConfirmation }); + + if (password === passwordConfirmation) { + this.setState({ passwordMismatch: false }); + } + } + + onPasswordConfirmBlur = e => { + this.setState({ passwordMismatch: !this.passwordsMatch() }); + } + launchModal = () => { const { dispatch, intl, needsConfirmation, needsApproval } = this.props; @@ -113,9 +139,19 @@ class RegistrationForm extends ImmutablePureComponent { } } + passwordsMatch = () => { + const { params, passwordConfirmation } = this.state; + return params.get('password', '') === passwordConfirmation; + } + onSubmit = e => { const { dispatch, inviteToken } = this.props; + if (!this.passwordsMatch()) { + this.setState({ passwordMismatch: true }); + return; + } + const params = this.state.params.withMutations(params => { // Locale for confirmation email params.set('locale', this.props.locale); @@ -159,7 +195,7 @@ class RegistrationForm extends ImmutablePureComponent { render() { const { instance, intl, supportsEmailList } = this.props; - const { params } = this.state; + const { params, passwordConfirmation, passwordMismatch } = this.state; const isLoading = this.state.captchaLoading || this.state.submissionLoading; return ( @@ -176,6 +212,7 @@ class RegistrationForm extends ImmutablePureComponent { autoCapitalize='off' pattern='^[a-zA-Z\d_-]+' onChange={this.onInputChange} + value={params.get('username', '')} required /> + {passwordMismatch && ( +
    + +
    + )} {instance.get('approval_required') && @@ -215,6 +263,7 @@ class RegistrationForm extends ImmutablePureComponent { name='reason' maxLength={500} onChange={this.onInputChange} + value={params.get('reason', '')} required />}
  • @@ -232,12 +281,14 @@ class RegistrationForm extends ImmutablePureComponent { label={intl.formatMessage(messages.agreement, { tos: {intl.formatMessage(messages.tos)} })} name='agreement' onChange={this.onCheckboxChange} + checked={params.get('agreement', false)} required /> {supportsEmailList && }
    diff --git a/app/soapbox/features/forms/index.js b/app/soapbox/features/forms/index.js index 3273a0d4e..816a5e61a 100644 --- a/app/soapbox/features/forms/index.js +++ b/app/soapbox/features/forms/index.js @@ -18,6 +18,7 @@ export const InputContainer = (props) => { 'with_label': props.label, 'required': props.required, 'boolean': props.type === 'checkbox', + 'field_with_errors': props.error, }, props.extraClass); return ( @@ -35,6 +36,7 @@ InputContainer.propTypes = { type: PropTypes.string, children: PropTypes.node, extraClass: PropTypes.string, + error: PropTypes.bool, }; export const LabelInputContainer = ({ label, hint, children, ...props }) => { @@ -87,10 +89,11 @@ export class SimpleInput extends ImmutablePureComponent { static propTypes = { label: FormPropTypes.label, hint: PropTypes.node, + error: PropTypes.bool, } render() { - const { hint, ...props } = this.props; + const { hint, error, ...props } = this.props; const Input = this.props.label ? LabelInput : 'input'; return ( @@ -164,7 +167,7 @@ FieldsGroup.propTypes = { }; export const Checkbox = props => ( - + ); export class RadioGroup extends ImmutablePureComponent { diff --git a/app/styles/forms.scss b/app/styles/forms.scss index 05bcbc0a4..b28676a32 100644 --- a/app/styles/forms.scss +++ b/app/styles/forms.scss @@ -371,13 +371,13 @@ code { select { border-color: lighten($error-red, 12%); } + } - .error { - display: block; - font-weight: 500; - color: lighten($error-red, 12%); - margin-top: 4px; - } + .error { + display: block; + font-weight: 500; + color: lighten($error-red, 12%); + margin-top: 4px; } .input.disabled { From 5a767c8960bc025a5bcdba9e8a27d0459f3f961f Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Mon, 8 Nov 2021 16:08:25 -0600 Subject: [PATCH 012/278] Jest: update snapshot --- .../features/forms/__tests__/__snapshots__/forms-test.js.snap | 1 - 1 file changed, 1 deletion(-) diff --git a/app/soapbox/features/forms/__tests__/__snapshots__/forms-test.js.snap b/app/soapbox/features/forms/__tests__/__snapshots__/forms-test.js.snap index a1ad8c210..9eb7b4358 100644 --- a/app/soapbox/features/forms/__tests__/__snapshots__/forms-test.js.snap +++ b/app/soapbox/features/forms/__tests__/__snapshots__/forms-test.js.snap @@ -6,7 +6,6 @@ exports[` renders correctly 1`] = ` >
    `; From def3c542c05f646931fc975efaab687c43ebd628 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Mon, 8 Nov 2021 16:27:32 -0600 Subject: [PATCH 013/278] RegistrationForm: prevent small error with password matching --- app/soapbox/features/auth_login/components/registration_form.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/soapbox/features/auth_login/components/registration_form.js b/app/soapbox/features/auth_login/components/registration_form.js index 86e5812ff..a4c52ef62 100644 --- a/app/soapbox/features/auth_login/components/registration_form.js +++ b/app/soapbox/features/auth_login/components/registration_form.js @@ -90,7 +90,7 @@ class RegistrationForm extends ImmutablePureComponent { } onPasswordConfirmChange = e => { - const password = this.state.params.get('password'); + const password = this.state.params.get('password', ''); const passwordConfirmation = e.target.value; this.setState({ passwordConfirmation }); From 4c1b3dd88b30dbd916fd13fded949b9bf3739694 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 12 Nov 2021 11:48:43 -0600 Subject: [PATCH 014/278] ThumbNavigation: balance the width of the icons so the labels overflow --- app/styles/navigation.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/app/styles/navigation.scss b/app/styles/navigation.scss index f590cbd5c..e53d1383b 100644 --- a/app/styles/navigation.scss +++ b/app/styles/navigation.scss @@ -87,6 +87,7 @@ color: var(--primary-text-color); text-decoration: none; font-size: 20px; + width: 55px; span { margin-top: 1px; From a354fd325db5569a96cf8134f77adacde5cf5e83 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 12 Nov 2021 12:18:11 -0600 Subject: [PATCH 015/278] Statuses: optimistic reply counter --- .../reducers/__tests__/statuses-test.js | 35 ++++++++++++++++++- app/soapbox/reducers/statuses.js | 22 ++++++++++++ 2 files changed, 56 insertions(+), 1 deletion(-) diff --git a/app/soapbox/reducers/__tests__/statuses-test.js b/app/soapbox/reducers/__tests__/statuses-test.js index 00687224a..7bcc67c9e 100644 --- a/app/soapbox/reducers/__tests__/statuses-test.js +++ b/app/soapbox/reducers/__tests__/statuses-test.js @@ -1,8 +1,41 @@ import reducer from '../statuses'; -import { Map as ImmutableMap } from 'immutable'; +import { Map as ImmutableMap, fromJS } from 'immutable'; +import { + STATUS_CREATE_REQUEST, + STATUS_CREATE_FAIL, +} from 'soapbox/actions/statuses'; + describe('statuses reducer', () => { it('should return the initial state', () => { expect(reducer(undefined, {})).toEqual(ImmutableMap()); }); + + describe('STATUS_CREATE_REQUEST', () => { + it('increments the replies_count of its parent', () => { + const state = fromJS({ '123': { replies_count: 4 } }); + + const action = { + type: STATUS_CREATE_REQUEST, + params: { in_reply_to_id: '123' }, + }; + + const result = reducer(state, action).getIn(['123', 'replies_count']); + expect(result).toEqual(5); + }); + }); + + describe('STATUS_CREATE_FAIL', () => { + it('decrements the replies_count of its parent', () => { + const state = fromJS({ '123': { replies_count: 5 } }); + + const action = { + type: STATUS_CREATE_FAIL, + params: { in_reply_to_id: '123' }, + }; + + const result = reducer(state, action).getIn(['123', 'replies_count']); + expect(result).toEqual(4); + }); + }); }); diff --git a/app/soapbox/reducers/statuses.js b/app/soapbox/reducers/statuses.js index f8c01ed2b..118d2d7aa 100644 --- a/app/soapbox/reducers/statuses.js +++ b/app/soapbox/reducers/statuses.js @@ -6,6 +6,8 @@ import { FAVOURITE_FAIL, } from '../actions/interactions'; import { + STATUS_CREATE_REQUEST, + STATUS_CREATE_FAIL, STATUS_MUTE_SUCCESS, STATUS_UNMUTE_SUCCESS, STATUS_REVEAL, @@ -33,6 +35,22 @@ const deleteStatus = (state, id, references) => { return state.delete(id); }; +const importPendingStatus = (state, { in_reply_to_id }) => { + if (in_reply_to_id) { + return state.updateIn([in_reply_to_id, 'replies_count'], 0, count => count + 1); + } else { + return state; + } +}; + +const deletePendingStatus = (state, { in_reply_to_id }) => { + if (in_reply_to_id) { + return state.updateIn([in_reply_to_id, 'replies_count'], 0, count => Math.max(0, count - 1)); + } else { + return state; + } +}; + const initialState = ImmutableMap(); export default function statuses(state = initialState, action) { @@ -41,6 +59,10 @@ export default function statuses(state = initialState, action) { return importStatus(state, action.status); case STATUSES_IMPORT: return importStatuses(state, action.statuses); + case STATUS_CREATE_REQUEST: + return importPendingStatus(state, action.params); + case STATUS_CREATE_FAIL: + return deletePendingStatus(state, action.params); case FAVOURITE_REQUEST: return state.update(action.status.get('id'), status => status From 47a3ecc30e61f0f8522b5f3fe306d30ff963d330 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 12 Nov 2021 12:28:58 -0600 Subject: [PATCH 016/278] ComposeModal: conditional title when replying --- .../features/ui/components/compose_modal.js | 22 ++++++++++++++----- 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/app/soapbox/features/ui/components/compose_modal.js b/app/soapbox/features/ui/components/compose_modal.js index 9b24f9429..658398626 100644 --- a/app/soapbox/features/ui/components/compose_modal.js +++ b/app/soapbox/features/ui/components/compose_modal.js @@ -20,6 +20,7 @@ const mapStateToProps = state => { account: state.getIn(['accounts', me]), composeText: state.getIn(['compose', 'text']), privacy: state.getIn(['compose', 'privacy']), + inReplyTo: state.getIn(['compose', 'in_reply_to']), }; }; @@ -31,6 +32,7 @@ class ComposeModal extends ImmutablePureComponent { onClose: PropTypes.func.isRequired, composeText: PropTypes.string, privacy: PropTypes.string, + inReplyTo: PropTypes.string, dispatch: PropTypes.func.isRequired, }; @@ -49,18 +51,26 @@ class ComposeModal extends ImmutablePureComponent { } }; + renderTitle = () => { + const { privacy, inReplyTo } = this.props; + + if (privacy === 'direct') { + return ; + } else if (inReplyTo) { + return ; + } else { + return ; + } + } + render() { - const { intl, privacy } = this.props; + const { intl } = this.props; return (

    - {privacy === 'direct' ? ( - - ) : ( - - )} + {this.renderTitle()}

    Date: Fri, 12 Nov 2021 12:47:36 -0600 Subject: [PATCH 017/278] SubNavigation: increase Back button size, constrain message width --- app/styles/navigation.scss | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/app/styles/navigation.scss b/app/styles/navigation.scss index e53d1383b..035cc55f8 100644 --- a/app/styles/navigation.scss +++ b/app/styles/navigation.scss @@ -156,19 +156,24 @@ justify-content: center; color: var(--primary-text-color); opacity: 0.6; + font-size: 16px; .svg-icon { margin-right: 7px; - width: 22px; - height: 22px; + width: 26px; + height: 26px; } } &__message { position: absolute; + padding: 0 10px; align-self: center; justify-self: center; font-weight: bold; + overflow-x: hidden; + text-overflow: ellipsis; + max-width: calc(100vw - 200px); } &__cog { From 1e603b82552f82b56a8488acbea7f5a43b420cc1 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 12 Nov 2021 13:39:06 -0600 Subject: [PATCH 018/278] Interactions: don't reimport updated status with unfavourite and unreblog. Mastodon doesn't decrement the counter in the API response, and we actually don't want updated counters anyway. --- app/soapbox/actions/interactions.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/soapbox/actions/interactions.js b/app/soapbox/actions/interactions.js index 1e2d729f1..c292abaac 100644 --- a/app/soapbox/actions/interactions.js +++ b/app/soapbox/actions/interactions.js @@ -77,7 +77,6 @@ export function unreblog(status) { dispatch(unreblogRequest(status)); api(getState).post(`/api/v1/statuses/${status.get('id')}/unreblog`).then(response => { - dispatch(importFetchedStatus(response.data)); dispatch(unreblogSuccess(status)); }).catch(error => { dispatch(unreblogFail(status, error)); @@ -157,7 +156,6 @@ export function unfavourite(status) { dispatch(unfavouriteRequest(status)); api(getState).post(`/api/v1/statuses/${status.get('id')}/unfavourite`).then(response => { - dispatch(importFetchedStatus(response.data)); dispatch(unfavouriteSuccess(status)); }).catch(error => { dispatch(unfavouriteFail(status, error)); From 59db8fb442371252c3e22b86e67dc2d802eaedba Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 12 Nov 2021 14:39:25 -0600 Subject: [PATCH 019/278] Notifications: drop status notifications if status is null --- .../reducers/__tests__/notifications-test.js | 163 +++++++++++------- app/soapbox/reducers/notifications.js | 4 +- 2 files changed, 106 insertions(+), 61 deletions(-) diff --git a/app/soapbox/reducers/__tests__/notifications-test.js b/app/soapbox/reducers/__tests__/notifications-test.js index 581c4b38e..dbaeff40a 100644 --- a/app/soapbox/reducers/__tests__/notifications-test.js +++ b/app/soapbox/reducers/__tests__/notifications-test.js @@ -1,7 +1,18 @@ -import * as actions from 'soapbox/actions/notifications'; +import { + NOTIFICATIONS_EXPAND_SUCCESS, + NOTIFICATIONS_EXPAND_REQUEST, + NOTIFICATIONS_EXPAND_FAIL, + NOTIFICATIONS_FILTER_SET, + NOTIFICATIONS_SCROLL_TOP, + NOTIFICATIONS_UPDATE, + NOTIFICATIONS_UPDATE_QUEUE, + NOTIFICATIONS_DEQUEUE, + NOTIFICATIONS_CLEAR, + NOTIFICATIONS_MARK_READ_REQUEST, +} from 'soapbox/actions/notifications'; import reducer from '../notifications'; import notifications from 'soapbox/__fixtures__/notifications.json'; -import { Map as ImmutableMap, OrderedMap as ImmutableOrderedMap } from 'immutable'; +import { Map as ImmutableMap, OrderedMap as ImmutableOrderedMap, fromJS } from 'immutable'; import { take } from 'lodash'; import { ACCOUNT_BLOCK_SUCCESS, ACCOUNT_MUTE_SUCCESS } from 'soapbox/actions/accounts'; import notification from 'soapbox/__fixtures__/notification.json'; @@ -28,55 +39,89 @@ describe('notifications reducer', () => { })); }); - it('should handle NOTIFICATIONS_EXPAND_SUCCESS', () => { - const state = undefined; - const action = { - type: actions.NOTIFICATIONS_EXPAND_SUCCESS, - notifications: take(notifications, 3), - next: null, - skipLoading: true, - }; - expect(reducer(state, action)).toEqual(ImmutableMap({ - items: ImmutableOrderedMap([ - ['10744', ImmutableMap({ - id: '10744', - type: 'pleroma:emoji_reaction', - account: '9vMAje101ngtjlMj7w', + describe('NOTIFICATIONS_EXPAND_SUCCESS', () => { + it('imports the notifications', () => { + const state = undefined; + + const action = { + type: NOTIFICATIONS_EXPAND_SUCCESS, + notifications: take(notifications, 3), + next: null, + skipLoading: true, + }; + + expect(reducer(state, action)).toEqual(ImmutableMap({ + items: ImmutableOrderedMap([ + ['10744', ImmutableMap({ + id: '10744', + type: 'pleroma:emoji_reaction', + account: '9vMAje101ngtjlMj7w', + target: null, + created_at: '2020-06-10T02:54:39.000Z', + status: '9vvNxoo5EFbbnfdXQu', + emoji: '😢', + chat_message: undefined, + })], + ['10743', ImmutableMap({ + id: '10743', + type: 'favourite', + account: '9v5c6xSEgAi3Zu1Lv6', + target: null, + created_at: '2020-06-10T02:51:05.000Z', + status: '9vvNxoo5EFbbnfdXQu', + emoji: undefined, + chat_message: undefined, + })], + ['10741', ImmutableMap({ + id: '10741', + type: 'favourite', + account: '9v5cKMOPGqPcgfcWp6', + target: null, + created_at: '2020-06-10T02:05:06.000Z', + status: '9vvNxoo5EFbbnfdXQu', + emoji: undefined, + chat_message: undefined, + })], + ]), + hasMore: false, + top: false, + unread: 0, + isLoading: false, + queuedNotifications: ImmutableOrderedMap(), + totalQueuedNotificationsCount: 0, + lastRead: -1, + })); + }); + + it('drops invalid notifications', () => { + const action = { + type: NOTIFICATIONS_EXPAND_SUCCESS, + notifications: [ + { id: '1', type: 'mention', status: null, account: { id: '10' } }, + { id: '2', type: 'reblog', status: null, account: { id: '9' } }, + { id: '3', type: 'favourite', status: null, account: { id: '8' } }, + { id: '4', type: 'mention', status: { id: 'a' }, account: { id: '7' } }, + { id: '5', type: 'reblog', status: { id: 'b' }, account: null }, + ], + next: null, + skipLoading: true, + }; + + const expected = ImmutableOrderedMap([ + ['4', fromJS({ + id: '4', + type: 'mention', + account: '7', target: null, - created_at: '2020-06-10T02:54:39.000Z', - status: '9vvNxoo5EFbbnfdXQu', - emoji: '😢', - chat_message: undefined, - })], - ['10743', ImmutableMap({ - id: '10743', - type: 'favourite', - account: '9v5c6xSEgAi3Zu1Lv6', - target: null, - created_at: '2020-06-10T02:51:05.000Z', - status: '9vvNxoo5EFbbnfdXQu', + created_at: undefined, + status: 'a', emoji: undefined, chat_message: undefined, })], - ['10741', ImmutableMap({ - id: '10741', - type: 'favourite', - account: '9v5cKMOPGqPcgfcWp6', - target: null, - created_at: '2020-06-10T02:05:06.000Z', - status: '9vvNxoo5EFbbnfdXQu', - emoji: undefined, - chat_message: undefined, - })], - ]), - hasMore: false, - top: false, - unread: 0, - isLoading: false, - queuedNotifications: ImmutableOrderedMap(), - totalQueuedNotificationsCount: 0, - lastRead: -1, - })); + ]); + + expect(reducer(undefined, action).get('items')).toEqual(expected); + }); }); it('should handle NOTIFICATIONS_EXPAND_REQUEST', () => { @@ -84,7 +129,7 @@ describe('notifications reducer', () => { isLoading: false, }); const action = { - type: actions.NOTIFICATIONS_EXPAND_REQUEST, + type: NOTIFICATIONS_EXPAND_REQUEST, }; expect(reducer(state, action)).toEqual(ImmutableMap({ isLoading: true, @@ -96,7 +141,7 @@ describe('notifications reducer', () => { isLoading: true, }); const action = { - type: actions.NOTIFICATIONS_EXPAND_FAIL, + type: NOTIFICATIONS_EXPAND_FAIL, }; expect(reducer(state, action)).toEqual(ImmutableMap({ isLoading: false, @@ -146,7 +191,7 @@ describe('notifications reducer', () => { lastRead: -1, }); const action = { - type: actions.NOTIFICATIONS_FILTER_SET, + type: NOTIFICATIONS_FILTER_SET, }; expect(reducer(state, action)).toEqual(ImmutableMap({ items: ImmutableOrderedMap(), @@ -165,7 +210,7 @@ describe('notifications reducer', () => { unread: 1, }); const action = { - type: actions.NOTIFICATIONS_SCROLL_TOP, + type: NOTIFICATIONS_SCROLL_TOP, top: true, }; expect(reducer(state, action)).toEqual(ImmutableMap({ @@ -179,7 +224,7 @@ describe('notifications reducer', () => { unread: 3, }); const action = { - type: actions.NOTIFICATIONS_SCROLL_TOP, + type: NOTIFICATIONS_SCROLL_TOP, top: false, }; expect(reducer(state, action)).toEqual(ImmutableMap({ @@ -195,7 +240,7 @@ describe('notifications reducer', () => { unread: 1, }); const action = { - type: actions.NOTIFICATIONS_UPDATE, + type: NOTIFICATIONS_UPDATE, notification: notification, }; expect(reducer(state, action)).toEqual(ImmutableMap({ @@ -223,7 +268,7 @@ describe('notifications reducer', () => { totalQueuedNotificationsCount: 0, }); const action = { - type: actions.NOTIFICATIONS_UPDATE_QUEUE, + type: NOTIFICATIONS_UPDATE_QUEUE, notification: notification, intlMessages: intlMessages, intlLocale: 'en', @@ -246,7 +291,7 @@ describe('notifications reducer', () => { totalQueuedNotificationsCount: 1, }); const action = { - type: actions.NOTIFICATIONS_DEQUEUE, + type: NOTIFICATIONS_DEQUEUE, }; expect(reducer(state, action)).toEqual(ImmutableMap({ items: ImmutableOrderedMap(), @@ -274,7 +319,7 @@ describe('notifications reducer', () => { isLoading: false, }); const action = { - type: actions.NOTIFICATIONS_EXPAND_SUCCESS, + type: NOTIFICATIONS_EXPAND_SUCCESS, notifications: take(notifications, 3), next: true, }; @@ -335,7 +380,7 @@ describe('notifications reducer', () => { isLoading: false, }); const action = { - type: actions.NOTIFICATIONS_EXPAND_SUCCESS, + type: NOTIFICATIONS_EXPAND_SUCCESS, notifications: take(notifications, 3), next: true, }; @@ -514,7 +559,7 @@ describe('notifications reducer', () => { hasMore: true, }); const action = { - type: actions.NOTIFICATIONS_CLEAR, + type: NOTIFICATIONS_CLEAR, }; expect(reducer(state, action)).toEqual(ImmutableMap({ items: ImmutableOrderedMap(), @@ -527,7 +572,7 @@ describe('notifications reducer', () => { items: ImmutableOrderedMap(), }); const action = { - type: actions.NOTIFICATIONS_MARK_READ_REQUEST, + type: NOTIFICATIONS_MARK_READ_REQUEST, lastRead: 35098814, }; expect(reducer(state, action)).toEqual(ImmutableMap({ diff --git a/app/soapbox/reducers/notifications.js b/app/soapbox/reducers/notifications.js index c4b4e82eb..56e1bd7c2 100644 --- a/app/soapbox/reducers/notifications.js +++ b/app/soapbox/reducers/notifications.js @@ -64,8 +64,8 @@ const isValid = notification => { return false; } - // Mastodon can return mentions with a null status - if (notification.type === 'mention' && !notification.status.id) { + // Mastodon can return status notifications with a null status + if (['mention', 'reblog', 'favourite', 'poll'].includes(notification.type) && !notification.status.id) { return false; } From 6c201c7b130ab34fa25c782097c505618129fcce Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 12 Nov 2021 17:06:14 -0600 Subject: [PATCH 020/278] SubNavigation: fix weird overflow issue --- app/styles/navigation.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/styles/navigation.scss b/app/styles/navigation.scss index 035cc55f8..930991aec 100644 --- a/app/styles/navigation.scss +++ b/app/styles/navigation.scss @@ -171,7 +171,7 @@ align-self: center; justify-self: center; font-weight: bold; - overflow-x: hidden; + overflow: hidden; text-overflow: ellipsis; max-width: calc(100vw - 200px); } From c356597523fe635de0666f2f492382e0d1631769 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Mon, 15 Nov 2021 13:03:30 -0600 Subject: [PATCH 021/278] sw.js: fix match against undefined pathname --- webpack/production.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/webpack/production.js b/webpack/production.js index 220ed8991..37a1a4397 100644 --- a/webpack/production.js +++ b/webpack/production.js @@ -104,7 +104,11 @@ module.exports = merge(sharedConfig, { ]; const isBackendRoute = ({ pathname }) => { - return backendRoutes.some(pathname.startsWith); + if (pathname) { + return backendRoutes.some(pathname.startsWith); + } else { + return false; + } }; return isBackendRoute(requestUrl) && requestUrl; From 9a42ebb41fc29ea565a373d6cc39191fd9975e3f Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Mon, 15 Nov 2021 13:13:36 -0600 Subject: [PATCH 022/278] resizeImage: output error to console --- app/soapbox/utils/resize_image.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/soapbox/utils/resize_image.js b/app/soapbox/utils/resize_image.js index d801cc200..939653b6b 100644 --- a/app/soapbox/utils/resize_image.js +++ b/app/soapbox/utils/resize_image.js @@ -189,6 +189,9 @@ export default (inputFile, maxPixels = DEFAULT_MAX_PIXELS) => new Promise((resol resizeImage(img, inputFile, maxPixels) .then(resolve) - .catch(() => resolve(inputFile)); + .catch(error => { + console.error(error); + resolve(inputFile); + }); }).catch(() => resolve(inputFile)); }); From 30a9de817a67c15f738edc21e7f56ee1a8f54d23 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Mon, 15 Nov 2021 13:41:33 -0600 Subject: [PATCH 023/278] resizeImage: skip canvas reliability check for now (it's unreliable) --- app/soapbox/utils/resize_image.js | 76 ++++++++++++++++--------------- 1 file changed, 39 insertions(+), 37 deletions(-) diff --git a/app/soapbox/utils/resize_image.js b/app/soapbox/utils/resize_image.js index 939653b6b..f6a4ca34c 100644 --- a/app/soapbox/utils/resize_image.js +++ b/app/soapbox/utils/resize_image.js @@ -43,41 +43,41 @@ const dropOrientationIfNeeded = (orientation) => new Promise(resolve => { // Some browsers don't allow reading from a canvas and instead return all-white // or randomized data. Use a pre-defined image to check if reading the canvas // works. -const checkCanvasReliability = () => new Promise((resolve, reject) => { - switch(_browser_quirks['canvas-read-unreliable']) { - case true: - reject('Canvas reading unreliable'); - break; - case false: - resolve(); - break; - default: - // 2×2 GIF with white, red, green and blue pixels - const testImageURL = - 'data:image/gif;base64,R0lGODdhAgACAKEDAAAA//8AAAD/AP///ywAAAAAAgACAAACA1wEBQA7'; - const refData = - [255, 255, 255, 255, 255, 0, 0, 255, 0, 255, 0, 255, 0, 0, 255, 255]; - const img = new Image(); - img.onload = () => { - const canvas = document.createElement('canvas'); - const context = canvas.getContext('2d'); - context.drawImage(img, 0, 0, 2, 2); - const imageData = context.getImageData(0, 0, 2, 2); - if (imageData.data.every((x, i) => refData[i] === x)) { - _browser_quirks['canvas-read-unreliable'] = false; - resolve(); - } else { - _browser_quirks['canvas-read-unreliable'] = true; - reject('Canvas reading unreliable'); - } - }; - img.onerror = () => { - _browser_quirks['canvas-read-unreliable'] = true; - reject('Failed to load test image'); - }; - img.src = testImageURL; - } -}); +// const checkCanvasReliability = () => new Promise((resolve, reject) => { +// switch(_browser_quirks['canvas-read-unreliable']) { +// case true: +// reject('Canvas reading unreliable'); +// break; +// case false: +// resolve(); +// break; +// default: +// // 2×2 GIF with white, red, green and blue pixels +// const testImageURL = +// 'data:image/gif;base64,R0lGODdhAgACAKEDAAAA//8AAAD/AP///ywAAAAAAgACAAACA1wEBQA7'; +// const refData = +// [255, 255, 255, 255, 255, 0, 0, 255, 0, 255, 0, 255, 0, 0, 255, 255]; +// const img = new Image(); +// img.onload = () => { +// const canvas = document.createElement('canvas'); +// const context = canvas.getContext('2d'); +// context.drawImage(img, 0, 0, 2, 2); +// const imageData = context.getImageData(0, 0, 2, 2); +// if (imageData.data.every((x, i) => refData[i] === x)) { +// _browser_quirks['canvas-read-unreliable'] = false; +// resolve(); +// } else { +// _browser_quirks['canvas-read-unreliable'] = true; +// reject('Canvas reading unreliable'); +// } +// }; +// img.onerror = () => { +// _browser_quirks['canvas-read-unreliable'] = true; +// reject('Failed to load test image'); +// }; +// img.src = testImageURL; +// } +// }); const getImageUrl = inputFile => new Promise((resolve, reject) => { if (window.URL && URL.createObjectURL) { @@ -162,8 +162,10 @@ const resizeImage = (img, inputFile, maxPixels) => new Promise((resolve, reject) const newWidth = Math.round(Math.sqrt(maxPixels * (width / height))); const newHeight = Math.round(Math.sqrt(maxPixels * (height / width))); - checkCanvasReliability() - .then(getOrientation(img, type)) + // Skip canvas reliability check for now (it's unreliable) + // checkCanvasReliability() + // .then(getOrientation(img, type)) + getOrientation(img, type) .then(orientation => processImage(img, { width: newWidth, height: newHeight, From 926e92a7420bb3b0d09fa40114fffdd1cb28bc52 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Mon, 15 Nov 2021 14:34:33 -0600 Subject: [PATCH 024/278] Bundle: componentWillReceiveProps --> componentDidUpdate, React 17 compatibility --- app/soapbox/features/ui/components/bundle.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/app/soapbox/features/ui/components/bundle.js b/app/soapbox/features/ui/components/bundle.js index 7089597b6..b70ad342c 100644 --- a/app/soapbox/features/ui/components/bundle.js +++ b/app/soapbox/features/ui/components/bundle.js @@ -26,7 +26,7 @@ class Bundle extends React.PureComponent { onFetchFail: noop, } - static cache = new Map + static cache = new Map() state = { mod: undefined, @@ -34,12 +34,12 @@ class Bundle extends React.PureComponent { } componentDidMount() { - this.load(this.props); + this.load(); } - componentWillReceiveProps(nextProps) { - if (nextProps.fetchComponent !== this.props.fetchComponent) { - this.load(nextProps); + componentDidUpdate(prevProps) { + if (this.props.fetchComponent !== prevProps.fetchComponent) { + this.load(); } } @@ -49,8 +49,8 @@ class Bundle extends React.PureComponent { } } - load = (props) => { - const { fetchComponent, onFetch, onFetchSuccess, onFetchFail, renderDelay } = props || this.props; + load = () => { + const { fetchComponent, onFetch, onFetchSuccess, onFetchFail, renderDelay } = this.props; const cachedMod = Bundle.cache.get(fetchComponent); if (fetchComponent === undefined) { From e46e217d572ebb3ed0b015c70ee472ee11609355 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Mon, 15 Nov 2021 15:20:17 -0600 Subject: [PATCH 025/278] Search: don't infer filter from results, leave it alone --- app/soapbox/reducers/search.js | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/app/soapbox/reducers/search.js b/app/soapbox/reducers/search.js index 47aa9f00b..8a4ec4c5b 100644 --- a/app/soapbox/reducers/search.js +++ b/app/soapbox/reducers/search.js @@ -28,21 +28,7 @@ const toIds = items => { return ImmutableOrderedSet(items.map(item => item.id)); }; -const getResultsFilter = results => { - if (results.accounts.length > 0) { - return 'accounts'; - } else if (results.statuses.length > 0) { - return 'statuses'; - } else if (results.hashtags.length > 0) { - return 'hashtags'; - } else { - return 'accounts'; - } -}; - const importResults = (state, results) => { - const filter = getResultsFilter(results); - return state.withMutations(state => { state.set('results', ImmutableMap({ accounts: toIds(results.accounts), @@ -57,7 +43,6 @@ const importResults = (state, results) => { })); state.set('submitted', true); - state.set('filter', filter); }); }; From 2c1e6d12f92d8853cd89984ffcb026da1da600df Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Mon, 15 Nov 2021 15:31:32 -0600 Subject: [PATCH 026/278] Search: clear search when backspaced all the way --- app/soapbox/actions/search.js | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/app/soapbox/actions/search.js b/app/soapbox/actions/search.js index 6f1621ad4..d23457ba5 100644 --- a/app/soapbox/actions/search.js +++ b/app/soapbox/actions/search.js @@ -17,9 +17,16 @@ export const SEARCH_EXPAND_SUCCESS = 'SEARCH_EXPAND_SUCCESS'; export const SEARCH_EXPAND_FAIL = 'SEARCH_EXPAND_FAIL'; export function changeSearch(value) { - return { - type: SEARCH_CHANGE, - value, + return (dispatch, getState) => { + // If backspaced all the way, clear the search + if (value.length === 0) { + return dispatch(clearSearch()); + } else { + return dispatch({ + type: SEARCH_CHANGE, + value, + }); + } }; } @@ -33,6 +40,7 @@ export function submitSearch() { return (dispatch, getState) => { const value = getState().getIn(['search', 'value']); + // An empty search doesn't return any results if (value.length === 0) { return; } From ad70e391436b1682b60cfb8088e5b00fc43395f1 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Mon, 15 Nov 2021 15:59:08 -0600 Subject: [PATCH 027/278] Search: only search for results in the current filter --- app/soapbox/actions/search.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/soapbox/actions/search.js b/app/soapbox/actions/search.js index d23457ba5..76d39416a 100644 --- a/app/soapbox/actions/search.js +++ b/app/soapbox/actions/search.js @@ -39,6 +39,7 @@ export function clearSearch() { export function submitSearch() { return (dispatch, getState) => { const value = getState().getIn(['search', 'value']); + const filter = getState().getIn(['search', 'filter'], 'accounts'); // An empty search doesn't return any results if (value.length === 0) { @@ -52,6 +53,7 @@ export function submitSearch() { q: value, resolve: true, limit: 20, + type: filter, }, }).then(response => { if (response.data.accounts) { From e54253d155bc4bc0579436aaa6c8155752200d71 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Mon, 15 Nov 2021 16:03:20 -0600 Subject: [PATCH 028/278] Search: resubmit when changing tabs --- app/soapbox/actions/search.js | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/app/soapbox/actions/search.js b/app/soapbox/actions/search.js index 76d39416a..ca515d06c 100644 --- a/app/soapbox/actions/search.js +++ b/app/soapbox/actions/search.js @@ -36,10 +36,9 @@ export function clearSearch() { }; } -export function submitSearch() { +export function submitSearch(filter) { return (dispatch, getState) => { const value = getState().getIn(['search', 'value']); - const filter = getState().getIn(['search', 'filter'], 'accounts'); // An empty search doesn't return any results if (value.length === 0) { @@ -53,7 +52,7 @@ export function submitSearch() { q: value, resolve: true, limit: 20, - type: filter, + type: filter || getState().getIn(['search', 'filter'], 'accounts'), }, }).then(response => { if (response.data.accounts) { @@ -93,13 +92,17 @@ export function fetchSearchFail(error) { }; } -export const setFilter = filterType => dispatch => { - dispatch({ - type: SEARCH_FILTER_SET, - path: ['search', 'filter'], - value: filterType, - }); -}; +export function setFilter(filterType) { + return (dispatch) => { + dispatch(submitSearch(filterType)); + + dispatch({ + type: SEARCH_FILTER_SET, + path: ['search', 'filter'], + value: filterType, + }); + }; +} export const expandSearch = type => (dispatch, getState) => { const value = getState().getIn(['search', 'value']); From 7259ed58fb3e532f5aa49bd076b0607c66fab82c Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Mon, 15 Nov 2021 16:56:33 -0600 Subject: [PATCH 029/278] Offline: persist Soapbox config (eg frontend_configurations or soapbox.json) --- app/soapbox/actions/instance.js | 2 +- app/soapbox/actions/soapbox.js | 49 +++++++++++++++++++++++++------ app/soapbox/containers/soapbox.js | 4 +-- app/soapbox/reducers/soapbox.js | 14 ++++++++- app/soapbox/utils/instance.js | 11 +++++++ 5 files changed, 67 insertions(+), 13 deletions(-) create mode 100644 app/soapbox/utils/instance.js diff --git a/app/soapbox/actions/instance.js b/app/soapbox/actions/instance.js index ec77742b2..bc3b95758 100644 --- a/app/soapbox/actions/instance.js +++ b/app/soapbox/actions/instance.js @@ -22,7 +22,7 @@ const getMeUrl = state => { }; // Figure out the appropriate instance to fetch depending on the state -const getHost = state => { +export const getHost = state => { const accountUrl = getMeUrl(state) || getAuthUserUrl(state); try { diff --git a/app/soapbox/actions/soapbox.js b/app/soapbox/actions/soapbox.js index 64fbe1891..4358e9f1d 100644 --- a/app/soapbox/actions/soapbox.js +++ b/app/soapbox/actions/soapbox.js @@ -2,10 +2,16 @@ import api, { staticClient } from '../api'; import { Map as ImmutableMap, List as ImmutableList } from 'immutable'; import { getFeatures } from 'soapbox/utils/features'; import { createSelector } from 'reselect'; +import { getHost } from 'soapbox/actions/instance'; +import KVStore from 'soapbox/storage/kv_store'; export const SOAPBOX_CONFIG_REQUEST_SUCCESS = 'SOAPBOX_CONFIG_REQUEST_SUCCESS'; export const SOAPBOX_CONFIG_REQUEST_FAIL = 'SOAPBOX_CONFIG_REQUEST_FAIL'; +export const SOAPBOX_CONFIG_REMEMBER_REQUEST = 'SOAPBOX_CONFIG_REMEMBER_REQUEST'; +export const SOAPBOX_CONFIG_REMEMBER_SUCCESS = 'SOAPBOX_CONFIG_REMEMBER_SUCCESS'; +export const SOAPBOX_CONFIG_REMEMBER_FAIL = 'SOAPBOX_CONFIG_REMEMBER_FAIL'; + const allowedEmoji = ImmutableList([ '👍', '❤', @@ -61,46 +67,71 @@ export const getSoapboxConfig = createSelector([ return makeDefaultConfig(features).merge(soapbox); }); -export function fetchSoapboxConfig() { +export function rememberSoapboxConfig(host) { + return (dispatch, getState) => { + dispatch({ type: SOAPBOX_CONFIG_REMEMBER_REQUEST, host }); + return KVStore.getItemOrError(`soapbox_config:${host}`).then(soapboxConfig => { + dispatch({ type: SOAPBOX_CONFIG_REMEMBER_SUCCESS, host, soapboxConfig }); + return soapboxConfig; + }).catch(error => { + dispatch({ type: SOAPBOX_CONFIG_REMEMBER_FAIL, host, error, skipAlert: true }); + }); + }; +} + +export function fetchSoapboxConfig(host) { return (dispatch, getState) => { api(getState).get('/api/pleroma/frontend_configurations').then(response => { if (response.data.soapbox_fe) { - dispatch(importSoapboxConfig(response.data.soapbox_fe)); + dispatch(importSoapboxConfig(response.data.soapbox_fe, host)); } else { - dispatch(fetchSoapboxJson()); + dispatch(fetchSoapboxJson(host)); } }).catch(error => { - dispatch(fetchSoapboxJson()); + dispatch(fetchSoapboxJson(host)); }); }; } -export function fetchSoapboxJson() { +// Tries to remember the config from browser storage before fetching it +export function loadSoapboxConfig() { + return (dispatch, getState) => { + const host = getHost(getState()); + + return dispatch(rememberSoapboxConfig(host)).finally(() => { + return dispatch(fetchSoapboxConfig(host)); + }); + }; +} + +export function fetchSoapboxJson(host) { return (dispatch, getState) => { staticClient.get('/instance/soapbox.json').then(({ data }) => { if (!isObject(data)) throw 'soapbox.json failed'; - dispatch(importSoapboxConfig(data)); + dispatch(importSoapboxConfig(data, host)); }).catch(error => { - dispatch(soapboxConfigFail(error)); + dispatch(soapboxConfigFail(error, host)); }); }; } -export function importSoapboxConfig(soapboxConfig) { +export function importSoapboxConfig(soapboxConfig, host) { if (!soapboxConfig.brandColor) { soapboxConfig.brandColor = '#0482d8'; } return { type: SOAPBOX_CONFIG_REQUEST_SUCCESS, soapboxConfig, + host, }; } -export function soapboxConfigFail(error) { +export function soapboxConfigFail(error, host) { return { type: SOAPBOX_CONFIG_REQUEST_FAIL, error, skipAlert: true, + host, }; } diff --git a/app/soapbox/containers/soapbox.js b/app/soapbox/containers/soapbox.js index c011d0f0d..a4e48c587 100644 --- a/app/soapbox/containers/soapbox.js +++ b/app/soapbox/containers/soapbox.js @@ -17,7 +17,7 @@ import { preload } from '../actions/preload'; import { IntlProvider } from 'react-intl'; import ErrorBoundary from '../components/error_boundary'; import { loadInstance } from 'soapbox/actions/instance'; -import { fetchSoapboxConfig } from 'soapbox/actions/soapbox'; +import { loadSoapboxConfig } from 'soapbox/actions/soapbox'; import { fetchMe } from 'soapbox/actions/me'; import PublicLayout from 'soapbox/features/public_layout'; import { getSettings } from 'soapbox/actions/settings'; @@ -43,7 +43,7 @@ store.dispatch(fetchMe()) .then(() => { // Postpone for authenticated fetch store.dispatch(loadInstance()); - store.dispatch(fetchSoapboxConfig()); + store.dispatch(loadSoapboxConfig()); }) .catch(() => {}); diff --git a/app/soapbox/reducers/soapbox.js b/app/soapbox/reducers/soapbox.js index 60e339786..c1ab95d3e 100644 --- a/app/soapbox/reducers/soapbox.js +++ b/app/soapbox/reducers/soapbox.js @@ -6,6 +6,7 @@ import { import { PLEROMA_PRELOAD_IMPORT } from 'soapbox/actions/preload'; import { Map as ImmutableMap, fromJS } from 'immutable'; import { ConfigDB } from 'soapbox/utils/config_db'; +import KVStore from 'soapbox/storage/kv_store'; const initialState = ImmutableMap(); @@ -36,12 +37,23 @@ const preloadImport = (state, action) => { } }; +const persistSoapboxConfig = (soapboxConfig, host) => { + if (host) { + KVStore.setItem(`soapbox_config:${host}`, soapboxConfig.toJS()).catch(console.error); + } +}; + +const importSoapboxConfig = (state, soapboxConfig, host) => { + persistSoapboxConfig(soapboxConfig, host); + return soapboxConfig; +}; + export default function soapbox(state = initialState, action) { switch(action.type) { case PLEROMA_PRELOAD_IMPORT: return preloadImport(state, action); case SOAPBOX_CONFIG_REQUEST_SUCCESS: - return fromJS(action.soapboxConfig); + return importSoapboxConfig(state, fromJS(action.soapboxConfig), action.host); case SOAPBOX_CONFIG_REQUEST_FAIL: return fallbackState.mergeDeep(state); case ADMIN_CONFIG_UPDATE_SUCCESS: diff --git a/app/soapbox/utils/instance.js b/app/soapbox/utils/instance.js new file mode 100644 index 000000000..af9d69665 --- /dev/null +++ b/app/soapbox/utils/instance.js @@ -0,0 +1,11 @@ +export const getHost = instance => { + try { + return new URL(instance.get('uri')).host; + } catch { + try { + return new URL(`https://${instance.get('uri')}`).host; + } catch { + return null; + } + } +}; From 1714ac03d2f1980a9e5687df307797f3bb841ebb Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Mon, 15 Nov 2021 19:08:27 -0600 Subject: [PATCH 030/278] Status: display a placeholder Card on own links, poll for updated card --- app/soapbox/actions/statuses.js | 24 +++++++++++++++ app/soapbox/components/status.js | 5 ++++ .../components/placeholder_card.js | 29 +++++++++++++++++++ .../features/status/components/card.js | 2 +- app/soapbox/utils/status.js | 15 ++++++++++ app/styles/placeholder.scss | 16 +++++++++- 6 files changed, 89 insertions(+), 2 deletions(-) create mode 100644 app/soapbox/features/placeholder/components/placeholder_card.js create mode 100644 app/soapbox/utils/status.js diff --git a/app/soapbox/actions/statuses.js b/app/soapbox/actions/statuses.js index 5b65349b7..440d4150e 100644 --- a/app/soapbox/actions/statuses.js +++ b/app/soapbox/actions/statuses.js @@ -3,6 +3,7 @@ import { deleteFromTimelines } from './timelines'; import { importFetchedStatus, importFetchedStatuses } from './importer'; import { openModal } from './modal'; import { isLoggedIn } from 'soapbox/utils/auth'; +import { shouldHaveCard } from 'soapbox/utils/status'; export const STATUS_CREATE_REQUEST = 'STATUS_CREATE_REQUEST'; export const STATUS_CREATE_SUCCESS = 'STATUS_CREATE_SUCCESS'; @@ -44,8 +45,31 @@ export function createStatus(params, idempotencyKey) { return api(getState).post('/api/v1/statuses', params, { headers: { 'Idempotency-Key': idempotencyKey }, }).then(({ data: status }) => { + // The backend might still be processing the rich media attachment + if (!status.card && shouldHaveCard(status)) { + status.expectsCard = true; + } + dispatch(importFetchedStatus(status, idempotencyKey)); dispatch({ type: STATUS_CREATE_SUCCESS, status, params, idempotencyKey }); + + // Poll the backend for the updated card + if (status.expectsCard) { + const delay = 1000; + + const poll = (retries = 5) => { + api(getState).get(`/api/v1/statuses/${status.id}`).then(response => { + if (response.data && response.data.card) { + dispatch(importFetchedStatus(response.data)); + } else if (retries > 0 && response.status === 200) { + setTimeout(() => poll(retries - 1), delay); + } + }).catch(console.error); + }; + + setTimeout(() => poll(), delay); + } + return status; }).catch(error => { dispatch({ type: STATUS_CREATE_FAIL, error, params, idempotencyKey }); diff --git a/app/soapbox/components/status.js b/app/soapbox/components/status.js index de7a330ca..ff7f23f52 100644 --- a/app/soapbox/components/status.js +++ b/app/soapbox/components/status.js @@ -19,6 +19,7 @@ import Icon from 'soapbox/components/icon'; import { Link, NavLink } from 'react-router-dom'; import { getDomain } from 'soapbox/utils/accounts'; import HoverRefWrapper from 'soapbox/components/hover_ref_wrapper'; +import PlaceholderCard from 'soapbox/features/placeholder/components/placeholder_card'; // We use the component (and not the container) since we do not want // to use the progress bar to show download progress @@ -465,6 +466,10 @@ class Status extends ImmutablePureComponent { defaultWidth={this.props.cachedMediaWidth} /> ); + } else if (status.get('expectsCard', false)) { + media = ( + + ); } if (otherAccounts && otherAccounts.size > 1) { diff --git a/app/soapbox/features/placeholder/components/placeholder_card.js b/app/soapbox/features/placeholder/components/placeholder_card.js new file mode 100644 index 000000000..7f211709d --- /dev/null +++ b/app/soapbox/features/placeholder/components/placeholder_card.js @@ -0,0 +1,29 @@ +import React from 'react'; +import { randomIntFromInterval, generateText } from '../utils'; + +export default class PlaceholderCard extends React.Component { + + shouldComponentUpdate() { + // Re-rendering this will just cause the random lengths to jump around. + // There's basically no reason to ever do it. + return false; + } + + render() { + return ( +
    +
    +
    + {generateText(randomIntFromInterval(5, 25))} +

    + {generateText(randomIntFromInterval(5, 75))} +

    + + {generateText(randomIntFromInterval(5, 15))} + +
    +
    + ); + } + +} diff --git a/app/soapbox/features/status/components/card.js b/app/soapbox/features/status/components/card.js index 4e870640e..2653f9304 100644 --- a/app/soapbox/features/status/components/card.js +++ b/app/soapbox/features/status/components/card.js @@ -171,7 +171,7 @@ export default class Card extends React.PureComponent { const description = (
    - {title} + {title}

    {trim(card.get('description') || '', maxDescription)}

    {provider}
    diff --git a/app/soapbox/utils/status.js b/app/soapbox/utils/status.js new file mode 100644 index 000000000..48554ced9 --- /dev/null +++ b/app/soapbox/utils/status.js @@ -0,0 +1,15 @@ +export const getFirstExternalLink = status => { + try { + // Pulled from Pleroma's media parser + const selector = 'a:not(.mention,.hashtag,.attachment,[rel~="tag"])'; + const element = document.createElement('div'); + element.innerHTML = status.content; + return element.querySelector(selector); + } catch { + return null; + } +}; + +export const shouldHaveCard = status => { + return Boolean(getFirstExternalLink(status)); +}; diff --git a/app/styles/placeholder.scss b/app/styles/placeholder.scss index 0427e9463..8366261ac 100644 --- a/app/styles/placeholder.scss +++ b/app/styles/placeholder.scss @@ -1,6 +1,7 @@ .placeholder-status, .placeholder-hashtag, -.notification--placeholder { +.notification--placeholder, +.status-card--placeholder { position: relative; &::before { @@ -105,3 +106,16 @@ background: transparent; box-shadow: none; } + +.status-card--placeholder { + pointer-events: none; + + .status-card__title, + .status-card__description, + .status-card__host { + letter-spacing: -1px; + color: var(--brand-color) !important; + word-break: break-all; + opacity: 0.1; + } +} From dc363cd04b2be36027d78a607d45a111da88e58d Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Mon, 15 Nov 2021 21:31:17 -0600 Subject: [PATCH 031/278] Fix navigating between profiles --- app/soapbox/features/account_gallery/index.js | 2 +- app/soapbox/features/account_timeline/index.js | 2 +- app/soapbox/features/favourited_statuses/index.js | 2 +- app/soapbox/features/followers/index.js | 2 +- app/soapbox/features/following/index.js | 2 +- app/soapbox/pages/profile_page.js | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/soapbox/features/account_gallery/index.js b/app/soapbox/features/account_gallery/index.js index 843f83e4d..953c18add 100644 --- a/app/soapbox/features/account_gallery/index.js +++ b/app/soapbox/features/account_gallery/index.js @@ -21,7 +21,7 @@ import { FormattedMessage } from 'react-intl'; const mapStateToProps = (state, { params, withReplies = false }) => { const username = params.username || ''; const me = state.get('me'); - const accountFetchError = (state.getIn(['accounts', -1, 'username'], '').toLowerCase() === username.toLowerCase()); + const accountFetchError = ((state.getIn(['accounts', -1, 'username']) || '').toLowerCase() === username.toLowerCase()); let accountId = -1; let accountUsername = username; diff --git a/app/soapbox/features/account_timeline/index.js b/app/soapbox/features/account_timeline/index.js index 8700710a7..9172b26af 100644 --- a/app/soapbox/features/account_timeline/index.js +++ b/app/soapbox/features/account_timeline/index.js @@ -27,7 +27,7 @@ const makeMapStateToProps = () => { const mapStateToProps = (state, { params, withReplies = false }) => { const username = params.username || ''; const me = state.get('me'); - const accountFetchError = (state.getIn(['accounts', -1, 'username'], '').toLowerCase() === username.toLowerCase()); + const accountFetchError = ((state.getIn(['accounts', -1, 'username']) || '').toLowerCase() === username.toLowerCase()); const soapboxConfig = getSoapboxConfig(state); let accountId = -1; diff --git a/app/soapbox/features/favourited_statuses/index.js b/app/soapbox/features/favourited_statuses/index.js index e10455f21..5dd5b9f67 100644 --- a/app/soapbox/features/favourited_statuses/index.js +++ b/app/soapbox/features/favourited_statuses/index.js @@ -29,7 +29,7 @@ const mapStateToProps = (state, { params }) => { }; } - const accountFetchError = (state.getIn(['accounts', -1, 'username'], '').toLowerCase() === username.toLowerCase()); + const accountFetchError = ((state.getIn(['accounts', -1, 'username']) || '').toLowerCase() === username.toLowerCase()); let accountId = -1; if (accountFetchError) { diff --git a/app/soapbox/features/followers/index.js b/app/soapbox/features/followers/index.js index a4d48e0d0..85b258e03 100644 --- a/app/soapbox/features/followers/index.js +++ b/app/soapbox/features/followers/index.js @@ -22,7 +22,7 @@ import { findAccountByUsername } from 'soapbox/selectors'; const mapStateToProps = (state, { params, withReplies = false }) => { const username = params.username || ''; const me = state.get('me'); - const accountFetchError = (state.getIn(['accounts', -1, 'username'], '').toLowerCase() === username.toLowerCase()); + const accountFetchError = ((state.getIn(['accounts', -1, 'username']) || '').toLowerCase() === username.toLowerCase()); let accountId = -1; if (accountFetchError) { diff --git a/app/soapbox/features/following/index.js b/app/soapbox/features/following/index.js index c09a742b8..434ec4797 100644 --- a/app/soapbox/features/following/index.js +++ b/app/soapbox/features/following/index.js @@ -22,7 +22,7 @@ import { findAccountByUsername } from 'soapbox/selectors'; const mapStateToProps = (state, { params, withReplies = false }) => { const username = params.username || ''; const me = state.get('me'); - const accountFetchError = (state.getIn(['accounts', -1, 'username'], '').toLowerCase() === username.toLowerCase()); + const accountFetchError = ((state.getIn(['accounts', -1, 'username']) || '').toLowerCase() === username.toLowerCase()); let accountId = -1; if (accountFetchError) { diff --git a/app/soapbox/pages/profile_page.js b/app/soapbox/pages/profile_page.js index 84f6c4f5e..1f4bdbc49 100644 --- a/app/soapbox/pages/profile_page.js +++ b/app/soapbox/pages/profile_page.js @@ -24,7 +24,7 @@ import { findAccountByUsername } from 'soapbox/selectors'; const mapStateToProps = (state, { params, withReplies = false }) => { const username = params.username || ''; const accounts = state.getIn(['accounts']); - const accountFetchError = (state.getIn(['accounts', -1, 'username'], '').toLowerCase() === username.toLowerCase()); + const accountFetchError = ((state.getIn(['accounts', -1, 'username']) || '').toLowerCase() === username.toLowerCase()); const getAccount = makeGetAccount(); let accountId = -1; From a3ce7b2afe92ec4e6659c73e15cb638e90aac58e Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Mon, 15 Nov 2021 21:58:30 -0600 Subject: [PATCH 032/278] PendingStatus: display PlaceholderCard when a card is expected --- .../features/ui/components/pending_status.js | 28 +++++++++++++++---- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/app/soapbox/features/ui/components/pending_status.js b/app/soapbox/features/ui/components/pending_status.js index dd9327979..13daa5928 100644 --- a/app/soapbox/features/ui/components/pending_status.js +++ b/app/soapbox/features/ui/components/pending_status.js @@ -12,6 +12,11 @@ import Avatar from 'soapbox/components/avatar'; import DisplayName from 'soapbox/components/display_name'; import AttachmentThumbs from 'soapbox/components/attachment_thumbs'; import PollPreview from './poll_preview'; +import PlaceholderCard from 'soapbox/features/placeholder/components/placeholder_card'; + +const shouldHaveCard = pendingStatus => { + return Boolean(pendingStatus.get('content').match(/https?:\/\/\S*/)); +}; const mapStateToProps = (state, props) => { const { idempotencyKey } = props; @@ -24,6 +29,23 @@ const mapStateToProps = (state, props) => { export default @connect(mapStateToProps) class PendingStatus extends ImmutablePureComponent { + renderMedia = () => { + const { status } = this.props; + + if (status.get('media_attachments') && !status.get('media_attachments').isEmpty()) { + return ( + + ); + } else if (shouldHaveCard(status)) { + return ; + } else { + return null; + } + } + render() { const { status, className, showThread } = this.props; if (!status) return null; @@ -67,11 +89,7 @@ class PendingStatus extends ImmutablePureComponent { collapsable /> - - + {this.renderMedia()} {status.get('poll') && } {showThread && status.get('in_reply_to_id') && status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) && ( From 164a150fbb38ea9eb49f209f443bf645ffc883f7 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 16 Nov 2021 07:12:55 -0600 Subject: [PATCH 033/278] Admin: fix display of 0% retention --- app/soapbox/features/admin/index.js | 19 +++++++++++-------- app/soapbox/utils/numbers.js | 2 +- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/app/soapbox/features/admin/index.js b/app/soapbox/features/admin/index.js index b09affd66..e80ae1231 100644 --- a/app/soapbox/features/admin/index.js +++ b/app/soapbox/features/admin/index.js @@ -12,6 +12,7 @@ import sourceCode from 'soapbox/utils/code'; import { getSubscribersCsv, getUnsubscribersCsv, getCombinedCsv } from 'soapbox/actions/email_list'; import { getFeatures } from 'soapbox/utils/features'; import { isAdmin } from 'soapbox/utils/accounts'; +import { isNumber } from 'soapbox/utils/numbers'; // https://stackoverflow.com/a/53230807 const download = (response, filename) => { @@ -102,16 +103,18 @@ class Dashboard extends ImmutablePureComponent {
    - {retention &&
    -
    -
    - {retention}% -
    -
    - + {isNumber(retention) && ( +
    +
    +
    + {retention}% +
    +
    + +
    -
    } + )}
    diff --git a/app/soapbox/utils/numbers.js b/app/soapbox/utils/numbers.js index 434473b40..8191692b3 100644 --- a/app/soapbox/utils/numbers.js +++ b/app/soapbox/utils/numbers.js @@ -1,7 +1,7 @@ import React from 'react'; import { FormattedNumber } from 'react-intl'; -const isNumber = number => typeof number === 'number' && !isNaN(number); +export const isNumber = number => typeof number === 'number' && !isNaN(number); export const shortNumberFormat = number => { if (!isNumber(number)) return '•'; From e419f82b38b37cd3ca0869be66eaef46aeeee12a Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 16 Nov 2021 10:52:13 -0600 Subject: [PATCH 034/278] ActionsModal: check props before calling .map, fixes delete post on mobile --- app/soapbox/features/ui/components/actions_modal.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/soapbox/features/ui/components/actions_modal.js b/app/soapbox/features/ui/components/actions_modal.js index f70e4c85c..982ee7122 100644 --- a/app/soapbox/features/ui/components/actions_modal.js +++ b/app/soapbox/features/ui/components/actions_modal.js @@ -51,7 +51,7 @@ class ActionsModal extends ImmutablePureComponent { } render() { - const { onClose } = this.props; + const { actions, onClose } = this.props; const status = this.props.status && (
    @@ -82,7 +82,7 @@ class ActionsModal extends ImmutablePureComponent { {status}