Merge remote-tracking branch 'origin/develop' into chats
This commit is contained in:
commit
6b38e37019
58 changed files with 434 additions and 178 deletions
34
.dockerignore
Normal file
34
.dockerignore
Normal file
|
@ -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
|
|
@ -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
|
1
.vscode/extensions.json
vendored
1
.vscode/extensions.json
vendored
|
@ -1,6 +1,5 @@
|
|||
{
|
||||
"recommendations": [
|
||||
"editorconfig.editorconfig",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"bradlc.vscode-tailwindcss",
|
||||
"stylelint.vscode-stylelint",
|
||||
|
|
9
.vscode/settings.json
vendored
Normal file
9
.vscode/settings.json
vendored
Normal file
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"editor.insertSpaces": true,
|
||||
"editor.tabSize": 2,
|
||||
"files.associations": {
|
||||
"*.conf.template": "properties"
|
||||
},
|
||||
"files.eol": "\n",
|
||||
"files.insertFinalNewline": false
|
||||
}
|
2
.vscode/soapbox.code-snippets
vendored
2
.vscode/soapbox.code-snippets
vendored
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
17
Dockerfile
Normal file
17
Dockerfile
Normal file
|
@ -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
|
12
README.md
12
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
|
||||
|
|
7
app.json
Normal file
7
app.json
Normal file
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"name": "Soapbox",
|
||||
"description": "Software for the next generation of social media.",
|
||||
"keywords": ["fediverse"],
|
||||
"website": "https://soapbox.pub",
|
||||
"stack": "container"
|
||||
}
|
|
@ -23,6 +23,5 @@
|
|||
</ol>
|
||||
|
||||
<h1 id="opensource">Open Source Software</h1>
|
||||
<p>Soapbox is free and open source (FOSS) software that runs atop a Pleroma server</p>
|
||||
<p>The Soapbox repository can be found at <a href="https://gitlab.com/soapbox-pub/soapbox-fe">Soapbox-fe</a></p>
|
||||
<p>The Pleroma server repository can be found at <a href="https://git.pleroma.social/pleroma/pleroma">Pleroma-be</a></p>
|
||||
<p>Soapbox is free and open source (FOSS) software.</p>
|
||||
<p>The Soapbox repository can be found at <a href="https://gitlab.com/soapbox-pub/soapbox">Soapbox</a></p>
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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 });
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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<IPollFooter> = ({ poll, showResults, selected }): JSX
|
|||
</Button>
|
||||
)}
|
||||
|
||||
<HStack space={1.5} alignItems='center'>
|
||||
<HStack space={1.5} alignItems='center' wrap>
|
||||
{poll.pleroma.get('non_anonymous') && (
|
||||
<>
|
||||
<Tooltip text={intl.formatMessage(messages.nonAnonymous)}>
|
||||
|
|
|
@ -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<IPoll> = ({ id, status }): JSX.Element | null => {
|
||||
|
|
|
@ -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<RelativeTimestampProps, RelativeTimestampState> {
|
||||
|
||||
_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 (
|
||||
<Text {...textProps} theme={theme} tag='time' title={intl.formatDate(date, dateFormatOptions)}>
|
||||
|
@ -188,3 +196,5 @@ class RelativeTimestamp extends React.Component {
|
|||
}
|
||||
|
||||
}
|
||||
|
||||
export default injectIntl(RelativeTimestamp);
|
|
@ -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<VirtuosoHandle, IScrollableList>(({
|
|||
{isLoading ? (
|
||||
<Spinner />
|
||||
) : (
|
||||
<Text>{emptyMessage}</Text>
|
||||
emptyMessage
|
||||
)}
|
||||
</Card>
|
||||
</div>
|
||||
|
|
|
@ -301,12 +301,12 @@ const StatusActionBar: React.FC<IStatusActionBar> = ({
|
|||
};
|
||||
|
||||
const handleCopy: React.EventHandler<React.MouseEvent> = (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);
|
||||
|
|
|
@ -42,11 +42,13 @@ interface IHStack {
|
|||
grow?: boolean,
|
||||
/** Extra CSS styles for the <div> */
|
||||
style?: React.CSSProperties
|
||||
/** Whether to let the flexbox wrap onto multiple lines. */
|
||||
wrap?: boolean,
|
||||
}
|
||||
|
||||
/** Horizontal row of child elements. */
|
||||
const HStack = forwardRef<HTMLDivElement, IHStack>((props, ref) => {
|
||||
const { space, alignItems, grow, justifyContent, className, ...filteredProps } = props;
|
||||
const { space, alignItems, grow, justifyContent, wrap, className, ...filteredProps } = props;
|
||||
|
||||
return (
|
||||
<div
|
||||
|
@ -60,6 +62,7 @@ const HStack = forwardRef<HTMLDivElement, IHStack>((props, ref) => {
|
|||
// @ts-ignore
|
||||
[spaces[space]]: typeof space !== 'undefined',
|
||||
'flex-grow': grow,
|
||||
'flex-wrap': wrap,
|
||||
}, className)}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -38,12 +38,13 @@ interface IStack extends React.HTMLAttributes<HTMLDivElement> {
|
|||
}
|
||||
|
||||
/** Vertical stack of child elements. */
|
||||
const Stack: React.FC<IStack> = (props) => {
|
||||
const Stack: React.FC<IStack> = React.forwardRef((props, ref: React.LegacyRef<HTMLDivElement> | undefined) => {
|
||||
const { space, alignItems, justifyContent, className, grow, ...filteredProps } = props;
|
||||
|
||||
return (
|
||||
<div
|
||||
{...filteredProps}
|
||||
ref={ref}
|
||||
className={classNames('flex flex-col', {
|
||||
// @ts-ignore
|
||||
[spaces[space]]: typeof space !== 'undefined',
|
||||
|
@ -55,6 +56,6 @@ const Stack: React.FC<IStack> = (props) => {
|
|||
}, className)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
export default Stack;
|
||||
|
|
|
@ -84,7 +84,9 @@ interface IText extends Pick<React.HTMLAttributes<HTMLParagraphElement>, '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<IText> = React.forwardRef(
|
|||
},
|
||||
);
|
||||
|
||||
export default Text;
|
||||
export {
|
||||
Text as default,
|
||||
IText,
|
||||
};
|
||||
|
|
|
@ -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 (
|
||||
<div className='w-full'>
|
||||
<label htmlFor='search' className='sr-only'>{intl.formatMessage(messages.placeholder)}</label>
|
||||
|
||||
<div className='relative'>
|
||||
<Component
|
||||
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={value}
|
||||
onChange={handleChange}
|
||||
onKeyDown={handleKeyDown}
|
||||
onFocus={handleFocus}
|
||||
onSelected={handleSelected}
|
||||
autoFocus={autoFocus}
|
||||
autoSelect={false}
|
||||
menu={makeMenu()}
|
||||
/>
|
||||
{autosuggest ? (
|
||||
<AutosuggestAccountInput {...componentProps} />
|
||||
) : (
|
||||
<input {...componentProps} />
|
||||
)}
|
||||
|
||||
<div
|
||||
role='button'
|
||||
|
|
|
@ -6,7 +6,7 @@ import { getSettings } from 'soapbox/actions/settings';
|
|||
import Avatar from 'soapbox/components/avatar';
|
||||
import DisplayName from 'soapbox/components/display-name';
|
||||
import Permalink from 'soapbox/components/permalink';
|
||||
import RelativeTimestamp from 'soapbox/components/relative_timestamp';
|
||||
import RelativeTimestamp from 'soapbox/components/relative-timestamp';
|
||||
import { Text } from 'soapbox/components/ui';
|
||||
import ActionButton from 'soapbox/features/ui/components/action-button';
|
||||
import { useAppSelector } from 'soapbox/hooks';
|
||||
|
|
|
@ -99,7 +99,7 @@ const DetailedStatus: React.FC<IDetailedStatus> = ({
|
|||
|
||||
{quote}
|
||||
|
||||
<HStack justifyContent='between' alignItems='center' className='py-2'>
|
||||
<HStack justifyContent='between' alignItems='center' className='py-2' wrap>
|
||||
<StatusInteractionBar status={actualStatus} />
|
||||
|
||||
<HStack space={1} alignItems='center'>
|
||||
|
|
|
@ -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<any>,
|
||||
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<BundleProps, BundleState> {
|
||||
|
||||
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) ? <Loading /> : null;
|
||||
return (elapsed >= renderDelay! || forceRender) ? <Loading /> : null;
|
||||
}
|
||||
|
||||
if (mod === null) {
|
|
@ -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' },
|
||||
|
|
|
@ -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));
|
||||
},
|
||||
});
|
25
app/soapbox/hooks/__mocks__/resize-observer.ts
Normal file
25
app/soapbox/hooks/__mocks__/resize-observer.ts
Normal file
|
@ -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 };
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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 });
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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<Emoji>(),
|
||||
|
@ -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<any>,
|
||||
|
@ -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<string, any>) => {
|
||||
return account.update('pleroma', ImmutableMap(), (pleroma: ImmutableMap<string, any>) => {
|
||||
return pleroma.withMutations(pleroma => {
|
||||
|
|
|
@ -21,7 +21,7 @@ import type { Emoji, PollOption } from 'soapbox/types/entities';
|
|||
export const PollRecord = ImmutableRecord({
|
||||
emojis: ImmutableList<Emoji>(),
|
||||
expired: false,
|
||||
expires_at: new Date(),
|
||||
expires_at: '',
|
||||
id: '',
|
||||
multiple: false,
|
||||
options: ImmutableList<PollOption>(),
|
||||
|
|
|
@ -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<Emoji>(),
|
||||
favourited: false,
|
||||
favourites_count: 0,
|
||||
|
|
|
@ -105,7 +105,7 @@ const ProfilePage: React.FC<IProfilePage> = ({ params, children }) => {
|
|||
</BundleContainer>
|
||||
|
||||
{account && showTabs && (
|
||||
<Tabs items={tabItems} activeItem={activeItem} />
|
||||
<Tabs key={`profile-tabs-${account.id}`} items={tabItems} activeItem={activeItem} />
|
||||
)}
|
||||
|
||||
{children}
|
||||
|
|
|
@ -20,7 +20,7 @@ const importEmojis = (customEmojis: APIEntity[]) => {
|
|||
const emojis = (fromJS(customEmojis) as ImmutableList<ImmutableMap<string, string>>).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];
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
}));
|
||||
};
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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));
|
||||
};
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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!
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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/).
|
||||
|
|
|
@ -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``
|
||||
|
||||
|
|
3
heroku.yml
Normal file
3
heroku.yml
Normal file
|
@ -0,0 +1,3 @@
|
|||
build:
|
||||
docker:
|
||||
web: Dockerfile
|
118
installation/docker.conf.template
Normal file
118
installation/docker.conf.template
Normal file
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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",
|
||||
|
|
|
@ -113,6 +113,7 @@ module.exports = merge(sharedConfig, {
|
|||
'/objects',
|
||||
'/ostatus_subscribe',
|
||||
'/pghero',
|
||||
'/phoenix',
|
||||
'/pleroma',
|
||||
'/proxy',
|
||||
'/relay',
|
||||
|
|
Loading…
Reference in a new issue