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/__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/__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/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, +}; 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/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/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/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 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/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/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/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 = ( 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 = () => { } > - )} 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}

diff --git a/app/soapbox/features/status/index.tsx b/app/soapbox/features/status/index.tsx index 75ef1f7f9..804005fa5 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 {