diff --git a/.dockerignore b/.dockerignore
index 41b87855a..2d53bc7e4 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -12,10 +12,11 @@
yarn-error.log*
/junit.xml
+/dist/
/static/
-/static-test/
/public/
/dist/
+/soapbox.zip
.idea
.DS_Store
diff --git a/.eslintignore b/.eslintignore
index 1ab6f8d8c..dc7fe3106 100644
--- a/.eslintignore
+++ b/.eslintignore
@@ -1,6 +1,6 @@
/node_modules/**
+/dist/**
/static/**
-/static-test/**
/public/**
/tmp/**
/coverage/**
diff --git a/.eslintrc.cjs b/.eslintrc.cjs
index 786d52d13..0105144fb 100644
--- a/.eslintrc.cjs
+++ b/.eslintrc.cjs
@@ -19,8 +19,6 @@ module.exports = {
ATTACHMENT_HOST: false,
},
- parser: '@babel/eslint-parser',
-
plugins: [
'react',
'jsdoc',
@@ -50,9 +48,8 @@ module.exports = {
'\\.(css|scss|json)$',
],
'import/resolver': {
- node: {
- paths: ['app'],
- },
+ typescript: true,
+ node: true,
},
polyfills: [
'es:all', // core-js
@@ -79,6 +76,7 @@ module.exports = {
},
],
'comma-style': ['warn', 'last'],
+ 'import/no-duplicates': 'error',
'space-before-function-paren': ['error', 'never'],
'space-infix-ops': 'error',
'space-in-parens': ['error', 'never'],
@@ -260,7 +258,6 @@ module.exports = {
alphabetize: { order: 'asc' },
},
],
- '@typescript-eslint/no-duplicate-imports': 'error',
'@typescript-eslint/member-delimiter-style': [
'error',
{
@@ -298,7 +295,7 @@ module.exports = {
{
// Only enforce JSDoc comments on UI components for now.
// https://www.npmjs.com/package/eslint-plugin-jsdoc
- files: ['app/soapbox/components/ui/**/*'],
+ files: ['src/components/ui/**/*'],
rules: {
'jsdoc/require-jsdoc': ['error', {
publicOnly: true,
diff --git a/.gitignore b/.gitignore
index 92e9362d8..176314352 100644
--- a/.gitignore
+++ b/.gitignore
@@ -9,11 +9,14 @@
/.vs/
yarn-error.log*
/junit.xml
+*.timestamp-*
+*.bundled_*
+/dist/
/static/
-/static-test/
/public/
/dist/
+/soapbox.zip
.idea
.DS_Store
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 97892e166..defe89e3b 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -2,6 +2,7 @@ image: node:20
variables:
NODE_ENV: test
+ DS_EXCLUDED_ANALYZERS: gemnasium-python
default:
interruptible: true
@@ -30,20 +31,9 @@ deps:
<<: *cache
policy: push
-danger:
+lint:
stage: test
- script:
- # https://github.com/danger/danger-js/issues/1029#issuecomment-998915436
- - export CI_MERGE_REQUEST_IID=${CI_OPEN_MERGE_REQUESTS#*!}
- - npx danger ci
- except:
- variables:
- - $CI_DEFAULT_BRANCH == $CI_COMMIT_REF_NAME
- allow_failure: true
-
-lint-js:
- stage: test
- script: yarn lint:js
+ script: yarn lint
only:
changes:
- "**/*.js"
@@ -52,63 +42,25 @@ lint-js:
- "**/*.mjs"
- "**/*.ts"
- "**/*.tsx"
- - ".eslintignore"
- - ".eslintrc.cjs"
-
-lint-sass:
- stage: test
- script: yarn lint:sass
- only:
- changes:
- "**/*.scss"
- "**/*.css"
+ - ".eslintignore"
+ - ".eslintrc.cjs"
- ".stylelintrc.json"
-jest:
+build:
stage: test
- script: yarn test:coverage --runInBand
- only:
- changes:
- - "**/*.js"
- - "**/*.json"
- - "app/soapbox/**/*"
- - "webpack/**/*"
- - "custom/**/*"
- - "jest.config.cjs"
- - "package.json"
- - "yarn.lock"
- - ".gitlab-ci.yml"
- coverage: /All files[^|]*\|[^|]*\s+([\d\.]+)/
- artifacts:
- reports:
- junit: junit.xml
- coverage_report:
- coverage_format: cobertura
- path: .coverage/cobertura-coverage.xml
-
-nginx-test:
- stage: test
- image: nginx:latest
before_script:
- - cp installation/mastodon.conf /etc/nginx/conf.d/default.conf
- script: nginx -t
- only:
- changes:
- - "installation/mastodon.conf"
-
-build-production:
- stage: test
+ - apt-get update -y && apt-get install -y zip
script:
- yarn build
- - yarn manage:translations en
- # Fail if files got changed.
- # https://stackoverflow.com/a/9066385
- - git diff --quiet
+ - cp dist/index.html dist/404.html
+ - cd dist && zip -r ../soapbox.zip . && cd ..
variables:
NODE_ENV: production
artifacts:
paths:
- - static
+ - soapbox.zip
docs-deploy:
stage: deploy
@@ -128,16 +80,20 @@ review:
environment:
name: review/$CI_COMMIT_REF_NAME
url: https://$CI_COMMIT_REF_SLUG.git.soapbox.pub
+ before_script:
+ - apt-get update -y && apt-get install -y unzip
script:
- - npx -y surge static $CI_COMMIT_REF_SLUG.git.soapbox.pub
+ - unzip soapbox.zip -d dist
+ - npx -y surge dist $CI_COMMIT_REF_SLUG.git.soapbox.pub
allow_failure: true
pages:
stage: deploy
- before_script: []
+ before_script:
+ - apt-get update -y && apt-get install -y unzip
script:
# artifacts are kept between jobs
- - mv static public
+ - unzip soapbox.zip -d public
variables:
NODE_ENV: production
artifacts:
@@ -149,9 +105,9 @@ pages:
docker:
stage: deploy
- image: docker:23.0.0
+ image: docker:24.0.6
services:
- - docker:23.0.0-dind
+ - docker:24.0.6-dind
tags:
- 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
@@ -173,4 +129,3 @@ release:
include:
- template: Jobs/Dependency-Scanning.gitlab-ci.yml
- - template: Security/License-Scanning.gitlab-ci.yml
\ No newline at end of file
diff --git a/.lintstagedrc.json b/.lintstagedrc.json
index 97bad7f28..fc508d7e3 100644
--- a/.lintstagedrc.json
+++ b/.lintstagedrc.json
@@ -4,5 +4,5 @@
"*.mjs": "eslint --cache",
"*.ts": "eslint --cache",
"*.tsx": "eslint --cache",
- "app/styles/**/*.scss": "stylelint"
+ "src/styles/**/*.scss": "stylelint"
}
diff --git a/.storybook/main.ts b/.storybook/main.ts
deleted file mode 100644
index bb4c1d232..000000000
--- a/.storybook/main.ts
+++ /dev/null
@@ -1,43 +0,0 @@
-import sharedConfig from '../webpack/shared';
-
-import type { StorybookConfig } from '@storybook/core-common';
-
-const config: StorybookConfig = {
- stories: [
- '../stories/**/*.stories.mdx',
- '../stories/**/*.stories.@(js|jsx|ts|tsx)'
- ],
- addons: [
- '@storybook/addon-links',
- '@storybook/addon-essentials',
- '@storybook/addon-interactions',
- 'storybook-react-intl',
- {
- name: '@storybook/addon-postcss',
- options: {
- postcssLoaderOptions: {
- implementation: require('postcss'),
- },
- },
- },
- ],
- framework: '@storybook/react',
- core: {
- builder: '@storybook/builder-webpack5',
- },
- webpackFinal: async (config) => {
- config.resolve!.alias = {
- ...sharedConfig.resolve!.alias,
- ...config.resolve!.alias,
- };
-
- config.resolve!.modules = [
- ...sharedConfig.resolve!.modules!,
- ...config.resolve!.modules!,
- ];
-
- return config;
- },
-};
-
-export default config;
\ No newline at end of file
diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx
deleted file mode 100644
index df2195f0c..000000000
--- a/.storybook/preview.tsx
+++ /dev/null
@@ -1,22 +0,0 @@
-import '../app/styles/tailwind.css';
-import '../stories/theme.css';
-
-import { addDecorator, Story } from '@storybook/react';
-import { IntlProvider } from 'react-intl';
-import React from 'react';
-
-const withProvider = (Story: Story) => (
-
-);
-
-addDecorator(withProvider);
-
-export const parameters = {
- actions: { argTypesRegex: '^on[A-Z].*' },
- controls: {
- matchers: {
- color: /(background|color)$/i,
- date: /Date$/,
- },
- },
-};
diff --git a/.stylelintrc.json b/.stylelintrc.json
index 1a610d5eb..d01dd67b6 100644
--- a/.stylelintrc.json
+++ b/.stylelintrc.json
@@ -16,7 +16,6 @@
"no-invalid-position-at-import-rule": null,
"scss/at-rule-no-unknown": [true, { "ignoreAtRules": ["tailwind", "apply", "layer", "config"]}],
"scss/operator-no-unspaced": null,
- "selector-class-pattern": null,
- "string-quotes": "single"
+ "selector-class-pattern": null
}
}
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 43ca09934..baaf46aa9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12,9 +12,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Reactions: Support custom emoji reactions
- Compatbility: Support Mastodon v2 timeline filters.
- Compatbility: Preliminary support for Ditto backend.
+- Compatibility: Support Firefish.
- Posts: Support dislikes on Friendica.
- UI: added a character counter to some textareas.
- UI: added new experience for viewing Media
+- Hotkeys: Added `/` as a hotkey for search field.
### Changed
- Posts: truncate Nostr pubkeys in reply mentions.
@@ -23,10 +25,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- UI: unified design of "approve" and "reject" buttons in follow requests and waitlist.
- UI: added sticky column header.
- UI: add specific zones the user can drag-and-drop files.
+- UI: disable toast notifications for API errors.
+- Chats: Display year for older messages creation date.
### Fixed
- Posts: fixed emojis being cut off in reactions modal.
- Posts: fix audio player progress bar visibility.
+- Posts: fix audio player avatar aspect ratio for non-square avatars.
- Posts: added missing gap in pending status.
- Compatibility: fixed quote posting compatibility with custom Pleroma forks.
- Profile: fix "load more" button height on account gallery page.
@@ -36,6 +41,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- UI: fixed various overflow issues related to long usernames.
- UI: fixed display of Markdown code blocks in the reply indicator.
- Auth: fixed too many API requests when the server has an error.
+- Auth: Don't display "username or e-mail" if username is not allowed.
## [3.2.0] - 2023-02-15
diff --git a/Dockerfile b/Dockerfile
index b02bf86e1..2765f2053 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -14,4 +14,4 @@ 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
+COPY --from=build /app/dist /usr/share/nginx/html
diff --git a/app/soapbox/actions/__tests__/account-notes.test.ts b/app/soapbox/actions/__tests__/account-notes.test.ts
deleted file mode 100644
index fdae30838..000000000
--- a/app/soapbox/actions/__tests__/account-notes.test.ts
+++ /dev/null
@@ -1,109 +0,0 @@
-import { Map as ImmutableMap } from 'immutable';
-
-import { __stub } from 'soapbox/api';
-import { buildAccount, buildRelationship } from 'soapbox/jest/factory';
-import { mockStore, rootState } from 'soapbox/jest/test-helpers';
-import { ReducerRecord, EditRecord } from 'soapbox/reducers/account-notes';
-
-import { changeAccountNoteComment, initAccountNoteModal, submitAccountNote } from '../account-notes';
-
-describe('submitAccountNote()', () => {
- let store: ReturnType;
-
- beforeEach(() => {
- const state = rootState
- .set('account_notes', ReducerRecord({ edit: EditRecord({ account: '1', comment: 'hello' }) }));
- store = mockStore(state);
- });
-
- describe('with a successful API request', () => {
- beforeEach(() => {
- __stub((mock) => {
- mock.onPost('/api/v1/accounts/1/note').reply(200, {});
- });
- });
-
- it('post the note to the API', async() => {
- const expectedActions = [
- { type: 'ACCOUNT_NOTE_SUBMIT_REQUEST' },
- { type: 'MODAL_CLOSE', modalType: undefined },
- { type: 'ACCOUNT_NOTE_SUBMIT_SUCCESS', relationship: {} },
- ];
- await store.dispatch(submitAccountNote());
- const actions = store.getActions();
-
- expect(actions).toEqual(expectedActions);
- });
- });
-
- describe('with an unsuccessful API request', () => {
- beforeEach(() => {
- __stub((mock) => {
- mock.onPost('/api/v1/accounts/1/note').networkError();
- });
- });
-
- it('should dispatch failed action', async() => {
- const expectedActions = [
- { type: 'ACCOUNT_NOTE_SUBMIT_REQUEST' },
- {
- type: 'ACCOUNT_NOTE_SUBMIT_FAIL',
- error: new Error('Network Error'),
- },
- ];
- await store.dispatch(submitAccountNote());
- const actions = store.getActions();
-
- expect(actions).toEqual(expectedActions);
- });
- });
-});
-
-describe('initAccountNoteModal()', () => {
- let store: ReturnType;
-
- beforeEach(() => {
- const state = rootState
- .set('relationships', ImmutableMap({ '1': buildRelationship({ note: 'hello' }) }));
- store = mockStore(state);
- });
-
- it('dispatches the proper actions', async() => {
- const account = buildAccount({
- id: '1',
- acct: 'justin-username',
- display_name: 'Justin L',
- avatar: 'test.jpg',
- verified: true,
- });
- const expectedActions = [
- { type: 'ACCOUNT_NOTE_INIT_MODAL', account, comment: 'hello' },
- { type: 'MODAL_CLOSE', modalType: 'ACCOUNT_NOTE' },
- { type: 'MODAL_OPEN', modalType: 'ACCOUNT_NOTE' },
- ];
- await store.dispatch(initAccountNoteModal(account));
- const actions = store.getActions();
-
- expect(actions).toEqual(expectedActions);
- });
-});
-
-describe('changeAccountNoteComment()', () => {
- let store: ReturnType;
-
- beforeEach(() => {
- const state = rootState;
- store = mockStore(state);
- });
-
- it('dispatches the proper actions', async() => {
- const comment = 'hello world';
- const expectedActions = [
- { type: 'ACCOUNT_NOTE_CHANGE_COMMENT', comment },
- ];
- await store.dispatch(changeAccountNoteComment(comment));
- const actions = store.getActions();
-
- expect(actions).toEqual(expectedActions);
- });
-});
diff --git a/app/soapbox/actions/account-notes.ts b/app/soapbox/actions/account-notes.ts
deleted file mode 100644
index 691f63fc3..000000000
--- a/app/soapbox/actions/account-notes.ts
+++ /dev/null
@@ -1,82 +0,0 @@
-import api from '../api';
-
-import { openModal, closeModal } from './modals';
-
-import type { AxiosError } from 'axios';
-import type { AnyAction } from 'redux';
-import type { Account } from 'soapbox/schemas';
-import type { AppDispatch, RootState } from 'soapbox/store';
-
-const ACCOUNT_NOTE_SUBMIT_REQUEST = 'ACCOUNT_NOTE_SUBMIT_REQUEST';
-const ACCOUNT_NOTE_SUBMIT_SUCCESS = 'ACCOUNT_NOTE_SUBMIT_SUCCESS';
-const ACCOUNT_NOTE_SUBMIT_FAIL = 'ACCOUNT_NOTE_SUBMIT_FAIL';
-
-const ACCOUNT_NOTE_INIT_MODAL = 'ACCOUNT_NOTE_INIT_MODAL';
-
-const ACCOUNT_NOTE_CHANGE_COMMENT = 'ACCOUNT_NOTE_CHANGE_COMMENT';
-
-const submitAccountNote = () => (dispatch: React.Dispatch, getState: () => RootState) => {
- dispatch(submitAccountNoteRequest());
-
- const id = getState().account_notes.edit.account;
-
- return api(getState)
- .post(`/api/v1/accounts/${id}/note`, {
- comment: getState().account_notes.edit.comment,
- })
- .then(response => {
- dispatch(closeModal());
- dispatch(submitAccountNoteSuccess(response.data));
- })
- .catch(error => dispatch(submitAccountNoteFail(error)));
-};
-
-function submitAccountNoteRequest() {
- return {
- type: ACCOUNT_NOTE_SUBMIT_REQUEST,
- };
-}
-
-function submitAccountNoteSuccess(relationship: any) {
- return {
- type: ACCOUNT_NOTE_SUBMIT_SUCCESS,
- relationship,
- };
-}
-
-function submitAccountNoteFail(error: AxiosError) {
- return {
- type: ACCOUNT_NOTE_SUBMIT_FAIL,
- error,
- };
-}
-
-const initAccountNoteModal = (account: Account) => (dispatch: AppDispatch, getState: () => RootState) => {
- const comment = getState().relationships.get(account.id)!.note;
-
- dispatch({
- type: ACCOUNT_NOTE_INIT_MODAL,
- account,
- comment,
- });
-
- dispatch(openModal('ACCOUNT_NOTE'));
-};
-
-function changeAccountNoteComment(comment: string) {
- return {
- type: ACCOUNT_NOTE_CHANGE_COMMENT,
- comment,
- };
-}
-
-export {
- submitAccountNote,
- initAccountNoteModal,
- changeAccountNoteComment,
- ACCOUNT_NOTE_SUBMIT_REQUEST,
- ACCOUNT_NOTE_SUBMIT_SUCCESS,
- ACCOUNT_NOTE_SUBMIT_FAIL,
- ACCOUNT_NOTE_INIT_MODAL,
- ACCOUNT_NOTE_CHANGE_COMMENT,
-};
diff --git a/app/soapbox/build-config.js b/app/soapbox/build-config.js
deleted file mode 100644
index a11faa0e8..000000000
--- a/app/soapbox/build-config.js
+++ /dev/null
@@ -1,46 +0,0 @@
-// @preval
-/**
- * Build config: configuration set at build time.
- * @module soapbox/build-config
- */
-
-const trim = require('lodash/trim');
-const trimEnd = require('lodash/trimEnd');
-
-const {
- NODE_ENV,
- BACKEND_URL,
- FE_SUBDIRECTORY,
- FE_BUILD_DIR,
- FE_INSTANCE_SOURCE_DIR,
- SENTRY_DSN,
-} = process.env;
-
-const sanitizeURL = url => {
- try {
- return trimEnd(new URL(url).toString(), '/');
- } catch {
- return '';
- }
-};
-
-const sanitizeBasename = path => {
- return `/${trim(path, '/')}`;
-};
-
-const sanitizePath = path => {
- return trim(path, '/');
-};
-
-// JSON.parse/stringify is to emulate what @preval is doing and avoid any
-// inconsistent behavior in dev mode
-const sanitize = obj => JSON.parse(JSON.stringify(obj));
-
-module.exports = sanitize({
- NODE_ENV: NODE_ENV || 'development',
- BACKEND_URL: sanitizeURL(BACKEND_URL),
- FE_SUBDIRECTORY: sanitizeBasename(FE_SUBDIRECTORY),
- FE_BUILD_DIR: sanitizePath(FE_BUILD_DIR) || 'static',
- FE_INSTANCE_SOURCE_DIR: FE_INSTANCE_SOURCE_DIR || 'instance',
- SENTRY_DSN,
-});
diff --git a/app/soapbox/features/compose/containers/warning-container.tsx b/app/soapbox/features/compose/containers/warning-container.tsx
deleted file mode 100644
index fd073cda8..000000000
--- a/app/soapbox/features/compose/containers/warning-container.tsx
+++ /dev/null
@@ -1,46 +0,0 @@
-import React from 'react';
-import { FormattedMessage } from 'react-intl';
-import { Link } from 'react-router-dom';
-
-import { useAppSelector, useCompose } from 'soapbox/hooks';
-
-import Warning from '../components/warning';
-
-const APPROX_HASHTAG_RE = /(?:^|[^\/\)\w])#(\w*[a-zA-Z·]\w*)/i;
-
-interface IWarningWrapper {
- composeId: string
-}
-
-const WarningWrapper: React.FC = ({ composeId }) => {
- const compose = useCompose(composeId);
-
- const me = useAppSelector((state) => state.me);
-
- const needsLockWarning = useAppSelector((state) => compose.privacy === 'private' && !state.accounts.get(me)!.locked);
- const hashtagWarning = (compose.privacy !== 'public' && compose.privacy !== 'group') && APPROX_HASHTAG_RE.test(compose.text);
- const directMessageWarning = compose.privacy === 'direct';
-
- if (needsLockWarning) {
- return }} />} />;
- }
-
- if (hashtagWarning) {
- return } />;
- }
-
- if (directMessageWarning) {
- const message = (
-
-
- {/* */}
-
- );
-
- return ;
- }
-
- return null;
-};
-
-export default WarningWrapper;
diff --git a/app/soapbox/features/hashtag-timeline/index.tsx b/app/soapbox/features/hashtag-timeline/index.tsx
deleted file mode 100644
index bf906ce01..000000000
--- a/app/soapbox/features/hashtag-timeline/index.tsx
+++ /dev/null
@@ -1,146 +0,0 @@
-import React, { useEffect, useRef } from 'react';
-import { useIntl, defineMessages, FormattedMessage } from 'react-intl';
-
-import { connectHashtagStream } from 'soapbox/actions/streaming';
-import { fetchHashtag, followHashtag, unfollowHashtag } from 'soapbox/actions/tags';
-import { expandHashtagTimeline, clearTimeline } from 'soapbox/actions/timelines';
-import List, { ListItem } from 'soapbox/components/list';
-import { Column, Toggle } from 'soapbox/components/ui';
-import Timeline from 'soapbox/features/ui/components/timeline';
-import { useAppDispatch, useAppSelector, useFeatures } from 'soapbox/hooks';
-
-import type { Tag as TagEntity } from 'soapbox/types/entities';
-
-type Mode = 'any' | 'all' | 'none';
-
-type Tag = { value: string };
-type Tags = { [k in Mode]: Tag[] };
-
-const messages = defineMessages({
- any: { id: 'hashtag.column_header.tag_mode.any', defaultMessage: 'or {additional}' },
- all: { id: 'hashtag.column_header.tag_mode.all', defaultMessage: 'and {additional}' },
- none: { id: 'hashtag.column_header.tag_mode.none', defaultMessage: 'without {additional}' },
- empty: { id: 'empty_column.hashtag', defaultMessage: 'There is nothing in this hashtag yet.' },
-});
-
-interface IHashtagTimeline {
- params?: {
- id?: string
- tags?: Tags
- }
-}
-
-export const HashtagTimeline: React.FC = ({ params }) => {
- const intl = useIntl();
- const id = params?.id || '';
- const tags = params?.tags || { any: [], all: [], none: [] };
-
- const features = useFeatures();
- const dispatch = useAppDispatch();
- const disconnects = useRef<(() => void)[]>([]);
- const tag = useAppSelector((state) => state.tags.get(id));
- const next = useAppSelector(state => state.timelines.get(`hashtag:${id}`)?.next);
-
- // Mastodon supports displaying results from multiple hashtags.
- // https://github.com/mastodon/mastodon/issues/6359
- const title = (): string => {
- const title: string[] = [`#${id}`];
-
- if (additionalFor('any')) {
- title.push(' ', intl.formatMessage(messages.any, { additional: additionalFor('any') }));
- }
-
- if (additionalFor('all')) {
- title.push(' ', intl.formatMessage(messages.any, { additional: additionalFor('all') }));
- }
-
- if (additionalFor('none')) {
- title.push(' ', intl.formatMessage(messages.any, { additional: additionalFor('none') }));
- }
-
- return title.join('');
- };
-
- const additionalFor = (mode: Mode) => {
- if (tags && (tags[mode] || []).length > 0) {
- return tags[mode].map(tag => tag.value).join('/');
- } else {
- return '';
- }
- };
-
- const subscribe = () => {
- const any = tags.any.map(tag => tag.value);
- const all = tags.all.map(tag => tag.value);
- const none = tags.none.map(tag => tag.value);
-
- [id, ...any].map(tag => {
- disconnects.current.push(dispatch(connectHashtagStream(id, tag, status => {
- const tags = status.tags.map((tag: TagEntity) => tag.name);
-
- return all.filter(tag => tags.includes(tag)).length === all.length &&
- none.filter(tag => tags.includes(tag)).length === 0;
- })));
- });
- };
-
- const unsubscribe = () => {
- disconnects.current.map(disconnect => disconnect());
- disconnects.current = [];
- };
-
- const handleLoadMore = (maxId: string) => {
- dispatch(expandHashtagTimeline(id, { url: next, maxId, tags }));
- };
-
- const handleFollow = () => {
- if (tag?.following) {
- dispatch(unfollowHashtag(id));
- } else {
- dispatch(followHashtag(id));
- }
- };
-
- useEffect(() => {
- subscribe();
- dispatch(expandHashtagTimeline(id, { tags }));
- dispatch(fetchHashtag(id));
-
- return () => {
- unsubscribe();
- };
- }, []);
-
- useEffect(() => {
- unsubscribe();
- subscribe();
- dispatch(clearTimeline(`hashtag:${id}`));
- dispatch(expandHashtagTimeline(id, { tags }));
- }, [id]);
-
- return (
-
- {features.followHashtags && (
-
- }
- >
-
-
-
- )}
-
-
- );
-};
-
-export default HashtagTimeline;
diff --git a/app/soapbox/features/ui/__tests__/index.test.tsx b/app/soapbox/features/ui/__tests__/index.test.tsx
deleted file mode 100644
index 6fb4b1e83..000000000
--- a/app/soapbox/features/ui/__tests__/index.test.tsx
+++ /dev/null
@@ -1,89 +0,0 @@
-import React from 'react';
-import { Route, Switch } from 'react-router-dom';
-
-import { buildAccount } from 'soapbox/jest/factory';
-
-import { render, screen, waitFor } from '../../../jest/test-helpers';
-import { normalizeInstance } from '../../../normalizers';
-import UI from '../index';
-import { WrappedRoute } from '../util/react-router-helpers';
-
-const TestableComponent = () => (
-
-
-
-
- Sign in
-
- {/* WrappedRount will redirect to /login for logged out users... which will resolve to the route above! */}
- null} />
-
-);
-
-describe('', () => {
- let store: any;
-
- beforeEach(() => {
- store = {
- me: false,
- accounts: {
- '1': buildAccount({
- id: '1',
- acct: 'username',
- display_name: 'My name',
- avatar: 'test.jpg',
- }),
- },
- instance: normalizeInstance({ registrations: true }),
- };
- });
-
- describe('when logged out', () => {
- describe('when viewing a Profile Page', () => {
- it('should render the Profile page', async() => {
- render(
- ,
- {},
- store,
- { initialEntries: ['/@username'] },
- );
-
- await waitFor(() => {
- expect(screen.getByTestId('cta-banner')).toHaveTextContent('Sign up now to discuss');
- }, {
- timeout: 5000,
- });
- });
- });
-
- describe('when viewing a Status Page', () => {
- it('should render the Status page', async() => {
- render(
- ,
- {},
- store,
- { initialEntries: ['/@username/posts/12'] },
- );
-
- await waitFor(() => {
- expect(screen.getByTestId('cta-banner')).toHaveTextContent('Sign up now to discuss');
- });
- });
- });
-
- describe('when viewing Notifications', () => {
- it('should redirect to the login page', async() => {
- render(
- ,
- {},
- store,
- { initialEntries: ['/notifications'] },
- );
-
- await waitFor(() => {
- expect(screen.getByTestId('sign-in')).toHaveTextContent('Sign in');
- });
- });
- });
- });
-});
diff --git a/app/soapbox/features/ui/components/modals/account-note-modal.tsx b/app/soapbox/features/ui/components/modals/account-note-modal.tsx
deleted file mode 100644
index 95afc614e..000000000
--- a/app/soapbox/features/ui/components/modals/account-note-modal.tsx
+++ /dev/null
@@ -1,67 +0,0 @@
-import React from 'react';
-import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
-
-import { changeAccountNoteComment, submitAccountNote } from 'soapbox/actions/account-notes';
-import { closeModal } from 'soapbox/actions/modals';
-import { useAccount } from 'soapbox/api/hooks';
-import { Modal, Text } from 'soapbox/components/ui';
-import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
-
-const messages = defineMessages({
- placeholder: { id: 'account_note.placeholder', defaultMessage: 'No comment provided' },
- save: { id: 'account_note.save', defaultMessage: 'Save' },
-});
-
-const AccountNoteModal = () => {
- const intl = useIntl();
- const dispatch = useAppDispatch();
-
- const isSubmitting = useAppSelector((state) => state.account_notes.edit.isSubmitting);
- const accountId = useAppSelector((state) => state.account_notes.edit.account);
- const { account } = useAccount(accountId || undefined);
- const comment = useAppSelector((state) => state.account_notes.edit.comment);
-
- const onClose = () => {
- dispatch(closeModal('ACCOUNT_NOTE'));
- };
-
- const handleCommentChange: React.ChangeEventHandler = e => {
- dispatch(changeAccountNoteComment(e.target.value));
- };
-
- const handleSubmit = () => {
- dispatch(submitAccountNote());
- };
-
- const handleKeyDown: React.KeyboardEventHandler = e => {
- if (e.keyCode === 13 && (e.ctrlKey || e.metaKey)) {
- handleSubmit();
- }
- };
-
- return (
- }
- onClose={onClose}
- confirmationAction={handleSubmit}
- confirmationText={intl.formatMessage(messages.save)}
- confirmationDisabled={isSubmitting}
- >
-
-
-
-
-
-
- );
-};
-
-export default AccountNoteModal;
diff --git a/app/soapbox/features/ui/util/async-components.ts b/app/soapbox/features/ui/util/async-components.ts
deleted file mode 100644
index 4891185fa..000000000
--- a/app/soapbox/features/ui/util/async-components.ts
+++ /dev/null
@@ -1,643 +0,0 @@
-export function EmojiPicker() {
- return import(/* webpackChunkName: "emoji_picker" */'../../emoji/components/emoji-picker');
-}
-
-export function Notifications() {
- return import(/* webpackChunkName: "features/notifications" */'../../notifications');
-}
-
-export function HomeTimeline() {
- return import(/* webpackChunkName: "features/home_timeline" */'../../home-timeline');
-}
-
-export function PublicTimeline() {
- return import(/* webpackChunkName: "features/public_timeline" */'../../public-timeline');
-}
-
-export function RemoteTimeline() {
- return import(/* webpackChunkName: "features/remote_timeline" */'../../remote-timeline');
-}
-
-export function CommunityTimeline() {
- return import(/* webpackChunkName: "features/community_timeline" */'../../community-timeline');
-}
-
-export function HashtagTimeline() {
- return import(/* webpackChunkName: "features/hashtag_timeline" */'../../hashtag-timeline');
-}
-
-export function DirectTimeline() {
- return import(/* webpackChunkName: "features/direct_timeline" */'../../direct-timeline');
-}
-
-export function Conversations() {
- return import(/* webpackChunkName: "features/conversations" */'../../conversations');
-}
-
-export function ListTimeline() {
- return import(/* webpackChunkName: "features/list_timeline" */'../../list-timeline');
-}
-
-export function Lists() {
- return import(/* webpackChunkName: "features/lists" */'../../lists');
-}
-
-export function Bookmarks() {
- return import(/* webpackChunkName: "features/bookmarks" */'../../bookmarks');
-}
-
-export function Status() {
- return import(/* webpackChunkName: "features/status" */'../../status');
-}
-
-export function PinnedStatuses() {
- return import(/* webpackChunkName: "features/pinned_statuses" */'../../pinned-statuses');
-}
-
-export function AccountTimeline() {
- return import(/* webpackChunkName: "features/account_timeline" */'../../account-timeline');
-}
-
-export function AccountGallery() {
- return import(/* webpackChunkName: "features/account_gallery" */'../../account-gallery');
-}
-
-export function Followers() {
- return import(/* webpackChunkName: "features/followers" */'../../followers');
-}
-
-export function Following() {
- return import(/* webpackChunkName: "features/following" */'../../following');
-}
-
-export function FollowRequests() {
- return import(/* webpackChunkName: "features/follow_requests" */'../../follow-requests');
-}
-
-export function GenericNotFound() {
- return import(/* webpackChunkName: "features/generic_not_found" */'../../generic-not-found');
-}
-
-export function FavouritedStatuses() {
- return import(/* webpackChunkName: "features/favourited_statuses" */'../../favourited-statuses');
-}
-
-export function Blocks() {
- return import(/* webpackChunkName: "features/blocks" */'../../blocks');
-}
-
-export function DomainBlocks() {
- return import(/* webpackChunkName: "features/domain_blocks" */'../../domain-blocks');
-}
-
-export function Mutes() {
- return import(/* webpackChunkName: "features/mutes" */'../../mutes');
-}
-
-export function MuteModal() {
- return import(/* webpackChunkName: "modals/mute_modal" */'../components/modals/mute-modal');
-}
-
-export function Filters() {
- return import(/* webpackChunkName: "features/filters" */'../../filters');
-}
-
-export function EditFilter() {
- return import(/* webpackChunkName: "features/filters" */'../../filters/edit-filter');
-}
-
-export function ReportModal() {
- return import(/* webpackChunkName: "modals/report-modal/report-modal" */'../components/modals/report-modal/report-modal');
-}
-
-export function AccountModerationModal() {
- return import(/* webpackChunkName: "modals/account-moderation-modal" */'../components/modals/account-moderation-modal/account-moderation-modal');
-}
-
-export function PolicyModal() {
- return import(/* webpackChunkName: "modals/policy-modal" */'../components/modals/policy-modal');
-}
-
-export function MediaGallery() {
- return import(/* webpackChunkName: "status/media_gallery" */'../../../components/media-gallery');
-}
-
-export function Video() {
- return import(/* webpackChunkName: "features/video" */'../../video');
-}
-
-export function Audio() {
- return import(/* webpackChunkName: "features/audio" */'../../audio');
-}
-
-export function MediaModal() {
- return import(/* webpackChunkName: "features/ui" */'../components/modals/media-modal');
-}
-
-export function VideoModal() {
- return import(/* webpackChunkName: "features/ui" */'../components/modals/video-modal');
-}
-
-export function BoostModal() {
- return import(/* webpackChunkName: "features/ui" */'../components/modals/boost-modal');
-}
-
-export function ConfirmationModal() {
- return import(/* webpackChunkName: "features/ui" */'../components/modals/confirmation-modal');
-}
-
-export function MissingDescriptionModal() {
- return import(/* webpackChunkName: "features/ui" */'../components/modals/missing-description-modal');
-}
-
-export function ActionsModal() {
- return import(/* webpackChunkName: "features/ui" */'../components/modals/actions-modal');
-}
-
-export function HotkeysModal() {
- return import(/* webpackChunkName: "features/ui" */'../components/modals/hotkeys-modal');
-}
-
-export function ComposeModal() {
- return import(/* webpackChunkName: "features/ui" */'../components/modals/compose-modal');
-}
-
-export function ReplyMentionsModal() {
- return import(/* webpackChunkName: "features/ui" */'../components/modals/reply-mentions-modal');
-}
-
-export function UnauthorizedModal() {
- return import(/* webpackChunkName: "features/ui" */'../components/modals/unauthorized-modal');
-}
-
-export function EditFederationModal() {
- return import(/* webpackChunkName: "features/ui" */'../components/modals/edit-federation-modal');
-}
-
-export function EmbedModal() {
- return import(/* webpackChunkName: "modals/embed_modal" */'../components/modals/embed-modal');
-}
-
-export function ComponentModal() {
- return import(/* webpackChunkName: "features/ui" */'../components/modals/component-modal');
-}
-
-export function ReblogsModal() {
- return import(/* webpackChunkName: "features/ui" */'../components/modals/reblogs-modal');
-}
-
-export function FavouritesModal() {
- return import(/* webpackChunkName: "features/ui" */'../components/modals/favourites-modal');
-}
-
-export function DislikesModal() {
- return import(/* webpackChunkName: "features/ui" */'../components/modals/dislikes-modal');
-}
-
-export function ReactionsModal() {
- return import(/* webpackChunkName: "features/ui" */'../components/modals/reactions-modal');
-}
-
-export function MentionsModal() {
- return import(/* webpackChunkName: "features/ui" */'../components/modals/mentions-modal');
-}
-
-export function LandingPageModal() {
- return import(/* webpackChunkName: "features/ui/modals/landing-page-modal" */'../components/modals/landing-page-modal');
-}
-
-export function BirthdaysModal() {
- return import(/* webpackChunkName: "features/ui" */'../components/modals/birthdays-modal');
-}
-
-export function BirthdayPanel() {
- return import(/* webpackChunkName: "features/ui" */'../../../components/birthday-panel');
-}
-
-export function AccountNoteModal() {
- return import(/* webpackChunkName: "features/ui" */'../components/modals/account-note-modal');
-}
-
-export function ListEditor() {
- return import(/* webpackChunkName: "features/list_editor" */'../../list-editor');
-}
-
-export function ListAdder() {
- return import(/*webpackChunkName: "features/list_adder" */'../../list-adder');
-}
-
-export function Search() {
- return import(/*webpackChunkName: "features/search" */'../../search');
-}
-
-export function LoginPage() {
- return import(/* webpackChunkName: "features/auth_login" */'../../auth-login/components/login-page');
-}
-
-export function ExternalLogin() {
- return import(/* webpackChunkName: "features/external_login" */'../../external-login');
-}
-
-export function LogoutPage() {
- return import(/* webpackChunkName: "features/auth_login" */'../../auth-login/components/logout');
-}
-
-export function Settings() {
- return import(/* webpackChunkName: "features/settings" */'../../settings');
-}
-
-export function EditProfile() {
- return import(/* webpackChunkName: "features/edit_profile" */'../../edit-profile');
-}
-
-export function EditEmail() {
- return import(/* webpackChunkName: "features/edit_email" */'../../edit-email');
-}
-
-export function EmailConfirmation() {
- return import(/* webpackChunkName: "features/email_confirmation" */'../../email-confirmation');
-}
-
-export function EditPassword() {
- return import(/* webpackChunkName: "features/edit_password" */'../../edit-password');
-}
-
-export function DeleteAccount() {
- return import(/* webpackChunkName: "features/delete_account" */'../../delete-account');
-}
-
-export function SoapboxConfig() {
- return import(/* webpackChunkName: "features/soapbox_config" */'../../soapbox-config');
-}
-
-export function ExportData() {
- return import(/* webpackChunkName: "features/export_data" */ '../../export-data');
-}
-
-export function ImportData() {
- return import(/* webpackChunkName: "features/import_data" */'../../import-data');
-}
-
-export function Backups() {
- return import(/* webpackChunkName: "features/backups" */'../../backups');
-}
-
-export function PasswordReset() {
- return import(/* webpackChunkName: "features/auth_login" */'../../auth-login/components/password-reset');
-}
-
-export function PasswordResetConfirm() {
- return import(/* webpackChunkName: "features/auth_login/password_reset_confirm" */'../../auth-login/components/password-reset-confirm');
-}
-
-export function MfaForm() {
- return import(/* webpackChunkName: "features/security/mfa_form" */'../../security/mfa-form');
-}
-
-export function ChatIndex() {
- return import(/* webpackChunkName: "features/chats" */'../../chats');
-}
-
-export function ChatWidget() {
- return import(/* webpackChunkName: "features/chats/components/chat-widget" */'../../chats/components/chat-widget/chat-widget');
-}
-
-export function ServerInfo() {
- return import(/* webpackChunkName: "features/server_info" */'../../server-info');
-}
-
-export function Dashboard() {
- return import(/* webpackChunkName: "features/admin" */'../../admin');
-}
-
-export function ModerationLog() {
- return import(/* webpackChunkName: "features/admin/moderation_log" */'../../admin/moderation-log');
-}
-
-export function ThemeEditor() {
- return import(/* webpackChunkName: "features/theme-editor" */'../../theme-editor');
-}
-
-export function UserPanel() {
- return import(/* webpackChunkName: "features/ui" */'../components/user-panel');
-}
-
-export function PromoPanel() {
- return import(/* webpackChunkName: "features/ui" */'../components/promo-panel');
-}
-
-export function SignUpPanel() {
- return import(/* webpackChunkName: "features/ui" */'../components/panels/sign-up-panel');
-}
-
-export function CtaBanner() {
- return import(/* webpackChunkName: "features/ui" */'../components/cta-banner');
-}
-
-export function FundingPanel() {
- return import(/* webpackChunkName: "features/ui" */'../components/funding-panel');
-}
-
-export function TrendsPanel() {
- return import(/* webpackChunkName: "features/trends" */'../components/trends-panel');
-}
-
-export function ProfileInfoPanel() {
- return import(/* webpackChunkName: "features/account_timeline" */'../components/profile-info-panel');
-}
-
-export function ProfileMediaPanel() {
- return import(/* webpackChunkName: "features/account_gallery" */'../components/profile-media-panel');
-}
-
-export function ProfileFieldsPanel() {
- return import(/* webpackChunkName: "features/account_timeline" */'../components/profile-fields-panel');
-}
-
-export function PinnedAccountsPanel() {
- return import(/* webpackChunkName: "features/pinned_accounts" */'../components/pinned-accounts-panel');
-}
-
-export function InstanceInfoPanel() {
- return import(/* webpackChunkName: "features/remote_timeline" */'../components/instance-info-panel');
-}
-
-export function InstanceModerationPanel() {
- return import(/* webpackChunkName: "features/remote_timeline" */'../components/instance-moderation-panel');
-}
-
-export function LatestAccountsPanel() {
- return import(/* webpackChunkName: "features/admin" */'../../admin/components/latest-accounts-panel');
-}
-
-export function SidebarMenu() {
- return import(/* webpackChunkName: "features/ui" */'../../../components/sidebar-menu');
-}
-
-export function ModalContainer() {
- return import(/* webpackChunkName: "features/ui" */'../containers/modal-container');
-}
-
-export function ProfileHoverCard() {
- return import(/* webpackChunkName: "features/ui" */'soapbox/components/profile-hover-card');
-}
-
-export function StatusHoverCard() {
- return import(/* webpackChunkName: "features/ui" */'soapbox/components/status-hover-card');
-}
-
-export function CryptoDonate() {
- return import(/* webpackChunkName: "features/crypto_donate" */'../../crypto-donate');
-}
-
-export function CryptoDonatePanel() {
- return import(/* webpackChunkName: "features/crypto_donate" */'../../crypto-donate/components/crypto-donate-panel');
-}
-
-export function CryptoAddress() {
- return import(/* webpackChunkName: "features/crypto_donate" */'../../crypto-donate/components/crypto-address');
-}
-
-export function CryptoDonateModal() {
- return import(/* webpackChunkName: "features/crypto_donate" */'../components/modals/crypto-donate-modal');
-}
-
-export function ScheduledStatuses() {
- return import(/* webpackChunkName: "features/scheduled_statuses" */'../../scheduled-statuses');
-}
-
-export function UserIndex() {
- return import(/* webpackChunkName: "features/admin/user_index" */'../../admin/user-index');
-}
-
-export function FederationRestrictions() {
- return import(/* webpackChunkName: "features/federation_restrictions" */'../../federation-restrictions');
-}
-
-export function Aliases() {
- return import(/* webpackChunkName: "features/aliases" */'../../aliases');
-}
-
-export function Migration() {
- return import(/* webpackChunkName: "features/migration" */'../../migration');
-}
-
-export function ScheduleForm() {
- return import(/* webpackChunkName: "features/compose" */'../../compose/components/schedule-form');
-}
-
-export function WhoToFollowPanel() {
- return import(/* webpackChunkName: "features/follow_recommendations" */'../components/who-to-follow-panel');
-}
-
-export function FollowRecommendations() {
- return import(/* webpackChunkName: "features/follow-recommendations" */'../../follow-recommendations');
-}
-
-export function Directory() {
- return import(/* webpackChunkName: "features/directory" */'../../directory');
-}
-
-export function RegisterInvite() {
- return import(/* webpackChunkName: "features/register_invite" */'../../register-invite');
-}
-
-export function Share() {
- return import(/* webpackChunkName: "features/share" */'../../share');
-}
-
-export function NewStatus() {
- return import(/* webpackChunkName: "features/new_status" */'../../new-status');
-}
-
-export function IntentionalError() {
- return import(/* webpackChunkName: "error" */'../../intentional-error');
-}
-
-export function Developers() {
- return import(/* webpackChunkName: "features/developers" */'../../developers');
-}
-
-export function CreateApp() {
- return import(/* webpackChunkName: "features/developers" */'../../developers/apps/create');
-}
-
-export function SettingsStore() {
- return import(/* webpackChunkName: "features/developers" */'../../developers/settings-store');
-}
-
-export function TestTimeline() {
- return import(/* webpackChunkName: "features/test_timeline" */'../../test-timeline');
-}
-
-export function ServiceWorkerInfo() {
- return import(/* webpackChunkName: "features/developers" */'../../developers/service-worker-info');
-}
-
-export function DatePicker() {
- return import(/* webpackChunkName: "date_picker" */'../../birthdays/date-picker');
-}
-
-export function OnboardingWizard() {
- return import(/* webpackChunkName: "features/onboarding" */'../../onboarding/onboarding-wizard');
-}
-
-export function WaitlistPage() {
- return import(/* webpackChunkName: "features/verification" */'../../verification/waitlist-page');
-}
-
-export function CompareHistoryModal() {
- return import(/*webpackChunkName: "modals/compare_history_modal" */'../components/modals/compare-history-modal');
-}
-
-export function AuthTokenList() {
- return import(/* webpackChunkName: "features/auth_token_list" */'../../auth-token-list');
-}
-
-export function VerifySmsModal() {
- return import(/* webpackChunkName: "features/ui" */'../components/modals/verify-sms-modal');
-}
-
-export function FamiliarFollowersModal() {
- return import(/*webpackChunkName: "modals/familiar_followers_modal" */'../components/modals/familiar-followers-modal');
-}
-
-export function AnnouncementsPanel() {
- return import(/* webpackChunkName: "features/announcements" */'../../../components/announcements/announcements-panel');
-}
-
-export function Quotes() {
- return import(/*webpackChunkName: "features/quotes" */'../../quotes');
-}
-
-export function ComposeEventModal() {
- return import(/* webpackChunkName: "features/compose_event_modal" */'../components/modals/compose-event-modal/compose-event-modal');
-}
-
-export function JoinEventModal() {
- return import(/* webpackChunkName: "features/join_event_modal" */'../components/modals/join-event-modal');
-}
-
-export function EventHeader() {
- return import(/* webpackChunkName: "features/event" */'../../event/components/event-header');
-}
-
-export function EventInformation() {
- return import(/* webpackChunkName: "features/event" */'../../event/event-information');
-}
-
-export function EventDiscussion() {
- return import(/* webpackChunkName: "features/event" */'../../event/event-discussion');
-}
-
-export function EventMapModal() {
- return import(/* webpackChunkName: "modals/event-map-modal" */'../components/modals/event-map-modal');
-}
-
-export function EventParticipantsModal() {
- return import(/* webpackChunkName: "modals/event-participants-modal" */'../components/modals/event-participants-modal');
-}
-
-export function Events() {
- return import(/* webpackChunkName: "features/events" */'../../events');
-}
-
-export function Groups() {
- return import(/* webpackChunkName: "features/groups" */'../../groups');
-}
-
-export function GroupsDiscover() {
- return import(/* webpackChunkName: "features/groups" */'../../groups/discover');
-}
-
-export function GroupsPopular() {
- return import(/* webpackChunkName: "features/groups" */'../../groups/popular');
-}
-
-export function GroupsSuggested() {
- return import(/* webpackChunkName: "features/groups" */'../../groups/suggested');
-}
-
-export function GroupsTag() {
- return import(/* webpackChunkName: "features/groups" */'../../groups/tag');
-}
-
-export function GroupsTags() {
- return import(/* webpackChunkName: "features/groups" */'../../groups/tags');
-}
-
-export function PendingGroupRequests() {
- return import(/* webpackChunkName: "features/groups" */'../../groups/pending-requests');
-}
-
-export function GroupMembers() {
- return import(/* webpackChunkName: "features/groups" */'../../group/group-members');
-}
-
-export function GroupTags() {
- return import(/* webpackChunkName: "features/groups" */'../../group/group-tags');
-}
-
-export function GroupTagTimeline() {
- return import(/* webpackChunkName: "features/groups" */'../../group/group-tag-timeline');
-}
-
-export function GroupTimeline() {
- return import(/* webpackChunkName: "features/groups" */'../../group/group-timeline');
-}
-
-export function ManageGroup() {
- return import(/* webpackChunkName: "features/groups" */'../../group/manage-group');
-}
-
-export function EditGroup() {
- return import(/* webpackChunkName: "features/groups" */'../../group/edit-group');
-}
-
-export function GroupBlockedMembers() {
- return import(/* webpackChunkName: "features/groups" */'../../group/group-blocked-members');
-}
-
-export function GroupMembershipRequests() {
- return import(/* webpackChunkName: "features/groups" */'../../group/group-membership-requests');
-}
-
-export function GroupGallery() {
- return import(/* webpackChunkName: "features/groups" */'../../group/group-gallery');
-}
-
-export function CreateGroupModal() {
- return import(/* webpackChunkName: "features/groups" */'../components/modals/manage-group-modal/create-group-modal');
-}
-
-export function NewGroupPanel() {
- return import(/* webpackChunkName: "features/groups" */'../components/panels/new-group-panel');
-}
-
-export function MyGroupsPanel() {
- return import(/* webpackChunkName: "features/groups" */'../components/panels/my-groups-panel');
-}
-
-export function SuggestedGroupsPanel() {
- return import(/* webpackChunkName: "features/groups" */'../components/panels/suggested-groups-panel');
-}
-
-export function GroupMediaPanel() {
- return import(/* webpackChunkName: "features/groups" */'../components/group-media-panel');
-}
-
-export function NewEventPanel() {
- return import(/* webpackChunkName: "features/events" */'../components/panels/new-event-panel');
-}
-
-export function Announcements() {
- return import(/* webpackChunkName: "features/admin/announcements" */'../../admin/announcements');
-}
-
-export function EditAnnouncementModal() {
- return import(/* webpackChunkName: "features/admin/announcements" */'../components/modals/edit-announcement-modal');
-}
-
-export function FollowedTags() {
- return import(/* webpackChunkName: "features/followed-tags" */'../../followed-tags');
-}
diff --git a/app/soapbox/reducers/__tests__/accounts.test.ts b/app/soapbox/reducers/__tests__/accounts.test.ts
deleted file mode 100644
index 02baa6d92..000000000
--- a/app/soapbox/reducers/__tests__/accounts.test.ts
+++ /dev/null
@@ -1,29 +0,0 @@
-import { Map as ImmutableMap, Record as ImmutableRecord } from 'immutable';
-
-import { ACCOUNT_IMPORT } from 'soapbox/actions/importer';
-
-import reducer from '../accounts';
-
-describe('accounts reducer', () => {
- it('should return the initial state', () => {
- expect(reducer(undefined, {} as any)).toEqual(ImmutableMap());
- });
-
- describe('ACCOUNT_IMPORT', () => {
- it('parses the account as a Record', () => {
- const account = require('soapbox/__fixtures__/pleroma-account.json');
- const action = { type: ACCOUNT_IMPORT, account };
- const result = reducer(undefined, action).get('9v5bmRalQvjOy0ECcC');
-
- expect(ImmutableRecord.isRecord(result)).toBe(true);
- });
-
- it('minifies a moved account', () => {
- const account = require('soapbox/__fixtures__/account-moved.json');
- const action = { type: ACCOUNT_IMPORT, account };
- const result = reducer(undefined, action).get('106801667066418367');
-
- expect(result?.moved).toBe('107945464165013501');
- });
- });
-});
diff --git a/app/soapbox/reducers/account-notes.ts b/app/soapbox/reducers/account-notes.ts
deleted file mode 100644
index 0f30b8d60..000000000
--- a/app/soapbox/reducers/account-notes.ts
+++ /dev/null
@@ -1,43 +0,0 @@
-import { Record as ImmutableRecord } from 'immutable';
-
-import {
- ACCOUNT_NOTE_INIT_MODAL,
- ACCOUNT_NOTE_CHANGE_COMMENT,
- ACCOUNT_NOTE_SUBMIT_REQUEST,
- ACCOUNT_NOTE_SUBMIT_FAIL,
- ACCOUNT_NOTE_SUBMIT_SUCCESS,
-} from '../actions/account-notes';
-
-import type { AnyAction } from 'redux';
-
-export const EditRecord = ImmutableRecord({
- isSubmitting: false,
- account: null as string | null,
- comment: '',
-});
-
-export const ReducerRecord = ImmutableRecord({
- edit: EditRecord(),
-});
-
-type State = ReturnType;
-
-export default function account_notes(state: State = ReducerRecord(), action: AnyAction) {
- switch (action.type) {
- case ACCOUNT_NOTE_INIT_MODAL:
- return state.withMutations((state) => {
- state.setIn(['edit', 'isSubmitting'], false);
- state.setIn(['edit', 'account'], action.account.get('id'));
- state.setIn(['edit', 'comment'], action.comment);
- });
- case ACCOUNT_NOTE_CHANGE_COMMENT:
- return state.setIn(['edit', 'comment'], action.comment);
- case ACCOUNT_NOTE_SUBMIT_REQUEST:
- return state.setIn(['edit', 'isSubmitting'], true);
- case ACCOUNT_NOTE_SUBMIT_FAIL:
- case ACCOUNT_NOTE_SUBMIT_SUCCESS:
- return state.setIn(['edit', 'isSubmitting'], false);
- default:
- return state;
- }
-}
diff --git a/app/soapbox/service-worker/entry.ts b/app/soapbox/service-worker/entry.ts
deleted file mode 100644
index 3dbfee2ce..000000000
--- a/app/soapbox/service-worker/entry.ts
+++ /dev/null
@@ -1 +0,0 @@
-import './web-push-notifications';
diff --git a/app/soapbox/types/nostr.ts b/app/soapbox/types/nostr.ts
deleted file mode 100644
index b395268ef..000000000
--- a/app/soapbox/types/nostr.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-import type { Event, EventTemplate } from 'nostr-tools';
-
-interface Nostr {
- getPublicKey(): Promise
- signEvent(event: EventTemplate): Promise
-}
-
-export default Nostr;
\ No newline at end of file
diff --git a/dangerfile.ts b/dangerfile.ts
deleted file mode 100644
index 6ed716fbc..000000000
--- a/dangerfile.ts
+++ /dev/null
@@ -1,42 +0,0 @@
-import { danger, warn, message } from 'danger';
-
-// App changes
-const app = danger.git.fileMatch('app/soapbox/**');
-
-// Docs changes
-const docs = danger.git.fileMatch('docs/**/*.md');
-
-if (docs.edited) {
- message('Thanks - We :heart: our [documentarians](http://www.writethedocs.org/)!');
-}
-
-// Enforce CHANGELOG.md additions
-const changelog = danger.git.fileMatch('CHANGELOG.md');
-
-if (app.edited && !changelog.edited) {
- warn('You have not updated `CHANGELOG.md`. If this change directly impacts admins or users, please update the changelog. Otherwise you can ignore this message. See: https://keepachangelog.com');
-}
-
-// UI components
-const uiCode = danger.git.fileMatch('app/soapbox/components/ui/**');
-const uiTests = danger.git.fileMatch('app/soapbox/components/ui/**/__tests__/**');
-
-if (uiCode.edited && !uiTests.edited) {
- warn('You have UI changes (`soapbox/components/ui`) without tests.');
-}
-
-// Actions
-const actionsCode = danger.git.fileMatch('app/soapbox/actions/**');
-const actionsTests = danger.git.fileMatch('app/soapbox/actions/**__tests__/**');
-
-if (actionsCode.edited && !actionsTests.edited) {
- warn('You have actions changes (`soapbox/actions`) without tests.');
-}
-
-// Reducers
-const reducersCode = danger.git.fileMatch('app/soapbox/reducers/**');
-const reducersTests = danger.git.fileMatch('app/soapbox/reducers/**__tests__/**');
-
-if (reducersCode.edited && !reducersTests.edited) {
- warn('You have reducer changes (`soapbox/reducers`) without tests.');
-}
diff --git a/docs/administration/deploy-at-scale.md b/docs/administration/deploy-at-scale.md
index 40e878a0a..4280a82b2 100644
--- a/docs/administration/deploy-at-scale.md
+++ b/docs/administration/deploy-at-scale.md
@@ -11,7 +11,7 @@ The best way to get Soapbox builds is from a GitLab CI job.
The official build URL is here:
```
-https://gitlab.com/soapbox-pub/soapbox/-/jobs/artifacts/develop/download?job=build-production
+https://dl.soapbox.pub/main/soapbox.zip
```
(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/-/blob/develop/installation/mastodon.conf) as a starting point.
+We recommend trying [`mastodon.conf`](https://gitlab.com/soapbox-pub/soapbox/-/blob/main/installation/mastodon.conf) as a starting point.
It is fine-tuned, includes support for federation, and should work with any backend.
## The ServiceWorker
diff --git a/docs/administration/install-yunohost.md b/docs/administration/install-yunohost.md
index 156547d9e..83a96d800 100644
--- a/docs/administration/install-yunohost.md
+++ b/docs/administration/install-yunohost.md
@@ -7,7 +7,7 @@ If you want to install Soapbox to a Pleroma instance installed using [YunoHost](
First, download the latest build of Soapbox from GitLab.
```sh
-curl -L https://gitlab.com/soapbox-pub/soapbox/-/jobs/artifacts/develop/download?job=build-production -o soapbox-fe.zip
+curl -O https://dl.soapbox.pub/main/soapbox.zip
```
## 2. Unzip the build
@@ -15,7 +15,7 @@ curl -L https://gitlab.com/soapbox-pub/soapbox/-/jobs/artifacts/develop/download
Then, unzip the build to the Pleroma directory under YunoHost's directory:
```sh
-busybox unzip soapbox-fe.zip -o -d /home/yunohost.app/pleroma/
+busybox unzip soapbox.zip -o -d /home/yunohost.app/pleroma/static
```
## 3. Change YunoHost Admin Static directory
diff --git a/docs/administration/mastodon.md b/docs/administration/mastodon.md
index 345408ad1..8f0914461 100644
--- a/docs/administration/mastodon.md
+++ b/docs/administration/mastodon.md
@@ -8,16 +8,16 @@ To do so, shell into your server and unpack Soapbox:
```sh
mkdir -p /opt/soapbox
-curl -L https://gitlab.com/soapbox-pub/soapbox/-/jobs/artifacts/develop/download?job=build-production -o soapbox-fe.zip
+curl -O https://dl.soapbox.pub/main/soapbox.zip
-busybox unzip soapbox-fe.zip -o -d /opt/soapbox
+busybox unzip soapbox.zip -o -d /opt/soapbox
```
Now create an Nginx file for Soapbox with Mastodon.
If you already have one, replace it:
```sh
-curl https://gitlab.com/soapbox-pub/soapbox/-/raw/develop/installation/mastodon.conf > /etc/nginx/sites-available/mastodon
+curl https://gitlab.com/soapbox-pub/soapbox/-/raw/main/installation/mastodon.conf > /etc/nginx/sites-available/mastodon
```
Edit this file and replace all occurrences of `example.com` with your domain name.
diff --git a/docs/administration/updating.md b/docs/administration/updating.md
index 6e5252efa..21eb8ffa0 100644
--- a/docs/administration/updating.md
+++ b/docs/administration/updating.md
@@ -1,6 +1,6 @@
# Updating Soapbox
-You should always check the [release notes/changelog](https://gitlab.com/soapbox-pub/soapbox/-/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/main/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,15 +10,10 @@ To update Soapbox via the command line, do the following:
```
# Download the build.
-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
-rm /opt/pleroma/instance/static/index.html
-rm -R /opt/pleroma/instance/static/sounds
+curl -O https://dl.soapbox.pub/main/soapbox.zip
# Unzip the new build to Pleroma's instance directory.
-busybox unzip soapbox-fe.zip -o -d /opt/pleroma/instance
+busybox unzip soapbox.zip -o -d /opt/pleroma/instance/static
```
## After updating Soapbox
diff --git a/docs/contributing.md b/docs/contributing.md
index bb59effc7..2d6aed726 100644
--- a/docs/contributing.md
+++ b/docs/contributing.md
@@ -15,7 +15,7 @@ When contributing to Soapbox, please first discuss the change you wish to make b
When you push to a branch, the CI pipeline will run.
-[Soapbox uses GitLab CI](https://gitlab.com/soapbox-pub/soapbox/-/blob/develop/.gitlab-ci.yml) to lint, run tests, and verify changes.
+[Soapbox uses GitLab CI](https://gitlab.com/soapbox-pub/soapbox/-/blob/main/.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/-/blob/develop/.vscode/extensions.json) for the full list.
+See [`.vscode/extensions.json`](https://gitlab.com/soapbox-pub/soapbox/-/blob/main/.vscode/extensions.json) for the full list.
diff --git a/docs/development/build-config.md b/docs/development/build-config.md
index 65fa70a95..b4236d39d 100644
--- a/docs/development/build-config.md
+++ b/docs/development/build-config.md
@@ -71,7 +71,7 @@ For example:
}
```
-See `app/soapbox/utils/features.js` for the full list of features.
+See `src/utils/features.js` for the full list of features.
### Embedded app (`custom/app.json`)
@@ -118,7 +118,7 @@ When compiling Soapbox, environment variables may be passed to change the build
For example:
```sh
-NODE_ENV="production" FE_BUILD_DIR="public" FE_SUBDIRECTORY="/soapbox" yarn build
+NODE_ENV="production" FE_SUBDIRECTORY="/soapbox" yarn build
```
### `NODE_ENV`
@@ -147,16 +147,6 @@ Options:
Default: `""`
-### `FE_BUILD_DIR`
-
-The folder to put build files in. This is mostly useful for CI tasks like GitLab Pages.
-
-Options:
-
-- Any directory name, eg `"public"`
-
-Default: `"static"`
-
### `FE_SUBDIRECTORY`
Subdirectory to host Soapbox out of.
diff --git a/docs/development/developing-backend.md b/docs/development/developing-backend.md
index 723a28002..7896323ce 100644
--- a/docs/development/developing-backend.md
+++ b/docs/development/developing-backend.md
@@ -48,7 +48,7 @@ Typically checks are done against `BACKEND_NAME` and `VERSION`.
The version string is similar in purpose to a [User-Agent](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/User-Agent) string.
The format was first invented by Pleroma, but is now widely used, including by Pixelfed, Mitra, and Soapbox BE.
-See [`features.ts`](https://gitlab.com/soapbox-pub/soapbox/-/blob/develop/app/soapbox/utils/features.ts) for the complete list of features.
+See [`features.ts`](https://gitlab.com/soapbox-pub/soapbox/-/blob/main/src/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/-/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/main/src/utils/features.ts) and submit a merge request to enable features for your backend!
diff --git a/docs/development/how-it-works.md b/docs/development/how-it-works.md
index 52a326d8a..f8578d893 100644
--- a/docs/development/how-it-works.md
+++ b/docs/development/how-it-works.md
@@ -18,7 +18,7 @@ location / {
}
```
-(See [`mastodon.conf`](https://gitlab.com/soapbox-pub/soapbox/-/blob/develop/installation/mastodon.conf) for a full example.)
+(See [`mastodon.conf`](https://gitlab.com/soapbox-pub/soapbox/-/blob/main/installation/mastodon.conf) for a full example.)
Soapbox incorporates much of the [Mastodon API](https://docs.joinmastodon.org/methods/), [Pleroma API](https://api.pleroma.social/), and more.
It detects features supported by the backend to provide the right experience for the backend.
diff --git a/docs/development/running-locally.md b/docs/development/running-locally.md
index 7cd1164a6..08728ab75 100644
--- a/docs/development/running-locally.md
+++ b/docs/development/running-locally.md
@@ -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/-/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/main/.tool-versions).
If they don't match, try installing [asdf](https://asdf-vm.com/).
diff --git a/docs/development/yarn-commands.md b/docs/development/yarn-commands.md
index 0180ed89f..6838706fc 100644
--- a/docs/development/yarn-commands.md
+++ b/docs/development/yarn-commands.md
@@ -12,7 +12,7 @@ NODE_ENV=development
- `yarn dev` - Run the local dev server.
## Building
-- `yarn build` - Compile without a dev server, into `/static` directory.
+- `yarn build` - Compile without a dev server, into `/dist` directory.
## Translations
- `yarn i18n` - Rebuilds app and updates English locale to prepare for translations in other languages. Should always be run after editing i18n strings.
diff --git a/docs/installing.md b/docs/installing.md
index 37c9c36e5..c70448b70 100644
--- a/docs/installing.md
+++ b/docs/installing.md
@@ -10,9 +10,9 @@ 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/-/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 -O https://dl.soapbox.pub/main/soapbox.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.zip -o -d /opt/pleroma/instance/static`
**That's it! 🎉 Soapbox is installed.** The change will take effect immediately, just refresh your browser tab. It's not necessary to restart the Pleroma service.
diff --git a/app/index.ejs b/index.html
similarity index 90%
rename from app/index.ejs
rename to index.html
index e7c9b7330..25e84494a 100644
--- a/app/index.ejs
+++ b/index.html
@@ -7,7 +7,8 @@
- <%= snippets %>
+
+ <%- snippets %>
@@ -21,4 +22,4 @@
-