Merge remote-tracking branch 'origin/develop' into customize-redirect-from-root-when-not-logged-in-settings-input
This commit is contained in:
commit
1e07c03479
|
@ -18,7 +18,7 @@ module.exports = {
|
|||
ATTACHMENT_HOST: false,
|
||||
},
|
||||
|
||||
parser: 'babel-eslint',
|
||||
parser: '@babel/eslint-parser',
|
||||
|
||||
plugins: [
|
||||
'react',
|
||||
|
|
|
@ -3,6 +3,9 @@ image: node:18
|
|||
variables:
|
||||
NODE_ENV: test
|
||||
|
||||
default:
|
||||
interruptible: true
|
||||
|
||||
cache: &cache
|
||||
key:
|
||||
files:
|
||||
|
@ -15,6 +18,7 @@ stages:
|
|||
- deps
|
||||
- test
|
||||
- deploy
|
||||
- release
|
||||
|
||||
deps:
|
||||
stage: deps
|
||||
|
@ -25,7 +29,6 @@ deps:
|
|||
cache:
|
||||
<<: *cache
|
||||
policy: push
|
||||
interruptible: true
|
||||
|
||||
danger:
|
||||
stage: test
|
||||
|
@ -33,8 +36,10 @@ danger:
|
|||
# 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
|
||||
interruptible: true
|
||||
|
||||
lint-js:
|
||||
stage: test
|
||||
|
@ -47,7 +52,6 @@ lint-js:
|
|||
- "**/*.tsx"
|
||||
- ".eslintignore"
|
||||
- ".eslintrc.js"
|
||||
interruptible: true
|
||||
|
||||
lint-sass:
|
||||
stage: test
|
||||
|
@ -57,7 +61,6 @@ lint-sass:
|
|||
- "**/*.scss"
|
||||
- "**/*.css"
|
||||
- ".stylelintrc.json"
|
||||
interruptible: true
|
||||
|
||||
jest:
|
||||
stage: test
|
||||
|
@ -80,27 +83,30 @@ jest:
|
|||
coverage_report:
|
||||
coverage_format: cobertura
|
||||
path: .coverage/cobertura-coverage.xml
|
||||
interruptible: true
|
||||
|
||||
nginx-test:
|
||||
stage: test
|
||||
image: nginx:latest
|
||||
before_script: cp installation/mastodon.conf /etc/nginx/conf.d/default.conf
|
||||
before_script:
|
||||
- cp installation/mastodon.conf /etc/nginx/conf.d/default.conf
|
||||
script: nginx -t
|
||||
only:
|
||||
changes:
|
||||
- "installation/mastodon.conf"
|
||||
interruptible: true
|
||||
|
||||
build-production:
|
||||
stage: test
|
||||
script: yarn build
|
||||
script:
|
||||
- yarn build
|
||||
- yarn manage:translations en
|
||||
# Fail if files got changed.
|
||||
# https://stackoverflow.com/a/9066385
|
||||
- git diff --quiet
|
||||
variables:
|
||||
NODE_ENV: production
|
||||
artifacts:
|
||||
paths:
|
||||
- static
|
||||
interruptible: true
|
||||
|
||||
docs-deploy:
|
||||
stage: deploy
|
||||
|
@ -110,22 +116,10 @@ docs-deploy:
|
|||
script:
|
||||
- curl -X POST -F"token=$CI_JOB_TOKEN" -F'ref=master' https://gitlab.com/api/v4/projects/15685485/trigger/pipeline
|
||||
only:
|
||||
refs:
|
||||
- develop
|
||||
variables:
|
||||
- $CI_DEFAULT_BRANCH == $CI_COMMIT_REF_NAME
|
||||
changes:
|
||||
- "docs/**/*"
|
||||
interruptible: true
|
||||
|
||||
# Supposed to fail when translations are outdated, instead always passes
|
||||
#
|
||||
# i18n:
|
||||
# stage: build
|
||||
# script: yarn manage:translations
|
||||
# variables:
|
||||
# NODE_ENV: development
|
||||
# before_script:
|
||||
# - yarn
|
||||
# - yarn build
|
||||
|
||||
review:
|
||||
stage: deploy
|
||||
|
@ -135,7 +129,6 @@ review:
|
|||
script:
|
||||
- npx -y surge static $CI_COMMIT_REF_SLUG.git.soapbox.pub
|
||||
allow_failure: true
|
||||
interruptible: true
|
||||
|
||||
pages:
|
||||
stage: deploy
|
||||
|
@ -149,15 +142,14 @@ pages:
|
|||
paths:
|
||||
- public
|
||||
only:
|
||||
refs:
|
||||
- develop
|
||||
interruptible: true
|
||||
variables:
|
||||
- $CI_DEFAULT_BRANCH == $CI_COMMIT_REF_NAME
|
||||
|
||||
docker:
|
||||
stage: deploy
|
||||
image: docker:20.10.17
|
||||
image: docker:20.10.22
|
||||
services:
|
||||
- docker:20.10.17-dind
|
||||
- docker:20.10.22-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
|
||||
|
@ -166,9 +158,17 @@ docker:
|
|||
- docker build -t $CI_REGISTRY_IMAGE .
|
||||
- docker push $CI_REGISTRY_IMAGE
|
||||
only:
|
||||
refs:
|
||||
- develop
|
||||
interruptible: true
|
||||
variables:
|
||||
- $CI_DEFAULT_BRANCH == $CI_COMMIT_REF_NAME
|
||||
|
||||
release:
|
||||
stage: release
|
||||
rules:
|
||||
- if: $CI_COMMIT_TAG
|
||||
script:
|
||||
- npx ts-node ./scripts/do-release.ts
|
||||
interruptible: false
|
||||
|
||||
include:
|
||||
- template: Jobs/Dependency-Scanning.gitlab-ci.yml
|
||||
- template: Jobs/Dependency-Scanning.gitlab-ci.yml
|
||||
- template: Security/License-Scanning.gitlab-ci.yml
|
|
@ -1,16 +1,22 @@
|
|||
{
|
||||
"extends": ["stylelint-config-standard"],
|
||||
"ignoreFiles": ["app/styles/reset.scss"],
|
||||
"plugins": ["stylelint-scss"],
|
||||
"extends": ["stylelint-config-standard-scss"],
|
||||
"rules": {
|
||||
"alpha-value-notation": null,
|
||||
"at-rule-no-unknown": null,
|
||||
"at-rule-empty-line-before": ["always", { "ignore": ["after-comment", "first-nested", "inside-block", "blockless-after-same-name-blockless", "blockless-after-blockless"] }],
|
||||
"color-function-notation": null,
|
||||
"custom-property-pattern": null,
|
||||
"declaration-block-no-redundant-longhand-properties": null,
|
||||
"declaration-colon-newline-after": null,
|
||||
"declaration-empty-line-before": "never",
|
||||
"font-family-no-missing-generic-family-keyword": [true, { "ignoreFontFamilies": ["ForkAwesome", "Font Awesome 5 Free", "OpenDyslexic", "soapbox"] }],
|
||||
"font-family-no-missing-generic-family-keyword": [true, { "ignoreFontFamilies": ["ForkAwesome", "Font Awesome 5 Free"] }],
|
||||
"max-line-length": null,
|
||||
"no-descending-specificity": null,
|
||||
"no-duplicate-selectors": null,
|
||||
"scss/at-rule-no-unknown": [true, { "ignoreAtRules": ["/tailwind/", "layer"]}],
|
||||
"no-invalid-position-at-import-rule": null
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1 +1 @@
|
|||
nodejs 18.2.0
|
||||
nodejs 18.13.0
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
"dbaeumer.vscode-eslint",
|
||||
"bradlc.vscode-tailwindcss",
|
||||
"stylelint.vscode-stylelint",
|
||||
"wix.vscode-import-cost"
|
||||
"wix.vscode-import-cost",
|
||||
"redhat.vscode-yaml"
|
||||
]
|
||||
}
|
||||
|
|
|
@ -5,5 +5,15 @@
|
|||
"*.conf.template": "properties"
|
||||
},
|
||||
"files.eol": "\n",
|
||||
"files.insertFinalNewline": false
|
||||
"files.insertFinalNewline": false,
|
||||
"json.schemas": [
|
||||
{
|
||||
"fileMatch": [".lintstagedrc.json"],
|
||||
"url": "https://json.schemastore.org/lintstagedrc.schema.json"
|
||||
},
|
||||
{
|
||||
"fileMatch": ["renovate.json"],
|
||||
"url": "https://docs.renovatebot.com/renovate-schema.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
17
CHANGELOG.md
17
CHANGELOG.md
|
@ -8,12 +8,29 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
|
||||
### Added
|
||||
- Compatibility: rudimentary support for Takahē.
|
||||
- UI: added backdrop blur behind modals.
|
||||
- Admin: let admins configure media preview for attachment thumbnails.
|
||||
- Login: accept `?server` param in external login, eg `fe.soapbox.pub/login/external?server=gleasonator.com`.
|
||||
|
||||
### Changed
|
||||
- Posts: letterbox images to 19:6 again.
|
||||
- Status Info: moved context (repost, pinned) to improve UX.
|
||||
- Posts: remove file icon from empty link previews.
|
||||
|
||||
### Fixed
|
||||
- Layout: use accent color for "floating action button" (mobile compose button).
|
||||
- ServiceWorker: don't serve favicon, robots.txt, and others from ServiceWorker.
|
||||
- Datepicker: correctly default to the current year.
|
||||
- Scheduled posts: fix page crashing on deleting a scheduled post.
|
||||
- Events: don't crash when searching for a location.
|
||||
- Search: fixes an abort error when using the navbar search component.
|
||||
- Posts: fix monospace font in Markdown code blocks.
|
||||
- Modals: fix action buttons overflow
|
||||
- Editing: don't insert edited posts to the top of the feed.
|
||||
- Modals: close modal when navigating to a different page.
|
||||
- Modals: fix "View context" button in media modal.
|
||||
- Posts: let unauthenticated users to translate posts if allowed by backend.
|
||||
- Chats: fix jumpy scrollbar.
|
||||
|
||||
## [3.0.0] - 2022-12-25
|
||||
|
||||
|
|
|
@ -1,17 +0,0 @@
|
|||
import loadPolyfills from './soapbox/load-polyfills';
|
||||
|
||||
// Load iframe event listener
|
||||
require('./soapbox/iframe');
|
||||
|
||||
// @ts-ignore
|
||||
require.context('./assets/images/', true);
|
||||
|
||||
// Load stylesheet
|
||||
require('react-datepicker/dist/react-datepicker.css');
|
||||
require('./styles/application.scss');
|
||||
|
||||
loadPolyfills().then(() => {
|
||||
require('./soapbox/main').default();
|
||||
}).catch(e => {
|
||||
console.error(e);
|
||||
});
|
|
@ -1,94 +0,0 @@
|
|||
Copyright (c) 2019-07-29, Abbie Gonzalez (https://abbiecod.es|support@abbiecod.es),
|
||||
with Reserved Font Name OpenDyslexic.
|
||||
Copyright (c) 12/2012 - 2019
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
http://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -2,7 +2,9 @@ import { Map as ImmutableMap } from 'immutable';
|
|||
|
||||
import { __stub } from 'soapbox/api';
|
||||
import { mockStore, rootState } from 'soapbox/jest/test-helpers';
|
||||
import { AccountRecord } from 'soapbox/normalizers';
|
||||
|
||||
import { AuthUserRecord, ReducerRecord } from '../../reducers/auth';
|
||||
import {
|
||||
fetchMe, patchMe,
|
||||
} from '../me';
|
||||
|
@ -38,18 +40,18 @@ describe('fetchMe()', () => {
|
|||
|
||||
beforeEach(() => {
|
||||
const state = rootState
|
||||
.set('auth', ImmutableMap({
|
||||
.set('auth', ReducerRecord({
|
||||
me: accountUrl,
|
||||
users: ImmutableMap({
|
||||
[accountUrl]: ImmutableMap({
|
||||
[accountUrl]: AuthUserRecord({
|
||||
'access_token': token,
|
||||
}),
|
||||
}),
|
||||
}))
|
||||
.set('accounts', ImmutableMap({
|
||||
[accountUrl]: {
|
||||
[accountUrl]: AccountRecord({
|
||||
url: accountUrl,
|
||||
},
|
||||
}),
|
||||
}) as any);
|
||||
store = mockStore(state);
|
||||
});
|
||||
|
@ -112,4 +114,4 @@ describe('patchMe()', () => {
|
|||
expect(actions).toEqual(expectedActions);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -10,10 +10,10 @@ import {
|
|||
} from './importer';
|
||||
|
||||
import type { AxiosError, CancelToken } from 'axios';
|
||||
import type { History } from 'history';
|
||||
import type { Map as ImmutableMap } from 'immutable';
|
||||
import type { AppDispatch, RootState } from 'soapbox/store';
|
||||
import type { APIEntity, Status } from 'soapbox/types/entities';
|
||||
import type { History } from 'soapbox/types/history';
|
||||
|
||||
const ACCOUNT_CREATE_REQUEST = 'ACCOUNT_CREATE_REQUEST';
|
||||
const ACCOUNT_CREATE_SUCCESS = 'ACCOUNT_CREATE_SUCCESS';
|
||||
|
|
|
@ -77,6 +77,16 @@ const ADMIN_USERS_UNSUGGEST_REQUEST = 'ADMIN_USERS_UNSUGGEST_REQUEST';
|
|||
const ADMIN_USERS_UNSUGGEST_SUCCESS = 'ADMIN_USERS_UNSUGGEST_SUCCESS';
|
||||
const ADMIN_USERS_UNSUGGEST_FAIL = 'ADMIN_USERS_UNSUGGEST_FAIL';
|
||||
|
||||
const ADMIN_USER_INDEX_EXPAND_FAIL = 'ADMIN_USER_INDEX_EXPAND_FAIL';
|
||||
const ADMIN_USER_INDEX_EXPAND_REQUEST = 'ADMIN_USER_INDEX_EXPAND_REQUEST';
|
||||
const ADMIN_USER_INDEX_EXPAND_SUCCESS = 'ADMIN_USER_INDEX_EXPAND_SUCCESS';
|
||||
|
||||
const ADMIN_USER_INDEX_FETCH_FAIL = 'ADMIN_USER_INDEX_FETCH_FAIL';
|
||||
const ADMIN_USER_INDEX_FETCH_REQUEST = 'ADMIN_USER_INDEX_FETCH_REQUEST';
|
||||
const ADMIN_USER_INDEX_FETCH_SUCCESS = 'ADMIN_USER_INDEX_FETCH_SUCCESS';
|
||||
|
||||
const ADMIN_USER_INDEX_QUERY_SET = 'ADMIN_USER_INDEX_QUERY_SET';
|
||||
|
||||
const nicknamesFromIds = (getState: () => RootState, ids: string[]) => ids.map(id => getState().accounts.get(id)!.acct);
|
||||
|
||||
const fetchConfig = () =>
|
||||
|
@ -544,6 +554,50 @@ const unsuggestUsers = (accountIds: string[]) =>
|
|||
});
|
||||
};
|
||||
|
||||
const setUserIndexQuery = (query: string) => ({ type: ADMIN_USER_INDEX_QUERY_SET, query });
|
||||
|
||||
const fetchUserIndex = () =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
const { filters, page, query, pageSize, isLoading } = getState().admin_user_index;
|
||||
|
||||
if (isLoading) return;
|
||||
|
||||
dispatch({ type: ADMIN_USER_INDEX_FETCH_REQUEST });
|
||||
|
||||
dispatch(fetchUsers(filters.toJS() as string[], page + 1, query, pageSize))
|
||||
.then((data: any) => {
|
||||
if (data.error) {
|
||||
dispatch({ type: ADMIN_USER_INDEX_FETCH_FAIL });
|
||||
} else {
|
||||
const { users, count, next } = (data);
|
||||
dispatch({ type: ADMIN_USER_INDEX_FETCH_SUCCESS, users, count, next });
|
||||
}
|
||||
}).catch(() => {
|
||||
dispatch({ type: ADMIN_USER_INDEX_FETCH_FAIL });
|
||||
});
|
||||
};
|
||||
|
||||
const expandUserIndex = () =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
const { filters, page, query, pageSize, isLoading, next, loaded } = getState().admin_user_index;
|
||||
|
||||
if (!loaded || isLoading) return;
|
||||
|
||||
dispatch({ type: ADMIN_USER_INDEX_EXPAND_REQUEST });
|
||||
|
||||
dispatch(fetchUsers(filters.toJS() as string[], page + 1, query, pageSize, next))
|
||||
.then((data: any) => {
|
||||
if (data.error) {
|
||||
dispatch({ type: ADMIN_USER_INDEX_EXPAND_FAIL });
|
||||
} else {
|
||||
const { users, count, next } = (data);
|
||||
dispatch({ type: ADMIN_USER_INDEX_EXPAND_SUCCESS, users, count, next });
|
||||
}
|
||||
}).catch(() => {
|
||||
dispatch({ type: ADMIN_USER_INDEX_EXPAND_FAIL });
|
||||
});
|
||||
};
|
||||
|
||||
export {
|
||||
ADMIN_CONFIG_FETCH_REQUEST,
|
||||
ADMIN_CONFIG_FETCH_SUCCESS,
|
||||
|
@ -596,6 +650,13 @@ export {
|
|||
ADMIN_USERS_UNSUGGEST_REQUEST,
|
||||
ADMIN_USERS_UNSUGGEST_SUCCESS,
|
||||
ADMIN_USERS_UNSUGGEST_FAIL,
|
||||
ADMIN_USER_INDEX_EXPAND_FAIL,
|
||||
ADMIN_USER_INDEX_EXPAND_REQUEST,
|
||||
ADMIN_USER_INDEX_EXPAND_SUCCESS,
|
||||
ADMIN_USER_INDEX_FETCH_FAIL,
|
||||
ADMIN_USER_INDEX_FETCH_REQUEST,
|
||||
ADMIN_USER_INDEX_FETCH_SUCCESS,
|
||||
ADMIN_USER_INDEX_QUERY_SET,
|
||||
fetchConfig,
|
||||
updateConfig,
|
||||
updateSoapboxConfig,
|
||||
|
@ -622,4 +683,7 @@ export {
|
|||
setRole,
|
||||
suggestUsers,
|
||||
unsuggestUsers,
|
||||
setUserIndexQuery,
|
||||
fetchUserIndex,
|
||||
expandUserIndex,
|
||||
};
|
||||
|
|
|
@ -29,7 +29,6 @@ import api, { baseClient } from '../api';
|
|||
import { importFetchedAccount } from './importer';
|
||||
|
||||
import type { AxiosError } from 'axios';
|
||||
import type { Map as ImmutableMap } from 'immutable';
|
||||
import type { AppDispatch, RootState } from 'soapbox/store';
|
||||
|
||||
export const SWITCH_ACCOUNT = 'SWITCH_ACCOUNT';
|
||||
|
@ -94,11 +93,11 @@ const createAuthApp = () =>
|
|||
|
||||
const createAppToken = () =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
const app = getState().auth.get('app');
|
||||
const app = getState().auth.app;
|
||||
|
||||
const params = {
|
||||
client_id: app.get('client_id'),
|
||||
client_secret: app.get('client_secret'),
|
||||
client_id: app.client_id!,
|
||||
client_secret: app.client_secret!,
|
||||
redirect_uri: 'urn:ietf:wg:oauth:2.0:oob',
|
||||
grant_type: 'client_credentials',
|
||||
scope: getScopes(getState()),
|
||||
|
@ -111,11 +110,11 @@ const createAppToken = () =>
|
|||
|
||||
const createUserToken = (username: string, password: string) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
const app = getState().auth.get('app');
|
||||
const app = getState().auth.app;
|
||||
|
||||
const params = {
|
||||
client_id: app.get('client_id'),
|
||||
client_secret: app.get('client_secret'),
|
||||
client_id: app.client_id!,
|
||||
client_secret: app.client_secret!,
|
||||
redirect_uri: 'urn:ietf:wg:oauth:2.0:oob',
|
||||
grant_type: 'password',
|
||||
username: username,
|
||||
|
@ -127,32 +126,12 @@ const createUserToken = (username: string, password: string) =>
|
|||
.then((token: Record<string, string | number>) => dispatch(authLoggedIn(token)));
|
||||
};
|
||||
|
||||
export const refreshUserToken = () =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
const refreshToken = getState().auth.getIn(['user', 'refresh_token']);
|
||||
const app = getState().auth.get('app');
|
||||
|
||||
if (!refreshToken) return dispatch(noOp);
|
||||
|
||||
const params = {
|
||||
client_id: app.get('client_id'),
|
||||
client_secret: app.get('client_secret'),
|
||||
refresh_token: refreshToken,
|
||||
redirect_uri: 'urn:ietf:wg:oauth:2.0:oob',
|
||||
grant_type: 'refresh_token',
|
||||
scope: getScopes(getState()),
|
||||
};
|
||||
|
||||
return dispatch(obtainOAuthToken(params))
|
||||
.then((token: Record<string, string | number>) => dispatch(authLoggedIn(token)));
|
||||
};
|
||||
|
||||
export const otpVerify = (code: string, mfa_token: string) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
const app = getState().auth.get('app');
|
||||
const app = getState().auth.app;
|
||||
return api(getState, 'app').post('/oauth/mfa/challenge', {
|
||||
client_id: app.get('client_id'),
|
||||
client_secret: app.get('client_secret'),
|
||||
client_id: app.client_id,
|
||||
client_secret: app.client_secret,
|
||||
mfa_token: mfa_token,
|
||||
code: code,
|
||||
challenge_type: 'totp',
|
||||
|
@ -211,7 +190,7 @@ export const logIn = (username: string, password: string) =>
|
|||
(dispatch: AppDispatch) => dispatch(getAuthApp()).then(() => {
|
||||
return dispatch(createUserToken(normalizeUsername(username), password));
|
||||
}).catch((error: AxiosError) => {
|
||||
if ((error.response?.data as any).error === 'mfa_required') {
|
||||
if ((error.response?.data as any)?.error === 'mfa_required') {
|
||||
// If MFA is required, throw the error and handle it in the component.
|
||||
throw error;
|
||||
} else {
|
||||
|
@ -233,9 +212,9 @@ export const logOut = () =>
|
|||
if (!account) return dispatch(noOp);
|
||||
|
||||
const params = {
|
||||
client_id: state.auth.getIn(['app', 'client_id']),
|
||||
client_secret: state.auth.getIn(['app', 'client_secret']),
|
||||
token: state.auth.getIn(['users', account.url, 'access_token']),
|
||||
client_id: state.auth.app.client_id!,
|
||||
client_secret: state.auth.app.client_secret!,
|
||||
token: state.auth.users.get(account.url)!.access_token,
|
||||
};
|
||||
|
||||
return dispatch(revokeOAuthToken(params))
|
||||
|
@ -263,10 +242,10 @@ export const switchAccount = (accountId: string, background = false) =>
|
|||
export const fetchOwnAccounts = () =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
const state = getState();
|
||||
return state.auth.get('users').forEach((user: ImmutableMap<string, string>) => {
|
||||
const account = state.accounts.get(user.get('id'));
|
||||
return state.auth.users.forEach((user) => {
|
||||
const account = state.accounts.get(user.id);
|
||||
if (!account) {
|
||||
dispatch(verifyCredentials(user.get('access_token')!, user.get('url')));
|
||||
dispatch(verifyCredentials(user.access_token, user.url));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
|
|
@ -6,8 +6,8 @@ import { getFeatures } from 'soapbox/utils/features';
|
|||
|
||||
import api, { getLinks } from '../api';
|
||||
|
||||
import type { History } from 'history';
|
||||
import type { AppDispatch, RootState } from 'soapbox/store';
|
||||
import type { History } from 'soapbox/types/history';
|
||||
|
||||
const CHATS_FETCH_REQUEST = 'CHATS_FETCH_REQUEST';
|
||||
const CHATS_FETCH_SUCCESS = 'CHATS_FETCH_SUCCESS';
|
||||
|
|
|
@ -19,11 +19,11 @@ import { openModal, closeModal } from './modals';
|
|||
import { getSettings } from './settings';
|
||||
import { createStatus } from './statuses';
|
||||
|
||||
import type { History } from 'history';
|
||||
import type { Emoji } from 'soapbox/components/autosuggest-emoji';
|
||||
import type { AutoSuggestion } from 'soapbox/components/autosuggest-input';
|
||||
import type { AppDispatch, RootState } from 'soapbox/store';
|
||||
import type { Account, APIEntity, Status, Tag } from 'soapbox/types/entities';
|
||||
import type { History } from 'soapbox/types/history';
|
||||
|
||||
const { CancelToken, isCancel } = axios;
|
||||
|
||||
|
|
|
@ -10,12 +10,12 @@ import api from '../api';
|
|||
|
||||
const getMeUrl = (state: RootState) => {
|
||||
const me = state.me;
|
||||
return state.accounts.getIn([me, 'url']);
|
||||
return state.accounts.get(me)?.url;
|
||||
};
|
||||
|
||||
/** Figure out the appropriate instance to fetch depending on the state */
|
||||
export const getHost = (state: RootState) => {
|
||||
const accountUrl = getMeUrl(state) || getAuthUserUrl(state);
|
||||
const accountUrl = getMeUrl(state) || getAuthUserUrl(state) as string;
|
||||
|
||||
try {
|
||||
return new URL(accountUrl).host;
|
||||
|
|
|
@ -6,7 +6,7 @@ import api from '../api';
|
|||
import { loadCredentials } from './auth';
|
||||
import { importFetchedAccount } from './importer';
|
||||
|
||||
import type { AxiosError, AxiosRequestHeaders } from 'axios';
|
||||
import type { AxiosError, RawAxiosRequestHeaders } from 'axios';
|
||||
import type { AppDispatch, RootState } from 'soapbox/store';
|
||||
import type { APIEntity } from 'soapbox/types/entities';
|
||||
|
||||
|
@ -30,8 +30,8 @@ const getMeUrl = (state: RootState) => {
|
|||
|
||||
const getMeToken = (state: RootState) => {
|
||||
// Fallback for upgrading IDs to URLs
|
||||
const accountUrl = getMeUrl(state) || state.auth.get('me');
|
||||
return state.auth.getIn(['users', accountUrl, 'access_token']);
|
||||
const accountUrl = getMeUrl(state) || state.auth.me;
|
||||
return state.auth.users.get(accountUrl!)?.access_token;
|
||||
};
|
||||
|
||||
const fetchMe = () =>
|
||||
|
@ -46,7 +46,7 @@ const fetchMe = () =>
|
|||
}
|
||||
|
||||
dispatch(fetchMeRequest());
|
||||
return dispatch(loadCredentials(token, accountUrl))
|
||||
return dispatch(loadCredentials(token, accountUrl!))
|
||||
.catch(error => dispatch(fetchMeFail(error)));
|
||||
};
|
||||
|
||||
|
@ -66,7 +66,7 @@ const patchMe = (params: Record<string, any>, isFormData = false) =>
|
|||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
dispatch(patchMeRequest());
|
||||
|
||||
const headers: AxiosRequestHeaders = isFormData ? {
|
||||
const headers: RawAxiosRequestHeaders = isFormData ? {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
} : {};
|
||||
|
||||
|
|
|
@ -107,7 +107,10 @@ const updateNotificationsQueue = (notification: APIEntity, intlMessages: Record<
|
|||
|
||||
// Desktop notifications
|
||||
try {
|
||||
if (showAlert && !filtered) {
|
||||
// eslint-disable-next-line compat/compat
|
||||
const isNotificationsEnabled = window.Notification?.permission === 'granted';
|
||||
|
||||
if (showAlert && !filtered && isNotificationsEnabled) {
|
||||
const title = new IntlMessageFormat(intlMessages[`notification.${notification.type}`], intlLocale).format({ name: notification.account.display_name.length > 0 ? notification.account.display_name : notification.account.username });
|
||||
const body = (notification.status && notification.status.spoiler_text.length > 0) ? notification.status.spoiler_text : unescapeHTML(notification.status ? notification.status.content : '');
|
||||
|
||||
|
|
|
@ -47,7 +47,6 @@ const defaultSettings = ImmutableMap({
|
|||
autoloadMore: true,
|
||||
|
||||
systemFont: false,
|
||||
dyslexicFont: false,
|
||||
demetricator: false,
|
||||
|
||||
isDeveloper: false,
|
||||
|
|
|
@ -68,7 +68,7 @@ const createStatus = (params: Record<string, any>, idempotencyKey: string, statu
|
|||
}
|
||||
|
||||
dispatch(importFetchedStatus(status, idempotencyKey));
|
||||
dispatch({ type: STATUS_CREATE_SUCCESS, status, params, idempotencyKey });
|
||||
dispatch({ type: STATUS_CREATE_SUCCESS, status, params, idempotencyKey, editing: !!statusId });
|
||||
|
||||
// Poll the backend for the updated card
|
||||
if (status.expectsCard) {
|
||||
|
|
|
@ -43,7 +43,7 @@ const maybeParseJSON = (data: string) => {
|
|||
|
||||
const getAuthBaseURL = createSelector([
|
||||
(state: RootState, me: string | false | null) => state.accounts.getIn([me, 'url']),
|
||||
(state: RootState, _me: string | false | null) => state.auth.get('me'),
|
||||
(state: RootState, _me: string | false | null) => state.auth.me,
|
||||
], (accountUrl, authUserUrl) => {
|
||||
const baseURL = parseBaseURL(accountUrl) || parseBaseURL(authUserUrl);
|
||||
return baseURL !== window.location.origin ? baseURL : '';
|
||||
|
|
|
@ -1,50 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
import 'intl';
|
||||
import 'intl/locale-data/jsonp/en';
|
||||
import 'es6-symbol/implement';
|
||||
// @ts-ignore: No types
|
||||
import includes from 'array-includes';
|
||||
// @ts-ignore: No types
|
||||
import isNaN from 'is-nan';
|
||||
import assign from 'object-assign';
|
||||
// @ts-ignore: No types
|
||||
import values from 'object.values';
|
||||
|
||||
import { decode as decodeBase64 } from './utils/base64';
|
||||
|
||||
if (!Array.prototype.includes) {
|
||||
includes.shim();
|
||||
}
|
||||
|
||||
if (!Object.assign) {
|
||||
Object.assign = assign;
|
||||
}
|
||||
|
||||
if (!Object.values) {
|
||||
values.shim();
|
||||
}
|
||||
|
||||
if (!Number.isNaN) {
|
||||
Number.isNaN = isNaN;
|
||||
}
|
||||
|
||||
if (!HTMLCanvasElement.prototype.toBlob) {
|
||||
const BASE64_MARKER = ';base64,';
|
||||
|
||||
Object.defineProperty(HTMLCanvasElement.prototype, 'toBlob', {
|
||||
value(callback: any, type = 'image/png', quality: any) {
|
||||
const dataURL = this.toDataURL(type, quality);
|
||||
let data;
|
||||
|
||||
if (dataURL.includes(BASE64_MARKER)) {
|
||||
const [, base64] = dataURL.split(BASE64_MARKER);
|
||||
data = decodeBase64(base64);
|
||||
} else {
|
||||
[, data] = dataURL.split(',');
|
||||
}
|
||||
|
||||
callback(new Blob([data], { type }));
|
||||
},
|
||||
});
|
||||
}
|
|
@ -1,31 +0,0 @@
|
|||
import React from 'react';
|
||||
|
||||
import { normalizeAccount } from 'soapbox/normalizers';
|
||||
|
||||
import { render, screen } from '../../jest/test-helpers';
|
||||
import AvatarOverlay from '../avatar-overlay';
|
||||
|
||||
import type { ReducerAccount } from 'soapbox/reducers/accounts';
|
||||
|
||||
describe('<AvatarOverlay', () => {
|
||||
const account = normalizeAccount({
|
||||
username: 'alice',
|
||||
acct: 'alice',
|
||||
display_name: 'Alice',
|
||||
avatar: '/animated/alice.gif',
|
||||
avatar_static: '/static/alice.jpg',
|
||||
}) as ReducerAccount;
|
||||
|
||||
const friend = normalizeAccount({
|
||||
username: 'eve',
|
||||
acct: 'eve@blackhat.lair',
|
||||
display_name: 'Evelyn',
|
||||
avatar: '/animated/eve.gif',
|
||||
avatar_static: '/static/eve.jpg',
|
||||
}) as ReducerAccount;
|
||||
|
||||
it('renders a overlay avatar', () => {
|
||||
render(<AvatarOverlay account={account} friend={friend} />);
|
||||
expect(screen.queryAllByRole('img')).toHaveLength(2);
|
||||
});
|
||||
});
|
|
@ -1,38 +0,0 @@
|
|||
import React from 'react';
|
||||
|
||||
import { normalizeAccount } from 'soapbox/normalizers';
|
||||
|
||||
import { render, screen } from '../../jest/test-helpers';
|
||||
import Avatar from '../avatar';
|
||||
|
||||
import type { ReducerAccount } from 'soapbox/reducers/accounts';
|
||||
|
||||
describe('<Avatar />', () => {
|
||||
const account = normalizeAccount({
|
||||
username: 'alice',
|
||||
acct: 'alice',
|
||||
display_name: 'Alice',
|
||||
avatar: '/animated/alice.gif',
|
||||
avatar_static: '/static/alice.jpg',
|
||||
}) as ReducerAccount;
|
||||
|
||||
const size = 100;
|
||||
|
||||
// describe('Autoplay', () => {
|
||||
// it('renders an animated avatar', () => {
|
||||
// render(<Avatar account={account} animate size={size} />);
|
||||
|
||||
// expect(screen.getByRole('img').getAttribute('src')).toBe(account.get('avatar'));
|
||||
// });
|
||||
// });
|
||||
|
||||
describe('Still', () => {
|
||||
it('renders a still avatar', () => {
|
||||
render(<Avatar account={account} size={size} />);
|
||||
|
||||
expect(screen.getByRole('img').getAttribute('src')).toBe(account.get('avatar'));
|
||||
});
|
||||
});
|
||||
|
||||
// TODO add autoplay test if possible
|
||||
});
|
|
@ -15,14 +15,17 @@ import type { Account as AccountEntity } from 'soapbox/types/entities';
|
|||
|
||||
interface IInstanceFavicon {
|
||||
account: AccountEntity,
|
||||
disabled?: boolean,
|
||||
}
|
||||
|
||||
const InstanceFavicon: React.FC<IInstanceFavicon> = ({ account }) => {
|
||||
const InstanceFavicon: React.FC<IInstanceFavicon> = ({ account, disabled }) => {
|
||||
const history = useHistory();
|
||||
|
||||
const handleClick: React.MouseEventHandler = (e) => {
|
||||
e.stopPropagation();
|
||||
|
||||
if (disabled) return;
|
||||
|
||||
const timelineUrl = `/timeline/${account.domain}`;
|
||||
if (!(e.ctrlKey || e.metaKey)) {
|
||||
history.push(timelineUrl);
|
||||
|
@ -32,7 +35,11 @@ const InstanceFavicon: React.FC<IInstanceFavicon> = ({ account }) => {
|
|||
};
|
||||
|
||||
return (
|
||||
<button className='w-4 h-4 flex-none focus:ring-primary-500 focus:ring-2 focus:ring-offset-2' onClick={handleClick}>
|
||||
<button
|
||||
className='w-4 h-4 flex-none focus:ring-primary-500 focus:ring-2 focus:ring-offset-2'
|
||||
onClick={handleClick}
|
||||
disabled={disabled}
|
||||
>
|
||||
<img src={account.favicon} alt='' title={account.domain} className='w-full max-h-full' />
|
||||
</button>
|
||||
);
|
||||
|
@ -46,7 +53,7 @@ interface IProfilePopper {
|
|||
const ProfilePopper: React.FC<IProfilePopper> = ({ condition, wrapper, children }): any =>
|
||||
condition ? wrapper(children) : children;
|
||||
|
||||
interface IAccount {
|
||||
export interface IAccount {
|
||||
account: AccountEntity,
|
||||
action?: React.ReactElement,
|
||||
actionAlignment?: 'center' | 'top',
|
||||
|
@ -219,7 +226,7 @@ const Account = ({
|
|||
<Text theme='muted' size='sm' direction='ltr' truncate>@{username}</Text>
|
||||
|
||||
{account.favicon && (
|
||||
<InstanceFavicon account={account} />
|
||||
<InstanceFavicon account={account} disabled={!withLinkToProfile} />
|
||||
)}
|
||||
|
||||
{(timestamp) ? (
|
||||
|
|
|
@ -44,7 +44,7 @@ const AutosuggestAccountInput: React.FC<IAutosuggestAccountInput> = ({
|
|||
setAccountIds(ImmutableOrderedSet());
|
||||
};
|
||||
|
||||
const handleAccountSearch = useCallback(throttle(q => {
|
||||
const handleAccountSearch = useCallback(throttle((q) => {
|
||||
const params = { q, limit, resolve: false };
|
||||
|
||||
dispatch(accountSearch(params, controller.current.signal))
|
||||
|
@ -53,7 +53,7 @@ const AutosuggestAccountInput: React.FC<IAutosuggestAccountInput> = ({
|
|||
setAccountIds(ImmutableOrderedSet(accountIds));
|
||||
})
|
||||
.catch(noOp);
|
||||
}, 900, { leading: false, trailing: true }), [limit]);
|
||||
}, 900, { leading: true, trailing: true }), [limit]);
|
||||
|
||||
const handleChange: React.ChangeEventHandler<HTMLInputElement> = e => {
|
||||
refreshCancelToken();
|
||||
|
|
|
@ -46,7 +46,7 @@ export default class AutosuggestInput extends ImmutablePureComponent<IAutosugges
|
|||
|
||||
getFirstIndex = () => {
|
||||
return this.props.autoSelect ? 0 : -1;
|
||||
}
|
||||
};
|
||||
|
||||
state = {
|
||||
suggestionsHidden: true,
|
||||
|
@ -76,7 +76,7 @@ export default class AutosuggestInput extends ImmutablePureComponent<IAutosugges
|
|||
if (this.props.onChange) {
|
||||
this.props.onChange(e);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
onKeyDown: React.KeyboardEventHandler<HTMLInputElement> = (e) => {
|
||||
const { suggestions, menu, disabled } = this.props;
|
||||
|
@ -145,15 +145,15 @@ export default class AutosuggestInput extends ImmutablePureComponent<IAutosugges
|
|||
if (this.props.onKeyDown) {
|
||||
this.props.onKeyDown(e);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
onBlur = () => {
|
||||
this.setState({ suggestionsHidden: true, focused: false });
|
||||
}
|
||||
};
|
||||
|
||||
onFocus = () => {
|
||||
this.setState({ focused: true });
|
||||
}
|
||||
};
|
||||
|
||||
onSuggestionClick: React.EventHandler<React.MouseEvent | React.TouchEvent> = (e) => {
|
||||
const index = Number(e.currentTarget?.getAttribute('data-index'));
|
||||
|
@ -161,7 +161,7 @@ export default class AutosuggestInput extends ImmutablePureComponent<IAutosugges
|
|||
this.props.onSuggestionSelected(this.state.tokenStart, this.state.lastToken, suggestion);
|
||||
this.input?.focus();
|
||||
e.preventDefault();
|
||||
}
|
||||
};
|
||||
|
||||
componentDidUpdate(prevProps: IAutosuggestInput, prevState: any) {
|
||||
const { suggestions } = this.props;
|
||||
|
@ -172,7 +172,7 @@ export default class AutosuggestInput extends ImmutablePureComponent<IAutosugges
|
|||
|
||||
setInput = (c: HTMLInputElement) => {
|
||||
this.input = c;
|
||||
}
|
||||
};
|
||||
|
||||
renderSuggestion = (suggestion: AutoSuggestion, i: number) => {
|
||||
const { selectedSuggestion } = this.state;
|
||||
|
@ -209,21 +209,21 @@ export default class AutosuggestInput extends ImmutablePureComponent<IAutosugges
|
|||
{inner}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
handleMenuItemAction = (item: MenuItem | null, e: React.MouseEvent | React.KeyboardEvent) => {
|
||||
this.onBlur();
|
||||
if (item?.action) {
|
||||
item.action(e);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
handleMenuItemClick = (item: MenuItem | null): React.MouseEventHandler => {
|
||||
return e => {
|
||||
e.preventDefault();
|
||||
this.handleMenuItemAction(item, e);
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
renderMenu = () => {
|
||||
const { menu, suggestions } = this.props;
|
||||
|
|
|
@ -32,7 +32,7 @@ const AutosuggestLocation: React.FC<IAutosuggestLocation> = ({ id }) => {
|
|||
<Icon src={ADDRESS_ICONS[location.type] || mapPinIcon} />
|
||||
<Stack>
|
||||
<Text>{location.description}</Text>
|
||||
<Text size='xs' theme='muted'>{[location.street, location.locality, location.country].filter(val => val.trim()).join(' · ')}</Text>
|
||||
<Text size='xs' theme='muted'>{[location.street, location.locality, location.country].filter(val => val?.trim()).join(' · ')}</Text>
|
||||
</Stack>
|
||||
</HStack>
|
||||
);
|
||||
|
|
|
@ -64,7 +64,7 @@ class AutosuggestTextarea extends ImmutablePureComponent<IAutosuggesteTextarea>
|
|||
}
|
||||
|
||||
this.props.onChange(e);
|
||||
}
|
||||
};
|
||||
|
||||
onKeyDown: React.KeyboardEventHandler<HTMLTextAreaElement> = (e) => {
|
||||
const { suggestions, disabled } = this.props;
|
||||
|
@ -122,7 +122,7 @@ class AutosuggestTextarea extends ImmutablePureComponent<IAutosuggesteTextarea>
|
|||
}
|
||||
|
||||
this.props.onKeyDown(e);
|
||||
}
|
||||
};
|
||||
|
||||
onBlur = () => {
|
||||
this.setState({ suggestionsHidden: true, focused: false });
|
||||
|
@ -130,7 +130,7 @@ class AutosuggestTextarea extends ImmutablePureComponent<IAutosuggesteTextarea>
|
|||
if (this.props.onBlur) {
|
||||
this.props.onBlur();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
onFocus = () => {
|
||||
this.setState({ focused: true });
|
||||
|
@ -138,14 +138,14 @@ class AutosuggestTextarea extends ImmutablePureComponent<IAutosuggesteTextarea>
|
|||
if (this.props.onFocus) {
|
||||
this.props.onFocus();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
onSuggestionClick: React.MouseEventHandler<HTMLDivElement> = (e) => {
|
||||
const suggestion = this.props.suggestions.get(e.currentTarget.getAttribute('data-index') as any);
|
||||
e.preventDefault();
|
||||
this.props.onSuggestionSelected(this.state.tokenStart, this.state.lastToken, suggestion);
|
||||
this.textarea?.focus();
|
||||
}
|
||||
};
|
||||
|
||||
shouldComponentUpdate(nextProps: IAutosuggesteTextarea, nextState: any) {
|
||||
// Skip updating when only the lastToken changes so the
|
||||
|
@ -169,14 +169,14 @@ class AutosuggestTextarea extends ImmutablePureComponent<IAutosuggesteTextarea>
|
|||
|
||||
setTextarea: React.Ref<HTMLTextAreaElement> = (c) => {
|
||||
this.textarea = c;
|
||||
}
|
||||
};
|
||||
|
||||
onPaste: React.ClipboardEventHandler<HTMLTextAreaElement> = (e) => {
|
||||
if (e.clipboardData && e.clipboardData.files.length === 1) {
|
||||
this.props.onPaste(e.clipboardData.files);
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
renderSuggestion = (suggestion: string | Emoji, i: number) => {
|
||||
const { selectedSuggestion } = this.state;
|
||||
|
@ -208,7 +208,7 @@ class AutosuggestTextarea extends ImmutablePureComponent<IAutosuggesteTextarea>
|
|||
{inner}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
setPortalPosition() {
|
||||
if (!this.textarea) {
|
||||
|
|
|
@ -1,19 +0,0 @@
|
|||
import React from 'react';
|
||||
|
||||
import StillImage from 'soapbox/components/still-image';
|
||||
|
||||
import type { Account as AccountEntity } from 'soapbox/types/entities';
|
||||
|
||||
interface IAvatarOverlay {
|
||||
account: AccountEntity,
|
||||
friend: AccountEntity,
|
||||
}
|
||||
|
||||
const AvatarOverlay: React.FC<IAvatarOverlay> = ({ account, friend }) => (
|
||||
<div className='account__avatar-overlay'>
|
||||
<StillImage src={account.avatar} className='account__avatar-overlay-base' />
|
||||
<StillImage src={friend.avatar} className='account__avatar-overlay-overlay' />
|
||||
</div>
|
||||
);
|
||||
|
||||
export default AvatarOverlay;
|
|
@ -1,38 +0,0 @@
|
|||
import classNames from 'clsx';
|
||||
import React from 'react';
|
||||
|
||||
import StillImage from 'soapbox/components/still-image';
|
||||
|
||||
import type { Account } from 'soapbox/types/entities';
|
||||
|
||||
interface IAvatar {
|
||||
account?: Account | null,
|
||||
size?: number,
|
||||
className?: string,
|
||||
}
|
||||
|
||||
/**
|
||||
* Legacy avatar component.
|
||||
* @see soapbox/components/ui/avatar/avatar.tsx
|
||||
* @deprecated
|
||||
*/
|
||||
const Avatar: React.FC<IAvatar> = ({ account, size, className }) => {
|
||||
if (!account) return null;
|
||||
|
||||
// : TODO : remove inline and change all avatars to be sized using css
|
||||
const style: React.CSSProperties = !size ? {} : {
|
||||
width: `${size}px`,
|
||||
height: `${size}px`,
|
||||
};
|
||||
|
||||
return (
|
||||
<StillImage
|
||||
className={classNames('rounded-full overflow-hidden', className)}
|
||||
style={style}
|
||||
src={account.avatar}
|
||||
alt=''
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default Avatar;
|
|
@ -1,8 +1,8 @@
|
|||
import React from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
import { unblockDomain } from 'soapbox/actions/domain-blocks';
|
||||
import { useAppDispatch } from 'soapbox/hooks';
|
||||
|
||||
import { HStack, IconButton, Text } from './ui';
|
||||
|
||||
|
@ -16,7 +16,7 @@ interface IDomain {
|
|||
}
|
||||
|
||||
const Domain: React.FC<IDomain> = ({ domain }) => {
|
||||
const dispatch = useDispatch();
|
||||
const dispatch = useAppDispatch();
|
||||
const intl = useIntl();
|
||||
|
||||
// const onBlockDomain = () => {
|
||||
|
|
|
@ -64,7 +64,7 @@ class DropdownMenu extends React.PureComponent<IDropdownMenu, IDropdownMenuState
|
|||
if (this.node && !this.node.contains(e.target as Node)) {
|
||||
this.props.onClose();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
document.addEventListener('click', this.handleDocumentClick, false);
|
||||
|
@ -84,11 +84,11 @@ class DropdownMenu extends React.PureComponent<IDropdownMenu, IDropdownMenuState
|
|||
|
||||
setRef: React.RefCallback<HTMLDivElement> = c => {
|
||||
this.node = c;
|
||||
}
|
||||
};
|
||||
|
||||
setFocusRef: React.RefCallback<HTMLAnchorElement> = c => {
|
||||
this.focusedItem = c;
|
||||
}
|
||||
};
|
||||
|
||||
handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (!this.node) return;
|
||||
|
@ -127,13 +127,13 @@ class DropdownMenu extends React.PureComponent<IDropdownMenu, IDropdownMenuState
|
|||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
handleItemKeyPress: React.EventHandler<React.KeyboardEvent> = e => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
this.handleClick(e);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
handleClick: React.EventHandler<React.MouseEvent | React.KeyboardEvent> = e => {
|
||||
const i = Number(e.currentTarget.getAttribute('data-index'));
|
||||
|
@ -152,7 +152,7 @@ class DropdownMenu extends React.PureComponent<IDropdownMenu, IDropdownMenuState
|
|||
e.preventDefault();
|
||||
action(e);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
handleMiddleClick: React.EventHandler<React.MouseEvent> = e => {
|
||||
const i = Number(e.currentTarget.getAttribute('data-index'));
|
||||
|
@ -166,13 +166,13 @@ class DropdownMenu extends React.PureComponent<IDropdownMenu, IDropdownMenuState
|
|||
e.preventDefault();
|
||||
middleClick(e);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
handleAuxClick: React.EventHandler<React.MouseEvent> = e => {
|
||||
if (e.button === 1) {
|
||||
this.handleMiddleClick(e);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
renderItem(option: MenuItem | null, i: number): JSX.Element {
|
||||
if (option === null) {
|
||||
|
@ -303,7 +303,7 @@ class Dropdown extends React.PureComponent<IDropdown, IDropdownState> {
|
|||
|
||||
onOpen(this.state.id, this.handleItemClick, placement, e.type !== 'click');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
handleClose = () => {
|
||||
if (this.activeElement && this.activeElement === this.target) {
|
||||
|
@ -314,13 +314,13 @@ class Dropdown extends React.PureComponent<IDropdown, IDropdownState> {
|
|||
if (this.props.onClose) {
|
||||
this.props.onClose(this.state.id);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
handleMouseDown: React.EventHandler<React.MouseEvent | React.KeyboardEvent> = () => {
|
||||
if (!this.state.open) {
|
||||
this.activeElement = document.activeElement;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
handleButtonKeyDown: React.EventHandler<React.KeyboardEvent> = (e) => {
|
||||
switch (e.key) {
|
||||
|
@ -329,7 +329,7 @@ class Dropdown extends React.PureComponent<IDropdown, IDropdownState> {
|
|||
this.handleMouseDown(e);
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
handleKeyPress: React.EventHandler<React.KeyboardEvent<HTMLButtonElement>> = (e) => {
|
||||
switch (e.key) {
|
||||
|
@ -340,7 +340,7 @@ class Dropdown extends React.PureComponent<IDropdown, IDropdownState> {
|
|||
e.preventDefault();
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
handleItemClick: React.EventHandler<React.MouseEvent> = e => {
|
||||
const i = Number(e.currentTarget.getAttribute('data-index'));
|
||||
|
@ -358,21 +358,21 @@ class Dropdown extends React.PureComponent<IDropdown, IDropdownState> {
|
|||
} else if (to) {
|
||||
this.props.history?.push(to);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
setTargetRef: React.RefCallback<HTMLButtonElement> = c => {
|
||||
this.target = c;
|
||||
}
|
||||
};
|
||||
|
||||
findTarget = () => {
|
||||
return this.target;
|
||||
}
|
||||
};
|
||||
|
||||
componentWillUnmount = () => {
|
||||
if (this.state.id === this.props.openDropdownId) {
|
||||
this.handleClose();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { src = require('@tabler/icons/dots.svg'), items, title, disabled, dropdownPlacement, openDropdownId, openedViaKeyboard = false, pressed, text, children, dropdownMenuStyle } = this.props;
|
||||
|
|
|
@ -1,12 +1,11 @@
|
|||
import classNames from 'clsx';
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { usePopper } from 'react-popper';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
import { simpleEmojiReact } from 'soapbox/actions/emoji-reacts';
|
||||
import { openModal } from 'soapbox/actions/modals';
|
||||
import { EmojiSelector } from 'soapbox/components/ui';
|
||||
import { useAppSelector, useOwnAccount, useSoapboxConfig } from 'soapbox/hooks';
|
||||
import { useAppDispatch, useAppSelector, useOwnAccount, useSoapboxConfig } from 'soapbox/hooks';
|
||||
import { isUserTouching } from 'soapbox/is-mobile';
|
||||
import { getReactForStatus } from 'soapbox/utils/emoji-reacts';
|
||||
|
||||
|
@ -17,7 +16,7 @@ interface IEmojiButtonWrapper {
|
|||
|
||||
/** Provides emoji reaction functionality to the underlying button component */
|
||||
const EmojiButtonWrapper: React.FC<IEmojiButtonWrapper> = ({ statusId, children }): JSX.Element | null => {
|
||||
const dispatch = useDispatch();
|
||||
const dispatch = useAppDispatch();
|
||||
const ownAccount = useOwnAccount();
|
||||
const status = useAppSelector(state => state.statuses.get(statusId));
|
||||
const soapboxConfig = useSoapboxConfig();
|
||||
|
|
|
@ -28,7 +28,7 @@ class EmojiSelector extends ImmutablePureComponent<IEmojiSelector> {
|
|||
onReact: () => { },
|
||||
onUnfocus: () => { },
|
||||
visible: false,
|
||||
}
|
||||
};
|
||||
|
||||
node?: HTMLDivElement = undefined;
|
||||
|
||||
|
@ -38,7 +38,7 @@ class EmojiSelector extends ImmutablePureComponent<IEmojiSelector> {
|
|||
if (focused && (!e.currentTarget || !e.currentTarget.classList.contains('emoji-react-selector__emoji'))) {
|
||||
onUnfocus();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
_selectPreviousEmoji = (i: number): void => {
|
||||
if (!this.node) return;
|
||||
|
@ -85,7 +85,7 @@ class EmojiSelector extends ImmutablePureComponent<IEmojiSelector> {
|
|||
onUnfocus();
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
handleReact = (emoji: string) => (): void => {
|
||||
const { onReact, focused, onUnfocus } = this.props;
|
||||
|
@ -95,7 +95,7 @@ class EmojiSelector extends ImmutablePureComponent<IEmojiSelector> {
|
|||
if (focused) {
|
||||
onUnfocus();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
handlers = {
|
||||
open: () => { },
|
||||
|
@ -103,7 +103,7 @@ class EmojiSelector extends ImmutablePureComponent<IEmojiSelector> {
|
|||
|
||||
setRef = (c: HTMLDivElement): void => {
|
||||
this.node = c;
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { visible, focused, allowedEmoji, onReact } = this.props;
|
||||
|
|
|
@ -42,7 +42,7 @@ class ErrorBoundary extends React.PureComponent<Props, State> {
|
|||
error: undefined,
|
||||
componentStack: undefined,
|
||||
browser: undefined,
|
||||
}
|
||||
};
|
||||
|
||||
textarea: HTMLTextAreaElement | null = null;
|
||||
|
||||
|
@ -71,7 +71,7 @@ class ErrorBoundary extends React.PureComponent<Props, State> {
|
|||
|
||||
setTextareaRef: React.RefCallback<HTMLTextAreaElement> = c => {
|
||||
this.textarea = c;
|
||||
}
|
||||
};
|
||||
|
||||
handleCopy: React.MouseEventHandler = () => {
|
||||
if (!this.textarea) return;
|
||||
|
@ -80,12 +80,12 @@ class ErrorBoundary extends React.PureComponent<Props, State> {
|
|||
this.textarea.setSelectionRange(0, 99999);
|
||||
|
||||
document.execCommand('copy');
|
||||
}
|
||||
};
|
||||
|
||||
getErrorText = (): string => {
|
||||
const { error, componentStack } = this.state;
|
||||
return error + componentStack;
|
||||
}
|
||||
};
|
||||
|
||||
clearCookies: React.MouseEventHandler = (e) => {
|
||||
localStorage.clear();
|
||||
|
@ -96,7 +96,7 @@ class ErrorBoundary extends React.PureComponent<Props, State> {
|
|||
e.preventDefault();
|
||||
unregisterSw().then(goHome).catch(goHome);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { browser, hasError } = this.state;
|
||||
|
|
|
@ -1,188 +0,0 @@
|
|||
import classNames from 'clsx';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import spring from 'react-motion/lib/spring';
|
||||
|
||||
import Icon from 'soapbox/components/icon';
|
||||
import emojify from 'soapbox/features/emoji/emoji';
|
||||
|
||||
import Motion from '../features/ui/util/optional-motion';
|
||||
|
||||
export default class IconButton extends React.PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
className: PropTypes.string,
|
||||
iconClassName: PropTypes.string,
|
||||
title: PropTypes.string.isRequired,
|
||||
icon: PropTypes.string,
|
||||
src: PropTypes.string,
|
||||
onClick: PropTypes.func,
|
||||
onMouseDown: PropTypes.func,
|
||||
onKeyUp: PropTypes.func,
|
||||
onKeyDown: PropTypes.func,
|
||||
onKeyPress: PropTypes.func,
|
||||
onMouseEnter: PropTypes.func,
|
||||
onMouseLeave: PropTypes.func,
|
||||
size: PropTypes.number,
|
||||
active: PropTypes.bool,
|
||||
pressed: PropTypes.bool,
|
||||
expanded: PropTypes.bool,
|
||||
style: PropTypes.object,
|
||||
activeStyle: PropTypes.object,
|
||||
disabled: PropTypes.bool,
|
||||
inverted: PropTypes.bool,
|
||||
animate: PropTypes.bool,
|
||||
overlay: PropTypes.bool,
|
||||
tabIndex: PropTypes.string,
|
||||
text: PropTypes.string,
|
||||
emoji: PropTypes.string,
|
||||
type: PropTypes.string,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
size: 18,
|
||||
active: false,
|
||||
disabled: false,
|
||||
animate: false,
|
||||
overlay: false,
|
||||
tabIndex: '0',
|
||||
onKeyUp: () => {},
|
||||
onKeyDown: () => {},
|
||||
onClick: () => {},
|
||||
onMouseEnter: () => {},
|
||||
onMouseLeave: () => {},
|
||||
type: 'button',
|
||||
};
|
||||
|
||||
handleClick = (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!this.props.disabled) {
|
||||
this.props.onClick(e);
|
||||
}
|
||||
}
|
||||
|
||||
handleMouseDown = (e) => {
|
||||
if (!this.props.disabled && this.props.onMouseDown) {
|
||||
this.props.onMouseDown(e);
|
||||
}
|
||||
}
|
||||
|
||||
handleKeyDown = (e) => {
|
||||
if (!this.props.disabled && this.props.onKeyDown) {
|
||||
this.props.onKeyDown(e);
|
||||
}
|
||||
}
|
||||
|
||||
handleKeyUp = (e) => {
|
||||
if (!this.props.disabled && this.props.onKeyUp) {
|
||||
this.props.onKeyUp(e);
|
||||
}
|
||||
}
|
||||
|
||||
handleKeyPress = (e) => {
|
||||
if (this.props.onKeyPress && !this.props.disabled) {
|
||||
this.props.onKeyPress(e);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const style = {
|
||||
fontSize: `${this.props.size}px`,
|
||||
width: `${this.props.size * 1.28571429}px`,
|
||||
height: `${this.props.size * 1.28571429}px`,
|
||||
lineHeight: `${this.props.size}px`,
|
||||
...this.props.style,
|
||||
...(this.props.active ? this.props.activeStyle : {}),
|
||||
};
|
||||
|
||||
const {
|
||||
active,
|
||||
animate,
|
||||
className,
|
||||
iconClassName,
|
||||
disabled,
|
||||
expanded,
|
||||
icon,
|
||||
src,
|
||||
inverted,
|
||||
overlay,
|
||||
pressed,
|
||||
tabIndex,
|
||||
title,
|
||||
text,
|
||||
emoji,
|
||||
type,
|
||||
} = this.props;
|
||||
|
||||
const classes = classNames(className, 'icon-button', {
|
||||
active,
|
||||
disabled,
|
||||
inverted,
|
||||
overlayed: overlay,
|
||||
});
|
||||
|
||||
if (!animate) {
|
||||
// Perf optimization: avoid unnecessary <Motion> components unless
|
||||
// we actually need to animate.
|
||||
return (
|
||||
<button
|
||||
aria-label={title}
|
||||
aria-pressed={pressed}
|
||||
aria-expanded={expanded}
|
||||
title={title}
|
||||
className={classes}
|
||||
onClick={this.handleClick}
|
||||
onMouseDown={this.handleMouseDown}
|
||||
onKeyDown={this.handleKeyDown}
|
||||
onKeyUp={this.handleKeyUp}
|
||||
onKeyPress={this.handleKeyPress}
|
||||
onMouseEnter={this.props.onMouseEnter}
|
||||
onMouseLeave={this.props.onMouseLeave}
|
||||
tabIndex={tabIndex}
|
||||
disabled={disabled}
|
||||
type={type}
|
||||
>
|
||||
<div style={src ? {} : style}>
|
||||
{emoji
|
||||
? <div className='icon-button__emoji' dangerouslySetInnerHTML={{ __html: emojify(emoji) }} aria-hidden='true' />
|
||||
: <Icon className={iconClassName} id={icon} src={src} fixedWidth aria-hidden='true' />}
|
||||
</div>
|
||||
{text && <span className='icon-button__text'>{text}</span>}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Motion defaultStyle={{ rotate: active ? -360 : 0 }} style={{ rotate: animate ? spring(active ? -360 : 0, { stiffness: 120, damping: 7 }) : 0 }}>
|
||||
{({ rotate }) => (
|
||||
<button
|
||||
aria-label={title}
|
||||
aria-pressed={pressed}
|
||||
aria-expanded={expanded}
|
||||
title={title}
|
||||
className={classes}
|
||||
onClick={this.handleClick}
|
||||
onMouseDown={this.handleMouseDown}
|
||||
onKeyDown={this.handleKeyDown}
|
||||
onKeyUp={this.handleKeyUp}
|
||||
onKeyPress={this.handleKeyPress}
|
||||
onMouseEnter={this.props.onMouseEnter}
|
||||
onMouseLeave={this.props.onMouseLeave}
|
||||
tabIndex={tabIndex}
|
||||
disabled={disabled}
|
||||
type={type}
|
||||
>
|
||||
<div style={src ? {} : style}>
|
||||
{emoji
|
||||
? <div className='icon-button__emoji' style={{ transform: `rotate(${rotate}deg)` }} dangerouslySetInnerHTML={{ __html: emojify(emoji) }} aria-hidden='true' />
|
||||
: <Icon className={iconClassName} id={icon} src={src} style={{ transform: `rotate(${rotate}deg)` }} fixedWidth aria-hidden='true' />}
|
||||
</div>
|
||||
{text && <span className='icon-button__text'>{text}</span>}
|
||||
</button>
|
||||
)}
|
||||
</Motion>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,100 @@
|
|||
import classNames from 'clsx';
|
||||
import React from 'react';
|
||||
|
||||
import Icon from 'soapbox/components/icon';
|
||||
|
||||
interface IIconButton extends Pick<React.ButtonHTMLAttributes<HTMLButtonElement>, 'className' | 'disabled' | 'onClick' | 'onKeyDown' | 'onKeyPress' | 'onKeyUp' | 'onMouseDown' | 'onMouseEnter' | 'onMouseLeave' | 'tabIndex' | 'title'> {
|
||||
active?: boolean
|
||||
expanded?: boolean
|
||||
iconClassName?: string
|
||||
pressed?: boolean
|
||||
size?: number
|
||||
src: string
|
||||
text?: React.ReactNode
|
||||
}
|
||||
|
||||
const IconButton: React.FC<IIconButton> = ({
|
||||
active,
|
||||
className,
|
||||
disabled,
|
||||
expanded,
|
||||
iconClassName,
|
||||
onClick,
|
||||
onKeyDown,
|
||||
onKeyUp,
|
||||
onKeyPress,
|
||||
onMouseDown,
|
||||
onMouseEnter,
|
||||
onMouseLeave,
|
||||
pressed,
|
||||
size = 18,
|
||||
src,
|
||||
tabIndex = 0,
|
||||
text,
|
||||
title,
|
||||
}) => {
|
||||
|
||||
const handleClick: React.MouseEventHandler<HTMLButtonElement> = (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!disabled && onClick) {
|
||||
onClick(e);
|
||||
}
|
||||
};
|
||||
|
||||
const handleMouseDown: React.MouseEventHandler<HTMLButtonElement> = (e) => {
|
||||
if (!disabled && onMouseDown) {
|
||||
onMouseDown(e);
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyDown: React.KeyboardEventHandler<HTMLButtonElement> = (e) => {
|
||||
if (!disabled && onKeyDown) {
|
||||
onKeyDown(e);
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyUp: React.KeyboardEventHandler<HTMLButtonElement> = (e) => {
|
||||
if (!disabled && onKeyUp) {
|
||||
onKeyUp(e);
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyPress: React.KeyboardEventHandler<HTMLButtonElement> = (e) => {
|
||||
if (onKeyPress && !disabled) {
|
||||
onKeyPress(e);
|
||||
}
|
||||
};
|
||||
|
||||
const classes = classNames(className, 'icon-button', {
|
||||
active,
|
||||
disabled,
|
||||
});
|
||||
|
||||
return (
|
||||
<button
|
||||
aria-label={title}
|
||||
aria-pressed={pressed}
|
||||
aria-expanded={expanded}
|
||||
title={title}
|
||||
className={classes}
|
||||
onClick={handleClick}
|
||||
onMouseDown={handleMouseDown}
|
||||
onKeyDown={handleKeyDown}
|
||||
onKeyUp={handleKeyUp}
|
||||
onKeyPress={handleKeyPress}
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}
|
||||
tabIndex={tabIndex}
|
||||
disabled={disabled}
|
||||
type='button'
|
||||
>
|
||||
<div>
|
||||
<Icon className={iconClassName} src={src} aria-hidden='true' />
|
||||
</div>
|
||||
{text && <span className='icon-button__text'>{text}</span>}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
export default IconButton;
|
|
@ -1,27 +1,28 @@
|
|||
/**
|
||||
* Icon: abstract icon class that can render icons from multiple sets.
|
||||
* Icon: abstact component to render SVG icons.
|
||||
* @module soapbox/components/icon
|
||||
* @see soapbox/components/fork_awesome_icon
|
||||
* @see soapbox/components/svg_icon
|
||||
*/
|
||||
|
||||
import classNames from 'clsx';
|
||||
import React from 'react';
|
||||
import InlineSVG from 'react-inlinesvg'; // eslint-disable-line no-restricted-imports
|
||||
|
||||
import ForkAwesomeIcon, { IForkAwesomeIcon } from './fork-awesome-icon';
|
||||
import SvgIcon, { ISvgIcon } from './svg-icon';
|
||||
export interface IIcon extends React.HTMLAttributes<HTMLDivElement> {
|
||||
src: string,
|
||||
id?: string,
|
||||
alt?: string,
|
||||
className?: string,
|
||||
}
|
||||
|
||||
export type IIcon = IForkAwesomeIcon | ISvgIcon;
|
||||
|
||||
const Icon: React.FC<IIcon> = (props) => {
|
||||
if ((props as ISvgIcon).src) {
|
||||
const { src, ...rest } = (props as ISvgIcon);
|
||||
|
||||
return <SvgIcon src={src} {...rest} />;
|
||||
} else {
|
||||
const { id, fixedWidth, ...rest } = (props as IForkAwesomeIcon);
|
||||
|
||||
return <ForkAwesomeIcon id={id} fixedWidth={fixedWidth} {...rest} />;
|
||||
}
|
||||
const Icon: React.FC<IIcon> = ({ src, alt, className, ...rest }) => {
|
||||
return (
|
||||
<div
|
||||
className={classNames('svg-icon', className)}
|
||||
{...rest}
|
||||
>
|
||||
<InlineSVG src={src} title={alt} loader={<></>} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Icon;
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import classNames from 'clsx';
|
||||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import React, { useState, useRef, useLayoutEffect } from 'react';
|
||||
|
||||
import Blurhash from 'soapbox/components/blurhash';
|
||||
import Icon from 'soapbox/components/icon';
|
||||
import StillImage from 'soapbox/components/still-image';
|
||||
import { MIMETYPE_ICONS } from 'soapbox/components/upload';
|
||||
import { useSettings } from 'soapbox/hooks';
|
||||
import { useSettings, useSoapboxConfig } from 'soapbox/hooks';
|
||||
import { Attachment } from 'soapbox/types/entities';
|
||||
import { truncateFilename } from 'soapbox/utils/media';
|
||||
|
||||
|
@ -72,6 +72,7 @@ const Item: React.FC<IItem> = ({
|
|||
}) => {
|
||||
const settings = useSettings();
|
||||
const autoPlayGif = settings.get('autoPlayGif') === true;
|
||||
const { mediaPreview } = useSoapboxConfig();
|
||||
|
||||
const handleMouseEnter: React.MouseEventHandler<HTMLVideoElement> = ({ currentTarget: video }) => {
|
||||
if (hoverToPlay()) {
|
||||
|
@ -171,7 +172,7 @@ const Item: React.FC<IItem> = ({
|
|||
>
|
||||
<StillImage
|
||||
className='w-full h-full'
|
||||
src={attachment.url}
|
||||
src={mediaPreview ? attachment.preview_url : attachment.url}
|
||||
alt={attachment.description}
|
||||
letterboxed={letterboxed}
|
||||
showExt
|
||||
|
@ -294,7 +295,7 @@ const MediaGallery: React.FC<IMediaGallery> = (props) => {
|
|||
const aspectRatio = media.getIn([0, 'meta', 'original', 'aspect']) as number | undefined;
|
||||
|
||||
const getHeight = () => {
|
||||
if (!aspectRatio) return w;
|
||||
if (!aspectRatio) return w * 9 / 16;
|
||||
if (isPanoramic(aspectRatio)) return Math.floor(w / maximumAspectRatio);
|
||||
if (isPortrait(aspectRatio)) return Math.floor(w / minimumAspectRatio);
|
||||
return Math.floor(w / aspectRatio);
|
||||
|
@ -532,7 +533,7 @@ const MediaGallery: React.FC<IMediaGallery> = (props) => {
|
|||
/>
|
||||
));
|
||||
|
||||
useEffect(() => {
|
||||
useLayoutEffect(() => {
|
||||
if (node.current) {
|
||||
const { offsetWidth } = node.current;
|
||||
|
||||
|
|
|
@ -11,7 +11,6 @@ import { useAppDispatch, usePrevious } from 'soapbox/hooks';
|
|||
import { queryClient } from 'soapbox/queries/client';
|
||||
import { IPolicy, PolicyKeys } from 'soapbox/queries/policies';
|
||||
|
||||
import type { UnregisterCallback } from 'history';
|
||||
import type { ModalType } from 'soapbox/features/ui/components/modal-root';
|
||||
import type { ReducerCompose } from 'soapbox/reducers/compose';
|
||||
import type { ReducerRecord as ReducerComposeEvent } from 'soapbox/reducers/compose-event';
|
||||
|
@ -55,7 +54,7 @@ const ModalRoot: React.FC<IModalRoot> = ({ children, onCancel, onClose, type })
|
|||
const ref = useRef<HTMLDivElement>(null);
|
||||
const activeElement = useRef<HTMLDivElement | null>(revealed ? document.activeElement as HTMLDivElement | null : null);
|
||||
const modalHistoryKey = useRef<number>();
|
||||
const unlistenHistory = useRef<UnregisterCallback>();
|
||||
const unlistenHistory = useRef<ReturnType<typeof history.listen>>();
|
||||
|
||||
const prevChildren = usePrevious(children);
|
||||
const prevType = usePrevious(type);
|
||||
|
@ -152,8 +151,10 @@ const ModalRoot: React.FC<IModalRoot> = ({ children, onCancel, onClose, type })
|
|||
|
||||
const handleModalOpen = () => {
|
||||
modalHistoryKey.current = Date.now();
|
||||
unlistenHistory.current = history.listen((_, action) => {
|
||||
if (action === 'POP') {
|
||||
unlistenHistory.current = history.listen(({ state }, action) => {
|
||||
if (!(state as any)?.soapboxModalKey) {
|
||||
onClose();
|
||||
} else if (action === 'POP') {
|
||||
handleOnClose();
|
||||
|
||||
if (onCancel) onCancel();
|
||||
|
@ -165,11 +166,9 @@ const ModalRoot: React.FC<IModalRoot> = ({ children, onCancel, onClose, type })
|
|||
if (unlistenHistory.current) {
|
||||
unlistenHistory.current();
|
||||
}
|
||||
if (!['FAVOURITES', 'MENTIONS', 'REACTIONS', 'REBLOGS', 'MEDIA'].includes(type)) {
|
||||
const { state } = history.location;
|
||||
if (state && (state as any).soapboxModalKey === modalHistoryKey.current) {
|
||||
history.goBack();
|
||||
}
|
||||
const { state } = history.location;
|
||||
if (state && (state as any).soapboxModalKey === modalHistoryKey.current) {
|
||||
history.goBack();
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -221,7 +220,7 @@ const ModalRoot: React.FC<IModalRoot> = ({ children, onCancel, onClose, type })
|
|||
|
||||
ensureHistoryBuffer();
|
||||
}
|
||||
});
|
||||
}, [children]);
|
||||
|
||||
if (!visible) {
|
||||
return (
|
||||
|
@ -241,7 +240,7 @@ const ModalRoot: React.FC<IModalRoot> = ({ children, onCancel, onClose, type })
|
|||
<div
|
||||
role='presentation'
|
||||
id='modal-overlay'
|
||||
className='fixed inset-0 bg-gray-500/90 dark:bg-gray-700/90'
|
||||
className='fixed inset-0 bg-gray-500/90 dark:bg-gray-700/90 backdrop-blur'
|
||||
onClick={handleOnClose}
|
||||
/>
|
||||
|
||||
|
|
|
@ -24,13 +24,13 @@ type SavedScrollPosition = {
|
|||
// NOTE: It's crucial to space lists with **padding** instead of margin!
|
||||
// Pass an `itemClassName` like `pb-3`, NOT a `space-y-3` className
|
||||
// https://virtuoso.dev/troubleshooting#list-does-not-scroll-to-the-bottom--items-jump-around
|
||||
const Item: Components<Context>['Item'] = ({ context, ...rest }) => (
|
||||
const Item: Components<JSX.Element, Context>['Item'] = ({ context, ...rest }) => (
|
||||
<div className={context?.itemClassName} {...rest} />
|
||||
);
|
||||
|
||||
/** Custom Virtuoso List component for the outer container. */
|
||||
// Ensure the className winds up here
|
||||
const List: Components<Context>['List'] = React.forwardRef((props, ref) => {
|
||||
const List: Components<JSX.Element, Context>['List'] = React.forwardRef((props, ref) => {
|
||||
const { context, ...rest } = props;
|
||||
return <div ref={ref} className={context?.listClassName} {...rest} />;
|
||||
});
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
import classNames from 'clsx';
|
||||
import React from 'react';
|
||||
import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { Link, NavLink } from 'react-router-dom';
|
||||
|
||||
import { fetchOwnAccounts, logOut, switchAccount } from 'soapbox/actions/auth';
|
||||
|
@ -11,7 +10,7 @@ import { closeSidebar } from 'soapbox/actions/sidebar';
|
|||
import Account from 'soapbox/components/account';
|
||||
import { Stack } from 'soapbox/components/ui';
|
||||
import ProfileStats from 'soapbox/features/ui/components/profile-stats';
|
||||
import { useAppSelector, useFeatures } from 'soapbox/hooks';
|
||||
import { useAppDispatch, useAppSelector, useFeatures } from 'soapbox/hooks';
|
||||
import { makeGetAccount, makeGetOtherAccounts } from 'soapbox/selectors';
|
||||
|
||||
import { Divider, HStack, Icon, IconButton, Text } from './ui';
|
||||
|
@ -81,7 +80,7 @@ const getOtherAccounts = makeGetOtherAccounts();
|
|||
|
||||
const SidebarMenu: React.FC = (): JSX.Element | null => {
|
||||
const intl = useIntl();
|
||||
const dispatch = useDispatch();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const features = useFeatures();
|
||||
const getAccount = makeGetAccount();
|
||||
|
|
|
@ -26,7 +26,7 @@ interface IReadMoreButton {
|
|||
const ReadMoreButton: React.FC<IReadMoreButton> = ({ onClick }) => (
|
||||
<button className='flex items-center text-gray-900 dark:text-gray-300 border-0 bg-transparent p-0 pt-2 hover:underline active:underline' onClick={onClick}>
|
||||
<FormattedMessage id='status.read_more' defaultMessage='Read more' />
|
||||
<Icon className='inline-block h-5 w-5' src={require('@tabler/icons/chevron-right.svg')} fixedWidth />
|
||||
<Icon className='inline-block h-5 w-5' src={require('@tabler/icons/chevron-right.svg')} />
|
||||
</button>
|
||||
);
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@ import classNames from 'clsx';
|
|||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { HotKeys } from 'react-hotkeys';
|
||||
import { useIntl, FormattedMessage, defineMessages } from 'react-intl';
|
||||
import { NavLink, useHistory } from 'react-router-dom';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
|
||||
import { mentionCompose, replyCompose } from 'soapbox/actions/compose';
|
||||
import { toggleFavourite, toggleReblog } from 'soapbox/actions/interactions';
|
||||
|
@ -21,9 +21,9 @@ import StatusContent from './status-content';
|
|||
import StatusMedia from './status-media';
|
||||
import StatusReplyMentions from './status-reply-mentions';
|
||||
import SensitiveContentOverlay from './statuses/sensitive-content-overlay';
|
||||
import { Card, HStack, Stack, Text } from './ui';
|
||||
import StatusInfo from './statuses/status-info';
|
||||
import { Card, Stack, Text } from './ui';
|
||||
|
||||
import type { Map as ImmutableMap } from 'immutable';
|
||||
import type {
|
||||
Account as AccountEntity,
|
||||
Status as StatusEntity,
|
||||
|
@ -38,6 +38,7 @@ const messages = defineMessages({
|
|||
|
||||
export interface IStatus {
|
||||
id?: string,
|
||||
avatarSize?: number,
|
||||
status: StatusEntity,
|
||||
onClick?: () => void,
|
||||
muted?: boolean,
|
||||
|
@ -45,7 +46,6 @@ export interface IStatus {
|
|||
unread?: boolean,
|
||||
onMoveUp?: (statusId: string, featured?: boolean) => void,
|
||||
onMoveDown?: (statusId: string, featured?: boolean) => void,
|
||||
group?: ImmutableMap<string, any>,
|
||||
focusable?: boolean,
|
||||
featured?: boolean,
|
||||
hideActionBar?: boolean,
|
||||
|
@ -58,6 +58,8 @@ export interface IStatus {
|
|||
const Status: React.FC<IStatus> = (props) => {
|
||||
const {
|
||||
status,
|
||||
accountAction,
|
||||
avatarSize = 42,
|
||||
focusable = true,
|
||||
hoverable = true,
|
||||
onClick,
|
||||
|
@ -86,7 +88,7 @@ const Status: React.FC<IStatus> = (props) => {
|
|||
const [minHeight, setMinHeight] = useState(208);
|
||||
|
||||
const actualStatus = getActualStatus(status);
|
||||
|
||||
const isReblog = status.reblog && typeof status.reblog === 'object';
|
||||
const statusUrl = `/@${actualStatus.getIn(['account', 'acct'])}/posts/${actualStatus.id}`;
|
||||
|
||||
// Track height changes we know about to compensate scrolling.
|
||||
|
@ -203,8 +205,49 @@ const Status: React.FC<IStatus> = (props) => {
|
|||
firstEmoji?.focus();
|
||||
};
|
||||
|
||||
const renderStatusInfo = () => {
|
||||
if (isReblog) {
|
||||
return (
|
||||
<StatusInfo
|
||||
avatarSize={avatarSize}
|
||||
to={`/@${status.getIn(['account', 'acct'])}`}
|
||||
icon={<Icon src={require('@tabler/icons/repeat.svg')} className='text-green-600' />}
|
||||
text={
|
||||
<FormattedMessage
|
||||
id='status.reblogged_by'
|
||||
defaultMessage='{name} reposted'
|
||||
values={{
|
||||
name: (
|
||||
<bdi className='truncate pr-1 rtl:pl-1'>
|
||||
<strong
|
||||
className='text-gray-800 dark:text-gray-200'
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: String(status.getIn(['account', 'display_name_html'])),
|
||||
}}
|
||||
/>
|
||||
</bdi>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
} else if (featured) {
|
||||
return (
|
||||
<StatusInfo
|
||||
avatarSize={avatarSize}
|
||||
icon={<Icon src={require('@tabler/icons/pinned.svg')} className='text-gray-600 dark:text-gray-400' />}
|
||||
text={
|
||||
<Text size='xs' theme='muted' weight='medium'>
|
||||
<FormattedMessage id='status.pinned' defaultMessage='Pinned post' />
|
||||
</Text>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
if (!status) return null;
|
||||
let rebloggedByText, reblogElement, reblogElementMobile;
|
||||
|
||||
if (hidden) {
|
||||
return (
|
||||
|
@ -230,55 +273,8 @@ const Status: React.FC<IStatus> = (props) => {
|
|||
);
|
||||
}
|
||||
|
||||
let rebloggedByText;
|
||||
if (status.reblog && typeof status.reblog === 'object') {
|
||||
const displayNameHtml = { __html: String(status.getIn(['account', 'display_name_html'])) };
|
||||
|
||||
reblogElement = (
|
||||
<NavLink
|
||||
to={`/@${status.getIn(['account', 'acct'])}`}
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
className='hidden sm:flex items-center text-gray-700 dark:text-gray-600 text-xs font-medium space-x-1 rtl:space-x-reverse hover:underline'
|
||||
>
|
||||
<Icon src={require('@tabler/icons/repeat.svg')} className='text-green-600' />
|
||||
|
||||
<HStack alignItems='center'>
|
||||
<FormattedMessage
|
||||
id='status.reblogged_by'
|
||||
defaultMessage='{name} reposted'
|
||||
values={{
|
||||
name: <bdi className='max-w-[100px] truncate pr-1 rtl:px-1'>
|
||||
<strong className='text-gray-800 dark:text-gray-200' dangerouslySetInnerHTML={displayNameHtml} />
|
||||
</bdi>,
|
||||
}}
|
||||
/>
|
||||
</HStack>
|
||||
</NavLink>
|
||||
);
|
||||
|
||||
reblogElementMobile = (
|
||||
<div className='pb-5 -mt-2 sm:hidden truncate'>
|
||||
<NavLink
|
||||
to={`/@${status.getIn(['account', 'acct'])}`}
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
className='flex items-center text-gray-700 dark:text-gray-600 text-xs font-medium space-x-1 hover:underline'
|
||||
>
|
||||
<Icon src={require('@tabler/icons/repeat.svg')} className='text-green-600' />
|
||||
|
||||
<span>
|
||||
<FormattedMessage
|
||||
id='status.reblogged_by'
|
||||
defaultMessage='{name} reposted'
|
||||
values={{
|
||||
name: <bdi>
|
||||
<strong className='text-gray-800 dark:text-gray-200' dangerouslySetInnerHTML={displayNameHtml} />
|
||||
</bdi>,
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
</NavLink>
|
||||
</div>
|
||||
);
|
||||
|
||||
rebloggedByText = intl.formatMessage(
|
||||
messages.reblogged_by,
|
||||
{ name: String(status.getIn(['account', 'acct'])) },
|
||||
|
@ -314,8 +310,6 @@ const Status: React.FC<IStatus> = (props) => {
|
|||
react: handleHotkeyReact,
|
||||
};
|
||||
|
||||
const accountAction = props.accountAction || reblogElement;
|
||||
|
||||
const isUnderReview = actualStatus.visibility === 'self';
|
||||
const isSensitive = actualStatus.hidden;
|
||||
|
||||
|
@ -330,21 +324,9 @@ const Status: React.FC<IStatus> = (props) => {
|
|||
onClick={handleClick}
|
||||
role='link'
|
||||
>
|
||||
{featured && (
|
||||
<div className='pt-4 px-4'>
|
||||
<HStack alignItems='center' space={1}>
|
||||
<Icon src={require('@tabler/icons/pinned.svg')} className='text-gray-600 dark:text-gray-400' />
|
||||
|
||||
<Text size='sm' theme='muted' weight='medium'>
|
||||
<FormattedMessage id='status.pinned' defaultMessage='Pinned post' />
|
||||
</Text>
|
||||
</HStack>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Card
|
||||
variant={variant}
|
||||
className={classNames('status__wrapper', `status-${actualStatus.visibility}`, {
|
||||
className={classNames('status__wrapper space-y-4', `status-${actualStatus.visibility}`, {
|
||||
'py-6 sm:p-5': variant === 'rounded',
|
||||
'status-reply': !!status.in_reply_to_id,
|
||||
muted,
|
||||
|
@ -352,21 +334,20 @@ const Status: React.FC<IStatus> = (props) => {
|
|||
})}
|
||||
data-id={status.id}
|
||||
>
|
||||
{reblogElementMobile}
|
||||
{renderStatusInfo()}
|
||||
|
||||
<div className='mb-4'>
|
||||
<AccountContainer
|
||||
key={String(actualStatus.getIn(['account', 'id']))}
|
||||
id={String(actualStatus.getIn(['account', 'id']))}
|
||||
timestamp={actualStatus.created_at}
|
||||
timestampUrl={statusUrl}
|
||||
action={accountAction}
|
||||
hideActions={!accountAction}
|
||||
showEdit={!!actualStatus.edited_at}
|
||||
showProfileHoverCard={hoverable}
|
||||
withLinkToProfile={hoverable}
|
||||
/>
|
||||
</div>
|
||||
<AccountContainer
|
||||
key={String(actualStatus.getIn(['account', 'id']))}
|
||||
id={String(actualStatus.getIn(['account', 'id']))}
|
||||
timestamp={actualStatus.created_at}
|
||||
timestampUrl={statusUrl}
|
||||
action={accountAction}
|
||||
hideActions={!accountAction}
|
||||
showEdit={!!actualStatus.edited_at}
|
||||
showProfileHoverCard={hoverable}
|
||||
withLinkToProfile={hoverable}
|
||||
avatarSize={avatarSize}
|
||||
/>
|
||||
|
||||
<div className='status__content-wrapper'>
|
||||
<StatusReplyMentions status={actualStatus} hoverable={hoverable} />
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
interface IStatusInfo {
|
||||
avatarSize: number
|
||||
to?: string
|
||||
icon: React.ReactNode
|
||||
text: React.ReactNode
|
||||
}
|
||||
|
||||
const StatusInfo = (props: IStatusInfo) => {
|
||||
const { avatarSize, to, icon, text } = props;
|
||||
|
||||
const onClick = (event: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => {
|
||||
event.stopPropagation();
|
||||
};
|
||||
|
||||
const Container = to ? Link : 'div';
|
||||
const containerProps: any = to ? { onClick, to } : {};
|
||||
|
||||
return (
|
||||
<Container
|
||||
{...containerProps}
|
||||
className='flex items-center text-gray-700 dark:text-gray-600 text-xs font-medium space-x-3 rtl:space-x-reverse hover:underline'
|
||||
>
|
||||
<div
|
||||
className='flex justify-end'
|
||||
style={{ width: avatarSize }}
|
||||
>
|
||||
{icon}
|
||||
</div>
|
||||
|
||||
{text}
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
export default StatusInfo;
|
|
@ -1,29 +0,0 @@
|
|||
/**
|
||||
* SvgIcon: abstact component to render SVG icons.
|
||||
* @module soapbox/components/svg_icon
|
||||
* @see soapbox/components/icon
|
||||
*/
|
||||
|
||||
import classNames from 'clsx';
|
||||
import React from 'react';
|
||||
import InlineSVG from 'react-inlinesvg'; // eslint-disable-line no-restricted-imports
|
||||
|
||||
export interface ISvgIcon extends React.HTMLAttributes<HTMLDivElement> {
|
||||
src: string,
|
||||
id?: string,
|
||||
alt?: string,
|
||||
className?: string,
|
||||
}
|
||||
|
||||
const SvgIcon: React.FC<ISvgIcon> = ({ src, alt, className, ...rest }) => {
|
||||
return (
|
||||
<div
|
||||
className={classNames('svg-icon', className)}
|
||||
{...rest}
|
||||
>
|
||||
<InlineSVG src={src} title={alt} loader={<></>} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SvgIcon;
|
|
@ -4,10 +4,11 @@ import { FormattedMessage, useIntl } from 'react-intl';
|
|||
|
||||
import { translateStatus, undoStatusTranslation } from 'soapbox/actions/statuses';
|
||||
import { useAppDispatch, useAppSelector, useFeatures, useInstance } from 'soapbox/hooks';
|
||||
import { isLocal } from 'soapbox/utils/accounts';
|
||||
|
||||
import { Stack } from './ui';
|
||||
|
||||
import type { Status } from 'soapbox/types/entities';
|
||||
import type { Account, Status } from 'soapbox/types/entities';
|
||||
|
||||
interface ITranslateButton {
|
||||
status: Status,
|
||||
|
@ -21,10 +22,13 @@ const TranslateButton: React.FC<ITranslateButton> = ({ status }) => {
|
|||
|
||||
const me = useAppSelector((state) => state.me);
|
||||
|
||||
const allowUnauthenticated = instance.pleroma.getIn(['metadata', 'translation', 'allow_unauthenticated'], false);
|
||||
const allowRemote = instance.pleroma.getIn(['metadata', 'translation', 'allow_remote'], true);
|
||||
|
||||
const sourceLanguages = instance.pleroma.getIn(['metadata', 'translation', 'source_languages']) as ImmutableList<string>;
|
||||
const targetLanguages = instance.pleroma.getIn(['metadata', 'translation', 'target_languages']) as ImmutableList<string>;
|
||||
|
||||
const renderTranslate = me && ['public', 'unlisted'].includes(status.visibility) && status.contentHtml.length > 0 && status.language !== null && intl.locale !== status.language;
|
||||
const renderTranslate = (me || allowUnauthenticated) && (allowRemote || isLocal(status.account as Account)) && ['public', 'unlisted'].includes(status.visibility) && status.contentHtml.length > 0 && status.language !== null && intl.locale !== status.language;
|
||||
|
||||
const supportsLanguages = (!sourceLanguages || sourceLanguages.includes(status.language!)) && (!targetLanguages || targetLanguages.includes(intl.locale));
|
||||
|
||||
|
|
|
@ -20,7 +20,7 @@ const Datepicker = ({ onChange }: IDatepicker) => {
|
|||
|
||||
const [month, setMonth] = useState<number>(new Date().getMonth());
|
||||
const [day, setDay] = useState<number>(new Date().getDate());
|
||||
const [year, setYear] = useState<number>(2022);
|
||||
const [year, setYear] = useState<number>(new Date().getFullYear());
|
||||
|
||||
const numberOfDays = useMemo(() => {
|
||||
return getDaysInMonth(month, year);
|
||||
|
|
|
@ -3,8 +3,6 @@ import React from 'react';
|
|||
import { render, screen } from '../../../../jest/test-helpers';
|
||||
import FormGroup from '../form-group';
|
||||
|
||||
jest.mock('uuid', () => jest.requireActual('uuid'));
|
||||
|
||||
describe('<FormGroup />', () => {
|
||||
it('connects the label and input', () => {
|
||||
render(
|
||||
|
|
|
@ -27,7 +27,7 @@ const FormGroup: React.FC<IFormGroup> = (props) => {
|
|||
if (React.isValidElement(inputChildren[0])) {
|
||||
firstChild = React.cloneElement(
|
||||
inputChildren[0],
|
||||
{ id: formFieldId, hasError },
|
||||
{ id: formFieldId },
|
||||
);
|
||||
}
|
||||
const isCheckboxFormGroup = firstChild?.type === Checkbox;
|
||||
|
|
|
@ -33,8 +33,6 @@ interface IInput extends Pick<React.InputHTMLAttributes<HTMLInputElement>, 'maxL
|
|||
value?: string | number,
|
||||
/** Change event handler for the input. */
|
||||
onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void,
|
||||
/** Whether to display the input in red. */
|
||||
hasError?: boolean,
|
||||
/** An element to display as prefix to input. Cannot be used with icon. */
|
||||
prepend?: React.ReactElement,
|
||||
/** An element to display as suffix to input. Cannot be used with password type. */
|
||||
|
@ -48,7 +46,7 @@ const Input = React.forwardRef<HTMLInputElement, IInput>(
|
|||
(props, ref) => {
|
||||
const intl = useIntl();
|
||||
|
||||
const { type = 'text', icon, className, outerClassName, hasError, append, prepend, theme = 'normal', ...filteredProps } = props;
|
||||
const { type = 'text', icon, className, outerClassName, append, prepend, theme = 'normal', ...filteredProps } = props;
|
||||
|
||||
const [revealed, setRevealed] = React.useState(false);
|
||||
|
||||
|
@ -91,7 +89,6 @@ const Input = React.forwardRef<HTMLInputElement, IInput>(
|
|||
'rounded-md bg-white dark:bg-gray-900 border-gray-400 dark:border-gray-800': theme === 'normal',
|
||||
'rounded-full bg-gray-200 border-gray-200 dark:bg-gray-800 dark:border-gray-800 focus:bg-white': theme === 'search',
|
||||
'pr-7 rtl:pl-7 rtl:pr-3': isPassword || append,
|
||||
'text-red-600 border-red-600': hasError,
|
||||
'pl-8': typeof icon !== 'undefined',
|
||||
'pl-16': typeof prepend !== 'undefined',
|
||||
}, className)}
|
||||
|
|
|
@ -11,7 +11,7 @@ const Select = React.forwardRef<HTMLSelectElement, ISelect>((props, ref) => {
|
|||
return (
|
||||
<select
|
||||
ref={ref}
|
||||
className={`w-full pl-3 pr-10 py-2 text-base border-gray-300 dark:border-gray-800 focus:outline-none focus:ring-primary-500 focus:border-primary-500 dark:bg-gray-900 dark:text-gray-100 dark:ring-1 dark:ring-gray-800 dark:focus:ring-primary-500 dark:focus:border-primary-500 sm:text-sm rounded-md disabled:opacity-50 ${className}`}
|
||||
className={`w-full pl-3 pr-10 py-2 text-base truncate border-gray-300 dark:border-gray-800 focus:outline-none focus:ring-primary-500 focus:border-primary-500 dark:bg-gray-900 dark:text-gray-100 dark:ring-1 dark:ring-gray-800 dark:focus:ring-primary-500 dark:focus:border-primary-500 sm:text-sm rounded-md disabled:opacity-50 ${className}`}
|
||||
{...filteredProps}
|
||||
>
|
||||
{children}
|
||||
|
|
|
@ -1,77 +0,0 @@
|
|||
import React from 'react';
|
||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import {
|
||||
followAccount,
|
||||
unfollowAccount,
|
||||
blockAccount,
|
||||
unblockAccount,
|
||||
muteAccount,
|
||||
unmuteAccount,
|
||||
} from '../actions/accounts';
|
||||
import { openModal } from '../actions/modals';
|
||||
import { initMuteModal } from '../actions/mutes';
|
||||
import { getSettings } from '../actions/settings';
|
||||
import Account from '../components/account';
|
||||
import { makeGetAccount } from '../selectors';
|
||||
|
||||
const messages = defineMessages({
|
||||
unfollowConfirm: { id: 'confirmations.unfollow.confirm', defaultMessage: 'Unfollow' },
|
||||
});
|
||||
|
||||
const makeMapStateToProps = () => {
|
||||
const getAccount = makeGetAccount();
|
||||
|
||||
const mapStateToProps = (state, props) => ({
|
||||
account: getAccount(state, props.id),
|
||||
});
|
||||
|
||||
return mapStateToProps;
|
||||
};
|
||||
|
||||
const mapDispatchToProps = (dispatch, { intl }) => ({
|
||||
|
||||
onFollow(account) {
|
||||
dispatch((_, getState) => {
|
||||
const unfollowModal = getSettings(getState()).get('unfollowModal');
|
||||
if (account.relationship?.following || account.relationship?.requested) {
|
||||
if (unfollowModal) {
|
||||
dispatch(openModal('CONFIRM', {
|
||||
icon: require('@tabler/icons/minus.svg'),
|
||||
heading: <FormattedMessage id='confirmations.unfollow.heading' defaultMessage='Unfollow {name}' values={{ name: <strong>@{account.get('acct')}</strong> }} />,
|
||||
message: <FormattedMessage id='confirmations.unfollow.message' defaultMessage='Are you sure you want to unfollow {name}?' values={{ name: <strong>@{account.get('acct')}</strong> }} />,
|
||||
confirm: intl.formatMessage(messages.unfollowConfirm),
|
||||
onConfirm: () => dispatch(unfollowAccount(account.get('id'))),
|
||||
}));
|
||||
} else {
|
||||
dispatch(unfollowAccount(account.get('id')));
|
||||
}
|
||||
} else {
|
||||
dispatch(followAccount(account.get('id')));
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
onBlock(account) {
|
||||
if (account.relationship?.blocking) {
|
||||
dispatch(unblockAccount(account.get('id')));
|
||||
} else {
|
||||
dispatch(blockAccount(account.get('id')));
|
||||
}
|
||||
},
|
||||
|
||||
onMute(account) {
|
||||
if (account.relationship?.muting) {
|
||||
dispatch(unmuteAccount(account.get('id')));
|
||||
} else {
|
||||
dispatch(initMuteModal(account));
|
||||
}
|
||||
},
|
||||
|
||||
onMuteNotifications(account, notifications) {
|
||||
dispatch(muteAccount(account.get('id'), notifications));
|
||||
},
|
||||
});
|
||||
|
||||
export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(Account));
|
|
@ -0,0 +1,21 @@
|
|||
import React, { useCallback } from 'react';
|
||||
|
||||
import { useAppSelector } from 'soapbox/hooks';
|
||||
|
||||
import Account, { IAccount } from '../components/account';
|
||||
import { makeGetAccount } from '../selectors';
|
||||
|
||||
interface IAccountContainer extends Omit<IAccount, 'account'> {
|
||||
id: string
|
||||
}
|
||||
|
||||
const AccountContainer: React.FC<IAccountContainer> = ({ id, ...props }) => {
|
||||
const getAccount = useCallback(makeGetAccount(), []);
|
||||
const account = useAppSelector(state => getAccount(state, id));
|
||||
|
||||
return (
|
||||
<Account account={account!} {...props} />
|
||||
);
|
||||
};
|
||||
|
||||
export default AccountContainer;
|
|
@ -271,7 +271,6 @@ const SoapboxHead: React.FC<ISoapboxHead> = ({ children }) => {
|
|||
const bodyClass = classNames('bg-white dark:bg-gray-800 text-base h-full', {
|
||||
'no-reduce-motion': !settings.get('reduceMotion'),
|
||||
'underline-links': settings.get('underlineLinks'),
|
||||
'dyslexic': settings.get('dyslexicFont'),
|
||||
'demetricator': settings.get('demetricator'),
|
||||
});
|
||||
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
import React, { createContext, useContext, useEffect, useMemo, useState } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useHistory, useParams } from 'react-router-dom';
|
||||
|
||||
import { toggleMainWindow } from 'soapbox/actions/chats';
|
||||
import { useOwnAccount, useSettings } from 'soapbox/hooks';
|
||||
import { useAppDispatch, useOwnAccount, useSettings } from 'soapbox/hooks';
|
||||
import { IChat, useChat } from 'soapbox/queries/chats';
|
||||
|
||||
type WindowState = 'open' | 'minimized';
|
||||
|
@ -22,7 +21,7 @@ enum ChatWidgetScreens {
|
|||
|
||||
const ChatProvider: React.FC = ({ children }) => {
|
||||
const history = useHistory();
|
||||
const dispatch = useDispatch();
|
||||
const dispatch = useAppDispatch();
|
||||
const settings = useSettings();
|
||||
const account = useOwnAccount();
|
||||
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
import 'intersection-observer';
|
||||
import 'requestidlecallback';
|
||||
import objectFitImages from 'object-fit-images';
|
||||
|
||||
objectFitImages();
|
|
@ -56,7 +56,7 @@ const MediaItem: React.FC<IMediaItem> = ({ attachment, displayWidth, onOpenMedia
|
|||
const width = `${Math.floor((displayWidth - 4) / 3) - 4}px`;
|
||||
const height = width;
|
||||
const status = attachment.get('status');
|
||||
const title = status.get('spoiler_text') || attachment.get('description');
|
||||
const title = status.get('spoiler_text') || attachment.get('description');
|
||||
|
||||
let thumbnail: React.ReactNode = '';
|
||||
let icon;
|
||||
|
|
|
@ -4,9 +4,8 @@ import { Link } from 'react-router-dom';
|
|||
|
||||
import { closeReports } from 'soapbox/actions/admin';
|
||||
import { deactivateUserModal, deleteUserModal } from 'soapbox/actions/moderation';
|
||||
import Avatar from 'soapbox/components/avatar';
|
||||
import HoverRefWrapper from 'soapbox/components/hover-ref-wrapper';
|
||||
import { Accordion, Button, Stack, HStack, Text } from 'soapbox/components/ui';
|
||||
import { Accordion, Avatar, Button, Stack, HStack, Text } from 'soapbox/components/ui';
|
||||
import DropdownMenu from 'soapbox/containers/dropdown-menu-container';
|
||||
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
|
||||
import { makeGetReport } from 'soapbox/selectors';
|
||||
|
@ -86,7 +85,7 @@ const Report: React.FC<IReport> = ({ id }) => {
|
|||
<HStack space={3} className='p-3' key={report.id}>
|
||||
<HoverRefWrapper accountId={targetAccount.id} inline>
|
||||
<Link to={`/@${acct}`} title={acct}>
|
||||
<Avatar account={targetAccount} size={32} />
|
||||
<Avatar src={targetAccount.avatar} size={32} className='overflow-hidden' />
|
||||
</Link>
|
||||
</HoverRefWrapper>
|
||||
|
||||
|
|
|
@ -1,132 +0,0 @@
|
|||
import { Set as ImmutableSet, OrderedSet as ImmutableOrderedSet, is } from 'immutable';
|
||||
import debounce from 'lodash/debounce';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { injectIntl, defineMessages } from 'react-intl';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { fetchUsers } from 'soapbox/actions/admin';
|
||||
import ScrollableList from 'soapbox/components/scrollable-list';
|
||||
import { Column } from 'soapbox/components/ui';
|
||||
import AccountContainer from 'soapbox/containers/account-container';
|
||||
import { SimpleForm, TextInput } from 'soapbox/features/forms';
|
||||
|
||||
const messages = defineMessages({
|
||||
heading: { id: 'column.admin.users', defaultMessage: 'Users' },
|
||||
empty: { id: 'admin.user_index.empty', defaultMessage: 'No users found.' },
|
||||
searchPlaceholder: { id: 'admin.user_index.search_input_placeholder', defaultMessage: 'Who are you looking for?' },
|
||||
});
|
||||
|
||||
class UserIndex extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
state = {
|
||||
isLoading: true,
|
||||
filters: ImmutableSet(['local', 'active']),
|
||||
accountIds: ImmutableOrderedSet(),
|
||||
total: Infinity,
|
||||
pageSize: 50,
|
||||
page: 0,
|
||||
query: '',
|
||||
nextLink: undefined,
|
||||
}
|
||||
|
||||
clearState = callback => {
|
||||
this.setState({
|
||||
isLoading: true,
|
||||
accountIds: ImmutableOrderedSet(),
|
||||
page: 0,
|
||||
}, callback);
|
||||
}
|
||||
|
||||
fetchNextPage = () => {
|
||||
const { filters, page, query, pageSize, nextLink } = this.state;
|
||||
const nextPage = page + 1;
|
||||
|
||||
this.props.dispatch(fetchUsers(filters, nextPage, query, pageSize, nextLink))
|
||||
.then(({ users, count, next }) => {
|
||||
const newIds = users.map(user => user.id);
|
||||
|
||||
this.setState({
|
||||
isLoading: false,
|
||||
accountIds: this.state.accountIds.union(newIds),
|
||||
total: count,
|
||||
page: nextPage,
|
||||
nextLink: next,
|
||||
});
|
||||
})
|
||||
.catch(() => { });
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.fetchNextPage();
|
||||
}
|
||||
|
||||
refresh = () => {
|
||||
this.clearState(() => {
|
||||
this.fetchNextPage();
|
||||
});
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
const { filters, query } = this.state;
|
||||
const filtersChanged = !is(filters, prevState.filters);
|
||||
const queryChanged = query !== prevState.query;
|
||||
|
||||
if (filtersChanged || queryChanged) {
|
||||
this.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
handleLoadMore = debounce(() => {
|
||||
this.fetchNextPage();
|
||||
}, 2000, { leading: true });
|
||||
|
||||
updateQuery = debounce(query => {
|
||||
this.setState({ query });
|
||||
}, 900)
|
||||
|
||||
handleQueryChange = e => {
|
||||
this.updateQuery(e.target.value);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { intl } = this.props;
|
||||
const { accountIds, isLoading } = this.state;
|
||||
const hasMore = accountIds.count() < this.state.total && this.state.nextLink !== false;
|
||||
|
||||
const showLoading = isLoading && accountIds.isEmpty();
|
||||
|
||||
return (
|
||||
<Column label={intl.formatMessage(messages.heading)}>
|
||||
<SimpleForm style={{ paddingBottom: 0 }}>
|
||||
<TextInput
|
||||
onChange={this.handleQueryChange}
|
||||
placeholder={intl.formatMessage(messages.searchPlaceholder)}
|
||||
/>
|
||||
</SimpleForm>
|
||||
<ScrollableList
|
||||
scrollKey='user-index'
|
||||
hasMore={hasMore}
|
||||
isLoading={isLoading}
|
||||
showLoading={showLoading}
|
||||
onLoadMore={this.handleLoadMore}
|
||||
emptyMessage={intl.formatMessage(messages.empty)}
|
||||
className='mt-4'
|
||||
itemClassName='pb-4'
|
||||
>
|
||||
{accountIds.map(id =>
|
||||
<AccountContainer key={id} id={id} withDate />,
|
||||
)}
|
||||
</ScrollableList>
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default injectIntl(connect()(UserIndex));
|
|
@ -0,0 +1,71 @@
|
|||
import debounce from 'lodash/debounce';
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
|
||||
import { expandUserIndex, fetchUserIndex, setUserIndexQuery } from 'soapbox/actions/admin';
|
||||
import ScrollableList from 'soapbox/components/scrollable-list';
|
||||
import { Column } from 'soapbox/components/ui';
|
||||
import AccountContainer from 'soapbox/containers/account-container';
|
||||
import { SimpleForm, TextInput } from 'soapbox/features/forms';
|
||||
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
|
||||
|
||||
const messages = defineMessages({
|
||||
heading: { id: 'column.admin.users', defaultMessage: 'Users' },
|
||||
empty: { id: 'admin.user_index.empty', defaultMessage: 'No users found.' },
|
||||
searchPlaceholder: { id: 'admin.user_index.search_input_placeholder', defaultMessage: 'Who are you looking for?' },
|
||||
});
|
||||
|
||||
const UserIndex: React.FC = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const intl = useIntl();
|
||||
|
||||
const { isLoading, items, total, query, next } = useAppSelector((state) => state.admin_user_index);
|
||||
|
||||
const handleLoadMore = () => {
|
||||
dispatch(expandUserIndex());
|
||||
};
|
||||
|
||||
const updateQuery = useCallback(debounce(() => {
|
||||
dispatch(fetchUserIndex());
|
||||
}, 900, { leading: true }), []);
|
||||
|
||||
const handleQueryChange: React.ChangeEventHandler<HTMLInputElement> = e => {
|
||||
dispatch(setUserIndexQuery(e.target.value));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
updateQuery();
|
||||
}, [query]);
|
||||
|
||||
const hasMore = items.count() < total && next !== null;
|
||||
|
||||
const showLoading = isLoading && items.isEmpty();
|
||||
|
||||
return (
|
||||
<Column label={intl.formatMessage(messages.heading)}>
|
||||
<SimpleForm style={{ paddingBottom: 0 }}>
|
||||
<TextInput
|
||||
value={query}
|
||||
onChange={handleQueryChange}
|
||||
placeholder={intl.formatMessage(messages.searchPlaceholder)}
|
||||
/>
|
||||
</SimpleForm>
|
||||
<ScrollableList
|
||||
scrollKey='user-index'
|
||||
hasMore={hasMore}
|
||||
isLoading={isLoading}
|
||||
showLoading={showLoading}
|
||||
onLoadMore={handleLoadMore}
|
||||
emptyMessage={intl.formatMessage(messages.empty)}
|
||||
className='mt-4'
|
||||
itemClassName='pb-4'
|
||||
>
|
||||
{items.map(id =>
|
||||
<AccountContainer key={id} id={id} withDate />,
|
||||
)}
|
||||
</ScrollableList>
|
||||
</Column>
|
||||
);
|
||||
};
|
||||
|
||||
export default UserIndex;
|
|
@ -1,12 +1,11 @@
|
|||
import classNames from 'clsx';
|
||||
import React from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
import { fetchAliasesSuggestions, clearAliasesSuggestions, changeAliasesSuggestions } from 'soapbox/actions/aliases';
|
||||
import Icon from 'soapbox/components/icon';
|
||||
import { Button } from 'soapbox/components/ui';
|
||||
import { useAppSelector } from 'soapbox/hooks';
|
||||
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
|
||||
|
||||
const messages = defineMessages({
|
||||
search: { id: 'aliases.search', defaultMessage: 'Search your old account' },
|
||||
|
@ -14,7 +13,7 @@ const messages = defineMessages({
|
|||
});
|
||||
|
||||
const Search: React.FC = () => {
|
||||
const dispatch = useDispatch();
|
||||
const dispatch = useAppDispatch();
|
||||
const intl = useIntl();
|
||||
|
||||
const value = useAppSelector(state => state.aliases.suggestions.value);
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import classNames from 'clsx';
|
||||
import debounce from 'lodash/debounce';
|
||||
import throttle from 'lodash/throttle';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import React, { useEffect, useLayoutEffect, useRef, useState } from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
|
||||
import Icon from 'soapbox/components/icon';
|
||||
|
@ -397,7 +397,7 @@ const Audio: React.FC<IAudio> = (props) => {
|
|||
|
||||
const progress = Math.min((currentTime / getDuration()) * 100, 100);
|
||||
|
||||
useEffect(() => {
|
||||
useLayoutEffect(() => {
|
||||
if (player.current) {
|
||||
_setDimensions();
|
||||
}
|
||||
|
|
|
@ -15,10 +15,10 @@ const hex2rgba = (hex: string, alpha = 1) => {
|
|||
|
||||
export default class Visualizer {
|
||||
|
||||
tickSize: number
|
||||
canvas?: HTMLCanvasElement
|
||||
context?: CanvasRenderingContext2D
|
||||
analyser?: AnalyserNode
|
||||
tickSize: number;
|
||||
canvas?: HTMLCanvasElement;
|
||||
context?: CanvasRenderingContext2D;
|
||||
analyser?: AnalyserNode;
|
||||
|
||||
constructor(tickSize: number) {
|
||||
this.tickSize = tickSize;
|
||||
|
|
|
@ -238,7 +238,6 @@ const RegistrationForm: React.FC<IRegistrationForm> = ({ inviteToken }) => {
|
|||
pattern='^[a-zA-Z\d_-]+'
|
||||
onChange={onUsernameChange}
|
||||
value={params.get('username', '')}
|
||||
hasError={usernameUnavailable}
|
||||
required
|
||||
/>
|
||||
</FormGroup>
|
||||
|
|
|
@ -7,8 +7,6 @@ import { Button, Card, CardBody, CardHeader, CardTitle, Column, Spinner, Stack,
|
|||
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
|
||||
import { Token } from 'soapbox/reducers/security';
|
||||
|
||||
import type { Map as ImmutableMap } from 'immutable';
|
||||
|
||||
const messages = defineMessages({
|
||||
header: { id: 'security.headers.tokens', defaultMessage: 'Sessions' },
|
||||
revoke: { id: 'security.tokens.revoke', defaultMessage: 'Revoke' },
|
||||
|
@ -75,9 +73,9 @@ const AuthTokenList: React.FC = () => {
|
|||
const intl = useIntl();
|
||||
const tokens = useAppSelector(state => state.security.get('tokens').reverse());
|
||||
const currentTokenId = useAppSelector(state => {
|
||||
const currentToken = state.auth.get('tokens').valueSeq().find((token: ImmutableMap<string, any>) => token.get('me') === state.auth.get('me'));
|
||||
const currentToken = state.auth.tokens.valueSeq().find((token) => token.me === state.auth.me);
|
||||
|
||||
return currentToken?.get('id');
|
||||
return currentToken?.id;
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
import React, { useCallback } from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import Avatar from 'soapbox/components/avatar';
|
||||
import DisplayName from 'soapbox/components/display-name';
|
||||
import AccountComponent from 'soapbox/components/account';
|
||||
import Icon from 'soapbox/components/icon';
|
||||
import { HStack } from 'soapbox/components/ui';
|
||||
import { useAppSelector } from 'soapbox/hooks';
|
||||
import { makeGetAccount } from 'soapbox/selectors';
|
||||
|
||||
|
@ -22,12 +21,6 @@ const Account: React.FC<IAccount> = ({ accountId }) => {
|
|||
|
||||
const account = useAppSelector((state) => getAccount(state, accountId));
|
||||
|
||||
// useEffect(() => {
|
||||
// if (accountId && !account) {
|
||||
// fetchAccount(accountId);
|
||||
// }
|
||||
// }, [accountId]);
|
||||
|
||||
if (!account) return null;
|
||||
|
||||
const birthday = account.birthday;
|
||||
|
@ -36,26 +29,20 @@ const Account: React.FC<IAccount> = ({ accountId }) => {
|
|||
const formattedBirthday = intl.formatDate(birthday, { day: 'numeric', month: 'short', year: 'numeric' });
|
||||
|
||||
return (
|
||||
<div className='account'>
|
||||
<div className='account__wrapper'>
|
||||
<Link className='account__display-name' title={account.get('acct')} to={`/@${account.get('acct')}`}>
|
||||
<div className='account__display-name'>
|
||||
<div className='account__avatar-wrapper'><Avatar account={account} size={36} /></div>
|
||||
<DisplayName account={account} />
|
||||
|
||||
</div>
|
||||
</Link>
|
||||
<div
|
||||
className='flex items-center gap-0.5'
|
||||
title={intl.formatMessage(messages.birthday, {
|
||||
date: formattedBirthday,
|
||||
})}
|
||||
>
|
||||
<Icon src={require('@tabler/icons/ballon.svg')} />
|
||||
{formattedBirthday}
|
||||
</div>
|
||||
<HStack space={1} alignItems='center' justifyContent='between' className='p-2.5'>
|
||||
<div className='w-full'>
|
||||
<AccountComponent account={account} withRelationship={false} />
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className='flex items-center gap-0.5'
|
||||
title={intl.formatMessage(messages.birthday, {
|
||||
date: formattedBirthday,
|
||||
})}
|
||||
>
|
||||
<Icon src={require('@tabler/icons/ballon.svg')} />
|
||||
{formattedBirthday}
|
||||
</div>
|
||||
</HStack>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -1,13 +1,12 @@
|
|||
import debounce from 'lodash/debounce';
|
||||
import React from 'react';
|
||||
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
import { fetchBlocks, expandBlocks } from 'soapbox/actions/blocks';
|
||||
import ScrollableList from 'soapbox/components/scrollable-list';
|
||||
import { Column, Spinner } from 'soapbox/components/ui';
|
||||
import AccountContainer from 'soapbox/containers/account-container';
|
||||
import { useAppSelector } from 'soapbox/hooks';
|
||||
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
|
||||
|
||||
const messages = defineMessages({
|
||||
heading: { id: 'column.blocks', defaultMessage: 'Blocked users' },
|
||||
|
@ -18,7 +17,7 @@ const handleLoadMore = debounce((dispatch) => {
|
|||
}, 300, { leading: true });
|
||||
|
||||
const Blocks: React.FC = () => {
|
||||
const dispatch = useDispatch();
|
||||
const dispatch = useAppDispatch();
|
||||
const intl = useIntl();
|
||||
|
||||
const accountIds = useAppSelector((state) => state.user_lists.blocks.items);
|
||||
|
|
|
@ -66,6 +66,21 @@ const List: Components['List'] = React.forwardRef((props, ref) => {
|
|||
return <div ref={ref} {...rest} className='mb-2' />;
|
||||
});
|
||||
|
||||
const Scroller: Components['Scroller'] = React.forwardRef((props, ref) => {
|
||||
const { style, context, ...rest } = props;
|
||||
|
||||
return (
|
||||
<div
|
||||
{...rest}
|
||||
ref={ref}
|
||||
style={{
|
||||
...style,
|
||||
scrollbarGutter: 'stable',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
interface IChatMessageList {
|
||||
/** Chat the messages are being rendered from. */
|
||||
chat: IChat,
|
||||
|
@ -472,6 +487,7 @@ const ChatMessageList: React.FC<IChatMessageList> = ({ chat }) => {
|
|||
}}
|
||||
components={{
|
||||
List,
|
||||
Scroller,
|
||||
Header: () => {
|
||||
if (hasNextPage || isFetchingNextPage) {
|
||||
return <Spinner withText={false} />;
|
||||
|
|
|
@ -6,7 +6,7 @@ import { __stub } from 'soapbox/api';
|
|||
import { normalizeAccount } from 'soapbox/normalizers';
|
||||
import { ReducerAccount } from 'soapbox/reducers/accounts';
|
||||
|
||||
import { render, screen } from '../../../../../jest/test-helpers';
|
||||
import { render, screen, waitFor } from '../../../../../jest/test-helpers';
|
||||
import ChatPage from '../chat-page';
|
||||
|
||||
describe('<ChatPage />', () => {
|
||||
|
@ -48,7 +48,10 @@ describe('<ChatPage />', () => {
|
|||
await userEvent.click(screen.getByTestId('button'));
|
||||
|
||||
expect(screen.getByTestId('chat-page')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('toast')).toHaveTextContent('Chat Settings updated successfully');
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('toast')).toHaveTextContent('Chat Settings updated successfully');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -77,7 +80,10 @@ describe('<ChatPage />', () => {
|
|||
it('renders the Chats', async () => {
|
||||
render(<ChatPage />, undefined, store);
|
||||
await userEvent.click(screen.getByTestId('button'));
|
||||
expect(screen.getByTestId('toast')).toHaveTextContent('Chat Settings failed to update.');
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('toast')).toHaveTextContent('Chat Settings failed to update.');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import classNames from 'clsx';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import React, { useEffect, useLayoutEffect, useRef, useState } from 'react';
|
||||
import { matchPath, Route, Switch, useHistory } from 'react-router-dom';
|
||||
|
||||
import { Stack } from 'soapbox/components/ui';
|
||||
|
@ -44,7 +44,7 @@ const ChatPage: React.FC<IChatPage> = ({ chatId }) => {
|
|||
setHeight(fullHeight - top + offset);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
useLayoutEffect(() => {
|
||||
calculateHeight();
|
||||
}, [containerRef.current]);
|
||||
|
||||
|
|
|
@ -238,7 +238,7 @@ const ChatPageMain = () => {
|
|||
|
||||
<div className='h-full overflow-hidden'>
|
||||
<Chat
|
||||
className='h-full overflow-hidden'
|
||||
className='h-full'
|
||||
chat={chat}
|
||||
inputRef={inputRef}
|
||||
/>
|
||||
|
|
|
@ -1,15 +1,21 @@
|
|||
import React from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
|
||||
import { CardTitle, HStack, IconButton, Stack } from 'soapbox/components/ui';
|
||||
|
||||
import ChatSearch from '../../chat-search/chat-search';
|
||||
|
||||
const messages = defineMessages({
|
||||
title: { id: 'chat.new_message.title', defaultMessage: 'New Message' },
|
||||
});
|
||||
|
||||
interface IChatPageNew {
|
||||
}
|
||||
|
||||
/** New message form to create a chat. */
|
||||
const ChatPageNew: React.FC<IChatPageNew> = () => {
|
||||
const intl = useIntl();
|
||||
const history = useHistory();
|
||||
|
||||
return (
|
||||
|
@ -22,7 +28,7 @@ const ChatPageNew: React.FC<IChatPageNew> = () => {
|
|||
onClick={() => history.push('/chats')}
|
||||
/>
|
||||
|
||||
<CardTitle title='New Message' />
|
||||
<CardTitle title={intl.formatMessage(messages.title)} />
|
||||
</HStack>
|
||||
</Stack>
|
||||
|
||||
|
@ -31,4 +37,4 @@ const ChatPageNew: React.FC<IChatPageNew> = () => {
|
|||
);
|
||||
};
|
||||
|
||||
export default ChatPageNew;
|
||||
export default ChatPageNew;
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { useMutation } from '@tanstack/react-query';
|
||||
import { AxiosError } from 'axios';
|
||||
import React, { useState } from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
|
||||
import { Icon, Input, Stack } from 'soapbox/components/ui';
|
||||
|
@ -17,11 +18,16 @@ import Blankslate from './blankslate';
|
|||
import EmptyResultsBlankslate from './empty-results-blankslate';
|
||||
import Results from './results';
|
||||
|
||||
const messages = defineMessages({
|
||||
placeholder: { id: 'chat_search.placeholder', defaultMessage: 'Type a name' },
|
||||
});
|
||||
|
||||
interface IChatSearch {
|
||||
isMainPage?: boolean
|
||||
}
|
||||
|
||||
const ChatSearch = (props: IChatSearch) => {
|
||||
const intl = useIntl();
|
||||
const { isMainPage = false } = props;
|
||||
|
||||
const debounce = useDebounce;
|
||||
|
@ -88,7 +94,7 @@ const ChatSearch = (props: IChatSearch) => {
|
|||
data-testid='search'
|
||||
type='text'
|
||||
autoFocus
|
||||
placeholder='Type a name'
|
||||
placeholder={intl.formatMessage(messages.placeholder)}
|
||||
value={value || ''}
|
||||
onChange={(event) => setValue(event.target.value)}
|
||||
outerClassName='mt-0'
|
||||
|
@ -112,4 +118,4 @@ const ChatSearch = (props: IChatSearch) => {
|
|||
);
|
||||
};
|
||||
|
||||
export default ChatSearch;
|
||||
export default ChatSearch;
|
||||
|
|
|
@ -75,7 +75,7 @@ const ComposeForm = <ID extends string>({ id, shouldCondense, autoFocus, clickab
|
|||
const showSearch = useAppSelector((state) => state.search.submitted && !state.search.hidden);
|
||||
const isModalOpen = useAppSelector((state) => !!(state.modals.size && state.modals.last()!.modalType === 'COMPOSE'));
|
||||
const maxTootChars = configuration.getIn(['statuses', 'max_characters']) as number;
|
||||
const scheduledStatusCount = useAppSelector((state) => state.get('scheduled_statuses').size);
|
||||
const scheduledStatusCount = useAppSelector((state) => state.scheduled_statuses.size);
|
||||
const features = useFeatures();
|
||||
|
||||
const { text, suggestions, spoiler, spoiler_text: spoilerText, privacy, focusDate, caretPosition, is_submitting: isSubmitting, is_changing_upload: isChangingUpload, is_uploading: isUploading, schedule: scheduledAt } = compose;
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
import classNames from 'clsx';
|
||||
import { Map as ImmutableMap } from 'immutable';
|
||||
import debounce from 'lodash/debounce';
|
||||
import React, { useCallback } from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
|
||||
import {
|
||||
|
@ -17,7 +15,8 @@ import {
|
|||
import AutosuggestAccountInput from 'soapbox/components/autosuggest-account-input';
|
||||
import { Input } from 'soapbox/components/ui';
|
||||
import SvgIcon from 'soapbox/components/ui/icon/svg-icon';
|
||||
import { useAppSelector } from 'soapbox/hooks';
|
||||
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
|
||||
import { AppDispatch, RootState } from 'soapbox/store';
|
||||
|
||||
const messages = defineMessages({
|
||||
placeholder: { id: 'search.placeholder', defaultMessage: 'Search' },
|
||||
|
@ -25,7 +24,7 @@ const messages = defineMessages({
|
|||
});
|
||||
|
||||
function redirectToAccount(accountId: string, routerHistory: any) {
|
||||
return (_dispatch: any, getState: () => ImmutableMap<string, any>) => {
|
||||
return (_dispatch: AppDispatch, getState: () => RootState) => {
|
||||
const acct = getState().getIn(['accounts', accountId, 'acct']);
|
||||
|
||||
if (acct && routerHistory) {
|
||||
|
@ -49,7 +48,7 @@ const Search = (props: ISearch) => {
|
|||
openInRoute = false,
|
||||
} = props;
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const dispatch = useAppDispatch();
|
||||
const history = useHistory();
|
||||
const intl = useIntl();
|
||||
|
||||
|
|
|
@ -1,36 +0,0 @@
|
|||
import React from 'react';
|
||||
|
||||
interface ITextIconButton {
|
||||
label: string,
|
||||
title: string,
|
||||
active: boolean,
|
||||
onClick: () => void,
|
||||
ariaControls: string,
|
||||
unavailable: boolean,
|
||||
}
|
||||
|
||||
const TextIconButton: React.FC<ITextIconButton> = ({
|
||||
label,
|
||||
title,
|
||||
active,
|
||||
ariaControls,
|
||||
unavailable,
|
||||
onClick,
|
||||
}) => {
|
||||
const handleClick: React.MouseEventHandler = (e) => {
|
||||
e.preventDefault();
|
||||
onClick();
|
||||
};
|
||||
|
||||
if (unavailable) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<button title={title} aria-label={title} className={`text-icon-button ${active ? 'active' : ''}`} aria-expanded={active} onClick={handleClick} aria-controls={ariaControls}>
|
||||
{label}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
export default TextIconButton;
|
|
@ -1,9 +1,9 @@
|
|||
import React, { useState } from 'react';
|
||||
import { FormattedMessage, defineMessages, useIntl } from 'react-intl';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
import { changeSettingImmediate } from 'soapbox/actions/settings';
|
||||
import { Column, Button, Form, FormActions, FormGroup, Input, Text } from 'soapbox/components/ui';
|
||||
import { useAppDispatch } from 'soapbox/hooks';
|
||||
import toast from 'soapbox/toast';
|
||||
|
||||
const messages = defineMessages({
|
||||
|
@ -15,7 +15,7 @@ const messages = defineMessages({
|
|||
});
|
||||
|
||||
const DevelopersChallenge = () => {
|
||||
const dispatch = useDispatch();
|
||||
const dispatch = useAppDispatch();
|
||||
const intl = useIntl();
|
||||
|
||||
const [answer, setAnswer] = useState('');
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import React from 'react';
|
||||
import { FormattedMessage, defineMessages, useIntl } from 'react-intl';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { Link, useHistory } from 'react-router-dom';
|
||||
|
||||
import { changeSettingImmediate } from 'soapbox/actions/settings';
|
||||
import { Column, Text } from 'soapbox/components/ui';
|
||||
import SvgIcon from 'soapbox/components/ui/icon/svg-icon';
|
||||
import { useAppDispatch } from 'soapbox/hooks';
|
||||
import toast from 'soapbox/toast';
|
||||
import sourceCode from 'soapbox/utils/code';
|
||||
|
||||
|
@ -31,7 +31,7 @@ const DashWidget: React.FC<IDashWidget> = ({ to, onClick, children }) => {
|
|||
};
|
||||
|
||||
const Developers: React.FC = () => {
|
||||
const dispatch = useDispatch();
|
||||
const dispatch = useAppDispatch();
|
||||
const history = useHistory();
|
||||
const intl = useIntl();
|
||||
|
||||
|
|
|
@ -37,7 +37,7 @@ const SettingsStore: React.FC = () => {
|
|||
const intl = useIntl();
|
||||
const dispatch = useAppDispatch();
|
||||
const settings = useSettings();
|
||||
const settingsStore = useAppSelector(state => state.get('settings'));
|
||||
const settingsStore = useAppSelector(state => state.settings);
|
||||
|
||||
const [rawJSON, setRawJSON] = useState<string>(JSON.stringify(settingsStore, null, 2));
|
||||
const [jsonValid, setJsonValid] = useState(true);
|
||||
|
@ -134,12 +134,6 @@ const SettingsStore: React.FC = () => {
|
|||
<SettingToggle settings={settings} settingPath={['systemFont']} onChange={onToggleChange} />
|
||||
</ListItem>
|
||||
|
||||
<div className='dyslexic'>
|
||||
<ListItem label={<FormattedMessage id='preferences.fields.dyslexic_font_label' defaultMessage='Dyslexic mode' />}>
|
||||
<SettingToggle settings={settings} settingPath={['dyslexicFont']} onChange={onToggleChange} />
|
||||
</ListItem>
|
||||
</div>
|
||||
|
||||
<ListItem
|
||||
label={<FormattedMessage id='preferences.fields.demetricator_label' defaultMessage='Use Demetricator' />}
|
||||
hint={<FormattedMessage id='preferences.hints.demetricator' defaultMessage='Decrease social media anxiety by hiding all numbers from the site.' />}
|
||||
|
|
|
@ -1,13 +1,12 @@
|
|||
import classNames from 'clsx';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
import { fetchDirectory, expandDirectory } from 'soapbox/actions/directory';
|
||||
import LoadMore from 'soapbox/components/load-more';
|
||||
import { Column, RadioButton, Stack, Text } from 'soapbox/components/ui';
|
||||
import { useAppSelector, useFeatures, useInstance } from 'soapbox/hooks';
|
||||
import { useAppDispatch, useAppSelector, useFeatures, useInstance } from 'soapbox/hooks';
|
||||
|
||||
import AccountCard from './components/account-card';
|
||||
|
||||
|
@ -21,7 +20,7 @@ const messages = defineMessages({
|
|||
|
||||
const Directory = () => {
|
||||
const intl = useIntl();
|
||||
const dispatch = useDispatch();
|
||||
const dispatch = useAppDispatch();
|
||||
const { search } = useLocation();
|
||||
const params = new URLSearchParams(search);
|
||||
const instance = useInstance();
|
||||
|
|
|
@ -1,13 +1,12 @@
|
|||
import debounce from 'lodash/debounce';
|
||||
import React from 'react';
|
||||
import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
import { fetchDomainBlocks, expandDomainBlocks } from 'soapbox/actions/domain-blocks';
|
||||
import Domain from 'soapbox/components/domain';
|
||||
import ScrollableList from 'soapbox/components/scrollable-list';
|
||||
import { Column, Spinner } from 'soapbox/components/ui';
|
||||
import { useAppSelector } from 'soapbox/hooks';
|
||||
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
|
||||
|
||||
const messages = defineMessages({
|
||||
heading: { id: 'column.domain_blocks', defaultMessage: 'Hidden domains' },
|
||||
|
@ -19,7 +18,7 @@ const handleLoadMore = debounce((dispatch) => {
|
|||
}, 300, { leading: true });
|
||||
|
||||
const DomainBlocks: React.FC = () => {
|
||||
const dispatch = useDispatch();
|
||||
const dispatch = useAppDispatch();
|
||||
const intl = useIntl();
|
||||
|
||||
const domains = useAppSelector((state) => state.domain_lists.blocks.items);
|
||||
|
|
|
@ -17,12 +17,14 @@ const messages = defineMessages({
|
|||
|
||||
/** Form for logging into a remote instance */
|
||||
const ExternalLoginForm: React.FC = () => {
|
||||
const code = new URLSearchParams(window.location.search).get('code');
|
||||
const query = new URLSearchParams(window.location.search);
|
||||
const code = query.get('code');
|
||||
const server = query.get('server');
|
||||
|
||||
const intl = useIntl();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const [host, setHost] = useState('');
|
||||
const [host, setHost] = useState(server || '');
|
||||
const [isLoading, setLoading] = useState(false);
|
||||
|
||||
const handleHostChange: React.ChangeEventHandler<HTMLInputElement> = ({ currentTarget }) => {
|
||||
|
@ -44,6 +46,12 @@ const ExternalLoginForm: React.FC = () => {
|
|||
toast.error(intl.formatMessage(messages.networkFailed));
|
||||
}
|
||||
|
||||
// If the server was invalid, clear it from the URL.
|
||||
// https://stackoverflow.com/a/40592892
|
||||
if (server) {
|
||||
window.history.pushState(null, '', window.location.pathname);
|
||||
}
|
||||
|
||||
setLoading(false);
|
||||
});
|
||||
};
|
||||
|
@ -54,7 +62,13 @@ const ExternalLoginForm: React.FC = () => {
|
|||
}
|
||||
}, [code]);
|
||||
|
||||
if (code) {
|
||||
useEffect(() => {
|
||||
if (server && !code) {
|
||||
handleSubmit();
|
||||
}
|
||||
}, [server]);
|
||||
|
||||
if (code || server) {
|
||||
return <Spinner />;
|
||||
}
|
||||
|
||||
|
|
|
@ -78,6 +78,9 @@ describe('<FeedCarousel />', () => {
|
|||
expect(screen.getAllByTestId('carousel-item-avatar')[0]).toHaveClass('ring-primary-600');
|
||||
});
|
||||
|
||||
// HACK: wait for state change
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
|
||||
// Marked as seen, not selected
|
||||
await userEvent.click(screen.getAllByTestId('carousel-item-avatar')[0]);
|
||||
await waitFor(() => {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import classNames from 'clsx';
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import { replaceHomeTimeline } from 'soapbox/actions/timelines';
|
||||
|
@ -11,7 +11,7 @@ import PlaceholderAvatar from '../placeholder/components/placeholder-avatar';
|
|||
|
||||
const CarouselItem = React.forwardRef((
|
||||
{ avatar, seen, onViewed, onPinned }: { avatar: Avatar, seen: boolean, onViewed: (account_id: string) => void, onPinned?: (avatar: null | Avatar) => void },
|
||||
ref: any,
|
||||
ref: React.ForwardedRef<HTMLDivElement>,
|
||||
) => {
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
|
@ -40,8 +40,11 @@ const CarouselItem = React.forwardRef((
|
|||
onPinned(avatar);
|
||||
}
|
||||
|
||||
onViewed(avatar.account_id);
|
||||
markAsSeen.mutate(avatar.account_id);
|
||||
if (!seen) {
|
||||
onViewed(avatar.account_id);
|
||||
markAsSeen.mutate(avatar.account_id);
|
||||
}
|
||||
|
||||
dispatch(replaceHomeTimeline(avatar.account_id, { maxId: null }, () => setLoading(false)));
|
||||
}
|
||||
};
|
||||
|
@ -51,7 +54,7 @@ const CarouselItem = React.forwardRef((
|
|||
ref={ref}
|
||||
aria-disabled={isFetching}
|
||||
onClick={handleClick}
|
||||
className='cursor-pointer snap-start py-4'
|
||||
className='cursor-pointer py-4'
|
||||
role='filter-feed-by-user'
|
||||
data-testid='carousel-item'
|
||||
>
|
||||
|
@ -87,6 +90,7 @@ const FeedCarousel = () => {
|
|||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const [_ref, setContainerRef, { width }] = useDimensions();
|
||||
const carouselItemRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const [seenAccountIds, setSeenAccountIds] = useState<string[]>([]);
|
||||
const [pageSize, setPageSize] = useState<number>(0);
|
||||
|
@ -94,13 +98,21 @@ const FeedCarousel = () => {
|
|||
const [pinnedAvatar, setPinnedAvatar] = useState<Avatar | null>(null);
|
||||
|
||||
const avatarsToList = useMemo(() => {
|
||||
const list = avatars.filter((avatar) => avatar.account_id !== pinnedAvatar?.account_id);
|
||||
let list: (Avatar | null)[] = avatars.filter((avatar) => avatar.account_id !== pinnedAvatar?.account_id);
|
||||
|
||||
// If we have an Avatar pinned, let's create a new array with "null"
|
||||
// in the first position of each page.
|
||||
if (pinnedAvatar) {
|
||||
return [null, ...list];
|
||||
const index = (currentPage - 1) * pageSize;
|
||||
list = [
|
||||
...list.slice(0, index),
|
||||
null,
|
||||
...list.slice(index),
|
||||
];
|
||||
}
|
||||
|
||||
return list;
|
||||
}, [avatars, pinnedAvatar]);
|
||||
}, [avatars, pinnedAvatar, currentPage, pageSize]);
|
||||
|
||||
const numberOfPages = Math.ceil(avatars.length / pageSize);
|
||||
const widthPerAvatar = width / (Math.floor(width / 80));
|
||||
|
@ -151,23 +163,23 @@ const FeedCarousel = () => {
|
|||
data-testid='feed-carousel'
|
||||
>
|
||||
<HStack alignItems='stretch'>
|
||||
<div className='z-10 rounded-l-xl bg-white dark:bg-gray-900 w-8 flex self-stretch items-center justify-center'>
|
||||
<div className='z-10 rounded-l-xl bg-white dark:bg-primary-900 w-8 flex self-stretch items-center justify-center'>
|
||||
<button
|
||||
data-testid='prev-page'
|
||||
onClick={handlePrevPage}
|
||||
className='h-7 w-7 flex items-center justify-center disabled:opacity-25 transition-opacity duration-500'
|
||||
className='h-full w-7 flex items-center justify-center disabled:opacity-25 transition-opacity duration-500'
|
||||
disabled={!hasPrevPage}
|
||||
>
|
||||
<Icon src={require('@tabler/icons/chevron-left.svg')} className='text-black dark:text-white h-5 w-5' />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className='overflow-hidden relative'>
|
||||
<div className='overflow-hidden relative w-full'>
|
||||
{pinnedAvatar ? (
|
||||
<div
|
||||
className='z-10 flex items-center justify-center absolute left-0 top-0 bottom-0 bg-white dark:bg-primary-900'
|
||||
style={{
|
||||
width: widthPerAvatar,
|
||||
width: widthPerAvatar || 'auto',
|
||||
}}
|
||||
>
|
||||
<CarouselItem
|
||||
|
@ -175,6 +187,7 @@ const FeedCarousel = () => {
|
|||
seen={seenAccountIds?.includes(pinnedAvatar.account_id)}
|
||||
onViewed={markAsSeen}
|
||||
onPinned={(avatar) => setPinnedAvatar(avatar)}
|
||||
ref={carouselItemRef}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
|
@ -189,7 +202,11 @@ const FeedCarousel = () => {
|
|||
>
|
||||
{isFetching ? (
|
||||
new Array(20).fill(0).map((_, idx) => (
|
||||
<div className='flex flex-shrink-0 justify-center' style={{ width: widthPerAvatar }} key={idx}>
|
||||
<div
|
||||
className='flex flex-shrink-0 justify-center'
|
||||
style={{ width: widthPerAvatar || 'auto' }}
|
||||
key={idx}
|
||||
>
|
||||
<PlaceholderAvatar size={56} withText />
|
||||
</div>
|
||||
))
|
||||
|
@ -199,11 +216,15 @@ const FeedCarousel = () => {
|
|||
key={avatar?.account_id || index}
|
||||
className='flex flex-shrink-0 justify-center'
|
||||
style={{
|
||||
width: widthPerAvatar,
|
||||
width: widthPerAvatar || 'auto',
|
||||
}}
|
||||
>
|
||||
{avatar === null ? (
|
||||
<Stack className='w-14 snap-start py-4 h-auto' space={3}>
|
||||
<Stack
|
||||
className='w-14 py-4 h-auto'
|
||||
space={3}
|
||||
style={{ height: carouselItemRef.current?.clientHeight }}
|
||||
>
|
||||
<div className='block mx-auto relative w-16 h-16 rounded-full'>
|
||||
<div className='w-16 h-16' />
|
||||
</div>
|
||||
|
@ -227,11 +248,11 @@ const FeedCarousel = () => {
|
|||
</HStack>
|
||||
</div>
|
||||
|
||||
<div className='z-10 rounded-r-xl bg-white dark:bg-gray-900 w-8 self-stretch flex items-center justify-center'>
|
||||
<div className='z-10 rounded-r-xl bg-white dark:bg-primary-900 w-8 self-stretch flex items-center justify-center'>
|
||||
<button
|
||||
data-testid='next-page'
|
||||
onClick={handleNextPage}
|
||||
className='h-7 w-7 flex items-center justify-center disabled:opacity-25 transition-opacity duration-500'
|
||||
className='h-full w-7 flex items-center justify-center disabled:opacity-25 transition-opacity duration-500'
|
||||
disabled={!hasNextPage}
|
||||
>
|
||||
<Icon src={require('@tabler/icons/chevron-right.svg')} className='text-black dark:text-white h-5 w-5' />
|
||||
|
|
|
@ -1,14 +1,10 @@
|
|||
import React, { useCallback } from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { authorizeFollowRequest, rejectFollowRequest } from 'soapbox/actions/accounts';
|
||||
import Avatar from 'soapbox/components/avatar';
|
||||
import DisplayName from 'soapbox/components/display-name';
|
||||
import IconButton from 'soapbox/components/icon-button';
|
||||
import { Text } from 'soapbox/components/ui';
|
||||
import { useAppSelector } from 'soapbox/hooks';
|
||||
import Account from 'soapbox/components/account';
|
||||
import { Button, HStack } from 'soapbox/components/ui';
|
||||
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
|
||||
import { makeGetAccount } from 'soapbox/selectors';
|
||||
|
||||
const messages = defineMessages({
|
||||
|
@ -22,7 +18,7 @@ interface IAccountAuthorize {
|
|||
|
||||
const AccountAuthorize: React.FC<IAccountAuthorize> = ({ id }) => {
|
||||
const intl = useIntl();
|
||||
const dispatch = useDispatch();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const getAccount = useCallback(makeGetAccount(), []);
|
||||
|
||||
|
@ -38,24 +34,28 @@ const AccountAuthorize: React.FC<IAccountAuthorize> = ({ id }) => {
|
|||
|
||||
if (!account) return null;
|
||||
|
||||
const content = { __html: account.note_emojified };
|
||||
|
||||
return (
|
||||
<div className='account-authorize__wrapper'>
|
||||
<div className='account-authorize'>
|
||||
<Link to={`/@${account.acct}`}>
|
||||
<div className='account-authorize__avatar'><Avatar account={account} size={48} /></div>
|
||||
<DisplayName account={account} />
|
||||
</Link>
|
||||
|
||||
<Text className='account__header__content' dangerouslySetInnerHTML={content} />
|
||||
<HStack space={1} alignItems='center' justifyContent='between' className='p-2.5'>
|
||||
<div className='w-full'>
|
||||
<Account account={account} withRelationship={false} />
|
||||
</div>
|
||||
|
||||
<div className='account--panel'>
|
||||
<div className='account--panel__button'><IconButton title={intl.formatMessage(messages.authorize)} src={require('@tabler/icons/check.svg')} onClick={onAuthorize} /></div>
|
||||
<div className='account--panel__button'><IconButton title={intl.formatMessage(messages.reject)} src={require('@tabler/icons/x.svg')} onClick={onReject} /></div>
|
||||
</div>
|
||||
</div>
|
||||
<HStack space={2}>
|
||||
<Button
|
||||
theme='secondary'
|
||||
size='sm'
|
||||
text={intl.formatMessage(messages.authorize)}
|
||||
icon={require('@tabler/icons/check.svg')}
|
||||
onClick={onAuthorize}
|
||||
/>
|
||||
<Button
|
||||
theme='danger'
|
||||
size='sm'
|
||||
text={intl.formatMessage(messages.reject)}
|
||||
icon={require('@tabler/icons/x.svg')}
|
||||
onClick={onReject}
|
||||
/>
|
||||
</HStack>
|
||||
</HStack>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -1,12 +1,11 @@
|
|||
import debounce from 'lodash/debounce';
|
||||
import React from 'react';
|
||||
import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
import { fetchFollowRequests, expandFollowRequests } from 'soapbox/actions/accounts';
|
||||
import ScrollableList from 'soapbox/components/scrollable-list';
|
||||
import { Column, Spinner } from 'soapbox/components/ui';
|
||||
import { useAppSelector } from 'soapbox/hooks';
|
||||
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
|
||||
|
||||
import AccountAuthorize from './components/account-authorize';
|
||||
|
||||
|
@ -19,7 +18,7 @@ const handleLoadMore = debounce((dispatch) => {
|
|||
}, 300, { leading: true });
|
||||
|
||||
const FollowRequests: React.FC = () => {
|
||||
const dispatch = useDispatch();
|
||||
const dispatch = useAppDispatch();
|
||||
const intl = useIntl();
|
||||
|
||||
const accountIds = useAppSelector((state) => state.user_lists.follow_requests.items);
|
||||
|
|
|
@ -37,7 +37,7 @@ const List: React.FC<IList> = ({ listId }) => {
|
|||
|
||||
return (
|
||||
<div className='flex items-center gap-1.5 px-2 py-4 text-black dark:text-white'>
|
||||
<Icon src={require('@tabler/icons/list.svg')} fixedWidth />
|
||||
<Icon src={require('@tabler/icons/list.svg')} />
|
||||
<span className='flex-grow'>
|
||||
{list.title}
|
||||
</span>
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
import React from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
import { changeListEditorTitle, submitListEditor } from 'soapbox/actions/lists';
|
||||
import { Button, Form, HStack, Input } from 'soapbox/components/ui';
|
||||
import { useAppSelector } from 'soapbox/hooks';
|
||||
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
|
||||
|
||||
const messages = defineMessages({
|
||||
label: { id: 'lists.new.title_placeholder', defaultMessage: 'New list title' },
|
||||
|
@ -13,7 +12,7 @@ const messages = defineMessages({
|
|||
});
|
||||
|
||||
const NewListForm: React.FC = () => {
|
||||
const dispatch = useDispatch();
|
||||
const dispatch = useAppDispatch();
|
||||
const intl = useIntl();
|
||||
|
||||
const value = useAppSelector((state) => state.listEditor.get('title'));
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import React, { useEffect } from 'react';
|
||||
import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { createSelector } from 'reselect';
|
||||
|
||||
|
@ -9,7 +8,7 @@ import { openModal } from 'soapbox/actions/modals';
|
|||
import Icon from 'soapbox/components/icon';
|
||||
import ScrollableList from 'soapbox/components/scrollable-list';
|
||||
import { Column, IconButton, Spinner } from 'soapbox/components/ui';
|
||||
import { useAppSelector } from 'soapbox/hooks';
|
||||
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
|
||||
|
||||
import NewListForm from './components/new-list-form';
|
||||
|
||||
|
@ -35,7 +34,7 @@ const getOrderedLists = createSelector([(state: RootState) => state.lists], list
|
|||
});
|
||||
|
||||
const Lists: React.FC = () => {
|
||||
const dispatch = useDispatch();
|
||||
const dispatch = useAppDispatch();
|
||||
const intl = useIntl();
|
||||
|
||||
const lists = useAppSelector((state) => getOrderedLists(state));
|
||||
|
@ -85,7 +84,7 @@ const Lists: React.FC = () => {
|
|||
>
|
||||
{lists.map((list: any) => (
|
||||
<Link key={list.id} to={`/list/${list.id}`} className='flex items-center gap-1.5 p-2 text-gray-900 dark:text-gray-100 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg'>
|
||||
<Icon src={require('@tabler/icons/list.svg')} fixedWidth />
|
||||
<Icon src={require('@tabler/icons/list.svg')} />
|
||||
<span className='flex-grow'>
|
||||
{list.title}
|
||||
</span>
|
||||
|
|
|
@ -1,13 +1,12 @@
|
|||
import debounce from 'lodash/debounce';
|
||||
import React from 'react';
|
||||
import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
import { fetchMutes, expandMutes } from 'soapbox/actions/mutes';
|
||||
import ScrollableList from 'soapbox/components/scrollable-list';
|
||||
import { Column, Spinner } from 'soapbox/components/ui';
|
||||
import AccountContainer from 'soapbox/containers/account-container';
|
||||
import { useAppSelector } from 'soapbox/hooks';
|
||||
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
|
||||
|
||||
const messages = defineMessages({
|
||||
heading: { id: 'column.mutes', defaultMessage: 'Muted users' },
|
||||
|
@ -18,7 +17,7 @@ const handleLoadMore = debounce((dispatch) => {
|
|||
}, 300, { leading: true });
|
||||
|
||||
const Mutes: React.FC = () => {
|
||||
const dispatch = useDispatch();
|
||||
const dispatch = useAppDispatch();
|
||||
const intl = useIntl();
|
||||
|
||||
const accountIds = useAppSelector((state) => state.user_lists.mutes.items);
|
||||
|
|
|
@ -64,7 +64,7 @@ const NotificationFilterBar = () => {
|
|||
name: 'pleroma:emoji_reaction',
|
||||
});
|
||||
items.push({
|
||||
text: <Icon src={require('feather-icons/dist/icons/repeat.svg')} />,
|
||||
text: <Icon src={require('@tabler/icons/repeat.svg')} />,
|
||||
title: intl.formatMessage(messages.boosts),
|
||||
action: onClick('reblog'),
|
||||
name: 'reblog',
|
||||
|
|
|
@ -17,7 +17,7 @@ import { makeGetNotification } from 'soapbox/selectors';
|
|||
import { NotificationType, validType } from 'soapbox/utils/notification';
|
||||
|
||||
import type { ScrollPosition } from 'soapbox/components/status';
|
||||
import type { Account, Status as StatusEntity, Notification as NotificationEntity } from 'soapbox/types/entities';
|
||||
import type { Account as AccountEntity, Status as StatusEntity, Notification as NotificationEntity } from 'soapbox/types/entities';
|
||||
|
||||
const notificationForScreenReader = (intl: IntlShape, message: string, timestamp: Date) => {
|
||||
const output = [message];
|
||||
|
@ -27,7 +27,7 @@ const notificationForScreenReader = (intl: IntlShape, message: string, timestamp
|
|||
return output.join(', ');
|
||||
};
|
||||
|
||||
const buildLink = (account: Account): JSX.Element => (
|
||||
const buildLink = (account: AccountEntity): JSX.Element => (
|
||||
<bdi>
|
||||
<Link
|
||||
className='text-gray-800 dark:text-gray-200 font-bold hover:underline'
|
||||
|
@ -127,7 +127,7 @@ const messages: Record<NotificationType, MessageDescriptor> = defineMessages({
|
|||
const buildMessage = (
|
||||
intl: IntlShape,
|
||||
type: NotificationType,
|
||||
account: Account,
|
||||
account: AccountEntity,
|
||||
totalCount: number | null,
|
||||
targetName: string,
|
||||
instanceTitle: string,
|
||||
|
@ -151,6 +151,8 @@ const buildMessage = (
|
|||
});
|
||||
};
|
||||
|
||||
const avatarSize = 48;
|
||||
|
||||
interface INotificaton {
|
||||
hidden?: boolean,
|
||||
notification: NotificationEntity,
|
||||
|
@ -290,7 +292,7 @@ const Notification: React.FC<INotificaton> = (props) => {
|
|||
<AccountContainer
|
||||
id={account.id}
|
||||
hidden={hidden}
|
||||
avatarSize={48}
|
||||
avatarSize={avatarSize}
|
||||
/>
|
||||
) : null;
|
||||
case 'follow_request':
|
||||
|
@ -298,7 +300,7 @@ const Notification: React.FC<INotificaton> = (props) => {
|
|||
<AccountContainer
|
||||
id={account.id}
|
||||
hidden={hidden}
|
||||
avatarSize={48}
|
||||
avatarSize={avatarSize}
|
||||
actionType='follow_request'
|
||||
/>
|
||||
) : null;
|
||||
|
@ -307,7 +309,7 @@ const Notification: React.FC<INotificaton> = (props) => {
|
|||
<AccountContainer
|
||||
id={notification.target.id}
|
||||
hidden={hidden}
|
||||
avatarSize={48}
|
||||
avatarSize={avatarSize}
|
||||
/>
|
||||
) : null;
|
||||
case 'favourite':
|
||||
|
@ -327,6 +329,7 @@ const Notification: React.FC<INotificaton> = (props) => {
|
|||
hidden={hidden}
|
||||
onMoveDown={handleMoveDown}
|
||||
onMoveUp={handleMoveUp}
|
||||
avatarSize={avatarSize}
|
||||
/>
|
||||
) : null;
|
||||
default:
|
||||
|
@ -358,13 +361,18 @@ const Notification: React.FC<INotificaton> = (props) => {
|
|||
>
|
||||
<div className='p-4 focusable'>
|
||||
<div className='mb-2'>
|
||||
<HStack alignItems='center' space={1.5}>
|
||||
{renderIcon()}
|
||||
<HStack alignItems='center' space={3}>
|
||||
<div
|
||||
className='flex justify-end'
|
||||
style={{ flexBasis: avatarSize }}
|
||||
>
|
||||
{renderIcon()}
|
||||
</div>
|
||||
|
||||
<div className='truncate'>
|
||||
<Text
|
||||
theme='muted'
|
||||
size='sm'
|
||||
size='xs'
|
||||
truncate
|
||||
data-testid='message'
|
||||
>
|
||||
|
|
|
@ -1,12 +1,11 @@
|
|||
import classNames from 'clsx';
|
||||
import React from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import ReactSwipeableViews from 'react-swipeable-views';
|
||||
|
||||
import { endOnboarding } from 'soapbox/actions/onboarding';
|
||||
import LandingGradient from 'soapbox/components/landing-gradient';
|
||||
import { HStack } from 'soapbox/components/ui';
|
||||
import { useFeatures } from 'soapbox/hooks';
|
||||
import { useAppDispatch, useFeatures } from 'soapbox/hooks';
|
||||
|
||||
import AvatarSelectionStep from './steps/avatar-selection-step';
|
||||
import BioStep from './steps/bio-step';
|
||||
|
@ -17,7 +16,7 @@ import FediverseStep from './steps/fediverse-step';
|
|||
import SuggestedAccountsStep from './steps/suggested-accounts-step';
|
||||
|
||||
const OnboardingWizard = () => {
|
||||
const dispatch = useDispatch();
|
||||
const dispatch = useAppDispatch();
|
||||
const features = useFeatures();
|
||||
|
||||
const [currentStep, setCurrentStep] = React.useState<number>(0);
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
import classNames from 'clsx';
|
||||
import React from 'react';
|
||||
import { defineMessages, FormattedMessage } from 'react-intl';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
import { patchMe } from 'soapbox/actions/me';
|
||||
import { Avatar, Button, Card, CardBody, Icon, Spinner, Stack, Text } from 'soapbox/components/ui';
|
||||
import { useOwnAccount } from 'soapbox/hooks';
|
||||
import { useAppDispatch, useOwnAccount } from 'soapbox/hooks';
|
||||
import toast from 'soapbox/toast';
|
||||
import { isDefaultAvatar } from 'soapbox/utils/accounts';
|
||||
import resizeImage from 'soapbox/utils/resize-image';
|
||||
|
@ -17,7 +16,7 @@ const messages = defineMessages({
|
|||
});
|
||||
|
||||
const AvatarSelectionStep = ({ onNext }: { onNext: () => void }) => {
|
||||
const dispatch = useDispatch();
|
||||
const dispatch = useAppDispatch();
|
||||
const account = useOwnAccount();
|
||||
|
||||
const fileInput = React.useRef<HTMLInputElement>(null);
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue