Porównaj commity

..

13 Commity

Autor SHA1 Wiadomość Data
Alex Gleason 49b996a901
Add StillVideo component 2023-02-08 16:56:33 -06:00
Alex Gleason fd2bb2e16f
Consolidate around MediaPreview component 2023-02-08 16:07:39 -06:00
Alex Gleason bbb29d0388
Merge remote-tracking branch 'origin/develop' into media-gallery 2023-02-08 15:49:50 -06:00
Alex Gleason c8509627a1
Merge remote-tracking branch 'origin/develop' into media-gallery 2023-02-03 12:26:48 -06:00
Alex Gleason ffc8ade279
Reuse ExtensionBadge from StillImage, move ext to MediaItemThumbnail 2023-01-31 17:56:28 -06:00
Alex Gleason 0354e8e96f
Tailwindify MoreMediaOverlay component 2023-01-31 16:42:52 -06:00
Alex Gleason 07323c19b0
Refactor letterboxing code 2023-01-31 16:25:38 -06:00
Alex Gleason 4e74ca3c55
MediaGallery: fix layout adaptive by aspect ratio 2023-01-31 15:48:53 -06:00
Alex Gleason 215c857648
Move gigantic media sizing code into useMediaSizeData file/hook 2023-01-31 14:37:27 -06:00
Alex Gleason 6bbd00c658
Move MediaItem to separate file 2023-01-31 13:48:28 -06:00
Alex Gleason ca4fa5e5c5
Create components/media-gallery subdirectory 2023-01-31 13:37:38 -06:00
Alex Gleason 4167a1de05
Break out MediaItemThumbnail into a separate component 2023-01-31 13:34:29 -06:00
Alex Gleason c5cf252668
media-gallery.scss: switch to @apply rules where possible 2023-01-31 12:22:42 -06:00
683 zmienionych plików z 10722 dodań i 19103 usunięć

Wyświetl plik

@ -56,7 +56,6 @@ 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
@ -261,29 +260,12 @@ 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/classnames-order': 'error',
'tailwindcss/migration-from-tailwind-2': 'error',
},
overrides: [

Wyświetl plik

@ -157,11 +157,11 @@ docker:
# 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

@ -6,61 +6,27 @@ 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.
- Groups: Initial support for groups.
- 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';
@ -278,7 +277,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,
@ -290,10 +289,9 @@ const submitCompose = (composeId: string, routerHistory?: History, force = false
poll: compose.poll,
scheduled_at: compose.schedule,
to,
group_id: compose.privacy === 'group' ? compose.group_id : null,
};
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');
@ -517,9 +515,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 +560,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));

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

@ -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,6 +1,5 @@
import { defineMessages } from 'react-intl';
import { deleteEntities } from 'soapbox/entity-store/actions';
import toast from 'soapbox/toast';
import api, { getLinks } from '../api';
@ -41,6 +40,14 @@ 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_JOIN_REQUEST = 'GROUP_JOIN_REQUEST';
const GROUP_JOIN_SUCCESS = 'GROUP_JOIN_SUCCESS';
const GROUP_JOIN_FAIL = 'GROUP_JOIN_FAIL';
const GROUP_LEAVE_REQUEST = 'GROUP_LEAVE_REQUEST';
const GROUP_LEAVE_SUCCESS = 'GROUP_LEAVE_SUCCESS';
const GROUP_LEAVE_FAIL = 'GROUP_LEAVE_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';
@ -141,8 +148,7 @@ const createGroup = (params: Record<string, any>, shouldReset?: boolean) =>
if (shouldReset) {
dispatch(resetGroupEditor());
}
return data;
dispatch(closeModal('MANAGE_GROUP'));
}).catch(err => dispatch(createGroupFail(err)));
};
@ -192,7 +198,7 @@ const updateGroupFail = (error: AxiosError) => ({
});
const deleteGroup = (id: string) => (dispatch: AppDispatch, getState: () => RootState) => {
dispatch(deleteEntities([id], 'Group'));
dispatch(deleteGroupRequest(id));
return api(getState).delete(`/api/v1/groups/${id}`)
.then(() => dispatch(deleteGroupSuccess(id)))
@ -306,6 +312,70 @@ const fetchGroupRelationshipsFail = (error: AxiosError) => ({
skipNotFound: true,
});
const joinGroup = (id: string) =>
(dispatch: AppDispatch, getState: () => RootState) => {
const locked = (getState().groups.items.get(id) as any).locked || false;
dispatch(joinGroupRequest(id, locked));
return api(getState).post(`/api/v1/groups/${id}/join`).then(response => {
dispatch(joinGroupSuccess(response.data));
toast.success(locked ? messages.joinRequestSuccess : messages.joinSuccess);
}).catch(error => {
dispatch(joinGroupFail(error, locked));
});
};
const leaveGroup = (id: string) =>
(dispatch: AppDispatch, getState: () => RootState) => {
dispatch(leaveGroupRequest(id));
return api(getState).post(`/api/v1/groups/${id}/leave`).then(response => {
dispatch(leaveGroupSuccess(response.data));
toast.success(messages.leaveSuccess);
}).catch(error => {
dispatch(leaveGroupFail(error));
});
};
const joinGroupRequest = (id: string, locked: boolean) => ({
type: GROUP_JOIN_REQUEST,
id,
locked,
skipLoading: true,
});
const joinGroupSuccess = (relationship: APIEntity) => ({
type: GROUP_JOIN_SUCCESS,
relationship,
skipLoading: true,
});
const joinGroupFail = (error: AxiosError, locked: boolean) => ({
type: GROUP_JOIN_FAIL,
error,
locked,
skipLoading: true,
});
const leaveGroupRequest = (id: string) => ({
type: GROUP_LEAVE_REQUEST,
id,
skipLoading: true,
});
const leaveGroupSuccess = (relationship: APIEntity) => ({
type: GROUP_LEAVE_SUCCESS,
relationship,
skipLoading: true,
});
const leaveGroupFail = (error: AxiosError) => ({
type: GROUP_LEAVE_FAIL,
error,
skipLoading: true,
});
const groupDeleteStatus = (groupId: string, statusId: string) =>
(dispatch: AppDispatch, getState: () => RootState) => {
dispatch(groupDeleteStatusRequest(groupId, statusId));
@ -789,11 +859,9 @@ const submitGroupEditor = (shouldReset?: boolean) => (dispatch: AppDispatch, get
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,
};
@ -801,9 +869,9 @@ const submitGroupEditor = (shouldReset?: boolean) => (dispatch: AppDispatch, get
if (header) params.header = header;
if (groupId === null) {
return dispatch(createGroup(params, shouldReset));
dispatch(createGroup(params, shouldReset));
} else {
return dispatch(updateGroup(groupId, params, shouldReset));
dispatch(updateGroup(groupId, params, shouldReset));
}
};
@ -827,6 +895,12 @@ export {
GROUP_RELATIONSHIPS_FETCH_REQUEST,
GROUP_RELATIONSHIPS_FETCH_SUCCESS,
GROUP_RELATIONSHIPS_FETCH_FAIL,
GROUP_JOIN_REQUEST,
GROUP_JOIN_SUCCESS,
GROUP_JOIN_FAIL,
GROUP_LEAVE_REQUEST,
GROUP_LEAVE_SUCCESS,
GROUP_LEAVE_FAIL,
GROUP_DELETE_STATUS_REQUEST,
GROUP_DELETE_STATUS_SUCCESS,
GROUP_DELETE_STATUS_FAIL,
@ -899,6 +973,14 @@ export {
fetchGroupRelationshipsRequest,
fetchGroupRelationshipsSuccess,
fetchGroupRelationshipsFail,
joinGroup,
leaveGroup,
joinGroupRequest,
joinGroupSuccess,
joinGroupFail,
leaveGroupRequest,
leaveGroupSuccess,
leaveGroupFail,
groupDeleteStatus,
groupDeleteStatusRequest,
groupDeleteStatusSuccess,

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';
@ -23,11 +18,11 @@ const importAccount = (account: APIEntity) =>
const importAccounts = (accounts: APIEntity[]) =>
({ type: ACCOUNTS_IMPORT, accounts });
const importGroup = (group: Group) =>
importEntities([group], Entities.GROUPS);
const importGroup = (group: APIEntity) =>
({ type: GROUP_IMPORT, group });
const importGroups = (groups: Group[]) =>
importEntities(groups, Entities.GROUPS);
const importGroups = (groups: APIEntity[]) =>
({ type: GROUPS_IMPORT, groups });
const importStatus = (status: APIEntity, idempotencyKey?: string) =>
(dispatch: AppDispatch, getState: () => RootState) => {
@ -74,8 +69,17 @@ const importFetchedGroup = (group: APIEntity) =>
importFetchedGroups([group]);
const importFetchedGroups = (groups: APIEntity[]) => {
const entities = filteredArray(groupSchema).catch([]).parse(groups);
return importGroups(entities);
const normalGroups: APIEntity[] = [];
const processGroup = (group: APIEntity) => {
if (!group.id) return;
normalGroups.push(group);
};
groups.forEach(processGroup);
return importGroups(normalGroups);
};
const importFetchedStatus = (status: APIEntity, idempotencyKey?: string) =>

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

@ -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,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,
@ -220,7 +221,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 +231,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 +242,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

@ -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

@ -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. */

Wyświetl plik

@ -1,11 +1,11 @@
import React, { useRef } from 'react';
import React from 'react';
import { defineMessages, useIntl, FormattedMessage } 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';
@ -14,12 +14,11 @@ 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({
@ -54,7 +53,7 @@ const InstanceFavicon: React.FC<IInstanceFavicon> = ({ account, disabled }) => {
};
interface IProfilePopper {
condition: boolean
condition: boolean,
wrapper: (children: React.ReactNode) => React.ReactNode
children: React.ReactNode
}
@ -68,31 +67,30 @@ 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,
approvalStatus?: StatusApprovalStatus,
emoji?: string,
note?: string,
}
const Account = ({
@ -117,17 +115,21 @@ const Account = ({
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 = () => {
@ -146,7 +148,7 @@ const Account = ({
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'
iconClassName='w-4 h-4'
/>
);
}
@ -160,6 +162,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;
}
@ -180,7 +195,7 @@ const Account = ({
return (
<div data-testid='account' className='group block w-full shrink-0' 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 +208,14 @@ const Account = ({
<Avatar src={account.avatar} size={avatarSize} />
{emoji && (
<Emoji
className='absolute bottom-0 -right-1.5 h-5 w-5'
className='absolute -bottom-1.5 -right-1.5 h-5 w-5'
emoji={emoji}
src={emojiUrl}
/>
)}
</LinkEl>
</ProfilePopper>
<div className='grow overflow-hidden'>
<div className='grow'>
<ProfilePopper
condition={showProfileHoverCard}
wrapper={(children) => <HoverRefWrapper accountId={account.id} inline>{children}</HoverRefWrapper>}
@ -211,7 +225,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 +241,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 && (

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 }) => {

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,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 }) => {

Wyświetl plik

@ -2,7 +2,7 @@ import clsx 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 }) => {

Wyświetl plik

@ -2,28 +2,29 @@ import clsx 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 });
@ -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 { Portal } from '@reach/portal';
import clsx 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> {

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 { Portal } from '@reach/portal';
import clsx 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> {

Wyświetl plik

@ -2,8 +2,8 @@ import clsx 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 }) => {

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'>

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

@ -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 clsx 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={clsx('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 h-5 w-5 flex-none rtl:ml-3 rtl:mr-0' />}
<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={clsx({
'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 clsx 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={clsx('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 clsx 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={clsx('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> {

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

@ -0,0 +1,17 @@
import React from 'react';
interface IExtensionBadge {
/** File extension. */
ext: string,
}
/** Badge displaying a file extension. */
const ExtensionBadge: React.FC<IExtensionBadge> = ({ ext }) => {
return (
<div className='inline-flex items-center rounded bg-gray-100 px-2 py-0.5 text-sm font-medium text-gray-900 dark:bg-gray-800 dark:text-gray-100'>
{ext}
</div>
);
};
export default ExtensionBadge;

Wyświetl plik

@ -9,9 +9,9 @@ import clsx 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 }) => {

Wyświetl plik

@ -1,12 +1,7 @@
import React from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { FormattedMessage, 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 { Avatar, HStack, Icon, Stack, Text } from './ui';
import type { Group as GroupEntity } from 'soapbox/types/entities';
@ -22,42 +17,43 @@ 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}>
<div className='overflow-hidden'>
<Stack className='rounded-lg border border-solid border-gray-300 bg-white dark:border-primary-800 dark:bg-primary-900 sm:rounded-xl'>
<div className='relative -m-[1px] mb-0 h-[120px] rounded-t-lg bg-primary-100 dark:bg-gray-800 sm:rounded-t-xl'>
{group.header && <img className='h-full w-full rounded-t-lg object-cover sm:rounded-t-xl' src={group.header} alt={intl.formatMessage(messages.groupHeader)} />}
<div className='absolute left-1/2 bottom-0 -translate-x-1/2 translate-y-1/2'>
<Avatar className='ring-2 ring-white dark:ring-primary-900' src={group.avatar} size={64} />
</div>
</div>
<Stack className='p-3 pt-9' alignItems='center' space={3}>
<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>
<HStack className='text-gray-700 dark:text-gray-600' space={3} wrap>
{group.relationship?.role === 'admin' ? (
<HStack space={1} alignItems='center'>
<Icon className='h-4 w-4' src={require('@tabler/icons/users.svg')} />
<span><FormattedMessage id='group.role.admin' defaultMessage='Admin' /></span>
</HStack>
) : group.relationship?.role === 'moderator' && (
<HStack space={1} alignItems='center'>
<Icon className='h-4 w-4' src={require('@tabler/icons/gavel.svg')} />
<span><FormattedMessage id='group.role.moderator' defaultMessage='Moderator' /></span>
</HStack>
)}
{group.locked ? (
<HStack space={1} alignItems='center'>
<Icon className='h-4 w-4' src={require('@tabler/icons/lock.svg')} />
<span><FormattedMessage id='group.privacy.locked' defaultMessage='Private' /></span>
</HStack>
) : (
<HStack space={1} alignItems='center'>
<Icon className='h-4 w-4' src={require('@tabler/icons/world.svg')} />
<span><FormattedMessage id='group.privacy.public' defaultMessage='Public' /></span>
</HStack>
)}
</HStack>
</Stack>
</Stack>
</Stack>
</div>
);
};

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

@ -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. */

Wyświetl plik

@ -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. */

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

@ -8,15 +8,12 @@ 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

Wyświetl plik

@ -4,7 +4,8 @@ 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
@ -57,13 +58,13 @@ 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',
'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

@ -18,7 +18,7 @@ const messages = defineMessages({
});
interface ILocationSearch {
onSelected: (locationId: string) => void
onSelected: (locationId: string) => void,
}
const LocationSearch: React.FC<ILocationSearch> = ({ onSelected }) => {

Wyświetl plik

@ -1,298 +1,31 @@
import clsx from 'clsx';
import React, { useState, useRef, useLayoutEffect } from 'react';
import Blurhash from 'soapbox/components/blurhash';
import Icon from 'soapbox/components/icon';
import StillImage from 'soapbox/components/still-image';
import { MIMETYPE_ICONS } from 'soapbox/components/upload';
import { useSettings, useSoapboxConfig } from 'soapbox/hooks';
import { Attachment } from 'soapbox/types/entities';
import { truncateFilename } from 'soapbox/utils/media';
import { isIOS } from '../is-mobile';
import { isPanoramic, isPortrait, isNonConformingRatio, minimumAspectRatio, maximumAspectRatio } from '../utils/media-aspect-ratio';
import { ATTACHMENT_LIMIT } from './media-gallery/constants';
import MediaItem from './media-gallery/media-item';
import { useMediaSizeData } from './media-gallery/useMediaSizeData';
import type { Property } from 'csstype';
import type { List as ImmutableList } from 'immutable';
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
}
interface SizeData {
style: React.CSSProperties
itemsDimensions: Dimensions[]
size: number
width: number
}
const withinLimits = (aspectRatio: number) => {
return aspectRatio >= minimumAspectRatio && aspectRatio <= maximumAspectRatio;
};
const shouldLetterbox = (attachment: Attachment): boolean => {
const aspectRatio = attachment.getIn(['meta', 'original', 'aspect']) as number | undefined;
if (!aspectRatio) return true;
return !withinLimits(aspectRatio);
};
interface IItem {
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> = ({
attachment,
index,
onClick,
standalone = false,
visible,
dimensions,
last,
total,
}) => {
const settings = useSettings();
const autoPlayGif = settings.get('autoPlayGif') === true;
const { mediaPreview } = useSoapboxConfig();
const handleMouseEnter: React.MouseEventHandler<HTMLVideoElement> = ({ currentTarget: video }) => {
if (hoverToPlay()) {
video.play();
}
};
const handleMouseLeave: React.MouseEventHandler<HTMLVideoElement> = ({ currentTarget: video }) => {
if (hoverToPlay()) {
video.pause();
video.currentTime = 0;
}
};
const hoverToPlay = () => {
return !autoPlayGif && attachment.type === 'gifv';
};
// FIXME: wtf?
const handleClick: React.MouseEventHandler = (e: any) => {
if (isIOS() && !e.target.autoPlay) {
e.target.autoPlay = true;
e.preventDefault();
} else {
if (e.button === 0 && !(e.ctrlKey || e.metaKey)) {
if (hoverToPlay()) {
e.target.pause();
e.target.currentTime = 0;
}
e.preventDefault();
onClick(index);
}
}
e.stopPropagation();
};
const handleVideoHover: React.MouseEventHandler<HTMLVideoElement> = ({ currentTarget: video }) => {
video.playbackRate = 3.0;
video.play();
};
const handleVideoLeave: React.MouseEventHandler<HTMLVideoElement> = ({ currentTarget: video }) => {
video.pause();
video.currentTime = 0;
};
let width: Dimensions['w'] = 100;
let height: Dimensions['h'] = '100%';
let top: Dimensions['t'] = 'auto';
let left: Dimensions['l'] = 'auto';
let bottom: Dimensions['b'] = 'auto';
let right: Dimensions['r'] = 'auto';
let float: Dimensions['float'] = 'left';
let position: Dimensions['pos'] = 'relative';
if (dimensions) {
width = dimensions.w;
height = dimensions.h;
top = dimensions.t || 'auto';
right = dimensions.r || 'auto';
bottom = dimensions.b || 'auto';
left = dimensions.l || 'auto';
float = dimensions.float || 'left';
position = dimensions.pos || 'relative';
}
let thumbnail: React.ReactNode = '';
if (attachment.type === 'unknown') {
const filename = truncateFilename(attachment.url, MAX_FILENAME_LENGTH);
const attachmentIcon = (
<Icon
className='h-16 w-16 text-gray-800 dark:text-gray-200'
src={MIMETYPE_ICONS[attachment.getIn(['pleroma', 'mime_type']) as string] || require('@tabler/icons/paperclip.svg')}
/>
);
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}%` }}
>
<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>
<span className='media-gallery__filename__label'>{filename}</span>
</a>
</div>
);
} else if (attachment.type === 'image') {
const letterboxed = total === 1 && shouldLetterbox(attachment);
thumbnail = (
<a
className='media-gallery__item-thumbnail'
href={attachment.url}
onClick={handleClick}
target='_blank'
>
<StillImage
className='h-full w-full'
src={mediaPreview ? attachment.preview_url : attachment.url}
alt={attachment.description}
letterboxed={letterboxed}
showExt
/>
</a>
);
} else if (attachment.type === 'gifv') {
const conditionalAttributes: React.VideoHTMLAttributes<HTMLVideoElement> = {};
if (isIOS()) {
conditionalAttributes.playsInline = true;
}
if (autoPlayGif) {
conditionalAttributes.autoPlay = true;
}
thumbnail = (
<div className={clsx('media-gallery__gifv', { autoplay: autoPlayGif })}>
<video
className='media-gallery__item-gifv-thumbnail'
aria-label={attachment.description}
title={attachment.description}
role='application'
src={attachment.url}
onClick={handleClick}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
loop
muted
{...conditionalAttributes}
/>
<span className='media-gallery__gifv__label'>GIF</span>
</div>
);
} else if (attachment.type === 'audio') {
const ext = attachment.url.split('.').pop()?.toUpperCase();
thumbnail = (
<a
className={clsx('media-gallery__item-thumbnail')}
href={attachment.url}
onClick={handleClick}
target='_blank'
title={attachment.description}
>
<span className='media-gallery__item__icons'><Icon src={require('@tabler/icons/volume.svg')} /></span>
<span className='media-gallery__file-extension__label'>{ext}</span>
</a>
);
} else if (attachment.type === 'video') {
const ext = attachment.url.split('.').pop()?.toUpperCase();
thumbnail = (
<a
className={clsx('media-gallery__item-thumbnail')}
href={attachment.url}
onClick={handleClick}
target='_blank'
title={attachment.description}
>
<video
muted
loop
onMouseOver={handleVideoHover}
onMouseOut={handleVideoLeave}
>
<source src={attachment.url} />
</video>
<span className='media-gallery__file-extension__label'>{ext}</span>
</a>
);
}
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}%` }}
>
{last && total > ATTACHMENT_LIMIT && (
<div className='media-gallery__item-overflow'>
+{total - ATTACHMENT_LIMIT + 1}
</div>
)}
<Blurhash
hash={attachment.blurhash}
className='media-gallery__preview'
/>
{visible && thumbnail}
</div>
);
};
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,
@ -301,241 +34,14 @@ const MediaGallery: React.FC<IMediaGallery> = (props) => {
const [width, setWidth] = useState<number>(defaultWidth);
const node = useRef<HTMLDivElement>(null);
const sizeData = useMediaSizeData({ media, width, height, defaultWidth });
const handleClick = (index: number) => {
onOpenMedia(media, index);
};
const getSizeDataSingle = (): SizeData => {
const w = width || defaultWidth;
const aspectRatio = media.getIn([0, 'meta', 'original', 'aspect']) as number | undefined;
const getHeight = () => {
if (!aspectRatio) return w * 9 / 16;
if (isPanoramic(aspectRatio)) return Math.floor(w / maximumAspectRatio);
if (isPortrait(aspectRatio)) return Math.floor(w / minimumAspectRatio);
return Math.floor(w / aspectRatio);
};
return {
style: { height: getHeight() },
itemsDimensions: [],
size: 1,
width,
};
};
const getSizeDataMultiple = (size: number): SizeData => {
const w = width || defaultWidth;
const panoSize = Math.floor(w / maximumAspectRatio);
const panoSize_px = `${Math.floor(w / maximumAspectRatio)}px`;
const style: React.CSSProperties = {};
let itemsDimensions: Dimensions[] = [];
const ratios = Array(size).fill(null).map((_, i) =>
media.getIn([i, 'meta', 'original', 'aspect']) as number,
);
const [ar1, ar2, ar3, ar4] = ratios;
if (size === 2) {
if (isPortrait(ar1) && isPortrait(ar2)) {
style.height = w - (w / maximumAspectRatio);
} else if (isPanoramic(ar1) && isPanoramic(ar2)) {
style.height = panoSize * 2;
} else if (
(isPanoramic(ar1) && isPortrait(ar2)) ||
(isPortrait(ar1) && isPanoramic(ar2)) ||
(isPanoramic(ar1) && isNonConformingRatio(ar2)) ||
(isNonConformingRatio(ar1) && isPanoramic(ar2))
) {
style.height = (w * 0.6) + (w / maximumAspectRatio);
} else {
style.height = w / 2;
}
if (isPortrait(ar1) && isPortrait(ar2)) {
itemsDimensions = [
{ w: 50, h: '100%', r: '2px' },
{ w: 50, h: '100%', l: '2px' },
];
} else if (isPanoramic(ar1) && isPanoramic(ar2)) {
itemsDimensions = [
{ w: 100, h: panoSize_px, b: '2px' },
{ w: 100, h: panoSize_px, t: '2px' },
];
} else if (
(isPanoramic(ar1) && isPortrait(ar2)) ||
(isPanoramic(ar1) && isNonConformingRatio(ar2))
) {
itemsDimensions = [
{ w: 100, h: `${(w / maximumAspectRatio)}px`, b: '2px' },
{ w: 100, h: `${(w * 0.6)}px`, t: '2px' },
];
} else if (
(isPortrait(ar1) && isPanoramic(ar2)) ||
(isNonConformingRatio(ar1) && isPanoramic(ar2))
) {
itemsDimensions = [
{ w: 100, h: `${(w * 0.6)}px`, b: '2px' },
{ w: 100, h: `${(w / maximumAspectRatio)}px`, t: '2px' },
];
} else {
itemsDimensions = [
{ w: 50, h: '100%', r: '2px' },
{ w: 50, h: '100%', l: '2px' },
];
}
} else if (size === 3) {
if (isPanoramic(ar1) && isPanoramic(ar2) && isPanoramic(ar3)) {
style.height = panoSize * 3;
} else if (isPortrait(ar1) && isPortrait(ar2) && isPortrait(ar3)) {
style.height = Math.floor(w / minimumAspectRatio);
} else {
style.height = w;
}
if (isPanoramic(ar1) && isNonConformingRatio(ar2) && isNonConformingRatio(ar3)) {
itemsDimensions = [
{ w: 100, h: '50%', b: '2px' },
{ w: 50, h: '50%', t: '2px', r: '2px' },
{ w: 50, h: '50%', t: '2px', l: '2px' },
];
} else if (isPanoramic(ar1) && isPanoramic(ar2) && isPanoramic(ar3)) {
itemsDimensions = [
{ w: 100, h: panoSize_px, b: '4px' },
{ w: 100, h: panoSize_px },
{ w: 100, h: panoSize_px, t: '4px' },
];
} else if (isPortrait(ar1) && isNonConformingRatio(ar2) && isNonConformingRatio(ar3)) {
itemsDimensions = [
{ w: 50, h: '100%', r: '2px' },
{ w: 50, h: '50%', b: '2px', l: '2px' },
{ w: 50, h: '50%', t: '2px', l: '2px' },
];
} else if (isNonConformingRatio(ar1) && isNonConformingRatio(ar2) && isPortrait(ar3)) {
itemsDimensions = [
{ w: 50, h: '50%', b: '2px', r: '2px' },
{ w: 50, h: '50%', l: '-2px', b: '-2px', pos: 'absolute', float: 'none' },
{ w: 50, h: '100%', r: '-2px', t: '0px', b: '0px', pos: 'absolute', float: 'none' },
];
} else if (
(isNonConformingRatio(ar1) && isPortrait(ar2) && isNonConformingRatio(ar3)) ||
(isPortrait(ar1) && isPortrait(ar2) && isPortrait(ar3))
) {
itemsDimensions = [
{ w: 50, h: '50%', b: '2px', r: '2px' },
{ w: 50, h: '100%', l: '2px', float: 'right' },
{ w: 50, h: '50%', t: '2px', r: '2px' },
];
} else if (
(isPanoramic(ar1) && isPanoramic(ar2) && isNonConformingRatio(ar3)) ||
(isPanoramic(ar1) && isPanoramic(ar2) && isPortrait(ar3))
) {
itemsDimensions = [
{ w: 50, h: panoSize_px, b: '2px', r: '2px' },
{ w: 50, h: panoSize_px, b: '2px', l: '2px' },
{ w: 100, h: `${w - panoSize}px`, t: '2px' },
];
} else if (
(isNonConformingRatio(ar1) && isPanoramic(ar2) && isPanoramic(ar3)) ||
(isPortrait(ar1) && isPanoramic(ar2) && isPanoramic(ar3))
) {
itemsDimensions = [
{ w: 100, h: `${w - panoSize}px`, b: '2px' },
{ w: 50, h: panoSize_px, t: '2px', r: '2px' },
{ w: 50, h: panoSize_px, t: '2px', l: '2px' },
];
} else {
itemsDimensions = [
{ w: 50, h: '50%', b: '2px', r: '2px' },
{ w: 50, h: '50%', b: '2px', l: '2px' },
{ w: 100, h: '50%', t: '2px' },
];
}
} else if (size >= 4) {
if (
(isPortrait(ar1) && isPortrait(ar2) && isPortrait(ar3) && isPortrait(ar4)) ||
(isPortrait(ar1) && isPortrait(ar2) && isPortrait(ar3) && isNonConformingRatio(ar4)) ||
(isPortrait(ar1) && isPortrait(ar2) && isNonConformingRatio(ar3) && isPortrait(ar4)) ||
(isPortrait(ar1) && isNonConformingRatio(ar2) && isPortrait(ar3) && isPortrait(ar4)) ||
(isNonConformingRatio(ar1) && isPortrait(ar2) && isPortrait(ar3) && isPortrait(ar4))
) {
style.height = Math.floor(w / minimumAspectRatio);
} else if (isPanoramic(ar1) && isPanoramic(ar2) && isPanoramic(ar3) && isPanoramic(ar4)) {
style.height = panoSize * 2;
} else if (
(isPanoramic(ar1) && isPanoramic(ar2) && isNonConformingRatio(ar3) && isNonConformingRatio(ar4)) ||
(isNonConformingRatio(ar1) && isNonConformingRatio(ar2) && isPanoramic(ar3) && isPanoramic(ar4))
) {
style.height = panoSize + (w / 2);
} else {
style.height = w;
}
if (isPanoramic(ar1) && isPanoramic(ar2) && isNonConformingRatio(ar3) && isNonConformingRatio(ar4)) {
itemsDimensions = [
{ w: 50, h: panoSize_px, b: '2px', r: '2px' },
{ w: 50, h: panoSize_px, b: '2px', l: '2px' },
{ w: 50, h: `${(w / 2)}px`, t: '2px', r: '2px' },
{ w: 50, h: `${(w / 2)}px`, t: '2px', l: '2px' },
];
} else if (isNonConformingRatio(ar1) && isNonConformingRatio(ar2) && isPanoramic(ar3) && isPanoramic(ar4)) {
itemsDimensions = [
{ w: 50, h: `${(w / 2)}px`, b: '2px', r: '2px' },
{ w: 50, h: `${(w / 2)}px`, b: '2px', l: '2px' },
{ w: 50, h: panoSize_px, t: '2px', r: '2px' },
{ w: 50, h: panoSize_px, t: '2px', l: '2px' },
];
} else if (
(isPortrait(ar1) && isNonConformingRatio(ar2) && isNonConformingRatio(ar3) && isNonConformingRatio(ar4)) ||
(isPortrait(ar1) && isPanoramic(ar2) && isPanoramic(ar3) && isPanoramic(ar4))
) {
itemsDimensions = [
{ w: 67, h: '100%', r: '2px' },
{ w: 33, h: '33%', b: '4px', l: '2px' },
{ w: 33, h: '33%', l: '2px' },
{ w: 33, h: '33%', t: '4px', l: '2px' },
];
} else {
itemsDimensions = [
{ w: 50, h: '50%', b: '2px', r: '2px' },
{ w: 50, h: '50%', b: '2px', l: '2px' },
{ w: 50, h: '50%', t: '2px', r: '2px' },
{ w: 50, h: '50%', t: '2px', l: '2px' },
];
}
}
return {
style,
itemsDimensions,
size,
width: w,
};
};
const getSizeData = (size: number): Readonly<SizeData> => {
const w = width || defaultWidth;
if (w) {
if (size === 1) return getSizeDataSingle();
if (size > 1) return getSizeDataMultiple(size);
}
return {
style: { height },
itemsDimensions: [],
size,
width: w,
};
};
const sizeData: SizeData = getSizeData(media.size);
const children = media.take(ATTACHMENT_LIMIT).map((attachment, i) => (
<Item
<MediaItem
key={attachment.id}
onClick={handleClick}
attachment={attachment}
@ -562,11 +68,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={clsx('media-gallery', { 'media-gallery--compact': compact })} style={sizeData.style} ref={node}>
{children}
</div>
);

Wyświetl plik

@ -0,0 +1,2 @@
export const ATTACHMENT_LIMIT = 4;
export const MAX_FILENAME_LENGTH = 45;

Wyświetl plik

@ -0,0 +1,91 @@
import clsx from 'clsx';
import React from 'react';
import Blurhash from 'soapbox/components/blurhash';
import MediaPreview from '../media/media-preview';
import { ATTACHMENT_LIMIT } from './constants';
import MoreMediaOverlay from './more-media-overlay';
import type { Dimensions } from './types';
import type { Attachment } from 'soapbox/types/entities';
interface IMediaItem {
attachment: Attachment,
standalone?: boolean,
index: number,
size: number,
onClick: (index: number) => void,
displayWidth?: number,
visible: boolean,
dimensions: Dimensions,
last?: boolean,
total: number,
}
const MediaItem: React.FC<IMediaItem> = ({
attachment,
index,
onClick,
standalone = false,
visible,
dimensions,
last,
total,
}) => {
const handleClick: React.MouseEventHandler<HTMLButtonElement> = (e) => {
onClick(index);
e.stopPropagation();
};
let width: Dimensions['w'] = 100;
let height: Dimensions['h'] = '100%';
let top: Dimensions['t'] = 'auto';
let left: Dimensions['l'] = 'auto';
let bottom: Dimensions['b'] = 'auto';
let right: Dimensions['r'] = 'auto';
let float: Dimensions['float'] = 'left';
let position: Dimensions['pos'] = 'relative';
if (dimensions) {
width = dimensions.w;
height = dimensions.h;
top = dimensions.t || 'auto';
right = dimensions.r || 'auto';
bottom = dimensions.b || 'auto';
left = dimensions.l || 'auto';
float = dimensions.float || 'left';
position = dimensions.pos || 'relative';
}
return (
<div
key={attachment.id}
className={clsx('media-gallery__item', `media-gallery__item--${attachment.type}`, { standalone })}
style={{ position, float, left, top, right, bottom, height, width: `${width}%` }}
>
{last && total > ATTACHMENT_LIMIT && (
<MoreMediaOverlay
count={total - ATTACHMENT_LIMIT + 1}
/>
)}
<Blurhash
hash={attachment.blurhash}
className='pointer-events-none absolute inset-0 -z-10 h-full w-full'
/>
{visible && (
<button
onClick={handleClick}
className='h-full w-full cursor-zoom-in'
>
<MediaPreview attachment={attachment} />
</button>
)}
</div>
);
};
export default MediaItem;

Wyświetl plik

@ -0,0 +1,16 @@
import React from 'react';
interface IMoreMediaOverlay {
count: number
}
/** Overlay on the final image in a gallery to indicate more images can be seen by clicking into the modal. */
const MoreMediaOverlay: React.FC<IMoreMediaOverlay> = ({ count }) => {
return (
<div className='pointer-events-none absolute inset-0 z-20 flex h-full w-full items-center justify-center bg-white/75 text-center text-5xl font-bold text-gray-900'>
<span>+{count}</span>
</div>
);
};
export default MoreMediaOverlay;

Wyświetl plik

@ -0,0 +1,24 @@
import type { Property } from 'csstype';
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,
}
interface SizeData {
style: React.CSSProperties,
itemsDimensions: Dimensions[],
size: number,
width: number,
}
export type {
Dimensions,
SizeData,
};

Wyświetl plik

@ -0,0 +1,242 @@
import { containAspectRatio, isNonConformingRatio, isPanoramic, isPortrait, maximumAspectRatio, minimumAspectRatio } from 'soapbox/utils/media-aspect-ratio';
import type { Dimensions, SizeData } from './types';
import type { List as ImmutableList } from 'immutable';
import type { Attachment } from 'soapbox/types/entities';
interface UseMediaSizeDataOpts {
width: number
height?: number
defaultWidth: number
media: ImmutableList<Attachment>
}
const useMediaSizeData = ({ width, height, defaultWidth, media }: UseMediaSizeDataOpts) => {
const getSizeDataSingle = (): SizeData => {
const w = width || defaultWidth;
const aspectRatio = media.getIn([0, 'meta', 'original', 'aspect']) as number | undefined;
const getHeight = () => {
if (!aspectRatio) return w * 9 / 16;
return Math.floor(w / containAspectRatio(aspectRatio));
};
return {
style: { height: getHeight() },
itemsDimensions: [],
size: 1,
width,
};
};
const getSizeDataMultiple = (size: number): SizeData => {
const w = width || defaultWidth;
const panoSize = Math.floor(w / maximumAspectRatio);
const panoSize_px = `${Math.floor(w / maximumAspectRatio)}px`;
const style: React.CSSProperties = {};
let itemsDimensions: Dimensions[] = [];
const ratios = Array(size).fill(null).map((_, i) =>
media.getIn([i, 'meta', 'original', 'aspect']) as number,
);
const [ar1, ar2, ar3, ar4] = ratios;
if (size === 2) {
if (isPortrait(ar1) && isPortrait(ar2)) {
style.height = w - (w / maximumAspectRatio);
} else if (isPanoramic(ar1) && isPanoramic(ar2)) {
style.height = panoSize * 2;
} else if (
(isPanoramic(ar1) && isPortrait(ar2)) ||
(isPortrait(ar1) && isPanoramic(ar2)) ||
(isPanoramic(ar1) && isNonConformingRatio(ar2)) ||
(isNonConformingRatio(ar1) && isPanoramic(ar2))
) {
style.height = (w * 0.6) + (w / maximumAspectRatio);
} else {
style.height = w / 2;
}
if (isPortrait(ar1) && isPortrait(ar2)) {
itemsDimensions = [
{ w: 50, h: '100%', r: '2px' },
{ w: 50, h: '100%', l: '2px' },
];
} else if (isPanoramic(ar1) && isPanoramic(ar2)) {
itemsDimensions = [
{ w: 100, h: panoSize_px, b: '2px' },
{ w: 100, h: panoSize_px, t: '2px' },
];
} else if (
(isPanoramic(ar1) && isPortrait(ar2)) ||
(isPanoramic(ar1) && isNonConformingRatio(ar2))
) {
itemsDimensions = [
{ w: 100, h: `${(w / maximumAspectRatio)}px`, b: '2px' },
{ w: 100, h: `${(w * 0.6)}px`, t: '2px' },
];
} else if (
(isPortrait(ar1) && isPanoramic(ar2)) ||
(isNonConformingRatio(ar1) && isPanoramic(ar2))
) {
itemsDimensions = [
{ w: 100, h: `${(w * 0.6)}px`, b: '2px' },
{ w: 100, h: `${(w / maximumAspectRatio)}px`, t: '2px' },
];
} else {
itemsDimensions = [
{ w: 50, h: '100%', r: '2px' },
{ w: 50, h: '100%', l: '2px' },
];
}
} else if (size === 3) {
if (isPanoramic(ar1) && isPanoramic(ar2) && isPanoramic(ar3)) {
style.height = panoSize * 3;
} else if (isPortrait(ar1) && isPortrait(ar2) && isPortrait(ar3)) {
style.height = Math.floor(w / minimumAspectRatio);
} else {
style.height = w;
}
if (isPanoramic(ar1) && isNonConformingRatio(ar2) && isNonConformingRatio(ar3)) {
itemsDimensions = [
{ w: 100, h: '50%', b: '2px' },
{ w: 50, h: '50%', t: '2px', r: '2px' },
{ w: 50, h: '50%', t: '2px', l: '2px' },
];
} else if (isPanoramic(ar1) && isPanoramic(ar2) && isPanoramic(ar3)) {
itemsDimensions = [
{ w: 100, h: panoSize_px, b: '4px' },
{ w: 100, h: panoSize_px },
{ w: 100, h: panoSize_px, t: '4px' },
];
} else if (isPortrait(ar1) && isNonConformingRatio(ar2) && isNonConformingRatio(ar3)) {
itemsDimensions = [
{ w: 50, h: '100%', r: '2px' },
{ w: 50, h: '50%', b: '2px', l: '2px' },
{ w: 50, h: '50%', t: '2px', l: '2px' },
];
} else if (isNonConformingRatio(ar1) && isNonConformingRatio(ar2) && isPortrait(ar3)) {
itemsDimensions = [
{ w: 50, h: '50%', b: '2px', r: '2px' },
{ w: 50, h: '50%', l: '-2px', b: '-2px', pos: 'absolute', float: 'none' },
{ w: 50, h: '100%', r: '-2px', t: '0px', b: '0px', pos: 'absolute', float: 'none' },
];
} else if (
(isNonConformingRatio(ar1) && isPortrait(ar2) && isNonConformingRatio(ar3)) ||
(isPortrait(ar1) && isPortrait(ar2) && isPortrait(ar3))
) {
itemsDimensions = [
{ w: 50, h: '50%', b: '2px', r: '2px' },
{ w: 50, h: '100%', l: '2px', float: 'right' },
{ w: 50, h: '50%', t: '2px', r: '2px' },
];
} else if (
(isPanoramic(ar1) && isPanoramic(ar2) && isNonConformingRatio(ar3)) ||
(isPanoramic(ar1) && isPanoramic(ar2) && isPortrait(ar3))
) {
itemsDimensions = [
{ w: 50, h: panoSize_px, b: '2px', r: '2px' },
{ w: 50, h: panoSize_px, b: '2px', l: '2px' },
{ w: 100, h: `${w - panoSize}px`, t: '2px' },
];
} else if (
(isNonConformingRatio(ar1) && isPanoramic(ar2) && isPanoramic(ar3)) ||
(isPortrait(ar1) && isPanoramic(ar2) && isPanoramic(ar3))
) {
itemsDimensions = [
{ w: 100, h: `${w - panoSize}px`, b: '2px' },
{ w: 50, h: panoSize_px, t: '2px', r: '2px' },
{ w: 50, h: panoSize_px, t: '2px', l: '2px' },
];
} else {
itemsDimensions = [
{ w: 50, h: '50%', b: '2px', r: '2px' },
{ w: 50, h: '50%', b: '2px', l: '2px' },
{ w: 100, h: '50%', t: '2px' },
];
}
} else if (size >= 4) {
if (
(isPortrait(ar1) && isPortrait(ar2) && isPortrait(ar3) && isPortrait(ar4)) ||
(isPortrait(ar1) && isPortrait(ar2) && isPortrait(ar3) && isNonConformingRatio(ar4)) ||
(isPortrait(ar1) && isPortrait(ar2) && isNonConformingRatio(ar3) && isPortrait(ar4)) ||
(isPortrait(ar1) && isNonConformingRatio(ar2) && isPortrait(ar3) && isPortrait(ar4)) ||
(isNonConformingRatio(ar1) && isPortrait(ar2) && isPortrait(ar3) && isPortrait(ar4))
) {
style.height = Math.floor(w / minimumAspectRatio);
} else if (isPanoramic(ar1) && isPanoramic(ar2) && isPanoramic(ar3) && isPanoramic(ar4)) {
style.height = panoSize * 2;
} else if (
(isPanoramic(ar1) && isPanoramic(ar2) && isNonConformingRatio(ar3) && isNonConformingRatio(ar4)) ||
(isNonConformingRatio(ar1) && isNonConformingRatio(ar2) && isPanoramic(ar3) && isPanoramic(ar4))
) {
style.height = panoSize + (w / 2);
} else {
style.height = w;
}
if (isPanoramic(ar1) && isPanoramic(ar2) && isNonConformingRatio(ar3) && isNonConformingRatio(ar4)) {
itemsDimensions = [
{ w: 50, h: panoSize_px, b: '2px', r: '2px' },
{ w: 50, h: panoSize_px, b: '2px', l: '2px' },
{ w: 50, h: `${(w / 2)}px`, t: '2px', r: '2px' },
{ w: 50, h: `${(w / 2)}px`, t: '2px', l: '2px' },
];
} else if (isNonConformingRatio(ar1) && isNonConformingRatio(ar2) && isPanoramic(ar3) && isPanoramic(ar4)) {
itemsDimensions = [
{ w: 50, h: `${(w / 2)}px`, b: '2px', r: '2px' },
{ w: 50, h: `${(w / 2)}px`, b: '2px', l: '2px' },
{ w: 50, h: panoSize_px, t: '2px', r: '2px' },
{ w: 50, h: panoSize_px, t: '2px', l: '2px' },
];
} else if (
(isPortrait(ar1) && isNonConformingRatio(ar2) && isNonConformingRatio(ar3) && isNonConformingRatio(ar4)) ||
(isPortrait(ar1) && isPanoramic(ar2) && isPanoramic(ar3) && isPanoramic(ar4))
) {
itemsDimensions = [
{ w: 67, h: '100%', r: '2px' },
{ w: 33, h: '33%', b: '4px', l: '2px' },
{ w: 33, h: '33%', l: '2px' },
{ w: 33, h: '33%', t: '4px', l: '2px' },
];
} else {
itemsDimensions = [
{ w: 50, h: '50%', b: '2px', r: '2px' },
{ w: 50, h: '50%', b: '2px', l: '2px' },
{ w: 50, h: '50%', t: '2px', r: '2px' },
{ w: 50, h: '50%', t: '2px', l: '2px' },
];
}
}
return {
style,
itemsDimensions,
size,
width: w,
};
};
const getSizeData = (size: number): Readonly<SizeData> => {
const w = width || defaultWidth;
if (w) {
if (size === 1) return getSizeDataSingle();
if (size > 1) return getSizeDataMultiple(size);
}
return {
style: { height },
itemsDimensions: [],
size,
width: w,
};
};
return getSizeData(media.size);
};
export { useMediaSizeData };

Wyświetl plik

@ -1,5 +1,6 @@
import React from 'react';
import StillVideo from 'soapbox/components/still-video';
import { Icon } from 'soapbox/components/ui';
import { MIMETYPE_ICONS } from 'soapbox/components/upload';
@ -7,16 +8,16 @@ import type { Attachment } from 'soapbox/types/entities';
const defaultIcon = require('@tabler/icons/paperclip.svg');
interface IChatUploadPreview {
className?: string
interface IMediaPreview {
attachment: Attachment
withExt?: boolean
}
/**
* Displays a generic preview for an upload depending on its media type.
* Displays a generic preview for an attachment depending on its media type.
* It fills its container and is expected to be sized by its parent.
*/
const ChatUploadPreview: React.FC<IChatUploadPreview> = ({ className, attachment }) => {
const MediaPreview: React.FC<IMediaPreview> = ({ attachment, withExt }) => {
const mimeType = attachment.pleroma.get('mime_type') as string | undefined;
switch (attachment.type) {
@ -31,14 +32,10 @@ const ChatUploadPreview: React.FC<IChatUploadPreview> = ({ className, attachment
);
case 'video':
return (
<video
className='pointer-events-none h-full w-full object-cover'
src={attachment.preview_url}
autoPlay
playsInline
controls={false}
muted
loop
<StillVideo
className='h-full w-full object-cover'
src={attachment.url}
withExt={withExt}
/>
);
default:
@ -53,4 +50,4 @@ const ChatUploadPreview: React.FC<IChatUploadPreview> = ({ className, attachment
}
};
export default ChatUploadPreview;
export default MediaPreview;

Wyświetl plik

@ -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 }) => {

Wyświetl plik

@ -2,8 +2,8 @@ import clsx 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. */
@ -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

@ -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 }) => {
@ -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 => {

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

@ -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. */

Wyświetl plik

@ -2,10 +2,10 @@ import clsx 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 }) => {

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;
}
/**

Wyświetl plik

@ -3,7 +3,7 @@ import React from 'react';
import PullToRefresh from './pull-to-refresh';
interface IPullable {
children: React.ReactNode
children: React.ReactNode,
}
/**

Wyświetl plik

@ -23,11 +23,11 @@ const messages = defineMessages({
interface IQuotedStatus {
/** The quoted status entity. */
status?: StatusEntity
status?: StatusEntity,
/** Callback when cancelled (during compose). */
onCancel?: Function
onCancel?: Function,
/** Whether the status is shown in the post composer. */
compose?: boolean
compose?: boolean,
}
/** Status embedded in a quote post. */
@ -133,7 +133,7 @@ const QuotedStatus: React.FC<IQuotedStatus> = ({ status, onCancel, compose }) =>
collapsable
/>
{status.media_attachments.size > 0 && (
{(status.card || status.media_attachments.size > 0) && (
<StatusMedia
status={status}
muted={compose}

Wyświetl plik

@ -16,11 +16,11 @@ const RadioGroup = ({ onChange, children }: IRadioGroup) => {
};
interface IRadioItem {
label: React.ReactNode
hint?: React.ReactNode
value: string
checked: boolean
onChange?: React.ChangeEventHandler
label: React.ReactNode,
hint?: React.ReactNode,
value: string,
checked: boolean,
onChange?: React.ChangeEventHandler,
}
const RadioItem: React.FC<IRadioItem> = ({ label, hint, checked = false, onChange, value }) => {

Wyświetl plik

@ -113,14 +113,14 @@ const timeRemainingString = (intl: IntlShape, date: Date, now: number) => {
};
interface RelativeTimestampProps extends IText {
intl: IntlShape
timestamp: string
year?: number
futureDate?: boolean
intl: IntlShape,
timestamp: string,
year?: number,
futureDate?: boolean,
}
interface RelativeTimestampState {
now: number
now: number,
}
/** Displays a timestamp compared to the current time, eg "1m" for one minute ago. */

Wyświetl plik

@ -2,13 +2,13 @@ import React, { useCallback, useEffect, useRef, useState } from 'react';
interface ISafeEmbed {
/** Styles for the outer frame element. */
className?: string
className?: string,
/** Space-separate list of restrictions to ALLOW for the iframe. */
sandbox?: string
sandbox?: string,
/** Unique title for the iframe. */
title: string
title: string,
/** HTML body to embed. */
html?: string
html?: string,
}
/** Safely embeds arbitrary HTML content on the page (by putting it in an iframe). */

Wyświetl plik

@ -9,15 +9,15 @@ import { useSettings } from 'soapbox/hooks';
interface IScrollTopButton {
/** Callback when clicked, and also when scrolled to the top. */
onClick: () => void
onClick: () => void,
/** Number of unread items. */
count: number
count: number,
/** Message to display in the button (should contain a `{count}` value). */
message: MessageDescriptor
message: MessageDescriptor,
/** Distance from the top of the screen (scrolling down) before the button appears. */
threshold?: number
threshold?: number,
/** Distance from the top of the screen (scrolling up) before the action is triggered. */
autoloadThreshold?: number
autoloadThreshold?: number,
}
/** Floating new post counter above timelines, clicked to scroll to top. */

Wyświetl plik

@ -10,14 +10,14 @@ import { Card, Spinner } from './ui';
/** Custom Viruoso component context. */
type Context = {
itemClassName?: string
listClassName?: string
itemClassName?: string,
listClassName?: string,
}
/** Scroll position saved in sessionStorage. */
type SavedScrollPosition = {
index: number
offset: number
index: number,
offset: number,
}
/** Custom Virtuoso Item component representing a single scrollable item. */
@ -37,46 +37,44 @@ const List: Components<JSX.Element, Context>['List'] = React.forwardRef((props,
interface IScrollableList extends VirtuosoProps<any, any> {
/** Unique key to preserve the scroll position when navigating back. */
scrollKey?: string
scrollKey?: string,
/** Pagination callback when the end of the list is reached. */
onLoadMore?: () => void
onLoadMore?: () => void,
/** Whether the data is currently being fetched. */
isLoading?: boolean
isLoading?: boolean,
/** Whether to actually display the loading state. */
showLoading?: boolean
showLoading?: boolean,
/** Whether we expect an additional page of data. */
hasMore?: boolean
hasMore?: boolean,
/** Additional element to display at the top of the list. */
prepend?: React.ReactNode
prepend?: React.ReactNode,
/** Whether to display the prepended element. */
alwaysPrepend?: boolean
alwaysPrepend?: boolean,
/** Message to display when the list is loaded but empty. */
emptyMessage?: React.ReactNode
/** Should the empty message be displayed in a Card */
emptyMessageCard?: boolean
emptyMessage?: React.ReactNode,
/** Scrollable content. */
children: Iterable<React.ReactNode>
children: Iterable<React.ReactNode>,
/** Callback when the list is scrolled to the top. */
onScrollToTop?: () => void
onScrollToTop?: () => void,
/** Callback when the list is scrolled. */
onScroll?: () => void
onScroll?: () => void,
/** Placeholder component to render while loading. */
placeholderComponent?: React.ComponentType | React.NamedExoticComponent
placeholderComponent?: React.ComponentType | React.NamedExoticComponent,
/** Number of placeholders to render while loading. */
placeholderCount?: number
placeholderCount?: number,
/**
* Pull to refresh callback.
* @deprecated Put a PTR around the component instead.
*/
onRefresh?: () => Promise<any>
onRefresh?: () => Promise<any>,
/** Extra class names on the Virtuoso element. */
className?: string
className?: string,
/** Class names on each item container. */
itemClassName?: string
itemClassName?: string,
/** `id` attribute on the Virtuoso element. */
id?: string
id?: string,
/** CSS styles on the Virtuoso element. */
style?: React.CSSProperties
style?: React.CSSProperties,
/** Whether to use the window to scroll the content instead of Virtuoso's container. */
useWindowScroll?: boolean
}
@ -89,7 +87,6 @@ const ScrollableList = React.forwardRef<VirtuosoHandle, IScrollableList>(({
children,
isLoading,
emptyMessage,
emptyMessageCard = true,
showLoading,
onRefresh,
onScroll,
@ -161,17 +158,13 @@ const ScrollableList = React.forwardRef<VirtuosoHandle, IScrollableList>(({
<div className='mt-2'>
{alwaysPrepend && prepend}
{isLoading ? (
<Spinner />
) : (
<>
{emptyMessageCard ? (
<Card variant='rounded' size='lg'>
{emptyMessage}
</Card>
) : emptyMessage}
</>
)}
<Card variant='rounded' size='lg'>
{isLoading ? (
<Spinner />
) : (
emptyMessage
)}
</Card>
</div>
);
};

Wyświetl plik

@ -10,7 +10,7 @@ import { closeSidebar } from 'soapbox/actions/sidebar';
import Account from 'soapbox/components/account';
import { Stack } from 'soapbox/components/ui';
import ProfileStats from 'soapbox/features/ui/components/profile-stats';
import { useAppDispatch, useAppSelector, useGroupsPath, useFeatures } from 'soapbox/hooks';
import { useAppDispatch, useAppSelector, useFeatures } from 'soapbox/hooks';
import { makeGetAccount, makeGetOtherAccounts } from 'soapbox/selectors';
import { Divider, HStack, Icon, IconButton, Text } from './ui';
@ -43,11 +43,11 @@ const messages = defineMessages({
});
interface ISidebarLink {
href?: string
to?: string
icon: string
text: string | JSX.Element
onClick: React.EventHandler<React.MouseEvent>
href?: string,
to?: string,
icon: string,
text: string | JSX.Element,
onClick: React.EventHandler<React.MouseEvent>,
}
const SidebarLink: React.FC<ISidebarLink> = ({ href, to, icon, text, onClick }) => {
@ -90,7 +90,6 @@ const SidebarMenu: React.FC = (): JSX.Element | null => {
const sidebarOpen = useAppSelector((state) => state.sidebar.sidebarOpen);
const settings = useAppSelector((state) => getSettings(state));
const followRequestsCount = useAppSelector((state) => state.user_lists.follow_requests.items.count());
const groupsPath = useGroupsPath();
const closeButtonRef = React.useRef(null);
@ -211,7 +210,7 @@ const SidebarMenu: React.FC = (): JSX.Element | null => {
{features.groups && (
<SidebarLink
to={groupsPath}
to='/groups'
icon={require('@tabler/icons/circles.svg')}
text={intl.formatMessage(messages.groups)}
onClick={onClose}
@ -297,7 +296,7 @@ const SidebarMenu: React.FC = (): JSX.Element | null => {
/>
)}
{(features.filters || features.filtersV2) && (
{features.filters && (
<SidebarLink
to='/filters'
icon={require('@tabler/icons/filter.svg')}

Wyświetl plik

@ -6,17 +6,17 @@ import { Icon, Text } from './ui';
interface ISidebarNavigationLink {
/** Notification count, if any. */
count?: number
count?: number,
/** Optional max to cap count (ie: N+) */
countMax?: number
/** URL to an SVG icon. */
icon: string
icon: string,
/** Link label. */
text: React.ReactNode
text: React.ReactNode,
/** Route to an internal page. */
to?: string
to?: string,
/** Callback when the link is clicked. */
onClick?: React.EventHandler<React.MouseEvent>
onClick?: React.EventHandler<React.MouseEvent>,
}
/** Desktop sidebar navigation link. */

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