Merge branch 'chats' into alex-chats

chats-router
Justin 2 years ago
commit e333a5042e

@ -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,6 +1,5 @@
{
"recommendations": [
"editorconfig.editorconfig",
"dbaeumer.vscode-eslint",
"bradlc.vscode-tailwindcss",
"stylelint.vscode-stylelint",

@ -0,0 +1,9 @@
{
"editor.insertSpaces": true,
"editor.tabSize": 2,
"files.associations": {
"*.conf.template": "properties"
},
"files.eol": "\n",
"files.insertFinalNewline": false
}

@ -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

@ -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

@ -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

@ -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)}
/>
);

@ -39,12 +39,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',
@ -56,6 +57,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 {
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 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,
}
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));
},
});

@ -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``

@ -0,0 +1,3 @@
build:
docker:
web: Dockerfile

@ -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…
Cancel
Save