From 5149ed13426776f3ac56be1498ee861931087e4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Thu, 12 May 2022 19:16:59 +0200 Subject: [PATCH 001/102] Fix external links in sidebar/sidenav 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-navigation.tsx | 3 ++- app/soapbox/components/sidebar_menu.tsx | 27 ++++++++++++++----- 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/app/soapbox/components/sidebar-navigation.tsx b/app/soapbox/components/sidebar-navigation.tsx index 6839ba8a3..39eea4ec4 100644 --- a/app/soapbox/components/sidebar-navigation.tsx +++ b/app/soapbox/components/sidebar-navigation.tsx @@ -57,9 +57,10 @@ const SidebarNavigation = () => { if (instance.invites_enabled) { menu.push({ - to: `${baseURL}/invites`, + href: `${baseURL}/invites`, icon: require('@tabler/icons/icons/mailbox.svg'), text: , + newTab: true, }); } diff --git a/app/soapbox/components/sidebar_menu.tsx b/app/soapbox/components/sidebar_menu.tsx index 2a0946e6e..15661c017 100644 --- a/app/soapbox/components/sidebar_menu.tsx +++ b/app/soapbox/components/sidebar_menu.tsx @@ -42,14 +42,15 @@ const messages = defineMessages({ }); interface ISidebarLink { - to: string, + href?: string, + to?: string, icon: string, text: string | JSX.Element, onClick: React.EventHandler, } -const SidebarLink: React.FC = ({ to, icon, text, onClick }) => ( - +const SidebarLink: React.FC = ({ href, to, icon, text, onClick }) => { + const body = (
@@ -57,8 +58,22 @@ const SidebarLink: React.FC = ({ to, icon, text, onClick }) => ( {text} - -); + ); + + if (to) { + return ( + + {body} + + ); + } + + return ( + + {body} + + ); +}; const getOtherAccounts = makeGetOtherAccounts(); @@ -228,7 +243,7 @@ const SidebarMenu: React.FC = (): JSX.Element | null => { {instance.invites_enabled && ( Date: Sun, 15 May 2022 15:11:59 +0200 Subject: [PATCH 002/102] Mastodon admin API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- app/soapbox/actions/admin.js | 136 ++++++++++++++++-- .../features/admin/components/report.tsx | 41 +++--- .../admin/components/report_status.tsx | 5 +- .../admin/components/unapproved_account.tsx | 4 +- app/soapbox/features/admin/tabs/reports.tsx | 2 +- app/soapbox/features/admin/user_index.js | 10 +- app/soapbox/features/ui/index.tsx | 2 +- app/soapbox/normalizers/admin_account.ts | 55 +++++++ app/soapbox/normalizers/admin_report.ts | 51 +++++++ app/soapbox/normalizers/index.ts | 2 + app/soapbox/reducers/accounts.ts | 2 +- app/soapbox/reducers/admin.ts | 75 +++++++--- app/soapbox/selectors/index.ts | 17 ++- app/soapbox/types/entities.ts | 6 + app/soapbox/utils/features.ts | 14 ++ 15 files changed, 361 insertions(+), 61 deletions(-) create mode 100644 app/soapbox/normalizers/admin_account.ts create mode 100644 app/soapbox/normalizers/admin_report.ts diff --git a/app/soapbox/actions/admin.js b/app/soapbox/actions/admin.js index 3704e114e..313cf6203 100644 --- a/app/soapbox/actions/admin.js +++ b/app/soapbox/actions/admin.js @@ -1,7 +1,8 @@ import { fetchRelationships } from 'soapbox/actions/accounts'; -import { importFetchedAccount, importFetchedStatuses } from 'soapbox/actions/importer'; +import { importFetchedAccount, importFetchedAccounts, importFetchedStatuses } from 'soapbox/actions/importer'; +import { getFeatures } from 'soapbox/utils/features'; -import api from '../api'; +import api, { getLinks } from '../api'; export const ADMIN_CONFIG_FETCH_REQUEST = 'ADMIN_CONFIG_FETCH_REQUEST'; export const ADMIN_CONFIG_FETCH_SUCCESS = 'ADMIN_CONFIG_FETCH_SUCCESS'; @@ -99,11 +100,36 @@ export function updateConfig(configs) { }; } -export function fetchReports(params) { +export function fetchReports(params = {}) { return (dispatch, getState) => { + const state = getState(); + + const instance = state.get('instance'); + const features = getFeatures(instance); + dispatch({ type: ADMIN_REPORTS_FETCH_REQUEST, params }); + + if (features.mastodonAdminApi) { + return api(getState) + .get('/api/v1/admin/reports', { params }) + .then(({ data: reports }) => { + reports.forEach(report => { + dispatch(importFetchedAccount(report.account?.account)); + dispatch(importFetchedAccount(report.target_account?.account)); + dispatch(importFetchedStatuses(report.statuses)); + }); + dispatch({ type: ADMIN_REPORTS_FETCH_SUCCESS, reports, params }); + }).catch(error => { + dispatch({ type: ADMIN_REPORTS_FETCH_FAIL, error, params }); + }); + } + + const { resolved } = params; + return api(getState) - .get('/api/pleroma/admin/reports', { params }) + .get('/api/pleroma/admin/reports', { params: { + state: resolved === false ? 'open' : (resolved ? 'resolved' : null), + } }) .then(({ data: { reports } }) => { reports.forEach(report => { dispatch(importFetchedAccount(report.account)); @@ -118,9 +144,27 @@ export function fetchReports(params) { } function patchReports(ids, state) { - const reports = ids.map(id => ({ id, state })); return (dispatch, getState) => { + const state = getState(); + + const instance = state.get('instance'); + const features = getFeatures(instance); + + const reports = ids.map(id => ({ id, state })); + dispatch({ type: ADMIN_REPORTS_PATCH_REQUEST, reports }); + + if (features.mastodonAdminApi) { + return Promise.all(ids.map(id => api(getState) + .post(`/api/v1/admin/reports/${id}/${state === 'resolved' ? 'reopen' : 'resolve'}`) + .then(({ data }) => { + dispatch({ type: ADMIN_REPORTS_PATCH_SUCCESS, reports }); + }).catch(error => { + dispatch({ type: ADMIN_REPORTS_PATCH_FAIL, error, reports }); + }), + )); + } + return api(getState) .patch('/api/pleroma/admin/reports', { reports }) .then(() => { @@ -134,12 +178,45 @@ export function closeReports(ids) { return patchReports(ids, 'closed'); } -export function fetchUsers(filters = [], page = 1, query, pageSize = 50) { +export function fetchUsers(filters = [], page = 1, query, pageSize = 50, next) { return (dispatch, getState) => { + const state = getState(); + + const instance = state.get('instance'); + const features = getFeatures(instance); + + dispatch({ type: ADMIN_USERS_FETCH_REQUEST, filters, page, pageSize }); + + if (features.mastodonAdminApi) { + const params = { + username: query, + }; + + if (filters.includes('local')) params.local = true; + if (filters.includes('active')) params.active = true; + if (filters.includes('need_approval')) params.pending = true; + + return api(getState) + .get(next || '/api/v1/admin/accounts', { params }) + .then(({ data: accounts, ...response }) => { + const next = getLinks(response).refs.find(link => link.rel === 'next'); + + const count = next + ? page * pageSize + 1 + : (page - 1) * pageSize + accounts.length; + + dispatch(importFetchedAccounts(accounts.map(({ account }) => account))); + dispatch(fetchRelationships(accounts.map(account => account.id))); + dispatch({ type: ADMIN_USERS_FETCH_SUCCESS, users: accounts, count, pageSize, filters, page, next: next?.uri || false }); + return { users: accounts, count, pageSize, next: next?.uri || false }; + }).catch(error => { + dispatch({ type: ADMIN_USERS_FETCH_FAIL, error, filters, page, pageSize }); + }); + } + const params = { filters: filters.join(), page, page_size: pageSize }; if (query) params.query = query; - dispatch({ type: ADMIN_USERS_FETCH_REQUEST, filters, page, pageSize }); return api(getState) .get('/api/pleroma/admin/users', { params }) .then(({ data: { users, count, page_size: pageSize } }) => { @@ -152,10 +229,31 @@ export function fetchUsers(filters = [], page = 1, query, pageSize = 50) { }; } -export function deactivateUsers(accountIds) { +export function deactivateUsers(accountIds, reportId) { return (dispatch, getState) => { - const nicknames = nicknamesFromIds(getState, accountIds); + const state = getState(); + + const instance = state.get('instance'); + const features = getFeatures(instance); + dispatch({ type: ADMIN_USERS_DEACTIVATE_REQUEST, accountIds }); + + if (features.mastodonAdminApi) { + return Promise.all(accountIds.map(accountId => { + api(getState) + .post(`/api/v1/admin/accounts/${accountId}/action`, { + type: 'disable', + report_id: reportId, + }) + .then(() => { + dispatch({ type: ADMIN_USERS_DEACTIVATE_SUCCESS, accountIds: [accountId] }); + }).catch(error => { + dispatch({ type: ADMIN_USERS_DEACTIVATE_FAIL, error, accountIds: [accountId] }); + }); + })); + } + + const nicknames = nicknamesFromIds(getState, accountIds); return api(getState) .patch('/api/pleroma/admin/users/deactivate', { nicknames }) .then(({ data: { users } }) => { @@ -182,8 +280,26 @@ export function deleteUsers(accountIds) { export function approveUsers(accountIds) { return (dispatch, getState) => { - const nicknames = nicknamesFromIds(getState, accountIds); + const state = getState(); + + const instance = state.get('instance'); + const features = getFeatures(instance); + dispatch({ type: ADMIN_USERS_APPROVE_REQUEST, accountIds }); + + if (features.mastodonAdminApi) { + return Promise.all(accountIds.map(accountId => { + api(getState) + .post(`/api/v1/admin/accounts/${accountId}/approve`) + .then(({ data: user }) => { + dispatch({ type: ADMIN_USERS_APPROVE_SUCCESS, users: [user], accountIds: [accountId] }); + }).catch(error => { + dispatch({ type: ADMIN_USERS_APPROVE_FAIL, error, accountIds: [accountId] }); + }); + })); + } + + const nicknames = nicknamesFromIds(getState, accountIds); return api(getState) .patch('/api/pleroma/admin/users/approve', { nicknames }) .then(({ data: { users } }) => { diff --git a/app/soapbox/features/admin/components/report.tsx b/app/soapbox/features/admin/components/report.tsx index 6d3da6009..82ec5804e 100644 --- a/app/soapbox/features/admin/components/report.tsx +++ b/app/soapbox/features/admin/components/report.tsx @@ -14,8 +14,8 @@ import { useAppDispatch } from 'soapbox/hooks'; import ReportStatus from './report_status'; -import type { Map as ImmutableMap, List as ImmutableList } from 'immutable'; -import type { Status } from 'soapbox/types/entities'; +import type { List as ImmutableList } from 'immutable'; +import type { Account, AdminReport, Status } from 'soapbox/types/entities'; const messages = defineMessages({ reportClosed: { id: 'admin.reports.report_closed_message', defaultMessage: 'Report on @{name} was closed' }, @@ -24,7 +24,7 @@ const messages = defineMessages({ }); interface IReport { - report: ImmutableMap; + report: AdminReport; } const Report: React.FC = ({ report }) => { @@ -33,32 +33,35 @@ const Report: React.FC = ({ report }) => { const [accordionExpanded, setAccordionExpanded] = useState(false); + const account = report.account as Account; + const targetAccount = report.target_account as Account; + const makeMenu = () => { return [{ - text: intl.formatMessage(messages.deactivateUser, { name: report.getIn(['account', 'username']) as string }), + text: intl.formatMessage(messages.deactivateUser, { name: targetAccount.username as string }), action: handleDeactivateUser, icon: require('@tabler/icons/icons/user-off.svg'), }, { - text: intl.formatMessage(messages.deleteUser, { name: report.getIn(['account', 'username']) as string }), + text: intl.formatMessage(messages.deleteUser, { name: targetAccount.username as string }), action: handleDeleteUser, icon: require('@tabler/icons/icons/user-minus.svg'), }]; }; const handleCloseReport = () => { - dispatch(closeReports([report.get('id')])).then(() => { - const message = intl.formatMessage(messages.reportClosed, { name: report.getIn(['account', 'username']) as string }); + dispatch(closeReports([report.id])).then(() => { + const message = intl.formatMessage(messages.reportClosed, { name: targetAccount.username as string }); dispatch(snackbar.success(message)); }).catch(() => {}); }; const handleDeactivateUser = () => { - const accountId = report.getIn(['account', 'id']); + const accountId = targetAccount.id; dispatch(deactivateUserModal(intl, accountId, () => handleCloseReport())); }; const handleDeleteUser = () => { - const accountId = report.getIn(['account', 'id']) as string; + const accountId = targetAccount.id as string; dispatch(deleteUserModal(intl, accountId, () => handleCloseReport())); }; @@ -67,17 +70,17 @@ const Report: React.FC = ({ report }) => { }; const menu = makeMenu(); - const statuses = report.get('statuses') as ImmutableList; + const statuses = report.statuses as ImmutableList; const statusCount = statuses.count(); - const acct = report.getIn(['account', 'acct']) as string; - const reporterAcct = report.getIn(['actor', 'acct']) as string; + const acct = targetAccount.acct as string; + const reporterAcct = account.acct as string; return ( -
+
- + - +
@@ -87,7 +90,7 @@ const Report: React.FC = ({ report }) => { id='admin.reports.report_title' defaultMessage='Report on {acct}' values={{ acct: ( - + @{acct} ) }} @@ -105,12 +108,12 @@ const Report: React.FC = ({ report }) => { )}
- {report.get('content', '').length > 0 && ( -
+ {(report.comment || '').length > 0 && ( +
)} — - + @{reporterAcct} diff --git a/app/soapbox/features/admin/components/report_status.tsx b/app/soapbox/features/admin/components/report_status.tsx index 00755a6c4..c882f0f5a 100644 --- a/app/soapbox/features/admin/components/report_status.tsx +++ b/app/soapbox/features/admin/components/report_status.tsx @@ -10,8 +10,7 @@ import Bundle from 'soapbox/features/ui/components/bundle'; import { MediaGallery, Video, Audio } from 'soapbox/features/ui/util/async-components'; import { useAppDispatch } from 'soapbox/hooks'; -import type { Map as ImmutableMap } from 'immutable'; -import type { Status, Attachment } from 'soapbox/types/entities'; +import type { AdminReport, Attachment, Status } from 'soapbox/types/entities'; const messages = defineMessages({ viewStatus: { id: 'admin.reports.actions.view_status', defaultMessage: 'View post' }, @@ -20,7 +19,7 @@ const messages = defineMessages({ interface IReportStatus { status: Status, - report?: ImmutableMap, + report?: AdminReport, } const ReportStatus: React.FC = ({ status }) => { diff --git a/app/soapbox/features/admin/components/unapproved_account.tsx b/app/soapbox/features/admin/components/unapproved_account.tsx index f000bb173..5f38684cb 100644 --- a/app/soapbox/features/admin/components/unapproved_account.tsx +++ b/app/soapbox/features/admin/components/unapproved_account.tsx @@ -26,6 +26,7 @@ const UnapprovedAccount: React.FC = ({ accountId }) => { const dispatch = useAppDispatch(); const account = useAppSelector(state => getAccount(state, accountId)); + const adminAccount = useAppSelector(state => state.admin.users.get(accountId)); if (!account) return null; @@ -45,12 +46,11 @@ const UnapprovedAccount: React.FC = ({ accountId }) => { })); }; - return (
@{account.get('acct')}
-
{account.pleroma.getIn(['admin', 'registration_reason'], '') as string}
+
{adminAccount?.invite_request || ''}
diff --git a/app/soapbox/features/admin/tabs/reports.tsx b/app/soapbox/features/admin/tabs/reports.tsx index 78cc7a6de..8a2eca8c7 100644 --- a/app/soapbox/features/admin/tabs/reports.tsx +++ b/app/soapbox/features/admin/tabs/reports.tsx @@ -42,7 +42,7 @@ const Reports: React.FC = () => { scrollKey='admin-reports' emptyMessage={intl.formatMessage(messages.emptyMessage)} > - {reports.map(report => )} + {reports.map(report => report && )} ); }; diff --git a/app/soapbox/features/admin/user_index.js b/app/soapbox/features/admin/user_index.js index eeeba734a..7c06609a3 100644 --- a/app/soapbox/features/admin/user_index.js +++ b/app/soapbox/features/admin/user_index.js @@ -34,6 +34,7 @@ class UserIndex extends ImmutablePureComponent { pageSize: 50, page: 0, query: '', + nextLink: undefined, } clearState = callback => { @@ -45,11 +46,11 @@ class UserIndex extends ImmutablePureComponent { } fetchNextPage = () => { - const { filters, page, query, pageSize } = this.state; + const { filters, page, query, pageSize, nextLink } = this.state; const nextPage = page + 1; - this.props.dispatch(fetchUsers(filters, nextPage, query, pageSize)) - .then(({ users, count }) => { + this.props.dispatch(fetchUsers(filters, nextPage, query, pageSize, nextLink)) + .then(({ users, count, next }) => { const newIds = users.map(user => user.id); this.setState({ @@ -57,6 +58,7 @@ class UserIndex extends ImmutablePureComponent { accountIds: this.state.accountIds.union(newIds), total: count, page: nextPage, + nextLink: next, }); }) .catch(() => {}); @@ -97,7 +99,7 @@ class UserIndex extends ImmutablePureComponent { render() { const { intl } = this.props; const { accountIds, isLoading } = this.state; - const hasMore = accountIds.count() < this.state.total; + const hasMore = accountIds.count() < this.state.total && this.state.nextLink !== false; const showLoading = isLoading && accountIds.isEmpty(); diff --git a/app/soapbox/features/ui/index.tsx b/app/soapbox/features/ui/index.tsx index 6cfab8d89..1a97617cb 100644 --- a/app/soapbox/features/ui/index.tsx +++ b/app/soapbox/features/ui/index.tsx @@ -455,7 +455,7 @@ const UI: React.FC = ({ children }) => { } if (account.staff) { - dispatch(fetchReports({ state: 'open' })); + dispatch(fetchReports({ resolved: false })); dispatch(fetchUsers(['local', 'need_approval'])); } diff --git a/app/soapbox/normalizers/admin_account.ts b/app/soapbox/normalizers/admin_account.ts new file mode 100644 index 000000000..4e040dfb1 --- /dev/null +++ b/app/soapbox/normalizers/admin_account.ts @@ -0,0 +1,55 @@ +/** + * Admin account normalizer: + * Converts API admin-level account information into our internal format. + */ +import { + Map as ImmutableMap, + List as ImmutableList, + Record as ImmutableRecord, + fromJS, +} from 'immutable'; + +import type { ReducerAccount } from 'soapbox/reducers/accounts'; +import type { Account, EmbeddedEntity } from 'soapbox/types/entities'; + +export const AdminAccountRecord = ImmutableRecord({ + account: null as EmbeddedEntity, + approved: false, + confirmed: false, + created_at: new Date(), + disabled: false, + domain: '', + email: '', + id: '', + invite_request: null as string | null, + ip: null as string | null, + ips: ImmutableList(), + locale: null as string | null, + role: null as 'admin' | 'moderator' | null, + sensitized: false, + silenced: false, + suspended: false, + username: '', +}); + +const normalizePleromaAccount = (account: ImmutableMap) => { + if (!account.get('account')) { + return account.withMutations(account => { + account.set('approved', account.get('is_approved')); + account.set('confirmed', account.get('is_confirmed')); + account.set('disabled', !account.get('is_active')); + account.set('invite_request', account.get('registration_reason')); + account.set('role', account.getIn(['roles', 'admin']) ? 'admin' : (account.getIn(['roles', 'moderator']) ? 'moderator' : null)); + }); + } + + return account; +}; + +export const normalizeAdminAccount = (account: Record) => { + return AdminAccountRecord( + ImmutableMap(fromJS(account)).withMutations((account: ImmutableMap) => { + normalizePleromaAccount(account); + }), + ); +}; diff --git a/app/soapbox/normalizers/admin_report.ts b/app/soapbox/normalizers/admin_report.ts new file mode 100644 index 000000000..23a60bc04 --- /dev/null +++ b/app/soapbox/normalizers/admin_report.ts @@ -0,0 +1,51 @@ +/** + * Admin report normalizer: + * Converts API admin-level report information into our internal format. + */ +import { + Map as ImmutableMap, + List as ImmutableList, + Record as ImmutableRecord, + fromJS, +} from 'immutable'; + +import type { ReducerAccount } from 'soapbox/reducers/accounts'; +import type { Account, EmbeddedEntity, Status } from 'soapbox/types/entities'; + +export const AdminReportRecord = ImmutableRecord({ + account: null as EmbeddedEntity, + action_taken: false, + action_taken_by_account: null as EmbeddedEntity | null, + assigned_account: null as EmbeddedEntity | null, + category: '', + comment: '', + created_at: new Date(), + id: '', + rules: ImmutableList(), + statuses: ImmutableList>(), + target_account: null as EmbeddedEntity, + updated_at: new Date(), +}); + +const normalizePleromaReport = (report: ImmutableMap) => { + if (report.get('actor')){ + return report.withMutations(report => { + report.set('target_account', report.get('account')); + report.set('account', report.get('actor')); + + report.set('action_taken', report.get('state') !== 'open'); + report.set('comment', report.get('content')); + report.set('updated_at', report.get('created_at')); + }); + } + + return report; +}; + +export const normalizeAdminReport = (report: Record) => { + return AdminReportRecord( + ImmutableMap(fromJS(report)).withMutations((report: ImmutableMap) => { + normalizePleromaReport(report); + }), + ); +}; diff --git a/app/soapbox/normalizers/index.ts b/app/soapbox/normalizers/index.ts index 60802a057..9beba41af 100644 --- a/app/soapbox/normalizers/index.ts +++ b/app/soapbox/normalizers/index.ts @@ -1,4 +1,6 @@ export { AccountRecord, FieldRecord, normalizeAccount } from './account'; +export { AdminAccountRecord, normalizeAdminAccount } from './admin_account'; +export { AdminReportRecord, normalizeAdminReport } from './admin_report'; export { AttachmentRecord, normalizeAttachment } from './attachment'; export { CardRecord, normalizeCard } from './card'; export { ChatRecord, normalizeChat } from './chat'; diff --git a/app/soapbox/reducers/accounts.ts b/app/soapbox/reducers/accounts.ts index e7f4d4753..5952d2ef1 100644 --- a/app/soapbox/reducers/accounts.ts +++ b/app/soapbox/reducers/accounts.ts @@ -228,7 +228,7 @@ const importAdminUser = (state: State, adminUser: ImmutableMap): St const importAdminUsers = (state: State, adminUsers: Array>): State => { return state.withMutations((state: State) => { - adminUsers.forEach(adminUser => { + adminUsers.filter(adminUser => !adminUser.account).forEach(adminUser => { importAdminUser(state, ImmutableMap(fromJS(adminUser))); }); }); diff --git a/app/soapbox/reducers/admin.ts b/app/soapbox/reducers/admin.ts index a22650da4..fb2635371 100644 --- a/app/soapbox/reducers/admin.ts +++ b/app/soapbox/reducers/admin.ts @@ -19,15 +19,18 @@ import { ADMIN_USERS_DELETE_SUCCESS, ADMIN_USERS_APPROVE_REQUEST, ADMIN_USERS_APPROVE_SUCCESS, -} from '../actions/admin'; +} from 'soapbox/actions/admin'; +import { normalizeAdminReport, normalizeAdminAccount } from 'soapbox/normalizers'; +import { APIEntity } from 'soapbox/types/entities'; +import { normalizeId } from 'soapbox/utils/normalizers'; import type { AnyAction } from 'redux'; import type { Config } from 'soapbox/utils/config_db'; const ReducerRecord = ImmutableRecord({ - reports: ImmutableMap(), + reports: ImmutableMap(), openReports: ImmutableOrderedSet(), - users: ImmutableMap(), + users: ImmutableMap(), latestUsers: ImmutableOrderedSet(), awaitingApproval: ImmutableOrderedSet(), configs: ImmutableList(), @@ -36,6 +39,21 @@ const ReducerRecord = ImmutableRecord({ type State = ReturnType; +type AdminAccountRecord = ReturnType; +type AdminReportRecord = ReturnType; + +export interface ReducerAdminAccount extends AdminAccountRecord { + account: string | null, +} + +export interface ReducerAdminReport extends AdminReportRecord { + account: string | null, + target_account: string | null, + action_taken_by_account: string | null, + assigned_account: string | null, + statuses: ImmutableList, +} + // Umm... based? // https://itnext.io/typescript-extract-unpack-a-type-from-a-generic-baca7af14e51 type InnerRecord = R extends ImmutableRecord ? TProps : never; @@ -46,7 +64,6 @@ type InnerState = InnerRecord; type FilterConditionally = Pick; type SetKeys = keyof FilterConditionally>; - type APIReport = { id: string, state: string, statuses: any[] }; type APIUser = { id: string, email: string, nickname: string, registration_reason: string }; @@ -84,12 +101,17 @@ const maybeImportLatest = (state: State, users: APIUser[], filters: Filter[], pa } }; -const importUser = (state: State, user: APIUser): State => ( - state.setIn(['users', user.id], ImmutableMap({ - email: user.email, - registration_reason: user.registration_reason, - })) -); +const minifyUser = (user: AdminAccountRecord): ReducerAdminAccount => { + return user.mergeWith((o, n) => n || o, { + account: normalizeId(user.getIn(['account', 'id'])), + }) as ReducerAdminAccount; +}; + +const fixUser = (user: APIEntity): ReducerAdminAccount => { + return normalizeAdminAccount(user).withMutations(user => { + minifyUser(user); + }) as ReducerAdminAccount; +}; function importUsers(state: State, users: APIUser[], filters: Filter[], page: number): State { return state.withMutations(state => { @@ -97,7 +119,8 @@ function importUsers(state: State, users: APIUser[], filters: Filter[], page: nu maybeImportLatest(state, users, filters, page); users.forEach(user => { - importUser(state, user); + const normalizedUser = fixUser(user); + state.setIn(['users', user.id], normalizedUser); }); }); } @@ -114,20 +137,38 @@ function deleteUsers(state: State, accountIds: string[]): State { function approveUsers(state: State, users: APIUser[]): State { return state.withMutations(state => { users.forEach(user => { - state.update('awaitingApproval', orderedSet => orderedSet.delete(user.nickname)); - state.setIn(['users', user.nickname], fromJS(user)); + const normalizedUser = fixUser(user); + state.update('awaitingApproval', orderedSet => orderedSet.delete(user.id)); + state.setIn(['users', user.id], normalizedUser); }); }); } -function importReports(state: State, reports: APIReport[]): State { +const minifyReport = (report: AdminReportRecord): ReducerAdminReport => { + return report.mergeWith((o, n) => n || o, { + account: normalizeId(report.getIn(['account', 'id'])), + target_account: normalizeId(report.getIn(['target_account', 'id'])), + action_taken_by_account: normalizeId(report.getIn(['action_taken_by_account', 'id'])), + assigned_account: normalizeId(report.getIn(['assigned_account', 'id'])), + + statuses: report.get('statuses').map((status: any) => normalizeId(status.get('id'))), + }) as ReducerAdminReport; +}; + +const fixReport = (report: APIEntity): ReducerAdminReport => { + return normalizeAdminReport(report).withMutations(report => { + minifyReport(report); + }) as ReducerAdminReport; +}; + +function importReports(state: State, reports: APIEntity[]): State { return state.withMutations(state => { reports.forEach(report => { - report.statuses = report.statuses.map(status => status.id); - if (report.state === 'open') { + const normalizedReport = fixReport(report); + if (!normalizedReport.action_taken) { state.update('openReports', orderedSet => orderedSet.add(report.id)); } - state.setIn(['reports', report.id], fromJS(report)); + state.setIn(['reports', report.id], normalizedReport); }); }); } diff --git a/app/soapbox/selectors/index.ts b/app/soapbox/selectors/index.ts index d6e69e90f..7d77b4891 100644 --- a/app/soapbox/selectors/index.ts +++ b/app/soapbox/selectors/index.ts @@ -266,15 +266,26 @@ export const makeGetReport = () => { return createSelector( [ (state: RootState, id: string) => state.admin.reports.get(id), - (state: RootState, id: string) => ImmutableList(fromJS(state.admin.reports.getIn([id, 'statuses']))).map( + (state: RootState, id: string) => state.accounts.get(state.admin.reports.get(id)?.account || ''), + (state: RootState, id: string) => state.accounts.get(state.admin.reports.get(id)?.target_account || ''), + // (state: RootState, id: string) => state.accounts.get(state.admin.reports.get(id)?.action_taken_by_account || ''), + // (state: RootState, id: string) => state.accounts.get(state.admin.reports.get(id)?.assigned_account || ''), + (state: RootState, id: string) => ImmutableList(fromJS(state.admin.reports.get(id)?.statuses)).map( statusId => state.statuses.get(normalizeId(statusId))) .filter((s: any) => s) .map((s: any) => getStatus(state, s.toJS())), ], - (report, statuses) => { + (report, account, targetAccount, statuses) => { if (!report) return null; - return report.set('statuses', statuses); + return report.withMutations((report) => { + // @ts-ignore + report.set('account', account); + // @ts-ignore + report.set('target_account', targetAccount); + // @ts-ignore + report.set('statuses', statuses); + }); }, ); }; diff --git a/app/soapbox/types/entities.ts b/app/soapbox/types/entities.ts index d01698e46..c65cecaa4 100644 --- a/app/soapbox/types/entities.ts +++ b/app/soapbox/types/entities.ts @@ -1,4 +1,6 @@ import { + AdminAccountRecord, + AdminReportRecord, AccountRecord, AttachmentRecord, CardRecord, @@ -17,6 +19,8 @@ import { import type { Record as ImmutableRecord } from 'immutable'; +type AdminAccount = ReturnType; +type AdminReport = ReturnType; type Attachment = ReturnType; type Card = ReturnType; type Chat = ReturnType; @@ -47,6 +51,8 @@ type APIEntity = Record; type EmbeddedEntity = null | string | ReturnType>; export { + AdminAccount, + AdminReport, Account, Attachment, Card, diff --git a/app/soapbox/utils/features.ts b/app/soapbox/utils/features.ts index 1e4d76266..2f2a69d32 100644 --- a/app/soapbox/utils/features.ts +++ b/app/soapbox/utils/features.ts @@ -316,6 +316,20 @@ const getInstanceFeatures = (instance: Instance) => { v.software === PLEROMA && gte(v.version, '0.9.9'), ]), + /** + * Can perform moderation actions with account and reports. + * @see {@link https://docs.joinmastodon.org/methods/admin/} + * @see GET /api/v1/admin/reports + * @see POST /api/v1/admin/reports/:report_id/resolve + * @see POST /api/v1/admin/reports/:report_id/reopen + * @see POST /api/v1/admin/accounts/:account_id/action + * @see POST /api/v1/admin/accounts/:account_id/approve + */ + mastodonAdminApi: any([ + v.software === MASTODON && gte(v.compatVersion, '2.9.1'), + v.software === PLEROMA && v.build === SOAPBOX && gte(v.version, '2.4.50'), + ]), + /** * Can upload media attachments to statuses. * @see POST /api/v1/media From f70d44f67cce1573fbebbc63da498b19d1432ad7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Fri, 13 May 2022 23:14:55 +0200 Subject: [PATCH 003/102] Display familiar followers on Mastodon MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- app/soapbox/actions/familiar_followers.ts | 59 +++++++++++++ .../components/status_reply_mentions.js | 2 +- .../ui/components/birthdays_modal.tsx | 4 +- .../components/familiar_followers_modal.tsx | 57 +++++++++++++ .../features/ui/components/modal_root.js | 2 + .../components/profile_familiar_followers.tsx | 83 +++++++++++++++++++ .../ui/components/profile_info_panel.tsx | 3 + .../features/ui/util/async-components.ts | 4 + app/soapbox/locales/pl.json | 5 ++ .../reducers/__tests__/user_lists-test.js | 1 + app/soapbox/reducers/user_lists.js | 6 ++ app/soapbox/utils/features.ts | 6 ++ 12 files changed, 229 insertions(+), 3 deletions(-) create mode 100644 app/soapbox/actions/familiar_followers.ts create mode 100644 app/soapbox/features/ui/components/familiar_followers_modal.tsx create mode 100644 app/soapbox/features/ui/components/profile_familiar_followers.tsx diff --git a/app/soapbox/actions/familiar_followers.ts b/app/soapbox/actions/familiar_followers.ts new file mode 100644 index 000000000..ec6eca6d8 --- /dev/null +++ b/app/soapbox/actions/familiar_followers.ts @@ -0,0 +1,59 @@ +import { RootState } from 'soapbox/store'; + +import api from '../api'; + +import { ACCOUNTS_IMPORT, importFetchedAccounts } from './importer'; + +import type { APIEntity } from 'soapbox/types/entities'; + +export const FAMILIAR_FOLLOWERS_FETCH_REQUEST = 'FAMILIAR_FOLLOWERS_FETCH_REQUEST'; +export const FAMILIAR_FOLLOWERS_FETCH_SUCCESS = 'FAMILIAR_FOLLOWERS_FETCH_SUCCESS'; +export const FAMILIAR_FOLLOWERS_FETCH_FAIL = 'FAMILIAR_FOLLOWERS_FETCH_FAIL'; + +type FamiliarFollowersFetchRequestAction = { + type: typeof FAMILIAR_FOLLOWERS_FETCH_REQUEST, + id: string, +} + +type FamiliarFollowersFetchRequestSuccessAction = { + type: typeof FAMILIAR_FOLLOWERS_FETCH_SUCCESS, + id: string, + accounts: Array, +} + +type FamiliarFollowersFetchRequestFailAction = { + type: typeof FAMILIAR_FOLLOWERS_FETCH_FAIL, + id: string, + error: any, +} + +type AccountsImportAction = { + type: typeof ACCOUNTS_IMPORT, + accounts: Array, +} + +export type FamiliarFollowersActions = FamiliarFollowersFetchRequestAction | FamiliarFollowersFetchRequestSuccessAction | FamiliarFollowersFetchRequestFailAction | AccountsImportAction + +export const fetchAccountFamiliarFollowers = (accountId: string) => (dispatch: React.Dispatch, getState: () => RootState) => { + dispatch({ + type: FAMILIAR_FOLLOWERS_FETCH_REQUEST, + id: accountId, + }); + + api(getState).get(`/api/v1/accounts/familiar_followers?id=${accountId}`) + .then(({ data }) => { + const accounts = data.find(({ id }: { id: string }) => id === accountId).accounts; + + dispatch(importFetchedAccounts(accounts) as AccountsImportAction); + dispatch({ + type: FAMILIAR_FOLLOWERS_FETCH_SUCCESS, + id: accountId, + accounts, + }); + }) + .catch(error => dispatch({ + type: FAMILIAR_FOLLOWERS_FETCH_FAIL, + id: accountId, + error, + })); +}; diff --git a/app/soapbox/components/status_reply_mentions.js b/app/soapbox/components/status_reply_mentions.js index 76a48d5df..21b68ff21 100644 --- a/app/soapbox/components/status_reply_mentions.js +++ b/app/soapbox/components/status_reply_mentions.js @@ -64,7 +64,7 @@ class StatusReplyMentions extends ImmutablePureComponent { id='reply_mentions.reply' defaultMessage='Replying to {accounts}{more}' values={{ - accounts: to.slice(0, 2).map(account => (<> + accounts: to.slice(0, 2).map((account) => (<> @{account.get('username')} diff --git a/app/soapbox/features/ui/components/birthdays_modal.tsx b/app/soapbox/features/ui/components/birthdays_modal.tsx index 6c43bd7b4..f0b25f5f4 100644 --- a/app/soapbox/features/ui/components/birthdays_modal.tsx +++ b/app/soapbox/features/ui/components/birthdays_modal.tsx @@ -22,11 +22,11 @@ const BirthdaysModal = ({ onClose }: IBirthdaysModal) => { if (!accountIds) { body = ; } else { - const emptyMessage = ; + const emptyMessage = ; body = ( diff --git a/app/soapbox/features/ui/components/familiar_followers_modal.tsx b/app/soapbox/features/ui/components/familiar_followers_modal.tsx new file mode 100644 index 000000000..0ec081d58 --- /dev/null +++ b/app/soapbox/features/ui/components/familiar_followers_modal.tsx @@ -0,0 +1,57 @@ +import { OrderedSet as ImmutableOrderedSet } from 'immutable'; +import React from 'react'; +import { FormattedMessage } from 'react-intl'; + +import ScrollableList from 'soapbox/components/scrollable_list'; +import { Modal, Spinner } from 'soapbox/components/ui'; +import AccountContainer from 'soapbox/containers/account_container'; +import { useAppSelector } from 'soapbox/hooks'; +import { makeGetAccount } from 'soapbox/selectors'; + +const getAccount = makeGetAccount(); + +interface IFamiliarFollowersModal { + accountId: string, + onClose: (string: string) => void, +} + +const FamiliarFollowersModal = ({ accountId, onClose }: IFamiliarFollowersModal) => { + const account = useAppSelector(state => getAccount(state, accountId)); + const familiarFollowerIds: ImmutableOrderedSet = useAppSelector(state => state.user_lists.getIn(['familiar_followers', accountId])); + + const onClickClose = () => { + onClose('FAMILIAR_FOLLOWERS'); + }; + + let body; + + if (!account || !familiarFollowerIds) { + body = ; + } else { + const emptyMessage = }} />; + + body = ( + + {familiarFollowerIds.map(id => + , + )} + + ); + } + + + return ( + }} />} + onClose={onClickClose} + > + {body} + + ); +}; + +export default FamiliarFollowersModal; diff --git a/app/soapbox/features/ui/components/modal_root.js b/app/soapbox/features/ui/components/modal_root.js index 78f9ee2a4..98daaa78d 100644 --- a/app/soapbox/features/ui/components/modal_root.js +++ b/app/soapbox/features/ui/components/modal_root.js @@ -30,6 +30,7 @@ import { BirthdaysModal, AccountNoteModal, CompareHistoryModal, + FamiliarFollowersModal, } from 'soapbox/features/ui/util/async-components'; import BundleContainer from '../containers/bundle_container'; @@ -66,6 +67,7 @@ const MODAL_COMPONENTS = { 'BIRTHDAYS': BirthdaysModal, 'ACCOUNT_NOTE': AccountNoteModal, 'COMPARE_HISTORY': CompareHistoryModal, + 'FAMILIAR_FOLLOWERS': FamiliarFollowersModal, }; export default class ModalRoot extends React.PureComponent { diff --git a/app/soapbox/features/ui/components/profile_familiar_followers.tsx b/app/soapbox/features/ui/components/profile_familiar_followers.tsx new file mode 100644 index 000000000..a0dcef2c9 --- /dev/null +++ b/app/soapbox/features/ui/components/profile_familiar_followers.tsx @@ -0,0 +1,83 @@ +import { OrderedSet as ImmutableOrderedSet } from 'immutable'; +import React from 'react'; +import { useEffect } from 'react'; +import { FormattedList, FormattedMessage } from 'react-intl'; +import { useDispatch } from 'react-redux'; +import { Link } from 'react-router-dom'; + +import { fetchAccountFamiliarFollowers } from 'soapbox/actions/familiar_followers'; +import { openModal } from 'soapbox/actions/modals'; +import HoverRefWrapper from 'soapbox/components/hover_ref_wrapper'; +import { Text } from 'soapbox/components/ui'; +import VerificationBadge from 'soapbox/components/verification_badge'; +import { useAppSelector } from 'soapbox/hooks'; +import { makeGetAccount } from 'soapbox/selectors'; +import { getFeatures } from 'soapbox/utils/features'; + +import type { Account } from 'soapbox/types/entities'; + +const getAccount = makeGetAccount(); + +interface IProfileFamiliarFollowers { + account: Account, +} + +const ProfileFamiliarFollowers: React.FC = ({ account }) => { + const dispatch = useDispatch(); + const me = useAppSelector((state) => state.me); + const features = useAppSelector((state) => getFeatures(state.instance)); + const familiarFollowerIds: ImmutableOrderedSet = useAppSelector(state => state.user_lists.getIn(['familiar_followers', account.id], ImmutableOrderedSet())); + const familiarFollowers: ImmutableOrderedSet = useAppSelector(state => familiarFollowerIds.slice(0, 2).map(accountId => getAccount(state, accountId))); + + useEffect(() => { + if (me && features.familiarFollowers) { + dispatch(fetchAccountFamiliarFollowers(account.id)); + } + }, []); + + const openFamiliarFollowersModal = () => { + dispatch(openModal('FAMILIAR_FOLLOWERS', { + accountId: account.id, + })); + }; + + if (familiarFollowerIds.size === 0) { + return null; + } + + const accounts: Array = familiarFollowers.map(account => !!account && ( + + + + + {account.verified && } + + + )).toArray(); + + if (familiarFollowerIds.size > 2) { + accounts.push( + + + , + ); + } + + return ( + + , + }} + /> + + ); +}; + +export default ProfileFamiliarFollowers; \ No newline at end of file diff --git a/app/soapbox/features/ui/components/profile_info_panel.tsx b/app/soapbox/features/ui/components/profile_info_panel.tsx index 7fe628ddb..e3d9ac8f4 100644 --- a/app/soapbox/features/ui/components/profile_info_panel.tsx +++ b/app/soapbox/features/ui/components/profile_info_panel.tsx @@ -9,6 +9,7 @@ import VerificationBadge from 'soapbox/components/verification_badge'; import { useSoapboxConfig } from 'soapbox/hooks'; import { isLocal } from 'soapbox/utils/accounts'; +import ProfileFamiliarFollowers from './profile_familiar_followers'; import ProfileStats from './profile_stats'; import type { Account } from 'soapbox/types/entities'; @@ -222,6 +223,8 @@ const ProfileInfoPanel: React.FC = ({ account, username }) => {renderBirthday()}
+ +
); diff --git a/app/soapbox/features/ui/util/async-components.ts b/app/soapbox/features/ui/util/async-components.ts index aab5bf76d..ed1abd55e 100644 --- a/app/soapbox/features/ui/util/async-components.ts +++ b/app/soapbox/features/ui/util/async-components.ts @@ -501,3 +501,7 @@ export function CompareHistoryModal() { export function AuthTokenList() { return import(/* webpackChunkName: "features/auth_token_list" */'../../auth_token_list'); } + +export function FamiliarFollowersModal() { + return import(/*webpackChunkName: "modals/familiar_followers_modal" */'../components/familiar_followers_modal'); +} diff --git a/app/soapbox/locales/pl.json b/app/soapbox/locales/pl.json index e9a38c69c..7a19661ae 100644 --- a/app/soapbox/locales/pl.json +++ b/app/soapbox/locales/pl.json @@ -16,6 +16,9 @@ "account.direct": "Wyślij wiadomość bezpośrednią do @{name}", "account.edit_profile": "Edytuj profil", "account.endorse": "Polecaj na profilu", + "account.familiar_followers": "Obserwowany(-a) przez {accounts}", + "account.familiar_followers.empty": "Nie znasz nikogo obserwującego {name}.", + "account.familiar_followers.more": "{count} {count, plural, one {innego użytkownika, którego obserwujesz} other {innych użytkowników, których obserwujesz}}", "account.follow": "Śledź", "account.followers": "Śledzący", "account.followers.empty": "Nikt jeszcze nie śledzi tego użytkownika.", @@ -155,6 +158,7 @@ "backups.empty_message.action": "Chcesz utworzyć?", "backups.pending": "Oczekująca", "beta.also_available": "Dostępne w językach:", + "birthdays_modal.empty": "Nikt kogo znasz nie ma dziś urodzin.", "birthday_panel.title": "Birthdays", "boost_modal.combo": "Naciśnij {combo}, aby pominąć to następnym razem", "bundle_column_error.body": "Coś poszło nie tak podczas ładowania tego składnika.", @@ -203,6 +207,7 @@ "column.domain_blocks": "Ukryte domeny", "column.edit_profile": "Edytuj profil", "column.export_data": "Eksportuj dane", + "column.familiar_followers": "Obserwujący {name} których znasz", "column.favourited_statuses": "Polubione wpisy", "column.favourites": "Polubienia", "column.federation_restrictions": "Ograniczenia federacji", diff --git a/app/soapbox/reducers/__tests__/user_lists-test.js b/app/soapbox/reducers/__tests__/user_lists-test.js index a168f3cdc..7c5ec7e20 100644 --- a/app/soapbox/reducers/__tests__/user_lists-test.js +++ b/app/soapbox/reducers/__tests__/user_lists-test.js @@ -17,6 +17,7 @@ describe('user_lists reducer', () => { groups_removed_accounts: ImmutableMap(), pinned: ImmutableMap(), birthday_reminders: ImmutableMap(), + familiar_followers: ImmutableMap(), })); }); }); diff --git a/app/soapbox/reducers/user_lists.js b/app/soapbox/reducers/user_lists.js index 992f930cc..69e8c181c 100644 --- a/app/soapbox/reducers/user_lists.js +++ b/app/soapbox/reducers/user_lists.js @@ -27,6 +27,9 @@ import { DIRECTORY_EXPAND_SUCCESS, DIRECTORY_EXPAND_FAIL, } from '../actions/directory'; +import { + FAMILIAR_FOLLOWERS_FETCH_SUCCESS, +} from '../actions/familiar_followers'; import { GROUP_MEMBERS_FETCH_SUCCESS, GROUP_MEMBERS_EXPAND_SUCCESS, @@ -60,6 +63,7 @@ const initialState = ImmutableMap({ groups_removed_accounts: ImmutableMap(), pinned: ImmutableMap(), birthday_reminders: ImmutableMap(), + familiar_followers: ImmutableMap(), }); const normalizeList = (state, type, id, accounts, next) => { @@ -138,6 +142,8 @@ export default function userLists(state = initialState, action) { return normalizeList(state, 'pinned', action.id, action.accounts, action.next); case BIRTHDAY_REMINDERS_FETCH_SUCCESS: return state.setIn(['birthday_reminders', action.id], ImmutableOrderedSet(action.accounts.map(item => item.id))); + case FAMILIAR_FOLLOWERS_FETCH_SUCCESS: + return state.setIn(['familiar_followers', action.id], ImmutableOrderedSet(action.accounts.map(item => item.id))); default: return state; } diff --git a/app/soapbox/utils/features.ts b/app/soapbox/utils/features.ts index 9f60b5a05..45eed8936 100644 --- a/app/soapbox/utils/features.ts +++ b/app/soapbox/utils/features.ts @@ -256,6 +256,12 @@ const getInstanceFeatures = (instance: Instance) => { features.includes('exposable_reactions'), ]), + /** + * Can see accounts' followers you know + * @see GET /api/v1/accounts/familiar_followers + */ + familiarFollowers: v.software === MASTODON && gte(v.version, '3.5.0'), + /** Whether the instance federates. */ federating: federation.get('enabled', true) === true, // Assume true unless explicitly false From e45cd2d97d0290bcbcbd9e0fc2ef9a68c70ca7ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Mon, 16 May 2022 19:37:10 +0200 Subject: [PATCH 004/102] Update Polish translation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- app/soapbox/locales/pl.json | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/app/soapbox/locales/pl.json b/app/soapbox/locales/pl.json index 7a19661ae..69b0b6bf9 100644 --- a/app/soapbox/locales/pl.json +++ b/app/soapbox/locales/pl.json @@ -10,8 +10,8 @@ "account.block_domain": "Blokuj wszystko z {domain}", "account.blocked": "Zablokowany(-a)", "account.chat": "Napisz do @{name}", - "account.column_settings.description": "These settings apply to all account timelines.", - "account.column_settings.title": "Account timeline settings", + "account.column_settings.description": "Te ustawienia dotyczą wszystkich osi czasu konta.", + "account.column_settings.title": "Ustawienia osi czasu", "account.deactivated": "Dezaktywowany(-a)", "account.direct": "Wyślij wiadomość bezpośrednią do @{name}", "account.edit_profile": "Edytuj profil", @@ -107,14 +107,14 @@ "admin.users.actions.promote_to_admin_message": "Mianowano @{acct} administratorem", "admin.users.actions.promote_to_moderator": "Mianuj @{name} moderatorem", "admin.users.actions.promote_to_moderator_message": "Mianowano @{acct} moderatorem", - "admin.users.actions.remove_donor": "Remove @{name} as a donor", - "admin.users.actions.set_donor": "Set @{name} as a donor", + "admin.users.actions.remove_donor": "Usuń @{name} ze wspierających", + "admin.users.actions.set_donor": "Ustaw @{name} jako wspierającego", "admin.users.actions.suggest_user": "Polecaj @{name}", "admin.users.actions.unsuggest_user": "Przestań polecać @{name}", "admin.users.actions.unverify_user": "Cofnij weryfikację @{name}", "admin.users.actions.verify_user": "Weryfikuj @{name}", - "admin.users.remove_donor_message": "@{acct} was removed as a donor", - "admin.users.set_donor_message": "@{acct} was set as a donor", + "admin.users.remove_donor_message": "Usunięto @{acct} ze wspierających", + "admin.users.set_donor_message": "Ustawiono @{acct} jako wspierającego", "admin.users.user_deactivated_message": "Zdezaktywowano @{acct}", "admin.users.user_deleted_message": "Usunięto @{acct}", "admin.users.user_suggested_message": "Zaczęto polecać @{acct}", @@ -124,12 +124,12 @@ "admin_nav.awaiting_approval": "Oczekujące zgłoszenia", "admin_nav.dashboard": "Panel administracyjny", "admin_nav.reports": "Zgłoszenia", - "alert.unexpected.body": "We're sorry for the interruption. If the problem persists, please reach out to our support team. You may also try to {clearCookies} (this will log you out).", - "alert.unexpected.browser": "Browser", + "alert.unexpected.body": "Przepraszamy za niedogodność. Jeśli problem nie zniknie, skontaktuj się z naszym wsparciem technicznym. Możesz też spróbować {clearCookies} (zostaniesz wylogowany(-a) z konta).", + "alert.unexpected.browser": "Przeglądarka", "alert.unexpected.clear_cookies": "wyczyścić pliki cookies i dane przeglądarki", - "alert.unexpected.links.help": "Help Center", - "alert.unexpected.links.status": "Status", - "alert.unexpected.links.support": "Support", + "alert.unexpected.links.help": "Centrum pomocy", + "alert.unexpected.links.status": "Stan", + "alert.unexpected.links.support": "Wsparcie techniczne", "alert.unexpected.message": "Wystąpił nieoczekiwany błąd.", "alert.unexpected.return_home": "Wróć na stronę główną", "alert.unexpected.title": "O nie!", @@ -159,7 +159,7 @@ "backups.pending": "Oczekująca", "beta.also_available": "Dostępne w językach:", "birthdays_modal.empty": "Nikt kogo znasz nie ma dziś urodzin.", - "birthday_panel.title": "Birthdays", + "birthday_panel.title": "Urodziny", "boost_modal.combo": "Naciśnij {combo}, aby pominąć to następnym razem", "bundle_column_error.body": "Coś poszło nie tak podczas ładowania tego składnika.", "bundle_column_error.retry": "Spróbuj ponownie", @@ -747,7 +747,7 @@ "notifications.column_settings.filter_bar.category": "Szybkie filtrowanie", "notifications.column_settings.filter_bar.show": "Pokaż", "notifications.column_settings.follow": "Nowi śledzący:", - "notifications.column_settings.follow_request": "Nowe prośby o możliwość śledzenia:", + "notifications.column_settings.follow_request": "Nowe prośby o możliwość śledzenia:", "notifications.column_settings.mention": "Wspomnienia:", "notifications.column_settings.move": "Przenoszone konta:", "notifications.column_settings.poll": "Wyniki głosowania:", @@ -773,9 +773,9 @@ "onboarding.avatar.title": "Wybierz zdjęcie profilowe", "onboarding.display_name.subtitle": "Możesz ją zawsze zmienić później.", "onboarding.display_name.title": "Wybierz wyświetlaną nazwę", - "onboarding.done": "Done", + "onboarding.done": "Gotowe", "onboarding.finished.message": "Cieszymy się, że możemy powitać Cię w naszej społeczności! Naciśnij poniższy przycisk, aby rozpocząć.", - "onboarding.finished.title": "Onboarding complete", + "onboarding.finished.title": "Wprowadzenie", "onboarding.header.subtitle": "Będzie widoczny w górnej części Twojego profilu", "onboarding.header.title": "Wybierz obraz tła", "onboarding.next": "Dalej", @@ -789,8 +789,8 @@ "password_reset.confirmation": "Sprawdź swoją pocztę e-mail, aby potwierdzić.", "password_reset.fields.username_placeholder": "Adres e-mail lub nazwa użytkownika", "password_reset.reset": "Resetuj hasło", - "patron.donate": "Donate", - "patron.title": "Funding Goal", + "patron.donate": "Wesprzyj", + "patron.title": "Cel wsparcia", "pinned_accounts.title": "Polecani przez {name}", "pinned_statuses.none": "Brak przypięć do pokazania.", "poll.closed": "Zamknięte", @@ -827,7 +827,7 @@ "profile_dropdown.add_account": "Dodaj istniejące konto", "profile_dropdown.logout": "Wyloguj @{acct}", "profile_dropdown.theme": "Motyw", - "profile_fields_panel.title": "Profile fields", + "profile_fields_panel.title": "Pola konta", "public.column_settings.title": "Ustawienia osi czasu Fediwersum", "reactions.all": "Wszystkie", "regeneration_indicator.label": "Ładuję…", From eecbbb839a508cfbc18eabbb7f3d48a87a6e7769 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Mon, 16 May 2022 20:30:42 +0200 Subject: [PATCH 005/102] Use FormattedList for 'Replying to' MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- .../components/status_reply_mentions.js | 33 +++++++++++-------- .../compose/components/reply_mentions.tsx | 17 +++++++--- .../status/components/quoted_status.tsx | 18 ++++++---- app/soapbox/locales/ar.json | 2 -- app/soapbox/locales/ast.json | 2 -- app/soapbox/locales/bg.json | 2 -- app/soapbox/locales/bn.json | 2 -- app/soapbox/locales/br.json | 2 -- app/soapbox/locales/ca.json | 2 -- app/soapbox/locales/co.json | 2 -- app/soapbox/locales/cs.json | 2 -- app/soapbox/locales/cy.json | 2 -- app/soapbox/locales/da.json | 2 -- app/soapbox/locales/de.json | 4 +-- app/soapbox/locales/el.json | 2 -- app/soapbox/locales/en-Shaw.json | 4 +-- app/soapbox/locales/en.json | 2 -- app/soapbox/locales/eo.json | 2 -- app/soapbox/locales/es-AR.json | 2 -- app/soapbox/locales/es.json | 2 -- app/soapbox/locales/et.json | 2 -- app/soapbox/locales/eu.json | 2 -- app/soapbox/locales/fa.json | 2 -- app/soapbox/locales/fi.json | 2 -- app/soapbox/locales/fr.json | 2 -- app/soapbox/locales/ga.json | 2 -- app/soapbox/locales/gl.json | 2 -- app/soapbox/locales/he.json | 4 +-- app/soapbox/locales/hi.json | 2 -- app/soapbox/locales/hr.json | 2 -- app/soapbox/locales/hu.json | 2 -- app/soapbox/locales/hy.json | 2 -- app/soapbox/locales/id.json | 2 -- app/soapbox/locales/io.json | 2 -- app/soapbox/locales/is.json | 4 +-- app/soapbox/locales/it.json | 4 +-- app/soapbox/locales/ja.json | 2 -- app/soapbox/locales/ka.json | 2 -- app/soapbox/locales/kk.json | 2 -- app/soapbox/locales/ko.json | 2 -- app/soapbox/locales/lt.json | 2 -- app/soapbox/locales/lv.json | 2 -- app/soapbox/locales/mk.json | 2 -- app/soapbox/locales/ms.json | 2 -- app/soapbox/locales/nl.json | 2 -- app/soapbox/locales/nn.json | 2 -- app/soapbox/locales/no.json | 2 -- app/soapbox/locales/oc.json | 2 -- app/soapbox/locales/pl.json | 4 +-- app/soapbox/locales/pt-BR.json | 2 -- app/soapbox/locales/pt.json | 2 -- app/soapbox/locales/ro.json | 2 -- app/soapbox/locales/ru.json | 2 -- app/soapbox/locales/sk.json | 2 -- app/soapbox/locales/sl.json | 2 -- app/soapbox/locales/sq.json | 2 -- app/soapbox/locales/sr-Latn.json | 2 -- app/soapbox/locales/sr.json | 2 -- app/soapbox/locales/sv.json | 2 -- app/soapbox/locales/ta.json | 2 -- app/soapbox/locales/te.json | 2 -- app/soapbox/locales/th.json | 2 -- app/soapbox/locales/tr.json | 2 -- app/soapbox/locales/uk.json | 2 -- app/soapbox/locales/zh-CN.json | 2 -- app/soapbox/locales/zh-HK.json | 2 -- app/soapbox/locales/zh-TW.json | 2 -- 67 files changed, 56 insertions(+), 152 deletions(-) diff --git a/app/soapbox/components/status_reply_mentions.js b/app/soapbox/components/status_reply_mentions.js index 76a48d5df..0809d6085 100644 --- a/app/soapbox/components/status_reply_mentions.js +++ b/app/soapbox/components/status_reply_mentions.js @@ -1,8 +1,9 @@ +import { List as ImmutableList } from 'immutable'; import PropTypes from 'prop-types'; import React from 'react'; import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePureComponent from 'react-immutable-pure-component'; -import { FormattedMessage, injectIntl } from 'react-intl'; +import { FormattedList, FormattedMessage, injectIntl } from 'react-intl'; import { connect } from 'react-redux'; import { Link } from 'react-router-dom'; @@ -42,7 +43,7 @@ class StatusReplyMentions extends ImmutablePureComponent { return null; } - const to = status.get('mentions', []); + const to = status.get('mentions', ImmutableList()); // The post is a reply, but it has no mentions. // Rare, but it can happen. @@ -58,23 +59,27 @@ class StatusReplyMentions extends ImmutablePureComponent { } // The typical case with a reply-to and a list of mentions. + const accounts = to.slice(0, 2).map(account => ( + + @{account.get('username')} + + )).toArray(); + + if (to.size > 2) { + accounts.push( + + + , + ); + } + return (
(<> - - @{account.get('username')} - - {' '} - )), - more: to.size > 2 && ( - - - - ), + accounts: , }} />
diff --git a/app/soapbox/features/compose/components/reply_mentions.tsx b/app/soapbox/features/compose/components/reply_mentions.tsx index 2d09134e0..c25c2f3df 100644 --- a/app/soapbox/features/compose/components/reply_mentions.tsx +++ b/app/soapbox/features/compose/components/reply_mentions.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { FormattedMessage } from 'react-intl'; +import { FormattedList, FormattedMessage } from 'react-intl'; import { useDispatch } from 'react-redux'; import { openModal } from 'soapbox/actions/modals'; @@ -47,14 +47,23 @@ const ReplyMentions: React.FC = () => { ); } + const accounts = to.slice(0, 2).map((acct: string) => ( + @{acct.split('@')[0]} + )).toArray(); + + if (to.size > 2) { + accounts.push( + , + ); + } + return ( <>@{acct.split('@')[0]}{' '}), - more: to.size > 2 && , + accounts: , }} /> diff --git a/app/soapbox/features/status/components/quoted_status.tsx b/app/soapbox/features/status/components/quoted_status.tsx index aff5940be..c91dd616d 100644 --- a/app/soapbox/features/status/components/quoted_status.tsx +++ b/app/soapbox/features/status/components/quoted_status.tsx @@ -2,7 +2,7 @@ import classNames from 'classnames'; import { History } from 'history'; import React from 'react'; import ImmutablePureComponent from 'react-immutable-pure-component'; -import { defineMessages, injectIntl, FormattedMessage, IntlShape } from 'react-intl'; +import { defineMessages, injectIntl, FormattedMessage, IntlShape, FormattedList } from 'react-intl'; import { withRouter } from 'react-router-dom'; import AttachmentThumbs from 'soapbox/components/attachment_thumbs'; @@ -67,10 +67,9 @@ class QuotedStatus extends ImmutablePureComponent {
@@ -84,14 +83,21 @@ class QuotedStatus extends ImmutablePureComponent { } } + const accounts = to.slice(0, 2).map(account => <>@{account.username}).toArray(); + + if (to.size > 2) { + accounts.push( + , + ); + } + return (
`@${account.username} `), - more: to.size > 2 && , + accounts: , }} />
diff --git a/app/soapbox/locales/ar.json b/app/soapbox/locales/ar.json index 5031eb79d..d1e705697 100644 --- a/app/soapbox/locales/ar.json +++ b/app/soapbox/locales/ar.json @@ -859,8 +859,6 @@ "reply_indicator.cancel": "إلغاء", "reply_mentions.account.add": "Add to mentions", "reply_mentions.account.remove": "Remove from mentions", - "reply_mentions.more": "and {count} more", - "reply_mentions.reply": "Replying to {accounts}{more}", "reply_mentions.reply_empty": "Replying to post", "report.block": "Block {target}", "report.block_hint": "Do you also want to block this account?", diff --git a/app/soapbox/locales/ast.json b/app/soapbox/locales/ast.json index 18f0c07df..ef5c65bd9 100644 --- a/app/soapbox/locales/ast.json +++ b/app/soapbox/locales/ast.json @@ -859,8 +859,6 @@ "reply_indicator.cancel": "Encaboxar", "reply_mentions.account.add": "Add to mentions", "reply_mentions.account.remove": "Remove from mentions", - "reply_mentions.more": "and {count} more", - "reply_mentions.reply": "Replying to {accounts}{more}", "reply_mentions.reply_empty": "Replying to post", "report.block": "Block {target}", "report.block_hint": "Do you also want to block this account?", diff --git a/app/soapbox/locales/bg.json b/app/soapbox/locales/bg.json index b9f8ae1e1..7a62a52f0 100644 --- a/app/soapbox/locales/bg.json +++ b/app/soapbox/locales/bg.json @@ -859,8 +859,6 @@ "reply_indicator.cancel": "Отказ", "reply_mentions.account.add": "Add to mentions", "reply_mentions.account.remove": "Remove from mentions", - "reply_mentions.more": "and {count} more", - "reply_mentions.reply": "Replying to {accounts}{more}", "reply_mentions.reply_empty": "Replying to post", "report.block": "Block {target}", "report.block_hint": "Do you also want to block this account?", diff --git a/app/soapbox/locales/bn.json b/app/soapbox/locales/bn.json index 54b6a65eb..c030722fa 100644 --- a/app/soapbox/locales/bn.json +++ b/app/soapbox/locales/bn.json @@ -859,8 +859,6 @@ "reply_indicator.cancel": "বাতিল করতে", "reply_mentions.account.add": "Add to mentions", "reply_mentions.account.remove": "Remove from mentions", - "reply_mentions.more": "and {count} more", - "reply_mentions.reply": "Replying to {accounts}{more}", "reply_mentions.reply_empty": "Replying to post", "report.block": "Block {target}", "report.block_hint": "Do you also want to block this account?", diff --git a/app/soapbox/locales/br.json b/app/soapbox/locales/br.json index af5a58c8d..1a0d58e9a 100644 --- a/app/soapbox/locales/br.json +++ b/app/soapbox/locales/br.json @@ -859,8 +859,6 @@ "reply_indicator.cancel": "Cancel", "reply_mentions.account.add": "Add to mentions", "reply_mentions.account.remove": "Remove from mentions", - "reply_mentions.more": "and {count} more", - "reply_mentions.reply": "Replying to {accounts}{more}", "reply_mentions.reply_empty": "Replying to post", "report.block": "Block {target}", "report.block_hint": "Do you also want to block this account?", diff --git a/app/soapbox/locales/ca.json b/app/soapbox/locales/ca.json index 0a3b7fa54..e36ad89bc 100644 --- a/app/soapbox/locales/ca.json +++ b/app/soapbox/locales/ca.json @@ -859,8 +859,6 @@ "reply_indicator.cancel": "Cancel·lar", "reply_mentions.account.add": "Add to mentions", "reply_mentions.account.remove": "Remove from mentions", - "reply_mentions.more": "and {count} more", - "reply_mentions.reply": "Replying to {accounts}{more}", "reply_mentions.reply_empty": "Replying to post", "report.block": "Bloquejar {target}", "report.block_hint": "També vols bloquejar aquest compte?", diff --git a/app/soapbox/locales/co.json b/app/soapbox/locales/co.json index 390ef9764..79214836a 100644 --- a/app/soapbox/locales/co.json +++ b/app/soapbox/locales/co.json @@ -859,8 +859,6 @@ "reply_indicator.cancel": "Annullà", "reply_mentions.account.add": "Add to mentions", "reply_mentions.account.remove": "Remove from mentions", - "reply_mentions.more": "and {count} more", - "reply_mentions.reply": "Replying to {accounts}{more}", "reply_mentions.reply_empty": "Replying to post", "report.block": "Block {target}", "report.block_hint": "Do you also want to block this account?", diff --git a/app/soapbox/locales/cs.json b/app/soapbox/locales/cs.json index 96d275865..155e1fa95 100644 --- a/app/soapbox/locales/cs.json +++ b/app/soapbox/locales/cs.json @@ -859,8 +859,6 @@ "reply_indicator.cancel": "Zrušit", "reply_mentions.account.add": "Add to mentions", "reply_mentions.account.remove": "Remove from mentions", - "reply_mentions.more": "and {count} more", - "reply_mentions.reply": "Replying to {accounts}{more}", "reply_mentions.reply_empty": "Replying to post", "report.block": "Zablokovat {target}", "report.block_hint": "Chcete zablokovat tento účet?", diff --git a/app/soapbox/locales/cy.json b/app/soapbox/locales/cy.json index a087fd35c..bfc9c701b 100644 --- a/app/soapbox/locales/cy.json +++ b/app/soapbox/locales/cy.json @@ -859,8 +859,6 @@ "reply_indicator.cancel": "Canslo", "reply_mentions.account.add": "Add to mentions", "reply_mentions.account.remove": "Remove from mentions", - "reply_mentions.more": "and {count} more", - "reply_mentions.reply": "Replying to {accounts}{more}", "reply_mentions.reply_empty": "Replying to post", "report.block": "Block {target}", "report.block_hint": "Do you also want to block this account?", diff --git a/app/soapbox/locales/da.json b/app/soapbox/locales/da.json index b879b4a51..a935d51cf 100644 --- a/app/soapbox/locales/da.json +++ b/app/soapbox/locales/da.json @@ -859,8 +859,6 @@ "reply_indicator.cancel": "Annuller", "reply_mentions.account.add": "Add to mentions", "reply_mentions.account.remove": "Remove from mentions", - "reply_mentions.more": "and {count} more", - "reply_mentions.reply": "Replying to {accounts}{more}", "reply_mentions.reply_empty": "Replying to post", "report.block": "Block {target}", "report.block_hint": "Do you also want to block this account?", diff --git a/app/soapbox/locales/de.json b/app/soapbox/locales/de.json index 95bce86fc..67067f2bf 100644 --- a/app/soapbox/locales/de.json +++ b/app/soapbox/locales/de.json @@ -859,8 +859,8 @@ "reply_indicator.cancel": "Abbrechen", "reply_mentions.account.add": "Add to mentions", "reply_mentions.account.remove": "Remove from mentions", - "reply_mentions.more": "und {count, plural, one {einen weiteren Nutzer} other {# weitere Nutzer}}", - "reply_mentions.reply": "Antwort an {accounts}{more}", + "reply_mentions.more": "{count, plural, one {einen weiteren Nutzer} other {# weitere Nutzer}}", + "reply_mentions.reply": "Antwort an {accounts}", "reply_mentions.reply_empty": "Antwort auf einen Beitrag", "report.block": "{target} blockieren.", "report.block_hint": "Soll dieses Konto zusammen mit der Meldung auch gleich blockiert werden?", diff --git a/app/soapbox/locales/el.json b/app/soapbox/locales/el.json index 4aba909aa..b601e68e8 100644 --- a/app/soapbox/locales/el.json +++ b/app/soapbox/locales/el.json @@ -859,8 +859,6 @@ "reply_indicator.cancel": "Άκυρο", "reply_mentions.account.add": "Add to mentions", "reply_mentions.account.remove": "Remove from mentions", - "reply_mentions.more": "and {count} more", - "reply_mentions.reply": "Replying to {accounts}{more}", "reply_mentions.reply_empty": "Replying to post", "report.block": "Block {target}", "report.block_hint": "Do you also want to block this account?", diff --git a/app/soapbox/locales/en-Shaw.json b/app/soapbox/locales/en-Shaw.json index 50f8c36ae..a0c457f11 100644 --- a/app/soapbox/locales/en-Shaw.json +++ b/app/soapbox/locales/en-Shaw.json @@ -859,8 +859,8 @@ "reply_indicator.cancel": "𐑒𐑨𐑯𐑕𐑩𐑤", "reply_mentions.account.add": "𐑨𐑛 𐑑 𐑥𐑧𐑯𐑖𐑩𐑯𐑟", "reply_mentions.account.remove": "𐑮𐑦𐑥𐑵𐑝 𐑓𐑮𐑪𐑥 𐑥𐑧𐑯𐑖𐑩𐑯𐑟", - "reply_mentions.more": "𐑯 {count} 𐑥𐑹", - "reply_mentions.reply": "𐑮𐑦𐑐𐑤𐑲𐑦𐑙 𐑑 {accounts}{more}", + "reply_mentions.more": "{count} 𐑥𐑹", + "reply_mentions.reply": "𐑮𐑦𐑐𐑤𐑲𐑦𐑙 𐑑 {accounts}", "reply_mentions.reply_empty": "𐑮𐑦𐑐𐑤𐑲𐑦𐑙 𐑑 𐑐𐑴𐑕𐑑", "report.block": "𐑚𐑤𐑪𐑒 {target}", "report.block_hint": "𐑛𐑵 𐑿 𐑷𐑤𐑕𐑴 𐑢𐑪𐑯𐑑 𐑑 𐑚𐑤𐑪𐑒 𐑞𐑦𐑕 𐑩𐑒𐑬𐑯𐑑?", diff --git a/app/soapbox/locales/en.json b/app/soapbox/locales/en.json index a7c1d1ce1..e6267ac94 100644 --- a/app/soapbox/locales/en.json +++ b/app/soapbox/locales/en.json @@ -859,8 +859,6 @@ "reply_indicator.cancel": "Cancel", "reply_mentions.account.add": "Add to mentions", "reply_mentions.account.remove": "Remove from mentions", - "reply_mentions.more": "and {count} more", - "reply_mentions.reply": "Replying to {accounts}{more}", "reply_mentions.reply_empty": "Replying to post", "report.block": "Block {target}", "report.block_hint": "Do you also want to block this account?", diff --git a/app/soapbox/locales/eo.json b/app/soapbox/locales/eo.json index 879fdde3b..4c3bd6166 100644 --- a/app/soapbox/locales/eo.json +++ b/app/soapbox/locales/eo.json @@ -859,8 +859,6 @@ "reply_indicator.cancel": "Nuligi", "reply_mentions.account.add": "Add to mentions", "reply_mentions.account.remove": "Remove from mentions", - "reply_mentions.more": "and {count} more", - "reply_mentions.reply": "Replying to {accounts}{more}", "reply_mentions.reply_empty": "Replying to post", "report.block": "Block {target}", "report.block_hint": "Do you also want to block this account?", diff --git a/app/soapbox/locales/es-AR.json b/app/soapbox/locales/es-AR.json index e1fa60e68..eb56879a0 100644 --- a/app/soapbox/locales/es-AR.json +++ b/app/soapbox/locales/es-AR.json @@ -859,8 +859,6 @@ "reply_indicator.cancel": "Cancelar", "reply_mentions.account.add": "Add to mentions", "reply_mentions.account.remove": "Remove from mentions", - "reply_mentions.more": "and {count} more", - "reply_mentions.reply": "Replying to {accounts}{more}", "reply_mentions.reply_empty": "Replying to post", "report.block": "Block {target}", "report.block_hint": "Do you also want to block this account?", diff --git a/app/soapbox/locales/es.json b/app/soapbox/locales/es.json index 2e0ab4ec9..c4a3e99cb 100644 --- a/app/soapbox/locales/es.json +++ b/app/soapbox/locales/es.json @@ -859,8 +859,6 @@ "reply_indicator.cancel": "Cancelar", "reply_mentions.account.add": "Add to mentions", "reply_mentions.account.remove": "Remove from mentions", - "reply_mentions.more": "and {count} more", - "reply_mentions.reply": "Replying to {accounts}{more}", "reply_mentions.reply_empty": "Replying to post", "report.block": "Block {target}", "report.block_hint": "Do you also want to block this account?", diff --git a/app/soapbox/locales/et.json b/app/soapbox/locales/et.json index 5a4eb43c6..636be30e0 100644 --- a/app/soapbox/locales/et.json +++ b/app/soapbox/locales/et.json @@ -859,8 +859,6 @@ "reply_indicator.cancel": "Tühista", "reply_mentions.account.add": "Add to mentions", "reply_mentions.account.remove": "Remove from mentions", - "reply_mentions.more": "and {count} more", - "reply_mentions.reply": "Replying to {accounts}{more}", "reply_mentions.reply_empty": "Replying to post", "report.block": "Block {target}", "report.block_hint": "Do you also want to block this account?", diff --git a/app/soapbox/locales/eu.json b/app/soapbox/locales/eu.json index 3a3881aca..a16e4332b 100644 --- a/app/soapbox/locales/eu.json +++ b/app/soapbox/locales/eu.json @@ -859,8 +859,6 @@ "reply_indicator.cancel": "Utzi", "reply_mentions.account.add": "Add to mentions", "reply_mentions.account.remove": "Remove from mentions", - "reply_mentions.more": "and {count} more", - "reply_mentions.reply": "Replying to {accounts}{more}", "reply_mentions.reply_empty": "Replying to post", "report.block": "Block {target}", "report.block_hint": "Do you also want to block this account?", diff --git a/app/soapbox/locales/fa.json b/app/soapbox/locales/fa.json index 863046301..b9e9da44d 100644 --- a/app/soapbox/locales/fa.json +++ b/app/soapbox/locales/fa.json @@ -859,8 +859,6 @@ "reply_indicator.cancel": "لغو", "reply_mentions.account.add": "Add to mentions", "reply_mentions.account.remove": "Remove from mentions", - "reply_mentions.more": "and {count} more", - "reply_mentions.reply": "Replying to {accounts}{more}", "reply_mentions.reply_empty": "Replying to post", "report.block": "Block {target}", "report.block_hint": "Do you also want to block this account?", diff --git a/app/soapbox/locales/fi.json b/app/soapbox/locales/fi.json index 2a29e6c20..d806c2fc6 100644 --- a/app/soapbox/locales/fi.json +++ b/app/soapbox/locales/fi.json @@ -859,8 +859,6 @@ "reply_indicator.cancel": "Peruuta", "reply_mentions.account.add": "Add to mentions", "reply_mentions.account.remove": "Remove from mentions", - "reply_mentions.more": "and {count} more", - "reply_mentions.reply": "Replying to {accounts}{more}", "reply_mentions.reply_empty": "Replying to post", "report.block": "Block {target}", "report.block_hint": "Do you also want to block this account?", diff --git a/app/soapbox/locales/fr.json b/app/soapbox/locales/fr.json index 743341596..373c89021 100644 --- a/app/soapbox/locales/fr.json +++ b/app/soapbox/locales/fr.json @@ -859,8 +859,6 @@ "reply_indicator.cancel": "Annuler", "reply_mentions.account.add": "Add to mentions", "reply_mentions.account.remove": "Remove from mentions", - "reply_mentions.more": "and {count} more", - "reply_mentions.reply": "Replying to {accounts}{more}", "reply_mentions.reply_empty": "Replying to post", "report.block": "Block {target}", "report.block_hint": "Do you also want to block this account?", diff --git a/app/soapbox/locales/ga.json b/app/soapbox/locales/ga.json index 5e6cb3a8b..eec9f2971 100644 --- a/app/soapbox/locales/ga.json +++ b/app/soapbox/locales/ga.json @@ -859,8 +859,6 @@ "reply_indicator.cancel": "Cancel", "reply_mentions.account.add": "Add to mentions", "reply_mentions.account.remove": "Remove from mentions", - "reply_mentions.more": "and {count} more", - "reply_mentions.reply": "Replying to {accounts}{more}", "reply_mentions.reply_empty": "Replying to post", "report.block": "Block {target}", "report.block_hint": "Do you also want to block this account?", diff --git a/app/soapbox/locales/gl.json b/app/soapbox/locales/gl.json index a5a04c57c..3ea35cb9f 100644 --- a/app/soapbox/locales/gl.json +++ b/app/soapbox/locales/gl.json @@ -859,8 +859,6 @@ "reply_indicator.cancel": "Cancelar", "reply_mentions.account.add": "Add to mentions", "reply_mentions.account.remove": "Remove from mentions", - "reply_mentions.more": "and {count} more", - "reply_mentions.reply": "Replying to {accounts}{more}", "reply_mentions.reply_empty": "Replying to post", "report.block": "Block {target}", "report.block_hint": "Do you also want to block this account?", diff --git a/app/soapbox/locales/he.json b/app/soapbox/locales/he.json index 69ae4d20a..21717e89f 100644 --- a/app/soapbox/locales/he.json +++ b/app/soapbox/locales/he.json @@ -859,8 +859,8 @@ "reply_indicator.cancel": "ביטול", "reply_mentions.account.add": "הוסף לאזכורים", "reply_mentions.account.remove": "הסר מהאזכורים", - "reply_mentions.more": "ו-{count} עוד", - "reply_mentions.reply": "משיב ל-{accounts}{more}", + "reply_mentions.more": "{count} עוד", + "reply_mentions.reply": "משיב ל-{accounts}", "reply_mentions.reply_empty": "משיב לפוסט", "report.block": "חסום {target}", "report.block_hint": "האם גם אתה רוצה לחסום את החשבון הזה?", diff --git a/app/soapbox/locales/hi.json b/app/soapbox/locales/hi.json index 2080a931a..292d6a033 100644 --- a/app/soapbox/locales/hi.json +++ b/app/soapbox/locales/hi.json @@ -859,8 +859,6 @@ "reply_indicator.cancel": "Cancel", "reply_mentions.account.add": "Add to mentions", "reply_mentions.account.remove": "Remove from mentions", - "reply_mentions.more": "and {count} more", - "reply_mentions.reply": "Replying to {accounts}{more}", "reply_mentions.reply_empty": "Replying to post", "report.block": "Block {target}", "report.block_hint": "Do you also want to block this account?", diff --git a/app/soapbox/locales/hr.json b/app/soapbox/locales/hr.json index e78476954..ca5451121 100644 --- a/app/soapbox/locales/hr.json +++ b/app/soapbox/locales/hr.json @@ -859,8 +859,6 @@ "reply_indicator.cancel": "Otkaži", "reply_mentions.account.add": "Add to mentions", "reply_mentions.account.remove": "Remove from mentions", - "reply_mentions.more": "and {count} more", - "reply_mentions.reply": "Replying to {accounts}{more}", "reply_mentions.reply_empty": "Replying to post", "report.block": "Block {target}", "report.block_hint": "Do you also want to block this account?", diff --git a/app/soapbox/locales/hu.json b/app/soapbox/locales/hu.json index 031c6c0a7..0d5b56411 100644 --- a/app/soapbox/locales/hu.json +++ b/app/soapbox/locales/hu.json @@ -859,8 +859,6 @@ "reply_indicator.cancel": "Mégsem", "reply_mentions.account.add": "Add to mentions", "reply_mentions.account.remove": "Remove from mentions", - "reply_mentions.more": "and {count} more", - "reply_mentions.reply": "Replying to {accounts}{more}", "reply_mentions.reply_empty": "Replying to post", "report.block": "Block {target}", "report.block_hint": "Do you also want to block this account?", diff --git a/app/soapbox/locales/hy.json b/app/soapbox/locales/hy.json index e023bb148..c2638e2d4 100644 --- a/app/soapbox/locales/hy.json +++ b/app/soapbox/locales/hy.json @@ -859,8 +859,6 @@ "reply_indicator.cancel": "Չեղարկել", "reply_mentions.account.add": "Add to mentions", "reply_mentions.account.remove": "Remove from mentions", - "reply_mentions.more": "and {count} more", - "reply_mentions.reply": "Replying to {accounts}{more}", "reply_mentions.reply_empty": "Replying to post", "report.block": "Block {target}", "report.block_hint": "Do you also want to block this account?", diff --git a/app/soapbox/locales/id.json b/app/soapbox/locales/id.json index 92bd85bdd..904f73cc5 100644 --- a/app/soapbox/locales/id.json +++ b/app/soapbox/locales/id.json @@ -859,8 +859,6 @@ "reply_indicator.cancel": "Batal", "reply_mentions.account.add": "Add to mentions", "reply_mentions.account.remove": "Remove from mentions", - "reply_mentions.more": "and {count} more", - "reply_mentions.reply": "Replying to {accounts}{more}", "reply_mentions.reply_empty": "Replying to post", "report.block": "Block {target}", "report.block_hint": "Do you also want to block this account?", diff --git a/app/soapbox/locales/io.json b/app/soapbox/locales/io.json index 0a5bffb3c..f85f36a5a 100644 --- a/app/soapbox/locales/io.json +++ b/app/soapbox/locales/io.json @@ -859,8 +859,6 @@ "reply_indicator.cancel": "Nihiligar", "reply_mentions.account.add": "Add to mentions", "reply_mentions.account.remove": "Remove from mentions", - "reply_mentions.more": "and {count} more", - "reply_mentions.reply": "Replying to {accounts}{more}", "reply_mentions.reply_empty": "Replying to post", "report.block": "Block {target}", "report.block_hint": "Do you also want to block this account?", diff --git a/app/soapbox/locales/is.json b/app/soapbox/locales/is.json index e3eea2422..49e906eb5 100644 --- a/app/soapbox/locales/is.json +++ b/app/soapbox/locales/is.json @@ -788,8 +788,8 @@ "reply_indicator.cancel": "Hætta við", "reply_mentions.account.add": "Bæta við í tilvísanirnar", "reply_mentions.account.remove": "Fjarlægja úr tilvísunum", - "reply_mentions.more": "og {count} fleirum", - "reply_mentions.reply": "Að svara {accounts}{more}", + "reply_mentions.more": "{count} fleirum", + "reply_mentions.reply": "Að svara {accounts}", "reply_mentions.reply_empty": "Að svara færslu", "report.block": "Loka á {target}", "report.block_hint": "Viltu líka loka á þennan reikning?", diff --git a/app/soapbox/locales/it.json b/app/soapbox/locales/it.json index 57e54f718..e565fb5d6 100644 --- a/app/soapbox/locales/it.json +++ b/app/soapbox/locales/it.json @@ -859,8 +859,8 @@ "reply_indicator.cancel": "Annulla", "reply_mentions.account.add": "Add to mentions", "reply_mentions.account.remove": "Remove from mentions", - "reply_mentions.more": "e ancora {count}", - "reply_mentions.reply": "Risponde a {accounts}{more}", + "reply_mentions.more": "ancora {count}", + "reply_mentions.reply": "Risponde a {accounts}", "reply_mentions.reply_empty": "Rispondendo al contenuto", "report.block": "Blocca {target}", "report.block_hint": "Vuoi anche bloccare questa persona?", diff --git a/app/soapbox/locales/ja.json b/app/soapbox/locales/ja.json index ea90e3e07..931c89326 100644 --- a/app/soapbox/locales/ja.json +++ b/app/soapbox/locales/ja.json @@ -859,8 +859,6 @@ "reply_indicator.cancel": "キャンセル", "reply_mentions.account.add": "Add to mentions", "reply_mentions.account.remove": "Remove from mentions", - "reply_mentions.more": "and {count} more", - "reply_mentions.reply": "Replying to {accounts}{more}", "reply_mentions.reply_empty": "Replying to post", "report.block": "{target}さんをブロック", "report.block_hint": "このアカウントをブロックしますか?", diff --git a/app/soapbox/locales/ka.json b/app/soapbox/locales/ka.json index 05f781c9b..950b4e69c 100644 --- a/app/soapbox/locales/ka.json +++ b/app/soapbox/locales/ka.json @@ -859,8 +859,6 @@ "reply_indicator.cancel": "უარყოფა", "reply_mentions.account.add": "Add to mentions", "reply_mentions.account.remove": "Remove from mentions", - "reply_mentions.more": "and {count} more", - "reply_mentions.reply": "Replying to {accounts}{more}", "reply_mentions.reply_empty": "Replying to post", "report.block": "Block {target}", "report.block_hint": "Do you also want to block this account?", diff --git a/app/soapbox/locales/kk.json b/app/soapbox/locales/kk.json index 0bf4c7faf..40f4f25ac 100644 --- a/app/soapbox/locales/kk.json +++ b/app/soapbox/locales/kk.json @@ -859,8 +859,6 @@ "reply_indicator.cancel": "Қайтып алу", "reply_mentions.account.add": "Add to mentions", "reply_mentions.account.remove": "Remove from mentions", - "reply_mentions.more": "and {count} more", - "reply_mentions.reply": "Replying to {accounts}{more}", "reply_mentions.reply_empty": "Replying to post", "report.block": "Block {target}", "report.block_hint": "Do you also want to block this account?", diff --git a/app/soapbox/locales/ko.json b/app/soapbox/locales/ko.json index 6e56a125d..55bc1e1bb 100644 --- a/app/soapbox/locales/ko.json +++ b/app/soapbox/locales/ko.json @@ -859,8 +859,6 @@ "reply_indicator.cancel": "취소", "reply_mentions.account.add": "Add to mentions", "reply_mentions.account.remove": "Remove from mentions", - "reply_mentions.more": "and {count} more", - "reply_mentions.reply": "Replying to {accounts}{more}", "reply_mentions.reply_empty": "Replying to post", "report.block": "Block {target}", "report.block_hint": "Do you also want to block this account?", diff --git a/app/soapbox/locales/lt.json b/app/soapbox/locales/lt.json index a369a6ec5..44b913afb 100644 --- a/app/soapbox/locales/lt.json +++ b/app/soapbox/locales/lt.json @@ -859,8 +859,6 @@ "reply_indicator.cancel": "Cancel", "reply_mentions.account.add": "Add to mentions", "reply_mentions.account.remove": "Remove from mentions", - "reply_mentions.more": "and {count} more", - "reply_mentions.reply": "Replying to {accounts}{more}", "reply_mentions.reply_empty": "Replying to post", "report.block": "Block {target}", "report.block_hint": "Do you also want to block this account?", diff --git a/app/soapbox/locales/lv.json b/app/soapbox/locales/lv.json index a8fb3a793..c97bca375 100644 --- a/app/soapbox/locales/lv.json +++ b/app/soapbox/locales/lv.json @@ -859,8 +859,6 @@ "reply_indicator.cancel": "Cancel", "reply_mentions.account.add": "Add to mentions", "reply_mentions.account.remove": "Remove from mentions", - "reply_mentions.more": "and {count} more", - "reply_mentions.reply": "Replying to {accounts}{more}", "reply_mentions.reply_empty": "Replying to post", "report.block": "Block {target}", "report.block_hint": "Do you also want to block this account?", diff --git a/app/soapbox/locales/mk.json b/app/soapbox/locales/mk.json index 501a56cd9..c760d08e0 100644 --- a/app/soapbox/locales/mk.json +++ b/app/soapbox/locales/mk.json @@ -859,8 +859,6 @@ "reply_indicator.cancel": "Cancel", "reply_mentions.account.add": "Add to mentions", "reply_mentions.account.remove": "Remove from mentions", - "reply_mentions.more": "and {count} more", - "reply_mentions.reply": "Replying to {accounts}{more}", "reply_mentions.reply_empty": "Replying to post", "report.block": "Block {target}", "report.block_hint": "Do you also want to block this account?", diff --git a/app/soapbox/locales/ms.json b/app/soapbox/locales/ms.json index 880fd2938..eaafcd52f 100644 --- a/app/soapbox/locales/ms.json +++ b/app/soapbox/locales/ms.json @@ -859,8 +859,6 @@ "reply_indicator.cancel": "Cancel", "reply_mentions.account.add": "Add to mentions", "reply_mentions.account.remove": "Remove from mentions", - "reply_mentions.more": "and {count} more", - "reply_mentions.reply": "Replying to {accounts}{more}", "reply_mentions.reply_empty": "Replying to post", "report.block": "Block {target}", "report.block_hint": "Do you also want to block this account?", diff --git a/app/soapbox/locales/nl.json b/app/soapbox/locales/nl.json index 1c1c00c3f..47795ef3d 100644 --- a/app/soapbox/locales/nl.json +++ b/app/soapbox/locales/nl.json @@ -859,8 +859,6 @@ "reply_indicator.cancel": "Annuleren", "reply_mentions.account.add": "Add to mentions", "reply_mentions.account.remove": "Remove from mentions", - "reply_mentions.more": "and {count} more", - "reply_mentions.reply": "Replying to {accounts}{more}", "reply_mentions.reply_empty": "Replying to post", "report.block": "Block {target}", "report.block_hint": "Do you also want to block this account?", diff --git a/app/soapbox/locales/nn.json b/app/soapbox/locales/nn.json index 41a2b7f85..26e4c3134 100644 --- a/app/soapbox/locales/nn.json +++ b/app/soapbox/locales/nn.json @@ -859,8 +859,6 @@ "reply_indicator.cancel": "Cancel", "reply_mentions.account.add": "Add to mentions", "reply_mentions.account.remove": "Remove from mentions", - "reply_mentions.more": "and {count} more", - "reply_mentions.reply": "Replying to {accounts}{more}", "reply_mentions.reply_empty": "Replying to post", "report.block": "Block {target}", "report.block_hint": "Do you also want to block this account?", diff --git a/app/soapbox/locales/no.json b/app/soapbox/locales/no.json index 77ac02363..fe86be051 100644 --- a/app/soapbox/locales/no.json +++ b/app/soapbox/locales/no.json @@ -859,8 +859,6 @@ "reply_indicator.cancel": "Avbryt", "reply_mentions.account.add": "Add to mentions", "reply_mentions.account.remove": "Remove from mentions", - "reply_mentions.more": "and {count} more", - "reply_mentions.reply": "Replying to {accounts}{more}", "reply_mentions.reply_empty": "Replying to post", "report.block": "Block {target}", "report.block_hint": "Do you also want to block this account?", diff --git a/app/soapbox/locales/oc.json b/app/soapbox/locales/oc.json index c952d6eea..64b471abc 100644 --- a/app/soapbox/locales/oc.json +++ b/app/soapbox/locales/oc.json @@ -859,8 +859,6 @@ "reply_indicator.cancel": "Anullar", "reply_mentions.account.add": "Add to mentions", "reply_mentions.account.remove": "Remove from mentions", - "reply_mentions.more": "and {count} more", - "reply_mentions.reply": "Replying to {accounts}{more}", "reply_mentions.reply_empty": "Replying to post", "report.block": "Block {target}", "report.block_hint": "Do you also want to block this account?", diff --git a/app/soapbox/locales/pl.json b/app/soapbox/locales/pl.json index e9a38c69c..d5c8b15e5 100644 --- a/app/soapbox/locales/pl.json +++ b/app/soapbox/locales/pl.json @@ -876,8 +876,8 @@ "reply_indicator.cancel": "Anuluj", "reply_mentions.account.add": "Dodaj do wspomnianych", "reply_mentions.account.remove": "Usuń z wspomnianych", - "reply_mentions.more": "i {count} więcej", - "reply_mentions.reply": "W odpowiedzi do {accounts}{more}", + "reply_mentions.more": "{count} więcej", + "reply_mentions.reply": "W odpowiedzi do {accounts}", "reply_mentions.reply_empty": "W odpowiedzi na wpis", "report.block": "Zablokuj {target}", "report.block_hint": "Czy chcesz też zablokować to konto?", diff --git a/app/soapbox/locales/pt-BR.json b/app/soapbox/locales/pt-BR.json index 79701f4e8..f0b0a19df 100644 --- a/app/soapbox/locales/pt-BR.json +++ b/app/soapbox/locales/pt-BR.json @@ -859,8 +859,6 @@ "reply_indicator.cancel": "Cancelar", "reply_mentions.account.add": "Add to mentions", "reply_mentions.account.remove": "Remove from mentions", - "reply_mentions.more": "and {count} more", - "reply_mentions.reply": "Replying to {accounts}{more}", "reply_mentions.reply_empty": "Replying to post", "report.block": "Block {target}", "report.block_hint": "Do you also want to block this account?", diff --git a/app/soapbox/locales/pt.json b/app/soapbox/locales/pt.json index 6eadee36d..4cc86daad 100644 --- a/app/soapbox/locales/pt.json +++ b/app/soapbox/locales/pt.json @@ -859,8 +859,6 @@ "reply_indicator.cancel": "Cancelar", "reply_mentions.account.add": "Add to mentions", "reply_mentions.account.remove": "Remove from mentions", - "reply_mentions.more": "and {count} more", - "reply_mentions.reply": "Replying to {accounts}{more}", "reply_mentions.reply_empty": "Replying to post", "report.block": "Bloquear {target}", "report.block_hint": "Desejas também bloquear esta conta?", diff --git a/app/soapbox/locales/ro.json b/app/soapbox/locales/ro.json index 46c068630..2f49f24ce 100644 --- a/app/soapbox/locales/ro.json +++ b/app/soapbox/locales/ro.json @@ -859,8 +859,6 @@ "reply_indicator.cancel": "Anulează", "reply_mentions.account.add": "Add to mentions", "reply_mentions.account.remove": "Remove from mentions", - "reply_mentions.more": "and {count} more", - "reply_mentions.reply": "Replying to {accounts}{more}", "reply_mentions.reply_empty": "Replying to post", "report.block": "Block {target}", "report.block_hint": "Do you also want to block this account?", diff --git a/app/soapbox/locales/ru.json b/app/soapbox/locales/ru.json index 76e4c0eb2..b4d0b3e2d 100644 --- a/app/soapbox/locales/ru.json +++ b/app/soapbox/locales/ru.json @@ -859,8 +859,6 @@ "reply_indicator.cancel": "Отмена", "reply_mentions.account.add": "Add to mentions", "reply_mentions.account.remove": "Remove from mentions", - "reply_mentions.more": "and {count} more", - "reply_mentions.reply": "Replying to {accounts}{more}", "reply_mentions.reply_empty": "Replying to post", "report.block": "Block {target}", "report.block_hint": "Do you also want to block this account?", diff --git a/app/soapbox/locales/sk.json b/app/soapbox/locales/sk.json index 368c8d9b4..7a81a02c4 100644 --- a/app/soapbox/locales/sk.json +++ b/app/soapbox/locales/sk.json @@ -859,8 +859,6 @@ "reply_indicator.cancel": "Zrušiť", "reply_mentions.account.add": "Add to mentions", "reply_mentions.account.remove": "Remove from mentions", - "reply_mentions.more": "and {count} more", - "reply_mentions.reply": "Replying to {accounts}{more}", "reply_mentions.reply_empty": "Replying to post", "report.block": "Block {target}", "report.block_hint": "Do you also want to block this account?", diff --git a/app/soapbox/locales/sl.json b/app/soapbox/locales/sl.json index 5a5e08eaf..d1710325d 100644 --- a/app/soapbox/locales/sl.json +++ b/app/soapbox/locales/sl.json @@ -859,8 +859,6 @@ "reply_indicator.cancel": "Prekliči", "reply_mentions.account.add": "Add to mentions", "reply_mentions.account.remove": "Remove from mentions", - "reply_mentions.more": "and {count} more", - "reply_mentions.reply": "Replying to {accounts}{more}", "reply_mentions.reply_empty": "Replying to post", "report.block": "Block {target}", "report.block_hint": "Do you also want to block this account?", diff --git a/app/soapbox/locales/sq.json b/app/soapbox/locales/sq.json index ca6dc72cb..1c0a23d21 100644 --- a/app/soapbox/locales/sq.json +++ b/app/soapbox/locales/sq.json @@ -859,8 +859,6 @@ "reply_indicator.cancel": "Anuloje", "reply_mentions.account.add": "Add to mentions", "reply_mentions.account.remove": "Remove from mentions", - "reply_mentions.more": "and {count} more", - "reply_mentions.reply": "Replying to {accounts}{more}", "reply_mentions.reply_empty": "Replying to post", "report.block": "Block {target}", "report.block_hint": "Do you also want to block this account?", diff --git a/app/soapbox/locales/sr-Latn.json b/app/soapbox/locales/sr-Latn.json index f04b9bef5..ccf0c8ef1 100644 --- a/app/soapbox/locales/sr-Latn.json +++ b/app/soapbox/locales/sr-Latn.json @@ -859,8 +859,6 @@ "reply_indicator.cancel": "Poništi", "reply_mentions.account.add": "Add to mentions", "reply_mentions.account.remove": "Remove from mentions", - "reply_mentions.more": "and {count} more", - "reply_mentions.reply": "Replying to {accounts}{more}", "reply_mentions.reply_empty": "Replying to post", "report.block": "Block {target}", "report.block_hint": "Do you also want to block this account?", diff --git a/app/soapbox/locales/sr.json b/app/soapbox/locales/sr.json index efe2c4aa6..140a2738c 100644 --- a/app/soapbox/locales/sr.json +++ b/app/soapbox/locales/sr.json @@ -859,8 +859,6 @@ "reply_indicator.cancel": "Поништи", "reply_mentions.account.add": "Add to mentions", "reply_mentions.account.remove": "Remove from mentions", - "reply_mentions.more": "and {count} more", - "reply_mentions.reply": "Replying to {accounts}{more}", "reply_mentions.reply_empty": "Replying to post", "report.block": "Block {target}", "report.block_hint": "Do you also want to block this account?", diff --git a/app/soapbox/locales/sv.json b/app/soapbox/locales/sv.json index dd56ef574..a57d57e8f 100644 --- a/app/soapbox/locales/sv.json +++ b/app/soapbox/locales/sv.json @@ -859,8 +859,6 @@ "reply_indicator.cancel": "Ångra", "reply_mentions.account.add": "Add to mentions", "reply_mentions.account.remove": "Remove from mentions", - "reply_mentions.more": "and {count} more", - "reply_mentions.reply": "Replying to {accounts}{more}", "reply_mentions.reply_empty": "Replying to post", "report.block": "Block {target}", "report.block_hint": "Do you also want to block this account?", diff --git a/app/soapbox/locales/ta.json b/app/soapbox/locales/ta.json index 189eda2bb..69c44f84f 100644 --- a/app/soapbox/locales/ta.json +++ b/app/soapbox/locales/ta.json @@ -859,8 +859,6 @@ "reply_indicator.cancel": "எதிராணை", "reply_mentions.account.add": "Add to mentions", "reply_mentions.account.remove": "Remove from mentions", - "reply_mentions.more": "and {count} more", - "reply_mentions.reply": "Replying to {accounts}{more}", "reply_mentions.reply_empty": "Replying to post", "report.block": "Block {target}", "report.block_hint": "Do you also want to block this account?", diff --git a/app/soapbox/locales/te.json b/app/soapbox/locales/te.json index 04f5c1931..6b34989f9 100644 --- a/app/soapbox/locales/te.json +++ b/app/soapbox/locales/te.json @@ -859,8 +859,6 @@ "reply_indicator.cancel": "రద్దు చెయ్యి", "reply_mentions.account.add": "Add to mentions", "reply_mentions.account.remove": "Remove from mentions", - "reply_mentions.more": "and {count} more", - "reply_mentions.reply": "Replying to {accounts}{more}", "reply_mentions.reply_empty": "Replying to post", "report.block": "Block {target}", "report.block_hint": "Do you also want to block this account?", diff --git a/app/soapbox/locales/th.json b/app/soapbox/locales/th.json index 72d984068..c509dd4a3 100644 --- a/app/soapbox/locales/th.json +++ b/app/soapbox/locales/th.json @@ -859,8 +859,6 @@ "reply_indicator.cancel": "ยกเลิก", "reply_mentions.account.add": "Add to mentions", "reply_mentions.account.remove": "Remove from mentions", - "reply_mentions.more": "and {count} more", - "reply_mentions.reply": "Replying to {accounts}{more}", "reply_mentions.reply_empty": "Replying to post", "report.block": "Block {target}", "report.block_hint": "Do you also want to block this account?", diff --git a/app/soapbox/locales/tr.json b/app/soapbox/locales/tr.json index 133b7c649..45c7c02b0 100644 --- a/app/soapbox/locales/tr.json +++ b/app/soapbox/locales/tr.json @@ -859,8 +859,6 @@ "reply_indicator.cancel": "İptal", "reply_mentions.account.add": "Add to mentions", "reply_mentions.account.remove": "Remove from mentions", - "reply_mentions.more": "and {count} more", - "reply_mentions.reply": "Replying to {accounts}{more}", "reply_mentions.reply_empty": "Replying to post", "report.block": "Block {target}", "report.block_hint": "Do you also want to block this account?", diff --git a/app/soapbox/locales/uk.json b/app/soapbox/locales/uk.json index f88236624..bf601a95b 100644 --- a/app/soapbox/locales/uk.json +++ b/app/soapbox/locales/uk.json @@ -859,8 +859,6 @@ "reply_indicator.cancel": "Відмінити", "reply_mentions.account.add": "Add to mentions", "reply_mentions.account.remove": "Remove from mentions", - "reply_mentions.more": "and {count} more", - "reply_mentions.reply": "Replying to {accounts}{more}", "reply_mentions.reply_empty": "Replying to post", "report.block": "Block {target}", "report.block_hint": "Do you also want to block this account?", diff --git a/app/soapbox/locales/zh-CN.json b/app/soapbox/locales/zh-CN.json index 25d9a28f7..0ec6a2e0f 100644 --- a/app/soapbox/locales/zh-CN.json +++ b/app/soapbox/locales/zh-CN.json @@ -859,8 +859,6 @@ "reply_indicator.cancel": "取消", "reply_mentions.account.add": "Add to mentions", "reply_mentions.account.remove": "Remove from mentions", - "reply_mentions.more": "and {count} more", - "reply_mentions.reply": "Replying to {accounts}{more}", "reply_mentions.reply_empty": "Replying to post", "report.block": "屏蔽帐号 {target}", "report.block_hint": "你是否也要屏蔽这个帐号呢?", diff --git a/app/soapbox/locales/zh-HK.json b/app/soapbox/locales/zh-HK.json index b4143a647..b163f6675 100644 --- a/app/soapbox/locales/zh-HK.json +++ b/app/soapbox/locales/zh-HK.json @@ -859,8 +859,6 @@ "reply_indicator.cancel": "取消", "reply_mentions.account.add": "Add to mentions", "reply_mentions.account.remove": "Remove from mentions", - "reply_mentions.more": "and {count} more", - "reply_mentions.reply": "Replying to {accounts}{more}", "reply_mentions.reply_empty": "Replying to post", "report.block": "Block {target}", "report.block_hint": "Do you also want to block this account?", diff --git a/app/soapbox/locales/zh-TW.json b/app/soapbox/locales/zh-TW.json index 44db7da27..5eade39d5 100644 --- a/app/soapbox/locales/zh-TW.json +++ b/app/soapbox/locales/zh-TW.json @@ -859,8 +859,6 @@ "reply_indicator.cancel": "取消", "reply_mentions.account.add": "Add to mentions", "reply_mentions.account.remove": "Remove from mentions", - "reply_mentions.more": "and {count} more", - "reply_mentions.reply": "Replying to {accounts}{more}", "reply_mentions.reply_empty": "Replying to post", "report.block": "Block {target}", "report.block_hint": "Do you also want to block this account?", From e21ec04be614597611e1938890d2829db17343d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Tue, 17 May 2022 13:05:01 +0200 Subject: [PATCH 006/102] Add 'Remove from followers' MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- app/soapbox/actions/accounts.js | 40 +++++++++++++++++++ .../features/account/components/header.js | 9 +++++ .../account_timeline/components/header.js | 6 +++ .../containers/header_container.js | 17 ++++++++ app/soapbox/reducers/relationships.js | 2 + app/soapbox/utils/features.ts | 9 +++++ 6 files changed, 83 insertions(+) diff --git a/app/soapbox/actions/accounts.js b/app/soapbox/actions/accounts.js index 1005af838..4df56417e 100644 --- a/app/soapbox/actions/accounts.js +++ b/app/soapbox/actions/accounts.js @@ -57,6 +57,10 @@ export const ACCOUNT_UNPIN_REQUEST = 'ACCOUNT_UNPIN_REQUEST'; export const ACCOUNT_UNPIN_SUCCESS = 'ACCOUNT_UNPIN_SUCCESS'; export const ACCOUNT_UNPIN_FAIL = 'ACCOUNT_UNPIN_FAIL'; +export const ACCOUNT_REMOVE_FROM_FOLLOWERS_REQUEST = 'ACCOUNT_REMOVE_FROM_FOLLOWERS_REQUEST'; +export const ACCOUNT_REMOVE_FROM_FOLLOWERS_SUCCESS = 'ACCOUNT_REMOVE_FROM_FOLLOWERS_SUCCESS'; +export const ACCOUNT_REMOVE_FROM_FOLLOWERS_FAIL = 'ACCOUNT_REMOVE_FROM_FOLLOWERS_FAIL'; + export const PINNED_ACCOUNTS_FETCH_REQUEST = 'PINNED_ACCOUNTS_FETCH_REQUEST'; export const PINNED_ACCOUNTS_FETCH_SUCCESS = 'PINNED_ACCOUNTS_FETCH_SUCCESS'; export const PINNED_ACCOUNTS_FETCH_FAIL = 'PINNED_ACCOUNTS_FETCH_FAIL'; @@ -520,6 +524,42 @@ export function unsubscribeAccountFail(error) { }; } + +export function removeFromFollowers(id) { + return (dispatch, getState) => { + if (!isLoggedIn(getState)) return; + + dispatch(muteAccountRequest(id)); + + api(getState).post(`/api/v1/accounts/${id}/remove_from_followers`).then(response => { + dispatch(removeFromFollowersSuccess(response.data)); + }).catch(error => { + dispatch(removeFromFollowersFail(id, error)); + }); + }; +} + +export function removeFromFollowersRequest(id) { + return { + type: ACCOUNT_REMOVE_FROM_FOLLOWERS_REQUEST, + id, + }; +} + +export function removeFromFollowersSuccess(relationship) { + return { + type: ACCOUNT_REMOVE_FROM_FOLLOWERS_SUCCESS, + relationship, + }; +} + +export function removeFromFollowersFail(error) { + return { + type: ACCOUNT_REMOVE_FROM_FOLLOWERS_FAIL, + error, + }; +} + export function fetchFollowers(id) { return (dispatch, getState) => { dispatch(fetchFollowersRequest(id)); diff --git a/app/soapbox/features/account/components/header.js b/app/soapbox/features/account/components/header.js index 47c1172c7..7a65b0bef 100644 --- a/app/soapbox/features/account/components/header.js +++ b/app/soapbox/features/account/components/header.js @@ -48,6 +48,7 @@ const messages = defineMessages({ mutes: { id: 'navigation_bar.mutes', defaultMessage: 'Muted users' }, endorse: { id: 'account.endorse', defaultMessage: 'Feature on profile' }, unendorse: { id: 'account.unendorse', defaultMessage: 'Don\'t feature on profile' }, + removeFromFollowers: { id: 'account.remove_from_followers', defaultMessage: 'Remove this follower' }, admin_account: { id: 'status.admin_account', defaultMessage: 'Open moderation interface for @{name}' }, add_or_remove_from_list: { id: 'account.add_or_remove_from_list', defaultMessage: 'Add or Remove from lists' }, deactivateUser: { id: 'admin.users.actions.deactivate_user', defaultMessage: 'Deactivate @{name}' }, @@ -283,6 +284,14 @@ class Header extends ImmutablePureComponent { }); } + if (features.removeFromFollowers && account.getIn(['relationship', 'followed_by'])) { + menu.push({ + text: intl.formatMessage(messages.removeFromFollowers), + action: this.props.onRemoveFromFollowers, + icon: require('@tabler/icons/icons/user-x.svg'), + }); + } + if (account.getIn(['relationship', 'muting'])) { menu.push({ text: intl.formatMessage(messages.unmute, { name: account.get('username') }), diff --git a/app/soapbox/features/account_timeline/components/header.js b/app/soapbox/features/account_timeline/components/header.js index b969e0b61..bba6bbcd5 100644 --- a/app/soapbox/features/account_timeline/components/header.js +++ b/app/soapbox/features/account_timeline/components/header.js @@ -25,6 +25,7 @@ class Header extends ImmutablePureComponent { onUnblockDomain: PropTypes.func.isRequired, onEndorseToggle: PropTypes.func.isRequired, onAddToList: PropTypes.func.isRequired, + onRemoveFromFollowers: PropTypes.func.isRequired, username: PropTypes.string, history: PropTypes.object, }; @@ -141,6 +142,10 @@ class Header extends ImmutablePureComponent { this.props.onShowNote(this.props.account); } + handleRemoveFromFollowers = () => { + this.props.onRemoveFromFollowers(this.props.account); + } + render() { const { account } = this.props; const moved = (account) ? account.get('moved') : false; @@ -177,6 +182,7 @@ class Header extends ImmutablePureComponent { onSuggestUser={this.handleSuggestUser} onUnsuggestUser={this.handleUnsuggestUser} onShowNote={this.handleShowNote} + onRemoveFromFollowers={this.handleRemoveFromFollowers} username={this.props.username} /> diff --git a/app/soapbox/features/account_timeline/containers/header_container.js b/app/soapbox/features/account_timeline/containers/header_container.js index 43ceceb2f..baf0ccb17 100644 --- a/app/soapbox/features/account_timeline/containers/header_container.js +++ b/app/soapbox/features/account_timeline/containers/header_container.js @@ -13,6 +13,7 @@ import { unpinAccount, subscribeAccount, unsubscribeAccount, + removeFromFollowers, } from 'soapbox/actions/accounts'; import { verifyUser, @@ -56,6 +57,7 @@ const messages = defineMessages({ demotedToUser: { id: 'admin.users.actions.demote_to_user_message', defaultMessage: '@{acct} was demoted to a regular user' }, userSuggested: { id: 'admin.users.user_suggested_message', defaultMessage: '@{acct} was suggested' }, userUnsuggested: { id: 'admin.users.user_unsuggested_message', defaultMessage: '@{acct} was unsuggested' }, + removeFromFollowersConfirm: { id: 'confirmations.remove_from_followers.confirm', defaultMessage: 'Remove' }, }); const makeMapStateToProps = () => { @@ -269,6 +271,21 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ onShowNote(account) { dispatch(initAccountNoteModal(account)); }, + + onRemoveFromFollowers(account) { + dispatch((_, getState) => { + const unfollowModal = getSettings(getState()).get('unfollowModal'); + if (unfollowModal) { + dispatch(openModal('CONFIRM', { + message: @{account.get('acct')} }} />, + confirm: intl.formatMessage(messages.removeFromFollowersConfirm), + onConfirm: () => dispatch(removeFromFollowers(account.get('id'))), + })); + } else { + dispatch(removeFromFollowers(account.get('id'))); + } + }); + }, }); export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(Header)); diff --git a/app/soapbox/reducers/relationships.js b/app/soapbox/reducers/relationships.js index c63ee978e..80754842c 100644 --- a/app/soapbox/reducers/relationships.js +++ b/app/soapbox/reducers/relationships.js @@ -19,6 +19,7 @@ import { ACCOUNT_UNSUBSCRIBE_SUCCESS, ACCOUNT_PIN_SUCCESS, ACCOUNT_UNPIN_SUCCESS, + ACCOUNT_REMOVE_FROM_FOLLOWERS_SUCCESS, RELATIONSHIPS_FETCH_SUCCESS, } from '../actions/accounts'; import { @@ -108,6 +109,7 @@ export default function relationships(state = initialState, action) { case ACCOUNT_PIN_SUCCESS: case ACCOUNT_UNPIN_SUCCESS: case ACCOUNT_NOTE_SUBMIT_SUCCESS: + case ACCOUNT_REMOVE_FROM_FOLLOWERS_SUCCESS: return normalizeRelationship(state, action.relationship); case RELATIONSHIPS_FETCH_SUCCESS: return normalizeRelationships(state, action.relationships); diff --git a/app/soapbox/utils/features.ts b/app/soapbox/utils/features.ts index 1e4d76266..7819b2554 100644 --- a/app/soapbox/utils/features.ts +++ b/app/soapbox/utils/features.ts @@ -420,6 +420,15 @@ const getInstanceFeatures = (instance: Instance) => { */ remoteInteractionsAPI: v.software === PLEROMA && gte(v.version, '2.4.50'), + /** + * Ability to remove an account from your followers. + * @see POST /api/v1/accounts/:id/remove_from_followers + */ + removeFromFollowers: any([ + v.software === MASTODON && gte(v.compatVersion, '3.5.0'), + v.software === PLEROMA && v.build === SOAPBOX && gte(v.version, '2.4.50'), + ]), + reportMultipleStatuses: any([ v.software === MASTODON, v.software === PLEROMA, From 721772b08f74de8d09e1f4a9c8a85faed0ee0e12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Tue, 17 May 2022 15:09:53 +0200 Subject: [PATCH 007/102] Fix hotkey 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.tsx | 3 ++ app/soapbox/components/status.tsx | 8 ++--- app/soapbox/components/status_list.js | 23 ++++++------- .../components/conversations_list.js | 30 +++++++++-------- app/soapbox/features/notifications/index.js | 32 +++++++++++-------- app/soapbox/features/status/index.tsx | 2 +- 6 files changed, 54 insertions(+), 44 deletions(-) diff --git a/app/soapbox/components/scrollable_list.tsx b/app/soapbox/components/scrollable_list.tsx index cc4b55867..c7a93a880 100644 --- a/app/soapbox/components/scrollable_list.tsx +++ b/app/soapbox/components/scrollable_list.tsx @@ -42,6 +42,7 @@ interface IScrollableList extends VirtuosoProps { onRefresh?: () => Promise, className?: string, itemClassName?: string, + id?: string, } /** Legacy ScrollableList with Virtuoso for backwards-compatibility */ @@ -58,6 +59,7 @@ const ScrollableList = React.forwardRef(({ onLoadMore, className, itemClassName, + id, hasMore, placeholderComponent: Placeholder, placeholderCount = 0, @@ -130,6 +132,7 @@ const ScrollableList = React.forwardRef(({ { } handleHotkeyMoveUp = (e?: KeyboardEvent): void => { - // FIXME: what's going on here? - // this.props.onMoveUp(this.props.status.id, e?.target?.getAttribute('data-featured')); + this.props.onMoveUp(this.props.status.id, this.props.featured); } handleHotkeyMoveDown = (e?: KeyboardEvent): void => { - // FIXME: what's going on here? - // this.props.onMoveDown(this.props.status.id, e?.target?.getAttribute('data-featured')); + this.props.onMoveDown(this.props.status.id, this.props.featured); } handleHotkeyToggleHidden = (): void => { @@ -601,7 +599,7 @@ class Status extends ImmutablePureComponent { return (
{ + const element = document.querySelector(`#status-list [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(); + } + }, + }); } handleDequeueTimeline = () => { @@ -216,6 +216,7 @@ export default class StatusList extends ImmutablePureComponent { message={messages.queue} />, { const elementIndex = this.getCurrentIndex(id) - 1; - this._selectChild(elementIndex, true); + this._selectChild(elementIndex); } handleMoveDown = id => { const elementIndex = this.getCurrentIndex(id) + 1; - this._selectChild(elementIndex, false); + this._selectChild(elementIndex); } - _selectChild(index, align_top) { - const container = this.node.node; - const element = container.querySelector(`article:nth-of-type(${index + 1}) .focusable`); + _selectChild(index) { + this.node.scrollIntoView({ + index, + behavior: 'smooth', + done: () => { + const element = document.querySelector(`#direct-list [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(); + } + }, + }); } setRef = c => { @@ -58,7 +58,9 @@ export default class ConversationsList extends ImmutablePureComponent { diff --git a/app/soapbox/features/notifications/index.js b/app/soapbox/features/notifications/index.js index 69c18799e..05fed79a7 100644 --- a/app/soapbox/features/notifications/index.js +++ b/app/soapbox/features/notifications/index.js @@ -111,32 +111,37 @@ class Notifications extends React.PureComponent { this.props.dispatch(scrollTopNotifications(false)); }, 100); + setRef = c => { + this.node = c; + } + setColumnRef = c => { this.column = c; } handleMoveUp = id => { const elementIndex = this.props.notifications.findIndex(item => item !== null && item.get('id') === id) - 1; - this._selectChild(elementIndex, true); + this._selectChild(elementIndex); } handleMoveDown = id => { const elementIndex = this.props.notifications.findIndex(item => item !== null && item.get('id') === id) + 1; - this._selectChild(elementIndex, false); + this._selectChild(elementIndex); } - _selectChild(index, align_top) { - const container = this.column; - const element = container.querySelector(`article:nth-of-type(${index + 1}) .focusable`); + _selectChild(index) { + this.node.scrollIntoView({ + index, + behavior: 'smooth', + done: () => { + const container = this.column; + const element = container.querySelector(`[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(); + } + }, + }); } handleDequeueNotifications = () => { @@ -161,6 +166,7 @@ class Notifications extends React.PureComponent { const scrollContainer = ( {
Date: Wed, 18 May 2022 14:38:49 -0500 Subject: [PATCH 008/102] SoapboxMount: display a spinner while API requests are loading --- app/soapbox/components/ui/spinner/spinner.css | 2 +- app/soapbox/components/ui/spinner/spinner.tsx | 6 ++--- app/soapbox/containers/soapbox.tsx | 24 +++++++++++++++---- 3 files changed, 24 insertions(+), 8 deletions(-) diff --git a/app/soapbox/components/ui/spinner/spinner.css b/app/soapbox/components/ui/spinner/spinner.css index 177f9dad5..f048ee7fc 100644 --- a/app/soapbox/components/ui/spinner/spinner.css +++ b/app/soapbox/components/ui/spinner/spinner.css @@ -1,9 +1,9 @@ - /** * iOS style loading spinner. * Adapted from: https://loading.io/css/ * With some help scaling it: https://signalvnoise.com/posts/2577-loading-spinner-animation-using-css-and-webkit */ + .spinner { @apply inline-block relative w-20 h-20; } diff --git a/app/soapbox/components/ui/spinner/spinner.tsx b/app/soapbox/components/ui/spinner/spinner.tsx index c4b77a120..98c3eaae7 100644 --- a/app/soapbox/components/ui/spinner/spinner.tsx +++ b/app/soapbox/components/ui/spinner/spinner.tsx @@ -6,7 +6,7 @@ import Text from '../text/text'; import './spinner.css'; -interface ILoadingIndicator { +interface ISpinner { /** Width and height of the spinner in pixels. */ size?: number, /** Whether to display "Loading..." beneath the spinner. */ @@ -14,7 +14,7 @@ interface ILoadingIndicator { } /** Spinning loading placeholder. */ -const LoadingIndicator = ({ size = 30, withText = true }: ILoadingIndicator) => ( +const Spinner = ({ size = 30, withText = true }: ISpinner) => (
{Array.from(Array(12).keys()).map(i => ( @@ -30,4 +30,4 @@ const LoadingIndicator = ({ size = 30, withText = true }: ILoadingIndicator) => ); -export default LoadingIndicator; +export default Spinner; diff --git a/app/soapbox/containers/soapbox.tsx b/app/soapbox/containers/soapbox.tsx index cde450e65..d8ffd97d5 100644 --- a/app/soapbox/containers/soapbox.tsx +++ b/app/soapbox/containers/soapbox.tsx @@ -14,6 +14,7 @@ import { loadSoapboxConfig, getSoapboxConfig } from 'soapbox/actions/soapbox'; import { fetchVerificationConfig } from 'soapbox/actions/verification'; import * as BuildConfig from 'soapbox/build_config'; import Helmet from 'soapbox/components/helmet'; +import { Spinner } from 'soapbox/components/ui'; import AuthLayout from 'soapbox/features/auth_layout'; import OnboardingWizard from 'soapbox/features/onboarding/onboarding-wizard'; import PublicLayout from 'soapbox/features/public_layout'; @@ -115,10 +116,25 @@ const SoapboxMount = () => { return !(location.state?.soapboxModalKey && location.state?.soapboxModalKey !== prevRouterProps?.location?.state?.soapboxModalKey); }; - if (me === null) return null; - if (me && !account) return null; - if (!isLoaded) return null; - if (localeLoading) return null; + /** Whether to display a loading indicator. */ + const showLoading = [ + me === null, + me && !account, + !isLoaded, + localeLoading, + ].some(Boolean); + + if (showLoading) { + return ( +
+ + {themeCss && } + + + +
+ ); + } const waitlisted = account && !account.source.get('approved', true); From d2968118518833d9da75a266539e0a1d497cdfba Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 19 May 2022 20:14:49 -0500 Subject: [PATCH 009/102] Suggestions: store as OrderedSet --- app/soapbox/reducers/__tests__/suggestions-test.js | 4 ++-- app/soapbox/reducers/suggestions.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/soapbox/reducers/__tests__/suggestions-test.js b/app/soapbox/reducers/__tests__/suggestions-test.js index 01c1f1aff..4478f37a6 100644 --- a/app/soapbox/reducers/__tests__/suggestions-test.js +++ b/app/soapbox/reducers/__tests__/suggestions-test.js @@ -1,4 +1,4 @@ -import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable'; +import { Map as ImmutableMap, OrderedSet as ImmutableOrderedSet, fromJS } from 'immutable'; import { SUGGESTIONS_DISMISS } from 'soapbox/actions/suggestions'; @@ -7,7 +7,7 @@ import reducer from '../suggestions'; describe('suggestions reducer', () => { it('should return the initial state', () => { expect(reducer(undefined, {})).toEqual(ImmutableMap({ - items: ImmutableList(), + items: ImmutableOrderedSet(), next: null, isLoading: false, })); diff --git a/app/soapbox/reducers/suggestions.js b/app/soapbox/reducers/suggestions.js index 03f782d3e..6b3a63da9 100644 --- a/app/soapbox/reducers/suggestions.js +++ b/app/soapbox/reducers/suggestions.js @@ -1,4 +1,4 @@ -import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable'; +import { Map as ImmutableMap, OrderedSet as ImmutableOrderedSet, fromJS } from 'immutable'; import { ACCOUNT_BLOCK_SUCCESS, ACCOUNT_MUTE_SUCCESS } from 'soapbox/actions/accounts'; import { DOMAIN_BLOCK_SUCCESS } from 'soapbox/actions/domain_blocks'; @@ -14,7 +14,7 @@ import { } from '../actions/suggestions'; const initialState = ImmutableMap({ - items: ImmutableList(), + items: ImmutableOrderedSet(), next: null, isLoading: false, }); From bd6ce38e5d1dbc399d07cf30ab5d9f3c2c4fb348 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 20 May 2022 09:58:59 -0500 Subject: [PATCH 010/102] checkEmailAvailability: fail silently --- app/soapbox/actions/verification.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/soapbox/actions/verification.js b/app/soapbox/actions/verification.js index f90a5637a..8bd34b75c 100644 --- a/app/soapbox/actions/verification.js +++ b/app/soapbox/actions/verification.js @@ -244,7 +244,9 @@ function checkEmailAvailability(email) { return api(getState).get(`/api/v1/pepe/account/exists?email=${email}`, { headers: { Authorization: `Bearer ${token}` }, - }).finally(() => dispatch({ type: SET_LOADING, value: false })); + }) + .catch(() => {}) + .then(() => dispatch({ type: SET_LOADING, value: false })); }; } From f3f6a156866c4ce4c8597124317b1b48fd99c431 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 20 May 2022 10:15:47 -0500 Subject: [PATCH 011/102] LandingPageModal: use Text component --- .../features/ui/components/modals/landing-page-modal.tsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/app/soapbox/features/ui/components/modals/landing-page-modal.tsx b/app/soapbox/features/ui/components/modals/landing-page-modal.tsx index 4f3156ef5..48e445e96 100644 --- a/app/soapbox/features/ui/components/modals/landing-page-modal.tsx +++ b/app/soapbox/features/ui/components/modals/landing-page-modal.tsx @@ -3,7 +3,7 @@ import React from 'react'; import { defineMessages, useIntl } from 'react-intl'; import SiteLogo from 'soapbox/components/site-logo'; -import { Button, Icon, Modal } from 'soapbox/components/ui'; +import { Text, Button, Icon, Modal } from 'soapbox/components/ui'; import { useAppSelector, useFeatures, useSoapboxConfig } from 'soapbox/hooks'; const messages = defineMessages({ @@ -17,6 +17,7 @@ interface ILandingPageModal { onClose: (type: string) => void, } +/** Login and links to display from the hamburger menu of the homepage. */ const LandingPageModal: React.FC = ({ onClose }) => { const intl = useIntl(); @@ -41,13 +42,13 @@ const LandingPageModal: React.FC = ({ onClose }) => { - + {intl.formatMessage(messages.helpCenter)} - + )} From 84b04250ac6de48efb7d923bb3768d010384b175 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 20 May 2022 12:20:31 -0500 Subject: [PATCH 012/102] Add LoadingScreen for fullscreen, theme-specific loading --- app/soapbox/components/loading-screen.tsx | 19 +++++++++++++++++++ app/soapbox/containers/soapbox.tsx | 8 ++++---- 2 files changed, 23 insertions(+), 4 deletions(-) create mode 100644 app/soapbox/components/loading-screen.tsx diff --git a/app/soapbox/components/loading-screen.tsx b/app/soapbox/components/loading-screen.tsx new file mode 100644 index 000000000..84b5bf306 --- /dev/null +++ b/app/soapbox/components/loading-screen.tsx @@ -0,0 +1,19 @@ +import React from 'react'; + +import LandingGradient from 'soapbox/components/landing-gradient'; +import { Spinner } from 'soapbox/components/ui'; + +/** Fullscreen loading indicator. */ +const LoadingScreen: React.FC = () => { + return ( +
+ + +
+ +
+
+ ); +}; + +export default LoadingScreen; diff --git a/app/soapbox/containers/soapbox.tsx b/app/soapbox/containers/soapbox.tsx index d8ffd97d5..0a86ad0fc 100644 --- a/app/soapbox/containers/soapbox.tsx +++ b/app/soapbox/containers/soapbox.tsx @@ -14,7 +14,7 @@ import { loadSoapboxConfig, getSoapboxConfig } from 'soapbox/actions/soapbox'; import { fetchVerificationConfig } from 'soapbox/actions/verification'; import * as BuildConfig from 'soapbox/build_config'; import Helmet from 'soapbox/components/helmet'; -import { Spinner } from 'soapbox/components/ui'; +import LoadingScreen from 'soapbox/components/loading-screen'; import AuthLayout from 'soapbox/features/auth_layout'; import OnboardingWizard from 'soapbox/features/onboarding/onboarding-wizard'; import PublicLayout from 'soapbox/features/public_layout'; @@ -126,13 +126,13 @@ const SoapboxMount = () => { if (showLoading) { return ( -
+ <> {themeCss && } - -
+ + ); } From 4cddf0c962ebf706d6e2a1af75f321f1eb23fedb Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 20 May 2022 12:28:59 -0500 Subject: [PATCH 013/102] SoapboxMount: create showOnboarding variable --- app/soapbox/containers/soapbox.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/soapbox/containers/soapbox.tsx b/app/soapbox/containers/soapbox.tsx index 0a86ad0fc..e377be771 100644 --- a/app/soapbox/containers/soapbox.tsx +++ b/app/soapbox/containers/soapbox.tsx @@ -80,7 +80,9 @@ const SoapboxMount = () => { const locale = validLocale(settings.get('locale')) ? settings.get('locale') : 'en'; + const waitlisted = account && !account.source.get('approved', true); const needsOnboarding = useAppSelector(state => state.onboarding.needsOnboarding); + const showOnboarding = account && !waitlisted && needsOnboarding; const singleUserMode = soapboxConfig.singleUserMode && soapboxConfig.singleUserModeProfile; const [messages, setMessages] = useState>({}); @@ -136,8 +138,6 @@ const SoapboxMount = () => { ); } - const waitlisted = account && !account.source.get('approved', true); - const bodyClass = classNames('bg-white dark:bg-slate-900 text-base h-full', { 'no-reduce-motion': !settings.get('reduceMotion'), 'underline-links': settings.get('underlineLinks'), @@ -145,7 +145,7 @@ const SoapboxMount = () => { 'demetricator': settings.get('demetricator'), }); - if (account && !waitlisted && needsOnboarding) { + if (showOnboarding) { return ( From f72ee6aad0c2899d5c0b15b3caa8e71872f84adc Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 20 May 2022 12:30:29 -0500 Subject: [PATCH 014/102] SoapboxMount: add comments --- app/soapbox/containers/soapbox.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/soapbox/containers/soapbox.tsx b/app/soapbox/containers/soapbox.tsx index e377be771..75767f237 100644 --- a/app/soapbox/containers/soapbox.tsx +++ b/app/soapbox/containers/soapbox.tsx @@ -67,6 +67,7 @@ const loadInitial = () => { }; }; +/** Highest level node with the Redux store. */ const SoapboxMount = () => { useCachedLocationHandler(); const dispatch = useAppDispatch(); @@ -233,6 +234,7 @@ const SoapboxMount = () => { ); }; +/** The root React node of the application. */ const Soapbox = () => { return ( From 83544470475fc3d5b016e7449d47eac989ced0c0 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 20 May 2022 12:51:57 -0500 Subject: [PATCH 015/102] SoapboxMount: refactor render to be DRY --- app/soapbox/containers/soapbox.tsx | 179 +++++++++++++++-------------- 1 file changed, 92 insertions(+), 87 deletions(-) diff --git a/app/soapbox/containers/soapbox.tsx b/app/soapbox/containers/soapbox.tsx index 75767f237..2d89cc9a2 100644 --- a/app/soapbox/containers/soapbox.tsx +++ b/app/soapbox/containers/soapbox.tsx @@ -127,18 +127,6 @@ const SoapboxMount = () => { localeLoading, ].some(Boolean); - if (showLoading) { - return ( - <> - - {themeCss && } - - - - - ); - } - const bodyClass = classNames('bg-white dark:bg-slate-900 text-base h-full', { 'no-reduce-motion': !settings.get('reduceMotion'), 'underline-links': settings.get('underlineLinks'), @@ -146,88 +134,105 @@ const SoapboxMount = () => { 'demetricator': settings.get('demetricator'), }); - if (showOnboarding) { - return ( - - - - - {themeCss && } - - + const helmet = ( + + + + {themeCss && } + + + ); - - - - - - - - ); + /** Render loading screen. */ + const renderLoading = () => ( + <> + {helmet} + + + ); + + /** Render the onboarding flow. */ + const renderOnboarding = () => ( + <> + + + + ); + + /** Render the auth layout or UI. */ + const renderSwitch = () => ( + + + + {/* Redirect signup route depending on Pepe enablement. */} + {/* We should prefer using /signup in components. */} + {pepeEnabled ? ( + + ) : ( + + )} + + {waitlisted && ( + <> + } /> + + + {Component => } + + + )} + + {!me && (singleUserMode + ? + : )} + + {!me && ( + + )} + + + + + + {(features.accountCreation && instance.registrations) && ( + + )} + + {pepeEnabled && ( + + )} + + + + + + + + ); + + /** Render the onboarding flow or UI. */ + const renderBody = () => { + if (showOnboarding) { + return renderOnboarding(); + } else { + return renderSwitch(); + } + }; + + // intl is part of loading. + // It's important nothing in here depends on intl. + if (showLoading) { + return renderLoading(); } return ( - - - - {themeCss && } - - - + {helmet} - <> - - - - - {/* Redirect signup route depending on Pepe enablement. */} - {/* We should prefer using /signup in components. */} - {pepeEnabled ? ( - - ) : ( - - )} - - {waitlisted && ( - <> - } /> - - - {Component => } - - - )} - - {!me && (singleUserMode - ? - : )} - - {!me && ( - - )} - - - - - - {(features.accountCreation && instance.registrations) && ( - - )} - - {pepeEnabled && ( - - )} - - - - - - - - - + + {renderBody()} + From e298115fcf90ed9c0543e46afaebd2d3f18eafe7 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 20 May 2022 13:00:38 -0500 Subject: [PATCH 016/102] SoapboxMount: async import NotificationsContainer, ModalContainer, remove them elsewhere --- app/soapbox/containers/soapbox.tsx | 21 ++++++++++--------- app/soapbox/features/auth_layout/index.tsx | 6 ------ app/soapbox/features/public_layout/index.tsx | 13 ------------ app/soapbox/features/ui/index.tsx | 10 --------- .../features/verification/waitlist_page.js | 6 ------ 5 files changed, 11 insertions(+), 45 deletions(-) diff --git a/app/soapbox/containers/soapbox.tsx b/app/soapbox/containers/soapbox.tsx index 2d89cc9a2..2d339fea5 100644 --- a/app/soapbox/containers/soapbox.tsx +++ b/app/soapbox/containers/soapbox.tsx @@ -18,8 +18,8 @@ import LoadingScreen from 'soapbox/components/loading-screen'; import AuthLayout from 'soapbox/features/auth_layout'; import OnboardingWizard from 'soapbox/features/onboarding/onboarding-wizard'; import PublicLayout from 'soapbox/features/public_layout'; -import NotificationsContainer from 'soapbox/features/ui/containers/notifications_container'; -import { ModalContainer } from 'soapbox/features/ui/util/async-components'; +import BundleContainer from 'soapbox/features/ui/containers/bundle_container'; +import { ModalContainer, NotificationsContainer } from 'soapbox/features/ui/util/async-components'; import WaitlistPage from 'soapbox/features/verification/waitlist_page'; import { createGlobals } from 'soapbox/globals'; import { useAppSelector, useAppDispatch, useOwnAccount, useFeatures, useSoapboxConfig, useSettings, useSystemTheme } from 'soapbox/hooks'; @@ -31,7 +31,6 @@ import { checkOnboardingStatus } from '../actions/onboarding'; import { preload } from '../actions/preload'; import ErrorBoundary from '../components/error_boundary'; import UI from '../features/ui'; -import BundleContainer from '../features/ui/containers/bundle_container'; import { store } from '../store'; /** Ensure the given locale exists in our codebase */ @@ -173,13 +172,7 @@ const SoapboxMount = () => { )} {waitlisted && ( - <> - } /> - - - {Component => } - - + } /> )} {!me && (singleUserMode @@ -232,6 +225,14 @@ const SoapboxMount = () => { {renderBody()} + + + {(Component) => } + + + + {Component => } + diff --git a/app/soapbox/features/auth_layout/index.tsx b/app/soapbox/features/auth_layout/index.tsx index 1ac59b3de..ca72f4107 100644 --- a/app/soapbox/features/auth_layout/index.tsx +++ b/app/soapbox/features/auth_layout/index.tsx @@ -4,8 +4,6 @@ import { Link, Redirect, Route, Switch, useHistory } from 'react-router-dom'; import LandingGradient from 'soapbox/components/landing-gradient'; import SiteLogo from 'soapbox/components/site-logo'; -import BundleContainer from 'soapbox/features/ui/containers/bundle_container'; -import { NotificationsContainer } from 'soapbox/features/ui/util/async-components'; import { useAppSelector, useFeatures, useSoapboxConfig } from 'soapbox/hooks'; import { Button, Card, CardBody } from '../../components/ui'; @@ -86,10 +84,6 @@ const AuthLayout = () => {
- - - {(Component) => } -
); }; diff --git a/app/soapbox/features/public_layout/index.tsx b/app/soapbox/features/public_layout/index.tsx index 3fe4834a5..5c7e45107 100644 --- a/app/soapbox/features/public_layout/index.tsx +++ b/app/soapbox/features/public_layout/index.tsx @@ -2,11 +2,6 @@ import React from 'react'; import { Switch, Route, Redirect } from 'react-router-dom'; import LandingGradient from 'soapbox/components/landing-gradient'; -import BundleContainer from 'soapbox/features/ui/containers/bundle_container'; -import { - NotificationsContainer, - ModalContainer, -} from 'soapbox/features/ui/util/async-components'; import { useAppSelector } from 'soapbox/hooks'; import { isStandalone } from 'soapbox/utils/state'; @@ -42,14 +37,6 @@ const PublicLayout = () => {
- - - {(Component) => } - - - - {(Component) => } -
); diff --git a/app/soapbox/features/ui/index.tsx b/app/soapbox/features/ui/index.tsx index 164198e37..57bfb02f1 100644 --- a/app/soapbox/features/ui/index.tsx +++ b/app/soapbox/features/ui/index.tsx @@ -104,8 +104,6 @@ import { Directory, SidebarMenu, UploadArea, - NotificationsContainer, - ModalContainer, ProfileHoverCard, Share, NewStatus, @@ -670,14 +668,6 @@ const UI: React.FC = ({ children }) => { {me && floatingActionButton} - - {Component => } - - - - {Component => } - - {Component => } diff --git a/app/soapbox/features/verification/waitlist_page.js b/app/soapbox/features/verification/waitlist_page.js index 1e87e2078..14047b7b1 100644 --- a/app/soapbox/features/verification/waitlist_page.js +++ b/app/soapbox/features/verification/waitlist_page.js @@ -7,8 +7,6 @@ import { Link } from 'react-router-dom'; import { openModal } from 'soapbox/actions/modals'; import LandingGradient from 'soapbox/components/landing-gradient'; import SiteLogo from 'soapbox/components/site-logo'; -import BundleContainer from 'soapbox/features/ui/containers/bundle_container'; -import { NotificationsContainer } from 'soapbox/features/ui/util/async-components'; import { useAppSelector, useOwnAccount } from 'soapbox/hooks'; import { logOut } from '../../actions/auth'; @@ -74,10 +72,6 @@ const WaitlistPage = ({ account }) => { - - - {(Component) => } - ); }; From f0ba5a5a8cb25b3ba5901b809b978b4e8c553f1e Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 20 May 2022 13:13:17 -0500 Subject: [PATCH 017/102] SoapboxMount: load onboarding flow async, refactor --- app/soapbox/components/loading-screen.tsx | 2 +- app/soapbox/containers/soapbox.tsx | 45 ++++++++++--------- .../features/ui/util/async-components.ts | 4 ++ 3 files changed, 28 insertions(+), 23 deletions(-) diff --git a/app/soapbox/components/loading-screen.tsx b/app/soapbox/components/loading-screen.tsx index 84b5bf306..92882f98f 100644 --- a/app/soapbox/components/loading-screen.tsx +++ b/app/soapbox/components/loading-screen.tsx @@ -6,7 +6,7 @@ import { Spinner } from 'soapbox/components/ui'; /** Fullscreen loading indicator. */ const LoadingScreen: React.FC = () => { return ( -
+
diff --git a/app/soapbox/containers/soapbox.tsx b/app/soapbox/containers/soapbox.tsx index 2d339fea5..b9f41a464 100644 --- a/app/soapbox/containers/soapbox.tsx +++ b/app/soapbox/containers/soapbox.tsx @@ -16,10 +16,13 @@ import * as BuildConfig from 'soapbox/build_config'; import Helmet from 'soapbox/components/helmet'; import LoadingScreen from 'soapbox/components/loading-screen'; import AuthLayout from 'soapbox/features/auth_layout'; -import OnboardingWizard from 'soapbox/features/onboarding/onboarding-wizard'; import PublicLayout from 'soapbox/features/public_layout'; import BundleContainer from 'soapbox/features/ui/containers/bundle_container'; -import { ModalContainer, NotificationsContainer } from 'soapbox/features/ui/util/async-components'; +import { + ModalContainer, + NotificationsContainer, + OnboardingWizard, +} from 'soapbox/features/ui/util/async-components'; import WaitlistPage from 'soapbox/features/verification/waitlist_page'; import { createGlobals } from 'soapbox/globals'; import { useAppSelector, useAppDispatch, useOwnAccount, useFeatures, useSoapboxConfig, useSettings, useSystemTheme } from 'soapbox/hooks'; @@ -142,20 +145,11 @@ const SoapboxMount = () => { ); - /** Render loading screen. */ - const renderLoading = () => ( - <> - {helmet} - - - ); - /** Render the onboarding flow. */ const renderOnboarding = () => ( - <> - - - + + {(Component) => } + ); /** Render the auth layout or UI. */ @@ -215,7 +209,12 @@ const SoapboxMount = () => { // intl is part of loading. // It's important nothing in here depends on intl. if (showLoading) { - return renderLoading(); + return ( + <> + {helmet} + + + ); } return ( @@ -224,15 +223,17 @@ const SoapboxMount = () => { - {renderBody()} + <> + {renderBody()} - - {(Component) => } - + + {(Component) => } + - - {Component => } - + + {Component => } + + diff --git a/app/soapbox/features/ui/util/async-components.ts b/app/soapbox/features/ui/util/async-components.ts index 34d2f36b9..53efab79f 100644 --- a/app/soapbox/features/ui/util/async-components.ts +++ b/app/soapbox/features/ui/util/async-components.ts @@ -494,6 +494,10 @@ export function DatePicker() { return import(/* webpackChunkName: "date_picker" */'../../birthdays/date_picker'); } +export function OnboardingWizard() { + return import(/* webpackChunkName: "features/onboarding" */'../../onboarding/onboarding-wizard'); +} + export function CompareHistoryModal() { return import(/*webpackChunkName: "modals/compare_history_modal" */'../components/compare_history_modal'); } From 4e8a3cdce5f8c1a3f484ef49a74abd8a576cd2aa Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 20 May 2022 13:29:17 -0500 Subject: [PATCH 018/102] SoapboxMount: load WaitlistPage async --- app/soapbox/containers/soapbox.tsx | 9 +++++++-- app/soapbox/features/ui/util/async-components.ts | 4 ++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/app/soapbox/containers/soapbox.tsx b/app/soapbox/containers/soapbox.tsx index b9f41a464..fbb62d6e3 100644 --- a/app/soapbox/containers/soapbox.tsx +++ b/app/soapbox/containers/soapbox.tsx @@ -22,8 +22,8 @@ import { ModalContainer, NotificationsContainer, OnboardingWizard, + WaitlistPage, } from 'soapbox/features/ui/util/async-components'; -import WaitlistPage from 'soapbox/features/verification/waitlist_page'; import { createGlobals } from 'soapbox/globals'; import { useAppSelector, useAppDispatch, useOwnAccount, useFeatures, useSoapboxConfig, useSettings, useSystemTheme } from 'soapbox/hooks'; import MESSAGES from 'soapbox/locales/messages'; @@ -166,7 +166,12 @@ const SoapboxMount = () => { )} {waitlisted && ( - } /> + ( + + {(Component) => } + + )} + /> )} {!me && (singleUserMode diff --git a/app/soapbox/features/ui/util/async-components.ts b/app/soapbox/features/ui/util/async-components.ts index 53efab79f..5592772d1 100644 --- a/app/soapbox/features/ui/util/async-components.ts +++ b/app/soapbox/features/ui/util/async-components.ts @@ -498,6 +498,10 @@ export function OnboardingWizard() { return import(/* webpackChunkName: "features/onboarding" */'../../onboarding/onboarding-wizard'); } +export function WaitlistPage() { + return import(/* webpackChunkName: "features/verification" */'../../verification/waitlist_page'); +} + export function CompareHistoryModal() { return import(/*webpackChunkName: "modals/compare_history_modal" */'../components/compare_history_modal'); } From 3cd8a4b96634fffb6c7792f41331034dc9935baa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Fri, 20 May 2022 20:54:24 +0200 Subject: [PATCH 019/102] Use useFeatures hook MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- .../features/ui/components/profile_familiar_followers.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/app/soapbox/features/ui/components/profile_familiar_followers.tsx b/app/soapbox/features/ui/components/profile_familiar_followers.tsx index a0dcef2c9..f2e0c9b61 100644 --- a/app/soapbox/features/ui/components/profile_familiar_followers.tsx +++ b/app/soapbox/features/ui/components/profile_familiar_followers.tsx @@ -10,9 +10,8 @@ import { openModal } from 'soapbox/actions/modals'; import HoverRefWrapper from 'soapbox/components/hover_ref_wrapper'; import { Text } from 'soapbox/components/ui'; import VerificationBadge from 'soapbox/components/verification_badge'; -import { useAppSelector } from 'soapbox/hooks'; +import { useAppSelector, useFeatures } from 'soapbox/hooks'; import { makeGetAccount } from 'soapbox/selectors'; -import { getFeatures } from 'soapbox/utils/features'; import type { Account } from 'soapbox/types/entities'; @@ -25,7 +24,7 @@ interface IProfileFamiliarFollowers { const ProfileFamiliarFollowers: React.FC = ({ account }) => { const dispatch = useDispatch(); const me = useAppSelector((state) => state.me); - const features = useAppSelector((state) => getFeatures(state.instance)); + const features = useFeatures(); const familiarFollowerIds: ImmutableOrderedSet = useAppSelector(state => state.user_lists.getIn(['familiar_followers', account.id], ImmutableOrderedSet())); const familiarFollowers: ImmutableOrderedSet = useAppSelector(state => familiarFollowerIds.slice(0, 2).map(accountId => getAccount(state, accountId))); From 40c3793d20d876db99375ff52649ead20edebaf8 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 20 May 2022 17:18:10 -0500 Subject: [PATCH 020/102] Notifications: use a ScrollableList again --- app/soapbox/features/notifications/index.js | 87 ++++++++++----------- 1 file changed, 41 insertions(+), 46 deletions(-) diff --git a/app/soapbox/features/notifications/index.js b/app/soapbox/features/notifications/index.js index 05fed79a7..168a013f8 100644 --- a/app/soapbox/features/notifications/index.js +++ b/app/soapbox/features/notifications/index.js @@ -1,3 +1,4 @@ +import classNames from 'classnames'; import { List as ImmutableList } from 'immutable'; import { debounce } from 'lodash'; import PropTypes from 'prop-types'; @@ -5,11 +6,9 @@ import React from 'react'; import ImmutablePropTypes from 'react-immutable-proptypes'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import { connect } from 'react-redux'; -import { Virtuoso } from 'react-virtuoso'; import { createSelector } from 'reselect'; import { getSettings } from 'soapbox/actions/settings'; -import PullToRefresh from 'soapbox/components/pull-to-refresh'; import PlaceholderNotification from 'soapbox/features/placeholder/components/placeholder_notification'; import { @@ -17,8 +16,9 @@ import { scrollTopNotifications, dequeueNotifications, } from '../../actions/notifications'; +import ScrollableList from '../../components/scrollable_list'; import TimelineQueueButtonHeader from '../../components/timeline_queue_button_header'; -import { Column, Text } from '../../components/ui'; +import { Column } from '../../components/ui'; import FilterBarContainer from './containers/filter_bar_container'; import NotificationContainer from './containers/notification_container'; @@ -28,16 +28,6 @@ const messages = defineMessages({ queue: { id: 'notifications.queue_label', defaultMessage: 'Click to see {count} new {count, plural, one {notification} other {notifications}}' }, }); -const Footer = ({ context }) => ( - context.hasMore ? ( - - ) : null -); - -const Item = ({ context, ...rest }) => ( -
-); - const getNotifications = createSelector([ state => getSettings(state).getIn(['notifications', 'quickFilter', 'show']), state => getSettings(state).getIn(['notifications', 'quickFilter', 'active']), @@ -157,44 +147,49 @@ class Notifications extends React.PureComponent { const { intl, notifications, isLoading, hasMore, showFilterBar, totalQueuedNotificationsCount } = this.props; const emptyMessage = ; + let scrollableContent = null; + const filterBarContainer = showFilterBar ? () : null; - const showLoading = isLoading && !notifications || notifications.isEmpty(); - - const scrollContainer = ( - - isScrolling && this.handleScroll()} - itemContent={(_index, notification) => ( - showLoading ? ( - - ) : ( - - ) - )} - context={{ - hasMore, - }} - components={{ - ScrollSeekPlaceholder: PlaceholderNotification, - Footer, - EmptyPlaceholder: () => {emptyMessage}, - Item, - }} + if (isLoading && this.scrollableContent) { + scrollableContent = this.scrollableContent; + } else if (notifications.size > 0 || hasMore) { + scrollableContent = notifications.map((item, index) => ( + - + )); + } else { + scrollableContent = null; + } + + this.scrollableContent = scrollableContent; + + const scrollContainer = ( + 0, + 'space-y-2': notifications.size === 0, + })} + > + {scrollableContent} + ); return ( From 0587c3f245931eeb45bd6ba88efc9c5c2e3b0942 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 20 May 2022 18:47:49 -0500 Subject: [PATCH 021/102] GitLab CI: test Mastodon Nginx conf --- .gitlab-ci.yml | 9 +++++++++ installation/mastodon.conf | 5 +++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 0e9a716ee..faa87ef71 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -59,6 +59,15 @@ jest: - "package.json" - "yarn.lock" +nginx-test: + stage: test + image: nginx:latest + before_script: cp installation/mastodon.conf /etc/nginx/conf.d/default.conf + script: nginx -t + only: + changes: + - "installation/mastodon.conf" + build-production: stage: build script: yarn build diff --git a/installation/mastodon.conf b/installation/mastodon.conf index 33b2bd894..988a098b9 100644 --- a/installation/mastodon.conf +++ b/installation/mastodon.conf @@ -39,8 +39,9 @@ server { } server { - listen 443 ssl http2; - listen [::]:443 ssl http2; + # Uncomment these lines once you acquire a certificate: + # listen 443 ssl http2; + # listen [::]:443 ssl http2; server_name example.com; ssl_protocols TLSv1.2 TLSv1.3; From 308877ab7b38909e6d0ae7d278f06289aadc1622 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Sat, 21 May 2022 07:55:51 +0200 Subject: [PATCH 022/102] Use setRef in notifications list MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- app/soapbox/features/notifications/index.js | 1 + 1 file changed, 1 insertion(+) diff --git a/app/soapbox/features/notifications/index.js b/app/soapbox/features/notifications/index.js index 168a013f8..a43ec5704 100644 --- a/app/soapbox/features/notifications/index.js +++ b/app/soapbox/features/notifications/index.js @@ -172,6 +172,7 @@ class Notifications extends React.PureComponent { const scrollContainer = ( Date: Sat, 21 May 2022 08:35:33 +0200 Subject: [PATCH 023/102] split functions for admin api actions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- app/soapbox/actions/admin.js | 279 +++++++++++++++++++++-------------- 1 file changed, 170 insertions(+), 109 deletions(-) diff --git a/app/soapbox/actions/admin.js b/app/soapbox/actions/admin.js index 313cf6203..ad56f1fbb 100644 --- a/app/soapbox/actions/admin.js +++ b/app/soapbox/actions/admin.js @@ -100,36 +100,27 @@ export function updateConfig(configs) { }; } -export function fetchReports(params = {}) { +function fetchMastodonReports(params) { return (dispatch, getState) => { - const state = getState(); - - const instance = state.get('instance'); - const features = getFeatures(instance); - - dispatch({ type: ADMIN_REPORTS_FETCH_REQUEST, params }); - - if (features.mastodonAdminApi) { - return api(getState) - .get('/api/v1/admin/reports', { params }) - .then(({ data: reports }) => { - reports.forEach(report => { - dispatch(importFetchedAccount(report.account?.account)); - dispatch(importFetchedAccount(report.target_account?.account)); - dispatch(importFetchedStatuses(report.statuses)); - }); - dispatch({ type: ADMIN_REPORTS_FETCH_SUCCESS, reports, params }); - }).catch(error => { - dispatch({ type: ADMIN_REPORTS_FETCH_FAIL, error, params }); - }); - } - - const { resolved } = params; - return api(getState) - .get('/api/pleroma/admin/reports', { params: { - state: resolved === false ? 'open' : (resolved ? 'resolved' : null), - } }) + .get('/api/v1/admin/reports', { params }) + .then(({ data: reports }) => { + reports.forEach(report => { + dispatch(importFetchedAccount(report.account?.account)); + dispatch(importFetchedAccount(report.target_account?.account)); + dispatch(importFetchedStatuses(report.statuses)); + }); + dispatch({ type: ADMIN_REPORTS_FETCH_SUCCESS, reports, params }); + }).catch(error => { + dispatch({ type: ADMIN_REPORTS_FETCH_FAIL, error, params }); + }); + }; +} + +function fetchPleromaReports(params) { + return (dispatch, getState) => { + return api(getState) + .get('/api/pleroma/admin/reports', { params }) .then(({ data: { reports } }) => { reports.forEach(report => { dispatch(importFetchedAccount(report.account)); @@ -143,28 +134,42 @@ export function fetchReports(params = {}) { }; } -function patchReports(ids, state) { +export function fetchReports(params = {}) { return (dispatch, getState) => { const state = getState(); const instance = state.get('instance'); const features = getFeatures(instance); - const reports = ids.map(id => ({ id, state })); - - dispatch({ type: ADMIN_REPORTS_PATCH_REQUEST, reports }); + dispatch({ type: ADMIN_REPORTS_FETCH_REQUEST, params }); if (features.mastodonAdminApi) { - return Promise.all(ids.map(id => api(getState) - .post(`/api/v1/admin/reports/${id}/${state === 'resolved' ? 'reopen' : 'resolve'}`) - .then(({ data }) => { - dispatch({ type: ADMIN_REPORTS_PATCH_SUCCESS, reports }); - }).catch(error => { - dispatch({ type: ADMIN_REPORTS_PATCH_FAIL, error, reports }); - }), - )); - } + return dispatch(fetchMastodonReports(params)); + } else { + const { resolved } = params; + return dispatch(fetchPleromaReports({ + state: resolved === false ? 'open' : (resolved ? 'resolved' : null), + })); + } + }; +} + +function patchMastodonReports(reports) { + return (dispatch, getState) => { + return Promise.all(reports.map(({ id, state }) => api(getState) + .post(`/api/v1/admin/reports/${id}/${state === 'resolved' ? 'reopen' : 'resolve'}`) + .then(() => { + dispatch({ type: ADMIN_REPORTS_PATCH_SUCCESS, reports }); + }).catch(error => { + dispatch({ type: ADMIN_REPORTS_PATCH_FAIL, error, reports }); + }), + )); + }; +} + +function patchPleromaReports(reports) { + return (dispatch, getState) => { return api(getState) .patch('/api/pleroma/admin/reports', { reports }) .then(() => { @@ -174,46 +179,61 @@ function patchReports(ids, state) { }); }; } -export function closeReports(ids) { - return patchReports(ids, 'closed'); -} -export function fetchUsers(filters = [], page = 1, query, pageSize = 50, next) { +function patchReports(ids, reportState) { return (dispatch, getState) => { const state = getState(); const instance = state.get('instance'); const features = getFeatures(instance); - dispatch({ type: ADMIN_USERS_FETCH_REQUEST, filters, page, pageSize }); + const reports = ids.map(id => ({ id, state: reportState })); + + dispatch({ type: ADMIN_REPORTS_PATCH_REQUEST, reports }); if (features.mastodonAdminApi) { - const params = { - username: query, - }; - - if (filters.includes('local')) params.local = true; - if (filters.includes('active')) params.active = true; - if (filters.includes('need_approval')) params.pending = true; - - return api(getState) - .get(next || '/api/v1/admin/accounts', { params }) - .then(({ data: accounts, ...response }) => { - const next = getLinks(response).refs.find(link => link.rel === 'next'); - - const count = next - ? page * pageSize + 1 - : (page - 1) * pageSize + accounts.length; - - dispatch(importFetchedAccounts(accounts.map(({ account }) => account))); - dispatch(fetchRelationships(accounts.map(account => account.id))); - dispatch({ type: ADMIN_USERS_FETCH_SUCCESS, users: accounts, count, pageSize, filters, page, next: next?.uri || false }); - return { users: accounts, count, pageSize, next: next?.uri || false }; - }).catch(error => { - dispatch({ type: ADMIN_USERS_FETCH_FAIL, error, filters, page, pageSize }); - }); + return dispatch(patchMastodonReports(reports)); + } else { + return dispatch(patchPleromaReports(reports)); } + }; +} +export function closeReports(ids) { + return patchReports(ids, 'closed'); +} + +function fetchMastodonUsers(filters, page, query, pageSize, next) { + return (dispatch, getState) => { + const params = { + username: query, + }; + + if (filters.includes('local')) params.local = true; + if (filters.includes('active')) params.active = true; + if (filters.includes('need_approval')) params.pending = true; + + return api(getState) + .get(next || '/api/v1/admin/accounts', { params }) + .then(({ data: accounts, ...response }) => { + const next = getLinks(response).refs.find(link => link.rel === 'next'); + + const count = next + ? page * pageSize + 1 + : (page - 1) * pageSize + accounts.length; + + dispatch(importFetchedAccounts(accounts.map(({ account }) => account))); + dispatch(fetchRelationships(accounts.map(account => account.id))); + dispatch({ type: ADMIN_USERS_FETCH_SUCCESS, users: accounts, count, pageSize, filters, page, next: next?.uri || false }); + return { users: accounts, count, pageSize, next: next?.uri || false }; + }).catch(error => { + dispatch({ type: ADMIN_USERS_FETCH_FAIL, error, filters, page, pageSize }); + }); + }; +} + +function fetchPleromaUsers(filters, page, query, pageSize) { + return (dispatch, getState) => { const params = { filters: filters.join(), page, page_size: pageSize }; if (query) params.query = query; @@ -229,6 +249,53 @@ export function fetchUsers(filters = [], page = 1, query, pageSize = 50, next) { }; } +export function fetchUsers(filters = [], page = 1, query, pageSize = 50, next) { + return (dispatch, getState) => { + const state = getState(); + + const instance = state.get('instance'); + const features = getFeatures(instance); + + dispatch({ type: ADMIN_USERS_FETCH_REQUEST, filters, page, pageSize }); + + if (features.mastodonAdminApi) { + return dispatch(fetchMastodonUsers(filters, page, query, pageSize, next)); + } else { + return dispatch(fetchPleromaUsers(filters, page, query, pageSize)); + } + }; +} + +function deactivateMastodonUsers(accountIds, reportId) { + return (dispatch, getState) => { + return Promise.all(accountIds.map(accountId => { + api(getState) + .post(`/api/v1/admin/accounts/${accountId}/action`, { + type: 'disable', + report_id: reportId, + }) + .then(() => { + dispatch({ type: ADMIN_USERS_DEACTIVATE_SUCCESS, accountIds: [accountId] }); + }).catch(error => { + dispatch({ type: ADMIN_USERS_DEACTIVATE_FAIL, error, accountIds: [accountId] }); + }); + })); + }; +} + +function deactivatePleromaUsers(accountIds) { + return (dispatch, getState) => { + const nicknames = nicknamesFromIds(getState, accountIds); + return api(getState) + .patch('/api/pleroma/admin/users/deactivate', { nicknames }) + .then(({ data: { users } }) => { + dispatch({ type: ADMIN_USERS_DEACTIVATE_SUCCESS, users, accountIds }); + }).catch(error => { + dispatch({ type: ADMIN_USERS_DEACTIVATE_FAIL, error, accountIds }); + }); + }; +} + export function deactivateUsers(accountIds, reportId) { return (dispatch, getState) => { const state = getState(); @@ -239,28 +306,10 @@ export function deactivateUsers(accountIds, reportId) { dispatch({ type: ADMIN_USERS_DEACTIVATE_REQUEST, accountIds }); if (features.mastodonAdminApi) { - return Promise.all(accountIds.map(accountId => { - api(getState) - .post(`/api/v1/admin/accounts/${accountId}/action`, { - type: 'disable', - report_id: reportId, - }) - .then(() => { - dispatch({ type: ADMIN_USERS_DEACTIVATE_SUCCESS, accountIds: [accountId] }); - }).catch(error => { - dispatch({ type: ADMIN_USERS_DEACTIVATE_FAIL, error, accountIds: [accountId] }); - }); - })); + return dispatch(deactivateMastodonUsers(accountIds, reportId)); + } else { + return dispatch(deactivatePleromaUsers(accountIds)); } - - const nicknames = nicknamesFromIds(getState, accountIds); - return api(getState) - .patch('/api/pleroma/admin/users/deactivate', { nicknames }) - .then(({ data: { users } }) => { - dispatch({ type: ADMIN_USERS_DEACTIVATE_SUCCESS, users, accountIds }); - }).catch(error => { - dispatch({ type: ADMIN_USERS_DEACTIVATE_FAIL, error, accountIds }); - }); }; } @@ -278,6 +327,33 @@ export function deleteUsers(accountIds) { }; } +function approveMastodonUsers(accountIds) { + return (dispatch, getState) => { + return Promise.all(accountIds.map(accountId => { + api(getState) + .post(`/api/v1/admin/accounts/${accountId}/approve`) + .then(({ data: user }) => { + dispatch({ type: ADMIN_USERS_APPROVE_SUCCESS, users: [user], accountIds: [accountId] }); + }).catch(error => { + dispatch({ type: ADMIN_USERS_APPROVE_FAIL, error, accountIds: [accountId] }); + }); + })); + }; +} + +function approvePleromaUsers(accountIds) { + return (dispatch, getState) => { + const nicknames = nicknamesFromIds(getState, accountIds); + return api(getState) + .patch('/api/pleroma/admin/users/approve', { nicknames }) + .then(({ data: { users } }) => { + dispatch({ type: ADMIN_USERS_APPROVE_SUCCESS, users, accountIds }); + }).catch(error => { + dispatch({ type: ADMIN_USERS_APPROVE_FAIL, error, accountIds }); + }); + }; +} + export function approveUsers(accountIds) { return (dispatch, getState) => { const state = getState(); @@ -288,25 +364,10 @@ export function approveUsers(accountIds) { dispatch({ type: ADMIN_USERS_APPROVE_REQUEST, accountIds }); if (features.mastodonAdminApi) { - return Promise.all(accountIds.map(accountId => { - api(getState) - .post(`/api/v1/admin/accounts/${accountId}/approve`) - .then(({ data: user }) => { - dispatch({ type: ADMIN_USERS_APPROVE_SUCCESS, users: [user], accountIds: [accountId] }); - }).catch(error => { - dispatch({ type: ADMIN_USERS_APPROVE_FAIL, error, accountIds: [accountId] }); - }); - })); + return dispatch(approveMastodonUsers(accountIds)); + } else { + return dispatch(approvePleromaUsers(accountIds)); } - - const nicknames = nicknamesFromIds(getState, accountIds); - return api(getState) - .patch('/api/pleroma/admin/users/approve', { nicknames }) - .then(({ data: { users } }) => { - dispatch({ type: ADMIN_USERS_APPROVE_SUCCESS, users, accountIds }); - }).catch(error => { - dispatch({ type: ADMIN_USERS_APPROVE_FAIL, error, accountIds }); - }); }; } From 76cf741bf837cc7064a04aecf83594379acdc35d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Sat, 21 May 2022 08:42:39 +0200 Subject: [PATCH 024/102] DeleteAccount: Do not show explanation related to federation if not federating MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- app/soapbox/features/delete_account/index.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/soapbox/features/delete_account/index.tsx b/app/soapbox/features/delete_account/index.tsx index d5927f763..af4282aef 100644 --- a/app/soapbox/features/delete_account/index.tsx +++ b/app/soapbox/features/delete_account/index.tsx @@ -4,12 +4,13 @@ import { defineMessages, useIntl } from 'react-intl'; import { deleteAccount } from 'soapbox/actions/security'; import snackbar from 'soapbox/actions/snackbar'; import { Button, Card, CardBody, CardHeader, CardTitle, Form, FormActions, FormGroup, Input } from 'soapbox/components/ui'; -import { useAppDispatch } from 'soapbox/hooks'; +import { useAppDispatch, useFeatures } from 'soapbox/hooks'; const messages = defineMessages({ passwordFieldLabel: { id: 'security.fields.password.label', defaultMessage: 'Password' }, deleteHeader: { id: 'security.headers.delete', defaultMessage: 'Delete Account' }, deleteText: { id: 'security.text.delete', defaultMessage: 'To delete your account, enter your password then click Delete Account. This is a permanent action that cannot be undone. Your account will be destroyed from this server, and a deletion request will be sent to other servers. It\'s not guaranteed that all servers will purge your account.' }, + localDeleteText: { id: 'security.text.delete.local', defaultMessage: 'To delete your account, enter your password then click Delete Account. This is a permanent action that cannot be undone.' }, deleteSubmit: { id: 'security.submit.delete', defaultMessage: 'Delete Account' }, deleteAccountSuccess: { id: 'security.delete_account.success', defaultMessage: 'Account successfully deleted.' }, deleteAccountFail: { id: 'security.delete_account.fail', defaultMessage: 'Account deletion failed.' }, @@ -18,6 +19,7 @@ const messages = defineMessages({ const DeleteAccount = () => { const intl = useIntl(); const dispatch = useAppDispatch(); + const features = useFeatures(); const [password, setPassword] = React.useState(''); const [isLoading, setLoading] = React.useState(false); @@ -49,7 +51,7 @@ const DeleteAccount = () => {

- {intl.formatMessage(messages.deleteText)} + {intl.formatMessage(features.federating ? messages.deleteText : messages.localDeleteText)}

From b2d59f751b4b707982fb049ea96914d82180d9cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Sat, 21 May 2022 14:37:02 +0200 Subject: [PATCH 025/102] types MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- .../features/admin/components/latest_accounts_panel.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/soapbox/features/admin/components/latest_accounts_panel.tsx b/app/soapbox/features/admin/components/latest_accounts_panel.tsx index 62636c366..7998c18d2 100644 --- a/app/soapbox/features/admin/components/latest_accounts_panel.tsx +++ b/app/soapbox/features/admin/components/latest_accounts_panel.tsx @@ -32,8 +32,8 @@ const LatestAccountsPanel: React.FC = ({ limit = 5 }) => { useEffect(() => { dispatch(fetchUsers(['local', 'active'], 1, null, limit)) - .then((value) => { - setTotal((value as { count: number }).count); + .then((value: { count: number }) => { + setTotal(value.count); }) .catch(() => {}); }, []); From 4992862943e18cef9f15c4641e6058ba427a4487 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sun, 22 May 2022 12:53:13 -0500 Subject: [PATCH 026/102] fetchMe: don't await verify_credentials if cached --- app/soapbox/actions/auth.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/app/soapbox/actions/auth.js b/app/soapbox/actions/auth.js index 79ff9d67f..a42bccdef 100644 --- a/app/soapbox/actions/auth.js +++ b/app/soapbox/actions/auth.js @@ -207,9 +207,12 @@ export function rememberAuthAccount(accountUrl) { export function loadCredentials(token, accountUrl) { return (dispatch, getState) => { - return dispatch(rememberAuthAccount(accountUrl)).finally(() => { - return dispatch(verifyCredentials(token, accountUrl)); - }); + return dispatch(rememberAuthAccount(accountUrl)) + .then(account => account) + .then(() => { + dispatch(verifyCredentials(token, accountUrl)); + }) + .catch(error => dispatch(verifyCredentials(token, accountUrl))); }; } From 2b7f4694da3309de8a59d43cff3c1ec7c414e3c5 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sun, 22 May 2022 13:27:08 -0500 Subject: [PATCH 027/102] LoadingScreen: bump up spinner vertically to counteract optical illusion --- app/soapbox/components/loading-screen.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/soapbox/components/loading-screen.tsx b/app/soapbox/components/loading-screen.tsx index 92882f98f..ffa1fa8f9 100644 --- a/app/soapbox/components/loading-screen.tsx +++ b/app/soapbox/components/loading-screen.tsx @@ -10,7 +10,10 @@ const LoadingScreen: React.FC = () => {
- + {/* Bump up spinner vertically to counteract optical illusion. */} +
+ +
); From 73c4457f281ff49daff22665a3e8f6d9a0fd12af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Mon, 23 May 2022 21:14:42 +0200 Subject: [PATCH 028/102] Scheduled statuses: TypeScript, fix styles MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- app/soapbox/components/account.tsx | 6 +- app/soapbox/components/ui/hstack/hstack.tsx | 4 +- .../{builder.js => builder.tsx} | 6 +- .../components/scheduled_status.js | 91 ------------------- .../components/scheduled_status.tsx | 66 ++++++++++++++ .../components/scheduled_status_action_bar.js | 83 ----------------- .../scheduled_status_action_bar.tsx | 59 ++++++++++++ .../features/scheduled_statuses/index.js | 66 -------------- .../features/scheduled_statuses/index.tsx | 51 +++++++++++ 9 files changed, 187 insertions(+), 245 deletions(-) rename app/soapbox/features/scheduled_statuses/{builder.js => builder.tsx} (85%) delete mode 100644 app/soapbox/features/scheduled_statuses/components/scheduled_status.js create mode 100644 app/soapbox/features/scheduled_statuses/components/scheduled_status.tsx delete mode 100644 app/soapbox/features/scheduled_statuses/components/scheduled_status_action_bar.js create mode 100644 app/soapbox/features/scheduled_statuses/components/scheduled_status_action_bar.tsx delete mode 100644 app/soapbox/features/scheduled_statuses/index.js create mode 100644 app/soapbox/features/scheduled_statuses/index.tsx diff --git a/app/soapbox/components/account.tsx b/app/soapbox/components/account.tsx index 1d670a344..cc91ded59 100644 --- a/app/soapbox/components/account.tsx +++ b/app/soapbox/components/account.tsx @@ -56,6 +56,7 @@ interface IAccount { showProfileHoverCard?: boolean, timestamp?: string | Date, timestampUrl?: string, + futureTimestamp?: boolean, withDate?: boolean, withRelationship?: boolean, showEdit?: boolean, @@ -75,6 +76,7 @@ const Account = ({ showProfileHoverCard = true, timestamp, timestampUrl, + futureTimestamp = false, withDate = false, withRelationship = true, showEdit = false, @@ -205,10 +207,10 @@ const Account = ({ {timestampUrl ? ( - + ) : ( - + )} ) : null} diff --git a/app/soapbox/components/ui/hstack/hstack.tsx b/app/soapbox/components/ui/hstack/hstack.tsx index 44dac93c7..803bbd7c1 100644 --- a/app/soapbox/components/ui/hstack/hstack.tsx +++ b/app/soapbox/components/ui/hstack/hstack.tsx @@ -4,6 +4,8 @@ import React from 'react'; const justifyContentOptions = { between: 'justify-between', center: 'justify-center', + start: 'justify-start', + end: 'justify-end', }; const alignItemsOptions = { @@ -29,7 +31,7 @@ interface IHStack { /** Extra class names on the
element. */ className?: string, /** Horizontal alignment of children. */ - justifyContent?: 'between' | 'center', + justifyContent?: 'between' | 'center' | 'start' | 'end', /** Size of the gap between elements. */ space?: 0.5 | 1 | 1.5 | 2 | 3 | 4 | 6, /** Whether to let the flexbox grow. */ diff --git a/app/soapbox/features/scheduled_statuses/builder.js b/app/soapbox/features/scheduled_statuses/builder.tsx similarity index 85% rename from app/soapbox/features/scheduled_statuses/builder.js rename to app/soapbox/features/scheduled_statuses/builder.tsx index 8e3417582..2927c8984 100644 --- a/app/soapbox/features/scheduled_statuses/builder.js +++ b/app/soapbox/features/scheduled_statuses/builder.tsx @@ -3,11 +3,13 @@ import { Map as ImmutableMap } from 'immutable'; import { normalizeStatus } from 'soapbox/normalizers/status'; import { calculateStatus } from 'soapbox/reducers/statuses'; import { makeGetAccount } from 'soapbox/selectors'; +import { RootState } from 'soapbox/store'; -export const buildStatus = (state, scheduledStatus) => { +export const buildStatus = (state: RootState, scheduledStatus: ImmutableMap) => { const getAccount = makeGetAccount(); - const me = state.get('me'); + const me = state.me as string; + const params = scheduledStatus.get('params'); const account = getAccount(state, me); diff --git a/app/soapbox/features/scheduled_statuses/components/scheduled_status.js b/app/soapbox/features/scheduled_statuses/components/scheduled_status.js deleted file mode 100644 index b88ccc450..000000000 --- a/app/soapbox/features/scheduled_statuses/components/scheduled_status.js +++ /dev/null @@ -1,91 +0,0 @@ -import classNames from 'classnames'; -import React from 'react'; -import ImmutablePureComponent from 'react-immutable-pure-component'; -import { connect } from 'react-redux'; -import { Link, NavLink } from 'react-router-dom'; - -import AttachmentThumbs from 'soapbox/components/attachment-thumbs'; -import Avatar from 'soapbox/components/avatar'; -import DisplayName from 'soapbox/components/display-name'; -import RelativeTimestamp from 'soapbox/components/relative_timestamp'; -import StatusContent from 'soapbox/components/status_content'; -import StatusReplyMentions from 'soapbox/components/status_reply_mentions'; -import PollPreview from 'soapbox/features/ui/components/poll_preview'; -import { getDomain } from 'soapbox/utils/accounts'; - -import { buildStatus } from '../builder'; - -import ScheduledStatusActionBar from './scheduled_status_action_bar'; - -const mapStateToProps = (state, props) => { - const scheduledStatus = state.getIn(['scheduled_statuses', props.statusId]); - return { - status: buildStatus(state, scheduledStatus), - }; -}; - -export default @connect(mapStateToProps) -class ScheduledStatus extends ImmutablePureComponent { - - render() { - const { status, account, ...other } = this.props; - if (!status.get('account')) return null; - - const statusUrl = `/scheduled_statuses/${status.get('id')}`; - const favicon = status.getIn(['account', 'pleroma', 'favicon']); - const domain = getDomain(status.get('account')); - - return ( -
-
-
-
-
- - - - - {favicon && -
- - - -
} - -
-
- - - -
- - - -
-
- - - - - - {status.get('media_attachments').size > 0 && ( - - )} - - {status.get('poll') && } - - -
-
-
- ); - } - -} diff --git a/app/soapbox/features/scheduled_statuses/components/scheduled_status.tsx b/app/soapbox/features/scheduled_statuses/components/scheduled_status.tsx new file mode 100644 index 000000000..0d8b14cd3 --- /dev/null +++ b/app/soapbox/features/scheduled_statuses/components/scheduled_status.tsx @@ -0,0 +1,66 @@ +import classNames from 'classnames'; +import React from 'react'; + +import AttachmentThumbs from 'soapbox/components/attachment-thumbs'; +import StatusContent from 'soapbox/components/status_content'; +import StatusReplyMentions from 'soapbox/components/status_reply_mentions'; +import { HStack } from 'soapbox/components/ui'; +import AccountContainer from 'soapbox/containers/account_container'; +import PollPreview from 'soapbox/features/ui/components/poll_preview'; +import { useAppSelector } from 'soapbox/hooks'; + +import { buildStatus } from '../builder'; + +import ScheduledStatusActionBar from './scheduled_status_action_bar'; + +import type { Account as AccountEntity, Status as StatusEntity } from 'soapbox/types/entities'; + +interface IScheduledStatus { + statusId: string, +} + +const ScheduledStatus: React.FC = ({ statusId, ...other }) => { + const status = useAppSelector((state) => buildStatus(state, state.scheduled_statuses.get(statusId))) as StatusEntity; + + if (!status) return null; + + const account = status.account as AccountEntity; + + return ( +
+
+
+ + + +
+ + + + + + {status.media_attachments.size > 0 && ( + + )} + + {status.poll && } + + +
+
+ ); +}; + +export default ScheduledStatus; diff --git a/app/soapbox/features/scheduled_statuses/components/scheduled_status_action_bar.js b/app/soapbox/features/scheduled_statuses/components/scheduled_status_action_bar.js deleted file mode 100644 index 7ed5c4d08..000000000 --- a/app/soapbox/features/scheduled_statuses/components/scheduled_status_action_bar.js +++ /dev/null @@ -1,83 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import ImmutablePureComponent from 'react-immutable-pure-component'; -import { defineMessages, injectIntl } from 'react-intl'; -import { connect } from 'react-redux'; - -import { openModal } from 'soapbox/actions/modals'; -import { cancelScheduledStatus } from 'soapbox/actions/scheduled_statuses'; -import { getSettings } from 'soapbox/actions/settings'; -import IconButton from 'soapbox/components/icon_button'; -import SoapboxPropTypes from 'soapbox/utils/soapbox_prop_types'; - -const messages = defineMessages({ - cancel: { id: 'scheduled_status.cancel', defaultMessage: 'Cancel' }, - deleteConfirm: { id: 'confirmations.scheduled_status_delete.confirm', defaultMessage: 'Cancel' }, - deleteHeading: { id: 'confirmations.scheduled_status_delete.heading', defaultMessage: 'Cancel scheduled post' }, - deleteMessage: { id: 'confirmations.scheduled_status_delete.message', defaultMessage: 'Are you sure you want to cancel this scheduled post?' }, -}); - -const mapStateToProps = state => { - const me = state.get('me'); - return { - me, - }; -}; - -const mapDispatchToProps = (dispatch, { intl }) => ({ - onCancelClick: (status) => { - dispatch((_, getState) => { - - const deleteModal = getSettings(getState()).get('deleteModal'); - if (!deleteModal) { - dispatch(cancelScheduledStatus(status.get('id'))); - } else { - dispatch(openModal('CONFIRM', { - icon: require('@tabler/icons/icons/calendar-stats.svg'), - heading: intl.formatMessage(messages.deleteHeading), - message: intl.formatMessage(messages.deleteMessage), - confirm: intl.formatMessage(messages.deleteConfirm), - onConfirm: () => dispatch(cancelScheduledStatus(status.get('id'))), - })); - } - }); - }, -}); - -class ScheduledStatusActionBar extends ImmutablePureComponent { - - static propTypes = { - status: ImmutablePropTypes.record.isRequired, - intl: PropTypes.object.isRequired, - me: SoapboxPropTypes.me, - onCancelClick: PropTypes.func.isRequired, - }; - - handleCancelClick = e => { - const { status, onCancelClick } = this.props; - - onCancelClick(status); - } - - render() { - const { intl } = this.props; - - return ( -
-
- -
-
- ); - } - -} - - -export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(ScheduledStatusActionBar)); diff --git a/app/soapbox/features/scheduled_statuses/components/scheduled_status_action_bar.tsx b/app/soapbox/features/scheduled_statuses/components/scheduled_status_action_bar.tsx new file mode 100644 index 000000000..7b44805d4 --- /dev/null +++ b/app/soapbox/features/scheduled_statuses/components/scheduled_status_action_bar.tsx @@ -0,0 +1,59 @@ +import React from 'react'; +import { defineMessages, useIntl } from 'react-intl'; + +import { openModal } from 'soapbox/actions/modals'; +import { cancelScheduledStatus } from 'soapbox/actions/scheduled_statuses'; +import { getSettings } from 'soapbox/actions/settings'; +import IconButton from 'soapbox/components/icon_button'; +import { HStack } from 'soapbox/components/ui'; +import { useAppDispatch } from 'soapbox/hooks'; + +import type { Status as StatusEntity } from 'soapbox/types/entities'; + +const messages = defineMessages({ + cancel: { id: 'scheduled_status.cancel', defaultMessage: 'Cancel' }, + deleteConfirm: { id: 'confirmations.scheduled_status_delete.confirm', defaultMessage: 'Cancel' }, + deleteHeading: { id: 'confirmations.scheduled_status_delete.heading', defaultMessage: 'Cancel scheduled post' }, + deleteMessage: { id: 'confirmations.scheduled_status_delete.message', defaultMessage: 'Are you sure you want to cancel this scheduled post?' }, +}); + +interface IScheduledStatusActionBar { + status: StatusEntity, +} + +const ScheduledStatusActionBar: React.FC = ({ status }) => { + const intl = useIntl(); + + const dispatch = useAppDispatch(); + + const handleCancelClick = () => { + dispatch((_, getState) => { + + const deleteModal = getSettings(getState()).get('deleteModal'); + if (!deleteModal) { + dispatch(cancelScheduledStatus(status.id)); + } else { + dispatch(openModal('CONFIRM', { + icon: require('@tabler/icons/icons/calendar-stats.svg'), + heading: intl.formatMessage(messages.deleteHeading), + message: intl.formatMessage(messages.deleteMessage), + confirm: intl.formatMessage(messages.deleteConfirm), + onConfirm: () => dispatch(cancelScheduledStatus(status.id)), + })); + } + }); + }; + + return ( + + + + ); +}; + +export default ScheduledStatusActionBar; diff --git a/app/soapbox/features/scheduled_statuses/index.js b/app/soapbox/features/scheduled_statuses/index.js deleted file mode 100644 index b8a63497e..000000000 --- a/app/soapbox/features/scheduled_statuses/index.js +++ /dev/null @@ -1,66 +0,0 @@ -import { debounce } from 'lodash'; -import PropTypes from 'prop-types'; -import React from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import ImmutablePureComponent from 'react-immutable-pure-component'; -import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; -import { connect } from 'react-redux'; - -import ScrollableList from 'soapbox/components/scrollable_list'; - -import { fetchScheduledStatuses, expandScheduledStatuses } from '../../actions/scheduled_statuses'; -import Column from '../ui/components/column'; - -import ScheduledStatus from './components/scheduled_status'; - -const messages = defineMessages({ - heading: { id: 'column.scheduled_statuses', defaultMessage: 'Scheduled Posts' }, -}); - -const mapStateToProps = state => ({ - statusIds: state.getIn(['status_lists', 'scheduled_statuses', 'items']), - isLoading: state.getIn(['status_lists', 'scheduled_statuses', 'isLoading'], true), - hasMore: !!state.getIn(['status_lists', 'scheduled_statuses', 'next']), -}); - -export default @connect(mapStateToProps) -@injectIntl -class ScheduledStatuses extends ImmutablePureComponent { - - static propTypes = { - dispatch: PropTypes.func.isRequired, - statusIds: ImmutablePropTypes.orderedSet.isRequired, - intl: PropTypes.object.isRequired, - hasMore: PropTypes.bool, - isLoading: PropTypes.bool, - }; - - componentDidMount() { - const { dispatch } = this.props; - dispatch(fetchScheduledStatuses()); - } - - handleLoadMore = debounce(() => { - this.props.dispatch(expandScheduledStatuses()); - }, 300, { leading: true }) - - - render() { - const { intl, statusIds, hasMore, isLoading } = this.props; - const emptyMessage = ; - - return ( - - - {statusIds.map(id => )} - - - ); - } - -} diff --git a/app/soapbox/features/scheduled_statuses/index.tsx b/app/soapbox/features/scheduled_statuses/index.tsx new file mode 100644 index 000000000..7d387022d --- /dev/null +++ b/app/soapbox/features/scheduled_statuses/index.tsx @@ -0,0 +1,51 @@ +import { debounce } from 'lodash'; +import React from 'react'; +import { useEffect } from 'react'; +import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; + +import { fetchScheduledStatuses, expandScheduledStatuses } from 'soapbox/actions/scheduled_statuses'; +import ScrollableList from 'soapbox/components/scrollable_list'; +import { useAppSelector, useAppDispatch } from 'soapbox/hooks'; + +import Column from '../ui/components/column'; + +import ScheduledStatus from './components/scheduled_status'; + +const messages = defineMessages({ + heading: { id: 'column.scheduled_statuses', defaultMessage: 'Scheduled Posts' }, +}); + +const handleLoadMore = debounce((dispatch) => { + dispatch(expandScheduledStatuses()); +}, 300, { leading: true }); + +const ScheduledStatuses = () => { + const intl = useIntl(); + const dispatch = useAppDispatch(); + + const statusIds = useAppSelector((state) => state.status_lists.getIn(['scheduled_statuses', 'items'])); + const isLoading = useAppSelector((state) => state.status_lists.getIn(['scheduled_statuses', 'isLoading'])); + const hasMore = useAppSelector((state) => !!state.status_lists.getIn(['scheduled_statuses', 'next'])); + + useEffect(() => { + dispatch(fetchScheduledStatuses()); + }, []); + + const emptyMessage = ; + + return ( + + handleLoadMore(dispatch)} + emptyMessage={emptyMessage} + > + {statusIds.map((id: string) => )} + + + ); +}; + +export default ScheduledStatuses; \ No newline at end of file From 58ddda82465ff350bf0a6285d2cffa3f1418b00e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Sun, 22 May 2022 21:26:53 +0200 Subject: [PATCH 029/102] Remove Invites link from sidebar 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-navigation.tsx | 11 ----------- app/soapbox/components/sidebar_menu.tsx | 12 ------------ 2 files changed, 23 deletions(-) diff --git a/app/soapbox/components/sidebar-navigation.tsx b/app/soapbox/components/sidebar-navigation.tsx index 39eea4ec4..b123b9c55 100644 --- a/app/soapbox/components/sidebar-navigation.tsx +++ b/app/soapbox/components/sidebar-navigation.tsx @@ -6,7 +6,6 @@ import { getSettings } from 'soapbox/actions/settings'; import DropdownMenu from 'soapbox/containers/dropdown_menu_container'; import ComposeButton from 'soapbox/features/ui/components/compose-button'; import { useAppSelector, useOwnAccount } from 'soapbox/hooks'; -import { getBaseURL } from 'soapbox/utils/accounts'; import { getFeatures } from 'soapbox/utils/features'; import SidebarNavigationLink from './sidebar-navigation-link'; @@ -23,7 +22,6 @@ const SidebarNavigation = () => { const followRequestsCount = useAppSelector((state) => state.user_lists.getIn(['follow_requests', 'items'], ImmutableOrderedSet()).count()); const dashboardCount = useAppSelector((state) => state.admin.openReports.count() + state.admin.awaitingApproval.count()); - const baseURL = account ? getBaseURL(account) : ''; const features = getFeatures(instance); const makeMenu = (): Menu => { @@ -55,15 +53,6 @@ const SidebarNavigation = () => { }); } - if (instance.invites_enabled) { - menu.push({ - href: `${baseURL}/invites`, - icon: require('@tabler/icons/icons/mailbox.svg'), - text: , - newTab: true, - }); - } - if (settings.get('isDeveloper')) { menu.push({ to: '/developers', diff --git a/app/soapbox/components/sidebar_menu.tsx b/app/soapbox/components/sidebar_menu.tsx index 15661c017..bf5fc26f4 100644 --- a/app/soapbox/components/sidebar_menu.tsx +++ b/app/soapbox/components/sidebar_menu.tsx @@ -13,7 +13,6 @@ import { Stack } from 'soapbox/components/ui'; import ProfileStats from 'soapbox/features/ui/components/profile_stats'; import { useAppSelector, useSoapboxConfig, useFeatures } from 'soapbox/hooks'; import { makeGetAccount, makeGetOtherAccounts } from 'soapbox/selectors'; -import { getBaseURL } from 'soapbox/utils/accounts'; import { HStack, Icon, IconButton, Text } from './ui'; @@ -91,8 +90,6 @@ const SidebarMenu: React.FC = (): JSX.Element | null => { const sidebarOpen = useAppSelector((state) => state.sidebar.sidebarOpen); const settings = useAppSelector((state) => getSettings(state)); - const baseURL = account ? getBaseURL(account) : ''; - const closeButtonRef = React.useRef(null); const [switcher, setSwitcher] = React.useState(false); @@ -241,15 +238,6 @@ const SidebarMenu: React.FC = (): JSX.Element | null => { /> )} - {instance.invites_enabled && ( - - )} - {settings.get('isDeveloper') && ( Date: Tue, 24 May 2022 11:46:51 +0200 Subject: [PATCH 030/102] Restore default post privacy/format settings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- app/soapbox/features/preferences/index.tsx | 50 ++++++++++++++++------ 1 file changed, 37 insertions(+), 13 deletions(-) diff --git a/app/soapbox/features/preferences/index.tsx b/app/soapbox/features/preferences/index.tsx index ccbd23498..593978dbc 100644 --- a/app/soapbox/features/preferences/index.tsx +++ b/app/soapbox/features/preferences/index.tsx @@ -8,6 +8,7 @@ import { Form } from 'soapbox/components/ui'; import { SelectDropdown } from 'soapbox/features/forms'; import SettingToggle from 'soapbox/features/notifications/components/setting_toggle'; import { useAppSelector } from 'soapbox/hooks'; +import { getFeatures } from 'soapbox/utils/features'; import ThemeToggle from '../ui/components/theme-toggle'; @@ -80,27 +81,24 @@ const messages = defineMessages({ display_media_default: { id: 'preferences.fields.display_media.default', defaultMessage: 'Hide media marked as sensitive' }, display_media_hide_all: { id: 'preferences.fields.display_media.hide_all', defaultMessage: 'Always hide media' }, display_media_show_all: { id: 'preferences.fields.display_media.show_all', defaultMessage: 'Always show media' }, + privacy_public: { id: 'preferences.options.privacy_public', defaultMessage: 'Public' }, + privacy_unlisted: { id: 'preferences.options.privacy_unlisted', defaultMessage: 'Unlisted' }, + privacy_followers_only: { id: 'preferences.options.privacy_followers_only', defaultMessage: 'Followers-only' }, + content_type_plaintext: { id: 'preferences.options.content_type_plaintext', defaultMessage: 'Plain text' }, + content_type_markdown: { id: 'preferences.options.content_type_markdown', defaultMessage: 'Markdown' }, }); const Preferences = () => { const intl = useIntl(); const dispatch = useDispatch(); - // const features = useAppSelector((state) => getFeatures(state.get('instance'))); + const features = useAppSelector((state) => getFeatures(state.instance)); const settings = useAppSelector((state) => getSettings(state)); const onSelectChange = (event: React.ChangeEvent, path: string[]) => { dispatch(changeSetting(path, event.target.value, intl)); }; - // const onDefaultPrivacyChange = (e) => { - // dispatch(changeSetting(['defaultPrivacy'], e.target.value)); - // } - - // const onDefaultContentTypeChange = (event: React.ChangeEvent) => { - // dispatch(changeSetting(['defaultContentType'], event.target.value)); - // }; - const onToggleChange = (key: string[], checked: boolean) => { dispatch(changeSetting(key, checked, intl)); }; @@ -111,6 +109,17 @@ const Preferences = () => { show_all: intl.formatMessage(messages.display_media_show_all), }), []); + const defaultPrivacyOptions = React.useMemo(() => ({ + public: intl.formatMessage(messages.privacy_public), + unlisted: intl.formatMessage(messages.privacy_unlisted), + private: intl.formatMessage(messages.privacy_followers_only), + }), []); + + const defaultContentTypeOptions = React.useMemo(() => ({ + 'text/plain': intl.formatMessage(messages.content_type_plaintext), + 'text/markdown': intl.formatMessage(messages.content_type_markdown), + }), []); + return ( @@ -149,6 +158,22 @@ const Preferences = () => { onChange={(event: React.ChangeEvent) => onSelectChange(event, ['displayMedia'])} /> + + {features.privacyScopes && }> + ) => onSelectChange(event, ['defaultPrivacy'])} + /> + } + + {features.richText && }> + ) => onSelectChange(event, ['defaultContentType'])} + /> + } {/* @@ -211,10 +236,9 @@ const Preferences = () => { - {/* } - path={['missingDescriptionModal']} - /> */} + {/* }> + + */} From 682d2a36474037303faa559c82b4ee7ec48b7eac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Tue, 24 May 2022 12:24:26 +0200 Subject: [PATCH 031/102] Avoid inline message definitions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- app/soapbox/actions/compose.js | 2 +- app/soapbox/components/badge.tsx | 2 +- app/soapbox/components/media_gallery.js | 4 +-- app/soapbox/components/profile-hover-card.tsx | 5 ++- app/soapbox/components/status.tsx | 16 +++++---- app/soapbox/components/status_list.js | 10 +++--- .../features/account/components/header.js | 12 +++---- .../admin/components/unapproved_account.tsx | 3 +- .../auth_login/components/login_form.tsx | 2 +- .../auth_login/components/otp_auth_form.js | 3 +- .../auth_login/components/password_reset.js | 2 +- ..._confirm.js => password_reset_confirm.tsx} | 28 +++++++-------- .../compose/components/compose_form.js | 6 ++-- .../features/compose/components/search.tsx | 9 +++-- app/soapbox/features/directory/index.js | 2 +- .../edit_email/{index.js => index.tsx} | 18 +++++----- .../{index.js => index.tsx} | 36 +++++++++---------- .../features/hashtag_timeline/index.js | 4 +-- app/soapbox/features/settings/index.tsx | 6 ++-- .../features/verification/registration.tsx | 35 ++++++++++-------- .../verification/steps/age-verification.js | 24 ++++++------- 21 files changed, 115 insertions(+), 114 deletions(-) rename app/soapbox/features/auth_login/components/{password_reset_confirm.js => password_reset_confirm.tsx} (74%) rename app/soapbox/features/edit_email/{index.js => index.tsx} (83%) rename app/soapbox/features/email_confirmation/{index.js => index.tsx} (60%) diff --git a/app/soapbox/actions/compose.js b/app/soapbox/actions/compose.js index 1d16fe554..efef9c786 100644 --- a/app/soapbox/actions/compose.js +++ b/app/soapbox/actions/compose.js @@ -83,7 +83,7 @@ export const COMPOSE_SET_STATUS = 'COMPOSE_SET_STATUS'; const messages = defineMessages({ exceededImageSizeLimit: { id: 'upload_error.image_size_limit', defaultMessage: 'Image exceeds the current file size limit ({limit})' }, exceededVideoSizeLimit: { id: 'upload_error.video_size_limit', defaultMessage: 'Video exceeds the current file size limit ({limit})' }, - scheduleError: { id: 'compose.invalid_schedule', defaultMessage: 'You must schedule a post at least 5 minutes out.' }, + scheduleError: { id: 'compose.invalid_schedule', defaultMessage: 'You must schedule a post at least 5 minutes out.' }, success: { id: 'compose.submit_success', defaultMessage: 'Your post was sent' }, uploadErrorLimit: { id: 'upload_error.limit', defaultMessage: 'File upload limit exceeded.' }, uploadErrorPoll: { id: 'upload_error.poll', defaultMessage: 'File upload not allowed with polls.' }, diff --git a/app/soapbox/components/badge.tsx b/app/soapbox/components/badge.tsx index 2eb2d221d..c14a58d56 100644 --- a/app/soapbox/components/badge.tsx +++ b/app/soapbox/components/badge.tsx @@ -2,7 +2,7 @@ import classNames from 'classnames'; import React from 'react'; interface IBadge { - title: string, + title: string | React.ReactNode, slug: 'patron' | 'donor' | 'admin' | 'moderator' | 'bot' | 'opaque', } diff --git a/app/soapbox/components/media_gallery.js b/app/soapbox/components/media_gallery.js index 30ddbbc34..44b08a3df 100644 --- a/app/soapbox/components/media_gallery.js +++ b/app/soapbox/components/media_gallery.js @@ -614,12 +614,12 @@ class MediaGallery extends React.PureComponent {
{warning} - {intl.formatMessage({ id: 'status.sensitive_warning.subtitle', defaultMessage: 'This content may not be suitable for all audiences.' })} +
diff --git a/app/soapbox/components/profile-hover-card.tsx b/app/soapbox/components/profile-hover-card.tsx index 8511839fb..9c9f2e79a 100644 --- a/app/soapbox/components/profile-hover-card.tsx +++ b/app/soapbox/components/profile-hover-card.tsx @@ -1,6 +1,6 @@ import classNames from 'classnames'; import React, { useEffect, useState } from 'react'; -import { useIntl } from 'react-intl'; +import { FormattedMessage } from 'react-intl'; import { usePopper } from 'react-popper'; import { useHistory } from 'react-router-dom'; @@ -64,7 +64,6 @@ interface IProfileHoverCard { export const ProfileHoverCard: React.FC = ({ visible = true }) => { const dispatch = useAppDispatch(); const history = useHistory(); - const intl = useIntl(); const [popperElement, setPopperElement] = useState(null); @@ -130,7 +129,7 @@ export const ProfileHoverCard: React.FC = ({ visible = true }
} />
)} diff --git a/app/soapbox/components/status.tsx b/app/soapbox/components/status.tsx index 9bcc772ee..141394420 100644 --- a/app/soapbox/components/status.tsx +++ b/app/soapbox/components/status.tsx @@ -2,7 +2,7 @@ import classNames from 'classnames'; import React from 'react'; import { HotKeys } from 'react-hotkeys'; import ImmutablePureComponent from 'react-immutable-pure-component'; -import { injectIntl, FormattedMessage, IntlShape } from 'react-intl'; +import { injectIntl, FormattedMessage, IntlShape, defineMessages } from 'react-intl'; import { NavLink, withRouter, RouteComponentProps } from 'react-router-dom'; import Icon from 'soapbox/components/icon'; @@ -31,6 +31,10 @@ import type { // Defined in components/scrollable_list export type ScrollPosition = { height: number, top: number }; +const messages = defineMessages({ + reblogged_by: { id: 'status.reblogged_by', defaultMessage: '{name} reposted' }, +}); + export const textForScreenReader = (intl: IntlShape, status: StatusEntity, rebloggedByText?: string): string => { const { account } = status; if (!account || typeof account !== 'object') return ''; @@ -437,12 +441,10 @@ class Status extends ImmutablePureComponent {
); - rebloggedByText = intl.formatMessage({ - id: 'status.reblogged_by', - defaultMessage: '{name} reposted', - }, { - name: String(status.getIn(['account', 'acct'])), - }); + rebloggedByText = intl.formatMessage( + messages.reblogged_by, + { name: String(status.getIn(['account', 'acct'])) }, + ); // @ts-ignore what the FUCK account = status.account; diff --git a/app/soapbox/components/status_list.js b/app/soapbox/components/status_list.js index bfc90869e..5d08ab1a2 100644 --- a/app/soapbox/components/status_list.js +++ b/app/soapbox/components/status_list.js @@ -102,7 +102,7 @@ export default class StatusList extends ImmutablePureComponent { } renderLoadGap(index) { - const { statusIds, onLoadMore, isLoading } = this.props; + const { statusIds, onLoadMore, isLoading } = this.props; return ( ( @@ -164,7 +164,7 @@ export default class StatusList extends ImmutablePureComponent { } renderStatuses() { - const { statusIds, isLoading } = this.props; + const { statusIds, isLoading } = this.props; if (isLoading || statusIds.size > 0) { return statusIds.map((statusId, index) => { @@ -193,7 +193,7 @@ export default class StatusList extends ImmutablePureComponent { } render() { - const { statusIds, divideType, featuredStatusIds, onLoadMore, timelineId, totalQueuedItemsCount, isLoading, isPartial, withGroupAdmin, group, ...other } = this.props; + const { statusIds, divideType, featuredStatusIds, onLoadMore, timelineId, totalQueuedItemsCount, isLoading, isPartial, withGroupAdmin, group, ...other } = this.props; if (isPartial) { return ( diff --git a/app/soapbox/features/account/components/header.js b/app/soapbox/features/account/components/header.js index 7a65b0bef..b6fe013a7 100644 --- a/app/soapbox/features/account/components/header.js +++ b/app/soapbox/features/account/components/header.js @@ -6,7 +6,7 @@ import PropTypes from 'prop-types'; import React from 'react'; import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePureComponent from 'react-immutable-pure-component'; -import { defineMessages, injectIntl } from 'react-intl'; +import { defineMessages, FormattedMessage, injectIntl } from 'react-intl'; import { connect } from 'react-redux'; import { Link } from 'react-router-dom'; @@ -457,7 +457,7 @@ class Header extends ImmutablePureComponent { } makeInfo() { - const { account, intl, me } = this.props; + const { account, me } = this.props; const info = []; @@ -468,7 +468,7 @@ class Header extends ImmutablePureComponent { } />, ); } else if (me !== account.get('id') && account.getIn(['relationship', 'blocking'])) { @@ -476,7 +476,7 @@ class Header extends ImmutablePureComponent { } />, ); } @@ -486,7 +486,7 @@ class Header extends ImmutablePureComponent { } />, ); } else if (me !== account.get('id') && account.getIn(['relationship', 'domain_blocking'])) { @@ -494,7 +494,7 @@ class Header extends ImmutablePureComponent { } />, ); } diff --git a/app/soapbox/features/admin/components/unapproved_account.tsx b/app/soapbox/features/admin/components/unapproved_account.tsx index 5f38684cb..ffd76fc30 100644 --- a/app/soapbox/features/admin/components/unapproved_account.tsx +++ b/app/soapbox/features/admin/components/unapproved_account.tsx @@ -2,13 +2,12 @@ import React from 'react'; import { defineMessages, useIntl } from 'react-intl'; import { approveUsers } from 'soapbox/actions/admin'; +import { rejectUserModal } from 'soapbox/actions/moderation'; import snackbar from 'soapbox/actions/snackbar'; import IconButton from 'soapbox/components/icon_button'; import { useAppSelector, useAppDispatch } from 'soapbox/hooks'; import { makeGetAccount } from 'soapbox/selectors'; -import { rejectUserModal } from '../../../actions/moderation'; - const messages = defineMessages({ approved: { id: 'admin.awaiting_approval.approved_message', defaultMessage: '{acct} was approved!' }, rejected: { id: 'admin.awaiting_approval.rejected_message', defaultMessage: '{acct} was rejected.' }, diff --git a/app/soapbox/features/auth_login/components/login_form.tsx b/app/soapbox/features/auth_login/components/login_form.tsx index dd125b568..64145453c 100644 --- a/app/soapbox/features/auth_login/components/login_form.tsx +++ b/app/soapbox/features/auth_login/components/login_form.tsx @@ -26,7 +26,7 @@ const LoginForm: React.FC = ({ isLoading, handleSubmit }) => { return (
-

{intl.formatMessage({ id: 'login_form.header', defaultMessage: 'Sign In' })}

+

diff --git a/app/soapbox/features/auth_login/components/otp_auth_form.js b/app/soapbox/features/auth_login/components/otp_auth_form.js index 637f78529..0446a7afe 100644 --- a/app/soapbox/features/auth_login/components/otp_auth_form.js +++ b/app/soapbox/features/auth_login/components/otp_auth_form.js @@ -11,6 +11,7 @@ import { Button, Form, FormActions, FormGroup, Input } from 'soapbox/components/ const messages = defineMessages({ otpCodeHint: { id: 'login.fields.otp_code_hint', defaultMessage: 'Enter the two-factor code generated by your phone app or use one of your recovery codes' }, otpCodeLabel: { id: 'login.fields.otp_code_label', defaultMessage: 'Two-factor code:' }, + otpLoginFail: { id: 'login.otp_log_in.fail', defaultMessage: 'Invalid code, please try again.' }, }); export default @connect() @@ -70,7 +71,7 @@ class OtpAuthForm extends ImmutablePureComponent {

- {intl.formatMessage({ id: 'password_reset.header', defaultMessage: 'Reset Password' })} +

diff --git a/app/soapbox/features/auth_login/components/password_reset_confirm.js b/app/soapbox/features/auth_login/components/password_reset_confirm.tsx similarity index 74% rename from app/soapbox/features/auth_login/components/password_reset_confirm.js rename to app/soapbox/features/auth_login/components/password_reset_confirm.tsx index 4d6372a48..2f30e700f 100644 --- a/app/soapbox/features/auth_login/components/password_reset_confirm.js +++ b/app/soapbox/features/auth_login/components/password_reset_confirm.tsx @@ -1,14 +1,17 @@ -import PropTypes from 'prop-types'; import * as React from 'react'; -import { FormattedMessage, injectIntl, useIntl } from 'react-intl'; -import { connect } from 'react-redux'; +import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; import { Redirect } from 'react-router-dom'; import { resetPasswordConfirm } from 'soapbox/actions/security'; import { Button, Form, FormActions, FormGroup, Input } from 'soapbox/components/ui'; +import { useAppDispatch } from 'soapbox/hooks'; const token = new URLSearchParams(window.location.search).get('reset_password_token'); +const messages = defineMessages({ + resetPasswordFail: { id: 'reset_password.fail', defaultMessage: 'Expired token, please try again.' }, +}); + const Statuses = { IDLE: 'IDLE', LOADING: 'LOADING', @@ -16,12 +19,9 @@ const Statuses = { FAIL: 'FAIL', }; -const mapDispatchToProps = dispatch => ({ - resetPasswordConfirm: (password, token) => dispatch(resetPasswordConfirm(password, token)), -}); - -const PasswordResetConfirm = ({ resetPasswordConfirm }) => { +const PasswordResetConfirm = () => { const intl = useIntl(); + const dispatch = useAppDispatch(); const [password, setPassword] = React.useState(''); const [status, setStatus] = React.useState(Statuses.IDLE); @@ -32,10 +32,10 @@ const PasswordResetConfirm = ({ resetPasswordConfirm }) => { event.preventDefault(); setStatus(Statuses.LOADING); - resetPasswordConfirm(password, token) + dispatch(resetPasswordConfirm(password, token)) .then(() => setStatus(Statuses.SUCCESS)) .catch(() => setStatus(Statuses.FAIL)); - }, [resetPasswordConfirm, password]); + }, [password]); const onChange = React.useCallback((event) => { setPassword(event.target.value); @@ -43,7 +43,7 @@ const PasswordResetConfirm = ({ resetPasswordConfirm }) => { const renderErrors = () => { if (status === Statuses.FAIL) { - return [intl.formatMessage({ id: 'reset_password.fail', defaultMessage: 'Expired token, please try again.' })]; + return [intl.formatMessage(messages.resetPasswordFail)]; } return []; @@ -84,8 +84,4 @@ const PasswordResetConfirm = ({ resetPasswordConfirm }) => { ); }; -PasswordResetConfirm.propTypes = { - resetPasswordConfirm: PropTypes.func, -}; - -export default injectIntl(connect(null, mapDispatchToProps)(PasswordResetConfirm)); +export default PasswordResetConfirm; diff --git a/app/soapbox/features/compose/components/compose_form.js b/app/soapbox/features/compose/components/compose_form.js index eb1fb1557..713b5d087 100644 --- a/app/soapbox/features/compose/components/compose_form.js +++ b/app/soapbox/features/compose/components/compose_form.js @@ -208,9 +208,9 @@ class ComposeForm extends ImmutablePureComponent { } handleEmojiPick = (data) => { - const { text } = this.props; - const position = this.autosuggestTextarea.textarea.selectionStart; - const needsSpace = data.custom && position > 0 && !allowedAroundShortCode.includes(text[position - 1]); + const { text } = this.props; + const position = this.autosuggestTextarea.textarea.selectionStart; + const needsSpace = data.custom && position > 0 && !allowedAroundShortCode.includes(text[position - 1]); this.props.onPickEmoji(position, data, needsSpace); } diff --git a/app/soapbox/features/compose/components/search.tsx b/app/soapbox/features/compose/components/search.tsx index 660045694..401001fc9 100644 --- a/app/soapbox/features/compose/components/search.tsx +++ b/app/soapbox/features/compose/components/search.tsx @@ -6,16 +6,15 @@ import { defineMessages, useIntl } from 'react-intl'; import { useDispatch } from 'react-redux'; import { useHistory } from 'react-router-dom'; -import AutosuggestAccountInput from 'soapbox/components/autosuggest_account_input'; -import SvgIcon from 'soapbox/components/ui/icon/svg-icon'; -import { useAppSelector } from 'soapbox/hooks'; - import { changeSearch, clearSearch, submitSearch, showSearch, -} from '../../../actions/search'; +} from 'soapbox/actions/search'; +import AutosuggestAccountInput from 'soapbox/components/autosuggest_account_input'; +import SvgIcon from 'soapbox/components/ui/icon/svg-icon'; +import { useAppSelector } from 'soapbox/hooks'; const messages = defineMessages({ placeholder: { id: 'search.placeholder', defaultMessage: 'Search' }, diff --git a/app/soapbox/features/directory/index.js b/app/soapbox/features/directory/index.js index 99eccf947..37795a4e9 100644 --- a/app/soapbox/features/directory/index.js +++ b/app/soapbox/features/directory/index.js @@ -86,7 +86,7 @@ class Directory extends React.PureComponent { render() { const { isLoading, accountIds, intl, title, features } = this.props; - const { order, local } = this.getParams(this.props, this.state); + const { order, local } = this.getParams(this.props, this.state); return ( diff --git a/app/soapbox/features/edit_email/index.js b/app/soapbox/features/edit_email/index.tsx similarity index 83% rename from app/soapbox/features/edit_email/index.js rename to app/soapbox/features/edit_email/index.tsx index b6ab94f1d..bd9becc2e 100644 --- a/app/soapbox/features/edit_email/index.js +++ b/app/soapbox/features/edit_email/index.tsx @@ -1,16 +1,17 @@ import * as React from 'react'; import { defineMessages, useIntl } from 'react-intl'; -import { useDispatch } from 'react-redux'; import { changeEmail } from 'soapbox/actions/security'; import snackbar from 'soapbox/actions/snackbar'; - -import { Button, Card, CardBody, CardHeader, CardTitle, Column, Form, FormActions, FormGroup, Input } from '../../components/ui'; +import { Button, Card, CardBody, CardHeader, CardTitle, Column, Form, FormActions, FormGroup, Input } from 'soapbox/components/ui'; +import { useAppDispatch } from 'soapbox/hooks'; const messages = defineMessages({ + header: { id: 'edit_email.header', defaultMessage: 'Change Email' }, updateEmailSuccess: { id: 'security.update_email.success', defaultMessage: 'Email successfully updated.' }, updateEmailFail: { id: 'security.update_email.fail', defaultMessage: 'Update email failed.' }, emailFieldLabel: { id: 'security.fields.email.label', defaultMessage: 'Email address' }, + emailFieldPlaceholder: { id: 'edit_email.placeholder', defaultMessage: 'me@example.com' }, passwordFieldLabel: { id: 'security.fields.password.label', defaultMessage: 'Password' }, submit: { id: 'security.submit', defaultMessage: 'Save changes' }, cancel: { id: 'common.cancel', defaultMessage: 'Cancel' }, @@ -20,7 +21,7 @@ const initialState = { email: '', password: '' }; const EditEmail = () => { const intl = useIntl(); - const dispatch = useDispatch(); + const dispatch = useAppDispatch(); const [state, setState] = React.useState(initialState); const [isLoading, setLoading] = React.useState(false); @@ -48,14 +49,14 @@ const EditEmail = () => { return ( @@ -63,9 +64,10 @@ const EditEmail = () => { diff --git a/app/soapbox/features/email_confirmation/index.js b/app/soapbox/features/email_confirmation/index.tsx similarity index 60% rename from app/soapbox/features/email_confirmation/index.js rename to app/soapbox/features/email_confirmation/index.tsx index 5e18f03a7..0c4939eed 100644 --- a/app/soapbox/features/email_confirmation/index.js +++ b/app/soapbox/features/email_confirmation/index.tsx @@ -1,14 +1,12 @@ -import PropTypes from 'prop-types'; import * as React from 'react'; -import { useIntl } from 'react-intl'; -import { useDispatch } from 'react-redux'; +import { defineMessages, useIntl } from 'react-intl'; import { Redirect } from 'react-router-dom'; +import { confirmChangedEmail } from 'soapbox/actions/security'; import snackbar from 'soapbox/actions/snackbar'; import { Spinner } from 'soapbox/components/ui'; - -import { confirmChangedEmail } from '../../actions/security'; -import { buildErrorMessage } from '../../utils/errors'; +import { useAppDispatch } from 'soapbox/hooks'; +import { buildErrorMessage } from 'soapbox/utils/errors'; const Statuses = { IDLE: 'IDLE', @@ -16,11 +14,15 @@ const Statuses = { FAIL: 'FAIL', }; +const messages = defineMessages({ + success: { id: 'email_confirmation.success', defaultMessage: 'Your email has been confirmed!' }, +}); + const token = new URLSearchParams(window.location.search).get('confirmation_token'); const EmailConfirmation = () => { const intl = useIntl(); - const dispatch = useDispatch(); + const dispatch = useAppDispatch(); const [status, setStatus] = React.useState(Statuses.IDLE); @@ -32,10 +34,7 @@ const EmailConfirmation = () => { dispatch( snackbar.success( - intl.formatMessage({ - id: 'email_confirmation.success', - defaultMessage: 'Your email has been confirmed!', - }), + intl.formatMessage(messages.success), ), ); }) @@ -43,14 +42,15 @@ const EmailConfirmation = () => { setStatus(Statuses.FAIL); if (error.response.data.error) { - const defaultMessage = buildErrorMessage(error.response.data.error); + const message = buildErrorMessage(error.response.data.error); dispatch( snackbar.error( - intl.formatMessage({ - id: 'email_confirmation.fail', - defaultMessage, - }), + message, + // intl.formatMessage({ + // id: 'email_confirmation.fail', + // defaultMessage, + // }), ), ); } @@ -67,8 +67,4 @@ const EmailConfirmation = () => { ); }; -EmailConfirmation.propTypes = { - history: PropTypes.object, -}; - export default EmailConfirmation; diff --git a/app/soapbox/features/hashtag_timeline/index.js b/app/soapbox/features/hashtag_timeline/index.js index a1854550f..bdc302f96 100644 --- a/app/soapbox/features/hashtag_timeline/index.js +++ b/app/soapbox/features/hashtag_timeline/index.js @@ -31,11 +31,11 @@ class HashtagTimeline extends React.PureComponent { // TODO: wtf is all this? // It exists in Mastodon's codebase, but undocumented if (this.additionalFor('any')) { - title.push(' ', ); + title.push(' ', ); } if (this.additionalFor('all')) { - title.push(' ', ); + title.push(' ', ); } if (this.additionalFor('none')) { diff --git a/app/soapbox/features/settings/index.tsx b/app/soapbox/features/settings/index.tsx index 1bb6722a8..c1386c288 100644 --- a/app/soapbox/features/settings/index.tsx +++ b/app/soapbox/features/settings/index.tsx @@ -23,6 +23,8 @@ const messages = defineMessages({ sessions: { id: 'settings.sessions', defaultMessage: 'Active sessions' }, deleteAccount: { id: 'settings.delete_account', defaultMessage: 'Delete Account' }, other: { id: 'settings.other', defaultMessage: 'Other options' }, + mfaEnabled: { id: 'mfa.enabled', defaultMessage: 'Enabled' }, + mfaDisabled: { id: 'mfa.disabled', defaultMessage: 'Disabled' }, }); /** User settings page. */ @@ -77,8 +79,8 @@ const Settings = () => { {isMfaEnabled ? - intl.formatMessage({ id: 'mfa.enabled', defaultMessage: 'Enabled' }) : - intl.formatMessage({ id: 'mfa.disabled', defaultMessage: 'Disabled' })} + intl.formatMessage(messages.mfaEnabled) : + intl.formatMessage(messages.mfaDisabled)} {features.sessionsAPI && ( diff --git a/app/soapbox/features/verification/registration.tsx b/app/soapbox/features/verification/registration.tsx index e038ae01a..400f6b6c4 100644 --- a/app/soapbox/features/verification/registration.tsx +++ b/app/soapbox/features/verification/registration.tsx @@ -1,6 +1,6 @@ import { AxiosError } from 'axios'; import * as React from 'react'; -import { useIntl } from 'react-intl'; +import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; import { useDispatch } from 'react-redux'; import { Redirect } from 'react-router-dom'; @@ -10,10 +10,24 @@ import { startOnboarding } from 'soapbox/actions/onboarding'; import snackbar from 'soapbox/actions/snackbar'; import { createAccount } from 'soapbox/actions/verification'; import { removeStoredVerification } from 'soapbox/actions/verification'; +import { Button, Form, FormGroup, Input } from 'soapbox/components/ui'; import { useAppSelector } from 'soapbox/hooks'; import { getRedirectUrl } from 'soapbox/utils/redirect'; -import { Button, Form, FormGroup, Input } from '../../components/ui'; +const messages = defineMessages({ + success: { + id: 'registrations.success', + defaultMessage: 'Welcome to {siteTitle}!', + }, + usernameTaken: { + id: 'registrations.unprocessable_entity', + defaultMessage: 'This username has already been taken.', + }, + error: { + id: 'registrations.error', + defaultMessage: 'Failed to register your account.', + }, +}); const initialState = { username: '', @@ -45,10 +59,7 @@ const Registration = () => { dispatch(startOnboarding()); dispatch( snackbar.success( - intl.formatMessage({ - id: 'registrations.success', - defaultMessage: 'Welcome to {siteTitle}!', - }, { siteTitle }), + intl.formatMessage(messages.success, { siteTitle }), ), ); }) @@ -56,19 +67,13 @@ const Registration = () => { if (error?.response?.status === 422) { dispatch( snackbar.error( - intl.formatMessage({ - id: 'registrations.unprocessable_entity', - defaultMessage: 'This username has already been taken.', - }), + intl.formatMessage(messages.usernameTaken), ), ); } else { dispatch( snackbar.error( - intl.formatMessage({ - id: 'registrations.error', - defaultMessage: 'Failed to register your account.', - }), + intl.formatMessage(messages.error), ), ); } @@ -90,7 +95,7 @@ const Registration = () => {

- {intl.formatMessage({ id: 'registration.header', defaultMessage: 'Register your account' })} +

diff --git a/app/soapbox/features/verification/steps/age-verification.js b/app/soapbox/features/verification/steps/age-verification.js index ff1ba51f9..caeed7704 100644 --- a/app/soapbox/features/verification/steps/age-verification.js +++ b/app/soapbox/features/verification/steps/age-verification.js @@ -1,13 +1,19 @@ import PropTypes from 'prop-types'; import * as React from 'react'; import DatePicker from 'react-datepicker'; -import { useIntl } from 'react-intl'; +import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; import { useDispatch, useSelector } from 'react-redux'; import snackbar from 'soapbox/actions/snackbar'; import { verifyAge } from 'soapbox/actions/verification'; +import { Button, Form, FormGroup, Text } from 'soapbox/components/ui'; -import { Button, Form, FormGroup, Text } from '../../../components/ui'; +const messages = defineMessages({ + fail: { + id: 'age_verification.fail', + defaultMessage: 'You must be {ageMinimum, plural, one {# year} other {# years}} old or older.', + }, +}); function meetsAgeMinimum(birthday, ageMinimum) { const month = birthday.getUTCMonth(); @@ -46,15 +52,9 @@ const AgeVerification = () => { dispatch(verifyAge(birthday)); } else { dispatch( - snackbar.error( - intl.formatMessage({ - id: 'age_verification.fail', - defaultMessage: 'You must be {ageMinimum, plural, one {# year} other {# years}} old or older.', - values: { - ageMinimum, - }, - }), - ), + snackbar.error(intl.formatMessage(messages.fail, { + ageMinimum, + })), ); } }, [date, ageMinimum]); @@ -63,7 +63,7 @@ const AgeVerification = () => {

- {intl.formatMessage({ id: 'age_verification.header', defaultMessage: 'Enter your birth date' })} +

From 1133b05714ef10f31a09fa1523faf9df86f0bfe0 Mon Sep 17 00:00:00 2001 From: Justin Date: Tue, 24 May 2022 09:46:21 -0400 Subject: [PATCH 032/102] Fix key warnings --- .../components/follow_recommendations_list.tsx | 4 ++-- app/soapbox/features/ui/components/profile-dropdown.tsx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/soapbox/features/follow_recommendations/components/follow_recommendations_list.tsx b/app/soapbox/features/follow_recommendations/components/follow_recommendations_list.tsx index 4ca838072..fc5ab38d0 100644 --- a/app/soapbox/features/follow_recommendations/components/follow_recommendations_list.tsx +++ b/app/soapbox/features/follow_recommendations/components/follow_recommendations_list.tsx @@ -31,8 +31,8 @@ const FollowRecommendationsList: React.FC = () => { return (
- {suggestions.size > 0 ? suggestions.map((suggestion: { account: string }) => ( - + {suggestions.size > 0 ? suggestions.map((suggestion: { account: string }, idx: number) => ( + )) : (
diff --git a/app/soapbox/features/ui/components/profile-dropdown.tsx b/app/soapbox/features/ui/components/profile-dropdown.tsx index e3c468b13..121304960 100644 --- a/app/soapbox/features/ui/components/profile-dropdown.tsx +++ b/app/soapbox/features/ui/components/profile-dropdown.tsx @@ -112,7 +112,7 @@ const ProfileDropdown: React.FC = ({ account, children }) => { {menu.map((menuItem, idx) => { if (menuItem.toggle) { return ( -
+
{menuItem.text} {menuItem.toggle} From 38b3b7150f9f781b5690603cf7955468e316ed65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Tue, 24 May 2022 17:16:07 +0200 Subject: [PATCH 033/102] string | React.ReactNode -> React.ReactNode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- app/soapbox/components/badge.tsx | 2 +- app/soapbox/components/ui/card/card.tsx | 2 +- app/soapbox/components/ui/modal/modal.tsx | 2 +- app/soapbox/features/ui/components/accordion.tsx | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/soapbox/components/badge.tsx b/app/soapbox/components/badge.tsx index c14a58d56..7686b1e47 100644 --- a/app/soapbox/components/badge.tsx +++ b/app/soapbox/components/badge.tsx @@ -2,7 +2,7 @@ import classNames from 'classnames'; import React from 'react'; interface IBadge { - title: string | React.ReactNode, + title: React.ReactNode, slug: 'patron' | 'donor' | 'admin' | 'moderator' | 'bot' | 'opaque', } diff --git a/app/soapbox/components/ui/card/card.tsx b/app/soapbox/components/ui/card/card.tsx index f8ed33c1e..08b501cde 100644 --- a/app/soapbox/components/ui/card/card.tsx +++ b/app/soapbox/components/ui/card/card.tsx @@ -80,7 +80,7 @@ const CardHeader: React.FC = ({ children, backHref, onBackClick }): }; interface ICardTitle { - title: string | React.ReactNode + title: React.ReactNode } /** A card's title. */ diff --git a/app/soapbox/components/ui/modal/modal.tsx b/app/soapbox/components/ui/modal/modal.tsx index 1cd8f7fd5..f0a661229 100644 --- a/app/soapbox/components/ui/modal/modal.tsx +++ b/app/soapbox/components/ui/modal/modal.tsx @@ -50,7 +50,7 @@ interface IModal { /** Don't focus the "confirm" button on mount. */ skipFocus?: boolean, /** Title text for the modal. */ - title: string | React.ReactNode, + title: React.ReactNode, width?: Widths, } diff --git a/app/soapbox/features/ui/components/accordion.tsx b/app/soapbox/features/ui/components/accordion.tsx index 8fe4415a8..40afbf7b1 100644 --- a/app/soapbox/features/ui/components/accordion.tsx +++ b/app/soapbox/features/ui/components/accordion.tsx @@ -14,7 +14,7 @@ const messages = defineMessages({ interface IAccordion { headline: React.ReactNode, - children?: string | React.ReactNode, + children?: React.ReactNode, menu?: Menu, expanded?: boolean, onToggle?: (value: boolean) => void, From 9401493af1088ccc23868ea3514b3939f9775182 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 24 May 2022 11:24:47 -0400 Subject: [PATCH 034/102] Fix feature-gating of messages --- app/soapbox/features/ui/index.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/app/soapbox/features/ui/index.tsx b/app/soapbox/features/ui/index.tsx index 2bfc70a9b..0ebb8f87a 100644 --- a/app/soapbox/features/ui/index.tsx +++ b/app/soapbox/features/ui/index.tsx @@ -184,9 +184,8 @@ const SwitchingColumnsArea: React.FC = ({ children }) => { {features.federating && } {features.conversations && } - {features.directTimeline ? ( - - ) : ( + {features.directTimeline && } + {(features.conversations && !features.directTimeline) && ( )} From f2164f09d4e0d0dfe4eeb610ba101043c97bbd0b Mon Sep 17 00:00:00 2001 From: Justin Date: Tue, 24 May 2022 13:45:00 -0400 Subject: [PATCH 035/102] Add d-screen Tailwind utility --- app/soapbox/components/loading-screen.tsx | 6 ++---- app/styles/application.scss | 4 ++++ 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/app/soapbox/components/loading-screen.tsx b/app/soapbox/components/loading-screen.tsx index ffa1fa8f9..d132933a4 100644 --- a/app/soapbox/components/loading-screen.tsx +++ b/app/soapbox/components/loading-screen.tsx @@ -9,11 +9,9 @@ const LoadingScreen: React.FC = () => {
-
+
{/* Bump up spinner vertically to counteract optical illusion. */} -
- -
+
); diff --git a/app/styles/application.scss b/app/styles/application.scss index 03f8ff65d..660fb4a68 100644 --- a/app/styles/application.scss +++ b/app/styles/application.scss @@ -102,6 +102,10 @@ .shadow-inset { box-shadow: inset 0 0 0 1px rgb(255 255 255 / 10%); } + + .d-screen { + height: 100dvh; + } } @import 'forms'; From 86e673404e87f373f91a38b460f91616ffdfa1e4 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 24 May 2022 14:06:59 -0400 Subject: [PATCH 036/102] Fix feature gating of Pleroma data imports --- app/soapbox/features/import_data/index.tsx | 5 +---- app/soapbox/features/ui/index.tsx | 2 +- app/soapbox/utils/features.ts | 6 ++++-- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/app/soapbox/features/import_data/index.tsx b/app/soapbox/features/import_data/index.tsx index b03b20f29..5d26fa0ec 100644 --- a/app/soapbox/features/import_data/index.tsx +++ b/app/soapbox/features/import_data/index.tsx @@ -6,8 +6,6 @@ import { importBlocks, importMutes, } from 'soapbox/actions/import_data'; -import { useAppSelector } from 'soapbox/hooks'; -import { getFeatures } from 'soapbox/utils/features'; import Column from '../ui/components/column'; @@ -38,13 +36,12 @@ const muteMessages = defineMessages({ const ImportData = () => { const intl = useIntl(); - const features = getFeatures(useAppSelector((state) => state.instance)); return ( - {features.importMutes && } + ); }; diff --git a/app/soapbox/features/ui/index.tsx b/app/soapbox/features/ui/index.tsx index 0ebb8f87a..948e5a5e2 100644 --- a/app/soapbox/features/ui/index.tsx +++ b/app/soapbox/features/ui/index.tsx @@ -287,7 +287,7 @@ const SwitchingColumnsArea: React.FC = ({ children }) => { - + {features.importData && } {features.accountAliasesAPI && } {features.accountMoving && } diff --git a/app/soapbox/utils/features.ts b/app/soapbox/utils/features.ts index e020456d1..e98b7262c 100644 --- a/app/soapbox/utils/features.ts +++ b/app/soapbox/utils/features.ts @@ -307,10 +307,12 @@ const getInstanceFeatures = (instance: Instance) => { importAPI: v.software === PLEROMA, /** - * Pleroma import mutes API. + * Pleroma import endpoints. + * @see POST /api/pleroma/follow_import + * @see POST /api/pleroma/blocks_import * @see POST /api/pleroma/mutes_import */ - importMutes: v.software === PLEROMA && gte(v.version, '2.2.0'), + importData: v.software === PLEROMA && gte(v.version, '2.2.0'), /** * Can create, view, and manage lists. From 8c00f5816244c199f22fb4347e70652f6854fe43 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 24 May 2022 14:14:22 -0400 Subject: [PATCH 037/102] Export: fix Mastodon fqn --- app/soapbox/actions/export_data.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/soapbox/actions/export_data.ts b/app/soapbox/actions/export_data.ts index de81215dd..b558c9e6e 100644 --- a/app/soapbox/actions/export_data.ts +++ b/app/soapbox/actions/export_data.ts @@ -1,8 +1,8 @@ import { defineMessages } from 'react-intl'; -import api, { getLinks } from '../api'; - -import snackbar from './snackbar'; +import snackbar from 'soapbox/actions/snackbar'; +import api, { getLinks } from 'soapbox/api'; +import { normalizeAccount } from 'soapbox/normalizers'; import type { SnackbarAction } from './snackbar'; import type { AxiosResponse } from 'axios'; @@ -60,7 +60,7 @@ const listAccounts = (getState: () => RootState) => async(apiResponse: AxiosResp Array.prototype.push.apply(followings, apiResponse.data); } - accounts = followings.map((account: { fqn: string }) => account.fqn); + accounts = followings.map((account: any) => normalizeAccount(account).fqn); return Array.from(new Set(accounts)); }; From 4873034206add786889b8018f15ffb2507238e87 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 24 May 2022 14:15:39 -0400 Subject: [PATCH 038/102] Disable exports for now --- app/soapbox/features/ui/index.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/soapbox/features/ui/index.tsx b/app/soapbox/features/ui/index.tsx index 948e5a5e2..dfdcfeb9f 100644 --- a/app/soapbox/features/ui/index.tsx +++ b/app/soapbox/features/ui/index.tsx @@ -84,7 +84,7 @@ import { EmailConfirmation, DeleteAccount, SoapboxConfig, - ExportData, + // ExportData, ImportData, // Backups, MfaForm, @@ -286,7 +286,8 @@ const SwitchingColumnsArea: React.FC = ({ children }) => { {features.scheduledStatuses && } - + {/* FIXME: this could DDoS our API? :\ */} + {/* */} {features.importData && } {features.accountAliasesAPI && } {features.accountMoving && } From 66a113b61a7d97878dba2bc3f9cc50e3d5fe1da2 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 24 May 2022 15:31:46 -0400 Subject: [PATCH 039/102] d-screen: fall back to h-screen --- app/soapbox/components/loading-screen.tsx | 5 +++-- app/styles/application.scss | 2 ++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/app/soapbox/components/loading-screen.tsx b/app/soapbox/components/loading-screen.tsx index d132933a4..991007ba8 100644 --- a/app/soapbox/components/loading-screen.tsx +++ b/app/soapbox/components/loading-screen.tsx @@ -10,8 +10,9 @@ const LoadingScreen: React.FC = () => {
- {/* Bump up spinner vertically to counteract optical illusion. */} - +
+ +
); diff --git a/app/styles/application.scss b/app/styles/application.scss index 660fb4a68..c0033a050 100644 --- a/app/styles/application.scss +++ b/app/styles/application.scss @@ -104,6 +104,8 @@ } .d-screen { + height: 100vh; // Backwards compatibility + /* stylelint-disable-next-line unit-no-unknown */ height: 100dvh; } } From 1eec48e7d3b65c9e59412e81b23313fd30ba3980 Mon Sep 17 00:00:00 2001 From: Justin Date: Tue, 24 May 2022 16:30:47 -0400 Subject: [PATCH 040/102] Refactor notifications --- .../notifications/components/notification.tsx | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/app/soapbox/features/notifications/components/notification.tsx b/app/soapbox/features/notifications/components/notification.tsx index 9dd8a8bd0..b3453dde1 100644 --- a/app/soapbox/features/notifications/components/notification.tsx +++ b/app/soapbox/features/notifications/components/notification.tsx @@ -1,6 +1,7 @@ import React from 'react'; import { HotKeys } from 'react-hotkeys'; -import { FormattedMessage, useIntl } from 'react-intl'; +import { defineMessages, IntlShape, MessageDescriptor } from 'react-intl'; +import { useIntl } from 'react-intl'; import { useHistory } from 'react-router-dom'; import { useAppSelector } from 'soapbox/hooks'; @@ -24,11 +25,6 @@ const notificationForScreenReader = (intl: ReturnType, message: return output.join(', '); }; -// Workaround for dynamic messages (https://github.com/formatjs/babel-plugin-react-intl/issues/119#issuecomment-326202499) -function FormattedMessageFixed(props: any) { - return ; -} - const buildLink = (account: Account): JSX.Element => ( = { user_approved: require('@tabler/icons/icons/user-plus.svg'), }; -const messages: Record = { +const messages: Record = defineMessages({ follow: { id: 'notification.follow', defaultMessage: '{name} followed you', @@ -100,18 +96,22 @@ const messages: Record id: 'notification.user_approved', defaultMessage: 'Welcome to {instance}!', }, -}; +}); -const buildMessage = (type: NotificationType, account: Account, targetName: string, instanceTitle: string): JSX.Element => { +const buildMessage = ( + intl: IntlShape, + type: NotificationType, + account: Account, + targetName: string, + instanceTitle: string, +): React.ReactNode => { const link = buildLink(account); - return ( - - ); + return intl.formatMessage(messages[type], { + name: link, + targetName, + instance: instanceTitle, + }); }; interface INotificaton { @@ -268,7 +268,7 @@ const Notification: React.FC = (props) => { const targetName = notification.target && typeof notification.target === 'object' ? notification.target.acct : ''; - const message: React.ReactNode = type && account && typeof account === 'object' ? buildMessage(type, account, targetName, instance.title) : null; + const message: React.ReactNode = type && account && typeof account === 'object' ? buildMessage(intl, type, account, targetName, instance.title) : null; return ( From 517c21ae52e2eb8de6d3a4b8c5ffb0ff83f891d0 Mon Sep 17 00:00:00 2001 From: Justin Date: Tue, 24 May 2022 16:49:59 -0400 Subject: [PATCH 041/102] Add grouped notifications messaging --- .../__tests__/notification.test.tsx | 25 +++++++++++++++++++ .../notifications/components/notification.tsx | 21 +++++++++++++--- app/soapbox/normalizers/notification.ts | 1 + .../reducers/__tests__/notifications-test.js | 6 +++++ 4 files changed, 50 insertions(+), 3 deletions(-) diff --git a/app/soapbox/features/notifications/components/__tests__/notification.test.tsx b/app/soapbox/features/notifications/components/__tests__/notification.test.tsx index f11d43f09..14c43f50c 100644 --- a/app/soapbox/features/notifications/components/__tests__/notification.test.tsx +++ b/app/soapbox/features/notifications/components/__tests__/notification.test.tsx @@ -28,6 +28,31 @@ describe('', () => { expect(screen.getByTestId('notification')).toBeInTheDocument(); expect(screen.getByTestId('account')).toContainHTML('neko@rdrama.cc'); + expect(screen.getByTestId('message')).toHaveTextContent('Nekobit followed you'); + }); + + describe('grouped notifications', () => { + it('renders a grouped follow notification for more than 2', async() => { + const { notification, state } = normalize(require('soapbox/__fixtures__/notification-follow.json')); + const groupedNotification = { ...notification.toJS(), total_count: 5 }; + + render(, undefined, state); + + expect(screen.getByTestId('notification')).toBeInTheDocument(); + expect(screen.getByTestId('account')).toContainHTML('neko@rdrama.cc'); + expect(screen.getByTestId('message')).toHaveTextContent('Nekobit + 4 others followed you'); + }); + + it('renders a grouped follow notification for 1', async() => { + const { notification, state } = normalize(require('soapbox/__fixtures__/notification-follow.json')); + const groupedNotification = { ...notification.toJS(), total_count: 2 }; + + render(, undefined, state); + + expect(screen.getByTestId('notification')).toBeInTheDocument(); + expect(screen.getByTestId('account')).toContainHTML('neko@rdrama.cc'); + expect(screen.getByTestId('message')).toHaveTextContent('Nekobit + 1 other followed you'); + }); }); it('renders a favourite notification', async() => { diff --git a/app/soapbox/features/notifications/components/notification.tsx b/app/soapbox/features/notifications/components/notification.tsx index b3453dde1..1a9dad8eb 100644 --- a/app/soapbox/features/notifications/components/notification.tsx +++ b/app/soapbox/features/notifications/components/notification.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { HotKeys } from 'react-hotkeys'; -import { defineMessages, IntlShape, MessageDescriptor } from 'react-intl'; +import { defineMessages, FormattedMessage, IntlShape, MessageDescriptor } from 'react-intl'; import { useIntl } from 'react-intl'; import { useHistory } from 'react-router-dom'; @@ -102,13 +102,27 @@ const buildMessage = ( intl: IntlShape, type: NotificationType, account: Account, + totalCount: number | null, targetName: string, instanceTitle: string, ): React.ReactNode => { const link = buildLink(account); + const name = intl.formatMessage({ + id: 'notification.name', + defaultMessage: '{link}{others}', + }, { + link, + others: totalCount && totalCount > 0 ? ( + + ) : '', + }); return intl.formatMessage(messages[type], { - name: link, + name, targetName, instance: instanceTitle, }); @@ -268,7 +282,7 @@ const Notification: React.FC = (props) => { const targetName = notification.target && typeof notification.target === 'object' ? notification.target.acct : ''; - const message: React.ReactNode = type && account && typeof account === 'object' ? buildMessage(intl, type, account, targetName, instance.title) : null; + const message: React.ReactNode = type && account && typeof account === 'object' ? buildMessage(intl, type, account, notification.total_count, targetName, instance.title) : null; return ( @@ -300,6 +314,7 @@ const Notification: React.FC = (props) => { theme='muted' size='sm' truncate + data-testid='message' > {message} diff --git a/app/soapbox/normalizers/notification.ts b/app/soapbox/normalizers/notification.ts index cee69f7ce..2bac3f2b3 100644 --- a/app/soapbox/normalizers/notification.ts +++ b/app/soapbox/normalizers/notification.ts @@ -34,6 +34,7 @@ export const NotificationRecord = ImmutableRecord({ status: null as EmbeddedEntity, target: null as EmbeddedEntity, // move type: '' as NotificationType | '', + total_count: null as number | null, // grouped notifications }); export const normalizeNotification = (notification: Record) => { diff --git a/app/soapbox/reducers/__tests__/notifications-test.js b/app/soapbox/reducers/__tests__/notifications-test.js index aa7d6f714..7061c60bb 100644 --- a/app/soapbox/reducers/__tests__/notifications-test.js +++ b/app/soapbox/reducers/__tests__/notifications-test.js @@ -274,6 +274,7 @@ describe('notifications reducer', () => { status: '9vvNxoo5EFbbnfdXQu', emoji: '😢', chat_message: null, + total_count: null, })], ['10743', ImmutableMap({ id: '10743', @@ -284,6 +285,7 @@ describe('notifications reducer', () => { status: '9vvNxoo5EFbbnfdXQu', emoji: null, chat_message: null, + total_count: null, })], ['10741', ImmutableMap({ id: '10741', @@ -294,6 +296,7 @@ describe('notifications reducer', () => { status: '9vvNxoo5EFbbnfdXQu', emoji: null, chat_message: null, + total_count: null, })], ['10734', ImmutableMap({ id: '10734', @@ -339,6 +342,7 @@ describe('notifications reducer', () => { status: '9vvNxoo5EFbbnfdXQu', emoji: '😢', chat_message: null, + total_count: null, })], ['10743', ImmutableMap({ id: '10743', @@ -349,6 +353,7 @@ describe('notifications reducer', () => { status: '9vvNxoo5EFbbnfdXQu', emoji: null, chat_message: null, + total_count: null, })], ['10741', ImmutableMap({ id: '10741', @@ -359,6 +364,7 @@ describe('notifications reducer', () => { status: '9vvNxoo5EFbbnfdXQu', emoji: null, chat_message: null, + total_count: null, })], ]), unread: 1, From 1b970d662ed4ef2f9ecc004f149b17da13fa8a95 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 25 May 2022 10:58:19 -0400 Subject: [PATCH 042/102] GitLab CI: move `lint` jobs into `test` step --- .gitlab-ci.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index faa87ef71..6fdcf8c36 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -12,7 +12,6 @@ cache: stages: - install - - lint - test - build - deploy @@ -26,7 +25,7 @@ install-dependencies: - node_modules/ lint-js: - stage: lint + stage: test script: yarn lint:js only: changes: @@ -38,7 +37,7 @@ lint-js: - ".eslintrc.js" lint-sass: - stage: lint + stage: test script: yarn lint:sass only: changes: From b798f486d07c94d670b13ba9655af74398d70803 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 25 May 2022 11:00:28 -0400 Subject: [PATCH 043/102] GitLab CI: install-dependencies --> deps, simplify --- .gitlab-ci.yml | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 6fdcf8c36..bed1c2b04 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -11,18 +11,14 @@ cache: - node_modules/ stages: - - install + - deps - test - build - deploy -install-dependencies: - stage: install - script: - - yarn install --ignore-scripts - artifacts: - paths: - - node_modules/ +deps: + stage: deps + script: yarn install --ignore-scripts lint-js: stage: test From 91adeca74ad7bd6174538dabe4cea8ff8b0ec005 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 25 May 2022 11:15:41 -0400 Subject: [PATCH 044/102] GitLab CI: move build-production to test step --- .gitlab-ci.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index bed1c2b04..9fa27818b 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -13,7 +13,6 @@ cache: stages: - deps - test - - build - deploy deps: @@ -64,13 +63,13 @@ nginx-test: - "installation/mastodon.conf" build-production: - stage: build + stage: test script: yarn build variables: NODE_ENV: production artifacts: paths: - - static + - static docs-deploy: stage: deploy From b387c3fed1bc161989b5bc089a0992156b28b92e Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 25 May 2022 11:23:55 -0400 Subject: [PATCH 045/102] GitLab CI: allow failure on review job --- .gitlab-ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 9fa27818b..49bbf1c58 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -102,6 +102,7 @@ review: url: https://$CI_COMMIT_REF_SLUG.git.soapbox.pub script: - npx -y surge static $CI_COMMIT_REF_SLUG.git.soapbox.pub + allow_failure: true pages: stage: deploy From b54396588e74b311bbc8f1e3fcec1bc3350685a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Wed, 25 May 2022 17:59:54 +0200 Subject: [PATCH 046/102] Add outline in some places MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- app/soapbox/components/account.tsx | 2 +- app/soapbox/components/ui/card/card.tsx | 2 +- app/soapbox/components/ui/menu/menu.css | 4 ++++ app/soapbox/components/ui/tabs/tabs.css | 3 ++- app/styles/components/dropdown-menu.scss | 4 +++- 5 files changed, 11 insertions(+), 4 deletions(-) diff --git a/app/soapbox/components/account.tsx b/app/soapbox/components/account.tsx index cc91ded59..e0fe7ff14 100644 --- a/app/soapbox/components/account.tsx +++ b/app/soapbox/components/account.tsx @@ -26,7 +26,7 @@ const InstanceFavicon: React.FC = ({ account }) => { }; return ( - ); diff --git a/app/soapbox/components/ui/card/card.tsx b/app/soapbox/components/ui/card/card.tsx index 08b501cde..3eedaaf40 100644 --- a/app/soapbox/components/ui/card/card.tsx +++ b/app/soapbox/components/ui/card/card.tsx @@ -63,7 +63,7 @@ const CardHeader: React.FC = ({ children, backHref, onBackClick }): const backAttributes = backHref ? { to: backHref } : { onClick: onBackClick }; return ( - + {intl.formatMessage(messages.back)} diff --git a/app/soapbox/components/ui/menu/menu.css b/app/soapbox/components/ui/menu/menu.css index cb72cd702..21f916508 100644 --- a/app/soapbox/components/ui/menu/menu.css +++ b/app/soapbox/components/ui/menu/menu.css @@ -4,6 +4,10 @@ z-index: 1003; } +[data-reach-menu-button] { + @apply focus:ring-primary-500 focus:ring-2 focus:ring-offset-2; +} + div:focus[data-reach-menu-list] { outline: none; } diff --git a/app/soapbox/components/ui/tabs/tabs.css b/app/soapbox/components/ui/tabs/tabs.css index 180641acf..ee2b5cccb 100644 --- a/app/soapbox/components/ui/tabs/tabs.css +++ b/app/soapbox/components/ui/tabs/tabs.css @@ -13,7 +13,8 @@ [data-reach-tab] { @apply flex-1 flex justify-center items-center py-4 px-1 text-center font-medium text-sm text-gray-500 - dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200; + dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200 + focus:ring-primary-500 focus:ring-2; } [data-reach-tab][data-selected] { diff --git a/app/styles/components/dropdown-menu.scss b/app/styles/components/dropdown-menu.scss index 4be465849..47e2da514 100644 --- a/app/styles/components/dropdown-menu.scss +++ b/app/styles/components/dropdown-menu.scss @@ -44,8 +44,10 @@ } &__item { + @apply focus-within:ring-primary-500 focus-within:ring-2; + a { - @apply flex px-4 py-2.5 text-sm text-gray-700 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-slate-800 cursor-pointer; + @apply flex px-4 py-2.5 text-sm text-gray-700 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-slate-800 focus:bg-gray-100 focus:hover:bg-slate-800 cursor-pointer; > .svg-icon:first-child { @apply h-5 w-5 mr-2.5 transition-none; From 260aee93b184011d314f3193f2bf2909c7cbd8aa Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 25 May 2022 12:34:56 -0400 Subject: [PATCH 047/102] Add Bug issue template --- .gitlab/issue_templates/Bug.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .gitlab/issue_templates/Bug.md diff --git a/.gitlab/issue_templates/Bug.md b/.gitlab/issue_templates/Bug.md new file mode 100644 index 000000000..579c32f66 --- /dev/null +++ b/.gitlab/issue_templates/Bug.md @@ -0,0 +1,7 @@ +### Environment + +* Soapbox version: +* Backend (Mastodon, Pleroma, etc): +* Browser/OS: + +### Bug description From 890a3f2ec53094a02a106c69efc9492029b8fb8b Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 25 May 2022 15:36:30 -0400 Subject: [PATCH 048/102] GitLab CI: only run deps if yarn.lock changes --- .gitlab-ci.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 49bbf1c58..f8b732738 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -18,6 +18,9 @@ stages: deps: stage: deps script: yarn install --ignore-scripts + only: + changes: + - yarn.lock lint-js: stage: test @@ -49,6 +52,7 @@ jest: - "**/*.json" - "app/soapbox/**/*" - "webpack/**/*" + - "custom/**/*" - "jest.config.js" - "package.json" - "yarn.lock" From a335c06f1302f90b4d5ac6714aa1b46d5e7b1b56 Mon Sep 17 00:00:00 2001 From: Justin Date: Thu, 26 May 2022 13:37:22 -0400 Subject: [PATCH 049/102] Convert blocks action to TypeScript --- app/soapbox/__fixtures__/blocks.json | 8 + app/soapbox/__mocks__/api.ts | 7 +- app/soapbox/actions/__tests__/blocks.test.ts | 183 +++++++++++++++++++ app/soapbox/actions/blocks.js | 95 ---------- app/soapbox/actions/blocks.ts | 109 +++++++++++ 5 files changed, 306 insertions(+), 96 deletions(-) create mode 100644 app/soapbox/__fixtures__/blocks.json create mode 100644 app/soapbox/actions/__tests__/blocks.test.ts delete mode 100644 app/soapbox/actions/blocks.js create mode 100644 app/soapbox/actions/blocks.ts diff --git a/app/soapbox/__fixtures__/blocks.json b/app/soapbox/__fixtures__/blocks.json new file mode 100644 index 000000000..42e8753c5 --- /dev/null +++ b/app/soapbox/__fixtures__/blocks.json @@ -0,0 +1,8 @@ +[ + { + "id": "22", + "username": "twoods", + "acct": "twoods", + "display_name": "Tiger Woods" + } +] diff --git a/app/soapbox/__mocks__/api.ts b/app/soapbox/__mocks__/api.ts index 99797009e..f81d4a3c4 100644 --- a/app/soapbox/__mocks__/api.ts +++ b/app/soapbox/__mocks__/api.ts @@ -1,6 +1,7 @@ import { jest } from '@jest/globals'; -import { AxiosInstance } from 'axios'; +import { AxiosInstance, AxiosResponse } from 'axios'; import MockAdapter from 'axios-mock-adapter'; +import LinkHeader from 'http-link-header'; const api = jest.requireActual('../api') as Record; let mocks: Array = []; @@ -15,6 +16,10 @@ const setupMock = (axios: AxiosInstance) => { export const staticClient = api.staticClient; +export const getLinks = (response: AxiosResponse): LinkHeader => { + return new LinkHeader(response.headers?.link); +}; + export const baseClient = (...params: any[]) => { const axios = api.baseClient(...params); setupMock(axios); diff --git a/app/soapbox/actions/__tests__/blocks.test.ts b/app/soapbox/actions/__tests__/blocks.test.ts new file mode 100644 index 000000000..55055722a --- /dev/null +++ b/app/soapbox/actions/__tests__/blocks.test.ts @@ -0,0 +1,183 @@ +import { __stub } from 'soapbox/api'; +import { mockStore } from 'soapbox/jest/test-helpers'; +import rootReducer from 'soapbox/reducers'; + +import { expandBlocks, fetchBlocks } from '../blocks'; + +const account = { + acct: 'twoods', + display_name: 'Tiger Woods', + id: '22', + username: 'twoods', +}; + +describe('fetchBlocks()', () => { + let store; + + describe('if logged out', () => { + beforeEach(() => { + const state = rootReducer(undefined, {}).set('me', null); + store = mockStore(state); + }); + + it('should do nothing', async() => { + await store.dispatch(fetchBlocks()); + const actions = store.getActions(); + + expect(actions).toEqual([]); + }); + }); + + describe('if logged in', () => { + beforeEach(() => { + const state = rootReducer(undefined, {}).set('me', '1234'); + store = mockStore(state); + }); + + describe('with a successful API request', () => { + beforeEach(() => { + const blocks = require('soapbox/__fixtures__/blocks.json'); + + __stub((mock) => { + mock.onGet('/api/v1/blocks').reply(200, blocks, { + link: '; rel=\'prev\'', + }); + }); + }); + + it('should fetch blocks from the API', async() => { + const expectedActions = [ + { type: 'BLOCKS_FETCH_REQUEST' }, + { type: 'ACCOUNTS_IMPORT', accounts: [account] }, + { type: 'BLOCKS_FETCH_SUCCESS', accounts: [account], next: null }, + { + type: 'RELATIONSHIPS_FETCH_REQUEST', + ids: ['22'], + skipLoading: true, + }, + ]; + await store.dispatch(fetchBlocks()); + const actions = store.getActions(); + + expect(actions).toEqual(expectedActions); + }); + }); + + describe('with an unsuccessful API request', () => { + beforeEach(() => { + __stub((mock) => { + mock.onGet('/api/v1/blocks').networkError(); + }); + }); + + it('should dispatch failed action', async() => { + const expectedActions = [ + { type: 'BLOCKS_FETCH_REQUEST' }, + { type: 'BLOCKS_FETCH_FAIL', error: new Error('Network Error') }, + ]; + await store.dispatch(fetchBlocks()); + const actions = store.getActions(); + + expect(actions).toEqual(expectedActions); + }); + }); + }); +}); + +describe('expandBlocks()', () => { + let store; + + describe('if logged out', () => { + beforeEach(() => { + const state = rootReducer(undefined, {}).set('me', null); + store = mockStore(state); + }); + + it('should do nothing', async() => { + await store.dispatch(expandBlocks()); + const actions = store.getActions(); + + expect(actions).toEqual([]); + }); + }); + + describe('if logged in', () => { + beforeEach(() => { + const state = rootReducer(undefined, {}).set('me', '1234'); + store = mockStore(state); + }); + + describe('without a url', () => { + beforeEach(() => { + const state = rootReducer(undefined, {}) + .set('me', '1234') + .set('user_lists', { blocks: { next: null } }); + store = mockStore(state); + }); + + it('should do nothing', async() => { + await store.dispatch(expandBlocks()); + const actions = store.getActions(); + + expect(actions).toEqual([]); + }); + }); + + describe('with a url', () => { + beforeEach(() => { + const state = rootReducer(undefined, {}) + .set('me', '1234') + .set('user_lists', { blocks: { next: 'example' } }); + store = mockStore(state); + }); + + describe('with a successful API request', () => { + beforeEach(() => { + const blocks = require('soapbox/__fixtures__/blocks.json'); + + __stub((mock) => { + mock.onGet('example').reply(200, blocks, { + link: '; rel=\'prev\'', + }); + }); + }); + + it('should fetch blocks from the url', async() => { + const expectedActions = [ + { type: 'BLOCKS_EXPAND_REQUEST' }, + { type: 'ACCOUNTS_IMPORT', accounts: [account] }, + { type: 'BLOCKS_EXPAND_SUCCESS', accounts: [account], next: null }, + { + type: 'RELATIONSHIPS_FETCH_REQUEST', + ids: ['22'], + skipLoading: true, + }, + ]; + await store.dispatch(expandBlocks()); + const actions = store.getActions(); + + expect(actions).toEqual(expectedActions); + }); + }); + + describe('with an unsuccessful API request', () => { + beforeEach(() => { + __stub((mock) => { + mock.onGet('example').networkError(); + }); + }); + + it('should dispatch failed action', async() => { + const expectedActions = [ + { type: 'BLOCKS_EXPAND_REQUEST' }, + { type: 'BLOCKS_EXPAND_FAIL', error: new Error('Network Error') }, + ]; + await store.dispatch(expandBlocks()); + const actions = store.getActions(); + + expect(actions).toEqual(expectedActions); + }); + }); + }); + }); +}); diff --git a/app/soapbox/actions/blocks.js b/app/soapbox/actions/blocks.js deleted file mode 100644 index 554446f2f..000000000 --- a/app/soapbox/actions/blocks.js +++ /dev/null @@ -1,95 +0,0 @@ -import { isLoggedIn } from 'soapbox/utils/auth'; -import { getNextLinkName } from 'soapbox/utils/quirks'; - -import api, { getLinks } from '../api'; - -import { fetchRelationships } from './accounts'; -import { importFetchedAccounts } from './importer'; - -export const BLOCKS_FETCH_REQUEST = 'BLOCKS_FETCH_REQUEST'; -export const BLOCKS_FETCH_SUCCESS = 'BLOCKS_FETCH_SUCCESS'; -export const BLOCKS_FETCH_FAIL = 'BLOCKS_FETCH_FAIL'; - -export const BLOCKS_EXPAND_REQUEST = 'BLOCKS_EXPAND_REQUEST'; -export const BLOCKS_EXPAND_SUCCESS = 'BLOCKS_EXPAND_SUCCESS'; -export const BLOCKS_EXPAND_FAIL = 'BLOCKS_EXPAND_FAIL'; - -export function fetchBlocks() { - return (dispatch, getState) => { - if (!isLoggedIn(getState)) return; - const nextLinkName = getNextLinkName(getState); - - dispatch(fetchBlocksRequest()); - - api(getState).get('/api/v1/blocks').then(response => { - const next = getLinks(response).refs.find(link => link.rel === nextLinkName); - dispatch(importFetchedAccounts(response.data)); - dispatch(fetchBlocksSuccess(response.data, next ? next.uri : null)); - dispatch(fetchRelationships(response.data.map(item => item.id))); - }).catch(error => dispatch(fetchBlocksFail(error))); - }; -} - -export function fetchBlocksRequest() { - return { - type: BLOCKS_FETCH_REQUEST, - }; -} - -export function fetchBlocksSuccess(accounts, next) { - return { - type: BLOCKS_FETCH_SUCCESS, - accounts, - next, - }; -} - -export function fetchBlocksFail(error) { - return { - type: BLOCKS_FETCH_FAIL, - error, - }; -} - -export function expandBlocks() { - return (dispatch, getState) => { - if (!isLoggedIn(getState)) return; - const nextLinkName = getNextLinkName(getState); - - const url = getState().getIn(['user_lists', 'blocks', 'next']); - - if (url === null) { - return; - } - - dispatch(expandBlocksRequest()); - - api(getState).get(url).then(response => { - const next = getLinks(response).refs.find(link => link.rel === nextLinkName); - dispatch(importFetchedAccounts(response.data)); - dispatch(expandBlocksSuccess(response.data, next ? next.uri : null)); - dispatch(fetchRelationships(response.data.map(item => item.id))); - }).catch(error => dispatch(expandBlocksFail(error))); - }; -} - -export function expandBlocksRequest() { - return { - type: BLOCKS_EXPAND_REQUEST, - }; -} - -export function expandBlocksSuccess(accounts, next) { - return { - type: BLOCKS_EXPAND_SUCCESS, - accounts, - next, - }; -} - -export function expandBlocksFail(error) { - return { - type: BLOCKS_EXPAND_FAIL, - error, - }; -} diff --git a/app/soapbox/actions/blocks.ts b/app/soapbox/actions/blocks.ts new file mode 100644 index 000000000..af68b7b83 --- /dev/null +++ b/app/soapbox/actions/blocks.ts @@ -0,0 +1,109 @@ +import { AnyAction } from '@reduxjs/toolkit'; +import { AxiosError } from 'axios'; + +import { isLoggedIn } from 'soapbox/utils/auth'; +import { getNextLinkName } from 'soapbox/utils/quirks'; + +import api, { getLinks } from '../api'; + +import { fetchRelationships } from './accounts'; +import { importFetchedAccounts } from './importer'; + +const BLOCKS_FETCH_REQUEST = 'BLOCKS_FETCH_REQUEST'; +const BLOCKS_FETCH_SUCCESS = 'BLOCKS_FETCH_SUCCESS'; +const BLOCKS_FETCH_FAIL = 'BLOCKS_FETCH_FAIL'; + +const BLOCKS_EXPAND_REQUEST = 'BLOCKS_EXPAND_REQUEST'; +const BLOCKS_EXPAND_SUCCESS = 'BLOCKS_EXPAND_SUCCESS'; +const BLOCKS_EXPAND_FAIL = 'BLOCKS_EXPAND_FAIL'; + +const fetchBlocks = () => (dispatch: React.Dispatch, getState: any) => { + if (!isLoggedIn(getState)) return null; + const nextLinkName = getNextLinkName(getState); + + dispatch(fetchBlocksRequest()); + + return api(getState) + .get('/api/v1/blocks') + .then(response => { + const next = getLinks(response).refs.find(link => link.rel === nextLinkName); + dispatch(importFetchedAccounts(response.data)); + dispatch(fetchBlocksSuccess(response.data, next ? next.uri : null)); + dispatch(fetchRelationships(response.data.map((item: any) => item.id)) as any); + }) + .catch(error => dispatch(fetchBlocksFail(error))); +}; + +function fetchBlocksRequest() { + return { type: BLOCKS_FETCH_REQUEST }; +} + +function fetchBlocksSuccess(accounts: any, next: any) { + return { + type: BLOCKS_FETCH_SUCCESS, + accounts, + next, + }; +} + +function fetchBlocksFail(error: AxiosError) { + return { + type: BLOCKS_FETCH_FAIL, + error, + }; +} + +const expandBlocks = () => (dispatch: React.Dispatch, getState: any) => { + if (!isLoggedIn(getState)) return null; + const nextLinkName = getNextLinkName(getState); + + const url = getState().getIn(['user_lists', 'blocks', 'next']); + + if (url === null) { + return null; + } + + dispatch(expandBlocksRequest()); + + return api(getState) + .get(url) + .then(response => { + const next = getLinks(response).refs.find(link => link.rel === nextLinkName); + dispatch(importFetchedAccounts(response.data)); + dispatch(expandBlocksSuccess(response.data, next ? next.uri : null)); + dispatch(fetchRelationships(response.data.map((item: any) => item.id)) as any); + }) + .catch(error => dispatch(expandBlocksFail(error))); +}; + +function expandBlocksRequest() { + return { + type: BLOCKS_EXPAND_REQUEST, + }; +} + +function expandBlocksSuccess(accounts: any, next: any) { + return { + type: BLOCKS_EXPAND_SUCCESS, + accounts, + next, + }; +} + +function expandBlocksFail(error: AxiosError) { + return { + type: BLOCKS_EXPAND_FAIL, + error, + }; +} + +export { + fetchBlocks, + expandBlocks, + BLOCKS_FETCH_REQUEST, + BLOCKS_FETCH_SUCCESS, + BLOCKS_FETCH_FAIL, + BLOCKS_EXPAND_REQUEST, + BLOCKS_EXPAND_SUCCESS, + BLOCKS_EXPAND_FAIL, +}; From 5c49cc0b84109b9cebaecb016f0f87a20e851ec2 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 26 May 2022 14:51:59 -0400 Subject: [PATCH 050/102] Convert ServiceWorker to TypeScript --- .../service_worker/{entry.js => entry.ts} | 0 ...fications.js => web_push_notifications.ts} | 129 ++++++++++++------ tsconfig.json | 1 + webpack/production.js | 2 +- 4 files changed, 88 insertions(+), 44 deletions(-) rename app/soapbox/service_worker/{entry.js => entry.ts} (100%) rename app/soapbox/service_worker/{web_push_notifications.js => web_push_notifications.ts} (58%) diff --git a/app/soapbox/service_worker/entry.js b/app/soapbox/service_worker/entry.ts similarity index 100% rename from app/soapbox/service_worker/entry.js rename to app/soapbox/service_worker/entry.ts diff --git a/app/soapbox/service_worker/web_push_notifications.js b/app/soapbox/service_worker/web_push_notifications.ts similarity index 58% rename from app/soapbox/service_worker/web_push_notifications.js rename to app/soapbox/service_worker/web_push_notifications.ts index 5dbd749f4..dee650600 100644 --- a/app/soapbox/service_worker/web_push_notifications.js +++ b/app/soapbox/service_worker/web_push_notifications.ts @@ -4,18 +4,60 @@ import { unescape } from 'lodash'; import locales from './web_push_locales'; +import type { + Account as AccountEntity, + Notification as NotificationEntity, + Status as StatusEntity, +} from 'soapbox/types/entities'; + const MAX_NOTIFICATIONS = 5; const GROUP_TAG = 'tag'; -const notify = options => +// https://www.devextent.com/create-service-worker-typescript/ +declare const self: ServiceWorkerGlobalScope; + +interface NotificationData { + access_token?: string, + preferred_locale: string, + hiddenBody?: string, + hiddenImage?: string, + id?: string, + url: string, + count?: number, +} + +interface ExtendedNotificationOptions extends NotificationOptions { + title: string, + data: NotificationData, +} + +interface ClonedNotification { + body?: string, + image?: string, + actions?: NotificationAction[], + data: NotificationData, + title: string, + tag?: string, +} + +interface APIStatus extends Omit { + media_attachments: { preview_url: string }[], +} + +interface APINotification extends Omit { + account: AccountEntity, + status?: APIStatus, +} + +const notify = (options: ExtendedNotificationOptions): Promise => self.registration.getNotifications().then(notifications => { if (notifications.length >= MAX_NOTIFICATIONS) { // Reached the maximum number of notifications, proceed with grouping - const group = { + const group: ClonedNotification = { title: formatMessage('notifications.group', options.data.preferred_locale, { count: notifications.length + 1 }), - body: notifications.sort((n1, n2) => n1.timestamp < n2.timestamp).map(notification => notification.title).join('\n'), + body: notifications.map(notification => notification.title).join('\n'), tag: GROUP_TAG, data: { - url: (new URL('/notifications', self.location)).href, + url: (new URL('/notifications', self.location.href)).href, count: notifications.length + 1, preferred_locale: options.data.preferred_locale, }, @@ -26,10 +68,11 @@ const notify = options => return self.registration.showNotification(group.title, group); } else if (notifications.length === 1 && notifications[0].tag === GROUP_TAG) { // Already grouped, proceed with appending the notification to the group const group = cloneNotification(notifications[0]); + const count = (group.data.count || 0) + 1; - group.title = formatMessage('notifications.group', options.data.preferred_locale, { count: group.data.count + 1 }); + group.title = formatMessage('notifications.group', options.data.preferred_locale, { count }); group.body = `${options.title}\n${group.body}`; - group.data = { ...group.data, count: group.data.count + 1 }; + group.data = { ...group.data, count }; return self.registration.showNotification(group.title, group); } @@ -37,8 +80,8 @@ const notify = options => return self.registration.showNotification(options.title, options); }); -const fetchFromApi = (path, method, accessToken) => { - const url = (new URL(path, self.location)).href; +const fetchFromApi = (path: string, method: string, accessToken: string): Promise => { + const url = (new URL(path, self.location.href)).href; return fetch(url, { headers: { @@ -52,50 +95,50 @@ const fetchFromApi = (path, method, accessToken) => { if (res.ok) { return res; } else { - throw new Error(res.status); + throw new Error(String(res.status)); } }).then(res => res.json()); }; -const cloneNotification = notification => { - const clone = {}; - let k; +const cloneNotification = (notification: Notification): ClonedNotification => { + const clone: any = {}; + let k: string; // Object.assign() does not work with notifications for (k in notification) { - clone[k] = notification[k]; + clone[k] = (notification as any)[k]; } - return clone; + return clone as ClonedNotification; }; -const formatMessage = (messageId, locale, values = {}) => - (new IntlMessageFormat(locales[locale][messageId], locale)).format(values); +const formatMessage = (messageId: string, locale: string, values = {}): string => + (new IntlMessageFormat(locales[locale][messageId], locale)).format(values) as string; -const htmlToPlainText = html => +const htmlToPlainText = (html: string): string => unescape(html.replace(//g, '\n').replace(/<\/p><[^>]*>/g, '\n\n').replace(/<[^>]*>/g, '')); -const handlePush = (event) => { - const { access_token, notification_id, preferred_locale, title, body, icon } = event.data.json(); +const handlePush = (event: PushEvent) => { + const { access_token, notification_id, preferred_locale, title, body, icon } = event.data?.json(); // Placeholder until more information can be loaded event.waitUntil( fetchFromApi(`/api/v1/notifications/${notification_id}`, 'get', access_token).then(notification => { - const options = {}; + const options: ExtendedNotificationOptions = { + title: formatMessage(`notification.${notification.type}`, preferred_locale, { name: notification.account.display_name.length > 0 ? notification.account.display_name : notification.account.username }), + body: notification.status && htmlToPlainText(notification.status.content), + icon: notification.account.avatar_static, + timestamp: notification.created_at && Number(new Date(notification.created_at)), + tag: notification.id, + image: notification.status?.media_attachments[0]?.preview_url, + data: { access_token, preferred_locale, id: notification.status ? notification.status.id : notification.account.id, url: notification.status ? `/@${notification.account.username}/posts/${notification.status.id}` : `/@${notification.account.username}` }, + }; - options.title = formatMessage(`notification.${notification.type}`, preferred_locale, { name: notification.account.display_name.length > 0 ? notification.account.display_name : notification.account.username }); - options.body = notification.status && htmlToPlainText(notification.status.content); - options.icon = notification.account.avatar_static; - options.timestamp = notification.created_at && new Date(notification.created_at); - options.tag = notification.id; - options.image = notification.status && notification.status.media_attachments.length > 0 && notification.status.media_attachments[0].preview_url || undefined; - options.data = { access_token, preferred_locale, id: notification.status ? notification.status.id : notification.account.id, url: notification.status ? `/@${notification.account.username}/posts/${notification.status.id}` : `/@${notification.account.username}` }; + if (notification.status?.spoiler_text || notification.status?.sensitive) { + options.data.hiddenBody = htmlToPlainText(notification.status?.content); + options.data.hiddenImage = notification.status?.media_attachments[0]?.preview_url; - if (notification.status?.spoiler_text || notification.status.sensitive) { - options.data.hiddenBody = htmlToPlainText(notification.status.content); - options.data.hiddenImage = notification.status.media_attachments.length > 0 && notification.status.media_attachments[0].preview_url; - - if (notification.status.spoiler_text) { + if (notification.status?.spoiler_text) { options.body = notification.status.spoiler_text; } @@ -112,39 +155,39 @@ const handlePush = (event) => { body, icon, tag: notification_id, - timestamp: new Date(), + timestamp: Number(new Date()), data: { access_token, preferred_locale, url: '/notifications' }, }); }), ); }; -const actionExpand = preferred_locale => ({ +const actionExpand = (preferred_locale: string) => ({ action: 'expand', icon: `/${require('../../images/web-push/web-push-icon_expand.png')}`, title: formatMessage('status.show_more', preferred_locale), }); -const actionReblog = preferred_locale => ({ +const actionReblog = (preferred_locale: string) => ({ action: 'reblog', icon: `/${require('../../images/web-push/web-push-icon_reblog.png')}`, title: formatMessage('status.reblog', preferred_locale), }); -const actionFavourite = preferred_locale => ({ +const actionFavourite = (preferred_locale: string) => ({ action: 'favourite', icon: `/${require('../../images/web-push/web-push-icon_favourite.png')}`, title: formatMessage('status.favourite', preferred_locale), }); -const findBestClient = clients => { +const findBestClient = (clients: readonly WindowClient[]): WindowClient => { const focusedClient = clients.find(client => client.focused); const visibleClient = clients.find(client => client.visibilityState === 'visible'); return focusedClient || visibleClient || clients[0]; }; -const expandNotification = notification => { +const expandNotification = (notification: Notification) => { const newNotification = cloneNotification(notification); newNotification.body = newNotification.data.hiddenBody; @@ -154,25 +197,25 @@ const expandNotification = notification => { return self.registration.showNotification(newNotification.title, newNotification); }; -const removeActionFromNotification = (notification, action) => { +const removeActionFromNotification = (notification: Notification, action: string) => { const newNotification = cloneNotification(notification); - newNotification.actions = newNotification.actions.filter(item => item.action !== action); + newNotification.actions = newNotification.actions?.filter(item => item.action !== action); return self.registration.showNotification(newNotification.title, newNotification); }; -const openUrl = url => +const openUrl = (url: string) => self.clients.matchAll({ type: 'window' }).then(clientList => { if (clientList.length === 0) { return self.clients.openWindow(url); } else { const client = findBestClient(clientList); - return client.navigate(url).then(client => client.focus()); + return client.navigate(url).then(client => client?.focus()); } }); -const handleNotificationClick = (event) => { +const handleNotificationClick = (event: NotificationEvent) => { const reactToNotificationClick = new Promise((resolve, reject) => { if (event.action) { if (event.action === 'expand') { diff --git a/tsconfig.json b/tsconfig.json index 5e0e8d07c..8989bd57e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -5,6 +5,7 @@ "sourceMap": true, "strict": true, "module": "es2022", + "lib": ["es2019", "es6", "dom", "webworker"], "target": "es5", "jsx": "react", "allowJs": true, diff --git a/webpack/production.js b/webpack/production.js index ce47d647b..18666a109 100644 --- a/webpack/production.js +++ b/webpack/production.js @@ -84,7 +84,7 @@ module.exports = merge(sharedConfig, { ], ServiceWorker: { cacheName: 'soapbox', - entry: join(__dirname, '../app/soapbox/service_worker/entry.js'), + entry: join(__dirname, '../app/soapbox/service_worker/entry.ts'), minify: true, }, cacheMaps: [{ From d111c4c2d2ee60f687aceba43e5bba4fe9e18790 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 26 May 2022 15:16:03 -0400 Subject: [PATCH 051/102] ServiceWorker: add jsdoc comments --- .../service_worker/web_push_notifications.ts | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/app/soapbox/service_worker/web_push_notifications.ts b/app/soapbox/service_worker/web_push_notifications.ts index dee650600..f60c21f54 100644 --- a/app/soapbox/service_worker/web_push_notifications.ts +++ b/app/soapbox/service_worker/web_push_notifications.ts @@ -10,12 +10,15 @@ import type { Status as StatusEntity, } from 'soapbox/types/entities'; +/** Limit before we start grouping device notifications into a single notification. */ const MAX_NOTIFICATIONS = 5; +/** Tag for the grouped notification. */ const GROUP_TAG = 'tag'; // https://www.devextent.com/create-service-worker-typescript/ declare const self: ServiceWorkerGlobalScope; +/** Soapbox notification data from push event. */ interface NotificationData { access_token?: string, preferred_locale: string, @@ -26,11 +29,13 @@ interface NotificationData { count?: number, } +/** ServiceWorker Notification options with extra fields. */ interface ExtendedNotificationOptions extends NotificationOptions { title: string, data: NotificationData, } +/** Partial clone of ServiceWorker Notification with mutability. */ interface ClonedNotification { body?: string, image?: string, @@ -40,15 +45,20 @@ interface ClonedNotification { tag?: string, } +/** Status entitiy from the API (kind of). */ +// HACK interface APIStatus extends Omit { media_attachments: { preview_url: string }[], } +/** Notification entity from the API (kind of). */ +// HACK interface APINotification extends Omit { account: AccountEntity, status?: APIStatus, } +/** Show the actual push notification on the device. */ const notify = (options: ExtendedNotificationOptions): Promise => self.registration.getNotifications().then(notifications => { if (notifications.length >= MAX_NOTIFICATIONS) { // Reached the maximum number of notifications, proceed with grouping @@ -80,6 +90,7 @@ const notify = (options: ExtendedNotificationOptions): Promise => return self.registration.showNotification(options.title, options); }); +/** Perform an API request to the backend. */ const fetchFromApi = (path: string, method: string, accessToken: string): Promise => { const url = (new URL(path, self.location.href)).href; @@ -100,6 +111,7 @@ const fetchFromApi = (path: string, method: string, accessToken: string): Promis }).then(res => res.json()); }; +/** Create a mutable object that loosely matches the Notification. */ const cloneNotification = (notification: Notification): ClonedNotification => { const clone: any = {}; let k: string; @@ -112,12 +124,15 @@ const cloneNotification = (notification: Notification): ClonedNotification => { return clone as ClonedNotification; }; +/** Get translated message for the user's locale. */ const formatMessage = (messageId: string, locale: string, values = {}): string => (new IntlMessageFormat(locales[locale][messageId], locale)).format(values) as string; +/** Strip HTML for display in a native notification. */ const htmlToPlainText = (html: string): string => unescape(html.replace(//g, '\n').replace(/<\/p><[^>]*>/g, '\n\n').replace(/<[^>]*>/g, '')); +/** ServiceWorker `push` event callback. */ const handlePush = (event: PushEvent) => { const { access_token, notification_id, preferred_locale, title, body, icon } = event.data?.json(); @@ -162,24 +177,28 @@ const handlePush = (event: PushEvent) => { ); }; +/** Native action to open a status on the device. */ const actionExpand = (preferred_locale: string) => ({ action: 'expand', icon: `/${require('../../images/web-push/web-push-icon_expand.png')}`, title: formatMessage('status.show_more', preferred_locale), }); +/** Native action to repost status. */ const actionReblog = (preferred_locale: string) => ({ action: 'reblog', icon: `/${require('../../images/web-push/web-push-icon_reblog.png')}`, title: formatMessage('status.reblog', preferred_locale), }); +/** Native action to like status. */ const actionFavourite = (preferred_locale: string) => ({ action: 'favourite', icon: `/${require('../../images/web-push/web-push-icon_favourite.png')}`, title: formatMessage('status.favourite', preferred_locale), }); +/** Get the active tab if possible, or any open tab. */ const findBestClient = (clients: readonly WindowClient[]): WindowClient => { const focusedClient = clients.find(client => client.focused); const visibleClient = clients.find(client => client.visibilityState === 'visible'); @@ -197,6 +216,7 @@ const expandNotification = (notification: Notification) => { return self.registration.showNotification(newNotification.title, newNotification); }; +/** Update the native notification, but delete the action (because it was performed). */ const removeActionFromNotification = (notification: Notification, action: string) => { const newNotification = cloneNotification(notification); @@ -205,6 +225,7 @@ const removeActionFromNotification = (notification: Notification, action: string return self.registration.showNotification(newNotification.title, newNotification); }; +/** Open a URL on the device. */ const openUrl = (url: string) => self.clients.matchAll({ type: 'window' }).then(clientList => { if (clientList.length === 0) { @@ -215,6 +236,7 @@ const openUrl = (url: string) => } }); +/** Callback when a native notification is clicked/touched on the device. */ const handleNotificationClick = (event: NotificationEvent) => { const reactToNotificationClick = new Promise((resolve, reject) => { if (event.action) { @@ -238,5 +260,6 @@ const handleNotificationClick = (event: NotificationEvent) => { event.waitUntil(reactToNotificationClick); }; +// ServiceWorker event listeners self.addEventListener('push', handlePush); self.addEventListener('notificationclick', handleNotificationClick); From b8cfb567d1bb48388c342cc303fb5003adfe1027 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 26 May 2022 15:16:54 -0400 Subject: [PATCH 052/102] ServiceWorker: alphabetize type definitions --- app/soapbox/service_worker/web_push_notifications.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/app/soapbox/service_worker/web_push_notifications.ts b/app/soapbox/service_worker/web_push_notifications.ts index f60c21f54..099bf8f45 100644 --- a/app/soapbox/service_worker/web_push_notifications.ts +++ b/app/soapbox/service_worker/web_push_notifications.ts @@ -21,28 +21,28 @@ declare const self: ServiceWorkerGlobalScope; /** Soapbox notification data from push event. */ interface NotificationData { access_token?: string, - preferred_locale: string, + count?: number, hiddenBody?: string, hiddenImage?: string, id?: string, + preferred_locale: string, url: string, - count?: number, } /** ServiceWorker Notification options with extra fields. */ interface ExtendedNotificationOptions extends NotificationOptions { - title: string, data: NotificationData, + title: string, } /** Partial clone of ServiceWorker Notification with mutability. */ interface ClonedNotification { - body?: string, - image?: string, actions?: NotificationAction[], + body?: string, data: NotificationData, - title: string, + image?: string, tag?: string, + title: string, } /** Status entitiy from the API (kind of). */ From 03e97edff79adea1a704575593ecfb0dc1a1790f Mon Sep 17 00:00:00 2001 From: Justin Date: Thu, 26 May 2022 14:38:24 -0400 Subject: [PATCH 053/102] Improve Quote Status visuals --- app/soapbox/features/compose/components/compose_form.js | 4 +++- app/soapbox/features/status/components/quoted_status.tsx | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/app/soapbox/features/compose/components/compose_form.js b/app/soapbox/features/compose/components/compose_form.js index 713b5d087..b59c61da0 100644 --- a/app/soapbox/features/compose/components/compose_form.js +++ b/app/soapbox/features/compose/components/compose_form.js @@ -365,7 +365,9 @@ class ComposeForm extends ImmutablePureComponent { } - +
+ +
{ From 5c549a46e5ef45e5c15043f3cd5947c8ea4092a1 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 26 May 2022 17:21:38 -0400 Subject: [PATCH 054/102] ServiceWorker: add missing jsdoc comment to expandNotification --- app/soapbox/service_worker/web_push_notifications.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/app/soapbox/service_worker/web_push_notifications.ts b/app/soapbox/service_worker/web_push_notifications.ts index 099bf8f45..9939f88a2 100644 --- a/app/soapbox/service_worker/web_push_notifications.ts +++ b/app/soapbox/service_worker/web_push_notifications.ts @@ -206,6 +206,7 @@ const findBestClient = (clients: readonly WindowClient[]): WindowClient => { return focusedClient || visibleClient || clients[0]; }; +/** Update a notification with CW to display the full status. */ const expandNotification = (notification: Notification) => { const newNotification = cloneNotification(notification); From 4e7256698945997f878d6119398c7a6399de7a98 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 26 May 2022 17:29:44 -0400 Subject: [PATCH 055/102] Jest: fix ServiceWorker filename in collectCoverageFrom --- jest.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jest.config.js b/jest.config.js index c2c762be2..8796e5595 100644 --- a/jest.config.js +++ b/jest.config.js @@ -22,7 +22,7 @@ module.exports = { 'app/soapbox/**/*.tsx', '!app/soapbox/features/emoji/emoji_compressed.js', '!app/soapbox/locales/locale-data/*.js', - '!app/soapbox/service_worker/entry.js', + '!app/soapbox/service_worker/entry.ts', '!app/soapbox/jest/test-setup.ts', '!app/soapbox/jest/test-helpers.ts', ], From 9007e237a02df1470ba6704e31b76b78520b716d Mon Sep 17 00:00:00 2001 From: Justin Date: Fri, 27 May 2022 09:24:54 -0400 Subject: [PATCH 056/102] Add coverage definition --- .gitlab-ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index f8b732738..44578e04e 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -46,6 +46,7 @@ lint-sass: jest: stage: test script: yarn test:coverage + coverage: /All files[^|]*\|[^|]*\s+([\d\.]+)/ only: changes: - "**/*.js" From 05bb3fe7a0c769768b9d2630e2fe047c0e027c6b Mon Sep 17 00:00:00 2001 From: Justin Date: Fri, 27 May 2022 09:30:51 -0400 Subject: [PATCH 057/102] Add covertura report location --- .gitlab-ci.yml | 7 ++++++- jest.config.js | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 44578e04e..ca7e19793 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -46,7 +46,6 @@ lint-sass: jest: stage: test script: yarn test:coverage - coverage: /All files[^|]*\|[^|]*\s+([\d\.]+)/ only: changes: - "**/*.js" @@ -57,6 +56,12 @@ jest: - "jest.config.js" - "package.json" - "yarn.lock" + coverage: /All files[^|]*\|[^|]*\s+([\d\.]+)/ + artifacts: + reports: + coverage_report: + coverage_format: cobertura + path: .coverage/cobertura-coverage.xml nginx-test: stage: test diff --git a/jest.config.js b/jest.config.js index 8796e5595..aa0c7445c 100644 --- a/jest.config.js +++ b/jest.config.js @@ -27,7 +27,7 @@ module.exports = { '!app/soapbox/jest/test-helpers.ts', ], 'coverageDirectory': '/.coverage/', - 'coverageReporters': ['html', 'text', 'text-summary', 'cobertura'], + 'coverageReporters': ['cobertura'], 'moduleDirectories': [ '/node_modules', '/app', From 712e3ecb2e4ae2638757f8fe847f1bec3fee2c3e Mon Sep 17 00:00:00 2001 From: Justin Date: Fri, 27 May 2022 10:15:32 -0400 Subject: [PATCH 058/102] Remove artifacts --- .gitlab-ci.yml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index ca7e19793..365de6d78 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -57,11 +57,6 @@ jest: - "package.json" - "yarn.lock" coverage: /All files[^|]*\|[^|]*\s+([\d\.]+)/ - artifacts: - reports: - coverage_report: - coverage_format: cobertura - path: .coverage/cobertura-coverage.xml nginx-test: stage: test From 3bae33ce6e8f060185015e7f508f4e936a1782ae Mon Sep 17 00:00:00 2001 From: Justin Date: Fri, 27 May 2022 10:23:59 -0400 Subject: [PATCH 059/102] Add back other reporters --- jest.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jest.config.js b/jest.config.js index aa0c7445c..8796e5595 100644 --- a/jest.config.js +++ b/jest.config.js @@ -27,7 +27,7 @@ module.exports = { '!app/soapbox/jest/test-helpers.ts', ], 'coverageDirectory': '/.coverage/', - 'coverageReporters': ['cobertura'], + 'coverageReporters': ['html', 'text', 'text-summary', 'cobertura'], 'moduleDirectories': [ '/node_modules', '/app', 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 060/102] 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 061/102] 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 062/102] 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 063/102] 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 064/102] 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 065/102] 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 066/102] 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 067/102] 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}

From 1e74c6d3d758b1734c9b6298f7cf88d8062e8961 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Sat, 28 May 2022 18:02:04 +0200 Subject: [PATCH 068/102] TypeScript, FC, styles and fixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- app/soapbox/actions/remote_timeline.js | 4 +- app/soapbox/components/avatar_overlay.js | 24 ---- app/soapbox/components/avatar_overlay.tsx | 19 +++ app/soapbox/components/icon_button.js | 6 +- app/soapbox/components/load_gap.js | 35 ----- app/soapbox/components/load_gap.tsx | 28 ++++ app/soapbox/components/pull-to-refresh.tsx | 4 +- app/soapbox/components/pullable.js | 30 ---- app/soapbox/components/pullable.tsx | 24 ++++ app/soapbox/components/radio_button.js | 35 ----- app/soapbox/components/radio_button.tsx | 28 ++++ app/soapbox/components/site-logo.tsx | 8 +- .../components/status-reply-mentions.tsx | 75 ++++++++++ app/soapbox/components/status.tsx | 11 +- .../components/status_reply_mentions.js | 89 ------------ app/soapbox/components/ui/widget/widget.tsx | 6 +- .../features/auth_token_list/index.tsx | 3 +- .../features/compose/components/poll_form.js | 5 +- .../components/instance_restrictions.js | 107 ++++++-------- .../follow_recommendations_list.tsx | 3 +- app/soapbox/features/list_timeline/index.tsx | 3 +- app/soapbox/features/lists/index.tsx | 3 +- .../components/pinned_hosts_picker.tsx | 16 ++- .../features/remote_timeline/index.tsx | 19 +-- .../components/scheduled_status.tsx | 2 +- .../features/scheduled_statuses/index.tsx | 3 +- .../components/site-preview.tsx | 4 +- .../status/components/detailed-status.tsx | 10 +- app/soapbox/features/status/index.tsx | 10 +- .../features/ui/components/component_modal.js | 26 ---- .../ui/components/component_modal.tsx | 19 +++ .../ui/components/edit_federation_modal.tsx | 125 ++++++++++------ .../ui/components/favourites_modal.js | 76 ---------- .../ui/components/favourites_modal.tsx | 62 ++++++++ .../ui/components/instance_info_panel.tsx | 32 ++--- .../components/instance_moderation_panel.tsx | 25 ++-- .../features/ui/components/mentions_modal.js | 83 ----------- .../features/ui/components/mentions_modal.tsx | 64 +++++++++ .../{modal_loading.js => modal_loading.tsx} | 0 .../features/ui/components/pending_status.js | 95 ------------ .../features/ui/components/pending_status.tsx | 97 +++++++++++++ .../ui/components/pinned_accounts_panel.js | 79 ---------- .../ui/components/pinned_accounts_panel.tsx | 50 +++++++ .../components/profile_familiar_followers.tsx | 3 +- .../ui/components/reactions_modal.tsx | 13 +- .../features/ui/components/reblogs_modal.js | 95 ------------ .../features/ui/components/reblogs_modal.tsx | 64 +++++++++ .../features/ui/components/user_panel.js | 136 ------------------ .../features/ui/components/user_panel.tsx | 119 +++++++++++++++ ...s_builder.js => pending_status_builder.ts} | 18 +-- app/styles/components/columns.scss | 17 --- app/styles/components/dropdown-menu.scss | 2 +- app/styles/polls.scss | 16 --- 53 files changed, 875 insertions(+), 1055 deletions(-) delete mode 100644 app/soapbox/components/avatar_overlay.js create mode 100644 app/soapbox/components/avatar_overlay.tsx delete mode 100644 app/soapbox/components/load_gap.js create mode 100644 app/soapbox/components/load_gap.tsx delete mode 100644 app/soapbox/components/pullable.js create mode 100644 app/soapbox/components/pullable.tsx delete mode 100644 app/soapbox/components/radio_button.js create mode 100644 app/soapbox/components/radio_button.tsx create mode 100644 app/soapbox/components/status-reply-mentions.tsx delete mode 100644 app/soapbox/components/status_reply_mentions.js delete mode 100644 app/soapbox/features/ui/components/component_modal.js create mode 100644 app/soapbox/features/ui/components/component_modal.tsx delete mode 100644 app/soapbox/features/ui/components/favourites_modal.js create mode 100644 app/soapbox/features/ui/components/favourites_modal.tsx delete mode 100644 app/soapbox/features/ui/components/mentions_modal.js create mode 100644 app/soapbox/features/ui/components/mentions_modal.tsx rename app/soapbox/features/ui/components/{modal_loading.js => modal_loading.tsx} (100%) delete mode 100644 app/soapbox/features/ui/components/pending_status.js create mode 100644 app/soapbox/features/ui/components/pending_status.tsx delete mode 100644 app/soapbox/features/ui/components/pinned_accounts_panel.js create mode 100644 app/soapbox/features/ui/components/pinned_accounts_panel.tsx delete mode 100644 app/soapbox/features/ui/components/reblogs_modal.js create mode 100644 app/soapbox/features/ui/components/reblogs_modal.tsx delete mode 100644 app/soapbox/features/ui/components/user_panel.js create mode 100644 app/soapbox/features/ui/components/user_panel.tsx rename app/soapbox/features/ui/util/{pending_status_builder.js => pending_status_builder.ts} (66%) diff --git a/app/soapbox/actions/remote_timeline.js b/app/soapbox/actions/remote_timeline.js index 38249c009..8238c3326 100644 --- a/app/soapbox/actions/remote_timeline.js +++ b/app/soapbox/actions/remote_timeline.js @@ -10,7 +10,7 @@ export function pinHost(host) { const state = getState(); const pinnedHosts = getPinnedHosts(state); - return dispatch(changeSetting(['remote_timeline', 'pinnedHosts'], pinnedHosts.add(host))); + return dispatch(changeSetting(['remote_timeline', 'pinnedHosts'], pinnedHosts.push(host))); }; } @@ -19,6 +19,6 @@ export function unpinHost(host) { const state = getState(); const pinnedHosts = getPinnedHosts(state); - return dispatch(changeSetting(['remote_timeline', 'pinnedHosts'], pinnedHosts.delete(host))); + return dispatch(changeSetting(['remote_timeline', 'pinnedHosts'], pinnedHosts.filter((value) => value !== host))); }; } diff --git a/app/soapbox/components/avatar_overlay.js b/app/soapbox/components/avatar_overlay.js deleted file mode 100644 index ce9784c95..000000000 --- a/app/soapbox/components/avatar_overlay.js +++ /dev/null @@ -1,24 +0,0 @@ -import React from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; - -import StillImage from 'soapbox/components/still_image'; - -export default class AvatarOverlay extends React.PureComponent { - - static propTypes = { - account: ImmutablePropTypes.record.isRequired, - friend: ImmutablePropTypes.map.isRequired, - }; - - render() { - const { account, friend } = this.props; - - return ( -
- - -
- ); - } - -} diff --git a/app/soapbox/components/avatar_overlay.tsx b/app/soapbox/components/avatar_overlay.tsx new file mode 100644 index 000000000..ae38b5e4c --- /dev/null +++ b/app/soapbox/components/avatar_overlay.tsx @@ -0,0 +1,19 @@ +import React from 'react'; + +import StillImage from 'soapbox/components/still_image'; + +import type { Account as AccountEntity } from 'soapbox/types/entities'; + +interface IAvatarOverlay { + account: AccountEntity, + friend: AccountEntity, +} + +const AvatarOverlay: React.FC = ({ account, friend }) => ( +
+ + +
+); + +export default AvatarOverlay; diff --git a/app/soapbox/components/icon_button.js b/app/soapbox/components/icon_button.js index fa5ec1782..b6ed8b3d4 100644 --- a/app/soapbox/components/icon_button.js +++ b/app/soapbox/components/icon_button.js @@ -12,6 +12,7 @@ export default class IconButton extends React.PureComponent { static propTypes = { className: PropTypes.string, + iconClassName: PropTypes.string, title: PropTypes.string.isRequired, icon: PropTypes.string, src: PropTypes.string, @@ -99,6 +100,7 @@ export default class IconButton extends React.PureComponent { active, animate, className, + iconClassName, disabled, expanded, icon, @@ -144,7 +146,7 @@ export default class IconButton extends React.PureComponent {
{emoji ? {text && {text}} @@ -174,7 +176,7 @@ export default class IconButton extends React.PureComponent {
{emoji ? {text && {text}} diff --git a/app/soapbox/components/load_gap.js b/app/soapbox/components/load_gap.js deleted file mode 100644 index 84eeec000..000000000 --- a/app/soapbox/components/load_gap.js +++ /dev/null @@ -1,35 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import { injectIntl, defineMessages } from 'react-intl'; - -import Icon from 'soapbox/components/icon'; - -const messages = defineMessages({ - load_more: { id: 'status.load_more', defaultMessage: 'Load more' }, -}); - -export default @injectIntl -class LoadGap extends React.PureComponent { - - static propTypes = { - disabled: PropTypes.bool, - maxId: PropTypes.string, - onClick: PropTypes.func.isRequired, - intl: PropTypes.object.isRequired, - }; - - handleClick = () => { - this.props.onClick(this.props.maxId); - } - - render() { - const { disabled, intl } = this.props; - - return ( - - ); - } - -} diff --git a/app/soapbox/components/load_gap.tsx b/app/soapbox/components/load_gap.tsx new file mode 100644 index 000000000..b784c871d --- /dev/null +++ b/app/soapbox/components/load_gap.tsx @@ -0,0 +1,28 @@ +import React from 'react'; +import { defineMessages, useIntl } from 'react-intl'; + +import Icon from 'soapbox/components/icon'; + +const messages = defineMessages({ + load_more: { id: 'status.load_more', defaultMessage: 'Load more' }, +}); + +interface ILoadGap { + disabled?: boolean, + maxId: string, + onClick: (id: string) => void, +} + +const LoadGap: React.FC = ({ disabled, maxId, onClick }) => { + const intl = useIntl(); + + const handleClick = () => onClick(maxId); + + return ( + + ); +}; + +export default LoadGap; diff --git a/app/soapbox/components/pull-to-refresh.tsx b/app/soapbox/components/pull-to-refresh.tsx index 6ef199f3c..7596fabea 100644 --- a/app/soapbox/components/pull-to-refresh.tsx +++ b/app/soapbox/components/pull-to-refresh.tsx @@ -4,7 +4,9 @@ import PTRComponent from 'react-simple-pull-to-refresh'; import { Spinner } from 'soapbox/components/ui'; interface IPullToRefresh { - onRefresh?: () => Promise + onRefresh?: () => Promise; + refreshingContent?: JSX.Element | string; + pullingContent?: JSX.Element | string; } /** diff --git a/app/soapbox/components/pullable.js b/app/soapbox/components/pullable.js deleted file mode 100644 index 0f7546a9c..000000000 --- a/app/soapbox/components/pullable.js +++ /dev/null @@ -1,30 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; - -import PullToRefresh from './pull-to-refresh'; - -/** - * Pullable: - * Basic "pull to refresh" without the refresh. - * Just visual feedback. - */ -export default class Pullable extends React.Component { - - static propTypes = { - children: PropTypes.node.isRequired, - } - - render() { - const { children } = this.props; - - return ( - - {children} - - ); - } - -} diff --git a/app/soapbox/components/pullable.tsx b/app/soapbox/components/pullable.tsx new file mode 100644 index 000000000..0304a1d46 --- /dev/null +++ b/app/soapbox/components/pullable.tsx @@ -0,0 +1,24 @@ +import React from 'react'; + +import PullToRefresh from './pull-to-refresh'; + +interface IPullable { + children: JSX.Element, +} + +/** + * Pullable: + * Basic "pull to refresh" without the refresh. + * Just visual feedback. + */ +const Pullable: React.FC = ({ children }) =>( + + {children} + +); + +export default Pullable; diff --git a/app/soapbox/components/radio_button.js b/app/soapbox/components/radio_button.js deleted file mode 100644 index 0f82af95f..000000000 --- a/app/soapbox/components/radio_button.js +++ /dev/null @@ -1,35 +0,0 @@ -import classNames from 'classnames'; -import PropTypes from 'prop-types'; -import React from 'react'; - -export default class RadioButton extends React.PureComponent { - - static propTypes = { - value: PropTypes.string.isRequired, - checked: PropTypes.bool, - name: PropTypes.string.isRequired, - onChange: PropTypes.func.isRequired, - label: PropTypes.node.isRequired, - }; - - render() { - const { name, value, checked, onChange, label } = this.props; - - return ( - - ); - } - -} \ No newline at end of file diff --git a/app/soapbox/components/radio_button.tsx b/app/soapbox/components/radio_button.tsx new file mode 100644 index 000000000..c3f87ce02 --- /dev/null +++ b/app/soapbox/components/radio_button.tsx @@ -0,0 +1,28 @@ +import classNames from 'classnames'; +import React from 'react'; + +interface IRadioButton { + value: string, + checked?: boolean, + name: string, + onChange: React.ChangeEventHandler, + label: JSX.Element, +} + +const RadioButton: React.FC = ({ name, value, checked, onChange, label }) => ( + +); + +export default RadioButton; diff --git a/app/soapbox/components/site-logo.tsx b/app/soapbox/components/site-logo.tsx index 81f9ed417..01bfc54ed 100644 --- a/app/soapbox/components/site-logo.tsx +++ b/app/soapbox/components/site-logo.tsx @@ -6,16 +6,20 @@ import { useSoapboxConfig, useSettings, useSystemTheme } from 'soapbox/hooks'; interface ISiteLogo extends React.ComponentProps<'img'> { /** Extra class names for the element. */ className?: string, + /** Override theme setting for */ + theme?: 'dark' | 'light', } /** Display the most appropriate site logo based on the theme and configuration. */ -const SiteLogo: React.FC = ({ className, ...rest }) => { +const SiteLogo: React.FC = ({ className, theme, ...rest }) => { const { logo, logoDarkMode } = useSoapboxConfig(); const settings = useSettings(); const systemTheme = useSystemTheme(); const userTheme = settings.get('themeMode'); - const darkMode = userTheme === 'dark' || (userTheme === 'system' && systemTheme === 'dark'); + const darkMode = theme + ? theme === 'dark' + : (userTheme === 'dark' || (userTheme === 'system' && systemTheme === 'dark')); /** Soapbox logo. */ const soapboxLogo = darkMode diff --git a/app/soapbox/components/status-reply-mentions.tsx b/app/soapbox/components/status-reply-mentions.tsx new file mode 100644 index 000000000..955924acc --- /dev/null +++ b/app/soapbox/components/status-reply-mentions.tsx @@ -0,0 +1,75 @@ +import { List as ImmutableList } from 'immutable'; +import React from 'react'; +import { FormattedList, FormattedMessage } from 'react-intl'; +import { Link } from 'react-router-dom'; + +import { openModal } from 'soapbox/actions/modals'; +import HoverRefWrapper from 'soapbox/components/hover_ref_wrapper'; +import { useAppDispatch } from 'soapbox/hooks'; + +import type { Status } from 'soapbox/types/entities'; + +interface IStatusReplyMentions { + status: Status, +} + +const StatusReplyMentions: React.FC = ({ status }) => { + const dispatch = useAppDispatch(); + + const handleOpenMentionsModal: React.MouseEventHandler = (e) => { + e.stopPropagation(); + + dispatch(openModal('MENTIONS', { + username: status.getIn(['account', 'acct']), + statusId: status.get('id'), + })); + }; + + if (!status.get('in_reply_to_id')) { + return null; + } + + const to = status.get('mentions', ImmutableList()); + + // The post is a reply, but it has no mentions. + // Rare, but it can happen. + if (to.size === 0) { + return ( +
+ +
+ ); + } + + // The typical case with a reply-to and a list of mentions. + const accounts = to.slice(0, 2).map(account => ( + + @{account.get('username')} + + )).toArray(); + + if (to.size > 2) { + accounts.push( + + + , + ); + } + + return ( +
+ , + }} + /> +
+ ); +}; + +export default StatusReplyMentions; diff --git a/app/soapbox/components/status.tsx b/app/soapbox/components/status.tsx index 141394420..3cea7c280 100644 --- a/app/soapbox/components/status.tsx +++ b/app/soapbox/components/status.tsx @@ -6,18 +6,17 @@ import { injectIntl, FormattedMessage, IntlShape, defineMessages } from 'react-i import { NavLink, withRouter, RouteComponentProps } from 'react-router-dom'; import Icon from 'soapbox/components/icon'; +import AccountContainer from 'soapbox/containers/account_container'; import PlaceholderCard from 'soapbox/features/placeholder/components/placeholder_card'; +import Card from 'soapbox/features/status/components/card'; import QuotedStatus from 'soapbox/features/status/containers/quoted_status_container'; - -import AccountContainer from '../containers/account_container'; -import Card from '../features/status/components/card'; -import Bundle from '../features/ui/components/bundle'; -import { MediaGallery, Video, Audio } from '../features/ui/util/async-components'; +import Bundle from 'soapbox/features/ui/components/bundle'; +import { MediaGallery, Video, Audio } from 'soapbox/features/ui/util/async-components'; import AttachmentThumbs from './attachment-thumbs'; +import StatusReplyMentions from './status-reply-mentions'; import StatusActionBar from './status_action_bar'; import StatusContent from './status_content'; -import StatusReplyMentions from './status_reply_mentions'; import { HStack, Text } from './ui'; import type { History } from 'history'; diff --git a/app/soapbox/components/status_reply_mentions.js b/app/soapbox/components/status_reply_mentions.js deleted file mode 100644 index 0809d6085..000000000 --- a/app/soapbox/components/status_reply_mentions.js +++ /dev/null @@ -1,89 +0,0 @@ -import { List as ImmutableList } from 'immutable'; -import PropTypes from 'prop-types'; -import React from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import ImmutablePureComponent from 'react-immutable-pure-component'; -import { FormattedList, FormattedMessage, injectIntl } from 'react-intl'; -import { connect } from 'react-redux'; -import { Link } from 'react-router-dom'; - -import { openModal } from 'soapbox/actions/modals'; -import HoverRefWrapper from 'soapbox/components/hover_ref_wrapper'; - -const mapDispatchToProps = (dispatch) => ({ - onOpenMentionsModal(username, statusId) { - dispatch(openModal('MENTIONS', { - username, - statusId, - })); - }, -}); - -export default @connect(null, mapDispatchToProps) -@injectIntl -class StatusReplyMentions extends ImmutablePureComponent { - - static propTypes = { - status: ImmutablePropTypes.record.isRequired, - onOpenMentionsModal: PropTypes.func, - } - - handleOpenMentionsModal = (e) => { - const { status, onOpenMentionsModal } = this.props; - - e.stopPropagation(); - - onOpenMentionsModal(status.getIn(['account', 'acct']), status.get('id')); - } - - render() { - const { status } = this.props; - - if (!status.get('in_reply_to_id')) { - return null; - } - - const to = status.get('mentions', ImmutableList()); - - // The post is a reply, but it has no mentions. - // Rare, but it can happen. - if (to.size === 0) { - return ( -
- -
- ); - } - - // The typical case with a reply-to and a list of mentions. - const accounts = to.slice(0, 2).map(account => ( - - @{account.get('username')} - - )).toArray(); - - if (to.size > 2) { - accounts.push( - - - , - ); - } - - return ( -
- , - }} - /> -
- ); - } - -} diff --git a/app/soapbox/components/ui/widget/widget.tsx b/app/soapbox/components/ui/widget/widget.tsx index 5a3b887df..dee12198d 100644 --- a/app/soapbox/components/ui/widget/widget.tsx +++ b/app/soapbox/components/ui/widget/widget.tsx @@ -28,6 +28,7 @@ interface IWidget { actionIcon?: string, /** Text for the action. */ actionTitle?: string, + action?: JSX.Element, } /** Sidebar widget. */ @@ -37,19 +38,20 @@ const Widget: React.FC = ({ onActionClick, actionIcon = require('@tabler/icons/icons/arrow-right.svg'), actionTitle, + action, }): JSX.Element => { return ( - {onActionClick && ( + {action || (onActionClick && ( - )} + ))} {children} diff --git a/app/soapbox/features/auth_token_list/index.tsx b/app/soapbox/features/auth_token_list/index.tsx index 4563e4b34..458d8448b 100644 --- a/app/soapbox/features/auth_token_list/index.tsx +++ b/app/soapbox/features/auth_token_list/index.tsx @@ -1,5 +1,4 @@ -import React from 'react'; -import { useEffect } from 'react'; +import React, { useEffect } from 'react'; import { defineMessages, FormattedDate, useIntl } from 'react-intl'; import { fetchOAuthTokens, revokeOAuthTokenById } from 'soapbox/actions/security'; diff --git a/app/soapbox/features/compose/components/poll_form.js b/app/soapbox/features/compose/components/poll_form.js index d566b3055..41b51ca5e 100644 --- a/app/soapbox/features/compose/components/poll_form.js +++ b/app/soapbox/features/compose/components/poll_form.js @@ -11,6 +11,7 @@ import { connect } from 'react-redux'; import AutosuggestInput from 'soapbox/components/autosuggest_input'; import Icon from 'soapbox/components/icon'; import IconButton from 'soapbox/components/icon_button'; +import { HStack } from 'soapbox/components/ui'; const messages = defineMessages({ option_placeholder: { id: 'compose_form.poll.option_placeholder', defaultMessage: 'Choice {number}' }, @@ -177,7 +178,7 @@ class PollForm extends ImmutablePureComponent { ))} -
+ {options.size < maxOptions && ( )} @@ -191,7 +192,7 @@ class PollForm extends ImmutablePureComponent { -
+
); } diff --git a/app/soapbox/features/federation_restrictions/components/instance_restrictions.js b/app/soapbox/features/federation_restrictions/components/instance_restrictions.js index f4f1145f4..5539b845b 100644 --- a/app/soapbox/features/federation_restrictions/components/instance_restrictions.js +++ b/app/soapbox/features/federation_restrictions/components/instance_restrictions.js @@ -8,6 +8,7 @@ import { FormattedMessage } from 'react-intl'; import { connect } from 'react-redux'; import Icon from 'soapbox/components/icon'; +import { Text } from 'soapbox/components/ui'; const hasRestrictions = remoteInstance => { return remoteInstance @@ -49,77 +50,57 @@ class InstanceRestrictions extends ImmutablePureComponent { if (followers_only) { items.push(( -
-
- -
-
- -
-
+ + + + )); } else if (federated_timeline_removal) { items.push(( -
-
- -
-
- -
-
+ + + + )); } if (fullMediaRemoval) { items.push(( -
-
- -
-
- -
-
+ + + + )); } else if (partialMediaRemoval) { items.push(( -
-
- -
-
- -
-
+ + + + )); } if (!fullMediaRemoval && media_nsfw) { items.push(( -
-
- -
-
- -
-
+ + + + )); } @@ -135,38 +116,38 @@ class InstanceRestrictions extends ImmutablePureComponent { if (remoteInstance.getIn(['federation', 'reject']) === true) { return ( -
- + + -
+ ); } else if (hasRestrictions(remoteInstance)) { return [ ( -
+ -
+ ), this.renderRestrictions(), ]; } else { return ( -
- + + -
+ ); } } diff --git a/app/soapbox/features/follow_recommendations/components/follow_recommendations_list.tsx b/app/soapbox/features/follow_recommendations/components/follow_recommendations_list.tsx index fc5ab38d0..841f5e7d0 100644 --- a/app/soapbox/features/follow_recommendations/components/follow_recommendations_list.tsx +++ b/app/soapbox/features/follow_recommendations/components/follow_recommendations_list.tsx @@ -1,5 +1,4 @@ -import React from 'react'; -import { useEffect } from 'react'; +import React, { useEffect } from 'react'; import { FormattedMessage } from 'react-intl'; import { useDispatch } from 'react-redux'; diff --git a/app/soapbox/features/list_timeline/index.tsx b/app/soapbox/features/list_timeline/index.tsx index 2697c201c..85441c98f 100644 --- a/app/soapbox/features/list_timeline/index.tsx +++ b/app/soapbox/features/list_timeline/index.tsx @@ -1,5 +1,4 @@ -import React from 'react'; -import { useEffect } from 'react'; +import React, { useEffect } from 'react'; import { FormattedMessage } from 'react-intl'; import { useDispatch } from 'react-redux'; import { useParams } from 'react-router-dom'; diff --git a/app/soapbox/features/lists/index.tsx b/app/soapbox/features/lists/index.tsx index a25534910..82f4b48a9 100644 --- a/app/soapbox/features/lists/index.tsx +++ b/app/soapbox/features/lists/index.tsx @@ -1,5 +1,4 @@ -import React from 'react'; -import { useEffect } from 'react'; +import React, { useEffect } from 'react'; import { defineMessages, useIntl, FormattedMessage } from 'react-intl'; import { useDispatch } from 'react-redux'; import { Link } from 'react-router-dom'; diff --git a/app/soapbox/features/remote_timeline/components/pinned_hosts_picker.tsx b/app/soapbox/features/remote_timeline/components/pinned_hosts_picker.tsx index f1c1ac425..aa7940761 100644 --- a/app/soapbox/features/remote_timeline/components/pinned_hosts_picker.tsx +++ b/app/soapbox/features/remote_timeline/components/pinned_hosts_picker.tsx @@ -4,6 +4,7 @@ import classNames from 'classnames'; import React from 'react'; import { Link } from 'react-router-dom'; +import { Button, HStack } from 'soapbox/components/ui'; import { useSettings } from 'soapbox/hooks'; interface IPinnedHostsPicker { @@ -18,13 +19,18 @@ const PinnedHostsPicker: React.FC = ({ host: activeHost }) = if (!pinnedHosts || pinnedHosts.isEmpty()) return null; return ( -
+ {pinnedHosts.map((host: any) => ( -
- {host} -
+ ))} -
+ ); }; diff --git a/app/soapbox/features/remote_timeline/index.tsx b/app/soapbox/features/remote_timeline/index.tsx index c429549f5..6f8fe9477 100644 --- a/app/soapbox/features/remote_timeline/index.tsx +++ b/app/soapbox/features/remote_timeline/index.tsx @@ -3,6 +3,7 @@ import { defineMessages, useIntl, FormattedMessage } from 'react-intl'; import { useHistory } from 'react-router-dom'; import IconButton from 'soapbox/components/icon_button'; +import { HStack, Text } from 'soapbox/components/ui'; import Column from 'soapbox/features/ui/components/column'; import { useAppDispatch, useSettings } from 'soapbox/hooks'; @@ -66,14 +67,16 @@ const RemoteTimeline: React.FC = ({ params }) => { return ( {instance && } - {!pinned &&
- - -
} + {!pinned && + + + + + } = ({ soapbox }) => { 'bg-slate-800': dark, })} > - Logo + + {/* Logo */}
); diff --git a/app/soapbox/features/status/components/detailed-status.tsx b/app/soapbox/features/status/components/detailed-status.tsx index 5397c2e4c..093e0a896 100644 --- a/app/soapbox/features/status/components/detailed-status.tsx +++ b/app/soapbox/features/status/components/detailed-status.tsx @@ -5,13 +5,13 @@ import { FormattedMessage, injectIntl, WrappedComponentProps as IntlProps } from import { FormattedDate } from 'react-intl'; import Icon from 'soapbox/components/icon'; +import MediaGallery from 'soapbox/components/media_gallery'; +import StatusReplyMentions from 'soapbox/components/status-reply-mentions'; +import StatusContent from 'soapbox/components/status_content'; +import { HStack, Text } from 'soapbox/components/ui'; +import AccountContainer from 'soapbox/containers/account_container'; import QuotedStatus from 'soapbox/features/status/containers/quoted_status_container'; -import MediaGallery from '../../../components/media_gallery'; -import StatusContent from '../../../components/status_content'; -import StatusReplyMentions from '../../../components/status_reply_mentions'; -import { HStack, Text } from '../../../components/ui'; -import AccountContainer from '../../../containers/account_container'; import Audio from '../../audio'; import scheduleIdleTask from '../../ui/util/schedule_idle_task'; import Video from '../../video'; diff --git a/app/soapbox/features/status/index.tsx b/app/soapbox/features/status/index.tsx index ed762b7b5..75ef1f7f9 100644 --- a/app/soapbox/features/status/index.tsx +++ b/app/soapbox/features/status/index.tsx @@ -583,7 +583,7 @@ class Status extends ImmutablePureComponent { } renderPendingStatus(id: string) { - const { status } = this.props; + // const { status } = this.props; const idempotencyKey = id.replace(/^末pending-/, ''); return ( @@ -591,10 +591,10 @@ class Status extends ImmutablePureComponent { className='thread__status' key={id} idempotencyKey={idempotencyKey} - focusedStatusId={status.id} - onMoveUp={this.handleMoveUp} - onMoveDown={this.handleMoveDown} - contextType='thread' + // focusedStatusId={status.id} + // onMoveUp={this.handleMoveUp} + // onMoveDown={this.handleMoveDown} + // contextType='thread' /> ); } diff --git a/app/soapbox/features/ui/components/component_modal.js b/app/soapbox/features/ui/components/component_modal.js deleted file mode 100644 index 51c23a8e1..000000000 --- a/app/soapbox/features/ui/components/component_modal.js +++ /dev/null @@ -1,26 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; - -export default class ComponentModal extends React.PureComponent { - - static propTypes = { - onClose: PropTypes.func.isRequired, - component: PropTypes.oneOfType([PropTypes.func, PropTypes.object]).isRequired, - componentProps: PropTypes.object, - } - - static defaultProps = { - componentProps: {}, - } - - render() { - const { onClose, component: Component, componentProps } = this.props; - - return ( -
- -
- ); - } - -} diff --git a/app/soapbox/features/ui/components/component_modal.tsx b/app/soapbox/features/ui/components/component_modal.tsx new file mode 100644 index 000000000..b4daa41e7 --- /dev/null +++ b/app/soapbox/features/ui/components/component_modal.tsx @@ -0,0 +1,19 @@ +import React from 'react'; + +import { Modal } from 'soapbox/components/ui'; + +interface IComponentModal { + onClose: (type?: string) => void, + component: React.ComponentType<{ + onClose: (type?: string) => void, + }>, + componentProps: Record, +} + +const ComponentModal: React.FC = ({ onClose, component: Component, componentProps = {} }) => ( + + + +); + +export default ComponentModal; diff --git a/app/soapbox/features/ui/components/edit_federation_modal.tsx b/app/soapbox/features/ui/components/edit_federation_modal.tsx index 3d4456a75..9dbde1f79 100644 --- a/app/soapbox/features/ui/components/edit_federation_modal.tsx +++ b/app/soapbox/features/ui/components/edit_federation_modal.tsx @@ -1,17 +1,18 @@ import { Map as ImmutableMap } from 'immutable'; import React, { useState, useEffect } from 'react'; -import { defineMessages, useIntl } from 'react-intl'; +import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; +import Toggle from 'react-toggle'; import { updateMrf } from 'soapbox/actions/mrf'; import snackbar from 'soapbox/actions/snackbar'; -import { SimpleForm, Checkbox } from 'soapbox/features/forms'; +import { HStack, Modal, Stack, Text } from 'soapbox/components/ui'; +import { SimpleForm } from 'soapbox/features/forms'; import { useAppSelector, useAppDispatch } from 'soapbox/hooks'; import { makeGetRemoteInstance } from 'soapbox/selectors'; const getRemoteInstance = makeGetRemoteInstance(); const messages = defineMessages({ - reject: { id: 'edit_federation.reject', defaultMessage: 'Reject all activities' }, mediaRemoval: { id: 'edit_federation.media_removal', defaultMessage: 'Strip media' }, forceNsfw: { id: 'edit_federation.force_nsfw', defaultMessage: 'Force attachments to be marked sensitive' }, unlisted: { id: 'edit_federation.unlisted', defaultMessage: 'Force posts unlisted' }, @@ -54,7 +55,7 @@ const EditFederationModal: React.FC = ({ host, onClose }) setData(newData); }; - const handleSubmit: React.FormEventHandler = () => { + const handleSubmit = () => { dispatch(updateMrf(host, data)) .then(() => dispatch(snackbar.success(intl.formatMessage(messages.success, { host })))) .catch(() => {}); @@ -75,47 +76,81 @@ const EditFederationModal: React.FC = ({ host, onClose }) const fullMediaRemoval = avatar_removal && banner_removal && media_removal; return ( -
-
-
- {host} -
- - - - - - - - -
-
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); }; diff --git a/app/soapbox/features/ui/components/favourites_modal.js b/app/soapbox/features/ui/components/favourites_modal.js deleted file mode 100644 index 34e6ceb34..000000000 --- a/app/soapbox/features/ui/components/favourites_modal.js +++ /dev/null @@ -1,76 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import { FormattedMessage } from 'react-intl'; -import { connect } from 'react-redux'; - -import { fetchFavourites } from 'soapbox/actions/interactions'; -import ScrollableList from 'soapbox/components/scrollable_list'; -import { Modal, Spinner } from 'soapbox/components/ui'; -import AccountContainer from 'soapbox/containers/account_container'; - -const mapStateToProps = (state, props) => { - return { - accountIds: state.getIn(['user_lists', 'favourited_by', props.statusId]), - }; -}; - -export default @connect(mapStateToProps) -class FavouritesModal extends React.PureComponent { - - static propTypes = { - onClose: PropTypes.func.isRequired, - statusId: PropTypes.string.isRequired, - username: PropTypes.string.isRequired, - dispatch: PropTypes.func.isRequired, - accountIds: ImmutablePropTypes.orderedSet, - }; - - fetchData = () => { - const { dispatch, statusId } = this.props; - - dispatch(fetchFavourites(statusId)); - } - - componentDidMount() { - this.fetchData(); - } - - onClickClose = () => { - this.props.onClose('FAVOURITES'); - }; - - render() { - const { accountIds } = this.props; - - let body; - - if (!accountIds) { - body = ; - } else { - const emptyMessage = ; - - body = ( - - {accountIds.map(id => - , - )} - - ); - } - - return ( - } - onClose={this.onClickClose} - > - {body} - - ); - } - -} diff --git a/app/soapbox/features/ui/components/favourites_modal.tsx b/app/soapbox/features/ui/components/favourites_modal.tsx new file mode 100644 index 000000000..f6089acf6 --- /dev/null +++ b/app/soapbox/features/ui/components/favourites_modal.tsx @@ -0,0 +1,62 @@ +import React, { useEffect } from 'react'; +import { FormattedMessage } from 'react-intl'; + +import { fetchFavourites } from 'soapbox/actions/interactions'; +import ScrollableList from 'soapbox/components/scrollable_list'; +import { Modal, Spinner } from 'soapbox/components/ui'; +import AccountContainer from 'soapbox/containers/account_container'; +import { useAppDispatch, useAppSelector } from 'soapbox/hooks'; + +interface IFavouritesModal { + onClose: (type: string) => void, + statusId: string, +} + +const FavouritesModal: React.FC = ({ onClose, statusId }) => { + const dispatch = useAppDispatch(); + + const accountIds = useAppSelector((state) => state.user_lists.getIn(['favourited_by', statusId])); + + const fetchData = () => { + dispatch(fetchFavourites(statusId)); + }; + + useEffect(() => { + fetchData(); + }, []); + + const onClickClose = () => { + onClose('FAVOURITES'); + }; + + let body; + + if (!accountIds) { + body = ; + } else { + const emptyMessage = ; + + body = ( + + {accountIds.map((id: string) => + , + )} + + ); + } + + return ( + } + onClose={onClickClose} + > + {body} + + ); +}; + +export default FavouritesModal; diff --git a/app/soapbox/features/ui/components/instance_info_panel.tsx b/app/soapbox/features/ui/components/instance_info_panel.tsx index a100abc2c..4c2462e10 100644 --- a/app/soapbox/features/ui/components/instance_info_panel.tsx +++ b/app/soapbox/features/ui/components/instance_info_panel.tsx @@ -4,7 +4,7 @@ import React from 'react'; import { useIntl, defineMessages } from 'react-intl'; import { pinHost, unpinHost } from 'soapbox/actions/remote_timeline'; -import DropdownMenu from 'soapbox/containers/dropdown_menu_container'; +import { Widget } from 'soapbox/components/ui'; import { useAppSelector, useAppDispatch, useSettings } from 'soapbox/hooks'; import { makeGetRemoteInstance } from 'soapbox/selectors'; @@ -29,7 +29,7 @@ const InstanceInfoPanel: React.FC = ({ host }) => { const remoteInstance: any = useAppSelector(state => getRemoteInstance(state, host)); const pinned: boolean = (settings.getIn(['remote_timeline', 'pinnedHosts']) as any).includes(host); - const handlePinHost: React.MouseEventHandler = () => { + const handlePinHost = () => { if (!pinned) { dispatch(pinHost(host)); } else { @@ -37,31 +37,15 @@ const InstanceInfoPanel: React.FC = ({ host }) => { } }; - const makeMenu = () => { - return [{ - text: intl.formatMessage(pinned ? messages.unpinHost : messages.pinHost, { host }), - action: handlePinHost, - icon: require(pinned ? '@tabler/icons/icons/pinned-off.svg' : '@tabler/icons/icons/pin.svg'), - }]; - }; - - const menu = makeMenu(); - const icon = pinned ? 'thumbtack' : 'globe-w'; - if (!remoteInstance) return null; return ( -
-
- - - {remoteInstance.get('host')} - -
- -
-
-
+ ); }; diff --git a/app/soapbox/features/ui/components/instance_moderation_panel.tsx b/app/soapbox/features/ui/components/instance_moderation_panel.tsx index 1a494df7e..ed97495f1 100644 --- a/app/soapbox/features/ui/components/instance_moderation_panel.tsx +++ b/app/soapbox/features/ui/components/instance_moderation_panel.tsx @@ -4,6 +4,7 @@ import React from 'react'; import { useIntl, defineMessages, FormattedMessage } from 'react-intl'; import { openModal } from 'soapbox/actions/modals'; +import { Widget } from 'soapbox/components/ui'; import DropdownMenu from 'soapbox/containers/dropdown_menu_container'; import InstanceRestrictions from 'soapbox/features/federation_restrictions/components/instance_restrictions'; import { useAppSelector, useAppDispatch, useOwnAccount } from 'soapbox/hooks'; @@ -43,22 +44,14 @@ const InstanceModerationPanel: React.FC = ({ host }) = const menu = makeMenu(); return ( -
-
- - - - - {account?.admin && ( -
- -
- )} -
-
- -
-
+ } + action={account?.admin ? ( + + ) : undefined} + > + + ); }; diff --git a/app/soapbox/features/ui/components/mentions_modal.js b/app/soapbox/features/ui/components/mentions_modal.js deleted file mode 100644 index 0f4c4626b..000000000 --- a/app/soapbox/features/ui/components/mentions_modal.js +++ /dev/null @@ -1,83 +0,0 @@ -import { OrderedSet as ImmutableOrderedSet } from 'immutable'; -import PropTypes from 'prop-types'; -import React from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import { injectIntl, FormattedMessage } from 'react-intl'; -import { connect } from 'react-redux'; - -import { fetchStatusWithContext } from 'soapbox/actions/statuses'; -import ScrollableList from 'soapbox/components/scrollable_list'; -import { Modal, Spinner } from 'soapbox/components/ui'; -import AccountContainer from 'soapbox/containers/account_container'; -import { makeGetStatus } from 'soapbox/selectors'; - -const mapStateToProps = (state, props) => { - const getStatus = makeGetStatus(); - const status = getStatus(state, { - id: props.statusId, - username: props.username, - }); - - return { - accountIds: status ? ImmutableOrderedSet(status.get('mentions').map(m => m.get('id'))) : null, - }; -}; - -export default @connect(mapStateToProps) -@injectIntl -class MentionsModal extends React.PureComponent { - - static propTypes = { - onClose: PropTypes.func.isRequired, - intl: PropTypes.object.isRequired, - statusId: PropTypes.string.isRequired, - username: PropTypes.string.isRequired, - dispatch: PropTypes.func.isRequired, - accountIds: ImmutablePropTypes.orderedSet, - }; - - fetchData = () => { - const { dispatch, statusId } = this.props; - - dispatch(fetchStatusWithContext(statusId)); - } - - componentDidMount() { - this.fetchData(); - } - - onClickClose = () => { - this.props.onClose('MENTIONS'); - }; - - render() { - const { accountIds } = this.props; - - let body; - - if (!accountIds) { - body = ; - } else { - body = ( - - {accountIds.map(id => - , - )} - - ); - } - - return ( - } - onClose={this.onClickClose} - > - {body} - - ); - } - -} diff --git a/app/soapbox/features/ui/components/mentions_modal.tsx b/app/soapbox/features/ui/components/mentions_modal.tsx new file mode 100644 index 000000000..445858843 --- /dev/null +++ b/app/soapbox/features/ui/components/mentions_modal.tsx @@ -0,0 +1,64 @@ +import { OrderedSet as ImmutableOrderedSet } from 'immutable'; +import React, { useEffect } from 'react'; +import { FormattedMessage } from 'react-intl'; + +import { fetchStatusWithContext } from 'soapbox/actions/statuses'; +import ScrollableList from 'soapbox/components/scrollable_list'; +import { Modal, Spinner } from 'soapbox/components/ui'; +import AccountContainer from 'soapbox/containers/account_container'; +import { useAppDispatch, useAppSelector } from 'soapbox/hooks'; +import { makeGetStatus } from 'soapbox/selectors'; + +const getStatus = makeGetStatus(); + +interface IMentionsModal { + onClose: (type: string) => void, + statusId: string, +} + +const MentionsModal: React.FC = ({ onClose, statusId }) => { + const dispatch = useAppDispatch(); + + const status = useAppSelector((state) => getStatus(state, { id: statusId })); + const accountIds = status ? ImmutableOrderedSet(status.mentions.map(m => m.get('id'))) : null; + + const fetchData = () => { + dispatch(fetchStatusWithContext(statusId)); + }; + + const onClickClose = () => { + onClose('MENTIONS'); + }; + + useEffect(() => { + fetchData(); + }, []); + + let body; + + if (!accountIds) { + body = ; + } else { + body = ( + + {accountIds.map(id => + , + )} + + ); + } + + return ( + } + onClose={onClickClose} + > + {body} + + ); +}; + +export default MentionsModal; diff --git a/app/soapbox/features/ui/components/modal_loading.js b/app/soapbox/features/ui/components/modal_loading.tsx similarity index 100% rename from app/soapbox/features/ui/components/modal_loading.js rename to app/soapbox/features/ui/components/modal_loading.tsx diff --git a/app/soapbox/features/ui/components/pending_status.js b/app/soapbox/features/ui/components/pending_status.js deleted file mode 100644 index e8da15a73..000000000 --- a/app/soapbox/features/ui/components/pending_status.js +++ /dev/null @@ -1,95 +0,0 @@ -import classNames from 'classnames'; -import React from 'react'; -import ImmutablePureComponent from 'react-immutable-pure-component'; -import { injectIntl } from 'react-intl'; -import { connect } from 'react-redux'; - -import StatusContent from 'soapbox/components/status_content'; -import StatusReplyMentions from 'soapbox/components/status_reply_mentions'; -import { HStack } from 'soapbox/components/ui'; -import AccountContainer from 'soapbox/containers/account_container'; -import PlaceholderCard from 'soapbox/features/placeholder/components/placeholder_card'; -import PlaceholderMediaGallery from 'soapbox/features/placeholder/components/placeholder_media_gallery'; -import QuotedStatus from 'soapbox/features/status/containers/quoted_status_container'; - -import { buildStatus } from '../util/pending_status_builder'; - -import PollPreview from './poll_preview'; - -const shouldHaveCard = pendingStatus => { - return Boolean(pendingStatus.get('content').match(/https?:\/\/\S*/)); -}; - -const mapStateToProps = (state, props) => { - const { idempotencyKey } = props; - const pendingStatus = state.getIn(['pending_statuses', idempotencyKey]); - return { - status: pendingStatus ? buildStatus(state, pendingStatus, idempotencyKey) : null, - }; -}; - -export default @connect(mapStateToProps) -@injectIntl -class PendingStatus extends ImmutablePureComponent { - - renderMedia = () => { - const { status } = this.props; - - if (status.get('media_attachments') && !status.get('media_attachments').isEmpty()) { - return ( - - ); - } else if (!status.get('quote') && shouldHaveCard(status)) { - return ; - } else { - return null; - } - } - - render() { - const { status, className } = this.props; - if (!status) return null; - if (!status.get('account')) return null; - - return ( -
-
-
-
- - - -
- -
- - - - - {this.renderMedia()} - {status.get('poll') && } - - {status.get('quote') && } -
- - {/* TODO */} - {/* */} -
-
-
- ); - } - -} diff --git a/app/soapbox/features/ui/components/pending_status.tsx b/app/soapbox/features/ui/components/pending_status.tsx new file mode 100644 index 000000000..f4b990fc6 --- /dev/null +++ b/app/soapbox/features/ui/components/pending_status.tsx @@ -0,0 +1,97 @@ +import classNames from 'classnames'; +import React from 'react'; + +import StatusReplyMentions from 'soapbox/components/status-reply-mentions'; +import StatusContent from 'soapbox/components/status_content'; +import { HStack } from 'soapbox/components/ui'; +import AccountContainer from 'soapbox/containers/account_container'; +import PlaceholderCard from 'soapbox/features/placeholder/components/placeholder_card'; +import PlaceholderMediaGallery from 'soapbox/features/placeholder/components/placeholder_media_gallery'; +import QuotedStatus from 'soapbox/features/status/containers/quoted_status_container'; +import { useAppSelector } from 'soapbox/hooks'; + +import { buildStatus } from '../util/pending_status_builder'; + +import PollPreview from './poll_preview'; + +import type { Account as AccountEntity, Status as StatusEntity } from 'soapbox/types/entities'; + +const shouldHaveCard = (pendingStatus: StatusEntity) => { + return Boolean(pendingStatus.content.match(/https?:\/\/\S*/)); +}; + +interface IPendingStatus { + className?: string, + idempotencyKey: string, + muted?: boolean, +} + +interface IPendingStatusMedia { + status: StatusEntity, +} + +const PendingStatusMedia: React.FC = ({ status }) => { + if (status.media_attachments && !status.media_attachments.isEmpty()) { + return ( + + ); + } else if (!status.quote && shouldHaveCard(status)) { + return ; + } else { + return null; + } +}; + +const PendingStatus: React.FC = ({ idempotencyKey, className, muted }) => { + const status = useAppSelector((state) => { + const pendingStatus = state.pending_statuses.get(idempotencyKey); + return pendingStatus ? buildStatus(state, pendingStatus, idempotencyKey) : null; + }) as StatusEntity | null; + + if (!status) return null; + if (!status.account) return null; + + const account = status.account as AccountEntity; + + return ( +
+
+
+
+ + + +
+ +
+ + + + + + + {status.poll && } + + {status.quote && } +
+ + {/* TODO */} + {/* */} +
+
+
+ ); +}; + +export default PendingStatus; diff --git a/app/soapbox/features/ui/components/pinned_accounts_panel.js b/app/soapbox/features/ui/components/pinned_accounts_panel.js deleted file mode 100644 index e0cfb1f93..000000000 --- a/app/soapbox/features/ui/components/pinned_accounts_panel.js +++ /dev/null @@ -1,79 +0,0 @@ -import { List as ImmutableList } from 'immutable'; -import PropTypes from 'prop-types'; -import React from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import ImmutablePureComponent from 'react-immutable-pure-component'; -import { FormattedMessage, injectIntl } from 'react-intl'; -import { connect } from 'react-redux'; - -import Icon from 'soapbox/components/icon'; - -import { fetchPinnedAccounts } from '../../../actions/accounts'; -import AccountContainer from '../../../containers/account_container'; - -class PinnedAccountsPanel extends ImmutablePureComponent { - - static propTypes = { - pinned: ImmutablePropTypes.list.isRequired, - fetchPinned: PropTypes.func.isRequired, - intl: PropTypes.object.isRequired, - }; - - componentDidMount() { - this.props.fetchPinned(); - } - - render() { - const { account } = this.props; - const pinned = this.props.pinned.slice(0, this.props.limit); - - if (pinned.isEmpty()) { - return null; - } - - return ( -
-
- - - , - }} - /> - -
-
-
- {pinned && pinned.map(suggestion => ( - - ))} -
-
-
- ); - } - -} - -const mapStateToProps = (state, { account }) => ({ - pinned: state.getIn(['user_lists', 'pinned', account.get('id'), 'items'], ImmutableList()), -}); - -const mapDispatchToProps = (dispatch, { account }) => { - return { - fetchPinned: () => dispatch(fetchPinnedAccounts(account.get('id'))), - }; -}; - -export default injectIntl( - connect(mapStateToProps, mapDispatchToProps, null, { - forwardRef: true, - }, - )(PinnedAccountsPanel)); diff --git a/app/soapbox/features/ui/components/pinned_accounts_panel.tsx b/app/soapbox/features/ui/components/pinned_accounts_panel.tsx new file mode 100644 index 000000000..d364d61e8 --- /dev/null +++ b/app/soapbox/features/ui/components/pinned_accounts_panel.tsx @@ -0,0 +1,50 @@ +import { List as ImmutableList } from 'immutable'; +import React, { useEffect } from 'react'; +import { FormattedMessage } from 'react-intl'; + +import { fetchPinnedAccounts } from 'soapbox/actions/accounts'; +import { Widget } from 'soapbox/components/ui'; +import AccountContainer from 'soapbox/containers/account_container'; +import { useAppDispatch, useAppSelector } from 'soapbox/hooks'; + +import type { Account } from 'soapbox/types/entities'; + +interface IPinnedAccountsPanel { + account: Account, + limit: number, +} + +const PinnedAccountsPanel: React.FC = ({ account, limit }) => { + const dispatch = useAppDispatch(); + const pinned = useAppSelector((state) => state.user_lists.getIn(['pinned', account.id, 'items'], ImmutableList())).slice(0, limit); + + useEffect(() => { + dispatch(fetchPinnedAccounts(account.id)); + }, []); + + if (pinned.isEmpty()) { + return null; + } + + return ( + , + }} + />} + > + {pinned && pinned.map((suggestion: string) => ( + + ))} + + ); +}; + +export default PinnedAccountsPanel; diff --git a/app/soapbox/features/ui/components/profile_familiar_followers.tsx b/app/soapbox/features/ui/components/profile_familiar_followers.tsx index f2e0c9b61..0b7c5b144 100644 --- a/app/soapbox/features/ui/components/profile_familiar_followers.tsx +++ b/app/soapbox/features/ui/components/profile_familiar_followers.tsx @@ -1,6 +1,5 @@ import { OrderedSet as ImmutableOrderedSet } from 'immutable'; -import React from 'react'; -import { useEffect } from 'react'; +import React, { useEffect } from 'react'; import { FormattedList, FormattedMessage } from 'react-intl'; import { useDispatch } from 'react-redux'; import { Link } from 'react-router-dom'; diff --git a/app/soapbox/features/ui/components/reactions_modal.tsx b/app/soapbox/features/ui/components/reactions_modal.tsx index 31422ab05..4fb4df72a 100644 --- a/app/soapbox/features/ui/components/reactions_modal.tsx +++ b/app/soapbox/features/ui/components/reactions_modal.tsx @@ -1,16 +1,14 @@ import { List as ImmutableList } from 'immutable'; -import React from 'react'; -import { useEffect } from 'react'; +import React, { useEffect } from 'react'; import { useState } from 'react'; import { FormattedMessage, defineMessages, useIntl } from 'react-intl'; -import { useDispatch } from 'react-redux'; import { fetchFavourites, fetchReactions } from 'soapbox/actions/interactions'; import FilterBar from 'soapbox/components/filter_bar'; import ScrollableList from 'soapbox/components/scrollable_list'; import { Modal, Spinner } from 'soapbox/components/ui'; import AccountContainer from 'soapbox/containers/account_container'; -import { useAppSelector } from 'soapbox/hooks'; +import { useAppDispatch, useAppSelector } from 'soapbox/hooks'; const messages = defineMessages({ close: { id: 'lightbox.close', defaultMessage: 'Close' }, @@ -20,14 +18,13 @@ const messages = defineMessages({ interface IReactionsModal { onClose: (string: string) => void, statusId: string, - username: string, reaction?: string, } -const ReactionsModal: React.FC = ({ onClose, statusId, ...props }) => { - const dispatch = useDispatch(); +const ReactionsModal: React.FC = ({ onClose, statusId, reaction: initialReaction }) => { + const dispatch = useAppDispatch(); const intl = useIntl(); - const [reaction, setReaction] = useState(props.reaction); + const [reaction, setReaction] = useState(initialReaction); const reactions = useAppSelector, count: number, diff --git a/app/soapbox/features/ui/components/reblogs_modal.js b/app/soapbox/features/ui/components/reblogs_modal.js deleted file mode 100644 index a5945c3a1..000000000 --- a/app/soapbox/features/ui/components/reblogs_modal.js +++ /dev/null @@ -1,95 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import { injectIntl, FormattedMessage } from 'react-intl'; -import { connect } from 'react-redux'; -import { withRouter } from 'react-router-dom'; - -import { fetchReblogs } from 'soapbox/actions/interactions'; -import { fetchStatus } from 'soapbox/actions/statuses'; -import ScrollableList from 'soapbox/components/scrollable_list'; -import { Modal, Spinner } from 'soapbox/components/ui'; -import AccountContainer from 'soapbox/containers/account_container'; - -const mapStateToProps = (state, props) => { - return { - accountIds: state.getIn(['user_lists', 'reblogged_by', props.statusId]), - }; -}; - -export default @connect(mapStateToProps) -@injectIntl -@withRouter -class ReblogsModal extends React.PureComponent { - - static propTypes = { - onClose: PropTypes.func.isRequired, - intl: PropTypes.object.isRequired, - statusId: PropTypes.string.isRequired, - username: PropTypes.string.isRequired, - dispatch: PropTypes.func.isRequired, - accountIds: ImmutablePropTypes.orderedSet, - history: PropTypes.object, - }; - - fetchData = () => { - const { dispatch, statusId } = this.props; - - dispatch(fetchReblogs(statusId)); - dispatch(fetchStatus(statusId)); - } - - componentDidMount() { - this.fetchData(); - this.unlistenHistory = this.props.history.listen((_, action) => { - if (action === 'PUSH') { - this.onClickClose(null, true); - } - }); - } - - componentWillUnmount() { - if (this.unlistenHistory) { - this.unlistenHistory(); - } - } - - onClickClose = () => { - this.props.onClose('REBLOGS'); - }; - - render() { - const { accountIds } = this.props; - - let body; - - if (!accountIds) { - body = ; - } else { - const emptyMessage = ; - - body = ( - - {accountIds.map(id => - , - )} - - ); - } - - - return ( - } - onClose={this.onClickClose} - > - {body} - - ); - } - -} diff --git a/app/soapbox/features/ui/components/reblogs_modal.tsx b/app/soapbox/features/ui/components/reblogs_modal.tsx new file mode 100644 index 000000000..cb9906bad --- /dev/null +++ b/app/soapbox/features/ui/components/reblogs_modal.tsx @@ -0,0 +1,64 @@ +import React, { useEffect } from 'react'; +import { FormattedMessage } from 'react-intl'; + +import { fetchReblogs } from 'soapbox/actions/interactions'; +import { fetchStatus } from 'soapbox/actions/statuses'; +import ScrollableList from 'soapbox/components/scrollable_list'; +import { Modal, Spinner } from 'soapbox/components/ui'; +import AccountContainer from 'soapbox/containers/account_container'; +import { useAppDispatch, useAppSelector } from 'soapbox/hooks'; + +interface IReblogsModal { + onClose: (string: string) => void, + statusId: string, +} + +const ReblogsModal: React.FC = ({ onClose, statusId }) => { + const dispatch = useAppDispatch(); + const accountIds = useAppSelector((state) => state.user_lists.getIn(['reblogged_by', statusId])); + + const fetchData = () => { + dispatch(fetchReblogs(statusId)); + dispatch(fetchStatus(statusId)); + }; + + useEffect(() => { + fetchData(); + }, []); + + const onClickClose = () => { + onClose('REBLOGS'); + }; + + let body; + + if (!accountIds) { + body = ; + } else { + const emptyMessage = ; + + body = ( + + {accountIds.map((id: string) => + , + )} + + ); + } + + + return ( + } + onClose={onClickClose} + > + {body} + + ); +}; + +export default ReblogsModal; diff --git a/app/soapbox/features/ui/components/user_panel.js b/app/soapbox/features/ui/components/user_panel.js deleted file mode 100644 index 747717add..000000000 --- a/app/soapbox/features/ui/components/user_panel.js +++ /dev/null @@ -1,136 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import ImmutablePureComponent from 'react-immutable-pure-component'; -import { injectIntl, FormattedMessage } from 'react-intl'; -import { connect } from 'react-redux'; -import { Link } from 'react-router-dom'; - -import Avatar from 'soapbox/components/avatar'; -import StillImage from 'soapbox/components/still_image'; -import VerificationBadge from 'soapbox/components/verification_badge'; -import { getAcct } from 'soapbox/utils/accounts'; -import { shortNumberFormat } from 'soapbox/utils/numbers'; -import { displayFqn } from 'soapbox/utils/state'; - -import { HStack, Stack, Text } from '../../../components/ui'; -import { makeGetAccount } from '../../../selectors'; - -class UserPanel extends ImmutablePureComponent { - - static propTypes = { - account: ImmutablePropTypes.record, - displayFqn: PropTypes.bool, - intl: PropTypes.object.isRequired, - domain: PropTypes.string, - } - - render() { - const { account, action, badges, displayFqn, intl, domain } = this.props; - if (!account) return null; - const displayNameHtml = { __html: account.get('display_name_html') }; - const acct = account.get('acct').indexOf('@') === -1 && domain ? `${account.get('acct')}@${domain}` : account.get('acct'); - const header = account.get('header'); - const verified = account.get('verified'); - - return ( -
- - -
- {header && ( - - )} -
- - - - - - - {action && ( -
{action}
- )} -
-
- - - - - - - {verified && } - - {badges.length > 0 && ( - - {badges} - - )} - - - - - @{getAcct(account, displayFqn)} - - - - - {account.get('followers_count') >= 0 && ( - - - - {shortNumberFormat(account.get('followers_count'))} - - - - - - - )} - - {account.get('following_count') >= 0 && ( - - - - {shortNumberFormat(account.get('following_count'))} - - - - - - - )} - -
-
- ); - } - -} - -const makeMapStateToProps = () => { - const getAccount = makeGetAccount(); - - const mapStateToProps = (state, { accountId }) => ({ - account: getAccount(state, accountId), - displayFqn: displayFqn(state), - }); - - return mapStateToProps; -}; - -export default injectIntl( - connect(makeMapStateToProps, null, null, { - forwardRef: true, - })(UserPanel)); diff --git a/app/soapbox/features/ui/components/user_panel.tsx b/app/soapbox/features/ui/components/user_panel.tsx new file mode 100644 index 000000000..6bf77b659 --- /dev/null +++ b/app/soapbox/features/ui/components/user_panel.tsx @@ -0,0 +1,119 @@ +import React from 'react'; +import { FormattedMessage, useIntl } from 'react-intl'; +import { Link } from 'react-router-dom'; + +import Avatar from 'soapbox/components/avatar'; +import StillImage from 'soapbox/components/still_image'; +import { HStack, Stack, Text } from 'soapbox/components/ui'; +import VerificationBadge from 'soapbox/components/verification_badge'; +import { useAppSelector } from 'soapbox/hooks'; +import { makeGetAccount } from 'soapbox/selectors'; +import { getAcct } from 'soapbox/utils/accounts'; +import { shortNumberFormat } from 'soapbox/utils/numbers'; +import { displayFqn } from 'soapbox/utils/state'; + +const getAccount = makeGetAccount(); + +interface IUserPanel { + accountId: string, + action?: JSX.Element, + badges?: JSX.Element[], + domain?: string, +} + +const UserPanel: React.FC = ({ accountId, action, badges, domain }) => { + const intl = useIntl(); + const account = useAppSelector((state) => getAccount(state, accountId)); + const fqn = useAppSelector((state) => displayFqn(state)); + + if (!account) return null; + const displayNameHtml = { __html: account.get('display_name_html') }; + const acct = account.get('acct').indexOf('@') === -1 && domain ? `${account.get('acct')}@${domain}` : account.get('acct'); + const header = account.get('header'); + const verified = account.get('verified'); + + return ( +
+ + +
+ {header && ( + + )} +
+ + + + + + + {action && ( +
{action}
+ )} +
+
+ + + + + + + {verified && } + + {badges && badges.length > 0 && ( + + {badges} + + )} + + + + + @{getAcct(account, fqn)} + + + + + {account.get('followers_count') >= 0 && ( + + + + {shortNumberFormat(account.get('followers_count'))} + + + + + + + )} + + {account.get('following_count') >= 0 && ( + + + + {shortNumberFormat(account.get('following_count'))} + + + + + + + )} + +
+
+ ); +}; + +export default UserPanel; diff --git a/app/soapbox/features/ui/util/pending_status_builder.js b/app/soapbox/features/ui/util/pending_status_builder.ts similarity index 66% rename from app/soapbox/features/ui/util/pending_status_builder.js rename to app/soapbox/features/ui/util/pending_status_builder.ts index 8ea186f56..e74b0a897 100644 --- a/app/soapbox/features/ui/util/pending_status_builder.js +++ b/app/soapbox/features/ui/util/pending_status_builder.ts @@ -4,9 +4,11 @@ import { normalizeStatus } from 'soapbox/normalizers/status'; import { calculateStatus } from 'soapbox/reducers/statuses'; import { makeGetAccount } from 'soapbox/selectors'; +import type { RootState } from 'soapbox/store'; + const getAccount = makeGetAccount(); -const buildMentions = pendingStatus => { +const buildMentions = (pendingStatus: ImmutableMap) => { if (pendingStatus.get('in_reply_to_id')) { return ImmutableList(pendingStatus.get('to') || []).map(acct => ImmutableMap({ acct })); } else { @@ -14,18 +16,18 @@ const buildMentions = pendingStatus => { } }; -const buildPoll = pendingStatus => { +const buildPoll = (pendingStatus: ImmutableMap) => { if (pendingStatus.hasIn(['poll', 'options'])) { - return pendingStatus.get('poll').update('options', options => { - return options.map(title => ImmutableMap({ title })); + return pendingStatus.get('poll').update('options', (options: ImmutableMap) => { + return options.map((title: string) => ImmutableMap({ title })); }); } else { return null; } }; -export const buildStatus = (state, pendingStatus, idempotencyKey) => { - const me = state.get('me'); +export const buildStatus = (state: RootState, pendingStatus: ImmutableMap, idempotencyKey: string) => { + const me = state.me as string; const account = getAccount(state, me); const inReplyToId = pendingStatus.get('in_reply_to_id'); @@ -33,9 +35,9 @@ export const buildStatus = (state, pendingStatus, idempotencyKey) => { account, content: pendingStatus.get('status', '').replace(new RegExp('\n', 'g'), '
'), /* eslint-disable-line no-control-regex */ id: `末pending-${idempotencyKey}`, - in_reply_to_account_id: state.getIn(['statuses', inReplyToId, 'account'], null), + in_reply_to_account_id: state.statuses.getIn([inReplyToId, 'account'], null), in_reply_to_id: inReplyToId, - media_attachments: pendingStatus.get('media_ids', ImmutableList()).map(id => ImmutableMap({ id })), + media_attachments: pendingStatus.get('media_ids', ImmutableList()).map((id: string) => ImmutableMap({ id })), mentions: buildMentions(pendingStatus), poll: buildPoll(pendingStatus), quote: pendingStatus.get('quote_id', null), diff --git a/app/styles/components/columns.scss b/app/styles/components/columns.scss index 160fef0b4..5c2c4e1ff 100644 --- a/app/styles/components/columns.scss +++ b/app/styles/components/columns.scss @@ -802,23 +802,6 @@ } } -.timeline-filter-message { - display: flex; - align-items: center; - background-color: var(--brand-color--faint); - color: var(--primary-text-color); - padding: 15px 20px; - - .icon-button { - margin: 2px 8px 2px 0; - - .svg-icon { - height: 20px; - width: 20px; - } - } -} - .column--better { .column__top { display: flex; diff --git a/app/styles/components/dropdown-menu.scss b/app/styles/components/dropdown-menu.scss index 47e2da514..e3a717fbc 100644 --- a/app/styles/components/dropdown-menu.scss +++ b/app/styles/components/dropdown-menu.scss @@ -47,7 +47,7 @@ @apply focus-within:ring-primary-500 focus-within:ring-2; a { - @apply flex px-4 py-2.5 text-sm text-gray-700 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-slate-800 focus:bg-gray-100 focus:hover:bg-slate-800 cursor-pointer; + @apply flex px-4 py-2.5 text-sm text-gray-700 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-slate-800 focus:bg-gray-100 dark:focus:bg-slate-800 cursor-pointer; > .svg-icon:first-child { @apply h-5 w-5 mr-2.5 transition-none; diff --git a/app/styles/polls.scss b/app/styles/polls.scss index a49315450..13adcd8ff 100644 --- a/app/styles/polls.scss +++ b/app/styles/polls.scss @@ -133,10 +133,6 @@ line-height: 18px; } - &__footer { - @apply pt-1.5 pb-[5px] text-black dark:text-white; - } - &__link { display: inline; background: transparent; @@ -180,18 +176,6 @@ padding: 10px; } - .poll__footer { - border-top: 1px solid var(--foreground-color); - padding: 10px; - margin: -5px 0 0 -5px; - - button, - select { - flex: 1 1 50%; - margin: 5px 0 0 5px; - } - } - .button.button-secondary { @apply h-auto py-1.5 px-2.5 text-primary-600 dark:text-primary-400 border-primary-600; } From 4e3935d0448dce2c997f2fc5126cd64fca9b7603 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sat, 28 May 2022 13:15:23 -0500 Subject: [PATCH 069/102] Upgrade to React 17 --- package.json | 4 ++-- yarn.lock | 28 +++++++++++++--------------- 2 files changed, 15 insertions(+), 17 deletions(-) diff --git a/package.json b/package.json index 6747d5a61..05a64183b 100644 --- a/package.json +++ b/package.json @@ -149,10 +149,10 @@ "prop-types": "^15.5.10", "punycode": "^2.1.0", "qrcode.react": "^1.0.0", - "react": "^16.13.1", + "react": "^17.0.2", "react-color": "^2.18.1", "react-datepicker": "^4.7.0", - "react-dom": "^16.13.1", + "react-dom": "^17.0.2", "react-helmet": "^6.0.0", "react-hotkeys": "^1.1.4", "react-immutable-proptypes": "^2.1.0", diff --git a/yarn.lock b/yarn.lock index ab23759cf..26d668711 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8796,15 +8796,14 @@ react-datepicker@^4.7.0: react-onclickoutside "^6.12.0" react-popper "^2.2.5" -react-dom@^16.13.1: - version "16.14.0" - resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.14.0.tgz#7ad838ec29a777fb3c75c3a190f661cf92ab8b89" - integrity sha512-1gCeQXDLoIqMgqD3IO2Ah9bnf0w9kzhwN5q4FGnHZ67hBm9yePzB5JJAIQCc8x3pFnNlwFq4RidZggNAAkzWWw== +react-dom@^17.0.2: + version "17.0.2" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-17.0.2.tgz#ecffb6845e3ad8dbfcdc498f0d0a939736502c23" + integrity sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA== dependencies: loose-envify "^1.1.0" object-assign "^4.1.1" - prop-types "^15.6.2" - scheduler "^0.19.1" + scheduler "^0.20.2" react-event-listener@^0.6.0: version "0.6.6" @@ -9091,14 +9090,13 @@ react-virtuoso@^2.9.1: "@virtuoso.dev/react-urx" "^0.2.12" "@virtuoso.dev/urx" "^0.2.12" -react@^16.13.1: - version "16.14.0" - resolved "https://registry.yarnpkg.com/react/-/react-16.14.0.tgz#94d776ddd0aaa37da3eda8fc5b6b18a4c9a3114d" - integrity sha512-0X2CImDkJGApiAlcf0ODKIneSwBPhqJawOa5wCtKbu7ZECrmS26NvtSILynQ66cgkT/RJ4LidJOc3bUESwmU8g== +react@^17.0.2: + version "17.0.2" + resolved "https://registry.yarnpkg.com/react/-/react-17.0.2.tgz#d0b5cc516d29eb3eee383f75b62864cfb6800037" + integrity sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA== dependencies: loose-envify "^1.1.0" object-assign "^4.1.1" - prop-types "^15.6.2" reactcss@^1.2.0: version "1.2.3" @@ -9531,10 +9529,10 @@ saxes@^5.0.1: dependencies: xmlchars "^2.2.0" -scheduler@^0.19.1: - version "0.19.1" - resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.19.1.tgz#4f3e2ed2c1a7d65681f4c854fa8c5a1ccb40f196" - integrity sha512-n/zwRWRYSUj0/3g/otKDRPMh6qv2SYMWNq85IEa8iZyAv8od9zDYpGSnpBEjNgcMNq6Scbu5KfIPxNF72R/2EA== +scheduler@^0.20.2: + version "0.20.2" + resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.20.2.tgz#4baee39436e34aa93b4874bddcbf0fe8b8b50e91" + integrity sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ== dependencies: loose-envify "^1.1.0" object-assign "^4.1.1" From a3d1d2dc9191f6581127598264ec21e815a19cb1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Sat, 28 May 2022 21:53:22 +0200 Subject: [PATCH 070/102] TS, FC MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- .../auth_login/components/otp_auth_form.js | 101 ------------------ .../auth_login/components/otp_auth_form.tsx | 88 +++++++++++++++ .../auth_login/components/password_reset.js | 72 ------------- .../auth_login/components/password_reset.tsx | 66 ++++++++++++ app/soapbox/features/new_status/index.js | 34 ------ app/soapbox/features/new_status/index.tsx | 20 ++++ .../components/pinned_hosts_picker.tsx | 2 - .../features/reply_mentions/account.js | 94 ---------------- .../features/reply_mentions/account.tsx | 67 ++++++++++++ app/soapbox/features/server_info/index.js | 48 --------- app/soapbox/features/server_info/index.tsx | 36 +++++++ .../{media_display.js => media_display.tsx} | 19 ++-- app/soapbox/features/share/index.js | 47 -------- app/soapbox/features/share/index.tsx | 30 ++++++ .../components/site-preview.tsx | 1 - .../ui/components/reply_mentions_modal.tsx | 2 +- .../{email_passthru.js => email_passthru.tsx} | 13 +-- .../{waitlist_page.js => waitlist_page.tsx} | 11 +- 18 files changed, 323 insertions(+), 428 deletions(-) delete mode 100644 app/soapbox/features/auth_login/components/otp_auth_form.js create mode 100644 app/soapbox/features/auth_login/components/otp_auth_form.tsx delete mode 100644 app/soapbox/features/auth_login/components/password_reset.js create mode 100644 app/soapbox/features/auth_login/components/password_reset.tsx delete mode 100644 app/soapbox/features/new_status/index.js create mode 100644 app/soapbox/features/new_status/index.tsx delete mode 100644 app/soapbox/features/reply_mentions/account.js create mode 100644 app/soapbox/features/reply_mentions/account.tsx delete mode 100644 app/soapbox/features/server_info/index.js create mode 100644 app/soapbox/features/server_info/index.tsx rename app/soapbox/features/settings/{media_display.js => media_display.tsx} (78%) delete mode 100644 app/soapbox/features/share/index.js create mode 100644 app/soapbox/features/share/index.tsx rename app/soapbox/features/verification/{email_passthru.js => email_passthru.tsx} (96%) rename app/soapbox/features/verification/{waitlist_page.js => waitlist_page.tsx} (91%) diff --git a/app/soapbox/features/auth_login/components/otp_auth_form.js b/app/soapbox/features/auth_login/components/otp_auth_form.js deleted file mode 100644 index 0446a7afe..000000000 --- a/app/soapbox/features/auth_login/components/otp_auth_form.js +++ /dev/null @@ -1,101 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import ImmutablePureComponent from 'react-immutable-pure-component'; -import { injectIntl, FormattedMessage, defineMessages } from 'react-intl'; -import { connect } from 'react-redux'; -import { Redirect } from 'react-router-dom'; - -import { otpVerify, verifyCredentials, switchAccount } from 'soapbox/actions/auth'; -import { Button, Form, FormActions, FormGroup, Input } from 'soapbox/components/ui'; - -const messages = defineMessages({ - otpCodeHint: { id: 'login.fields.otp_code_hint', defaultMessage: 'Enter the two-factor code generated by your phone app or use one of your recovery codes' }, - otpCodeLabel: { id: 'login.fields.otp_code_label', defaultMessage: 'Two-factor code:' }, - otpLoginFail: { id: 'login.otp_log_in.fail', defaultMessage: 'Invalid code, please try again.' }, -}); - -export default @connect() -@injectIntl -class OtpAuthForm extends ImmutablePureComponent { - - state = { - isLoading: false, - code_error: '', - shouldRedirect: false, - } - - static propTypes = { - intl: PropTypes.object.isRequired, - dispatch: PropTypes.func.isRequired, - mfa_token: PropTypes.string, - }; - - getFormData = (form) => { - return Object.fromEntries( - Array.from(form).map(i => [i.name, i.value]), - ); - } - - handleSubmit = (event) => { - const { dispatch, mfa_token } = this.props; - const { code } = this.getFormData(event.target); - dispatch(otpVerify(code, mfa_token)).then(({ access_token }) => { - this.setState({ code_error: false }); - return dispatch(verifyCredentials(access_token)); - }).then(account => { - this.setState({ shouldRedirect: true }); - return dispatch(switchAccount(account.id)); - }).catch(error => { - this.setState({ isLoading: false, code_error: true }); - }); - this.setState({ isLoading: true }); - event.preventDefault(); - } - - render() { - const { intl } = this.props; - const { code_error, shouldRedirect } = this.state; - - if (shouldRedirect) return ; - - return ( -
-
-

- -

-
- -
- - - - - - - - - -
-
- ); - } - -} diff --git a/app/soapbox/features/auth_login/components/otp_auth_form.tsx b/app/soapbox/features/auth_login/components/otp_auth_form.tsx new file mode 100644 index 000000000..eaeade08b --- /dev/null +++ b/app/soapbox/features/auth_login/components/otp_auth_form.tsx @@ -0,0 +1,88 @@ +import React, { useState } from 'react'; +import { FormattedMessage, defineMessages, useIntl } from 'react-intl'; +import { Redirect } from 'react-router-dom'; + +import { otpVerify, verifyCredentials, switchAccount } from 'soapbox/actions/auth'; +import { Button, Form, FormActions, FormGroup, Input } from 'soapbox/components/ui'; +import { useAppDispatch } from 'soapbox/hooks'; + +const messages = defineMessages({ + otpCodeHint: { id: 'login.fields.otp_code_hint', defaultMessage: 'Enter the two-factor code generated by your phone app or use one of your recovery codes' }, + otpCodeLabel: { id: 'login.fields.otp_code_label', defaultMessage: 'Two-factor code:' }, + otpLoginFail: { id: 'login.otp_log_in.fail', defaultMessage: 'Invalid code, please try again.' }, +}); + +interface IOtpAuthForm { + mfa_token: string, +} + +const OtpAuthForm: React.FC = ({ mfa_token }) => { + const dispatch = useAppDispatch(); + const intl = useIntl(); + + const [isLoading, setIsLoading] = useState(false); + const [shouldRedirect, setShouldRedirect] = useState(false); + const [codeError, setCodeError] = useState(''); + + const getFormData = (form: any) => Object.fromEntries( + Array.from(form).map((i: any) => [i.name, i.value]), + ); + + const handleSubmit = (event: React.FormEvent) => { + const { code } = getFormData(event.target); + dispatch(otpVerify(code, mfa_token)).then(({ access_token }) => { + setCodeError(false); + return dispatch(verifyCredentials(access_token)); + }).then(account => { + setShouldRedirect(true); + return dispatch(switchAccount(account.id)); + }).catch(() => { + setIsLoading(false); + setCodeError(true); + }); + setIsLoading(true); + event.preventDefault(); + }; + + if (shouldRedirect) return ; + + return ( +
+
+

+ +

+
+ +
+
+ + + + + + + +
+
+
+ ); +}; + +export default OtpAuthForm; diff --git a/app/soapbox/features/auth_login/components/password_reset.js b/app/soapbox/features/auth_login/components/password_reset.js deleted file mode 100644 index f047207be..000000000 --- a/app/soapbox/features/auth_login/components/password_reset.js +++ /dev/null @@ -1,72 +0,0 @@ -import React from 'react'; -import ImmutablePureComponent from 'react-immutable-pure-component'; -import { defineMessages, FormattedMessage, injectIntl } from 'react-intl'; -import { connect } from 'react-redux'; -import { Redirect } from 'react-router-dom'; - -import { resetPassword } from 'soapbox/actions/security'; -import snackbar from 'soapbox/actions/snackbar'; - -import { Button, Form, FormActions, FormGroup, Input } from '../../../components/ui'; - -const messages = defineMessages({ - nicknameOrEmail: { id: 'password_reset.fields.username_placeholder', defaultMessage: 'Email or username' }, - confirmation: { id: 'password_reset.confirmation', defaultMessage: 'Check your email for confirmation.' }, -}); - -export default @connect() -@injectIntl -class PasswordReset extends ImmutablePureComponent { - - state = { - isLoading: false, - success: false, - } - - handleSubmit = e => { - const { dispatch, intl } = this.props; - const nicknameOrEmail = e.target.nickname_or_email.value; - this.setState({ isLoading: true }); - dispatch(resetPassword(nicknameOrEmail)).then(() => { - this.setState({ isLoading: false, success: true }); - dispatch(snackbar.info(intl.formatMessage(messages.confirmation))); - }).catch(error => { - this.setState({ isLoading: false }); - }); - } - - render() { - const { intl } = this.props; - - if (this.state.success) return ; - - return ( -
-
-

- -

-
- -
-
- - - - - - - -
-
-
- ); - } - -} diff --git a/app/soapbox/features/auth_login/components/password_reset.tsx b/app/soapbox/features/auth_login/components/password_reset.tsx new file mode 100644 index 000000000..bcd7976c6 --- /dev/null +++ b/app/soapbox/features/auth_login/components/password_reset.tsx @@ -0,0 +1,66 @@ +import React, { useState } from 'react'; +import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; +import { Redirect } from 'react-router-dom'; + +import { resetPassword } from 'soapbox/actions/security'; +import snackbar from 'soapbox/actions/snackbar'; +import { Button, Form, FormActions, FormGroup, Input } from 'soapbox/components/ui'; +import { useAppDispatch } from 'soapbox/hooks'; + +const messages = defineMessages({ + nicknameOrEmail: { id: 'password_reset.fields.username_placeholder', defaultMessage: 'Email or username' }, + confirmation: { id: 'password_reset.confirmation', defaultMessage: 'Check your email for confirmation.' }, +}); + +const PasswordReset = () => { + const dispatch = useAppDispatch(); + const intl = useIntl(); + + const [isLoading, setIsLoading] = useState(false); + const [success, setSuccess] = useState(false); + + const handleSubmit = (e: React.FormEvent) => { + const nicknameOrEmail = (e.target as any).nickname_or_email.value; + setIsLoading(true); + dispatch(resetPassword(nicknameOrEmail)).then(() => { + setIsLoading(false); + setSuccess(true); + dispatch(snackbar.info(intl.formatMessage(messages.confirmation))); + }).catch(() => { + setIsLoading(false); + }); + }; + + if (success) return ; + + return ( +
+
+

+ +

+
+ +
+
+ + + + + + + +
+
+
+ ); +}; + +export default PasswordReset; diff --git a/app/soapbox/features/new_status/index.js b/app/soapbox/features/new_status/index.js deleted file mode 100644 index ef6092b11..000000000 --- a/app/soapbox/features/new_status/index.js +++ /dev/null @@ -1,34 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import { connect } from 'react-redux'; -import { Redirect } from 'react-router-dom'; - -import { openModal } from '../../actions/modals'; - -const mapDispatchToProps = dispatch => ({ - - onLoad: (text) => { - dispatch(openModal('COMPOSE')); - }, - -}); - -export default @connect(null, mapDispatchToProps) -class NewStatus extends React.Component { - - static propTypes = { - onLoad: PropTypes.func.isRequired, - }; - - constructor(props) { - super(props); - this.props.onLoad(); - } - - render() { - return ( - - ); - } - -} diff --git a/app/soapbox/features/new_status/index.tsx b/app/soapbox/features/new_status/index.tsx new file mode 100644 index 000000000..322976afc --- /dev/null +++ b/app/soapbox/features/new_status/index.tsx @@ -0,0 +1,20 @@ +import React from 'react'; +import { useEffect } from 'react'; +import { Redirect } from 'react-router-dom'; + +import { openModal } from 'soapbox/actions/modals'; +import { useAppDispatch } from 'soapbox/hooks'; + +const NewStatus = () => { + const dispatch = useAppDispatch(); + + useEffect(() => { + dispatch(openModal('COMPOSE')); + }, []); + + return ( + + ); +}; + +export default NewStatus; diff --git a/app/soapbox/features/remote_timeline/components/pinned_hosts_picker.tsx b/app/soapbox/features/remote_timeline/components/pinned_hosts_picker.tsx index aa7940761..3f08e94d6 100644 --- a/app/soapbox/features/remote_timeline/components/pinned_hosts_picker.tsx +++ b/app/soapbox/features/remote_timeline/components/pinned_hosts_picker.tsx @@ -1,8 +1,6 @@ 'use strict'; -import classNames from 'classnames'; import React from 'react'; -import { Link } from 'react-router-dom'; import { Button, HStack } from 'soapbox/components/ui'; import { useSettings } from 'soapbox/hooks'; diff --git a/app/soapbox/features/reply_mentions/account.js b/app/soapbox/features/reply_mentions/account.js deleted file mode 100644 index b9bf62d51..000000000 --- a/app/soapbox/features/reply_mentions/account.js +++ /dev/null @@ -1,94 +0,0 @@ -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 { fetchAccount } from 'soapbox/actions/accounts'; -import { addToMentions, removeFromMentions } from 'soapbox/actions/compose'; -import Avatar from 'soapbox/components/avatar'; -import DisplayName from 'soapbox/components/display-name'; -import IconButton from 'soapbox/components/icon_button'; -import { makeGetAccount } from 'soapbox/selectors'; - -const messages = defineMessages({ - remove: { id: 'reply_mentions.account.remove', defaultMessage: 'Remove from mentions' }, - add: { id: 'reply_mentions.account.add', defaultMessage: 'Add to mentions' }, -}); - -const makeMapStateToProps = () => { - const getAccount = makeGetAccount(); - - const mapStateToProps = (state, { accountId }) => { - const account = getAccount(state, accountId); - - return { - added: !!account && state.getIn(['compose', 'to']).includes(account.get('acct')), - account, - }; - }; - - return mapStateToProps; -}; - -const mapDispatchToProps = (dispatch, { accountId }) => ({ - onRemove: () => dispatch(removeFromMentions(accountId)), - onAdd: () => dispatch(addToMentions(accountId)), - fetchAccount: () => dispatch(fetchAccount(accountId)), -}); - -export default @connect(makeMapStateToProps, mapDispatchToProps) -@injectIntl -class Account extends ImmutablePureComponent { - - static propTypes = { - accountId: PropTypes.string.isRequired, - intl: PropTypes.object.isRequired, - onRemove: PropTypes.func.isRequired, - onAdd: PropTypes.func.isRequired, - added: PropTypes.bool, - author: PropTypes.bool, - }; - - static defaultProps = { - added: false, - }; - - componentDidMount() { - const { account, accountId } = this.props; - - if (accountId && !account) { - this.props.fetchAccount(accountId); - } - } - - render() { - const { account, intl, onRemove, onAdd, added, author } = this.props; - - if (!account) return null; - - let button; - - if (added) { - button = ; - } else { - button = ; - } - - return ( -
-
-
-
- -
- -
- {!author && button} -
-
-
- ); - } - -} diff --git a/app/soapbox/features/reply_mentions/account.tsx b/app/soapbox/features/reply_mentions/account.tsx new file mode 100644 index 000000000..c4817e8cf --- /dev/null +++ b/app/soapbox/features/reply_mentions/account.tsx @@ -0,0 +1,67 @@ +import React from 'react'; +import { useEffect } from 'react'; +import { defineMessages, useIntl } from 'react-intl'; + +import { fetchAccount } from 'soapbox/actions/accounts'; +import { addToMentions, removeFromMentions } from 'soapbox/actions/compose'; +import Avatar from 'soapbox/components/avatar'; +import DisplayName from 'soapbox/components/display-name'; +import IconButton from 'soapbox/components/icon_button'; +import { useAppDispatch, useAppSelector } from 'soapbox/hooks'; +import { makeGetAccount } from 'soapbox/selectors'; + +const messages = defineMessages({ + remove: { id: 'reply_mentions.account.remove', defaultMessage: 'Remove from mentions' }, + add: { id: 'reply_mentions.account.add', defaultMessage: 'Add to mentions' }, +}); + +const getAccount = makeGetAccount(); + +interface IAccount { + accountId: string, + author: boolean, +} + +const Account: React.FC = ({ accountId, author }) => { + const intl = useIntl(); + const dispatch = useAppDispatch(); + + const account = useAppSelector((state) => getAccount(state, accountId)); + const added = useAppSelector((state) => !!account && state.compose.get('to').includes(account.acct)); + + const onRemove = () => dispatch(removeFromMentions(accountId)); + const onAdd = () => dispatch(addToMentions(accountId)); + + useEffect(() => { + if (accountId && !account) { + dispatch(fetchAccount(accountId)); + } + }, []); + + if (!account) return null; + + let button; + + if (added) { + button = ; + } else { + button = ; + } + + return ( +
+
+
+
+ +
+ +
+ {!author && button} +
+
+
+ ); +}; + +export default Account; diff --git a/app/soapbox/features/server_info/index.js b/app/soapbox/features/server_info/index.js deleted file mode 100644 index a7d19c72a..000000000 --- a/app/soapbox/features/server_info/index.js +++ /dev/null @@ -1,48 +0,0 @@ -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 Column from '../ui/components/column'; -import LinkFooter from '../ui/components/link_footer'; -import PromoPanel from '../ui/components/promo_panel'; - -const messages = defineMessages({ - heading: { id: 'column.info', defaultMessage: 'Server information' }, -}); - -const mapStateToProps = (state, props) => ({ - instance: state.get('instance'), -}); - -export default @connect(mapStateToProps) -@injectIntl -class ServerInfo extends ImmutablePureComponent { - - static propTypes = { - intl: PropTypes.object.isRequired, - }; - - render() { - const { intl, instance } = this.props; - - return ( - -
-
-
-

{instance.get('title')}

-
-
- {instance.get('description')} -
-
- - -
-
- ); - } - -} diff --git a/app/soapbox/features/server_info/index.tsx b/app/soapbox/features/server_info/index.tsx new file mode 100644 index 000000000..0e12538d4 --- /dev/null +++ b/app/soapbox/features/server_info/index.tsx @@ -0,0 +1,36 @@ +import React from 'react'; +import { defineMessages, useIntl } from 'react-intl'; + +import { useAppSelector } from 'soapbox/hooks'; + +import Column from '../ui/components/column'; +import LinkFooter from '../ui/components/link_footer'; +import PromoPanel from '../ui/components/promo_panel'; + +const messages = defineMessages({ + heading: { id: 'column.info', defaultMessage: 'Server information' }, +}); + +const ServerInfo = () => { + const intl = useIntl(); + const instance = useAppSelector((state) => state.instance); + + return ( + +
+
+
+

{instance.title}

+
+
+ {instance.description} +
+
+ + +
+
+ ); +}; + +export default ServerInfo; diff --git a/app/soapbox/features/settings/media_display.js b/app/soapbox/features/settings/media_display.tsx similarity index 78% rename from app/soapbox/features/settings/media_display.js rename to app/soapbox/features/settings/media_display.tsx index 0ce0eb51a..ee522347f 100644 --- a/app/soapbox/features/settings/media_display.js +++ b/app/soapbox/features/settings/media_display.tsx @@ -1,16 +1,11 @@ import React from 'react'; import { defineMessages, useIntl } from 'react-intl'; -import { useDispatch } from 'react-redux'; import { getSettings, changeSettingImmediate } from 'soapbox/actions/settings'; -import { - SimpleForm, - SelectDropdown, -} from 'soapbox/features/forms'; -import { useAppSelector } from 'soapbox/hooks'; - -import List, { ListItem } from '../../components/list'; -import { Card, CardBody, CardHeader, CardTitle } from '../../components/ui'; +import List, { ListItem } from 'soapbox/components/list'; +import { Card, CardBody, CardHeader, CardTitle } from 'soapbox/components/ui'; +import { SimpleForm, SelectDropdown } from 'soapbox/features/forms'; +import { useAppDispatch, useAppSelector } from 'soapbox/hooks'; const messages = defineMessages({ mediaDisplay: { id: 'preferences.fields.media_display_label', defaultMessage: 'Media display' }, @@ -20,7 +15,7 @@ const messages = defineMessages({ }); const MediaDisplay = () => { - const dispatch = useDispatch(); + const dispatch = useAppDispatch(); const intl = useIntl(); const settings = useAppSelector((state) => getSettings(state)); @@ -31,7 +26,7 @@ const MediaDisplay = () => { show_all: intl.formatMessage(messages.display_media_show_all), }; - const onSelectChange = path => { + const onSelectChange: (path: string[]) => React.ChangeEventHandler = path => { return e => { dispatch(changeSettingImmediate(path, e.target.value)); }; @@ -49,7 +44,7 @@ const MediaDisplay = () => { diff --git a/app/soapbox/features/share/index.js b/app/soapbox/features/share/index.js deleted file mode 100644 index ff6dd18f3..000000000 --- a/app/soapbox/features/share/index.js +++ /dev/null @@ -1,47 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import { connect } from 'react-redux'; -import { Redirect } from 'react-router-dom'; - -import { openComposeWithText } from '../../actions/compose'; - -const mapDispatchToProps = dispatch => ({ - - onShare: (text) => { - dispatch(openComposeWithText(text)); - }, - -}); - -export default @connect(null, mapDispatchToProps) -class Share extends React.Component { - - static propTypes = { - onShare: PropTypes.func.isRequired, - }; - - constructor(props) { - super(props); - - const params = new URLSearchParams(window.location.search); - - const text = [ - params.get('title'), - params.get('text'), - params.get('url'), - ] - .filter(v => v) - .join('\n\n'); - - if (text) { - this.props.onShare(text); - } - } - - render() { - return ( - - ); - } - -} diff --git a/app/soapbox/features/share/index.tsx b/app/soapbox/features/share/index.tsx new file mode 100644 index 000000000..562f23689 --- /dev/null +++ b/app/soapbox/features/share/index.tsx @@ -0,0 +1,30 @@ +import React from 'react'; +import { Redirect, useLocation } from 'react-router-dom'; + +import { openComposeWithText } from 'soapbox/actions/compose'; +import { useAppDispatch } from 'soapbox/hooks'; + +const Share = () => { + const dispatch = useAppDispatch(); + + const { search } = useLocation(); + const params = new URLSearchParams(search); + + const text = [ + params.get('title'), + params.get('text'), + params.get('url'), + ] + .filter(v => v) + .join('\n\n'); + + if (text) { + dispatch(openComposeWithText(text)); + } + + return ( + + ); +}; + +export default Share; \ No newline at end of file diff --git a/app/soapbox/features/soapbox_config/components/site-preview.tsx b/app/soapbox/features/soapbox_config/components/site-preview.tsx index 8721be6ff..1c4efc92f 100644 --- a/app/soapbox/features/soapbox_config/components/site-preview.tsx +++ b/app/soapbox/features/soapbox_config/components/site-preview.tsx @@ -49,7 +49,6 @@ const SitePreview: React.FC = ({ soapbox }) => { })} > - {/* Logo */}
); diff --git a/app/soapbox/features/ui/components/reply_mentions_modal.tsx b/app/soapbox/features/ui/components/reply_mentions_modal.tsx index c1d3422fb..b1a959afb 100644 --- a/app/soapbox/features/ui/components/reply_mentions_modal.tsx +++ b/app/soapbox/features/ui/components/reply_mentions_modal.tsx @@ -33,7 +33,7 @@ const ReplyMentionsModal: React.FC = ({ onClose }) => { closePosition='left' >
- {mentions.map(accountId => )} + {mentions.map(accountId => )}
); diff --git a/app/soapbox/features/verification/email_passthru.js b/app/soapbox/features/verification/email_passthru.tsx similarity index 96% rename from app/soapbox/features/verification/email_passthru.js rename to app/soapbox/features/verification/email_passthru.tsx index 52b791c52..d014143aa 100644 --- a/app/soapbox/features/verification/email_passthru.js +++ b/app/soapbox/features/verification/email_passthru.tsx @@ -1,7 +1,8 @@ -import PropTypes from 'prop-types'; +import { AxiosError } from 'axios'; import * as React from 'react'; import { defineMessages, useIntl } from 'react-intl'; import { useDispatch } from 'react-redux'; +import { useParams } from 'react-router-dom'; import snackbar from 'soapbox/actions/snackbar'; import { confirmEmailVerification } from 'soapbox/actions/verification'; @@ -91,8 +92,8 @@ const TokenExpired = () => { ); }; -const EmailPassThru = ({ match }) => { - const { token } = match.params; +const EmailPassThru = () => { + const { token } = useParams<{ token: string }>(); const dispatch = useDispatch(); const intl = useIntl(); @@ -106,7 +107,7 @@ const EmailPassThru = ({ match }) => { setStatus(Statuses.SUCCESS); dispatch(snackbar.success(intl.formatMessage({ id: 'email_passthru.success', defaultMessage: 'Your email has been verified!' }))); }) - .catch((error) => { + .catch((error: AxiosError) => { const errorKey = error?.response?.data?.error; let message = intl.formatMessage({ id: 'email_passthru.fail.generic', @@ -155,8 +156,4 @@ const EmailPassThru = ({ match }) => { } }; -EmailPassThru.propTypes = { - match: PropTypes.object, -}; - export default EmailPassThru; diff --git a/app/soapbox/features/verification/waitlist_page.js b/app/soapbox/features/verification/waitlist_page.tsx similarity index 91% rename from app/soapbox/features/verification/waitlist_page.js rename to app/soapbox/features/verification/waitlist_page.tsx index 14047b7b1..0a28f4993 100644 --- a/app/soapbox/features/verification/waitlist_page.js +++ b/app/soapbox/features/verification/waitlist_page.tsx @@ -1,4 +1,3 @@ -import PropTypes from 'prop-types'; import React, { useEffect } from 'react'; import { useIntl } from 'react-intl'; import { useDispatch } from 'react-redux'; @@ -12,15 +11,15 @@ import { useAppSelector, useOwnAccount } from 'soapbox/hooks'; import { logOut } from '../../actions/auth'; import { Button, Stack, Text } from '../../components/ui'; -const WaitlistPage = ({ account }) => { +const WaitlistPage = (/* { account } */) => { const dispatch = useDispatch(); const intl = useIntl(); const title = useAppSelector((state) => state.instance.title); const me = useOwnAccount(); - const isSmsVerified = me.getIn(['source', 'sms_verified']); + const isSmsVerified = me?.source.get('sms_verified'); - const onClickLogOut = (event) => { + const onClickLogOut: React.MouseEventHandler = (event) => { event.preventDefault(); dispatch(logOut(intl)); }; @@ -76,8 +75,4 @@ const WaitlistPage = ({ account }) => { ); }; -WaitlistPage.propTypes = { - account: PropTypes.object, -}; - export default WaitlistPage; From e6c3f4fbdfa84d54033d344641edebf0df439cd4 Mon Sep 17 00:00:00 2001 From: Isabell Deinschnitzel <6174132-iss_dein_schnitzel@users.noreply.gitlab.com> Date: Sun, 29 May 2022 20:05:31 +0000 Subject: [PATCH 071/102] =?UTF-8?q?Update=20de.json=20Erg=C3=A4nzungen=20I?= =?UTF-8?q?II?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/soapbox/locales/de.json | 70 ++++++++++++++++++------------------- 1 file changed, 35 insertions(+), 35 deletions(-) diff --git a/app/soapbox/locales/de.json b/app/soapbox/locales/de.json index 67067f2bf..d11b7e0f2 100644 --- a/app/soapbox/locales/de.json +++ b/app/soapbox/locales/de.json @@ -148,7 +148,7 @@ "app_create.scopes_placeholder": "e.g. 'read write follow'", "app_create.submit": "App erstellen", "app_create.website_label": "Webseite", - "auth.invalid_credentials": "Falsches Passwort oder faslscher Nutzername", + "auth.invalid_credentials": "Falsches Passwort oder falscher Nutzername", "auth.logged_out": "Abgemeldet.", "backups.actions.create": "Backup erstellen", "backups.empty_message": "Kein Backup gefunden. {action}", @@ -157,13 +157,13 @@ "beta.also_available": "Available in:", "birthday_panel.title": "Birthdays", "boost_modal.combo": "Drücke {combo}, um dieses Fenster zu überspringen", - "bundle_column_error.body": "Etwas ist beim Laden schiefgelaufen.", + "bundle_column_error.body": "Beim Laden ist ein Fehler aufgetreten.", "bundle_column_error.retry": "Erneut versuchen", "bundle_column_error.title": "Netzwerkfehler", "bundle_modal_error.close": "Schließen", - "bundle_modal_error.message": "Etwas ist beim Laden schiefgelaufen.", + "bundle_modal_error.message": "Beim Laden ist ein Fehler aufgetreten.", "bundle_modal_error.retry": "Erneut versuchen", - "card.back.label": "Back", + "card.back.label": "Zurück", "chat_box.actions.send": "Senden", "chat_box.input.placeholder": "Nachricht senden…", "chat_panels.main_window.empty": "Keine Chats vorhanden. Besuche ein Nutzerprofil, um einen Chat zu starten.", @@ -191,13 +191,13 @@ "column.aliases.subheading_aliases": "Bestehende Aliases", "column.app_create": "App erstellen", "column.backups": "Backups", - "column.birthdays": "Birthdays", + "column.birthdays": "Geburtstage", "column.blocks": "Blockierte Profile", "column.bookmarks": "Lesezeichen", "column.chats": "Chats", "column.community": "Lokale Zeitleiste", "column.crypto_donate": "Mit Kryptowährungen spenden", - "column.developers": "Developers", + "column.developers": "Entwickler", "column.direct": "Direktnachrichten", "column.directory": "Profile entdecken", "column.domain_blocks": "Versteckte Domains", @@ -257,7 +257,7 @@ "column_forbidden.body": "Zugriff nicht erlaubt", "column_forbidden.title": "Zugriffsbeschränkung", "column_header.show_settings": "Einstellungen anzeigen", - "common.cancel": "Cancel", + "common.cancel": "Abbrechen", "community.column_settings.media_only": "Nur Medien", "community.column_settings.title": "Einstellungen für die lokale Zeitleiste", "compose.character_counter.title": "{chars} von {maxChars} Zeichen verwendet", @@ -294,10 +294,10 @@ "confirmations.admin.deactivate_user.message": "Hiermit wird das Konto von @{acct} deaktiviert. Diese Entscheidung kann später zurückgenommen werden.", "confirmations.admin.delete_local_user.checkbox": "Hiermit wird ein lokaler Nutzer unwiderruflich gelöscht.", "confirmations.admin.delete_status.confirm": "Beitrag löschen", - "confirmations.admin.delete_status.heading": "Delete post", + "confirmations.admin.delete_status.heading": "Beitrag löschen", "confirmations.admin.delete_status.message": "Hiermit wird ein Beitrag von @{acct} gelöscht. Der gelöschte Beitrag kann nicht wiederhergestellt werden..", "confirmations.admin.delete_user.confirm": "@{name} löschen", - "confirmations.admin.delete_user.heading": "Delete @{acct}", + "confirmations.admin.delete_user.heading": "@{acct} löschen", "confirmations.admin.delete_user.message": "Hiermit wird das Konto @{acct} unwiderruflich gelöscht. DAS KONTO KANN NICHT WIEDERHERGESTELLT WERDEN.", "confirmations.admin.mark_status_not_sensitive.confirm": "Markierung als heikel aufheben", "confirmations.admin.mark_status_not_sensitive.heading": "Mark post not sensitive.", @@ -306,7 +306,7 @@ "confirmations.admin.mark_status_sensitive.heading": "Mark post sensitive", "confirmations.admin.mark_status_sensitive.message": "Hiermit wird der Beitrag von @{acct} als heikel markiert.", "confirmations.admin.reject_user.confirm": "@{name} ablehnen", - "confirmations.admin.reject_user.heading": "Reject @{acct}", + "confirmations.admin.reject_user.heading": "@{acct} ablehnen", "confirmations.admin.reject_user.message": "Hiermit wird der Aufnahmeantrag von @{acct} abgelehnt. Diese Entscheidung kann nicht rückgängig gemacht werden.", "confirmations.block.block_and_report": "Blockieren und melden", "confirmations.block.confirm": "Blockieren", @@ -316,28 +316,28 @@ "confirmations.delete.heading": "Beitrag löschen", "confirmations.delete.message": "Bist du dir sicher, dass du diesen Beitrag löschen möchtest?", "confirmations.delete_list.confirm": "Löschen", - "confirmations.delete_list.heading": "Delete list", + "confirmations.delete_list.heading": "Liste löschen", "confirmations.delete_list.message": "Bist du dir sicher, dass du diese Liste permanent löschen möchtest?", "confirmations.domain_block.confirm": "Die ganze Domain verbergen", "confirmations.domain_block.heading": "Block {domain}", "confirmations.domain_block.message": "Bist du dir wirklich sicher, dass du die ganze Domain {domain} blockieren willst? In den meisten Fällen reichen ein paar gezielte Blockierungen oder Stummschaltungen aus. Du wirst den Inhalt von dieser Domain dann nicht mehr in irgendwelchen öffentlichen Timelines oder den Benachrichtigungen finden. Von dieser Domain kann dir auch niemand mehr folgen.", "confirmations.mute.confirm": "Stummschalten", - "confirmations.mute.heading": "Mute @{name}", + "confirmations.mute.heading": "@{name} stummschalten", "confirmations.mute.message": "Bist du dir sicher, dass du {name} stummschalten möchtest?", "confirmations.redraft.confirm": "Löschen und neu erstellen", - "confirmations.redraft.heading": "Delete & redraft", + "confirmations.redraft.heading": "Löschen und nue erstellen", "confirmations.redraft.message": "Bist du dir sicher, dass du diesen Beitrag löschen und neu erstellen möchtest? Favorisierungen, geteilte Beiträge und Antworten werden verloren gehen.", "confirmations.register.needs_approval": "Das Konto muss manuell durch den Administrator freigeschaltet werden. Die Überprüfung kann einige Zeit dauern.", - "confirmations.register.needs_approval.header": "Approval needed", + "confirmations.register.needs_approval.header": "Bestätigung erforderlich", "confirmations.register.needs_confirmation": "Im nächsten Schritt muss die angegebene Emailadresse bestätigt werden. Informationen zum weiteren Vorgehen wurden an die Adresse {email} geschickt.", - "confirmations.register.needs_confirmation.header": "Confirmation needed", + "confirmations.register.needs_confirmation.header": "Bestätigung erforderlich", "confirmations.reply.confirm": "Antworten", "confirmations.reply.message": "Wenn du jetzt antwortest, wird die gesamte Nachricht verworfen, die du gerade schreibst. Möchtest du wirklich fortfahren?", - "confirmations.scheduled_status_delete.confirm": "Cancel", - "confirmations.scheduled_status_delete.heading": "Cancel scheduled post", - "confirmations.scheduled_status_delete.message": "Are you sure you want to cancel this scheduled post?", + "confirmations.scheduled_status_delete.confirm": "Abbrechen", + "confirmations.scheduled_status_delete.heading": "Vorbereiteten Beitrag verwerfen", + "confirmations.scheduled_status_delete.message": "Den vorbereiteten Beitrag wirklich verwerfen?", "confirmations.unfollow.confirm": "Entfolgen", - "confirmations.unfollow.heading": "Unfollow {name}", + "confirmations.unfollow.heading": "{name} entfolgen", "confirmations.unfollow.message": "Bist du dir sicher, dass du {name} entfolgen möchtest?", "crypto_donate.explanation_box.message": "{siteTitle} akzeptiert Kryptowährungen. Du kannst an eine der angegebenen Adressen eine Spende senden. Danke für deine Unterstützung!", "crypto_donate.explanation_box.title": "Spenden mit Kryptowährungen", @@ -381,8 +381,8 @@ "edit_profile.fields.avatar_label": "Profilbild", "edit_profile.fields.bio_label": "Kurzbeschreibung", "edit_profile.fields.bio_placeholder": "Schreibe etwas über dich.", - "edit_profile.fields.birthday_label": "Birthday", - "edit_profile.fields.birthday_placeholder": "Your birthday", + "edit_profile.fields.birthday_label": "Geburtstag", + "edit_profile.fields.birthday_placeholder": "Dein Geburtstag", "edit_profile.fields.bot_label": "Dieses Konto wird für einen Bot genutzt", "edit_profile.fields.discoverable_label": "Allow account discovery", "edit_profile.fields.display_name_label": "Angezeigter Nutzername", @@ -538,12 +538,12 @@ "hashtag.column_header.tag_mode.all": "und {additional}", "hashtag.column_header.tag_mode.any": "oder {additional}", "hashtag.column_header.tag_mode.none": "ohne {additional}", - "header.home.label": "Home", - "header.login.forgot_password": "Forgot password?", + "header.home.label": "Start", + "header.login.forgot_password": "Passwort vergessen?", "header.login.label": "Anmelden", "header.login.password.label": "Password", - "header.login.username.placeholder": "Email or username", - "header.register.label": "Register", + "header.login.username.placeholder": "Email oder Nutzername", + "header.register.label": "Anmelden", "home.column_settings.show_direct": "Direktnachricht anzeigen", "home.column_settings.show_reblogs": "Geteilte Beiträge anzeigen", "home.column_settings.show_replies": "Antworten anzeigen", @@ -564,8 +564,8 @@ "import_data.success.blocks": "Blockliste erfolgreich importiert", "import_data.success.followers": "Followers imported successfully", "import_data.success.mutes": "Liste mit stummgeschalteten Nutzern erfolgreich importiert", - "input.password.hide_password": "Hide password", - "input.password.show_password": "Show password", + "input.password.hide_password": "Passwort verbergen", + "input.password.show_password": "Passwort anzeigen", "intervals.full.days": "{number, plural, one {# Tag} other {# Tage}}", "intervals.full.hours": "{number, plural, one {# Stunde} other {# Stunden}}", "intervals.full.minutes": "{number, plural, one {# Minute} other {# Minuten}}", @@ -667,8 +667,8 @@ "migration.submit": "Move followers", "missing_description_modal.cancel": "Abbrechen", "missing_description_modal.continue": "Senden", - "missing_description_modal.description": "Continue anyway?", - "missing_description_modal.text": "You have not entered a description for all attachments. Continue anyway?", + "missing_description_modal.description": "Dennoch fortfahren?", + "missing_description_modal.text": "Du hast nicht für alle Anhänge Beschreibungen angegeben. Dennoch fortfahren?", "missing_indicator.label": "Nicht gefunden", "missing_indicator.sublabel": "Der Eintrag konnte nicht gefunden werden", "mobile.also_available": "Available in:", @@ -678,7 +678,7 @@ "navigation.chats": "Chats", "navigation.compose": "Compose", "navigation.dashboard": "Steuerung", - "navigation.developers": "Developers", + "navigation.developers": "Entwickler", "navigation.direct_messages": "Nachrichten", "navigation.home": "Start", "navigation.invites": "Einladungen", @@ -688,7 +688,7 @@ "navigation_bar.blocks": "Blockierte Profile", "navigation_bar.compose": "Neuen Beitrag verfassen", "navigation_bar.compose_direct": "Direktnachrichten", - "navigation_bar.compose_quote": "Quote post", + "navigation_bar.compose_quote": "Beitrag zitieren", "navigation_bar.compose_reply": "Auf Beitrag antworten", "navigation_bar.domain_blocks": "Versteckte Domains", "navigation_bar.favourites": "Favoriten", @@ -720,8 +720,8 @@ "notifications.clear_confirmation": "Bist du dir sicher, dass du alle Benachrichtigungen löschen möchtest?", "notifications.clear_heading": "Clear notifications", "notifications.column_settings.alert": "Desktop-Benachrichtigungen", - "notifications.column_settings.birthdays.category": "Birthdays", - "notifications.column_settings.birthdays.show": "Show birthday reminders", + "notifications.column_settings.birthdays.category": "Geburtstage", + "notifications.column_settings.birthdays.show": "Geburtstagserinnerungen anzeigen", "notifications.column_settings.emoji_react": "Emoji-Reaktionen:", "notifications.column_settings.favourite": "Favorisierungen:", "notifications.column_settings.filter_bar.advanced": "Zeige alle Kategorien an", @@ -751,7 +751,7 @@ "notifications.group": "{count} Benachrichtigungen", "notifications.queue_label": "{count, plural, one {Eine neue Benachrichtigung} other {# neue Benachrichtigungen}}. Hier klicken, um sie anzuzeigen.", "onboarding.avatar.subtitle": "Just have fun with it.", - "onboarding.avatar.title": "Choose a profile picture", + "onboarding.avatar.title": "Profilbild auswählen", "onboarding.display_name.subtitle": "You can always edit this later.", "onboarding.display_name.title": "Choose a display name", "onboarding.done": "Done", @@ -1021,7 +1021,7 @@ "status.unmute_conversation": "Stummschaltung der Unterhaltung aufheben", "status.unpin": "Vom Profil lösen", "status_list.queue_label": "{count, plural, one {Ein neuer Beitrag} other {# neue Beiträge}}. Hier klicken, um {count, plural, one {ihn} other {sie}} anzuzeigen.", - "statuses.quote_tombstone": "Post is unavailable.", + "statuses.quote_tombstone": "Der Beitrag kann nicht angezeigt werden.", "statuses.tombstone": "Beitrag oder Beiträge können nicht angezeigt werden.", "suggestions.dismiss": "Empfehlung ausblenden", "tabs_bar.all": "All", From 88637f55fb1485b229a46db6a56fdcedc87c90a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Mon, 30 May 2022 10:01:52 +0200 Subject: [PATCH 072/102] Fix: Change scope button no longer works MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- app/soapbox/features/compose/components/privacy_dropdown.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/soapbox/features/compose/components/privacy_dropdown.js b/app/soapbox/features/compose/components/privacy_dropdown.js index 9edbd58dc..d6ddf771c 100644 --- a/app/soapbox/features/compose/components/privacy_dropdown.js +++ b/app/soapbox/features/compose/components/privacy_dropdown.js @@ -181,7 +181,7 @@ class PrivacyDropdown extends React.PureComponent { ]; } - handleToggle = ({ target }) => { + handleToggle = (e) => { if (this.props.isUserTouching()) { if (this.state.open) { this.props.onModalClose(); @@ -192,13 +192,14 @@ class PrivacyDropdown extends React.PureComponent { }); } } else { - const { top } = target.getBoundingClientRect(); + const { top } = e.target.getBoundingClientRect(); if (this.state.open && this.activeElement) { this.activeElement.focus(); } this.setState({ placement: top * 2 < innerHeight ? 'bottom' : 'top' }); this.setState({ open: !this.state.open }); } + e.stopPropagation(); } handleModalActionClick = (e) => { From beb7b8e713c4e72d8f57d824ba3dbb68e4c17d15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Mon, 30 May 2022 18:18:31 +0200 Subject: [PATCH 073/102] TypeScript, FC MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- app/soapbox/components/radio_button.tsx | 2 +- .../components/latest_accounts_panel.tsx | 3 +- .../conversations/components/conversation.js | 64 ---------- .../conversations/components/conversation.tsx | 63 ++++++++++ .../components/conversations_list.js | 79 ------------ .../components/conversations_list.tsx | 74 +++++++++++ .../containers/conversation_container.js | 20 --- .../conversations_list_container.js | 16 --- app/soapbox/features/conversations/index.js | 75 ----------- app/soapbox/features/conversations/index.tsx | 50 ++++++++ .../developers/developers_challenge.tsx | 3 +- app/soapbox/features/direct_timeline/index.js | 84 ------------- .../features/direct_timeline/index.tsx | 66 ++++++++++ .../directory/components/account_card.js | 85 ------------- .../directory/components/account_card.tsx | 82 +++++++++++++ app/soapbox/features/directory/index.js | 116 ------------------ app/soapbox/features/directory/index.tsx | 80 ++++++++++++ .../edit_password/{index.js => index.tsx} | 7 +- .../export_data/components/csv_exporter.tsx | 3 +- .../components/restricted_instance.js | 61 --------- .../components/restricted_instance.tsx | 47 +++++++ .../features/federation_restrictions/index.js | 76 ------------ .../federation_restrictions/index.tsx | 62 ++++++++++ .../import_data/components/csv_importer.tsx | 3 +- app/soapbox/features/new_status/index.tsx | 3 +- .../features/reply_mentions/account.tsx | 3 +- .../ui/components/reactions_modal.tsx | 3 +- app/soapbox/features/ui/index.tsx | 2 +- 28 files changed, 536 insertions(+), 696 deletions(-) delete mode 100644 app/soapbox/features/conversations/components/conversation.js create mode 100644 app/soapbox/features/conversations/components/conversation.tsx delete mode 100644 app/soapbox/features/conversations/components/conversations_list.js create mode 100644 app/soapbox/features/conversations/components/conversations_list.tsx delete mode 100644 app/soapbox/features/conversations/containers/conversation_container.js delete mode 100644 app/soapbox/features/conversations/containers/conversations_list_container.js delete mode 100644 app/soapbox/features/conversations/index.js create mode 100644 app/soapbox/features/conversations/index.tsx delete mode 100644 app/soapbox/features/direct_timeline/index.js create mode 100644 app/soapbox/features/direct_timeline/index.tsx delete mode 100644 app/soapbox/features/directory/components/account_card.js create mode 100644 app/soapbox/features/directory/components/account_card.tsx delete mode 100644 app/soapbox/features/directory/index.js create mode 100644 app/soapbox/features/directory/index.tsx rename app/soapbox/features/edit_password/{index.js => index.tsx} (96%) delete mode 100644 app/soapbox/features/federation_restrictions/components/restricted_instance.js create mode 100644 app/soapbox/features/federation_restrictions/components/restricted_instance.tsx delete mode 100644 app/soapbox/features/federation_restrictions/index.js create mode 100644 app/soapbox/features/federation_restrictions/index.tsx diff --git a/app/soapbox/components/radio_button.tsx b/app/soapbox/components/radio_button.tsx index c3f87ce02..b317e4259 100644 --- a/app/soapbox/components/radio_button.tsx +++ b/app/soapbox/components/radio_button.tsx @@ -6,7 +6,7 @@ interface IRadioButton { checked?: boolean, name: string, onChange: React.ChangeEventHandler, - label: JSX.Element, + label: React.ReactNode, } const RadioButton: React.FC = ({ name, value, checked, onChange, label }) => ( diff --git a/app/soapbox/features/admin/components/latest_accounts_panel.tsx b/app/soapbox/features/admin/components/latest_accounts_panel.tsx index 7998c18d2..804744d5e 100644 --- a/app/soapbox/features/admin/components/latest_accounts_panel.tsx +++ b/app/soapbox/features/admin/components/latest_accounts_panel.tsx @@ -1,6 +1,5 @@ import { OrderedSet as ImmutableOrderedSet, is } from 'immutable'; -import React, { useState } from 'react'; -import { useEffect } from 'react'; +import React, { useEffect, useState } from 'react'; import { defineMessages, useIntl } from 'react-intl'; import { useHistory } from 'react-router-dom'; diff --git a/app/soapbox/features/conversations/components/conversation.js b/app/soapbox/features/conversations/components/conversation.js deleted file mode 100644 index 717949968..000000000 --- a/app/soapbox/features/conversations/components/conversation.js +++ /dev/null @@ -1,64 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import ImmutablePureComponent from 'react-immutable-pure-component'; -import { withRouter } from 'react-router-dom'; - -import StatusContainer from '../../../containers/status_container'; - -export default @withRouter -class Conversation extends ImmutablePureComponent { - - static propTypes = { - conversationId: PropTypes.string.isRequired, - accounts: ImmutablePropTypes.list.isRequired, - lastStatusId: PropTypes.string, - unread: PropTypes.bool.isRequired, - onMoveUp: PropTypes.func, - onMoveDown: PropTypes.func, - markRead: PropTypes.func.isRequired, - history: PropTypes.object, - }; - - handleClick = () => { - if (!this.props.history) { - return; - } - - const { lastStatusId, unread, markRead } = this.props; - - if (unread) { - markRead(); - } - - this.props.history.push(`/statuses/${lastStatusId}`); - } - - handleHotkeyMoveUp = () => { - this.props.onMoveUp(this.props.conversationId); - } - - handleHotkeyMoveDown = () => { - this.props.onMoveDown(this.props.conversationId); - } - - render() { - const { accounts, lastStatusId, unread } = this.props; - - if (lastStatusId === null) { - return null; - } - - return ( - - ); - } - -} diff --git a/app/soapbox/features/conversations/components/conversation.tsx b/app/soapbox/features/conversations/components/conversation.tsx new file mode 100644 index 000000000..995af88b3 --- /dev/null +++ b/app/soapbox/features/conversations/components/conversation.tsx @@ -0,0 +1,63 @@ +import React from 'react'; +import { useHistory } from 'react-router-dom'; + +import { markConversationRead } from 'soapbox/actions/conversations'; +import StatusContainer from 'soapbox/containers/status_container'; +import { useAppDispatch, useAppSelector } from 'soapbox/hooks'; + +import type { Map as ImmutableMap } from 'immutable'; + +interface IConversation { + conversationId: string, + onMoveUp: (id: string) => void, + onMoveDown: (id: string) => void, +} + +const Conversation: React.FC = ({ conversationId, onMoveUp, onMoveDown }) => { + const dispatch = useAppDispatch(); + const history = useHistory(); + + const { accounts, unread, lastStatusId } = useAppSelector((state) => { + const conversation = state.conversations.get('items').find((x: ImmutableMap) => x.get('id') === conversationId); + + return { + accounts: conversation.get('accounts').map((accountId: string) => state.accounts.get(accountId, null)), + unread: conversation.get('unread'), + lastStatusId: conversation.get('last_status', null), + }; + }); + + const handleClick = () => { + if (unread) { + dispatch(markConversationRead(conversationId)); + } + + history.push(`/statuses/${lastStatusId}`); + }; + + const handleHotkeyMoveUp = () => { + onMoveUp(conversationId); + }; + + const handleHotkeyMoveDown = () => { + onMoveDown(conversationId); + }; + + if (lastStatusId === null) { + return null; + } + + return ( + + ); +}; + +export default Conversation; diff --git a/app/soapbox/features/conversations/components/conversations_list.js b/app/soapbox/features/conversations/components/conversations_list.js deleted file mode 100644 index 6e2f751a9..000000000 --- a/app/soapbox/features/conversations/components/conversations_list.js +++ /dev/null @@ -1,79 +0,0 @@ -import { debounce } from 'lodash'; -import PropTypes from 'prop-types'; -import React from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import ImmutablePureComponent from 'react-immutable-pure-component'; - -import ScrollableList from '../../../components/scrollable_list'; -import ConversationContainer from '../containers/conversation_container'; - -export default class ConversationsList extends ImmutablePureComponent { - - static propTypes = { - conversations: ImmutablePropTypes.list.isRequired, - hasMore: PropTypes.bool, - isLoading: PropTypes.bool, - onLoadMore: PropTypes.func, - }; - - getCurrentIndex = id => this.props.conversations.findIndex(x => x.get('id') === id) - - handleMoveUp = id => { - const elementIndex = this.getCurrentIndex(id) - 1; - this._selectChild(elementIndex); - } - - handleMoveDown = id => { - const elementIndex = this.getCurrentIndex(id) + 1; - this._selectChild(elementIndex); - } - - _selectChild(index) { - this.node.scrollIntoView({ - index, - behavior: 'smooth', - done: () => { - const element = document.querySelector(`#direct-list [data-index="${index}"] .focusable`); - - if (element) { - element.focus(); - } - }, - }); - } - - setRef = c => { - this.node = c; - } - - handleLoadOlder = debounce(() => { - const maxId = this.props.conversations.getIn([-1, 'id']); - if (maxId) this.props.onLoadMore(maxId); - }, 300, { leading: true }) - - render() { - const { conversations, isLoading, onLoadMore, ...other } = this.props; - - return ( - - {conversations.map(item => ( - - ))} - - ); - } - -} diff --git a/app/soapbox/features/conversations/components/conversations_list.tsx b/app/soapbox/features/conversations/components/conversations_list.tsx new file mode 100644 index 000000000..e7bc79078 --- /dev/null +++ b/app/soapbox/features/conversations/components/conversations_list.tsx @@ -0,0 +1,74 @@ +import { debounce } from 'lodash'; +import React from 'react'; +import { useRef } from 'react'; +import { FormattedMessage } from 'react-intl'; + +import { expandConversations } from 'soapbox/actions/conversations'; +import ScrollableList from 'soapbox/components/scrollable_list'; +import { useAppDispatch, useAppSelector } from 'soapbox/hooks'; + +import Conversation from '../components/conversation'; + +import type { VirtuosoHandle } from 'react-virtuoso'; + +const ConversationsList: React.FC = () => { + const dispatch = useAppDispatch(); + const ref = useRef(null); + + const conversations = useAppSelector((state) => state.conversations.get('items')); + const isLoading = useAppSelector((state) => state.conversations.get('isLoading', true)); + + const getCurrentIndex = (id: string) => conversations.findIndex((x: any) => x.get('id') === id); + + const handleMoveUp = (id: string) => { + const elementIndex = getCurrentIndex(id) - 1; + selectChild(elementIndex); + }; + + const handleMoveDown = (id: string) => { + const elementIndex = getCurrentIndex(id) + 1; + selectChild(elementIndex); + }; + + const selectChild = (index: number) => { + ref.current?.scrollIntoView({ + index, + behavior: 'smooth', + done: () => { + const element = document.querySelector(`#direct-list [data-index="${index}"] .focusable`); + + if (element) { + element.focus(); + } + }, + }); + }; + + const handleLoadOlder = debounce(() => { + const maxId = conversations.getIn([-1, 'id']); + if (maxId) dispatch(expandConversations({ maxId })); + }, 300, { leading: true }); + + return ( + } + > + {conversations.map((item: any) => ( + + ))} + + ); +}; + +export default ConversationsList; diff --git a/app/soapbox/features/conversations/containers/conversation_container.js b/app/soapbox/features/conversations/containers/conversation_container.js deleted file mode 100644 index 885fada9c..000000000 --- a/app/soapbox/features/conversations/containers/conversation_container.js +++ /dev/null @@ -1,20 +0,0 @@ -import { connect } from 'react-redux'; - -import { markConversationRead } from '../../../actions/conversations'; -import Conversation from '../components/conversation'; - -const mapStateToProps = (state, { conversationId }) => { - const conversation = state.getIn(['conversations', 'items']).find(x => x.get('id') === conversationId); - - return { - accounts: conversation.get('accounts').map(accountId => state.getIn(['accounts', accountId], null)), - unread: conversation.get('unread'), - lastStatusId: conversation.get('last_status', null), - }; -}; - -const mapDispatchToProps = (dispatch, { conversationId }) => ({ - markRead: () => dispatch(markConversationRead(conversationId)), -}); - -export default connect(mapStateToProps, mapDispatchToProps)(Conversation); diff --git a/app/soapbox/features/conversations/containers/conversations_list_container.js b/app/soapbox/features/conversations/containers/conversations_list_container.js deleted file mode 100644 index 1dcd3ec1b..000000000 --- a/app/soapbox/features/conversations/containers/conversations_list_container.js +++ /dev/null @@ -1,16 +0,0 @@ -import { connect } from 'react-redux'; - -import { expandConversations } from '../../../actions/conversations'; -import ConversationsList from '../components/conversations_list'; - -const mapStateToProps = state => ({ - conversations: state.getIn(['conversations', 'items']), - isLoading: state.getIn(['conversations', 'isLoading'], true), - hasMore: state.getIn(['conversations', 'hasMore'], false), -}); - -const mapDispatchToProps = dispatch => ({ - onLoadMore: maxId => dispatch(expandConversations({ maxId })), -}); - -export default connect(mapStateToProps, mapDispatchToProps)(ConversationsList); diff --git a/app/soapbox/features/conversations/index.js b/app/soapbox/features/conversations/index.js deleted file mode 100644 index 6d4b2ec22..000000000 --- a/app/soapbox/features/conversations/index.js +++ /dev/null @@ -1,75 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; -import { connect } from 'react-redux'; - -import { directComposeById } from 'soapbox/actions/compose'; -import AccountSearch from 'soapbox/components/account_search'; - -import { mountConversations, unmountConversations, expandConversations } from '../../actions/conversations'; -import { connectDirectStream } from '../../actions/streaming'; -import { Column } from '../../components/ui'; - -import ConversationsListContainer from './containers/conversations_list_container'; - -const messages = defineMessages({ - title: { id: 'column.direct', defaultMessage: 'Direct messages' }, - searchPlaceholder: { id: 'direct.search_placeholder', defaultMessage: 'Send a message to…' }, -}); - -export default @connect() -@injectIntl -class ConversationsTimeline extends React.PureComponent { - - static propTypes = { - dispatch: PropTypes.func.isRequired, - intl: PropTypes.object.isRequired, - hasUnread: PropTypes.bool, - }; - - componentDidMount() { - const { dispatch } = this.props; - - dispatch(mountConversations()); - dispatch(expandConversations()); - this.disconnect = dispatch(connectDirectStream()); - } - - componentWillUnmount() { - this.props.dispatch(unmountConversations()); - - if (this.disconnect) { - this.disconnect(); - this.disconnect = null; - } - } - - handleSuggestion = accountId => { - this.props.dispatch(directComposeById(accountId)); - } - - handleLoadMore = maxId => { - this.props.dispatch(expandConversations({ maxId })); - } - - render() { - const { intl } = this.props; - - return ( - - - - } - /> - - ); - } - -} diff --git a/app/soapbox/features/conversations/index.tsx b/app/soapbox/features/conversations/index.tsx new file mode 100644 index 000000000..8e81ac1c5 --- /dev/null +++ b/app/soapbox/features/conversations/index.tsx @@ -0,0 +1,50 @@ +import React, { useEffect } from 'react'; +import { defineMessages, useIntl } from 'react-intl'; + +import { directComposeById } from 'soapbox/actions/compose'; +import { mountConversations, unmountConversations, expandConversations } from 'soapbox/actions/conversations'; +import { connectDirectStream } from 'soapbox/actions/streaming'; +import AccountSearch from 'soapbox/components/account_search'; +import { Column } from 'soapbox/components/ui'; +import { useAppDispatch } from 'soapbox/hooks'; + +import ConversationsList from './components/conversations_list'; + +const messages = defineMessages({ + title: { id: 'column.direct', defaultMessage: 'Direct messages' }, + searchPlaceholder: { id: 'direct.search_placeholder', defaultMessage: 'Send a message to…' }, +}); + +const ConversationsTimeline = () => { + const intl = useIntl(); + const dispatch = useAppDispatch(); + + useEffect(() => { + dispatch(mountConversations()); + dispatch(expandConversations()); + + const disconnect = dispatch(connectDirectStream()); + + return () => { + dispatch(unmountConversations()); + disconnect(); + }; + }, []); + + const handleSuggestion = (accountId: string) => { + dispatch(directComposeById(accountId)); + }; + + return ( + + + + + + ); +}; + +export default ConversationsTimeline; diff --git a/app/soapbox/features/developers/developers_challenge.tsx b/app/soapbox/features/developers/developers_challenge.tsx index d27b46429..50d107384 100644 --- a/app/soapbox/features/developers/developers_challenge.tsx +++ b/app/soapbox/features/developers/developers_challenge.tsx @@ -1,5 +1,4 @@ -import React from 'react'; -import { useState } from 'react'; +import React, { useState } from 'react'; import { FormattedMessage, defineMessages, useIntl } from 'react-intl'; import { useDispatch } from 'react-redux'; diff --git a/app/soapbox/features/direct_timeline/index.js b/app/soapbox/features/direct_timeline/index.js deleted file mode 100644 index b09a58158..000000000 --- a/app/soapbox/features/direct_timeline/index.js +++ /dev/null @@ -1,84 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; -import { connect } from 'react-redux'; - -import { directComposeById } from 'soapbox/actions/compose'; -import AccountSearch from 'soapbox/components/account_search'; - -import { connectDirectStream } from '../../actions/streaming'; -import { expandDirectTimeline } from '../../actions/timelines'; -import ColumnHeader from '../../components/column_header'; -import { Column } from '../../components/ui'; -import StatusListContainer from '../ui/containers/status_list_container'; - -const messages = defineMessages({ - title: { id: 'column.direct', defaultMessage: 'Direct messages' }, - searchPlaceholder: { id: 'direct.search_placeholder', defaultMessage: 'Send a message to…' }, -}); - -const mapStateToProps = state => ({ - hasUnread: state.getIn(['timelines', 'direct', 'unread']) > 0, -}); - -export default @connect(mapStateToProps) -@injectIntl -class DirectTimeline extends React.PureComponent { - - static propTypes = { - dispatch: PropTypes.func.isRequired, - intl: PropTypes.object.isRequired, - hasUnread: PropTypes.bool, - }; - - componentDidMount() { - const { dispatch } = this.props; - - dispatch(expandDirectTimeline()); - this.disconnect = dispatch(connectDirectStream()); - } - - componentWillUnmount() { - if (this.disconnect) { - this.disconnect(); - this.disconnect = null; - } - } - - handleSuggestion = accountId => { - this.props.dispatch(directComposeById(accountId)); - } - - handleLoadMore = maxId => { - this.props.dispatch(expandDirectTimeline({ maxId })); - } - - render() { - const { intl, hasUnread } = this.props; - - return ( - - - - - - } - divideType='space' - /> - - ); - } - -} diff --git a/app/soapbox/features/direct_timeline/index.tsx b/app/soapbox/features/direct_timeline/index.tsx new file mode 100644 index 000000000..5244533a8 --- /dev/null +++ b/app/soapbox/features/direct_timeline/index.tsx @@ -0,0 +1,66 @@ +import React, { useEffect } from 'react'; +import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; + +import { directComposeById } from 'soapbox/actions/compose'; +import { connectDirectStream } from 'soapbox/actions/streaming'; +import { expandDirectTimeline } from 'soapbox/actions/timelines'; +import AccountSearch from 'soapbox/components/account_search'; +import ColumnHeader from 'soapbox/components/column_header'; +import { Column } from 'soapbox/components/ui'; +import { useAppDispatch, useAppSelector } from 'soapbox/hooks'; + +import StatusListContainer from '../ui/containers/status_list_container'; + +const messages = defineMessages({ + title: { id: 'column.direct', defaultMessage: 'Direct messages' }, + searchPlaceholder: { id: 'direct.search_placeholder', defaultMessage: 'Send a message to…' }, +}); + +const DirectTimeline = () => { + const intl = useIntl(); + const dispatch = useAppDispatch(); + + const hasUnread = useAppSelector((state) => state.timelines.getIn(['direct', 'unread']) > 0); + + useEffect(() => { + dispatch(expandDirectTimeline()); + const disconnect = dispatch(connectDirectStream()); + + return (() => { + disconnect(); + }); + }, []); + + const handleSuggestion = (accountId: string) => { + dispatch(directComposeById(accountId)); + }; + + const handleLoadMore = (maxId: string) => { + dispatch(expandDirectTimeline({ maxId })); + }; + + return ( + + + + + + } + divideType='space' + /> + + ); +}; + +export default DirectTimeline; diff --git a/app/soapbox/features/directory/components/account_card.js b/app/soapbox/features/directory/components/account_card.js deleted file mode 100644 index 542a05344..000000000 --- a/app/soapbox/features/directory/components/account_card.js +++ /dev/null @@ -1,85 +0,0 @@ -import classNames from 'classnames'; -import PropTypes from 'prop-types'; -import React from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import ImmutablePureComponent from 'react-immutable-pure-component'; -import { FormattedMessage, injectIntl } from 'react-intl'; -import { connect } from 'react-redux'; - -import { getSettings } from 'soapbox/actions/settings'; -import Avatar from 'soapbox/components/avatar'; -import DisplayName from 'soapbox/components/display-name'; -import Permalink from 'soapbox/components/permalink'; -import RelativeTimestamp from 'soapbox/components/relative_timestamp'; -import { Text } from 'soapbox/components/ui'; -import ActionButton from 'soapbox/features/ui/components/action-button'; -import { makeGetAccount } from 'soapbox/selectors'; -import { shortNumberFormat } from 'soapbox/utils/numbers'; -import SoapboxPropTypes from 'soapbox/utils/soapbox_prop_types'; - -const makeMapStateToProps = () => { - const getAccount = makeGetAccount(); - - const mapStateToProps = (state, { id }) => ({ - me: state.get('me'), - account: getAccount(state, id), - autoPlayGif: getSettings(state).get('autoPlayGif'), - }); - - return mapStateToProps; -}; - -export default @injectIntl -@connect(makeMapStateToProps) -class AccountCard extends ImmutablePureComponent { - - static propTypes = { - me: SoapboxPropTypes.me, - account: ImmutablePropTypes.record.isRequired, - autoPlayGif: PropTypes.bool, - }; - - render() { - const { account, autoPlayGif, me } = this.props; - - const followedBy = me !== account.get('id') && account.getIn(['relationship', 'followed_by']); - - return ( -
- {followedBy && -
- - - -
} -
- -
-
- -
- -
- - - - -
- -
-

') && 'empty')} - dangerouslySetInnerHTML={{ __html: account.get('note_emojified') }} - /> -
- -
-
{shortNumberFormat(account.get('statuses_count'))}
-
{shortNumberFormat(account.get('followers_count'))}
-
{account.get('last_status_at') === null ? : }
-
-
- ); - } - -} diff --git a/app/soapbox/features/directory/components/account_card.tsx b/app/soapbox/features/directory/components/account_card.tsx new file mode 100644 index 000000000..0b5a74b8b --- /dev/null +++ b/app/soapbox/features/directory/components/account_card.tsx @@ -0,0 +1,82 @@ +import classNames from 'classnames'; +import React from 'react'; +import { FormattedMessage } from 'react-intl'; + +import { getSettings } from 'soapbox/actions/settings'; +import Avatar from 'soapbox/components/avatar'; +import DisplayName from 'soapbox/components/display-name'; +import Permalink from 'soapbox/components/permalink'; +import RelativeTimestamp from 'soapbox/components/relative_timestamp'; +import { Text } from 'soapbox/components/ui'; +import ActionButton from 'soapbox/features/ui/components/action-button'; +import { useAppSelector } from 'soapbox/hooks'; +import { makeGetAccount } from 'soapbox/selectors'; +import { shortNumberFormat } from 'soapbox/utils/numbers'; + +const getAccount = makeGetAccount(); + +interface IAccountCard { + id: string, +} + +const AccountCard: React.FC = ({ id }) => { + const me = useAppSelector((state) => state.me); + const account = useAppSelector((state) => getAccount(state, id)); + const autoPlayGif = useAppSelector((state) => getSettings(state).get('autoPlayGif')); + + if (!account) return null; + + const followedBy = me !== account.id && account.relationship.get('followed_by'); + + return ( +
+ {followedBy && +
+ + + +
} +
+ +
+
+ +
+ +
+ + + + +
+ +
+

') && 'empty')} + dangerouslySetInnerHTML={{ __html: account.note_emojified }} + /> +
+ +
+
+ + {shortNumberFormat(account.statuses_count)} + +
+
+ + {shortNumberFormat(account.followers_count)} + + +
+
+ {account.last_status_at === null + ? + : } +
+
+
+ ); +}; + +export default AccountCard; diff --git a/app/soapbox/features/directory/index.js b/app/soapbox/features/directory/index.js deleted file mode 100644 index 37795a4e9..000000000 --- a/app/soapbox/features/directory/index.js +++ /dev/null @@ -1,116 +0,0 @@ -import classNames from 'classnames'; -import { List as ImmutableList } from 'immutable'; -import PropTypes from 'prop-types'; -import React from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import { defineMessages, injectIntl } from 'react-intl'; -import { connect } from 'react-redux'; - -import { fetchDirectory, expandDirectory } from 'soapbox/actions/directory'; -import LoadMore from 'soapbox/components/load_more'; -import RadioButton from 'soapbox/components/radio_button'; -import Column from 'soapbox/features/ui/components/column'; -import { getFeatures } from 'soapbox/utils/features'; - -import AccountCard from './components/account_card'; - -const messages = defineMessages({ - title: { id: 'column.directory', defaultMessage: 'Browse profiles' }, - recentlyActive: { id: 'directory.recently_active', defaultMessage: 'Recently active' }, - newArrivals: { id: 'directory.new_arrivals', defaultMessage: 'New arrivals' }, - local: { id: 'directory.local', defaultMessage: 'From {domain} only' }, - federated: { id: 'directory.federated', defaultMessage: 'From known fediverse' }, -}); - -const mapStateToProps = state => ({ - accountIds: state.getIn(['user_lists', 'directory', 'items'], ImmutableList()), - isLoading: state.getIn(['user_lists', 'directory', 'isLoading'], true), - title: state.getIn(['instance', 'title']), - features: getFeatures(state.get('instance')), -}); - -export default @connect(mapStateToProps) -@injectIntl -class Directory extends React.PureComponent { - - static propTypes = { - isLoading: PropTypes.bool, - accountIds: ImmutablePropTypes.list.isRequired, - dispatch: PropTypes.func.isRequired, - intl: PropTypes.object.isRequired, - title: PropTypes.string.isRequired, - params: PropTypes.shape({ - order: PropTypes.string, - local: PropTypes.bool, - }), - features: PropTypes.object.isRequired, - }; - - state = { - order: null, - local: null, - }; - - getParams = (props, state) => ({ - order: state.order === null ? (props.params.order || 'active') : state.order, - local: state.local === null ? (props.params.local || false) : state.local, - }); - - componentDidMount() { - const { dispatch } = this.props; - dispatch(fetchDirectory(this.getParams(this.props, this.state))); - } - - componentDidUpdate(prevProps, prevState) { - const { dispatch } = this.props; - const paramsOld = this.getParams(prevProps, prevState); - const paramsNew = this.getParams(this.props, this.state); - - if (paramsOld.order !== paramsNew.order || paramsOld.local !== paramsNew.local) { - dispatch(fetchDirectory(paramsNew)); - } - } - - handleChangeOrder = e => { - this.setState({ order: e.target.value }); - } - - handleChangeLocal = e => { - this.setState({ local: e.target.value === '1' }); - } - - handleLoadMore = () => { - const { dispatch } = this.props; - dispatch(expandDirectory(this.getParams(this.props, this.state))); - } - - render() { - const { isLoading, accountIds, intl, title, features } = this.props; - const { order, local } = this.getParams(this.props, this.state); - - return ( - -
-
- - -
- - {features.federating && ( -
- - -
- )} -
- -
- {accountIds.map(accountId => )} -
- - -
- ); - } - -} \ No newline at end of file diff --git a/app/soapbox/features/directory/index.tsx b/app/soapbox/features/directory/index.tsx new file mode 100644 index 000000000..c4bb5b4d8 --- /dev/null +++ b/app/soapbox/features/directory/index.tsx @@ -0,0 +1,80 @@ +import classNames from 'classnames'; +import { List as ImmutableList } from 'immutable'; +import React, { useEffect, useState } from 'react'; +import { defineMessages, useIntl } from 'react-intl'; +import { useDispatch } from 'react-redux'; +import { useLocation } from 'react-router-dom'; + +import { fetchDirectory, expandDirectory } from 'soapbox/actions/directory'; +import LoadMore from 'soapbox/components/load_more'; +import RadioButton from 'soapbox/components/radio_button'; +import Column from 'soapbox/features/ui/components/column'; +import { useAppSelector } from 'soapbox/hooks'; +import { getFeatures } from 'soapbox/utils/features'; + +import AccountCard from './components/account_card'; + +const messages = defineMessages({ + title: { id: 'column.directory', defaultMessage: 'Browse profiles' }, + recentlyActive: { id: 'directory.recently_active', defaultMessage: 'Recently active' }, + newArrivals: { id: 'directory.new_arrivals', defaultMessage: 'New arrivals' }, + local: { id: 'directory.local', defaultMessage: 'From {domain} only' }, + federated: { id: 'directory.federated', defaultMessage: 'From known fediverse' }, +}); + +const Directory = () => { + const intl = useIntl(); + const dispatch = useDispatch(); + const { search } = useLocation(); + const params = new URLSearchParams(search); + + const accountIds = useAppSelector((state) => state.user_lists.getIn(['directory', 'items'], ImmutableList())); + const isLoading = useAppSelector((state) => state.user_lists.getIn(['directory', 'isLoading'], true)); + const title = useAppSelector((state) => state.instance.get('title')); + const features = useAppSelector((state) => getFeatures(state.instance)); + + const [order, setOrder] = useState(params.get('order') || 'active'); + const [local, setLocal] = useState(!!params.get('local')); + + useEffect(() => { + dispatch(fetchDirectory({ order: order || 'active', local: local || false })); + }, [order, local]); + + const handleChangeOrder: React.ChangeEventHandler = e => { + setOrder(e.target.value); + }; + + const handleChangeLocal: React.ChangeEventHandler = e => { + setLocal(e.target.value === '1'); + }; + + const handleLoadMore = () => { + dispatch(expandDirectory({ order: order || 'active', local: local || false })); + }; + + return ( + +
+
+ + +
+ + {features.federating && ( +
+ + +
+ )} +
+ +
+ {accountIds.map((accountId: string) => )} +
+ + +
+ ); +}; + +export default Directory; diff --git a/app/soapbox/features/edit_password/index.js b/app/soapbox/features/edit_password/index.tsx similarity index 96% rename from app/soapbox/features/edit_password/index.js rename to app/soapbox/features/edit_password/index.tsx index a007b24bc..e95e6dec4 100644 --- a/app/soapbox/features/edit_password/index.js +++ b/app/soapbox/features/edit_password/index.tsx @@ -1,11 +1,10 @@ import * as React from 'react'; import { defineMessages, useIntl } from 'react-intl'; -import { useDispatch } from 'react-redux'; import { changePassword } from 'soapbox/actions/security'; import snackbar from 'soapbox/actions/snackbar'; - -import { Button, Card, CardBody, CardHeader, CardTitle, Column, Form, FormActions, FormGroup, Input } from '../../components/ui'; +import { Button, Card, CardBody, CardHeader, CardTitle, Column, Form, FormActions, FormGroup, Input } from 'soapbox/components/ui'; +import { useAppDispatch } from 'soapbox/hooks'; const messages = defineMessages({ updatePasswordSuccess: { id: 'security.update_password.success', defaultMessage: 'Password successfully updated.' }, @@ -22,7 +21,7 @@ const initialState = { currentPassword: '', newPassword: '', newPasswordConfirma const EditPassword = () => { const intl = useIntl(); - const dispatch = useDispatch(); + const dispatch = useAppDispatch(); const [state, setState] = React.useState(initialState); const [isLoading, setLoading] = React.useState(false); diff --git a/app/soapbox/features/export_data/components/csv_exporter.tsx b/app/soapbox/features/export_data/components/csv_exporter.tsx index f44aaa424..de524bcce 100644 --- a/app/soapbox/features/export_data/components/csv_exporter.tsx +++ b/app/soapbox/features/export_data/components/csv_exporter.tsx @@ -1,5 +1,4 @@ -import React from 'react'; -import { useState } from 'react'; +import React, { useState } from 'react'; import { MessageDescriptor, useIntl } from 'react-intl'; import { Button, Form, FormActions, Text } from 'soapbox/components/ui'; diff --git a/app/soapbox/features/federation_restrictions/components/restricted_instance.js b/app/soapbox/features/federation_restrictions/components/restricted_instance.js deleted file mode 100644 index 38ca1b602..000000000 --- a/app/soapbox/features/federation_restrictions/components/restricted_instance.js +++ /dev/null @@ -1,61 +0,0 @@ -import classNames from 'classnames'; -import PropTypes from 'prop-types'; -import React from 'react'; -import ImmutablePureComponent from 'react-immutable-pure-component'; -import { connect } from 'react-redux'; - -import Icon from 'soapbox/components/icon'; -import { makeGetRemoteInstance } from 'soapbox/selectors'; - -import InstanceRestrictions from './instance_restrictions'; - -const getRemoteInstance = makeGetRemoteInstance(); - -const mapStateToProps = (state, ownProps) => { - return { - remoteInstance: getRemoteInstance(state, ownProps.host), - }; -}; - -export default @connect(mapStateToProps) -class RestrictedInstance extends ImmutablePureComponent { - - static propTypes = { - host: PropTypes.string.isRequired, - } - - state = { - expanded: false, - } - - toggleExpanded = e => { - this.setState({ expanded: !this.state.expanded }); - e.preventDefault(); - } - - render() { - const { remoteInstance } = this.props; - const { expanded } = this.state; - - return ( - - ); - } - -} diff --git a/app/soapbox/features/federation_restrictions/components/restricted_instance.tsx b/app/soapbox/features/federation_restrictions/components/restricted_instance.tsx new file mode 100644 index 000000000..789cb215f --- /dev/null +++ b/app/soapbox/features/federation_restrictions/components/restricted_instance.tsx @@ -0,0 +1,47 @@ +import classNames from 'classnames'; +import React, { useState } from 'react'; + +import Icon from 'soapbox/components/icon'; +import { useAppSelector } from 'soapbox/hooks'; +import { makeGetRemoteInstance } from 'soapbox/selectors'; + +import InstanceRestrictions from './instance_restrictions'; + +const getRemoteInstance = makeGetRemoteInstance(); + +interface IRestrictedInstance { + host: string, +} + +const RestrictedInstance: React.FC = ({ host }) => { + const remoteInstance: any = useAppSelector((state) => getRemoteInstance(state, host)); + + const [expanded, setExpanded] = useState(false); + + const toggleExpanded: React.MouseEventHandler = e => { + setExpanded((value) => !value); + e.preventDefault(); + }; + + return ( + + ); +}; + +export default RestrictedInstance; diff --git a/app/soapbox/features/federation_restrictions/index.js b/app/soapbox/features/federation_restrictions/index.js deleted file mode 100644 index bb5ccdab2..000000000 --- a/app/soapbox/features/federation_restrictions/index.js +++ /dev/null @@ -1,76 +0,0 @@ -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 ScrollableList from 'soapbox/components/scrollable_list'; -import Accordion from 'soapbox/features/ui/components/accordion'; -import { makeGetHosts } from 'soapbox/selectors'; -import { federationRestrictionsDisclosed } from 'soapbox/utils/state'; - -import Column from '../ui/components/column'; - -import RestrictedInstance from './components/restricted_instance'; - -const messages = defineMessages({ - heading: { id: 'column.federation_restrictions', defaultMessage: 'Federation Restrictions' }, - boxTitle: { id: 'federation_restrictions.explanation_box.title', defaultMessage: 'Instance-specific policies' }, - boxMessage: { id: 'federation_restrictions.explanation_box.message', defaultMessage: 'Normally servers on the Fediverse can communicate freely. {siteTitle} has imposed restrictions on the following servers.' }, - emptyMessage: { id: 'federation_restrictions.empty_message', defaultMessage: '{siteTitle} has not restricted any instances.' }, - notDisclosed: { id: 'federation_restrictions.not_disclosed_message', defaultMessage: '{siteTitle} does not disclose federation restrictions through the API.' }, -}); - -const getHosts = makeGetHosts(); - -const mapStateToProps = state => ({ - siteTitle: state.getIn(['instance', 'title']), - hosts: getHosts(state), - disclosed: federationRestrictionsDisclosed(state), -}); - -export default @connect(mapStateToProps) -@injectIntl -class FederationRestrictions extends ImmutablePureComponent { - - static propTypes = { - intl: PropTypes.object.isRequired, - disclosed: PropTypes.bool, - }; - - state = { - explanationBoxExpanded: true, - } - - toggleExplanationBox = setting => { - this.setState({ explanationBoxExpanded: setting }); - } - - render() { - const { intl, hosts, siteTitle, disclosed } = this.props; - const { explanationBoxExpanded } = this.state; - - const emptyMessage = disclosed ? messages.emptyMessage : messages.notDisclosed; - - return ( - -
- - {intl.formatMessage(messages.boxMessage, { siteTitle })} - -
- -
- - {hosts.map(host => )} - -
-
- ); - } - -} diff --git a/app/soapbox/features/federation_restrictions/index.tsx b/app/soapbox/features/federation_restrictions/index.tsx new file mode 100644 index 000000000..396f527a0 --- /dev/null +++ b/app/soapbox/features/federation_restrictions/index.tsx @@ -0,0 +1,62 @@ +import React, { useState } from 'react'; +import { defineMessages, useIntl } from 'react-intl'; + +import ScrollableList from 'soapbox/components/scrollable_list'; +import Accordion from 'soapbox/features/ui/components/accordion'; +import { useAppSelector } from 'soapbox/hooks'; +import { makeGetHosts } from 'soapbox/selectors'; +import { federationRestrictionsDisclosed } from 'soapbox/utils/state'; + +import Column from '../ui/components/column'; + +import RestrictedInstance from './components/restricted_instance'; + +import type { OrderedSet as ImmutableOrderedSet } from 'immutable'; + +const messages = defineMessages({ + heading: { id: 'column.federation_restrictions', defaultMessage: 'Federation Restrictions' }, + boxTitle: { id: 'federation_restrictions.explanation_box.title', defaultMessage: 'Instance-specific policies' }, + boxMessage: { id: 'federation_restrictions.explanation_box.message', defaultMessage: 'Normally servers on the Fediverse can communicate freely. {siteTitle} has imposed restrictions on the following servers.' }, + emptyMessage: { id: 'federation_restrictions.empty_message', defaultMessage: '{siteTitle} has not restricted any instances.' }, + notDisclosed: { id: 'federation_restrictions.not_disclosed_message', defaultMessage: '{siteTitle} does not disclose federation restrictions through the API.' }, +}); + +const getHosts = makeGetHosts(); + +const FederationRestrictions = () => { + const intl = useIntl(); + + const siteTitle = useAppSelector((state) => state.instance.get('title')); + const hosts = useAppSelector((state) => getHosts(state)) as ImmutableOrderedSet; + const disclosed = useAppSelector((state) => federationRestrictionsDisclosed(state)); + + const [explanationBoxExpanded, setExplanationBoxExpanded] = useState(true); + + const toggleExplanationBox = (setting: boolean) => { + setExplanationBoxExpanded(setting); + }; + + const emptyMessage = disclosed ? messages.emptyMessage : messages.notDisclosed; + + return ( + +
+ + {intl.formatMessage(messages.boxMessage, { siteTitle })} + +
+ +
+ + {hosts.map((host) => )} + +
+
+ ); +}; + +export default FederationRestrictions; diff --git a/app/soapbox/features/import_data/components/csv_importer.tsx b/app/soapbox/features/import_data/components/csv_importer.tsx index aa5937af9..fafbe813d 100644 --- a/app/soapbox/features/import_data/components/csv_importer.tsx +++ b/app/soapbox/features/import_data/components/csv_importer.tsx @@ -1,5 +1,4 @@ -import React from 'react'; -import { useState } from 'react'; +import React, { useState } from 'react'; import { MessageDescriptor, useIntl } from 'react-intl'; import { Button, FileInput, Form, FormActions, FormGroup, Text } from 'soapbox/components/ui'; diff --git a/app/soapbox/features/new_status/index.tsx b/app/soapbox/features/new_status/index.tsx index 322976afc..e5b4aacd2 100644 --- a/app/soapbox/features/new_status/index.tsx +++ b/app/soapbox/features/new_status/index.tsx @@ -1,5 +1,4 @@ -import React from 'react'; -import { useEffect } from 'react'; +import React, { useEffect } from 'react'; import { Redirect } from 'react-router-dom'; import { openModal } from 'soapbox/actions/modals'; diff --git a/app/soapbox/features/reply_mentions/account.tsx b/app/soapbox/features/reply_mentions/account.tsx index c4817e8cf..b96d6cf5b 100644 --- a/app/soapbox/features/reply_mentions/account.tsx +++ b/app/soapbox/features/reply_mentions/account.tsx @@ -1,5 +1,4 @@ -import React from 'react'; -import { useEffect } from 'react'; +import React, { useEffect } from 'react'; import { defineMessages, useIntl } from 'react-intl'; import { fetchAccount } from 'soapbox/actions/accounts'; diff --git a/app/soapbox/features/ui/components/reactions_modal.tsx b/app/soapbox/features/ui/components/reactions_modal.tsx index 4fb4df72a..4832f943d 100644 --- a/app/soapbox/features/ui/components/reactions_modal.tsx +++ b/app/soapbox/features/ui/components/reactions_modal.tsx @@ -1,6 +1,5 @@ import { List as ImmutableList } from 'immutable'; -import React, { useEffect } from 'react'; -import { useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { FormattedMessage, defineMessages, useIntl } from 'react-intl'; import { fetchFavourites, fetchReactions } from 'soapbox/actions/interactions'; diff --git a/app/soapbox/features/ui/index.tsx b/app/soapbox/features/ui/index.tsx index dfdcfeb9f..1ebfc30a0 100644 --- a/app/soapbox/features/ui/index.tsx +++ b/app/soapbox/features/ui/index.tsx @@ -282,7 +282,7 @@ const SwitchingColumnsArea: React.FC = ({ children }) => { - + {features.scheduledStatuses && } From a5fdfb31fdc556b6c163749216e070fdb5abf3b0 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Mon, 30 May 2022 13:11:44 -0500 Subject: [PATCH 074/102] Warning: convert to TSX --- .../features/compose/components/warning.js | 27 ------------------- .../features/compose/components/warning.tsx | 21 +++++++++++++++ 2 files changed, 21 insertions(+), 27 deletions(-) delete mode 100644 app/soapbox/features/compose/components/warning.js create mode 100644 app/soapbox/features/compose/components/warning.tsx diff --git a/app/soapbox/features/compose/components/warning.js b/app/soapbox/features/compose/components/warning.js deleted file mode 100644 index a819b33ec..000000000 --- a/app/soapbox/features/compose/components/warning.js +++ /dev/null @@ -1,27 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import spring from 'react-motion/lib/spring'; - -import Motion from '../../ui/util/optional_motion'; - -export default class Warning extends React.PureComponent { - - static propTypes = { - message: PropTypes.node.isRequired, - }; - - render() { - const { message } = this.props; - - return ( - - {({ opacity, scaleX, scaleY }) => ( -
- {message} -
- )} -
- ); - } - -} diff --git a/app/soapbox/features/compose/components/warning.tsx b/app/soapbox/features/compose/components/warning.tsx new file mode 100644 index 000000000..b8ec90e09 --- /dev/null +++ b/app/soapbox/features/compose/components/warning.tsx @@ -0,0 +1,21 @@ +import React from 'react'; +import { spring } from 'react-motion'; + +import Motion from '../../ui/util/optional_motion'; + +interface IWarning { + message: React.ReactNode, +} + +/** Warning message displayed in ComposeForm. */ +const Warning: React.FC = ({ message }) => ( + + {({ opacity, scaleX, scaleY }) => ( +
+ {message} +
+ )} +
+); + +export default Warning; From 1beaccd3acdb4a7a26a67504e7c67a50a6f86700 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Mon, 30 May 2022 13:15:35 -0500 Subject: [PATCH 075/102] TextIconButton: convert to TSX --- .../compose/components/text_icon_button.js | 34 ------------------ .../compose/components/text_icon_button.tsx | 36 +++++++++++++++++++ 2 files changed, 36 insertions(+), 34 deletions(-) delete mode 100644 app/soapbox/features/compose/components/text_icon_button.js create mode 100644 app/soapbox/features/compose/components/text_icon_button.tsx diff --git a/app/soapbox/features/compose/components/text_icon_button.js b/app/soapbox/features/compose/components/text_icon_button.js deleted file mode 100644 index 1ca71fe6f..000000000 --- a/app/soapbox/features/compose/components/text_icon_button.js +++ /dev/null @@ -1,34 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; - -export default class TextIconButton extends React.PureComponent { - - static propTypes = { - label: PropTypes.string.isRequired, - title: PropTypes.string, - active: PropTypes.bool, - onClick: PropTypes.func.isRequired, - ariaControls: PropTypes.string, - unavailable: PropTypes.bool, - }; - - handleClick = (e) => { - e.preventDefault(); - this.props.onClick(); - } - - render() { - const { label, title, active, ariaControls, unavailable } = this.props; - - if (unavailable) { - return null; - } - - return ( - - ); - } - -} diff --git a/app/soapbox/features/compose/components/text_icon_button.tsx b/app/soapbox/features/compose/components/text_icon_button.tsx new file mode 100644 index 000000000..fd49d4ed0 --- /dev/null +++ b/app/soapbox/features/compose/components/text_icon_button.tsx @@ -0,0 +1,36 @@ +import React from 'react'; + +interface ITextIconButton { + label: string, + title: string, + active: boolean, + onClick: () => void, + ariaControls: string, + unavailable: boolean, +} + +const TextIconButton: React.FC = ({ + label, + title, + active, + ariaControls, + unavailable, + onClick, +}) => { + const handleClick: React.MouseEventHandler = (e) => { + e.preventDefault(); + onClick(); + }; + + if (unavailable) { + return null; + } + + return ( + + ); +}; + +export default TextIconButton; From 7a35aa727b76a8309d2ac96853950e1456c756ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Mon, 30 May 2022 20:23:55 +0200 Subject: [PATCH 076/102] =?UTF-8?q?import=20from=20'soapbox/=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- .../actions/push_notifications/registerer.js | 5 +- app/soapbox/features/account_gallery/index.js | 3 +- .../features/account_timeline/index.js | 11 ++-- app/soapbox/features/bookmarks/index.tsx | 5 +- .../features/community_timeline/index.js | 6 +-- .../compose/components/compose_form.js | 8 +-- .../compose/components/privacy_dropdown.js | 2 +- .../compose/components/reply_indicator.js | 3 +- .../compose/components/search_results.js | 9 ++-- .../containers/search_results_container.js | 4 +- .../components/conversations_list.tsx | 3 +- .../features/developers/apps/create.js | 3 +- .../features/developers/settings_store.js | 3 +- app/soapbox/features/edit_profile/index.tsx | 5 +- .../features/favourited_statuses/index.js | 6 +-- app/soapbox/features/followers/index.js | 18 +++---- app/soapbox/features/following/index.js | 18 +++---- app/soapbox/features/landing_page/index.tsx | 3 +- .../features/list_adder/components/list.js | 5 +- .../features/list_editor/components/search.js | 3 +- .../notifications/components/filter_bar.js | 3 +- .../notifications/components/notification.tsx | 11 ++-- .../containers/column_settings_container.js | 10 ++-- .../containers/filter_bar_container.js | 2 +- .../containers/notification_container.js | 14 ++--- app/soapbox/features/notifications/index.js | 13 +++-- .../steps/suggested-accounts-step.tsx | 3 +- app/soapbox/features/pinned_statuses/index.js | 4 +- .../public_layout/components/footer.js | 3 +- .../public_layout/components/header.tsx | 5 +- app/soapbox/features/public_timeline/index.js | 6 +-- .../features/remote_timeline/index.tsx | 4 +- .../security/mfa/disable_otp_form.tsx | 3 +- .../features/security/mfa/enable_otp_form.tsx | 3 +- .../security/mfa/otp_confirm_form.tsx | 9 ++-- app/soapbox/features/security/mfa_form.tsx | 5 +- .../features/status/components/action-bar.tsx | 7 ++- .../components/status-interaction-bar.tsx | 3 +- app/soapbox/features/status/index.tsx | 54 +++++++++---------- .../ui/components/account_list_panel.js | 3 +- .../ui/components/confirmation_modal.js | 3 +- .../features/ui/components/mute_modal.js | 4 +- app/soapbox/features/ui/components/navbar.tsx | 3 +- .../ui/components/profile-dropdown.tsx | 3 +- .../ui/components/profile_media_panel.tsx | 2 +- .../features/ui/components/profile_stats.tsx | 3 +- .../features/ui/components/trends-panel.tsx | 5 +- .../ui/components/who-to-follow-panel.tsx | 5 +- .../features/ui/containers/modal_container.js | 4 +- .../ui/containers/status_list_container.js | 5 +- .../verification/steps/sms-verification.js | 3 +- .../features/verification/waitlist_page.tsx | 5 +- app/soapbox/features/video/index.js | 2 +- 53 files changed, 149 insertions(+), 186 deletions(-) diff --git a/app/soapbox/actions/push_notifications/registerer.js b/app/soapbox/actions/push_notifications/registerer.js index b4d86631e..666be8560 100644 --- a/app/soapbox/actions/push_notifications/registerer.js +++ b/app/soapbox/actions/push_notifications/registerer.js @@ -1,8 +1,7 @@ import { createPushSubsription, updatePushSubscription } from 'soapbox/actions/push_subscriptions'; +import { pushNotificationsSetting } from 'soapbox/settings'; import { getVapidKey } from 'soapbox/utils/auth'; - -import { pushNotificationsSetting } from '../../settings'; -import { decode as decodeBase64 } from '../../utils/base64'; +import { decode as decodeBase64 } from 'soapbox/utils/base64'; import { setBrowserSupport, setSubscription, clearSubscription } from './setter'; diff --git a/app/soapbox/features/account_gallery/index.js b/app/soapbox/features/account_gallery/index.js index 28f77f1fa..a17fa85e1 100644 --- a/app/soapbox/features/account_gallery/index.js +++ b/app/soapbox/features/account_gallery/index.js @@ -10,6 +10,7 @@ import { fetchAccountByUsername, } from 'soapbox/actions/accounts'; import { openModal } from 'soapbox/actions/modals'; +import { expandAccountMediaTimeline } from 'soapbox/actions/timelines'; import LoadMore from 'soapbox/components/load_more'; import MissingIndicator from 'soapbox/components/missing_indicator'; import { Column } from 'soapbox/components/ui'; @@ -17,8 +18,6 @@ import { Spinner } from 'soapbox/components/ui'; import { getAccountGallery, findAccountByUsername } from 'soapbox/selectors'; import { getFeatures } from 'soapbox/utils/features'; -import { expandAccountMediaTimeline } from '../../actions/timelines'; - import MediaItem from './components/media_item'; const mapStateToProps = (state, { params, withReplies = false }) => { diff --git a/app/soapbox/features/account_timeline/index.js b/app/soapbox/features/account_timeline/index.js index c394eecf2..18b7d3107 100644 --- a/app/soapbox/features/account_timeline/index.js +++ b/app/soapbox/features/account_timeline/index.js @@ -6,18 +6,17 @@ import ImmutablePureComponent from 'react-immutable-pure-component'; import { FormattedMessage } from 'react-intl'; import { connect } from 'react-redux'; +import { fetchAccount, fetchAccountByUsername } from 'soapbox/actions/accounts'; +import { fetchPatronAccount } from 'soapbox/actions/patron'; import { getSettings } from 'soapbox/actions/settings'; import { getSoapboxConfig } from 'soapbox/actions/soapbox'; +import { expandAccountFeaturedTimeline, expandAccountTimeline } from 'soapbox/actions/timelines'; import MissingIndicator from 'soapbox/components/missing_indicator'; +import StatusList from 'soapbox/components/status_list'; +import { Card, CardBody, Spinner, Text } from 'soapbox/components/ui'; import { makeGetStatusIds, findAccountByUsername } from 'soapbox/selectors'; import { getFeatures } from 'soapbox/utils/features'; -import { fetchAccount, fetchAccountByUsername } from '../../actions/accounts'; -import { fetchPatronAccount } from '../../actions/patron'; -import { expandAccountFeaturedTimeline, expandAccountTimeline } from '../../actions/timelines'; -import StatusList from '../../components/status_list'; -import { Card, CardBody, Spinner, Text } from '../../components/ui'; - const makeMapStateToProps = () => { const getStatusIds = makeGetStatusIds(); diff --git a/app/soapbox/features/bookmarks/index.tsx b/app/soapbox/features/bookmarks/index.tsx index 288feff96..ebd4eb1cd 100644 --- a/app/soapbox/features/bookmarks/index.tsx +++ b/app/soapbox/features/bookmarks/index.tsx @@ -3,13 +3,12 @@ import React from 'react'; import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; import { useDispatch } from 'react-redux'; +import { fetchBookmarkedStatuses, expandBookmarkedStatuses } from 'soapbox/actions/bookmarks'; +import StatusList from 'soapbox/components/status_list'; import SubNavigation from 'soapbox/components/sub_navigation'; import { Column } from 'soapbox/components/ui'; import { useAppSelector } from 'soapbox/hooks'; -import { fetchBookmarkedStatuses, expandBookmarkedStatuses } from '../../actions/bookmarks'; -import StatusList from '../../components/status_list'; - const messages = defineMessages({ heading: { id: 'column.bookmarks', defaultMessage: 'Bookmarks' }, }); diff --git a/app/soapbox/features/community_timeline/index.js b/app/soapbox/features/community_timeline/index.js index 746bda692..1ccc6076f 100644 --- a/app/soapbox/features/community_timeline/index.js +++ b/app/soapbox/features/community_timeline/index.js @@ -4,11 +4,11 @@ import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import { connect } from 'react-redux'; import { getSettings } from 'soapbox/actions/settings'; +import { connectCommunityStream } from 'soapbox/actions/streaming'; +import { expandCommunityTimeline } from 'soapbox/actions/timelines'; import SubNavigation from 'soapbox/components/sub_navigation'; +import { Column } from 'soapbox/components/ui'; -import { connectCommunityStream } from '../../actions/streaming'; -import { expandCommunityTimeline } from '../../actions/timelines'; -import { Column } from '../../components/ui'; import StatusListContainer from '../ui/containers/status_list_container'; import ColumnSettings from './containers/column_settings_container'; diff --git a/app/soapbox/features/compose/components/compose_form.js b/app/soapbox/features/compose/components/compose_form.js index b59c61da0..9700c03f4 100644 --- a/app/soapbox/features/compose/components/compose_form.js +++ b/app/soapbox/features/compose/components/compose_form.js @@ -8,12 +8,12 @@ import { defineMessages, FormattedMessage } from 'react-intl'; import { Link, withRouter } from 'react-router-dom'; import { length } from 'stringz'; +import AutosuggestInput from 'soapbox/components/autosuggest_input'; +import AutosuggestTextarea from 'soapbox/components/autosuggest_textarea'; import Icon from 'soapbox/components/icon'; +import { Button } from 'soapbox/components/ui'; +import { isMobile } from 'soapbox/is_mobile'; -import AutosuggestInput from '../../../components/autosuggest_input'; -import AutosuggestTextarea from '../../../components/autosuggest_textarea'; -import { Button } from '../../../components/ui'; -import { isMobile } from '../../../is_mobile'; import ReplyMentions from '../components/reply_mentions'; import UploadForm from '../components/upload_form'; import Warning from '../components/warning'; diff --git a/app/soapbox/features/compose/components/privacy_dropdown.js b/app/soapbox/features/compose/components/privacy_dropdown.js index d6ddf771c..93bc33497 100644 --- a/app/soapbox/features/compose/components/privacy_dropdown.js +++ b/app/soapbox/features/compose/components/privacy_dropdown.js @@ -7,8 +7,8 @@ import spring from 'react-motion/lib/spring'; import Overlay from 'react-overlays/lib/Overlay'; import Icon from 'soapbox/components/icon'; +import { IconButton } from 'soapbox/components/ui'; -import { IconButton } from '../../../components/ui'; import Motion from '../../ui/util/optional_motion'; const messages = defineMessages({ diff --git a/app/soapbox/features/compose/components/reply_indicator.js b/app/soapbox/features/compose/components/reply_indicator.js index faa86ea9a..ffa3d6a11 100644 --- a/app/soapbox/features/compose/components/reply_indicator.js +++ b/app/soapbox/features/compose/components/reply_indicator.js @@ -6,8 +6,7 @@ import ImmutablePureComponent from 'react-immutable-pure-component'; import AttachmentThumbs from 'soapbox/components/attachment-thumbs'; import { Stack, Text } from 'soapbox/components/ui'; import AccountContainer from 'soapbox/containers/account_container'; - -import { isRtl } from '../../../rtl'; +import { isRtl } from 'soapbox/rtl'; export default class ReplyIndicator extends ImmutablePureComponent { diff --git a/app/soapbox/features/compose/components/search_results.js b/app/soapbox/features/compose/components/search_results.js index 05cbe2663..e4dfa936b 100644 --- a/app/soapbox/features/compose/components/search_results.js +++ b/app/soapbox/features/compose/components/search_results.js @@ -6,16 +6,15 @@ import ImmutablePureComponent from 'react-immutable-pure-component'; import { FormattedMessage } from 'react-intl'; import { defineMessages, injectIntl } from 'react-intl'; +import Hashtag from 'soapbox/components/hashtag'; import ScrollableList from 'soapbox/components/scrollable_list'; +import { Tabs } from 'soapbox/components/ui'; +import AccountContainer from 'soapbox/containers/account_container'; +import StatusContainer from 'soapbox/containers/status_container'; import PlaceholderAccount from 'soapbox/features/placeholder/components/placeholder_account'; import PlaceholderHashtag from 'soapbox/features/placeholder/components/placeholder_hashtag'; import PlaceholderStatus from 'soapbox/features/placeholder/components/placeholder_status'; -import Hashtag from '../../../components/hashtag'; -import { Tabs } from '../../../components/ui'; -import AccountContainer from '../../../containers/account_container'; -import StatusContainer from '../../../containers/status_container'; - const messages = defineMessages({ accounts: { id: 'search_results.accounts', defaultMessage: 'People' }, statuses: { id: 'search_results.statuses', defaultMessage: 'Posts' }, diff --git a/app/soapbox/features/compose/containers/search_results_container.js b/app/soapbox/features/compose/containers/search_results_container.js index 4ab9b712f..e284060d9 100644 --- a/app/soapbox/features/compose/containers/search_results_container.js +++ b/app/soapbox/features/compose/containers/search_results_container.js @@ -1,10 +1,10 @@ import { connect } from 'react-redux'; +import { expandSearch, setFilter } from 'soapbox/actions/search'; +import { fetchSuggestions, dismissSuggestion } from 'soapbox/actions/suggestions'; import { fetchTrendingStatuses } from 'soapbox/actions/trending_statuses'; import { getFeatures } from 'soapbox/utils/features'; -import { expandSearch, setFilter } from '../../../actions/search'; -import { fetchSuggestions, dismissSuggestion } from '../../../actions/suggestions'; import SearchResults from '../components/search_results'; const mapStateToProps = state => { diff --git a/app/soapbox/features/conversations/components/conversations_list.tsx b/app/soapbox/features/conversations/components/conversations_list.tsx index e7bc79078..32fbb3e55 100644 --- a/app/soapbox/features/conversations/components/conversations_list.tsx +++ b/app/soapbox/features/conversations/components/conversations_list.tsx @@ -1,6 +1,5 @@ import { debounce } from 'lodash'; -import React from 'react'; -import { useRef } from 'react'; +import React, { useRef } from 'react'; import { FormattedMessage } from 'react-intl'; import { expandConversations } from 'soapbox/actions/conversations'; diff --git a/app/soapbox/features/developers/apps/create.js b/app/soapbox/features/developers/apps/create.js index f328dd476..01187c34f 100644 --- a/app/soapbox/features/developers/apps/create.js +++ b/app/soapbox/features/developers/apps/create.js @@ -8,12 +8,11 @@ import { connect } from 'react-redux'; import { createApp } from 'soapbox/actions/apps'; import { obtainOAuthToken } from 'soapbox/actions/oauth'; +import { Button, Form, FormActions, FormGroup, Input, Stack, Text, Textarea } from 'soapbox/components/ui'; import Column from 'soapbox/features/ui/components/column'; import { getBaseURL } from 'soapbox/utils/accounts'; import { getFeatures } from 'soapbox/utils/features'; -import { Button, Form, FormActions, FormGroup, Input, Stack, Text, Textarea } from '../../../components/ui'; - const messages = defineMessages({ heading: { id: 'column.app_create', defaultMessage: 'Create app' }, namePlaceholder: { id: 'app_create.name_placeholder', defaultMessage: 'e.g. \'Soapbox\'' }, diff --git a/app/soapbox/features/developers/settings_store.js b/app/soapbox/features/developers/settings_store.js index 2ff9ce9df..02a423ae2 100644 --- a/app/soapbox/features/developers/settings_store.js +++ b/app/soapbox/features/developers/settings_store.js @@ -8,10 +8,9 @@ import { connect } from 'react-redux'; import { showAlertForError } from 'soapbox/actions/alerts'; import { patchMe } from 'soapbox/actions/me'; import { FE_NAME, SETTINGS_UPDATE } from 'soapbox/actions/settings'; +import { Button, Form, FormActions, FormGroup, Textarea } from 'soapbox/components/ui'; import Column from 'soapbox/features/ui/components/column'; -import { Button, Form, FormActions, FormGroup, Textarea } from '../../components/ui'; - const isJSONValid = text => { try { JSON.parse(text); diff --git a/app/soapbox/features/edit_profile/index.tsx b/app/soapbox/features/edit_profile/index.tsx index 788031852..e32473505 100644 --- a/app/soapbox/features/edit_profile/index.tsx +++ b/app/soapbox/features/edit_profile/index.tsx @@ -6,13 +6,12 @@ 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 { Button, Column, Form, FormActions, FormGroup, Input, Textarea, HStack, Toggle, FileInput } from 'soapbox/components/ui'; +import Streamfield, { StreamfieldComponent } from 'soapbox/components/ui/streamfield/streamfield'; import { useAppSelector, useAppDispatch, useOwnAccount, useFeatures } from 'soapbox/hooks'; import { normalizeAccount } from 'soapbox/normalizers'; import resizeImage from 'soapbox/utils/resize_image'; -import { Button, Column, Form, FormActions, FormGroup, Input, Textarea, HStack, Toggle, FileInput } from '../../components/ui'; -import Streamfield, { StreamfieldComponent } from '../../components/ui/streamfield/streamfield'; - import ProfilePreview from './components/profile-preview'; import type { Account } from 'soapbox/types/entities'; diff --git a/app/soapbox/features/favourited_statuses/index.js b/app/soapbox/features/favourited_statuses/index.js index 394f8216f..73ad5a255 100644 --- a/app/soapbox/features/favourited_statuses/index.js +++ b/app/soapbox/features/favourited_statuses/index.js @@ -6,14 +6,14 @@ import ImmutablePureComponent from 'react-immutable-pure-component'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import { connect } from 'react-redux'; +import { fetchAccount, fetchAccountByUsername } from 'soapbox/actions/accounts'; +import { fetchFavouritedStatuses, expandFavouritedStatuses, fetchAccountFavouritedStatuses, expandAccountFavouritedStatuses } from 'soapbox/actions/favourites'; import MissingIndicator from 'soapbox/components/missing_indicator'; +import StatusList from 'soapbox/components/status_list'; import { Spinner } from 'soapbox/components/ui'; import { findAccountByUsername } from 'soapbox/selectors'; import { getFeatures } from 'soapbox/utils/features'; -import { fetchAccount, fetchAccountByUsername } from '../../actions/accounts'; -import { fetchFavouritedStatuses, expandFavouritedStatuses, fetchAccountFavouritedStatuses, expandAccountFavouritedStatuses } from '../../actions/favourites'; -import StatusList from '../../components/status_list'; import Column from '../ui/components/column'; const messages = defineMessages({ diff --git a/app/soapbox/features/followers/index.js b/app/soapbox/features/followers/index.js index 1a98cb561..a146f4f6f 100644 --- a/app/soapbox/features/followers/index.js +++ b/app/soapbox/features/followers/index.js @@ -6,20 +6,20 @@ import ImmutablePureComponent from 'react-immutable-pure-component'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import { connect } from 'react-redux'; -import MissingIndicator from 'soapbox/components/missing_indicator'; -import { Spinner } from 'soapbox/components/ui'; -import { findAccountByUsername } from 'soapbox/selectors'; -import { getFollowDifference } from 'soapbox/utils/accounts'; -import { getFeatures } from 'soapbox/utils/features'; - import { fetchAccount, fetchFollowers, expandFollowers, fetchAccountByUsername, -} from '../../actions/accounts'; -import ScrollableList from '../../components/scrollable_list'; -import AccountContainer from '../../containers/account_container'; +} from 'soapbox/actions/accounts'; +import MissingIndicator from 'soapbox/components/missing_indicator'; +import ScrollableList from 'soapbox/components/scrollable_list'; +import { Spinner } from 'soapbox/components/ui'; +import AccountContainer from 'soapbox/containers/account_container'; +import { findAccountByUsername } from 'soapbox/selectors'; +import { getFollowDifference } from 'soapbox/utils/accounts'; +import { getFeatures } from 'soapbox/utils/features'; + import Column from '../ui/components/column'; const messages = defineMessages({ diff --git a/app/soapbox/features/following/index.js b/app/soapbox/features/following/index.js index 7ab00a7b9..447a0ae02 100644 --- a/app/soapbox/features/following/index.js +++ b/app/soapbox/features/following/index.js @@ -6,20 +6,20 @@ import ImmutablePureComponent from 'react-immutable-pure-component'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import { connect } from 'react-redux'; -import MissingIndicator from 'soapbox/components/missing_indicator'; -import { Spinner } from 'soapbox/components/ui'; -import { findAccountByUsername } from 'soapbox/selectors'; -import { getFollowDifference } from 'soapbox/utils/accounts'; -import { getFeatures } from 'soapbox/utils/features'; - import { fetchAccount, fetchFollowing, expandFollowing, fetchAccountByUsername, -} from '../../actions/accounts'; -import ScrollableList from '../../components/scrollable_list'; -import AccountContainer from '../../containers/account_container'; +} from 'soapbox/actions/accounts'; +import MissingIndicator from 'soapbox/components/missing_indicator'; +import ScrollableList from 'soapbox/components/scrollable_list'; +import { Spinner } from 'soapbox/components/ui'; +import AccountContainer from 'soapbox/containers/account_container'; +import { findAccountByUsername } from 'soapbox/selectors'; +import { getFollowDifference } from 'soapbox/utils/accounts'; +import { getFeatures } from 'soapbox/utils/features'; + import Column from '../ui/components/column'; const messages = defineMessages({ diff --git a/app/soapbox/features/landing_page/index.tsx b/app/soapbox/features/landing_page/index.tsx index 26f41f086..8b2956169 100644 --- a/app/soapbox/features/landing_page/index.tsx +++ b/app/soapbox/features/landing_page/index.tsx @@ -1,12 +1,11 @@ import * as React from 'react'; import { FormattedMessage } from 'react-intl'; +import { Button, Card, CardBody, Stack, Text } from 'soapbox/components/ui'; import VerificationBadge from 'soapbox/components/verification_badge'; import RegistrationForm from 'soapbox/features/auth_login/components/registration_form'; import { useAppSelector, useFeatures, useSoapboxConfig } from 'soapbox/hooks'; -import { Button, Card, CardBody, Stack, Text } from '../../components/ui'; - const LandingPage = () => { const features = useFeatures(); const soapboxConfig = useSoapboxConfig(); diff --git a/app/soapbox/features/list_adder/components/list.js b/app/soapbox/features/list_adder/components/list.js index d9b23ec3e..fd9930180 100644 --- a/app/soapbox/features/list_adder/components/list.js +++ b/app/soapbox/features/list_adder/components/list.js @@ -5,10 +5,9 @@ import ImmutablePureComponent from 'react-immutable-pure-component'; import { defineMessages, injectIntl } from 'react-intl'; import { connect } from 'react-redux'; +import { removeFromListAdder, addToListAdder } from 'soapbox/actions/lists'; import Icon from 'soapbox/components/icon'; - -import { removeFromListAdder, addToListAdder } from '../../../actions/lists'; -import IconButton from '../../../components/icon_button'; +import IconButton from 'soapbox/components/icon_button'; const messages = defineMessages({ remove: { id: 'lists.account.remove', defaultMessage: 'Remove from list' }, diff --git a/app/soapbox/features/list_editor/components/search.js b/app/soapbox/features/list_editor/components/search.js index fc2928a00..3314fa4e3 100644 --- a/app/soapbox/features/list_editor/components/search.js +++ b/app/soapbox/features/list_editor/components/search.js @@ -4,11 +4,10 @@ import React from 'react'; import { defineMessages, injectIntl } from 'react-intl'; import { connect } from 'react-redux'; +import { fetchListSuggestions, clearListSuggestions, changeListSuggestions } from 'soapbox/actions/lists'; import Icon from 'soapbox/components/icon'; import { Button } from 'soapbox/components/ui'; -import { fetchListSuggestions, clearListSuggestions, changeListSuggestions } from '../../../actions/lists'; - const messages = defineMessages({ search: { id: 'lists.search', defaultMessage: 'Search among people you follow' }, searchTitle: { id: 'tabs_bar.search', defaultMessage: 'Search' }, diff --git a/app/soapbox/features/notifications/components/filter_bar.js b/app/soapbox/features/notifications/components/filter_bar.js index d443205a7..6384b7788 100644 --- a/app/soapbox/features/notifications/components/filter_bar.js +++ b/app/soapbox/features/notifications/components/filter_bar.js @@ -3,8 +3,7 @@ import React from 'react'; import { defineMessages, injectIntl } from 'react-intl'; import Icon from 'soapbox/components/icon'; - -import { Tabs } from '../../../components/ui'; +import { Tabs } from 'soapbox/components/ui'; const messages = defineMessages({ all: { id: 'notifications.filter.all', defaultMessage: 'All' }, diff --git a/app/soapbox/features/notifications/components/notification.tsx b/app/soapbox/features/notifications/components/notification.tsx index 1a9dad8eb..f1b16e780 100644 --- a/app/soapbox/features/notifications/components/notification.tsx +++ b/app/soapbox/features/notifications/components/notification.tsx @@ -4,14 +4,13 @@ import { defineMessages, FormattedMessage, IntlShape, MessageDescriptor } from ' import { useIntl } from 'react-intl'; import { useHistory } from 'react-router-dom'; +import Icon from 'soapbox/components/icon'; +import Permalink from 'soapbox/components/permalink'; +import { HStack, Text, Emoji } from 'soapbox/components/ui'; +import AccountContainer from 'soapbox/containers/account_container'; +import StatusContainer from 'soapbox/containers/status_container'; import { useAppSelector } from 'soapbox/hooks'; -import Icon from '../../../components/icon'; -import Permalink from '../../../components/permalink'; -import { HStack, Text, Emoji } from '../../../components/ui'; -import AccountContainer from '../../../containers/account_container'; -import StatusContainer from '../../../containers/status_container'; - import type { History } from 'history'; import type { ScrollPosition } from 'soapbox/components/status'; import type { NotificationType } from 'soapbox/normalizers/notification'; diff --git a/app/soapbox/features/notifications/containers/column_settings_container.js b/app/soapbox/features/notifications/containers/column_settings_container.js index a60532d40..d21733b18 100644 --- a/app/soapbox/features/notifications/containers/column_settings_container.js +++ b/app/soapbox/features/notifications/containers/column_settings_container.js @@ -1,13 +1,13 @@ import { defineMessages, injectIntl } from 'react-intl'; import { connect } from 'react-redux'; +import { openModal } from 'soapbox/actions/modals'; +import { setFilter } from 'soapbox/actions/notifications'; +import { clearNotifications } from 'soapbox/actions/notifications'; +import { changeAlerts as changePushNotifications } from 'soapbox/actions/push_notifications'; +import { getSettings, changeSetting } from 'soapbox/actions/settings'; import { getFeatures } from 'soapbox/utils/features'; -import { openModal } from '../../../actions/modals'; -import { setFilter } from '../../../actions/notifications'; -import { clearNotifications } from '../../../actions/notifications'; -import { changeAlerts as changePushNotifications } from '../../../actions/push_notifications'; -import { getSettings, changeSetting } from '../../../actions/settings'; import ColumnSettings from '../components/column_settings'; const messages = defineMessages({ diff --git a/app/soapbox/features/notifications/containers/filter_bar_container.js b/app/soapbox/features/notifications/containers/filter_bar_container.js index 82805136a..6aa62b7d4 100644 --- a/app/soapbox/features/notifications/containers/filter_bar_container.js +++ b/app/soapbox/features/notifications/containers/filter_bar_container.js @@ -1,9 +1,9 @@ import { connect } from 'react-redux'; +import { setFilter } from 'soapbox/actions/notifications'; import { getSettings } from 'soapbox/actions/settings'; import { getFeatures } from 'soapbox/utils/features'; -import { setFilter } from '../../../actions/notifications'; import FilterBar from '../components/filter_bar'; const makeMapStateToProps = state => { diff --git a/app/soapbox/features/notifications/containers/notification_container.js b/app/soapbox/features/notifications/containers/notification_container.js index cfbef992f..d8713d204 100644 --- a/app/soapbox/features/notifications/containers/notification_container.js +++ b/app/soapbox/features/notifications/containers/notification_container.js @@ -1,20 +1,20 @@ import { connect } from 'react-redux'; -import { getSettings } from 'soapbox/actions/settings'; - -import { mentionCompose } from '../../../actions/compose'; +import { mentionCompose } from 'soapbox/actions/compose'; import { reblog, favourite, unreblog, unfavourite, -} from '../../../actions/interactions'; -import { openModal } from '../../../actions/modals'; +} from 'soapbox/actions/interactions'; +import { openModal } from 'soapbox/actions/modals'; +import { getSettings } from 'soapbox/actions/settings'; import { hideStatus, revealStatus, -} from '../../../actions/statuses'; -import { makeGetNotification } from '../../../selectors'; +} from 'soapbox/actions/statuses'; +import { makeGetNotification } from 'soapbox/selectors'; + import Notification from '../components/notification'; const makeMapStateToProps = () => { diff --git a/app/soapbox/features/notifications/index.js b/app/soapbox/features/notifications/index.js index a43ec5704..8ef05fb73 100644 --- a/app/soapbox/features/notifications/index.js +++ b/app/soapbox/features/notifications/index.js @@ -8,17 +8,16 @@ import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import { connect } from 'react-redux'; import { createSelector } from 'reselect'; -import { getSettings } from 'soapbox/actions/settings'; -import PlaceholderNotification from 'soapbox/features/placeholder/components/placeholder_notification'; - import { expandNotifications, scrollTopNotifications, dequeueNotifications, -} from '../../actions/notifications'; -import ScrollableList from '../../components/scrollable_list'; -import TimelineQueueButtonHeader from '../../components/timeline_queue_button_header'; -import { Column } from '../../components/ui'; +} from 'soapbox/actions/notifications'; +import { getSettings } from 'soapbox/actions/settings'; +import ScrollableList from 'soapbox/components/scrollable_list'; +import TimelineQueueButtonHeader from 'soapbox/components/timeline_queue_button_header'; +import { Column } from 'soapbox/components/ui'; +import PlaceholderNotification from 'soapbox/features/placeholder/components/placeholder_notification'; import FilterBarContainer from './containers/filter_bar_container'; import NotificationContainer from './containers/notification_container'; diff --git a/app/soapbox/features/onboarding/steps/suggested-accounts-step.tsx b/app/soapbox/features/onboarding/steps/suggested-accounts-step.tsx index 4421979cb..2ef5ab828 100644 --- a/app/soapbox/features/onboarding/steps/suggested-accounts-step.tsx +++ b/app/soapbox/features/onboarding/steps/suggested-accounts-step.tsx @@ -4,13 +4,12 @@ import * as React from 'react'; import { FormattedMessage } from 'react-intl'; import { useDispatch } from 'react-redux'; +import { fetchSuggestions } from 'soapbox/actions/suggestions'; import ScrollableList from 'soapbox/components/scrollable_list'; import { Button, Card, CardBody, Stack, Text } from 'soapbox/components/ui'; import AccountContainer from 'soapbox/containers/account_container'; import { useAppSelector } from 'soapbox/hooks'; -import { fetchSuggestions } from '../../../actions/suggestions'; - const SuggestedAccountsStep = ({ onNext }: { onNext: () => void }) => { const dispatch = useDispatch(); diff --git a/app/soapbox/features/pinned_statuses/index.js b/app/soapbox/features/pinned_statuses/index.js index a045641a4..b6649a01b 100644 --- a/app/soapbox/features/pinned_statuses/index.js +++ b/app/soapbox/features/pinned_statuses/index.js @@ -5,10 +5,10 @@ import ImmutablePureComponent from 'react-immutable-pure-component'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import { connect } from 'react-redux'; +import { fetchPinnedStatuses } from 'soapbox/actions/pin_statuses'; import MissingIndicator from 'soapbox/components/missing_indicator'; +import StatusList from 'soapbox/components/status_list'; -import { fetchPinnedStatuses } from '../../actions/pin_statuses'; -import StatusList from '../../components/status_list'; import Column from '../ui/components/column'; const messages = defineMessages({ diff --git a/app/soapbox/features/public_layout/components/footer.js b/app/soapbox/features/public_layout/components/footer.js index 291e29f84..11a56806b 100644 --- a/app/soapbox/features/public_layout/components/footer.js +++ b/app/soapbox/features/public_layout/components/footer.js @@ -8,8 +8,7 @@ import { Link } from 'react-router-dom'; import { getSettings } from 'soapbox/actions/settings'; import { getSoapboxConfig } from 'soapbox/actions/soapbox'; - -import { Text } from '../../../components/ui'; +import { Text } from 'soapbox/components/ui'; const mapStateToProps = (state, props) => { const soapboxConfig = getSoapboxConfig(state); diff --git a/app/soapbox/features/public_layout/components/header.tsx b/app/soapbox/features/public_layout/components/header.tsx index 2153dff05..dcb1859a7 100644 --- a/app/soapbox/features/public_layout/components/header.tsx +++ b/app/soapbox/features/public_layout/components/header.tsx @@ -5,12 +5,11 @@ import { Link, Redirect } from 'react-router-dom'; import { logIn, verifyCredentials } from 'soapbox/actions/auth'; import { fetchInstance } from 'soapbox/actions/instance'; +import { openModal } from 'soapbox/actions/modals'; import SiteLogo from 'soapbox/components/site-logo'; +import { Button, Form, HStack, IconButton, Input, Tooltip } from 'soapbox/components/ui'; import { useAppSelector, useFeatures, useSoapboxConfig } from 'soapbox/hooks'; -import { openModal } from '../../../actions/modals'; -import { Button, Form, HStack, IconButton, Input, Tooltip } from '../../../components/ui'; - import Sonar from './sonar'; import type { AxiosError } from 'axios'; diff --git a/app/soapbox/features/public_timeline/index.js b/app/soapbox/features/public_timeline/index.js index 14168e1cc..7404c6106 100644 --- a/app/soapbox/features/public_timeline/index.js +++ b/app/soapbox/features/public_timeline/index.js @@ -5,12 +5,12 @@ import { connect } from 'react-redux'; import { Link } from 'react-router-dom'; import { changeSetting, getSettings } from 'soapbox/actions/settings'; +import { connectPublicStream } from 'soapbox/actions/streaming'; +import { expandPublicTimeline } from 'soapbox/actions/timelines'; import SubNavigation from 'soapbox/components/sub_navigation'; +import { Column } from 'soapbox/components/ui'; import Accordion from 'soapbox/features/ui/components/accordion'; -import { connectPublicStream } from '../../actions/streaming'; -import { expandPublicTimeline } from '../../actions/timelines'; -import { Column } from '../../components/ui'; import PinnedHostsPicker from '../remote_timeline/components/pinned_hosts_picker'; import StatusListContainer from '../ui/containers/status_list_container'; diff --git a/app/soapbox/features/remote_timeline/index.tsx b/app/soapbox/features/remote_timeline/index.tsx index 6f8fe9477..0942a23b5 100644 --- a/app/soapbox/features/remote_timeline/index.tsx +++ b/app/soapbox/features/remote_timeline/index.tsx @@ -2,13 +2,13 @@ import React, { useEffect, useRef } from 'react'; import { defineMessages, useIntl, FormattedMessage } from 'react-intl'; import { useHistory } from 'react-router-dom'; +import { connectRemoteStream } from 'soapbox/actions/streaming'; +import { expandRemoteTimeline } from 'soapbox/actions/timelines'; import IconButton from 'soapbox/components/icon_button'; import { HStack, Text } from 'soapbox/components/ui'; import Column from 'soapbox/features/ui/components/column'; import { useAppDispatch, useSettings } from 'soapbox/hooks'; -import { connectRemoteStream } from '../../actions/streaming'; -import { expandRemoteTimeline } from '../../actions/timelines'; import StatusListContainer from '../ui/containers/status_list_container'; import PinnedHostsPicker from './components/pinned_hosts_picker'; diff --git a/app/soapbox/features/security/mfa/disable_otp_form.tsx b/app/soapbox/features/security/mfa/disable_otp_form.tsx index 2dd66b8c8..a62133b87 100644 --- a/app/soapbox/features/security/mfa/disable_otp_form.tsx +++ b/app/soapbox/features/security/mfa/disable_otp_form.tsx @@ -4,10 +4,9 @@ import { useHistory } from 'react-router-dom'; import { disableMfa } from 'soapbox/actions/mfa'; import snackbar from 'soapbox/actions/snackbar'; +import { Button, Form, FormGroup, Input, FormActions, Stack, Text } from 'soapbox/components/ui'; import { useAppDispatch } from 'soapbox/hooks'; -import { Button, Form, FormGroup, Input, FormActions, Stack, Text } from '../../../components/ui'; - const messages = defineMessages({ mfa_setup_disable_button: { id: 'column.mfa_disable_button', defaultMessage: 'Disable' }, disableFail: { id: 'security.disable.fail', defaultMessage: 'Incorrect password. Try again.' }, diff --git a/app/soapbox/features/security/mfa/enable_otp_form.tsx b/app/soapbox/features/security/mfa/enable_otp_form.tsx index adb02faff..a5608bf18 100644 --- a/app/soapbox/features/security/mfa/enable_otp_form.tsx +++ b/app/soapbox/features/security/mfa/enable_otp_form.tsx @@ -4,10 +4,9 @@ import { useHistory } from 'react-router-dom'; import { fetchBackupCodes } from 'soapbox/actions/mfa'; import snackbar from 'soapbox/actions/snackbar'; +import { Button, FormActions, Spinner, Stack, Text } from 'soapbox/components/ui'; import { useAppDispatch } from 'soapbox/hooks'; -import { Button, FormActions, Spinner, Stack, Text } from '../../../components/ui'; - const messages = defineMessages({ mfaCancelButton: { id: 'column.mfa_cancel', defaultMessage: 'Cancel' }, mfaSetupButton: { id: 'column.mfa_setup', defaultMessage: 'Proceed to Setup' }, diff --git a/app/soapbox/features/security/mfa/otp_confirm_form.tsx b/app/soapbox/features/security/mfa/otp_confirm_form.tsx index b5770d268..2f4fd07ab 100644 --- a/app/soapbox/features/security/mfa/otp_confirm_form.tsx +++ b/app/soapbox/features/security/mfa/otp_confirm_form.tsx @@ -3,14 +3,13 @@ import React, { useCallback, useEffect, useState } from 'react'; import { useIntl, FormattedMessage, defineMessages } from 'react-intl'; import { useHistory } from 'react-router-dom'; -import snackbar from 'soapbox/actions/snackbar'; -import { useAppDispatch } from 'soapbox/hooks'; - import { setupMfa, confirmMfa, -} from '../../../actions/mfa'; -import { Button, Form, FormActions, FormGroup, Input, Stack, Text } from '../../../components/ui'; +} from 'soapbox/actions/mfa'; +import snackbar from 'soapbox/actions/snackbar'; +import { Button, Form, FormActions, FormGroup, Input, Stack, Text } from 'soapbox/components/ui'; +import { useAppDispatch } from 'soapbox/hooks'; const messages = defineMessages({ mfaCancelButton: { id: 'column.mfa_cancel', defaultMessage: 'Cancel' }, diff --git a/app/soapbox/features/security/mfa_form.tsx b/app/soapbox/features/security/mfa_form.tsx index f85756c28..92a922148 100644 --- a/app/soapbox/features/security/mfa_form.tsx +++ b/app/soapbox/features/security/mfa_form.tsx @@ -1,11 +1,10 @@ import React, { useEffect, useState } from 'react'; import { useIntl, defineMessages } from 'react-intl'; +import { fetchMfa } from 'soapbox/actions/mfa'; +import { Card, CardBody, CardHeader, CardTitle, Column } from 'soapbox/components/ui'; import { useAppSelector, useAppDispatch } from 'soapbox/hooks'; -import { fetchMfa } from '../../actions/mfa'; -import { Card, CardBody, CardHeader, CardTitle, Column } from '../../components/ui'; - import DisableOtpForm from './mfa/disable_otp_form'; import EnableOtpForm from './mfa/enable_otp_form'; import OtpConfirmForm from './mfa/otp_confirm_form'; diff --git a/app/soapbox/features/status/components/action-bar.tsx b/app/soapbox/features/status/components/action-bar.tsx index d80c5d5d2..35cbaf79e 100644 --- a/app/soapbox/features/status/components/action-bar.tsx +++ b/app/soapbox/features/status/components/action-bar.tsx @@ -4,15 +4,14 @@ import { defineMessages, injectIntl, WrappedComponentProps as IntlComponentProps import { connect } from 'react-redux'; import { withRouter, RouteComponentProps } from 'react-router-dom'; +import { openModal } from 'soapbox/actions/modals'; import EmojiButtonWrapper from 'soapbox/components/emoji-button-wrapper'; +import { HStack, IconButton } from 'soapbox/components/ui'; +import DropdownMenuContainer from 'soapbox/containers/dropdown_menu_container'; import { isUserTouching } from 'soapbox/is_mobile'; import { getReactForStatus } from 'soapbox/utils/emoji_reacts'; import { getFeatures } from 'soapbox/utils/features'; -import { openModal } from '../../../actions/modals'; -import { HStack, IconButton } from '../../../components/ui'; -import DropdownMenuContainer from '../../../containers/dropdown_menu_container'; - import type { History } from 'history'; import type { List as ImmutableList } from 'immutable'; import type { AnyAction } from 'redux'; diff --git a/app/soapbox/features/status/components/status-interaction-bar.tsx b/app/soapbox/features/status/components/status-interaction-bar.tsx index 2fdc4808d..13ecf6c60 100644 --- a/app/soapbox/features/status/components/status-interaction-bar.tsx +++ b/app/soapbox/features/status/components/status-interaction-bar.tsx @@ -5,11 +5,10 @@ import { FormattedNumber } from 'react-intl'; import { useDispatch } from 'react-redux'; import { openModal } from 'soapbox/actions/modals'; +import { HStack, IconButton, Text, Emoji } from 'soapbox/components/ui'; import { useAppSelector, useSoapboxConfig, useFeatures } from 'soapbox/hooks'; import { reduceEmoji } from 'soapbox/utils/emoji_reacts'; -import { HStack, IconButton, Text, Emoji } from '../../../components/ui'; - import type { Status } from 'soapbox/types/entities'; interface IStatusInteractionBar { diff --git a/app/soapbox/features/status/index.tsx b/app/soapbox/features/status/index.tsx index 804005fa5..9f0e3dbf6 100644 --- a/app/soapbox/features/status/index.tsx +++ b/app/soapbox/features/status/index.tsx @@ -8,30 +8,15 @@ import { connect } from 'react-redux'; import { withRouter, RouteComponentProps } from 'react-router-dom'; import { createSelector } from 'reselect'; +import { blockAccount } from 'soapbox/actions/accounts'; import { launchChat } from 'soapbox/actions/chats'; -import { - deactivateUserModal, - deleteUserModal, - deleteStatusModal, - toggleStatusSensitivityModal, -} from 'soapbox/actions/moderation'; -import { getSettings } from 'soapbox/actions/settings'; -import { getSoapboxConfig } from 'soapbox/actions/soapbox'; -import ScrollableList from 'soapbox/components/scrollable_list'; -import SubNavigation from 'soapbox/components/sub_navigation'; -import Tombstone from 'soapbox/components/tombstone'; -import { Column, Stack } from 'soapbox/components/ui'; -import PlaceholderStatus from 'soapbox/features/placeholder/components/placeholder_status'; -import PendingStatus from 'soapbox/features/ui/components/pending_status'; - -import { blockAccount } from '../../actions/accounts'; import { replyCompose, mentionCompose, directCompose, quoteCompose, -} from '../../actions/compose'; -import { simpleEmojiReact } from '../../actions/emoji_reacts'; +} from 'soapbox/actions/compose'; +import { simpleEmojiReact } from 'soapbox/actions/emoji_reacts'; import { favourite, unfavourite, @@ -41,10 +26,18 @@ import { unbookmark, pin, unpin, -} from '../../actions/interactions'; -import { openModal } from '../../actions/modals'; -import { initMuteModal } from '../../actions/mutes'; -import { initReport } from '../../actions/reports'; +} from 'soapbox/actions/interactions'; +import { openModal } from 'soapbox/actions/modals'; +import { + deactivateUserModal, + deleteUserModal, + deleteStatusModal, + toggleStatusSensitivityModal, +} from 'soapbox/actions/moderation'; +import { initMuteModal } from 'soapbox/actions/mutes'; +import { initReport } from 'soapbox/actions/reports'; +import { getSettings } from 'soapbox/actions/settings'; +import { getSoapboxConfig } from 'soapbox/actions/soapbox'; import { muteStatus, unmuteStatus, @@ -52,11 +45,18 @@ import { hideStatus, revealStatus, editStatus, -} from '../../actions/statuses'; -import { fetchStatusWithContext, fetchNext } from '../../actions/statuses'; -import MissingIndicator from '../../components/missing_indicator'; -import { textForScreenReader, defaultMediaVisibility } from '../../components/status'; -import { makeGetStatus } from '../../selectors'; +} from 'soapbox/actions/statuses'; +import { fetchStatusWithContext, fetchNext } from 'soapbox/actions/statuses'; +import MissingIndicator from 'soapbox/components/missing_indicator'; +import ScrollableList from 'soapbox/components/scrollable_list'; +import { textForScreenReader, defaultMediaVisibility } from 'soapbox/components/status'; +import SubNavigation from 'soapbox/components/sub_navigation'; +import Tombstone from 'soapbox/components/tombstone'; +import { Column, Stack } from 'soapbox/components/ui'; +import PlaceholderStatus from 'soapbox/features/placeholder/components/placeholder_status'; +import PendingStatus from 'soapbox/features/ui/components/pending_status'; +import { makeGetStatus } from 'soapbox/selectors'; + import { attachFullscreenListener, detachFullscreenListener, isFullscreen } from '../ui/util/fullscreen'; import ActionBar from './components/action-bar'; diff --git a/app/soapbox/features/ui/components/account_list_panel.js b/app/soapbox/features/ui/components/account_list_panel.js index 210a7bf6a..61b703e4f 100644 --- a/app/soapbox/features/ui/components/account_list_panel.js +++ b/app/soapbox/features/ui/components/account_list_panel.js @@ -5,8 +5,7 @@ import ImmutablePureComponent from 'react-immutable-pure-component'; import { Link } from 'react-router-dom'; import Icon from 'soapbox/components/icon'; - -import AccountContainer from '../../../containers/account_container'; +import AccountContainer from 'soapbox/containers/account_container'; export default class AccountListPanel extends ImmutablePureComponent { diff --git a/app/soapbox/features/ui/components/confirmation_modal.js b/app/soapbox/features/ui/components/confirmation_modal.js index ab740a940..e205f6ce1 100644 --- a/app/soapbox/features/ui/components/confirmation_modal.js +++ b/app/soapbox/features/ui/components/confirmation_modal.js @@ -2,10 +2,9 @@ import PropTypes from 'prop-types'; import React from 'react'; import { injectIntl, FormattedMessage } from 'react-intl'; +import { Modal } from 'soapbox/components/ui'; import { SimpleForm, FieldsGroup, Checkbox } from 'soapbox/features/forms'; -import { Modal } from '../../../components/ui'; - export default @injectIntl class ConfirmationModal extends React.PureComponent { diff --git a/app/soapbox/features/ui/components/mute_modal.js b/app/soapbox/features/ui/components/mute_modal.js index 0a8de57ea..99bdb7ef8 100644 --- a/app/soapbox/features/ui/components/mute_modal.js +++ b/app/soapbox/features/ui/components/mute_modal.js @@ -7,9 +7,7 @@ import Toggle from 'react-toggle'; import { muteAccount } from 'soapbox/actions/accounts'; import { closeModal } from 'soapbox/actions/modals'; import { toggleHideNotifications } from 'soapbox/actions/mutes'; -import { Modal, HStack, Stack } from 'soapbox/components/ui'; - -import { Text } from '../../../components/ui'; +import { Modal, HStack, Stack, Text } from 'soapbox/components/ui'; const mapStateToProps = state => { return { diff --git a/app/soapbox/features/ui/components/navbar.tsx b/app/soapbox/features/ui/components/navbar.tsx index ac81681d9..9b09edc1c 100644 --- a/app/soapbox/features/ui/components/navbar.tsx +++ b/app/soapbox/features/ui/components/navbar.tsx @@ -7,13 +7,12 @@ import { Link, Redirect } from 'react-router-dom'; import { logIn, verifyCredentials } from 'soapbox/actions/auth'; import { fetchInstance } from 'soapbox/actions/instance'; +import { openSidebar } from 'soapbox/actions/sidebar'; import SiteLogo from 'soapbox/components/site-logo'; import { Avatar, Button, Form, IconButton, Input, Tooltip } from 'soapbox/components/ui'; import Search from 'soapbox/features/compose/components/search'; import { useOwnAccount, useSoapboxConfig } from 'soapbox/hooks'; -import { openSidebar } from '../../../actions/sidebar'; - import ProfileDropdown from './profile-dropdown'; const messages = defineMessages({ diff --git a/app/soapbox/features/ui/components/profile-dropdown.tsx b/app/soapbox/features/ui/components/profile-dropdown.tsx index 121304960..f364cee68 100644 --- a/app/soapbox/features/ui/components/profile-dropdown.tsx +++ b/app/soapbox/features/ui/components/profile-dropdown.tsx @@ -6,12 +6,11 @@ import { Link } from 'react-router-dom'; import { logOut, switchAccount } from 'soapbox/actions/auth'; import { fetchOwnAccounts } from 'soapbox/actions/auth'; +import Account from 'soapbox/components/account'; import { Menu, MenuButton, MenuDivider, MenuItem, MenuLink, MenuList } from 'soapbox/components/ui'; import { useAppSelector, useFeatures } from 'soapbox/hooks'; import { makeGetAccount } from 'soapbox/selectors'; -import Account from '../../../components/account'; - import ThemeToggle from './theme-toggle'; import type { Account as AccountEntity } from 'soapbox/types/entities'; diff --git a/app/soapbox/features/ui/components/profile_media_panel.tsx b/app/soapbox/features/ui/components/profile_media_panel.tsx index 29738ec3f..78ecfa2c7 100644 --- a/app/soapbox/features/ui/components/profile_media_panel.tsx +++ b/app/soapbox/features/ui/components/profile_media_panel.tsx @@ -4,11 +4,11 @@ import { FormattedMessage } from 'react-intl'; import { useDispatch } from 'react-redux'; import { openModal } from 'soapbox/actions/modals'; +import { expandAccountMediaTimeline } from 'soapbox/actions/timelines'; import { Spinner, Widget } from 'soapbox/components/ui'; import { useAppSelector } from 'soapbox/hooks'; import { getAccountGallery } from 'soapbox/selectors'; -import { expandAccountMediaTimeline } from '../../../actions/timelines'; import MediaItem from '../../account_gallery/components/media_item'; import type { Account, Attachment } from 'soapbox/types/entities'; diff --git a/app/soapbox/features/ui/components/profile_stats.tsx b/app/soapbox/features/ui/components/profile_stats.tsx index 998baa379..9fff5ffe3 100644 --- a/app/soapbox/features/ui/components/profile_stats.tsx +++ b/app/soapbox/features/ui/components/profile_stats.tsx @@ -2,10 +2,9 @@ import React from 'react'; import { useIntl, defineMessages } from 'react-intl'; import { NavLink } from 'react-router-dom'; +import { HStack, Text } from 'soapbox/components/ui'; import { shortNumberFormat } from 'soapbox/utils/numbers'; -import { HStack, Text } from '../../../components/ui'; - import type { Account } from 'soapbox/types/entities'; const messages = defineMessages({ diff --git a/app/soapbox/features/ui/components/trends-panel.tsx b/app/soapbox/features/ui/components/trends-panel.tsx index 4fa4a7b26..9fafc930f 100644 --- a/app/soapbox/features/ui/components/trends-panel.tsx +++ b/app/soapbox/features/ui/components/trends-panel.tsx @@ -3,12 +3,11 @@ import * as React from 'react'; import { FormattedMessage } from 'react-intl'; import { useDispatch } from 'react-redux'; +import { fetchTrends } from 'soapbox/actions/trends'; +import Hashtag from 'soapbox/components/hashtag'; import { Widget } from 'soapbox/components/ui'; import { useAppSelector } from 'soapbox/hooks'; -import { fetchTrends } from '../../../actions/trends'; -import Hashtag from '../../../components/hashtag'; - interface ITrendsPanel { limit: number } diff --git a/app/soapbox/features/ui/components/who-to-follow-panel.tsx b/app/soapbox/features/ui/components/who-to-follow-panel.tsx index 25a730603..8decff7db 100644 --- a/app/soapbox/features/ui/components/who-to-follow-panel.tsx +++ b/app/soapbox/features/ui/components/who-to-follow-panel.tsx @@ -3,12 +3,11 @@ import * as React from 'react'; import { FormattedMessage, defineMessages, useIntl } from 'react-intl'; import { useDispatch } from 'react-redux'; +import { fetchSuggestions, dismissSuggestion } from 'soapbox/actions/suggestions'; import { Widget } from 'soapbox/components/ui'; +import AccountContainer from 'soapbox/containers/account_container'; import { useAppSelector } from 'soapbox/hooks'; -import { fetchSuggestions, dismissSuggestion } from '../../../actions/suggestions'; -import AccountContainer from '../../../containers/account_container'; - const messages = defineMessages({ dismissSuggestion: { id: 'suggestions.dismiss', defaultMessage: 'Dismiss suggestion' }, }); diff --git a/app/soapbox/features/ui/containers/modal_container.js b/app/soapbox/features/ui/containers/modal_container.js index 1ecc91b98..54f07a371 100644 --- a/app/soapbox/features/ui/containers/modal_container.js +++ b/app/soapbox/features/ui/containers/modal_container.js @@ -1,9 +1,9 @@ import { connect } from 'react-redux'; +import { cancelReplyCompose } from 'soapbox/actions/compose'; +import { closeModal } from 'soapbox/actions/modals'; import { cancelReport } from 'soapbox/actions/reports'; -import { cancelReplyCompose } from '../../../actions/compose'; -import { closeModal } from '../../../actions/modals'; import ModalRoot from '../components/modal_root'; const mapStateToProps = state => { diff --git a/app/soapbox/features/ui/containers/status_list_container.js b/app/soapbox/features/ui/containers/status_list_container.js index 925c5d732..c5d180c7e 100644 --- a/app/soapbox/features/ui/containers/status_list_container.js +++ b/app/soapbox/features/ui/containers/status_list_container.js @@ -3,11 +3,10 @@ import { debounce } from 'lodash'; import { connect } from 'react-redux'; import { dequeueTimeline } from 'soapbox/actions/timelines'; +import { scrollTopTimeline } from 'soapbox/actions/timelines'; +import StatusList from 'soapbox/components/status_list'; import { makeGetStatusIds } from 'soapbox/selectors'; -import { scrollTopTimeline } from '../../../actions/timelines'; -import StatusList from '../../../components/status_list'; - const makeMapStateToProps = () => { const getStatusIds = makeGetStatusIds(); diff --git a/app/soapbox/features/verification/steps/sms-verification.js b/app/soapbox/features/verification/steps/sms-verification.js index 20a17981d..01050c832 100644 --- a/app/soapbox/features/verification/steps/sms-verification.js +++ b/app/soapbox/features/verification/steps/sms-verification.js @@ -5,10 +5,9 @@ import { useDispatch, useSelector } from 'react-redux'; import snackbar from 'soapbox/actions/snackbar'; import { confirmPhoneVerification, requestPhoneVerification } from 'soapbox/actions/verification'; +import { Button, Form, FormGroup, Input, Text } from 'soapbox/components/ui'; import { formatPhoneNumber } from 'soapbox/utils/phone'; -import { Button, Form, FormGroup, Input, Text } from '../../../components/ui'; - const Statuses = { IDLE: 'IDLE', REQUESTED: 'REQUESTED', diff --git a/app/soapbox/features/verification/waitlist_page.tsx b/app/soapbox/features/verification/waitlist_page.tsx index 0a28f4993..00182e58f 100644 --- a/app/soapbox/features/verification/waitlist_page.tsx +++ b/app/soapbox/features/verification/waitlist_page.tsx @@ -3,14 +3,13 @@ import { useIntl } from 'react-intl'; import { useDispatch } from 'react-redux'; import { Link } from 'react-router-dom'; +import { logOut } from 'soapbox/actions/auth'; import { openModal } from 'soapbox/actions/modals'; import LandingGradient from 'soapbox/components/landing-gradient'; import SiteLogo from 'soapbox/components/site-logo'; +import { Button, Stack, Text } from 'soapbox/components/ui'; import { useAppSelector, useOwnAccount } from 'soapbox/hooks'; -import { logOut } from '../../actions/auth'; -import { Button, Stack, Text } from '../../components/ui'; - const WaitlistPage = (/* { account } */) => { const dispatch = useDispatch(); const intl = useIntl(); diff --git a/app/soapbox/features/video/index.js b/app/soapbox/features/video/index.js index 14ce1ce6b..423468f65 100644 --- a/app/soapbox/features/video/index.js +++ b/app/soapbox/features/video/index.js @@ -9,8 +9,8 @@ import { connect } from 'react-redux'; import { getSettings } from 'soapbox/actions/settings'; import Blurhash from 'soapbox/components/blurhash'; import Icon from 'soapbox/components/icon'; +import { isPanoramic, isPortrait, minimumAspectRatio, maximumAspectRatio } from 'soapbox/utils/media_aspect_ratio'; -import { isPanoramic, isPortrait, minimumAspectRatio, maximumAspectRatio } from '../../utils/media_aspect_ratio'; import { isFullscreen, requestFullscreen, exitFullscreen } from '../ui/util/fullscreen'; const DEFAULT_HEIGHT = 300; From 4821703edb59cd1a81e84f4e907540e1bf455374 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Mon, 30 May 2022 13:26:24 -0500 Subject: [PATCH 077/102] UploadButton: convert to TSX --- .../compose/components/upload_button.js | 92 ------------------- .../compose/components/upload_button.tsx | 81 ++++++++++++++++ 2 files changed, 81 insertions(+), 92 deletions(-) delete mode 100644 app/soapbox/features/compose/components/upload_button.js create mode 100644 app/soapbox/features/compose/components/upload_button.tsx diff --git a/app/soapbox/features/compose/components/upload_button.js b/app/soapbox/features/compose/components/upload_button.js deleted file mode 100644 index a42e3be1f..000000000 --- a/app/soapbox/features/compose/components/upload_button.js +++ /dev/null @@ -1,92 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import ImmutablePureComponent from 'react-immutable-pure-component'; -import { defineMessages, injectIntl } from 'react-intl'; -import { connect } from 'react-redux'; - -import { IconButton } from '../../../components/ui'; - -const messages = defineMessages({ - upload: { id: 'upload_button.label', defaultMessage: 'Add media attachment' }, -}); - -const onlyImages = types => { - return Boolean(types && types.every(type => type.startsWith('image/'))); -}; - -const makeMapStateToProps = () => { - const mapStateToProps = state => ({ - attachmentTypes: state.getIn(['instance', 'configuration', 'media_attachments', 'supported_mime_types']), - }); - - return mapStateToProps; -}; - -export default @connect(makeMapStateToProps) -@injectIntl -class UploadButton extends ImmutablePureComponent { - - static propTypes = { - disabled: PropTypes.bool, - unavailable: PropTypes.bool, - onSelectFile: PropTypes.func.isRequired, - style: PropTypes.object, - resetFileKey: PropTypes.number, - attachmentTypes: ImmutablePropTypes.listOf(PropTypes.string), - intl: PropTypes.object.isRequired, - }; - - handleChange = (e) => { - if (e.target.files.length > 0) { - this.props.onSelectFile(e.target.files); - } - } - - handleClick = () => { - this.fileElement.click(); - } - - setRef = (c) => { - this.fileElement = c; - } - - render() { - const { intl, resetFileKey, attachmentTypes, unavailable, disabled } = this.props; - - if (unavailable) { - return null; - } - - const src = onlyImages(attachmentTypes) - ? require('@tabler/icons/icons/photo.svg') - : require('@tabler/icons/icons/paperclip.svg'); - - return ( -
- - - -
- ); - } - -} diff --git a/app/soapbox/features/compose/components/upload_button.tsx b/app/soapbox/features/compose/components/upload_button.tsx new file mode 100644 index 000000000..a34e2b4ad --- /dev/null +++ b/app/soapbox/features/compose/components/upload_button.tsx @@ -0,0 +1,81 @@ +import React, { useRef } from 'react'; +import { defineMessages, useIntl } from 'react-intl'; + +import { IconButton } from 'soapbox/components/ui'; +import { useAppSelector } from 'soapbox/hooks'; + +import type { List as ImmutableList } from 'immutable'; + +const messages = defineMessages({ + upload: { id: 'upload_button.label', defaultMessage: 'Add media attachment' }, +}); + +const onlyImages = (types: ImmutableList) => { + return Boolean(types && types.every(type => type.startsWith('image/'))); +}; + +interface IUploadButton { + disabled: boolean, + unavailable: boolean, + onSelectFile: (files: FileList) => void, + style: React.CSSProperties, + resetFileKey: number, +} + +const UploadButton: React.FC = ({ + disabled, + unavailable, + onSelectFile, + resetFileKey, +}) => { + const intl = useIntl(); + + const fileElement = useRef(null); + const attachmentTypes = useAppSelector(state => state.instance.configuration.getIn(['media_attachments', 'supported_mime_types']) as ImmutableList); + + const handleChange: React.ChangeEventHandler = (e) => { + if (e.target.files?.length) { + onSelectFile(e.target.files); + } + }; + + const handleClick = () => { + fileElement.current?.click(); + }; + + if (unavailable) { + return null; + } + + const src = onlyImages(attachmentTypes) + ? require('@tabler/icons/icons/photo.svg') + : require('@tabler/icons/icons/paperclip.svg'); + + return ( +
+ + + +
+ ); +}; + +export default UploadButton; From e813eecacbf45d20460dd50d49df1e0d09209e5e Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Mon, 30 May 2022 13:53:14 -0500 Subject: [PATCH 078/102] Upload: convert to TSX --- .../features/compose/components/upload.js | 212 ------------------ .../features/compose/components/upload.tsx | 205 +++++++++++++++++ 2 files changed, 205 insertions(+), 212 deletions(-) delete mode 100644 app/soapbox/features/compose/components/upload.js create mode 100644 app/soapbox/features/compose/components/upload.tsx diff --git a/app/soapbox/features/compose/components/upload.js b/app/soapbox/features/compose/components/upload.js deleted file mode 100644 index a293509f9..000000000 --- a/app/soapbox/features/compose/components/upload.js +++ /dev/null @@ -1,212 +0,0 @@ -import classNames from 'classnames'; -import PropTypes from 'prop-types'; -import React from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import ImmutablePureComponent from 'react-immutable-pure-component'; -import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; -import spring from 'react-motion/lib/spring'; -import { withRouter } from 'react-router-dom'; - -import Blurhash from 'soapbox/components/blurhash'; -import Icon from 'soapbox/components/icon'; -import IconButton from 'soapbox/components/icon_button'; - -import Motion from '../../ui/util/optional_motion'; - -const bookIcon = require('@tabler/icons/icons/book.svg'); -const fileAnalyticsIcon = require('@tabler/icons/icons/file-analytics.svg'); -const fileCodeIcon = require('@tabler/icons/icons/file-code.svg'); -const fileTextIcon = require('@tabler/icons/icons/file-text.svg'); -const fileZipIcon = require('@tabler/icons/icons/file-zip.svg'); -const presentationIcon = require('@tabler/icons/icons/presentation.svg'); - -export const MIMETYPE_ICONS = { - 'application/x-freearc': fileZipIcon, - 'application/x-bzip': fileZipIcon, - 'application/x-bzip2': fileZipIcon, - 'application/gzip': fileZipIcon, - 'application/vnd.rar': fileZipIcon, - 'application/x-tar': fileZipIcon, - 'application/zip': fileZipIcon, - 'application/x-7z-compressed': fileZipIcon, - 'application/x-csh': fileCodeIcon, - 'application/html': fileCodeIcon, - 'text/javascript': fileCodeIcon, - 'application/json': fileCodeIcon, - 'application/ld+json': fileCodeIcon, - 'application/x-httpd-php': fileCodeIcon, - 'application/x-sh': fileCodeIcon, - 'application/xhtml+xml': fileCodeIcon, - 'application/xml': fileCodeIcon, - 'application/epub+zip': bookIcon, - 'application/vnd.oasis.opendocument.spreadsheet': fileAnalyticsIcon, - 'application/vnd.ms-excel': fileAnalyticsIcon, - 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': fileAnalyticsIcon, - 'application/pdf': fileTextIcon, - 'application/vnd.oasis.opendocument.presentation': presentationIcon, - 'application/vnd.ms-powerpoint': presentationIcon, - 'application/vnd.openxmlformats-officedocument.presentationml.presentation': presentationIcon, - 'text/plain': fileTextIcon, - 'application/rtf': fileTextIcon, - 'application/msword': fileTextIcon, - 'application/x-abiword': fileTextIcon, - 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': fileTextIcon, - 'application/vnd.oasis.opendocument.text': fileTextIcon, -}; - -const messages = defineMessages({ - description: { id: 'upload_form.description', defaultMessage: 'Describe for the visually impaired' }, - delete: { id: 'upload_form.undo', defaultMessage: 'Delete' }, -}); - -export default @injectIntl @withRouter -class Upload extends ImmutablePureComponent { - - static propTypes = { - media: ImmutablePropTypes.map.isRequired, - intl: PropTypes.object.isRequired, - onUndo: PropTypes.func.isRequired, - onDescriptionChange: PropTypes.func.isRequired, - onOpenFocalPoint: PropTypes.func.isRequired, - onSubmit: PropTypes.func.isRequired, - history: PropTypes.object.isRequired, - }; - - state = { - hovered: false, - focused: false, - dirtyDescription: null, - }; - - handleKeyDown = (e) => { - if (e.keyCode === 13 && (e.ctrlKey || e.metaKey)) { - this.handleSubmit(); - } - } - - handleSubmit = () => { - this.handleInputBlur(); - this.props.onSubmit(this.props.history); - } - - handleUndoClick = e => { - e.stopPropagation(); - this.props.onUndo(this.props.media.get('id')); - } - - handleFocalPointClick = e => { - e.stopPropagation(); - this.props.onOpenFocalPoint(this.props.media.get('id')); - } - - handleInputChange = e => { - this.setState({ dirtyDescription: e.target.value }); - } - - handleMouseEnter = () => { - this.setState({ hovered: true }); - } - - handleMouseLeave = () => { - this.setState({ hovered: false }); - } - - handleInputFocus = () => { - this.setState({ focused: true }); - } - - handleClick = () => { - this.setState({ focused: true }); - } - - handleInputBlur = () => { - const { dirtyDescription } = this.state; - - this.setState({ focused: false, dirtyDescription: null }); - - if (dirtyDescription !== null) { - this.props.onDescriptionChange(this.props.media.get('id'), dirtyDescription); - } - } - - handleOpenModal = () => { - this.props.onOpenModal(this.props.media); - } - - render() { - const { intl, media, descriptionLimit } = this.props; - const active = this.state.hovered || this.state.focused; - const description = this.state.dirtyDescription || (this.state.dirtyDescription !== '' && media.get('description')) || ''; - const focusX = media.getIn(['meta', 'focus', 'x']); - const focusY = media.getIn(['meta', 'focus', 'y']); - const x = ((focusX / 2) + .5) * 100; - const y = ((focusY / -2) + .5) * 100; - const mediaType = media.get('type'); - const uploadIcon = mediaType === 'unknown' && ( - - ); - - return ( -
- - - {({ scale }) => ( -
-
- } - /> - - {/* Only display the "Preview" button for a valid attachment with a URL */} - {(mediaType !== 'unknown' && Boolean(media.get('url'))) && ( - } - /> - )} -
- -
-