diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 000000000..41b87855a --- /dev/null +++ b/.dockerignore @@ -0,0 +1,34 @@ +.git + +/node_modules/ +/tmp/ +/build/ +/coverage/ +/.coverage/ +/.eslintcache +/.env +/deploy.sh +/.vs/ +yarn-error.log* +/junit.xml + +/static/ +/static-test/ +/public/ +/dist/ + +.idea +.DS_Store + +# Custom build files +/custom/**/* +!/custom/* +/custom/*.* +!/custom/.gitkeep +!/custom/**/.gitkeep + +# surge.sh +/CNAME +/AUTH +/CORS +/ROUTER diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 90c856e40..aaca197c3 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -142,3 +142,17 @@ pages: only: refs: - develop + +docker: + stage: deploy + image: docker:20.10.17 + services: + - docker:20.10.17-dind + # https://medium.com/devops-with-valentine/how-to-build-a-docker-image-and-push-it-to-the-gitlab-container-registry-from-a-gitlab-ci-pipeline-acac0d1f26df + script: + - echo $CI_REGISTRY_PASSWORD | docker login -u $CI_REGISTRY_USER $CI_REGISTRY --password-stdin + - docker build -t $CI_REGISTRY_IMAGE . + - docker push $CI_REGISTRY_IMAGE + only: + refs: + - develop \ No newline at end of file diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 527dfacad..57a35ab4f 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,6 +1,5 @@ { "recommendations": [ - "editorconfig.editorconfig", "dbaeumer.vscode-eslint", "bradlc.vscode-tailwindcss", "stylelint.vscode-stylelint", diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 000000000..4a7155a74 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,9 @@ +{ + "editor.insertSpaces": true, + "editor.tabSize": 2, + "files.associations": { + "*.conf.template": "properties" + }, + "files.eol": "\n", + "files.insertFinalNewline": false +} diff --git a/.vscode/soapbox.code-snippets b/.vscode/soapbox.code-snippets index 66da1a25b..b31d50ff5 100644 --- a/.vscode/soapbox.code-snippets +++ b/.vscode/soapbox.code-snippets @@ -1,5 +1,5 @@ { - // Place your soapbox-fe workspace snippets here. Each snippet is defined under a snippet name and has a scope, prefix, body and + // Place your Soapbox workspace snippets here. Each snippet is defined under a snippet name and has a scope, prefix, body and // description. Add comma separated ids of the languages where the snippet is applicable in the scope field. If scope // is left empty or omitted, the snippet gets applied to all languages. The prefix is what is // used to trigger the snippet and the body will be expanded and inserted. Possible variables are: diff --git a/CHANGELOG.md b/CHANGELOG.md index b6971b861..6a3850599 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -211,7 +211,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Initial beta release. -[Unreleased]: https://gitlab.com/soapbox-pub/soapbox-fe/-/compare/v1.0.0...develop -[Unreleased patch]: https://gitlab.com/soapbox-pub/soapbox-fe/-/compare/v1.0.0...stable/1.0.x -[1.0.0]: https://gitlab.com/soapbox-pub/soapbox-fe/-/compare/v0.9.0...v1.0.0 -[0.9.0]: https://gitlab.com/soapbox-pub/soapbox-fe/-/tags/v0.9.0 +[Unreleased]: https://gitlab.com/soapbox-pub/soapbox/-/compare/v1.0.0...develop +[Unreleased patch]: https://gitlab.com/soapbox-pub/soapbox/-/compare/v1.0.0...stable/1.0.x +[1.0.0]: https://gitlab.com/soapbox-pub/soapbox/-/compare/v0.9.0...v1.0.0 +[0.9.0]: https://gitlab.com/soapbox-pub/soapbox/-/tags/v0.9.0 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 000000000..bfb7c2e48 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,17 @@ +FROM node:18 as build +WORKDIR /app +COPY package.json . +COPY yarn.lock . +RUN yarn +COPY . . +ARG NODE_ENV=production +RUN yarn build + +FROM nginx:stable-alpine +EXPOSE 5000 +ENV PORT=5000 +ENV FALLBACK_PORT=4444 +ENV BACKEND_URL=http://localhost:4444 +ENV CSP= +COPY installation/docker.conf.template /etc/nginx/templates/default.conf.template +COPY --from=build /app/static /usr/share/nginx/html diff --git a/README.md b/README.md index c8ddf4480..07ba0d7a7 100644 --- a/README.md +++ b/README.md @@ -14,13 +14,13 @@ Installing Soapbox on an existing Pleroma server is extremely easy. Just ssh into the server and download a .zip of the latest build: ```sh -curl -L https://gitlab.com/soapbox-pub/soapbox-fe/-/jobs/artifacts/develop/download?job=build-production -o soapbox-fe.zip +curl -L https://gitlab.com/soapbox-pub/soapbox/-/jobs/artifacts/develop/download?job=build-production -o soapbox.zip ``` Then unpack it into Pleroma's `instance` directory: ```sh -busybox unzip soapbox-fe.zip -o -d /opt/pleroma/instance +busybox unzip soapbox.zip -o -d /opt/pleroma/instance ``` **That's it!** :tada: @@ -54,7 +54,7 @@ location / { } ``` -(See [`mastodon.conf`](https://gitlab.com/soapbox-pub/soapbox-fe/-/blob/develop/installation/mastodon.conf) for a full example.) +(See [`mastodon.conf`](https://gitlab.com/soapbox-pub/soapbox/-/blob/develop/installation/mastodon.conf) for a full example.) Soapbox incorporates much of the [Mastodon API](https://docs.joinmastodon.org/methods/), [Pleroma API](https://api.pleroma.social/), and more. It detects features supported by the backend to provide the right experience for the backend. @@ -64,8 +64,8 @@ It detects features supported by the backend to provide the right experience for To get it running, just clone the repo: ```sh -git clone https://gitlab.com/soapbox-pub/soapbox-fe.git -cd soapbox-fe +git clone https://gitlab.com/soapbox-pub/soapbox.git +cd soapbox ``` Ensure that Node.js and Yarn are installed, then install dependencies: @@ -101,7 +101,7 @@ Try again. ### Troubleshooting: it's not working! -Run `node -V` and compare your Node.js version with the version in [`.tool-versions`](https://gitlab.com/soapbox-pub/soapbox-fe/-/blob/develop/.tool-versions). +Run `node -V` and compare your Node.js version with the version in [`.tool-versions`](https://gitlab.com/soapbox-pub/soapbox/-/blob/develop/.tool-versions). If they don't match, try installing [asdf](https://asdf-vm.com/). ## Local Dev Configuration diff --git a/app.json b/app.json new file mode 100644 index 000000000..bd168fb5a --- /dev/null +++ b/app.json @@ -0,0 +1,7 @@ +{ + "name": "Soapbox", + "description": "Software for the next generation of social media.", + "keywords": ["fediverse"], + "website": "https://soapbox.pub", + "stack": "container" +} diff --git a/app/instance/about.example/index.html b/app/instance/about.example/index.html index 5efb11fc9..6af826f85 100644 --- a/app/instance/about.example/index.html +++ b/app/instance/about.example/index.html @@ -23,6 +23,5 @@

Open Source Software

-

Soapbox is free and open source (FOSS) software that runs atop a Pleroma server

-

The Soapbox repository can be found at Soapbox-fe

-

The Pleroma server repository can be found at Pleroma-be

+

Soapbox is free and open source (FOSS) software.

+

The Soapbox repository can be found at Soapbox

diff --git a/app/soapbox/actions/importer/index.ts b/app/soapbox/actions/importer/index.ts index 20041180b..a5d86c9b0 100644 --- a/app/soapbox/actions/importer/index.ts +++ b/app/soapbox/actions/importer/index.ts @@ -106,10 +106,10 @@ export function importFetchedStatus(status: APIEntity, idempotencyKey?: string) const isBroken = (status: APIEntity) => { try { // Skip empty accounts - // https://gitlab.com/soapbox-pub/soapbox-fe/-/issues/424 + // https://gitlab.com/soapbox-pub/soapbox/-/issues/424 if (!status.account.id) return true; // Skip broken reposts - // https://gitlab.com/soapbox-pub/soapbox/-/issues/28 + // https://gitlab.com/soapbox-pub/rebased/-/issues/28 if (status.reblog && !status.reblog.account.id) return true; return false; } catch (e) { diff --git a/app/soapbox/actions/statuses.ts b/app/soapbox/actions/statuses.ts index db15e7a21..b1bced1f0 100644 --- a/app/soapbox/actions/statuses.ts +++ b/app/soapbox/actions/statuses.ts @@ -101,7 +101,7 @@ const editStatus = (id: string) => (dispatch: AppDispatch, getState: () => RootS api(getState).get(`/api/v1/statuses/${id}/source`).then(response => { dispatch({ type: STATUS_FETCH_SOURCE_SUCCESS }); - dispatch(setComposeToStatus(status, response.data.text, response.data.spoiler_text, false)); + dispatch(setComposeToStatus(status, response.data.text, response.data.spoiler_text, response.data.content_type, false)); dispatch(openModal('COMPOSE')); }).catch(error => { dispatch({ type: STATUS_FETCH_SOURCE_FAIL, error }); diff --git a/app/soapbox/components/account.tsx b/app/soapbox/components/account.tsx index 281e1aee2..438d2f0f4 100644 --- a/app/soapbox/components/account.tsx +++ b/app/soapbox/components/account.tsx @@ -8,7 +8,7 @@ import { useAppSelector, useOnScreen } from 'soapbox/hooks'; import { getAcct } from 'soapbox/utils/accounts'; import { displayFqn } from 'soapbox/utils/state'; -import RelativeTimestamp from './relative_timestamp'; +import RelativeTimestamp from './relative-timestamp'; import { Avatar, Emoji, HStack, Icon, IconButton, Stack, Text } from './ui'; import type { Account as AccountEntity } from 'soapbox/types/entities'; @@ -54,7 +54,7 @@ interface IAccount { id?: string, onActionClick?: (account: any) => void, showProfileHoverCard?: boolean, - timestamp?: string | Date, + timestamp?: string, timestampUrl?: string, futureTimestamp?: boolean, withAccountNote?: boolean, diff --git a/app/soapbox/components/display-name.tsx b/app/soapbox/components/display-name.tsx index 1bb72a319..63028ccfe 100644 --- a/app/soapbox/components/display-name.tsx +++ b/app/soapbox/components/display-name.tsx @@ -6,7 +6,7 @@ import { useSoapboxConfig } from 'soapbox/hooks'; import { getAcct } from '../utils/accounts'; import Icon from './icon'; -import RelativeTimestamp from './relative_timestamp'; +import RelativeTimestamp from './relative-timestamp'; import VerificationBadge from './verification_badge'; import type { Account } from 'soapbox/types/entities'; diff --git a/app/soapbox/components/polls/poll-footer.tsx b/app/soapbox/components/polls/poll-footer.tsx index 366f34d1c..dfa91e663 100644 --- a/app/soapbox/components/polls/poll-footer.tsx +++ b/app/soapbox/components/polls/poll-footer.tsx @@ -4,7 +4,7 @@ import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; import { fetchPoll, vote } from 'soapbox/actions/polls'; import { useAppDispatch } from 'soapbox/hooks'; -import RelativeTimestamp from '../relative_timestamp'; +import RelativeTimestamp from '../relative-timestamp'; import { Button, HStack, Stack, Text, Tooltip } from '../ui'; import type { Selected } from './poll'; @@ -54,7 +54,7 @@ const PollFooter: React.FC = ({ poll, showResults, selected }): JSX )} - + {poll.pleroma.get('non_anonymous') && ( <> diff --git a/app/soapbox/components/polls/poll.tsx b/app/soapbox/components/polls/poll.tsx index a30348678..2b6f99392 100644 --- a/app/soapbox/components/polls/poll.tsx +++ b/app/soapbox/components/polls/poll.tsx @@ -18,7 +18,7 @@ interface IPoll { } const messages = defineMessages({ - multiple: { id: 'poll.chooseMultiple', defaultMessage: 'Choose as many as you\'d like.' }, + multiple: { id: 'poll.choose_multiple', defaultMessage: 'Choose as many as you\'d like.' }, }); const Poll: React.FC = ({ id, status }): JSX.Element | null => { diff --git a/app/soapbox/components/relative_timestamp.js b/app/soapbox/components/relative-timestamp.tsx similarity index 78% rename from app/soapbox/components/relative_timestamp.js rename to app/soapbox/components/relative-timestamp.tsx index 1e64f9807..d530051d8 100644 --- a/app/soapbox/components/relative_timestamp.js +++ b/app/soapbox/components/relative-timestamp.tsx @@ -1,8 +1,7 @@ -import PropTypes from 'prop-types'; import React from 'react'; -import { injectIntl, defineMessages } from 'react-intl'; +import { injectIntl, defineMessages, IntlShape, FormatDateOptions } from 'react-intl'; -import { Text } from './ui'; +import Text, { IText } from './ui/text/text'; const messages = defineMessages({ just_now: { id: 'relative_time.just_now', defaultMessage: 'now' }, @@ -17,7 +16,7 @@ const messages = defineMessages({ days_remaining: { id: 'time_remaining.days', defaultMessage: '{number, plural, one {# day} other {# days}} left' }, }); -const dateFormatOptions = { +const dateFormatOptions: FormatDateOptions = { hour12: false, year: 'numeric', month: 'short', @@ -26,7 +25,7 @@ const dateFormatOptions = { minute: '2-digit', }; -const shortDateFormatOptions = { +const shortDateFormatOptions: FormatDateOptions = { month: 'short', day: 'numeric', }; @@ -38,7 +37,7 @@ const DAY = 1000 * 60 * 60 * 24; const MAX_DELAY = 2147483647; -const selectUnits = delta => { +const selectUnits = (delta: number) => { const absDelta = Math.abs(delta); if (absDelta < MINUTE) { @@ -52,7 +51,7 @@ const selectUnits = delta => { return 'day'; }; -const getUnitDelay = units => { +const getUnitDelay = (units: string) => { switch (units) { case 'second': return SECOND; @@ -67,7 +66,7 @@ const getUnitDelay = units => { } }; -export const timeAgoString = (intl, date, now, year) => { +export const timeAgoString = (intl: IntlShape, date: Date, now: number, year: number) => { const delta = now - date.getTime(); let relativeTime; @@ -93,7 +92,7 @@ export const timeAgoString = (intl, date, now, year) => { return relativeTime; }; -const timeRemainingString = (intl, date, now) => { +const timeRemainingString = (intl: IntlShape, date: Date, now: number) => { const delta = date.getTime() - now; let relativeTime; @@ -113,16 +112,21 @@ const timeRemainingString = (intl, date, now) => { return relativeTime; }; -export default @injectIntl -class RelativeTimestamp extends React.Component { +interface RelativeTimestampProps extends IText { + intl: IntlShape, + timestamp: string, + year?: number, + futureDate?: boolean, +} - static propTypes = { - intl: PropTypes.object.isRequired, - timestamp: PropTypes.string.isRequired, - year: PropTypes.number.isRequired, - theme: PropTypes.string, - futureDate: PropTypes.bool, - }; +interface RelativeTimestampState { + now: number, +} + +/** Displays a timestamp compared to the current time, eg "1m" for one minute ago. */ +class RelativeTimestamp extends React.Component { + + _timer: NodeJS.Timeout | undefined; state = { now: Date.now(), @@ -130,10 +134,10 @@ class RelativeTimestamp extends React.Component { static defaultProps = { year: (new Date()).getFullYear(), - theme: 'inherit', + theme: 'inherit' as const, }; - shouldComponentUpdate(nextProps, nextState) { + shouldComponentUpdate(nextProps: RelativeTimestampProps, nextState: RelativeTimestampState) { // As of right now the locale doesn't change without a new page load, // but we might as well check in case that ever changes. return this.props.timestamp !== nextProps.timestamp || @@ -141,14 +145,14 @@ class RelativeTimestamp extends React.Component { this.state.now !== nextState.now; } - UNSAFE_componentWillReceiveProps(prevProps) { + UNSAFE_componentWillReceiveProps(prevProps: RelativeTimestampProps) { if (this.props.timestamp !== prevProps.timestamp) { this.setState({ now: Date.now() }); } } componentDidMount() { - this._scheduleNextUpdate(this.props, this.state); + this._scheduleNextUpdate(); } UNSAFE_componentWillUpdate() { @@ -156,11 +160,15 @@ class RelativeTimestamp extends React.Component { } componentWillUnmount() { - clearTimeout(this._timer); + if (this._timer) { + clearTimeout(this._timer); + } } _scheduleNextUpdate() { - clearTimeout(this._timer); + if (this._timer) { + clearTimeout(this._timer); + } const { timestamp } = this.props; const delta = (new Date(timestamp)).getTime() - this.state.now; @@ -177,8 +185,8 @@ class RelativeTimestamp extends React.Component { render() { const { timestamp, intl, year, futureDate, theme, ...textProps } = this.props; - const date = new Date(timestamp); - const relativeTime = futureDate ? timeRemainingString(intl, date, this.state.now) : timeAgoString(intl, date, this.state.now, year); + const date = new Date(timestamp); + const relativeTime = futureDate ? timeRemainingString(intl, date, this.state.now) : timeAgoString(intl, date, this.state.now, year!); return ( @@ -188,3 +196,5 @@ class RelativeTimestamp extends React.Component { } } + +export default injectIntl(RelativeTimestamp); diff --git a/app/soapbox/components/scrollable_list.tsx b/app/soapbox/components/scrollable_list.tsx index d5bfd6025..cdb74e580 100644 --- a/app/soapbox/components/scrollable_list.tsx +++ b/app/soapbox/components/scrollable_list.tsx @@ -6,7 +6,7 @@ import { Virtuoso, Components, VirtuosoProps, VirtuosoHandle, ListRange, IndexLo import { useSettings } from 'soapbox/hooks'; import LoadMore from './load_more'; -import { Card, Spinner, Text } from './ui'; +import { Card, Spinner } from './ui'; /** Custom Viruoso component context. */ type Context = { @@ -162,7 +162,7 @@ const ScrollableList = React.forwardRef(({ {isLoading ? ( ) : ( - {emptyMessage} + emptyMessage )} diff --git a/app/soapbox/components/status-action-bar.tsx b/app/soapbox/components/status-action-bar.tsx index c07bf818d..5bce513a5 100644 --- a/app/soapbox/components/status-action-bar.tsx +++ b/app/soapbox/components/status-action-bar.tsx @@ -301,12 +301,12 @@ const StatusActionBar: React.FC = ({ }; const handleCopy: React.EventHandler = (e) => { - const { url } = status; + const { uri } = status; const textarea = document.createElement('textarea'); e.stopPropagation(); - textarea.textContent = url; + textarea.textContent = uri; textarea.style.position = 'fixed'; document.body.appendChild(textarea); diff --git a/app/soapbox/components/ui/hstack/hstack.tsx b/app/soapbox/components/ui/hstack/hstack.tsx index 3eef11055..f959cdd51 100644 --- a/app/soapbox/components/ui/hstack/hstack.tsx +++ b/app/soapbox/components/ui/hstack/hstack.tsx @@ -42,11 +42,13 @@ interface IHStack { grow?: boolean, /** Extra CSS styles for the
*/ style?: React.CSSProperties + /** Whether to let the flexbox wrap onto multiple lines. */ + wrap?: boolean, } /** Horizontal row of child elements. */ const HStack = forwardRef((props, ref) => { - const { space, alignItems, grow, justifyContent, className, ...filteredProps } = props; + const { space, alignItems, grow, justifyContent, wrap, className, ...filteredProps } = props; return (
((props, ref) => { // @ts-ignore [spaces[space]]: typeof space !== 'undefined', 'flex-grow': grow, + 'flex-wrap': wrap, }, className)} /> ); diff --git a/app/soapbox/components/ui/stack/stack.tsx b/app/soapbox/components/ui/stack/stack.tsx index bac3ce89b..65ec97cbb 100644 --- a/app/soapbox/components/ui/stack/stack.tsx +++ b/app/soapbox/components/ui/stack/stack.tsx @@ -38,12 +38,13 @@ interface IStack extends React.HTMLAttributes { } /** Vertical stack of child elements. */ -const Stack: React.FC = (props) => { +const Stack: React.FC = React.forwardRef((props, ref: React.LegacyRef | undefined) => { const { space, alignItems, justifyContent, className, grow, ...filteredProps } = props; return (
= (props) => { }, className)} /> ); -}; +}); export default Stack; diff --git a/app/soapbox/components/ui/text/text.tsx b/app/soapbox/components/ui/text/text.tsx index 6f67f8c4c..2e0736809 100644 --- a/app/soapbox/components/ui/text/text.tsx +++ b/app/soapbox/components/ui/text/text.tsx @@ -84,7 +84,9 @@ interface IText extends Pick, 'danger /** Whether to truncate the text if its container is too small. */ truncate?: boolean, /** Font weight of the text. */ - weight?: Weights + weight?: Weights, + /** Tooltip title. */ + title?: string, } /** UI-friendly text container with dark mode support. */ @@ -133,4 +135,7 @@ const Text: React.FC = React.forwardRef( }, ); -export default Text; +export { + Text as default, + IText, +}; diff --git a/app/soapbox/features/compose/components/search.tsx b/app/soapbox/features/compose/components/search.tsx index 9c1aab765..ec7c89468 100644 --- a/app/soapbox/features/compose/components/search.tsx +++ b/app/soapbox/features/compose/components/search.tsx @@ -115,27 +115,34 @@ const Search = (props: ISearch) => { ]; const hasValue = value.length > 0 || submitted; - const Component = autosuggest ? AutosuggestAccountInput : 'input'; + const componentProps: any = { + className: 'block w-full pl-3 pr-10 py-2 border border-gray-200 dark:border-gray-800 rounded-full leading-5 bg-gray-200 dark:bg-gray-800 dark:text-white placeholder:text-gray-600 dark:placeholder:text-gray-600 focus:outline-none focus:ring-2 focus:ring-primary-500 sm:text-sm', + type: 'text', + id: 'search', + placeholder: intl.formatMessage(messages.placeholder), + value, + onChange: handleChange, + onKeyDown: handleKeyDown, + onFocus: handleFocus, + autoFocus: autoFocus, + }; + + if (autosuggest) { + componentProps.onSelected = handleSelected; + componentProps.menu = makeMenu(); + componentProps.autoSelect = false; + } return (
- + {autosuggest ? ( + + ) : ( + + )}
= ({ {quote} - + diff --git a/app/soapbox/features/ui/components/bundle.js b/app/soapbox/features/ui/components/bundle.tsx similarity index 68% rename from app/soapbox/features/ui/components/bundle.js rename to app/soapbox/features/ui/components/bundle.tsx index 11622ec19..55f6478bc 100644 --- a/app/soapbox/features/ui/components/bundle.js +++ b/app/soapbox/features/ui/components/bundle.tsx @@ -1,21 +1,29 @@ -import PropTypes from 'prop-types'; import React from 'react'; const emptyComponent = () => null; const noop = () => { }; -class Bundle extends React.PureComponent { +interface BundleProps { + fetchComponent: () => Promise, + loading: React.ComponentType, + error: React.ComponentType<{ onRetry: (props: BundleProps) => void }>, + children: (mod: any) => React.ReactNode, + renderDelay?: number, + onFetch: () => void, + onFetchSuccess: () => void, + onFetchFail: (error: any) => void, +} - static propTypes = { - fetchComponent: PropTypes.func.isRequired, - loading: PropTypes.func, - error: PropTypes.func, - children: PropTypes.func.isRequired, - renderDelay: PropTypes.number, - onFetch: PropTypes.func, - onFetchSuccess: PropTypes.func, - onFetchFail: PropTypes.func, - } +interface BundleState { + mod: any, + forceRender: boolean, +} + +/** Fetches and renders an async component. */ +class Bundle extends React.PureComponent { + + timeout: NodeJS.Timeout | undefined; + timestamp: Date | undefined; static defaultProps = { loading: emptyComponent, @@ -37,7 +45,7 @@ class Bundle extends React.PureComponent { this.load(this.props); } - componentWillReceiveProps(nextProps) { + componentWillReceiveProps(nextProps: BundleProps) { if (nextProps.fetchComponent !== this.props.fetchComponent) { this.load(nextProps); } @@ -49,7 +57,7 @@ class Bundle extends React.PureComponent { } } - load = (props) => { + load = (props: BundleProps) => { const { fetchComponent, onFetch, onFetchSuccess, onFetchFail, renderDelay } = props || this.props; const cachedMod = Bundle.cache.get(fetchComponent); @@ -88,10 +96,10 @@ class Bundle extends React.PureComponent { render() { const { loading: Loading, error: Error, children, renderDelay } = this.props; const { mod, forceRender } = this.state; - const elapsed = this.timestamp ? (new Date() - this.timestamp) : renderDelay; + const elapsed = this.timestamp ? ((new Date()).getTime() - this.timestamp.getTime()) : renderDelay!; if (mod === undefined) { - return (elapsed >= renderDelay || forceRender) ? : null; + return (elapsed >= renderDelay! || forceRender) ? : null; } if (mod === null) { diff --git a/app/soapbox/features/ui/components/modals/report-modal/steps/other-actions-step.tsx b/app/soapbox/features/ui/components/modals/report-modal/steps/other-actions-step.tsx index e26cbbf44..e50a90778 100644 --- a/app/soapbox/features/ui/components/modals/report-modal/steps/other-actions-step.tsx +++ b/app/soapbox/features/ui/components/modals/report-modal/steps/other-actions-step.tsx @@ -14,7 +14,7 @@ import { isRemote, getDomain } from 'soapbox/utils/accounts'; import type { ReducerAccount } from 'soapbox/reducers/accounts'; const messages = defineMessages({ - addAdditionalStatuses: { id: 'report.otherActions.addAdditionl', defaultMessage: 'Would you like to add additional statuses to this report?' }, + addAdditionalStatuses: { id: 'report.otherActions.addAdditional', defaultMessage: 'Would you like to add additional statuses to this report?' }, addMore: { id: 'report.otherActions.addMore', defaultMessage: 'Add more' }, furtherActions: { id: 'report.otherActions.furtherActions', defaultMessage: 'Further actions:' }, hideAdditonalStatuses: { id: 'report.otherActions.hideAdditional', defaultMessage: 'Hide additional statuses' }, diff --git a/app/soapbox/features/ui/containers/bundle_container.js b/app/soapbox/features/ui/containers/bundle_container.tsx similarity index 74% rename from app/soapbox/features/ui/containers/bundle_container.js rename to app/soapbox/features/ui/containers/bundle_container.tsx index b12e29a43..12e4b3787 100644 --- a/app/soapbox/features/ui/containers/bundle_container.js +++ b/app/soapbox/features/ui/containers/bundle_container.tsx @@ -3,14 +3,16 @@ import { connect } from 'react-redux'; import { fetchBundleRequest, fetchBundleSuccess, fetchBundleFail } from '../../../actions/bundles'; import Bundle from '../components/bundle'; -const mapDispatchToProps = dispatch => ({ +import type { AppDispatch } from 'soapbox/store'; + +const mapDispatchToProps = (dispatch: AppDispatch) => ({ onFetch() { dispatch(fetchBundleRequest()); }, onFetchSuccess() { dispatch(fetchBundleSuccess()); }, - onFetchFail(error) { + onFetchFail(error: any) { dispatch(fetchBundleFail(error)); }, }); diff --git a/app/soapbox/hooks/__mocks__/resize-observer.ts b/app/soapbox/hooks/__mocks__/resize-observer.ts new file mode 100644 index 000000000..c75e292ea --- /dev/null +++ b/app/soapbox/hooks/__mocks__/resize-observer.ts @@ -0,0 +1,25 @@ +let listener: ((rect: any) => void) | undefined = undefined; +const mockDisconnect = jest.fn(); + +class ResizeObserver { + + constructor(ls: any) { + listener = ls; + } + + observe() { + // do nothing + } + unobserve() { + // do nothing + } + disconnect() { + mockDisconnect(); + } + +} + +// eslint-disable-next-line compat/compat +(window as any).ResizeObserver = ResizeObserver; + +export { ResizeObserver as default, listener, mockDisconnect }; \ No newline at end of file diff --git a/app/soapbox/hooks/__tests__/useDimensions.test.ts b/app/soapbox/hooks/__tests__/useDimensions.test.ts index 78524ad77..4e97fdef3 100644 --- a/app/soapbox/hooks/__tests__/useDimensions.test.ts +++ b/app/soapbox/hooks/__tests__/useDimensions.test.ts @@ -1,21 +1,13 @@ import { renderHook, act } from '@testing-library/react-hooks'; +import { listener, mockDisconnect } from '../__mocks__/resize-observer'; import { useDimensions } from '../useDimensions'; -let listener: ((rect: any) => void) | undefined = undefined; - -(window as any).ResizeObserver = class ResizeObserver { - - constructor(ls: any) { - listener = ls; - } - - observe() {} - disconnect() {} - -}; - describe('useDimensions()', () => { + beforeEach(() => { + mockDisconnect.mockClear(); + }); + it('defaults to 0', () => { const { result } = renderHook(() => useDimensions()); @@ -56,16 +48,6 @@ describe('useDimensions()', () => { }); it('disconnects on unmount', () => { - const disconnect = jest.fn(); - (window as any).ResizeObserver = class ResizeObserver { - - observe() {} - disconnect() { - disconnect(); - } - - }; - const { result, unmount } = renderHook(() => useDimensions()); act(() => { @@ -73,8 +55,8 @@ describe('useDimensions()', () => { (result.current[1] as any)(div); }); - expect(disconnect).toHaveBeenCalledTimes(0); + expect(mockDisconnect).toHaveBeenCalledTimes(0); unmount(); - expect(disconnect).toHaveBeenCalledTimes(1); + expect(mockDisconnect).toHaveBeenCalledTimes(1); }); }); diff --git a/app/soapbox/hooks/useDimensions.ts b/app/soapbox/hooks/useDimensions.ts index 2a265c4a6..bf7fc78b8 100644 --- a/app/soapbox/hooks/useDimensions.ts +++ b/app/soapbox/hooks/useDimensions.ts @@ -1,4 +1,5 @@ import { useEffect, useMemo, useState } from 'react'; +import ResizeObserver from 'resize-observer-polyfill'; type UseDimensionsRect = { width: number, height: number }; type UseDimensionsResult = [Element | null, any, any] @@ -14,7 +15,7 @@ const useDimensions = (): UseDimensionsResult => { const observer = useMemo( () => - new (window as any).ResizeObserver((entries: any) => { + new ResizeObserver((entries: any) => { if (entries[0]) { const { width, height } = entries[0].contentRect; setRect({ width, height }); diff --git a/app/soapbox/locales/pl.json b/app/soapbox/locales/pl.json index bd359fbc9..70a7b3902 100644 --- a/app/soapbox/locales/pl.json +++ b/app/soapbox/locales/pl.json @@ -73,6 +73,8 @@ "account_note.target": "Notatka o @{target}", "account_search.placeholder": "Szukaj konta", "account_timeline.column_settings.show_pinned": "Show pinned posts", + "actualStatus.edited": "Edytowano {date}", + "actualStatuses.quote_tombstone": "Wpis jest niedostępny", "admin.awaiting_approval.approved_message": "Przyjęto {acct}!", "admin.awaiting_approval.empty_message": "Nikt nie oczekuje przyjęcia. Gdy zarejestruje się nowy użytkownik, możesz zatwierdzić go tutaj.", "admin.awaiting_approval.rejected_message": "Odrzucono {acct}!", @@ -134,8 +136,8 @@ "admin_nav.awaiting_approval": "Oczekujące zgłoszenia", "admin_nav.dashboard": "Panel administracyjny", "admin_nav.reports": "Zgłoszenia", - "age_verification.header": "Wprowadź datę urodzenia", "age_verification.fail": "Musisz mieć przynajmniej {ageMinimum, plural, one {# rok} few {# lata} many {# lat} other {# lat}}.", + "age_verification.header": "Wprowadź datę urodzenia", "alert.unexpected.body": "Przepraszamy za niedogodności. Jeżeli problem nie ustanie, skontaktuj się z naszym wsparciem technicznym. Możesz też spróbować {clearCookies} (zostaniesz wylogowany(-a)).", "alert.unexpected.browser": "Przeglądarka", "alert.unexpected.clear_cookies": "wyczyścić pliki cookies i dane przeglądarki", @@ -164,16 +166,16 @@ "app_create.scopes_placeholder": "np. „read write follow”", "app_create.submit": "Utwórz aplikację", "app_create.website_label": "Strona", - "auth_layout.register": "Utwórz konto", "auth.invalid_credentials": "Nieprawidłowa nazwa użytkownika lub hasło", "auth.logged_out": "Wylogowano.", + "auth_layout.register": "Utwórz konto", "backups.actions.create": "Utwórz kopię zapasową", "backups.empty_message": "Nie znaleziono kopii zapasowych. {action}", "backups.empty_message.action": "Chcesz utworzyć?", "backups.pending": "Oczekująca", "beta.also_available": "Dostępne w językach:", - "birthdays_modal.empty": "Żaden z Twoich znajomych nie ma dziś urodzin.", "birthday_panel.title": "Urodziny", + "birthdays_modal.empty": "Żaden z Twoich znajomych nie ma dziś urodzin.", "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", @@ -283,8 +285,8 @@ "community.column_settings.title": "Ustawienia lokalnej osi czasu", "compare_history_modal.header": "Historia edycji", "compose.character_counter.title": "Wykorzystano {chars} z {maxChars} znaków", - "compose.invalid_schedule": "Musisz zaplanować wpis przynajmniej 5 minut wcześniej.", "compose.edit_success": "Twój wpis został zedytowany", + "compose.invalid_schedule": "Musisz zaplanować wpis przynajmniej 5 minut wcześniej.", "compose.submit_success": "Twój wpis został wysłany", "compose_form.direct_message_warning": "Ten wpis będzie widoczny tylko dla wszystkich wspomnianych użytkowników.", "compose_form.hashtag_warning": "Ten wpis nie będzie widoczny pod podanymi hashtagami, ponieważ jest oznaczony jako niewidoczny. Tylko publiczne wpisy mogą zostać znalezione z użyciem hashtagów.", @@ -342,6 +344,7 @@ "confirmations.block.confirm": "Zablokuj", "confirmations.block.heading": "Zablokuj @{name}", "confirmations.block.message": "Czy na pewno chcesz zablokować {name}?", + "confirmations.cancel_editing.confirm": "Anuluj edycję", "confirmations.cancel_editing.heading": "Anuluj edycję wpisu", "confirmations.cancel_editing.message": "Czy na pewno chcesz anulować edytowanie wpisu? Niezapisane zmiany zostaną utracone.", "confirmations.delete.confirm": "Usuń", @@ -432,9 +435,9 @@ "edit_profile.fields.location_label": "Lokalizacja", "edit_profile.fields.location_placeholder": "Lokalizacja", "edit_profile.fields.locked_label": "Zablokuj konto", - "edit_profile.fields.meta_fields_label": "Pola profilu", "edit_profile.fields.meta_fields.content_placeholder": "Treść", "edit_profile.fields.meta_fields.label_placeholder": "Podpis", + "edit_profile.fields.meta_fields_label": "Pola profilu", "edit_profile.fields.stranger_notifications_label": "Blokuj powiadomienia od nieznajomych", "edit_profile.fields.website_label": "Strona internetowa", "edit_profile.fields.website_placeholder": "Wyświetl link", @@ -446,7 +449,7 @@ "edit_profile.hints.header": "PNG, GIF lub JPG. Zostanie zmniejszony do {size}", "edit_profile.hints.hide_network": "To, kogo obserwujesz i kto Cię obserwuje nie będzie wyświetlane na Twoim profilu", "edit_profile.hints.locked": "Wymaga ręcznego zatwierdzania obserwacji", - "edit_profile.hints.meta_fields": "Możesz ustawić {count, plural, one {# niestandardowe pole} few {# niestandardowe pola} many {# niestandardowych pól} wyświetlanych na Twoim profilu.", + "edit_profile.hints.meta_fields": "Możesz ustawić {count, plural, one {# niestandardowe pole wyświetlane} few {# niestandardowe pola wyświetlane} many {# niestandardowych pól wyświetlanych}} na Twoim profilu.", "edit_profile.hints.stranger_notifications": "Wyświetlaj tylko powiadomienia od osób, które obserwujesz", "edit_profile.save": "Zapisz", "edit_profile.success": "Zapisano profil!", @@ -564,10 +567,11 @@ "forms.copy": "Kopiuj", "forms.hide_password": "Ukryj hasło", "forms.show_password": "Pokaż hasło", - "gdpr.accept": "Aceptuj", - "gdpr.learn_more": "Dowiedz się więcej", + "gdpr.accept": "Akceptuj", + "gdpr.learn_more": "Dowiedz się więcej", "gdpr.message": "{siteTitle} korzysta z ciasteczek sesji, które są niezbędne dla działania strony.", "gdpr.title": "{siteTitle} korzysta z ciasteczek", + "generic.saved": "Zapisano", "getting_started.open_source_notice": "{code_name} jest oprogramowaniem o otwartym źródle. Możesz pomóc w rozwoju lub zgłaszać błędy na GitLabie tutaj: {code_link} (v{code_version}).", "group.detail.archived_group": "Archived group", "group.members.empty": "Ta grupa nie ma żadnych członków.", @@ -625,6 +629,7 @@ "import_data.success.blocks": "Pomyślnie zaimportowano zablokowane konta", "import_data.success.followers": "Pomyślnie zaimportowano obserwowane konta", "import_data.success.mutes": "Pomyślnie zaimportowano wyciszone konta", + "input.copy": "Kopiuj", "input.password.hide_password": "Ukryj hasło", "input.password.show_password": "Pokazuj hasło", "intervals.full.days": "{number, plural, one {# dzień} few {# dni} many {# dni} other {# dni}}", @@ -831,8 +836,8 @@ "notifications.filter.statuses": "Nowe wpisy osób, które subskrybujesz", "notifications.group": "{count, number} {count, plural, one {powiadomienie} few {powiadomienia} many {powiadomień} more {powiadomień}}", "notifications.queue_label": "Naciśnij aby zobaczyć {count} {count, plural, one {nowe powiadomienie} few {nowe powiadomienia} many {nowych powiadomień} other {nowe powiadomienia}}", - "oauth_consumers.title": "Inne opcje logowania", "oauth_consumer.tooltip": "Zaloguj się używając {provider}", + "oauth_consumers.title": "Inne opcje logowania", "onboarding.avatar.subtitle": "Just have fun with it.", "onboarding.avatar.title": "Wybierz zdjęcie profilowe", "onboarding.display_name.subtitle": "Możesz ją zawsze zmienić później.", @@ -859,8 +864,10 @@ "patron.title": "Cel wsparcia", "pinned_accounts.title": "Polecani przez {name}", "pinned_statuses.none": "Brak przypięć do pokazania.", - "poll.chooseMultiple": "Wybierz tyle, ile potrzebujesz.", + "poll.choose_multiple": "Wybierz tyle, ile potrzebujesz.", "poll.closed": "Zamknięte", + "poll.non_anonymous": "Publiczne głosowanie", + "poll.non_anonymous.label": "Inne instancje mogą wyświetlać, które odpowiedzi wybrałeś(-aś)", "poll.refresh": "Odśwież", "poll.total_people": "{count, plural, one {# osoba} few {# osoby} many {# osób} other {# osób}}", "poll.total_votes": "{count, plural, one {# głos} few {# głosy} many {# głosów} other {# głosów}}", @@ -889,8 +896,9 @@ "preferences.fields.reduce_motion_label": "Ogranicz ruch w animacjach", "preferences.fields.system_font_label": "Używaj domyślnej czcionki systemu", "preferences.fields.theme": "Motyw", - "preferences.fields.underline_links_label": "Zawsze podkreślaj odnośniki we wpisach", + "preferences.fields.underline_links_label": "Zawsze podkreślaj odnośniki we wpisach", "preferences.fields.unfollow_modal_label": "Pokazuj prośbę o potwierdzenie przed cofnięciem obserwacji", + "preferences.hints.demetricator": "Ogranicz skutki uzależnienia od mediów społecznościowych, ukrywając wyświetlane liczby.", "preferences.hints.feed": "Na stronie głównej", "preferences.notifications.advanced": "Pokazuj wszystkie kategorie powiadomień", "preferences.options.content_type_markdown": "Markdown", @@ -918,12 +926,6 @@ "regeneration_indicator.sublabel": "Twoja oś czasu jest przygotowywana!", "register_invite.lead": "Wypełnij poniższy formularz, aby utworzyć konto.", "register_invite.title": "Otrzymałeś(-aś) zaproszenie na {siteTitle}!", - "registrations.create_account": "Utwórz konto", - "registrations.error": "Nie udało się zarejestrować konta.", - "registrations.get_started": "Rozpocznijmy!", - "registrations.success": "Witamy na {siteTitle}!", - "registrations.tagline": "Media społecznościowe, które nie wykluczają", - "registrations.unprocessable_entity": "Ta nazwa użytkownika jest już zajęta.", "registration.acceptance": "Rejestrując się, wyrażasz zgodę na {terms} i {privacy}.", "registration.agreement": "Akceptuję {tos}.", "registration.captcha.hint": "Naciśnij na obrazek, aby uzyskać nową captchę", @@ -948,6 +950,12 @@ "registration.validation.capital_letter": "1 wielka litera", "registration.validation.lowercase_letter": "1 mała litera", "registration.validation.minimum_characters": "8 znaków", + "registrations.create_account": "Utwórz konto", + "registrations.error": "Nie udało się zarejestrować konta.", + "registrations.get_started": "Rozpocznijmy!", + "registrations.success": "Witamy na {siteTitle}!", + "registrations.tagline": "Media społecznościowe, które nie wykluczają", + "registrations.unprocessable_entity": "Ta nazwa użytkownika jest już zajęta.", "relative_time.days": "{number} dni", "relative_time.hours": "{number} godz.", "relative_time.just_now": "teraz", @@ -988,7 +996,7 @@ "report.forward": "Przekaż na {target}", "report.forward_hint": "To konto znajduje się na innej instancji. Czy chcesz wysłać anonimową kopię zgłoszenia rnież na nią?", "report.next": "Dalej", - "report.otherActions.addAdditionl": "Czy chcesz uwzględnić inne wpisy w tym zgłoszeniu?", + "report.otherActions.addAdditional": "Czy chcesz uwzględnić inne wpisy w tym zgłoszeniu?", "report.otherActions.addMore": "Dodaj więcej", "report.otherActions.furtherActions": "Dodatkowe działania:", "report.otherActions.hideAdditional": "Ukryj dodatkowe wpisy", @@ -1116,8 +1124,8 @@ "soapbox_config.single_user_mode_profile_hint": "@nazwa", "soapbox_config.single_user_mode_profile_label": "Nazwa głównego użytkownika", "soapbox_config.verified_can_edit_name_label": "Pozwól zweryfikowanym użytkownikom na zmianę swojej nazwy wyświetlanej.", - "sponsored.info.title": "Dlaczego widzę tę reklamę?", "sponsored.info.message": "{siteTitle} wyświetla reklamy, aby utrzymać naszą usługę.", + "sponsored.info.title": "Dlaczego widzę tę reklamę?", "sponsored.subtitle": "Wpis sponsorowany", "status.actions.more": "Więcej", "status.admin_account": "Otwórz interfejs moderacyjny dla @{name}", @@ -1137,6 +1145,10 @@ "status.embed": "Osadź", "status.favourite": "Zareaguj", "status.filtered": "Filtrowany(-a)", + "status.in_review_summary.contact": "Jeżeli uważasz że to błąd, {link}.", + "status.in_review_summary.link": "skontaktuj się z działem pomocy", + "status.in_review_summary.summary": "Ten wpis został wysłany do weryfikacji moderatorom i jest widoczny tylko dla Ciebie.", + "status.in_review_warning": "Treści w trakcie weryfikacji", "status.load_more": "Załaduj więcej", "status.media_hidden": "Zawartość multimedialna ukryta", "status.mention": "Wspomnij o @{name}", @@ -1186,7 +1198,7 @@ "streamfield.add": "Dodaj", "streamfield.remove": "Usuń", "suggestions.dismiss": "Odrzuć sugestię", - "sw.update": "Aktualizacja", + "sw.update": "Aktualizacja", "sw.update_text": "Dostępna jest aktualizacja.", "tabs_bar.all": "Wszystkie", "tabs_bar.apps": "Aplikacje", diff --git a/app/soapbox/normalizers/__tests__/poll.test.ts b/app/soapbox/normalizers/__tests__/poll.test.ts index 8acf2ece4..b7ba0a46f 100644 --- a/app/soapbox/normalizers/__tests__/poll.test.ts +++ b/app/soapbox/normalizers/__tests__/poll.test.ts @@ -21,7 +21,6 @@ describe('normalizePoll()', () => { expect(ImmutableRecord.isRecord(result)).toBe(true); expect(ImmutableRecord.isRecord(result.options.get(0))).toBe(true); expect(result.toJS()).toMatchObject(expected); - expect(result.expires_at instanceof Date).toBe(true); }); it('normalizes a Pleroma logged-out poll', () => { diff --git a/app/soapbox/normalizers/__tests__/status.test.ts b/app/soapbox/normalizers/__tests__/status.test.ts index 43336d00f..b60373975 100644 --- a/app/soapbox/normalizers/__tests__/status.test.ts +++ b/app/soapbox/normalizers/__tests__/status.test.ts @@ -164,7 +164,6 @@ describe('normalizeStatus()', () => { expect(ImmutableRecord.isRecord(poll)).toBe(true); expect(ImmutableRecord.isRecord(poll.options.get(0))).toBe(true); expect(poll.toJS()).toMatchObject(expected); - expect(poll.expires_at instanceof Date).toBe(true); }); it('normalizes a Pleroma logged-out poll', () => { diff --git a/app/soapbox/normalizers/account.ts b/app/soapbox/normalizers/account.ts index 1a519b8a8..37f42ab8f 100644 --- a/app/soapbox/normalizers/account.ts +++ b/app/soapbox/normalizers/account.ts @@ -26,7 +26,7 @@ export const AccountRecord = ImmutableRecord({ avatar_static: '', birthday: '', bot: false, - created_at: new Date(), + created_at: '', discoverable: false, display_name: '', emojis: ImmutableList(), @@ -38,7 +38,7 @@ export const AccountRecord = ImmutableRecord({ header: '', header_static: '', id: '', - last_status_at: new Date(), + last_status_at: '', location: '', locked: false, moved: null as EmbeddedEntity, @@ -78,7 +78,7 @@ export const FieldRecord = ImmutableRecord({ value_plain: '', }); -// https://gitlab.com/soapbox-pub/soapbox-fe/-/issues/549 +// https://gitlab.com/soapbox-pub/soapbox/-/issues/549 const normalizePleromaLegacyFields = (account: ImmutableMap) => { return account.update('pleroma', ImmutableMap(), (pleroma: ImmutableMap) => { return pleroma.withMutations(pleroma => { diff --git a/app/soapbox/normalizers/poll.ts b/app/soapbox/normalizers/poll.ts index fb0f786ed..efae796c3 100644 --- a/app/soapbox/normalizers/poll.ts +++ b/app/soapbox/normalizers/poll.ts @@ -21,7 +21,7 @@ import type { Emoji, PollOption } from 'soapbox/types/entities'; export const PollRecord = ImmutableRecord({ emojis: ImmutableList(), expired: false, - expires_at: new Date(), + expires_at: '', id: '', multiple: false, options: ImmutableList(), diff --git a/app/soapbox/normalizers/status.ts b/app/soapbox/normalizers/status.ts index 6f35a3900..4788758d3 100644 --- a/app/soapbox/normalizers/status.ts +++ b/app/soapbox/normalizers/status.ts @@ -28,8 +28,8 @@ export const StatusRecord = ImmutableRecord({ bookmarked: false, card: null as Card | null, content: '', - created_at: new Date(), - edited_at: null as Date | null, + created_at: '', + edited_at: null as string | null, emojis: ImmutableList(), favourited: false, favourites_count: 0, diff --git a/app/soapbox/pages/profile_page.tsx b/app/soapbox/pages/profile_page.tsx index 9c5a4a55e..f4ff974e0 100644 --- a/app/soapbox/pages/profile_page.tsx +++ b/app/soapbox/pages/profile_page.tsx @@ -105,7 +105,7 @@ const ProfilePage: React.FC = ({ params, children }) => { {account && showTabs && ( - + )} {children} diff --git a/app/soapbox/reducers/custom_emojis.ts b/app/soapbox/reducers/custom_emojis.ts index 477e7cce9..38b54a673 100644 --- a/app/soapbox/reducers/custom_emojis.ts +++ b/app/soapbox/reducers/custom_emojis.ts @@ -20,7 +20,7 @@ const importEmojis = (customEmojis: APIEntity[]) => { const emojis = (fromJS(customEmojis) as ImmutableList>).filter((emoji) => { // If a custom emoji has the shortcode of a Unicode emoji, skip it. // Otherwise it breaks EmojiMart. - // https://gitlab.com/soapbox-pub/soapbox-fe/-/issues/610 + // https://gitlab.com/soapbox-pub/soapbox/-/issues/610 const shortcode = emoji.get('shortcode', '').toLowerCase(); return !emojiData[shortcode]; }); diff --git a/app/soapbox/reducers/notifications.js b/app/soapbox/reducers/notifications.js index 08b87e4f0..d7697d4dd 100644 --- a/app/soapbox/reducers/notifications.js +++ b/app/soapbox/reducers/notifications.js @@ -67,7 +67,7 @@ const fixNotification = notification => { const isValid = notification => { try { - // https://gitlab.com/soapbox-pub/soapbox-fe/-/issues/424 + // https://gitlab.com/soapbox-pub/soapbox/-/issues/424 if (!notification.account.id) { return false; } diff --git a/app/soapbox/reducers/timelines.ts b/app/soapbox/reducers/timelines.ts index cd24d9df3..def257407 100644 --- a/app/soapbox/reducers/timelines.ts +++ b/app/soapbox/reducers/timelines.ts @@ -242,7 +242,7 @@ const timelineDisconnect = (state: State, timelineId: string) => { if (items.isEmpty()) return; // This is causing problems. Disable for now. - // https://gitlab.com/soapbox-pub/soapbox-fe/-/issues/716 + // https://gitlab.com/soapbox-pub/soapbox/-/issues/716 // timeline.set('items', addStatusId(items, null)); })); }; diff --git a/app/soapbox/selectors/index.ts b/app/soapbox/selectors/index.ts index 2a026afe1..84165fc98 100644 --- a/app/soapbox/selectors/index.ts +++ b/app/soapbox/selectors/index.ts @@ -59,7 +59,7 @@ const findAccountsByUsername = (state: RootState, username: string) => { const accounts = state.accounts; return accounts.filter(account => { - return username.toLowerCase() === account.acct.toLowerCase(); + return username.toLowerCase() === account?.acct.toLowerCase(); }); }; diff --git a/app/soapbox/utils/features.ts b/app/soapbox/utils/features.ts index 0a04e0a92..fd61b9faf 100644 --- a/app/soapbox/utils/features.ts +++ b/app/soapbox/utils/features.ts @@ -46,8 +46,8 @@ export const PIXELFED = 'Pixelfed'; export const TRUTHSOCIAL = 'TruthSocial'; /** - * Soapbox BE, the recommended Pleroma fork for Soapbox. - * @see {@link https://gitlab.com/soapbox-pub/soapbox-be} + * Rebased, the recommended backend for Soapbox. + * @see {@link https://gitlab.com/soapbox-pub/rebased} */ export const SOAPBOX = 'soapbox'; diff --git a/app/soapbox/utils/status.ts b/app/soapbox/utils/status.ts index 6f03b1ce0..66e380b5a 100644 --- a/app/soapbox/utils/status.ts +++ b/app/soapbox/utils/status.ts @@ -35,7 +35,7 @@ export const shouldHaveCard = (status: StatusEntity): boolean => { }; /** Whether the media IDs on this status have integer IDs (opposed to FlakeIds). */ -// https://gitlab.com/soapbox-pub/soapbox-fe/-/merge_requests/1087 +// https://gitlab.com/soapbox-pub/soapbox/-/merge_requests/1087 export const hasIntegerMediaIds = (status: StatusEntity): boolean => { return status.media_attachments.some(({ id }) => isIntegerId(id)); }; diff --git a/docs/administration/deploy-at-scale.md b/docs/administration/deploy-at-scale.md index 9d413fb0a..40e878a0a 100644 --- a/docs/administration/deploy-at-scale.md +++ b/docs/administration/deploy-at-scale.md @@ -11,7 +11,7 @@ The best way to get Soapbox builds is from a GitLab CI job. The official build URL is here: ``` -https://gitlab.com/soapbox-pub/soapbox-fe/-/jobs/artifacts/develop/download?job=build-production +https://gitlab.com/soapbox-pub/soapbox/-/jobs/artifacts/develop/download?job=build-production ``` (Note that `develop` in that URL can be replaced with any git ref, eg `v2.0.0`, and thus will be updated with the latest zip whenever a new commit is pushed to `develop`.) @@ -44,7 +44,7 @@ location ~ ^/(api|oauth|admin) { } ``` -We recommend trying [`mastodon.conf`](https://gitlab.com/soapbox-pub/soapbox-fe/-/blob/develop/installation/mastodon.conf) as a starting point. +We recommend trying [`mastodon.conf`](https://gitlab.com/soapbox-pub/soapbox/-/blob/develop/installation/mastodon.conf) as a starting point. It is fine-tuned, includes support for federation, and should work with any backend. ## The ServiceWorker diff --git a/docs/administration/install-subdomain.md b/docs/administration/install-subdomain.md index 513d8dd93..34a8cb37b 100644 --- a/docs/administration/install-subdomain.md +++ b/docs/administration/install-subdomain.md @@ -13,7 +13,7 @@ mkdir -p /opt/soapbox Fetch the build. ```sh -curl -L https://gitlab.com/soapbox-pub/soapbox-fe/-/jobs/artifacts/v1.3.0/download?job=build-production -o /tmp/soapbox-fe.zip +curl -L https://gitlab.com/soapbox-pub/soapbox/-/jobs/artifacts/v1.3.0/download?job=build-production -o /tmp/soapbox-fe.zip ``` Unzip the build. diff --git a/docs/administration/install-yunohost.md b/docs/administration/install-yunohost.md index d5cba3ba6..af99231b4 100644 --- a/docs/administration/install-yunohost.md +++ b/docs/administration/install-yunohost.md @@ -7,7 +7,7 @@ If you want to install Soapbox to a Pleroma instance installed using [YunoHost]( First, download the latest build of Soapbox from GitLab. ```sh -curl -L https://gitlab.com/soapbox-pub/soapbox-fe/-/jobs/artifacts/v1.3.0/download?job=build-production -o soapbox-fe.zip +curl -L https://gitlab.com/soapbox-pub/soapbox/-/jobs/artifacts/v1.3.0/download?job=build-production -o soapbox-fe.zip ``` ## 2. Unzip the build diff --git a/docs/administration/mastodon.md b/docs/administration/mastodon.md index d8261d9de..345408ad1 100644 --- a/docs/administration/mastodon.md +++ b/docs/administration/mastodon.md @@ -8,7 +8,7 @@ To do so, shell into your server and unpack Soapbox: ```sh mkdir -p /opt/soapbox -curl -L https://gitlab.com/soapbox-pub/soapbox-fe/-/jobs/artifacts/develop/download?job=build-production -o soapbox-fe.zip +curl -L https://gitlab.com/soapbox-pub/soapbox/-/jobs/artifacts/develop/download?job=build-production -o soapbox-fe.zip busybox unzip soapbox-fe.zip -o -d /opt/soapbox ``` @@ -17,7 +17,7 @@ Now create an Nginx file for Soapbox with Mastodon. If you already have one, replace it: ```sh -curl https://gitlab.com/soapbox-pub/soapbox-fe/-/raw/develop/installation/mastodon.conf > /etc/nginx/sites-available/mastodon +curl https://gitlab.com/soapbox-pub/soapbox/-/raw/develop/installation/mastodon.conf > /etc/nginx/sites-available/mastodon ``` Edit this file and replace all occurrences of `example.com` with your domain name. diff --git a/docs/administration/updating.md b/docs/administration/updating.md index ddfb62e08..6e5252efa 100644 --- a/docs/administration/updating.md +++ b/docs/administration/updating.md @@ -1,6 +1,6 @@ # Updating Soapbox -You should always check the [release notes/changelog](https://gitlab.com/soapbox-pub/soapbox-fe/-/blob/develop/CHANGELOG.md) in case there are deprecations, special update changes, etc. +You should always check the [release notes/changelog](https://gitlab.com/soapbox-pub/soapbox/-/blob/develop/CHANGELOG.md) in case there are deprecations, special update changes, etc. Besides that, it's relatively pretty easy to update Soapbox. There's two ways to go about it: with the command line or with an unofficial script. @@ -10,7 +10,7 @@ To update Soapbox via the command line, do the following: ``` # Download the build. -curl -L https://gitlab.com/soapbox-pub/soapbox-fe/-/jobs/artifacts/develop/download?job=build-production -o soapbox-fe.zip +curl -L https://gitlab.com/soapbox-pub/soapbox/-/jobs/artifacts/develop/download?job=build-production -o soapbox-fe.zip # Remove all the current Soapbox build in Pleroma's instance directory. rm -R /opt/pleroma/instance/static/packs diff --git a/docs/contributing.md b/docs/contributing.md index 47a7b747d..bb59effc7 100644 --- a/docs/contributing.md +++ b/docs/contributing.md @@ -2,20 +2,20 @@ Thank you for your interest in Soapbox! -When contributing to Soapbox, please first discuss the change you wish to make by [opening an issue](https://gitlab.com/soapbox-pub/soapbox-fe/-/issues). +When contributing to Soapbox, please first discuss the change you wish to make by [opening an issue](https://gitlab.com/soapbox-pub/soapbox/-/issues). ## Opening an MR (merge request) 1. Smash that "fork" button on GitLab to make a copy of the repo. 2. Clone the repo locally, then begin work on a new branch (eg not `develop`). 3. Push your branch to your fork. -4. Once pushed, GitLab should provide you with a URL to open a new merge request right in your terminal. If not, do it [manually](https://gitlab.com/soapbox-pub/soapbox-fe/-/merge_requests/new). +4. Once pushed, GitLab should provide you with a URL to open a new merge request right in your terminal. If not, do it [manually](https://gitlab.com/soapbox-pub/soapbox/-/merge_requests/new). ### Ensuring the CI pipeline succeeds When you push to a branch, the CI pipeline will run. -[Soapbox uses GitLab CI](https://gitlab.com/soapbox-pub/soapbox-fe/-/blob/develop/.gitlab-ci.yml) to lint, run tests, and verify changes. +[Soapbox uses GitLab CI](https://gitlab.com/soapbox-pub/soapbox/-/blob/develop/.gitlab-ci.yml) to lint, run tests, and verify changes. It's important this pipeline passes, otherwise we cannot merge the change. New users of gitlab.com may see a "detatched pipeline" error. @@ -31,4 +31,4 @@ We recommend developing Soapbox with [VSCodium](https://vscodium.com/) (or its p This will help give you feedback about your changes _in the editor itself_ before GitLab CI performs linting, etc. When this project is opened in Code it will automatically recommend extensions. -See [`.vscode/extensions.json`](https://gitlab.com/soapbox-pub/soapbox-fe/-/blob/develop/.vscode/extensions.json) for the full list. +See [`.vscode/extensions.json`](https://gitlab.com/soapbox-pub/soapbox/-/blob/develop/.vscode/extensions.json) for the full list. diff --git a/docs/development/developing-backend.md b/docs/development/developing-backend.md index af4400e9f..723a28002 100644 --- a/docs/development/developing-backend.md +++ b/docs/development/developing-backend.md @@ -48,7 +48,7 @@ Typically checks are done against `BACKEND_NAME` and `VERSION`. The version string is similar in purpose to a [User-Agent](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/User-Agent) string. The format was first invented by Pleroma, but is now widely used, including by Pixelfed, Mitra, and Soapbox BE. -See [`features.ts`](https://gitlab.com/soapbox-pub/soapbox-fe/-/blob/develop/app/soapbox/utils/features.ts) for the complete list of features. +See [`features.ts`](https://gitlab.com/soapbox-pub/soapbox/-/blob/develop/app/soapbox/utils/features.ts) for the complete list of features. ## Forks of other software @@ -73,4 +73,4 @@ For Pleroma forks, the fork name should be in the compat section (eg Soapbox BE) ## Adding support for a new backend -If the backend conforms to the above format, please modify [`features.ts`](https://gitlab.com/soapbox-pub/soapbox-fe/-/blob/develop/app/soapbox/utils/features.ts) and submit a merge request to enable features for your backend! +If the backend conforms to the above format, please modify [`features.ts`](https://gitlab.com/soapbox-pub/soapbox/-/blob/develop/app/soapbox/utils/features.ts) and submit a merge request to enable features for your backend! diff --git a/docs/development/how-it-works.md b/docs/development/how-it-works.md index 68aa0e5ee..52a326d8a 100644 --- a/docs/development/how-it-works.md +++ b/docs/development/how-it-works.md @@ -18,7 +18,7 @@ location / { } ``` -(See [`mastodon.conf`](https://gitlab.com/soapbox-pub/soapbox-fe/-/blob/develop/installation/mastodon.conf) for a full example.) +(See [`mastodon.conf`](https://gitlab.com/soapbox-pub/soapbox/-/blob/develop/installation/mastodon.conf) for a full example.) Soapbox incorporates much of the [Mastodon API](https://docs.joinmastodon.org/methods/), [Pleroma API](https://api.pleroma.social/), and more. It detects features supported by the backend to provide the right experience for the backend. diff --git a/docs/development/running-locally.md b/docs/development/running-locally.md index d11c59396..7cd1164a6 100644 --- a/docs/development/running-locally.md +++ b/docs/development/running-locally.md @@ -3,7 +3,7 @@ To get it running, just clone the repo: ``` -git clone https://gitlab.com/soapbox-pub/soapbox-fe.git +git clone https://gitlab.com/soapbox-pub/soapbox.git cd soapbox-fe ``` @@ -40,5 +40,5 @@ Try again. ## Troubleshooting: it's not working! -Run `node -V` and compare your Node.js version with the version in [`.tool-versions`](https://gitlab.com/soapbox-pub/soapbox-fe/-/blob/develop/.tool-versions). +Run `node -V` and compare your Node.js version with the version in [`.tool-versions`](https://gitlab.com/soapbox-pub/soapbox/-/blob/develop/.tool-versions). If they don't match, try installing [asdf](https://asdf-vm.com/). diff --git a/docs/installing.md b/docs/installing.md index fb659e751..37c9c36e5 100644 --- a/docs/installing.md +++ b/docs/installing.md @@ -10,7 +10,7 @@ First, follow the instructions to [install Pleroma](https://docs-develop.pleroma The Soapbox frontend is the main component of Soapbox. Once you've installed Pleroma, installing Soapbox is a breeze. -First, ssh into the server and download a .zip of the latest build: ``curl -L https://gitlab.com/soapbox-pub/soapbox-fe/-/jobs/artifacts/develop/download?job=build-production -o soapbox-fe.zip`` +First, ssh into the server and download a .zip of the latest build: ``curl -L https://gitlab.com/soapbox-pub/soapbox/-/jobs/artifacts/develop/download?job=build-production -o soapbox-fe.zip`` Then unpack it into Pleroma's ``instance`` directory: ``busybox unzip soapbox-fe.zip -o -d /opt/pleroma/instance`` diff --git a/heroku.yml b/heroku.yml new file mode 100644 index 000000000..8eec25b9c --- /dev/null +++ b/heroku.yml @@ -0,0 +1,3 @@ +build: + docker: + web: Dockerfile diff --git a/installation/docker.conf.template b/installation/docker.conf.template new file mode 100644 index 000000000..0938b756e --- /dev/null +++ b/installation/docker.conf.template @@ -0,0 +1,118 @@ +# Soapbox Nginx for Docker. +# It's intended to be used by the official nginx image, which has templating functionality. +# Mount at: `/etc/nginx/templates/default.conf.template` + +map_hash_bucket_size 128; + +map $http_upgrade $connection_upgrade { + default upgrade; + '' close; +} + +# ActivityPub routing. +map $http_accept $activitypub_location { + default @soapbox; + "application/activity+json" @backend; + 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"' @backend; +} + +proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=CACHE:10m inactive=7d max_size=1g; + +# Fake backend for when BACKEND_URL isn't defined. +server { + listen ${FALLBACK_PORT}; + listen [::]:${FALLBACK_PORT}; + + location / { + add_header Content-Type "application/json" always; + return 404 '{"error": "Not implemented"}'; + } +} + +server { + listen ${PORT}; + listen [::]:${PORT}; + + keepalive_timeout 70; + sendfile on; + client_max_body_size 80m; + + root /usr/share/nginx/html; + + gzip on; + gzip_disable "msie6"; + gzip_vary on; + gzip_proxied any; + gzip_comp_level 6; + gzip_buffers 16 8k; + gzip_http_version 1.1; + gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript image/svg+xml image/x-icon; + + add_header Strict-Transport-Security "max-age=31536000" always; + + # Content Security Policy (CSP) + # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy + add_header Content-Security-Policy "${CSP}"; + + # Fallback route. + # Try static files, then fall back to the SPA. + location / { + try_files $uri @soapbox; + } + + # Backend routes. + # These are routes to the backend's API and important rendered pages. + location ~ ^/(api|oauth|auth|admin|pghero|sidekiq|manifest.json|media|nodeinfo|unsubscribe|.well-known/(webfinger|host-meta|nodeinfo|change-password)|@(.+)/embed$) { + try_files /dev/null @backend; + } + + # Backend ActivityPub routes. + # Conditionally send to the backend by Accept header. + location ~ ^/(inbox|users|@(.+)) { + try_files /dev/null $activitypub_location; + } + + # Soapbox build files. + # New builds produce hashed filenames, so these should be cached heavily. + location /packs { + add_header Cache-Control "public, max-age=31536000, immutable"; + add_header Strict-Transport-Security "max-age=31536000" always; + } + + # Soapbox ServiceWorker. + location = /sw.js { + add_header Cache-Control "public, max-age=0"; + add_header Strict-Transport-Security "max-age=31536000" always; + } + + # Soapbox SPA (Single Page App). + location @soapbox { + try_files /index.html /dev/null; + } + + # Proxy to the backend. + location @backend { + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header Proxy ""; + proxy_pass_header Server; + + proxy_pass "${BACKEND_URL}"; + proxy_buffering on; + proxy_redirect off; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + + proxy_cache CACHE; + proxy_cache_valid 200 7d; + proxy_cache_valid 410 24h; + proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504; + add_header X-Cached $upstream_cache_status; + add_header Strict-Transport-Security "max-age=31536000" always; + + tcp_nodelay on; + } +} diff --git a/package.json b/package.json index 8cb1e8a4b..eef743978 100644 --- a/package.json +++ b/package.json @@ -1,19 +1,19 @@ { - "name": "soapbox-fe", + "name": "soapbox", "displayName": "Soapbox", "version": "3.0.0", "description": "Soapbox frontend for the Fediverse.", "homepage": "https://soapbox.pub/", "repository": { "type": "git", - "url": "https://gitlab.com/soapbox-pub/soapbox-fe" + "url": "https://gitlab.com/soapbox-pub/soapbox" }, "keywords": [ "fediverse", "pleroma" ], "bugs": { - "url": "https://gitlab.com/soapbox-pub/soapbox-fe/-/issues" + "url": "https://gitlab.com/soapbox-pub/soapbox/-/issues" }, "scripts": { "start": "npx webpack-dev-server", @@ -181,6 +181,7 @@ "redux-thunk": "^2.2.0", "requestidlecallback": "^0.3.0", "reselect": "^4.0.0", + "resize-observer-polyfill": "^1.5.1", "sass": "^1.20.3", "sass-loader": "^13.0.0", "semver": "^7.3.2", diff --git a/webpack/production.js b/webpack/production.js index 9bd16e045..e1d833abc 100644 --- a/webpack/production.js +++ b/webpack/production.js @@ -113,6 +113,7 @@ module.exports = merge(sharedConfig, { '/objects', '/ostatus_subscribe', '/pghero', + '/phoenix', '/pleroma', '/proxy', '/relay',