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 (
{intl.formatMessage(messages.placeholder)}
-
+ {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',