Porównaj commity

..

1 Commity

Autor SHA1 Wiadomość Data
tassoman eee7126e25 added: support for intl date-picker, italian datepicker 2023-01-21 16:51:10 +01:00
810 zmienionych plików z 24487 dodań i 32571 usunięć

Wyświetl plik

@ -5,7 +5,6 @@ module.exports = {
'eslint:recommended',
'plugin:import/typescript',
'plugin:compat/recommended',
'plugin:tailwindcss/recommended',
],
env: {
@ -55,17 +54,13 @@ module.exports = {
},
},
polyfills: [
'es:all', // core-js
'fetch', // not polyfilled, but ignore it
'IntersectionObserver', // npm:intersection-observer
'Promise', // core-js
'ResizeObserver', // npm:resize-observer-polyfill
'URL', // core-js
'URLSearchParams', // core-js
'es:all',
'fetch',
'IntersectionObserver',
'Promise',
'URL',
'URLSearchParams',
],
tailwindcss: {
config: 'tailwind.config.cjs',
},
},
rules: {
@ -240,7 +235,18 @@ module.exports = {
},
],
'import/newline-after-import': 'error',
'import/no-extraneous-dependencies': 'error',
'import/no-extraneous-dependencies': [
'error',
// {
// devDependencies: [
// 'webpack/**',
// 'app/soapbox/test_setup.js',
// 'app/soapbox/test_helpers.js',
// 'app/**/__tests__/**',
// 'app/**/__mocks__/**',
// ],
// },
],
'import/no-unresolved': 'error',
'import/no-webpack-loader-syntax': 'error',
'import/order': [
@ -261,30 +267,10 @@ module.exports = {
},
],
'@typescript-eslint/no-duplicate-imports': 'error',
'@typescript-eslint/member-delimiter-style': [
'error',
{
multiline: {
delimiter: 'none',
},
singleline: {
delimiter: 'comma',
},
},
],
'promise/catch-or-return': 'error',
'react-hooks/rules-of-hooks': 'error',
'tailwindcss/classnames-order': [
'error',
{
classRegex: '^(base|container|icon|item|list|outer|wrapper)?[c|C]lass(Name)?$',
config: 'tailwind.config.cjs',
},
],
'tailwindcss/migration-from-tailwind-2': 'error',
},
overrides: [
{

Wyświetl plik

@ -149,19 +149,19 @@ pages:
docker:
stage: deploy
image: docker:23.0.0
image: docker:20.10.22
services:
- docker:23.0.0-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
script:
- echo $CI_REGISTRY_PASSWORD | docker login -u $CI_REGISTRY_USER $CI_REGISTRY --password-stdin
- docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_TAG .
- docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_TAG
rules:
- if: $CI_COMMIT_TAG
interruptible: false
- docker build -t $CI_REGISTRY_IMAGE .
- docker push $CI_REGISTRY_IMAGE
only:
variables:
- $CI_DEFAULT_BRANCH == $CI_COMMIT_REF_NAME
release:
stage: release

Wyświetl plik

@ -1,43 +0,0 @@
import sharedConfig from '../webpack/shared';
import type { StorybookConfig } from '@storybook/core-common';
const config: StorybookConfig = {
stories: [
'../stories/**/*.stories.mdx',
'../stories/**/*.stories.@(js|jsx|ts|tsx)'
],
addons: [
'@storybook/addon-links',
'@storybook/addon-essentials',
'@storybook/addon-interactions',
'storybook-react-intl',
{
name: '@storybook/addon-postcss',
options: {
postcssLoaderOptions: {
implementation: require('postcss'),
},
},
},
],
framework: '@storybook/react',
core: {
builder: '@storybook/builder-webpack5',
},
webpackFinal: async (config) => {
config.resolve!.alias = {
...sharedConfig.resolve!.alias,
...config.resolve!.alias,
};
config.resolve!.modules = [
...sharedConfig.resolve!.modules!,
...config.resolve!.modules!,
];
return config;
},
};
export default config;

Wyświetl plik

@ -1,22 +0,0 @@
import '../app/styles/tailwind.css';
import '../stories/theme.css';
import { addDecorator, Story } from '@storybook/react';
import { IntlProvider } from 'react-intl';
import React from 'react';
const withProvider = (Story: Story) => (
<IntlProvider locale='en'><Story /></IntlProvider>
);
addDecorator(withProvider);
export const parameters = {
actions: { argTypesRegex: '^on[A-Z].*' },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
},
};

Wyświetl plik

@ -1 +1 @@
nodejs 18.14.0
nodejs 18.13.0

Wyświetl plik

@ -6,61 +6,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
### Added
- Posts: Support posts filtering on recent Mastodon versions
- Reactions: Support custom emoji reactions
- Compatbility: Support Mastodon v2 timeline filters.
- Posts: Support dislikes on Friendica.
- UI: added a character counter to some textareas.
### Changed
- Posts: truncate Nostr pubkeys in reply mentions.
- Posts: upgraded emoji picker component.
- UI: unified design of "approve" and "reject" buttons in follow requests and waitlist.
### Fixed
- Posts: fixed emojis being cut off in reactions modal.
- Posts: fix audio player progress bar visibility.
- Posts: added missing gap in pending status.
- Compatibility: fixed quote posting compatibility with custom Pleroma forks.
- Profile: fix "load more" button height on account gallery page.
- 18n: fixed Chinese language being detected from the browser.
- Conversations: fixed pagination (Mastodon).
- Compatibility: fix version parsing for Friendica.
## [3.2.0] - 2023-02-15
### Added
- Admin: redirect the homepage to any URL.
- Compatibility: added compatibility with Friendica.
- Posts: bot badge on statuses from bot accounts.
- Compatibility: improved browser support for older browsers.
- Events: allow to repost events in event menu.
- Profile: Add RSS link to user profiles.
- Reactions: adds support for reacting to chat messages.
- Groups: initial support for groups.
- Profile: add RSS link to user profiles.
- Chats: reset chat message field height after sending a message.
- Admin: allow to manage announcements.
### Changed
- Chats: improved display of media attachments.
- ServiceWorker: switch to a network-first strategy. The "An update is available!" prompt goes away.
- Posts: increased font size of focused status in threads.
- Posts: let "mute conversation" be clicked from any feed, not just noficiations.
- Posts: display all emoji reactions.
- Reactions: improved UI of reactions on statuses.
- Profile: make verified badge more prominent, overlapping with avatar.
### Fixed
- Admin: fixed hover card in reports modal shows reporter not reportee
- Chats: media attachments rendering at the wrong size and/or causing the chat to scroll on load.
- Chats: don't display "copy" button for messages without text.
- Posts: don't have to click the play button twice for embedded videos.
- index.html: remove `referrer` meta tag so it doesn't conflict with backend's `Referrer-Policy` header.
- Modals: fix media modal automatically switching to video.
- Navigation: profile dropdown erratic behavior.
- Posts: fix posts filtering.
### Removed
- Admin: single user mode. Now the homepage can be redirected to any URL.

Wyświetl plik

@ -75,7 +75,7 @@ One disadvantage of this approach is that it does not help the software spread.
© Alex Gleason & other Soapbox contributors
© Eugen Rochko & other Mastodon contributors
© Trump Media & Technology Group
© Gab AI, Inc.
© Gab AI, Inc.
Soapbox is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by

Wyświetl plik

@ -2,4 +2,4 @@
- verified.svg - Created by Alex Gleason. CC0
Fediverse logo: https://en.wikipedia.org/wiki/Fediverse#/media/File:Fediverse_logo_proposal.svg
Fediverse logo: https://en.wikipedia.org/wiki/Fediverse#/media/File:Fediverse_logo_proposal.svg

Wyświetl plik

@ -1,107 +1 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
id="svg2"
style="clip-rule:evenodd;fill-rule:evenodd;image-rendering:optimizeQuality;shape-rendering:geometricPrecision;text-rendering:geometricPrecision"
sodipodi:docname="soapbox-logo-white.svg"
xml:space="preserve"
version="1.1"
inkscape:version="1.2.1 (9c6d41e410, 2022-07-14)"
viewBox="0 0 100 100"
width="100"
height="100"
inkscape:export-filename="/home/miklobit/Downloads/citizen4/logo/citizen4-logo-250px.png"
inkscape:export-xdpi="63.5"
inkscape:export-ydpi="63.5"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/"><sodipodi:namedview
id="namedview12"
bordercolor="#666666"
inkscape:pageshadow="2"
guidetolerance="10"
pagecolor="#ffffff"
gridtolerance="10"
inkscape:zoom="5.0135101"
objecttolerance="10"
borderopacity="1"
inkscape:current-layer="g1133"
inkscape:cx="54.253406"
inkscape:cy="42.086282"
inkscape:window-width="1920"
showgrid="false"
inkscape:pageopacity="0"
inkscape:window-height="1016"
inkscape:document-rotation="0"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0"
inkscape:window-x="0"
inkscape:window-y="36"
inkscape:window-maximized="1"
units="mm"
inkscape:showpageshadow="2"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="px" />
<defs
id="defs4">
<style
id="style6"
type="text/css">
.fil0 {fill:black}
</style>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath932"><g
id="g936"
style="fill:#ffffff;stroke:#000000;stroke-width:0.468608;stroke-miterlimit:4;stroke-dasharray:none"
transform="matrix(40.327178,0,0,40.327178,-206.26309,-5.404074)">
<path
id="path934"
sodipodi:nodetypes="ccccc"
style="fill:#ffffff;stroke:#000000;stroke-width:0.468608;stroke-miterlimit:4;stroke-dasharray:none"
d="M 4.25,1.3382 0.4955,2.4662 C 0.58759,5.2588 1.2884,8.6655 4.25,9.6618 7.2442,8.694 7.8774,5.2291 8.0045,2.4662 Z" />
</g></clipPath><clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath932-3"><g
id="g936-6"
style="fill:#ffffff;stroke:#000000;stroke-width:0.468608;stroke-miterlimit:4;stroke-dasharray:none"
transform="matrix(40.327178,0,0,40.327178,-206.26309,-5.404074)"><path
id="path934-7"
sodipodi:nodetypes="ccccc"
style="fill:#ffffff;stroke:#000000;stroke-width:0.468608;stroke-miterlimit:4;stroke-dasharray:none"
d="M 4.25,1.3382 0.4955,2.4662 C 0.58759,5.2588 1.2884,8.6655 4.25,9.6618 7.2442,8.694 7.8774,5.2291 8.0045,2.4662 Z" /></g></clipPath>
</defs>
<metadata
id="metadata7"><rdf:RDF><cc:Work><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><cc:license
rdf:resource="http://creativecommons.org/licenses/by-nc-sa/4.0/" /><dc:date>2023-02-18T14:20:55</dc:date><dc:source>https://soc.citizen4.eu</dc:source><dc:subject><rdf:Bag><rdf:li>citizen4</rdf:li><rdf:li>logo</rdf:li><rdf:li>shield</rdf:li></rdf:Bag></dc:subject><dc:creator><cc:Agent><dc:title>miklo</dc:title></cc:Agent></dc:creator><dc:description>Citizen4 logo</dc:description></cc:Work><cc:License
rdf:about="http://creativecommons.org/licenses/by-nc-sa/4.0/"><cc:permits
rdf:resource="http://creativecommons.org/ns#Reproduction" /><cc:permits
rdf:resource="http://creativecommons.org/ns#Distribution" /><cc:requires
rdf:resource="http://creativecommons.org/ns#Notice" /><cc:requires
rdf:resource="http://creativecommons.org/ns#Attribution" /><cc:prohibits
rdf:resource="http://creativecommons.org/ns#CommercialUse" /><cc:permits
rdf:resource="http://creativecommons.org/ns#DerivativeWorks" /><cc:requires
rdf:resource="http://creativecommons.org/ns#ShareAlike" /></cc:License></rdf:RDF></metadata><g
inkscape:groupmode="layer"
id="g1133"
inkscape:label="citizen4"
style="display:inline;fill:#ffffff"
transform="translate(9.1709534,9.343974)"><g
id="g1149"
transform="matrix(0.28130772,0,0,0.28130772,-1.9206898,-6.7154381)"
style="fill:#ffffff"><path
id="path1127"
style="fill:#ffffff;fill-opacity:1;stroke-width:2.298"
d="m 233.61288,175.15307 h -30.0693 v -8.40148 h -35.71547 v -22.85127 h 35.71317 v -8.40148 h 30.0716 c 1.31445,0 2.42898,0.4596 3.34818,1.3765 0.9169,0.9215 1.3788,2.03832 1.3788,3.34818 v 30.20487 c 0,1.22484 -0.4596,2.32098 -1.3788,3.28154 -0.9192,0.95827 -2.03602,1.44314 -3.34818,1.44314 z m 18.90793,-30.07388 c 6.91237,0 10.37315,3.41253 10.37315,10.24447 0,6.83195 -3.45618,10.24217 -10.37315,10.24217 -6.82735,0 -10.24218,-3.41482 -10.24218,-10.24217 0,-6.82734 3.41483,-10.24447 10.24218,-10.24447 z M 70.322893,175.15307 h 30.069297 v -8.40148 h 35.71546 v -22.85127 h -35.71317 v -8.40148 H 70.322893 c -1.31446,0 -2.42898,0.4596 -3.34818,1.3765 -0.9169,0.9215 -1.3788,2.03832 -1.3788,3.34818 v 30.20487 c 0,1.22484 0.4596,2.32098 1.3788,3.28154 0.9192,0.95827 2.03602,1.44314 3.34818,1.44314 z m -18.90793,-30.07388 c -6.91237,0 -10.37315,3.41253 -10.37315,10.24447 0,6.83195 3.45618,10.24217 10.37315,10.24217 6.82735,0 10.24218,-3.41482 10.24218,-10.24217 0,-6.82734 -3.41483,-10.24447 -10.24218,-10.24447 z M 171.79501,73.680969 v 30.069301 h -8.40148 v 35.71546 h -22.85128 v -35.71317 h -8.40147 V 73.680969 c 0,-1.31445 0.45959,-2.42898 1.37649,-3.34818 0.9215,-0.9169 2.03832,-1.3788 3.34819,-1.3788 h 30.20487 c 1.22483,0 2.32097,0.4596 3.28153,1.3788 0.95827,0.9192 1.44315,2.03602 1.44315,3.34818 z m -30.07388,-18.90793 c 0,-6.91237 3.41252,-10.37315 10.24446,-10.37315 6.83195,0 10.24218,3.45618 10.24218,10.37315 0,6.82735 -3.41482,10.24218 -10.24218,10.24218 -6.82734,0 -10.24446,-3.41483 -10.24446,-10.24218 z m 30.07388,182.197911 v -30.06929 h -8.40148 v -35.71547 h -22.85128 v 35.71317 h -8.40147 v 30.07159 c 0,1.31446 0.45959,2.42898 1.37649,3.34818 0.9215,0.9169 2.03832,1.3788 3.34819,1.3788 h 30.20487 c 1.22483,0 2.32097,-0.4596 3.28153,-1.3788 0.95827,-0.9192 1.44315,-2.03602 1.44315,-3.34818 z m -30.07388,18.90793 c 0,6.91237 3.41252,10.37315 10.24446,10.37315 6.83195,0 10.24218,-3.45618 10.24218,-10.37315 0,-6.82735 -3.41482,-10.24218 -10.24218,-10.24218 -6.82734,0 -10.24446,3.41483 -10.24446,10.24218 z M 151.92079,0.52207567 0.51240486,46.01113 C 4.2261345,158.6288 32.487823,296.01139 151.92079,336.18936 272.66842,297.16072 298.20359,157.43109 303.32917,46.01113 Z" /></g></g></svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 95 95" width="100" height="100"><path d="M94.909 19.374C94.909 8.674 86.235 0 75.534 0c-10.647 0-19.28 8.591-19.365 19.217l-15.631 2.09c-1.961-6.007-7.598-10.35-14.258-10.35-8.284 0-15.002 6.716-15.002 15.002 0 6.642 4.321 12.267 10.303 14.24l-2.205 16.056c-10.66.049-19.285 8.7-19.285 19.37C.091 86.325 8.765 95 19.466 95c10.677 0 19.332-8.638 19.37-19.304l18.093-2.501c1.979 5.972 7.598 10.285 14.234 10.285 8.284 0 15.002-6.716 15.002-15.002 0-6.891-4.652-12.682-10.983-14.441l1.365-15.339c10.229-.53 18.363-8.966 18.363-19.324zM56.194 67.8l-18.116 2.505a19.39 19.39 0 0 0-13.312-13.3l2.205-16.077a14.98 14.98 0 0 0 14.27-14.222l15.655-2.094c1.894 6.757 7.351 12.009 14.225 13.612l-1.365 15.322c-7.4.688-13.224 6.753-13.562 14.254z" fill="#ffffff"/></svg>

Przed

Szerokość:  |  Wysokość:  |  Rozmiar: 6.7 KiB

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 812 B

Wyświetl plik

@ -1,127 +1 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
id="svg2"
style="clip-rule:evenodd;fill-rule:evenodd;image-rendering:optimizeQuality;shape-rendering:geometricPrecision;text-rendering:geometricPrecision"
sodipodi:docname="soapbox-logo.svg"
xml:space="preserve"
version="1.1"
inkscape:version="1.2.1 (9c6d41e410, 2022-07-14)"
viewBox="0 0 100 100"
width="100"
height="100"
inkscape:export-filename="/home/miklobit/Downloads/citizen4/logo/citizen4-logo-250px.png"
inkscape:export-xdpi="63.5"
inkscape:export-ydpi="63.5"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/"><sodipodi:namedview
id="namedview12"
bordercolor="#666666"
inkscape:pageshadow="2"
guidetolerance="10"
pagecolor="#ffffff"
gridtolerance="10"
inkscape:zoom="5.0135101"
objecttolerance="10"
borderopacity="1"
inkscape:current-layer="svg2"
inkscape:cx="54.253406"
inkscape:cy="42.086282"
inkscape:window-width="1920"
showgrid="false"
inkscape:pageopacity="0"
inkscape:window-height="1016"
inkscape:document-rotation="0"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0"
inkscape:window-x="0"
inkscape:window-y="36"
inkscape:window-maximized="1"
units="mm"
inkscape:showpageshadow="2"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="px" />
<defs
id="defs4">
<style
id="style6"
type="text/css">
.fil0 {fill:black}
</style>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath932"><g
id="g936"
style="fill:#ffffff;stroke:#000000;stroke-width:0.468608;stroke-miterlimit:4;stroke-dasharray:none"
transform="matrix(40.327178,0,0,40.327178,-206.26309,-5.404074)">
<path
id="path934"
sodipodi:nodetypes="ccccc"
style="fill:#ffffff;stroke:#000000;stroke-width:0.468608;stroke-miterlimit:4;stroke-dasharray:none"
d="M 4.25,1.3382 0.4955,2.4662 C 0.58759,5.2588 1.2884,8.6655 4.25,9.6618 7.2442,8.694 7.8774,5.2291 8.0045,2.4662 Z" />
</g></clipPath><clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath932-3"><g
id="g936-6"
style="fill:#ffffff;stroke:#000000;stroke-width:0.468608;stroke-miterlimit:4;stroke-dasharray:none"
transform="matrix(40.327178,0,0,40.327178,-206.26309,-5.404074)"><path
id="path934-7"
sodipodi:nodetypes="ccccc"
style="fill:#ffffff;stroke:#000000;stroke-width:0.468608;stroke-miterlimit:4;stroke-dasharray:none"
d="M 4.25,1.3382 0.4955,2.4662 C 0.58759,5.2588 1.2884,8.6655 4.25,9.6618 7.2442,8.694 7.8774,5.2291 8.0045,2.4662 Z" /></g></clipPath>
</defs>
<metadata
id="metadata7"><rdf:RDF><cc:Work><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><cc:license
rdf:resource="http://creativecommons.org/licenses/by-nc-sa/4.0/" /><dc:date>2023-02-18T14:20:55</dc:date><dc:source>https://soc.citizen4.eu</dc:source><dc:subject><rdf:Bag><rdf:li>citizen4</rdf:li><rdf:li>logo</rdf:li><rdf:li>shield</rdf:li></rdf:Bag></dc:subject><dc:creator><cc:Agent><dc:title>miklo</dc:title></cc:Agent></dc:creator><dc:description>Citizen4 logo</dc:description></cc:Work><cc:License
rdf:about="http://creativecommons.org/licenses/by-nc-sa/4.0/"><cc:permits
rdf:resource="http://creativecommons.org/ns#Reproduction" /><cc:permits
rdf:resource="http://creativecommons.org/ns#Distribution" /><cc:requires
rdf:resource="http://creativecommons.org/ns#Notice" /><cc:requires
rdf:resource="http://creativecommons.org/ns#Attribution" /><cc:prohibits
rdf:resource="http://creativecommons.org/ns#CommercialUse" /><cc:permits
rdf:resource="http://creativecommons.org/ns#DerivativeWorks" /><cc:requires
rdf:resource="http://creativecommons.org/ns#ShareAlike" /></cc:License></rdf:RDF></metadata><g
inkscape:groupmode="layer"
id="layer1"
inkscape:label="shield"
style="display:inline"
transform="translate(9.1709534,9.343974)"><g
id="g912"
style="clip-rule:evenodd;fill:#003399;fill-opacity:1;fill-rule:evenodd;stroke:#888888;stroke-width:0.468608;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;image-rendering:optimizeQuality;shape-rendering:geometricPrecision;text-rendering:geometricPrecision"
transform="matrix(11.344346,0,0,11.344346,-7.3976698,-21.749578)"><path
id="path910"
sodipodi:nodetypes="ccccc"
style="fill:#003399;fill-opacity:1;stroke:#888888;stroke-width:0.468608;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 4.25,1.3382 0.4955,2.4662 C 0.58759,5.2588 1.2884,8.6655 4.25,9.6618 7.2442,8.694 7.8774,5.2291 8.0045,2.4662 Z" /></g></g><g
inkscape:groupmode="layer"
id="g1133"
inkscape:label="citizen2"
style="display:inline"
transform="translate(9.1709534,9.343974)"><g
id="g1149"
transform="matrix(0.28130772,0,0,0.28130772,-1.9206898,-6.7154381)"><path
id="path1119"
style="fill:#ffcc00;fill-opacity:1;stroke-width:2.298"
d="m 171.79501,236.97095 v -30.06929 h -8.40148 v -35.71547 h -22.85128 v 35.71317 h -8.40147 v 30.07159 c 0,1.31446 0.45959,2.42898 1.37649,3.34818 0.9215,0.9169 2.03832,1.3788 3.34819,1.3788 h 30.20487 c 1.22483,0 2.32097,-0.4596 3.28153,-1.3788 0.95827,-0.9192 1.44315,-2.03602 1.44315,-3.34818 z m -30.07388,18.90793 c 0,6.91237 3.41252,10.37315 10.24446,10.37315 6.83195,0 10.24218,-3.45618 10.24218,-10.37315 0,-6.82735 -3.41482,-10.24218 -10.24218,-10.24218 -6.82734,0 -10.24446,3.41483 -10.24446,10.24218 z" /><path
id="path1121"
style="fill:#ffcc00;fill-opacity:1;stroke-width:2.298"
d="m 171.79501,73.680969 v 30.069301 h -8.40148 v 35.71546 h -22.85128 v -35.71317 h -8.40147 V 73.680969 c 0,-1.31445 0.45959,-2.42898 1.37649,-3.34818 0.9215,-0.9169 2.03832,-1.3788 3.34819,-1.3788 h 30.20487 c 1.22483,0 2.32097,0.4596 3.28153,1.3788 0.95827,0.9192 1.44315,2.03602 1.44315,3.34818 z m -30.07388,-18.90793 c 0,-6.91237 3.41252,-10.37315 10.24446,-10.37315 6.83195,0 10.24218,3.45618 10.24218,10.37315 0,6.82735 -3.41482,10.24218 -10.24218,10.24218 -6.82734,0 -10.24446,-3.41483 -10.24446,-10.24218 z" /><path
id="path1125"
style="fill:#ffcc00;fill-opacity:1;stroke-width:2.298"
d="m 70.322893,175.15307 h 30.069297 v -8.40148 h 35.71546 v -22.85127 h -35.71317 v -8.40148 H 70.322893 c -1.31446,0 -2.42898,0.4596 -3.34818,1.3765 -0.9169,0.9215 -1.3788,2.03832 -1.3788,3.34818 v 30.20487 c 0,1.22484 0.4596,2.32098 1.3788,3.28154 0.9192,0.95827 2.03602,1.44314 3.34818,1.44314 z m -18.90793,-30.07388 c -6.91237,0 -10.37315,3.41253 -10.37315,10.24447 0,6.83195 3.45618,10.24217 10.37315,10.24217 6.82735,0 10.24218,-3.41482 10.24218,-10.24217 0,-6.82734 -3.41483,-10.24447 -10.24218,-10.24447 z" /><path
id="path1127"
style="fill:#ffcc00;fill-opacity:1;stroke-width:2.298"
d="m 233.61288,175.15307 h -30.0693 v -8.40148 h -35.71547 v -22.85127 h 35.71317 v -8.40148 h 30.0716 c 1.31445,0 2.42898,0.4596 3.34818,1.3765 0.9169,0.9215 1.3788,2.03832 1.3788,3.34818 v 30.20487 c 0,1.22484 -0.4596,2.32098 -1.3788,3.28154 -0.9192,0.95827 -2.03602,1.44314 -3.34818,1.44314 z m 18.90793,-30.07388 c 6.91237,0 10.37315,3.41253 10.37315,10.24447 0,6.83195 -3.45618,10.24217 -10.37315,10.24217 -6.82735,0 -10.24218,-3.41482 -10.24218,-10.24217 0,-6.82734 3.41483,-10.24447 10.24218,-10.24447 z" /></g></g></svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 95 95" width="100" height="100"><path d="M94.909 19.374C94.909 8.674 86.235 0 75.534 0c-10.647 0-19.28 8.591-19.365 19.217l-15.631 2.09c-1.961-6.007-7.598-10.35-14.258-10.35-8.284 0-15.002 6.716-15.002 15.002 0 6.642 4.321 12.267 10.303 14.24l-2.205 16.056c-10.66.049-19.285 8.7-19.285 19.37C.091 86.325 8.765 95 19.466 95c10.677 0 19.332-8.638 19.37-19.304l18.093-2.501c1.979 5.972 7.598 10.285 14.234 10.285 8.284 0 15.002-6.716 15.002-15.002 0-6.891-4.652-12.682-10.983-14.441l1.365-15.339c10.229-.53 18.363-8.966 18.363-19.324zM56.194 67.8l-18.116 2.505a19.39 19.39 0 0 0-13.312-13.3l2.205-16.077a14.98 14.98 0 0 0 14.27-14.222l15.655-2.094c1.894 6.757 7.351 12.009 14.225 13.612l-1.365 15.322c-7.4.688-13.224 6.753-13.562 14.254z" fill="#0482d8"/></svg>

Przed

Szerokość:  |  Wysokość:  |  Rozmiar: 7.6 KiB

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 812 B

Wyświetl plik

@ -1,16 +0,0 @@
{
"note": "patriots 900000001",
"discoverable": true,
"id": "109989480368015378",
"domain": null,
"avatar": "https://media.covfefe.social/groups/avatars/109/989/480/368/015/378/original/50b0d899bc5aae13.jpg",
"avatar_static": "https://media.covfefe.social/groups/avatars/109/989/480/368/015/378/original/50b0d899bc5aae13.jpg",
"header": "https://media.covfefe.social/groups/headers/109/989/480/368/015/378/original/c5063b59f919cd4a.png",
"header_static": "https://media.covfefe.social/groups/headers/109/989/480/368/015/378/original/c5063b59f919cd4a.png",
"group_visibility": "everyone",
"created_at": "2023-03-08T00:00:00.000Z",
"display_name": "PATRIOT PATRIOTS",
"membership_required": true,
"members_count": 1,
"tags": []
}

Wyświetl plik

@ -228,7 +228,7 @@ const fetchAccountFail = (id: string | null, error: AxiosError) => ({
});
type FollowAccountOpts = {
reblogs?: boolean
reblogs?: boolean,
notify?: boolean
};

Wyświetl plik

@ -1,18 +1,13 @@
import { defineMessages } from 'react-intl';
import { fetchRelationships } from 'soapbox/actions/accounts';
import { importFetchedAccount, importFetchedAccounts, importFetchedStatuses } from 'soapbox/actions/importer';
import toast from 'soapbox/toast';
import { filterBadges, getTagDiff } from 'soapbox/utils/badges';
import { getFeatures } from 'soapbox/utils/features';
import api, { getLinks } from '../api';
import { openModal } from './modals';
import type { AxiosResponse } from 'axios';
import type { AppDispatch, RootState } from 'soapbox/store';
import type { APIEntity, Announcement } from 'soapbox/types/entities';
import type { APIEntity } from 'soapbox/types/entities';
const ADMIN_CONFIG_FETCH_REQUEST = 'ADMIN_CONFIG_FETCH_REQUEST';
const ADMIN_CONFIG_FETCH_SUCCESS = 'ADMIN_CONFIG_FETCH_SUCCESS';
@ -82,45 +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_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_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 ADMIN_ANNOUNCEMENTS_FETCH_FAIL = 'ADMIN_ANNOUNCEMENTS_FETCH_FAILS';
const ADMIN_ANNOUNCEMENTS_FETCH_REQUEST = 'ADMIN_ANNOUNCEMENTS_FETCH_REQUEST';
const ADMIN_ANNOUNCEMENTS_FETCH_SUCCESS = 'ADMIN_ANNOUNCEMENTS_FETCH_SUCCESS';
const ADMIN_ANNOUNCEMENTS_EXPAND_FAIL = 'ADMIN_ANNOUNCEMENTS_EXPAND_FAILS';
const ADMIN_ANNOUNCEMENTS_EXPAND_REQUEST = 'ADMIN_ANNOUNCEMENTS_EXPAND_REQUEST';
const ADMIN_ANNOUNCEMENTS_EXPAND_SUCCESS = 'ADMIN_ANNOUNCEMENTS_EXPAND_SUCCESS';
const ADMIN_ANNOUNCEMENT_CHANGE_CONTENT = 'ADMIN_ANNOUNCEMENT_CHANGE_CONTENT';
const ADMIN_ANNOUNCEMENT_CHANGE_START_TIME = 'ADMIN_ANNOUNCEMENT_CHANGE_START_TIME';
const ADMIN_ANNOUNCEMENT_CHANGE_END_TIME = 'ADMIN_ANNOUNCEMENT_CHANGE_END_TIME';
const ADMIN_ANNOUNCEMENT_CHANGE_ALL_DAY = 'ADMIN_ANNOUNCEMENT_CHANGE_ALL_DAY';
const ADMIN_ANNOUNCEMENT_CREATE_REQUEST = 'ADMIN_ANNOUNCEMENT_CREATE_REQUEST';
const ADMIN_ANNOUNCEMENT_CREATE_SUCCESS = 'ADMIN_ANNOUNCEMENT_CREATE_REQUEST';
const ADMIN_ANNOUNCEMENT_CREATE_FAIL = 'ADMIN_ANNOUNCEMENT_CREATE_FAIL';
const ADMIN_ANNOUNCEMENT_DELETE_REQUEST = 'ADMIN_ANNOUNCEMENT_DELETE_REQUEST';
const ADMIN_ANNOUNCEMENT_DELETE_SUCCESS = 'ADMIN_ANNOUNCEMENT_DELETE_REQUEST';
const ADMIN_ANNOUNCEMENT_DELETE_FAIL = 'ADMIN_ANNOUNCEMENT_DELETE_FAIL';
const ADMIN_ANNOUNCEMENT_MODAL_INIT = 'ADMIN_ANNOUNCEMENT_MODAL_INIT';
const messages = defineMessages({
announcementCreateSuccess: { id: 'admin.edit_announcement.created', defaultMessage: 'Announcement created' },
announcementDeleteSuccess: { id: 'admin.edit_announcement.deleted', defaultMessage: 'Announcement deleted' },
announcementUpdateSuccess: { id: 'admin.edit_announcement.updated', defaultMessage: 'Announcement edited' },
});
const nicknamesFromIds = (getState: () => RootState, ids: string[]) => ids.map(id => getState().accounts.get(id)!.acct);
const fetchConfig = () =>
@ -632,93 +598,6 @@ const expandUserIndex = () =>
});
};
const fetchAdminAnnouncements = () =>
(dispatch: AppDispatch, getState: () => RootState) => {
dispatch({ type: ADMIN_ANNOUNCEMENTS_FETCH_REQUEST });
return api(getState)
.get('/api/pleroma/admin/announcements', { params: { limit: 50 } })
.then(({ data }) => {
dispatch({ type: ADMIN_ANNOUNCEMENTS_FETCH_SUCCESS, announcements: data });
return data;
}).catch(error => {
dispatch({ type: ADMIN_ANNOUNCEMENTS_FETCH_FAIL, error });
});
};
const expandAdminAnnouncements = () =>
(dispatch: AppDispatch, getState: () => RootState) => {
const page = getState().admin_announcements.page;
dispatch({ type: ADMIN_ANNOUNCEMENTS_EXPAND_REQUEST });
return api(getState)
.get('/api/pleroma/admin/announcements', { params: { limit: 50, offset: page * 50 } })
.then(({ data }) => {
dispatch({ type: ADMIN_ANNOUNCEMENTS_EXPAND_SUCCESS, announcements: data });
return data;
}).catch(error => {
dispatch({ type: ADMIN_ANNOUNCEMENTS_EXPAND_FAIL, error });
});
};
const changeAnnouncementContent = (content: string) => ({
type: ADMIN_ANNOUNCEMENT_CHANGE_CONTENT,
value: content,
});
const changeAnnouncementStartTime = (time: Date | null) => ({
type: ADMIN_ANNOUNCEMENT_CHANGE_START_TIME,
value: time,
});
const changeAnnouncementEndTime = (time: Date | null) => ({
type: ADMIN_ANNOUNCEMENT_CHANGE_END_TIME,
value: time,
});
const changeAnnouncementAllDay = (allDay: boolean) => ({
type: ADMIN_ANNOUNCEMENT_CHANGE_ALL_DAY,
value: allDay,
});
const handleCreateAnnouncement = () =>
(dispatch: AppDispatch, getState: () => RootState) => {
dispatch({ type: ADMIN_ANNOUNCEMENT_CREATE_REQUEST });
const { id, content, starts_at, ends_at, all_day } = getState().admin_announcements.form;
return api(getState)[id ? 'patch' : 'post'](
id ? `/api/pleroma/admin/announcements/${id}` : '/api/pleroma/admin/announcements',
{ content, starts_at, ends_at, all_day },
).then(({ data }) => {
dispatch({ type: ADMIN_ANNOUNCEMENT_CREATE_SUCCESS, announcement: data });
toast.success(id ? messages.announcementUpdateSuccess : messages.announcementCreateSuccess);
dispatch(fetchAdminAnnouncements());
return data;
}).catch(error => {
dispatch({ type: ADMIN_ANNOUNCEMENT_CREATE_FAIL, error });
});
};
const deleteAnnouncement = (id: string) =>
(dispatch: AppDispatch, getState: () => RootState) => {
dispatch({ type: ADMIN_ANNOUNCEMENT_DELETE_REQUEST, id });
return api(getState).delete(`/api/pleroma/admin/announcements/${id}`).then(({ data }) => {
dispatch({ type: ADMIN_ANNOUNCEMENT_DELETE_SUCCESS, id });
toast.success(messages.announcementDeleteSuccess);
dispatch(fetchAdminAnnouncements());
return data;
}).catch(error => {
dispatch({ type: ADMIN_ANNOUNCEMENT_DELETE_FAIL, id, error });
});
};
const initAnnouncementModal = (announcement?: Announcement) =>
(dispatch: AppDispatch) => {
dispatch({ type: ADMIN_ANNOUNCEMENT_MODAL_INIT, announcement });
dispatch(openModal('EDIT_ANNOUNCEMENT'));
};
export {
ADMIN_CONFIG_FETCH_REQUEST,
ADMIN_CONFIG_FETCH_SUCCESS,
@ -778,23 +657,6 @@ export {
ADMIN_USER_INDEX_FETCH_REQUEST,
ADMIN_USER_INDEX_FETCH_SUCCESS,
ADMIN_USER_INDEX_QUERY_SET,
ADMIN_ANNOUNCEMENTS_FETCH_FAIL,
ADMIN_ANNOUNCEMENTS_FETCH_REQUEST,
ADMIN_ANNOUNCEMENTS_FETCH_SUCCESS,
ADMIN_ANNOUNCEMENTS_EXPAND_FAIL,
ADMIN_ANNOUNCEMENTS_EXPAND_REQUEST,
ADMIN_ANNOUNCEMENTS_EXPAND_SUCCESS,
ADMIN_ANNOUNCEMENT_CHANGE_CONTENT,
ADMIN_ANNOUNCEMENT_CHANGE_START_TIME,
ADMIN_ANNOUNCEMENT_CHANGE_END_TIME,
ADMIN_ANNOUNCEMENT_CHANGE_ALL_DAY,
ADMIN_ANNOUNCEMENT_CREATE_FAIL,
ADMIN_ANNOUNCEMENT_CREATE_REQUEST,
ADMIN_ANNOUNCEMENT_CREATE_SUCCESS,
ADMIN_ANNOUNCEMENT_DELETE_FAIL,
ADMIN_ANNOUNCEMENT_DELETE_REQUEST,
ADMIN_ANNOUNCEMENT_DELETE_SUCCESS,
ADMIN_ANNOUNCEMENT_MODAL_INIT,
fetchConfig,
updateConfig,
updateSoapboxConfig,
@ -824,13 +686,4 @@ export {
setUserIndexQuery,
fetchUserIndex,
expandUserIndex,
fetchAdminAnnouncements,
expandAdminAnnouncements,
changeAnnouncementContent,
changeAnnouncementStartTime,
changeAnnouncementEndTime,
changeAnnouncementAllDay,
handleCreateAnnouncement,
deleteAnnouncement,
initAnnouncementModal,
};

Wyświetl plik

@ -4,8 +4,7 @@ import throttle from 'lodash/throttle';
import { defineMessages, IntlShape } from 'react-intl';
import api from 'soapbox/api';
import { isNativeEmoji } from 'soapbox/features/emoji';
import emojiSearch from 'soapbox/features/emoji/search';
import { search as emojiSearch } from 'soapbox/features/emoji/emoji-mart-search-light';
import { tagHistory } from 'soapbox/settings';
import toast from 'soapbox/toast';
import { isLoggedIn } from 'soapbox/utils/auth';
@ -20,8 +19,8 @@ import { openModal, closeModal } from './modals';
import { getSettings } from './settings';
import { createStatus } from './statuses';
import type { Emoji } from 'soapbox/components/autosuggest-emoji';
import type { AutoSuggestion } from 'soapbox/components/autosuggest-input';
import type { Emoji } from 'soapbox/features/emoji';
import type { AppDispatch, RootState } from 'soapbox/store';
import type { Account, APIEntity, Status, Tag } from 'soapbox/types/entities';
import type { History } from 'soapbox/types/history';
@ -47,7 +46,6 @@ const COMPOSE_UPLOAD_SUCCESS = 'COMPOSE_UPLOAD_SUCCESS';
const COMPOSE_UPLOAD_FAIL = 'COMPOSE_UPLOAD_FAIL';
const COMPOSE_UPLOAD_PROGRESS = 'COMPOSE_UPLOAD_PROGRESS';
const COMPOSE_UPLOAD_UNDO = 'COMPOSE_UPLOAD_UNDO';
const COMPOSE_GROUP_POST = 'COMPOSE_GROUP_POST';
const COMPOSE_SUGGESTIONS_CLEAR = 'COMPOSE_SUGGESTIONS_CLEAR';
const COMPOSE_SUGGESTIONS_READY = 'COMPOSE_SUGGESTIONS_READY';
@ -88,7 +86,7 @@ const COMPOSE_SET_STATUS = 'COMPOSE_SET_STATUS';
const messages = defineMessages({
exceededImageSizeLimit: { id: 'upload_error.image_size_limit', defaultMessage: 'Image exceeds the current file size limit ({limit})' },
exceededVideoSizeLimit: { id: 'upload_error.video_size_limit', defaultMessage: 'Video exceeds the current file size limit ({limit})' },
exceededVideoDurationLimit: { id: 'upload_error.video_duration_limit', defaultMessage: 'Video exceeds the current duration limit ({limit, plural, one {# second} other {# seconds}})' },
exceededVideoDurationLimit: { id: 'upload_error.video_duration_limit', defaultMessage: 'Video exceeds the current duration limit ({limit} seconds)' },
scheduleError: { id: 'compose.invalid_schedule', defaultMessage: 'You must schedule a post at least 5 minutes out.' },
success: { id: 'compose.submit_success', defaultMessage: 'Your post was sent' },
editSuccess: { id: 'compose.edit_success', defaultMessage: 'Your post was edited' },
@ -278,7 +276,7 @@ const submitCompose = (composeId: string, routerHistory?: History, force = false
const idempotencyKey = compose.idempotencyKey;
const params: Record<string, any> = {
const params = {
status,
in_reply_to_id: compose.in_reply_to,
quote_id: compose.quote,
@ -292,8 +290,6 @@ const submitCompose = (composeId: string, routerHistory?: History, force = false
to,
};
if (compose.privacy === 'group') params.group_id = compose.group_id;
dispatch(createStatus(params, idempotencyKey, statusId)).then(function(data) {
if (!statusId && data.visibility === 'direct' && getState().conversations.mounted <= 0 && routerHistory) {
routerHistory.push('/messages');
@ -474,15 +470,6 @@ const undoUploadCompose = (composeId: string, media_id: string) => ({
media_id: media_id,
});
const groupCompose = (composeId: string, groupId: string) =>
(dispatch: AppDispatch, getState: () => RootState) => {
dispatch({
type: COMPOSE_GROUP_POST,
id: composeId,
group_id: groupId,
});
};
const clearComposeSuggestions = (composeId: string) => {
if (cancelFetchComposeSuggestionsAccounts) {
cancelFetchComposeSuggestionsAccounts();
@ -517,9 +504,7 @@ const fetchComposeSuggestionsAccounts = throttle((dispatch, getState, composeId,
}, 200, { leading: true, trailing: true });
const fetchComposeSuggestionsEmojis = (dispatch: AppDispatch, getState: () => RootState, composeId: string, token: string) => {
const state = getState();
const results = emojiSearch(token.replace(':', ''), { maxResults: 5 }, state.custom_emojis);
const results = emojiSearch(token.replace(':', ''), { maxResults: 5 } as any);
dispatch(readyComposeSuggestionsEmojis(composeId, token, results));
};
@ -564,7 +549,7 @@ const selectComposeSuggestion = (composeId: string, position: number, token: str
let completion, startPosition;
if (typeof suggestion === 'object' && suggestion.id) {
completion = isNativeEmoji(suggestion) ? suggestion.native : suggestion.colons;
completion = suggestion.native || suggestion.colons;
startPosition = position - 1;
dispatch(useEmoji(suggestion));
@ -737,7 +722,7 @@ const eventDiscussionCompose = (composeId: string, status: Status) =>
const instance = state.instance;
const { explicitAddressing } = getFeatures(instance);
return dispatch({
dispatch({
type: COMPOSE_EVENT_REPLY,
id: composeId,
status: status,
@ -764,7 +749,6 @@ export {
COMPOSE_UPLOAD_FAIL,
COMPOSE_UPLOAD_PROGRESS,
COMPOSE_UPLOAD_UNDO,
COMPOSE_GROUP_POST,
COMPOSE_SUGGESTIONS_CLEAR,
COMPOSE_SUGGESTIONS_READY,
COMPOSE_SUGGESTION_SELECT,
@ -817,7 +801,6 @@ export {
uploadComposeSuccess,
uploadComposeFail,
undoUploadCompose,
groupCompose,
clearComposeSuggestions,
fetchComposeSuggestions,
readyComposeSuggestionsEmojis,

Wyświetl plik

@ -1,8 +1,13 @@
import type { DropdownPlacement } from 'soapbox/components/dropdown-menu';
const DROPDOWN_MENU_OPEN = 'DROPDOWN_MENU_OPEN';
const DROPDOWN_MENU_CLOSE = 'DROPDOWN_MENU_CLOSE';
const openDropdownMenu = () => ({ type: DROPDOWN_MENU_OPEN });
const closeDropdownMenu = () => ({ type: DROPDOWN_MENU_CLOSE });
const openDropdownMenu = (id: number, placement: DropdownPlacement, keyboard: boolean) =>
({ type: DROPDOWN_MENU_OPEN, id, placement, keyboard });
const closeDropdownMenu = (id: number) =>
({ type: DROPDOWN_MENU_CLOSE, id });
export {
DROPDOWN_MENU_OPEN,

Wyświetl plik

@ -25,7 +25,7 @@ const EMOJI_REACTS_FETCH_FAIL = 'EMOJI_REACTS_FETCH_FAIL';
const noOp = () => () => new Promise(f => f(undefined));
const simpleEmojiReact = (status: Status, emoji: string, custom?: string) =>
const simpleEmojiReact = (status: Status, emoji: string) =>
(dispatch: AppDispatch) => {
const emojiReacts: ImmutableList<ImmutableMap<string, any>> = status.pleroma.get('emoji_reactions') || ImmutableList();
@ -43,7 +43,7 @@ const simpleEmojiReact = (status: Status, emoji: string, custom?: string) =>
if (emoji === '👍') {
dispatch(favourite(status));
} else {
dispatch(emojiReact(status, emoji, custom));
dispatch(emojiReact(status, emoji));
}
}).catch(err => {
console.error(err);
@ -70,11 +70,11 @@ const fetchEmojiReacts = (id: string, emoji: string) =>
});
};
const emojiReact = (status: Status, emoji: string, custom?: string) =>
const emojiReact = (status: Status, emoji: string) =>
(dispatch: AppDispatch, getState: () => RootState) => {
if (!isLoggedIn(getState)) return dispatch(noOp());
dispatch(emojiReactRequest(status, emoji, custom));
dispatch(emojiReactRequest(status, emoji));
return api(getState)
.put(`/api/v1/pleroma/statuses/${status.get('id')}/reactions/${emoji}`)
@ -120,11 +120,10 @@ const fetchEmojiReactsFail = (id: string, error: AxiosError) => ({
error,
});
const emojiReactRequest = (status: Status, emoji: string, custom?: string) => ({
const emojiReactRequest = (status: Status, emoji: string) => ({
type: EMOJI_REACT_REQUEST,
status,
emoji,
custom,
skipLoading: true,
});

Wyświetl plik

@ -1,6 +1,6 @@
import { saveSettings } from './settings';
import type { Emoji } from 'soapbox/features/emoji';
import type { Emoji } from 'soapbox/components/autosuggest-emoji';
import type { AppDispatch } from 'soapbox/store';
const EMOJI_USE = 'EMOJI_USE';

Wyświetl plik

@ -569,7 +569,7 @@ const rejectEventParticipationRequestFail = (id: string, accountId: string, erro
});
const fetchEventIcs = (id: string) =>
(dispatch: AppDispatch, getState: () => RootState) =>
(dispatch: any, getState: () => RootState) =>
api(getState).get(`/api/v1/pleroma/events/${id}/ics`);
const cancelEventCompose = () => ({

Wyświetl plik

@ -34,8 +34,8 @@ type ExportDataActions = {
| typeof EXPORT_BLOCKS_FAIL
| typeof EXPORT_MUTES_REQUEST
| typeof EXPORT_MUTES_SUCCESS
| typeof EXPORT_MUTES_FAIL
error?: any
| typeof EXPORT_MUTES_FAIL,
error?: any,
}
function fileExport(content: string, fileName: string) {

Wyświetl plik

@ -15,7 +15,7 @@ import sourceCode from 'soapbox/utils/code';
import { getWalletAndSign } from 'soapbox/utils/ethereum';
import { getFeatures } from 'soapbox/utils/features';
import { getQuirks } from 'soapbox/utils/quirks';
import { getInstanceScopes } from 'soapbox/utils/scopes';
import { getScopes } from 'soapbox/utils/scopes';
import { baseClient } from '../api';
@ -38,7 +38,7 @@ const fetchExternalInstance = (baseURL?: string) => {
};
const createExternalApp = (instance: Instance, baseURL?: string) =>
(dispatch: AppDispatch, _getState: () => RootState) => {
(dispatch: AppDispatch, getState: () => RootState) => {
// Mitra: skip creating the auth app
if (getQuirks(instance).noApps) return new Promise(f => f({}));
@ -46,15 +46,15 @@ const createExternalApp = (instance: Instance, baseURL?: string) =>
client_name: sourceCode.displayName,
redirect_uris: `${window.location.origin}/login/external`,
website: sourceCode.homepage,
scopes: getInstanceScopes(instance),
scopes: getScopes(getState()),
};
return dispatch(createApp(params, baseURL));
};
const externalAuthorize = (instance: Instance, baseURL: string) =>
(dispatch: AppDispatch, _getState: () => RootState) => {
const scopes = getInstanceScopes(instance);
(dispatch: AppDispatch, getState: () => RootState) => {
const scopes = getScopes(getState());
return dispatch(createExternalApp(instance, baseURL)).then((app) => {
const { client_id, redirect_uri } = app as Record<string, string>;
@ -88,7 +88,7 @@ const externalEthereumLogin = (instance: Instance, baseURL?: string) =>
client_secret: client_secret,
password: signature as string,
redirect_uri: 'urn:ietf:wg:oauth:2.0:oob',
scope: getInstanceScopes(instance),
scope: getScopes(getState()),
};
return dispatch(obtainOAuthToken(params, baseURL))

Wyświetl plik

@ -11,25 +11,25 @@ export const FAMILIAR_FOLLOWERS_FETCH_SUCCESS = 'FAMILIAR_FOLLOWERS_FETCH_SUCCES
export const FAMILIAR_FOLLOWERS_FETCH_FAIL = 'FAMILIAR_FOLLOWERS_FETCH_FAIL';
type FamiliarFollowersFetchRequestAction = {
type: typeof FAMILIAR_FOLLOWERS_FETCH_REQUEST
id: string
type: typeof FAMILIAR_FOLLOWERS_FETCH_REQUEST,
id: string,
}
type FamiliarFollowersFetchRequestSuccessAction = {
type: typeof FAMILIAR_FOLLOWERS_FETCH_SUCCESS
id: string
accounts: Array<APIEntity>
type: typeof FAMILIAR_FOLLOWERS_FETCH_SUCCESS,
id: string,
accounts: Array<APIEntity>,
}
type FamiliarFollowersFetchRequestFailAction = {
type: typeof FAMILIAR_FOLLOWERS_FETCH_FAIL
id: string
error: any
type: typeof FAMILIAR_FOLLOWERS_FETCH_FAIL,
id: string,
error: any,
}
type AccountsImportAction = {
type: typeof ACCOUNTS_IMPORT
accounts: Array<APIEntity>
type: typeof ACCOUNTS_IMPORT,
accounts: Array<APIEntity>,
}
export type FamiliarFollowersActions = FamiliarFollowersFetchRequestAction | FamiliarFollowersFetchRequestSuccessAction | FamiliarFollowersFetchRequestFailAction | AccountsImportAction

Wyświetl plik

@ -12,18 +12,10 @@ const FILTERS_FETCH_REQUEST = 'FILTERS_FETCH_REQUEST';
const FILTERS_FETCH_SUCCESS = 'FILTERS_FETCH_SUCCESS';
const FILTERS_FETCH_FAIL = 'FILTERS_FETCH_FAIL';
const FILTER_FETCH_REQUEST = 'FILTER_FETCH_REQUEST';
const FILTER_FETCH_SUCCESS = 'FILTER_FETCH_SUCCESS';
const FILTER_FETCH_FAIL = 'FILTER_FETCH_FAIL';
const FILTERS_CREATE_REQUEST = 'FILTERS_CREATE_REQUEST';
const FILTERS_CREATE_SUCCESS = 'FILTERS_CREATE_SUCCESS';
const FILTERS_CREATE_FAIL = 'FILTERS_CREATE_FAIL';
const FILTERS_UPDATE_REQUEST = 'FILTERS_UPDATE_REQUEST';
const FILTERS_UPDATE_SUCCESS = 'FILTERS_UPDATE_SUCCESS';
const FILTERS_UPDATE_FAIL = 'FILTERS_UPDATE_FAIL';
const FILTERS_DELETE_REQUEST = 'FILTERS_DELETE_REQUEST';
const FILTERS_DELETE_SUCCESS = 'FILTERS_DELETE_SUCCESS';
const FILTERS_DELETE_FAIL = 'FILTERS_DELETE_FAIL';
@ -33,16 +25,22 @@ const messages = defineMessages({
removed: { id: 'filters.removed', defaultMessage: 'Filter deleted.' },
});
type FilterKeywords = { keyword: string, whole_word: boolean }[];
const fetchFiltersV1 = () =>
const fetchFilters = () =>
(dispatch: AppDispatch, getState: () => RootState) => {
if (!isLoggedIn(getState)) return;
const state = getState();
const instance = state.instance;
const features = getFeatures(instance);
if (!features.filters) return;
dispatch({
type: FILTERS_FETCH_REQUEST,
skipLoading: true,
});
return api(getState)
api(getState)
.get('/api/v1/filters')
.then(({ data }) => dispatch({
type: FILTERS_FETCH_SUCCESS,
@ -57,105 +55,15 @@ const fetchFiltersV1 = () =>
}));
};
const fetchFiltersV2 = () =>
(dispatch: AppDispatch, getState: () => RootState) => {
dispatch({
type: FILTERS_FETCH_REQUEST,
skipLoading: true,
});
return api(getState)
.get('/api/v2/filters')
.then(({ data }) => dispatch({
type: FILTERS_FETCH_SUCCESS,
filters: data,
skipLoading: true,
}))
.catch(err => dispatch({
type: FILTERS_FETCH_FAIL,
err,
skipLoading: true,
skipAlert: true,
}));
};
const fetchFilters = (fromFiltersPage = false) =>
(dispatch: AppDispatch, getState: () => RootState) => {
if (!isLoggedIn(getState)) return;
const state = getState();
const instance = state.instance;
const features = getFeatures(instance);
if (features.filtersV2 && fromFiltersPage) return dispatch(fetchFiltersV2());
if (features.filters) return dispatch(fetchFiltersV1());
};
const fetchFilterV1 = (id: string) =>
(dispatch: AppDispatch, getState: () => RootState) => {
dispatch({
type: FILTER_FETCH_REQUEST,
skipLoading: true,
});
return api(getState)
.get(`/api/v1/filters/${id}`)
.then(({ data }) => dispatch({
type: FILTER_FETCH_SUCCESS,
filter: data,
skipLoading: true,
}))
.catch(err => dispatch({
type: FILTER_FETCH_FAIL,
err,
skipLoading: true,
skipAlert: true,
}));
};
const fetchFilterV2 = (id: string) =>
(dispatch: AppDispatch, getState: () => RootState) => {
dispatch({
type: FILTER_FETCH_REQUEST,
skipLoading: true,
});
return api(getState)
.get(`/api/v2/filters/${id}`)
.then(({ data }) => dispatch({
type: FILTER_FETCH_SUCCESS,
filter: data,
skipLoading: true,
}))
.catch(err => dispatch({
type: FILTER_FETCH_FAIL,
err,
skipLoading: true,
skipAlert: true,
}));
};
const fetchFilter = (id: string) =>
(dispatch: AppDispatch, getState: () => RootState) => {
const state = getState();
const instance = state.instance;
const features = getFeatures(instance);
if (features.filtersV2) return dispatch(fetchFilterV2(id));
if (features.filters) return dispatch(fetchFilterV1(id));
};
const createFilterV1 = (title: string, expires_in: string | null, context: Array<string>, hide: boolean, keywords: FilterKeywords) =>
const createFilter = (phrase: string, expires_at: string, context: Array<string>, whole_word: boolean, irreversible: boolean) =>
(dispatch: AppDispatch, getState: () => RootState) => {
dispatch({ type: FILTERS_CREATE_REQUEST });
return api(getState).post('/api/v1/filters', {
phrase: keywords[0].keyword,
phrase,
context,
irreversible: hide,
whole_word: keywords[0].whole_word,
expires_in,
irreversible,
whole_word,
expires_at,
}).then(response => {
dispatch({ type: FILTERS_CREATE_SUCCESS, filter: response.data });
toast.success(messages.added);
@ -164,80 +72,7 @@ const createFilterV1 = (title: string, expires_in: string | null, context: Array
});
};
const createFilterV2 = (title: string, expires_in: string | null, context: Array<string>, hide: boolean, keywords_attributes: FilterKeywords) =>
(dispatch: AppDispatch, getState: () => RootState) => {
dispatch({ type: FILTERS_CREATE_REQUEST });
return api(getState).post('/api/v2/filters', {
title,
context,
filter_action: hide ? 'hide' : 'warn',
expires_in,
keywords_attributes,
}).then(response => {
dispatch({ type: FILTERS_CREATE_SUCCESS, filter: response.data });
toast.success(messages.added);
}).catch(error => {
dispatch({ type: FILTERS_CREATE_FAIL, error });
});
};
const createFilter = (title: string, expires_in: string | null, context: Array<string>, hide: boolean, keywords: FilterKeywords) =>
(dispatch: AppDispatch, getState: () => RootState) => {
const state = getState();
const instance = state.instance;
const features = getFeatures(instance);
if (features.filtersV2) return dispatch(createFilterV2(title, expires_in, context, hide, keywords));
return dispatch(createFilterV1(title, expires_in, context, hide, keywords));
};
const updateFilterV1 = (id: string, title: string, expires_in: string | null, context: Array<string>, hide: boolean, keywords: FilterKeywords) =>
(dispatch: AppDispatch, getState: () => RootState) => {
dispatch({ type: FILTERS_UPDATE_REQUEST });
return api(getState).patch(`/api/v1/filters/${id}`, {
phrase: keywords[0].keyword,
context,
irreversible: hide,
whole_word: keywords[0].whole_word,
expires_in,
}).then(response => {
dispatch({ type: FILTERS_UPDATE_SUCCESS, filter: response.data });
toast.success(messages.added);
}).catch(error => {
dispatch({ type: FILTERS_UPDATE_FAIL, error });
});
};
const updateFilterV2 = (id: string, title: string, expires_in: string | null, context: Array<string>, hide: boolean, keywords_attributes: FilterKeywords) =>
(dispatch: AppDispatch, getState: () => RootState) => {
dispatch({ type: FILTERS_UPDATE_REQUEST });
return api(getState).patch(`/api/v2/filters/${id}`, {
title,
context,
filter_action: hide ? 'hide' : 'warn',
expires_in,
keywords_attributes,
}).then(response => {
dispatch({ type: FILTERS_UPDATE_SUCCESS, filter: response.data });
toast.success(messages.added);
}).catch(error => {
dispatch({ type: FILTERS_UPDATE_FAIL, error });
});
};
const updateFilter = (id: string, title: string, expires_in: string | null, context: Array<string>, hide: boolean, keywords: FilterKeywords) =>
(dispatch: AppDispatch, getState: () => RootState) => {
const state = getState();
const instance = state.instance;
const features = getFeatures(instance);
if (features.filtersV2) return dispatch(updateFilterV2(id, title, expires_in, context, hide, keywords));
return dispatch(updateFilterV1(id, title, expires_in, context, hide, keywords));
};
const deleteFilterV1 = (id: string) =>
const deleteFilter = (id: string) =>
(dispatch: AppDispatch, getState: () => RootState) => {
dispatch({ type: FILTERS_DELETE_REQUEST });
return api(getState).delete(`/api/v1/filters/${id}`).then(response => {
@ -248,47 +83,17 @@ const deleteFilterV1 = (id: string) =>
});
};
const deleteFilterV2 = (id: string) =>
(dispatch: AppDispatch, getState: () => RootState) => {
dispatch({ type: FILTERS_DELETE_REQUEST });
return api(getState).delete(`/api/v2/filters/${id}`).then(response => {
dispatch({ type: FILTERS_DELETE_SUCCESS, filter: response.data });
toast.success(messages.removed);
}).catch(error => {
dispatch({ type: FILTERS_DELETE_FAIL, error });
});
};
const deleteFilter = (id: string) =>
(dispatch: AppDispatch, getState: () => RootState) => {
const state = getState();
const instance = state.instance;
const features = getFeatures(instance);
if (features.filtersV2) return dispatch(deleteFilterV2(id));
return dispatch(deleteFilterV1(id));
};
export {
FILTERS_FETCH_REQUEST,
FILTERS_FETCH_SUCCESS,
FILTERS_FETCH_FAIL,
FILTER_FETCH_REQUEST,
FILTER_FETCH_SUCCESS,
FILTER_FETCH_FAIL,
FILTERS_CREATE_REQUEST,
FILTERS_CREATE_SUCCESS,
FILTERS_CREATE_FAIL,
FILTERS_UPDATE_REQUEST,
FILTERS_UPDATE_SUCCESS,
FILTERS_UPDATE_FAIL,
FILTERS_DELETE_REQUEST,
FILTERS_DELETE_SUCCESS,
FILTERS_DELETE_FAIL,
fetchFilters,
fetchFilter,
createFilter,
updateFilter,
deleteFilter,
};
};

Wyświetl plik

@ -1,964 +0,0 @@
import { defineMessages } from 'react-intl';
import { deleteEntities } from 'soapbox/entity-store/actions';
import toast from 'soapbox/toast';
import api, { getLinks } from '../api';
import { fetchRelationships } from './accounts';
import { importFetchedGroups, importFetchedAccounts } from './importer';
import { closeModal, openModal } from './modals';
import { deleteFromTimelines } from './timelines';
import type { AxiosError } from 'axios';
import type { GroupRole } from 'soapbox/reducers/group-memberships';
import type { AppDispatch, RootState } from 'soapbox/store';
import type { APIEntity, Group } from 'soapbox/types/entities';
const GROUP_EDITOR_SET = 'GROUP_EDITOR_SET';
const GROUP_CREATE_REQUEST = 'GROUP_CREATE_REQUEST';
const GROUP_CREATE_SUCCESS = 'GROUP_CREATE_SUCCESS';
const GROUP_CREATE_FAIL = 'GROUP_CREATE_FAIL';
const GROUP_UPDATE_REQUEST = 'GROUP_UPDATE_REQUEST';
const GROUP_UPDATE_SUCCESS = 'GROUP_UPDATE_SUCCESS';
const GROUP_UPDATE_FAIL = 'GROUP_UPDATE_FAIL';
const GROUP_DELETE_REQUEST = 'GROUP_DELETE_REQUEST';
const GROUP_DELETE_SUCCESS = 'GROUP_DELETE_SUCCESS';
const GROUP_DELETE_FAIL = 'GROUP_DELETE_FAIL';
const GROUP_FETCH_REQUEST = 'GROUP_FETCH_REQUEST';
const GROUP_FETCH_SUCCESS = 'GROUP_FETCH_SUCCESS';
const GROUP_FETCH_FAIL = 'GROUP_FETCH_FAIL';
const GROUPS_FETCH_REQUEST = 'GROUPS_FETCH_REQUEST';
const GROUPS_FETCH_SUCCESS = 'GROUPS_FETCH_SUCCESS';
const GROUPS_FETCH_FAIL = 'GROUPS_FETCH_FAIL';
const GROUP_RELATIONSHIPS_FETCH_REQUEST = 'GROUP_RELATIONSHIPS_FETCH_REQUEST';
const GROUP_RELATIONSHIPS_FETCH_SUCCESS = 'GROUP_RELATIONSHIPS_FETCH_SUCCESS';
const GROUP_RELATIONSHIPS_FETCH_FAIL = 'GROUP_RELATIONSHIPS_FETCH_FAIL';
const GROUP_DELETE_STATUS_REQUEST = 'GROUP_DELETE_STATUS_REQUEST';
const GROUP_DELETE_STATUS_SUCCESS = 'GROUP_DELETE_STATUS_SUCCESS';
const GROUP_DELETE_STATUS_FAIL = 'GROUP_DELETE_STATUS_FAIL';
const GROUP_KICK_REQUEST = 'GROUP_KICK_REQUEST';
const GROUP_KICK_SUCCESS = 'GROUP_KICK_SUCCESS';
const GROUP_KICK_FAIL = 'GROUP_KICK_FAIL';
const GROUP_BLOCKS_FETCH_REQUEST = 'GROUP_BLOCKS_FETCH_REQUEST';
const GROUP_BLOCKS_FETCH_SUCCESS = 'GROUP_BLOCKS_FETCH_SUCCESS';
const GROUP_BLOCKS_FETCH_FAIL = 'GROUP_BLOCKS_FETCH_FAIL';
const GROUP_BLOCKS_EXPAND_REQUEST = 'GROUP_BLOCKS_EXPAND_REQUEST';
const GROUP_BLOCKS_EXPAND_SUCCESS = 'GROUP_BLOCKS_EXPAND_SUCCESS';
const GROUP_BLOCKS_EXPAND_FAIL = 'GROUP_BLOCKS_EXPAND_FAIL';
const GROUP_BLOCK_REQUEST = 'GROUP_BLOCK_REQUEST';
const GROUP_BLOCK_SUCCESS = 'GROUP_BLOCK_SUCCESS';
const GROUP_BLOCK_FAIL = 'GROUP_BLOCK_FAIL';
const GROUP_UNBLOCK_REQUEST = 'GROUP_UNBLOCK_REQUEST';
const GROUP_UNBLOCK_SUCCESS = 'GROUP_UNBLOCK_SUCCESS';
const GROUP_UNBLOCK_FAIL = 'GROUP_UNBLOCK_FAIL';
const GROUP_PROMOTE_REQUEST = 'GROUP_PROMOTE_REQUEST';
const GROUP_PROMOTE_SUCCESS = 'GROUP_PROMOTE_SUCCESS';
const GROUP_PROMOTE_FAIL = 'GROUP_PROMOTE_FAIL';
const GROUP_DEMOTE_REQUEST = 'GROUP_DEMOTE_REQUEST';
const GROUP_DEMOTE_SUCCESS = 'GROUP_DEMOTE_SUCCESS';
const GROUP_DEMOTE_FAIL = 'GROUP_DEMOTE_FAIL';
const GROUP_MEMBERSHIPS_FETCH_REQUEST = 'GROUP_MEMBERSHIPS_FETCH_REQUEST';
const GROUP_MEMBERSHIPS_FETCH_SUCCESS = 'GROUP_MEMBERSHIPS_FETCH_SUCCESS';
const GROUP_MEMBERSHIPS_FETCH_FAIL = 'GROUP_MEMBERSHIPS_FETCH_FAIL';
const GROUP_MEMBERSHIPS_EXPAND_REQUEST = 'GROUP_MEMBERSHIPS_EXPAND_REQUEST';
const GROUP_MEMBERSHIPS_EXPAND_SUCCESS = 'GROUP_MEMBERSHIPS_EXPAND_SUCCESS';
const GROUP_MEMBERSHIPS_EXPAND_FAIL = 'GROUP_MEMBERSHIPS_EXPAND_FAIL';
const GROUP_MEMBERSHIP_REQUESTS_FETCH_REQUEST = 'GROUP_MEMBERSHIP_REQUESTS_FETCH_REQUEST';
const GROUP_MEMBERSHIP_REQUESTS_FETCH_SUCCESS = 'GROUP_MEMBERSHIP_REQUESTS_FETCH_SUCCESS';
const GROUP_MEMBERSHIP_REQUESTS_FETCH_FAIL = 'GROUP_MEMBERSHIP_REQUESTS_FETCH_FAIL';
const GROUP_MEMBERSHIP_REQUESTS_EXPAND_REQUEST = 'GROUP_MEMBERSHIP_REQUESTS_EXPAND_REQUEST';
const GROUP_MEMBERSHIP_REQUESTS_EXPAND_SUCCESS = 'GROUP_MEMBERSHIP_REQUESTS_EXPAND_SUCCESS';
const GROUP_MEMBERSHIP_REQUESTS_EXPAND_FAIL = 'GROUP_MEMBERSHIP_REQUESTS_EXPAND_FAIL';
const GROUP_MEMBERSHIP_REQUEST_AUTHORIZE_REQUEST = 'GROUP_MEMBERSHIP_REQUEST_AUTHORIZE_REQUEST';
const GROUP_MEMBERSHIP_REQUEST_AUTHORIZE_SUCCESS = 'GROUP_MEMBERSHIP_REQUEST_AUTHORIZE_SUCCESS';
const GROUP_MEMBERSHIP_REQUEST_AUTHORIZE_FAIL = 'GROUP_MEMBERSHIP_REQUEST_AUTHORIZE_FAIL';
const GROUP_MEMBERSHIP_REQUEST_REJECT_REQUEST = 'GROUP_MEMBERSHIP_REQUEST_REJECT_REQUEST';
const GROUP_MEMBERSHIP_REQUEST_REJECT_SUCCESS = 'GROUP_MEMBERSHIP_REQUEST_REJECT_SUCCESS';
const GROUP_MEMBERSHIP_REQUEST_REJECT_FAIL = 'GROUP_MEMBERSHIP_REQUEST_REJECT_FAIL';
const GROUP_EDITOR_TITLE_CHANGE = 'GROUP_EDITOR_TITLE_CHANGE';
const GROUP_EDITOR_DESCRIPTION_CHANGE = 'GROUP_EDITOR_DESCRIPTION_CHANGE';
const GROUP_EDITOR_PRIVACY_CHANGE = 'GROUP_EDITOR_PRIVACY_CHANGE';
const GROUP_EDITOR_MEDIA_CHANGE = 'GROUP_EDITOR_MEDIA_CHANGE';
const GROUP_EDITOR_RESET = 'GROUP_EDITOR_RESET';
const messages = defineMessages({
success: { id: 'manage_group.submit_success', defaultMessage: 'The group was created' },
editSuccess: { id: 'manage_group.edit_success', defaultMessage: 'The group was edited' },
joinSuccess: { id: 'group.join.success', defaultMessage: 'Joined the group' },
joinRequestSuccess: { id: 'group.join.request_success', defaultMessage: 'Requested to join the group' },
leaveSuccess: { id: 'group.leave.success', defaultMessage: 'Left the group' },
view: { id: 'toast.view', defaultMessage: 'View' },
});
const editGroup = (group: Group) => (dispatch: AppDispatch) => {
dispatch({
type: GROUP_EDITOR_SET,
group,
});
dispatch(openModal('MANAGE_GROUP'));
};
const createGroup = (params: Record<string, any>, shouldReset?: boolean) =>
(dispatch: AppDispatch, getState: () => RootState) => {
dispatch(createGroupRequest());
return api(getState).post('/api/v1/groups', params, {
headers: {
'Content-Type': 'multipart/form-data',
},
})
.then(({ data }) => {
dispatch(importFetchedGroups([data]));
dispatch(createGroupSuccess(data));
toast.success(messages.success, {
actionLabel: messages.view,
actionLink: `/groups/${data.id}`,
});
if (shouldReset) {
dispatch(resetGroupEditor());
}
return data;
}).catch(err => dispatch(createGroupFail(err)));
};
const createGroupRequest = () => ({
type: GROUP_CREATE_REQUEST,
});
const createGroupSuccess = (group: APIEntity) => ({
type: GROUP_CREATE_SUCCESS,
group,
});
const createGroupFail = (error: AxiosError) => ({
type: GROUP_CREATE_FAIL,
error,
});
const updateGroup = (id: string, params: Record<string, any>, shouldReset?: boolean) =>
(dispatch: AppDispatch, getState: () => RootState) => {
dispatch(updateGroupRequest());
return api(getState).put(`/api/v1/groups/${id}`, params)
.then(({ data }) => {
dispatch(importFetchedGroups([data]));
dispatch(updateGroupSuccess(data));
toast.success(messages.editSuccess);
if (shouldReset) {
dispatch(resetGroupEditor());
}
dispatch(closeModal('MANAGE_GROUP'));
}).catch(err => dispatch(updateGroupFail(err)));
};
const updateGroupRequest = () => ({
type: GROUP_UPDATE_REQUEST,
});
const updateGroupSuccess = (group: APIEntity) => ({
type: GROUP_UPDATE_SUCCESS,
group,
});
const updateGroupFail = (error: AxiosError) => ({
type: GROUP_UPDATE_FAIL,
error,
});
const deleteGroup = (id: string) => (dispatch: AppDispatch, getState: () => RootState) => {
dispatch(deleteEntities([id], 'Group'));
return api(getState).delete(`/api/v1/groups/${id}`)
.then(() => dispatch(deleteGroupSuccess(id)))
.catch(err => dispatch(deleteGroupFail(id, err)));
};
const deleteGroupRequest = (id: string) => ({
type: GROUP_DELETE_REQUEST,
id,
});
const deleteGroupSuccess = (id: string) => ({
type: GROUP_DELETE_SUCCESS,
id,
});
const deleteGroupFail = (id: string, error: AxiosError) => ({
type: GROUP_DELETE_FAIL,
id,
error,
});
const fetchGroup = (id: string) => (dispatch: AppDispatch, getState: () => RootState) => {
dispatch(fetchGroupRelationships([id]));
dispatch(fetchGroupRequest(id));
return api(getState).get(`/api/v1/groups/${id}`)
.then(({ data }) => {
dispatch(importFetchedGroups([data]));
dispatch(fetchGroupSuccess(data));
})
.catch(err => dispatch(fetchGroupFail(id, err)));
};
const fetchGroupRequest = (id: string) => ({
type: GROUP_FETCH_REQUEST,
id,
});
const fetchGroupSuccess = (group: APIEntity) => ({
type: GROUP_FETCH_SUCCESS,
group,
});
const fetchGroupFail = (id: string, error: AxiosError) => ({
type: GROUP_FETCH_FAIL,
id,
error,
});
const fetchGroups = () => (dispatch: AppDispatch, getState: () => RootState) => {
dispatch(fetchGroupsRequest());
return api(getState).get('/api/v1/groups')
.then(({ data }) => {
dispatch(importFetchedGroups(data));
dispatch(fetchGroupsSuccess(data));
dispatch(fetchGroupRelationships(data.map((item: APIEntity) => item.id)));
}).catch(err => dispatch(fetchGroupsFail(err)));
};
const fetchGroupsRequest = () => ({
type: GROUPS_FETCH_REQUEST,
});
const fetchGroupsSuccess = (groups: APIEntity[]) => ({
type: GROUPS_FETCH_SUCCESS,
groups,
});
const fetchGroupsFail = (error: AxiosError) => ({
type: GROUPS_FETCH_FAIL,
error,
});
const fetchGroupRelationships = (groupIds: string[]) =>
(dispatch: AppDispatch, getState: () => RootState) => {
const state = getState();
const loadedRelationships = state.group_relationships;
const newGroupIds = groupIds.filter(id => loadedRelationships.get(id, null) === null);
if (!state.me || newGroupIds.length === 0) {
return;
}
dispatch(fetchGroupRelationshipsRequest(newGroupIds));
return api(getState).get(`/api/v1/groups/relationships?${newGroupIds.map(id => `id[]=${id}`).join('&')}`).then(response => {
dispatch(fetchGroupRelationshipsSuccess(response.data));
}).catch(error => {
dispatch(fetchGroupRelationshipsFail(error));
});
};
const fetchGroupRelationshipsRequest = (ids: string[]) => ({
type: GROUP_RELATIONSHIPS_FETCH_REQUEST,
ids,
skipLoading: true,
});
const fetchGroupRelationshipsSuccess = (relationships: APIEntity[]) => ({
type: GROUP_RELATIONSHIPS_FETCH_SUCCESS,
relationships,
skipLoading: true,
});
const fetchGroupRelationshipsFail = (error: AxiosError) => ({
type: GROUP_RELATIONSHIPS_FETCH_FAIL,
error,
skipLoading: true,
skipNotFound: true,
});
const groupDeleteStatus = (groupId: string, statusId: string) =>
(dispatch: AppDispatch, getState: () => RootState) => {
dispatch(groupDeleteStatusRequest(groupId, statusId));
return api(getState).delete(`/api/v1/groups/${groupId}/statuses/${statusId}`)
.then(() => {
dispatch(deleteFromTimelines(statusId));
dispatch(groupDeleteStatusSuccess(groupId, statusId));
}).catch(err => dispatch(groupDeleteStatusFail(groupId, statusId, err)));
};
const groupDeleteStatusRequest = (groupId: string, statusId: string) => ({
type: GROUP_DELETE_STATUS_REQUEST,
groupId,
statusId,
});
const groupDeleteStatusSuccess = (groupId: string, statusId: string) => ({
type: GROUP_DELETE_STATUS_SUCCESS,
groupId,
statusId,
});
const groupDeleteStatusFail = (groupId: string, statusId: string, error: AxiosError) => ({
type: GROUP_DELETE_STATUS_SUCCESS,
groupId,
statusId,
error,
});
const groupKick = (groupId: string, accountId: string) =>
(dispatch: AppDispatch, getState: () => RootState) => {
dispatch(groupKickRequest(groupId, accountId));
return api(getState).post(`/api/v1/groups/${groupId}/kick`, { account_ids: [accountId] })
.then(() => dispatch(groupKickSuccess(groupId, accountId)))
.catch(err => dispatch(groupKickFail(groupId, accountId, err)));
};
const groupKickRequest = (groupId: string, accountId: string) => ({
type: GROUP_KICK_REQUEST,
groupId,
accountId,
});
const groupKickSuccess = (groupId: string, accountId: string) => ({
type: GROUP_KICK_SUCCESS,
groupId,
accountId,
});
const groupKickFail = (groupId: string, accountId: string, error: AxiosError) => ({
type: GROUP_KICK_SUCCESS,
groupId,
accountId,
error,
});
const fetchGroupBlocks = (id: string) =>
(dispatch: AppDispatch, getState: () => RootState) => {
dispatch(fetchGroupBlocksRequest(id));
return api(getState).get(`/api/v1/groups/${id}/blocks`).then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(importFetchedAccounts(response.data));
dispatch(fetchGroupBlocksSuccess(id, response.data, next ? next.uri : null));
}).catch(error => {
dispatch(fetchGroupBlocksFail(id, error));
});
};
const fetchGroupBlocksRequest = (id: string) => ({
type: GROUP_BLOCKS_FETCH_REQUEST,
id,
});
const fetchGroupBlocksSuccess = (id: string, accounts: APIEntity[], next: string | null) => ({
type: GROUP_BLOCKS_FETCH_SUCCESS,
id,
accounts,
next,
});
const fetchGroupBlocksFail = (id: string, error: AxiosError) => ({
type: GROUP_BLOCKS_FETCH_FAIL,
id,
error,
skipNotFound: true,
});
const expandGroupBlocks = (id: string) =>
(dispatch: AppDispatch, getState: () => RootState) => {
const url = getState().user_lists.group_blocks.get(id)?.next || null;
if (url === null) {
return;
}
dispatch(expandGroupBlocksRequest(id));
return api(getState).get(url).then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(importFetchedAccounts(response.data));
dispatch(expandGroupBlocksSuccess(id, response.data, next ? next.uri : null));
dispatch(fetchRelationships(response.data.map((item: APIEntity) => item.id)));
}).catch(error => {
dispatch(expandGroupBlocksFail(id, error));
});
};
const expandGroupBlocksRequest = (id: string) => ({
type: GROUP_BLOCKS_EXPAND_REQUEST,
id,
});
const expandGroupBlocksSuccess = (id: string, accounts: APIEntity[], next: string | null) => ({
type: GROUP_BLOCKS_EXPAND_SUCCESS,
id,
accounts,
next,
});
const expandGroupBlocksFail = (id: string, error: AxiosError) => ({
type: GROUP_BLOCKS_EXPAND_FAIL,
id,
error,
});
const groupBlock = (groupId: string, accountId: string) =>
(dispatch: AppDispatch, getState: () => RootState) => {
dispatch(groupBlockRequest(groupId, accountId));
return api(getState).post(`/api/v1/groups/${groupId}/blocks`, { account_ids: [accountId] })
.then(() => dispatch(groupBlockSuccess(groupId, accountId)))
.catch(err => dispatch(groupBlockFail(groupId, accountId, err)));
};
const groupBlockRequest = (groupId: string, accountId: string) => ({
type: GROUP_BLOCK_REQUEST,
groupId,
accountId,
});
const groupBlockSuccess = (groupId: string, accountId: string) => ({
type: GROUP_BLOCK_SUCCESS,
groupId,
accountId,
});
const groupBlockFail = (groupId: string, accountId: string, error: AxiosError) => ({
type: GROUP_BLOCK_FAIL,
groupId,
accountId,
error,
});
const groupUnblock = (groupId: string, accountId: string) =>
(dispatch: AppDispatch, getState: () => RootState) => {
dispatch(groupUnblockRequest(groupId, accountId));
return api(getState).delete(`/api/v1/groups/${groupId}/blocks?account_ids[]=${accountId}`)
.then(() => dispatch(groupUnblockSuccess(groupId, accountId)))
.catch(err => dispatch(groupUnblockFail(groupId, accountId, err)));
};
const groupUnblockRequest = (groupId: string, accountId: string) => ({
type: GROUP_UNBLOCK_REQUEST,
groupId,
accountId,
});
const groupUnblockSuccess = (groupId: string, accountId: string) => ({
type: GROUP_UNBLOCK_SUCCESS,
groupId,
accountId,
});
const groupUnblockFail = (groupId: string, accountId: string, error: AxiosError) => ({
type: GROUP_UNBLOCK_FAIL,
groupId,
accountId,
error,
});
const groupPromoteAccount = (groupId: string, accountId: string, role: GroupRole) =>
(dispatch: AppDispatch, getState: () => RootState) => {
dispatch(groupPromoteAccountRequest(groupId, accountId));
return api(getState).post(`/api/v1/groups/${groupId}/promote`, { account_ids: [accountId], role: role })
.then((response) => dispatch(groupPromoteAccountSuccess(groupId, accountId, response.data)))
.catch(err => dispatch(groupPromoteAccountFail(groupId, accountId, err)));
};
const groupPromoteAccountRequest = (groupId: string, accountId: string) => ({
type: GROUP_PROMOTE_REQUEST,
groupId,
accountId,
});
const groupPromoteAccountSuccess = (groupId: string, accountId: string, memberships: APIEntity[]) => ({
type: GROUP_PROMOTE_SUCCESS,
groupId,
accountId,
memberships,
});
const groupPromoteAccountFail = (groupId: string, accountId: string, error: AxiosError) => ({
type: GROUP_PROMOTE_FAIL,
groupId,
accountId,
error,
});
const groupDemoteAccount = (groupId: string, accountId: string, role: GroupRole) =>
(dispatch: AppDispatch, getState: () => RootState) => {
dispatch(groupDemoteAccountRequest(groupId, accountId));
return api(getState).post(`/api/v1/groups/${groupId}/demote`, { account_ids: [accountId], role: role })
.then((response) => dispatch(groupDemoteAccountSuccess(groupId, accountId, response.data)))
.catch(err => dispatch(groupDemoteAccountFail(groupId, accountId, err)));
};
const groupDemoteAccountRequest = (groupId: string, accountId: string) => ({
type: GROUP_DEMOTE_REQUEST,
groupId,
accountId,
});
const groupDemoteAccountSuccess = (groupId: string, accountId: string, memberships: APIEntity[]) => ({
type: GROUP_DEMOTE_SUCCESS,
groupId,
accountId,
memberships,
});
const groupDemoteAccountFail = (groupId: string, accountId: string, error: AxiosError) => ({
type: GROUP_DEMOTE_FAIL,
groupId,
accountId,
error,
});
const fetchGroupMemberships = (id: string, role: GroupRole) =>
(dispatch: AppDispatch, getState: () => RootState) => {
dispatch(fetchGroupMembershipsRequest(id, role));
return api(getState).get(`/api/v1/groups/${id}/memberships`, { params: { role } }).then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(importFetchedAccounts(response.data.map((membership: APIEntity) => membership.account)));
dispatch(fetchGroupMembershipsSuccess(id, role, response.data, next ? next.uri : null));
}).catch(error => {
dispatch(fetchGroupMembershipsFail(id, role, error));
});
};
const fetchGroupMembershipsRequest = (id: string, role: GroupRole) => ({
type: GROUP_MEMBERSHIPS_FETCH_REQUEST,
id,
role,
});
const fetchGroupMembershipsSuccess = (id: string, role: GroupRole, memberships: APIEntity[], next: string | null) => ({
type: GROUP_MEMBERSHIPS_FETCH_SUCCESS,
id,
role,
memberships,
next,
});
const fetchGroupMembershipsFail = (id: string, role: GroupRole, error: AxiosError) => ({
type: GROUP_MEMBERSHIPS_FETCH_FAIL,
id,
role,
error,
skipNotFound: true,
});
const expandGroupMemberships = (id: string, role: GroupRole) =>
(dispatch: AppDispatch, getState: () => RootState) => {
const url = getState().group_memberships.get(role).get(id)?.next || null;
if (url === null) {
return;
}
dispatch(expandGroupMembershipsRequest(id, role));
return api(getState).get(url).then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(importFetchedAccounts(response.data.map((membership: APIEntity) => membership.account)));
dispatch(expandGroupMembershipsSuccess(id, role, response.data, next ? next.uri : null));
dispatch(fetchRelationships(response.data.map((item: APIEntity) => item.id)));
}).catch(error => {
dispatch(expandGroupMembershipsFail(id, role, error));
});
};
const expandGroupMembershipsRequest = (id: string, role: GroupRole) => ({
type: GROUP_MEMBERSHIPS_EXPAND_REQUEST,
id,
role,
});
const expandGroupMembershipsSuccess = (id: string, role: GroupRole, memberships: APIEntity[], next: string | null) => ({
type: GROUP_MEMBERSHIPS_EXPAND_SUCCESS,
id,
role,
memberships,
next,
});
const expandGroupMembershipsFail = (id: string, role: GroupRole, error: AxiosError) => ({
type: GROUP_MEMBERSHIPS_EXPAND_FAIL,
id,
role,
error,
});
const fetchGroupMembershipRequests = (id: string) =>
(dispatch: AppDispatch, getState: () => RootState) => {
dispatch(fetchGroupMembershipRequestsRequest(id));
return api(getState).get(`/api/v1/groups/${id}/membership_requests`).then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(importFetchedAccounts(response.data));
dispatch(fetchGroupMembershipRequestsSuccess(id, response.data, next ? next.uri : null));
}).catch(error => {
dispatch(fetchGroupMembershipRequestsFail(id, error));
});
};
const fetchGroupMembershipRequestsRequest = (id: string) => ({
type: GROUP_MEMBERSHIP_REQUESTS_FETCH_REQUEST,
id,
});
const fetchGroupMembershipRequestsSuccess = (id: string, accounts: APIEntity[], next: string | null) => ({
type: GROUP_MEMBERSHIP_REQUESTS_FETCH_SUCCESS,
id,
accounts,
next,
});
const fetchGroupMembershipRequestsFail = (id: string, error: AxiosError) => ({
type: GROUP_MEMBERSHIP_REQUESTS_FETCH_FAIL,
id,
error,
skipNotFound: true,
});
const expandGroupMembershipRequests = (id: string) =>
(dispatch: AppDispatch, getState: () => RootState) => {
const url = getState().user_lists.membership_requests.get(id)?.next || null;
if (url === null) {
return;
}
dispatch(expandGroupMembershipRequestsRequest(id));
return api(getState).get(url).then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(importFetchedAccounts(response.data));
dispatch(expandGroupMembershipRequestsSuccess(id, response.data, next ? next.uri : null));
dispatch(fetchRelationships(response.data.map((item: APIEntity) => item.id)));
}).catch(error => {
dispatch(expandGroupMembershipRequestsFail(id, error));
});
};
const expandGroupMembershipRequestsRequest = (id: string) => ({
type: GROUP_MEMBERSHIP_REQUESTS_EXPAND_REQUEST,
id,
});
const expandGroupMembershipRequestsSuccess = (id: string, accounts: APIEntity[], next: string | null) => ({
type: GROUP_MEMBERSHIP_REQUESTS_EXPAND_SUCCESS,
id,
accounts,
next,
});
const expandGroupMembershipRequestsFail = (id: string, error: AxiosError) => ({
type: GROUP_MEMBERSHIP_REQUESTS_EXPAND_FAIL,
id,
error,
});
const authorizeGroupMembershipRequest = (groupId: string, accountId: string) =>
(dispatch: AppDispatch, getState: () => RootState) => {
dispatch(authorizeGroupMembershipRequestRequest(groupId, accountId));
return api(getState)
.post(`/api/v1/groups/${groupId}/membership_requests/${accountId}/authorize`)
.then(() => dispatch(authorizeGroupMembershipRequestSuccess(groupId, accountId)))
.catch(error => dispatch(authorizeGroupMembershipRequestFail(groupId, accountId, error)));
};
const authorizeGroupMembershipRequestRequest = (groupId: string, accountId: string) => ({
type: GROUP_MEMBERSHIP_REQUEST_AUTHORIZE_REQUEST,
groupId,
accountId,
});
const authorizeGroupMembershipRequestSuccess = (groupId: string, accountId: string) => ({
type: GROUP_MEMBERSHIP_REQUEST_AUTHORIZE_SUCCESS,
groupId,
accountId,
});
const authorizeGroupMembershipRequestFail = (groupId: string, accountId: string, error: AxiosError) => ({
type: GROUP_MEMBERSHIP_REQUEST_AUTHORIZE_FAIL,
groupId,
accountId,
error,
});
const rejectGroupMembershipRequest = (groupId: string, accountId: string) =>
(dispatch: AppDispatch, getState: () => RootState) => {
dispatch(rejectGroupMembershipRequestRequest(groupId, accountId));
return api(getState)
.post(`/api/v1/groups/${groupId}/membership_requests/${accountId}/reject`)
.then(() => dispatch(rejectGroupMembershipRequestSuccess(groupId, accountId)))
.catch(error => dispatch(rejectGroupMembershipRequestFail(groupId, accountId, error)));
};
const rejectGroupMembershipRequestRequest = (groupId: string, accountId: string) => ({
type: GROUP_MEMBERSHIP_REQUEST_REJECT_REQUEST,
groupId,
accountId,
});
const rejectGroupMembershipRequestSuccess = (groupId: string, accountId: string) => ({
type: GROUP_MEMBERSHIP_REQUEST_REJECT_SUCCESS,
groupId,
accountId,
});
const rejectGroupMembershipRequestFail = (groupId: string, accountId: string, error?: AxiosError) => ({
type: GROUP_MEMBERSHIP_REQUEST_REJECT_FAIL,
groupId,
accountId,
error,
});
const changeGroupEditorTitle = (value: string) => ({
type: GROUP_EDITOR_TITLE_CHANGE,
value,
});
const changeGroupEditorDescription = (value: string) => ({
type: GROUP_EDITOR_DESCRIPTION_CHANGE,
value,
});
const changeGroupEditorPrivacy = (value: boolean) => ({
type: GROUP_EDITOR_PRIVACY_CHANGE,
value,
});
const changeGroupEditorMedia = (mediaType: 'header' | 'avatar', file: File) => ({
type: GROUP_EDITOR_MEDIA_CHANGE,
mediaType,
value: file,
});
const resetGroupEditor = () => ({
type: GROUP_EDITOR_RESET,
});
const submitGroupEditor = (shouldReset?: boolean) => (dispatch: AppDispatch, getState: () => RootState) => {
const groupId = getState().group_editor.groupId;
const displayName = getState().group_editor.displayName;
const note = getState().group_editor.note;
const avatar = getState().group_editor.avatar;
const header = getState().group_editor.header;
const visibility = getState().group_editor.locked ? 'members_only' : 'everyone'; // Truth Social
const params: Record<string, any> = {
display_name: displayName,
group_visibility: visibility,
note,
};
if (avatar) params.avatar = avatar;
if (header) params.header = header;
if (groupId === null) {
return dispatch(createGroup(params, shouldReset));
} else {
return dispatch(updateGroup(groupId, params, shouldReset));
}
};
export {
GROUP_EDITOR_SET,
GROUP_CREATE_REQUEST,
GROUP_CREATE_SUCCESS,
GROUP_CREATE_FAIL,
GROUP_UPDATE_REQUEST,
GROUP_UPDATE_SUCCESS,
GROUP_UPDATE_FAIL,
GROUP_DELETE_REQUEST,
GROUP_DELETE_SUCCESS,
GROUP_DELETE_FAIL,
GROUP_FETCH_REQUEST,
GROUP_FETCH_SUCCESS,
GROUP_FETCH_FAIL,
GROUPS_FETCH_REQUEST,
GROUPS_FETCH_SUCCESS,
GROUPS_FETCH_FAIL,
GROUP_RELATIONSHIPS_FETCH_REQUEST,
GROUP_RELATIONSHIPS_FETCH_SUCCESS,
GROUP_RELATIONSHIPS_FETCH_FAIL,
GROUP_DELETE_STATUS_REQUEST,
GROUP_DELETE_STATUS_SUCCESS,
GROUP_DELETE_STATUS_FAIL,
GROUP_KICK_REQUEST,
GROUP_KICK_SUCCESS,
GROUP_KICK_FAIL,
GROUP_BLOCKS_FETCH_REQUEST,
GROUP_BLOCKS_FETCH_SUCCESS,
GROUP_BLOCKS_FETCH_FAIL,
GROUP_BLOCKS_EXPAND_REQUEST,
GROUP_BLOCKS_EXPAND_SUCCESS,
GROUP_BLOCKS_EXPAND_FAIL,
GROUP_BLOCK_REQUEST,
GROUP_BLOCK_SUCCESS,
GROUP_BLOCK_FAIL,
GROUP_UNBLOCK_REQUEST,
GROUP_UNBLOCK_SUCCESS,
GROUP_UNBLOCK_FAIL,
GROUP_PROMOTE_REQUEST,
GROUP_PROMOTE_SUCCESS,
GROUP_PROMOTE_FAIL,
GROUP_DEMOTE_REQUEST,
GROUP_DEMOTE_SUCCESS,
GROUP_DEMOTE_FAIL,
GROUP_MEMBERSHIPS_FETCH_REQUEST,
GROUP_MEMBERSHIPS_FETCH_SUCCESS,
GROUP_MEMBERSHIPS_FETCH_FAIL,
GROUP_MEMBERSHIPS_EXPAND_REQUEST,
GROUP_MEMBERSHIPS_EXPAND_SUCCESS,
GROUP_MEMBERSHIPS_EXPAND_FAIL,
GROUP_MEMBERSHIP_REQUESTS_FETCH_REQUEST,
GROUP_MEMBERSHIP_REQUESTS_FETCH_SUCCESS,
GROUP_MEMBERSHIP_REQUESTS_FETCH_FAIL,
GROUP_MEMBERSHIP_REQUESTS_EXPAND_REQUEST,
GROUP_MEMBERSHIP_REQUESTS_EXPAND_SUCCESS,
GROUP_MEMBERSHIP_REQUESTS_EXPAND_FAIL,
GROUP_MEMBERSHIP_REQUEST_AUTHORIZE_REQUEST,
GROUP_MEMBERSHIP_REQUEST_AUTHORIZE_SUCCESS,
GROUP_MEMBERSHIP_REQUEST_AUTHORIZE_FAIL,
GROUP_MEMBERSHIP_REQUEST_REJECT_REQUEST,
GROUP_MEMBERSHIP_REQUEST_REJECT_SUCCESS,
GROUP_MEMBERSHIP_REQUEST_REJECT_FAIL,
GROUP_EDITOR_TITLE_CHANGE,
GROUP_EDITOR_DESCRIPTION_CHANGE,
GROUP_EDITOR_PRIVACY_CHANGE,
GROUP_EDITOR_MEDIA_CHANGE,
GROUP_EDITOR_RESET,
editGroup,
createGroup,
createGroupRequest,
createGroupSuccess,
createGroupFail,
updateGroup,
updateGroupRequest,
updateGroupSuccess,
updateGroupFail,
deleteGroup,
deleteGroupRequest,
deleteGroupSuccess,
deleteGroupFail,
fetchGroup,
fetchGroupRequest,
fetchGroupSuccess,
fetchGroupFail,
fetchGroups,
fetchGroupsRequest,
fetchGroupsSuccess,
fetchGroupsFail,
fetchGroupRelationships,
fetchGroupRelationshipsRequest,
fetchGroupRelationshipsSuccess,
fetchGroupRelationshipsFail,
groupDeleteStatus,
groupDeleteStatusRequest,
groupDeleteStatusSuccess,
groupDeleteStatusFail,
groupKick,
groupKickRequest,
groupKickSuccess,
groupKickFail,
fetchGroupBlocks,
fetchGroupBlocksRequest,
fetchGroupBlocksSuccess,
fetchGroupBlocksFail,
expandGroupBlocks,
expandGroupBlocksRequest,
expandGroupBlocksSuccess,
expandGroupBlocksFail,
groupBlock,
groupBlockRequest,
groupBlockSuccess,
groupBlockFail,
groupUnblock,
groupUnblockRequest,
groupUnblockSuccess,
groupUnblockFail,
groupPromoteAccount,
groupPromoteAccountRequest,
groupPromoteAccountSuccess,
groupPromoteAccountFail,
groupDemoteAccount,
groupDemoteAccountRequest,
groupDemoteAccountSuccess,
groupDemoteAccountFail,
fetchGroupMemberships,
fetchGroupMembershipsRequest,
fetchGroupMembershipsSuccess,
fetchGroupMembershipsFail,
expandGroupMemberships,
expandGroupMembershipsRequest,
expandGroupMembershipsSuccess,
expandGroupMembershipsFail,
fetchGroupMembershipRequests,
fetchGroupMembershipRequestsRequest,
fetchGroupMembershipRequestsSuccess,
fetchGroupMembershipRequestsFail,
expandGroupMembershipRequests,
expandGroupMembershipRequestsRequest,
expandGroupMembershipRequestsSuccess,
expandGroupMembershipRequestsFail,
authorizeGroupMembershipRequest,
authorizeGroupMembershipRequestRequest,
authorizeGroupMembershipRequestSuccess,
authorizeGroupMembershipRequestFail,
rejectGroupMembershipRequest,
rejectGroupMembershipRequestRequest,
rejectGroupMembershipRequestSuccess,
rejectGroupMembershipRequestFail,
changeGroupEditorTitle,
changeGroupEditorDescription,
changeGroupEditorPrivacy,
changeGroupEditorMedia,
resetGroupEditor,
submitGroupEditor,
};

Wyświetl plik

@ -27,8 +27,8 @@ type ImportDataActions = {
| typeof IMPORT_BLOCKS_FAIL
| typeof IMPORT_MUTES_REQUEST
| typeof IMPORT_MUTES_SUCCESS
| typeof IMPORT_MUTES_FAIL
error?: any
| typeof IMPORT_MUTES_FAIL,
error?: any,
config?: string
}

Wyświetl plik

@ -1,8 +1,3 @@
import { importEntities } from 'soapbox/entity-store/actions';
import { Entities } from 'soapbox/entity-store/entities';
import { Group, groupSchema } from 'soapbox/schemas';
import { filteredArray } from 'soapbox/schemas/utils';
import { getSettings } from '../settings';
import type { AppDispatch, RootState } from 'soapbox/store';
@ -10,44 +5,42 @@ import type { APIEntity } from 'soapbox/types/entities';
const ACCOUNT_IMPORT = 'ACCOUNT_IMPORT';
const ACCOUNTS_IMPORT = 'ACCOUNTS_IMPORT';
const GROUP_IMPORT = 'GROUP_IMPORT';
const GROUPS_IMPORT = 'GROUPS_IMPORT';
const STATUS_IMPORT = 'STATUS_IMPORT';
const STATUSES_IMPORT = 'STATUSES_IMPORT';
const POLLS_IMPORT = 'POLLS_IMPORT';
const ACCOUNT_FETCH_FAIL_FOR_USERNAME_LOOKUP = 'ACCOUNT_FETCH_FAIL_FOR_USERNAME_LOOKUP';
const importAccount = (account: APIEntity) =>
({ type: ACCOUNT_IMPORT, account });
export function importAccount(account: APIEntity) {
return { type: ACCOUNT_IMPORT, account };
}
const importAccounts = (accounts: APIEntity[]) =>
({ type: ACCOUNTS_IMPORT, accounts });
export function importAccounts(accounts: APIEntity[]) {
return { type: ACCOUNTS_IMPORT, accounts };
}
const importGroup = (group: Group) =>
importEntities([group], Entities.GROUPS);
const importGroups = (groups: Group[]) =>
importEntities(groups, Entities.GROUPS);
const importStatus = (status: APIEntity, idempotencyKey?: string) =>
(dispatch: AppDispatch, getState: () => RootState) => {
export function importStatus(status: APIEntity, idempotencyKey?: string) {
return (dispatch: AppDispatch, getState: () => RootState) => {
const expandSpoilers = getSettings(getState()).get('expandSpoilers');
return dispatch({ type: STATUS_IMPORT, status, idempotencyKey, expandSpoilers });
};
}
const importStatuses = (statuses: APIEntity[]) =>
(dispatch: AppDispatch, getState: () => RootState) => {
export function importStatuses(statuses: APIEntity[]) {
return (dispatch: AppDispatch, getState: () => RootState) => {
const expandSpoilers = getSettings(getState()).get('expandSpoilers');
return dispatch({ type: STATUSES_IMPORT, statuses, expandSpoilers });
};
}
const importPolls = (polls: APIEntity[]) =>
({ type: POLLS_IMPORT, polls });
export function importPolls(polls: APIEntity[]) {
return { type: POLLS_IMPORT, polls };
}
const importFetchedAccount = (account: APIEntity) =>
importFetchedAccounts([account]);
export function importFetchedAccount(account: APIEntity) {
return importFetchedAccounts([account]);
}
const importFetchedAccounts = (accounts: APIEntity[], args = { should_refetch: false }) => {
export function importFetchedAccounts(accounts: APIEntity[], args = { should_refetch: false }) {
const { should_refetch } = args;
const normalAccounts: APIEntity[] = [];
@ -68,18 +61,10 @@ const importFetchedAccounts = (accounts: APIEntity[], args = { should_refetch: f
accounts.forEach(processAccount);
return importAccounts(normalAccounts);
};
}
const importFetchedGroup = (group: APIEntity) =>
importFetchedGroups([group]);
const importFetchedGroups = (groups: APIEntity[]) => {
const entities = filteredArray(groupSchema).catch([]).parse(groups);
return importGroups(entities);
};
const importFetchedStatus = (status: APIEntity, idempotencyKey?: string) =>
(dispatch: AppDispatch) => {
export function importFetchedStatus(status: APIEntity, idempotencyKey?: string) {
return (dispatch: AppDispatch) => {
// Skip broken statuses
if (isBroken(status)) return;
@ -111,13 +96,10 @@ const importFetchedStatus = (status: APIEntity, idempotencyKey?: string) =>
dispatch(importFetchedPoll(status.poll));
}
if (status.group?.id) {
dispatch(importFetchedGroup(status.group));
}
dispatch(importFetchedAccount(status.account));
dispatch(importStatus(status, idempotencyKey));
};
}
// Sometimes Pleroma can return an empty account,
// or a repost can appear of a deleted account. Skip these statuses.
@ -135,8 +117,8 @@ const isBroken = (status: APIEntity) => {
}
};
const importFetchedStatuses = (statuses: APIEntity[]) =>
(dispatch: AppDispatch, getState: () => RootState) => {
export function importFetchedStatuses(statuses: APIEntity[]) {
return (dispatch: AppDispatch, getState: () => RootState) => {
const accounts: APIEntity[] = [];
const normalStatuses: APIEntity[] = [];
const polls: APIEntity[] = [];
@ -164,10 +146,6 @@ const importFetchedStatuses = (statuses: APIEntity[]) =>
if (status.poll?.id) {
polls.push(status.poll);
}
if (status.group?.id) {
dispatch(importFetchedGroup(status.group));
}
}
statuses.forEach(processStatus);
@ -176,37 +154,23 @@ const importFetchedStatuses = (statuses: APIEntity[]) =>
dispatch(importFetchedAccounts(accounts));
dispatch(importStatuses(normalStatuses));
};
}
const importFetchedPoll = (poll: APIEntity) =>
(dispatch: AppDispatch) => {
export function importFetchedPoll(poll: APIEntity) {
return (dispatch: AppDispatch) => {
dispatch(importPolls([poll]));
};
}
const importErrorWhileFetchingAccountByUsername = (username: string) =>
({ type: ACCOUNT_FETCH_FAIL_FOR_USERNAME_LOOKUP, username });
export function importErrorWhileFetchingAccountByUsername(username: string) {
return { type: ACCOUNT_FETCH_FAIL_FOR_USERNAME_LOOKUP, username };
}
export {
ACCOUNT_IMPORT,
ACCOUNTS_IMPORT,
GROUP_IMPORT,
GROUPS_IMPORT,
STATUS_IMPORT,
STATUSES_IMPORT,
POLLS_IMPORT,
ACCOUNT_FETCH_FAIL_FOR_USERNAME_LOOKUP,
importAccount,
importAccounts,
importGroup,
importGroups,
importStatus,
importStatuses,
importPolls,
importFetchedAccount,
importFetchedAccounts,
importFetchedGroup,
importFetchedGroups,
importFetchedStatus,
importFetchedStatuses,
importFetchedPoll,
importErrorWhileFetchingAccountByUsername,
};

Wyświetl plik

@ -20,10 +20,6 @@ const FAVOURITE_REQUEST = 'FAVOURITE_REQUEST';
const FAVOURITE_SUCCESS = 'FAVOURITE_SUCCESS';
const FAVOURITE_FAIL = 'FAVOURITE_FAIL';
const DISLIKE_REQUEST = 'DISLIKE_REQUEST';
const DISLIKE_SUCCESS = 'DISLIKE_SUCCESS';
const DISLIKE_FAIL = 'DISLIKE_FAIL';
const UNREBLOG_REQUEST = 'UNREBLOG_REQUEST';
const UNREBLOG_SUCCESS = 'UNREBLOG_SUCCESS';
const UNREBLOG_FAIL = 'UNREBLOG_FAIL';
@ -32,10 +28,6 @@ const UNFAVOURITE_REQUEST = 'UNFAVOURITE_REQUEST';
const UNFAVOURITE_SUCCESS = 'UNFAVOURITE_SUCCESS';
const UNFAVOURITE_FAIL = 'UNFAVOURITE_FAIL';
const UNDISLIKE_REQUEST = 'UNDISLIKE_REQUEST';
const UNDISLIKE_SUCCESS = 'UNDISLIKE_SUCCESS';
const UNDISLIKE_FAIL = 'UNDISLIKE_FAIL';
const REBLOGS_FETCH_REQUEST = 'REBLOGS_FETCH_REQUEST';
const REBLOGS_FETCH_SUCCESS = 'REBLOGS_FETCH_SUCCESS';
const REBLOGS_FETCH_FAIL = 'REBLOGS_FETCH_FAIL';
@ -44,10 +36,6 @@ const FAVOURITES_FETCH_REQUEST = 'FAVOURITES_FETCH_REQUEST';
const FAVOURITES_FETCH_SUCCESS = 'FAVOURITES_FETCH_SUCCESS';
const FAVOURITES_FETCH_FAIL = 'FAVOURITES_FETCH_FAIL';
const DISLIKES_FETCH_REQUEST = 'DISLIKES_FETCH_REQUEST';
const DISLIKES_FETCH_SUCCESS = 'DISLIKES_FETCH_SUCCESS';
const DISLIKES_FETCH_FAIL = 'DISLIKES_FETCH_FAIL';
const REACTIONS_FETCH_REQUEST = 'REACTIONS_FETCH_REQUEST';
const REACTIONS_FETCH_SUCCESS = 'REACTIONS_FETCH_SUCCESS';
const REACTIONS_FETCH_FAIL = 'REACTIONS_FETCH_FAIL';
@ -108,7 +96,7 @@ const unreblog = (status: StatusEntity) =>
};
const toggleReblog = (status: StatusEntity) =>
(dispatch: AppDispatch) => {
(dispatch: AppDispatch, getState: () => RootState) => {
if (status.reblogged) {
dispatch(unreblog(status));
} else {
@ -181,7 +169,7 @@ const unfavourite = (status: StatusEntity) =>
};
const toggleFavourite = (status: StatusEntity) =>
(dispatch: AppDispatch) => {
(dispatch: AppDispatch, getState: () => RootState) => {
if (status.favourited) {
dispatch(unfavourite(status));
} else {
@ -227,79 +215,6 @@ const unfavouriteFail = (status: StatusEntity, error: AxiosError) => ({
skipLoading: true,
});
const dislike = (status: StatusEntity) =>
(dispatch: AppDispatch, getState: () => RootState) => {
if (!isLoggedIn(getState)) return;
dispatch(dislikeRequest(status));
api(getState).post(`/api/friendica/statuses/${status.get('id')}/dislike`).then(function() {
dispatch(dislikeSuccess(status));
}).catch(function(error) {
dispatch(dislikeFail(status, error));
});
};
const undislike = (status: StatusEntity) =>
(dispatch: AppDispatch, getState: () => RootState) => {
if (!isLoggedIn(getState)) return;
dispatch(undislikeRequest(status));
api(getState).post(`/api/friendica/statuses/${status.get('id')}/undislike`).then(() => {
dispatch(undislikeSuccess(status));
}).catch(error => {
dispatch(undislikeFail(status, error));
});
};
const toggleDislike = (status: StatusEntity) =>
(dispatch: AppDispatch) => {
if (status.disliked) {
dispatch(undislike(status));
} else {
dispatch(dislike(status));
}
};
const dislikeRequest = (status: StatusEntity) => ({
type: DISLIKE_REQUEST,
status: status,
skipLoading: true,
});
const dislikeSuccess = (status: StatusEntity) => ({
type: DISLIKE_SUCCESS,
status: status,
skipLoading: true,
});
const dislikeFail = (status: StatusEntity, error: AxiosError) => ({
type: DISLIKE_FAIL,
status: status,
error: error,
skipLoading: true,
});
const undislikeRequest = (status: StatusEntity) => ({
type: UNDISLIKE_REQUEST,
status: status,
skipLoading: true,
});
const undislikeSuccess = (status: StatusEntity) => ({
type: UNDISLIKE_SUCCESS,
status: status,
skipLoading: true,
});
const undislikeFail = (status: StatusEntity, error: AxiosError) => ({
type: UNDISLIKE_FAIL,
status: status,
error: error,
skipLoading: true,
});
const bookmark = (status: StatusEntity) =>
(dispatch: AppDispatch, getState: () => RootState) => {
dispatch(bookmarkRequest(status));
@ -436,38 +351,6 @@ const fetchFavouritesFail = (id: string, error: AxiosError) => ({
error,
});
const fetchDislikes = (id: string) =>
(dispatch: AppDispatch, getState: () => RootState) => {
if (!isLoggedIn(getState)) return;
dispatch(fetchDislikesRequest(id));
api(getState).get(`/api/friendica/statuses/${id}/disliked_by`).then(response => {
dispatch(importFetchedAccounts(response.data));
dispatch(fetchRelationships(response.data.map((item: APIEntity) => item.id)));
dispatch(fetchDislikesSuccess(id, response.data));
}).catch(error => {
dispatch(fetchDislikesFail(id, error));
});
};
const fetchDislikesRequest = (id: string) => ({
type: DISLIKES_FETCH_REQUEST,
id,
});
const fetchDislikesSuccess = (id: string, accounts: APIEntity[]) => ({
type: DISLIKES_FETCH_SUCCESS,
id,
accounts,
});
const fetchDislikesFail = (id: string, error: AxiosError) => ({
type: DISLIKES_FETCH_FAIL,
id,
error,
});
const fetchReactions = (id: string) =>
(dispatch: AppDispatch, getState: () => RootState) => {
dispatch(fetchReactionsRequest(id));
@ -615,27 +498,18 @@ export {
FAVOURITE_REQUEST,
FAVOURITE_SUCCESS,
FAVOURITE_FAIL,
DISLIKE_REQUEST,
DISLIKE_SUCCESS,
DISLIKE_FAIL,
UNREBLOG_REQUEST,
UNREBLOG_SUCCESS,
UNREBLOG_FAIL,
UNFAVOURITE_REQUEST,
UNFAVOURITE_SUCCESS,
UNFAVOURITE_FAIL,
UNDISLIKE_REQUEST,
UNDISLIKE_SUCCESS,
UNDISLIKE_FAIL,
REBLOGS_FETCH_REQUEST,
REBLOGS_FETCH_SUCCESS,
REBLOGS_FETCH_FAIL,
FAVOURITES_FETCH_REQUEST,
FAVOURITES_FETCH_SUCCESS,
FAVOURITES_FETCH_FAIL,
DISLIKES_FETCH_REQUEST,
DISLIKES_FETCH_SUCCESS,
DISLIKES_FETCH_FAIL,
REACTIONS_FETCH_REQUEST,
REACTIONS_FETCH_SUCCESS,
REACTIONS_FETCH_FAIL,
@ -672,15 +546,6 @@ export {
unfavouriteRequest,
unfavouriteSuccess,
unfavouriteFail,
dislike,
undislike,
toggleDislike,
dislikeRequest,
dislikeSuccess,
dislikeFail,
undislikeRequest,
undislikeSuccess,
undislikeFail,
bookmark,
unbookmark,
toggleBookmark,
@ -698,10 +563,6 @@ export {
fetchFavouritesRequest,
fetchFavouritesSuccess,
fetchFavouritesFail,
fetchDislikes,
fetchDislikesRequest,
fetchDislikesSuccess,
fetchDislikesFail,
fetchReactions,
fetchReactionsRequest,
fetchReactionsSuccess,

Wyświetl plik

@ -112,6 +112,27 @@ const deleteUserModal = (intl: IntlShape, accountId: string, afterConfirm = () =
}));
};
const rejectUserModal = (intl: IntlShape, accountId: string, afterConfirm = () => {}) =>
(dispatch: AppDispatch, getState: () => RootState) => {
const state = getState();
const acct = state.accounts.get(accountId)!.acct;
const name = state.accounts.get(accountId)!.username;
dispatch(openModal('CONFIRM', {
icon: require('@tabler/icons/user-off.svg'),
heading: intl.formatMessage(messages.rejectUserHeading, { acct }),
message: intl.formatMessage(messages.rejectUserPrompt, { acct }),
confirm: intl.formatMessage(messages.rejectUserConfirm, { name }),
onConfirm: () => {
dispatch(deleteUsers([accountId]))
.then(() => {
afterConfirm();
})
.catch(() => {});
},
}));
};
const toggleStatusSensitivityModal = (intl: IntlShape, statusId: string, sensitive: boolean, afterConfirm = () => {}) =>
(dispatch: AppDispatch, getState: () => RootState) => {
const state = getState();
@ -157,6 +178,7 @@ const deleteStatusModal = (intl: IntlShape, statusId: string, afterConfirm = ()
export {
deactivateUserModal,
deleteUserModal,
rejectUserModal,
toggleStatusSensitivityModal,
deleteStatusModal,
};

Wyświetl plik

@ -47,7 +47,7 @@ const MAX_QUEUED_NOTIFICATIONS = 40;
defineMessages({
mention: { id: 'notification.mention', defaultMessage: '{name} mentioned you' },
group: { id: 'notifications.group', defaultMessage: '{count, plural, one {# notification} other {# notifications}}' },
group: { id: 'notifications.group', defaultMessage: '{count} notifications' },
});
const fetchRelatedRelationships = (dispatch: AppDispatch, notifications: APIEntity[]) => {

Wyświetl plik

@ -37,8 +37,8 @@ const subscribe = (registration: ServiceWorkerRegistration, getState: () => Root
});
const unsubscribe = ({ registration, subscription }: {
registration: ServiceWorkerRegistration
subscription: PushSubscription | null
registration: ServiceWorkerRegistration,
subscription: PushSubscription | null,
}) =>
subscription ? subscription.unsubscribe().then(() => registration) : new Promise<ServiceWorkerRegistration>(r => r(registration));
@ -82,8 +82,8 @@ const register = () =>
.then(getPushSubscription)
// @ts-ignore
.then(({ registration, subscription }: {
registration: ServiceWorkerRegistration
subscription: PushSubscription | null
registration: ServiceWorkerRegistration,
subscription: PushSubscription | null,
}) => {
if (subscription !== null) {
// We have a subscription, check if it is still valid

Wyświetl plik

@ -4,7 +4,7 @@ import { openModal } from './modals';
import type { AxiosError } from 'axios';
import type { AppDispatch, RootState } from 'soapbox/store';
import type { Account, ChatMessage, Group, Status } from 'soapbox/types/entities';
import type { Account, ChatMessage, Status } from 'soapbox/types/entities';
const REPORT_INIT = 'REPORT_INIT';
const REPORT_CANCEL = 'REPORT_CANCEL';
@ -20,29 +20,19 @@ const REPORT_BLOCK_CHANGE = 'REPORT_BLOCK_CHANGE';
const REPORT_RULE_CHANGE = 'REPORT_RULE_CHANGE';
enum ReportableEntities {
ACCOUNT = 'ACCOUNT',
CHAT_MESSAGE = 'CHAT_MESSAGE',
GROUP = 'GROUP',
STATUS = 'STATUS'
}
type ReportedEntity = {
status?: Status
status?: Status,
chatMessage?: ChatMessage
group?: Group
}
const initReport = (entityType: ReportableEntities, account: Account, entities?: ReportedEntity) => (dispatch: AppDispatch) => {
const { status, chatMessage, group } = entities || {};
const initReport = (account: Account, entities?: ReportedEntity) => (dispatch: AppDispatch) => {
const { status, chatMessage } = entities || {};
dispatch({
type: REPORT_INIT,
entityType,
account,
status,
chatMessage,
group,
});
return dispatch(openModal('REPORT'));
@ -66,8 +56,7 @@ const submitReport = () =>
return api(getState).post('/api/v1/reports', {
account_id: reports.getIn(['new', 'account_id']),
status_ids: reports.getIn(['new', 'status_ids']),
message_ids: [reports.getIn(['new', 'chat_message', 'id'])].filter(Boolean),
group_id: reports.getIn(['new', 'group', 'id']),
message_ids: [reports.getIn(['new', 'chat_message', 'id'])],
rule_ids: reports.getIn(['new', 'rule_ids']),
comment: reports.getIn(['new', 'comment']),
forward: reports.getIn(['new', 'forward']),
@ -108,7 +97,6 @@ const changeReportRule = (ruleId: string) => ({
});
export {
ReportableEntities,
REPORT_INIT,
REPORT_CANCEL,
REPORT_SUBMIT_REQUEST,

Wyświetl plik

@ -1,7 +1,7 @@
import api from '../api';
import { fetchRelationships } from './accounts';
import { importFetchedAccounts, importFetchedGroups, importFetchedStatuses } from './importer';
import { importFetchedAccounts, importFetchedStatuses } from './importer';
import type { AxiosError } from 'axios';
import type { SearchFilter } from 'soapbox/reducers/search';
@ -83,10 +83,6 @@ const submitSearch = (filter?: SearchFilter) =>
dispatch(importFetchedStatuses(response.data.statuses));
}
if (response.data.groups) {
dispatch(importFetchedGroups(response.data.groups));
}
dispatch(fetchSearchSuccess(response.data, value, type));
dispatch(fetchRelationships(response.data.accounts.map((item: APIEntity) => item.id)));
}).catch(error => {
@ -143,10 +139,6 @@ const expandSearch = (type: SearchFilter) => (dispatch: AppDispatch, getState: (
dispatch(importFetchedStatuses(data.statuses));
}
if (data.groups) {
dispatch(importFetchedGroups(data.groups));
}
dispatch(expandSearchSuccess(data, value, type));
dispatch(fetchRelationships(data.accounts.map((item: APIEntity) => item.id)));
}).catch(error => {

Wyświetl plik

@ -50,7 +50,7 @@ const MOVE_ACCOUNT_FAIL = 'MOVE_ACCOUNT_FAIL';
const fetchOAuthTokens = () =>
(dispatch: AppDispatch, getState: () => RootState) => {
dispatch({ type: FETCH_TOKENS_REQUEST });
return api(getState).get('/api/oauth_tokens').then(({ data: tokens }) => {
return api(getState).get('/api/oauth_tokens.json').then(({ data: tokens }) => {
dispatch({ type: FETCH_TOKENS_SUCCESS, tokens });
}).catch(() => {
dispatch({ type: FETCH_TOKENS_FAIL });

Wyświetl plik

@ -1,10 +1,9 @@
import { Map as ImmutableMap, List as ImmutableList, OrderedSet as ImmutableOrderedSet } from 'immutable';
import { defineMessage } from 'react-intl';
import { defineMessages } from 'react-intl';
import { createSelector } from 'reselect';
import { v4 as uuid } from 'uuid';
import { patchMe } from 'soapbox/actions/me';
import messages from 'soapbox/locales/messages';
import toast from 'soapbox/toast';
import { isLoggedIn } from 'soapbox/utils/auth';
@ -19,10 +18,12 @@ const FE_NAME = 'soapbox_fe';
/** Options when changing/saving settings. */
type SettingOpts = {
/** Whether to display an alert when settings are saved. */
showAlert?: boolean
showAlert?: boolean,
}
const saveSuccessMessage = defineMessage({ id: 'settings.save.success', defaultMessage: 'Your preferences have been saved!' });
const messages = defineMessages({
saveSuccess: { id: 'settings.save.success', defaultMessage: 'Your preferences have been saved!' },
});
const defaultSettings = ImmutableMap({
onboarded: false,
@ -39,7 +40,7 @@ const defaultSettings = ImmutableMap({
defaultPrivacy: 'public',
defaultContentType: 'text/plain',
themeMode: 'system',
locale: navigator.language || 'en',
locale: navigator.language.split(/[-_]/)[0] || 'en',
showExplanationBox: true,
explanationBox: true,
autoloadTimelines: true,
@ -155,8 +156,6 @@ const defaultSettings = ImmutableMap({
}),
}),
groups: ImmutableMap({}),
trends: ImmutableMap({
show: true,
}),
@ -220,7 +219,7 @@ const saveSettingsImmediate = (opts?: SettingOpts) =>
dispatch({ type: SETTING_SAVE });
if (opts?.showAlert) {
toast.success(saveSuccessMessage);
toast.success(messages.saveSuccess);
}
}).catch(error => {
toast.showAlertForError(error);
@ -230,12 +229,6 @@ const saveSettingsImmediate = (opts?: SettingOpts) =>
const saveSettings = (opts?: SettingOpts) =>
(dispatch: AppDispatch) => dispatch(saveSettingsImmediate(opts));
const getLocale = (state: RootState, fallback = 'en') => {
const localeWithVariant = (getSettings(state).get('locale') as string).replace('_', '-');
const locale = localeWithVariant.split('-')[0];
return Object.keys(messages).includes(localeWithVariant) ? localeWithVariant : Object.keys(messages).includes(locale) ? locale : fallback;
};
export {
SETTING_CHANGE,
SETTING_SAVE,
@ -247,5 +240,4 @@ export {
changeSetting,
saveSettingsImmediate,
saveSettings,
getLocale,
};

Wyświetl plik

@ -32,8 +32,8 @@ const getSoapboxConfig = createSelector([
}
// If RGI reacts aren't supported, strip VS16s
// https://git.pleroma.social/pleroma/pleroma/-/issues/2355
if (features.emojiReactsNonRGI) {
// // https://git.pleroma.social/pleroma/pleroma/-/issues/2355
if (!features.emojiReactsRGI) {
soapboxConfig.set('allowedEmoji', soapboxConfig.allowedEmoji.map(removeVS16s));
}
});

Wyświetl plik

@ -48,8 +48,6 @@ const STATUS_TRANSLATE_SUCCESS = 'STATUS_TRANSLATE_SUCCESS';
const STATUS_TRANSLATE_FAIL = 'STATUS_TRANSLATE_FAIL';
const STATUS_TRANSLATE_UNDO = 'STATUS_TRANSLATE_UNDO';
const STATUS_UNFILTER = 'STATUS_UNFILTER';
const statusExists = (getState: () => RootState, statusId: string) => {
return (getState().statuses.get(statusId) || null) !== null;
};
@ -337,11 +335,6 @@ const undoStatusTranslation = (id: string) => ({
id,
});
const unfilterStatus = (id: string) => ({
type: STATUS_UNFILTER,
id,
});
export {
STATUS_CREATE_REQUEST,
STATUS_CREATE_SUCCESS,
@ -370,7 +363,6 @@ export {
STATUS_TRANSLATE_SUCCESS,
STATUS_TRANSLATE_FAIL,
STATUS_TRANSLATE_UNDO,
STATUS_UNFILTER,
createStatus,
editStatus,
fetchStatus,
@ -389,5 +381,4 @@ export {
toggleStatusHidden,
translateStatus,
undoStatusTranslation,
unfilterStatus,
};

Wyświetl plik

@ -1,8 +1,8 @@
import { getLocale, getSettings } from 'soapbox/actions/settings';
import { getSettings } from 'soapbox/actions/settings';
import messages from 'soapbox/locales/messages';
import { ChatKeys, IChat, isLastMessage } from 'soapbox/queries/chats';
import { queryClient } from 'soapbox/queries/client';
import { getUnreadChatsCount, updateChatListItem, updateChatMessage } from 'soapbox/utils/chats';
import { getUnreadChatsCount, updateChatListItem } from 'soapbox/utils/chats';
import { removePageItem } from 'soapbox/utils/queries';
import { play, soundCache } from 'soapbox/utils/sounds';
@ -34,6 +34,13 @@ import type { APIEntity, Chat } from 'soapbox/types/entities';
const STREAMING_CHAT_UPDATE = 'STREAMING_CHAT_UPDATE';
const STREAMING_FOLLOW_RELATIONSHIPS_UPDATE = 'STREAMING_FOLLOW_RELATIONSHIPS_UPDATE';
const validLocale = (locale: string) => Object.keys(messages).includes(locale);
const getLocale = (state: RootState) => {
const locale = getSettings(state).get('locale') as string;
return validLocale(locale) ? locale : 'en';
};
const updateFollowRelationships = (relationships: APIEntity) =>
(dispatch: AppDispatch, getState: () => RootState) => {
const me = getState().me;
@ -74,7 +81,7 @@ const updateChatQuery = (chat: IChat) => {
};
interface StreamOpts {
statContext?: IStatContext
statContext?: IStatContext,
}
const connectTimelineStream = (
@ -163,9 +170,6 @@ const connectTimelineStream = (
}
});
break;
case 'chat_message.reaction': // TruthSocial
updateChatMessage(JSON.parse(data.payload));
break;
case 'pleroma:follow_relationships_update':
dispatch(updateFollowRelationships(JSON.parse(data.payload)));
break;

Wyświetl plik

@ -219,9 +219,6 @@ const expandListTimeline = (id: string, { maxId }: Record<string, any> = {}, don
const expandGroupTimeline = (id: string, { maxId }: Record<string, any> = {}, done = noOp) =>
expandTimeline(`group:${id}`, `/api/v1/timelines/group/${id}`, { max_id: maxId }, done);
const expandGroupMediaTimeline = (id: string | number, { maxId }: Record<string, any> = {}) =>
expandTimeline(`group:${id}:media`, `/api/v1/timelines/group/${id}`, { max_id: maxId, only_media: true, limit: 40, with_muted: true });
const expandHashtagTimeline = (hashtag: string, { maxId, tags }: Record<string, any> = {}, done = noOp) => {
return expandTimeline(`hashtag:${hashtag}`, `/api/v1/timelines/tag/${hashtag}`, {
max_id: maxId,
@ -312,7 +309,6 @@ export {
expandAccountMediaTimeline,
expandListTimeline,
expandGroupTimeline,
expandGroupMediaTimeline,
expandHashtagTimeline,
expandTimelineRequest,
expandTimelineSuccess,

Wyświetl plik

@ -31,14 +31,14 @@ const AGE: Challenge = 'age';
export type Challenge = 'age' | 'sms' | 'email'
type Challenges = {
email?: 0 | 1
sms?: 0 | 1
age?: 0 | 1
email?: 0 | 1,
sms?: 0 | 1,
age?: 0 | 1,
}
type Verification = {
token?: string
challenges?: Challenges
token?: string,
challenges?: Challenges,
challengeTypes?: Array<'age' | 'sms' | 'email'>
};

Wyświetl plik

@ -23,12 +23,7 @@ export const getLinks = (response: AxiosResponse): LinkHeader => {
export const getNextLink = (response: AxiosResponse) => {
const nextLink = new LinkHeader(response.headers?.link);
return nextLink.refs.find(link => link.rel === 'next')?.uri;
};
export const getPrevLink = (response: AxiosResponse) => {
const prevLink = new LinkHeader(response.headers?.link);
return prevLink.refs.find(link => link.rel === 'prev')?.uri;
return nextLink.refs.find((ref) => ref.uri)?.uri;
};
export const baseClient = (...params: any[]) => {

Wyświetl plik

@ -29,10 +29,6 @@ export const getNextLink = (response: AxiosResponse): string | undefined => {
return getLinks(response).refs.find(link => link.rel === 'next')?.uri;
};
export const getPrevLink = (response: AxiosResponse): string | undefined => {
return getLinks(response).refs.find(link => link.rel === 'prev')?.uri;
};
const getToken = (state: RootState, authType: string) => {
return authType === 'app' ? getAppToken(state) : getAccessToken(state);
};

Wyświetl plik

@ -1,7 +1,7 @@
import React from 'react';
interface IInlineSVG {
loader?: JSX.Element
loader?: JSX.Element,
}
const InlineSVG: React.FC<IInlineSVG> = ({ loader }): JSX.Element => {

Wyświetl plik

@ -0,0 +1,16 @@
import React from 'react';
import { render, screen } from '../../jest/test-helpers';
import EmojiSelector from '../emoji-selector';
describe('<EmojiSelector />', () => {
it('renders correctly', () => {
const children = <EmojiSelector />;
// @ts-ignore
children.__proto__.addEventListener = () => {};
render(children);
expect(screen.queryAllByRole('button')).toHaveLength(6);
});
});

Wyświetl plik

@ -1,4 +1,4 @@
import clsx from 'clsx';
import classNames from 'clsx';
import React, { useState } from 'react';
import { defineMessages, useIntl } from 'react-intl';
@ -12,9 +12,9 @@ const messages = defineMessages({
interface IAccountSearch {
/** Callback when a searched account is chosen. */
onSelected: (accountId: string) => void
onSelected: (accountId: string) => void,
/** Override the default placeholder of the input. */
placeholder?: string
placeholder?: string,
}
/** Input to search for accounts. */
@ -72,17 +72,17 @@ const AccountSearch: React.FC<IAccountSearch> = ({ onSelected, ...rest }) => {
<div
role='button'
tabIndex={0}
className='absolute inset-y-0 right-0 flex cursor-pointer items-center px-3'
className='absolute inset-y-0 right-0 px-3 flex items-center cursor-pointer'
onClick={handleClear}
>
<SvgIcon
src={require('@tabler/icons/search.svg')}
className={clsx('h-4 w-4 text-gray-400', { hidden: !isEmpty() })}
className={classNames('h-4 w-4 text-gray-400', { hidden: !isEmpty() })}
/>
<SvgIcon
src={require('@tabler/icons/x.svg')}
className={clsx('h-4 w-4 text-gray-400', { hidden: isEmpty() })}
className={classNames('h-4 w-4 text-gray-400', { hidden: isEmpty() })}
aria-label={intl.formatMessage(messages.placeholder)}
/>
</div>

Wyświetl plik

@ -1,11 +1,11 @@
import React, { useRef } from 'react';
import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
import React from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { Link, useHistory } from 'react-router-dom';
import HoverRefWrapper from 'soapbox/components/hover-ref-wrapper';
import VerificationBadge from 'soapbox/components/verification-badge';
import ActionButton from 'soapbox/features/ui/components/action-button';
import { useAppSelector } from 'soapbox/hooks';
import { useAppSelector, useOnScreen } from 'soapbox/hooks';
import { getAcct } from 'soapbox/utils/accounts';
import { displayFqn } from 'soapbox/utils/state';
@ -13,13 +13,11 @@ import Badge from './badge';
import RelativeTimestamp from './relative-timestamp';
import { Avatar, Emoji, HStack, Icon, IconButton, Stack, Text } from './ui';
import type { StatusApprovalStatus } from 'soapbox/normalizers/status';
import type { Account as AccountSchema } from 'soapbox/schemas';
import type { Account as AccountEntity } from 'soapbox/types/entities';
interface IInstanceFavicon {
account: AccountEntity | AccountSchema
disabled?: boolean
account: AccountEntity,
disabled?: boolean,
}
const messages = defineMessages({
@ -44,17 +42,17 @@ const InstanceFavicon: React.FC<IInstanceFavicon> = ({ account, disabled }) => {
return (
<button
className='h-4 w-4 flex-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2'
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='max-h-full w-full' />
<img src={account.favicon} alt='' title={account.domain} className='w-full max-h-full' />
</button>
);
};
interface IProfilePopper {
condition: boolean
condition: boolean,
wrapper: (children: React.ReactNode) => React.ReactNode
children: React.ReactNode
}
@ -68,31 +66,29 @@ const ProfilePopper: React.FC<IProfilePopper> = ({ condition, wrapper, children
};
export interface IAccount {
account: AccountEntity | AccountSchema
action?: React.ReactElement
actionAlignment?: 'center' | 'top'
actionIcon?: string
actionTitle?: string
account: AccountEntity,
action?: React.ReactElement,
actionAlignment?: 'center' | 'top',
actionIcon?: string,
actionTitle?: string,
/** Override other actions for specificity like mute/unmute. */
actionType?: 'muting' | 'blocking' | 'follow_request'
avatarSize?: number
hidden?: boolean
hideActions?: boolean
id?: string
onActionClick?: (account: any) => void
showProfileHoverCard?: boolean
timestamp?: string
timestampUrl?: string
futureTimestamp?: boolean
withAccountNote?: boolean
withDate?: boolean
withLinkToProfile?: boolean
withRelationship?: boolean
showEdit?: boolean
approvalStatus?: StatusApprovalStatus
emoji?: string
emojiUrl?: string
note?: string
actionType?: 'muting' | 'blocking' | 'follow_request',
avatarSize?: number,
hidden?: boolean,
hideActions?: boolean,
id?: string,
onActionClick?: (account: any) => void,
showProfileHoverCard?: boolean,
timestamp?: string,
timestampUrl?: string,
futureTimestamp?: boolean,
withAccountNote?: boolean,
withDate?: boolean,
withLinkToProfile?: boolean,
withRelationship?: boolean,
showEdit?: boolean,
emoji?: string,
note?: string,
}
const Account = ({
@ -115,19 +111,22 @@ const Account = ({
withLinkToProfile = true,
withRelationship = true,
showEdit = false,
approvalStatus,
emoji,
emojiUrl,
note,
}: IAccount) => {
const overflowRef = useRef<HTMLDivElement>(null);
const actionRef = useRef<HTMLDivElement>(null);
const overflowRef = React.useRef<HTMLDivElement>(null);
const actionRef = React.useRef<HTMLDivElement>(null);
// @ts-ignore
const isOnScreen = useOnScreen(overflowRef);
const [style, setStyle] = React.useState<React.CSSProperties>({ visibility: 'hidden' });
const me = useAppSelector((state) => state.me);
const username = useAppSelector((state) => account ? getAcct(account, displayFqn(state)) : null);
const handleAction = () => {
onActionClick!(account);
// @ts-ignore
onActionClick(account);
};
const renderAction = () => {
@ -145,8 +144,8 @@ const Account = ({
src={actionIcon}
title={actionTitle}
onClick={handleAction}
className='bg-transparent text-gray-600 hover:text-gray-700 dark:text-gray-600 dark:hover:text-gray-500'
iconClassName='h-4 w-4'
className='bg-transparent text-gray-600 dark:text-gray-600 hover:text-gray-700 dark:hover:text-gray-500'
iconClassName='w-4 h-4'
/>
);
}
@ -160,6 +159,19 @@ const Account = ({
const intl = useIntl();
React.useEffect(() => {
const style: React.CSSProperties = {};
const actionWidth = actionRef.current?.clientWidth || 0;
if (overflowRef.current) {
style.maxWidth = overflowRef.current.clientWidth - 30 - avatarSize - actionWidth;
} else {
style.visibility = 'hidden';
}
setStyle(style);
}, [isOnScreen, overflowRef, actionRef]);
if (!account) {
return null;
}
@ -178,9 +190,9 @@ const Account = ({
const LinkEl: any = withLinkToProfile ? Link : 'div';
return (
<div data-testid='account' className='group block w-full shrink-0' ref={overflowRef}>
<div data-testid='account' className='flex-shrink-0 group block w-full' ref={overflowRef}>
<HStack alignItems={actionAlignment} justifyContent='between'>
<HStack alignItems={withAccountNote || note ? 'top' : 'center'} space={3} className='overflow-hidden'>
<HStack alignItems={withAccountNote || note ? 'top' : 'center'} space={3}>
<ProfilePopper
condition={showProfileHoverCard}
wrapper={(children) => <HoverRefWrapper className='relative' accountId={account.id} inline>{children}</HoverRefWrapper>}
@ -193,15 +205,14 @@ const Account = ({
<Avatar src={account.avatar} size={avatarSize} />
{emoji && (
<Emoji
className='absolute bottom-0 -right-1.5 h-5 w-5'
className='w-5 h-5 absolute -bottom-1.5 -right-1.5'
emoji={emoji}
src={emojiUrl}
/>
)}
</LinkEl>
</ProfilePopper>
<div className='grow overflow-hidden'>
<div className='flex-grow'>
<ProfilePopper
condition={showProfileHoverCard}
wrapper={(children) => <HoverRefWrapper accountId={account.id} inline>{children}</HoverRefWrapper>}
@ -211,7 +222,7 @@ const Account = ({
title={account.acct}
onClick={(event: React.MouseEvent) => event.stopPropagation()}
>
<HStack space={1} alignItems='center' grow>
<HStack space={1} alignItems='center' grow style={style}>
<Text
size='sm'
weight='semibold'
@ -227,7 +238,7 @@ const Account = ({
</ProfilePopper>
<Stack space={withAccountNote || note ? 1 : 0}>
<HStack alignItems='center' space={1}>
<HStack alignItems='center' space={1} style={style}>
<Text theme='muted' size='sm' direction='ltr' truncate>@{username}</Text>
{account.favicon && (
@ -248,18 +259,6 @@ const Account = ({
</>
) : null}
{approvalStatus && ['pending', 'rejected'].includes(approvalStatus) && (
<>
<Text tag='span' theme='muted' size='sm'>&middot;</Text>
<Text tag='span' theme='muted' size='sm'>
{approvalStatus === 'pending'
? <FormattedMessage id='status.approval.pending' defaultMessage='Pending approval' />
: <FormattedMessage id='status.approval.rejected' defaultMessage='Rejected' />}
</Text>
</>
)}
{showEdit ? (
<>
<Text tag='span' theme='muted' size='sm'>&middot;</Text>

Wyświetl plik

@ -15,8 +15,8 @@ const obfuscatedCount = (count: number) => {
};
interface IAnimatedNumber {
value: number
obfuscate?: boolean
value: number;
obfuscate?: boolean;
}
const AnimatedNumber: React.FC<IAnimatedNumber> = ({ value, obfuscate }) => {
@ -50,7 +50,7 @@ const AnimatedNumber: React.FC<IAnimatedNumber> = ({ value, obfuscate }) => {
return (
<TransitionMotion styles={styles} willEnter={willEnter} willLeave={willLeave}>
{items => (
<span className='relative inline-flex flex-col items-stretch overflow-hidden'>
<span className='inline-flex flex-col items-stretch relative overflow-hidden'>
{items.map(({ key, data, style }) => (
<span key={key} style={{ position: (direction * style.y) > 0 ? 'absolute' : 'static', transform: `translateY(${style.y * 100}%)` }}>{obfuscate ? obfuscatedCount(data) : <FormattedNumber value={data} />}</span>
))}

Wyświetl plik

@ -4,7 +4,7 @@ import { useHistory } from 'react-router-dom';
import type { Announcement as AnnouncementEntity, Mention as MentionEntity } from 'soapbox/types/entities';
interface IAnnouncementContent {
announcement: AnnouncementEntity
announcement: AnnouncementEntity;
}
const AnnouncementContent: React.FC<IAnnouncementContent> = ({ announcement }) => {

Wyświetl plik

@ -11,10 +11,10 @@ import type { Map as ImmutableMap } from 'immutable';
import type { Announcement as AnnouncementEntity } from 'soapbox/types/entities';
interface IAnnouncement {
announcement: AnnouncementEntity
addReaction: (id: string, name: string) => void
removeReaction: (id: string, name: string) => void
emojiMap: ImmutableMap<string, ImmutableMap<string, string>>
announcement: AnnouncementEntity;
addReaction: (id: string, name: string) => void;
removeReaction: (id: string, name: string) => void;
emojiMap: ImmutableMap<string, ImmutableMap<string, string>>;
}
const Announcement: React.FC<IAnnouncement> = ({ announcement, addReaction, removeReaction, emojiMap }) => {

Wyświetl plik

@ -1,4 +1,4 @@
import clsx from 'clsx';
import classNames from 'clsx';
import { List as ImmutableList, Map as ImmutableMap } from 'immutable';
import React, { useState } from 'react';
import { FormattedMessage } from 'react-intl';
@ -52,7 +52,7 @@ const AnnouncementsPanel = () => {
key={i}
tabIndex={0}
onClick={() => setIndex(i)}
className={clsx({
className={classNames({
'w-2 h-2 rounded-full focus:ring-primary-600 focus:ring-2 focus:ring-offset-2': true,
'bg-gray-200 hover:bg-gray-300': i !== index,
'bg-primary-600': i === index,

Wyświetl plik

@ -1,15 +1,15 @@
import React from 'react';
import unicodeMapping from 'soapbox/features/emoji/mapping';
import unicodeMapping from 'soapbox/features/emoji/emoji-unicode-mapping-light';
import { useSettings } from 'soapbox/hooks';
import { joinPublicPath } from 'soapbox/utils/static';
import type { Map as ImmutableMap } from 'immutable';
interface IEmoji {
emoji: string
emojiMap: ImmutableMap<string, ImmutableMap<string, string>>
hovered: boolean
emoji: string;
emojiMap: ImmutableMap<string, ImmutableMap<string, string>>;
hovered: boolean;
}
const Emoji: React.FC<IEmoji> = ({ emoji, emojiMap, hovered }) => {
@ -24,7 +24,7 @@ const Emoji: React.FC<IEmoji> = ({ emoji, emojiMap, hovered }) => {
return (
<img
draggable='false'
className='emojione m-0 block'
className='emojione block m-0'
alt={emoji}
title={title}
src={joinPublicPath(`packs/emoji/${filename}.svg`)}
@ -37,7 +37,7 @@ const Emoji: React.FC<IEmoji> = ({ emoji, emojiMap, hovered }) => {
return (
<img
draggable='false'
className='emojione m-0 block'
className='emojione block m-0'
alt={shortCode}
title={shortCode}
src={filename as string}

Wyświetl plik

@ -1,8 +1,8 @@
import clsx from 'clsx';
import classNames from 'clsx';
import React, { useState } from 'react';
import AnimatedNumber from 'soapbox/components/animated-number';
import unicodeMapping from 'soapbox/features/emoji/mapping';
import unicodeMapping from 'soapbox/features/emoji/emoji-unicode-mapping-light';
import Emoji from './emoji';
@ -10,12 +10,12 @@ import type { Map as ImmutableMap } from 'immutable';
import type { AnnouncementReaction } from 'soapbox/types/entities';
interface IReaction {
announcementId: string
reaction: AnnouncementReaction
emojiMap: ImmutableMap<string, ImmutableMap<string, string>>
addReaction: (id: string, name: string) => void
removeReaction: (id: string, name: string) => void
style: React.CSSProperties
announcementId: string;
reaction: AnnouncementReaction;
emojiMap: ImmutableMap<string, ImmutableMap<string, string>>;
addReaction: (id: string, name: string) => void;
removeReaction: (id: string, name: string) => void;
style: React.CSSProperties;
}
const Reaction: React.FC<IReaction> = ({ announcementId, reaction, addReaction, removeReaction, emojiMap, style }) => {
@ -43,7 +43,7 @@ const Reaction: React.FC<IReaction> = ({ announcementId, reaction, addReaction,
return (
<button
className={clsx('flex shrink-0 items-center gap-1.5 rounded-sm bg-gray-100 px-1.5 py-1 transition-colors dark:bg-primary-900', {
className={classNames('flex shrink-0 items-center gap-1.5 bg-gray-100 dark:bg-primary-900 rounded-sm px-1.5 py-1 transition-colors', {
'bg-gray-200 dark:bg-primary-800': hovered,
'bg-primary-200 dark:bg-primary-500': reaction.me,
})}

Wyświetl plik

@ -1,29 +1,30 @@
import clsx from 'clsx';
import classNames from 'clsx';
import React from 'react';
import { TransitionMotion, spring } from 'react-motion';
import EmojiPickerDropdown from 'soapbox/features/emoji/containers/emoji-picker-dropdown-container';
import { Icon } from 'soapbox/components/ui';
import EmojiPickerDropdown from 'soapbox/features/compose/components/emoji-picker/emoji-picker-dropdown';
import { useSettings } from 'soapbox/hooks';
import Reaction from './reaction';
import type { List as ImmutableList, Map as ImmutableMap } from 'immutable';
import type { Emoji, NativeEmoji } from 'soapbox/features/emoji';
import type { Emoji } from 'soapbox/components/autosuggest-emoji';
import type { AnnouncementReaction } from 'soapbox/types/entities';
interface IReactionsBar {
announcementId: string
reactions: ImmutableList<AnnouncementReaction>
emojiMap: ImmutableMap<string, ImmutableMap<string, string>>
addReaction: (id: string, name: string) => void
removeReaction: (id: string, name: string) => void
announcementId: string;
reactions: ImmutableList<AnnouncementReaction>;
emojiMap: ImmutableMap<string, ImmutableMap<string, string>>;
addReaction: (id: string, name: string) => void;
removeReaction: (id: string, name: string) => void;
}
const ReactionsBar: React.FC<IReactionsBar> = ({ announcementId, reactions, addReaction, removeReaction, emojiMap }) => {
const reduceMotion = useSettings().get('reduceMotion');
const handleEmojiPick = (data: Emoji) => {
addReaction(announcementId, (data as NativeEmoji).native.replace(/:/g, ''));
addReaction(announcementId, data.native.replace(/:/g, ''));
};
const willEnter = () => ({ scale: reduceMotion ? 1 : 0 });
@ -41,7 +42,7 @@ const ReactionsBar: React.FC<IReactionsBar> = ({ announcementId, reactions, addR
return (
<TransitionMotion styles={styles} willEnter={willEnter} willLeave={willLeave}>
{items => (
<div className={clsx('flex flex-wrap items-center gap-1', { 'reactions-bar--empty': visibleReactions.isEmpty() })}>
<div className={classNames('flex flex-wrap items-center gap-1', { 'reactions-bar--empty': visibleReactions.isEmpty() })}>
{items.map(({ key, data, style }) => (
<Reaction
key={key}
@ -54,7 +55,7 @@ const ReactionsBar: React.FC<IReactionsBar> = ({ announcementId, reactions, addR
/>
))}
{visibleReactions.size < 8 && <EmojiPickerDropdown onPickEmoji={handleEmojiPick} />}
{visibleReactions.size < 8 && <EmojiPickerDropdown onPickEmoji={handleEmojiPick} button={<Icon className='h-4 w-4 text-gray-400 hover:text-gray-600 dark:hover:text-white' src={require('@tabler/icons/plus.svg')} />} />}
</div>
)}
</TransitionMotion>

Wyświetl plik

@ -1,139 +0,0 @@
import clsx from 'clsx';
import React, { useEffect, useRef, useState } from 'react';
import { FormattedMessage } from 'react-intl';
import { HStack, IconButton, Text } from 'soapbox/components/ui';
interface IAuthorizeRejectButtons {
onAuthorize(): Promise<unknown> | unknown
onReject(): Promise<unknown> | unknown
countdown?: number
}
/** Buttons to approve or reject a pending item, usually an account. */
const AuthorizeRejectButtons: React.FC<IAuthorizeRejectButtons> = ({ onAuthorize, onReject, countdown }) => {
const [state, setState] = useState<'authorizing' | 'rejecting' | 'authorized' | 'rejected' | 'pending'>('pending');
const timeout = useRef<NodeJS.Timeout>();
function handleAction(
present: 'authorizing' | 'rejecting',
past: 'authorized' | 'rejected',
action: () => Promise<unknown> | unknown,
): void {
if (state === present) {
if (timeout.current) {
clearTimeout(timeout.current);
}
setState('pending');
} else {
const doAction = async () => {
try {
await action();
setState(past);
} catch (e) {
console.error(e);
}
};
if (typeof countdown === 'number') {
setState(present);
timeout.current = setTimeout(doAction, countdown);
} else {
doAction();
}
}
}
const handleAuthorize = async () => handleAction('authorizing', 'authorized', onAuthorize);
const handleReject = async () => handleAction('rejecting', 'rejected', onReject);
useEffect(() => {
return () => {
if (timeout.current) {
clearTimeout(timeout.current);
}
};
}, []);
switch (state) {
case 'authorized':
return (
<ActionEmblem text={<FormattedMessage id='authorize.success' defaultMessage='Approved' />} />
);
case 'rejected':
return (
<ActionEmblem text={<FormattedMessage id='reject.success' defaultMessage='Rejected' />} />
);
default:
return (
<HStack space={3} alignItems='center'>
<AuthorizeRejectButton
theme='danger'
icon={require('@tabler/icons/x.svg')}
action={handleReject}
isLoading={state === 'rejecting'}
disabled={state === 'authorizing'}
/>
<AuthorizeRejectButton
theme='primary'
icon={require('@tabler/icons/check.svg')}
action={handleAuthorize}
isLoading={state === 'authorizing'}
disabled={state === 'rejecting'}
/>
</HStack>
);
}
};
interface IActionEmblem {
text: React.ReactNode
}
const ActionEmblem: React.FC<IActionEmblem> = ({ text }) => {
return (
<div className='rounded-full bg-gray-100 px-4 py-2 dark:bg-gray-800'>
<Text theme='muted' size='sm'>
{text}
</Text>
</div>
);
};
interface IAuthorizeRejectButton {
theme: 'primary' | 'danger'
icon: string
action(): void
isLoading?: boolean
disabled?: boolean
}
const AuthorizeRejectButton: React.FC<IAuthorizeRejectButton> = ({ theme, icon, action, isLoading, disabled }) => {
return (
<div className='relative'>
<IconButton
src={isLoading ? require('@tabler/icons/player-stop-filled.svg') : icon}
onClick={action}
theme='seamless'
className={clsx('h-10 w-10 items-center justify-center border-2', {
'border-primary-500/10 hover:border-primary-500': theme === 'primary',
'border-danger-600/10 hover:border-danger-600': theme === 'danger',
})}
iconClassName={clsx('h-6 w-6', {
'text-primary-500': theme === 'primary',
'text-danger-600': theme === 'danger',
})}
disabled={disabled}
/>
{(isLoading) && (
<div
className={clsx('pointer-events-none absolute inset-0 h-10 w-10 animate-spin rounded-full border-2 border-transparent', {
'border-t-primary-500': theme === 'primary',
'border-t-danger-600': theme === 'danger',
})}
/>
)}
</div>
);
};
export { AuthorizeRejectButtons };

Wyświetl plik

@ -12,16 +12,16 @@ import type { InputThemes } from 'soapbox/components/ui/input/input';
const noOp = () => { };
interface IAutosuggestAccountInput {
onChange: React.ChangeEventHandler<HTMLInputElement>
onSelected: (accountId: string) => void
autoFocus?: boolean
value: string
limit?: number
className?: string
autoSelect?: boolean
menu?: Menu
onKeyDown?: React.KeyboardEventHandler
theme?: InputThemes
onChange: React.ChangeEventHandler<HTMLInputElement>,
onSelected: (accountId: string) => void,
autoFocus?: boolean,
value: string,
limit?: number,
className?: string,
autoSelect?: boolean,
menu?: Menu,
onKeyDown?: React.KeyboardEventHandler,
theme?: InputThemes,
}
const AutosuggestAccountInput: React.FC<IAutosuggestAccountInput> = ({

Wyświetl plik

@ -1,30 +1,38 @@
import React from 'react';
import { isCustomEmoji } from 'soapbox/features/emoji';
import unicodeMapping from 'soapbox/features/emoji/mapping';
import unicodeMapping from 'soapbox/features/emoji/emoji-unicode-mapping-light';
import { joinPublicPath } from 'soapbox/utils/static';
import type { Emoji } from 'soapbox/features/emoji';
export type Emoji = {
id: string,
custom: boolean,
imageUrl: string,
native: string,
colons: string,
}
type UnicodeMapping = {
filename: string,
}
interface IAutosuggestEmoji {
emoji: Emoji
emoji: Emoji,
}
const AutosuggestEmoji: React.FC<IAutosuggestEmoji> = ({ emoji }) => {
let url, alt;
let url;
if (isCustomEmoji(emoji)) {
if (emoji.custom) {
url = emoji.imageUrl;
alt = emoji.colons;
} else {
const mapping = unicodeMapping[emoji.native] || unicodeMapping[emoji.native.replace(/\uFE0F$/, '')];
// @ts-ignore
const mapping: UnicodeMapping = unicodeMapping[emoji.native] || unicodeMapping[emoji.native.replace(/\uFE0F$/, '')];
if (!mapping) {
return null;
}
url = joinPublicPath(`packs/emoji/${mapping.unified}.svg`);
alt = emoji.native;
url = joinPublicPath(`packs/emoji/${mapping.filename}.svg`);
}
return (
@ -32,7 +40,7 @@ const AutosuggestEmoji: React.FC<IAutosuggestEmoji> = ({ emoji }) => {
<img
className='emojione'
src={url}
alt={alt}
alt={emoji.native || emoji.colons}
/>
{emoji.colons}

Wyświetl plik

@ -1,39 +1,39 @@
import clsx from 'clsx';
import { Portal } from '@reach/portal';
import classNames from 'clsx';
import { List as ImmutableList } from 'immutable';
import React from 'react';
import ImmutablePureComponent from 'react-immutable-pure-component';
import AutosuggestEmoji from 'soapbox/components/autosuggest-emoji';
import AutosuggestEmoji, { Emoji } from 'soapbox/components/autosuggest-emoji';
import Icon from 'soapbox/components/icon';
import { Input, Portal } from 'soapbox/components/ui';
import { Input } from 'soapbox/components/ui';
import AutosuggestAccount from 'soapbox/features/compose/components/autosuggest-account';
import { isRtl } from 'soapbox/rtl';
import { textAtCursorMatchesToken } from 'soapbox/utils/suggestions';
import type { Menu, MenuItem } from 'soapbox/components/dropdown-menu';
import type { InputThemes } from 'soapbox/components/ui/input/input';
import type { Emoji } from 'soapbox/features/emoji';
export type AutoSuggestion = string | Emoji;
export interface IAutosuggestInput extends Pick<React.HTMLAttributes<HTMLInputElement>, 'onChange' | 'onKeyUp' | 'onKeyDown'> {
value: string
suggestions: ImmutableList<any>
disabled?: boolean
placeholder?: string
onSuggestionSelected: (tokenStart: number, lastToken: string | null, suggestion: AutoSuggestion) => void
onSuggestionsClearRequested: () => void
onSuggestionsFetchRequested: (token: string) => void
autoFocus: boolean
autoSelect: boolean
className?: string
id?: string
searchTokens: string[]
maxLength?: number
menu?: Menu
renderSuggestion?: React.FC<{ id: string }>
hidePortal?: boolean
theme?: InputThemes
value: string,
suggestions: ImmutableList<any>,
disabled?: boolean,
placeholder?: string,
onSuggestionSelected: (tokenStart: number, lastToken: string | null, suggestion: AutoSuggestion) => void,
onSuggestionsClearRequested: () => void,
onSuggestionsFetchRequested: (token: string) => void,
autoFocus: boolean,
autoSelect: boolean,
className?: string,
id?: string,
searchTokens: string[],
maxLength?: number,
menu?: Menu,
renderSuggestion?: React.FC<{ id: string }>,
hidePortal?: boolean,
theme?: InputThemes,
}
export default class AutosuggestInput extends ImmutablePureComponent<IAutosuggestInput> {
@ -199,7 +199,7 @@ export default class AutosuggestInput extends ImmutablePureComponent<IAutosugges
tabIndex={0}
key={key}
data-index={i}
className={clsx({
className={classNames({
'px-4 py-2.5 text-sm text-gray-700 dark:text-gray-500 hover:bg-gray-100 dark:hover:bg-gray-800 focus:bg-gray-100 dark:focus:bg-primary-800 group': true,
'bg-gray-100 dark:bg-gray-800 hover:bg-gray-100 dark:hover:bg-gray-800': i === selectedSuggestion,
})}
@ -235,7 +235,7 @@ export default class AutosuggestInput extends ImmutablePureComponent<IAutosugges
return menu.map((item, i) => (
<a
className={clsx('flex cursor-pointer items-center space-x-2 px-4 py-2.5 text-sm text-gray-700 hover:bg-gray-100 focus:bg-gray-100 dark:text-gray-500 dark:hover:bg-gray-800 dark:focus:bg-primary-800', { selected: suggestions.size - selectedSuggestion === i })}
className={classNames('flex items-center space-x-2 px-4 py-2.5 text-sm cursor-pointer text-gray-700 dark:text-gray-500 hover:bg-gray-100 dark:hover:bg-gray-800 focus:bg-gray-100 dark:focus:bg-primary-800', { selected: suggestions.size - selectedSuggestion === i })}
href='#'
role='button'
tabIndex={0}
@ -302,7 +302,7 @@ export default class AutosuggestInput extends ImmutablePureComponent<IAutosugges
<Portal key='portal'>
<div
style={this.setPortalPosition()}
className={clsx({
className={classNames({
'fixed w-full z-[1001] shadow bg-white dark:bg-gray-900 rounded-lg py-1 dark:ring-2 dark:ring-primary-700 focus:outline-none': true,
hidden: !visible,
block: visible,

Wyświetl plik

@ -19,7 +19,7 @@ export const ADDRESS_ICONS: Record<string, string> = {
};
interface IAutosuggestLocation {
id: string
id: string,
}
const AutosuggestLocation: React.FC<IAutosuggestLocation> = ({ id }) => {

Wyświetl plik

@ -1,36 +1,36 @@
import clsx from 'clsx';
import { Portal } from '@reach/portal';
import classNames from 'clsx';
import React from 'react';
import ImmutablePureComponent from 'react-immutable-pure-component';
import Textarea from 'react-textarea-autosize';
import { Portal } from 'soapbox/components/ui';
import AutosuggestAccount from 'soapbox/features/compose/components/autosuggest-account';
import { isRtl } from 'soapbox/rtl';
import { textAtCursorMatchesToken } from 'soapbox/utils/suggestions';
import AutosuggestEmoji from './autosuggest-emoji';
import AutosuggestAccount from '../features/compose/components/autosuggest-account';
import { isRtl } from '../rtl';
import AutosuggestEmoji, { Emoji } from './autosuggest-emoji';
import type { List as ImmutableList } from 'immutable';
import type { Emoji } from 'soapbox/features/emoji';
interface IAutosuggesteTextarea {
id?: string
value: string
suggestions: ImmutableList<string>
disabled: boolean
placeholder: string
onSuggestionSelected: (tokenStart: number, token: string | null, value: string | undefined) => void
onSuggestionsClearRequested: () => void
onSuggestionsFetchRequested: (token: string | number) => void
onChange: React.ChangeEventHandler<HTMLTextAreaElement>
onKeyUp?: React.KeyboardEventHandler<HTMLTextAreaElement>
onKeyDown?: React.KeyboardEventHandler<HTMLTextAreaElement>
onPaste: (files: FileList) => void
autoFocus: boolean
onFocus: () => void
onBlur?: () => void
condensed?: boolean
children: React.ReactNode
id?: string,
value: string,
suggestions: ImmutableList<string>,
disabled: boolean,
placeholder: string,
onSuggestionSelected: (tokenStart: number, token: string | null, value: string | undefined) => void,
onSuggestionsClearRequested: () => void,
onSuggestionsFetchRequested: (token: string | number) => void,
onChange: React.ChangeEventHandler<HTMLTextAreaElement>,
onKeyUp?: React.KeyboardEventHandler<HTMLTextAreaElement>,
onKeyDown?: React.KeyboardEventHandler<HTMLTextAreaElement>,
onPaste: (files: FileList) => void,
autoFocus: boolean,
onFocus: () => void,
onBlur?: () => void,
condensed?: boolean,
children: React.ReactNode,
}
class AutosuggestTextarea extends ImmutablePureComponent<IAutosuggesteTextarea> {
@ -157,8 +157,7 @@ class AutosuggestTextarea extends ImmutablePureComponent<IAutosuggesteTextarea>
if (lastTokenUpdated && !valueUpdated) {
return false;
} else {
// https://stackoverflow.com/a/35962835
return super.shouldComponentUpdate!.bind(this)(nextProps, nextState, undefined);
return super.shouldComponentUpdate!(nextProps, nextState, undefined);
}
}
@ -201,7 +200,7 @@ class AutosuggestTextarea extends ImmutablePureComponent<IAutosuggesteTextarea>
tabIndex={0}
key={key}
data-index={i}
className={clsx({
className={classNames({
'px-4 py-2.5 text-sm text-gray-700 dark:text-gray-500 hover:bg-gray-100 dark:hover:bg-gray-800 focus:bg-gray-100 dark:focus:bg-primary-800 group': true,
'bg-gray-100 dark:bg-gray-800 hover:bg-gray-100 dark:hover:bg-gray-800': i === selectedSuggestion,
})}
@ -244,7 +243,7 @@ class AutosuggestTextarea extends ImmutablePureComponent<IAutosuggesteTextarea>
<Textarea
ref={this.setTextarea}
className={clsx('w-full resize-none border-0 px-0 text-gray-800 transition-[min-height] placeholder:text-gray-600 focus:border-0 focus:shadow-none focus:ring-0 motion-reduce:transition-none dark:bg-transparent dark:text-white dark:placeholder:text-gray-600', {
className={classNames('transition-[min-height] motion-reduce:transition-none dark:bg-transparent px-0 border-0 text-gray-800 dark:text-white placeholder:text-gray-600 dark:placeholder:text-gray-600 resize-none w-full focus:shadow-none focus:border-0 focus:ring-0', {
'min-h-[40px]': condensed,
'min-h-[100px]': !condensed,
})}
@ -271,7 +270,7 @@ class AutosuggestTextarea extends ImmutablePureComponent<IAutosuggesteTextarea>
<Portal key='portal'>
<div
style={this.setPortalPosition()}
className={clsx({
className={classNames({
'fixed z-1000 shadow bg-white dark:bg-gray-900 rounded-lg py-1 space-y-0 dark:ring-2 dark:ring-primary-700 focus:outline-none': true,
hidden: suggestionsHidden || suggestions.isEmpty(),
block: !suggestionsHidden && !suggestions.isEmpty(),

Wyświetl plik

@ -1,9 +1,9 @@
import clsx from 'clsx';
import classNames from 'clsx';
import React from 'react';
interface IBadge {
title: React.ReactNode
slug: string
title: React.ReactNode,
slug: string,
}
/** Badge to display on a user's profile. */
const Badge: React.FC<IBadge> = ({ title, slug }) => {
@ -12,13 +12,13 @@ const Badge: React.FC<IBadge> = ({ title, slug }) => {
return (
<span
data-testid='badge'
className={clsx('inline-flex items-center rounded px-2 py-0.5 text-xs font-medium', {
className={classNames('inline-flex items-center px-2 py-0.5 rounded text-xs font-medium', {
'bg-fuchsia-700 text-white': slug === 'patron',
'bg-emerald-800 text-white': slug === 'badge:donor',
'bg-black text-white': slug === 'admin',
'bg-cyan-600 text-white': slug === 'moderator',
'bg-gray-100 dark:bg-gray-800 text-gray-900 dark:text-gray-100': fallback,
'bg-white/75 text-gray-900': slug === 'opaque',
'bg-white bg-opacity-75 text-gray-900': slug === 'opaque',
})}
>
{title}

Wyświetl plik

@ -15,9 +15,9 @@ const messages = defineMessages({
});
interface IBirthdayInput {
value?: string
onChange: (value: string) => void
required?: boolean
value?: string,
onChange: (value: string) => void,
required?: boolean,
}
const BirthdayInput: React.FC<IBirthdayInput> = ({ value, onChange, required }) => {
@ -56,15 +56,15 @@ const BirthdayInput: React.FC<IBirthdayInput> = ({ value, onChange, required })
nextYearButtonDisabled,
date,
}: {
decreaseMonth(): void
increaseMonth(): void
prevMonthButtonDisabled: boolean
nextMonthButtonDisabled: boolean
decreaseYear(): void
increaseYear(): void
prevYearButtonDisabled: boolean
nextYearButtonDisabled: boolean
date: Date
decreaseMonth(): void,
increaseMonth(): void,
prevMonthButtonDisabled: boolean,
nextMonthButtonDisabled: boolean,
decreaseYear(): void,
increaseYear(): void,
prevYearButtonDisabled: boolean,
nextYearButtonDisabled: boolean,
date: Date,
}) => {
return (
<div className='flex flex-col gap-2'>
@ -113,7 +113,7 @@ const BirthdayInput: React.FC<IBirthdayInput> = ({ value, onChange, required })
const handleChange = (date: Date) => onChange(date ? new Date(date.getTime() - (date.getTimezoneOffset() * 60000)).toISOString().slice(0, 10) : '');
return (
<div className='relative mt-1 rounded-md shadow-sm'>
<div className='mt-1 relative rounded-md shadow-sm'>
<BundleContainer fetchComponent={DatePicker}>
{Component => (<Component
selected={selected}

Wyświetl plik

@ -3,18 +3,18 @@ import React, { useRef, useEffect } from 'react';
interface IBlurhash {
/** Hash to render */
hash: string | null | undefined
hash: string | null | undefined,
/** Width of the blurred region in pixels. Defaults to 32. */
width?: number
width?: number,
/** Height of the blurred region in pixels. Defaults to width. */
height?: number
height?: number,
/**
* Whether dummy mode is enabled. If enabled, nothing is rendered
* and canvas left untouched.
*/
dummy?: boolean
dummy?: boolean,
/** className of the canvas element. */
className?: string
className?: string,
}
/**

Wyświetl plik

@ -5,7 +5,7 @@ import { Button, HStack, Input } from './ui';
interface ICopyableInput {
/** Text to be copied. */
value: string
value: string,
}
/** An input with copy abilities. */
@ -29,7 +29,7 @@ const CopyableInput: React.FC<ICopyableInput> = ({ value }) => {
type='text'
value={value}
className='rounded-r-none rtl:rounded-l-none rtl:rounded-r-lg'
outerClassName='grow'
outerClassName='flex-grow'
onClick={selectInput}
readOnly
/>

Wyświetl plik

@ -5,6 +5,8 @@ import { useSoapboxConfig } from 'soapbox/hooks';
import { getAcct } from '../utils/accounts';
import Icon from './icon';
import RelativeTimestamp from './relative-timestamp';
import { HStack, Text } from './ui';
import VerificationBadge from './verification-badge';
@ -13,12 +15,20 @@ import type { Account } from 'soapbox/types/entities';
interface IDisplayName {
account: Account
withSuffix?: boolean
withDate?: boolean
children?: React.ReactNode
}
const DisplayName: React.FC<IDisplayName> = ({ account, children, withSuffix = true }) => {
const DisplayName: React.FC<IDisplayName> = ({ account, children, withSuffix = true, withDate = false }) => {
const { displayFqn = false } = useSoapboxConfig();
const { verified } = account;
const { created_at: createdAt, verified } = account;
const joinedAt = createdAt ? (
<div className='account__joined-at'>
<Icon src={require('@tabler/icons/clock.svg')} />
<RelativeTimestamp timestamp={createdAt} />
</div>
) : null;
const displayName = (
<HStack space={1} alignItems='center' grow>
@ -30,6 +40,7 @@ const DisplayName: React.FC<IDisplayName> = ({ account, children, withSuffix = t
/>
{verified && <VerificationBadge />}
{withDate && joinedAt}
</HStack>
);

Wyświetl plik

@ -12,7 +12,7 @@ const messages = defineMessages({
});
interface IDomain {
domain: string
domain: string,
}
const Domain: React.FC<IDomain> = ({ domain }) => {

Wyświetl plik

@ -0,0 +1,420 @@
import classNames from 'clsx';
import { supportsPassiveEvents } from 'detect-passive-events';
import React from 'react';
import { spring } from 'react-motion';
// @ts-ignore: TODO: upgrade react-overlays. v3.1 and above have TS definitions
import Overlay from 'react-overlays/lib/Overlay';
import { withRouter, RouteComponentProps } from 'react-router-dom';
import { Counter, IconButton } from 'soapbox/components/ui';
import SvgIcon from 'soapbox/components/ui/icon/svg-icon';
import Motion from 'soapbox/features/ui/util/optional-motion';
import type { Status } from 'soapbox/types/entities';
const listenerOptions = supportsPassiveEvents ? { passive: true } : false;
let id = 0;
export interface MenuItem {
action?: React.EventHandler<React.KeyboardEvent | React.MouseEvent>,
middleClick?: React.EventHandler<React.MouseEvent>,
text: string,
href?: string,
to?: string,
newTab?: boolean,
isLogout?: boolean,
icon?: string,
count?: number,
destructive?: boolean,
meta?: string,
active?: boolean,
}
export type Menu = Array<MenuItem | null>;
interface IDropdownMenu extends RouteComponentProps {
items: Menu,
onClose: () => void,
style?: React.CSSProperties,
placement?: DropdownPlacement,
arrowOffsetLeft?: string,
arrowOffsetTop?: string,
openedViaKeyboard: boolean,
}
interface IDropdownMenuState {
mounted: boolean,
}
class DropdownMenu extends React.PureComponent<IDropdownMenu, IDropdownMenuState> {
static defaultProps: Partial<IDropdownMenu> = {
style: {},
placement: 'bottom',
};
state = {
mounted: false,
};
node: HTMLDivElement | null = null;
focusedItem: HTMLAnchorElement | null = null;
handleDocumentClick = (e: Event) => {
if (this.node && !this.node.contains(e.target as Node)) {
this.props.onClose();
}
};
componentDidMount() {
document.addEventListener('click', this.handleDocumentClick, false);
document.addEventListener('keydown', this.handleKeyDown, false);
document.addEventListener('touchend', this.handleDocumentClick, listenerOptions);
if (this.focusedItem && this.props.openedViaKeyboard) {
this.focusedItem.focus({ preventScroll: true });
}
this.setState({ mounted: true });
}
componentWillUnmount() {
document.removeEventListener('click', this.handleDocumentClick);
document.removeEventListener('keydown', this.handleKeyDown);
document.removeEventListener('touchend', this.handleDocumentClick);
}
setRef: React.RefCallback<HTMLDivElement> = c => {
this.node = c;
};
setFocusRef: React.RefCallback<HTMLAnchorElement> = c => {
this.focusedItem = c;
};
handleKeyDown = (e: KeyboardEvent) => {
if (!this.node) return;
const items = Array.from(this.node.getElementsByTagName('a'));
const index = items.indexOf(document.activeElement as any);
let element = null;
switch (e.key) {
case 'ArrowDown':
element = items[index + 1] || items[0];
break;
case 'ArrowUp':
element = items[index - 1] || items[items.length - 1];
break;
case 'Tab':
if (e.shiftKey) {
element = items[index - 1] || items[items.length - 1];
} else {
element = items[index + 1] || items[0];
}
break;
case 'Home':
element = items[0];
break;
case 'End':
element = items[items.length - 1];
break;
case 'Escape':
this.props.onClose();
break;
}
if (element) {
element.focus();
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'));
const item = this.props.items[i];
if (!item) return;
const { action, to } = item;
this.props.onClose();
e.stopPropagation();
if (to) {
e.preventDefault();
this.props.history.push(to);
} else if (typeof action === 'function') {
e.preventDefault();
action(e);
}
};
handleMiddleClick: React.EventHandler<React.MouseEvent> = e => {
const i = Number(e.currentTarget.getAttribute('data-index'));
const item = this.props.items[i];
if (!item) return;
const { middleClick } = item;
this.props.onClose();
if (e.button === 1 && typeof middleClick === 'function') {
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) {
return <li key={`sep-${i}`} className='dropdown-menu__separator' />;
}
const { text, href, to, newTab, isLogout, icon, count, destructive } = option;
return (
<li className={classNames('dropdown-menu__item truncate', { destructive })} key={`${text}-${i}`}>
<a
href={href || to || '#'}
role='button'
tabIndex={0}
ref={i === 0 ? this.setFocusRef : null}
onClick={this.handleClick}
onAuxClick={this.handleAuxClick}
onKeyPress={this.handleItemKeyPress}
data-index={i}
target={newTab ? '_blank' : undefined}
data-method={isLogout ? 'delete' : undefined}
title={text}
>
{icon && <SvgIcon src={icon} className='mr-3 rtl:ml-3 rtl:mr-0 h-5 w-5 flex-none' />}
<span className='truncate'>{text}</span>
{count ? (
<span className='ml-auto h-5 w-5 flex-none'>
<Counter count={count} />
</span>
) : null}
</a>
</li>
);
}
render() {
const { items, style, placement, arrowOffsetLeft, arrowOffsetTop } = this.props;
const { mounted } = this.state;
return (
<Motion defaultStyle={{ opacity: 0, scaleX: 1, scaleY: 1 }} style={{ opacity: spring(1, { damping: 35, stiffness: 400 }), scaleX: spring(1, { damping: 35, stiffness: 400 }), scaleY: spring(1, { damping: 35, stiffness: 400 }) }}>
{({ opacity, scaleX, scaleY }) => (
// It should not be transformed when mounting because the resulting
// size will be used to determine the coordinate of the menu by
// react-overlays
<div
className={`dropdown-menu ${placement}`}
style={{ ...style, opacity: opacity, transform: mounted ? `scale(${scaleX}, ${scaleY})` : undefined }}
ref={this.setRef}
data-testid='dropdown-menu'
>
<div className={`dropdown-menu__arrow ${placement}`} style={{ left: arrowOffsetLeft, top: arrowOffsetTop }} />
<ul>
{items.map((option, i) => this.renderItem(option, i))}
</ul>
</div>
)}
</Motion>
);
}
}
const RouterDropdownMenu = withRouter(DropdownMenu);
export interface IDropdown extends RouteComponentProps {
icon?: string,
src?: string,
items: Menu,
size?: number,
active?: boolean,
pressed?: boolean,
title?: string,
disabled?: boolean,
status?: Status,
isUserTouching?: () => boolean,
isModalOpen?: boolean,
onOpen?: (
id: number,
onItemClick: React.EventHandler<React.MouseEvent | React.KeyboardEvent>,
dropdownPlacement: DropdownPlacement,
keyboard: boolean,
) => void,
onClose?: (id: number) => void,
dropdownPlacement?: string,
openDropdownId?: number | null,
openedViaKeyboard?: boolean,
text?: string,
onShiftClick?: React.EventHandler<React.MouseEvent | React.KeyboardEvent>,
children?: JSX.Element,
dropdownMenuStyle?: React.CSSProperties,
}
interface IDropdownState {
id: number,
open: boolean,
}
export type DropdownPlacement = 'top' | 'bottom';
class Dropdown extends React.PureComponent<IDropdown, IDropdownState> {
static defaultProps: Partial<IDropdown> = {
title: 'Menu',
};
state = {
id: id++,
open: false,
};
target: HTMLButtonElement | null = null;
activeElement: Element | null = null;
handleClick: React.EventHandler<React.MouseEvent<HTMLButtonElement> | React.KeyboardEvent<HTMLButtonElement>> = e => {
const { onOpen, onShiftClick, openDropdownId } = this.props;
e.stopPropagation();
if (onShiftClick && e.shiftKey) {
e.preventDefault();
onShiftClick(e);
} else if (this.state.id === openDropdownId) {
this.handleClose();
} else if (onOpen) {
const { top } = e.currentTarget.getBoundingClientRect();
const placement: DropdownPlacement = top * 2 < innerHeight ? 'bottom' : 'top';
onOpen(this.state.id, this.handleItemClick, placement, e.type !== 'click');
}
};
handleClose = () => {
if (this.activeElement && this.activeElement === this.target) {
(this.activeElement as HTMLButtonElement).focus();
this.activeElement = null;
}
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) {
case ' ':
case 'Enter':
this.handleMouseDown(e);
break;
}
};
handleKeyPress: React.EventHandler<React.KeyboardEvent<HTMLButtonElement>> = (e) => {
switch (e.key) {
case ' ':
case 'Enter':
this.handleClick(e);
e.stopPropagation();
e.preventDefault();
break;
}
};
handleItemClick: React.EventHandler<React.MouseEvent> = e => {
const i = Number(e.currentTarget.getAttribute('data-index'));
const item = this.props.items[i];
if (!item) return;
const { action, to } = item;
this.handleClose();
e.preventDefault();
e.stopPropagation();
if (typeof action === 'function') {
action(e);
} 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;
const open = this.state.id === openDropdownId;
return (
<>
{children ? (
React.cloneElement(children, {
disabled,
onClick: this.handleClick,
onMouseDown: this.handleMouseDown,
onKeyDown: this.handleButtonKeyDown,
onKeyPress: this.handleKeyPress,
ref: this.setTargetRef,
})
) : (
<IconButton
disabled={disabled}
className={classNames({
'text-gray-600 hover:text-gray-700 dark:hover:text-gray-500': true,
'text-gray-700 dark:text-gray-500': open,
})}
title={title}
src={src}
aria-pressed={pressed}
text={text}
onClick={this.handleClick}
onMouseDown={this.handleMouseDown}
onKeyDown={this.handleButtonKeyDown}
onKeyPress={this.handleKeyPress}
ref={this.setTargetRef}
/>
)}
<Overlay show={open} placement={dropdownPlacement} target={this.findTarget}>
<RouterDropdownMenu items={items} onClose={this.handleClose} openedViaKeyboard={openedViaKeyboard} style={dropdownMenuStyle} />
</Overlay>
</>
);
}
}
export default withRouter(Dropdown);

Wyświetl plik

@ -1,109 +0,0 @@
import clsx from 'clsx';
import React, { useEffect, useRef } from 'react';
import { useHistory } from 'react-router-dom';
import { Counter, Icon } from '../ui';
export interface MenuItem {
action?: React.EventHandler<React.KeyboardEvent | React.MouseEvent>
active?: boolean
count?: number
destructive?: boolean
href?: string
icon?: string
meta?: string
middleClick?(event: React.MouseEvent): void
target?: React.HTMLAttributeAnchorTarget
text: string
to?: string
}
interface IDropdownMenuItem {
index: number
item: MenuItem | null
onClick?(): void
}
const DropdownMenuItem = ({ index, item, onClick }: IDropdownMenuItem) => {
const history = useHistory();
const itemRef = useRef<HTMLAnchorElement>(null);
const handleClick: React.EventHandler<React.MouseEvent | React.KeyboardEvent> = (event) => {
event.stopPropagation();
if (!item) return;
if (onClick) onClick();
if (item.to) {
event.preventDefault();
history.push(item.to);
} else if (typeof item.action === 'function') {
event.preventDefault();
item.action(event);
}
};
const handleAuxClick: React.EventHandler<React.MouseEvent> = (event) => {
if (!item) return;
if (onClick) onClick();
if (event.button === 1 && item.middleClick) {
item.middleClick(event);
}
};
const handleItemKeyPress: React.EventHandler<React.KeyboardEvent> = (event) => {
if (event.key === 'Enter' || event.key === ' ') {
handleClick(event);
}
};
useEffect(() => {
const firstItem = index === 0;
if (itemRef.current && firstItem) {
itemRef.current.focus({ preventScroll: true });
}
}, [itemRef.current, index]);
if (item === null) {
return <li className='my-1 mx-2 h-[2px] bg-gray-100 dark:bg-gray-800' />;
}
return (
<li className='truncate focus-visible:ring-2 focus-visible:ring-primary-500'>
<a
href={item.href || item.to || '#'}
role='button'
tabIndex={0}
ref={itemRef}
data-index={index}
onClick={handleClick}
onAuxClick={handleAuxClick}
onKeyPress={handleItemKeyPress}
target={item.target}
title={item.text}
className={
clsx({
'flex px-4 py-2.5 text-sm text-gray-700 dark:text-gray-500 hover:bg-gray-100 dark:hover:bg-gray-800 focus:outline-none cursor-pointer': true,
'text-danger-600 dark:text-danger-400': item.destructive,
})
}
>
{item.icon && <Icon src={item.icon} className='mr-3 h-5 w-5 flex-none rtl:ml-3 rtl:mr-0' />}
<span className='truncate'>{item.text}</span>
{item.count ? (
<span className='ml-auto h-5 w-5 flex-none'>
<Counter count={item.count} />
</span>
) : null}
</a>
</li>
);
};
export default DropdownMenuItem;

Wyświetl plik

@ -1,346 +0,0 @@
import { offset, Placement, useFloating, flip, arrow } from '@floating-ui/react';
import clsx from 'clsx';
import { supportsPassiveEvents } from 'detect-passive-events';
import React, { useEffect, useMemo, useRef, useState } from 'react';
import { useHistory } from 'react-router-dom';
import {
closeDropdownMenu as closeDropdownMenuRedux,
openDropdownMenu,
} from 'soapbox/actions/dropdown-menu';
import { closeModal, openModal } from 'soapbox/actions/modals';
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
import { isUserTouching } from 'soapbox/is-mobile';
import { IconButton, Portal } from '../ui';
import DropdownMenuItem, { MenuItem } from './dropdown-menu-item';
import type { Status } from 'soapbox/types/entities';
export type Menu = Array<MenuItem | null>;
interface IDropdownMenu {
children?: React.ReactElement
disabled?: boolean
items: Menu
onClose?: () => void
onOpen?: () => void
onShiftClick?: React.EventHandler<React.MouseEvent | React.KeyboardEvent>
placement?: Placement
src?: string
status?: Status
title?: string
}
const listenerOptions = supportsPassiveEvents ? { passive: true } : false;
const DropdownMenu = (props: IDropdownMenu) => {
const {
children,
disabled,
items,
onClose,
onOpen,
onShiftClick,
placement: initialPlacement = 'top',
src = require('@tabler/icons/dots.svg'),
title = 'Menu',
...filteredProps
} = props;
const dispatch = useAppDispatch();
const history = useHistory();
const [isOpen, setIsOpen] = useState<boolean>(false);
const isOpenRedux = useAppSelector(state => state.dropdown_menu.isOpen);
const arrowRef = useRef<HTMLDivElement>(null);
const activeElement = useRef<Element | null>(null);
const isOnMobile = isUserTouching();
const { x, y, strategy, refs, middlewareData, placement } = useFloating<HTMLButtonElement>({
placement: initialPlacement,
middleware: [
offset(12),
flip(),
arrow({
element: arrowRef,
}),
],
});
const handleClick: React.EventHandler<
React.MouseEvent<HTMLButtonElement> | React.KeyboardEvent<HTMLButtonElement>
> = (event) => {
event.stopPropagation();
if (onShiftClick && event.shiftKey) {
event.preventDefault();
onShiftClick(event);
return;
}
if (isOpen) {
handleClose();
} else {
handleOpen();
}
};
/**
* On mobile screens, let's replace the Popper dropdown with a Modal.
*/
const handleOpen = () => {
if (isOnMobile) {
dispatch(
openModal('ACTIONS', {
status: filteredProps.status,
actions: items,
onClick: handleItemClick,
}),
);
} else {
dispatch(openDropdownMenu());
setIsOpen(true);
}
if (onOpen) {
onOpen();
}
};
const handleClose = () => {
if (activeElement.current && activeElement.current === refs.reference.current) {
(activeElement.current as any).focus();
activeElement.current = null;
}
if (isOnMobile) {
dispatch(closeModal('ACTIONS'));
} else {
closeDropdownMenu();
setIsOpen(false);
}
if (onClose) {
onClose();
}
};
const closeDropdownMenu = () => {
if (isOpenRedux) {
dispatch(closeDropdownMenuRedux());
}
};
const handleMouseDown: React.EventHandler<React.MouseEvent | React.KeyboardEvent> = () => {
if (!isOpen) {
activeElement.current = document.activeElement;
}
};
const handleButtonKeyDown: React.EventHandler<React.KeyboardEvent> = (event) => {
switch (event.key) {
case ' ':
case 'Enter':
handleMouseDown(event);
break;
}
};
const handleKeyPress: React.EventHandler<React.KeyboardEvent<HTMLButtonElement>> = (event) => {
switch (event.key) {
case ' ':
case 'Enter':
event.stopPropagation();
event.preventDefault();
handleClick(event);
break;
}
};
const handleItemClick: React.EventHandler<React.MouseEvent> = (event) => {
event.preventDefault();
event.stopPropagation();
const i = Number(event.currentTarget.getAttribute('data-index'));
const item = items[i];
if (!item) return;
const { action, to } = item;
handleClose();
if (typeof action === 'function') {
action(event);
} else if (to) {
history.push(to);
}
};
const handleDocumentClick = (event: Event) => {
if (refs.floating.current && !refs.floating.current.contains(event.target as Node)) {
handleClose();
}
};
const handleKeyDown = (e: KeyboardEvent) => {
if (!refs.floating.current) return;
const items = Array.from(refs.floating.current.getElementsByTagName('a'));
const index = items.indexOf(document.activeElement as any);
let element = null;
switch (e.key) {
case 'ArrowDown':
element = items[index + 1] || items[0];
break;
case 'ArrowUp':
element = items[index - 1] || items[items.length - 1];
break;
case 'Tab':
if (e.shiftKey) {
element = items[index - 1] || items[items.length - 1];
} else {
element = items[index + 1] || items[0];
}
break;
case 'Home':
element = items[0];
break;
case 'End':
element = items[items.length - 1];
break;
case 'Escape':
handleClose();
break;
}
if (element) {
element.focus();
e.preventDefault();
e.stopPropagation();
}
};
const arrowProps: React.CSSProperties = useMemo(() => {
if (middlewareData.arrow) {
const { x, y } = middlewareData.arrow;
const staticPlacement = {
top: 'bottom',
right: 'left',
bottom: 'top',
left: 'right',
}[placement.split('-')[0]];
return {
left: x !== null ? `${x}px` : '',
top: y !== null ? `${y}px` : '',
// Ensure the static side gets unset when
// flipping to other placements' axes.
right: '',
bottom: '',
[staticPlacement as string]: `${(-(arrowRef.current?.offsetWidth || 0)) / 2}px`,
transform: 'rotate(45deg)',
};
}
return {};
}, [middlewareData.arrow, placement]);
useEffect(() => {
return () => {
closeDropdownMenu();
};
}, []);
useEffect(() => {
document.addEventListener('click', handleDocumentClick, false);
document.addEventListener('keydown', handleKeyDown, false);
document.addEventListener('touchend', handleDocumentClick, listenerOptions);
return () => {
document.removeEventListener('click', handleDocumentClick);
document.removeEventListener('keydown', handleKeyDown);
document.removeEventListener('touchend', handleDocumentClick);
};
}, [refs.floating.current]);
if (items.length === 0) {
return null;
}
return (
<>
{children ? (
React.cloneElement(children, {
disabled,
onClick: handleClick,
onMouseDown: handleMouseDown,
onKeyDown: handleButtonKeyDown,
onKeyPress: handleKeyPress,
ref: refs.setReference,
})
) : (
<IconButton
disabled={disabled}
className={clsx({
'text-gray-600 hover:text-gray-700 dark:hover:text-gray-500': true,
'text-gray-700 dark:text-gray-500': isOpen,
})}
title={title}
src={src}
onClick={handleClick}
onMouseDown={handleMouseDown}
onKeyDown={handleButtonKeyDown}
onKeyPress={handleKeyPress}
ref={refs.setReference}
/>
)}
{isOpen ? (
<Portal>
<div
data-testid='dropdown-menu'
ref={refs.setFloating}
className={
clsx('z-[1001] w-56 rounded-md bg-white py-1 shadow-lg transition-opacity duration-100 focus:outline-none dark:bg-gray-900 dark:ring-2 dark:ring-primary-700', {
'opacity-0 pointer-events-none': !isOpen,
})
}
style={{
position: strategy,
top: y ?? 0,
left: x ?? 0,
}}
>
<ul>
{items.map((item, idx) => (
<DropdownMenuItem
key={idx}
item={item}
index={idx}
onClick={handleClose}
/>
))}
</ul>
{/* Arrow */}
<div
ref={arrowRef}
style={arrowProps}
className='pointer-events-none absolute z-[-1] h-3 w-3 bg-white dark:bg-gray-900'
/>
</div>
</Portal>
) : null}
</>
);
};
export default DropdownMenu;

Wyświetl plik

@ -1,3 +0,0 @@
export { default } from './dropdown-menu';
export type { Menu } from './dropdown-menu';
export type { MenuItem } from './dropdown-menu-item';

Wyświetl plik

@ -1,19 +1,21 @@
import classNames from 'clsx';
import React, { useState, useEffect, useRef } from 'react';
import { usePopper } from 'react-popper';
import { simpleEmojiReact } from 'soapbox/actions/emoji-reacts';
import { openModal } from 'soapbox/actions/modals';
import { EmojiSelector, Portal } from 'soapbox/components/ui';
import { EmojiSelector } from 'soapbox/components/ui';
import { useAppDispatch, useAppSelector, useOwnAccount, useSoapboxConfig } from 'soapbox/hooks';
import { isUserTouching } from 'soapbox/is-mobile';
import { getReactForStatus } from 'soapbox/utils/emoji-reacts';
interface IStatusReactionWrapper {
statusId: string
children: JSX.Element
interface IEmojiButtonWrapper {
statusId: string,
children: JSX.Element,
}
/** Provides emoji reaction functionality to the underlying button component */
const StatusReactionWrapper: React.FC<IStatusReactionWrapper> = ({ statusId, children }): JSX.Element | null => {
const EmojiButtonWrapper: React.FC<IEmojiButtonWrapper> = ({ statusId, children }): JSX.Element | null => {
const dispatch = useAppDispatch();
const ownAccount = useOwnAccount();
const status = useAppSelector(state => state.statuses.get(statusId));
@ -21,8 +23,24 @@ const StatusReactionWrapper: React.FC<IStatusReactionWrapper> = ({ statusId, chi
const timeout = useRef<NodeJS.Timeout>();
const [visible, setVisible] = useState(false);
// const [focused, setFocused] = useState(false);
// `useRef` won't trigger a re-render, while `useState` does.
// https://popper.js.org/react-popper/v2/
const [referenceElement, setReferenceElement] = useState<HTMLDivElement | null>(null);
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
const { styles, attributes } = usePopper(referenceElement, popperElement, {
placement: 'top-start',
modifiers: [
{
name: 'offset',
options: {
offset: [-10, 0],
},
},
],
});
useEffect(() => {
return () => {
@ -60,9 +78,9 @@ const StatusReactionWrapper: React.FC<IStatusReactionWrapper> = ({ statusId, chi
}
};
const handleReact = (emoji: string, custom?: string): void => {
const handleReact = (emoji: string): void => {
if (ownAccount) {
dispatch(simpleEmojiReact(status, emoji, custom));
dispatch(simpleEmojiReact(status, emoji));
} else {
handleUnauthorized();
}
@ -71,7 +89,7 @@ const StatusReactionWrapper: React.FC<IStatusReactionWrapper> = ({ statusId, chi
};
const handleClick: React.EventHandler<React.MouseEvent> = e => {
const meEmojiReact = getReactForStatus(status, soapboxConfig.allowedEmoji)?.get('name') || '👍';
const meEmojiReact = getReactForStatus(status, soapboxConfig.allowedEmoji) || '👍';
if (isUserTouching()) {
if (ownAccount) {
@ -98,6 +116,28 @@ const StatusReactionWrapper: React.FC<IStatusReactionWrapper> = ({ statusId, chi
}));
};
// const handleUnfocus: React.EventHandler<React.KeyboardEvent> = () => {
// setFocused(false);
// };
const selector = (
<div
className={classNames('z-50 transition-opacity duration-100', {
'opacity-0 pointer-events-none': !visible,
})}
ref={setPopperElement}
style={styles.popper}
{...attributes.popper}
>
<EmojiSelector
emojis={soapboxConfig.allowedEmoji}
onReact={handleReact}
// focused={focused}
// onUnfocus={handleUnfocus}
/>
</div>
);
return (
<div className='relative' onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave}>
{React.cloneElement(children, {
@ -105,19 +145,9 @@ const StatusReactionWrapper: React.FC<IStatusReactionWrapper> = ({ statusId, chi
ref: setReferenceElement,
})}
{visible && (
<Portal>
<EmojiSelector
placement='top-start'
referenceElement={referenceElement}
onReact={handleReact}
visible={visible}
onClose={() => setVisible(false)}
/>
</Portal>
)}
{selector}
</div>
);
};
export default StatusReactionWrapper;
export default EmojiButtonWrapper;

Wyświetl plik

@ -0,0 +1,142 @@
// import classNames from 'clsx';
import React from 'react';
import { HotKeys } from 'react-hotkeys';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { connect } from 'react-redux';
import { getSoapboxConfig } from 'soapbox/actions/soapbox';
import { EmojiSelector as RealEmojiSelector } from 'soapbox/components/ui';
import type { List as ImmutableList } from 'immutable';
import type { RootState } from 'soapbox/store';
const mapStateToProps = (state: RootState) => ({
allowedEmoji: getSoapboxConfig(state).allowedEmoji,
});
interface IEmojiSelector {
allowedEmoji: ImmutableList<string>,
onReact: (emoji: string) => void,
onUnfocus: () => void,
visible: boolean,
focused?: boolean,
}
class EmojiSelector extends ImmutablePureComponent<IEmojiSelector> {
static defaultProps: Partial<IEmojiSelector> = {
onReact: () => { },
onUnfocus: () => { },
visible: false,
};
node?: HTMLDivElement = undefined;
handleBlur: React.FocusEventHandler<HTMLDivElement> = e => {
const { focused, onUnfocus } = this.props;
if (focused && (!e.currentTarget || !e.currentTarget.classList.contains('emoji-react-selector__emoji'))) {
onUnfocus();
}
};
_selectPreviousEmoji = (i: number): void => {
if (!this.node) return;
if (i !== 0) {
const button: HTMLButtonElement | null = this.node.querySelector(`.emoji-react-selector__emoji:nth-child(${i})`);
button?.focus();
} else {
const button: HTMLButtonElement | null = this.node.querySelector('.emoji-react-selector__emoji:last-child');
button?.focus();
}
};
_selectNextEmoji = (i: number) => {
if (!this.node) return;
if (i !== this.props.allowedEmoji.size - 1) {
const button: HTMLButtonElement | null = this.node.querySelector(`.emoji-react-selector__emoji:nth-child(${i + 2})`);
button?.focus();
} else {
const button: HTMLButtonElement | null = this.node.querySelector('.emoji-react-selector__emoji:first-child');
button?.focus();
}
};
handleKeyDown = (i: number): React.KeyboardEventHandler => e => {
const { onUnfocus } = this.props;
switch (e.key) {
case 'Tab':
e.preventDefault();
if (e.shiftKey) this._selectPreviousEmoji(i);
else this._selectNextEmoji(i);
break;
case 'Left':
case 'ArrowLeft':
this._selectPreviousEmoji(i);
break;
case 'Right':
case 'ArrowRight':
this._selectNextEmoji(i);
break;
case 'Escape':
onUnfocus();
break;
}
};
handleReact = (emoji: string) => (): void => {
const { onReact, focused, onUnfocus } = this.props;
onReact(emoji);
if (focused) {
onUnfocus();
}
};
handlers = {
open: () => { },
};
setRef = (c: HTMLDivElement): void => {
this.node = c;
};
render() {
const { visible, focused, allowedEmoji, onReact } = this.props;
return (
<HotKeys handlers={this.handlers}>
{/*<div
className={classNames('flex absolute bg-white dark:bg-gray-500 px-2 py-3 rounded-full shadow-md opacity-0 pointer-events-none duration-100 w-max', { 'opacity-100 pointer-events-auto z-[999]': visible || focused })}
onBlur={this.handleBlur}
ref={this.setRef}
>
{allowedEmoji.map((emoji, i) => (
<button
key={i}
className='emoji-react-selector__emoji'
onClick={this.handleReact(emoji)}
onKeyDown={this.handleKeyDown(i)}
tabIndex={(visible || focused) ? 0 : -1}
>
<Emoji emoji={emoji} />
</button>
))}
</div>*/}
<RealEmojiSelector
emojis={allowedEmoji.toArray()}
onReact={onReact}
visible={visible}
focused={focused}
/>
</HotKeys>
);
}
}
export default connect(mapStateToProps)(EmojiSelector);

Wyświetl plik

@ -31,10 +31,10 @@ interface Props extends ReturnType<typeof mapStateToProps> {
}
type State = {
hasError: boolean
error: any
componentStack: any
browser?: Bowser.Parser.Parser
hasError: boolean,
error: any,
componentStack: any,
browser?: Bowser.Parser.Parser,
}
class ErrorBoundary extends React.PureComponent<Props, State> {
@ -113,17 +113,17 @@ class ErrorBoundary extends React.PureComponent<Props, State> {
const errorText = this.getErrorText();
return (
<div className='flex h-screen flex-col bg-white pt-16 pb-12 dark:bg-primary-900'>
<main className='mx-auto flex w-full max-w-7xl grow flex-col justify-center px-4 sm:px-6 lg:px-8'>
<div className='flex shrink-0 justify-center'>
<div className='h-screen pt-16 pb-12 flex flex-col bg-white dark:bg-primary-900'>
<main className='flex-grow flex flex-col justify-center max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8'>
<div className='flex-shrink-0 flex justify-center'>
<a href='/' className='inline-flex'>
<SiteLogo alt='Logo' className='h-12 w-auto cursor-pointer' />
</a>
</div>
<div className='py-8'>
<div className='mx-auto max-w-xl space-y-2 text-center'>
<h1 className='text-3xl font-extrabold tracking-tight text-gray-900 dark:text-gray-500 sm:text-4xl'>
<div className='text-center max-w-xl mx-auto space-y-2'>
<h1 className='text-3xl font-extrabold text-gray-900 dark:text-gray-500 tracking-tight sm:text-4xl'>
<FormattedMessage id='alert.unexpected.message' defaultMessage='Something went wrong.' />
</h1>
<p className='text-lg text-gray-700 dark:text-gray-600'>
@ -132,7 +132,7 @@ class ErrorBoundary extends React.PureComponent<Props, State> {
defaultMessage="We're sorry for the interruption. If the problem persists, please reach out to our support team. You may also try to {clearCookies} (this will log you out)."
values={{
clearCookies: (
<a href='/' onClick={this.clearCookies} className='text-primary-600 hover:underline dark:text-accent-blue'>
<a href='/' onClick={this.clearCookies} className='text-primary-600 dark:text-accent-blue hover:underline'>
<FormattedMessage
id='alert.unexpected.clear_cookies'
defaultMessage='clear cookies and browser data'
@ -150,7 +150,7 @@ class ErrorBoundary extends React.PureComponent<Props, State> {
</Text>
<div className='mt-10'>
<a href='/' className='text-base font-medium text-primary-600 hover:underline dark:text-accent-blue'>
<a href='/' className='text-base font-medium text-primary-600 dark:text-accent-blue hover:underline'>
<FormattedMessage id='alert.unexpected.return_home' defaultMessage='Return Home' />
<span aria-hidden='true'> &rarr;</span>
</a>
@ -158,11 +158,11 @@ class ErrorBoundary extends React.PureComponent<Props, State> {
</div>
{!isProduction && (
<div className='mx-auto max-w-lg space-y-4 py-16'>
<div className='py-16 max-w-lg mx-auto space-y-4'>
{errorText && (
<textarea
ref={this.setTextareaRef}
className='block h-48 w-full rounded-md border-gray-300 bg-gray-100 p-4 font-mono text-gray-900 shadow-sm focus:border-primary-500 focus:ring-2 focus:ring-primary-500 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100 sm:text-sm'
className='h-48 p-4 shadow-sm bg-gray-100 text-gray-900 dark:text-gray-100 dark:bg-gray-800 focus:ring-2 focus:ring-primary-500 focus:border-primary-500 block w-full sm:text-sm border-gray-300 dark:border-gray-700 rounded-md font-mono'
value={errorText}
onClick={this.handleCopy}
readOnly
@ -180,11 +180,11 @@ class ErrorBoundary extends React.PureComponent<Props, State> {
</div>
</main>
<footer className='mx-auto w-full max-w-7xl shrink-0 px-4 sm:px-6 lg:px-8'>
<footer className='flex-shrink-0 max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8'>
<HStack justifyContent='center' space={4} element='nav'>
{links.get('status') && (
<>
<a href={links.get('status')} className='text-sm font-medium text-gray-700 hover:underline dark:text-gray-600'>
<a href={links.get('status')} className='text-sm font-medium text-gray-700 dark:text-gray-600 hover:underline'>
<FormattedMessage id='alert.unexpected.links.status' defaultMessage='Status' />
</a>
</>
@ -193,7 +193,7 @@ class ErrorBoundary extends React.PureComponent<Props, State> {
{links.get('help') && (
<>
<span className='inline-block border-l border-gray-300' aria-hidden='true' />
<a href={links.get('help')} className='text-sm font-medium text-gray-700 hover:underline dark:text-gray-600'>
<a href={links.get('help')} className='text-sm font-medium text-gray-700 dark:text-gray-600 hover:underline'>
<FormattedMessage id='alert.unexpected.links.help' defaultMessage='Help Center' />
</a>
</>
@ -202,7 +202,7 @@ class ErrorBoundary extends React.PureComponent<Props, State> {
{links.get('support') && (
<>
<span className='inline-block border-l border-gray-300' aria-hidden='true' />
<a href={links.get('support')} className='text-sm font-medium text-gray-700 hover:underline dark:text-gray-600'>
<a href={links.get('support')} className='text-sm font-medium text-gray-700 dark:text-gray-600 hover:underline'>
<FormattedMessage id='alert.unexpected.links.support' defaultMessage='Support' />
</a>
</>

Wyświetl plik

@ -1,4 +1,4 @@
import clsx from 'clsx';
import classNames from 'clsx';
import React from 'react';
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
@ -13,7 +13,7 @@ import VerificationBadge from './verification-badge';
import type { Account as AccountEntity, Status as StatusEntity } from 'soapbox/types/entities';
const messages = defineMessages({
eventBanner: { id: 'event.banner', defaultMessage: 'Event banner' },
bannerHeader: { id: 'event.banner', defaultMessage: 'Event banner' },
leaveConfirm: { id: 'confirmations.leave_event.confirm', defaultMessage: 'Leave event' },
leaveMessage: { id: 'confirmations.leave_event.message', defaultMessage: 'If you want to rejoin the event, the request will be manually reviewed again. Are you sure you want to proceed?' },
});
@ -51,12 +51,12 @@ const EventPreview: React.FC<IEventPreview> = ({ status, className, hideAction,
));
return (
<div className={clsx('relative w-full overflow-hidden rounded-lg bg-gray-100 dark:bg-primary-800', className)}>
<div className={classNames('w-full rounded-lg bg-gray-100 dark:bg-primary-800 relative overflow-hidden', className)}>
<div className='absolute top-28 right-3'>
{floatingAction && action}
</div>
<div className='h-40 bg-primary-200 dark:bg-gray-600'>
{banner && <img className='h-full w-full object-cover' src={banner.url} alt={intl.formatMessage(messages.eventBanner)} />}
<div className='bg-primary-200 dark:bg-gray-600 h-40'>
{banner && <img className='h-full w-full object-cover' src={banner.url} alt={intl.formatMessage(messages.bannerHeader)} />}
</div>
<Stack className='p-2.5' space={2}>
<HStack space={2} alignItems='center' justifyContent='between'>
@ -65,7 +65,7 @@ const EventPreview: React.FC<IEventPreview> = ({ status, className, hideAction,
{!floatingAction && action}
</HStack>
<div className='flex flex-wrap gap-y-1 gap-x-2 text-gray-700 dark:text-gray-600'>
<div className='flex gap-y-1 gap-x-2 flex-wrap text-gray-700 dark:text-gray-600'>
<HStack alignItems='center' space={2}>
<Icon src={require('@tabler/icons/user.svg')} />
<HStack space={1} alignItems='center' grow>

Wyświetl plik

@ -3,14 +3,14 @@ import React, { useEffect, useRef } from 'react';
import { isIOS } from 'soapbox/is-mobile';
interface IExtendedVideoPlayer {
src: string
alt?: string
width?: number
height?: number
time?: number
controls?: boolean
muted?: boolean
onClick?: () => void
src: string,
alt?: string,
width?: number,
height?: number,
time?: number,
controls?: boolean,
muted?: boolean,
onClick?: () => void,
}
const ExtendedVideoPlayer: React.FC<IExtendedVideoPlayer> = ({ src, alt, time, controls, muted, onClick }) => {

Wyświetl plik

@ -5,13 +5,13 @@
* @see soapbox/components/icon
*/
import clsx from 'clsx';
import classNames from 'clsx';
import React from 'react';
export interface IForkAwesomeIcon extends React.HTMLAttributes<HTMLLIElement> {
id: string
className?: string
fixedWidth?: boolean
id: string,
className?: string,
fixedWidth?: boolean,
}
const ForkAwesomeIcon: React.FC<IForkAwesomeIcon> = ({ id, className, fixedWidth, ...rest }) => {
@ -25,7 +25,7 @@ const ForkAwesomeIcon: React.FC<IForkAwesomeIcon> = ({ id, className, fixedWidth
<i
role='img'
// alt={alt}
className={clsx('fa', `fa-${id}`, className, { 'fa-fw': fixedWidth })}
className={classNames('fa', `fa-${id}`, className, { 'fa-fw': fixedWidth })}
{...rest}
/>
);

Wyświetl plik

@ -1,4 +1,4 @@
import clsx from 'clsx';
import classNames from 'clsx';
import React, { useState } from 'react';
import { FormattedMessage } from 'react-intl';
@ -30,8 +30,8 @@ const GdprBanner: React.FC = () => {
}
return (
<Banner theme='opaque' className={clsx('transition-transform', { 'translate-y-full': slideout })}>
<div className='flex flex-col space-y-4 rtl:space-x-reverse lg:flex-row lg:items-center lg:justify-between lg:space-y-0 lg:space-x-4'>
<Banner theme='opaque' className={classNames('transition-transform', { 'translate-y-full': slideout })}>
<div className='flex flex-col space-y-4 lg:space-y-0 lg:space-x-4 rtl:space-x-reverse lg:flex-row lg:items-center lg:justify-between'>
<Stack space={2}>
<Text size='xl' weight='bold'>
<FormattedMessage id='gdpr.title' defaultMessage='{siteTitle} uses cookies' values={{ siteTitle: instance.title }} />

Wyświetl plik

@ -1,64 +0,0 @@
import React from 'react';
import { defineMessages, useIntl } from 'react-intl';
import GroupMemberCount from 'soapbox/features/group/components/group-member-count';
import GroupPrivacy from 'soapbox/features/group/components/group-privacy';
import GroupRelationship from 'soapbox/features/group/components/group-relationship';
import GroupAvatar from './groups/group-avatar';
import { HStack, Stack, Text } from './ui';
import type { Group as GroupEntity } from 'soapbox/types/entities';
const messages = defineMessages({
groupHeader: { id: 'group.header.alt', defaultMessage: 'Group header' },
});
interface IGroupCard {
group: GroupEntity
}
const GroupCard: React.FC<IGroupCard> = ({ group }) => {
const intl = useIntl();
return (
<Stack
className='relative h-[240px] rounded-lg border border-solid border-gray-300 bg-white dark:border-primary-800 dark:bg-primary-900'
data-testid='group-card'
>
{/* Group Cover Image */}
<Stack grow className='relative basis-1/2 rounded-t-lg bg-primary-100 dark:bg-gray-800'>
{group.header && (
<img
className='absolute inset-0 h-full w-full rounded-t-lg object-cover'
src={group.header} alt={intl.formatMessage(messages.groupHeader)}
/>
)}
</Stack>
{/* Group Avatar */}
<div className='absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2'>
<GroupAvatar group={group} size={64} withRing />
</div>
{/* Group Info */}
<Stack alignItems='center' justifyContent='end' grow className='basis-1/2 py-4' space={0.5}>
<HStack alignItems='center' space={1.5}>
<Text size='lg' weight='bold' dangerouslySetInnerHTML={{ __html: group.display_name_html }} />
{group.relationship?.pending_requests && (
<div className='h-2 w-2 rounded-full bg-secondary-500' />
)}
</HStack>
<HStack className='text-gray-700 dark:text-gray-600' space={2} wrap>
<GroupRelationship group={group} />
<GroupPrivacy group={group} />
<GroupMemberCount group={group} />
</HStack>
</Stack>
</Stack>
);
};
export default GroupCard;

Wyświetl plik

@ -1,37 +0,0 @@
import clsx from 'clsx';
import React from 'react';
import { GroupRoles } from 'soapbox/schemas/group-member';
import { Avatar } from '../ui';
import type { Group } from 'soapbox/schemas';
interface IGroupAvatar {
group: Group
size: number
withRing?: boolean
}
const GroupAvatar = (props: IGroupAvatar) => {
const { group, size, withRing = false } = props;
const isOwner = group.relationship?.role === GroupRoles.OWNER;
return (
<Avatar
className={
clsx('relative rounded-full', {
'shadow-[0_0_0_2px_theme(colors.primary.600),0_0_0_4px_theme(colors.white)]': isOwner && withRing,
'dark:shadow-[0_0_0_2px_theme(colors.primary.600),0_0_0_4px_theme(colors.gray.800)]': isOwner && withRing,
'shadow-[0_0_0_2px_theme(colors.primary.600)]': isOwner && !withRing,
'shadow-[0_0_0_2px_theme(colors.white)] dark:shadow-[0_0_0_2px_theme(colors.gray.800)]': !isOwner && withRing,
})
}
src={group.avatar}
size={size}
/>
);
};
export default GroupAvatar;

Wyświetl plik

@ -1,99 +0,0 @@
import React from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { Link } from 'react-router-dom';
import { Button, Divider, HStack, Popover, Stack, Text } from 'soapbox/components/ui';
import GroupMemberCount from 'soapbox/features/group/components/group-member-count';
import GroupPrivacy from 'soapbox/features/group/components/group-privacy';
import GroupAvatar from '../group-avatar';
import type { Group } from 'soapbox/schemas';
interface IGroupPopoverContainer {
children: React.ReactElement<any, string | React.JSXElementConstructor<any>>
isEnabled: boolean
group: Group
}
const messages = defineMessages({
title: { id: 'group.popover.title', defaultMessage: 'Membership required' },
summary: { id: 'group.popover.summary', defaultMessage: 'You must be a member of the group in order to reply to this status.' },
action: { id: 'group.popover.action', defaultMessage: 'View Group' },
});
const GroupPopover = (props: IGroupPopoverContainer) => {
const { children, group, isEnabled } = props;
const intl = useIntl();
if (!isEnabled) {
return children;
}
return (
<Popover
interaction='click'
referenceElementClassName='cursor-pointer'
content={
<Stack space={4} className='w-80'>
<Stack
className='relative h-60 rounded-lg bg-white dark:border-primary-800 dark:bg-primary-900'
data-testid='group-card'
>
{/* Group Cover Image */}
<Stack grow className='relative basis-1/2 rounded-t-lg bg-primary-100 dark:bg-gray-800'>
{group.header && (
<img
className='absolute inset-0 h-full w-full rounded-t-lg object-cover'
src={group.header}
alt=''
/>
)}
</Stack>
{/* Group Avatar */}
<div className='absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2'>
<GroupAvatar group={group} size={64} withRing />
</div>
{/* Group Info */}
<Stack alignItems='center' justifyContent='end' grow className='basis-1/2 py-4' space={0.5}>
<Text size='lg' weight='bold' dangerouslySetInnerHTML={{ __html: group.display_name_html }} />
<HStack className='text-gray-700 dark:text-gray-600' space={2} wrap>
<GroupPrivacy group={group} />
<GroupMemberCount group={group} />
</HStack>
</Stack>
</Stack>
<Divider />
<Stack space={0.5} className='px-4'>
<Text weight='semibold'>
{intl.formatMessage(messages.title)}
</Text>
<Text theme='muted'>
{intl.formatMessage(messages.summary)}
</Text>
</Stack>
<div className='px-4 pb-4'>
<Link to={`/groups/${group.id}`}>
<Button type='button' theme='secondary' block>
{intl.formatMessage(messages.action)}
</Button>
</Link>
</div>
</Stack>
}
isFlush
children={
<div className='inline-block'>{children}</div>
}
/>
);
};
export default GroupPopover;

Wyświetl plik

@ -10,7 +10,7 @@ import { HStack, Stack, Text } from './ui';
import type { Tag } from 'soapbox/types/entities';
interface IHashtag {
hashtag: Tag
hashtag: Tag,
}
const Hashtag: React.FC<IHashtag> = ({ hashtag }) => {

Wyświetl plik

@ -1,4 +1,4 @@
import clsx from 'clsx';
import classNames from 'clsx';
import debounce from 'lodash/debounce';
import React, { useRef } from 'react';
@ -15,10 +15,10 @@ const showProfileHoverCard = debounce((dispatch, ref, accountId) => {
}, 600);
interface IHoverRefWrapper {
accountId: string
inline?: boolean
className?: string
children: React.ReactNode
accountId: string,
inline?: boolean,
className?: string,
children: React.ReactNode,
}
/** Makes a profile hover card appear when the wrapped element is hovered. */
@ -47,7 +47,7 @@ export const HoverRefWrapper: React.FC<IHoverRefWrapper> = ({ accountId, childre
return (
<Elem
ref={ref}
className={clsx('hover-ref-wrapper', className)}
className={classNames('hover-ref-wrapper', className)}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
onClick={handleClick}

Wyświetl plik

@ -1,4 +1,4 @@
import clsx from 'clsx';
import classNames from 'clsx';
import debounce from 'lodash/debounce';
import React, { useRef } from 'react';
import { useDispatch } from 'react-redux';
@ -14,10 +14,10 @@ const showStatusHoverCard = debounce((dispatch, ref, statusId) => {
}, 300);
interface IHoverStatusWrapper {
statusId: any
inline: boolean
className?: string
children: React.ReactNode
statusId: any,
inline: boolean,
className?: string,
children: React.ReactNode,
}
/** Makes a status hover card appear when the wrapped element is hovered. */
@ -45,7 +45,7 @@ export const HoverStatusWrapper: React.FC<IHoverStatusWrapper> = ({ statusId, ch
return (
<Elem
ref={ref}
className={clsx('hover-status-wrapper', className)}
className={classNames('hover-status-wrapper', className)}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
onClick={handleClick}

Wyświetl plik

@ -1,4 +1,4 @@
import clsx from 'clsx';
import classNames from 'clsx';
import React from 'react';
import Icon from 'soapbox/components/icon';
@ -66,7 +66,7 @@ const IconButton: React.FC<IIconButton> = ({
}
};
const classes = clsx(className, 'icon-button', {
const classes = classNames(className, 'icon-button', {
active,
disabled,
});

Wyświetl plik

@ -4,10 +4,10 @@ import Icon, { IIcon } from 'soapbox/components/icon';
import { Counter } from 'soapbox/components/ui';
interface IIconWithCounter extends React.HTMLAttributes<HTMLDivElement> {
count: number
count: number,
countMax?: number
icon?: string
src?: string
icon?: string;
src?: string;
}
const IconWithCounter: React.FC<IIconWithCounter> = ({ icon, count, countMax, ...rest }) => {

Wyświetl plik

@ -3,24 +3,21 @@
* @module soapbox/components/icon
*/
import clsx from 'clsx';
import classNames from 'clsx';
import React from 'react';
import InlineSVG from 'react-inlinesvg'; // eslint-disable-line no-restricted-imports
export interface IIcon extends React.HTMLAttributes<HTMLDivElement> {
src: string
id?: string
alt?: string
className?: string
src: string,
id?: string,
alt?: string,
className?: string,
}
/**
* @deprecated Use the UI Icon component directly.
*/
const Icon: React.FC<IIcon> = ({ src, alt, className, ...rest }) => {
return (
<div
className={clsx('svg-icon', className)}
className={classNames('svg-icon', className)}
{...rest}
>
<InlineSVG src={src} title={alt} loader={<></>} />

Wyświetl plik

@ -2,7 +2,7 @@ import React from 'react';
/** Fullscreen gradient used as a backdrop to public pages. */
const LandingGradient: React.FC = () => (
<div className='fixed h-screen w-full bg-gradient-to-tr from-primary-50 via-white to-gradient-end/10 dark:from-primary-900/50 dark:via-primary-900 dark:to-primary-800/50' />
<div className='fixed h-screen w-full bg-gradient-to-tr from-primary-50 dark:from-primary-900/50 via-white dark:via-primary-900 to-gradient-end/10 dark:to-primary-800/50' />
);
export default LandingGradient;

Wyświetl plik

@ -4,7 +4,7 @@ import { Link as Comp, LinkProps } from 'react-router-dom';
const Link = (props: LinkProps) => (
<Comp
{...props}
className='text-primary-600 hover:underline dark:text-accent-blue'
className='text-primary-600 dark:text-accent-blue hover:underline'
/>
);

Wyświetl plik

@ -1,10 +1,11 @@
import clsx from 'clsx';
import classNames from 'clsx';
import React from 'react';
import { v4 as uuidv4 } from 'uuid';
import { SelectDropdown } from '../features/forms';
import { Icon, HStack, Select } from './ui';
import Icon from './icon';
import { HStack, Select } from './ui';
interface IList {
children: React.ReactNode
@ -15,9 +16,9 @@ const List: React.FC<IList> = ({ children }) => (
);
interface IListItem {
label: React.ReactNode
hint?: React.ReactNode
onClick?(): void
label: React.ReactNode,
hint?: React.ReactNode,
onClick?(): void,
onSelect?(): void
isSelected?: boolean
children?: React.ReactNode
@ -44,7 +45,7 @@ const ListItem: React.FC<IListItem> = ({ label, hint, children, onClick, onSelec
return React.cloneElement(child, {
id: domId,
className: clsx({
className: classNames({
'w-auto': isSelect,
}, child.props.className),
});
@ -56,14 +57,14 @@ const ListItem: React.FC<IListItem> = ({ label, hint, children, onClick, onSelec
return (
<Comp
className={clsx({
'flex items-center justify-between px-4 py-2 first:rounded-t-lg last:rounded-b-lg bg-gradient-to-r from-gradient-start/20 to-gradient-end/20 dark:from-gradient-start/10 dark:to-gradient-end/10': true,
'cursor-pointer hover:from-gradient-start/30 hover:to-gradient-end/30 dark:hover:from-gradient-start/5 dark:hover:to-gradient-end/5': typeof onClick !== 'undefined' || typeof onSelect !== 'undefined',
className={classNames({
'flex items-center justify-between px-3 py-2 first:rounded-t-lg last:rounded-b-lg bg-gradient-to-r from-gradient-start/10 to-gradient-end/10': true,
'cursor-pointer hover:from-gradient-start/20 hover:to-gradient-end/20 dark:hover:from-gradient-start/5 dark:hover:to-gradient-end/5': typeof onClick !== 'undefined' || typeof onSelect !== 'undefined',
})}
{...linkProps}
>
<div className='flex flex-col py-1.5 pr-4 rtl:pl-4 rtl:pr-0'>
<LabelComp className='font-medium text-gray-900 dark:text-gray-100' htmlFor={domId}>{label}</LabelComp>
<LabelComp className='text-gray-900 dark:text-gray-100' htmlFor={domId}>{label}</LabelComp>
{hint ? (
<span className='text-sm text-gray-700 dark:text-gray-600'>{hint}</span>
@ -82,26 +83,9 @@ const ListItem: React.FC<IListItem> = ({ label, hint, children, onClick, onSelec
<div className='flex flex-row items-center text-gray-700 dark:text-gray-600'>
{children}
<div
className={
clsx({
'flex h-6 w-6 items-center justify-center rounded-full border-2 border-solid border-primary-500 dark:border-primary-400 transition': true,
'bg-primary-500 dark:bg-primary-400': isSelected,
'bg-transparent': !isSelected,
})
}
>
<Icon
src={require('@tabler/icons/check.svg')}
className={
clsx({
'h-4 w-4 text-white dark:text-white transition-all duration-500': true,
'opacity-0 scale-50': !isSelected,
'opacity-100 scale-100': isSelected,
})
}
/>
</div>
{isSelected ? (
<Icon src={require('@tabler/icons/check.svg')} className='ml-1 text-primary-500 dark:text-primary-400' />
) : null}
</div>
) : null}

Wyświetl plik

@ -8,9 +8,9 @@ const messages = defineMessages({
});
interface ILoadGap {
disabled?: boolean
maxId: string
onClick: (id: string) => void
disabled?: boolean,
maxId: string,
onClick: (id: string) => void,
}
const LoadGap: React.FC<ILoadGap> = ({ disabled, maxId, onClick }) => {

Wyświetl plik

@ -4,19 +4,18 @@ import { FormattedMessage } from 'react-intl';
import { Button } from 'soapbox/components/ui';
interface ILoadMore {
onClick: React.MouseEventHandler
disabled?: boolean
visible?: boolean
className?: string
onClick: React.MouseEventHandler,
disabled?: boolean,
visible?: Boolean,
}
const LoadMore: React.FC<ILoadMore> = ({ onClick, disabled, visible = true, className }) => {
const LoadMore: React.FC<ILoadMore> = ({ onClick, disabled, visible = true }) => {
if (!visible) {
return null;
}
return (
<Button className={className} theme='primary' block disabled={disabled || !visible} onClick={onClick}>
<Button theme='primary' block disabled={disabled || !visible} onClick={onClick}>
<FormattedMessage id='status.load_more' defaultMessage='Load more' />
</Button>
);

Wyświetl plik

@ -9,7 +9,7 @@ const LoadingScreen: React.FC = () => {
<div className='fixed h-screen w-screen'>
<LandingGradient />
<div className='d-screen fixed z-10 flex w-screen items-center justify-center'>
<div className='fixed d-screen w-screen flex items-center justify-center z-10'>
<div className='p-4'>
<Spinner size={40} withText={false} />
</div>

Wyświetl plik

@ -1,4 +1,4 @@
import clsx from 'clsx';
import classNames from 'clsx';
import { OrderedSet as ImmutableOrderedSet } from 'immutable';
import throttle from 'lodash/throttle';
import React, { useCallback, useEffect, useRef, useState } from 'react';
@ -18,7 +18,7 @@ const messages = defineMessages({
});
interface ILocationSearch {
onSelected: (locationId: string) => void
onSelected: (locationId: string) => void,
}
const LocationSearch: React.FC<ILocationSearch> = ({ onSelected }) => {
@ -100,8 +100,8 @@ const LocationSearch: React.FC<ILocationSearch> = ({ onSelected }) => {
renderSuggestion={AutosuggestLocation}
/>
<div role='button' tabIndex={0} className='search__icon' onClick={handleClear}>
<Icon src={require('@tabler/icons/search.svg')} className={clsx('svg-icon--search', { active: isEmpty() })} />
<Icon src={require('@tabler/icons/backspace.svg')} className={clsx('svg-icon--backspace', { active: !isEmpty() })} aria-label={intl.formatMessage(messages.placeholder)} />
<Icon src={require('@tabler/icons/search.svg')} className={classNames('svg-icon--search', { active: isEmpty() })} />
<Icon src={require('@tabler/icons/backspace.svg')} className={classNames('svg-icon--backspace', { active: !isEmpty() })} aria-label={intl.formatMessage(messages.placeholder)} />
</div>
</div>
);

Wyświetl plik

@ -1,4 +1,4 @@
import clsx from 'clsx';
import classNames from 'clsx';
import React, { useState, useRef, useLayoutEffect } from 'react';
import Blurhash from 'soapbox/components/blurhash';
@ -19,21 +19,21 @@ const ATTACHMENT_LIMIT = 4;
const MAX_FILENAME_LENGTH = 45;
interface Dimensions {
w: Property.Width | number
h: Property.Height | number
t?: Property.Top
r?: Property.Right
b?: Property.Bottom
l?: Property.Left
float?: Property.Float
pos?: Property.Position
w: Property.Width | number,
h: Property.Height | number,
t?: Property.Top,
r?: Property.Right,
b?: Property.Bottom,
l?: Property.Left,
float?: Property.Float,
pos?: Property.Position,
}
interface SizeData {
style: React.CSSProperties
itemsDimensions: Dimensions[]
size: number
width: number
style: React.CSSProperties,
itemsDimensions: Dimensions[],
size: number,
width: number,
}
const withinLimits = (aspectRatio: number) => {
@ -48,16 +48,16 @@ const shouldLetterbox = (attachment: Attachment): boolean => {
};
interface IItem {
attachment: Attachment
standalone?: boolean
index: number
size: number
onClick: (index: number) => void
displayWidth?: number
visible: boolean
dimensions: Dimensions
last?: boolean
total: number
attachment: Attachment,
standalone?: boolean,
index: number,
size: number,
onClick: (index: number) => void,
displayWidth?: number,
visible: boolean,
dimensions: Dimensions,
last?: boolean,
total: number,
}
const Item: React.FC<IItem> = ({
@ -152,14 +152,7 @@ const Item: React.FC<IItem> = ({
);
return (
<div
className={clsx('media-gallery__item', {
standalone,
'rounded-md': total > 1,
})}
key={attachment.id}
style={{ position, float, left, top, right, bottom, height, width: `${width}%` }}
>
<div className={classNames('media-gallery__item', { standalone })} key={attachment.id} style={{ position, float, left, top, right, bottom, height, width: `${width}%` }}>
<a className='media-gallery__item-thumbnail' href={attachment.url} target='_blank' style={{ cursor: 'pointer' }}>
<Blurhash hash={attachment.blurhash} className='media-gallery__preview' />
<span className='media-gallery__item__icons'>{attachmentIcon}</span>
@ -178,7 +171,7 @@ const Item: React.FC<IItem> = ({
target='_blank'
>
<StillImage
className='h-full w-full'
className='w-full h-full'
src={mediaPreview ? attachment.preview_url : attachment.url}
alt={attachment.description}
letterboxed={letterboxed}
@ -196,7 +189,7 @@ const Item: React.FC<IItem> = ({
}
thumbnail = (
<div className={clsx('media-gallery__gifv', { autoplay: autoPlayGif })}>
<div className={classNames('media-gallery__gifv', { autoplay: autoPlayGif })}>
<video
className='media-gallery__item-gifv-thumbnail'
aria-label={attachment.description}
@ -218,7 +211,7 @@ const Item: React.FC<IItem> = ({
const ext = attachment.url.split('.').pop()?.toUpperCase();
thumbnail = (
<a
className={clsx('media-gallery__item-thumbnail')}
className={classNames('media-gallery__item-thumbnail')}
href={attachment.url}
onClick={handleClick}
target='_blank'
@ -232,7 +225,7 @@ const Item: React.FC<IItem> = ({
const ext = attachment.url.split('.').pop()?.toUpperCase();
thumbnail = (
<a
className={clsx('media-gallery__item-thumbnail')}
className={classNames('media-gallery__item-thumbnail')}
href={attachment.url}
onClick={handleClick}
target='_blank'
@ -252,14 +245,7 @@ const Item: React.FC<IItem> = ({
}
return (
<div
className={clsx('media-gallery__item', `media-gallery__item--${attachment.type}`, {
standalone,
'rounded-md': total > 1,
})}
key={attachment.id}
style={{ position, float, left, top, right, bottom, height, width: `${width}%` }}
>
<div className={classNames('media-gallery__item', `media-gallery__item--${attachment.type}`, { standalone })} key={attachment.id} style={{ position, float, left, top, right, bottom, height, width: `${width}%` }}>
{last && total > ATTACHMENT_LIMIT && (
<div className='media-gallery__item-overflow'>
+{total - ATTACHMENT_LIMIT + 1}
@ -274,25 +260,23 @@ const Item: React.FC<IItem> = ({
);
};
export interface IMediaGallery {
sensitive?: boolean
media: ImmutableList<Attachment>
height?: number
onOpenMedia: (media: ImmutableList<Attachment>, index: number) => void
defaultWidth?: number
cacheWidth?: (width: number) => void
visible?: boolean
onToggleVisibility?: () => void
displayMedia?: string
compact?: boolean
className?: string
interface IMediaGallery {
sensitive?: boolean,
media: ImmutableList<Attachment>,
height?: number,
onOpenMedia: (media: ImmutableList<Attachment>, index: number) => void,
defaultWidth?: number,
cacheWidth?: (width: number) => void,
visible?: boolean,
onToggleVisibility?: () => void,
displayMedia?: string,
compact: boolean,
}
const MediaGallery: React.FC<IMediaGallery> = (props) => {
const {
media,
defaultWidth = 0,
className,
onOpenMedia,
cacheWidth,
compact,
@ -562,11 +546,7 @@ const MediaGallery: React.FC<IMediaGallery> = (props) => {
}, [node.current]);
return (
<div
className={clsx(className, 'media-gallery', { 'media-gallery--compact': compact })}
style={sizeData.style}
ref={node}
>
<div className={classNames('media-gallery', { 'media-gallery--compact': compact })} style={sizeData.style} ref={node}>
{children}
</div>
);

Wyświetl plik

@ -1,4 +1,4 @@
import clsx from 'clsx';
import classNames from 'clsx';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import 'wicg-inert';
import { FormattedMessage, defineMessages, useIntl } from 'react-intl';
@ -39,10 +39,10 @@ export const checkEventComposeContent = (compose?: ReturnType<typeof ReducerComp
};
interface IModalRoot {
onCancel?: () => void
onClose: (type?: ModalType) => void
type: ModalType
children: React.ReactNode
onCancel?: () => void,
onClose: (type?: ModalType) => void,
type: ModalType,
children: React.ReactNode,
}
const ModalRoot: React.FC<IModalRoot> = ({ children, onCancel, onClose, type }) => {
@ -232,7 +232,7 @@ const ModalRoot: React.FC<IModalRoot> = ({ children, onCancel, onClose, type })
return (
<div
ref={ref}
className={clsx({
className={classNames({
'fixed top-0 left-0 z-[100] w-full h-full overflow-x-hidden overflow-y-auto': true,
'pointer-events-none': !visible,
})}
@ -241,16 +241,17 @@ const ModalRoot: React.FC<IModalRoot> = ({ children, onCancel, onClose, type })
<div
role='presentation'
id='modal-overlay'
className='fixed inset-0 bg-gray-500/90 backdrop-blur dark:bg-gray-700/90'
className='fixed inset-0 bg-gray-500/90 dark:bg-gray-700/90 backdrop-blur'
onClick={handleOnClose}
/>
<div
role='dialog'
className={clsx({
'my-2 mx-auto relative pointer-events-none flex items-center min-h-[calc(100%-3.5rem)]': true,
className={classNames({
'my-2 mx-auto relative pointer-events-none flex items-center': true,
'p-4 md:p-0': type !== 'MEDIA',
})}
style={{ minHeight: 'calc(100% - 3.5rem)' }}
>
{children}
</div>

Wyświetl plik

@ -1,16 +1,16 @@
import clsx from 'clsx';
import classNames from 'clsx';
import React from 'react';
interface IOutlineBox extends React.HTMLAttributes<HTMLDivElement> {
children: React.ReactNode
className?: string
children: React.ReactNode,
className?: string,
}
/** Wraps children in a container with an outline. */
const OutlineBox: React.FC<IOutlineBox> = ({ children, className, ...rest }) => {
return (
<div
className={clsx('rounded-lg border border-solid border-gray-300 p-4 dark:border-gray-800', className)}
className={classNames('p-4 rounded-lg border border-solid border-gray-300 dark:border-gray-800', className)}
{...rest}
>
{children}
@ -18,4 +18,4 @@ const OutlineBox: React.FC<IOutlineBox> = ({ children, className, ...rest }) =>
);
};
export default OutlineBox;
export default OutlineBox;

Wyświetl plik

@ -1,54 +0,0 @@
import clsx from 'clsx';
import React from 'react';
import { FormattedMessage } from 'react-intl';
import { Link } from 'react-router-dom';
import { HStack, Icon, Text } from 'soapbox/components/ui';
interface IPendingItemsRow {
/** Path to navigate the user when clicked. */
to: string
/** Number of pending items. */
count: number
/** Size of the icon. */
size?: 'md' | 'lg'
}
const PendingItemsRow: React.FC<IPendingItemsRow> = ({ to, count, size = 'md' }) => {
return (
<Link to={to} className='group' data-testid='pending-items-row'>
<HStack alignItems='center' justifyContent='between'>
<HStack alignItems='center' space={2}>
<div className={clsx('rounded-full bg-primary-200 text-primary-500 dark:bg-primary-800 dark:text-primary-200', {
'p-3': size === 'lg',
'p-2.5': size === 'md',
})}
>
<Icon
src={require('@tabler/icons/exclamation-circle.svg')}
className={clsx({
'h-5 w-5': size === 'md',
'h-7 w-7': size === 'lg',
})}
/>
</div>
<Text weight='bold' size='md'>
<FormattedMessage
id='groups.pending.count'
defaultMessage='{number, plural, one {# pending request} other {# pending requests}}'
values={{ number: count }}
/>
</Text>
</HStack>
<Icon
src={require('@tabler/icons/chevron-right.svg')}
className='h-5 w-5 text-gray-600 transition-colors group-hover:text-gray-700 dark:text-gray-600 dark:group-hover:text-gray-500'
/>
</HStack>
</Link>
);
};
export { PendingItemsRow };

Wyświetl plik

@ -16,9 +16,9 @@ const messages = defineMessages({
});
interface IPollFooter {
poll: PollEntity
showResults: boolean
selected: Selected
poll: PollEntity,
showResults: boolean,
selected: Selected,
}
const PollFooter: React.FC<IPollFooter> = ({ poll, showResults, selected }): JSX.Element => {

Wyświetl plik

@ -1,4 +1,4 @@
import clsx from 'clsx';
import classNames from 'clsx';
import React from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { Motion, presets, spring } from 'react-motion';
@ -20,7 +20,7 @@ const PollPercentageBar: React.FC<{ percent: number, leading: boolean }> = ({ pe
<Motion defaultStyle={{ width: 0 }} style={{ width: spring(percent, { ...presets.gentle, precision: 0.1 }) }}>
{({ width }) => (
<span
className='absolute inset-0 inline-block h-full rounded-l-md bg-primary-100 dark:bg-primary-500'
className='absolute inset-0 h-full inline-block bg-primary-100 dark:bg-primary-500 rounded-l-md'
style={{ width: `${width}%` }}
/>
)}
@ -29,7 +29,7 @@ const PollPercentageBar: React.FC<{ percent: number, leading: boolean }> = ({ pe
};
interface IPollOptionText extends IPollOption {
percent: number
percent: number,
}
const PollOptionText: React.FC<IPollOptionText> = ({ poll, option, index, active, onToggle }) => {
@ -46,7 +46,7 @@ const PollOptionText: React.FC<IPollOptionText> = ({ poll, option, index, active
return (
<label
className={
clsx('relative flex cursor-pointer rounded-3xl border border-solid bg-white p-2 hover:bg-primary-50 dark:bg-primary-900 dark:hover:bg-primary-800/50', {
classNames('flex relative p-2 bg-white dark:bg-primary-900 cursor-pointer rounded-3xl border border-solid hover:bg-primary-50 dark:hover:bg-primary-800/50', {
'border-primary-600 ring-1 ring-primary-600 bg-primary-50 dark:bg-primary-800/50 dark:border-primary-300 dark:ring-primary-300': active,
'border-primary-300 dark:border-primary-500': !active,
})
@ -61,8 +61,8 @@ const PollOptionText: React.FC<IPollOptionText> = ({ poll, option, index, active
onChange={handleOptionChange}
/>
<div className='grid w-full items-center'>
<div className='col-start-1 row-start-1 ml-4 mr-6 justify-self-center'>
<div className='grid items-center w-full'>
<div className='col-start-1 row-start-1 justify-self-center ml-4 mr-6'>
<div className='text-primary-600 dark:text-white'>
<Text
theme='inherit'
@ -72,9 +72,9 @@ const PollOptionText: React.FC<IPollOptionText> = ({ poll, option, index, active
</div>
</div>
<div className='col-start-1 row-start-1 flex items-center justify-self-end'>
<div className='col-start-1 row-start-1 justify-self-end flex items-center'>
<span
className={clsx('flex h-6 w-6 flex-none items-center justify-center rounded-full border border-solid', {
className={classNames('flex items-center justify-center w-6 h-6 flex-none border border-solid rounded-full', {
'bg-primary-600 border-primary-600 dark:bg-primary-300 dark:border-primary-300': active,
'border-primary-300 bg-white dark:bg-primary-900 dark:border-primary-500': !active,
})}
@ -85,7 +85,7 @@ const PollOptionText: React.FC<IPollOptionText> = ({ poll, option, index, active
aria-label={option.title}
>
{active && (
<Icon src={require('@tabler/icons/check.svg')} className='h-4 w-4 text-white dark:text-primary-900' />
<Icon src={require('@tabler/icons/check.svg')} className='text-white dark:text-primary-900 w-4 h-4' />
)}
</span>
</div>
@ -95,12 +95,12 @@ const PollOptionText: React.FC<IPollOptionText> = ({ poll, option, index, active
};
interface IPollOption {
poll: PollEntity
option: PollOptionEntity
index: number
showResults?: boolean
active: boolean
onToggle: (value: number) => void
poll: PollEntity,
option: PollOptionEntity,
index: number,
showResults?: boolean,
active: boolean,
onToggle: (value: number) => void,
}
const PollOption: React.FC<IPollOption> = (props): JSX.Element | null => {
@ -123,7 +123,7 @@ const PollOption: React.FC<IPollOption> = (props): JSX.Element | null => {
<HStack
justifyContent='between'
alignItems='center'
className='relative w-full overflow-hidden rounded-md bg-white p-2 dark:bg-primary-800'
className='relative p-2 w-full bg-white dark:bg-primary-800 rounded-md overflow-hidden'
>
<PollPercentageBar percent={percent} leading={leading} />
@ -141,7 +141,7 @@ const PollOption: React.FC<IPollOption> = (props): JSX.Element | null => {
<Icon
src={require('@tabler/icons/circle-check.svg')}
alt={intl.formatMessage(messages.voted)}
className='h-4 w-4 text-primary-600 dark:fill-white dark:text-primary-800'
className='text-primary-600 dark:text-primary-800 dark:fill-white w-4 h-4'
/>
) : (
<div className='svg-icon' />

Wyświetl plik

@ -13,8 +13,8 @@ import PollOption from './poll-option';
export type Selected = Record<number, boolean>;
interface IPoll {
id: string
status?: string
id: string,
status?: string,
}
const messages = defineMessages({

Wyświetl plik

@ -1,4 +1,4 @@
import clsx from 'clsx';
import classNames from 'clsx';
import React, { useEffect, useState } from 'react';
import { useIntl, FormattedMessage } from 'react-intl';
import { usePopper } from 'react-popper';
@ -54,7 +54,7 @@ const handleMouseLeave = (dispatch: AppDispatch): React.MouseEventHandler => {
};
interface IProfileHoverCard {
visible: boolean
visible: boolean,
}
/** Popup profile preview that appears when hovering avatars and display names. */
@ -95,7 +95,7 @@ export const ProfileHoverCard: React.FC<IProfileHoverCard> = ({ visible = true }
return (
<div
className={clsx({
className={classNames({
'absolute transition-opacity w-[320px] z-[101] top-0 left-0': true,
'opacity-100': visible,
'opacity-0 pointer-events-none': !visible,
@ -123,7 +123,7 @@ export const ProfileHoverCard: React.FC<IProfileHoverCard> = ({ visible = true }
<HStack alignItems='center' space={0.5}>
<Icon
src={require('@tabler/icons/calendar.svg')}
className='h-4 w-4 text-gray-800 dark:text-gray-200'
className='w-4 h-4 text-gray-800 dark:text-gray-200'
/>
<Text size='sm'>

Wyświetl plik

@ -1,11 +1,11 @@
import clsx from 'clsx';
import classNames from 'clsx';
import React from 'react';
interface IProgressCircle {
progress: number
radius?: number
stroke?: number
title?: string
progress: number,
radius?: number,
stroke?: number,
title?: string,
}
const ProgressCircle: React.FC<IProgressCircle> = ({ progress, radius = 12, stroke = 4, title }) => {
@ -30,7 +30,7 @@ const ProgressCircle: React.FC<IProgressCircle> = ({ progress, radius = 12, stro
strokeWidth={stroke}
/>
<circle
className={clsx('stroke-primary-500', {
className={classNames('stroke-primary-500', {
'stroke-secondary-500': progress > 1,
})}
style={{

Wyświetl plik

@ -4,10 +4,10 @@ import PTRComponent from 'react-simple-pull-to-refresh';
import { Spinner } from 'soapbox/components/ui';
interface IPullToRefresh {
onRefresh?: () => Promise<any>
refreshingContent?: JSX.Element | string
pullingContent?: JSX.Element | string
children: React.ReactNode
onRefresh?: () => Promise<any>;
refreshingContent?: JSX.Element | string;
pullingContent?: JSX.Element | string;
children: React.ReactNode;
}
/**

Some files were not shown because too many files have changed in this diff Show More