diff --git a/.eslintrc.js b/.eslintrc.js index 164949e65..0ecb15a5b 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -277,6 +277,7 @@ module.exports = { files: ['**/*.ts', '**/*.tsx'], rules: { 'no-undef': 'off', // https://stackoverflow.com/a/69155899 + 'space-before-function-paren': 'off', }, parser: '@typescript-eslint/parser', }, diff --git a/app/soapbox/__fixtures__/truthsocial-status-in-moderation.json b/app/soapbox/__fixtures__/truthsocial-status-in-moderation.json index e9402876f..7613ebd93 100644 --- a/app/soapbox/__fixtures__/truthsocial-status-in-moderation.json +++ b/app/soapbox/__fixtures__/truthsocial-status-in-moderation.json @@ -82,4 +82,4 @@ "emojis": [], "card": null, "poll": null -} +} \ No newline at end of file diff --git a/app/soapbox/actions/auth.ts b/app/soapbox/actions/auth.ts index d3252e7fb..5a686d045 100644 --- a/app/soapbox/actions/auth.ts +++ b/app/soapbox/actions/auth.ts @@ -20,6 +20,7 @@ import KVStore from 'soapbox/storage/kv_store'; import { getLoggedInAccount, parseBaseURL } from 'soapbox/utils/auth'; import sourceCode from 'soapbox/utils/code'; import { getFeatures } from 'soapbox/utils/features'; +import { normalizeUsername } from 'soapbox/utils/input'; import { isStandalone } from 'soapbox/utils/state'; import api, { baseClient } from '../api'; @@ -207,16 +208,6 @@ export const loadCredentials = (token: string, accountUrl: string) => }) .catch(() => dispatch(verifyCredentials(token, accountUrl))); -/** Trim the username and strip the leading @. */ -const normalizeUsername = (username: string): string => { - const trimmed = username.trim(); - if (trimmed[0] === '@') { - return trimmed.slice(1); - } else { - return trimmed; - } -}; - export const logIn = (username: string, password: string) => (dispatch: AppDispatch) => dispatch(getAuthApp()).then(() => { return dispatch(createUserToken(normalizeUsername(username), password)); diff --git a/app/soapbox/actions/compose.ts b/app/soapbox/actions/compose.ts index df519fe6c..0859199b1 100644 --- a/app/soapbox/actions/compose.ts +++ b/app/soapbox/actions/compose.ts @@ -323,11 +323,13 @@ const uploadCompose = (composeId: string, files: FileList, intl: IntlShape) => const maxVideoSize = getState().instance.configuration.getIn(['media_attachments', 'video_size_limit']) as number | undefined; const maxVideoDuration = getState().instance.configuration.getIn(['media_attachments', 'video_duration_limit']) as number | undefined; - const media = getState().compose.get(composeId)!.media_attachments; + const media = getState().compose.get(composeId)?.media_attachments; const progress = new Array(files.length).fill(0); let total = Array.from(files).reduce((a, v) => a + v.size, 0); - if (files.length + media.size > attachmentLimit) { + const mediaCount = media ? media.size : 0; + + if (files.length + mediaCount > attachmentLimit) { dispatch(showAlert(undefined, messages.uploadErrorLimit, 'error')); return; } @@ -335,7 +337,7 @@ const uploadCompose = (composeId: string, files: FileList, intl: IntlShape) => dispatch(uploadComposeRequest(composeId)); Array.from(files).forEach(async(f, i) => { - if (media.size + i > attachmentLimit - 1) return; + if (mediaCount + i > attachmentLimit - 1) return; const isImage = f.type.match(/image.*/); const isVideo = f.type.match(/video.*/); diff --git a/app/soapbox/actions/moderation.tsx b/app/soapbox/actions/moderation.tsx index ea5861eca..bf0ccf332 100644 --- a/app/soapbox/actions/moderation.tsx +++ b/app/soapbox/actions/moderation.tsx @@ -5,6 +5,8 @@ import { fetchAccountByUsername } from 'soapbox/actions/accounts'; import { deactivateUsers, deleteUsers, deleteStatus, toggleStatusSensitivity } from 'soapbox/actions/admin'; import { openModal } from 'soapbox/actions/modals'; import snackbar from 'soapbox/actions/snackbar'; +import OutlineBox from 'soapbox/components/outline-box'; +import { Stack, Text } from 'soapbox/components/ui'; import AccountContainer from 'soapbox/containers/account_container'; import { isLocal } from 'soapbox/utils/accounts'; @@ -43,10 +45,22 @@ const deactivateUserModal = (intl: IntlShape, accountId: string, afterConfirm = const acct = state.accounts.get(accountId)!.acct; const name = state.accounts.get(accountId)!.username; + const message = ( + + + + + + + {intl.formatMessage(messages.deactivateUserPrompt, { acct })} + + + ); + dispatch(openModal('CONFIRM', { icon: require('@tabler/icons/user-off.svg'), heading: intl.formatMessage(messages.deactivateUserHeading, { acct }), - message: intl.formatMessage(messages.deactivateUserPrompt, { acct }), + message, confirm: intl.formatMessage(messages.deactivateUserConfirm, { name }), onConfirm: () => { dispatch(deactivateUsers([accountId])).then(() => { @@ -64,22 +78,21 @@ const deleteUserModal = (intl: IntlShape, accountId: string, afterConfirm = () = const account = state.accounts.get(accountId)!; const acct = account.acct; const name = account.username; - const favicon = account.pleroma.get('favicon'); const local = isLocal(account); - const message = (<> - - {intl.formatMessage(messages.deleteUserPrompt, { acct })} - ); + const message = ( + + + + - const confirm = (<> - {favicon && -
- -
} - {intl.formatMessage(messages.deleteUserConfirm, { name })} - ); + + {intl.formatMessage(messages.deleteUserPrompt, { acct })} + +
+ ); + const confirm = intl.formatMessage(messages.deleteUserConfirm, { name }); const checkbox = local ? intl.formatMessage(messages.deleteLocalUserCheckbox) : false; dispatch(openModal('CONFIRM', { diff --git a/app/soapbox/actions/security.ts b/app/soapbox/actions/security.ts index 430691a06..196e54dcb 100644 --- a/app/soapbox/actions/security.ts +++ b/app/soapbox/actions/security.ts @@ -7,6 +7,7 @@ import snackbar from 'soapbox/actions/snackbar'; import { getLoggedInAccount } from 'soapbox/utils/auth'; import { parseVersion, TRUTHSOCIAL } from 'soapbox/utils/features'; +import { normalizeUsername } from 'soapbox/utils/input'; import api from '../api'; @@ -84,15 +85,16 @@ const changePassword = (oldPassword: string, newPassword: string, confirmation: const resetPassword = (usernameOrEmail: string) => (dispatch: AppDispatch, getState: () => RootState) => { + const input = normalizeUsername(usernameOrEmail); const state = getState(); const v = parseVersion(state.instance.version); dispatch({ type: RESET_PASSWORD_REQUEST }); const params = - usernameOrEmail.includes('@') - ? { email: usernameOrEmail } - : { nickname: usernameOrEmail, username: usernameOrEmail }; + input.includes('@') + ? { email: input } + : { nickname: input, username: input }; const endpoint = v.software === TRUTHSOCIAL diff --git a/app/soapbox/components/media_gallery.js b/app/soapbox/components/media_gallery.js index f8e9dc51e..7bbbbd5b7 100644 --- a/app/soapbox/components/media_gallery.js +++ b/app/soapbox/components/media_gallery.js @@ -7,7 +7,6 @@ import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import { connect } from 'react-redux'; import { getSettings } from 'soapbox/actions/settings'; -import { getSoapboxConfig } from 'soapbox/actions/soapbox'; import Blurhash from 'soapbox/components/blurhash'; import Icon from 'soapbox/components/icon'; import StillImage from 'soapbox/components/still_image'; @@ -264,14 +263,9 @@ class Item extends React.PureComponent { } -const mapStateToMediaGalleryProps = state => { - const { links } = getSoapboxConfig(state); - - return { - displayMedia: getSettings(state).get('displayMedia'), - links, - }; -}; +const mapStateToMediaGalleryProps = state => ({ + displayMedia: getSettings(state).get('displayMedia'), +}); export default @connect(mapStateToMediaGalleryProps) @injectIntl @@ -291,7 +285,6 @@ class MediaGallery extends React.PureComponent { onToggleVisibility: PropTypes.func, displayMedia: PropTypes.string, compact: PropTypes.bool, - links: ImmutablePropTypes.map, }; static defaultProps = { @@ -575,7 +568,7 @@ class MediaGallery extends React.PureComponent { } render() { - const { media, intl, sensitive, compact, inReview, links } = this.props; + const { media, intl, sensitive, compact } = this.props; const { visible } = this.state; const sizeData = this.getSizeData(media.size); @@ -594,22 +587,14 @@ class MediaGallery extends React.PureComponent { /> )); - let warning, summary; + let warning; if (sensitive) { warning = ; - } else if (inReview) { - warning = ; } else { warning = ; } - if (inReview) { - summary = ; - } else { - summary = ; - } - return (
- {(sensitive || inReview) && ( + {sensitive && ( (visible || compact) ? ( + + )} + + + +
+ )} +
+ ); +}; + +export default ModerationOverlay; \ No newline at end of file diff --git a/app/soapbox/components/ui/widget/widget.tsx b/app/soapbox/components/ui/widget/widget.tsx index 7b966a6ef..3bced193f 100644 --- a/app/soapbox/components/ui/widget/widget.tsx +++ b/app/soapbox/components/ui/widget/widget.tsx @@ -41,8 +41,8 @@ const Widget: React.FC = ({ action, }): JSX.Element => { return ( - - + + {action || (onActionClick && ( { + id: ID extends 'default' ? never : ID, shouldCondense?: boolean, autoFocus?: boolean, clickableAreaRef?: React.RefObject, eventDiscussion?: boolean } -const ComposeForm: React.FC = ({ id, shouldCondense, autoFocus, clickableAreaRef, eventDiscussion }) => { +const ComposeForm = ({ id, shouldCondense, autoFocus, clickableAreaRef, eventDiscussion }: IComposeForm) => { const history = useHistory(); const intl = useIntl(); const dispatch = useAppDispatch(); @@ -78,6 +78,7 @@ const ComposeForm: React.FC = ({ id, shouldCondense, autoFocus, cl const features = useFeatures(); const { text, suggestions, spoiler, spoiler_text: spoilerText, privacy, focusDate, caretPosition, is_submitting: isSubmitting, is_changing_upload: isChangingUpload, is_uploading: isUploading, schedule: scheduledAt } = compose; + const prevSpoiler = usePrevious(spoiler); const hasPoll = !!compose.poll; const isEditing = compose.id !== null; @@ -208,9 +209,10 @@ const ComposeForm: React.FC = ({ id, shouldCondense, autoFocus, cl }, []); useEffect(() => { - switch (spoiler) { - case true: focusSpoilerInput(); break; - case false: focusTextarea(); break; + if (spoiler && !prevSpoiler) { + focusSpoilerInput(); + } else if (!spoiler && prevSpoiler) { + focusTextarea(); } }, [spoiler]); diff --git a/app/soapbox/features/compose/components/reply_mentions.tsx b/app/soapbox/features/compose/components/reply_mentions.tsx index 577334281..865f6539d 100644 --- a/app/soapbox/features/compose/components/reply_mentions.tsx +++ b/app/soapbox/features/compose/components/reply_mentions.tsx @@ -36,7 +36,9 @@ const ReplyMentions: React.FC = ({ composeId }) => { const handleClick = (e: React.MouseEvent) => { e.preventDefault(); - dispatch(openModal('REPLY_MENTIONS')); + dispatch(openModal('REPLY_MENTIONS', { + composeId, + })); }; if (!parentTo || (parentTo.size === 0)) { diff --git a/app/soapbox/features/compose/components/upload.tsx b/app/soapbox/features/compose/components/upload.tsx index 840cde67c..f631e94d2 100644 --- a/app/soapbox/features/compose/components/upload.tsx +++ b/app/soapbox/features/compose/components/upload.tsx @@ -119,7 +119,7 @@ const Upload: React.FC = ({ composeId, id }) => { setDirtyDescription(null); if (dirtyDescription !== null) { - dispatch(changeUploadCompose(composeId, media.id, { dirtyDescription })); + dispatch(changeUploadCompose(composeId, media.id, { description: dirtyDescription })); } }; diff --git a/app/soapbox/features/feed-suggestions/feed-suggestions.tsx b/app/soapbox/features/feed-suggestions/feed-suggestions.tsx index d946f5e8c..2545c72e5 100644 --- a/app/soapbox/features/feed-suggestions/feed-suggestions.tsx +++ b/app/soapbox/features/feed-suggestions/feed-suggestions.tsx @@ -11,7 +11,7 @@ import ActionButton from '../ui/components/action-button'; import type { Account } from 'soapbox/types/entities'; const messages = defineMessages({ - heading: { id: 'feed_suggestions.heading', defaultMessage: 'Suggested profiles' }, + heading: { id: 'feed_suggestions.heading', defaultMessage: 'Suggested Profiles' }, viewAll: { id: 'feed_suggestions.view_all', defaultMessage: 'View all' }, }); @@ -65,7 +65,7 @@ const FeedSuggestions = () => { if (!isLoading && suggestedProfiles.size === 0) return null; return ( - + diff --git a/app/soapbox/features/follow-recommendations/index.tsx b/app/soapbox/features/follow-recommendations/index.tsx index 7fda03c7a..221ca1491 100644 --- a/app/soapbox/features/follow-recommendations/index.tsx +++ b/app/soapbox/features/follow-recommendations/index.tsx @@ -10,7 +10,7 @@ import Column from 'soapbox/features/ui/components/column'; import { useAppDispatch, useAppSelector, useFeatures } from 'soapbox/hooks'; const messages = defineMessages({ - heading: { id: 'followRecommendations.heading', defaultMessage: 'Suggested profiles' }, + heading: { id: 'followRecommendations.heading', defaultMessage: 'Suggested Profiles' }, }); const FollowRecommendations: React.FC = () => { diff --git a/app/soapbox/features/migration/index.js b/app/soapbox/features/migration/index.js deleted file mode 100644 index b323c833c..000000000 --- a/app/soapbox/features/migration/index.js +++ /dev/null @@ -1,119 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import ImmutablePureComponent from 'react-immutable-pure-component'; -import { defineMessages, FormattedMessage, injectIntl } from 'react-intl'; -import { connect } from 'react-redux'; -import { Link } from 'react-router-dom'; - -import { moveAccount } from 'soapbox/actions/security'; -import snackbar from 'soapbox/actions/snackbar'; -// import Column from 'soapbox/features/ui/components/column'; -import { Button, Column, Form, FormActions, FormGroup, Input, Text } from 'soapbox/components/ui'; - -const messages = defineMessages({ - heading: { id: 'column.migration', defaultMessage: 'Account migration' }, - submit: { id: 'migration.submit', defaultMessage: 'Move followers' }, - moveAccountSuccess: { id: 'migration.move_account.success', defaultMessage: 'Account successfully moved.' }, - moveAccountFail: { id: 'migration.move_account.fail', defaultMessage: 'Account migration failed.' }, - acctFieldLabel: { id: 'migration.fields.acct.label', defaultMessage: 'Handle of the new account' }, - acctFieldPlaceholder: { id: 'migration.fields.acct.placeholder', defaultMessage: 'username@domain' }, - currentPasswordFieldLabel: { id: 'migration.fields.confirm_password.label', defaultMessage: 'Current password' }, -}); - -export default @connect() -@injectIntl -class Migration extends ImmutablePureComponent { - - static propTypes = { - dispatch: PropTypes.func.isRequired, - intl: PropTypes.object.isRequired, - }; - - state = { - targetAccount: '', - password: '', - isLoading: false, - } - - handleInputChange = e => { - this.setState({ [e.target.name]: e.target.value }); - } - - clearForm = () => { - this.setState({ targetAccount: '', password: '' }); - } - - handleSubmit = e => { - const { targetAccount, password } = this.state; - const { dispatch, intl } = this.props; - this.setState({ isLoading: true }); - return dispatch(moveAccount(targetAccount, password)).then(() => { - this.clearForm(); - dispatch(snackbar.success(intl.formatMessage(messages.moveAccountSuccess))); - }).catch(error => { - dispatch(snackbar.error(intl.formatMessage(messages.moveAccountFail))); - }).then(() => { - this.setState({ isLoading: false }); - }); - } - - render() { - const { intl } = this.props; - - return ( - -
- - - - - ), - }} - /> - - - - - - - - -