Merge remote-tracking branch 'origin/develop' into theme-editor

environments/review-theme-edit-1forjd/deployments/1766
Alex Gleason 2022-12-16 18:39:20 -06:00
commit 48bd830349
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 7211D1F99744FBB7
911 zmienionych plików z 38276 dodań i 29033 usunięć

1
.gitattributes vendored 100644
Wyświetl plik

@ -0,0 +1 @@
CHANGELOG.md merge=union

Wyświetl plik

@ -4,6 +4,79 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
### Added
- Editing: ability to edit posts and view edit history (on Rebased, Pleroma, and Mastodon).
- Events: ability to create, view, and comment on Events (on Rebased).
- Onboarding: display an introduction wizard to newly registered accounts.
- Posts: translate foreign language posts into your native language (on Rebased, Mastodon; if configured by the admin).
- Posts: hover the "replying to" line to see a preview card of the parent post.
- Chats: ability to leave a chat (on Rebased, Truth Social).
- Chats: ability to disable chats for yourself.
- Composer: support custom emoji categories.
- Search: ability to search posts from a specific account (on Pleroma, Rebased).
- Theme: auto-detect system theme by default.
- Profile: remove a specific user from your followers (on Rebased, Mastodon).
- Suggestions: ability to view all suggested profiles.
- Compatibility: added compatibility with Truth Social, Fedibird, Pixelfed, Akkoma, and Glitch.
- Developers: added Test feed, Service Worker debugger, and Network Error preview.
- Reports: display server rules in reports. Let users select rule violations when submitting a report.
- Admin: custom badges. Admins can add non-federating badges to any user's profile (on Rebased, Pleroma).
- Admin: consolidated user dropdown actions (verify/suggest/etc) into a unified "Moderate User" modal.
### Changed
- UI: the whole UI has been overhauled both inside and out. 97% of the codebase has been rewritten to TypeScript, and a new component library has been introduced with Tailwind CSS.
- Chats: redesigned chats. Includes an improved desktop UI, unified chat widget, expanding textarea, and autosuggestions.
- Lists: ability to edit and delete a list.
- Settings: unified settings under one path with separate sections.
- Posts: changed the thumbs-up icon to a heart.
- Posts: move instance favicon beside username instead of post timestamp.
- Posts: changed the behavior of content warnings. CWs and sensitive media are unified into one design.
- Profile: overhauled user profiles to be consistent with the rest of the UI.
- Composer: move emoji button alongside other composer buttons, add numerical counter.
- Birthdays: move today's birthdays out of notifications into right sidebar.
- Performance: improve scrolling/navigation between feeds by using a virtual window library.
### Removed
- Theme: Halloween theme.
- Settings: advanced notification settings.
- Settings: dyslexic mode.
- Settings: demetricator.
- Profile: ability to set and view private notes on an account.
- Feeds: per-feed filters for replies, media, etc.
- Backup and export functionality (for now).
- Posts: hide non-emoji images embedded in post content.
### Security
- Glitch Social: fixed XSS vulnerability on Glitch Social where custom emojis could be exploited to embed a script tag.
## [2.0.0] - 2022-05-01
### Added
- Quote Posting: repost with comment on Fedibird and Rebased.
- Profile: ability to feature other users on your profile (on Rebased, Mastodon).
- Profile: ability to add location to the user's profile (on Rebased, Truth Social).
- Birthdays: ability to add a birthday to your profile (on Rebased, Pleroma).
- Birthdays: support for age-gated registration if configured by the admin (on Rebased, Pleroma).
- Birthdays: display today's birthdays in notifications.
- Notifications: added unread badge to favicon when user has notifications.
- Notifications: display full attachments in notifications instead of links.
- Search: added a dedicated search page with prefilled suggestions.
- Compatibility: improved support for Mastodon, added support for Mitra.
- Ethereum: Metamask sign-in with Mitra.
- i18n: added Shavian alphabet (`en-Shaw`) transliteration.
### Changed
- Feeds: added gaps between posts in feeds.
- Feeds: automatically load new posts when scrolled to the top of the feed.
- Layout: improved design of top navigation bar.
- Layout: add left sidebar navigation.
- Icons: replaced Fork Awesome icons with Tabler icons.
- Posts: moved mentions out of the post content into an area above the post for replies (on Pleroma and Rebased - Mastodon falls back to the old behavior).
- Composer: use graphical ring counter for character count.
### Fixed
- Multi-Account: fix switching between profiles on different servers with the same local username.
## [1.3.0] - 2021-07-02
### Changed
- Layout: show right sidebar on all pages.

18
Dockerfile.dev 100644
Wyświetl plik

@ -0,0 +1,18 @@
FROM node:18
RUN apt-get update &&\
apt-get install -y inotify-tools &&\
# clean up apt
rm -rf /var/lib/apt/lists/*
WORKDIR /app
ENV NODE_ENV=development
COPY package.json .
COPY yarn.lock .
RUN yarn
COPY . .
ENV DEVSERVER_URL=http://0.0.0.0:3036
CMD yarn dev

Wyświetl plik

@ -28,6 +28,8 @@ busybox unzip soapbox.zip -o -d /opt/pleroma/instance
The change will take effect immediately, just refresh your browser tab.
It's not necessary to restart the Pleroma service.
***For OTP releases,*** *unpack to /var/lib/pleroma instead.*
To remove Soapbox and revert to the default pleroma-fe, simply `rm /opt/pleroma/instance/static/index.html` (you can delete other stuff in there too, but be careful not to delete your own HTML files).
## :elephant: Deploy on Mastodon

Wyświetl plik

@ -1,10 +1,10 @@
import loadPolyfills from './soapbox/load_polyfills';
import loadPolyfills from './soapbox/load-polyfills';
// Load iframe event listener
require('./soapbox/iframe');
// @ts-ignore
require.context('./images/', true);
require.context('./assets/images/', true);
// Load stylesheet
require('react-datepicker/dist/react-datepicker.css');

Wyświetl plik

@ -1,6 +1,5 @@
# Custom icons
- fediverse.svg - Modified from Wikipedia, CC0
- verified.svg - Created by Alex Gleason. CC0
Fediverse logo: https://en.wikipedia.org/wiki/Fediverse#/media/File:Fediverse_logo_proposal.svg

Wyświetl plik

Przed

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

Po

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

Wyświetl plik

Przed

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

Po

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

Wyświetl plik

Przed

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

Po

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

Wyświetl plik

Przed

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

Po

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

Wyświetl plik

Przed

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

Po

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

Wyświetl plik

Przed

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

Po

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

Wyświetl plik

Przed

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

Po

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

Wyświetl plik

Przed

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

Po

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

Wyświetl plik

Przed

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

Po

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

Wyświetl plik

Przed

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

Po

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

Wyświetl plik

Przed

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

Po

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

Wyświetl plik

Przed

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

Po

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

Plik binarny nie jest wyświetlany.

Wyświetl plik

@ -1,12 +0,0 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" >
<svg xmlns="http://www.w3.org/2000/svg">
<metadata>Generated by IcoMoon</metadata>
<defs>
<font id="icomoon" horiz-adv-x="1024">
<font-face units-per-em="1024" ascent="960" descent="-64" />
<missing-glyph horiz-adv-x="1024" />
<glyph unicode="&#x20;" horiz-adv-x="512" d="" />
<glyph unicode="&#xe900;" glyph-name="spinster" d="M512 857.6c-226.216 0-409.6-183.384-409.6-409.6v0c0-226.216 183.384-409.6 409.6-409.6v0c226.216 0 409.6 183.384 409.6 409.6v0c0 226.216-183.384 409.6-409.6 409.6v0zM525.84 706.224c32.633 0 64.793-3.777 96.48-11.344 31.687-7.095 59.576-17.748 83.696-31.936l-43.264-104.272c-47.294 25.539-93.176 38.304-137.632 38.304-27.903 0-48.239-4.255-61.008-12.768-12.769-8.040-19.152-18.677-19.152-31.92s7.57-23.171 22.704-29.792c15.134-6.621 39.493-13.482 73.072-20.576 37.835-8.040 69.039-16.797 93.632-26.256 25.066-8.986 46.588-23.648 64.56-43.984 18.445-19.863 27.664-47.059 27.664-81.584 0-29.795-8.279-56.744-24.832-80.864s-41.374-43.515-74.48-58.176c-33.106-14.188-73.314-21.28-120.608-21.28-40.2 0-79.205 4.964-117.040 14.896s-68.577 23.175-92.224 39.728l46.112 103.568c22.228-14.661 48.006-26.486 77.328-35.472s58.168-13.472 86.544-13.472c53.915 0 80.864 13.475 80.864 40.432 0 14.188-7.801 24.595-23.408 31.216-15.134 7.094-39.724 14.432-73.776 22-37.362 8.040-68.582 16.551-93.648 25.536-25.066 9.459-46.572 24.352-64.544 44.688s-26.96 47.763-26.96 82.288c0 30.268 8.279 57.464 24.832 81.584 16.553 24.592 41.143 43.988 73.776 58.176 33.106 14.188 73.545 21.28 121.312 21.28z" />
<glyph unicode="&#xe901;" glyph-name="fediverse" d="M553.99 908.789c-46.369-0.785-83.969-37.261-86.545-83.096l-0.010-0.231c-0.083-1.432-0.13-3.108-0.13-4.794 0-46.987 36.77-85.385 83.105-87.99l0.231-0.010c1.432-0.083 3.107-0.13 4.794-0.13 46.988 0 85.387 36.772 87.99 83.108l0.010 0.231c0.083 1.431 0.13 3.106 0.13 4.791 0 46.988-36.771 85.387-83.108 87.99l-0.231 0.010c-1.441 0.084-3.127 0.132-4.823 0.132-0.497 0-0.993-0.004-1.487-0.012l0.075 0.001zM459.882 805.031l-251.29-127.347c13.547-13.809 23-31.679 26.366-51.617l0.080-0.57 251.287 127.353c-13.545 13.808-22.997 31.675-26.363 51.611l-0.080 0.57zM641.318 775.903c-9.415-17.78-23.636-31.938-40.939-41.021l-0.532-0.254 198.787-199.554c9.415 17.78 23.634 31.938 40.936 41.021l0.532 0.254zM487.306 751.83l-147.023-287.024 43.408-43.576 155.667 303.891c-20.483 3.55-38.302 13.087-52.060 26.716l0.007-0.007zM599.388 734.397c-12.846-6.718-28.060-10.66-44.195-10.66-1.77 0-3.529 0.047-5.276 0.141l0.244-0.010c-3.232 0.199-6.15 0.516-9.026 0.959l0.542-0.069 22.259-142.535 60.737-9.746zM138.038 697.983c-46.37-0.783-83.972-37.26-86.548-83.095l-0.010-0.231c-0.083-1.432-0.13-3.107-0.13-4.794 0-46.988 36.772-85.387 83.108-87.99l0.231-0.010c1.432-0.083 3.107-0.13 4.794-0.13 46.988 0 85.387 36.772 87.99 83.108l0.010 0.231c0.083 1.432 0.13 3.107 0.13 4.794 0 46.988-36.772 85.387-83.108 87.99l-0.231 0.010c-1.43 0.083-3.103 0.13-4.787 0.13-0.51 0-1.018-0.004-1.526-0.013l0.076 0.001zM235.216 624.428c0.752-4.537 1.182-9.766 1.182-15.095 0-1.667-0.042-3.325-0.125-4.972l0.009 0.231c-0.796-13.969-4.43-26.918-10.33-38.52l0.254 0.551 142.645-22.911 28.036 54.751zM479.695 585.167l-28.039-54.757 337.040-54.13c-0.697 4.368-1.096 9.405-1.096 14.535 0 1.678 0.043 3.346 0.127 5.002l-0.009-0.232c0.815 14.158 4.546 27.272 10.597 38.992l-0.254-0.542zM883.076 578.43c-46.37-0.783-83.972-37.26-86.548-83.095l-0.010-0.231c-0.083-1.432-0.13-3.107-0.13-4.794 0-46.988 36.772-85.387 83.108-87.99l0.231-0.010c1.432-0.083 3.107-0.13 4.794-0.13 46.988 0 85.387 36.772 87.99 83.108l0.010 0.231c0.083 1.432 0.13 3.107 0.13 4.794 0 46.988-36.772 85.387-83.108 87.99l-0.231 0.010c-1.438 0.084-3.119 0.131-4.812 0.131-0.501 0-1-0.004-1.499-0.012l0.075 0.001zM225.366 565.098c-9.414-17.777-23.632-31.933-40.931-41.016l-0.532-0.254 227.623-228.511 54.877 27.811zM182.639 523.19c-12.642-6.466-27.577-10.256-43.397-10.256-1.77 0-3.529 0.047-5.276 0.141l0.244-0.010c-3.521 0.199-6.741 0.548-9.909 1.050l0.551-0.072 43.485-278.147c12.642 6.466 27.577 10.256 43.397 10.256 1.77 0 3.529-0.047 5.276-0.141l-0.244 0.010c3.519-0.2 6.737-0.548 9.903-1.050l-0.55 0.072zM576.873 499.359l52.629-336.996c12.457 6.245 27.143 9.902 42.682 9.902 1.773 0 3.535-0.048 5.285-0.142l-0.244 0.010c3.8-0.219 7.286-0.616 10.711-1.192l-0.569 0.079-49.754 318.595zM788.965 474.681l-128.865-65.308 9.501-60.776 145.806 73.896c-13.546 13.809-22.998 31.679-26.363 51.617l-0.080 0.57zM816.386 421.477l-128.362-250.594c20.486-3.55 38.307-13.087 52.065-26.719l-0.007 0.007 128.359 250.591c-20.485 3.551-38.305 13.090-52.062 26.722l0.007-0.007zM302.044 390.153l-74.471-145.382c20.481-3.55 38.298-13.086 52.054-26.714l-0.007 0.007 65.83 128.515zM585.292 371.462l-304.691-154.416c13.549-13.81 23.003-31.682 26.368-51.622l0.080-0.57 287.744 145.83zM525.607 263.696l-54.877-27.811 115.337-115.788c9.415 17.78 23.636 31.938 40.939 41.021l0.532 0.254zM210.049 237.339c-46.369-0.785-83.969-37.261-86.545-83.096l-0.010-0.231c-0.083-1.432-0.13-3.107-0.13-4.794 0-46.988 36.772-85.387 83.108-87.99l0.231-0.010c1.432-0.083 3.107-0.13 4.794-0.13 46.988 0 85.387 36.772 87.99 83.108l0.010 0.231c0.083 1.432 0.13 3.107 0.13 4.794 0 46.988-36.772 85.387-83.108 87.99l-0.231 0.010c-1.438 0.084-3.12 0.132-4.813 0.132-0.501 0-1.002-0.004-1.501-0.013l0.075 0.001zM307.279 163.476c0.72-4.438 1.131-9.554 1.131-14.766 0-1.675-0.042-3.34-0.126-4.993l0.009 0.232c-0.806-14.078-4.495-27.122-10.481-38.793l0.254 0.546 278.1-44.626c-0.721 4.442-1.133 9.563-1.133 14.779 0 1.671 0.042 3.332 0.126 4.983l-0.009-0.232c0.807 14.078 4.497 27.12 10.484 38.79l-0.254-0.546zM670.509 163.451c-46.37-0.783-83.972-37.26-86.548-83.095l-0.010-0.231c-0.083-1.432-0.13-3.107-0.13-4.794 0-46.988 36.772-85.387 83.108-87.99l0.231-0.010c1.432-0.083 3.107-0.13 4.794-0.13 46.988 0 85.387 36.772 87.99 83.108l0.010 0.231c0.083 1.432 0.13 3.107 0.13 4.794 0 46.988-36.772 85.387-83.108 87.99l-0.231 0.010c-1.438 0.084-3.119 0.131-4.812 0.131-0.501 0-1-0.004-1.499-0.012l0.075 0.001z" />
</font></defs></svg>

Przed

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

Plik binarny nie jest wyświetlany.

Plik binarny nie jest wyświetlany.

Wyświetl plik

@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1792" height="1792" viewBox="0 0 1792 1792" fill="currentColor">
<path d="M 343.16176,591.34767 A 171.09965,171.09965 0 0 1 163.0094,752.88799 171.09965,171.09965 0 0 1 1.4690156,572.73567 171.09965,171.09965 0 0 1 181.62138,411.19523 171.09965,171.09965 0 0 1 343.16176,591.34767 Z M 482.96755,1485.6494 A 171.09965,171.09965 0 0 1 302.81519,1647.1895 171.09965,171.09965 0 0 1 141.2748,1467.0372 171.09965,171.09965 0 0 1 321.42717,1305.4967 171.09965,171.09965 0 0 1 482.96755,1485.6494 Z m 893.94285,143.4473 a 171.09965,171.09965 0 0 1 -180.1523,161.5405 171.09965,171.09965 0 0 1 -161.5404,-180.1524 171.09965,171.09965 0 0 1 180.1524,-161.5405 171.09965,171.09965 0 0 1 161.5403,180.1524 z M 1789.5918,823.45087 A 171.09965,171.09965 0 0 1 1609.4395,984.99118 171.09965,171.09965 0 0 1 1447.899,804.83886 171.09965,171.09965 0 0 1 1628.0513,643.29854 171.09965,171.09965 0 0 1 1789.5918,823.45087 Z M 1150.7,182.08661 A 171.09965,171.09965 0 0 1 970.54747,343.627 171.09965,171.09965 0 0 1 809.00715,163.47462 171.09965,171.09965 0 0 1 989.15948,1.9342312 171.09965,171.09965 0 0 1 1150.7,182.08661 Z m -792.52346,371.6819 a 188.20963,188.20963 0 0 1 2.07029,38.5086 188.20963,188.20963 0 0 1 -19.56107,73.71432 l 276.93395,44.47923 54.4306,-106.2947 z m 474.63645,76.2221 -54.43595,106.30538 654.33596,105.0888 a 188.20963,188.20963 0 0 1 -1.8996,-37.47876 188.20963,188.20963 0 0 1 20.0788,-74.64799 z M 1065.1875,340.27205 a 188.20963,188.20963 0 0 1 -95.56964,20.44149 188.20963,188.20963 0 0 1 -16.47175,-1.72883 l 43.21482,276.72059 117.91607,18.92069 z m -43.7109,456.30786 102.1755,654.25059 a 188.20963,188.20963 0 0 1 92.651,-18.9688 188.20963,188.20963 0 0 1 19.6891,2.1609 L 1139.398,815.49535 Z M 794.34712,203.1417 306.48853,450.37661 a 188.20963,188.20963 0 0 1 51.34118,101.31626 L 845.683,304.44741 A 188.20963,188.20963 0 0 1 794.34712,203.1417 Z m 352.24368,56.54894 a 188.20963,188.20963 0 0 1 -80.5121,80.13321 l 385.9286,387.41724 a 188.20963,188.20963 0 0 1 80.5069,-80.13321 z m 339.8804,688.09027 -249.2037,486.50849 a 188.20963,188.20963 0 0 1 101.0656,51.8588 L 1587.5315,999.64494 A 188.20963,188.20963 0 0 1 1486.4712,947.78091 Z M 498.08153,1448.6696 a 188.20963,188.20963 0 0 1 1.9689,37.9111 188.20963,188.20963 0 0 1 -19.85455,74.2528 l 539.90952,86.6376 a 188.20963,188.20963 0 0 1 -1.9742,-37.9162 188.20963,188.20963 0 0 1 19.8598,-74.2477 z M 256.10246,750.31321 a 188.20963,188.20963 0 0 1 -94.02233,19.65712 188.20963,188.20963 0 0 1 -18.16843,-1.89976 l 84.42322,540.00023 a 188.20963,188.20963 0 0 1 94.02233,-19.6571 188.20963,188.20963 0 0 1 18.15773,1.9001 z M 847.58784,306.427 562.15394,863.6618 646.42776,948.26106 948.64274,358.28043 A 188.20963,188.20963 0 0 1 847.58784,306.427 Z m -359.67106,702.1662 -144.57913,282.2484 a 188.20963,188.20963 0 0 1 101.04426,51.8481 l 127.80337,-249.5025 z m 945.31912,-164.10293 -250.1803,126.78949 18.446,117.99084 283.07,-143.4639 A 188.20963,188.20963 0 0 1 1433.2359,844.49027 Z M 1037.8202,1044.882 446.28679,1344.6691 a 188.20963,188.20963 0 0 1 51.34651,101.3273 l 558.6328,-283.1181 z M 339.05298,668.95274 a 188.20963,188.20963 0 0 1 -80.49604,80.12252 l 441.91192,443.63544 106.54017,-53.9931 z m 582.89469,585.14656 -106.54012,53.993 223.91735,224.7922 a 188.20963,188.20963 0 0 1 80.5121,-80.1332 z" fill-opacity=".996"/>
</svg>

Przed

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

Plik binarny nie jest wyświetlany.

Przed

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

File diff suppressed because one or more lines are too long

Przed

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

Wyświetl plik

@ -1,69 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="63.161953mm"
height="181.12712mm"
viewBox="0 0 63.161953 181.12712"
version="1.1"
id="svg1199"
inkscape:version="0.92.4 (unknown)"
sodipodi:docname="spider.svg">
<defs
id="defs1193" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="0.35355339"
inkscape:cx="188.63933"
inkscape:cy="154.00309"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
showgrid="false"
inkscape:window-width="1366"
inkscape:window-height="705"
inkscape:window-x="0"
inkscape:window-y="30"
inkscape:window-maximized="1"
inkscape:snap-global="false"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0" />
<metadata
id="metadata1196">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-54.223528,-39.965002)">
<path
style="stroke-width:0.99999994"
d="m 329.96094,151.04883 -7.95132,372.20898 c -30.02705,2.9243 -45.57271,12.92382 -64.25977,32.67188 -25.16762,33.38088 -18.43249,69.4298 -0.4707,100.66992 12.24879,17.03193 32.3984,27.97627 53.07033,34.15036 0,0 -5.52814,0.0857 -11.58984,9.46094 -18.91001,-5.43999 -38.07073,-9.95039 -57.14063,-14.82032 -10.49976,0.9523 -28.58163,18.8274 -36.67969,24.9004 0.27746,13.19067 1.67361,27.14135 3.33008,39.15039 1.1699,-1.57002 0.83916,-3.5804 1.03906,-5.40039 0.9,-10.79003 0.60034,-21.66016 1.99024,-32.41016 9.28,-6.03999 17.7906,-13.20072 26.7207,-19.7207 18.99884,1.97067 39.37112,9.36858 55.91016,13.35156 -0.49,2.41999 -1.38047,5.27974 -4.23047,5.67969 -18.4,4.60002 -36.81969,9.10007 -55.17969,13.83007 -4.86555,6.81697 -23.47884,41.76065 -22.16992,48 3.32807,9.25919 3.76668,29.35751 8.58984,35.70899 -0.65616,-11.27353 -1.26587,-23.12102 -2.88086,-33.41016 4.366,-15.53732 14.77165,-31.85507 21.21094,-44.72851 16.36837,-5.03014 33.6873,-8.93673 49.58008,-11.32032 -0.0299,3.31998 -0.081,6.66013 -0.20117,9.99024 -10.89612,8.5036 -30.45632,23.65603 -40.40821,30.44922 -2.57681,15.80044 -3.38605,33.75066 -4.2207,48.55078 2.50279,8.85582 13.19431,23.74406 18.17156,23.90823 -2.93816,-7.30216 -8.51629,-14.68425 -10.88086,-21.31836 -0.17087,-16.87764 2.99403,-32.98356 3.70114,-48.41015 11.61344,-9.80937 25.4679,-15.10577 35.89062,-24.25 2.26541,6.18864 7.32913,9.97253 10.32813,15.05859 -2.15,3.10001 -5.51922,5.79 -5.94922,9.75 2.88,4.37998 6.60955,8.25101 10.68945,11.54102 -0.85,-3.43 -2.26023,-6.68056 -3.24023,-10.06055 l 6.20117,-7.18945 c 10.18753,5.69922 19.39911,4.81707 28.78906,0.75976 2.12,2.45 4.30149,5.11952 5.27149,8.26953 -0.85,3.26 -2.7418,6.14966 -3.5918,9.42969 4.21,-3.40003 8.09071,-7.32883 11.4707,-11.54883 -0.72,-4.08 -4.4693,-6.80104 -5.27929,-10.79101 3.66,-4.43003 7.97023,-8.42941 10.24023,-13.85938 5.68622,5.4072 34.43902,22.24881 34.94922,26.88086 0.36518,16.19209 3.11897,31.74502 2,46.75 -4.46916,8.68536 -7.12999,16.57554 -14.39063,22.67969 9.90723,0.50906 17.4253,-14.74937 21.52152,-22.69328 -0.18697,-17.91233 -0.74645,-33.39521 -1.16992,-49.66992 -13.47001,-10.57002 -27.16094,-20.89017 -40.46094,-31.66016 0.59,-3.81003 0.49976,-7.6583 0.50977,-11.48828 15.73,4.66001 31.80992,8.14868 47.66992,12.38867 7.58475,10.99663 15.5151,31.43552 20.24023,42.75977 0.43698,13.66208 -3.68079,27.5449 -4.08008,40.14062 1.49998,-1.33999 1.6498,-3.42013 2.17969,-5.24023 1.88197,-11.16719 9.61842,-29.63645 8.13086,-37.92969 -6.21997,-14.23003 -11.95978,-28.75009 -18.42969,-42.83008 -19.30273,-6.68031 -40.27482,-12.85569 -58.39062,-17.73047 -0.65,-1.72002 -1.1801,-3.47951 -1.5,-5.26953 17.78,-3.66999 35.60009,-7.40034 53.33008,-11.32031 5.35892,-0.14205 29.14876,22.09172 28.98047,23.98047 1.30016,6.78634 -2.08415,29.71011 1.61914,33.13086 2.05988,-11.02999 3.41097,-22.17002 5.12109,-33.25 -0.32862,-6.33401 -29.16337,-28.29439 -33.91016,-30.79102 -20.42635,4.13166 -40.67884,9.74123 -59.80078,12.63086 -5.16629,-4.96887 -11.64306,-7.41991 -17.4707,-10.33984 26.33,-1.87998 52.09,-16.02008 66.25,-38.58008 9.5235,-13.96814 12.87637,-29.769 13.1992,-45.79102 0.33714,-20.46694 -8.12112,-40.39069 -21.6211,-55.4707 -18.78284,-17.43524 -31.48782,-23.12017 -55.43945,-26.73828 l 6.93151,-372.80078 z"
id="path1768"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"
transform="scale(0.26458333)" />
</g>
</svg>

Przed

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

File diff suppressed because one or more lines are too long

Przed

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

Plik binarny nie jest wyświetlany.

Przed

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

Wyświetl plik

@ -1,34 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" id="svg46269" viewBox="0 0 340.00001 394.2857" height="111.27618mm" width="95.955559mm">
<defs id="defs46271">
<linearGradient id="linearGradient46839">
<stop id="stop46841" offset="0" style="stop-color:#904700;stop-opacity:1;"/>
<stop id="stop46843" offset="1" style="stop-color:#904700;stop-opacity:0;"/>
</linearGradient>
<linearGradient id="linearGradient46831">
<stop id="stop46833" offset="0" style="stop-color:#904700;stop-opacity:1;"/>
<stop id="stop46835" offset="1" style="stop-color:#904700;stop-opacity:0;"/>
</linearGradient>
<linearGradient id="linearGradient46823">
<stop id="stop46825" offset="0" style="stop-color:#904700;stop-opacity:1;"/>
<stop id="stop46827" offset="1" style="stop-color:#904700;stop-opacity:0;"/>
</linearGradient>
<radialGradient gradientTransform="matrix(4.9019612,0,0,4.9019612,-600.72836,-1264.1473)" gradientUnits="userSpaceOnUse" r="72.85714" fy="330.93362" fx="152.85715" cy="330.93362" cx="152.85715" id="radialGradient46829" xlink:href="#linearGradient46823"/>
<radialGradient gradientTransform="matrix(3.3636365,0,0,3.3636365,-602.85717,-938.05096)" gradientUnits="userSpaceOnUse" r="62.857143" fy="429.50507" fx="251.42857" cy="429.50507" cx="251.42857" id="radialGradient46837" xlink:href="#linearGradient46831"/>
<radialGradient gradientTransform="matrix(1.7317072,0,0,1.7317072,-145.78397,-287.44272)" gradientUnits="userSpaceOnUse" r="58.57143" fy="470.93369" fx="132.85715" cy="470.93369" cx="132.85715" id="radialGradient46845" xlink:href="#linearGradient46839"/>
</defs>
<metadata id="metadata46274">
<rdf:RDF>
<cc:Work rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
<dc:title/>
</cc:Work>
</rdf:RDF>
</metadata>
<g transform="translate(-8.5714264,-218.07648)" id="layer1">
<circle r="140" cy="358.07648" cx="148.57143" id="path46817" style="color:#000000;display:inline;overflow:visible;visibility:visible;fill:url(#radialGradient46829);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:20, 5;stroke-dashoffset:0;stroke-opacity:1;marker:none;enable-background:accumulate"/>
<circle r="105.71429" cy="506.64789" cx="242.85715" id="path46819" style="color:#000000;display:inline;overflow:visible;visibility:visible;fill:url(#radialGradient46837);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:20, 5;stroke-dashoffset:0;stroke-opacity:1;marker:none;enable-background:accumulate"/>
<circle r="58.57143" cy="528.07654" cx="84.285713" id="path46821" style="color:#000000;display:inline;overflow:visible;visibility:visible;fill:url(#radialGradient46845);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:20, 5;stroke-dashoffset:0;stroke-opacity:1;marker:none;enable-background:accumulate"/>
</g>
</svg>

Przed

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

Plik binarny nie jest wyświetlany.

Przed

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

Wyświetl plik

@ -8,7 +8,6 @@
<meta name="referrer" content="same-origin" />
<link href="/manifest.json" rel="manifest">
<!--server-generated-meta-->
<link rel="icon" type="image/png" href="/favicon.png">
<%= snippets %>
</head>
<body class="theme-mode-light no-reduce-motion">

Wyświetl plik

@ -0,0 +1,105 @@
{
"approval_required": false,
"avatar_upload_limit": 2000000,
"background_image": "https://fe.disroot.org/images/city.jpg",
"background_upload_limit": 4000000,
"banner_upload_limit": 4000000,
"description": "FEDIsroot - Federated social network powered by Pleroma (open beta)",
"description_limit": 5000,
"email": "admin@example.lan",
"languages": [
"en"
],
"max_toot_chars": 5000,
"pleroma": {
"metadata": {
"account_activation_required": false,
"features": [
"pleroma_api",
"akkoma_api",
"mastodon_api",
"mastodon_api_streaming",
"polls",
"v2_suggestions",
"pleroma_explicit_addressing",
"shareable_emoji_packs",
"multifetch",
"pleroma:api/v1/notifications:include_types_filter",
"editing",
"media_proxy",
"relay",
"pleroma_emoji_reactions",
"exposable_reactions",
"profile_directory",
"custom_emoji_reactions",
"pleroma:get:main/ostatus"
],
"federation": {
"enabled": true,
"exclusions": false,
"mrf_hashtag": {
"federated_timeline_removal": [],
"reject": [],
"sensitive": [
"nsfw"
]
},
"mrf_object_age": {
"actions": [
"delist",
"strip_followers"
],
"threshold": 604800
},
"mrf_policies": [
"ObjectAgePolicy",
"TagPolicy",
"HashtagPolicy",
"InlineQuotePolicy"
],
"quarantined_instances": [],
"quarantined_instances_info": {
"quarantined_instances": {}
}
},
"fields_limits": {
"max_fields": 10,
"max_remote_fields": 20,
"name_length": 512,
"value_length": 2048
},
"post_formats": [
"text/plain",
"text/html",
"text/markdown",
"text/bbcode",
"text/x.misskeymarkdown"
],
"privileged_staff": false
},
"stats": {
"mau": 83
},
"vapid_public_key": null
},
"poll_limits": {
"max_expiration": 31536000,
"max_option_chars": 200,
"max_options": 20,
"min_expiration": 0
},
"registrations": false,
"stats": {
"domain_count": 6972,
"status_count": 8081,
"user_count": 357
},
"thumbnail": "https://fe.disroot.org/instance/thumbnail.jpeg",
"title": "FEDIsroot",
"upload_limit": 16000000,
"uri": "https://fe.disroot.org",
"urls": {
"streaming_api": "wss://fe.disroot.org"
},
"version": "2.7.2 (compatible; Akkoma 3.3.1-0-gaf90a4e51)"
}

Wyświetl plik

@ -87,7 +87,7 @@
"compose_form.poll.add_option": "Add a choice",
"compose_form.poll.duration": "Poll duration",
"compose_form.poll.option_placeholder": "Choice {number}",
"compose_form.poll.remove_option": "Remove this choice",
"compose_form.poll.remove_option": "Delete",
"compose_form.poll.type.hint": "Click to toggle poll type. Radio button (default) is single. Checkbox is multiple.",
"compose_form.publish": "Publish",
"compose_form.publish_loud": "{publish}!",
@ -319,6 +319,7 @@
"poll_button.add_poll": "Add a poll",
"poll_button.remove_poll": "Remove poll",
"preferences.fields.auto_play_gif_label": "Auto-play animated GIFs",
"preferences.fields.auto_play_video_label": "Auto-play videos",
"preferences.fields.boost_modal_label": "Show confirmation dialog before reposting",
"preferences.fields.delete_modal_label": "Show confirmation dialog before deleting a post",
"preferences.fields.demetricator_label": "Use Demetricator",
@ -565,7 +566,7 @@
"compose_form.poll.add_option": "Add a choice",
"compose_form.poll.duration": "Poll duration",
"compose_form.poll.option_placeholder": "Choice {number}",
"compose_form.poll.remove_option": "Remove this choice",
"compose_form.poll.remove_option": "Delete",
"compose_form.poll.type.hint": "Click to toggle poll type. Radio button (default) is single. Checkbox is multiple.",
"compose_form.publish": "Publish",
"compose_form.publish_loud": "{publish}!",

Wyświetl plik

@ -0,0 +1,15 @@
[
{
"account": {
"id": "ABDSjI3Q0R8aDaz1U0"
},
"content": "quoast",
"id": "AJsajx9hY4Q7IKQXEe",
"pleroma": {
"quote": {
"content": "<p>10</p>",
"id": "AJmoVikzI3SkyITyim"
}
}
}
]

Wyświetl plik

@ -2,7 +2,7 @@ import { Map as ImmutableMap } from 'immutable';
import { __stub } from 'soapbox/api';
import { mockStore, rootState } from 'soapbox/jest/test-helpers';
import { ReducerRecord, EditRecord } from 'soapbox/reducers/account_notes';
import { ReducerRecord, EditRecord } from 'soapbox/reducers/account-notes';
import { normalizeAccount, normalizeRelationship } from '../../normalizers';
import { changeAccountNoteComment, initAccountNoteModal, submitAccountNote } from '../account-notes';

Wyświetl plik

@ -2,7 +2,7 @@ import { Map as ImmutableMap } from 'immutable';
import { __stub } from 'soapbox/api';
import { mockStore, rootState } from 'soapbox/jest/test-helpers';
import { ListRecord, ReducerRecord } from 'soapbox/reducers/user_lists';
import { ListRecord, ReducerRecord } from 'soapbox/reducers/user-lists';
import { normalizeAccount, normalizeInstance, normalizeRelationship } from '../../normalizers';
import {

Wyświetl plik

@ -1,6 +1,6 @@
import { __stub } from 'soapbox/api';
import { mockStore, rootState } from 'soapbox/jest/test-helpers';
import { ListRecord, ReducerRecord as UserListsRecord } from 'soapbox/reducers/user_lists';
import { ListRecord, ReducerRecord as UserListsRecord } from 'soapbox/reducers/user-lists';
import { expandBlocks, fetchBlocks } from '../blocks';

Wyświetl plik

@ -7,7 +7,7 @@ import {
fetchMe, patchMe,
} from '../me';
jest.mock('../../storage/kv_store', () => ({
jest.mock('../../storage/kv-store', () => ({
__esModule: true,
default: {
getItemOrError: jest.fn().mockReturnValue(Promise.resolve({})),

Wyświetl plik

@ -0,0 +1,150 @@
import { Map as ImmutableMap } from 'immutable';
import { __stub } from 'soapbox/api';
import { mockStore, rootState } from 'soapbox/jest/test-helpers';
import { StatusListRecord } from 'soapbox/reducers/status-lists';
import { fetchStatusQuotes, expandStatusQuotes } from '../status-quotes';
const status = {
account: {
id: 'ABDSjI3Q0R8aDaz1U0',
},
content: 'quoast',
id: 'AJsajx9hY4Q7IKQXEe',
pleroma: {
quote: {
content: '<p>10</p>',
id: 'AJmoVikzI3SkyITyim',
},
},
};
const statusId = 'AJmoVikzI3SkyITyim';
describe('fetchStatusQuotes()', () => {
let store: ReturnType<typeof mockStore>;
beforeEach(() => {
const state = rootState.set('me', '1234');
store = mockStore(state);
});
describe('with a successful API request', () => {
beforeEach(() => {
const quotes = require('soapbox/__fixtures__/status-quotes.json');
__stub((mock) => {
mock.onGet(`/api/v1/pleroma/statuses/${statusId}/quotes`).reply(200, quotes, {
link: `<https://example.com/api/v1/pleroma/statuses/${statusId}/quotes?since_id=1>; rel='prev'`,
});
});
});
it('should fetch quotes from the API', async() => {
const expectedActions = [
{ type: 'STATUS_QUOTES_FETCH_REQUEST', statusId },
{ type: 'POLLS_IMPORT', polls: [] },
{ type: 'ACCOUNTS_IMPORT', accounts: [status.account] },
{ type: 'STATUSES_IMPORT', statuses: [status], expandSpoilers: false },
{ type: 'STATUS_QUOTES_FETCH_SUCCESS', statusId, statuses: [status], next: null },
];
await store.dispatch(fetchStatusQuotes(statusId));
const actions = store.getActions();
expect(actions).toEqual(expectedActions);
});
});
describe('with an unsuccessful API request', () => {
beforeEach(() => {
__stub((mock) => {
mock.onGet(`/api/v1/pleroma/statuses/${statusId}/quotes`).networkError();
});
});
it('should dispatch failed action', async() => {
const expectedActions = [
{ type: 'STATUS_QUOTES_FETCH_REQUEST', statusId },
{ type: 'STATUS_QUOTES_FETCH_FAIL', statusId, error: new Error('Network Error') },
];
await store.dispatch(fetchStatusQuotes(statusId));
const actions = store.getActions();
expect(actions).toEqual(expectedActions);
});
});
});
describe('expandStatusQuotes()', () => {
let store: ReturnType<typeof mockStore>;
describe('without a url', () => {
beforeEach(() => {
const state = rootState
.set('me', '1234')
.set('status_lists', ImmutableMap({ [`quotes:${statusId}`]: StatusListRecord({ next: null }) }));
store = mockStore(state);
});
it('should do nothing', async() => {
await store.dispatch(expandStatusQuotes(statusId));
const actions = store.getActions();
expect(actions).toEqual([]);
});
});
describe('with a url', () => {
beforeEach(() => {
const state = rootState.set('me', '1234')
.set('status_lists', ImmutableMap({ [`quotes:${statusId}`]: StatusListRecord({ next: 'example' }) }));
store = mockStore(state);
});
describe('with a successful API request', () => {
beforeEach(() => {
const quotes = require('soapbox/__fixtures__/status-quotes.json');
__stub((mock) => {
mock.onGet('example').reply(200, quotes, {
link: `<https://example.com/api/v1/pleroma/statuses/${statusId}/quotes?since_id=1>; rel='prev'`,
});
});
});
it('should fetch quotes from the API', async() => {
const expectedActions = [
{ type: 'STATUS_QUOTES_EXPAND_REQUEST', statusId },
{ type: 'POLLS_IMPORT', polls: [] },
{ type: 'ACCOUNTS_IMPORT', accounts: [status.account] },
{ type: 'STATUSES_IMPORT', statuses: [status], expandSpoilers: false },
{ type: 'STATUS_QUOTES_EXPAND_SUCCESS', statusId, statuses: [status], next: null },
];
await store.dispatch(expandStatusQuotes(statusId));
const actions = store.getActions();
expect(actions).toEqual(expectedActions);
});
});
describe('with an unsuccessful API request', () => {
beforeEach(() => {
__stub((mock) => {
mock.onGet('example').networkError();
});
});
it('should dispatch failed action', async() => {
const expectedActions = [
{ type: 'STATUS_QUOTES_EXPAND_REQUEST', statusId },
{ type: 'STATUS_QUOTES_EXPAND_FAIL', statusId, error: new Error('Network Error') },
];
await store.dispatch(expandStatusQuotes(statusId));
const actions = store.getActions();
expect(actions).toEqual(expectedActions);
});
});
});
});

Wyświetl plik

@ -1,5 +1,5 @@
import { isLoggedIn } from 'soapbox/utils/auth';
import { getFeatures } from 'soapbox/utils/features';
import { getFeatures, parseVersion, PLEROMA } from 'soapbox/utils/features';
import api, { getLinks } from '../api';
@ -359,14 +359,30 @@ const unblockAccountFail = (error: AxiosError) => ({
error,
});
const muteAccount = (id: string, notifications?: boolean) =>
const muteAccount = (id: string, notifications?: boolean, duration = 0) =>
(dispatch: AppDispatch, getState: () => RootState) => {
if (!isLoggedIn(getState)) return null;
dispatch(muteAccountRequest(id));
const params: Record<string, any> = {
notifications,
};
if (duration) {
const state = getState();
const instance = state.instance;
const v = parseVersion(instance.version);
if (v.software === PLEROMA) {
params.expires_in = duration;
} else {
params.duration = duration;
}
}
return api(getState)
.post(`/api/v1/accounts/${id}/mute`, { notifications })
.post(`/api/v1/accounts/${id}/mute`, params)
.then(response => {
// Pass in entire statuses map so we can use it to filter stuff in different parts of the reducers
return dispatch(muteAccountSuccess(response.data, getState().statuses));

Wyświetl plik

@ -5,7 +5,7 @@ import { httpErrorMessages } from 'soapbox/utils/errors';
import type { SnackbarActionSeverity } from './snackbar';
import type { AnyAction } from '@reduxjs/toolkit';
import type { AxiosError } from 'axios';
import type { NotificationObject } from 'soapbox/react-notification';
import type { NotificationObject } from 'react-notification';
const messages = defineMessages({
unexpectedTitle: { id: 'alert.unexpected.title', defaultMessage: 'Oops!' },

Wyświetl plik

@ -16,7 +16,8 @@ import { obtainOAuthToken, revokeOAuthToken } from 'soapbox/actions/oauth';
import { startOnboarding } from 'soapbox/actions/onboarding';
import snackbar from 'soapbox/actions/snackbar';
import { custom } from 'soapbox/custom';
import KVStore from 'soapbox/storage/kv_store';
import { queryClient } from 'soapbox/queries/client';
import KVStore from 'soapbox/storage/kv-store';
import { getLoggedInAccount, parseBaseURL } from 'soapbox/utils/auth';
import sourceCode from 'soapbox/utils/code';
import { getFeatures } from 'soapbox/utils/features';
@ -203,9 +204,7 @@ export const rememberAuthAccount = (accountUrl: string) =>
export const loadCredentials = (token: string, accountUrl: string) =>
(dispatch: AppDispatch) => dispatch(rememberAuthAccount(accountUrl))
.then(() => {
dispatch(verifyCredentials(token, accountUrl));
})
.then(() => dispatch(verifyCredentials(token, accountUrl)))
.catch(() => dispatch(verifyCredentials(token, accountUrl)));
export const logIn = (username: string, password: string) =>
@ -239,15 +238,25 @@ export const logOut = () =>
token: state.auth.getIn(['users', account.url, 'access_token']),
};
return dispatch(revokeOAuthToken(params)).finally(() => {
dispatch({ type: AUTH_LOGGED_OUT, account, standalone });
return dispatch(snackbar.success(messages.loggedOut));
});
return dispatch(revokeOAuthToken(params))
.finally(() => {
// Clear all stored cache from React Query
queryClient.invalidateQueries();
queryClient.clear();
dispatch({ type: AUTH_LOGGED_OUT, account, standalone });
return dispatch(snackbar.success(messages.loggedOut));
});
};
export const switchAccount = (accountId: string, background = false) =>
(dispatch: AppDispatch, getState: () => RootState) => {
const account = getState().accounts.get(accountId);
// Clear all stored cache from React Query
queryClient.invalidateQueries();
queryClient.clear();
return dispatch({ type: SWITCH_ACCOUNT, account, background });
};

Wyświetl plik

@ -5,12 +5,12 @@ import { defineMessages, IntlShape } from 'react-intl';
import snackbar from 'soapbox/actions/snackbar';
import api from 'soapbox/api';
import { search as emojiSearch } from 'soapbox/features/emoji/emoji_mart_search_light';
import { search as emojiSearch } from 'soapbox/features/emoji/emoji-mart-search-light';
import { tagHistory } from 'soapbox/settings';
import { isLoggedIn } from 'soapbox/utils/auth';
import { getFeatures, parseVersion } from 'soapbox/utils/features';
import { formatBytes, getVideoDuration } from 'soapbox/utils/media';
import resizeImage from 'soapbox/utils/resize_image';
import resizeImage from 'soapbox/utils/resize-image';
import { showAlert, showAlertForError } from './alerts';
import { useEmoji } from './emojis';
@ -21,8 +21,8 @@ import { getSettings } from './settings';
import { createStatus } from './statuses';
import type { History } from 'history';
import type { Emoji } from 'soapbox/components/autosuggest_emoji';
import type { AutoSuggestion } from 'soapbox/components/autosuggest_input';
import type { Emoji } from 'soapbox/components/autosuggest-emoji';
import type { AutoSuggestion } from 'soapbox/components/autosuggest-input';
import type { AppDispatch, RootState } from 'soapbox/store';
import type { Account, APIEntity, Status, Tag } from 'soapbox/types/entities';
@ -35,6 +35,7 @@ const COMPOSE_SUBMIT_REQUEST = 'COMPOSE_SUBMIT_REQUEST';
const COMPOSE_SUBMIT_SUCCESS = 'COMPOSE_SUBMIT_SUCCESS';
const COMPOSE_SUBMIT_FAIL = 'COMPOSE_SUBMIT_FAIL';
const COMPOSE_REPLY = 'COMPOSE_REPLY';
const COMPOSE_EVENT_REPLY = 'COMPOSE_EVENT_REPLY';
const COMPOSE_REPLY_CANCEL = 'COMPOSE_REPLY_CANCEL';
const COMPOSE_QUOTE = 'COMPOSE_QUOTE';
const COMPOSE_QUOTE_CANCEL = 'COMPOSE_QUOTE_CANCEL';
@ -54,7 +55,6 @@ const COMPOSE_SUGGESTION_TAGS_UPDATE = 'COMPOSE_SUGGESTION_TAGS_UPDATE';
const COMPOSE_TAG_HISTORY_UPDATE = 'COMPOSE_TAG_HISTORY_UPDATE';
const COMPOSE_SENSITIVITY_CHANGE = 'COMPOSE_SENSITIVITY_CHANGE';
const COMPOSE_SPOILERNESS_CHANGE = 'COMPOSE_SPOILERNESS_CHANGE';
const COMPOSE_TYPE_CHANGE = 'COMPOSE_TYPE_CHANGE';
const COMPOSE_SPOILER_TEXT_CHANGE = 'COMPOSE_SPOILER_TEXT_CHANGE';
@ -600,11 +600,6 @@ const insertIntoTagHistory = (composeId: string, recognizedTags: APIEntity[], te
dispatch(updateTagHistory(composeId, newHistory));
};
const changeComposeSensitivity = (composeId: string) => ({
type: COMPOSE_SENSITIVITY_CHANGE,
id: composeId,
});
const changeComposeSpoilerness = (composeId: string) => ({
type: COMPOSE_SPOILERNESS_CHANGE,
id: composeId,
@ -719,6 +714,21 @@ const removeFromMentions = (composeId: string, accountId: string) =>
});
};
const eventDiscussionCompose = (composeId: string, status: Status) =>
(dispatch: AppDispatch, getState: () => RootState) => {
const state = getState();
const instance = state.instance;
const { explicitAddressing } = getFeatures(instance);
dispatch({
type: COMPOSE_EVENT_REPLY,
id: composeId,
status: status,
account: state.accounts.get(state.me),
explicitAddressing,
});
};
export {
COMPOSE_CHANGE,
COMPOSE_SUBMIT_REQUEST,
@ -726,6 +736,7 @@ export {
COMPOSE_SUBMIT_FAIL,
COMPOSE_REPLY,
COMPOSE_REPLY_CANCEL,
COMPOSE_EVENT_REPLY,
COMPOSE_QUOTE,
COMPOSE_QUOTE_CANCEL,
COMPOSE_DIRECT,
@ -741,7 +752,6 @@ export {
COMPOSE_SUGGESTION_SELECT,
COMPOSE_SUGGESTION_TAGS_UPDATE,
COMPOSE_TAG_HISTORY_UPDATE,
COMPOSE_SENSITIVITY_CHANGE,
COMPOSE_SPOILERNESS_CHANGE,
COMPOSE_TYPE_CHANGE,
COMPOSE_SPOILER_TEXT_CHANGE,
@ -796,7 +806,6 @@ export {
selectComposeSuggestion,
updateSuggestionTags,
updateTagHistory,
changeComposeSensitivity,
changeComposeSpoilerness,
changeComposeContentType,
changeComposeSpoilerText,
@ -814,4 +823,5 @@ export {
openComposeWithText,
addToMentions,
removeFromMentions,
eventDiscussionCompose,
};

Wyświetl plik

@ -1,6 +1,6 @@
import axios from 'axios';
import * as BuildConfig from 'soapbox/build_config';
import * as BuildConfig from 'soapbox/build-config';
import { isURL } from 'soapbox/utils/auth';
import sourceCode from 'soapbox/utils/code';
import { getFeatures } from 'soapbox/utils/features';

Wyświetl plik

@ -1,4 +1,4 @@
import type { DropdownPlacement } from 'soapbox/components/dropdown_menu';
import type { DropdownPlacement } from 'soapbox/components/dropdown-menu';
const DROPDOWN_MENU_OPEN = 'DROPDOWN_MENU_OPEN';
const DROPDOWN_MENU_CLOSE = 'DROPDOWN_MENU_CLOSE';

Wyświetl plik

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

Wyświetl plik

@ -0,0 +1,738 @@
import { defineMessages, IntlShape } from 'react-intl';
import api, { getLinks } from 'soapbox/api';
import { formatBytes } from 'soapbox/utils/media';
import resizeImage from 'soapbox/utils/resize-image';
import { importFetchedAccounts, importFetchedStatus, importFetchedStatuses } from './importer';
import { fetchMedia, uploadMedia } from './media';
import { closeModal, openModal } from './modals';
import snackbar from './snackbar';
import {
STATUS_FETCH_SOURCE_FAIL,
STATUS_FETCH_SOURCE_REQUEST,
STATUS_FETCH_SOURCE_SUCCESS,
} from './statuses';
import type { AxiosError } from 'axios';
import type { AppDispatch, RootState } from 'soapbox/store';
import type { APIEntity, Status as StatusEntity } from 'soapbox/types/entities';
const LOCATION_SEARCH_REQUEST = 'LOCATION_SEARCH_REQUEST';
const LOCATION_SEARCH_SUCCESS = 'LOCATION_SEARCH_SUCCESS';
const LOCATION_SEARCH_FAIL = 'LOCATION_SEARCH_FAIL';
const EDIT_EVENT_NAME_CHANGE = 'EDIT_EVENT_NAME_CHANGE';
const EDIT_EVENT_DESCRIPTION_CHANGE = 'EDIT_EVENT_DESCRIPTION_CHANGE';
const EDIT_EVENT_START_TIME_CHANGE = 'EDIT_EVENT_START_TIME_CHANGE';
const EDIT_EVENT_HAS_END_TIME_CHANGE = 'EDIT_EVENT_HAS_END_TIME_CHANGE';
const EDIT_EVENT_END_TIME_CHANGE = 'EDIT_EVENT_END_TIME_CHANGE';
const EDIT_EVENT_APPROVAL_REQUIRED_CHANGE = 'EDIT_EVENT_APPROVAL_REQUIRED_CHANGE';
const EDIT_EVENT_LOCATION_CHANGE = 'EDIT_EVENT_LOCATION_CHANGE';
const EVENT_BANNER_UPLOAD_REQUEST = 'EVENT_BANNER_UPLOAD_REQUEST';
const EVENT_BANNER_UPLOAD_PROGRESS = 'EVENT_BANNER_UPLOAD_PROGRESS';
const EVENT_BANNER_UPLOAD_SUCCESS = 'EVENT_BANNER_UPLOAD_SUCCESS';
const EVENT_BANNER_UPLOAD_FAIL = 'EVENT_BANNER_UPLOAD_FAIL';
const EVENT_BANNER_UPLOAD_UNDO = 'EVENT_BANNER_UPLOAD_UNDO';
const EVENT_SUBMIT_REQUEST = 'EVENT_SUBMIT_REQUEST';
const EVENT_SUBMIT_SUCCESS = 'EVENT_SUBMIT_SUCCESS';
const EVENT_SUBMIT_FAIL = 'EVENT_SUBMIT_FAIL';
const EVENT_JOIN_REQUEST = 'EVENT_JOIN_REQUEST';
const EVENT_JOIN_SUCCESS = 'EVENT_JOIN_SUCCESS';
const EVENT_JOIN_FAIL = 'EVENT_JOIN_FAIL';
const EVENT_LEAVE_REQUEST = 'EVENT_LEAVE_REQUEST';
const EVENT_LEAVE_SUCCESS = 'EVENT_LEAVE_SUCCESS';
const EVENT_LEAVE_FAIL = 'EVENT_LEAVE_FAIL';
const EVENT_PARTICIPATIONS_FETCH_REQUEST = 'EVENT_PARTICIPATIONS_FETCH_REQUEST';
const EVENT_PARTICIPATIONS_FETCH_SUCCESS = 'EVENT_PARTICIPATIONS_FETCH_SUCCESS';
const EVENT_PARTICIPATIONS_FETCH_FAIL = 'EVENT_PARTICIPATIONS_FETCH_FAIL';
const EVENT_PARTICIPATIONS_EXPAND_REQUEST = 'EVENT_PARTICIPATIONS_EXPAND_REQUEST';
const EVENT_PARTICIPATIONS_EXPAND_SUCCESS = 'EVENT_PARTICIPATIONS_EXPAND_SUCCESS';
const EVENT_PARTICIPATIONS_EXPAND_FAIL = 'EVENT_PARTICIPATIONS_EXPAND_FAIL';
const EVENT_PARTICIPATION_REQUESTS_FETCH_REQUEST = 'EVENT_PARTICIPATION_REQUESTS_FETCH_REQUEST';
const EVENT_PARTICIPATION_REQUESTS_FETCH_SUCCESS = 'EVENT_PARTICIPATION_REQUESTS_FETCH_SUCCESS';
const EVENT_PARTICIPATION_REQUESTS_FETCH_FAIL = 'EVENT_PARTICIPATION_REQUESTS_FETCH_FAIL';
const EVENT_PARTICIPATION_REQUESTS_EXPAND_REQUEST = 'EVENT_PARTICIPATION_REQUESTS_EXPAND_REQUEST';
const EVENT_PARTICIPATION_REQUESTS_EXPAND_SUCCESS = 'EVENT_PARTICIPATION_REQUESTS_EXPAND_SUCCESS';
const EVENT_PARTICIPATION_REQUESTS_EXPAND_FAIL = 'EVENT_PARTICIPATION_REQUESTS_EXPAND_FAIL';
const EVENT_PARTICIPATION_REQUEST_AUTHORIZE_REQUEST = 'EVENT_PARTICIPATION_REQUEST_AUTHORIZE_REQUEST';
const EVENT_PARTICIPATION_REQUEST_AUTHORIZE_SUCCESS = 'EVENT_PARTICIPATION_REQUEST_AUTHORIZE_SUCCESS';
const EVENT_PARTICIPATION_REQUEST_AUTHORIZE_FAIL = 'EVENT_PARTICIPATION_REQUEST_AUTHORIZE_FAIL';
const EVENT_PARTICIPATION_REQUEST_REJECT_REQUEST = 'EVENT_PARTICIPATION_REQUEST_REJECT_REQUEST';
const EVENT_PARTICIPATION_REQUEST_REJECT_SUCCESS = 'EVENT_PARTICIPATION_REQUEST_REJECT_SUCCESS';
const EVENT_PARTICIPATION_REQUEST_REJECT_FAIL = 'EVENT_PARTICIPATION_REQUEST_REJECT_FAIL';
const EVENT_COMPOSE_CANCEL = 'EVENT_COMPOSE_CANCEL';
const EVENT_FORM_SET = 'EVENT_FORM_SET';
const RECENT_EVENTS_FETCH_REQUEST = 'RECENT_EVENTS_FETCH_REQUEST';
const RECENT_EVENTS_FETCH_SUCCESS = 'RECENT_EVENTS_FETCH_SUCCESS';
const RECENT_EVENTS_FETCH_FAIL = 'RECENT_EVENTS_FETCH_FAIL';
const JOINED_EVENTS_FETCH_REQUEST = 'JOINED_EVENTS_FETCH_REQUEST';
const JOINED_EVENTS_FETCH_SUCCESS = 'JOINED_EVENTS_FETCH_SUCCESS';
const JOINED_EVENTS_FETCH_FAIL = 'JOINED_EVENTS_FETCH_FAIL';
const noOp = () => new Promise(f => f(undefined));
const messages = defineMessages({
exceededImageSizeLimit: { id: 'upload_error.image_size_limit', defaultMessage: 'Image exceeds the current file size limit ({limit})' },
success: { id: 'compose_event.submit_success', defaultMessage: 'Your event was created' },
editSuccess: { id: 'compose_event.edit_success', defaultMessage: 'Your event was edited' },
joinSuccess: { id: 'join_event.success', defaultMessage: 'Joined the event' },
joinRequestSuccess: { id: 'join_event.request_success', defaultMessage: 'Requested to join the event' },
view: { id: 'snackbar.view', defaultMessage: 'View' },
authorized: { id: 'compose_event.participation_requests.authorize_success', defaultMessage: 'User accepted' },
rejected: { id: 'compose_event.participation_requests.reject_success', defaultMessage: 'User rejected' },
});
const locationSearch = (query: string, signal?: AbortSignal) =>
(dispatch: AppDispatch, getState: () => RootState) => {
dispatch({ type: LOCATION_SEARCH_REQUEST, query });
return api(getState).get('/api/v1/pleroma/search/location', { params: { q: query }, signal }).then(({ data: locations }) => {
dispatch({ type: LOCATION_SEARCH_SUCCESS, locations });
return locations;
}).catch(error => {
dispatch({ type: LOCATION_SEARCH_FAIL });
throw error;
});
};
const changeEditEventName = (value: string) => ({
type: EDIT_EVENT_NAME_CHANGE,
value,
});
const changeEditEventDescription = (value: string) => ({
type: EDIT_EVENT_DESCRIPTION_CHANGE,
value,
});
const changeEditEventStartTime = (value: Date) => ({
type: EDIT_EVENT_START_TIME_CHANGE,
value,
});
const changeEditEventEndTime = (value: Date) => ({
type: EDIT_EVENT_END_TIME_CHANGE,
value,
});
const changeEditEventHasEndTime = (value: boolean) => ({
type: EDIT_EVENT_HAS_END_TIME_CHANGE,
value,
});
const changeEditEventApprovalRequired = (value: boolean) => ({
type: EDIT_EVENT_APPROVAL_REQUIRED_CHANGE,
value,
});
const changeEditEventLocation = (value: string | null) =>
(dispatch: AppDispatch, getState: () => RootState) => {
let location = null;
if (value) {
location = getState().locations.get(value);
}
dispatch({
type: EDIT_EVENT_LOCATION_CHANGE,
value: location,
});
};
const uploadEventBanner = (file: File, intl: IntlShape) =>
(dispatch: AppDispatch, getState: () => RootState) => {
const maxImageSize = getState().instance.configuration.getIn(['media_attachments', 'image_size_limit']) as number | undefined;
let progress = 0;
dispatch(uploadEventBannerRequest());
if (maxImageSize && (file.size > maxImageSize)) {
const limit = formatBytes(maxImageSize);
const message = intl.formatMessage(messages.exceededImageSizeLimit, { limit });
dispatch(snackbar.error(message));
dispatch(uploadEventBannerFail(true));
return;
}
resizeImage(file).then(file => {
const data = new FormData();
data.append('file', file);
// Account for disparity in size of original image and resized data
const onUploadProgress = ({ loaded }: any) => {
progress = loaded;
dispatch(uploadEventBannerProgress(progress));
};
return dispatch(uploadMedia(data, onUploadProgress))
.then(({ status, data }) => {
// If server-side processing of the media attachment has not completed yet,
// poll the server until it is, before showing the media attachment as uploaded
if (status === 200) {
dispatch(uploadEventBannerSuccess(data, file));
} else if (status === 202) {
const poll = () => {
dispatch(fetchMedia(data.id)).then(({ status, data }) => {
if (status === 200) {
dispatch(uploadEventBannerSuccess(data, file));
} else if (status === 206) {
setTimeout(() => poll(), 1000);
}
}).catch(error => dispatch(uploadEventBannerFail(error)));
};
poll();
}
});
}).catch(error => dispatch(uploadEventBannerFail(error)));
};
const uploadEventBannerRequest = () => ({
type: EVENT_BANNER_UPLOAD_REQUEST,
});
const uploadEventBannerProgress = (loaded: number) => ({
type: EVENT_BANNER_UPLOAD_PROGRESS,
loaded,
});
const uploadEventBannerSuccess = (media: APIEntity, file: File) => ({
type: EVENT_BANNER_UPLOAD_SUCCESS,
media,
file,
});
const uploadEventBannerFail = (error: AxiosError | true) => ({
type: EVENT_BANNER_UPLOAD_FAIL,
error,
});
const undoUploadEventBanner = () => ({
type: EVENT_BANNER_UPLOAD_UNDO,
});
const submitEvent = () =>
(dispatch: AppDispatch, getState: () => RootState) => {
const state = getState();
const id = state.compose_event.id;
const name = state.compose_event.name;
const status = state.compose_event.status;
const banner = state.compose_event.banner;
const startTime = state.compose_event.start_time;
const endTime = state.compose_event.end_time;
const joinMode = state.compose_event.approval_required ? 'restricted' : 'free';
const location = state.compose_event.location;
if (!name || !name.length) {
return;
}
dispatch(submitEventRequest());
const params: Record<string, any> = {
name,
status,
start_time: startTime,
join_mode: joinMode,
content_type: 'text/markdown',
};
if (endTime) params.end_time = endTime;
if (banner) params.banner_id = banner.id;
if (location) params.location_id = location.origin_id;
return api(getState).request({
url: id === null ? '/api/v1/pleroma/events' : `/api/v1/pleroma/events/${id}`,
method: id === null ? 'post' : 'put',
data: params,
}).then(({ data }) => {
dispatch(closeModal('COMPOSE_EVENT'));
dispatch(importFetchedStatus(data));
dispatch(submitEventSuccess(data));
dispatch(snackbar.success(id ? messages.editSuccess : messages.success, messages.view, `/@${data.account.acct}/events/${data.id}`));
}).catch(function(error) {
dispatch(submitEventFail(error));
});
};
const submitEventRequest = () => ({
type: EVENT_SUBMIT_REQUEST,
});
const submitEventSuccess = (status: APIEntity) => ({
type: EVENT_SUBMIT_SUCCESS,
status,
});
const submitEventFail = (error: AxiosError) => ({
type: EVENT_SUBMIT_FAIL,
error,
});
const joinEvent = (id: string, participationMessage?: string) =>
(dispatch: AppDispatch, getState: () => RootState) => {
const status = getState().statuses.get(id);
if (!status || !status.event || status.event.join_state) {
return dispatch(noOp);
}
dispatch(joinEventRequest(status));
return api(getState).post(`/api/v1/pleroma/events/${id}/join`, {
participation_message: participationMessage,
}).then(({ data }) => {
dispatch(importFetchedStatus(data));
dispatch(joinEventSuccess(data));
dispatch(snackbar.success(
data.pleroma.event?.join_state === 'pending' ? messages.joinRequestSuccess : messages.joinSuccess,
messages.view,
`/@${data.account.acct}/events/${data.id}`,
));
}).catch(function(error) {
dispatch(joinEventFail(error, status, status?.event?.join_state || null));
});
};
const joinEventRequest = (status: StatusEntity) => ({
type: EVENT_JOIN_REQUEST,
id: status.id,
});
const joinEventSuccess = (status: APIEntity) => ({
type: EVENT_JOIN_SUCCESS,
id: status.id,
});
const joinEventFail = (error: AxiosError, status: StatusEntity, previousState: string | null) => ({
type: EVENT_JOIN_FAIL,
error,
id: status.id,
previousState,
});
const leaveEvent = (id: string) =>
(dispatch: AppDispatch, getState: () => RootState) => {
const status = getState().statuses.get(id);
if (!status || !status.event || !status.event.join_state) {
return dispatch(noOp);
}
dispatch(leaveEventRequest(status));
return api(getState).post(`/api/v1/pleroma/events/${id}/leave`).then(({ data }) => {
dispatch(importFetchedStatus(data));
dispatch(leaveEventSuccess(data));
}).catch(function(error) {
dispatch(leaveEventFail(error, status));
});
};
const leaveEventRequest = (status: StatusEntity) => ({
type: EVENT_LEAVE_REQUEST,
id: status.id,
});
const leaveEventSuccess = (status: APIEntity) => ({
type: EVENT_LEAVE_SUCCESS,
id: status.id,
});
const leaveEventFail = (error: AxiosError, status: StatusEntity) => ({
type: EVENT_LEAVE_FAIL,
id: status.id,
error,
});
const fetchEventParticipations = (id: string) =>
(dispatch: AppDispatch, getState: () => RootState) => {
dispatch(fetchEventParticipationsRequest(id));
return api(getState).get(`/api/v1/pleroma/events/${id}/participations`).then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(importFetchedAccounts(response.data));
return dispatch(fetchEventParticipationsSuccess(id, response.data, next ? next.uri : null));
}).catch(error => {
dispatch(fetchEventParticipationsFail(id, error));
});
};
const fetchEventParticipationsRequest = (id: string) => ({
type: EVENT_PARTICIPATIONS_FETCH_REQUEST,
id,
});
const fetchEventParticipationsSuccess = (id: string, accounts: APIEntity[], next: string | null) => ({
type: EVENT_PARTICIPATIONS_FETCH_SUCCESS,
id,
accounts,
next,
});
const fetchEventParticipationsFail = (id: string, error: AxiosError) => ({
type: EVENT_PARTICIPATIONS_FETCH_FAIL,
id,
error,
});
const expandEventParticipations = (id: string) =>
(dispatch: AppDispatch, getState: () => RootState) => {
const url = getState().user_lists.event_participations.get(id)?.next || null;
if (url === null) {
return dispatch(noOp);
}
dispatch(expandEventParticipationsRequest(id));
return api(getState).get(url).then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(importFetchedAccounts(response.data));
return dispatch(expandEventParticipationsSuccess(id, response.data, next ? next.uri : null));
}).catch(error => {
dispatch(expandEventParticipationsFail(id, error));
});
};
const expandEventParticipationsRequest = (id: string) => ({
type: EVENT_PARTICIPATIONS_EXPAND_REQUEST,
id,
});
const expandEventParticipationsSuccess = (id: string, accounts: APIEntity[], next: string | null) => ({
type: EVENT_PARTICIPATIONS_EXPAND_SUCCESS,
id,
accounts,
next,
});
const expandEventParticipationsFail = (id: string, error: AxiosError) => ({
type: EVENT_PARTICIPATIONS_EXPAND_FAIL,
id,
error,
});
const fetchEventParticipationRequests = (id: string) =>
(dispatch: AppDispatch, getState: () => RootState) => {
dispatch(fetchEventParticipationRequestsRequest(id));
return api(getState).get(`/api/v1/pleroma/events/${id}/participation_requests`).then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(importFetchedAccounts(response.data.map(({ account }: APIEntity) => account)));
return dispatch(fetchEventParticipationRequestsSuccess(id, response.data, next ? next.uri : null));
}).catch(error => {
dispatch(fetchEventParticipationRequestsFail(id, error));
});
};
const fetchEventParticipationRequestsRequest = (id: string) => ({
type: EVENT_PARTICIPATION_REQUESTS_FETCH_REQUEST,
id,
});
const fetchEventParticipationRequestsSuccess = (id: string, participations: APIEntity[], next: string | null) => ({
type: EVENT_PARTICIPATION_REQUESTS_FETCH_SUCCESS,
id,
participations,
next,
});
const fetchEventParticipationRequestsFail = (id: string, error: AxiosError) => ({
type: EVENT_PARTICIPATION_REQUESTS_FETCH_FAIL,
id,
error,
});
const expandEventParticipationRequests = (id: string) =>
(dispatch: AppDispatch, getState: () => RootState) => {
const url = getState().user_lists.event_participations.get(id)?.next || null;
if (url === null) {
return dispatch(noOp);
}
dispatch(expandEventParticipationRequestsRequest(id));
return api(getState).get(url).then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(importFetchedAccounts(response.data.map(({ account }: APIEntity) => account)));
return dispatch(expandEventParticipationRequestsSuccess(id, response.data, next ? next.uri : null));
}).catch(error => {
dispatch(expandEventParticipationRequestsFail(id, error));
});
};
const expandEventParticipationRequestsRequest = (id: string) => ({
type: EVENT_PARTICIPATION_REQUESTS_EXPAND_REQUEST,
id,
});
const expandEventParticipationRequestsSuccess = (id: string, participations: APIEntity[], next: string | null) => ({
type: EVENT_PARTICIPATION_REQUESTS_EXPAND_SUCCESS,
id,
participations,
next,
});
const expandEventParticipationRequestsFail = (id: string, error: AxiosError) => ({
type: EVENT_PARTICIPATION_REQUESTS_EXPAND_FAIL,
id,
error,
});
const authorizeEventParticipationRequest = (id: string, accountId: string) =>
(dispatch: AppDispatch, getState: () => RootState) => {
dispatch(authorizeEventParticipationRequestRequest(id, accountId));
return api(getState)
.post(`/api/v1/pleroma/events/${id}/participation_requests/${accountId}/authorize`)
.then(() => {
dispatch(authorizeEventParticipationRequestSuccess(id, accountId));
dispatch(snackbar.success(messages.authorized));
})
.catch(error => dispatch(authorizeEventParticipationRequestFail(id, accountId, error)));
};
const authorizeEventParticipationRequestRequest = (id: string, accountId: string) => ({
type: EVENT_PARTICIPATION_REQUEST_AUTHORIZE_REQUEST,
id,
accountId,
});
const authorizeEventParticipationRequestSuccess = (id: string, accountId: string) => ({
type: EVENT_PARTICIPATION_REQUEST_AUTHORIZE_SUCCESS,
id,
accountId,
});
const authorizeEventParticipationRequestFail = (id: string, accountId: string, error: AxiosError) => ({
type: EVENT_PARTICIPATION_REQUEST_AUTHORIZE_FAIL,
id,
accountId,
error,
});
const rejectEventParticipationRequest = (id: string, accountId: string) =>
(dispatch: AppDispatch, getState: () => RootState) => {
dispatch(rejectEventParticipationRequestRequest(id, accountId));
return api(getState)
.post(`/api/v1/pleroma/events/${id}/participation_requests/${accountId}/reject`)
.then(() => {
dispatch(rejectEventParticipationRequestSuccess(id, accountId));
dispatch(snackbar.success(messages.rejected));
})
.catch(error => dispatch(rejectEventParticipationRequestFail(id, accountId, error)));
};
const rejectEventParticipationRequestRequest = (id: string, accountId: string) => ({
type: EVENT_PARTICIPATION_REQUEST_REJECT_REQUEST,
id,
accountId,
});
const rejectEventParticipationRequestSuccess = (id: string, accountId: string) => ({
type: EVENT_PARTICIPATION_REQUEST_REJECT_SUCCESS,
id,
accountId,
});
const rejectEventParticipationRequestFail = (id: string, accountId: string, error: AxiosError) => ({
type: EVENT_PARTICIPATION_REQUEST_REJECT_FAIL,
id,
accountId,
error,
});
const fetchEventIcs = (id: string) =>
(dispatch: any, getState: () => RootState) =>
api(getState).get(`/api/v1/pleroma/events/${id}/ics`);
const cancelEventCompose = () => ({
type: EVENT_COMPOSE_CANCEL,
});
const editEvent = (id: string) => (dispatch: AppDispatch, getState: () => RootState) => {
const status = getState().statuses.get(id)!;
dispatch({ type: STATUS_FETCH_SOURCE_REQUEST });
api(getState).get(`/api/v1/statuses/${id}/source`).then(response => {
dispatch({ type: STATUS_FETCH_SOURCE_SUCCESS });
dispatch({
type: EVENT_FORM_SET,
status,
text: response.data.text,
location: response.data.location,
});
dispatch(openModal('COMPOSE_EVENT'));
}).catch(error => {
dispatch({ type: STATUS_FETCH_SOURCE_FAIL, error });
});
};
const fetchRecentEvents = () =>
(dispatch: AppDispatch, getState: () => RootState) => {
if (getState().status_lists.get('recent_events')?.isLoading) {
return;
}
dispatch({ type: RECENT_EVENTS_FETCH_REQUEST });
api(getState).get('/api/v1/timelines/public?only_events=true').then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(importFetchedStatuses(response.data));
dispatch({
type: RECENT_EVENTS_FETCH_SUCCESS,
statuses: response.data,
next: next ? next.uri : null,
});
}).catch(error => {
dispatch({ type: RECENT_EVENTS_FETCH_FAIL, error });
});
};
const fetchJoinedEvents = () =>
(dispatch: AppDispatch, getState: () => RootState) => {
if (getState().status_lists.get('joined_events')?.isLoading) {
return;
}
dispatch({ type: JOINED_EVENTS_FETCH_REQUEST });
api(getState).get('/api/v1/pleroma/events/joined_events').then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(importFetchedStatuses(response.data));
dispatch({
type: JOINED_EVENTS_FETCH_SUCCESS,
statuses: response.data,
next: next ? next.uri : null,
});
}).catch(error => {
dispatch({ type: JOINED_EVENTS_FETCH_FAIL, error });
});
};
export {
LOCATION_SEARCH_REQUEST,
LOCATION_SEARCH_SUCCESS,
LOCATION_SEARCH_FAIL,
EDIT_EVENT_NAME_CHANGE,
EDIT_EVENT_DESCRIPTION_CHANGE,
EDIT_EVENT_START_TIME_CHANGE,
EDIT_EVENT_END_TIME_CHANGE,
EDIT_EVENT_HAS_END_TIME_CHANGE,
EDIT_EVENT_APPROVAL_REQUIRED_CHANGE,
EDIT_EVENT_LOCATION_CHANGE,
EVENT_BANNER_UPLOAD_REQUEST,
EVENT_BANNER_UPLOAD_PROGRESS,
EVENT_BANNER_UPLOAD_SUCCESS,
EVENT_BANNER_UPLOAD_FAIL,
EVENT_BANNER_UPLOAD_UNDO,
EVENT_SUBMIT_REQUEST,
EVENT_SUBMIT_SUCCESS,
EVENT_SUBMIT_FAIL,
EVENT_JOIN_REQUEST,
EVENT_JOIN_SUCCESS,
EVENT_JOIN_FAIL,
EVENT_LEAVE_REQUEST,
EVENT_LEAVE_SUCCESS,
EVENT_LEAVE_FAIL,
EVENT_PARTICIPATIONS_FETCH_REQUEST,
EVENT_PARTICIPATIONS_FETCH_SUCCESS,
EVENT_PARTICIPATIONS_FETCH_FAIL,
EVENT_PARTICIPATIONS_EXPAND_REQUEST,
EVENT_PARTICIPATIONS_EXPAND_SUCCESS,
EVENT_PARTICIPATIONS_EXPAND_FAIL,
EVENT_PARTICIPATION_REQUESTS_FETCH_REQUEST,
EVENT_PARTICIPATION_REQUESTS_FETCH_SUCCESS,
EVENT_PARTICIPATION_REQUESTS_FETCH_FAIL,
EVENT_PARTICIPATION_REQUESTS_EXPAND_REQUEST,
EVENT_PARTICIPATION_REQUESTS_EXPAND_SUCCESS,
EVENT_PARTICIPATION_REQUESTS_EXPAND_FAIL,
EVENT_PARTICIPATION_REQUEST_AUTHORIZE_REQUEST,
EVENT_PARTICIPATION_REQUEST_AUTHORIZE_SUCCESS,
EVENT_PARTICIPATION_REQUEST_AUTHORIZE_FAIL,
EVENT_PARTICIPATION_REQUEST_REJECT_REQUEST,
EVENT_PARTICIPATION_REQUEST_REJECT_SUCCESS,
EVENT_PARTICIPATION_REQUEST_REJECT_FAIL,
EVENT_COMPOSE_CANCEL,
EVENT_FORM_SET,
RECENT_EVENTS_FETCH_REQUEST,
RECENT_EVENTS_FETCH_SUCCESS,
RECENT_EVENTS_FETCH_FAIL,
JOINED_EVENTS_FETCH_REQUEST,
JOINED_EVENTS_FETCH_SUCCESS,
JOINED_EVENTS_FETCH_FAIL,
locationSearch,
changeEditEventName,
changeEditEventDescription,
changeEditEventStartTime,
changeEditEventEndTime,
changeEditEventHasEndTime,
changeEditEventApprovalRequired,
changeEditEventLocation,
uploadEventBanner,
uploadEventBannerRequest,
uploadEventBannerProgress,
uploadEventBannerSuccess,
uploadEventBannerFail,
undoUploadEventBanner,
submitEvent,
submitEventRequest,
submitEventSuccess,
submitEventFail,
joinEvent,
joinEventRequest,
joinEventSuccess,
joinEventFail,
leaveEvent,
leaveEventRequest,
leaveEventSuccess,
leaveEventFail,
fetchEventParticipations,
fetchEventParticipationsRequest,
fetchEventParticipationsSuccess,
fetchEventParticipationsFail,
expandEventParticipations,
expandEventParticipationsRequest,
expandEventParticipationsSuccess,
expandEventParticipationsFail,
fetchEventParticipationRequests,
fetchEventParticipationRequestsRequest,
fetchEventParticipationRequestsSuccess,
fetchEventParticipationRequestsFail,
expandEventParticipationRequests,
expandEventParticipationRequestsRequest,
expandEventParticipationRequestsSuccess,
expandEventParticipationRequestsFail,
authorizeEventParticipationRequest,
authorizeEventParticipationRequestRequest,
authorizeEventParticipationRequestSuccess,
authorizeEventParticipationRequestFail,
rejectEventParticipationRequest,
rejectEventParticipationRequestRequest,
rejectEventParticipationRequestSuccess,
rejectEventParticipationRequestFail,
fetchEventIcs,
cancelEventCompose,
editEvent,
fetchRecentEvents,
fetchJoinedEvents,
};

Wyświetl plik

@ -1,143 +0,0 @@
import { isLoggedIn } from 'soapbox/utils/auth';
import api from '../api';
import type { AxiosError } from 'axios';
import type { History } from 'history';
import type { AppDispatch, RootState } from 'soapbox/store';
import type { APIEntity } from 'soapbox/types/entities';
const GROUP_CREATE_REQUEST = 'GROUP_CREATE_REQUEST';
const GROUP_CREATE_SUCCESS = 'GROUP_CREATE_SUCCESS';
const GROUP_CREATE_FAIL = 'GROUP_CREATE_FAIL';
const GROUP_UPDATE_REQUEST = 'GROUP_UPDATE_REQUEST';
const GROUP_UPDATE_SUCCESS = 'GROUP_UPDATE_SUCCESS';
const GROUP_UPDATE_FAIL = 'GROUP_UPDATE_FAIL';
const GROUP_EDITOR_VALUE_CHANGE = 'GROUP_EDITOR_VALUE_CHANGE';
const GROUP_EDITOR_RESET = 'GROUP_EDITOR_RESET';
const GROUP_EDITOR_SETUP = 'GROUP_EDITOR_SETUP';
const submit = (routerHistory: History) =>
(dispatch: AppDispatch, getState: () => RootState) => {
const groupId = getState().group_editor.get('groupId') as string;
const title = getState().group_editor.get('title') as string;
const description = getState().group_editor.get('description') as string;
const coverImage = getState().group_editor.get('coverImage') as any;
if (groupId === null) {
dispatch(create(title, description, coverImage, routerHistory));
} else {
dispatch(update(groupId, title, description, coverImage, routerHistory));
}
};
const create = (title: string, description: string, coverImage: File, routerHistory: History) =>
(dispatch: AppDispatch, getState: () => RootState) => {
if (!isLoggedIn(getState)) return;
dispatch(createRequest());
const formData = new FormData();
formData.append('title', title);
formData.append('description', description);
if (coverImage !== null) {
formData.append('cover_image', coverImage);
}
api(getState).post('/api/v1/groups', formData, { headers: { 'Content-Type': 'multipart/form-data' } }).then(({ data }) => {
dispatch(createSuccess(data));
routerHistory.push(`/groups/${data.id}`);
}).catch(err => dispatch(createFail(err)));
};
const createRequest = (id?: string) => ({
type: GROUP_CREATE_REQUEST,
id,
});
const createSuccess = (group: APIEntity) => ({
type: GROUP_CREATE_SUCCESS,
group,
});
const createFail = (error: AxiosError) => ({
type: GROUP_CREATE_FAIL,
error,
});
const update = (groupId: string, title: string, description: string, coverImage: File, routerHistory: History) =>
(dispatch: AppDispatch, getState: () => RootState) => {
if (!isLoggedIn(getState)) return;
dispatch(updateRequest(groupId));
const formData = new FormData();
formData.append('title', title);
formData.append('description', description);
if (coverImage !== null) {
formData.append('cover_image', coverImage);
}
api(getState).put(`/api/v1/groups/${groupId}`, formData, { headers: { 'Content-Type': 'multipart/form-data' } }).then(({ data }) => {
dispatch(updateSuccess(data));
routerHistory.push(`/groups/${data.id}`);
}).catch(err => dispatch(updateFail(err)));
};
const updateRequest = (id: string) => ({
type: GROUP_UPDATE_REQUEST,
id,
});
const updateSuccess = (group: APIEntity) => ({
type: GROUP_UPDATE_SUCCESS,
group,
});
const updateFail = (error: AxiosError) => ({
type: GROUP_UPDATE_FAIL,
error,
});
const changeValue = (field: string, value: string | File) => ({
type: GROUP_EDITOR_VALUE_CHANGE,
field,
value,
});
const reset = () => ({
type: GROUP_EDITOR_RESET,
});
const setUp = (group: string) => ({
type: GROUP_EDITOR_SETUP,
group,
});
export {
GROUP_CREATE_REQUEST,
GROUP_CREATE_SUCCESS,
GROUP_CREATE_FAIL,
GROUP_UPDATE_REQUEST,
GROUP_UPDATE_SUCCESS,
GROUP_UPDATE_FAIL,
GROUP_EDITOR_VALUE_CHANGE,
GROUP_EDITOR_RESET,
GROUP_EDITOR_SETUP,
submit,
create,
createRequest,
createSuccess,
createFail,
update,
updateRequest,
updateSuccess,
updateFail,
changeValue,
reset,
setUp,
};

Wyświetl plik

@ -1,550 +0,0 @@
import { AxiosError } from 'axios';
import { isLoggedIn } from 'soapbox/utils/auth';
import api, { getLinks } from '../api';
import { fetchRelationships } from './accounts';
import { importFetchedAccounts } from './importer';
import type { AppDispatch, RootState } from 'soapbox/store';
import type { APIEntity } from 'soapbox/types/entities';
const GROUP_FETCH_REQUEST = 'GROUP_FETCH_REQUEST';
const GROUP_FETCH_SUCCESS = 'GROUP_FETCH_SUCCESS';
const GROUP_FETCH_FAIL = 'GROUP_FETCH_FAIL';
const GROUP_RELATIONSHIPS_FETCH_REQUEST = 'GROUP_RELATIONSHIPS_FETCH_REQUEST';
const GROUP_RELATIONSHIPS_FETCH_SUCCESS = 'GROUP_RELATIONSHIPS_FETCH_SUCCESS';
const GROUP_RELATIONSHIPS_FETCH_FAIL = 'GROUP_RELATIONSHIPS_FETCH_FAIL';
const GROUPS_FETCH_REQUEST = 'GROUPS_FETCH_REQUEST';
const GROUPS_FETCH_SUCCESS = 'GROUPS_FETCH_SUCCESS';
const GROUPS_FETCH_FAIL = 'GROUPS_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_MEMBERS_FETCH_REQUEST = 'GROUP_MEMBERS_FETCH_REQUEST';
const GROUP_MEMBERS_FETCH_SUCCESS = 'GROUP_MEMBERS_FETCH_SUCCESS';
const GROUP_MEMBERS_FETCH_FAIL = 'GROUP_MEMBERS_FETCH_FAIL';
const GROUP_MEMBERS_EXPAND_REQUEST = 'GROUP_MEMBERS_EXPAND_REQUEST';
const GROUP_MEMBERS_EXPAND_SUCCESS = 'GROUP_MEMBERS_EXPAND_SUCCESS';
const GROUP_MEMBERS_EXPAND_FAIL = 'GROUP_MEMBERS_EXPAND_FAIL';
const GROUP_REMOVED_ACCOUNTS_FETCH_REQUEST = 'GROUP_REMOVED_ACCOUNTS_FETCH_REQUEST';
const GROUP_REMOVED_ACCOUNTS_FETCH_SUCCESS = 'GROUP_REMOVED_ACCOUNTS_FETCH_SUCCESS';
const GROUP_REMOVED_ACCOUNTS_FETCH_FAIL = 'GROUP_REMOVED_ACCOUNTS_FETCH_FAIL';
const GROUP_REMOVED_ACCOUNTS_EXPAND_REQUEST = 'GROUP_REMOVED_ACCOUNTS_EXPAND_REQUEST';
const GROUP_REMOVED_ACCOUNTS_EXPAND_SUCCESS = 'GROUP_REMOVED_ACCOUNTS_EXPAND_SUCCESS';
const GROUP_REMOVED_ACCOUNTS_EXPAND_FAIL = 'GROUP_REMOVED_ACCOUNTS_EXPAND_FAIL';
const GROUP_REMOVED_ACCOUNTS_REMOVE_REQUEST = 'GROUP_REMOVED_ACCOUNTS_REMOVE_REQUEST';
const GROUP_REMOVED_ACCOUNTS_REMOVE_SUCCESS = 'GROUP_REMOVED_ACCOUNTS_REMOVE_SUCCESS';
const GROUP_REMOVED_ACCOUNTS_REMOVE_FAIL = 'GROUP_REMOVED_ACCOUNTS_REMOVE_FAIL';
const GROUP_REMOVED_ACCOUNTS_CREATE_REQUEST = 'GROUP_REMOVED_ACCOUNTS_CREATE_REQUEST';
const GROUP_REMOVED_ACCOUNTS_CREATE_SUCCESS = 'GROUP_REMOVED_ACCOUNTS_CREATE_SUCCESS';
const GROUP_REMOVED_ACCOUNTS_CREATE_FAIL = 'GROUP_REMOVED_ACCOUNTS_CREATE_FAIL';
const GROUP_REMOVE_STATUS_REQUEST = 'GROUP_REMOVE_STATUS_REQUEST';
const GROUP_REMOVE_STATUS_SUCCESS = 'GROUP_REMOVE_STATUS_SUCCESS';
const GROUP_REMOVE_STATUS_FAIL = 'GROUP_REMOVE_STATUS_FAIL';
const fetchGroup = (id: string) => (dispatch: AppDispatch, getState: () => RootState) => {
if (!isLoggedIn(getState)) return;
dispatch(fetchGroupRelationships([id]));
if (getState().groups.get(id)) {
return;
}
dispatch(fetchGroupRequest(id));
api(getState).get(`/api/v1/groups/${id}`)
.then(({ data }) => dispatch(fetchGroupSuccess(data)))
.catch(err => dispatch(fetchGroupFail(id, err)));
};
const fetchGroupRequest = (id: string) => ({
type: GROUP_FETCH_REQUEST,
id,
});
const fetchGroupSuccess = (group: APIEntity) => ({
type: GROUP_FETCH_SUCCESS,
group,
});
const fetchGroupFail = (id: string, error: AxiosError) => ({
type: GROUP_FETCH_FAIL,
id,
error,
});
const fetchGroupRelationships = (groupIds: string[]) =>
(dispatch: AppDispatch, getState: () => RootState) => {
if (!isLoggedIn(getState)) return;
const loadedRelationships = getState().group_relationships;
const newGroupIds = groupIds.filter(id => loadedRelationships.get(id, null) === null);
if (newGroupIds.length === 0) {
return;
}
dispatch(fetchGroupRelationshipsRequest(newGroupIds));
api(getState).get(`/api/v1/groups/${newGroupIds[0]}/relationships?${newGroupIds.map(id => `id[]=${id}`).join('&')}`).then(response => {
dispatch(fetchGroupRelationshipsSuccess(response.data));
}).catch(error => {
dispatch(fetchGroupRelationshipsFail(error));
});
};
const fetchGroupRelationshipsRequest = (ids: string[]) => ({
type: GROUP_RELATIONSHIPS_FETCH_REQUEST,
ids,
skipLoading: true,
});
const fetchGroupRelationshipsSuccess = (relationships: APIEntity[]) => ({
type: GROUP_RELATIONSHIPS_FETCH_SUCCESS,
relationships,
skipLoading: true,
});
const fetchGroupRelationshipsFail = (error: AxiosError) => ({
type: GROUP_RELATIONSHIPS_FETCH_FAIL,
error,
skipLoading: true,
});
const fetchGroups = (tab: string) => (dispatch: AppDispatch, getState: () => RootState) => {
if (!isLoggedIn(getState)) return;
dispatch(fetchGroupsRequest());
api(getState).get('/api/v1/groups?tab=' + tab)
.then(({ data }) => {
dispatch(fetchGroupsSuccess(data, tab));
dispatch(fetchGroupRelationships(data.map((item: APIEntity) => item.id)));
})
.catch(err => dispatch(fetchGroupsFail(err)));
};
const fetchGroupsRequest = () => ({
type: GROUPS_FETCH_REQUEST,
});
const fetchGroupsSuccess = (groups: APIEntity[], tab: string) => ({
type: GROUPS_FETCH_SUCCESS,
groups,
tab,
});
const fetchGroupsFail = (error: AxiosError) => ({
type: GROUPS_FETCH_FAIL,
error,
});
const joinGroup = (id: string) =>
(dispatch: AppDispatch, getState: () => RootState) => {
if (!isLoggedIn(getState)) return;
dispatch(joinGroupRequest(id));
api(getState).post(`/api/v1/groups/${id}/accounts`).then(response => {
dispatch(joinGroupSuccess(response.data));
}).catch(error => {
dispatch(joinGroupFail(id, error));
});
};
const leaveGroup = (id: string) =>
(dispatch: AppDispatch, getState: () => RootState) => {
if (!isLoggedIn(getState)) return;
dispatch(leaveGroupRequest(id));
api(getState).delete(`/api/v1/groups/${id}/accounts`).then(response => {
dispatch(leaveGroupSuccess(response.data));
}).catch(error => {
dispatch(leaveGroupFail(id, error));
});
};
const joinGroupRequest = (id: string) => ({
type: GROUP_JOIN_REQUEST,
id,
});
const joinGroupSuccess = (relationship: APIEntity) => ({
type: GROUP_JOIN_SUCCESS,
relationship,
});
const joinGroupFail = (id: string, error: AxiosError) => ({
type: GROUP_JOIN_FAIL,
id,
error,
});
const leaveGroupRequest = (id: string) => ({
type: GROUP_LEAVE_REQUEST,
id,
});
const leaveGroupSuccess = (relationship: APIEntity) => ({
type: GROUP_LEAVE_SUCCESS,
relationship,
});
const leaveGroupFail = (id: string, error: AxiosError) => ({
type: GROUP_LEAVE_FAIL,
id,
error,
});
const fetchMembers = (id: string) =>
(dispatch: AppDispatch, getState: () => RootState) => {
if (!isLoggedIn(getState)) return;
dispatch(fetchMembersRequest(id));
api(getState).get(`/api/v1/groups/${id}/accounts`).then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(importFetchedAccounts(response.data));
dispatch(fetchMembersSuccess(id, response.data, next ? next.uri : null));
dispatch(fetchRelationships(response.data.map((item: APIEntity) => item.id)));
}).catch(error => {
dispatch(fetchMembersFail(id, error));
});
};
const fetchMembersRequest = (id: string) => ({
type: GROUP_MEMBERS_FETCH_REQUEST,
id,
});
const fetchMembersSuccess = (id: string, accounts: APIEntity[], next: string | null) => ({
type: GROUP_MEMBERS_FETCH_SUCCESS,
id,
accounts,
next,
});
const fetchMembersFail = (id: string, error: AxiosError) => ({
type: GROUP_MEMBERS_FETCH_FAIL,
id,
error,
});
const expandMembers = (id: string) =>
(dispatch: AppDispatch, getState: () => RootState) => {
if (!isLoggedIn(getState)) return;
const url = getState().user_lists.groups.get(id)!.next;
if (url === null) {
return;
}
dispatch(expandMembersRequest(id));
api(getState).get(url).then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(importFetchedAccounts(response.data));
dispatch(expandMembersSuccess(id, response.data, next ? next.uri : null));
dispatch(fetchRelationships(response.data.map((item: APIEntity) => item.id)));
}).catch(error => {
dispatch(expandMembersFail(id, error));
});
};
const expandMembersRequest = (id: string) => ({
type: GROUP_MEMBERS_EXPAND_REQUEST,
id,
});
const expandMembersSuccess = (id: string, accounts: APIEntity[], next: string | null) => ({
type: GROUP_MEMBERS_EXPAND_SUCCESS,
id,
accounts,
next,
});
const expandMembersFail = (id: string, error: AxiosError) => ({
type: GROUP_MEMBERS_EXPAND_FAIL,
id,
error,
});
const fetchRemovedAccounts = (id: string) =>
(dispatch: AppDispatch, getState: () => RootState) => {
if (!isLoggedIn(getState)) return;
dispatch(fetchRemovedAccountsRequest(id));
api(getState).get(`/api/v1/groups/${id}/removed_accounts`).then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(importFetchedAccounts(response.data));
dispatch(fetchRemovedAccountsSuccess(id, response.data, next ? next.uri : null));
dispatch(fetchRelationships(response.data.map((item: APIEntity) => item.id)));
}).catch(error => {
dispatch(fetchRemovedAccountsFail(id, error));
});
};
const fetchRemovedAccountsRequest = (id: string) => ({
type: GROUP_REMOVED_ACCOUNTS_FETCH_REQUEST,
id,
});
const fetchRemovedAccountsSuccess = (id: string, accounts: APIEntity[], next: string | null) => ({
type: GROUP_REMOVED_ACCOUNTS_FETCH_SUCCESS,
id,
accounts,
next,
});
const fetchRemovedAccountsFail = (id: string, error: AxiosError) => ({
type: GROUP_REMOVED_ACCOUNTS_FETCH_FAIL,
id,
error,
});
const expandRemovedAccounts = (id: string) =>
(dispatch: AppDispatch, getState: () => RootState) => {
if (!isLoggedIn(getState)) return;
const url = getState().user_lists.groups_removed_accounts.get(id)!.next;
if (url === null) {
return;
}
dispatch(expandRemovedAccountsRequest(id));
api(getState).get(url).then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(importFetchedAccounts(response.data));
dispatch(expandRemovedAccountsSuccess(id, response.data, next ? next.uri : null));
dispatch(fetchRelationships(response.data.map((item: APIEntity) => item.id)));
}).catch(error => {
dispatch(expandRemovedAccountsFail(id, error));
});
};
const expandRemovedAccountsRequest = (id: string) => ({
type: GROUP_REMOVED_ACCOUNTS_EXPAND_REQUEST,
id,
});
const expandRemovedAccountsSuccess = (id: string, accounts: APIEntity[], next: string | null) => ({
type: GROUP_REMOVED_ACCOUNTS_EXPAND_SUCCESS,
id,
accounts,
next,
});
const expandRemovedAccountsFail = (id: string, error: AxiosError) => ({
type: GROUP_REMOVED_ACCOUNTS_EXPAND_FAIL,
id,
error,
});
const removeRemovedAccount = (groupId: string, id: string) =>
(dispatch: AppDispatch, getState: () => RootState) => {
if (!isLoggedIn(getState)) return;
dispatch(removeRemovedAccountRequest(groupId, id));
api(getState).delete(`/api/v1/groups/${groupId}/removed_accounts?account_id=${id}`).then(response => {
dispatch(removeRemovedAccountSuccess(groupId, id));
}).catch(error => {
dispatch(removeRemovedAccountFail(groupId, id, error));
});
};
const removeRemovedAccountRequest = (groupId: string, id: string) => ({
type: GROUP_REMOVED_ACCOUNTS_REMOVE_REQUEST,
groupId,
id,
});
const removeRemovedAccountSuccess = (groupId: string, id: string) => ({
type: GROUP_REMOVED_ACCOUNTS_REMOVE_SUCCESS,
groupId,
id,
});
const removeRemovedAccountFail = (groupId: string, id: string, error: AxiosError) => ({
type: GROUP_REMOVED_ACCOUNTS_REMOVE_FAIL,
groupId,
id,
error,
});
const createRemovedAccount = (groupId: string, id: string) =>
(dispatch: AppDispatch, getState: () => RootState) => {
if (!isLoggedIn(getState)) return;
dispatch(createRemovedAccountRequest(groupId, id));
api(getState).post(`/api/v1/groups/${groupId}/removed_accounts?account_id=${id}`).then(response => {
dispatch(createRemovedAccountSuccess(groupId, id));
}).catch(error => {
dispatch(createRemovedAccountFail(groupId, id, error));
});
};
const createRemovedAccountRequest = (groupId: string, id: string) => ({
type: GROUP_REMOVED_ACCOUNTS_CREATE_REQUEST,
groupId,
id,
});
const createRemovedAccountSuccess = (groupId: string, id: string) => ({
type: GROUP_REMOVED_ACCOUNTS_CREATE_SUCCESS,
groupId,
id,
});
const createRemovedAccountFail = (groupId: string, id: string, error: AxiosError) => ({
type: GROUP_REMOVED_ACCOUNTS_CREATE_FAIL,
groupId,
id,
error,
});
const groupRemoveStatus = (groupId: string, id: string) =>
(dispatch: AppDispatch, getState: () => RootState) => {
if (!isLoggedIn(getState)) return;
dispatch(groupRemoveStatusRequest(groupId, id));
api(getState).delete(`/api/v1/groups/${groupId}/statuses/${id}`).then(response => {
dispatch(groupRemoveStatusSuccess(groupId, id));
}).catch(error => {
dispatch(groupRemoveStatusFail(groupId, id, error));
});
};
const groupRemoveStatusRequest = (groupId: string, id: string) => ({
type: GROUP_REMOVE_STATUS_REQUEST,
groupId,
id,
});
const groupRemoveStatusSuccess = (groupId: string, id: string) => ({
type: GROUP_REMOVE_STATUS_SUCCESS,
groupId,
id,
});
const groupRemoveStatusFail = (groupId: string, id: string, error: AxiosError) => ({
type: GROUP_REMOVE_STATUS_FAIL,
groupId,
id,
error,
});
export {
GROUP_FETCH_REQUEST,
GROUP_FETCH_SUCCESS,
GROUP_FETCH_FAIL,
GROUP_RELATIONSHIPS_FETCH_REQUEST,
GROUP_RELATIONSHIPS_FETCH_SUCCESS,
GROUP_RELATIONSHIPS_FETCH_FAIL,
GROUPS_FETCH_REQUEST,
GROUPS_FETCH_SUCCESS,
GROUPS_FETCH_FAIL,
GROUP_JOIN_REQUEST,
GROUP_JOIN_SUCCESS,
GROUP_JOIN_FAIL,
GROUP_LEAVE_REQUEST,
GROUP_LEAVE_SUCCESS,
GROUP_LEAVE_FAIL,
GROUP_MEMBERS_FETCH_REQUEST,
GROUP_MEMBERS_FETCH_SUCCESS,
GROUP_MEMBERS_FETCH_FAIL,
GROUP_MEMBERS_EXPAND_REQUEST,
GROUP_MEMBERS_EXPAND_SUCCESS,
GROUP_MEMBERS_EXPAND_FAIL,
GROUP_REMOVED_ACCOUNTS_FETCH_REQUEST,
GROUP_REMOVED_ACCOUNTS_FETCH_SUCCESS,
GROUP_REMOVED_ACCOUNTS_FETCH_FAIL,
GROUP_REMOVED_ACCOUNTS_EXPAND_REQUEST,
GROUP_REMOVED_ACCOUNTS_EXPAND_SUCCESS,
GROUP_REMOVED_ACCOUNTS_EXPAND_FAIL,
GROUP_REMOVED_ACCOUNTS_REMOVE_REQUEST,
GROUP_REMOVED_ACCOUNTS_REMOVE_SUCCESS,
GROUP_REMOVED_ACCOUNTS_REMOVE_FAIL,
GROUP_REMOVED_ACCOUNTS_CREATE_REQUEST,
GROUP_REMOVED_ACCOUNTS_CREATE_SUCCESS,
GROUP_REMOVED_ACCOUNTS_CREATE_FAIL,
GROUP_REMOVE_STATUS_REQUEST,
GROUP_REMOVE_STATUS_SUCCESS,
GROUP_REMOVE_STATUS_FAIL,
fetchGroup,
fetchGroupRequest,
fetchGroupSuccess,
fetchGroupFail,
fetchGroupRelationships,
fetchGroupRelationshipsRequest,
fetchGroupRelationshipsSuccess,
fetchGroupRelationshipsFail,
fetchGroups,
fetchGroupsRequest,
fetchGroupsSuccess,
fetchGroupsFail,
joinGroup,
leaveGroup,
joinGroupRequest,
joinGroupSuccess,
joinGroupFail,
leaveGroupRequest,
leaveGroupSuccess,
leaveGroupFail,
fetchMembers,
fetchMembersRequest,
fetchMembersSuccess,
fetchMembersFail,
expandMembers,
expandMembersRequest,
expandMembersSuccess,
expandMembersFail,
fetchRemovedAccounts,
fetchRemovedAccountsRequest,
fetchRemovedAccountsSuccess,
fetchRemovedAccountsFail,
expandRemovedAccounts,
expandRemovedAccountsRequest,
expandRemovedAccountsSuccess,
expandRemovedAccountsFail,
removeRemovedAccount,
removeRemovedAccountRequest,
removeRemovedAccountSuccess,
removeRemovedAccountFail,
createRemovedAccount,
createRemovedAccountRequest,
createRemovedAccountSuccess,
createRemovedAccountFail,
groupRemoveStatus,
groupRemoveStatusRequest,
groupRemoveStatusSuccess,
groupRemoveStatusFail,
};

Wyświetl plik

@ -1,7 +1,7 @@
import { createAsyncThunk } from '@reduxjs/toolkit';
import get from 'lodash/get';
import KVStore from 'soapbox/storage/kv_store';
import KVStore from 'soapbox/storage/kv-store';
import { RootState } from 'soapbox/store';
import { getAuthUserUrl } from 'soapbox/utils/auth';
import { parseVersion } from 'soapbox/utils/features';
@ -66,7 +66,5 @@ export const loadInstance = createAsyncThunk<void, void, { state: RootState }>(
export const fetchNodeinfo = createAsyncThunk<void, void, { state: RootState }>(
'nodeinfo/fetch',
async(_arg, { getState }) => {
return await api(getState).get('/nodeinfo/2.1.json');
},
async(_arg, { getState }) => await api(getState).get('/nodeinfo/2.1.json'),
);

Wyświetl plik

@ -1,4 +1,4 @@
import KVStore from 'soapbox/storage/kv_store';
import KVStore from 'soapbox/storage/kv-store';
import { getAuthUserId, getAuthUserUrl } from 'soapbox/utils/auth';
import api from '../api';

Wyświetl plik

@ -1,8 +1,10 @@
import type { ModalType } from 'soapbox/features/ui/components/modal-root';
export const MODAL_OPEN = 'MODAL_OPEN';
export const MODAL_CLOSE = 'MODAL_CLOSE';
/** Open a modal of the given type */
export function openModal(type: string, props?: any) {
export function openModal(type: ModalType, props?: any) {
return {
type: MODAL_OPEN,
modalType: type,
@ -11,7 +13,7 @@ export function openModal(type: string, props?: any) {
}
/** Close the modal */
export function closeModal(type?: string) {
export function closeModal(type?: ModalType) {
return {
type: MODAL_CLOSE,
modalType: type,

Wyświetl plik

@ -7,7 +7,7 @@ import { openModal } from 'soapbox/actions/modals';
import snackbar from 'soapbox/actions/snackbar';
import OutlineBox from 'soapbox/components/outline-box';
import { Stack, Text } from 'soapbox/components/ui';
import AccountContainer from 'soapbox/containers/account_container';
import AccountContainer from 'soapbox/containers/account-container';
import { isLocal } from 'soapbox/utils/accounts';
import type { AppDispatch, RootState } from 'soapbox/store';

Wyświetl plik

@ -1,11 +1,11 @@
import { Map as ImmutableMap, Set as ImmutableSet } from 'immutable';
import ConfigDB from 'soapbox/utils/config_db';
import ConfigDB from 'soapbox/utils/config-db';
import { fetchConfig, updateConfig } from './admin';
import type { AppDispatch, RootState } from 'soapbox/store';
import type { Policy } from 'soapbox/utils/config_db';
import type { Policy } from 'soapbox/utils/config-db';
const simplePolicyMerge = (simplePolicy: Policy, host: string, restrictions: ImmutableMap<string, any>) => {
return simplePolicy.map((hosts, key) => {

Wyświetl plik

@ -21,6 +21,7 @@ const MUTES_EXPAND_FAIL = 'MUTES_EXPAND_FAIL';
const MUTES_INIT_MODAL = 'MUTES_INIT_MODAL';
const MUTES_TOGGLE_HIDE_NOTIFICATIONS = 'MUTES_TOGGLE_HIDE_NOTIFICATIONS';
const MUTES_CHANGE_DURATION = 'MUTES_CHANGE_DURATION';
const fetchMutes = () =>
(dispatch: AppDispatch, getState: () => RootState) => {
@ -103,6 +104,14 @@ const toggleHideNotifications = () =>
dispatch({ type: MUTES_TOGGLE_HIDE_NOTIFICATIONS });
};
const changeMuteDuration = (duration: number) =>
(dispatch: AppDispatch) => {
dispatch({
type: MUTES_CHANGE_DURATION,
duration,
});
};
export {
MUTES_FETCH_REQUEST,
MUTES_FETCH_SUCCESS,
@ -112,6 +121,7 @@ export {
MUTES_EXPAND_FAIL,
MUTES_INIT_MODAL,
MUTES_TOGGLE_HIDE_NOTIFICATIONS,
MUTES_CHANGE_DURATION,
fetchMutes,
fetchMutesRequest,
fetchMutesSuccess,
@ -122,4 +132,5 @@ export {
expandMutesFail,
initMuteModal,
toggleHideNotifications,
changeMuteDuration,
};

Wyświetl plik

@ -1,17 +1,14 @@
import {
List as ImmutableList,
Map as ImmutableMap,
} from 'immutable';
import IntlMessageFormat from 'intl-messageformat';
import 'intl-pluralrules';
import { defineMessages } from 'react-intl';
import api, { getLinks } from 'soapbox/api';
import compareId from 'soapbox/compare_id';
import { getFilters, regexFromFilters } from 'soapbox/selectors';
import { isLoggedIn } from 'soapbox/utils/auth';
import { compareId } from 'soapbox/utils/comparators';
import { getFeatures, parseVersion, PLEROMA } from 'soapbox/utils/features';
import { unescapeHTML } from 'soapbox/utils/html';
import { EXCLUDE_TYPES, NOTIFICATION_TYPES } from 'soapbox/utils/notification';
import { joinPublicPath } from 'soapbox/utils/static';
import { fetchRelationships } from './accounts';
@ -92,6 +89,7 @@ const updateNotificationsQueue = (notification: APIEntity, intlMessages: Record<
(dispatch: AppDispatch, getState: () => RootState) => {
if (!notification.type) return; // drop invalid notifications
if (notification.type === 'pleroma:chat_mention') return; // Drop chat notifications, handle them per-chat
if (notification.type === 'chat') return; // Drop Truth Social chat notifications.
const showAlert = getSettings(getState()).getIn(['notifications', 'alerts', notification.type]);
const filters = getFilters(getState(), { contextType: 'notifications' });
@ -149,13 +147,13 @@ const updateNotificationsQueue = (notification: APIEntity, intlMessages: Record<
const dequeueNotifications = () =>
(dispatch: AppDispatch, getState: () => RootState) => {
const queuedNotifications = getState().notifications.get('queuedNotifications');
const totalQueuedNotificationsCount = getState().notifications.get('totalQueuedNotificationsCount');
const queuedNotifications = getState().notifications.queuedNotifications;
const totalQueuedNotificationsCount = getState().notifications.totalQueuedNotificationsCount;
if (totalQueuedNotificationsCount === 0) {
return;
} else if (totalQueuedNotificationsCount > 0 && totalQueuedNotificationsCount <= MAX_QUEUED_NOTIFICATIONS) {
queuedNotifications.forEach((block: APIEntity) => {
queuedNotifications.forEach((block) => {
dispatch(updateNotifications(block.notification));
});
} else {
@ -168,11 +166,8 @@ const dequeueNotifications = () =>
dispatch(markReadNotifications());
};
// const excludeTypesFromSettings = (getState: () => RootState) => (getSettings(getState()).getIn(['notifications', 'shows']) as ImmutableMap<string, boolean>).filter(enabled => !enabled).keySeq().toJS();
const excludeTypesFromFilter = (filter: string) => {
const allTypes = ImmutableList(['follow', 'follow_request', 'favourite', 'reblog', 'mention', 'status', 'poll', 'move', 'pleroma:emoji_reaction']);
return allTypes.filterNot(item => item === filter).toJS();
return NOTIFICATION_TYPES.filter(item => item !== filter);
};
const noOp = () => new Promise(f => f(undefined));
@ -182,11 +177,12 @@ const expandNotifications = ({ maxId }: Record<string, any> = {}, done: () => an
if (!isLoggedIn(getState)) return dispatch(noOp);
const state = getState();
const features = getFeatures(state.instance);
const activeFilter = getSettings(state).getIn(['notifications', 'quickFilter', 'active']) as string;
const notifications = state.notifications;
const isLoadingMore = !!maxId;
if (notifications.get('isLoading')) {
if (notifications.isLoading) {
done();
return dispatch(noOp);
}
@ -195,10 +191,13 @@ const expandNotifications = ({ maxId }: Record<string, any> = {}, done: () => an
max_id: maxId,
};
if (activeFilter !== 'all') {
const instance = state.instance;
const features = getFeatures(instance);
if (activeFilter === 'all') {
if (features.notificationsIncludeTypes) {
params.types = NOTIFICATION_TYPES.filter(type => !EXCLUDE_TYPES.includes(type as any));
} else {
params.exclude_types = EXCLUDE_TYPES;
}
} else {
if (features.notificationsIncludeTypes) {
params.types = [activeFilter];
} else {
@ -206,7 +205,7 @@ const expandNotifications = ({ maxId }: Record<string, any> = {}, done: () => an
}
}
if (!maxId && notifications.get('items').size > 0) {
if (!maxId && notifications.items.size > 0) {
params.since_id = notifications.getIn(['items', 0, 'id']);
}
@ -305,8 +304,8 @@ const markReadNotifications = () =>
if (!isLoggedIn(getState)) return;
const state = getState();
const topNotificationId: string | undefined = state.notifications.get('items').first(ImmutableMap()).get('id');
const lastReadId: string | -1 = state.notifications.get('lastRead');
const topNotificationId = state.notifications.items.first()?.id;
const lastReadId = state.notifications.lastRead;
const v = parseVersion(state.instance.version);
if (topNotificationId && (lastReadId === -1 || compareId(topNotificationId, lastReadId) > 0)) {

Wyświetl plik

@ -1,4 +1,4 @@
import { createPushSubscription, updatePushSubscription } from 'soapbox/actions/push_subscriptions';
import { createPushSubscription, updatePushSubscription } from 'soapbox/actions/push-subscriptions';
import { pushNotificationsSetting } from 'soapbox/settings';
import { getVapidKey } from 'soapbox/utils/auth';
import { decode as decodeBase64 } from 'soapbox/utils/base64';

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, 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,26 +20,23 @@ const REPORT_BLOCK_CHANGE = 'REPORT_BLOCK_CHANGE';
const REPORT_RULE_CHANGE = 'REPORT_RULE_CHANGE';
const initReport = (account: Account, status?: Status) =>
(dispatch: AppDispatch) => {
dispatch({
type: REPORT_INIT,
account,
status,
});
type ReportedEntity = {
status?: Status,
chatMessage?: ChatMessage
}
return dispatch(openModal('REPORT'));
};
const initReport = (account: Account, entities?: ReportedEntity) => (dispatch: AppDispatch) => {
const { status, chatMessage } = entities || {};
const initReportById = (accountId: string) =>
(dispatch: AppDispatch, getState: () => RootState) => {
dispatch({
type: REPORT_INIT,
account: getState().accounts.get(accountId),
});
dispatch({
type: REPORT_INIT,
account,
status,
chatMessage,
});
dispatch(openModal('REPORT'));
};
return dispatch(openModal('REPORT'));
};
const cancelReport = () => ({
type: REPORT_CANCEL,
@ -59,6 +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'])],
rule_ids: reports.getIn(['new', 'rule_ids']),
comment: reports.getIn(['new', 'comment']),
forward: reports.getIn(['new', 'forward']),
@ -110,7 +108,6 @@ export {
REPORT_BLOCK_CHANGE,
REPORT_RULE_CHANGE,
initReport,
initReportById,
cancelReport,
toggleStatusReport,
submitReport,

Wyświetl plik

@ -2,7 +2,7 @@ import { createSelector } from 'reselect';
import { getHost } from 'soapbox/actions/instance';
import { normalizeSoapboxConfig } from 'soapbox/normalizers';
import KVStore from 'soapbox/storage/kv_store';
import KVStore from 'soapbox/storage/kv-store';
import { removeVS16s } from 'soapbox/utils/emoji';
import { getFeatures } from 'soapbox/utils/features';

Wyświetl plik

@ -0,0 +1,75 @@
import api, { getLinks } from '../api';
import { importFetchedStatuses } from './importer';
import type { AppDispatch, RootState } from 'soapbox/store';
export const STATUS_QUOTES_FETCH_REQUEST = 'STATUS_QUOTES_FETCH_REQUEST';
export const STATUS_QUOTES_FETCH_SUCCESS = 'STATUS_QUOTES_FETCH_SUCCESS';
export const STATUS_QUOTES_FETCH_FAIL = 'STATUS_QUOTES_FETCH_FAIL';
export const STATUS_QUOTES_EXPAND_REQUEST = 'STATUS_QUOTES_EXPAND_REQUEST';
export const STATUS_QUOTES_EXPAND_SUCCESS = 'STATUS_QUOTES_EXPAND_SUCCESS';
export const STATUS_QUOTES_EXPAND_FAIL = 'STATUS_QUOTES_EXPAND_FAIL';
const noOp = () => new Promise(f => f(null));
export const fetchStatusQuotes = (statusId: string) =>
(dispatch: AppDispatch, getState: () => RootState) => {
if (getState().status_lists.getIn([`quotes:${statusId}`, 'isLoading'])) {
return dispatch(noOp);
}
dispatch({
statusId,
type: STATUS_QUOTES_FETCH_REQUEST,
});
return api(getState).get(`/api/v1/pleroma/statuses/${statusId}/quotes`).then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(importFetchedStatuses(response.data));
return dispatch({
type: STATUS_QUOTES_FETCH_SUCCESS,
statusId,
statuses: response.data,
next: next ? next.uri : null,
});
}).catch(error => {
dispatch({
type: STATUS_QUOTES_FETCH_FAIL,
statusId,
error,
});
});
};
export const expandStatusQuotes = (statusId: string) =>
(dispatch: AppDispatch, getState: () => RootState) => {
const url = getState().status_lists.getIn([`quotes:${statusId}`, 'next'], null) as string | null;
if (url === null || getState().status_lists.getIn([`quotes:${statusId}`, 'isLoading'])) {
return dispatch(noOp);
}
dispatch({
type: STATUS_QUOTES_EXPAND_REQUEST,
statusId,
});
return api(getState).get(url).then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(importFetchedStatuses(response.data));
dispatch({
type: STATUS_QUOTES_EXPAND_SUCCESS,
statusId,
statuses: response.data,
next: next ? next.uri : null,
});
}).catch(error => {
dispatch({
type: STATUS_QUOTES_EXPAND_FAIL,
statusId,
error,
});
});
};

Wyświetl plik

@ -43,6 +43,11 @@ const STATUS_UNMUTE_FAIL = 'STATUS_UNMUTE_FAIL';
const STATUS_REVEAL = 'STATUS_REVEAL';
const STATUS_HIDE = 'STATUS_HIDE';
const STATUS_TRANSLATE_REQUEST = 'STATUS_TRANSLATE_REQUEST';
const STATUS_TRANSLATE_SUCCESS = 'STATUS_TRANSLATE_SUCCESS';
const STATUS_TRANSLATE_FAIL = 'STATUS_TRANSLATE_FAIL';
const STATUS_TRANSLATE_UNDO = 'STATUS_TRANSLATE_UNDO';
const statusExists = (getState: () => RootState, statusId: string) => {
return (getState().statuses.get(statusId) || null) !== null;
};
@ -305,6 +310,31 @@ const toggleStatusHidden = (status: Status) => {
}
};
const translateStatus = (id: string, targetLanguage?: string) => (dispatch: AppDispatch, getState: () => RootState) => {
dispatch({ type: STATUS_TRANSLATE_REQUEST, id });
api(getState).post(`/api/v1/statuses/${id}/translate`, {
target_language: targetLanguage,
}).then(response => {
dispatch({
type: STATUS_TRANSLATE_SUCCESS,
id,
translation: response.data,
});
}).catch(error => {
dispatch({
type: STATUS_TRANSLATE_FAIL,
id,
error,
});
});
};
const undoStatusTranslation = (id: string) => ({
type: STATUS_TRANSLATE_UNDO,
id,
});
export {
STATUS_CREATE_REQUEST,
STATUS_CREATE_SUCCESS,
@ -329,6 +359,10 @@ export {
STATUS_UNMUTE_FAIL,
STATUS_REVEAL,
STATUS_HIDE,
STATUS_TRANSLATE_REQUEST,
STATUS_TRANSLATE_SUCCESS,
STATUS_TRANSLATE_FAIL,
STATUS_TRANSLATE_UNDO,
createStatus,
editStatus,
fetchStatus,
@ -345,4 +379,6 @@ export {
hideStatus,
revealStatus,
toggleStatusHidden,
translateStatus,
undoStatusTranslation,
};

Wyświetl plik

@ -1,5 +1,10 @@
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 } from 'soapbox/utils/chats';
import { removePageItem } from 'soapbox/utils/queries';
import { play, soundCache } from 'soapbox/utils/sounds';
import { connectStream } from '../stream';
@ -22,8 +27,9 @@ import {
processTimelineUpdate,
} from './timelines';
import type { IStatContext } from 'soapbox/contexts/stat-context';
import type { AppDispatch, RootState } from 'soapbox/store';
import type { APIEntity } from 'soapbox/types/entities';
import type { APIEntity, Chat } from 'soapbox/types/entities';
const STREAMING_CHAT_UPDATE = 'STREAMING_CHAT_UPDATE';
const STREAMING_FOLLOW_RELATIONSHIPS_UPDATE = 'STREAMING_FOLLOW_RELATIONSHIPS_UPDATE';
@ -45,11 +51,45 @@ const updateFollowRelationships = (relationships: APIEntity) =>
});
};
const removeChatMessage = (payload: string) => {
const data = JSON.parse(payload);
const chatId = data.chat_id;
const chatMessageId = data.deleted_message_id;
// If the user just deleted the "last_message", then let's invalidate
// the Chat Search query so the Chat List will show the new "last_message".
if (isLastMessage(chatMessageId)) {
queryClient.invalidateQueries(ChatKeys.chatSearch());
}
removePageItem(ChatKeys.chatMessages(chatId), chatMessageId, (o: any, n: any) => String(o.id) === String(n));
};
// Update the specific Chat query data.
const updateChatQuery = (chat: IChat) => {
const cachedChat = queryClient.getQueryData<IChat>(ChatKeys.chat(chat.id));
if (!cachedChat) {
return;
}
const newChat = {
...cachedChat,
latest_read_message_by_account: chat.latest_read_message_by_account,
latest_read_message_created_at: chat.latest_read_message_created_at,
};
queryClient.setQueryData<Chat>(ChatKeys.chat(chat.id), newChat as any);
};
interface StreamOpts {
statContext?: IStatContext,
}
const connectTimelineStream = (
timelineId: string,
path: string,
pollingRefresh: ((dispatch: AppDispatch, done?: () => void) => void) | null = null,
accept: ((status: APIEntity) => boolean) | null = null,
opts?: StreamOpts,
) => connectStream(path, pollingRefresh, (dispatch: AppDispatch, getState: () => RootState) => {
const locale = getLocale(getState());
@ -78,7 +118,14 @@ const connectTimelineStream = (
// break;
case 'notification':
messages[locale]().then(messages => {
dispatch(updateNotificationsQueue(JSON.parse(data.payload), messages, locale, window.location.pathname));
dispatch(
updateNotificationsQueue(
JSON.parse(data.payload),
messages,
locale,
window.location.pathname,
),
);
}).catch(error => {
console.error(error);
});
@ -90,18 +137,37 @@ const connectTimelineStream = (
dispatch(fetchFilters());
break;
case 'pleroma:chat_update':
dispatch((dispatch: AppDispatch, getState: () => RootState) => {
case 'chat_message.created': // TruthSocial
dispatch((_dispatch: AppDispatch, getState: () => RootState) => {
const chat = JSON.parse(data.payload);
const me = getState().me;
const messageOwned = !(chat.last_message && chat.last_message.account_id !== me);
const messageOwned = chat.last_message?.account_id === me;
const settings = getSettings(getState());
dispatch({
type: STREAMING_CHAT_UPDATE,
chat,
me,
// Only play sounds for recipient messages
meta: !messageOwned && getSettings(getState()).getIn(['chats', 'sound']) && { sound: 'chat' },
});
// Don't update own messages from streaming
if (!messageOwned) {
updateChatListItem(chat);
if (settings.getIn(['chats', 'sound'])) {
play(soundCache.chat);
}
// Increment unread counter
opts?.statContext?.setUnreadChatsCount(getUnreadChatsCount());
}
});
break;
case 'chat_message.deleted': // TruthSocial
removeChatMessage(data.payload);
break;
case 'chat_message.read': // TruthSocial
dispatch((_dispatch: AppDispatch, getState: () => RootState) => {
const chat = JSON.parse(data.payload);
const me = getState().me;
const isFromOtherUser = chat.account.id !== me;
if (isFromOtherUser) {
updateChatQuery(JSON.parse(data.payload));
}
});
break;
case 'pleroma:follow_relationships_update':
@ -129,8 +195,8 @@ const refreshHomeTimelineAndNotification = (dispatch: AppDispatch, done?: () =>
dispatch(expandNotifications({}, () =>
dispatch(fetchAnnouncements(done))))));
const connectUserStream = () =>
connectTimelineStream('home', 'user', refreshHomeTimelineAndNotification);
const connectUserStream = (opts?: StreamOpts) =>
connectTimelineStream('home', 'user', refreshHomeTimelineAndNotification, null, opts);
const connectCommunityStream = ({ onlyMedia }: Record<string, any> = {}) =>
connectTimelineStream(`community${onlyMedia ? ':media' : ''}`, `public:local${onlyMedia ? ':media' : ''}`);

Wyświetl plik

@ -4,7 +4,7 @@ import LinkHeader from 'http-link-header';
import type { AxiosInstance, AxiosResponse } from 'axios';
const api = jest.requireActual('../api') as Record<string, Function>;
const api = jest.requireActual('../index') as Record<string, Function>;
let mocks: Array<Function> = [];
export const __stub = (func: (mock: MockAdapter) => void) => mocks.push(func);
@ -21,6 +21,11 @@ export const getLinks = (response: AxiosResponse): LinkHeader => {
return new LinkHeader(response.headers?.link);
};
export const getNextLink = (response: AxiosResponse) => {
const nextLink = new LinkHeader(response.headers?.link);
return nextLink.refs.find((ref) => ref.uri)?.uri;
};
export const baseClient = (...params: any[]) => {
const axios = api.baseClient(...params);
setupMock(axios);

Wyświetl plik

@ -9,18 +9,18 @@ import axios, { AxiosInstance, AxiosResponse } from 'axios';
import LinkHeader from 'http-link-header';
import { createSelector } from 'reselect';
import * as BuildConfig from 'soapbox/build_config';
import * as BuildConfig from 'soapbox/build-config';
import { RootState } from 'soapbox/store';
import { getAccessToken, getAppToken, isURL, parseBaseURL } from 'soapbox/utils/auth';
import type MockAdapter from 'axios-mock-adapter';
/**
Parse Link headers, mostly for pagination.
@see {@link https://www.npmjs.com/package/http-link-header}
@param {object} response - Axios response object
@returns {object} Link object
*/
Parse Link headers, mostly for pagination.
@see {@link https://www.npmjs.com/package/http-link-header}
@param {object} response - Axios response object
@returns {object} Link object
*/
export const getLinks = (response: AxiosResponse): LinkHeader => {
return new LinkHeader(response.headers?.link);
};
@ -50,11 +50,11 @@ const getAuthBaseURL = createSelector([
});
/**
* Base client for HTTP requests.
* @param {string} accessToken
* @param {string} baseURL
* @returns {object} Axios instance
*/
* Base client for HTTP requests.
* @param {string} accessToken
* @param {string} baseURL
* @returns {object} Axios instance
*/
export const baseClient = (accessToken?: string | null, baseURL: string = ''): AxiosInstance => {
return axios.create({
// When BACKEND_URL is set, always use it.
@ -68,22 +68,22 @@ export const baseClient = (accessToken?: string | null, baseURL: string = ''): A
};
/**
* Dumb client for grabbing static files.
* It uses FE_SUBDIRECTORY and parses JSON if possible.
* No authorization is needed.
*/
* Dumb client for grabbing static files.
* It uses FE_SUBDIRECTORY and parses JSON if possible.
* No authorization is needed.
*/
export const staticClient = axios.create({
baseURL: BuildConfig.FE_SUBDIRECTORY,
transformResponse: [maybeParseJSON],
});
/**
* Stateful API client.
* Uses credentials from the Redux store if available.
* @param {function} getState - Must return the Redux state
* @param {string} authType - Either 'user' or 'app'
* @returns {object} Axios instance
*/
* Stateful API client.
* Uses credentials from the Redux store if available.
* @param {function} getState - Must return the Redux state
* @param {string} authType - Either 'user' or 'app'
* @returns {object} Axios instance
*/
export default (getState: () => RootState, authType: string = 'user'): AxiosInstance => {
const state = getState();
const accessToken = getToken(state, authType);

Wyświetl plik

@ -1,7 +1,7 @@
// @preval
/**
* Build config: configuration set at build time.
* @module soapbox/build_config
* @module soapbox/build-config
*/
const trim = require('lodash/trim');

Wyświetl plik

@ -1,21 +0,0 @@
'use strict';
/**
* Compare numerical primary keys represented as strings.
* For example, '10' (as a string) is considered less than '9'
* when sorted alphabetically. So compare string length first.
*
* - `0`: id1 == id2
* - `1`: id1 > id2
* - `-1`: id1 < id2
*/
export default function compareId(id1: string, id2: string) {
if (id1 === id2) {
return 0;
}
if (id1.length === id2.length) {
return id1 > id2 ? 1 : -1;
} else {
return id1.length > id2.length ? 1 : -1;
}
}

Wyświetl plik

@ -1,4 +1,4 @@
import * as React from 'react';
import React from 'react';
interface IInlineSVG {
loader?: JSX.Element,

Wyświetl plik

@ -1,7 +1,7 @@
import React from 'react';
import { render, screen } from '../../jest/test-helpers';
import AutosuggestEmoji from '../autosuggest_emoji';
import AutosuggestEmoji from '../autosuggest-emoji';
describe('<AutosuggestEmoji />', () => {
it('renders native emoji', () => {

Wyświetl plik

@ -3,7 +3,7 @@ import React from 'react';
import { normalizeAccount } from 'soapbox/normalizers';
import { render, screen } from '../../jest/test-helpers';
import AvatarOverlay from '../avatar_overlay';
import AvatarOverlay from '../avatar-overlay';
import type { ReducerAccount } from 'soapbox/reducers/accounts';

Wyświetl plik

@ -1,7 +1,7 @@
import React from 'react';
import { render, screen } from '../../jest/test-helpers';
import EmojiSelector from '../emoji_selector';
import EmojiSelector from '../emoji-selector';
describe('<EmojiSelector />', () => {
it('renders correctly', () => {

Wyświetl plik

@ -0,0 +1,42 @@
import React from 'react';
import { render, screen, rootState } from '../../jest/test-helpers';
import { normalizeStatus, normalizeAccount } from '../../normalizers';
import Status from '../status';
import type { ReducerStatus } from 'soapbox/reducers/statuses';
const account = normalizeAccount({
id: '1',
acct: 'alex',
});
const status = normalizeStatus({
id: '1',
account,
content: 'hello world',
contentHtml: 'hello world',
}) as ReducerStatus;
describe('<Status />', () => {
const state = rootState.setIn(['accounts', '1'], account);
it('renders content', () => {
render(<Status status={status} />, undefined, state);
screen.getByText(/hello world/i);
expect(screen.getByTestId('status')).toHaveTextContent(/hello world/i);
});
describe('the Status Action Bar', () => {
it('is rendered', () => {
render(<Status status={status} />, undefined, state);
expect(screen.getByTestId('status-action-bar')).toBeInTheDocument();
});
it('is not rendered if status is under review', () => {
const inReviewStatus = normalizeStatus({ ...status, visibility: 'self' });
render(<Status status={inReviewStatus as ReducerStatus} />, undefined, state);
expect(screen.queryAllByTestId('status-action-bar')).toHaveLength(0);
});
});
});

Wyświetl plik

@ -2,8 +2,9 @@ import classNames from 'clsx';
import React, { useState } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import AutosuggestAccountInput from 'soapbox/components/autosuggest_account_input';
import Icon from 'soapbox/components/icon';
import AutosuggestAccountInput from 'soapbox/components/autosuggest-account-input';
import SvgIcon from './ui/icon/svg-icon';
const messages = defineMessages({
placeholder: { id: 'account_search.placeholder', defaultMessage: 'Search for an account' },
@ -14,8 +15,6 @@ interface IAccountSearch {
onSelected: (accountId: string) => void,
/** Override the default placeholder of the input. */
placeholder?: string,
/** Position of results relative to the input. */
resultsPosition?: 'above' | 'below',
}
/** Input to search for accounts. */
@ -56,9 +55,10 @@ const AccountSearch: React.FC<IAccountSearch> = ({ onSelected, ...rest }) => {
};
return (
<div className='search search--account'>
<label>
<span style={{ display: 'none' }}>{intl.formatMessage(messages.placeholder)}</span>
<div className='w-full'>
<label className='sr-only'>{intl.formatMessage(messages.placeholder)}</label>
<div className='relative'>
<AutosuggestAccountInput
className='rounded-full'
placeholder={intl.formatMessage(messages.placeholder)}
@ -68,10 +68,24 @@ const AccountSearch: React.FC<IAccountSearch> = ({ onSelected, ...rest }) => {
onKeyDown={handleKeyDown}
{...rest}
/>
</label>
<div role='button' tabIndex={0} className='search__icon' onClick={handleClear}>
<Icon src={require('@tabler/icons/search.svg')} className={classNames('svg-icon--search', { active: isEmpty() })} />
<Icon src={require('@tabler/icons/backspace.svg')} className={classNames('svg-icon--backspace', { active: !isEmpty() })} aria-label={intl.formatMessage(messages.placeholder)} />
<div
role='button'
tabIndex={0}
className='absolute inset-y-0 right-0 px-3 flex items-center cursor-pointer'
onClick={handleClear}
>
<SvgIcon
src={require('@tabler/icons/search.svg')}
className={classNames('h-4 w-4 text-gray-400', { hidden: !isEmpty() })}
/>
<SvgIcon
src={require('@tabler/icons/x.svg')}
className={classNames('h-4 w-4 text-gray-400', { hidden: isEmpty() })}
aria-label={intl.formatMessage(messages.placeholder)}
/>
</div>
</div>
</div>
);

Wyświetl plik

@ -1,8 +1,8 @@
import * as React from 'react';
import React from 'react';
import { Link, useHistory } from 'react-router-dom';
import HoverRefWrapper from 'soapbox/components/hover_ref_wrapper';
import VerificationBadge from 'soapbox/components/verification_badge';
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, useOnScreen } from 'soapbox/hooks';
import { getAcct } from 'soapbox/utils/accounts';
@ -22,7 +22,13 @@ const InstanceFavicon: React.FC<IInstanceFavicon> = ({ account }) => {
const handleClick: React.MouseEventHandler = (e) => {
e.stopPropagation();
history.push(`/timeline/${account.domain}`);
const timelineUrl = `/timeline/${account.domain}`;
if (!(e.ctrlKey || e.metaKey)) {
history.push(timelineUrl);
} else {
window.open(timelineUrl, '_blank');
}
};
return (
@ -63,6 +69,7 @@ interface IAccount {
withRelationship?: boolean,
showEdit?: boolean,
emoji?: string,
note?: string,
}
const Account = ({
@ -86,6 +93,7 @@ const Account = ({
withRelationship = true,
showEdit = false,
emoji,
note,
}: IAccount) => {
const overflowRef = React.useRef<HTMLDivElement>(null);
const actionRef = React.useRef<HTMLDivElement>(null);
@ -163,7 +171,7 @@ const Account = ({
return (
<div data-testid='account' className='flex-shrink-0 group block w-full' ref={overflowRef}>
<HStack alignItems={actionAlignment} justifyContent='between'>
<HStack alignItems={withAccountNote ? 'top' : 'center'} space={3}>
<HStack alignItems={withAccountNote || note ? 'top' : 'center'} space={3}>
<ProfilePopper
condition={showProfileHoverCard}
wrapper={(children) => <HoverRefWrapper className='relative' accountId={account.id} inline>{children}</HoverRefWrapper>}
@ -193,7 +201,7 @@ const Account = ({
title={account.acct}
onClick={(event: React.MouseEvent) => event.stopPropagation()}
>
<div className='flex items-center space-x-1 flex-grow' style={style}>
<HStack space={1} alignItems='center' grow style={style}>
<Text
size='sm'
weight='semibold'
@ -202,11 +210,11 @@ const Account = ({
/>
{account.verified && <VerificationBadge />}
</div>
</HStack>
</LinkEl>
</ProfilePopper>
<Stack space={withAccountNote ? 1 : 0}>
<Stack space={withAccountNote || note ? 1 : 0}>
<HStack alignItems='center' space={1} style={style}>
<Text theme='muted' size='sm' truncate>@{username}</Text>
@ -219,7 +227,7 @@ const Account = ({
<Text tag='span' theme='muted' size='sm'>&middot;</Text>
{timestampUrl ? (
<Link to={timestampUrl} className='hover:underline'>
<Link to={timestampUrl} className='hover:underline' onClick={(event) => event.stopPropagation()}>
<RelativeTimestamp timestamp={timestamp} theme='muted' size='sm' className='whitespace-nowrap' futureDate={futureTimestamp} />
</Link>
) : (
@ -235,13 +243,28 @@ const Account = ({
<Icon className='h-5 w-5 text-gray-700 dark:text-gray-600' src={require('@tabler/icons/pencil.svg')} />
</>
) : null}
{actionType === 'muting' && account.mute_expires_at ? (
<>
<Text tag='span' theme='muted' size='sm'>&middot;</Text>
<Text theme='muted' size='sm'><RelativeTimestamp timestamp={account.mute_expires_at} futureDate /></Text>
</>
) : null}
</HStack>
{withAccountNote && (
{note ? (
<Text
size='sm'
className='mr-2'
>
{note}
</Text>
) : withAccountNote && (
<Text
size='sm'
dangerouslySetInnerHTML={{ __html: account.note_emojified }}
className='mr-2'
className='mr-2 rtl:ml-2 rtl:mr-0'
/>
)}
</Stack>

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