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 0df30b234..4359767c2 100644
--- a/.eslintrc.cjs
+++ b/.eslintrc.cjs
@@ -19,9 +19,8 @@ module.exports = {
ATTACHMENT_HOST: false,
},
- parser: '@babel/eslint-parser',
-
plugins: [
+ 'jsdoc',
'react',
'jsx-a11y',
'import',
@@ -49,9 +48,8 @@ module.exports = {
'\\.(css|scss|json)$',
],
'import/resolver': {
- node: {
- paths: ['app'],
- },
+ typescript: true,
+ node: true,
},
polyfills: [
'es:all', // core-js
@@ -78,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'],
@@ -259,7 +258,6 @@ module.exports = {
alphabetize: { order: 'asc' },
},
],
- '@typescript-eslint/no-duplicate-imports': 'error',
'@typescript-eslint/member-delimiter-style': [
'error',
{
@@ -294,5 +292,23 @@ module.exports = {
},
parser: '@typescript-eslint/parser',
},
+ {
+ // Only enforce JSDoc comments on UI components for now.
+ // https://www.npmjs.com/package/eslint-plugin-jsdoc
+ files: ['src/components/ui/**/*'],
+ rules: {
+ 'jsdoc/require-jsdoc': ['error', {
+ publicOnly: true,
+ require: {
+ ArrowFunctionExpression: true,
+ ClassDeclaration: true,
+ ClassExpression: true,
+ FunctionDeclaration: true,
+ FunctionExpression: true,
+ MethodDefinition: 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/.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/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/assets/icons/COPYING.md b/app/assets/icons/COPYING.md
deleted file mode 100644
index a5dbe7d98..000000000
--- a/app/assets/icons/COPYING.md
+++ /dev/null
@@ -1,5 +0,0 @@
-# Custom icons
-
-- verified.svg - Created by Alex Gleason. CC0
-
-Fediverse logo: https://en.wikipedia.org/wiki/Fediverse#/media/File:Fediverse_logo_proposal.svg
diff --git a/app/soapbox/__fixtures__/truthsocial-status-with-external-video.json b/app/soapbox/__fixtures__/truthsocial-status-with-external-video.json
deleted file mode 100644
index eb6150192..000000000
--- a/app/soapbox/__fixtures__/truthsocial-status-with-external-video.json
+++ /dev/null
@@ -1,95 +0,0 @@
-{
- "id": "108046244464677537",
- "created_at": "2022-03-30T15:40:53.287Z",
- "in_reply_to_id": null,
- "in_reply_to_account_id": null,
- "sensitive": false,
- "spoiler_text": "",
- "visibility": "public",
- "language": null,
- "uri": "https://truthsocial.com/users/alex/statuses/108046244464677537",
- "url": "https://truthsocial.com/@alex/108046244464677537",
- "replies_count": 0,
- "reblogs_count": 0,
- "favourites_count": 0,
- "favourited": false,
- "reblogged": false,
- "muted": false,
- "bookmarked": false,
- "pinned": false,
- "content": "",
- "reblog": null,
- "application": {
- "name": "Soapbox FE",
- "website": "https://soapbox.pub/"
- },
- "account": {
- "id": "107759994408336377",
- "username": "alex",
- "acct": "alex",
- "display_name": "Alex Gleason",
- "locked": false,
- "bot": false,
- "discoverable": null,
- "group": false,
- "created_at": "2022-02-08T00:00:00.000Z",
- "note": "
Launching Truth Social
",
- "url": "https://truthsocial.com/@alex",
- "avatar": "https://static-assets.truthsocial.com/tmtg:prime-truth-social-assets/accounts/avatars/107/759/994/408/336/377/original/119cb0dd1fa615b7.png",
- "avatar_static": "https://static-assets.truthsocial.com/tmtg:prime-truth-social-assets/accounts/avatars/107/759/994/408/336/377/original/119cb0dd1fa615b7.png",
- "header": "https://static-assets.truthsocial.com/tmtg:prime-truth-social-assets/accounts/headers/107/759/994/408/336/377/original/31f62b0453ccf554.png",
- "header_static": "https://static-assets.truthsocial.com/tmtg:prime-truth-social-assets/accounts/headers/107/759/994/408/336/377/original/31f62b0453ccf554.png",
- "followers_count": 4713,
- "following_count": 43,
- "statuses_count": 7,
- "last_status_at": "2022-03-30",
- "verified": true,
- "location": "Texas",
- "website": "https://soapbox.pub/",
- "emojis": [],
- "fields": []
- },
- "media_attachments": [
- {
- "id": "108046243948255335",
- "type": "video",
- "url": "https://static-assets.truthsocial.com/tmtg:prime-truth-social-assets/media_attachments/files/108/046/243/948/255/335/original/3b17ce701c0d6f08.mp4",
- "preview_url": "https://static-assets.truthsocial.com/tmtg:prime-truth-social-assets/cache/preview_cards/images/000/543/912/original/e1fcf6ace01d9ded.jpg",
- "external_video_id": "vwfnq9",
- "remote_url": null,
- "preview_remote_url": null,
- "text_url": "https://truthsocial.com/media/SpbYvqKIT2VubC9FFn0",
- "meta": {
- "original": {
- "width": 988,
- "height": 556,
- "frame_rate": "60/1",
- "duration": 1.949025,
- "bitrate": 402396
- }
- },
- "description": null,
- "blurhash": null
- }
- ],
- "mentions": [],
- "tags": [],
- "emojis": [],
- "card": {
- "url": "https://rumble.com/vz1trd-video-upload-for-108046244464677537.html?mref=ummtf&mc=3nvg0",
- "title": "Video upload for 108046244464677537",
- "description": "",
- "type": "video",
- "author_name": "hostid1",
- "author_url": "https://rumble.com/user/hostid1",
- "provider_name": "Rumble.com",
- "provider_url": "https://rumble.com/",
- "html": "",
- "width": 988,
- "height": 556,
- "image": "https://static-assets.truthsocial.com/tmtg:prime-truth-social-assets/cache/preview_cards/images/000/543/912/original/e1fcf6ace01d9ded.jpg",
- "embed_url": "",
- "blurhash": "UQH1;m~8sks,%M~9?DRk-mRnR+xs9cWVj[bH"
- },
- "poll": null
-}
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/actions/verification.ts b/app/soapbox/actions/verification.ts
deleted file mode 100644
index 2273bb499..000000000
--- a/app/soapbox/actions/verification.ts
+++ /dev/null
@@ -1,427 +0,0 @@
-import api from '../api';
-
-import type { AppDispatch, RootState } from 'soapbox/store';
-
-/**
- * LocalStorage 'soapbox:verification'
- *
- * {
- * token: String,
- * challenges: {
- * email: Number (0 = incomplete, 1 = complete),
- * sms: Number,
- * age: Number
- * }
- * }
- */
-const LOCAL_STORAGE_VERIFICATION_KEY = 'soapbox:verification';
-
-const PEPE_FETCH_INSTANCE_SUCCESS = 'PEPE_FETCH_INSTANCE_SUCCESS';
-const FETCH_CHALLENGES_SUCCESS = 'FETCH_CHALLENGES_SUCCESS';
-const FETCH_TOKEN_SUCCESS = 'FETCH_TOKEN_SUCCESS';
-
-const SET_NEXT_CHALLENGE = 'SET_NEXT_CHALLENGE';
-const SET_CHALLENGES_COMPLETE = 'SET_CHALLENGES_COMPLETE';
-const SET_LOADING = 'SET_LOADING';
-
-const EMAIL: Challenge = 'email';
-const SMS: Challenge = 'sms';
-const AGE: Challenge = 'age';
-
-export type Challenge = 'age' | 'sms' | 'email'
-
-type Challenges = {
- email?: 0 | 1
- sms?: 0 | 1
- age?: 0 | 1
-}
-
-type Verification = {
- token?: string
- challenges?: Challenges
- challengeTypes?: Array<'age' | 'sms' | 'email'>
-};
-
-/**
- * Fetch the state of the user's verification in local storage.
- */
-const fetchStoredVerification = (): Verification | null => {
- try {
- return JSON.parse(localStorage.getItem(LOCAL_STORAGE_VERIFICATION_KEY) as string);
- } catch {
- return null;
- }
-};
-
-/**
- * Remove the state of the user's verification from local storage.
- */
-const removeStoredVerification = () => {
- localStorage.removeItem(LOCAL_STORAGE_VERIFICATION_KEY);
-};
-
-/**
- * Fetch and return the Registration token for Pepe.
- */
-const fetchStoredToken = () => {
- try {
- const verification: Verification | null = fetchStoredVerification();
- return verification!.token;
- } catch {
- return null;
- }
-};
-
-/**
- * Fetch and return the state of the verification challenges.
- */
-const fetchStoredChallenges = () => {
- try {
- const verification: Verification | null = fetchStoredVerification();
- return verification!.challenges;
- } catch {
- return null;
- }
-};
-
-/**
- * Fetch and return the state of the verification challenge types.
- */
-const fetchStoredChallengeTypes = () => {
- try {
- const verification: Verification | null = fetchStoredVerification();
- return verification!.challengeTypes;
- } catch {
- return null;
- }
-};
-
-/**
- * Update the verification object in local storage.
- *
- * @param {*} verification object
- */
-const updateStorage = ({ ...updatedVerification }: Verification) => {
- const verification = fetchStoredVerification();
-
- localStorage.setItem(
- LOCAL_STORAGE_VERIFICATION_KEY,
- JSON.stringify({ ...verification, ...updatedVerification }),
- );
-};
-
-/**
- * Fetch Pepe challenges and registration token
- */
-const fetchVerificationConfig = () =>
- async(dispatch: AppDispatch) => {
- await dispatch(fetchPepeInstance());
-
- dispatch(fetchRegistrationToken());
- };
-
-/**
- * Save the challenges in localStorage.
- *
- * - If the API removes a challenge after the client has stored it, remove that
- * challenge from localStorage.
- * - If the API adds a challenge after the client has stored it, add that
- * challenge to localStorage.
- * - Don't overwrite a challenge that has already been completed.
- * - Update localStorage to the new set of challenges.
- */
-function saveChallenges(challenges: Array<'age' | 'sms' | 'email'>) {
- const currentChallenges: Challenges = fetchStoredChallenges() || {};
-
- const challengesToRemove = Object.keys(currentChallenges).filter((currentChallenge) => !challenges.includes(currentChallenge as Challenge)) as Challenge[];
- challengesToRemove.forEach((challengeToRemove) => delete currentChallenges[challengeToRemove]);
-
- for (let i = 0; i < challenges.length; i++) {
- const challengeName = challenges[i];
-
- if (typeof currentChallenges[challengeName] !== 'number') {
- currentChallenges[challengeName] = 0;
- }
- }
-
- updateStorage({
- challenges: currentChallenges,
- challengeTypes: challenges,
- });
-}
-
-/**
- * Finish a challenge.
- */
-function finishChallenge(challenge: Challenge) {
- const currentChallenges: Challenges = fetchStoredChallenges() || {};
- // Set challenge to "complete"
- currentChallenges[challenge] = 1;
-
- updateStorage({ challenges: currentChallenges });
-}
-
-/**
- * Fetch the next challenge
- */
-const fetchNextChallenge = (): Challenge => {
- const currentChallenges: Challenges = fetchStoredChallenges() || {};
- return Object.keys(currentChallenges).find((challenge) => currentChallenges[challenge as Challenge] === 0) as Challenge;
-};
-
-/**
- * Dispatch the next challenge or set to complete if all challenges are completed.
- */
-const dispatchNextChallenge = (dispatch: AppDispatch) => {
- const nextChallenge = fetchNextChallenge();
-
- if (nextChallenge) {
- dispatch({ type: SET_NEXT_CHALLENGE, challenge: nextChallenge });
- } else {
- dispatch({ type: SET_CHALLENGES_COMPLETE });
- }
-};
-
-/**
- * Fetch the challenges and age mininum from Pepe
- */
-const fetchPepeInstance = () =>
- (dispatch: AppDispatch, getState: () => RootState) => {
- dispatch({ type: SET_LOADING });
-
- return api(getState).get('/api/v1/pepe/instance').then(response => {
- const { challenges, age_minimum: ageMinimum } = response.data;
- saveChallenges(challenges);
- const currentChallenge = fetchNextChallenge();
-
- dispatch({ type: PEPE_FETCH_INSTANCE_SUCCESS, instance: { isReady: true, ...response.data } });
-
- dispatch({
- type: FETCH_CHALLENGES_SUCCESS,
- ageMinimum,
- currentChallenge,
- isComplete: !currentChallenge,
- });
- })
- .finally(() => dispatch({ type: SET_LOADING, value: false }));
- };
-
-/**
- * Fetch the regristration token from Pepe unless it's already been stored locally
- */
-const fetchRegistrationToken = () =>
- (dispatch: AppDispatch, getState: () => RootState) => {
- dispatch({ type: SET_LOADING });
-
- const token = fetchStoredToken();
- if (token) {
- dispatch({
- type: FETCH_TOKEN_SUCCESS,
- value: token,
- });
- return null;
- }
-
- return api(getState).post('/api/v1/pepe/registrations')
- .then(response => {
- updateStorage({ token: response.data.access_token });
-
- return dispatch({
- type: FETCH_TOKEN_SUCCESS,
- value: response.data.access_token,
- });
- })
- .finally(() => dispatch({ type: SET_LOADING, value: false }));
- };
-
-const checkEmailAvailability = (email: string) =>
- (dispatch: AppDispatch, getState: () => RootState) => {
- dispatch({ type: SET_LOADING });
-
- const token = fetchStoredToken();
-
- return api(getState).get(`/api/v1/pepe/account/exists?email=${email}`, {
- headers: { Authorization: `Bearer ${token}` },
- })
- .catch(() => {})
- .then(() => dispatch({ type: SET_LOADING, value: false }));
- };
-
-/**
- * Send the user's email to Pepe to request confirmation
- */
-const requestEmailVerification = (email: string) =>
- (dispatch: AppDispatch, getState: () => RootState) => {
- dispatch({ type: SET_LOADING });
-
- const token = fetchStoredToken();
-
- return api(getState).post('/api/v1/pepe/verify_email/request', { email }, {
- headers: { Authorization: `Bearer ${token}` },
- })
- .finally(() => dispatch({ type: SET_LOADING, value: false }));
- };
-
-const checkEmailVerification = () =>
- (dispatch: AppDispatch, getState: () => RootState) => {
- const token = fetchStoredToken();
-
- return api(getState).get('/api/v1/pepe/verify_email', {
- headers: { Authorization: `Bearer ${token}` },
- });
- };
-
-/**
- * Confirm the user's email with Pepe
- */
-const confirmEmailVerification = (emailToken: string) =>
- (dispatch: AppDispatch, getState: () => RootState) => {
- dispatch({ type: SET_LOADING });
-
- const token = fetchStoredToken();
-
- return api(getState).post('/api/v1/pepe/verify_email/confirm', { token: emailToken }, {
- headers: { Authorization: `Bearer ${token}` },
- })
- .then((response) => {
- updateStorageFromEmailConfirmation(dispatch, response.data.token);
- })
- .finally(() => dispatch({ type: SET_LOADING, value: false }));
- };
-
-const updateStorageFromEmailConfirmation = (dispatch: AppDispatch, token: string) => {
- const challengeTypes = fetchStoredChallengeTypes();
- if (!challengeTypes) {
- return;
- }
-
- const indexOfEmail = challengeTypes.indexOf('email');
- const challenges: Challenges = {};
- challengeTypes?.forEach((challengeType, idx) => {
- const value = idx <= indexOfEmail ? 1 : 0;
- challenges[challengeType] = value;
- });
-
- updateStorage({ token, challengeTypes, challenges });
- dispatchNextChallenge(dispatch);
-};
-
-const postEmailVerification = () =>
- (dispatch: AppDispatch) => {
- finishChallenge(EMAIL);
- dispatchNextChallenge(dispatch);
- };
-
-/**
- * Send the user's phone number to Pepe to request confirmation
- */
-const requestPhoneVerification = (phone: string) =>
- (dispatch: AppDispatch, getState: () => RootState) => {
- dispatch({ type: SET_LOADING });
-
- const token = fetchStoredToken();
-
- return api(getState).post('/api/v1/pepe/verify_sms/request', { phone }, {
- headers: { Authorization: `Bearer ${token}` },
- })
- .finally(() => dispatch({ type: SET_LOADING, value: false }));
- };
-
-/**
- * Send the user's phone number to Pepe to re-request confirmation
- */
-const reRequestPhoneVerification = (phone: string) =>
- (dispatch: AppDispatch, getState: () => RootState) => {
- dispatch({ type: SET_LOADING });
-
- return api(getState).post('/api/v1/pepe/reverify_sms/request', { phone })
- .finally(() => dispatch({ type: SET_LOADING, value: false }));
- };
-
-/**
- * Confirm the user's phone number with Pepe
- */
-const confirmPhoneVerification = (code: string) =>
- (dispatch: AppDispatch, getState: () => RootState) => {
- dispatch({ type: SET_LOADING });
-
- const token = fetchStoredToken();
-
- return api(getState).post('/api/v1/pepe/verify_sms/confirm', { code }, {
- headers: { Authorization: `Bearer ${token}` },
- })
- .then(() => {
- finishChallenge(SMS);
- dispatchNextChallenge(dispatch);
- })
- .finally(() => dispatch({ type: SET_LOADING, value: false }));
- };
-
-/**
- * Re-Confirm the user's phone number with Pepe
- */
-const reConfirmPhoneVerification = (code: string) =>
- (dispatch: AppDispatch, getState: () => RootState) => {
- dispatch({ type: SET_LOADING });
-
- return api(getState).post('/api/v1/pepe/reverify_sms/confirm', { code })
- .finally(() => dispatch({ type: SET_LOADING, value: false }));
- };
-
-/**
- * Confirm the user's age with Pepe
- */
-const verifyAge = (birthday: Date) =>
- (dispatch: AppDispatch, getState: () => RootState) => {
- dispatch({ type: SET_LOADING });
-
- const token = fetchStoredToken();
-
- return api(getState).post('/api/v1/pepe/verify_age/confirm', { birthday }, {
- headers: { Authorization: `Bearer ${token}` },
- })
- .then(() => {
- finishChallenge(AGE);
- dispatchNextChallenge(dispatch);
- })
- .finally(() => dispatch({ type: SET_LOADING, value: false }));
- };
-
-/**
- * Create the user's account with Pepe
- */
-const createAccount = (username: string, password: string) =>
- (dispatch: AppDispatch, getState: () => RootState) => {
- dispatch({ type: SET_LOADING });
-
- const token = fetchStoredToken();
-
- return api(getState).post('/api/v1/pepe/accounts', { username, password }, {
- headers: { Authorization: `Bearer ${token}` },
- }).finally(() => dispatch({ type: SET_LOADING, value: false }));
- };
-
-export {
- PEPE_FETCH_INSTANCE_SUCCESS,
- FETCH_CHALLENGES_SUCCESS,
- FETCH_TOKEN_SUCCESS,
- LOCAL_STORAGE_VERIFICATION_KEY,
- SET_CHALLENGES_COMPLETE,
- SET_LOADING,
- SET_NEXT_CHALLENGE,
- checkEmailAvailability,
- confirmEmailVerification,
- confirmPhoneVerification,
- createAccount,
- fetchStoredChallenges,
- fetchVerificationConfig,
- fetchRegistrationToken,
- removeStoredVerification,
- requestEmailVerification,
- checkEmailVerification,
- postEmailVerification,
- reConfirmPhoneVerification,
- requestPhoneVerification,
- reRequestPhoneVerification,
- verifyAge,
-};
diff --git a/app/soapbox/api/hooks/streaming/useNostrStream.ts b/app/soapbox/api/hooks/streaming/useNostrStream.ts
deleted file mode 100644
index 6748f95ea..000000000
--- a/app/soapbox/api/hooks/streaming/useNostrStream.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-import { useFeatures, useLoggedIn } from 'soapbox/hooks';
-
-import { useTimelineStream } from './useTimelineStream';
-
-function useNostrStream() {
- const features = useFeatures();
- const { isLoggedIn } = useLoggedIn();
-
- return useTimelineStream(
- 'nostr',
- 'nostr',
- null,
- null,
- {
- enabled: isLoggedIn && features.nostrSign && Boolean(window.nostr),
- },
- );
-}
-
-export { useNostrStream };
\ No newline at end of file
diff --git a/app/soapbox/components/ui/phone-input/country-code-dropdown.tsx b/app/soapbox/components/ui/phone-input/country-code-dropdown.tsx
deleted file mode 100644
index 208db6952..000000000
--- a/app/soapbox/components/ui/phone-input/country-code-dropdown.tsx
+++ /dev/null
@@ -1,25 +0,0 @@
-import React from 'react';
-
-import { COUNTRY_CODES, CountryCode } from 'soapbox/utils/phone';
-
-interface ICountryCodeDropdown {
- countryCode: CountryCode
- onChange(countryCode: CountryCode): void
-}
-
-/** Dropdown menu to select a country code. */
-const CountryCodeDropdown: React.FC = ({ countryCode, onChange }) => {
- return (
-
- );
-};
-
-export default CountryCodeDropdown;
diff --git a/app/soapbox/components/ui/phone-input/phone-input.tsx b/app/soapbox/components/ui/phone-input/phone-input.tsx
deleted file mode 100644
index d3072f2a1..000000000
--- a/app/soapbox/components/ui/phone-input/phone-input.tsx
+++ /dev/null
@@ -1,81 +0,0 @@
-import { parsePhoneNumber, AsYouType } from 'libphonenumber-js';
-import React, { useState, useEffect } from 'react';
-
-import { CountryCode } from 'soapbox/utils/phone';
-
-import Input from '../input/input';
-
-import CountryCodeDropdown from './country-code-dropdown';
-
-interface IPhoneInput extends Pick, 'required' | 'autoFocus'> {
- /** E164 phone number. */
- value?: string
- /** Change handler which receives the E164 phone string. */
- onChange?: (phone: string | undefined) => void
- /** Country code that's selected on mount. */
- defaultCountryCode?: CountryCode
-}
-
-/** Internationalized phone input with country code picker. */
-const PhoneInput: React.FC = (props) => {
- const { value, onChange, defaultCountryCode = '1', ...rest } = props;
-
- const [countryCode, setCountryCode] = useState(defaultCountryCode);
- const [nationalNumber, setNationalNumber] = useState('');
-
- const handleChange: React.ChangeEventHandler = ({ target }) => {
- // HACK: AsYouType is not meant to be used this way. But it works!
- const asYouType = new AsYouType({ defaultCallingCode: countryCode });
- const formatted = asYouType.input(target.value);
-
- // If the new value is the same as before, we might be backspacing,
- // so use the actual event value instead of the formatted value.
- if (formatted === nationalNumber && target.value !== nationalNumber) {
- setNationalNumber(target.value);
- } else {
- setNationalNumber(formatted);
- }
- };
-
- // When the internal state changes, update the external state.
- useEffect(() => {
- if (onChange) {
- try {
- const opts = { defaultCallingCode: countryCode, extract: false } as any;
- const result = parsePhoneNumber(nationalNumber, opts);
-
- // Throw if the number is invalid, but catch it below.
- // We'll only ever call `onChange` with a valid E164 string or `undefined`.
- if (!result.isPossible()) {
- throw result;
- }
-
- onChange(result.format('E.164'));
- } catch (e) {
- // The value returned is always a valid E164 string.
- // If it's not valid, it'll return undefined.
- onChange(undefined);
- }
- }
- }, [countryCode, nationalNumber]);
-
- useEffect(() => {
- handleChange({ target: { value: nationalNumber } } as any);
- }, [countryCode, nationalNumber]);
-
- return (
-
- }
- {...rest}
- />
- );
-};
-
-export default PhoneInput;
diff --git a/app/soapbox/features/ads/components/ad.tsx b/app/soapbox/features/ads/components/ad.tsx
deleted file mode 100644
index 4636795bc..000000000
--- a/app/soapbox/features/ads/components/ad.tsx
+++ /dev/null
@@ -1,142 +0,0 @@
-import { useQuery, useQueryClient } from '@tanstack/react-query';
-import axios from 'axios';
-import React, { useState, useEffect, useRef } from 'react';
-import { FormattedMessage } from 'react-intl';
-
-import { Avatar, Card, HStack, Icon, IconButton, Stack, Text } from 'soapbox/components/ui';
-import StatusCard from 'soapbox/features/status/components/card';
-import { useInstance } from 'soapbox/hooks';
-import { AdKeys } from 'soapbox/queries/ads';
-
-import type { Ad as AdEntity } from 'soapbox/types/soapbox';
-
-interface IAd {
- ad: AdEntity
-}
-
-/** Displays an ad in sponsored post format. */
-const Ad: React.FC = ({ ad }) => {
- const queryClient = useQueryClient();
- const instance = useInstance();
-
- const timer = useRef(undefined);
- const infobox = useRef(null);
- const [showInfo, setShowInfo] = useState(false);
-
- // Fetch the impression URL (if any) upon displaying the ad.
- // Don't fetch it more than once.
- useQuery(['ads', 'impression', ad.impression], async () => {
- if (ad.impression) {
- return await axios.get(ad.impression);
- }
- }, { cacheTime: Infinity, staleTime: Infinity });
-
- /** Invalidate query cache for ads. */
- const bustCache = (): void => {
- queryClient.invalidateQueries(AdKeys.ads);
- };
-
- /** Toggle the info box on click. */
- const handleInfoButtonClick: React.MouseEventHandler = () => {
- setShowInfo(!showInfo);
- };
-
- /** Hide the info box when clicked outside. */
- const handleClickOutside = (event: MouseEvent) => {
- if (event.target && infobox.current && !infobox.current.contains(event.target as any)) {
- setShowInfo(false);
- }
- };
-
- // Hide the info box when clicked outside.
- // https://stackoverflow.com/a/42234988
- useEffect(() => {
- document.addEventListener('mousedown', handleClickOutside);
- return () => {
- document.removeEventListener('mousedown', handleClickOutside);
- };
- }, [infobox]);
-
- // Wait until the ad expires, then invalidate cache.
- useEffect(() => {
- if (ad.expires_at) {
- const delta = new Date(ad.expires_at).getTime() - (new Date()).getTime();
- timer.current = setTimeout(bustCache, delta);
- }
-
- return () => {
- if (timer.current) {
- clearTimeout(timer.current);
- }
- };
- }, [ad.expires_at]);
-
- return (
-
-
-
-
-
-
-
-
-
- {instance.title}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- { }} horizontal />
-
-
-
- {showInfo && (
-
-
-
-
-
-
-
-
- {ad.reason ? (
- ad.reason
- ) : (
-
- )}
-
-
-
-
- )}
-
- );
-};
-
-export default Ad;
diff --git a/app/soapbox/features/ads/providers/index.ts b/app/soapbox/features/ads/providers/index.ts
deleted file mode 100644
index 63067b81d..000000000
--- a/app/soapbox/features/ads/providers/index.ts
+++ /dev/null
@@ -1,42 +0,0 @@
-import { getSoapboxConfig } from 'soapbox/actions/soapbox';
-
-import type { RootState } from 'soapbox/store';
-import type { Card } from 'soapbox/types/entities';
-
-/** Map of available provider modules. */
-const PROVIDERS: Record Promise> = {
- soapbox: async() => (await import(/* webpackChunkName: "features/ads/soapbox" */'./soapbox-config')).default,
- truth: async() => (await import(/* webpackChunkName: "features/ads/truth" */'./truth')).default,
-};
-
-/** Ad server implementation. */
-interface AdProvider {
- getAds(getState: () => RootState): Promise
-}
-
-/** Entity representing an advertisement. */
-interface Ad {
- /** Ad data in Card (OEmbed-ish) format. */
- card: Card
- /** Impression URL to fetch when displaying the ad. */
- impression?: string
- /** Time when the ad expires and should no longer be displayed. */
- expires_at?: string
- /** Reason the ad is displayed. */
- reason?: string
-}
-
-/** Gets the current provider based on config. */
-const getProvider = async(getState: () => RootState): Promise => {
- const state = getState();
- const soapboxConfig = getSoapboxConfig(state);
- const isEnabled = soapboxConfig.extensions.getIn(['ads', 'enabled'], false) === true;
- const providerName = soapboxConfig.extensions.getIn(['ads', 'provider'], 'soapbox') as string;
-
- if (isEnabled && PROVIDERS[providerName]) {
- return PROVIDERS[providerName]();
- }
-};
-
-export { getProvider };
-export type { Ad, AdProvider };
diff --git a/app/soapbox/features/ads/providers/soapbox-config.ts b/app/soapbox/features/ads/providers/soapbox-config.ts
deleted file mode 100644
index 21163729c..000000000
--- a/app/soapbox/features/ads/providers/soapbox-config.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-import { getSoapboxConfig } from 'soapbox/actions/soapbox';
-
-import type { AdProvider } from '.';
-
-/** Provides ads from Soapbox Config. */
-const SoapboxConfigAdProvider: AdProvider = {
- getAds: async(getState) => {
- const state = getState();
- const soapboxConfig = getSoapboxConfig(state);
- return soapboxConfig.ads.toArray();
- },
-};
-
-export default SoapboxConfigAdProvider;
diff --git a/app/soapbox/features/ads/providers/truth.ts b/app/soapbox/features/ads/providers/truth.ts
deleted file mode 100644
index 5582bd3cf..000000000
--- a/app/soapbox/features/ads/providers/truth.ts
+++ /dev/null
@@ -1,40 +0,0 @@
-import axios from 'axios';
-import { z } from 'zod';
-
-import { getSettings } from 'soapbox/actions/settings';
-import { cardSchema } from 'soapbox/schemas/card';
-import { filteredArray } from 'soapbox/schemas/utils';
-
-import type { AdProvider } from '.';
-
-/** TruthSocial ad API entity. */
-const truthAdSchema = z.object({
- impression: z.string(),
- card: cardSchema,
- expires_at: z.string(),
- reason: z.string().catch(''),
-});
-
-/** Provides ads from the TruthSocial API. */
-const TruthAdProvider: AdProvider = {
- getAds: async(getState) => {
- const state = getState();
- const settings = getSettings(state);
-
- try {
- const { data } = await axios.get('/api/v2/truth/ads?device=desktop', {
- headers: {
- 'Accept-Language': z.string().catch('*').parse(settings.get('locale')),
- },
- });
-
- return filteredArray(truthAdSchema).parse(data);
- } catch (e) {
- // do nothing
- }
-
- return [];
- },
-};
-
-export default TruthAdProvider;
diff --git a/app/soapbox/features/auth-layout/index.tsx b/app/soapbox/features/auth-layout/index.tsx
deleted file mode 100644
index 9ad78c184..000000000
--- a/app/soapbox/features/auth-layout/index.tsx
+++ /dev/null
@@ -1,95 +0,0 @@
-import React from 'react';
-import { defineMessages, useIntl } from 'react-intl';
-import { Link, Redirect, Route, Switch, useHistory, useLocation } from 'react-router-dom';
-
-import LandingGradient from 'soapbox/components/landing-gradient';
-import SiteLogo from 'soapbox/components/site-logo';
-import { useOwnAccount, useInstance, useRegistrationStatus } from 'soapbox/hooks';
-
-import { Button, Card, CardBody } from '../../components/ui';
-import LoginPage from '../auth-login/components/login-page';
-import PasswordReset from '../auth-login/components/password-reset';
-import PasswordResetConfirm from '../auth-login/components/password-reset-confirm';
-import RegistrationForm from '../auth-login/components/registration-form';
-import ExternalLoginForm from '../external-login/components/external-login-form';
-import Footer from '../public-layout/components/footer';
-import RegisterInvite from '../register-invite';
-import Verification from '../verification';
-import EmailPassthru from '../verification/email-passthru';
-
-const messages = defineMessages({
- register: { id: 'auth_layout.register', defaultMessage: 'Create an account' },
-});
-
-const AuthLayout = () => {
- const intl = useIntl();
- const history = useHistory();
- const { search } = useLocation();
-
- const { account } = useOwnAccount();
- const instance = useInstance();
- const { isOpen } = useRegistrationStatus();
- const isLoginPage = history.location.pathname === '/login';
-
- return (
-
-
-
-
-
-
-
-
-
-
-
-
- {/* If already logged in, redirect home. */}
- {account && }
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- );
-};
-
-export default AuthLayout;
diff --git a/app/soapbox/features/auth-login/components/login-form.tsx b/app/soapbox/features/auth-login/components/login-form.tsx
deleted file mode 100644
index d7871fc75..000000000
--- a/app/soapbox/features/auth-login/components/login-form.tsx
+++ /dev/null
@@ -1,97 +0,0 @@
-import React from 'react';
-import { FormattedMessage, defineMessages, useIntl } from 'react-intl';
-import { Link } from 'react-router-dom';
-
-import { Button, Form, FormActions, FormGroup, Input, Stack } from 'soapbox/components/ui';
-import { useFeatures } from 'soapbox/hooks';
-
-import ConsumersList from './consumers-list';
-
-const messages = defineMessages({
- username: {
- id: 'login.fields.username_label',
- defaultMessage: 'E-mail or username',
- },
- email: {
- id: 'login.fields.email_label',
- defaultMessage: 'E-mail address',
- },
- password: {
- id: 'login.fields.password_placeholder',
- defaultMessage: 'Password',
- },
-});
-
-interface ILoginForm {
- isLoading: boolean
- handleSubmit: React.FormEventHandler
-}
-
-const LoginForm: React.FC = ({ isLoading, handleSubmit }) => {
- const intl = useIntl();
- const features = useFeatures();
-
- const usernameLabel = intl.formatMessage(features.logInWithUsername ? messages.username : messages.email);
- const passwordLabel = intl.formatMessage(messages.password);
-
- return (
-
- );
-};
-
-export default LoginForm;
diff --git a/app/soapbox/features/edit-password/index.tsx b/app/soapbox/features/edit-password/index.tsx
deleted file mode 100644
index eb6b1c100..000000000
--- a/app/soapbox/features/edit-password/index.tsx
+++ /dev/null
@@ -1,113 +0,0 @@
-import React from 'react';
-import { defineMessages, useIntl } from 'react-intl';
-
-import { changePassword } from 'soapbox/actions/security';
-import { Button, Card, CardBody, CardHeader, CardTitle, Column, Form, FormActions, FormGroup, Input } from 'soapbox/components/ui';
-import { useAppDispatch, useFeatures } from 'soapbox/hooks';
-import toast from 'soapbox/toast';
-
-import PasswordIndicator from '../verification/components/password-indicator';
-
-const messages = defineMessages({
- updatePasswordSuccess: { id: 'security.update_password.success', defaultMessage: 'Password successfully updated.' },
- updatePasswordFail: { id: 'security.update_password.fail', defaultMessage: 'Update password failed.' },
- oldPasswordFieldLabel: { id: 'security.fields.old_password.label', defaultMessage: 'Current password' },
- newPasswordFieldLabel: { id: 'security.fields.new_password.label', defaultMessage: 'New password' },
- confirmationFieldLabel: { id: 'security.fields.password_confirmation.label', defaultMessage: 'New password (again)' },
- header: { id: 'edit_password.header', defaultMessage: 'Change Password' },
- submit: { id: 'security.submit', defaultMessage: 'Save changes' },
- cancel: { id: 'common.cancel', defaultMessage: 'Cancel' },
-});
-
-const initialState = { currentPassword: '', newPassword: '', newPasswordConfirmation: '' };
-
-const EditPassword = () => {
- const intl = useIntl();
- const dispatch = useAppDispatch();
- const { passwordRequirements } = useFeatures();
-
- const [state, setState] = React.useState(initialState);
- const [isLoading, setLoading] = React.useState(false);
- const [hasValidPassword, setHasValidPassword] = React.useState(passwordRequirements ? false : true);
-
- const { currentPassword, newPassword, newPasswordConfirmation } = state;
-
- const resetState = () => setState(initialState);
-
- const handleInputChange: React.ChangeEventHandler = React.useCallback((event) => {
- event.persist();
-
- setState((prevState) => ({ ...prevState, [event.target.name]: event.target.value }));
- }, []);
-
- const handleSubmit = React.useCallback(() => {
- setLoading(true);
- dispatch(changePassword(currentPassword, newPassword, newPasswordConfirmation)).then(() => {
- resetState();
- toast.success(intl.formatMessage(messages.updatePasswordSuccess));
-
- }).finally(() => {
- setLoading(false);
- }).catch(() => {
- resetState();
- toast.error(intl.formatMessage(messages.updatePasswordFail));
- });
- }, [currentPassword, newPassword, newPasswordConfirmation, dispatch, intl]);
-
- return (
-
-
-
-
-
-
-
-
-
-
-
- );
-};
-
-export default EditPassword;
diff --git a/app/soapbox/features/external-login/index.tsx b/app/soapbox/features/external-login/index.tsx
deleted file mode 100644
index 510014dc2..000000000
--- a/app/soapbox/features/external-login/index.tsx
+++ /dev/null
@@ -1,10 +0,0 @@
-import React from 'react';
-
-import ExternalLoginForm from './components/external-login-form';
-
-/** Page for logging into a remote instance */
-const ExternalLoginPage: React.FC = () => {
- return ;
-};
-
-export default ExternalLoginPage;
diff --git a/app/soapbox/features/landing-page/__tests__/landing-page.test.tsx b/app/soapbox/features/landing-page/__tests__/landing-page.test.tsx
deleted file mode 100644
index 6e96672ba..000000000
--- a/app/soapbox/features/landing-page/__tests__/landing-page.test.tsx
+++ /dev/null
@@ -1,90 +0,0 @@
-import React from 'react';
-
-import LandingPage from '..';
-import { rememberInstance } from '../../../actions/instance';
-import { SOAPBOX_CONFIG_REMEMBER_SUCCESS } from '../../../actions/soapbox';
-import { PEPE_FETCH_INSTANCE_SUCCESS } from '../../../actions/verification';
-import { render, screen, rootReducer, applyActions } from '../../../jest/test-helpers';
-
-describe('', () => {
- it('renders a RegistrationForm for an open Pleroma instance', () => {
-
- const state = rootReducer(undefined, {
- type: rememberInstance.fulfilled.type,
- payload: {
- version: '2.7.2 (compatible; Pleroma 2.3.0)',
- registrations: true,
- },
- });
-
- render(, undefined, state);
-
- expect(screen.queryByTestId('registrations-open')).toBeInTheDocument();
- expect(screen.queryByTestId('registrations-closed')).not.toBeInTheDocument();
- expect(screen.queryByTestId('registrations-pepe')).not.toBeInTheDocument();
- });
-
- it('renders "closed" message for a closed Pleroma instance', () => {
-
- const state = rootReducer(undefined, {
- type: rememberInstance.fulfilled.type,
- payload: {
- version: '2.7.2 (compatible; Pleroma 2.3.0)',
- registrations: false,
- },
- });
-
- render(, undefined, state);
-
- expect(screen.queryByTestId('registrations-closed')).toBeInTheDocument();
- expect(screen.queryByTestId('registrations-open')).not.toBeInTheDocument();
- expect(screen.queryByTestId('registrations-pepe')).not.toBeInTheDocument();
- });
-
- it('renders Pepe flow if Pepe extension is enabled', () => {
-
- const state = applyActions(undefined, [{
- type: SOAPBOX_CONFIG_REMEMBER_SUCCESS,
- soapboxConfig: {
- extensions: {
- pepe: {
- enabled: true,
- },
- },
- },
- }, {
- type: PEPE_FETCH_INSTANCE_SUCCESS,
- instance: {
- registrations: true,
- },
- }], rootReducer);
-
- render(, undefined, state);
-
- expect(screen.queryByTestId('registrations-pepe')).toBeInTheDocument();
- expect(screen.queryByTestId('registrations-open')).not.toBeInTheDocument();
- expect(screen.queryByTestId('registrations-closed')).not.toBeInTheDocument();
- });
-
- it('renders "closed" message for a Truth Social instance with Pepe closed', () => {
-
- const state = applyActions(undefined, [{
- type: rememberInstance.fulfilled.type,
- payload: {
- version: '3.4.1 (compatible; TruthSocial 1.0.0)',
- registrations: false,
- },
- }, {
- type: PEPE_FETCH_INSTANCE_SUCCESS,
- instance: {
- registrations: false,
- },
- }], rootReducer);
-
- render(, undefined, state);
-
- expect(screen.queryByTestId('registrations-closed')).toBeInTheDocument();
- expect(screen.queryByTestId('registrations-pepe')).not.toBeInTheDocument();
- expect(screen.queryByTestId('registrations-open')).not.toBeInTheDocument();
- });
-});
diff --git a/app/soapbox/features/onboarding/steps/avatar-selection-step.tsx b/app/soapbox/features/onboarding/steps/avatar-selection-step.tsx
deleted file mode 100644
index 55a54c514..000000000
--- a/app/soapbox/features/onboarding/steps/avatar-selection-step.tsx
+++ /dev/null
@@ -1,133 +0,0 @@
-import clsx from 'clsx';
-import React from 'react';
-import { defineMessages, FormattedMessage } from 'react-intl';
-
-import { patchMe } from 'soapbox/actions/me';
-import { Avatar, Button, Card, CardBody, Icon, Spinner, Stack, Text } from 'soapbox/components/ui';
-import { useAppDispatch, useOwnAccount } from 'soapbox/hooks';
-import toast from 'soapbox/toast';
-import { isDefaultAvatar } from 'soapbox/utils/accounts';
-import resizeImage from 'soapbox/utils/resize-image';
-
-import type { AxiosError } from 'axios';
-
-const messages = defineMessages({
- error: { id: 'onboarding.error', defaultMessage: 'An unexpected error occurred. Please try again or skip this step.' },
-});
-
-const AvatarSelectionStep = ({ onNext }: { onNext: () => void }) => {
- const dispatch = useAppDispatch();
- const { account } = useOwnAccount();
-
- const fileInput = React.useRef(null);
- const [selectedFile, setSelectedFile] = React.useState();
- const [isSubmitting, setSubmitting] = React.useState(false);
- const [isDisabled, setDisabled] = React.useState(true);
- const isDefault = account ? isDefaultAvatar(account.avatar) : false;
-
- const openFilePicker = () => {
- fileInput.current?.click();
- };
-
- const handleFileChange = (event: React.ChangeEvent) => {
- const maxPixels = 400 * 400;
- const rawFile = event.target.files?.item(0);
-
- if (!rawFile) return;
-
- resizeImage(rawFile, maxPixels).then((file) => {
- const url = file ? URL.createObjectURL(file) : account?.avatar as string;
-
- setSelectedFile(url);
- setSubmitting(true);
-
- const formData = new FormData();
- formData.append('avatar', rawFile);
- const credentials = dispatch(patchMe(formData));
-
- Promise.all([credentials]).then(() => {
- setDisabled(false);
- setSubmitting(false);
- onNext();
- }).catch((error: AxiosError) => {
- setSubmitting(false);
- setDisabled(false);
- setSelectedFile(null);
-
- if (error.response?.status === 422) {
- toast.error((error.response.data as any).error.replace('Validation failed: ', ''));
- } else {
- toast.error(messages.error);
- }
- });
- }).catch(console.error);
- };
-
- return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {account && (
-
- )}
-
- {isSubmitting && (
-
-
-
- )}
-
-
-
-
-
-
-
-
-
- {isDisabled && (
-
- )}
-
-
-
-
-
-
- );
-};
-
-export default AvatarSelectionStep;
diff --git a/app/soapbox/features/onboarding/steps/bio-step.tsx b/app/soapbox/features/onboarding/steps/bio-step.tsx
deleted file mode 100644
index c0a029d80..000000000
--- a/app/soapbox/features/onboarding/steps/bio-step.tsx
+++ /dev/null
@@ -1,105 +0,0 @@
-import React from 'react';
-import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
-
-import { patchMe } from 'soapbox/actions/me';
-import { Button, Card, CardBody, FormGroup, Stack, Text, Textarea } from 'soapbox/components/ui';
-import { useAppDispatch, useOwnAccount } from 'soapbox/hooks';
-import toast from 'soapbox/toast';
-
-import type { AxiosError } from 'axios';
-
-const messages = defineMessages({
- bioPlaceholder: { id: 'onboarding.bio.placeholder', defaultMessage: 'Tell the world a little about yourself…' },
- error: { id: 'onboarding.error', defaultMessage: 'An unexpected error occurred. Please try again or skip this step.' },
-});
-
-const BioStep = ({ onNext }: { onNext: () => void }) => {
- const intl = useIntl();
- const dispatch = useAppDispatch();
-
- const { account } = useOwnAccount();
- const [value, setValue] = React.useState(account?.source?.note ?? '');
- const [isSubmitting, setSubmitting] = React.useState(false);
- const [errors, setErrors] = React.useState([]);
-
- const handleSubmit = () => {
- setSubmitting(true);
-
- const credentials = dispatch(patchMe({ note: value }));
-
- Promise.all([credentials])
- .then(() => {
- setSubmitting(false);
- onNext();
- }).catch((error: AxiosError) => {
- setSubmitting(false);
-
- if (error.response?.status === 422) {
- setErrors([(error.response.data as any).error.replace('Validation failed: ', '')]);
- } else {
- toast.error(messages.error);
- }
- });
- };
-
- return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- }
- labelText={}
- errors={errors}
- >
-
-
-
-
-
-
-
-
-
-
-
-
-
- );
-};
-
-export default BioStep;
diff --git a/app/soapbox/features/onboarding/steps/cover-photo-selection-step.tsx b/app/soapbox/features/onboarding/steps/cover-photo-selection-step.tsx
deleted file mode 100644
index 5e9c314f8..000000000
--- a/app/soapbox/features/onboarding/steps/cover-photo-selection-step.tsx
+++ /dev/null
@@ -1,156 +0,0 @@
-import clsx from 'clsx';
-import React from 'react';
-import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
-
-import { patchMe } from 'soapbox/actions/me';
-import StillImage from 'soapbox/components/still-image';
-import { Avatar, Button, Card, CardBody, Icon, Spinner, Stack, Text } from 'soapbox/components/ui';
-import { useAppDispatch, useOwnAccount } from 'soapbox/hooks';
-import toast from 'soapbox/toast';
-import { isDefaultHeader } from 'soapbox/utils/accounts';
-import resizeImage from 'soapbox/utils/resize-image';
-
-import type { AxiosError } from 'axios';
-
-const messages = defineMessages({
- header: { id: 'account.header.alt', defaultMessage: 'Profile header' },
- error: { id: 'onboarding.error', defaultMessage: 'An unexpected error occurred. Please try again or skip this step.' },
-});
-
-const CoverPhotoSelectionStep = ({ onNext }: { onNext: () => void }) => {
- const intl = useIntl();
- const dispatch = useAppDispatch();
- const { account } = useOwnAccount();
-
- const fileInput = React.useRef(null);
- const [selectedFile, setSelectedFile] = React.useState();
- const [isSubmitting, setSubmitting] = React.useState(false);
- const [isDisabled, setDisabled] = React.useState(true);
- const isDefault = account ? isDefaultHeader(account.header) : false;
-
- const openFilePicker = () => {
- fileInput.current?.click();
- };
-
- const handleFileChange = (event: React.ChangeEvent) => {
- const maxPixels = 1920 * 1080;
- const rawFile = event.target.files?.item(0);
-
- if (!rawFile) return;
-
- resizeImage(rawFile, maxPixels).then((file) => {
- const url = file ? URL.createObjectURL(file) : account?.header as string;
-
- setSelectedFile(url);
- setSubmitting(true);
-
- const formData = new FormData();
- formData.append('header', file);
- const credentials = dispatch(patchMe(formData));
-
- Promise.all([credentials]).then(() => {
- setDisabled(false);
- setSubmitting(false);
- onNext();
- }).catch((error: AxiosError) => {
- setSubmitting(false);
- setDisabled(false);
- setSelectedFile(null);
-
- if (error.response?.status === 422) {
- toast.error((error.response.data as any).error.replace('Validation failed: ', ''));
- } else {
- toast.error(messages.error);
- }
- });
- }).catch(console.error);
- };
-
- return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {selectedFile || account?.header && (
-
- )}
-
- {isSubmitting && (
-
-
-
- )}
-
-
-
-
-
-
-
- {account && (
-
- )}
-
-
{account?.display_name}
-
@{account?.username}
-
-
-
-
-
-
- {isDisabled && (
-
- )}
-
-
-
-
-
-
- );
-};
-
-export default CoverPhotoSelectionStep;
diff --git a/app/soapbox/features/onboarding/steps/display-name-step.tsx b/app/soapbox/features/onboarding/steps/display-name-step.tsx
deleted file mode 100644
index f9916396f..000000000
--- a/app/soapbox/features/onboarding/steps/display-name-step.tsx
+++ /dev/null
@@ -1,115 +0,0 @@
-import React from 'react';
-import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
-
-import { patchMe } from 'soapbox/actions/me';
-import { Button, Card, CardBody, FormGroup, Input, Stack, Text } from 'soapbox/components/ui';
-import { useAppDispatch, useOwnAccount } from 'soapbox/hooks';
-import toast from 'soapbox/toast';
-
-import type { AxiosError } from 'axios';
-
-const messages = defineMessages({
- usernamePlaceholder: { id: 'onboarding.display_name.placeholder', defaultMessage: 'Eg. John Smith' },
- error: { id: 'onboarding.error', defaultMessage: 'An unexpected error occurred. Please try again or skip this step.' },
-});
-
-const DisplayNameStep = ({ onNext }: { onNext: () => void }) => {
- const intl = useIntl();
- const dispatch = useAppDispatch();
-
- const { account } = useOwnAccount();
- const [value, setValue] = React.useState(account?.display_name || '');
- const [isSubmitting, setSubmitting] = React.useState(false);
- const [errors, setErrors] = React.useState([]);
-
- const trimmedValue = value.trim();
- const isValid = trimmedValue.length > 0;
- const isDisabled = !isValid || value.length > 30;
-
- const hintText = React.useMemo(() => {
- const charsLeft = 30 - value.length;
- const suffix = charsLeft === 1 ? 'character remaining' : 'characters remaining';
-
- return `${charsLeft} ${suffix}`;
- }, [value]);
-
- const handleSubmit = () => {
- setSubmitting(true);
-
- const credentials = dispatch(patchMe({ display_name: value }));
-
- Promise.all([credentials])
- .then(() => {
- setSubmitting(false);
- onNext();
- }).catch((error: AxiosError) => {
- setSubmitting(false);
-
- if (error.response?.status === 422) {
- setErrors([(error.response.data as any).error.replace('Validation failed: ', '')]);
- } else {
- toast.error(messages.error);
- }
- });
- };
-
- return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- }
- errors={errors}
- >
- setValue(event.target.value)}
- placeholder={intl.formatMessage(messages.usernamePlaceholder)}
- type='text'
- value={value}
- maxLength={30}
- />
-
-
-
-
-
-
-
-
-
-
-
-
- );
-};
-
-export default DisplayNameStep;
diff --git a/app/soapbox/features/timeline-insertion/__tests__/abovefold.test.ts b/app/soapbox/features/timeline-insertion/__tests__/abovefold.test.ts
deleted file mode 100644
index 81de8c1a4..000000000
--- a/app/soapbox/features/timeline-insertion/__tests__/abovefold.test.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-import { abovefoldAlgorithm } from '../abovefold';
-
-const DATA = Object.freeze(['a', 'b', 'c', 'd']);
-
-test('abovefoldAlgorithm', () => {
- const result = Array(50).fill('').map((_, i) => {
- return abovefoldAlgorithm(DATA, i, { seed: '!', range: [2, 6], pageSize: 20 });
- });
-
- // console.log(result);
- expect(result[0]).toBe(undefined);
- expect(result[4]).toBe('a');
- expect(result[5]).toBe(undefined);
- expect(result[24]).toBe('b');
- expect(result[30]).toBe(undefined);
- expect(result[42]).toBe('c');
- expect(result[43]).toBe(undefined);
-});
\ No newline at end of file
diff --git a/app/soapbox/features/timeline-insertion/__tests__/linear.test.ts b/app/soapbox/features/timeline-insertion/__tests__/linear.test.ts
deleted file mode 100644
index 09d484f12..000000000
--- a/app/soapbox/features/timeline-insertion/__tests__/linear.test.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-import { linearAlgorithm } from '../linear';
-
-const DATA = Object.freeze(['a', 'b', 'c', 'd']);
-
-test('linearAlgorithm', () => {
- const result = Array(50).fill('').map((_, i) => {
- return linearAlgorithm(DATA, i, { interval: 5 });
- });
-
- // console.log(result);
- expect(result[0]).toBe(undefined);
- expect(result[4]).toBe('a');
- expect(result[8]).toBe(undefined);
- expect(result[9]).toBe('b');
- expect(result[10]).toBe(undefined);
- expect(result[14]).toBe('c');
- expect(result[15]).toBe(undefined);
- expect(result[19]).toBe('d');
-});
\ No newline at end of file
diff --git a/app/soapbox/features/timeline-insertion/abovefold.ts b/app/soapbox/features/timeline-insertion/abovefold.ts
deleted file mode 100644
index 5298f029c..000000000
--- a/app/soapbox/features/timeline-insertion/abovefold.ts
+++ /dev/null
@@ -1,52 +0,0 @@
-import seedrandom from 'seedrandom';
-
-import type { PickAlgorithm } from './types';
-
-type Opts = {
- /** Randomization seed. */
- seed: string
- /**
- * Start/end index of the slot by which one item will be randomly picked per page.
- *
- * Eg. `[2, 6]` will cause one item to be picked among the third through seventh indexes.
- *
- * `end` must be larger than `start`.
- */
- range: [start: number, end: number]
- /** Number of items in the page. */
- pageSize: number
-};
-
-/**
- * Algorithm to display items per-page.
- * One item is randomly inserted into each page within the index range.
- */
-const abovefoldAlgorithm: PickAlgorithm = (items, iteration, rawOpts) => {
- const opts = normalizeOpts(rawOpts);
- /** Current page of the index. */
- const page = Math.floor(iteration / opts.pageSize);
- /** Current index within the page. */
- const pageIndex = (iteration % opts.pageSize);
- /** RNG for the page. */
- const rng = seedrandom(`${opts.seed}-page-${page}`);
- /** Index to insert the item. */
- const insertIndex = Math.floor(rng() * (opts.range[1] - opts.range[0])) + opts.range[0];
-
- if (pageIndex === insertIndex) {
- return items[page % items.length];
- }
-};
-
-const normalizeOpts = (opts: unknown): Opts => {
- const { seed, range, pageSize } = (opts && typeof opts === 'object' ? opts : {}) as Record;
-
- return {
- seed: typeof seed === 'string' ? seed : '',
- range: Array.isArray(range) ? [Number(range[0]), Number(range[1])] : [2, 6],
- pageSize: typeof pageSize === 'number' ? pageSize : 20,
- };
-};
-
-export {
- abovefoldAlgorithm,
-};
diff --git a/app/soapbox/features/timeline-insertion/index.ts b/app/soapbox/features/timeline-insertion/index.ts
deleted file mode 100644
index f4e00ed29..000000000
--- a/app/soapbox/features/timeline-insertion/index.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-import { abovefoldAlgorithm } from './abovefold';
-import { linearAlgorithm } from './linear';
-
-import type { PickAlgorithm } from './types';
-
-const ALGORITHMS: Record = {
- 'linear': linearAlgorithm,
- 'abovefold': abovefoldAlgorithm,
-};
-
-export { ALGORITHMS };
\ No newline at end of file
diff --git a/app/soapbox/features/timeline-insertion/linear.ts b/app/soapbox/features/timeline-insertion/linear.ts
deleted file mode 100644
index cae923944..000000000
--- a/app/soapbox/features/timeline-insertion/linear.ts
+++ /dev/null
@@ -1,28 +0,0 @@
-import type { PickAlgorithm } from './types';
-
-type Opts = {
- /** Number of iterations until the next item is picked. */
- interval: number
-};
-
-/** Picks the next item every iteration. */
-const linearAlgorithm: PickAlgorithm = (items, iteration, rawOpts) => {
- const opts = normalizeOpts(rawOpts);
- const itemIndex = items ? Math.floor(iteration / opts.interval) % items.length : 0;
- const item = items ? items[itemIndex] : undefined;
- const showItem = (iteration + 1) % opts.interval === 0;
-
- return showItem ? item : undefined;
-};
-
-const normalizeOpts = (opts: unknown): Opts => {
- const { interval } = (opts && typeof opts === 'object' ? opts : {}) as Record;
-
- return {
- interval: typeof interval === 'number' ? interval : 20,
- };
-};
-
-export {
- linearAlgorithm,
-};
\ No newline at end of file
diff --git a/app/soapbox/features/timeline-insertion/types.ts b/app/soapbox/features/timeline-insertion/types.ts
deleted file mode 100644
index b874754d0..000000000
--- a/app/soapbox/features/timeline-insertion/types.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-/**
- * Returns an item to insert at the index, or `undefined` if an item shouldn't be inserted.
- */
-type PickAlgorithm = (
- /** Elligible candidates to pick. */
- items: readonly D[],
- /** Current iteration by which an item may be chosen. */
- iteration: number,
- /** Implementation-specific opts. */
- opts: Record
-) => D | undefined;
-
-export {
- PickAlgorithm,
-};
\ No newline at end of file
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/components/modals/verify-sms-modal.tsx b/app/soapbox/features/ui/components/modals/verify-sms-modal.tsx
deleted file mode 100644
index b092763a4..000000000
--- a/app/soapbox/features/ui/components/modals/verify-sms-modal.tsx
+++ /dev/null
@@ -1,228 +0,0 @@
-import React, { useCallback, useEffect, useMemo, useState } from 'react';
-import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
-import OtpInput from 'react-otp-input';
-
-import { verifyCredentials } from 'soapbox/actions/auth';
-import { closeModal } from 'soapbox/actions/modals';
-import { reConfirmPhoneVerification, reRequestPhoneVerification } from 'soapbox/actions/verification';
-import { FormGroup, PhoneInput, Modal, Stack, Text } from 'soapbox/components/ui';
-import { useAppDispatch, useAppSelector, useInstance } from 'soapbox/hooks';
-import toast from 'soapbox/toast';
-import { getAccessToken } from 'soapbox/utils/auth';
-
-const messages = defineMessages({
- verificationInvalid: {
- id: 'sms_verification.invalid',
- defaultMessage: 'Please enter a valid phone number.',
- },
- verificationSuccess: {
- id: 'sms_verification.success',
- defaultMessage: 'A verification code has been sent to your phone number.',
- },
- verificationFail: {
- id: 'sms_verification.fail',
- defaultMessage: 'Failed to send SMS message to your phone number.',
- },
- verificationExpired: {
- id: 'sms_verification.expired',
- defaultMessage: 'Your SMS token has expired.',
- },
- verifySms: {
- id: 'sms_verification.modal.verify_sms',
- defaultMessage: 'Verify SMS',
- },
- verifyNumber: {
- id: 'sms_verification.modal.verify_number',
- defaultMessage: 'Verify phone number',
- },
- verifyCode: {
- id: 'sms_verification.modal.verify_code',
- defaultMessage: 'Verify code',
- },
-});
-
-interface IVerifySmsModal {
- onClose: (type: string) => void
-}
-
-enum Statuses {
- IDLE = 'IDLE',
- READY = 'READY',
- REQUESTED = 'REQUESTED',
- FAIL = 'FAIL',
- SUCCESS = 'SUCCESS',
-}
-
-const VerifySmsModal: React.FC = ({ onClose }) => {
- const dispatch = useAppDispatch();
- const intl = useIntl();
- const instance = useInstance();
- const accessToken = useAppSelector((state) => getAccessToken(state));
- const isLoading = useAppSelector((state) => state.verification.isLoading);
-
- const [status, setStatus] = useState(Statuses.IDLE);
- const [phone, setPhone] = useState();
- const [verificationCode, setVerificationCode] = useState('');
- const [requestedAnother, setAlreadyRequestedAnother] = useState(false);
-
- const isValid = !!phone;
-
- const onChange = useCallback((phone?: string) => {
- setPhone(phone);
- }, []);
-
- const handleSubmit = (event: React.MouseEvent) => {
- event.preventDefault();
-
- if (!isValid) {
- setStatus(Statuses.IDLE);
- toast.error(intl.formatMessage(messages.verificationInvalid));
- return;
- }
-
- dispatch(reRequestPhoneVerification(phone!)).then(() => {
- toast.success(
- intl.formatMessage(messages.verificationSuccess),
- );
- })
- .finally(() => setStatus(Statuses.REQUESTED))
- .catch(() => {
- toast.error(intl.formatMessage(messages.verificationFail));
- });
- };
-
- const resendVerificationCode = (event?: React.MouseEvent) => {
- setAlreadyRequestedAnother(true);
- handleSubmit(event as React.MouseEvent);
- };
-
- const onConfirmationClick = (event: any) => {
- switch (status) {
- case Statuses.IDLE:
- setStatus(Statuses.READY);
- break;
- case Statuses.READY:
- handleSubmit(event);
- break;
- case Statuses.REQUESTED:
- submitVerification();
- break;
- default: break;
- }
- };
-
- const confirmationText = useMemo(() => {
- switch (status) {
- case Statuses.IDLE:
- return intl.formatMessage(messages.verifySms);
- case Statuses.READY:
- return intl.formatMessage(messages.verifyNumber);
- case Statuses.REQUESTED:
- return intl.formatMessage(messages.verifyCode);
- default:
- return null;
- }
- }, [status]);
-
- const renderModalBody = () => {
- switch (status) {
- case Statuses.IDLE:
- return (
-
-
-
- );
- case Statuses.READY:
- return (
- }>
-
-
- );
- case Statuses.REQUESTED:
- return (
- <>
-
-
-
-
-
- >
- );
- default:
- return null;
- }
- };
-
- const submitVerification = () => {
- if (!accessToken) return;
- // TODO: handle proper validation from Pepe -- expired vs invalid
- dispatch(reConfirmPhoneVerification(verificationCode))
- .then(() => {
- setStatus(Statuses.SUCCESS);
- // eslint-disable-next-line promise/catch-or-return
- dispatch(verifyCredentials(accessToken))
- .then(() => dispatch(closeModal('VERIFY_SMS')));
-
- })
- .catch(() => toast.error(intl.formatMessage(messages.verificationExpired)));
- };
-
- useEffect(() => {
- if (verificationCode.length === 6) {
- submitVerification();
- }
- }, [verificationCode]);
-
- return (
-
- }
- onClose={() => onClose('VERIFY_SMS')}
- cancelAction={status === Statuses.IDLE ? () => onClose('VERIFY_SMS') : undefined}
- cancelText='Skip for now'
- confirmationAction={onConfirmationClick}
- confirmationText={confirmationText}
- secondaryAction={status === Statuses.REQUESTED ? resendVerificationCode : undefined}
- secondaryText={status === Statuses.REQUESTED ? (
-
- ) : undefined}
- secondaryDisabled={requestedAnother}
- >
-
- {renderModalBody()}
-
-
- );
-};
-
-export default VerifySmsModal;
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 4efd47413..000000000
--- a/app/soapbox/features/ui/util/async-components.ts
+++ /dev/null
@@ -1,647 +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');
-}
-
-export function ComposeEditor() {
- return import(/* webpackChunkName: "lexical" */'../../compose/editor');
-}
diff --git a/app/soapbox/features/verification/__tests__/index.test.tsx b/app/soapbox/features/verification/__tests__/index.test.tsx
deleted file mode 100644
index e24aee442..000000000
--- a/app/soapbox/features/verification/__tests__/index.test.tsx
+++ /dev/null
@@ -1,105 +0,0 @@
-import { Map as ImmutableMap, Record as ImmutableRecord } from 'immutable';
-import React from 'react';
-import { Route, Switch } from 'react-router-dom';
-
-import { __stub } from 'soapbox/api';
-
-import { render, screen } from '../../../jest/test-helpers';
-import Verification from '../index';
-
-const TestableComponent = () => (
-
-
- Homepage
-
-);
-
-const renderComponent = (store: any) => render(
- ,
- {},
- store,
- { initialEntries: ['/verify'] },
-);
-
-describe('', () => {
- let store: any;
-
- beforeEach(() => {
- store = {
- verification: ImmutableRecord({
- instance: ImmutableMap({
- isReady: true,
- registrations: true,
- }),
- ageMinimum: null,
- currentChallenge: null,
- isLoading: false,
- isComplete: false,
- token: null,
- })(),
- };
-
- __stub(mock => {
- mock.onGet('/api/v1/pepe/instance')
- .reply(200, {
- age_minimum: 18,
- approval_required: true,
- challenges: ['age', 'email', 'sms'],
- });
-
- mock.onPost('/api/v1/pepe/registrations')
- .reply(200, {
- access_token: 'N-dZmNqNSmTutJLsGjZ5AnJL4sLw_y-N3pn2acSqJY8',
- });
- });
- });
-
- describe('When registration is closed', () => {
- it('successfully redirects to the homepage', () => {
- const verification = store.verification.setIn(['instance', 'registrations'], false);
- store.verification = verification;
-
- renderComponent(store);
- expect(screen.getByTestId('home')).toHaveTextContent('Homepage');
- });
- });
-
- describe('When verification is complete', () => {
- it('successfully renders the Registration component', () => {
- const verification = store.verification.set('isComplete', true);
- store.verification = verification;
-
- renderComponent(store);
- expect(screen.getByRole('heading')).toHaveTextContent('Register your account');
- });
- });
-
- describe('Switching verification steps', () => {
- it('successfully renders the Birthday step', () => {
- const verification = store.verification.set('currentChallenge', 'age');
- store.verification = verification;
-
- renderComponent(store);
-
- expect(screen.getByRole('heading')).toHaveTextContent('Enter your birth date');
- });
-
- it('successfully renders the Email step', () => {
- const verification = store.verification.set('currentChallenge', 'email');
- store.verification = verification;
-
- renderComponent(store);
-
- expect(screen.getByRole('heading')).toHaveTextContent('Enter your email address');
- });
-
- it('successfully renders the SMS step', () => {
- const verification = store.verification.set('currentChallenge', 'sms');
- store.verification = verification;
-
- renderComponent(store);
-
- expect(screen.getByRole('heading')).toHaveTextContent('Enter your phone number');
- });
- });
-});
diff --git a/app/soapbox/features/verification/__tests__/registration.test.tsx b/app/soapbox/features/verification/__tests__/registration.test.tsx
deleted file mode 100644
index 8dfe3083f..000000000
--- a/app/soapbox/features/verification/__tests__/registration.test.tsx
+++ /dev/null
@@ -1,117 +0,0 @@
-import React from 'react';
-
-import { __stub } from 'soapbox/api';
-
-import { fireEvent, render, screen, waitFor } from '../../../jest/test-helpers';
-import Registration from '../registration';
-
-describe('', () => {
- it('renders', () => {
- render();
-
- expect(screen.getByRole('heading')).toHaveTextContent(/register your account/i);
- });
-
- describe('with valid data', () => {
- beforeEach(() => {
- __stub(mock => {
- mock.onPost('/api/v1/pepe/accounts').reply(200, {});
- mock.onPost('/api/v1/apps').reply(200, {});
- mock.onPost('/oauth/token').reply(200, {});
- mock.onGet('/api/v1/accounts/verify_credentials').reply(200, { id: '123' });
- mock.onGet('/api/v1/instance').reply(200, {});
- });
- });
-
- it('handles successful submission', async() => {
- render();
-
- await waitFor(() => {
- fireEvent.submit(screen.getByTestId('button'), { preventDefault: () => {} });
- });
-
- await waitFor(() => {
- expect(screen.getByTestId('toast')).toHaveTextContent(/welcome to/i);
- });
-
- expect(screen.queryAllByRole('heading')).toHaveLength(0);
- });
- });
-
- describe('with invalid data', () => {
- it('handles 422 errors', async() => {
- __stub(mock => {
- mock.onPost('/api/v1/pepe/accounts').reply(
- 422, {
- error: 'user_taken',
- },
- );
- });
-
- render();
-
- await waitFor(() => {
- fireEvent.submit(screen.getByTestId('button'), { preventDefault: () => {} });
- });
-
- await waitFor(() => {
- expect(screen.getByTestId('toast')).toHaveTextContent(/this username has already been taken/i);
- });
- });
-
- it('handles 422 errors with messages', async() => {
- __stub(mock => {
- mock.onPost('/api/v1/pepe/accounts').reply(
- 422, {
- error: 'user_vip',
- message: 'This username is unavailable.',
- },
- );
- });
-
- render();
-
- await waitFor(() => {
- fireEvent.submit(screen.getByTestId('button'), { preventDefault: () => {} });
- });
-
- await waitFor(() => {
- expect(screen.getByTestId('toast')).toHaveTextContent(/this username is unavailable/i);
- });
-
- });
-
- it('handles generic errors', async() => {
- __stub(mock => {
- mock.onPost('/api/v1/pepe/accounts').reply(500, {});
- });
-
- render();
-
- await waitFor(() => {
- fireEvent.submit(screen.getByTestId('button'), { preventDefault: () => {} });
- });
-
- await waitFor(() => {
- expect(screen.getByTestId('toast')).toHaveTextContent(/failed to register your account/i);
- });
- });
- });
-
- describe('validations', () => {
- it('should undisable button with valid password', async() => {
- render();
-
- expect(screen.getByTestId('button')).toBeDisabled();
- fireEvent.change(screen.getByTestId('password-input'), { target: { value: 'Password' } });
- expect(screen.getByTestId('button')).not.toBeDisabled();
- });
-
- it('should disable button with invalid password', async() => {
- render();
-
- fireEvent.change(screen.getByTestId('password-input'), { target: { value: 'Passwor' } });
- expect(screen.getByTestId('button')).toBeDisabled();
- });
- });
-});
diff --git a/app/soapbox/features/verification/components/password-indicator.tsx b/app/soapbox/features/verification/components/password-indicator.tsx
deleted file mode 100644
index 7b804d3d6..000000000
--- a/app/soapbox/features/verification/components/password-indicator.tsx
+++ /dev/null
@@ -1,72 +0,0 @@
-import React, { useEffect, useMemo } from 'react';
-import { defineMessages, useIntl } from 'react-intl';
-
-import { Stack } from 'soapbox/components/ui';
-import ValidationCheckmark from 'soapbox/components/validation-checkmark';
-
-const messages = defineMessages({
- minimumCharacters: {
- id: 'registration.validation.minimum_characters',
- defaultMessage: '8 characters',
- },
- capitalLetter: {
- id: 'registration.validation.capital_letter',
- defaultMessage: '1 capital letter',
- },
- lowercaseLetter: {
- id: 'registration.validation.lowercase_letter',
- defaultMessage: '1 lowercase letter',
- },
-});
-
-const hasUppercaseCharacter = (string: string) => {
- for (let i = 0; i < string.length; i++) {
- if (string.charAt(i) === string.charAt(i).toUpperCase() && string.charAt(i).match(/[a-z]/i)) {
- return true;
- }
- }
- return false;
-};
-
-const hasLowercaseCharacter = (string: string) => {
- return string.toUpperCase() !== string;
-};
-
-interface IPasswordIndicator {
- onChange(isValid: boolean): void
- password: string
-}
-
-const PasswordIndicator = ({ onChange, password }: IPasswordIndicator) => {
- const intl = useIntl();
-
- const meetsLengthRequirements = useMemo(() => password.length >= 8, [password]);
- const meetsCapitalLetterRequirements = useMemo(() => hasUppercaseCharacter(password), [password]);
- const meetsLowercaseLetterRequirements = useMemo(() => hasLowercaseCharacter(password), [password]);
- const hasValidPassword = meetsLengthRequirements && meetsCapitalLetterRequirements && meetsLowercaseLetterRequirements;
-
- useEffect(() => {
- onChange(hasValidPassword);
- }, [hasValidPassword]);
-
- return (
-
-
-
-
-
-
-
- );
-};
-
-export default PasswordIndicator;
diff --git a/app/soapbox/features/verification/email-passthru.tsx b/app/soapbox/features/verification/email-passthru.tsx
deleted file mode 100644
index 86152f12d..000000000
--- a/app/soapbox/features/verification/email-passthru.tsx
+++ /dev/null
@@ -1,167 +0,0 @@
-import React from 'react';
-import { defineMessages, useIntl } from 'react-intl';
-import { useHistory, useParams } from 'react-router-dom';
-
-import { confirmEmailVerification } from 'soapbox/actions/verification';
-import { Icon, Spinner, Stack, Text } from 'soapbox/components/ui';
-import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
-import toast from 'soapbox/toast';
-
-import { ChallengeTypes } from './index';
-
-import type { AxiosError } from 'axios';
-
-const Statuses = {
- IDLE: 'IDLE',
- SUCCESS: 'SUCCESS',
- GENERIC_FAIL: 'GENERIC_FAIL',
- TOKEN_NOT_FOUND: 'TOKEN_NOT_FOUND',
- TOKEN_EXPIRED: 'TOKEN_EXPIRED',
-};
-
-const messages = defineMessages({
- emailConfirmedHeading: { id: 'email_passthru.confirmed.heading', defaultMessage: 'Email Confirmed!' },
- emailConfirmedBody: { id: 'email_passthru.confirmed.body', defaultMessage: 'Close this tab and continue the registration process on the {bold} from which you sent this email confirmation.' },
- genericFailHeading: { id: 'email_passthru.generic_fail.heading', defaultMessage: 'Something Went Wrong' },
- genericFailBody: { id: 'email_passthru.generic_fail.body', defaultMessage: 'Please request a new email confirmation.' },
- tokenNotFoundHeading: { id: 'email_passthru.token_not_found.heading', defaultMessage: 'Invalid Token' },
- tokenNotFoundBody: { id: 'email_passthru.token_not_found.body', defaultMessage: 'Your email token was not found. Please request a new email confirmation from the {bold} from which you sent this email confirmation.' },
- tokenExpiredHeading: { id: 'email_passthru.token_expired.heading', defaultMessage: 'Token Expired' },
- tokenExpiredBody: { id: 'email_passthru.token_expired.body', defaultMessage: 'Your email token has expired. Please request a new email confirmation from the {bold} from which you sent this email confirmation.' },
- emailConfirmed: { id: 'email_passthru.success', defaultMessage: 'Your email has been verified!' },
- genericFail: { id: 'email_passthru.fail.generic', defaultMessage: 'Unable to confirm your email' },
- tokenExpired: { id: 'email_passthru.fail.expired', defaultMessage: 'Your email token has expired' },
- tokenNotFound: { id: 'email_passthru.fail.not_found', defaultMessage: 'Your email token is invalid.' },
- invalidToken: { id: 'email_passthru.fail.invalid_token', defaultMessage: 'Your token is invalid' },
-});
-
-const Success = () => {
- const intl = useIntl();
- const history = useHistory();
- const currentChallenge = useAppSelector((state) => state.verification.currentChallenge as ChallengeTypes);
-
- React.useEffect(() => {
- // Bypass the user straight to the next step.
- if (currentChallenge === ChallengeTypes.SMS) {
- history.push('/verify');
- }
- }, [currentChallenge]);
-
- return (
-
-
-
- {intl.formatMessage(messages.emailConfirmedHeading)}
-
-
- {intl.formatMessage(messages.emailConfirmedBody, { bold: same device })}
-
-
- );
-};
-
-const GenericFail = () => {
- const intl = useIntl();
-
- return (
-
-
-
- {intl.formatMessage(messages.genericFailHeading)}
-
-
- {intl.formatMessage(messages.genericFailBody)}
-
-
- );
-};
-
-const TokenNotFound = () => {
- const intl = useIntl();
-
- return (
-
-
-
- {intl.formatMessage(messages.tokenNotFoundHeading)}
-
-
- {intl.formatMessage(messages.tokenNotFoundBody, { bold: same device })}
-
-
-
- );
-};
-
-const TokenExpired = () => {
- const intl = useIntl();
-
- return (
-
-
-
- {intl.formatMessage(messages.tokenExpiredHeading)}
-
-
- {intl.formatMessage(messages.tokenExpiredBody, { bold: same device })}
-
-
- );
-};
-
-const EmailPassThru = () => {
- const { token } = useParams<{ token: string }>();
-
- const dispatch = useAppDispatch();
- const intl = useIntl();
-
- const [status, setStatus] = React.useState(Statuses.IDLE);
-
- React.useEffect(() => {
- if (token) {
- dispatch(confirmEmailVerification(token))
- .then(() => {
- setStatus(Statuses.SUCCESS);
- toast.success(intl.formatMessage(messages.emailConfirmed));
- })
- .catch((error: AxiosError) => {
- const errorKey = error?.response?.data?.error;
- let message = intl.formatMessage(messages.genericFail);
-
- if (errorKey) {
- switch (errorKey) {
- case 'token_expired':
- message = intl.formatMessage(messages.tokenExpired);
- setStatus(Statuses.TOKEN_EXPIRED);
- break;
- case 'token_not_found':
- message = intl.formatMessage(messages.tokenNotFound);
- message = intl.formatMessage(messages.invalidToken);
- setStatus(Statuses.TOKEN_NOT_FOUND);
- break;
- default:
- setStatus(Statuses.GENERIC_FAIL);
- break;
- }
- }
-
- toast.error(message);
- });
- }
- }, [token]);
-
- switch (status) {
- case Statuses.SUCCESS:
- return ;
- case Statuses.TOKEN_EXPIRED:
- return ;
- case Statuses.TOKEN_NOT_FOUND:
- return ;
- case Statuses.GENERIC_FAIL:
- return ;
- default:
- return ;
- }
-};
-
-export default EmailPassThru;
diff --git a/app/soapbox/features/verification/index.tsx b/app/soapbox/features/verification/index.tsx
deleted file mode 100644
index 88c9dc435..000000000
--- a/app/soapbox/features/verification/index.tsx
+++ /dev/null
@@ -1,56 +0,0 @@
-import React from 'react';
-import { Redirect } from 'react-router-dom';
-
-import { fetchVerificationConfig } from 'soapbox/actions/verification';
-import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
-
-import Registration from './registration';
-import AgeVerification from './steps/age-verification';
-import EmailVerification from './steps/email-verification';
-import SmsVerification from './steps/sms-verification';
-
-export enum ChallengeTypes {
- EMAIL = 'email',
- SMS = 'sms',
- AGE = 'age',
-}
-
-const verificationSteps = {
- email: EmailVerification,
- sms: SmsVerification,
- age: AgeVerification,
-};
-
-const Verification = () => {
- const dispatch = useAppDispatch();
-
- const isInstanceReady = useAppSelector((state) => state.verification.instance.get('isReady') === true);
- const isRegistrationOpen = useAppSelector(state => state.verification.instance.get('registrations') === true);
- const currentChallenge = useAppSelector((state) => state.verification.currentChallenge as ChallengeTypes);
- const isVerificationComplete = useAppSelector((state) => state.verification.isComplete);
- const StepToRender = verificationSteps[currentChallenge];
-
- React.useEffect(() => {
- dispatch(fetchVerificationConfig());
- }, []);
-
- if (isInstanceReady && !isRegistrationOpen) {
- return ;
- }
-
- if (isVerificationComplete) {
- return (
-
- );
- }
-
- if (!currentChallenge) {
- return null;
- }
-
- return (
-
- );
-};
-
-export default Verification;
diff --git a/app/soapbox/features/verification/registration.tsx b/app/soapbox/features/verification/registration.tsx
deleted file mode 100644
index bee939ec4..000000000
--- a/app/soapbox/features/verification/registration.tsx
+++ /dev/null
@@ -1,161 +0,0 @@
-import React from 'react';
-import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
-import { Redirect } from 'react-router-dom';
-
-import { logIn, verifyCredentials } from 'soapbox/actions/auth';
-import { fetchInstance } from 'soapbox/actions/instance';
-import { startOnboarding } from 'soapbox/actions/onboarding';
-import { createAccount, removeStoredVerification } from 'soapbox/actions/verification';
-import { Button, Form, FormGroup, Input, Text } from 'soapbox/components/ui';
-import { useAppDispatch, useAppSelector, useInstance, useSoapboxConfig } from 'soapbox/hooks';
-import toast from 'soapbox/toast';
-import { getRedirectUrl } from 'soapbox/utils/redirect';
-
-import PasswordIndicator from './components/password-indicator';
-
-import type { AxiosError } from 'axios';
-
-const messages = defineMessages({
- success: { id: 'registrations.success', defaultMessage: 'Welcome to {siteTitle}!' },
- usernameLabel: { id: 'registrations.username.label', defaultMessage: 'Your username' },
- usernameHint: { id: 'registrations.username.hint', defaultMessage: 'May only contain A-Z, 0-9, and underscores' },
- usernameTaken: { id: 'registrations.unprocessable_entity', defaultMessage: 'This username has already been taken.' },
- passwordLabel: { id: 'registrations.password.label', defaultMessage: 'Password' },
- error: { id: 'registrations.error', defaultMessage: 'Failed to register your account.' },
-});
-
-const initialState = {
- username: '',
- password: '',
-};
-
-const Registration = () => {
- const dispatch = useAppDispatch();
- const intl = useIntl();
- const instance = useInstance();
- const soapboxConfig = useSoapboxConfig();
- const { links } = soapboxConfig;
-
- const isLoading = useAppSelector((state) => state.verification.isLoading as boolean);
-
- const [state, setState] = React.useState(initialState);
- const [shouldRedirect, setShouldRedirect] = React.useState(false);
- const [hasValidPassword, setHasValidPassword] = React.useState(false);
- const { username, password } = state;
-
- const handleSubmit: React.FormEventHandler = React.useCallback((event) => {
- event.preventDefault();
-
- dispatch(createAccount(username, password))
- .then(() => dispatch(logIn(username, password)))
- .then(({ access_token }: any) => dispatch(verifyCredentials(access_token)))
- .then(() => dispatch(fetchInstance()))
- .then(() => {
- setShouldRedirect(true);
- removeStoredVerification();
- dispatch(startOnboarding());
- toast.success(
- intl.formatMessage(messages.success, { siteTitle: instance.title }),
- );
- })
- .catch((errorResponse: AxiosError<{ error: string, message: string }>) => {
- const error = errorResponse.response?.data?.error;
-
- if (error) {
- toast.error(errorResponse.response?.data?.message || intl.formatMessage(messages.usernameTaken));
- } else {
- toast.error(intl.formatMessage(messages.error));
- }
- });
- }, [username, password]);
-
- const handleInputChange: React.ChangeEventHandler = React.useCallback((event) => {
- event.persist();
-
- setState((prevState) => ({ ...prevState, [event.target.name]: event.target.value }));
- }, []);
-
- if (shouldRedirect) {
- const redirectUri = getRedirectUrl();
- return ;
- }
-
- return (
-
- );
-};
-
-export default Registration;
diff --git a/app/soapbox/features/verification/steps/__tests__/age-verification.test.tsx b/app/soapbox/features/verification/steps/__tests__/age-verification.test.tsx
deleted file mode 100644
index f310a0741..000000000
--- a/app/soapbox/features/verification/steps/__tests__/age-verification.test.tsx
+++ /dev/null
@@ -1,53 +0,0 @@
-import userEvent from '@testing-library/user-event';
-import { Map as ImmutableMap } from 'immutable';
-import React from 'react';
-
-import { __stub } from 'soapbox/api';
-import { fireEvent, render, screen } from 'soapbox/jest/test-helpers';
-
-import AgeVerification from '../age-verification';
-
-describe('', () => {
- let store: any;
-
- beforeEach(() => {
- store = {
- verification: ImmutableMap({
- ageMinimum: 13,
- }),
- };
-
- __stub(mock => {
- mock.onPost('/api/v1/pepe/verify_age/confirm')
- .reply(200, {});
- });
- });
-
- it('successfully renders the Birthday step', async() => {
- render(
- ,
- {},
- store,
- );
- expect(screen.getByRole('heading')).toHaveTextContent('Enter your birth date');
- });
-
- it('selects a date', async() => {
- render(
- ,
- {},
- store,
- );
-
- await userEvent.selectOptions(
- screen.getByTestId('datepicker-year'),
- screen.getByRole('option', { name: '2020' }),
- );
-
- fireEvent.submit(
- screen.getByRole('button'), {
- preventDefault: () => {},
- },
- );
- });
-});
diff --git a/app/soapbox/features/verification/steps/__tests__/email-verification.test.tsx b/app/soapbox/features/verification/steps/__tests__/email-verification.test.tsx
deleted file mode 100644
index 38e7cf8a7..000000000
--- a/app/soapbox/features/verification/steps/__tests__/email-verification.test.tsx
+++ /dev/null
@@ -1,68 +0,0 @@
-import userEvent from '@testing-library/user-event';
-import React from 'react';
-
-import { __stub } from 'soapbox/api';
-import { fireEvent, render, screen, waitFor } from 'soapbox/jest/test-helpers';
-
-import EmailVerification from '../email-verification';
-
-describe('', () => {
- it('successfully renders the Email step', async() => {
- render();
- expect(screen.getByRole('heading')).toHaveTextContent('Enter your email address');
- });
-
- describe('with valid data', () => {
- beforeEach(() => {
- __stub(mock => {
- mock.onPost('/api/v1/pepe/verify_email/request')
- .reply(200, {});
- });
- });
-
- it('successfully submits', async() => {
- render();
-
- await userEvent.type(screen.getByLabelText('E-mail address'), 'foo@bar.com{enter}');
-
- await waitFor(() => {
- fireEvent.submit(
- screen.getByTestId('button'), {
- preventDefault: () => {},
- },
- );
- });
-
- expect(screen.getByTestId('button')).toHaveTextContent('Resend verification email');
- });
- });
-
- describe('with invalid data', () => {
- beforeEach(() => {
- __stub(mock => {
- mock.onPost('/api/v1/pepe/verify_email/request')
- .reply(422, {
- error: 'email_taken',
- });
- });
- });
-
- it('renders errors', async() => {
- render();
-
- await userEvent.type(screen.getByLabelText('E-mail address'), 'foo@bar.com{enter}');
-
- await waitFor(() => {
- fireEvent.submit(
- screen.getByTestId('button'), {
- preventDefault: () => {},
- },
- );
- });
-
- await waitFor(() => {
- expect(screen.getByTestId('form-group-error')).toHaveTextContent('is taken');
- });
- });
- });
-});
diff --git a/app/soapbox/features/verification/steps/__tests__/sms-verification.test.tsx b/app/soapbox/features/verification/steps/__tests__/sms-verification.test.tsx
deleted file mode 100644
index d837f46b7..000000000
--- a/app/soapbox/features/verification/steps/__tests__/sms-verification.test.tsx
+++ /dev/null
@@ -1,120 +0,0 @@
-import userEvent from '@testing-library/user-event';
-import React from 'react';
-import { act } from 'react-dom/test-utils';
-import { toast } from 'react-hot-toast';
-
-import { __stub } from 'soapbox/api';
-import { fireEvent, render, screen, waitFor } from 'soapbox/jest/test-helpers';
-
-import SmsVerification from '../sms-verification';
-
-describe('', () => {
- it('successfully renders the SMS step', async() => {
- render();
- expect(screen.getByRole('heading')).toHaveTextContent('Enter your phone number');
- });
-
- describe('with valid data', () => {
- beforeEach(() => {
- __stub(mock => {
- mock.onPost('/api/v1/pepe/verify_sms/request').reply(200, {});
- });
- });
-
- it('successfully submits', async() => {
- __stub(mock => {
- mock.onPost('/api/v1/pepe/verify_sms/confirm').reply(200, {});
- });
-
- render();
-
- await userEvent.type(screen.getByLabelText('Phone number'), '+1 (555) 555-5555');
- await waitFor(() => {
- fireEvent.submit(
- screen.getByRole('button', { name: 'Next' }), {
- preventDefault: () => {},
- },
- );
- });
-
- await waitFor(() => {
- expect(screen.getByRole('heading')).toHaveTextContent('Verification code');
- expect(screen.getByTestId('toast')).toHaveTextContent('A verification code has been sent to your phone number.');
- });
-
- act(() => {
- toast.remove();
- });
-
- await userEvent.type(screen.getByLabelText('Please enter verification code. Digit 1'), '1');
- await userEvent.type(screen.getByLabelText('Digit 2'), '2');
- await userEvent.type(screen.getByLabelText('Digit 3'), '3');
- await userEvent.type(screen.getByLabelText('Digit 4'), '4');
- await userEvent.type(screen.getByLabelText('Digit 5'), '5');
- await userEvent.type(screen.getByLabelText('Digit 6'), '6');
- });
-
- it('handle expired tokens', async() => {
- __stub(mock => {
- mock.onPost('/api/v1/pepe/verify_sms/confirm').reply(422, {});
- });
-
- render();
-
- await userEvent.type(screen.getByLabelText('Phone number'), '+1 (555) 555-5555');
- await waitFor(() => {
- fireEvent.submit(
- screen.getByRole('button', { name: 'Next' }), {
- preventDefault: () => {},
- },
- );
- });
-
- await waitFor(() => {
- expect(screen.getByRole('heading')).toHaveTextContent('Verification code');
- expect(screen.getByTestId('toast')).toHaveTextContent('A verification code has been sent to your phone number.');
- });
-
- act(() => {
- toast.remove();
- });
-
- await userEvent.type(screen.getByLabelText('Please enter verification code. Digit 1'), '1');
- await userEvent.type(screen.getByLabelText('Digit 2'), '2');
- await userEvent.type(screen.getByLabelText('Digit 3'), '3');
- await userEvent.type(screen.getByLabelText('Digit 4'), '4');
- await userEvent.type(screen.getByLabelText('Digit 5'), '5');
- await userEvent.type(screen.getByLabelText('Digit 6'), '6');
-
- await waitFor(() => {
- expect(screen.getByTestId('toast')).toHaveTextContent('Your SMS token has expired.');
- });
- });
- });
-
- describe('with invalid data', () => {
- beforeEach(() => {
- __stub(mock => {
- mock.onPost('/api/v1/pepe/verify_sms/request')
- .reply(422, {});
- });
- });
-
- it('renders errors', async() => {
- render();
-
- await userEvent.type(screen.getByLabelText('Phone number'), '+1 (555) 555-5555');
- await waitFor(() => {
- fireEvent.submit(
- screen.getByRole('button', { name: 'Next' }), {
- preventDefault: () => {},
- },
- );
- });
-
- await waitFor(() => {
- expect(screen.getByTestId('toast')).toHaveTextContent('Failed to send SMS message to your phone number.');
- });
- });
- });
-});
diff --git a/app/soapbox/features/verification/steps/age-verification.tsx b/app/soapbox/features/verification/steps/age-verification.tsx
deleted file mode 100644
index 5129f5464..000000000
--- a/app/soapbox/features/verification/steps/age-verification.tsx
+++ /dev/null
@@ -1,84 +0,0 @@
-import React from 'react';
-import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
-
-import { verifyAge } from 'soapbox/actions/verification';
-import { Button, Datepicker, Form, Text } from 'soapbox/components/ui';
-import { useAppDispatch, useAppSelector, useInstance } from 'soapbox/hooks';
-import toast from 'soapbox/toast';
-
-const messages = defineMessages({
- fail: {
- id: 'age_verification.fail',
- defaultMessage: 'You must be {ageMinimum, plural, one {# year} other {# years}} old or older.',
- },
-});
-
-function meetsAgeMinimum(birthday: Date, ageMinimum: number) {
- const month = birthday.getUTCMonth();
- const day = birthday.getUTCDate();
- const year = birthday.getUTCFullYear();
-
- return new Date(year + ageMinimum, month, day) <= new Date();
-}
-
-const AgeVerification = () => {
- const intl = useIntl();
- const dispatch = useAppDispatch();
- const instance = useInstance();
-
- const isLoading = useAppSelector((state) => state.verification.isLoading) as boolean;
- const ageMinimum = useAppSelector((state) => state.verification.ageMinimum) as any;
-
- const [date, setDate] = React.useState();
- const isValid = typeof date === 'object';
-
- const onChange = React.useCallback((date: Date) => setDate(date), []);
-
- const handleSubmit: React.FormEventHandler = React.useCallback((event) => {
- event.preventDefault();
-
- const birthday = new Date(date!);
-
- if (meetsAgeMinimum(birthday, ageMinimum)) {
- dispatch(verifyAge(birthday));
- } else {
- toast.error(intl.formatMessage(messages.fail, { ageMinimum }));
- }
- }, [date, ageMinimum]);
-
- return (
-
- );
-};
-
-export default AgeVerification;
diff --git a/app/soapbox/features/verification/steps/email-verification.tsx b/app/soapbox/features/verification/steps/email-verification.tsx
deleted file mode 100644
index a7c15d686..000000000
--- a/app/soapbox/features/verification/steps/email-verification.tsx
+++ /dev/null
@@ -1,146 +0,0 @@
-import { AxiosError } from 'axios';
-import React from 'react';
-import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
-
-import { checkEmailVerification, postEmailVerification, requestEmailVerification } from 'soapbox/actions/verification';
-import Icon from 'soapbox/components/icon';
-import { Button, Form, FormGroup, Input, Text } from 'soapbox/components/ui';
-import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
-import toast from 'soapbox/toast';
-
-const messages = defineMessages({
- verificationSuccess: { id: 'email_verification.success', defaultMessage: 'Verification email sent successfully.' },
- verificationFail: { id: 'email_verification.fail', defaultMessage: 'Failed to request email verification.' },
- verificationFailTakenAlert: { id: 'email_verifilcation.exists', defaultMessage: 'This email has already been taken.' },
- verificationFailTaken: { id: 'email_verification.taken', defaultMessage: 'is taken' },
- emailLabel: { id: 'email_verification.email.label', defaultMessage: 'E-mail address' },
-});
-
-const Statuses = {
- IDLE: 'IDLE',
- REQUESTED: 'REQUESTED',
- FAIL: 'FAIL',
-};
-
-const EMAIL_REGEX = /^[^@\s]+@[^@\s]+$/;
-
-interface IEmailSent {
- handleSubmit: React.FormEventHandler
-}
-
-const EmailSent: React.FC = ({ handleSubmit }) => {
- const dispatch = useAppDispatch();
-
- const checkEmailConfirmation = () => {
- dispatch(checkEmailVerification())
- .then(() => dispatch(postEmailVerification()))
- .catch(() => null);
- };
-
- React.useEffect(() => {
- const intervalId = setInterval(() => checkEmailConfirmation(), 2500);
-
- return () => clearInterval(intervalId);
- }, []);
-
- return (
-
-
-
-
- We sent you an email
- Click on the link in the email to validate your email.
-
-
-
-
- );
-};
-
-const EmailVerification = () => {
- const intl = useIntl();
- const dispatch = useAppDispatch();
-
- const isLoading = useAppSelector((state) => state.verification.isLoading) as boolean;
-
- const [email, setEmail] = React.useState('');
- const [status, setStatus] = React.useState(Statuses.IDLE);
- const [errors, setErrors] = React.useState>([]);
-
- const isValid = email.length > 0 && EMAIL_REGEX.test(email);
-
- const onChange: React.ChangeEventHandler = React.useCallback((event) => {
- setEmail(event.target.value);
- }, []);
-
- const handleSubmit: React.FormEventHandler = React.useCallback((event) => {
- event.preventDefault();
- setErrors([]);
-
- submitEmailForVerification();
- }, [email]);
-
- const submitEmailForVerification = () => {
- return dispatch(requestEmailVerification((email)))
- .then(() => {
- setStatus(Statuses.REQUESTED);
-
- toast.success(intl.formatMessage(messages.verificationSuccess));
- })
- .catch((error: AxiosError) => {
- const errorMessage = (error.response?.data as any)?.error;
- const isEmailTaken = errorMessage === 'email_taken';
- let message = intl.formatMessage(messages.verificationFail);
-
- if (isEmailTaken) {
- message = intl.formatMessage(messages.verificationFailTakenAlert);
- } else if (errorMessage) {
- message = errorMessage;
- }
-
- if (isEmailTaken) {
- setErrors([intl.formatMessage(messages.verificationFailTaken)]);
- }
-
- toast.error(message);
- setStatus(Statuses.FAIL);
- });
- };
-
- if (status === Statuses.REQUESTED) {
- return ;
- }
-
- return (
-
- );
-};
-
-export default EmailVerification;
diff --git a/app/soapbox/features/verification/steps/sms-verification.tsx b/app/soapbox/features/verification/steps/sms-verification.tsx
deleted file mode 100644
index 0266b1581..000000000
--- a/app/soapbox/features/verification/steps/sms-verification.tsx
+++ /dev/null
@@ -1,151 +0,0 @@
-import { AxiosError } from 'axios';
-import React from 'react';
-import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
-import OtpInput from 'react-otp-input';
-
-import { confirmPhoneVerification, requestPhoneVerification } from 'soapbox/actions/verification';
-import { Button, Form, FormGroup, PhoneInput, Text } from 'soapbox/components/ui';
-import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
-import toast from 'soapbox/toast';
-
-const messages = defineMessages({
- verificationInvalid: { id: 'sms_verification.invalid', defaultMessage: 'Please enter a valid phone number.' },
- verificationSuccess: { id: 'sms_verification.success', defaultMessage: 'A verification code has been sent to your phone number.' },
- verificationFail: { id: 'sms_verification.fail', defaultMessage: 'Failed to send SMS message to your phone number.' },
- verificationExpired: { id: 'sms_verification.expired', defaultMessage: 'Your SMS token has expired.' },
- phoneLabel: { id: 'sms_verification.phone.label', defaultMessage: 'Phone number' },
-});
-
-const Statuses = {
- IDLE: 'IDLE',
- REQUESTED: 'REQUESTED',
- FAIL: 'FAIL',
-};
-
-const SmsVerification = () => {
- const intl = useIntl();
- const dispatch = useAppDispatch();
-
- const isLoading = useAppSelector((state) => state.verification.isLoading) as boolean;
-
- const [phone, setPhone] = React.useState();
- const [status, setStatus] = React.useState(Statuses.IDLE);
- const [verificationCode, setVerificationCode] = React.useState('');
- const [requestedAnother, setAlreadyRequestedAnother] = React.useState(false);
-
- const isValid = !!phone;
-
- const onChange = React.useCallback((phone?: string) => {
- setPhone(phone);
- }, []);
-
- const handleSubmit: React.FormEventHandler = React.useCallback((event) => {
- event.preventDefault();
-
- if (!isValid) {
- setStatus(Statuses.IDLE);
- toast.error(intl.formatMessage(messages.verificationInvalid));
- return;
- }
-
- dispatch(requestPhoneVerification(phone!)).then(() => {
- toast.success(intl.formatMessage(messages.verificationSuccess));
- setStatus(Statuses.REQUESTED);
- }).catch((error: AxiosError) => {
- const message = (error.response?.data as any)?.message || intl.formatMessage(messages.verificationFail);
-
- toast.error(message);
- setStatus(Statuses.FAIL);
- });
- }, [phone, isValid]);
-
- const resendVerificationCode: React.MouseEventHandler = React.useCallback((event) => {
- setAlreadyRequestedAnother(true);
- handleSubmit(event);
- }, [isValid]);
-
- const submitVerification = () => {
- // TODO: handle proper validation from Pepe -- expired vs invalid
- dispatch(confirmPhoneVerification(verificationCode))
- .catch(() => {
- toast.error(intl.formatMessage(messages.verificationExpired));
- });
- };
-
- React.useEffect(() => {
- if (verificationCode.length === 6) {
- submitVerification();
- }
- }, [verificationCode]);
-
- if (status === Statuses.REQUESTED) {
- return (
-
- );
- }
-
- return (
-
- );
-};
-
-export { SmsVerification as default };
diff --git a/app/soapbox/features/verification/waitlist-page.tsx b/app/soapbox/features/verification/waitlist-page.tsx
deleted file mode 100644
index 431c76698..000000000
--- a/app/soapbox/features/verification/waitlist-page.tsx
+++ /dev/null
@@ -1,79 +0,0 @@
-import React, { useEffect } from 'react';
-import { FormattedMessage } from 'react-intl';
-import { Link } from 'react-router-dom';
-
-import { logOut } from 'soapbox/actions/auth';
-import { openModal } from 'soapbox/actions/modals';
-import LandingGradient from 'soapbox/components/landing-gradient';
-import SiteLogo from 'soapbox/components/site-logo';
-import { Button, Stack, Text } from 'soapbox/components/ui';
-import { useAppDispatch, useInstance, useOwnAccount } from 'soapbox/hooks';
-
-const WaitlistPage = () => {
- const dispatch = useAppDispatch();
- const instance = useInstance();
-
- const { account: me } = useOwnAccount();
- const isSmsVerified = me?.source?.sms_verified ?? true;
-
- const onClickLogOut: React.MouseEventHandler = (event) => {
- event.preventDefault();
- dispatch(logOut());
- };
-
- const openVerifySmsModal = () => dispatch(openModal('VERIFY_SMS'));
-
- useEffect(() => {
- if (!isSmsVerified) {
- openVerifySmsModal();
- }
- }, []);
-
- return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- );
-};
-
-export default WaitlistPage;
diff --git a/app/soapbox/hooks/__tests__/useRegistrationStatus.test.ts b/app/soapbox/hooks/__tests__/useRegistrationStatus.test.ts
deleted file mode 100644
index 465ca3992..000000000
--- a/app/soapbox/hooks/__tests__/useRegistrationStatus.test.ts
+++ /dev/null
@@ -1,46 +0,0 @@
-import { storeClosed, storeOpen, storePepeClosed, storePepeOpen } from 'soapbox/jest/mock-stores';
-import { renderHook } from 'soapbox/jest/test-helpers';
-
-import { useRegistrationStatus } from '../useRegistrationStatus';
-
-describe('useRegistrationStatus()', () => {
- test('Registrations open', () => {
- const { result } = renderHook(useRegistrationStatus, undefined, storeOpen);
-
- expect(result.current).toMatchObject({
- isOpen: true,
- pepeEnabled: false,
- pepeOpen: false,
- });
- });
-
- test('Registrations closed', () => {
- const { result } = renderHook(useRegistrationStatus, undefined, storeClosed);
-
- expect(result.current).toMatchObject({
- isOpen: false,
- pepeEnabled: false,
- pepeOpen: false,
- });
- });
-
- test('Registrations closed, Pepe enabled & open', () => {
- const { result } = renderHook(useRegistrationStatus, undefined, storePepeOpen);
-
- expect(result.current).toMatchObject({
- isOpen: true,
- pepeEnabled: true,
- pepeOpen: true,
- });
- });
-
- test('Registrations closed, Pepe enabled & closed', () => {
- const { result } = renderHook(useRegistrationStatus, undefined, storePepeClosed);
-
- expect(result.current).toMatchObject({
- isOpen: false,
- pepeEnabled: true,
- pepeOpen: false,
- });
- });
-});
diff --git a/app/soapbox/hooks/useRegistrationStatus.ts b/app/soapbox/hooks/useRegistrationStatus.ts
deleted file mode 100644
index 6ede86941..000000000
--- a/app/soapbox/hooks/useRegistrationStatus.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-import { useAppSelector } from './useAppSelector';
-import { useFeatures } from './useFeatures';
-import { useInstance } from './useInstance';
-import { useSoapboxConfig } from './useSoapboxConfig';
-
-export const useRegistrationStatus = () => {
- const instance = useInstance();
- const features = useFeatures();
- const soapboxConfig = useSoapboxConfig();
-
- const pepeOpen = useAppSelector(state => state.verification.instance.get('registrations') === true);
- const pepeEnabled = soapboxConfig.getIn(['extensions', 'pepe', 'enabled']) === true;
-
- return {
- /** Registrations are open, either through Pepe or traditional account creation. */
- isOpen: (features.accountCreation && instance.registrations) || (pepeEnabled && pepeOpen),
- /** Whether Pepe is open. */
- pepeOpen,
- /** Whether Pepe is enabled. */
- pepeEnabled,
- };
-};
\ No newline at end of file
diff --git a/app/soapbox/jest/mock-stores.tsx b/app/soapbox/jest/mock-stores.tsx
deleted file mode 100644
index e8969780b..000000000
--- a/app/soapbox/jest/mock-stores.tsx
+++ /dev/null
@@ -1,42 +0,0 @@
-import { fromJS } from 'immutable';
-
-import alexJson from 'soapbox/__fixtures__/pleroma-account.json';
-import { normalizeInstance } from 'soapbox/normalizers';
-
-import { buildAccount } from './factory';
-
-/** Store with registrations open. */
-const storeOpen = { instance: normalizeInstance({ registrations: true }) };
-
-/** Store with registrations closed. */
-const storeClosed = { instance: normalizeInstance({ registrations: false }) };
-
-/** Store with registrations closed, and Pepe enabled & open. */
-const storePepeOpen = {
- instance: normalizeInstance({ registrations: false }),
- soapbox: fromJS({ extensions: { pepe: { enabled: true } } }),
- verification: { instance: fromJS({ registrations: true }) },
-};
-
-/** Store with registrations closed, and Pepe enabled & closed. */
-const storePepeClosed = {
- instance: normalizeInstance({ registrations: false }),
- soapbox: fromJS({ extensions: { pepe: { enabled: true } } }),
- verification: { instance: fromJS({ registrations: false }) },
-};
-
-/** Store with a logged-in user. */
-const storeLoggedIn = {
- me: alexJson.id,
- accounts: {
- [alexJson.id]: buildAccount(alexJson as any),
- },
-};
-
-export {
- storeOpen,
- storeClosed,
- storePepeOpen,
- storePepeClosed,
- storeLoggedIn,
-};
\ No newline at end of file
diff --git a/app/soapbox/queries/ads.ts b/app/soapbox/queries/ads.ts
deleted file mode 100644
index c45bf8b33..000000000
--- a/app/soapbox/queries/ads.ts
+++ /dev/null
@@ -1,42 +0,0 @@
-import { useQuery } from '@tanstack/react-query';
-
-import { Ad, getProvider } from 'soapbox/features/ads/providers';
-import { useAppDispatch } from 'soapbox/hooks';
-import { adSchema } from 'soapbox/schemas';
-import { filteredArray } from 'soapbox/schemas/utils';
-import { isExpired } from 'soapbox/utils/ads';
-
-const AdKeys = {
- ads: ['ads'] as const,
-};
-
-function useAds() {
- const dispatch = useAppDispatch();
-
- const getAds = async () => {
- return dispatch(async (_, getState) => {
- const provider = await getProvider(getState);
- if (provider) {
- return provider.getAds(getState);
- } else {
- return [];
- }
- });
- };
-
- const result = useQuery(AdKeys.ads, getAds, {
- placeholderData: [],
- });
-
- // Filter out expired ads.
- const data = filteredArray(adSchema)
- .parse(result.data)
- .filter(ad => !isExpired(ad));
-
- return {
- ...result,
- data,
- };
-}
-
-export { useAds as default, AdKeys };
diff --git a/app/soapbox/reducers/__tests__/verification.test.ts b/app/soapbox/reducers/__tests__/verification.test.ts
deleted file mode 100644
index f503de443..000000000
--- a/app/soapbox/reducers/__tests__/verification.test.ts
+++ /dev/null
@@ -1,177 +0,0 @@
-import { Map as ImmutableMap, Record as ImmutableRecord } from 'immutable';
-
-import {
- Challenge,
- FETCH_CHALLENGES_SUCCESS,
- FETCH_TOKEN_SUCCESS,
- SET_CHALLENGES_COMPLETE,
- SET_LOADING,
- SET_NEXT_CHALLENGE,
-} from 'soapbox/actions/verification';
-
-import reducer from '../verification';
-
-describe('verfication reducer', () => {
- it('returns the initial state', () => {
- expect(reducer(undefined, {} as any)).toMatchObject({
- ageMinimum: null,
- currentChallenge: null,
- isLoading: false,
- isComplete: false,
- token: null,
- instance: ImmutableMap(),
- });
- });
-
- describe('FETCH_CHALLENGES_SUCCESS', () => {
- it('sets the state', () => {
- const state = ImmutableRecord({
- ageMinimum: null,
- currentChallenge: null,
- isLoading: true,
- isComplete: null,
- token: null,
- instance: ImmutableMap(),
- })();
- const action = {
- type: FETCH_CHALLENGES_SUCCESS,
- ageMinimum: 13,
- currentChallenge: 'email',
- isComplete: false,
- };
- const expected = {
- ageMinimum: 13,
- currentChallenge: 'email',
- isLoading: false,
- isComplete: false,
- token: null,
- instance: ImmutableMap(),
- };
-
- expect(reducer(state, action)).toMatchObject(expected);
- });
- });
-
- describe('FETCH_TOKEN_SUCCESS', () => {
- it('sets the state', () => {
- const state = ImmutableRecord({
- ageMinimum: null,
- currentChallenge: 'email' as Challenge,
- isLoading: true,
- isComplete: false,
- token: null,
- instance: ImmutableMap(),
- })();
- const action = { type: FETCH_TOKEN_SUCCESS, value: '123' };
- const expected = {
- ageMinimum: null,
- currentChallenge: 'email',
- isLoading: false,
- isComplete: false,
- token: '123',
- instance: ImmutableMap(),
- };
-
- expect(reducer(state, action)).toMatchObject(expected);
- });
- });
-
- describe('SET_CHALLENGES_COMPLETE', () => {
- it('sets the state', () => {
- const state = ImmutableRecord({
- ageMinimum: null,
- currentChallenge: null,
- isLoading: true,
- isComplete: false,
- token: null,
- instance: ImmutableMap(),
- })();
- const action = { type: SET_CHALLENGES_COMPLETE };
- const expected = {
- ageMinimum: null,
- currentChallenge: null,
- isLoading: false,
- isComplete: true,
- token: null,
- instance: ImmutableMap(),
- };
-
- expect(reducer(state, action)).toMatchObject(expected);
- });
- });
-
- describe('SET_NEXT_CHALLENGE', () => {
- it('sets the state', () => {
- const state = ImmutableRecord({
- ageMinimum: null,
- currentChallenge: null,
- isLoading: true,
- isComplete: false,
- token: null,
- instance: ImmutableMap(),
- })();
- const action = {
- type: SET_NEXT_CHALLENGE,
- challenge: 'sms',
- };
- const expected = {
- ageMinimum: null,
- currentChallenge: 'sms',
- isLoading: false,
- isComplete: false,
- token: null,
- instance: ImmutableMap(),
- };
-
- expect(reducer(state, action)).toMatchObject(expected);
- });
- });
-
- describe('SET_LOADING with no value', () => {
- it('sets the state', () => {
- const state = ImmutableRecord({
- ageMinimum: null,
- currentChallenge: null,
- isLoading: false,
- isComplete: false,
- token: null,
- instance: ImmutableMap(),
- })();
- const action = { type: SET_LOADING };
- const expected = {
- ageMinimum: null,
- currentChallenge: null,
- isLoading: true,
- isComplete: false,
- token: null,
- instance: ImmutableMap(),
- };
-
- expect(reducer(state, action)).toMatchObject(expected);
- });
- });
-
- describe('SET_LOADING with a value', () => {
- it('sets the state', () => {
- const state = ImmutableRecord({
- ageMinimum: null,
- currentChallenge: null,
- isLoading: true,
- isComplete: false,
- token: null,
- instance: ImmutableMap(),
- })();
- const action = { type: SET_LOADING, value: false };
- const expected = {
- ageMinimum: null,
- currentChallenge: null,
- isLoading: false,
- isComplete: false,
- token: null,
- instance: ImmutableMap(),
- };
-
- expect(reducer(state, action)).toMatchObject(expected);
- });
- });
-});
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/reducers/verification.ts b/app/soapbox/reducers/verification.ts
deleted file mode 100644
index 94abdeeb1..000000000
--- a/app/soapbox/reducers/verification.ts
+++ /dev/null
@@ -1,51 +0,0 @@
-import { Map as ImmutableMap, Record as ImmutableRecord, fromJS } from 'immutable';
-
-import {
- PEPE_FETCH_INSTANCE_SUCCESS,
- FETCH_CHALLENGES_SUCCESS,
- FETCH_TOKEN_SUCCESS,
- SET_CHALLENGES_COMPLETE,
- SET_LOADING,
- SET_NEXT_CHALLENGE,
- Challenge,
-} from '../actions/verification';
-
-import type { AnyAction } from 'redux';
-
-const ReducerRecord = ImmutableRecord({
- ageMinimum: null as string | null,
- currentChallenge: null as Challenge | null,
- isLoading: false,
- isComplete: false as boolean | null,
- token: null as string | null,
- instance: ImmutableMap(),
-});
-
-export default function verification(state = ReducerRecord(), action: AnyAction) {
- switch (action.type) {
- case PEPE_FETCH_INSTANCE_SUCCESS:
- return state.set('instance', ImmutableMap(fromJS(action.instance)));
- case FETCH_CHALLENGES_SUCCESS:
- return state
- .set('ageMinimum', action.ageMinimum)
- .set('currentChallenge', action.currentChallenge)
- .set('isLoading', false)
- .set('isComplete', action.isComplete);
- case FETCH_TOKEN_SUCCESS:
- return state
- .set('isLoading', false)
- .set('token', action.value);
- case SET_CHALLENGES_COMPLETE:
- return state
- .set('isLoading', false)
- .set('isComplete', true);
- case SET_NEXT_CHALLENGE:
- return state
- .set('currentChallenge', action.challenge)
- .set('isLoading', false);
- case SET_LOADING:
- return state.set('isLoading', typeof action.value === 'boolean' ? action.value : true);
- default:
- return state;
- }
-}
diff --git a/app/soapbox/schemas/soapbox/ad.ts b/app/soapbox/schemas/soapbox/ad.ts
deleted file mode 100644
index 40dc05fb3..000000000
--- a/app/soapbox/schemas/soapbox/ad.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-import { z } from 'zod';
-
-import { cardSchema } from '../card';
-
-const adSchema = z.object({
- card: cardSchema,
- impression: z.string().optional().catch(undefined),
- expires_at: z.string().datetime().optional().catch(undefined),
- reason: z.string().optional().catch(undefined),
-});
-
-type Ad = z.infer;
-
-export { adSchema, type Ad };
\ No newline at end of file
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/app/soapbox/utils/__tests__/ads.test.ts b/app/soapbox/utils/__tests__/ads.test.ts
deleted file mode 100644
index 5ceb9d45b..000000000
--- a/app/soapbox/utils/__tests__/ads.test.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-import { buildAd } from 'soapbox/jest/factory';
-
-import { isExpired } from '../ads';
-
-/** 3 minutes in milliseconds. */
-const threeMins = 3 * 60 * 1000;
-
-/** 5 minutes in milliseconds. */
-const fiveMins = 5 * 60 * 1000;
-
-test('isExpired()', () => {
- const now = new Date();
- const iso = now.toISOString();
- const epoch = now.getTime();
-
- // Sanity tests.
- expect(isExpired(buildAd({ expires_at: iso }))).toBe(true);
- expect(isExpired(buildAd({ expires_at: new Date(epoch + 999999).toISOString() }))).toBe(false);
-
- // Testing the 5-minute mark.
- expect(isExpired(buildAd({ expires_at: new Date(epoch + threeMins).toISOString() }), fiveMins)).toBe(true);
- expect(isExpired(buildAd({ expires_at: new Date(epoch + fiveMins + 1000).toISOString() }), fiveMins)).toBe(false);
-});
diff --git a/app/soapbox/utils/ads.ts b/app/soapbox/utils/ads.ts
deleted file mode 100644
index ed2bf0cad..000000000
--- a/app/soapbox/utils/ads.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-import type { Ad } from 'soapbox/schemas';
-
-/** Time (ms) window to not display an ad if it's about to expire. */
-const AD_EXPIRY_THRESHOLD = 5 * 60 * 1000;
-
-/** Whether the ad is expired or about to expire. */
-const isExpired = (ad: Pick, threshold = AD_EXPIRY_THRESHOLD): boolean => {
- if (ad.expires_at) {
- const now = new Date();
- return now.getTime() > (new Date(ad.expires_at).getTime() - threshold);
- } else {
- return false;
- }
-};
-
-export { isExpired };
diff --git a/app/soapbox/utils/ethereum.ts b/app/soapbox/utils/ethereum.ts
deleted file mode 100644
index 63dace72d..000000000
--- a/app/soapbox/utils/ethereum.ts
+++ /dev/null
@@ -1,28 +0,0 @@
-import type { MetaMaskInpageProvider } from '@metamask/providers';
-
-export const ethereum: () => MetaMaskInpageProvider | undefined = () => (window as any).ethereum;
-
-export const hasEthereum = () => Boolean(ethereum());
-
-// Requests an Ethereum wallet from the browser
-// Returns a Promise containing the Ethereum wallet address (string).
-export const getWallet: () => Promise = () => {
- return ethereum()!.request({ method: 'eth_requestAccounts' })
- .then((wallets) => (wallets as Array)[0]);
-};
-
-// Asks the browser to sign a message with Ethereum.
-// Returns a Promise containing the signature (string).
-export const signMessage = (wallet: string, message: string) => {
- return ethereum()!.request({ method: 'personal_sign', params: [message, wallet] });
-};
-
-// Combines the above functions.
-// Returns an object with the `wallet` and `signature`
-export const getWalletAndSign = (message: string) => {
- return getWallet().then(wallet => {
- return signMessage(wallet, message).then(signature => {
- return { wallet, signature };
- });
- });
-};
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 @@
-