From 68fa3fe33990c1c88c1c9154f0edef6af21a0336 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Fri, 27 May 2022 16:29:54 +0200 Subject: [PATCH 1/8] Fix hotkey navigation in threads MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- app/soapbox/features/status/index.tsx | 36 +++++++++++++-------------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/app/soapbox/features/status/index.tsx b/app/soapbox/features/status/index.tsx index ed762b7b5..3f762c31b 100644 --- a/app/soapbox/features/status/index.tsx +++ b/app/soapbox/features/status/index.tsx @@ -496,15 +496,15 @@ class Status extends ImmutablePureComponent { const { status, ancestorsIds, descendantsIds } = this.props; if (id === status.id) { - this._selectChild(ancestorsIds.size - 1, true); + this._selectChild(ancestorsIds.size - 1); } else { let index = ImmutableList(ancestorsIds).indexOf(id); if (index === -1) { index = ImmutableList(descendantsIds).indexOf(id); - this._selectChild(ancestorsIds.size + index, true); + this._selectChild(ancestorsIds.size + index); } else { - this._selectChild(index - 1, true); + this._selectChild(index - 1); } } } @@ -513,15 +513,15 @@ class Status extends ImmutablePureComponent { const { status, ancestorsIds, descendantsIds } = this.props; if (id === status.id) { - this._selectChild(ancestorsIds.size + 1, false); + this._selectChild(ancestorsIds.size + 1); } else { let index = ImmutableList(ancestorsIds).indexOf(id); if (index === -1) { index = ImmutableList(descendantsIds).indexOf(id); - this._selectChild(ancestorsIds.size + index + 2, false); + this._selectChild(ancestorsIds.size + index + 2); } else { - this._selectChild(index + 1, false); + this._selectChild(index + 1); } } } @@ -544,19 +544,18 @@ class Status extends ImmutablePureComponent { firstEmoji?.focus(); }; - _selectChild(index: number, align_top: boolean) { - const container = this.node; - if (!container) return; - const element = container.querySelectorAll('.focusable')[index] as HTMLButtonElement; + _selectChild(index: number) { + this.scroller?.scrollIntoView({ + index, + behavior: 'smooth', + done: () => { + const element = document.querySelector(`#thread [data-index="${index}"] .focusable`); - if (element) { - if (align_top && container.scrollTop > element.offsetTop) { - element.scrollIntoView(true); - } else if (!align_top && container.scrollTop + container.clientHeight < element.offsetTop + element.offsetHeight) { - element.scrollIntoView(false); - } - element.focus(); - } + if (element) { + element.focus(); + } + }, + }); } renderTombstone(id: string) { @@ -791,6 +790,7 @@ class Status extends ImmutablePureComponent {
Date: Fri, 27 May 2022 11:38:01 -0400 Subject: [PATCH 2/8] Convert about action to TypeScript --- .../{about-test.js => about.test.ts} | 0 app/soapbox/actions/about.js | 19 ------------ app/soapbox/actions/about.ts | 29 +++++++++++++++++++ 3 files changed, 29 insertions(+), 19 deletions(-) rename app/soapbox/actions/__tests__/{about-test.js => about.test.ts} (100%) delete mode 100644 app/soapbox/actions/about.js create mode 100644 app/soapbox/actions/about.ts diff --git a/app/soapbox/actions/__tests__/about-test.js b/app/soapbox/actions/__tests__/about.test.ts similarity index 100% rename from app/soapbox/actions/__tests__/about-test.js rename to app/soapbox/actions/__tests__/about.test.ts diff --git a/app/soapbox/actions/about.js b/app/soapbox/actions/about.js deleted file mode 100644 index 86be6beb4..000000000 --- a/app/soapbox/actions/about.js +++ /dev/null @@ -1,19 +0,0 @@ -import { staticClient } from '../api'; - -export const FETCH_ABOUT_PAGE_REQUEST = 'FETCH_ABOUT_PAGE_REQUEST'; -export const FETCH_ABOUT_PAGE_SUCCESS = 'FETCH_ABOUT_PAGE_SUCCESS'; -export const FETCH_ABOUT_PAGE_FAIL = 'FETCH_ABOUT_PAGE_FAIL'; - -export function fetchAboutPage(slug = 'index', locale) { - return (dispatch, getState) => { - dispatch({ type: FETCH_ABOUT_PAGE_REQUEST, slug, locale }); - const filename = `${slug}${locale ? `.${locale}` : ''}.html`; - return staticClient.get(`/instance/about/${filename}`).then(({ data: html }) => { - dispatch({ type: FETCH_ABOUT_PAGE_SUCCESS, slug, locale, html }); - return html; - }).catch(error => { - dispatch({ type: FETCH_ABOUT_PAGE_FAIL, slug, locale, error }); - throw error; - }); - }; -} diff --git a/app/soapbox/actions/about.ts b/app/soapbox/actions/about.ts new file mode 100644 index 000000000..37713c401 --- /dev/null +++ b/app/soapbox/actions/about.ts @@ -0,0 +1,29 @@ +import { AnyAction } from 'redux'; + +import { staticClient } from '../api'; + +const FETCH_ABOUT_PAGE_REQUEST = 'FETCH_ABOUT_PAGE_REQUEST'; +const FETCH_ABOUT_PAGE_SUCCESS = 'FETCH_ABOUT_PAGE_SUCCESS'; +const FETCH_ABOUT_PAGE_FAIL = 'FETCH_ABOUT_PAGE_FAIL'; + +const fetchAboutPage = (slug = 'index', locale?: string) => (dispatch: React.Dispatch, getState: any) => { + dispatch({ type: FETCH_ABOUT_PAGE_REQUEST, slug, locale }); + + const filename = `${slug}${locale ? `.${locale}` : ''}.html`; + return staticClient.get(`/instance/about/${filename}`) + .then(({ data: html }) => { + dispatch({ type: FETCH_ABOUT_PAGE_SUCCESS, slug, locale, html }); + return html; + }) + .catch(error => { + dispatch({ type: FETCH_ABOUT_PAGE_FAIL, slug, locale, error }); + throw error; + }); +}; + +export { + fetchAboutPage, + FETCH_ABOUT_PAGE_REQUEST, + FETCH_ABOUT_PAGE_SUCCESS, + FETCH_ABOUT_PAGE_FAIL, +}; From afe670b8fc8878291b82620442652326ca0ae8f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Fri, 27 May 2022 19:13:44 +0200 Subject: [PATCH 3/8] Use BirthdayInput on Edit profile page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- .../{birthday_input.js => birthday_input.tsx} | 126 +++++++++--------- .../components/registration_form.tsx | 11 +- app/soapbox/features/edit_profile/index.tsx | 10 +- 3 files changed, 73 insertions(+), 74 deletions(-) rename app/soapbox/components/{birthday_input.js => birthday_input.tsx} (56%) diff --git a/app/soapbox/components/birthday_input.js b/app/soapbox/components/birthday_input.tsx similarity index 56% rename from app/soapbox/components/birthday_input.js rename to app/soapbox/components/birthday_input.tsx index 912ba82fb..2b4e4833f 100644 --- a/app/soapbox/components/birthday_input.js +++ b/app/soapbox/components/birthday_input.tsx @@ -1,13 +1,10 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import ImmutablePureComponent from 'react-immutable-pure-component'; -import { defineMessages, injectIntl } from 'react-intl'; -import { connect } from 'react-redux'; +import React, { useMemo } from 'react'; +import { defineMessages, useIntl } from 'react-intl'; import IconButton from 'soapbox/components/icon_button'; import BundleContainer from 'soapbox/features/ui/containers/bundle_container'; import { DatePicker } from 'soapbox/features/ui/util/async-components'; -import { getFeatures } from 'soapbox/utils/features'; +import { useAppSelector, useFeatures } from 'soapbox/hooks'; const messages = defineMessages({ birthdayPlaceholder: { id: 'edit_profile.fields.birthday_placeholder', defaultMessage: 'Your birthday' }, @@ -17,29 +14,37 @@ const messages = defineMessages({ nextYear: { id: 'datepicker.next_year', defaultMessage: 'Next year' }, }); -const mapStateToProps = state => { - const features = getFeatures(state.get('instance')); +interface IBirthdayInput { + value?: string, + onChange: (value: string) => void, + required?: boolean, +} - return { - supportsBirthdays: features.birthdays, - minAge: state.getIn(['instance', 'pleroma', 'metadata', 'birthday_min_age']), - }; -}; +const BirthdayInput: React.FC = ({ value, onChange, required }) => { + const intl = useIntl(); + const features = useFeatures(); -export default @connect(mapStateToProps) -@injectIntl -class BirthdayInput extends ImmutablePureComponent { + const supportsBirthdays = features.birthdays; + const minAge = useAppSelector((state) => state.instance.getIn(['pleroma', 'metadata', 'birthday_min_age'])) as number; - static propTypes = { - hint: PropTypes.node, - required: PropTypes.bool, - supportsBirthdays: PropTypes.bool, - minAge: PropTypes.number, - onChange: PropTypes.func.isRequired, - value: PropTypes.instanceOf(Date), - }; + const maxDate = useMemo(() => { + if (!supportsBirthdays) return null; - renderHeader = ({ + let maxDate = new Date(); + maxDate = new Date(maxDate.getTime() - minAge * 1000 * 60 * 60 * 24 + maxDate.getTimezoneOffset() * 1000 * 60); + return maxDate; + }, [minAge]); + + const selected = useMemo(() => { + if (!supportsBirthdays || !value) return null; + + const date = new Date(value); + return new Date(date.getTime() + (date.getTimezoneOffset() * 60000)); + }, [value]); + + if (!supportsBirthdays) return null; + + const renderCustomHeader = ({ decreaseMonth, increaseMonth, prevMonthButtonDisabled, @@ -49,12 +54,20 @@ class BirthdayInput extends ImmutablePureComponent { prevYearButtonDisabled, nextYearButtonDisabled, date, + }: { + decreaseMonth(): void, + increaseMonth(): void, + prevMonthButtonDisabled: boolean, + nextMonthButtonDisabled: boolean, + decreaseYear(): void, + increaseYear(): void, + prevYearButtonDisabled: boolean, + nextYearButtonDisabled: boolean, + date: Date, }) => { - const { intl } = this.props; - return ( -
-
+
+
-
+
); - } + }; - render() { - const { intl, value, onChange, supportsBirthdays, hint, required, minAge } = this.props; + const handleChange = (date: Date) => onChange(new Date(date.getTime() - (date.getTimezoneOffset() * 60000)).toISOString().slice(0, 10)); - if (!supportsBirthdays) return null; + return ( +
+ + {Component => ()} + +
+ ); +}; - let maxDate = new Date(); - maxDate = new Date(maxDate.getTime() - minAge * 1000 * 60 * 60 * 24 + maxDate.getTimezoneOffset() * 1000 * 60); - - return ( -
- {hint && ( -
- {hint} -
- )} -
- - {Component => ()} - -
-
- ); - } - -} +export default BirthdayInput; diff --git a/app/soapbox/features/auth_login/components/registration_form.tsx b/app/soapbox/features/auth_login/components/registration_form.tsx index 22a7bbca9..0c3e048ce 100644 --- a/app/soapbox/features/auth_login/components/registration_form.tsx +++ b/app/soapbox/features/auth_login/components/registration_form.tsx @@ -58,7 +58,6 @@ const RegistrationForm: React.FC = ({ inviteToken }) => { const [usernameUnavailable, setUsernameUnavailable] = useState(false); const [passwordConfirmation, setPasswordConfirmation] = useState(''); const [passwordMismatch, setPasswordMismatch] = useState(false); - const [birthday, setBirthday] = useState(undefined); const source = useRef(axios.CancelToken.source()); @@ -111,8 +110,8 @@ const RegistrationForm: React.FC = ({ inviteToken }) => { setPasswordMismatch(!passwordsMatch()); }; - const onBirthdayChange = (newBirthday: Date) => { - setBirthday(newBirthday); + const onBirthdayChange = (birthday: string) => { + updateParams({ birthday }); }; const launchModal = () => { @@ -187,10 +186,6 @@ const RegistrationForm: React.FC = ({ inviteToken }) => { if (inviteToken) { params.set('token', inviteToken); } - - if (birthday) { - params.set('birthday', new Date(birthday.getTime() - (birthday.getTimezoneOffset() * 60000)).toISOString().slice(0, 10)); - } }); setSubmissionLoading(true); @@ -291,7 +286,7 @@ const RegistrationForm: React.FC = ({ inviteToken }) => { {birthdayRequired && ( diff --git a/app/soapbox/features/edit_profile/index.tsx b/app/soapbox/features/edit_profile/index.tsx index d7f60b572..788031852 100644 --- a/app/soapbox/features/edit_profile/index.tsx +++ b/app/soapbox/features/edit_profile/index.tsx @@ -4,6 +4,7 @@ import { defineMessages, useIntl, FormattedMessage } from 'react-intl'; import { updateNotificationSettings } from 'soapbox/actions/accounts'; import { patchMe } from 'soapbox/actions/me'; import snackbar from 'soapbox/actions/snackbar'; +import BirthdayInput from 'soapbox/components/birthday_input'; import List, { ListItem } from 'soapbox/components/list'; import { useAppSelector, useAppDispatch, useOwnAccount, useFeatures } from 'soapbox/hooks'; import { normalizeAccount } from 'soapbox/normalizers'; @@ -242,6 +243,10 @@ const EditProfile: React.FC = () => { }; }; + const handleBirthdayChange = (date: string) => { + updateData('birthday', date); + }; + const handleHideNetworkChange: React.ChangeEventHandler = e => { const hide = e.target.checked; @@ -325,10 +330,9 @@ const EditProfile: React.FC = () => { } > - )} From 8433ff70b07aef725dd004aed3bce8f01d4183d5 Mon Sep 17 00:00:00 2001 From: Justin Date: Fri, 27 May 2022 14:08:41 -0400 Subject: [PATCH 4/8] Add tests for account-notes action --- .../actions/__tests__/account-notes.test.ts | 106 ++++++++++++++++++ app/soapbox/actions/account-notes.ts | 82 ++++++++++++++ app/soapbox/actions/account_notes.js | 67 ----------- app/soapbox/actions/modals.ts | 2 +- .../containers/header_container.js | 2 +- .../ui/components/account_note_modal.js | 2 +- app/soapbox/reducers/account_notes.ts | 4 +- app/soapbox/reducers/relationships.js | 2 +- 8 files changed, 194 insertions(+), 73 deletions(-) create mode 100644 app/soapbox/actions/__tests__/account-notes.test.ts create mode 100644 app/soapbox/actions/account-notes.ts delete mode 100644 app/soapbox/actions/account_notes.js diff --git a/app/soapbox/actions/__tests__/account-notes.test.ts b/app/soapbox/actions/__tests__/account-notes.test.ts new file mode 100644 index 000000000..e173fd17f --- /dev/null +++ b/app/soapbox/actions/__tests__/account-notes.test.ts @@ -0,0 +1,106 @@ +import { __stub } from 'soapbox/api'; +import { mockStore } from 'soapbox/jest/test-helpers'; +import rootReducer from 'soapbox/reducers'; + +import { normalizeAccount } from '../../normalizers'; +import { changeAccountNoteComment, initAccountNoteModal, submitAccountNote } from '../account-notes'; + +describe('submitAccountNote()', () => { + let store; + + beforeEach(() => { + const state = rootReducer(undefined, {}) + .set('account_notes', { edit: { account_id: 1, comment: 'hello' } }); + store = mockStore(state); + }); + + describe('with a successful API request', () => { + beforeEach(() => { + __stub((mock) => { + mock.onPost('/api/v1/accounts/1/note').reply(200, {}); + }); + }); + + it('post the note to the API', async() => { + const expectedActions = [ + { type: 'ACCOUNT_NOTE_SUBMIT_REQUEST' }, + { type: 'MODAL_CLOSE', modalType: undefined }, + { type: 'ACCOUNT_NOTE_SUBMIT_SUCCESS', relationship: {} }, + ]; + await store.dispatch(submitAccountNote()); + const actions = store.getActions(); + + expect(actions).toEqual(expectedActions); + }); + }); + + describe('with an unsuccessful API request', () => { + beforeEach(() => { + __stub((mock) => { + mock.onPost('/api/v1/accounts/1/note').networkError(); + }); + }); + + it('should dispatch failed action', async() => { + const expectedActions = [ + { type: 'ACCOUNT_NOTE_SUBMIT_REQUEST' }, + { + type: 'ACCOUNT_NOTE_SUBMIT_FAIL', + error: new Error('Network Error'), + }, + ]; + await store.dispatch(submitAccountNote()); + const actions = store.getActions(); + + expect(actions).toEqual(expectedActions); + }); + }); +}); + +describe('initAccountNoteModal()', () => { + let store; + + beforeEach(() => { + const state = rootReducer(undefined, {}) + .set('relationships', { 1: { note: 'hello' } }); + store = mockStore(state); + }); + + it('dispatches the proper actions', async() => { + const account = normalizeAccount({ + id: '1', + acct: 'justin-username', + display_name: 'Justin L', + avatar: 'test.jpg', + verified: true, + }); + const expectedActions = [ + { type: 'ACCOUNT_NOTE_INIT_MODAL', account, comment: 'hello' }, + { type: 'MODAL_OPEN', modalType: 'ACCOUNT_NOTE' }, + ]; + await store.dispatch(initAccountNoteModal(account)); + const actions = store.getActions(); + + expect(actions).toEqual(expectedActions); + }); +}); + +describe('changeAccountNoteComment()', () => { + let store; + + beforeEach(() => { + const state = rootReducer(undefined, {}); + store = mockStore(state); + }); + + it('dispatches the proper actions', async() => { + const comment = 'hello world'; + const expectedActions = [ + { type: 'ACCOUNT_NOTE_CHANGE_COMMENT', comment }, + ]; + await store.dispatch(changeAccountNoteComment(comment)); + const actions = store.getActions(); + + expect(actions).toEqual(expectedActions); + }); +}); diff --git a/app/soapbox/actions/account-notes.ts b/app/soapbox/actions/account-notes.ts new file mode 100644 index 000000000..bb1cc72ae --- /dev/null +++ b/app/soapbox/actions/account-notes.ts @@ -0,0 +1,82 @@ +import { AxiosError } from 'axios'; +import { AnyAction } from 'redux'; + +import api from '../api'; + +import { openModal, closeModal } from './modals'; + +import type { Account } from 'soapbox/types/entities'; + +const ACCOUNT_NOTE_SUBMIT_REQUEST = 'ACCOUNT_NOTE_SUBMIT_REQUEST'; +const ACCOUNT_NOTE_SUBMIT_SUCCESS = 'ACCOUNT_NOTE_SUBMIT_SUCCESS'; +const ACCOUNT_NOTE_SUBMIT_FAIL = 'ACCOUNT_NOTE_SUBMIT_FAIL'; + +const ACCOUNT_NOTE_INIT_MODAL = 'ACCOUNT_NOTE_INIT_MODAL'; + +const ACCOUNT_NOTE_CHANGE_COMMENT = 'ACCOUNT_NOTE_CHANGE_COMMENT'; + +const submitAccountNote = () => (dispatch: React.Dispatch, getState: any) => { + dispatch(submitAccountNoteRequest()); + + const id = getState().getIn(['account_notes', 'edit', 'account_id']); + + return api(getState) + .post(`/api/v1/accounts/${id}/note`, { + comment: getState().getIn(['account_notes', 'edit', 'comment']), + }) + .then(response => { + dispatch(closeModal()); + dispatch(submitAccountNoteSuccess(response.data)); + }) + .catch(error => dispatch(submitAccountNoteFail(error))); +}; + +function submitAccountNoteRequest() { + return { + type: ACCOUNT_NOTE_SUBMIT_REQUEST, + }; +} + +function submitAccountNoteSuccess(relationship: any) { + return { + type: ACCOUNT_NOTE_SUBMIT_SUCCESS, + relationship, + }; +} + +function submitAccountNoteFail(error: AxiosError) { + return { + type: ACCOUNT_NOTE_SUBMIT_FAIL, + error, + }; +} + +const initAccountNoteModal = (account: Account) => (dispatch: React.Dispatch, getState: any) => { + const comment = getState().getIn(['relationships', account.get('id'), 'note']); + + dispatch({ + type: ACCOUNT_NOTE_INIT_MODAL, + account, + comment, + }); + + dispatch(openModal('ACCOUNT_NOTE')); +}; + +function changeAccountNoteComment(comment: string) { + return { + type: ACCOUNT_NOTE_CHANGE_COMMENT, + comment, + }; +} + +export { + submitAccountNote, + initAccountNoteModal, + changeAccountNoteComment, + ACCOUNT_NOTE_SUBMIT_REQUEST, + ACCOUNT_NOTE_SUBMIT_SUCCESS, + ACCOUNT_NOTE_SUBMIT_FAIL, + ACCOUNT_NOTE_INIT_MODAL, + ACCOUNT_NOTE_CHANGE_COMMENT, +}; diff --git a/app/soapbox/actions/account_notes.js b/app/soapbox/actions/account_notes.js deleted file mode 100644 index d6aeefc49..000000000 --- a/app/soapbox/actions/account_notes.js +++ /dev/null @@ -1,67 +0,0 @@ -import api from '../api'; - -import { openModal, closeModal } from './modals'; - -export const ACCOUNT_NOTE_SUBMIT_REQUEST = 'ACCOUNT_NOTE_SUBMIT_REQUEST'; -export const ACCOUNT_NOTE_SUBMIT_SUCCESS = 'ACCOUNT_NOTE_SUBMIT_SUCCESS'; -export const ACCOUNT_NOTE_SUBMIT_FAIL = 'ACCOUNT_NOTE_SUBMIT_FAIL'; - -export const ACCOUNT_NOTE_INIT_MODAL = 'ACCOUNT_NOTE_INIT_MODAL'; - -export const ACCOUNT_NOTE_CHANGE_COMMENT = 'ACCOUNT_NOTE_CHANGE_COMMENT'; - -export function submitAccountNote() { - return (dispatch, getState) => { - dispatch(submitAccountNoteRequest()); - - const id = getState().getIn(['account_notes', 'edit', 'account_id']); - - api(getState).post(`/api/v1/accounts/${id}/note`, { - comment: getState().getIn(['account_notes', 'edit', 'comment']), - }).then(response => { - dispatch(closeModal()); - dispatch(submitAccountNoteSuccess(response.data)); - }).catch(error => dispatch(submitAccountNoteFail(error))); - }; -} - -export function submitAccountNoteRequest() { - return { - type: ACCOUNT_NOTE_SUBMIT_REQUEST, - }; -} - -export function submitAccountNoteSuccess(relationship) { - return { - type: ACCOUNT_NOTE_SUBMIT_SUCCESS, - relationship, - }; -} - -export function submitAccountNoteFail(error) { - return { - type: ACCOUNT_NOTE_SUBMIT_FAIL, - error, - }; -} - -export function initAccountNoteModal(account) { - return (dispatch, getState) => { - const comment = getState().getIn(['relationships', account.get('id'), 'note']); - - dispatch({ - type: ACCOUNT_NOTE_INIT_MODAL, - account, - comment, - }); - - dispatch(openModal('ACCOUNT_NOTE')); - }; -} - -export function changeAccountNoteComment(comment) { - return { - type: ACCOUNT_NOTE_CHANGE_COMMENT, - comment, - }; -} \ No newline at end of file diff --git a/app/soapbox/actions/modals.ts b/app/soapbox/actions/modals.ts index 9d6e85139..3e1a106cf 100644 --- a/app/soapbox/actions/modals.ts +++ b/app/soapbox/actions/modals.ts @@ -11,7 +11,7 @@ export function openModal(type: string, props?: any) { } /** Close the modal */ -export function closeModal(type: string) { +export function closeModal(type?: string) { return { type: MODAL_CLOSE, modalType: type, diff --git a/app/soapbox/features/account_timeline/containers/header_container.js b/app/soapbox/features/account_timeline/containers/header_container.js index baf0ccb17..0ffa9b81b 100644 --- a/app/soapbox/features/account_timeline/containers/header_container.js +++ b/app/soapbox/features/account_timeline/containers/header_container.js @@ -2,7 +2,7 @@ import React from 'react'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import { connect } from 'react-redux'; -import { initAccountNoteModal } from 'soapbox/actions/account_notes'; +import { initAccountNoteModal } from 'soapbox/actions/account-notes'; import { followAccount, unfollowAccount, diff --git a/app/soapbox/features/ui/components/account_note_modal.js b/app/soapbox/features/ui/components/account_note_modal.js index 2c7ed11ed..9242b7b8f 100644 --- a/app/soapbox/features/ui/components/account_note_modal.js +++ b/app/soapbox/features/ui/components/account_note_modal.js @@ -3,7 +3,7 @@ import React from 'react'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import { connect } from 'react-redux'; -import { changeAccountNoteComment, submitAccountNote } from 'soapbox/actions/account_notes'; +import { changeAccountNoteComment, submitAccountNote } from 'soapbox/actions/account-notes'; import { closeModal } from 'soapbox/actions/modals'; import { Modal, Text } from 'soapbox/components/ui'; import { makeGetAccount } from 'soapbox/selectors'; diff --git a/app/soapbox/reducers/account_notes.ts b/app/soapbox/reducers/account_notes.ts index 07d5cc89e..446b04ca9 100644 --- a/app/soapbox/reducers/account_notes.ts +++ b/app/soapbox/reducers/account_notes.ts @@ -7,7 +7,7 @@ import { ACCOUNT_NOTE_SUBMIT_REQUEST, ACCOUNT_NOTE_SUBMIT_FAIL, ACCOUNT_NOTE_SUBMIT_SUCCESS, -} from '../actions/account_notes'; +} from '../actions/account-notes'; const EditRecord = ImmutableRecord({ isSubmitting: false, @@ -39,4 +39,4 @@ export default function account_notes(state: State = ReducerRecord(), action: An default: return state; } -} \ No newline at end of file +} diff --git a/app/soapbox/reducers/relationships.js b/app/soapbox/reducers/relationships.js index 80754842c..a846b8199 100644 --- a/app/soapbox/reducers/relationships.js +++ b/app/soapbox/reducers/relationships.js @@ -3,7 +3,7 @@ import { get } from 'lodash'; import { STREAMING_FOLLOW_RELATIONSHIPS_UPDATE } from 'soapbox/actions/streaming'; -import { ACCOUNT_NOTE_SUBMIT_SUCCESS } from '../actions/account_notes'; +import { ACCOUNT_NOTE_SUBMIT_SUCCESS } from '../actions/account-notes'; import { ACCOUNT_FOLLOW_SUCCESS, ACCOUNT_FOLLOW_REQUEST, From 3972e16e439f03460b2a6fc68013a940ae69328a Mon Sep 17 00:00:00 2001 From: Justin Date: Fri, 27 May 2022 15:22:20 -0400 Subject: [PATCH 5/8] Add tests for alerts action --- app/soapbox/actions/__tests__/alerts.test.ts | 149 +++++++++++++++++++ app/soapbox/actions/alerts.js | 68 --------- app/soapbox/actions/alerts.ts | 74 +++++++++ app/soapbox/actions/snackbar.ts | 2 +- 4 files changed, 224 insertions(+), 69 deletions(-) create mode 100644 app/soapbox/actions/__tests__/alerts.test.ts delete mode 100644 app/soapbox/actions/alerts.js create mode 100644 app/soapbox/actions/alerts.ts diff --git a/app/soapbox/actions/__tests__/alerts.test.ts b/app/soapbox/actions/__tests__/alerts.test.ts new file mode 100644 index 000000000..f2419893a --- /dev/null +++ b/app/soapbox/actions/__tests__/alerts.test.ts @@ -0,0 +1,149 @@ +import { AxiosError } from 'axios'; + +import { mockStore } from 'soapbox/jest/test-helpers'; +import rootReducer from 'soapbox/reducers'; + +import { dismissAlert, showAlert, showAlertForError } from '../alerts'; + +const buildError = (message: string, status: number) => new AxiosError(message, String(status), null, null, { + data: { + error: message, + }, + statusText: String(status), + status, + headers: {}, + config: {}, +}); + +let store; + +beforeEach(() => { + const state = rootReducer(undefined, {}); + store = mockStore(state); +}); + +describe('dismissAlert()', () => { + it('dispatches the proper actions', async() => { + const alert = 'hello world'; + const expectedActions = [ + { type: 'ALERT_DISMISS', alert }, + ]; + await store.dispatch(dismissAlert(alert)); + const actions = store.getActions(); + + expect(actions).toEqual(expectedActions); + }); +}); + +describe('showAlert()', () => { + it('dispatches the proper actions', async() => { + const title = 'title'; + const message = 'msg'; + const severity = 'info'; + const expectedActions = [ + { type: 'ALERT_SHOW', title, message, severity }, + ]; + await store.dispatch(showAlert(title, message, severity)); + const actions = store.getActions(); + + expect(actions).toEqual(expectedActions); + }); +}); + +describe('showAlert()', () => { + describe('with a 502 status code', () => { + it('dispatches the proper actions', async() => { + const message = 'The server is down'; + const error = buildError(message, 502); + + const expectedActions = [ + { type: 'ALERT_SHOW', title: '', message, severity: 'error' }, + ]; + await store.dispatch(showAlertForError(error)); + const actions = store.getActions(); + + expect(actions).toEqual(expectedActions); + }); + }); + + describe('with a 404 status code', () => { + it('dispatches the proper actions', async() => { + const error = buildError('', 404); + + const expectedActions = []; + await store.dispatch(showAlertForError(error)); + const actions = store.getActions(); + + expect(actions).toEqual(expectedActions); + }); + }); + + describe('with a 410 status code', () => { + it('dispatches the proper actions', async() => { + const error = buildError('', 410); + + const expectedActions = []; + await store.dispatch(showAlertForError(error)); + const actions = store.getActions(); + + expect(actions).toEqual(expectedActions); + }); + }); + + describe('with an accepted status code', () => { + describe('with a message from the server', () => { + it('dispatches the proper actions', async() => { + const message = 'custom message'; + const error = buildError(message, 200); + + const expectedActions = [ + { type: 'ALERT_SHOW', title: '', message, severity: 'error' }, + ]; + await store.dispatch(showAlertForError(error)); + const actions = store.getActions(); + + expect(actions).toEqual(expectedActions); + }); + }); + + describe('without a message from the server', () => { + it('dispatches the proper actions', async() => { + const message = 'The request has been accepted for processing'; + const error = buildError(message, 202); + + const expectedActions = [ + { type: 'ALERT_SHOW', title: '', message, severity: 'error' }, + ]; + await store.dispatch(showAlertForError(error)); + const actions = store.getActions(); + + expect(actions).toEqual(expectedActions); + }); + }); + }); + + describe('without a response', () => { + it('dispatches the proper actions', async() => { + const error = new AxiosError(); + + const expectedActions = [ + { + type: 'ALERT_SHOW', + title: { + defaultMessage: 'Oops!', + id: 'alert.unexpected.title', + }, + message: { + defaultMessage: 'An unexpected error occurred.', + id: 'alert.unexpected.message', + }, + severity: 'error', + }, + ]; + await store.dispatch(showAlertForError(error)); + const actions = store.getActions(); + + expect(actions).toEqual(expectedActions); + }); + }); +}); diff --git a/app/soapbox/actions/alerts.js b/app/soapbox/actions/alerts.js deleted file mode 100644 index c71ce3e87..000000000 --- a/app/soapbox/actions/alerts.js +++ /dev/null @@ -1,68 +0,0 @@ -import { defineMessages } from 'react-intl'; - -import { httpErrorMessages } from 'soapbox/utils/errors'; - -const messages = defineMessages({ - unexpectedTitle: { id: 'alert.unexpected.title', defaultMessage: 'Oops!' }, - unexpectedMessage: { id: 'alert.unexpected.message', defaultMessage: 'An unexpected error occurred.' }, -}); - -export const ALERT_SHOW = 'ALERT_SHOW'; -export const ALERT_DISMISS = 'ALERT_DISMISS'; -export const ALERT_CLEAR = 'ALERT_CLEAR'; - -const noOp = () => {}; - -export function dismissAlert(alert) { - return { - type: ALERT_DISMISS, - alert, - }; -} - -export function clearAlert() { - return { - type: ALERT_CLEAR, - }; -} - -export function showAlert(title = messages.unexpectedTitle, message = messages.unexpectedMessage, severity = 'info') { - return { - type: ALERT_SHOW, - title, - message, - severity, - }; -} - -export function showAlertForError(error) { - return (dispatch, _getState) => { - if (error.response) { - const { data, status, statusText } = error.response; - - if (status === 502) { - return dispatch(showAlert('', 'The server is down', 'error')); - } - - if (status === 404 || status === 410) { - // Skip these errors as they are reflected in the UI - return dispatch(noOp); - } - - let message = statusText; - - if (data.error) { - message = data.error; - } - - if (!message) { - message = httpErrorMessages.find((httpError) => httpError.code === status)?.description; - } - - return dispatch(showAlert('', message, 'error')); - } else { - console.error(error); - return dispatch(showAlert(undefined, undefined, 'error')); - } - }; -} diff --git a/app/soapbox/actions/alerts.ts b/app/soapbox/actions/alerts.ts new file mode 100644 index 000000000..b0af2af35 --- /dev/null +++ b/app/soapbox/actions/alerts.ts @@ -0,0 +1,74 @@ +import { AnyAction } from '@reduxjs/toolkit'; +import { AxiosError } from 'axios'; +import { defineMessages, MessageDescriptor } from 'react-intl'; + +import { httpErrorMessages } from 'soapbox/utils/errors'; + +import { SnackbarActionSeverity } from './snackbar'; + +const messages = defineMessages({ + unexpectedTitle: { id: 'alert.unexpected.title', defaultMessage: 'Oops!' }, + unexpectedMessage: { id: 'alert.unexpected.message', defaultMessage: 'An unexpected error occurred.' }, +}); + +export const ALERT_SHOW = 'ALERT_SHOW'; +export const ALERT_DISMISS = 'ALERT_DISMISS'; +export const ALERT_CLEAR = 'ALERT_CLEAR'; + +const noOp = () => { }; + +function dismissAlert(alert: any) { + return { + type: ALERT_DISMISS, + alert, + }; +} + +function showAlert( + title: MessageDescriptor | string = messages.unexpectedTitle, + message: MessageDescriptor | string = messages.unexpectedMessage, + severity: SnackbarActionSeverity = 'info', +) { + return { + type: ALERT_SHOW, + title, + message, + severity, + }; +} + +const showAlertForError = (error: AxiosError) => (dispatch: React.Dispatch, _getState: any) => { + if (error.response) { + const { data, status, statusText } = error.response; + + if (status === 502) { + return dispatch(showAlert('', 'The server is down', 'error')); + } + + if (status === 404 || status === 410) { + // Skip these errors as they are reflected in the UI + return dispatch(noOp as any); + } + + let message: string | undefined = statusText; + + if (data.error) { + message = data.error; + } + + if (!message) { + message = httpErrorMessages.find((httpError) => httpError.code === status)?.description; + } + + return dispatch(showAlert('', message, 'error')); + } else { + console.error(error); + return dispatch(showAlert(undefined, undefined, 'error')); + } +}; + +export { + dismissAlert, + showAlert, + showAlertForError, +}; diff --git a/app/soapbox/actions/snackbar.ts b/app/soapbox/actions/snackbar.ts index d1cda0d94..d4238cf33 100644 --- a/app/soapbox/actions/snackbar.ts +++ b/app/soapbox/actions/snackbar.ts @@ -2,7 +2,7 @@ import { ALERT_SHOW } from './alerts'; import type { MessageDescriptor } from 'react-intl'; -type SnackbarActionSeverity = 'info' | 'success' | 'error' +export type SnackbarActionSeverity = 'info' | 'success' | 'error' type SnackbarMessage = string | MessageDescriptor From c1227079acd625dac6c7fa1698cde89852441b2a Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 27 May 2022 20:14:41 -0400 Subject: [PATCH 6/8] Chats: fix unread counter --- app/soapbox/features/chats/components/chat_panes.js | 8 ++++++-- app/soapbox/features/chats/components/chat_window.js | 8 ++++---- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/app/soapbox/features/chats/components/chat_panes.js b/app/soapbox/features/chats/components/chat_panes.js index 900873e64..8aab35825 100644 --- a/app/soapbox/features/chats/components/chat_panes.js +++ b/app/soapbox/features/chats/components/chat_panes.js @@ -12,8 +12,8 @@ import { createSelector } from 'reselect'; import { openChat, launchChat, toggleMainWindow } from 'soapbox/actions/chats'; import { getSettings } from 'soapbox/actions/settings'; import AccountSearch from 'soapbox/components/account_search'; +import { Counter } from 'soapbox/components/ui'; import AudioToggle from 'soapbox/features/chats/components/audio_toggle'; -import { shortNumberFormat } from 'soapbox/utils/numbers'; import ChatList from './chat_list'; import ChatWindow from './chat_window'; @@ -83,7 +83,11 @@ class ChatPanes extends ImmutablePureComponent { const mainWindowPane = (
- {unreadCount > 0 && {shortNumberFormat(unreadCount)}} + {unreadCount > 0 && ( +
+ +
+ )} diff --git a/app/soapbox/features/chats/components/chat_window.js b/app/soapbox/features/chats/components/chat_window.js index 4d811fe15..e525e3432 100644 --- a/app/soapbox/features/chats/components/chat_window.js +++ b/app/soapbox/features/chats/components/chat_window.js @@ -13,9 +13,9 @@ import { import Avatar from 'soapbox/components/avatar'; import HoverRefWrapper from 'soapbox/components/hover_ref_wrapper'; import IconButton from 'soapbox/components/icon_button'; +import { Counter } from 'soapbox/components/ui'; import { makeGetChat } from 'soapbox/selectors'; import { getAcct } from 'soapbox/utils/accounts'; -import { shortNumberFormat } from 'soapbox/utils/numbers'; import { displayFqn } from 'soapbox/utils/state'; import ChatBox from './chat_box'; @@ -98,9 +98,9 @@ class ChatWindow extends ImmutablePureComponent { const unreadCount = chat.get('unread'); const unreadIcon = ( - - {shortNumberFormat(unreadCount)} - +
+ +
); const avatar = ( From 139cd8f719d518574feeabe030bf6606f901b2ac Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 27 May 2022 20:22:56 -0400 Subject: [PATCH 7/8] Chats: fix audio toggle styles --- app/styles/chats.scss | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/app/styles/chats.scss b/app/styles/chats.scss index f41aaf4e9..ba712010b 100644 --- a/app/styles/chats.scss +++ b/app/styles/chats.scss @@ -104,25 +104,20 @@ } .audio-toggle .react-toggle-thumb { - height: 14px; - width: 14px; - border: 1px solid var(--brand-color--med); + @apply w-3.5 h-3.5 border border-solid border-primary-400; } .audio-toggle .react-toggle { - height: 16px; - top: 4px; + @apply top-1; } .audio-toggle .react-toggle-track { - height: 16px; - width: 34px; - background-color: var(--accent-color); + @apply h-4 w-8 bg-accent-500; } .audio-toggle .react-toggle-track-check { - left: 2px; - bottom: 5px; + left: 4px; + bottom: 0; } .react-toggle--checked .react-toggle-thumb { @@ -130,8 +125,8 @@ } .audio-toggle .react-toggle-track-x { - right: 8px; - bottom: 5px; + right: 5px; + bottom: 0; } .fa { From 973492d96fe410830633b135cb47f726a9056301 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 27 May 2022 20:50:51 -0400 Subject: [PATCH 8/8] Improve very long title on homepage --- app/soapbox/features/landing_page/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/soapbox/features/landing_page/index.tsx b/app/soapbox/features/landing_page/index.tsx index 9deec98cb..26f41f086 100644 --- a/app/soapbox/features/landing_page/index.tsx +++ b/app/soapbox/features/landing_page/index.tsx @@ -73,9 +73,9 @@ const LandingPage = () => {
-
+
-

+

{instance.title}