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: only:
refs: refs:
- develop - 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": [ "recommendations": [
"editorconfig.editorconfig",
"dbaeumer.vscode-eslint", "dbaeumer.vscode-eslint",
"bradlc.vscode-tailwindcss", "bradlc.vscode-tailwindcss",
"stylelint.vscode-stylelint", "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 // 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 // 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: // 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 ### Added
- Initial beta release. - Initial beta release.
[Unreleased]: https://gitlab.com/soapbox-pub/soapbox-fe/-/compare/v1.0.0...develop [Unreleased]: https://gitlab.com/soapbox-pub/soapbox/-/compare/v1.0.0...develop
[Unreleased patch]: https://gitlab.com/soapbox-pub/soapbox-fe/-/compare/v1.0.0...stable/1.0.x [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-fe/-/compare/v0.9.0...v1.0.0 [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-fe/-/tags/v0.9.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: Just ssh into the server and download a .zip of the latest build:
```sh ```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: Then unpack it into Pleroma's `instance` directory:
```sh ```sh
busybox unzip soapbox-fe.zip -o -d /opt/pleroma/instance busybox unzip soapbox.zip -o -d /opt/pleroma/instance
``` ```
**That's it!** :tada: **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. 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. 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: To get it running, just clone the repo:
```sh ```sh
git clone https://gitlab.com/soapbox-pub/soapbox-fe.git git clone https://gitlab.com/soapbox-pub/soapbox.git
cd soapbox-fe cd soapbox
``` ```
Ensure that Node.js and Yarn are installed, then install dependencies: Ensure that Node.js and Yarn are installed, then install dependencies:
@ -101,7 +101,7 @@ Try again.
### Troubleshooting: it's not working! ### 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/). If they don't match, try installing [asdf](https://asdf-vm.com/).
## Local Dev Configuration ## 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> </ol>
<h1 id="opensource">Open Source Software</h1> <h1 id="opensource">Open Source Software</h1>
<p>Soapbox is free and open source (FOSS) software that runs atop a Pleroma server</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-fe">Soapbox-fe</a></p> <p>The Soapbox repository can be found at <a href="https://gitlab.com/soapbox-pub/soapbox">Soapbox</a></p>
<p>The Pleroma server repository can be found at <a href="https://git.pleroma.social/pleroma/pleroma">Pleroma-be</a></p>

@ -106,10 +106,10 @@ export function importFetchedStatus(status: APIEntity, idempotencyKey?: string)
const isBroken = (status: APIEntity) => { const isBroken = (status: APIEntity) => {
try { try {
// Skip empty accounts // 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; if (!status.account.id) return true;
// Skip broken reposts // 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; if (status.reblog && !status.reblog.account.id) return true;
return false; return false;
} catch (e) { } catch (e) {

@ -101,7 +101,7 @@ const editStatus = (id: string) => (dispatch: AppDispatch, getState: () => RootS
api(getState).get(`/api/v1/statuses/${id}/source`).then(response => { api(getState).get(`/api/v1/statuses/${id}/source`).then(response => {
dispatch({ type: STATUS_FETCH_SOURCE_SUCCESS }); 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')); dispatch(openModal('COMPOSE'));
}).catch(error => { }).catch(error => {
dispatch({ type: STATUS_FETCH_SOURCE_FAIL, 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 { getAcct } from 'soapbox/utils/accounts';
import { displayFqn } from 'soapbox/utils/state'; 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 { Avatar, Emoji, HStack, Icon, IconButton, Stack, Text } from './ui';
import type { Account as AccountEntity } from 'soapbox/types/entities'; import type { Account as AccountEntity } from 'soapbox/types/entities';
@ -54,7 +54,7 @@ interface IAccount {
id?: string, id?: string,
onActionClick?: (account: any) => void, onActionClick?: (account: any) => void,
showProfileHoverCard?: boolean, showProfileHoverCard?: boolean,
timestamp?: string | Date, timestamp?: string,
timestampUrl?: string, timestampUrl?: string,
futureTimestamp?: boolean, futureTimestamp?: boolean,
withAccountNote?: boolean, withAccountNote?: boolean,

@ -6,7 +6,7 @@ import { useSoapboxConfig } from 'soapbox/hooks';
import { getAcct } from '../utils/accounts'; import { getAcct } from '../utils/accounts';
import Icon from './icon'; import Icon from './icon';
import RelativeTimestamp from './relative_timestamp'; import RelativeTimestamp from './relative-timestamp';
import VerificationBadge from './verification_badge'; import VerificationBadge from './verification_badge';
import type { Account } from 'soapbox/types/entities'; 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 { fetchPoll, vote } from 'soapbox/actions/polls';
import { useAppDispatch } from 'soapbox/hooks'; import { useAppDispatch } from 'soapbox/hooks';
import RelativeTimestamp from '../relative_timestamp'; import RelativeTimestamp from '../relative-timestamp';
import { Button, HStack, Stack, Text, Tooltip } from '../ui'; import { Button, HStack, Stack, Text, Tooltip } from '../ui';
import type { Selected } from './poll'; import type { Selected } from './poll';
@ -54,7 +54,7 @@ const PollFooter: React.FC<IPollFooter> = ({ poll, showResults, selected }): JSX
</Button> </Button>
)} )}
<HStack space={1.5} alignItems='center'> <HStack space={1.5} alignItems='center' wrap>
{poll.pleroma.get('non_anonymous') && ( {poll.pleroma.get('non_anonymous') && (
<> <>
<Tooltip text={intl.formatMessage(messages.nonAnonymous)}> <Tooltip text={intl.formatMessage(messages.nonAnonymous)}>

@ -18,7 +18,7 @@ interface IPoll {
} }
const messages = defineMessages({ 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 => { const Poll: React.FC<IPoll> = ({ id, status }): JSX.Element | null => {

@ -1,8 +1,7 @@
import PropTypes from 'prop-types';
import React from 'react'; 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({ const messages = defineMessages({
just_now: { id: 'relative_time.just_now', defaultMessage: 'now' }, 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' }, days_remaining: { id: 'time_remaining.days', defaultMessage: '{number, plural, one {# day} other {# days}} left' },
}); });
const dateFormatOptions = { const dateFormatOptions: FormatDateOptions = {
hour12: false, hour12: false,
year: 'numeric', year: 'numeric',
month: 'short', month: 'short',
@ -26,7 +25,7 @@ const dateFormatOptions = {
minute: '2-digit', minute: '2-digit',
}; };
const shortDateFormatOptions = { const shortDateFormatOptions: FormatDateOptions = {
month: 'short', month: 'short',
day: 'numeric', day: 'numeric',
}; };
@ -38,7 +37,7 @@ const DAY = 1000 * 60 * 60 * 24;
const MAX_DELAY = 2147483647; const MAX_DELAY = 2147483647;
const selectUnits = delta => { const selectUnits = (delta: number) => {
const absDelta = Math.abs(delta); const absDelta = Math.abs(delta);
if (absDelta < MINUTE) { if (absDelta < MINUTE) {
@ -52,7 +51,7 @@ const selectUnits = delta => {
return 'day'; return 'day';
}; };
const getUnitDelay = units => { const getUnitDelay = (units: string) => {
switch (units) { switch (units) {
case 'second': case 'second':
return 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(); const delta = now - date.getTime();
let relativeTime; let relativeTime;
@ -93,7 +92,7 @@ export const timeAgoString = (intl, date, now, year) => {
return relativeTime; return relativeTime;
}; };
const timeRemainingString = (intl, date, now) => { const timeRemainingString = (intl: IntlShape, date: Date, now: number) => {
const delta = date.getTime() - now; const delta = date.getTime() - now;
let relativeTime; let relativeTime;
@ -113,16 +112,21 @@ const timeRemainingString = (intl, date, now) => {
return relativeTime; return relativeTime;
}; };
export default @injectIntl interface RelativeTimestampProps extends IText {
class RelativeTimestamp extends React.Component { intl: IntlShape,
timestamp: string,
year?: number,
futureDate?: boolean,
}
static propTypes = { interface RelativeTimestampState {
intl: PropTypes.object.isRequired, now: number,
timestamp: PropTypes.string.isRequired, }
year: PropTypes.number.isRequired,
theme: PropTypes.string, /** Displays a timestamp compared to the current time, eg "1m" for one minute ago. */
futureDate: PropTypes.bool, class RelativeTimestamp extends React.Component<RelativeTimestampProps, RelativeTimestampState> {
};
_timer: NodeJS.Timeout | undefined;
state = { state = {
now: Date.now(), now: Date.now(),
@ -130,10 +134,10 @@ class RelativeTimestamp extends React.Component {
static defaultProps = { static defaultProps = {
year: (new Date()).getFullYear(), 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, // 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. // but we might as well check in case that ever changes.
return this.props.timestamp !== nextProps.timestamp || return this.props.timestamp !== nextProps.timestamp ||
@ -141,14 +145,14 @@ class RelativeTimestamp extends React.Component {
this.state.now !== nextState.now; this.state.now !== nextState.now;
} }
UNSAFE_componentWillReceiveProps(prevProps) { UNSAFE_componentWillReceiveProps(prevProps: RelativeTimestampProps) {
if (this.props.timestamp !== prevProps.timestamp) { if (this.props.timestamp !== prevProps.timestamp) {
this.setState({ now: Date.now() }); this.setState({ now: Date.now() });
} }
} }
componentDidMount() { componentDidMount() {
this._scheduleNextUpdate(this.props, this.state); this._scheduleNextUpdate();
} }
UNSAFE_componentWillUpdate() { UNSAFE_componentWillUpdate() {
@ -156,11 +160,15 @@ class RelativeTimestamp extends React.Component {
} }
componentWillUnmount() { componentWillUnmount() {
clearTimeout(this._timer); if (this._timer) {
clearTimeout(this._timer);
}
} }
_scheduleNextUpdate() { _scheduleNextUpdate() {
clearTimeout(this._timer); if (this._timer) {
clearTimeout(this._timer);
}
const { timestamp } = this.props; const { timestamp } = this.props;
const delta = (new Date(timestamp)).getTime() - this.state.now; const delta = (new Date(timestamp)).getTime() - this.state.now;
@ -177,8 +185,8 @@ class RelativeTimestamp extends React.Component {
render() { render() {
const { timestamp, intl, year, futureDate, theme, ...textProps } = this.props; const { timestamp, intl, year, futureDate, theme, ...textProps } = this.props;
const date = new Date(timestamp); const date = new Date(timestamp);
const relativeTime = futureDate ? timeRemainingString(intl, date, this.state.now) : timeAgoString(intl, date, this.state.now, year); const relativeTime = futureDate ? timeRemainingString(intl, date, this.state.now) : timeAgoString(intl, date, this.state.now, year!);
return ( return (
<Text {...textProps} theme={theme} tag='time' title={intl.formatDate(date, dateFormatOptions)}> <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 { useSettings } from 'soapbox/hooks';
import LoadMore from './load_more'; import LoadMore from './load_more';
import { Card, Spinner, Text } from './ui'; import { Card, Spinner } from './ui';
/** Custom Viruoso component context. */ /** Custom Viruoso component context. */
type Context = { type Context = {
@ -162,7 +162,7 @@ const ScrollableList = React.forwardRef<VirtuosoHandle, IScrollableList>(({
{isLoading ? ( {isLoading ? (
<Spinner /> <Spinner />
) : ( ) : (
<Text>{emptyMessage}</Text> emptyMessage
)} )}
</Card> </Card>
</div> </div>

@ -301,12 +301,12 @@ const StatusActionBar: React.FC<IStatusActionBar> = ({
}; };
const handleCopy: React.EventHandler<React.MouseEvent> = (e) => { const handleCopy: React.EventHandler<React.MouseEvent> = (e) => {
const { url } = status; const { uri } = status;
const textarea = document.createElement('textarea'); const textarea = document.createElement('textarea');
e.stopPropagation(); e.stopPropagation();
textarea.textContent = url; textarea.textContent = uri;
textarea.style.position = 'fixed'; textarea.style.position = 'fixed';
document.body.appendChild(textarea); document.body.appendChild(textarea);

@ -42,11 +42,13 @@ interface IHStack {
grow?: boolean, grow?: boolean,
/** Extra CSS styles for the <div> */ /** Extra CSS styles for the <div> */
style?: React.CSSProperties style?: React.CSSProperties
/** Whether to let the flexbox wrap onto multiple lines. */
wrap?: boolean,
} }
/** Horizontal row of child elements. */ /** Horizontal row of child elements. */
const HStack = forwardRef<HTMLDivElement, IHStack>((props, ref) => { 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 ( return (
<div <div
@ -60,6 +62,7 @@ const HStack = forwardRef<HTMLDivElement, IHStack>((props, ref) => {
// @ts-ignore // @ts-ignore
[spaces[space]]: typeof space !== 'undefined', [spaces[space]]: typeof space !== 'undefined',
'flex-grow': grow, 'flex-grow': grow,
'flex-wrap': wrap,
}, className)} }, className)}
/> />
); );

@ -39,12 +39,13 @@ interface IStack extends React.HTMLAttributes<HTMLDivElement> {
} }
/** Vertical stack of child elements. */ /** 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; const { space, alignItems, justifyContent, className, grow, ...filteredProps } = props;
return ( return (
<div <div
{...filteredProps} {...filteredProps}
ref={ref}
className={classNames('flex flex-col', { className={classNames('flex flex-col', {
// @ts-ignore // @ts-ignore
[spaces[space]]: typeof space !== 'undefined', [spaces[space]]: typeof space !== 'undefined',
@ -56,6 +57,6 @@ const Stack: React.FC<IStack> = (props) => {
}, className)} }, className)}
/> />
); );
}; });
export default Stack; 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. */ /** Whether to truncate the text if its container is too small. */
truncate?: boolean, truncate?: boolean,
/** Font weight of the text. */ /** Font weight of the text. */
weight?: Weights weight?: Weights,
/** Tooltip title. */
title?: string,
} }
/** UI-friendly text container with dark mode support. */ /** 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 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 ( return (
<div className='w-full'> <div className='w-full'>
<label htmlFor='search' className='sr-only'>{intl.formatMessage(messages.placeholder)}</label> <label htmlFor='search' className='sr-only'>{intl.formatMessage(messages.placeholder)}</label>
<div className='relative'> <div className='relative'>
<Component {autosuggest ? (
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' <AutosuggestAccountInput {...componentProps} />
type='text' ) : (
id='search' <input {...componentProps} />
placeholder={intl.formatMessage(messages.placeholder)} )}
value={value}
onChange={handleChange}
onKeyDown={handleKeyDown}
onFocus={handleFocus}
onSelected={handleSelected}
autoFocus={autoFocus}
autoSelect={false}
menu={makeMenu()}
/>
<div <div
role='button' role='button'

@ -6,7 +6,7 @@ import { getSettings } from 'soapbox/actions/settings';
import Avatar from 'soapbox/components/avatar'; import Avatar from 'soapbox/components/avatar';
import DisplayName from 'soapbox/components/display-name'; import DisplayName from 'soapbox/components/display-name';
import Permalink from 'soapbox/components/permalink'; 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 { Text } from 'soapbox/components/ui';
import ActionButton from 'soapbox/features/ui/components/action-button'; import ActionButton from 'soapbox/features/ui/components/action-button';
import { useAppSelector } from 'soapbox/hooks'; import { useAppSelector } from 'soapbox/hooks';

@ -99,7 +99,7 @@ const DetailedStatus: React.FC<IDetailedStatus> = ({
{quote} {quote}
<HStack justifyContent='between' alignItems='center' className='py-2'> <HStack justifyContent='between' alignItems='center' className='py-2' wrap>
<StatusInteractionBar status={actualStatus} /> <StatusInteractionBar status={actualStatus} />
<HStack space={1} alignItems='center'> <HStack space={1} alignItems='center'>

@ -1,21 +1,29 @@
import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
const emptyComponent = () => null; const emptyComponent = () => null;
const noop = () => { }; const noop = () => { };
class Bundle extends React.PureComponent { interface BundleProps {
fetchComponent: () => Promise<any>,
static propTypes = { loading: React.ComponentType,
fetchComponent: PropTypes.func.isRequired, error: React.ComponentType<{ onRetry: (props: BundleProps) => void }>,
loading: PropTypes.func, children: (mod: any) => React.ReactNode,
error: PropTypes.func, renderDelay?: number,
children: PropTypes.func.isRequired, onFetch: () => void,
renderDelay: PropTypes.number, onFetchSuccess: () => void,
onFetch: PropTypes.func, onFetchFail: (error: any) => void,
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 = { static defaultProps = {
loading: emptyComponent, loading: emptyComponent,
@ -37,7 +45,7 @@ class Bundle extends React.PureComponent {
this.load(this.props); this.load(this.props);
} }
componentWillReceiveProps(nextProps) { componentWillReceiveProps(nextProps: BundleProps) {
if (nextProps.fetchComponent !== this.props.fetchComponent) { if (nextProps.fetchComponent !== this.props.fetchComponent) {
this.load(nextProps); 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 { fetchComponent, onFetch, onFetchSuccess, onFetchFail, renderDelay } = props || this.props;
const cachedMod = Bundle.cache.get(fetchComponent); const cachedMod = Bundle.cache.get(fetchComponent);
@ -88,10 +96,10 @@ class Bundle extends React.PureComponent {
render() { render() {
const { loading: Loading, error: Error, children, renderDelay } = this.props; const { loading: Loading, error: Error, children, renderDelay } = this.props;
const { mod, forceRender } = this.state; 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) { if (mod === undefined) {
return (elapsed >= renderDelay || forceRender) ? <Loading /> : null; return (elapsed >= renderDelay! || forceRender) ? <Loading /> : null;
} }
if (mod === null) { if (mod === null) {

@ -14,7 +14,7 @@ import { isRemote, getDomain } from 'soapbox/utils/accounts';
import type { ReducerAccount } from 'soapbox/reducers/accounts'; import type { ReducerAccount } from 'soapbox/reducers/accounts';
const messages = defineMessages({ 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' }, addMore: { id: 'report.otherActions.addMore', defaultMessage: 'Add more' },
furtherActions: { id: 'report.otherActions.furtherActions', defaultMessage: 'Further actions:' }, furtherActions: { id: 'report.otherActions.furtherActions', defaultMessage: 'Further actions:' },
hideAdditonalStatuses: { id: 'report.otherActions.hideAdditional', defaultMessage: 'Hide additional statuses' }, 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 { fetchBundleRequest, fetchBundleSuccess, fetchBundleFail } from '../../../actions/bundles';
import Bundle from '../components/bundle'; import Bundle from '../components/bundle';
const mapDispatchToProps = dispatch => ({ import type { AppDispatch } from 'soapbox/store';
const mapDispatchToProps = (dispatch: AppDispatch) => ({
onFetch() { onFetch() {
dispatch(fetchBundleRequest()); dispatch(fetchBundleRequest());
}, },
onFetchSuccess() { onFetchSuccess() {
dispatch(fetchBundleSuccess()); dispatch(fetchBundleSuccess());
}, },
onFetchFail(error) { onFetchFail(error: any) {
dispatch(fetchBundleFail(error)); 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 { renderHook, act } from '@testing-library/react-hooks';
import { listener, mockDisconnect } from '../__mocks__/resize-observer';
import { useDimensions } from '../useDimensions'; 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()', () => { describe('useDimensions()', () => {
beforeEach(() => {
mockDisconnect.mockClear();
});
it('defaults to 0', () => { it('defaults to 0', () => {
const { result } = renderHook(() => useDimensions()); const { result } = renderHook(() => useDimensions());
@ -56,16 +48,6 @@ describe('useDimensions()', () => {
}); });
it('disconnects on unmount', () => { it('disconnects on unmount', () => {
const disconnect = jest.fn();
(window as any).ResizeObserver = class ResizeObserver {
observe() {}
disconnect() {
disconnect();
}
};
const { result, unmount } = renderHook(() => useDimensions()); const { result, unmount } = renderHook(() => useDimensions());
act(() => { act(() => {
@ -73,8 +55,8 @@ describe('useDimensions()', () => {
(result.current[1] as any)(div); (result.current[1] as any)(div);
}); });
expect(disconnect).toHaveBeenCalledTimes(0); expect(mockDisconnect).toHaveBeenCalledTimes(0);
unmount(); unmount();
expect(disconnect).toHaveBeenCalledTimes(1); expect(mockDisconnect).toHaveBeenCalledTimes(1);
}); });
}); });

@ -1,4 +1,5 @@
import { useEffect, useMemo, useState } from 'react'; import { useEffect, useMemo, useState } from 'react';
import ResizeObserver from 'resize-observer-polyfill';
type UseDimensionsRect = { width: number, height: number }; type UseDimensionsRect = { width: number, height: number };
type UseDimensionsResult = [Element | null, any, any] type UseDimensionsResult = [Element | null, any, any]
@ -14,7 +15,7 @@ const useDimensions = (): UseDimensionsResult => {
const observer = useMemo( const observer = useMemo(
() => () =>
new (window as any).ResizeObserver((entries: any) => { new ResizeObserver((entries: any) => {
if (entries[0]) { if (entries[0]) {
const { width, height } = entries[0].contentRect; const { width, height } = entries[0].contentRect;
setRect({ width, height }); setRect({ width, height });

@ -73,6 +73,8 @@
"account_note.target": "Notatka o @{target}", "account_note.target": "Notatka o @{target}",
"account_search.placeholder": "Szukaj konta", "account_search.placeholder": "Szukaj konta",
"account_timeline.column_settings.show_pinned": "Show pinned posts", "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.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.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}!", "admin.awaiting_approval.rejected_message": "Odrzucono {acct}!",
@ -134,8 +136,8 @@
"admin_nav.awaiting_approval": "Oczekujące zgłoszenia", "admin_nav.awaiting_approval": "Oczekujące zgłoszenia",
"admin_nav.dashboard": "Panel administracyjny", "admin_nav.dashboard": "Panel administracyjny",
"admin_nav.reports": "Zgłoszenia", "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.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.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.browser": "Przeglądarka",
"alert.unexpected.clear_cookies": "wyczyścić pliki cookies i dane przeglądarki", "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.scopes_placeholder": "np. „read write follow”",
"app_create.submit": "Utwórz aplikację", "app_create.submit": "Utwórz aplikację",
"app_create.website_label": "Strona", "app_create.website_label": "Strona",
"auth_layout.register": "Utwórz konto",
"auth.invalid_credentials": "Nieprawidłowa nazwa użytkownika lub hasło", "auth.invalid_credentials": "Nieprawidłowa nazwa użytkownika lub hasło",
"auth.logged_out": "Wylogowano.", "auth.logged_out": "Wylogowano.",
"auth_layout.register": "Utwórz konto",
"backups.actions.create": "Utwórz kopię zapasową", "backups.actions.create": "Utwórz kopię zapasową",
"backups.empty_message": "Nie znaleziono kopii zapasowych. {action}", "backups.empty_message": "Nie znaleziono kopii zapasowych. {action}",
"backups.empty_message.action": "Chcesz utworzyć?", "backups.empty_message.action": "Chcesz utworzyć?",
"backups.pending": "Oczekująca", "backups.pending": "Oczekująca",
"beta.also_available": "Dostępne w językach:", "beta.also_available": "Dostępne w językach:",
"birthdays_modal.empty": "Żaden z Twoich znajomych nie ma dziś urodzin.",
"birthday_panel.title": "Urodziny", "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", "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.body": "Coś poszło nie tak podczas ładowania tego składnika.",
"bundle_column_error.retry": "Spróbuj ponownie", "bundle_column_error.retry": "Spróbuj ponownie",
@ -283,8 +285,8 @@
"community.column_settings.title": "Ustawienia lokalnej osi czasu", "community.column_settings.title": "Ustawienia lokalnej osi czasu",
"compare_history_modal.header": "Historia edycji", "compare_history_modal.header": "Historia edycji",
"compose.character_counter.title": "Wykorzystano {chars} z {maxChars} znaków", "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.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.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.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.", "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.confirm": "Zablokuj",
"confirmations.block.heading": "Zablokuj @{name}", "confirmations.block.heading": "Zablokuj @{name}",
"confirmations.block.message": "Czy na pewno chcesz zablokować {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.heading": "Anuluj edycję wpisu",
"confirmations.cancel_editing.message": "Czy na pewno chcesz anulować edytowanie wpisu? Niezapisane zmiany zostaną utracone.", "confirmations.cancel_editing.message": "Czy na pewno chcesz anulować edytowanie wpisu? Niezapisane zmiany zostaną utracone.",
"confirmations.delete.confirm": "Usuń", "confirmations.delete.confirm": "Usuń",
@ -432,9 +435,9 @@
"edit_profile.fields.location_label": "Lokalizacja", "edit_profile.fields.location_label": "Lokalizacja",
"edit_profile.fields.location_placeholder": "Lokalizacja", "edit_profile.fields.location_placeholder": "Lokalizacja",
"edit_profile.fields.locked_label": "Zablokuj konto", "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.content_placeholder": "Treść",
"edit_profile.fields.meta_fields.label_placeholder": "Podpis", "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.stranger_notifications_label": "Blokuj powiadomienia od nieznajomych",
"edit_profile.fields.website_label": "Strona internetowa", "edit_profile.fields.website_label": "Strona internetowa",
"edit_profile.fields.website_placeholder": "Wyświetl link", "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.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.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.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.hints.stranger_notifications": "Wyświetlaj tylko powiadomienia od osób, które obserwujesz",
"edit_profile.save": "Zapisz", "edit_profile.save": "Zapisz",
"edit_profile.success": "Zapisano profil!", "edit_profile.success": "Zapisano profil!",
@ -564,10 +567,11 @@
"forms.copy": "Kopiuj", "forms.copy": "Kopiuj",
"forms.hide_password": "Ukryj hasło", "forms.hide_password": "Ukryj hasło",
"forms.show_password": "Pokaż hasło", "forms.show_password": "Pokaż hasło",
"gdpr.accept": "Aceptuj", "gdpr.accept": "Akceptuj",
"gdpr.learn_more": "Dowiedz się więcej", "gdpr.learn_more": "Dowiedz się więcej",
"gdpr.message": "{siteTitle} korzysta z ciasteczek sesji, które są niezbędne dla działania strony.", "gdpr.message": "{siteTitle} korzysta z ciasteczek sesji, które są niezbędne dla działania strony.",
"gdpr.title": "{siteTitle} korzysta z ciasteczek", "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}).", "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.detail.archived_group": "Archived group",
"group.members.empty": "Ta grupa nie ma żadnych członków.", "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.blocks": "Pomyślnie zaimportowano zablokowane konta",
"import_data.success.followers": "Pomyślnie zaimportowano obserwowane konta", "import_data.success.followers": "Pomyślnie zaimportowano obserwowane konta",
"import_data.success.mutes": "Pomyślnie zaimportowano wyciszone konta", "import_data.success.mutes": "Pomyślnie zaimportowano wyciszone konta",
"input.copy": "Kopiuj",
"input.password.hide_password": "Ukryj hasło", "input.password.hide_password": "Ukryj hasło",
"input.password.show_password": "Pokazuj hasło", "input.password.show_password": "Pokazuj hasło",
"intervals.full.days": "{number, plural, one {# dzień} few {# dni} many {# dni} other {# dni}}", "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.filter.statuses": "Nowe wpisy osób, które subskrybujesz",
"notifications.group": "{count, number} {count, plural, one {powiadomienie} few {powiadomienia} many {powiadomień} more {powiadomień}}", "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}}", "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_consumer.tooltip": "Zaloguj się używając {provider}",
"oauth_consumers.title": "Inne opcje logowania",
"onboarding.avatar.subtitle": "Just have fun with it.", "onboarding.avatar.subtitle": "Just have fun with it.",
"onboarding.avatar.title": "Wybierz zdjęcie profilowe", "onboarding.avatar.title": "Wybierz zdjęcie profilowe",
"onboarding.display_name.subtitle": "Możesz ją zawsze zmienić później.", "onboarding.display_name.subtitle": "Możesz ją zawsze zmienić później.",
@ -859,8 +864,10 @@
"patron.title": "Cel wsparcia", "patron.title": "Cel wsparcia",
"pinned_accounts.title": "Polecani przez {name}", "pinned_accounts.title": "Polecani przez {name}",
"pinned_statuses.none": "Brak przypięć do pokazania.", "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.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.refresh": "Odśwież",
"poll.total_people": "{count, plural, one {# osoba} few {# osoby} many {# osób} other {# osób}}", "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}}", "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.reduce_motion_label": "Ogranicz ruch w animacjach",
"preferences.fields.system_font_label": "Używaj domyślnej czcionki systemu", "preferences.fields.system_font_label": "Używaj domyślnej czcionki systemu",
"preferences.fields.theme": "Motyw", "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.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.hints.feed": "Na stronie głównej",
"preferences.notifications.advanced": "Pokazuj wszystkie kategorie powiadomień", "preferences.notifications.advanced": "Pokazuj wszystkie kategorie powiadomień",
"preferences.options.content_type_markdown": "Markdown", "preferences.options.content_type_markdown": "Markdown",
@ -918,12 +926,6 @@
"regeneration_indicator.sublabel": "Twoja oś czasu jest przygotowywana!", "regeneration_indicator.sublabel": "Twoja oś czasu jest przygotowywana!",
"register_invite.lead": "Wypełnij poniższy formularz, aby utworzyć konto.", "register_invite.lead": "Wypełnij poniższy formularz, aby utworzyć konto.",
"register_invite.title": "Otrzymałeś(-aś) zaproszenie na {siteTitle}!", "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.acceptance": "Rejestrując się, wyrażasz zgodę na {terms} i {privacy}.",
"registration.agreement": "Akceptuję {tos}.", "registration.agreement": "Akceptuję {tos}.",
"registration.captcha.hint": "Naciśnij na obrazek, aby uzyskać nową captchę", "registration.captcha.hint": "Naciśnij na obrazek, aby uzyskać nową captchę",
@ -948,6 +950,12 @@
"registration.validation.capital_letter": "1 wielka litera", "registration.validation.capital_letter": "1 wielka litera",
"registration.validation.lowercase_letter": "1 mała litera", "registration.validation.lowercase_letter": "1 mała litera",
"registration.validation.minimum_characters": "8 znaków", "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.days": "{number} dni",
"relative_time.hours": "{number} godz.", "relative_time.hours": "{number} godz.",
"relative_time.just_now": "teraz", "relative_time.just_now": "teraz",
@ -988,7 +996,7 @@
"report.forward": "Przekaż na {target}", "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.forward_hint": "To konto znajduje się na innej instancji. Czy chcesz wysłać anonimową kopię zgłoszenia rnież na nią?",
"report.next": "Dalej", "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.addMore": "Dodaj więcej",
"report.otherActions.furtherActions": "Dodatkowe działania:", "report.otherActions.furtherActions": "Dodatkowe działania:",
"report.otherActions.hideAdditional": "Ukryj dodatkowe wpisy", "report.otherActions.hideAdditional": "Ukryj dodatkowe wpisy",
@ -1116,8 +1124,8 @@
"soapbox_config.single_user_mode_profile_hint": "@nazwa", "soapbox_config.single_user_mode_profile_hint": "@nazwa",
"soapbox_config.single_user_mode_profile_label": "Nazwa głównego użytkownika", "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.", "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.message": "{siteTitle} wyświetla reklamy, aby utrzymać naszą usługę.",
"sponsored.info.title": "Dlaczego widzę tę reklamę?",
"sponsored.subtitle": "Wpis sponsorowany", "sponsored.subtitle": "Wpis sponsorowany",
"status.actions.more": "Więcej", "status.actions.more": "Więcej",
"status.admin_account": "Otwórz interfejs moderacyjny dla @{name}", "status.admin_account": "Otwórz interfejs moderacyjny dla @{name}",
@ -1137,6 +1145,10 @@
"status.embed": "Osadź", "status.embed": "Osadź",
"status.favourite": "Zareaguj", "status.favourite": "Zareaguj",
"status.filtered": "Filtrowany(-a)", "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.load_more": "Załaduj więcej",
"status.media_hidden": "Zawartość multimedialna ukryta", "status.media_hidden": "Zawartość multimedialna ukryta",
"status.mention": "Wspomnij o @{name}", "status.mention": "Wspomnij o @{name}",
@ -1186,7 +1198,7 @@
"streamfield.add": "Dodaj", "streamfield.add": "Dodaj",
"streamfield.remove": "Usuń", "streamfield.remove": "Usuń",
"suggestions.dismiss": "Odrzuć sugestię", "suggestions.dismiss": "Odrzuć sugestię",
"sw.update": "Aktualizacja", "sw.update": "Aktualizacja",
"sw.update_text": "Dostępna jest aktualizacja.", "sw.update_text": "Dostępna jest aktualizacja.",
"tabs_bar.all": "Wszystkie", "tabs_bar.all": "Wszystkie",
"tabs_bar.apps": "Aplikacje", "tabs_bar.apps": "Aplikacje",

@ -21,7 +21,6 @@ describe('normalizePoll()', () => {
expect(ImmutableRecord.isRecord(result)).toBe(true); expect(ImmutableRecord.isRecord(result)).toBe(true);
expect(ImmutableRecord.isRecord(result.options.get(0))).toBe(true); expect(ImmutableRecord.isRecord(result.options.get(0))).toBe(true);
expect(result.toJS()).toMatchObject(expected); expect(result.toJS()).toMatchObject(expected);
expect(result.expires_at instanceof Date).toBe(true);
}); });
it('normalizes a Pleroma logged-out poll', () => { it('normalizes a Pleroma logged-out poll', () => {

@ -164,7 +164,6 @@ describe('normalizeStatus()', () => {
expect(ImmutableRecord.isRecord(poll)).toBe(true); expect(ImmutableRecord.isRecord(poll)).toBe(true);
expect(ImmutableRecord.isRecord(poll.options.get(0))).toBe(true); expect(ImmutableRecord.isRecord(poll.options.get(0))).toBe(true);
expect(poll.toJS()).toMatchObject(expected); expect(poll.toJS()).toMatchObject(expected);
expect(poll.expires_at instanceof Date).toBe(true);
}); });
it('normalizes a Pleroma logged-out poll', () => { it('normalizes a Pleroma logged-out poll', () => {

@ -26,7 +26,7 @@ export const AccountRecord = ImmutableRecord({
avatar_static: '', avatar_static: '',
birthday: '', birthday: '',
bot: false, bot: false,
created_at: new Date(), created_at: '',
discoverable: false, discoverable: false,
display_name: '', display_name: '',
emojis: ImmutableList<Emoji>(), emojis: ImmutableList<Emoji>(),
@ -38,7 +38,7 @@ export const AccountRecord = ImmutableRecord({
header: '', header: '',
header_static: '', header_static: '',
id: '', id: '',
last_status_at: new Date(), last_status_at: '',
location: '', location: '',
locked: false, locked: false,
moved: null as EmbeddedEntity<any>, moved: null as EmbeddedEntity<any>,
@ -78,7 +78,7 @@ export const FieldRecord = ImmutableRecord({
value_plain: '', 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>) => { const normalizePleromaLegacyFields = (account: ImmutableMap<string, any>) => {
return account.update('pleroma', ImmutableMap(), (pleroma: ImmutableMap<string, any>) => { return account.update('pleroma', ImmutableMap(), (pleroma: ImmutableMap<string, any>) => {
return pleroma.withMutations(pleroma => { return pleroma.withMutations(pleroma => {

@ -21,7 +21,7 @@ import type { Emoji, PollOption } from 'soapbox/types/entities';
export const PollRecord = ImmutableRecord({ export const PollRecord = ImmutableRecord({
emojis: ImmutableList<Emoji>(), emojis: ImmutableList<Emoji>(),
expired: false, expired: false,
expires_at: new Date(), expires_at: '',
id: '', id: '',
multiple: false, multiple: false,
options: ImmutableList<PollOption>(), options: ImmutableList<PollOption>(),

@ -28,8 +28,8 @@ export const StatusRecord = ImmutableRecord({
bookmarked: false, bookmarked: false,
card: null as Card | null, card: null as Card | null,
content: '', content: '',
created_at: new Date(), created_at: '',
edited_at: null as Date | null, edited_at: null as string | null,
emojis: ImmutableList<Emoji>(), emojis: ImmutableList<Emoji>(),
favourited: false, favourited: false,
favourites_count: 0, favourites_count: 0,

@ -105,7 +105,7 @@ const ProfilePage: React.FC<IProfilePage> = ({ params, children }) => {
</BundleContainer> </BundleContainer>
{account && showTabs && ( {account && showTabs && (
<Tabs items={tabItems} activeItem={activeItem} /> <Tabs key={`profile-tabs-${account.id}`} items={tabItems} activeItem={activeItem} />
)} )}
{children} {children}

@ -20,7 +20,7 @@ const importEmojis = (customEmojis: APIEntity[]) => {
const emojis = (fromJS(customEmojis) as ImmutableList<ImmutableMap<string, string>>).filter((emoji) => { const emojis = (fromJS(customEmojis) as ImmutableList<ImmutableMap<string, string>>).filter((emoji) => {
// If a custom emoji has the shortcode of a Unicode emoji, skip it. // If a custom emoji has the shortcode of a Unicode emoji, skip it.
// Otherwise it breaks EmojiMart. // 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(); const shortcode = emoji.get('shortcode', '').toLowerCase();
return !emojiData[shortcode]; return !emojiData[shortcode];
}); });

@ -67,7 +67,7 @@ const fixNotification = notification => {
const isValid = notification => { const isValid = notification => {
try { try {
// https://gitlab.com/soapbox-pub/soapbox-fe/-/issues/424 // https://gitlab.com/soapbox-pub/soapbox/-/issues/424
if (!notification.account.id) { if (!notification.account.id) {
return false; return false;
} }

@ -242,7 +242,7 @@ const timelineDisconnect = (state: State, timelineId: string) => {
if (items.isEmpty()) return; if (items.isEmpty()) return;
// This is causing problems. Disable for now. // 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)); // timeline.set('items', addStatusId(items, null));
})); }));
}; };

@ -59,7 +59,7 @@ const findAccountsByUsername = (state: RootState, username: string) => {
const accounts = state.accounts; const accounts = state.accounts;
return accounts.filter(account => { 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'; export const TRUTHSOCIAL = 'TruthSocial';
/** /**
* Soapbox BE, the recommended Pleroma fork for Soapbox. * Rebased, the recommended backend for Soapbox.
* @see {@link https://gitlab.com/soapbox-pub/soapbox-be} * @see {@link https://gitlab.com/soapbox-pub/rebased}
*/ */
export const SOAPBOX = 'soapbox'; 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). */ /** 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 => { export const hasIntegerMediaIds = (status: StatusEntity): boolean => {
return status.media_attachments.some(({ id }) => isIntegerId(id)); 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: 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`.) (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. It is fine-tuned, includes support for federation, and should work with any backend.
## The ServiceWorker ## The ServiceWorker

@ -13,7 +13,7 @@ mkdir -p /opt/soapbox
Fetch the build. Fetch the build.
```sh ```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. 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. First, download the latest build of Soapbox from GitLab.
```sh ```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 ## 2. Unzip the build

@ -8,7 +8,7 @@ To do so, shell into your server and unpack Soapbox:
```sh ```sh
mkdir -p /opt/soapbox 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 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: If you already have one, replace it:
```sh ```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. Edit this file and replace all occurrences of `example.com` with your domain name.

@ -1,6 +1,6 @@
# Updating Soapbox # 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. 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. # 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. # Remove all the current Soapbox build in Pleroma's instance directory.
rm -R /opt/pleroma/instance/static/packs rm -R /opt/pleroma/instance/static/packs

@ -2,20 +2,20 @@
Thank you for your interest in Soapbox! 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) ## Opening an MR (merge request)
1. Smash that "fork" button on GitLab to make a copy of the repo. 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`). 2. Clone the repo locally, then begin work on a new branch (eg not `develop`).
3. Push your branch to your fork. 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 ### Ensuring the CI pipeline succeeds
When you push to a branch, the CI pipeline will run. 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. It's important this pipeline passes, otherwise we cannot merge the change.
New users of gitlab.com may see a "detatched pipeline" error. 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. 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. 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 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. 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 ## 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 ## 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. 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. 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: 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 cd soapbox-fe
``` ```
@ -40,5 +40,5 @@ Try again.
## Troubleshooting: it's not working! ## 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/). 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. 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`` 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", "displayName": "Soapbox",
"version": "3.0.0", "version": "3.0.0",
"description": "Soapbox frontend for the Fediverse.", "description": "Soapbox frontend for the Fediverse.",
"homepage": "https://soapbox.pub/", "homepage": "https://soapbox.pub/",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://gitlab.com/soapbox-pub/soapbox-fe" "url": "https://gitlab.com/soapbox-pub/soapbox"
}, },
"keywords": [ "keywords": [
"fediverse", "fediverse",
"pleroma" "pleroma"
], ],
"bugs": { "bugs": {
"url": "https://gitlab.com/soapbox-pub/soapbox-fe/-/issues" "url": "https://gitlab.com/soapbox-pub/soapbox/-/issues"
}, },
"scripts": { "scripts": {
"start": "npx webpack-dev-server", "start": "npx webpack-dev-server",
@ -181,6 +181,7 @@
"redux-thunk": "^2.2.0", "redux-thunk": "^2.2.0",
"requestidlecallback": "^0.3.0", "requestidlecallback": "^0.3.0",
"reselect": "^4.0.0", "reselect": "^4.0.0",
"resize-observer-polyfill": "^1.5.1",
"sass": "^1.20.3", "sass": "^1.20.3",
"sass-loader": "^13.0.0", "sass-loader": "^13.0.0",
"semver": "^7.3.2", "semver": "^7.3.2",

@ -113,6 +113,7 @@ module.exports = merge(sharedConfig, {
'/objects', '/objects',
'/ostatus_subscribe', '/ostatus_subscribe',
'/pghero', '/pghero',
'/phoenix',
'/pleroma', '/pleroma',
'/proxy', '/proxy',
'/relay', '/relay',

Loading…
Cancel
Save