sforkowany z mirror/soapbox
Porównaj commity
5 Commity
Autor | SHA1 | Data |
---|---|---|
tassoman | 3dfec790be | |
Alex Gleason | 2a3018ed36 | |
tassoman | 7337d2ce16 | |
tassoman | 912d6cf453 | |
tassoman | 18cdd9c77b |
|
@ -5,7 +5,6 @@ module.exports = {
|
|||
'eslint:recommended',
|
||||
'plugin:import/typescript',
|
||||
'plugin:compat/recommended',
|
||||
'plugin:tailwindcss/recommended',
|
||||
],
|
||||
|
||||
env: {
|
||||
|
@ -56,16 +55,12 @@ module.exports = {
|
|||
},
|
||||
polyfills: [
|
||||
'es:all', // core-js
|
||||
'fetch', // not polyfilled, but ignore it
|
||||
'IntersectionObserver', // npm:intersection-observer
|
||||
'Promise', // core-js
|
||||
'ResizeObserver', // npm:resize-observer-polyfill
|
||||
'URL', // core-js
|
||||
'URLSearchParams', // core-js
|
||||
],
|
||||
tailwindcss: {
|
||||
config: 'tailwind.config.cjs',
|
||||
},
|
||||
},
|
||||
|
||||
rules: {
|
||||
|
@ -240,7 +235,18 @@ module.exports = {
|
|||
},
|
||||
],
|
||||
'import/newline-after-import': 'error',
|
||||
'import/no-extraneous-dependencies': 'error',
|
||||
'import/no-extraneous-dependencies': [
|
||||
'error',
|
||||
// {
|
||||
// devDependencies: [
|
||||
// 'webpack/**',
|
||||
// 'app/soapbox/test_setup.js',
|
||||
// 'app/soapbox/test_helpers.js',
|
||||
// 'app/**/__tests__/**',
|
||||
// 'app/**/__mocks__/**',
|
||||
// ],
|
||||
// },
|
||||
],
|
||||
'import/no-unresolved': 'error',
|
||||
'import/no-webpack-loader-syntax': 'error',
|
||||
'import/order': [
|
||||
|
@ -261,30 +267,10 @@ module.exports = {
|
|||
},
|
||||
],
|
||||
'@typescript-eslint/no-duplicate-imports': 'error',
|
||||
'@typescript-eslint/member-delimiter-style': [
|
||||
'error',
|
||||
{
|
||||
multiline: {
|
||||
delimiter: 'none',
|
||||
},
|
||||
singleline: {
|
||||
delimiter: 'comma',
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
'promise/catch-or-return': 'error',
|
||||
|
||||
'react-hooks/rules-of-hooks': 'error',
|
||||
|
||||
'tailwindcss/classnames-order': [
|
||||
'error',
|
||||
{
|
||||
classRegex: '^(base|container|icon|item|list|outer|wrapper)?[c|C]lass(Name)?$',
|
||||
config: 'tailwind.config.cjs',
|
||||
},
|
||||
],
|
||||
'tailwindcss/migration-from-tailwind-2': 'error',
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
|
|
|
@ -149,19 +149,19 @@ pages:
|
|||
|
||||
docker:
|
||||
stage: deploy
|
||||
image: docker:23.0.0
|
||||
image: docker:20.10.23
|
||||
services:
|
||||
- docker:23.0.0-dind
|
||||
- docker:20.10.23-dind
|
||||
tags:
|
||||
- dind
|
||||
# https://medium.com/devops-with-valentine/how-to-build-a-docker-image-and-push-it-to-the-gitlab-container-registry-from-a-gitlab-ci-pipeline-acac0d1f26df
|
||||
script:
|
||||
- echo $CI_REGISTRY_PASSWORD | docker login -u $CI_REGISTRY_USER $CI_REGISTRY --password-stdin
|
||||
- docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_TAG .
|
||||
- docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_TAG
|
||||
rules:
|
||||
- if: $CI_COMMIT_TAG
|
||||
interruptible: false
|
||||
- docker build -t $CI_REGISTRY_IMAGE .
|
||||
- docker push $CI_REGISTRY_IMAGE
|
||||
only:
|
||||
variables:
|
||||
- $CI_DEFAULT_BRANCH == $CI_COMMIT_REF_NAME
|
||||
|
||||
release:
|
||||
stage: release
|
||||
|
|
|
@ -1,43 +0,0 @@
|
|||
import sharedConfig from '../webpack/shared';
|
||||
|
||||
import type { StorybookConfig } from '@storybook/core-common';
|
||||
|
||||
const config: StorybookConfig = {
|
||||
stories: [
|
||||
'../stories/**/*.stories.mdx',
|
||||
'../stories/**/*.stories.@(js|jsx|ts|tsx)'
|
||||
],
|
||||
addons: [
|
||||
'@storybook/addon-links',
|
||||
'@storybook/addon-essentials',
|
||||
'@storybook/addon-interactions',
|
||||
'storybook-react-intl',
|
||||
{
|
||||
name: '@storybook/addon-postcss',
|
||||
options: {
|
||||
postcssLoaderOptions: {
|
||||
implementation: require('postcss'),
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
framework: '@storybook/react',
|
||||
core: {
|
||||
builder: '@storybook/builder-webpack5',
|
||||
},
|
||||
webpackFinal: async (config) => {
|
||||
config.resolve!.alias = {
|
||||
...sharedConfig.resolve!.alias,
|
||||
...config.resolve!.alias,
|
||||
};
|
||||
|
||||
config.resolve!.modules = [
|
||||
...sharedConfig.resolve!.modules!,
|
||||
...config.resolve!.modules!,
|
||||
];
|
||||
|
||||
return config;
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
|
@ -1,22 +0,0 @@
|
|||
import '../app/styles/tailwind.css';
|
||||
import '../stories/theme.css';
|
||||
|
||||
import { addDecorator, Story } from '@storybook/react';
|
||||
import { IntlProvider } from 'react-intl';
|
||||
import React from 'react';
|
||||
|
||||
const withProvider = (Story: Story) => (
|
||||
<IntlProvider locale='en'><Story /></IntlProvider>
|
||||
);
|
||||
|
||||
addDecorator(withProvider);
|
||||
|
||||
export const parameters = {
|
||||
actions: { argTypesRegex: '^on[A-Z].*' },
|
||||
controls: {
|
||||
matchers: {
|
||||
color: /(background|color)$/i,
|
||||
date: /Date$/,
|
||||
},
|
||||
},
|
||||
};
|
|
@ -1 +1 @@
|
|||
nodejs 18.14.0
|
||||
nodejs 18.13.0
|
||||
|
|
118
CHANGELOG.md
118
CHANGELOG.md
|
@ -1,4 +1,5 @@
|
|||
# Changelog
|
||||
|
||||
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/),
|
||||
|
@ -7,67 +8,36 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
- Posts: Support posts filtering on recent Mastodon versions
|
||||
- Reactions: Support custom emoji reactions
|
||||
- Compatbility: Support Mastodon v2 timeline filters.
|
||||
- Posts: Support dislikes on Friendica.
|
||||
- UI: added a character counter to some textareas.
|
||||
|
||||
### Changed
|
||||
- Posts: truncate Nostr pubkeys in reply mentions.
|
||||
- Posts: upgraded emoji picker component.
|
||||
- UI: unified design of "approve" and "reject" buttons in follow requests and waitlist.
|
||||
|
||||
### Fixed
|
||||
- Posts: fixed emojis being cut off in reactions modal.
|
||||
- Posts: fix audio player progress bar visibility.
|
||||
- Posts: added missing gap in pending status.
|
||||
- Compatibility: fixed quote posting compatibility with custom Pleroma forks.
|
||||
- Profile: fix "load more" button height on account gallery page.
|
||||
- 18n: fixed Chinese language being detected from the browser.
|
||||
- Conversations: fixed pagination (Mastodon).
|
||||
- Compatibility: fix version parsing for Friendica.
|
||||
|
||||
## [3.2.0] - 2023-02-15
|
||||
|
||||
### Added
|
||||
- Admin: redirect the homepage to any URL.
|
||||
- Compatibility: added compatibility with Friendica.
|
||||
- Posts: bot badge on statuses from bot accounts.
|
||||
- Compatibility: improved browser support for older browsers.
|
||||
- Events: allow to repost events in event menu.
|
||||
- Profile: Add RSS link to user profiles.
|
||||
- Reactions: adds support for reacting to chat messages.
|
||||
- Groups: initial support for groups.
|
||||
- Profile: add RSS link to user profiles.
|
||||
- Chats: reset chat message field height after sending a message.
|
||||
- Admin: allow to manage announcements.
|
||||
- Groups: Initial support for groups.
|
||||
|
||||
### Changed
|
||||
|
||||
- Chats: improved display of media attachments.
|
||||
- ServiceWorker: switch to a network-first strategy. The "An update is available!" prompt goes away.
|
||||
- Posts: increased font size of focused status in threads.
|
||||
- Posts: let "mute conversation" be clicked from any feed, not just noficiations.
|
||||
- Posts: display all emoji reactions.
|
||||
- Reactions: improved UI of reactions on statuses.
|
||||
- Profile: make verified badge more prominent, overlapping with avatar.
|
||||
|
||||
### Fixed
|
||||
- Admin: fixed hover card in reports modal shows reporter not reportee
|
||||
|
||||
- Reports dashboard: usability improvement (see #1329)
|
||||
- Chats: media attachments rendering at the wrong size and/or causing the chat to scroll on load.
|
||||
- Chats: don't display "copy" button for messages without text.
|
||||
- Posts: don't have to click the play button twice for embedded videos.
|
||||
- index.html: remove `referrer` meta tag so it doesn't conflict with backend's `Referrer-Policy` header.
|
||||
- Modals: fix media modal automatically switching to video.
|
||||
- Navigation: profile dropdown erratic behavior.
|
||||
- Posts: fix posts filtering.
|
||||
|
||||
### Removed
|
||||
|
||||
- Admin: single user mode. Now the homepage can be redirected to any URL.
|
||||
|
||||
## [3.1.0] - 2023-01-13
|
||||
|
||||
### Added
|
||||
|
||||
- Compatibility: rudimentary support for Takahē.
|
||||
- UI: added backdrop blur behind modals.
|
||||
- Admin: let admins configure media preview for attachment thumbnails.
|
||||
|
@ -76,6 +46,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
- Export: restored "Export data" to CSV.
|
||||
|
||||
### Changed
|
||||
|
||||
- Posts: letterbox images to 19:6 again.
|
||||
- Status Info: moved context (repost, pinned) to improve UX.
|
||||
- Posts: remove file icon from empty link previews.
|
||||
|
@ -83,6 +54,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
- Composer: add more descriptive discard confirmation message.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Layout: use accent color for "floating action button" (mobile compose button).
|
||||
- ServiceWorker: don't serve favicon, robots.txt, and others from ServiceWorker.
|
||||
- Datepicker: correctly default to the current year.
|
||||
|
@ -104,6 +76,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
## [3.0.0] - 2022-12-25
|
||||
|
||||
### 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.
|
||||
|
@ -129,6 +102,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
- Toast: added the ability to dismiss toast notifications.
|
||||
|
||||
### 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.
|
||||
|
@ -147,6 +121,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
- Toast: redesigned toast notifications.
|
||||
|
||||
### Removed
|
||||
|
||||
- Theme: Halloween theme.
|
||||
- Settings: advanced notification settings.
|
||||
- Settings: dyslexic mode.
|
||||
|
@ -157,10 +132,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
- 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).
|
||||
|
@ -176,6 +154,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
- i18n: added Icelandic translation.
|
||||
|
||||
### 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.
|
||||
|
@ -185,16 +164,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
- 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.
|
||||
- Statuses: improve display of multiple rich media items.
|
||||
- Statuses: let media be cropped less (when dimensions are provided).
|
||||
- Profile metadata: show only 4 by default, let items be added and removed.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Performance: fixed various performance issues, especially related to the post composer and chats.
|
||||
- Composer: fixed upload form style on light theme.
|
||||
- Composer: fixed emoji search when a custom emoji was invalid.
|
||||
|
@ -210,6 +193,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
- Various iOS fixes.
|
||||
|
||||
### Added
|
||||
|
||||
- Statuses: added greentext support, configurable site-wide by admin.
|
||||
- Statuses: added Mastodon's audio player.
|
||||
- Statuses: indicate > 4 attachments.
|
||||
|
@ -227,31 +211,41 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
- Admin: add user retention % to dashboard.
|
||||
|
||||
## [1.2.3] - 2021-04-18
|
||||
|
||||
### Changed
|
||||
|
||||
- Twemoji now bundled
|
||||
|
||||
### Fixed
|
||||
|
||||
- Redirect user after registration
|
||||
- Delete invalid auth users from browser
|
||||
- Uploaded files ending in .blob
|
||||
|
||||
## [1.2.2] - 2021-04-13
|
||||
|
||||
### Fixed
|
||||
|
||||
- verify_credentials infinite loop bug
|
||||
- Emoji reacts not being sent through notifications
|
||||
- Contrast of Polls
|
||||
|
||||
### Added
|
||||
|
||||
- Configurable FQN for local accounts
|
||||
- Polish translations
|
||||
|
||||
## [1.2.1] - 2021-04-06
|
||||
|
||||
### Fixed
|
||||
|
||||
- "View context" button on videos
|
||||
- Login page successfully redirects Home
|
||||
|
||||
## [1.2.0] - 2021-04-02
|
||||
|
||||
### Added
|
||||
|
||||
- Remote follow button
|
||||
- Display "Bot" tag for bot users
|
||||
- Ability to view remote timelines
|
||||
|
@ -267,11 +261,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
- Portuguese translations
|
||||
|
||||
### Fixed
|
||||
|
||||
- Heart reaction works on Pleroma >= 2.3.0
|
||||
- Pagination of Blocks and Mutes
|
||||
|
||||
## [1.1.0] - 2020-10-05
|
||||
|
||||
### Fixed
|
||||
|
||||
- General user interface and ease-of-use improvements for both mobile and desktop
|
||||
- General loading and performance improvements, including shrinking bundle size
|
||||
- GIF handling: AutoPlayGif Preference support, including avatars and profile banners
|
||||
|
@ -291,29 +288,30 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
- Composer: Forcing the scope to default after settings save.
|
||||
|
||||
### Added
|
||||
|
||||
- Chats, currently one-to-one, evolving with Pleroma BE capabilities, including:
|
||||
- Initiate chat via `Message` button on profile
|
||||
- Up to 4 open foreground chat windows in desktop, with open/minimize/close and notification counter
|
||||
- Browser tab notification counter includes total chat and post notifications
|
||||
- Chats list with total chats notification counter and audio notification toggle
|
||||
- Unique chat audio notification
|
||||
- Add attachment
|
||||
- Delete chat message
|
||||
- Report chat account
|
||||
- Chats icon with notification counter in top navbar in mobile view
|
||||
- Chats marked read on chat hover or on chat key event
|
||||
- Initiate chat via `Message` button on profile
|
||||
- Up to 4 open foreground chat windows in desktop, with open/minimize/close and notification counter
|
||||
- Browser tab notification counter includes total chat and post notifications
|
||||
- Chats list with total chats notification counter and audio notification toggle
|
||||
- Unique chat audio notification
|
||||
- Add attachment
|
||||
- Delete chat message
|
||||
- Report chat account
|
||||
- Chats icon with notification counter in top navbar in mobile view
|
||||
- Chats marked read on chat hover or on chat key event
|
||||
- Audio player for audio uploads, including ogg, oga, and wav support
|
||||
- Integration with Patron recurring donations platform
|
||||
- Profile hover panels, with click to Follow/Unfollow
|
||||
- Posts: Favicon of user's home instance included on post
|
||||
- Soapbox configuration page, including:
|
||||
- Site preview, including light/dark theme toggle rendering
|
||||
- Logo
|
||||
- Brand color using color picker
|
||||
- Copyright footer
|
||||
- Promo panel custom links for timeline pages
|
||||
- Home footer custom links for static pages
|
||||
- Editable JSON based configuration option
|
||||
- Site preview, including light/dark theme toggle rendering
|
||||
- Logo
|
||||
- Brand color using color picker
|
||||
- Copyright footer
|
||||
- Promo panel custom links for timeline pages
|
||||
- Home footer custom links for static pages
|
||||
- Editable JSON based configuration option
|
||||
- Themes: Light/dark theme toggle in top navbar
|
||||
- Themes: Halloween mode in Preferences page
|
||||
- Markdown support in post composer, as default
|
||||
|
@ -340,6 +338,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
- [Renovate.json](https://docs.renovatebot.com/configuration-options/) support
|
||||
|
||||
### Changed
|
||||
|
||||
- Revoke OAuth token on logout
|
||||
- Home sidebar rearrangement
|
||||
- Compose form icons
|
||||
|
@ -351,12 +350,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
- Redux update
|
||||
|
||||
### Removed
|
||||
|
||||
- FontAwesome dependencies, with full switch to ForkAwesome
|
||||
- Requirement for use of soapbox.json for configuration
|
||||
- Direct Message links from menus, partial deprecation due to chats
|
||||
|
||||
## [1.0.0] - 2020-06-15
|
||||
|
||||
### Added
|
||||
|
||||
- Emoji reactions.
|
||||
- Ability to set brand color in soapbox.json.
|
||||
- Security UI.
|
||||
|
@ -367,6 +369,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
- Many new automated tests.
|
||||
|
||||
### Changed
|
||||
|
||||
- Overhauled theming system to use native CSS variables.
|
||||
- Reorganized folder structure.
|
||||
- Redesigned post composer.
|
||||
|
@ -377,6 +380,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
- Force media to have a static height, fixing jumpy timelines.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Composer: Move cursor to end of text.
|
||||
- Composer: Tagging yourself in replies.
|
||||
- Composer: State issues between compose modal and inline composer.
|
||||
|
@ -391,10 +395,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
- Hide reposted media from account media tab.
|
||||
|
||||
## [0.9.0] - 2020-04-30
|
||||
|
||||
### Added
|
||||
|
||||
- Initial beta release.
|
||||
|
||||
[Unreleased]: https://gitlab.com/soapbox-pub/soapbox/-/compare/v1.0.0...develop
|
||||
[Unreleased patch]: https://gitlab.com/soapbox-pub/soapbox/-/compare/v1.0.0...stable/1.0.x
|
||||
[unreleased]: https://gitlab.com/soapbox-pub/soapbox/-/compare/v1.0.0...develop
|
||||
[unreleased patch]: https://gitlab.com/soapbox-pub/soapbox/-/compare/v1.0.0...stable/1.0.x
|
||||
[1.0.0]: https://gitlab.com/soapbox-pub/soapbox/-/compare/v0.9.0...v1.0.0
|
||||
[0.9.0]: https://gitlab.com/soapbox-pub/soapbox/-/tags/v0.9.0
|
||||
|
|
|
@ -75,7 +75,7 @@ One disadvantage of this approach is that it does not help the software spread.
|
|||
© Alex Gleason & other Soapbox contributors
|
||||
© Eugen Rochko & other Mastodon contributors
|
||||
© Trump Media & Technology Group
|
||||
© Gab AI, Inc.
|
||||
© Gab AI, Inc.
|
||||
|
||||
Soapbox is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
|
|
|
@ -2,4 +2,4 @@
|
|||
|
||||
- verified.svg - Created by Alex Gleason. CC0
|
||||
|
||||
Fediverse logo: https://en.wikipedia.org/wiki/Fediverse#/media/File:Fediverse_logo_proposal.svg
|
||||
Fediverse logo: https://en.wikipedia.org/wiki/Fediverse#/media/File:Fediverse_logo_proposal.svg
|
||||
|
|
|
@ -1,107 +1 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
id="svg2"
|
||||
style="clip-rule:evenodd;fill-rule:evenodd;image-rendering:optimizeQuality;shape-rendering:geometricPrecision;text-rendering:geometricPrecision"
|
||||
sodipodi:docname="soapbox-logo-white.svg"
|
||||
xml:space="preserve"
|
||||
version="1.1"
|
||||
inkscape:version="1.2.1 (9c6d41e410, 2022-07-14)"
|
||||
viewBox="0 0 100 100"
|
||||
width="100"
|
||||
height="100"
|
||||
inkscape:export-filename="/home/miklobit/Downloads/citizen4/logo/citizen4-logo-250px.png"
|
||||
inkscape:export-xdpi="63.5"
|
||||
inkscape:export-ydpi="63.5"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"><sodipodi:namedview
|
||||
id="namedview12"
|
||||
bordercolor="#666666"
|
||||
inkscape:pageshadow="2"
|
||||
guidetolerance="10"
|
||||
pagecolor="#ffffff"
|
||||
gridtolerance="10"
|
||||
inkscape:zoom="5.0135101"
|
||||
objecttolerance="10"
|
||||
borderopacity="1"
|
||||
inkscape:current-layer="g1133"
|
||||
inkscape:cx="54.253406"
|
||||
inkscape:cy="42.086282"
|
||||
inkscape:window-width="1920"
|
||||
showgrid="false"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:window-height="1016"
|
||||
inkscape:document-rotation="0"
|
||||
fit-margin-top="0"
|
||||
fit-margin-left="0"
|
||||
fit-margin-right="0"
|
||||
fit-margin-bottom="0"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="36"
|
||||
inkscape:window-maximized="1"
|
||||
units="mm"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:document-units="px" />
|
||||
<defs
|
||||
id="defs4">
|
||||
<style
|
||||
id="style6"
|
||||
type="text/css">
|
||||
.fil0 {fill:black}
|
||||
</style>
|
||||
<clipPath
|
||||
clipPathUnits="userSpaceOnUse"
|
||||
id="clipPath932"><g
|
||||
id="g936"
|
||||
style="fill:#ffffff;stroke:#000000;stroke-width:0.468608;stroke-miterlimit:4;stroke-dasharray:none"
|
||||
transform="matrix(40.327178,0,0,40.327178,-206.26309,-5.404074)">
|
||||
<path
|
||||
id="path934"
|
||||
sodipodi:nodetypes="ccccc"
|
||||
style="fill:#ffffff;stroke:#000000;stroke-width:0.468608;stroke-miterlimit:4;stroke-dasharray:none"
|
||||
d="M 4.25,1.3382 0.4955,2.4662 C 0.58759,5.2588 1.2884,8.6655 4.25,9.6618 7.2442,8.694 7.8774,5.2291 8.0045,2.4662 Z" />
|
||||
</g></clipPath><clipPath
|
||||
clipPathUnits="userSpaceOnUse"
|
||||
id="clipPath932-3"><g
|
||||
id="g936-6"
|
||||
style="fill:#ffffff;stroke:#000000;stroke-width:0.468608;stroke-miterlimit:4;stroke-dasharray:none"
|
||||
transform="matrix(40.327178,0,0,40.327178,-206.26309,-5.404074)"><path
|
||||
id="path934-7"
|
||||
sodipodi:nodetypes="ccccc"
|
||||
style="fill:#ffffff;stroke:#000000;stroke-width:0.468608;stroke-miterlimit:4;stroke-dasharray:none"
|
||||
d="M 4.25,1.3382 0.4955,2.4662 C 0.58759,5.2588 1.2884,8.6655 4.25,9.6618 7.2442,8.694 7.8774,5.2291 8.0045,2.4662 Z" /></g></clipPath>
|
||||
|
||||
|
||||
|
||||
|
||||
</defs>
|
||||
|
||||
<metadata
|
||||
id="metadata7"><rdf:RDF><cc:Work><dc:format>image/svg+xml</dc:format><dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><cc:license
|
||||
rdf:resource="http://creativecommons.org/licenses/by-nc-sa/4.0/" /><dc:date>2023-02-18T14:20:55</dc:date><dc:source>https://soc.citizen4.eu</dc:source><dc:subject><rdf:Bag><rdf:li>citizen4</rdf:li><rdf:li>logo</rdf:li><rdf:li>shield</rdf:li></rdf:Bag></dc:subject><dc:creator><cc:Agent><dc:title>miklo</dc:title></cc:Agent></dc:creator><dc:description>Citizen4 logo</dc:description></cc:Work><cc:License
|
||||
rdf:about="http://creativecommons.org/licenses/by-nc-sa/4.0/"><cc:permits
|
||||
rdf:resource="http://creativecommons.org/ns#Reproduction" /><cc:permits
|
||||
rdf:resource="http://creativecommons.org/ns#Distribution" /><cc:requires
|
||||
rdf:resource="http://creativecommons.org/ns#Notice" /><cc:requires
|
||||
rdf:resource="http://creativecommons.org/ns#Attribution" /><cc:prohibits
|
||||
rdf:resource="http://creativecommons.org/ns#CommercialUse" /><cc:permits
|
||||
rdf:resource="http://creativecommons.org/ns#DerivativeWorks" /><cc:requires
|
||||
rdf:resource="http://creativecommons.org/ns#ShareAlike" /></cc:License></rdf:RDF></metadata><g
|
||||
inkscape:groupmode="layer"
|
||||
id="g1133"
|
||||
inkscape:label="citizen4"
|
||||
style="display:inline;fill:#ffffff"
|
||||
transform="translate(9.1709534,9.343974)"><g
|
||||
id="g1149"
|
||||
transform="matrix(0.28130772,0,0,0.28130772,-1.9206898,-6.7154381)"
|
||||
style="fill:#ffffff"><path
|
||||
id="path1127"
|
||||
style="fill:#ffffff;fill-opacity:1;stroke-width:2.298"
|
||||
d="m 233.61288,175.15307 h -30.0693 v -8.40148 h -35.71547 v -22.85127 h 35.71317 v -8.40148 h 30.0716 c 1.31445,0 2.42898,0.4596 3.34818,1.3765 0.9169,0.9215 1.3788,2.03832 1.3788,3.34818 v 30.20487 c 0,1.22484 -0.4596,2.32098 -1.3788,3.28154 -0.9192,0.95827 -2.03602,1.44314 -3.34818,1.44314 z m 18.90793,-30.07388 c 6.91237,0 10.37315,3.41253 10.37315,10.24447 0,6.83195 -3.45618,10.24217 -10.37315,10.24217 -6.82735,0 -10.24218,-3.41482 -10.24218,-10.24217 0,-6.82734 3.41483,-10.24447 10.24218,-10.24447 z M 70.322893,175.15307 h 30.069297 v -8.40148 h 35.71546 v -22.85127 h -35.71317 v -8.40148 H 70.322893 c -1.31446,0 -2.42898,0.4596 -3.34818,1.3765 -0.9169,0.9215 -1.3788,2.03832 -1.3788,3.34818 v 30.20487 c 0,1.22484 0.4596,2.32098 1.3788,3.28154 0.9192,0.95827 2.03602,1.44314 3.34818,1.44314 z m -18.90793,-30.07388 c -6.91237,0 -10.37315,3.41253 -10.37315,10.24447 0,6.83195 3.45618,10.24217 10.37315,10.24217 6.82735,0 10.24218,-3.41482 10.24218,-10.24217 0,-6.82734 -3.41483,-10.24447 -10.24218,-10.24447 z M 171.79501,73.680969 v 30.069301 h -8.40148 v 35.71546 h -22.85128 v -35.71317 h -8.40147 V 73.680969 c 0,-1.31445 0.45959,-2.42898 1.37649,-3.34818 0.9215,-0.9169 2.03832,-1.3788 3.34819,-1.3788 h 30.20487 c 1.22483,0 2.32097,0.4596 3.28153,1.3788 0.95827,0.9192 1.44315,2.03602 1.44315,3.34818 z m -30.07388,-18.90793 c 0,-6.91237 3.41252,-10.37315 10.24446,-10.37315 6.83195,0 10.24218,3.45618 10.24218,10.37315 0,6.82735 -3.41482,10.24218 -10.24218,10.24218 -6.82734,0 -10.24446,-3.41483 -10.24446,-10.24218 z m 30.07388,182.197911 v -30.06929 h -8.40148 v -35.71547 h -22.85128 v 35.71317 h -8.40147 v 30.07159 c 0,1.31446 0.45959,2.42898 1.37649,3.34818 0.9215,0.9169 2.03832,1.3788 3.34819,1.3788 h 30.20487 c 1.22483,0 2.32097,-0.4596 3.28153,-1.3788 0.95827,-0.9192 1.44315,-2.03602 1.44315,-3.34818 z m -30.07388,18.90793 c 0,6.91237 3.41252,10.37315 10.24446,10.37315 6.83195,0 10.24218,-3.45618 10.24218,-10.37315 0,-6.82735 -3.41482,-10.24218 -10.24218,-10.24218 -6.82734,0 -10.24446,3.41483 -10.24446,10.24218 z M 151.92079,0.52207567 0.51240486,46.01113 C 4.2261345,158.6288 32.487823,296.01139 151.92079,336.18936 272.66842,297.16072 298.20359,157.43109 303.32917,46.01113 Z" /></g></g></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 95 95" width="100" height="100"><path d="M94.909 19.374C94.909 8.674 86.235 0 75.534 0c-10.647 0-19.28 8.591-19.365 19.217l-15.631 2.09c-1.961-6.007-7.598-10.35-14.258-10.35-8.284 0-15.002 6.716-15.002 15.002 0 6.642 4.321 12.267 10.303 14.24l-2.205 16.056c-10.66.049-19.285 8.7-19.285 19.37C.091 86.325 8.765 95 19.466 95c10.677 0 19.332-8.638 19.37-19.304l18.093-2.501c1.979 5.972 7.598 10.285 14.234 10.285 8.284 0 15.002-6.716 15.002-15.002 0-6.891-4.652-12.682-10.983-14.441l1.365-15.339c10.229-.53 18.363-8.966 18.363-19.324zM56.194 67.8l-18.116 2.505a19.39 19.39 0 0 0-13.312-13.3l2.205-16.077a14.98 14.98 0 0 0 14.27-14.222l15.655-2.094c1.894 6.757 7.351 12.009 14.225 13.612l-1.365 15.322c-7.4.688-13.224 6.753-13.562 14.254z" fill="#ffffff"/></svg>
|
||||
|
|
Przed Szerokość: | Wysokość: | Rozmiar: 6.7 KiB Po Szerokość: | Wysokość: | Rozmiar: 812 B |
|
@ -1,127 +1 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
id="svg2"
|
||||
style="clip-rule:evenodd;fill-rule:evenodd;image-rendering:optimizeQuality;shape-rendering:geometricPrecision;text-rendering:geometricPrecision"
|
||||
sodipodi:docname="soapbox-logo.svg"
|
||||
xml:space="preserve"
|
||||
version="1.1"
|
||||
inkscape:version="1.2.1 (9c6d41e410, 2022-07-14)"
|
||||
viewBox="0 0 100 100"
|
||||
width="100"
|
||||
height="100"
|
||||
inkscape:export-filename="/home/miklobit/Downloads/citizen4/logo/citizen4-logo-250px.png"
|
||||
inkscape:export-xdpi="63.5"
|
||||
inkscape:export-ydpi="63.5"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"><sodipodi:namedview
|
||||
id="namedview12"
|
||||
bordercolor="#666666"
|
||||
inkscape:pageshadow="2"
|
||||
guidetolerance="10"
|
||||
pagecolor="#ffffff"
|
||||
gridtolerance="10"
|
||||
inkscape:zoom="5.0135101"
|
||||
objecttolerance="10"
|
||||
borderopacity="1"
|
||||
inkscape:current-layer="svg2"
|
||||
inkscape:cx="54.253406"
|
||||
inkscape:cy="42.086282"
|
||||
inkscape:window-width="1920"
|
||||
showgrid="false"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:window-height="1016"
|
||||
inkscape:document-rotation="0"
|
||||
fit-margin-top="0"
|
||||
fit-margin-left="0"
|
||||
fit-margin-right="0"
|
||||
fit-margin-bottom="0"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="36"
|
||||
inkscape:window-maximized="1"
|
||||
units="mm"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:document-units="px" />
|
||||
<defs
|
||||
id="defs4">
|
||||
<style
|
||||
id="style6"
|
||||
type="text/css">
|
||||
.fil0 {fill:black}
|
||||
</style>
|
||||
<clipPath
|
||||
clipPathUnits="userSpaceOnUse"
|
||||
id="clipPath932"><g
|
||||
id="g936"
|
||||
style="fill:#ffffff;stroke:#000000;stroke-width:0.468608;stroke-miterlimit:4;stroke-dasharray:none"
|
||||
transform="matrix(40.327178,0,0,40.327178,-206.26309,-5.404074)">
|
||||
<path
|
||||
id="path934"
|
||||
sodipodi:nodetypes="ccccc"
|
||||
style="fill:#ffffff;stroke:#000000;stroke-width:0.468608;stroke-miterlimit:4;stroke-dasharray:none"
|
||||
d="M 4.25,1.3382 0.4955,2.4662 C 0.58759,5.2588 1.2884,8.6655 4.25,9.6618 7.2442,8.694 7.8774,5.2291 8.0045,2.4662 Z" />
|
||||
</g></clipPath><clipPath
|
||||
clipPathUnits="userSpaceOnUse"
|
||||
id="clipPath932-3"><g
|
||||
id="g936-6"
|
||||
style="fill:#ffffff;stroke:#000000;stroke-width:0.468608;stroke-miterlimit:4;stroke-dasharray:none"
|
||||
transform="matrix(40.327178,0,0,40.327178,-206.26309,-5.404074)"><path
|
||||
id="path934-7"
|
||||
sodipodi:nodetypes="ccccc"
|
||||
style="fill:#ffffff;stroke:#000000;stroke-width:0.468608;stroke-miterlimit:4;stroke-dasharray:none"
|
||||
d="M 4.25,1.3382 0.4955,2.4662 C 0.58759,5.2588 1.2884,8.6655 4.25,9.6618 7.2442,8.694 7.8774,5.2291 8.0045,2.4662 Z" /></g></clipPath>
|
||||
|
||||
|
||||
|
||||
|
||||
</defs>
|
||||
|
||||
<metadata
|
||||
id="metadata7"><rdf:RDF><cc:Work><dc:format>image/svg+xml</dc:format><dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><cc:license
|
||||
rdf:resource="http://creativecommons.org/licenses/by-nc-sa/4.0/" /><dc:date>2023-02-18T14:20:55</dc:date><dc:source>https://soc.citizen4.eu</dc:source><dc:subject><rdf:Bag><rdf:li>citizen4</rdf:li><rdf:li>logo</rdf:li><rdf:li>shield</rdf:li></rdf:Bag></dc:subject><dc:creator><cc:Agent><dc:title>miklo</dc:title></cc:Agent></dc:creator><dc:description>Citizen4 logo</dc:description></cc:Work><cc:License
|
||||
rdf:about="http://creativecommons.org/licenses/by-nc-sa/4.0/"><cc:permits
|
||||
rdf:resource="http://creativecommons.org/ns#Reproduction" /><cc:permits
|
||||
rdf:resource="http://creativecommons.org/ns#Distribution" /><cc:requires
|
||||
rdf:resource="http://creativecommons.org/ns#Notice" /><cc:requires
|
||||
rdf:resource="http://creativecommons.org/ns#Attribution" /><cc:prohibits
|
||||
rdf:resource="http://creativecommons.org/ns#CommercialUse" /><cc:permits
|
||||
rdf:resource="http://creativecommons.org/ns#DerivativeWorks" /><cc:requires
|
||||
rdf:resource="http://creativecommons.org/ns#ShareAlike" /></cc:License></rdf:RDF></metadata><g
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
inkscape:label="shield"
|
||||
style="display:inline"
|
||||
transform="translate(9.1709534,9.343974)"><g
|
||||
id="g912"
|
||||
style="clip-rule:evenodd;fill:#003399;fill-opacity:1;fill-rule:evenodd;stroke:#888888;stroke-width:0.468608;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;image-rendering:optimizeQuality;shape-rendering:geometricPrecision;text-rendering:geometricPrecision"
|
||||
transform="matrix(11.344346,0,0,11.344346,-7.3976698,-21.749578)"><path
|
||||
id="path910"
|
||||
sodipodi:nodetypes="ccccc"
|
||||
style="fill:#003399;fill-opacity:1;stroke:#888888;stroke-width:0.468608;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="M 4.25,1.3382 0.4955,2.4662 C 0.58759,5.2588 1.2884,8.6655 4.25,9.6618 7.2442,8.694 7.8774,5.2291 8.0045,2.4662 Z" /></g></g><g
|
||||
inkscape:groupmode="layer"
|
||||
id="g1133"
|
||||
inkscape:label="citizen2"
|
||||
style="display:inline"
|
||||
transform="translate(9.1709534,9.343974)"><g
|
||||
id="g1149"
|
||||
transform="matrix(0.28130772,0,0,0.28130772,-1.9206898,-6.7154381)"><path
|
||||
id="path1119"
|
||||
style="fill:#ffcc00;fill-opacity:1;stroke-width:2.298"
|
||||
d="m 171.79501,236.97095 v -30.06929 h -8.40148 v -35.71547 h -22.85128 v 35.71317 h -8.40147 v 30.07159 c 0,1.31446 0.45959,2.42898 1.37649,3.34818 0.9215,0.9169 2.03832,1.3788 3.34819,1.3788 h 30.20487 c 1.22483,0 2.32097,-0.4596 3.28153,-1.3788 0.95827,-0.9192 1.44315,-2.03602 1.44315,-3.34818 z m -30.07388,18.90793 c 0,6.91237 3.41252,10.37315 10.24446,10.37315 6.83195,0 10.24218,-3.45618 10.24218,-10.37315 0,-6.82735 -3.41482,-10.24218 -10.24218,-10.24218 -6.82734,0 -10.24446,3.41483 -10.24446,10.24218 z" /><path
|
||||
id="path1121"
|
||||
style="fill:#ffcc00;fill-opacity:1;stroke-width:2.298"
|
||||
d="m 171.79501,73.680969 v 30.069301 h -8.40148 v 35.71546 h -22.85128 v -35.71317 h -8.40147 V 73.680969 c 0,-1.31445 0.45959,-2.42898 1.37649,-3.34818 0.9215,-0.9169 2.03832,-1.3788 3.34819,-1.3788 h 30.20487 c 1.22483,0 2.32097,0.4596 3.28153,1.3788 0.95827,0.9192 1.44315,2.03602 1.44315,3.34818 z m -30.07388,-18.90793 c 0,-6.91237 3.41252,-10.37315 10.24446,-10.37315 6.83195,0 10.24218,3.45618 10.24218,10.37315 0,6.82735 -3.41482,10.24218 -10.24218,10.24218 -6.82734,0 -10.24446,-3.41483 -10.24446,-10.24218 z" /><path
|
||||
id="path1125"
|
||||
style="fill:#ffcc00;fill-opacity:1;stroke-width:2.298"
|
||||
d="m 70.322893,175.15307 h 30.069297 v -8.40148 h 35.71546 v -22.85127 h -35.71317 v -8.40148 H 70.322893 c -1.31446,0 -2.42898,0.4596 -3.34818,1.3765 -0.9169,0.9215 -1.3788,2.03832 -1.3788,3.34818 v 30.20487 c 0,1.22484 0.4596,2.32098 1.3788,3.28154 0.9192,0.95827 2.03602,1.44314 3.34818,1.44314 z m -18.90793,-30.07388 c -6.91237,0 -10.37315,3.41253 -10.37315,10.24447 0,6.83195 3.45618,10.24217 10.37315,10.24217 6.82735,0 10.24218,-3.41482 10.24218,-10.24217 0,-6.82734 -3.41483,-10.24447 -10.24218,-10.24447 z" /><path
|
||||
id="path1127"
|
||||
style="fill:#ffcc00;fill-opacity:1;stroke-width:2.298"
|
||||
d="m 233.61288,175.15307 h -30.0693 v -8.40148 h -35.71547 v -22.85127 h 35.71317 v -8.40148 h 30.0716 c 1.31445,0 2.42898,0.4596 3.34818,1.3765 0.9169,0.9215 1.3788,2.03832 1.3788,3.34818 v 30.20487 c 0,1.22484 -0.4596,2.32098 -1.3788,3.28154 -0.9192,0.95827 -2.03602,1.44314 -3.34818,1.44314 z m 18.90793,-30.07388 c 6.91237,0 10.37315,3.41253 10.37315,10.24447 0,6.83195 -3.45618,10.24217 -10.37315,10.24217 -6.82735,0 -10.24218,-3.41482 -10.24218,-10.24217 0,-6.82734 3.41483,-10.24447 10.24218,-10.24447 z" /></g></g></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 95 95" width="100" height="100"><path d="M94.909 19.374C94.909 8.674 86.235 0 75.534 0c-10.647 0-19.28 8.591-19.365 19.217l-15.631 2.09c-1.961-6.007-7.598-10.35-14.258-10.35-8.284 0-15.002 6.716-15.002 15.002 0 6.642 4.321 12.267 10.303 14.24l-2.205 16.056c-10.66.049-19.285 8.7-19.285 19.37C.091 86.325 8.765 95 19.466 95c10.677 0 19.332-8.638 19.37-19.304l18.093-2.501c1.979 5.972 7.598 10.285 14.234 10.285 8.284 0 15.002-6.716 15.002-15.002 0-6.891-4.652-12.682-10.983-14.441l1.365-15.339c10.229-.53 18.363-8.966 18.363-19.324zM56.194 67.8l-18.116 2.505a19.39 19.39 0 0 0-13.312-13.3l2.205-16.077a14.98 14.98 0 0 0 14.27-14.222l15.655-2.094c1.894 6.757 7.351 12.009 14.225 13.612l-1.365 15.322c-7.4.688-13.224 6.753-13.562 14.254z" fill="#0482d8"/></svg>
|
||||
|
|
Przed Szerokość: | Wysokość: | Rozmiar: 7.6 KiB Po Szerokość: | Wysokość: | Rozmiar: 812 B |
|
@ -1,16 +0,0 @@
|
|||
{
|
||||
"note": "patriots 900000001",
|
||||
"discoverable": true,
|
||||
"id": "109989480368015378",
|
||||
"domain": null,
|
||||
"avatar": "https://media.covfefe.social/groups/avatars/109/989/480/368/015/378/original/50b0d899bc5aae13.jpg",
|
||||
"avatar_static": "https://media.covfefe.social/groups/avatars/109/989/480/368/015/378/original/50b0d899bc5aae13.jpg",
|
||||
"header": "https://media.covfefe.social/groups/headers/109/989/480/368/015/378/original/c5063b59f919cd4a.png",
|
||||
"header_static": "https://media.covfefe.social/groups/headers/109/989/480/368/015/378/original/c5063b59f919cd4a.png",
|
||||
"group_visibility": "everyone",
|
||||
"created_at": "2023-03-08T00:00:00.000Z",
|
||||
"display_name": "PATRIOT PATRIOTS",
|
||||
"membership_required": true,
|
||||
"members_count": 1,
|
||||
"tags": []
|
||||
}
|
|
@ -228,7 +228,7 @@ const fetchAccountFail = (id: string | null, error: AxiosError) => ({
|
|||
});
|
||||
|
||||
type FollowAccountOpts = {
|
||||
reblogs?: boolean
|
||||
reblogs?: boolean,
|
||||
notify?: boolean
|
||||
};
|
||||
|
||||
|
|
|
@ -1,18 +1,13 @@
|
|||
import { defineMessages } from 'react-intl';
|
||||
|
||||
import { fetchRelationships } from 'soapbox/actions/accounts';
|
||||
import { importFetchedAccount, importFetchedAccounts, importFetchedStatuses } from 'soapbox/actions/importer';
|
||||
import toast from 'soapbox/toast';
|
||||
import { filterBadges, getTagDiff } from 'soapbox/utils/badges';
|
||||
import { getFeatures } from 'soapbox/utils/features';
|
||||
|
||||
import api, { getLinks } from '../api';
|
||||
|
||||
import { openModal } from './modals';
|
||||
|
||||
import type { AxiosResponse } from 'axios';
|
||||
import type { AppDispatch, RootState } from 'soapbox/store';
|
||||
import type { APIEntity, Announcement } from 'soapbox/types/entities';
|
||||
import type { APIEntity } from 'soapbox/types/entities';
|
||||
|
||||
const ADMIN_CONFIG_FETCH_REQUEST = 'ADMIN_CONFIG_FETCH_REQUEST';
|
||||
const ADMIN_CONFIG_FETCH_SUCCESS = 'ADMIN_CONFIG_FETCH_SUCCESS';
|
||||
|
@ -82,45 +77,16 @@ const ADMIN_USERS_UNSUGGEST_REQUEST = 'ADMIN_USERS_UNSUGGEST_REQUEST';
|
|||
const ADMIN_USERS_UNSUGGEST_SUCCESS = 'ADMIN_USERS_UNSUGGEST_SUCCESS';
|
||||
const ADMIN_USERS_UNSUGGEST_FAIL = 'ADMIN_USERS_UNSUGGEST_FAIL';
|
||||
|
||||
const ADMIN_USER_INDEX_EXPAND_FAIL = 'ADMIN_USER_INDEX_EXPAND_FAIL';
|
||||
const ADMIN_USER_INDEX_EXPAND_FAIL = 'ADMIN_USER_INDEX_EXPAND_FAIL';
|
||||
const ADMIN_USER_INDEX_EXPAND_REQUEST = 'ADMIN_USER_INDEX_EXPAND_REQUEST';
|
||||
const ADMIN_USER_INDEX_EXPAND_SUCCESS = 'ADMIN_USER_INDEX_EXPAND_SUCCESS';
|
||||
|
||||
const ADMIN_USER_INDEX_FETCH_FAIL = 'ADMIN_USER_INDEX_FETCH_FAIL';
|
||||
const ADMIN_USER_INDEX_FETCH_FAIL = 'ADMIN_USER_INDEX_FETCH_FAIL';
|
||||
const ADMIN_USER_INDEX_FETCH_REQUEST = 'ADMIN_USER_INDEX_FETCH_REQUEST';
|
||||
const ADMIN_USER_INDEX_FETCH_SUCCESS = 'ADMIN_USER_INDEX_FETCH_SUCCESS';
|
||||
|
||||
const ADMIN_USER_INDEX_QUERY_SET = 'ADMIN_USER_INDEX_QUERY_SET';
|
||||
|
||||
const ADMIN_ANNOUNCEMENTS_FETCH_FAIL = 'ADMIN_ANNOUNCEMENTS_FETCH_FAILS';
|
||||
const ADMIN_ANNOUNCEMENTS_FETCH_REQUEST = 'ADMIN_ANNOUNCEMENTS_FETCH_REQUEST';
|
||||
const ADMIN_ANNOUNCEMENTS_FETCH_SUCCESS = 'ADMIN_ANNOUNCEMENTS_FETCH_SUCCESS';
|
||||
|
||||
const ADMIN_ANNOUNCEMENTS_EXPAND_FAIL = 'ADMIN_ANNOUNCEMENTS_EXPAND_FAILS';
|
||||
const ADMIN_ANNOUNCEMENTS_EXPAND_REQUEST = 'ADMIN_ANNOUNCEMENTS_EXPAND_REQUEST';
|
||||
const ADMIN_ANNOUNCEMENTS_EXPAND_SUCCESS = 'ADMIN_ANNOUNCEMENTS_EXPAND_SUCCESS';
|
||||
|
||||
const ADMIN_ANNOUNCEMENT_CHANGE_CONTENT = 'ADMIN_ANNOUNCEMENT_CHANGE_CONTENT';
|
||||
const ADMIN_ANNOUNCEMENT_CHANGE_START_TIME = 'ADMIN_ANNOUNCEMENT_CHANGE_START_TIME';
|
||||
const ADMIN_ANNOUNCEMENT_CHANGE_END_TIME = 'ADMIN_ANNOUNCEMENT_CHANGE_END_TIME';
|
||||
const ADMIN_ANNOUNCEMENT_CHANGE_ALL_DAY = 'ADMIN_ANNOUNCEMENT_CHANGE_ALL_DAY';
|
||||
|
||||
const ADMIN_ANNOUNCEMENT_CREATE_REQUEST = 'ADMIN_ANNOUNCEMENT_CREATE_REQUEST';
|
||||
const ADMIN_ANNOUNCEMENT_CREATE_SUCCESS = 'ADMIN_ANNOUNCEMENT_CREATE_REQUEST';
|
||||
const ADMIN_ANNOUNCEMENT_CREATE_FAIL = 'ADMIN_ANNOUNCEMENT_CREATE_FAIL';
|
||||
|
||||
const ADMIN_ANNOUNCEMENT_DELETE_REQUEST = 'ADMIN_ANNOUNCEMENT_DELETE_REQUEST';
|
||||
const ADMIN_ANNOUNCEMENT_DELETE_SUCCESS = 'ADMIN_ANNOUNCEMENT_DELETE_REQUEST';
|
||||
const ADMIN_ANNOUNCEMENT_DELETE_FAIL = 'ADMIN_ANNOUNCEMENT_DELETE_FAIL';
|
||||
|
||||
const ADMIN_ANNOUNCEMENT_MODAL_INIT = 'ADMIN_ANNOUNCEMENT_MODAL_INIT';
|
||||
|
||||
const messages = defineMessages({
|
||||
announcementCreateSuccess: { id: 'admin.edit_announcement.created', defaultMessage: 'Announcement created' },
|
||||
announcementDeleteSuccess: { id: 'admin.edit_announcement.deleted', defaultMessage: 'Announcement deleted' },
|
||||
announcementUpdateSuccess: { id: 'admin.edit_announcement.updated', defaultMessage: 'Announcement edited' },
|
||||
});
|
||||
|
||||
const nicknamesFromIds = (getState: () => RootState, ids: string[]) => ids.map(id => getState().accounts.get(id)!.acct);
|
||||
|
||||
const fetchConfig = () =>
|
||||
|
@ -632,93 +598,6 @@ const expandUserIndex = () =>
|
|||
});
|
||||
};
|
||||
|
||||
const fetchAdminAnnouncements = () =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
dispatch({ type: ADMIN_ANNOUNCEMENTS_FETCH_REQUEST });
|
||||
return api(getState)
|
||||
.get('/api/pleroma/admin/announcements', { params: { limit: 50 } })
|
||||
.then(({ data }) => {
|
||||
dispatch({ type: ADMIN_ANNOUNCEMENTS_FETCH_SUCCESS, announcements: data });
|
||||
return data;
|
||||
}).catch(error => {
|
||||
dispatch({ type: ADMIN_ANNOUNCEMENTS_FETCH_FAIL, error });
|
||||
});
|
||||
};
|
||||
|
||||
const expandAdminAnnouncements = () =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
const page = getState().admin_announcements.page;
|
||||
|
||||
dispatch({ type: ADMIN_ANNOUNCEMENTS_EXPAND_REQUEST });
|
||||
return api(getState)
|
||||
.get('/api/pleroma/admin/announcements', { params: { limit: 50, offset: page * 50 } })
|
||||
.then(({ data }) => {
|
||||
dispatch({ type: ADMIN_ANNOUNCEMENTS_EXPAND_SUCCESS, announcements: data });
|
||||
return data;
|
||||
}).catch(error => {
|
||||
dispatch({ type: ADMIN_ANNOUNCEMENTS_EXPAND_FAIL, error });
|
||||
});
|
||||
};
|
||||
|
||||
const changeAnnouncementContent = (content: string) => ({
|
||||
type: ADMIN_ANNOUNCEMENT_CHANGE_CONTENT,
|
||||
value: content,
|
||||
});
|
||||
|
||||
const changeAnnouncementStartTime = (time: Date | null) => ({
|
||||
type: ADMIN_ANNOUNCEMENT_CHANGE_START_TIME,
|
||||
value: time,
|
||||
});
|
||||
|
||||
const changeAnnouncementEndTime = (time: Date | null) => ({
|
||||
type: ADMIN_ANNOUNCEMENT_CHANGE_END_TIME,
|
||||
value: time,
|
||||
});
|
||||
|
||||
const changeAnnouncementAllDay = (allDay: boolean) => ({
|
||||
type: ADMIN_ANNOUNCEMENT_CHANGE_ALL_DAY,
|
||||
value: allDay,
|
||||
});
|
||||
|
||||
const handleCreateAnnouncement = () =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
dispatch({ type: ADMIN_ANNOUNCEMENT_CREATE_REQUEST });
|
||||
|
||||
const { id, content, starts_at, ends_at, all_day } = getState().admin_announcements.form;
|
||||
|
||||
return api(getState)[id ? 'patch' : 'post'](
|
||||
id ? `/api/pleroma/admin/announcements/${id}` : '/api/pleroma/admin/announcements',
|
||||
{ content, starts_at, ends_at, all_day },
|
||||
).then(({ data }) => {
|
||||
dispatch({ type: ADMIN_ANNOUNCEMENT_CREATE_SUCCESS, announcement: data });
|
||||
toast.success(id ? messages.announcementUpdateSuccess : messages.announcementCreateSuccess);
|
||||
dispatch(fetchAdminAnnouncements());
|
||||
return data;
|
||||
}).catch(error => {
|
||||
dispatch({ type: ADMIN_ANNOUNCEMENT_CREATE_FAIL, error });
|
||||
});
|
||||
};
|
||||
|
||||
const deleteAnnouncement = (id: string) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
dispatch({ type: ADMIN_ANNOUNCEMENT_DELETE_REQUEST, id });
|
||||
|
||||
return api(getState).delete(`/api/pleroma/admin/announcements/${id}`).then(({ data }) => {
|
||||
dispatch({ type: ADMIN_ANNOUNCEMENT_DELETE_SUCCESS, id });
|
||||
toast.success(messages.announcementDeleteSuccess);
|
||||
dispatch(fetchAdminAnnouncements());
|
||||
return data;
|
||||
}).catch(error => {
|
||||
dispatch({ type: ADMIN_ANNOUNCEMENT_DELETE_FAIL, id, error });
|
||||
});
|
||||
};
|
||||
|
||||
const initAnnouncementModal = (announcement?: Announcement) =>
|
||||
(dispatch: AppDispatch) => {
|
||||
dispatch({ type: ADMIN_ANNOUNCEMENT_MODAL_INIT, announcement });
|
||||
dispatch(openModal('EDIT_ANNOUNCEMENT'));
|
||||
};
|
||||
|
||||
export {
|
||||
ADMIN_CONFIG_FETCH_REQUEST,
|
||||
ADMIN_CONFIG_FETCH_SUCCESS,
|
||||
|
@ -778,23 +657,6 @@ export {
|
|||
ADMIN_USER_INDEX_FETCH_REQUEST,
|
||||
ADMIN_USER_INDEX_FETCH_SUCCESS,
|
||||
ADMIN_USER_INDEX_QUERY_SET,
|
||||
ADMIN_ANNOUNCEMENTS_FETCH_FAIL,
|
||||
ADMIN_ANNOUNCEMENTS_FETCH_REQUEST,
|
||||
ADMIN_ANNOUNCEMENTS_FETCH_SUCCESS,
|
||||
ADMIN_ANNOUNCEMENTS_EXPAND_FAIL,
|
||||
ADMIN_ANNOUNCEMENTS_EXPAND_REQUEST,
|
||||
ADMIN_ANNOUNCEMENTS_EXPAND_SUCCESS,
|
||||
ADMIN_ANNOUNCEMENT_CHANGE_CONTENT,
|
||||
ADMIN_ANNOUNCEMENT_CHANGE_START_TIME,
|
||||
ADMIN_ANNOUNCEMENT_CHANGE_END_TIME,
|
||||
ADMIN_ANNOUNCEMENT_CHANGE_ALL_DAY,
|
||||
ADMIN_ANNOUNCEMENT_CREATE_FAIL,
|
||||
ADMIN_ANNOUNCEMENT_CREATE_REQUEST,
|
||||
ADMIN_ANNOUNCEMENT_CREATE_SUCCESS,
|
||||
ADMIN_ANNOUNCEMENT_DELETE_FAIL,
|
||||
ADMIN_ANNOUNCEMENT_DELETE_REQUEST,
|
||||
ADMIN_ANNOUNCEMENT_DELETE_SUCCESS,
|
||||
ADMIN_ANNOUNCEMENT_MODAL_INIT,
|
||||
fetchConfig,
|
||||
updateConfig,
|
||||
updateSoapboxConfig,
|
||||
|
@ -824,13 +686,4 @@ export {
|
|||
setUserIndexQuery,
|
||||
fetchUserIndex,
|
||||
expandUserIndex,
|
||||
fetchAdminAnnouncements,
|
||||
expandAdminAnnouncements,
|
||||
changeAnnouncementContent,
|
||||
changeAnnouncementStartTime,
|
||||
changeAnnouncementEndTime,
|
||||
changeAnnouncementAllDay,
|
||||
handleCreateAnnouncement,
|
||||
deleteAnnouncement,
|
||||
initAnnouncementModal,
|
||||
};
|
||||
|
|
|
@ -4,8 +4,7 @@ import throttle from 'lodash/throttle';
|
|||
import { defineMessages, IntlShape } from 'react-intl';
|
||||
|
||||
import api from 'soapbox/api';
|
||||
import { isNativeEmoji } from 'soapbox/features/emoji';
|
||||
import emojiSearch from 'soapbox/features/emoji/search';
|
||||
import { search as emojiSearch } from 'soapbox/features/emoji/emoji-mart-search-light';
|
||||
import { tagHistory } from 'soapbox/settings';
|
||||
import toast from 'soapbox/toast';
|
||||
import { isLoggedIn } from 'soapbox/utils/auth';
|
||||
|
@ -20,8 +19,8 @@ import { openModal, closeModal } from './modals';
|
|||
import { getSettings } from './settings';
|
||||
import { createStatus } from './statuses';
|
||||
|
||||
import type { Emoji } from 'soapbox/components/autosuggest-emoji';
|
||||
import type { AutoSuggestion } from 'soapbox/components/autosuggest-input';
|
||||
import type { Emoji } from 'soapbox/features/emoji';
|
||||
import type { AppDispatch, RootState } from 'soapbox/store';
|
||||
import type { Account, APIEntity, Status, Tag } from 'soapbox/types/entities';
|
||||
import type { History } from 'soapbox/types/history';
|
||||
|
@ -278,7 +277,7 @@ const submitCompose = (composeId: string, routerHistory?: History, force = false
|
|||
|
||||
const idempotencyKey = compose.idempotencyKey;
|
||||
|
||||
const params: Record<string, any> = {
|
||||
const params = {
|
||||
status,
|
||||
in_reply_to_id: compose.in_reply_to,
|
||||
quote_id: compose.quote,
|
||||
|
@ -290,10 +289,9 @@ const submitCompose = (composeId: string, routerHistory?: History, force = false
|
|||
poll: compose.poll,
|
||||
scheduled_at: compose.schedule,
|
||||
to,
|
||||
group_id: compose.privacy === 'group' ? compose.group_id : null,
|
||||
};
|
||||
|
||||
if (compose.privacy === 'group') params.group_id = compose.group_id;
|
||||
|
||||
dispatch(createStatus(params, idempotencyKey, statusId)).then(function(data) {
|
||||
if (!statusId && data.visibility === 'direct' && getState().conversations.mounted <= 0 && routerHistory) {
|
||||
routerHistory.push('/messages');
|
||||
|
@ -517,9 +515,7 @@ const fetchComposeSuggestionsAccounts = throttle((dispatch, getState, composeId,
|
|||
}, 200, { leading: true, trailing: true });
|
||||
|
||||
const fetchComposeSuggestionsEmojis = (dispatch: AppDispatch, getState: () => RootState, composeId: string, token: string) => {
|
||||
const state = getState();
|
||||
const results = emojiSearch(token.replace(':', ''), { maxResults: 5 }, state.custom_emojis);
|
||||
|
||||
const results = emojiSearch(token.replace(':', ''), { maxResults: 5 } as any);
|
||||
dispatch(readyComposeSuggestionsEmojis(composeId, token, results));
|
||||
};
|
||||
|
||||
|
@ -564,7 +560,7 @@ const selectComposeSuggestion = (composeId: string, position: number, token: str
|
|||
let completion, startPosition;
|
||||
|
||||
if (typeof suggestion === 'object' && suggestion.id) {
|
||||
completion = isNativeEmoji(suggestion) ? suggestion.native : suggestion.colons;
|
||||
completion = suggestion.native || suggestion.colons;
|
||||
startPosition = position - 1;
|
||||
|
||||
dispatch(useEmoji(suggestion));
|
||||
|
|
|
@ -1,8 +1,13 @@
|
|||
import type { DropdownPlacement } from 'soapbox/components/dropdown-menu';
|
||||
|
||||
const DROPDOWN_MENU_OPEN = 'DROPDOWN_MENU_OPEN';
|
||||
const DROPDOWN_MENU_CLOSE = 'DROPDOWN_MENU_CLOSE';
|
||||
|
||||
const openDropdownMenu = () => ({ type: DROPDOWN_MENU_OPEN });
|
||||
const closeDropdownMenu = () => ({ type: DROPDOWN_MENU_CLOSE });
|
||||
const openDropdownMenu = (id: number, placement: DropdownPlacement, keyboard: boolean) =>
|
||||
({ type: DROPDOWN_MENU_OPEN, id, placement, keyboard });
|
||||
|
||||
const closeDropdownMenu = (id: number) =>
|
||||
({ type: DROPDOWN_MENU_CLOSE, id });
|
||||
|
||||
export {
|
||||
DROPDOWN_MENU_OPEN,
|
||||
|
|
|
@ -25,7 +25,7 @@ const EMOJI_REACTS_FETCH_FAIL = 'EMOJI_REACTS_FETCH_FAIL';
|
|||
|
||||
const noOp = () => () => new Promise(f => f(undefined));
|
||||
|
||||
const simpleEmojiReact = (status: Status, emoji: string, custom?: string) =>
|
||||
const simpleEmojiReact = (status: Status, emoji: string) =>
|
||||
(dispatch: AppDispatch) => {
|
||||
const emojiReacts: ImmutableList<ImmutableMap<string, any>> = status.pleroma.get('emoji_reactions') || ImmutableList();
|
||||
|
||||
|
@ -43,7 +43,7 @@ const simpleEmojiReact = (status: Status, emoji: string, custom?: string) =>
|
|||
if (emoji === '👍') {
|
||||
dispatch(favourite(status));
|
||||
} else {
|
||||
dispatch(emojiReact(status, emoji, custom));
|
||||
dispatch(emojiReact(status, emoji));
|
||||
}
|
||||
}).catch(err => {
|
||||
console.error(err);
|
||||
|
@ -70,11 +70,11 @@ const fetchEmojiReacts = (id: string, emoji: string) =>
|
|||
});
|
||||
};
|
||||
|
||||
const emojiReact = (status: Status, emoji: string, custom?: string) =>
|
||||
const emojiReact = (status: Status, emoji: string) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
if (!isLoggedIn(getState)) return dispatch(noOp());
|
||||
|
||||
dispatch(emojiReactRequest(status, emoji, custom));
|
||||
dispatch(emojiReactRequest(status, emoji));
|
||||
|
||||
return api(getState)
|
||||
.put(`/api/v1/pleroma/statuses/${status.get('id')}/reactions/${emoji}`)
|
||||
|
@ -120,11 +120,10 @@ const fetchEmojiReactsFail = (id: string, error: AxiosError) => ({
|
|||
error,
|
||||
});
|
||||
|
||||
const emojiReactRequest = (status: Status, emoji: string, custom?: string) => ({
|
||||
const emojiReactRequest = (status: Status, emoji: string) => ({
|
||||
type: EMOJI_REACT_REQUEST,
|
||||
status,
|
||||
emoji,
|
||||
custom,
|
||||
skipLoading: true,
|
||||
});
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { saveSettings } from './settings';
|
||||
|
||||
import type { Emoji } from 'soapbox/features/emoji';
|
||||
import type { Emoji } from 'soapbox/components/autosuggest-emoji';
|
||||
import type { AppDispatch } from 'soapbox/store';
|
||||
|
||||
const EMOJI_USE = 'EMOJI_USE';
|
||||
|
|
|
@ -569,7 +569,7 @@ const rejectEventParticipationRequestFail = (id: string, accountId: string, erro
|
|||
});
|
||||
|
||||
const fetchEventIcs = (id: string) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) =>
|
||||
(dispatch: any, getState: () => RootState) =>
|
||||
api(getState).get(`/api/v1/pleroma/events/${id}/ics`);
|
||||
|
||||
const cancelEventCompose = () => ({
|
||||
|
|
|
@ -34,8 +34,8 @@ type ExportDataActions = {
|
|||
| typeof EXPORT_BLOCKS_FAIL
|
||||
| typeof EXPORT_MUTES_REQUEST
|
||||
| typeof EXPORT_MUTES_SUCCESS
|
||||
| typeof EXPORT_MUTES_FAIL
|
||||
error?: any
|
||||
| typeof EXPORT_MUTES_FAIL,
|
||||
error?: any,
|
||||
}
|
||||
|
||||
function fileExport(content: string, fileName: string) {
|
||||
|
|
|
@ -11,25 +11,25 @@ export const FAMILIAR_FOLLOWERS_FETCH_SUCCESS = 'FAMILIAR_FOLLOWERS_FETCH_SUCCES
|
|||
export const FAMILIAR_FOLLOWERS_FETCH_FAIL = 'FAMILIAR_FOLLOWERS_FETCH_FAIL';
|
||||
|
||||
type FamiliarFollowersFetchRequestAction = {
|
||||
type: typeof FAMILIAR_FOLLOWERS_FETCH_REQUEST
|
||||
id: string
|
||||
type: typeof FAMILIAR_FOLLOWERS_FETCH_REQUEST,
|
||||
id: string,
|
||||
}
|
||||
|
||||
type FamiliarFollowersFetchRequestSuccessAction = {
|
||||
type: typeof FAMILIAR_FOLLOWERS_FETCH_SUCCESS
|
||||
id: string
|
||||
accounts: Array<APIEntity>
|
||||
type: typeof FAMILIAR_FOLLOWERS_FETCH_SUCCESS,
|
||||
id: string,
|
||||
accounts: Array<APIEntity>,
|
||||
}
|
||||
|
||||
type FamiliarFollowersFetchRequestFailAction = {
|
||||
type: typeof FAMILIAR_FOLLOWERS_FETCH_FAIL
|
||||
id: string
|
||||
error: any
|
||||
type: typeof FAMILIAR_FOLLOWERS_FETCH_FAIL,
|
||||
id: string,
|
||||
error: any,
|
||||
}
|
||||
|
||||
type AccountsImportAction = {
|
||||
type: typeof ACCOUNTS_IMPORT
|
||||
accounts: Array<APIEntity>
|
||||
type: typeof ACCOUNTS_IMPORT,
|
||||
accounts: Array<APIEntity>,
|
||||
}
|
||||
|
||||
export type FamiliarFollowersActions = FamiliarFollowersFetchRequestAction | FamiliarFollowersFetchRequestSuccessAction | FamiliarFollowersFetchRequestFailAction | AccountsImportAction
|
||||
|
|
|
@ -12,18 +12,10 @@ const FILTERS_FETCH_REQUEST = 'FILTERS_FETCH_REQUEST';
|
|||
const FILTERS_FETCH_SUCCESS = 'FILTERS_FETCH_SUCCESS';
|
||||
const FILTERS_FETCH_FAIL = 'FILTERS_FETCH_FAIL';
|
||||
|
||||
const FILTER_FETCH_REQUEST = 'FILTER_FETCH_REQUEST';
|
||||
const FILTER_FETCH_SUCCESS = 'FILTER_FETCH_SUCCESS';
|
||||
const FILTER_FETCH_FAIL = 'FILTER_FETCH_FAIL';
|
||||
|
||||
const FILTERS_CREATE_REQUEST = 'FILTERS_CREATE_REQUEST';
|
||||
const FILTERS_CREATE_SUCCESS = 'FILTERS_CREATE_SUCCESS';
|
||||
const FILTERS_CREATE_FAIL = 'FILTERS_CREATE_FAIL';
|
||||
|
||||
const FILTERS_UPDATE_REQUEST = 'FILTERS_UPDATE_REQUEST';
|
||||
const FILTERS_UPDATE_SUCCESS = 'FILTERS_UPDATE_SUCCESS';
|
||||
const FILTERS_UPDATE_FAIL = 'FILTERS_UPDATE_FAIL';
|
||||
|
||||
const FILTERS_DELETE_REQUEST = 'FILTERS_DELETE_REQUEST';
|
||||
const FILTERS_DELETE_SUCCESS = 'FILTERS_DELETE_SUCCESS';
|
||||
const FILTERS_DELETE_FAIL = 'FILTERS_DELETE_FAIL';
|
||||
|
@ -33,16 +25,22 @@ const messages = defineMessages({
|
|||
removed: { id: 'filters.removed', defaultMessage: 'Filter deleted.' },
|
||||
});
|
||||
|
||||
type FilterKeywords = { keyword: string, whole_word: boolean }[];
|
||||
|
||||
const fetchFiltersV1 = () =>
|
||||
const fetchFilters = () =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
if (!isLoggedIn(getState)) return;
|
||||
|
||||
const state = getState();
|
||||
const instance = state.instance;
|
||||
const features = getFeatures(instance);
|
||||
|
||||
if (!features.filters) return;
|
||||
|
||||
dispatch({
|
||||
type: FILTERS_FETCH_REQUEST,
|
||||
skipLoading: true,
|
||||
});
|
||||
|
||||
return api(getState)
|
||||
api(getState)
|
||||
.get('/api/v1/filters')
|
||||
.then(({ data }) => dispatch({
|
||||
type: FILTERS_FETCH_SUCCESS,
|
||||
|
@ -57,105 +55,15 @@ const fetchFiltersV1 = () =>
|
|||
}));
|
||||
};
|
||||
|
||||
const fetchFiltersV2 = () =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
dispatch({
|
||||
type: FILTERS_FETCH_REQUEST,
|
||||
skipLoading: true,
|
||||
});
|
||||
|
||||
return api(getState)
|
||||
.get('/api/v2/filters')
|
||||
.then(({ data }) => dispatch({
|
||||
type: FILTERS_FETCH_SUCCESS,
|
||||
filters: data,
|
||||
skipLoading: true,
|
||||
}))
|
||||
.catch(err => dispatch({
|
||||
type: FILTERS_FETCH_FAIL,
|
||||
err,
|
||||
skipLoading: true,
|
||||
skipAlert: true,
|
||||
}));
|
||||
};
|
||||
|
||||
const fetchFilters = (fromFiltersPage = false) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
if (!isLoggedIn(getState)) return;
|
||||
|
||||
const state = getState();
|
||||
const instance = state.instance;
|
||||
const features = getFeatures(instance);
|
||||
|
||||
if (features.filtersV2 && fromFiltersPage) return dispatch(fetchFiltersV2());
|
||||
|
||||
if (features.filters) return dispatch(fetchFiltersV1());
|
||||
};
|
||||
|
||||
const fetchFilterV1 = (id: string) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
dispatch({
|
||||
type: FILTER_FETCH_REQUEST,
|
||||
skipLoading: true,
|
||||
});
|
||||
|
||||
return api(getState)
|
||||
.get(`/api/v1/filters/${id}`)
|
||||
.then(({ data }) => dispatch({
|
||||
type: FILTER_FETCH_SUCCESS,
|
||||
filter: data,
|
||||
skipLoading: true,
|
||||
}))
|
||||
.catch(err => dispatch({
|
||||
type: FILTER_FETCH_FAIL,
|
||||
err,
|
||||
skipLoading: true,
|
||||
skipAlert: true,
|
||||
}));
|
||||
};
|
||||
|
||||
const fetchFilterV2 = (id: string) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
dispatch({
|
||||
type: FILTER_FETCH_REQUEST,
|
||||
skipLoading: true,
|
||||
});
|
||||
|
||||
return api(getState)
|
||||
.get(`/api/v2/filters/${id}`)
|
||||
.then(({ data }) => dispatch({
|
||||
type: FILTER_FETCH_SUCCESS,
|
||||
filter: data,
|
||||
skipLoading: true,
|
||||
}))
|
||||
.catch(err => dispatch({
|
||||
type: FILTER_FETCH_FAIL,
|
||||
err,
|
||||
skipLoading: true,
|
||||
skipAlert: true,
|
||||
}));
|
||||
};
|
||||
|
||||
const fetchFilter = (id: string) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
const state = getState();
|
||||
const instance = state.instance;
|
||||
const features = getFeatures(instance);
|
||||
|
||||
if (features.filtersV2) return dispatch(fetchFilterV2(id));
|
||||
|
||||
if (features.filters) return dispatch(fetchFilterV1(id));
|
||||
};
|
||||
|
||||
const createFilterV1 = (title: string, expires_in: string | null, context: Array<string>, hide: boolean, keywords: FilterKeywords) =>
|
||||
const createFilter = (phrase: string, expires_at: string, context: Array<string>, whole_word: boolean, irreversible: boolean) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
dispatch({ type: FILTERS_CREATE_REQUEST });
|
||||
return api(getState).post('/api/v1/filters', {
|
||||
phrase: keywords[0].keyword,
|
||||
phrase,
|
||||
context,
|
||||
irreversible: hide,
|
||||
whole_word: keywords[0].whole_word,
|
||||
expires_in,
|
||||
irreversible,
|
||||
whole_word,
|
||||
expires_at,
|
||||
}).then(response => {
|
||||
dispatch({ type: FILTERS_CREATE_SUCCESS, filter: response.data });
|
||||
toast.success(messages.added);
|
||||
|
@ -164,80 +72,7 @@ const createFilterV1 = (title: string, expires_in: string | null, context: Array
|
|||
});
|
||||
};
|
||||
|
||||
const createFilterV2 = (title: string, expires_in: string | null, context: Array<string>, hide: boolean, keywords_attributes: FilterKeywords) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
dispatch({ type: FILTERS_CREATE_REQUEST });
|
||||
return api(getState).post('/api/v2/filters', {
|
||||
title,
|
||||
context,
|
||||
filter_action: hide ? 'hide' : 'warn',
|
||||
expires_in,
|
||||
keywords_attributes,
|
||||
}).then(response => {
|
||||
dispatch({ type: FILTERS_CREATE_SUCCESS, filter: response.data });
|
||||
toast.success(messages.added);
|
||||
}).catch(error => {
|
||||
dispatch({ type: FILTERS_CREATE_FAIL, error });
|
||||
});
|
||||
};
|
||||
|
||||
const createFilter = (title: string, expires_in: string | null, context: Array<string>, hide: boolean, keywords: FilterKeywords) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
const state = getState();
|
||||
const instance = state.instance;
|
||||
const features = getFeatures(instance);
|
||||
|
||||
if (features.filtersV2) return dispatch(createFilterV2(title, expires_in, context, hide, keywords));
|
||||
|
||||
return dispatch(createFilterV1(title, expires_in, context, hide, keywords));
|
||||
};
|
||||
|
||||
const updateFilterV1 = (id: string, title: string, expires_in: string | null, context: Array<string>, hide: boolean, keywords: FilterKeywords) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
dispatch({ type: FILTERS_UPDATE_REQUEST });
|
||||
return api(getState).patch(`/api/v1/filters/${id}`, {
|
||||
phrase: keywords[0].keyword,
|
||||
context,
|
||||
irreversible: hide,
|
||||
whole_word: keywords[0].whole_word,
|
||||
expires_in,
|
||||
}).then(response => {
|
||||
dispatch({ type: FILTERS_UPDATE_SUCCESS, filter: response.data });
|
||||
toast.success(messages.added);
|
||||
}).catch(error => {
|
||||
dispatch({ type: FILTERS_UPDATE_FAIL, error });
|
||||
});
|
||||
};
|
||||
|
||||
const updateFilterV2 = (id: string, title: string, expires_in: string | null, context: Array<string>, hide: boolean, keywords_attributes: FilterKeywords) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
dispatch({ type: FILTERS_UPDATE_REQUEST });
|
||||
return api(getState).patch(`/api/v2/filters/${id}`, {
|
||||
title,
|
||||
context,
|
||||
filter_action: hide ? 'hide' : 'warn',
|
||||
expires_in,
|
||||
keywords_attributes,
|
||||
}).then(response => {
|
||||
dispatch({ type: FILTERS_UPDATE_SUCCESS, filter: response.data });
|
||||
toast.success(messages.added);
|
||||
}).catch(error => {
|
||||
dispatch({ type: FILTERS_UPDATE_FAIL, error });
|
||||
});
|
||||
};
|
||||
|
||||
const updateFilter = (id: string, title: string, expires_in: string | null, context: Array<string>, hide: boolean, keywords: FilterKeywords) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
const state = getState();
|
||||
const instance = state.instance;
|
||||
const features = getFeatures(instance);
|
||||
|
||||
if (features.filtersV2) return dispatch(updateFilterV2(id, title, expires_in, context, hide, keywords));
|
||||
|
||||
return dispatch(updateFilterV1(id, title, expires_in, context, hide, keywords));
|
||||
};
|
||||
|
||||
const deleteFilterV1 = (id: string) =>
|
||||
const deleteFilter = (id: string) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
dispatch({ type: FILTERS_DELETE_REQUEST });
|
||||
return api(getState).delete(`/api/v1/filters/${id}`).then(response => {
|
||||
|
@ -248,47 +83,17 @@ const deleteFilterV1 = (id: string) =>
|
|||
});
|
||||
};
|
||||
|
||||
const deleteFilterV2 = (id: string) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
dispatch({ type: FILTERS_DELETE_REQUEST });
|
||||
return api(getState).delete(`/api/v2/filters/${id}`).then(response => {
|
||||
dispatch({ type: FILTERS_DELETE_SUCCESS, filter: response.data });
|
||||
toast.success(messages.removed);
|
||||
}).catch(error => {
|
||||
dispatch({ type: FILTERS_DELETE_FAIL, error });
|
||||
});
|
||||
};
|
||||
|
||||
const deleteFilter = (id: string) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
const state = getState();
|
||||
const instance = state.instance;
|
||||
const features = getFeatures(instance);
|
||||
|
||||
if (features.filtersV2) return dispatch(deleteFilterV2(id));
|
||||
|
||||
return dispatch(deleteFilterV1(id));
|
||||
};
|
||||
|
||||
export {
|
||||
FILTERS_FETCH_REQUEST,
|
||||
FILTERS_FETCH_SUCCESS,
|
||||
FILTERS_FETCH_FAIL,
|
||||
FILTER_FETCH_REQUEST,
|
||||
FILTER_FETCH_SUCCESS,
|
||||
FILTER_FETCH_FAIL,
|
||||
FILTERS_CREATE_REQUEST,
|
||||
FILTERS_CREATE_SUCCESS,
|
||||
FILTERS_CREATE_FAIL,
|
||||
FILTERS_UPDATE_REQUEST,
|
||||
FILTERS_UPDATE_SUCCESS,
|
||||
FILTERS_UPDATE_FAIL,
|
||||
FILTERS_DELETE_REQUEST,
|
||||
FILTERS_DELETE_SUCCESS,
|
||||
FILTERS_DELETE_FAIL,
|
||||
fetchFilters,
|
||||
fetchFilter,
|
||||
createFilter,
|
||||
updateFilter,
|
||||
deleteFilter,
|
||||
};
|
||||
};
|
|
@ -1,6 +1,5 @@
|
|||
import { defineMessages } from 'react-intl';
|
||||
|
||||
import { deleteEntities } from 'soapbox/entity-store/actions';
|
||||
import toast from 'soapbox/toast';
|
||||
|
||||
import api, { getLinks } from '../api';
|
||||
|
@ -41,6 +40,14 @@ const GROUP_RELATIONSHIPS_FETCH_REQUEST = 'GROUP_RELATIONSHIPS_FETCH_REQUEST';
|
|||
const GROUP_RELATIONSHIPS_FETCH_SUCCESS = 'GROUP_RELATIONSHIPS_FETCH_SUCCESS';
|
||||
const GROUP_RELATIONSHIPS_FETCH_FAIL = 'GROUP_RELATIONSHIPS_FETCH_FAIL';
|
||||
|
||||
const GROUP_JOIN_REQUEST = 'GROUP_JOIN_REQUEST';
|
||||
const GROUP_JOIN_SUCCESS = 'GROUP_JOIN_SUCCESS';
|
||||
const GROUP_JOIN_FAIL = 'GROUP_JOIN_FAIL';
|
||||
|
||||
const GROUP_LEAVE_REQUEST = 'GROUP_LEAVE_REQUEST';
|
||||
const GROUP_LEAVE_SUCCESS = 'GROUP_LEAVE_SUCCESS';
|
||||
const GROUP_LEAVE_FAIL = 'GROUP_LEAVE_FAIL';
|
||||
|
||||
const GROUP_DELETE_STATUS_REQUEST = 'GROUP_DELETE_STATUS_REQUEST';
|
||||
const GROUP_DELETE_STATUS_SUCCESS = 'GROUP_DELETE_STATUS_SUCCESS';
|
||||
const GROUP_DELETE_STATUS_FAIL = 'GROUP_DELETE_STATUS_FAIL';
|
||||
|
@ -141,8 +148,7 @@ const createGroup = (params: Record<string, any>, shouldReset?: boolean) =>
|
|||
if (shouldReset) {
|
||||
dispatch(resetGroupEditor());
|
||||
}
|
||||
|
||||
return data;
|
||||
dispatch(closeModal('MANAGE_GROUP'));
|
||||
}).catch(err => dispatch(createGroupFail(err)));
|
||||
};
|
||||
|
||||
|
@ -192,7 +198,7 @@ const updateGroupFail = (error: AxiosError) => ({
|
|||
});
|
||||
|
||||
const deleteGroup = (id: string) => (dispatch: AppDispatch, getState: () => RootState) => {
|
||||
dispatch(deleteEntities([id], 'Group'));
|
||||
dispatch(deleteGroupRequest(id));
|
||||
|
||||
return api(getState).delete(`/api/v1/groups/${id}`)
|
||||
.then(() => dispatch(deleteGroupSuccess(id)))
|
||||
|
@ -306,6 +312,70 @@ const fetchGroupRelationshipsFail = (error: AxiosError) => ({
|
|||
skipNotFound: true,
|
||||
});
|
||||
|
||||
const joinGroup = (id: string) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
const locked = (getState().groups.items.get(id) as any).locked || false;
|
||||
|
||||
dispatch(joinGroupRequest(id, locked));
|
||||
|
||||
return api(getState).post(`/api/v1/groups/${id}/join`).then(response => {
|
||||
dispatch(joinGroupSuccess(response.data));
|
||||
toast.success(locked ? messages.joinRequestSuccess : messages.joinSuccess);
|
||||
}).catch(error => {
|
||||
dispatch(joinGroupFail(error, locked));
|
||||
});
|
||||
};
|
||||
|
||||
const leaveGroup = (id: string) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
dispatch(leaveGroupRequest(id));
|
||||
|
||||
return api(getState).post(`/api/v1/groups/${id}/leave`).then(response => {
|
||||
dispatch(leaveGroupSuccess(response.data));
|
||||
toast.success(messages.leaveSuccess);
|
||||
}).catch(error => {
|
||||
dispatch(leaveGroupFail(error));
|
||||
});
|
||||
};
|
||||
|
||||
const joinGroupRequest = (id: string, locked: boolean) => ({
|
||||
type: GROUP_JOIN_REQUEST,
|
||||
id,
|
||||
locked,
|
||||
skipLoading: true,
|
||||
});
|
||||
|
||||
const joinGroupSuccess = (relationship: APIEntity) => ({
|
||||
type: GROUP_JOIN_SUCCESS,
|
||||
relationship,
|
||||
skipLoading: true,
|
||||
});
|
||||
|
||||
const joinGroupFail = (error: AxiosError, locked: boolean) => ({
|
||||
type: GROUP_JOIN_FAIL,
|
||||
error,
|
||||
locked,
|
||||
skipLoading: true,
|
||||
});
|
||||
|
||||
const leaveGroupRequest = (id: string) => ({
|
||||
type: GROUP_LEAVE_REQUEST,
|
||||
id,
|
||||
skipLoading: true,
|
||||
});
|
||||
|
||||
const leaveGroupSuccess = (relationship: APIEntity) => ({
|
||||
type: GROUP_LEAVE_SUCCESS,
|
||||
relationship,
|
||||
skipLoading: true,
|
||||
});
|
||||
|
||||
const leaveGroupFail = (error: AxiosError) => ({
|
||||
type: GROUP_LEAVE_FAIL,
|
||||
error,
|
||||
skipLoading: true,
|
||||
});
|
||||
|
||||
const groupDeleteStatus = (groupId: string, statusId: string) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
dispatch(groupDeleteStatusRequest(groupId, statusId));
|
||||
|
@ -789,11 +859,9 @@ const submitGroupEditor = (shouldReset?: boolean) => (dispatch: AppDispatch, get
|
|||
const note = getState().group_editor.note;
|
||||
const avatar = getState().group_editor.avatar;
|
||||
const header = getState().group_editor.header;
|
||||
const visibility = getState().group_editor.locked ? 'members_only' : 'everyone'; // Truth Social
|
||||
|
||||
const params: Record<string, any> = {
|
||||
display_name: displayName,
|
||||
group_visibility: visibility,
|
||||
note,
|
||||
};
|
||||
|
||||
|
@ -801,9 +869,9 @@ const submitGroupEditor = (shouldReset?: boolean) => (dispatch: AppDispatch, get
|
|||
if (header) params.header = header;
|
||||
|
||||
if (groupId === null) {
|
||||
return dispatch(createGroup(params, shouldReset));
|
||||
dispatch(createGroup(params, shouldReset));
|
||||
} else {
|
||||
return dispatch(updateGroup(groupId, params, shouldReset));
|
||||
dispatch(updateGroup(groupId, params, shouldReset));
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -827,6 +895,12 @@ export {
|
|||
GROUP_RELATIONSHIPS_FETCH_REQUEST,
|
||||
GROUP_RELATIONSHIPS_FETCH_SUCCESS,
|
||||
GROUP_RELATIONSHIPS_FETCH_FAIL,
|
||||
GROUP_JOIN_REQUEST,
|
||||
GROUP_JOIN_SUCCESS,
|
||||
GROUP_JOIN_FAIL,
|
||||
GROUP_LEAVE_REQUEST,
|
||||
GROUP_LEAVE_SUCCESS,
|
||||
GROUP_LEAVE_FAIL,
|
||||
GROUP_DELETE_STATUS_REQUEST,
|
||||
GROUP_DELETE_STATUS_SUCCESS,
|
||||
GROUP_DELETE_STATUS_FAIL,
|
||||
|
@ -899,6 +973,14 @@ export {
|
|||
fetchGroupRelationshipsRequest,
|
||||
fetchGroupRelationshipsSuccess,
|
||||
fetchGroupRelationshipsFail,
|
||||
joinGroup,
|
||||
leaveGroup,
|
||||
joinGroupRequest,
|
||||
joinGroupSuccess,
|
||||
joinGroupFail,
|
||||
leaveGroupRequest,
|
||||
leaveGroupSuccess,
|
||||
leaveGroupFail,
|
||||
groupDeleteStatus,
|
||||
groupDeleteStatusRequest,
|
||||
groupDeleteStatusSuccess,
|
||||
|
|
|
@ -27,8 +27,8 @@ type ImportDataActions = {
|
|||
| typeof IMPORT_BLOCKS_FAIL
|
||||
| typeof IMPORT_MUTES_REQUEST
|
||||
| typeof IMPORT_MUTES_SUCCESS
|
||||
| typeof IMPORT_MUTES_FAIL
|
||||
error?: any
|
||||
| typeof IMPORT_MUTES_FAIL,
|
||||
error?: any,
|
||||
config?: string
|
||||
}
|
||||
|
||||
|
|
|
@ -1,8 +1,3 @@
|
|||
import { importEntities } from 'soapbox/entity-store/actions';
|
||||
import { Entities } from 'soapbox/entity-store/entities';
|
||||
import { Group, groupSchema } from 'soapbox/schemas';
|
||||
import { filteredArray } from 'soapbox/schemas/utils';
|
||||
|
||||
import { getSettings } from '../settings';
|
||||
|
||||
import type { AppDispatch, RootState } from 'soapbox/store';
|
||||
|
@ -23,11 +18,11 @@ const importAccount = (account: APIEntity) =>
|
|||
const importAccounts = (accounts: APIEntity[]) =>
|
||||
({ type: ACCOUNTS_IMPORT, accounts });
|
||||
|
||||
const importGroup = (group: Group) =>
|
||||
importEntities([group], Entities.GROUPS);
|
||||
const importGroup = (group: APIEntity) =>
|
||||
({ type: GROUP_IMPORT, group });
|
||||
|
||||
const importGroups = (groups: Group[]) =>
|
||||
importEntities(groups, Entities.GROUPS);
|
||||
const importGroups = (groups: APIEntity[]) =>
|
||||
({ type: GROUPS_IMPORT, groups });
|
||||
|
||||
const importStatus = (status: APIEntity, idempotencyKey?: string) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
|
@ -74,8 +69,17 @@ const importFetchedGroup = (group: APIEntity) =>
|
|||
importFetchedGroups([group]);
|
||||
|
||||
const importFetchedGroups = (groups: APIEntity[]) => {
|
||||
const entities = filteredArray(groupSchema).catch([]).parse(groups);
|
||||
return importGroups(entities);
|
||||
const normalGroups: APIEntity[] = [];
|
||||
|
||||
const processGroup = (group: APIEntity) => {
|
||||
if (!group.id) return;
|
||||
|
||||
normalGroups.push(group);
|
||||
};
|
||||
|
||||
groups.forEach(processGroup);
|
||||
|
||||
return importGroups(normalGroups);
|
||||
};
|
||||
|
||||
const importFetchedStatus = (status: APIEntity, idempotencyKey?: string) =>
|
||||
|
|
|
@ -20,10 +20,6 @@ const FAVOURITE_REQUEST = 'FAVOURITE_REQUEST';
|
|||
const FAVOURITE_SUCCESS = 'FAVOURITE_SUCCESS';
|
||||
const FAVOURITE_FAIL = 'FAVOURITE_FAIL';
|
||||
|
||||
const DISLIKE_REQUEST = 'DISLIKE_REQUEST';
|
||||
const DISLIKE_SUCCESS = 'DISLIKE_SUCCESS';
|
||||
const DISLIKE_FAIL = 'DISLIKE_FAIL';
|
||||
|
||||
const UNREBLOG_REQUEST = 'UNREBLOG_REQUEST';
|
||||
const UNREBLOG_SUCCESS = 'UNREBLOG_SUCCESS';
|
||||
const UNREBLOG_FAIL = 'UNREBLOG_FAIL';
|
||||
|
@ -32,10 +28,6 @@ const UNFAVOURITE_REQUEST = 'UNFAVOURITE_REQUEST';
|
|||
const UNFAVOURITE_SUCCESS = 'UNFAVOURITE_SUCCESS';
|
||||
const UNFAVOURITE_FAIL = 'UNFAVOURITE_FAIL';
|
||||
|
||||
const UNDISLIKE_REQUEST = 'UNDISLIKE_REQUEST';
|
||||
const UNDISLIKE_SUCCESS = 'UNDISLIKE_SUCCESS';
|
||||
const UNDISLIKE_FAIL = 'UNDISLIKE_FAIL';
|
||||
|
||||
const REBLOGS_FETCH_REQUEST = 'REBLOGS_FETCH_REQUEST';
|
||||
const REBLOGS_FETCH_SUCCESS = 'REBLOGS_FETCH_SUCCESS';
|
||||
const REBLOGS_FETCH_FAIL = 'REBLOGS_FETCH_FAIL';
|
||||
|
@ -44,10 +36,6 @@ const FAVOURITES_FETCH_REQUEST = 'FAVOURITES_FETCH_REQUEST';
|
|||
const FAVOURITES_FETCH_SUCCESS = 'FAVOURITES_FETCH_SUCCESS';
|
||||
const FAVOURITES_FETCH_FAIL = 'FAVOURITES_FETCH_FAIL';
|
||||
|
||||
const DISLIKES_FETCH_REQUEST = 'DISLIKES_FETCH_REQUEST';
|
||||
const DISLIKES_FETCH_SUCCESS = 'DISLIKES_FETCH_SUCCESS';
|
||||
const DISLIKES_FETCH_FAIL = 'DISLIKES_FETCH_FAIL';
|
||||
|
||||
const REACTIONS_FETCH_REQUEST = 'REACTIONS_FETCH_REQUEST';
|
||||
const REACTIONS_FETCH_SUCCESS = 'REACTIONS_FETCH_SUCCESS';
|
||||
const REACTIONS_FETCH_FAIL = 'REACTIONS_FETCH_FAIL';
|
||||
|
@ -108,7 +96,7 @@ const unreblog = (status: StatusEntity) =>
|
|||
};
|
||||
|
||||
const toggleReblog = (status: StatusEntity) =>
|
||||
(dispatch: AppDispatch) => {
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
if (status.reblogged) {
|
||||
dispatch(unreblog(status));
|
||||
} else {
|
||||
|
@ -181,7 +169,7 @@ const unfavourite = (status: StatusEntity) =>
|
|||
};
|
||||
|
||||
const toggleFavourite = (status: StatusEntity) =>
|
||||
(dispatch: AppDispatch) => {
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
if (status.favourited) {
|
||||
dispatch(unfavourite(status));
|
||||
} else {
|
||||
|
@ -227,79 +215,6 @@ const unfavouriteFail = (status: StatusEntity, error: AxiosError) => ({
|
|||
skipLoading: true,
|
||||
});
|
||||
|
||||
const dislike = (status: StatusEntity) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
if (!isLoggedIn(getState)) return;
|
||||
|
||||
dispatch(dislikeRequest(status));
|
||||
|
||||
api(getState).post(`/api/friendica/statuses/${status.get('id')}/dislike`).then(function() {
|
||||
dispatch(dislikeSuccess(status));
|
||||
}).catch(function(error) {
|
||||
dispatch(dislikeFail(status, error));
|
||||
});
|
||||
};
|
||||
|
||||
const undislike = (status: StatusEntity) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
if (!isLoggedIn(getState)) return;
|
||||
|
||||
dispatch(undislikeRequest(status));
|
||||
|
||||
api(getState).post(`/api/friendica/statuses/${status.get('id')}/undislike`).then(() => {
|
||||
dispatch(undislikeSuccess(status));
|
||||
}).catch(error => {
|
||||
dispatch(undislikeFail(status, error));
|
||||
});
|
||||
};
|
||||
|
||||
const toggleDislike = (status: StatusEntity) =>
|
||||
(dispatch: AppDispatch) => {
|
||||
if (status.disliked) {
|
||||
dispatch(undislike(status));
|
||||
} else {
|
||||
dispatch(dislike(status));
|
||||
}
|
||||
};
|
||||
|
||||
const dislikeRequest = (status: StatusEntity) => ({
|
||||
type: DISLIKE_REQUEST,
|
||||
status: status,
|
||||
skipLoading: true,
|
||||
});
|
||||
|
||||
const dislikeSuccess = (status: StatusEntity) => ({
|
||||
type: DISLIKE_SUCCESS,
|
||||
status: status,
|
||||
skipLoading: true,
|
||||
});
|
||||
|
||||
const dislikeFail = (status: StatusEntity, error: AxiosError) => ({
|
||||
type: DISLIKE_FAIL,
|
||||
status: status,
|
||||
error: error,
|
||||
skipLoading: true,
|
||||
});
|
||||
|
||||
const undislikeRequest = (status: StatusEntity) => ({
|
||||
type: UNDISLIKE_REQUEST,
|
||||
status: status,
|
||||
skipLoading: true,
|
||||
});
|
||||
|
||||
const undislikeSuccess = (status: StatusEntity) => ({
|
||||
type: UNDISLIKE_SUCCESS,
|
||||
status: status,
|
||||
skipLoading: true,
|
||||
});
|
||||
|
||||
const undislikeFail = (status: StatusEntity, error: AxiosError) => ({
|
||||
type: UNDISLIKE_FAIL,
|
||||
status: status,
|
||||
error: error,
|
||||
skipLoading: true,
|
||||
});
|
||||
|
||||
const bookmark = (status: StatusEntity) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
dispatch(bookmarkRequest(status));
|
||||
|
@ -436,38 +351,6 @@ const fetchFavouritesFail = (id: string, error: AxiosError) => ({
|
|||
error,
|
||||
});
|
||||
|
||||
const fetchDislikes = (id: string) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
if (!isLoggedIn(getState)) return;
|
||||
|
||||
dispatch(fetchDislikesRequest(id));
|
||||
|
||||
api(getState).get(`/api/friendica/statuses/${id}/disliked_by`).then(response => {
|
||||
dispatch(importFetchedAccounts(response.data));
|
||||
dispatch(fetchRelationships(response.data.map((item: APIEntity) => item.id)));
|
||||
dispatch(fetchDislikesSuccess(id, response.data));
|
||||
}).catch(error => {
|
||||
dispatch(fetchDislikesFail(id, error));
|
||||
});
|
||||
};
|
||||
|
||||
const fetchDislikesRequest = (id: string) => ({
|
||||
type: DISLIKES_FETCH_REQUEST,
|
||||
id,
|
||||
});
|
||||
|
||||
const fetchDislikesSuccess = (id: string, accounts: APIEntity[]) => ({
|
||||
type: DISLIKES_FETCH_SUCCESS,
|
||||
id,
|
||||
accounts,
|
||||
});
|
||||
|
||||
const fetchDislikesFail = (id: string, error: AxiosError) => ({
|
||||
type: DISLIKES_FETCH_FAIL,
|
||||
id,
|
||||
error,
|
||||
});
|
||||
|
||||
const fetchReactions = (id: string) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
dispatch(fetchReactionsRequest(id));
|
||||
|
@ -615,27 +498,18 @@ export {
|
|||
FAVOURITE_REQUEST,
|
||||
FAVOURITE_SUCCESS,
|
||||
FAVOURITE_FAIL,
|
||||
DISLIKE_REQUEST,
|
||||
DISLIKE_SUCCESS,
|
||||
DISLIKE_FAIL,
|
||||
UNREBLOG_REQUEST,
|
||||
UNREBLOG_SUCCESS,
|
||||
UNREBLOG_FAIL,
|
||||
UNFAVOURITE_REQUEST,
|
||||
UNFAVOURITE_SUCCESS,
|
||||
UNFAVOURITE_FAIL,
|
||||
UNDISLIKE_REQUEST,
|
||||
UNDISLIKE_SUCCESS,
|
||||
UNDISLIKE_FAIL,
|
||||
REBLOGS_FETCH_REQUEST,
|
||||
REBLOGS_FETCH_SUCCESS,
|
||||
REBLOGS_FETCH_FAIL,
|
||||
FAVOURITES_FETCH_REQUEST,
|
||||
FAVOURITES_FETCH_SUCCESS,
|
||||
FAVOURITES_FETCH_FAIL,
|
||||
DISLIKES_FETCH_REQUEST,
|
||||
DISLIKES_FETCH_SUCCESS,
|
||||
DISLIKES_FETCH_FAIL,
|
||||
REACTIONS_FETCH_REQUEST,
|
||||
REACTIONS_FETCH_SUCCESS,
|
||||
REACTIONS_FETCH_FAIL,
|
||||
|
@ -672,15 +546,6 @@ export {
|
|||
unfavouriteRequest,
|
||||
unfavouriteSuccess,
|
||||
unfavouriteFail,
|
||||
dislike,
|
||||
undislike,
|
||||
toggleDislike,
|
||||
dislikeRequest,
|
||||
dislikeSuccess,
|
||||
dislikeFail,
|
||||
undislikeRequest,
|
||||
undislikeSuccess,
|
||||
undislikeFail,
|
||||
bookmark,
|
||||
unbookmark,
|
||||
toggleBookmark,
|
||||
|
@ -698,10 +563,6 @@ export {
|
|||
fetchFavouritesRequest,
|
||||
fetchFavouritesSuccess,
|
||||
fetchFavouritesFail,
|
||||
fetchDislikes,
|
||||
fetchDislikesRequest,
|
||||
fetchDislikesSuccess,
|
||||
fetchDislikesFail,
|
||||
fetchReactions,
|
||||
fetchReactionsRequest,
|
||||
fetchReactionsSuccess,
|
||||
|
|
|
@ -112,6 +112,27 @@ const deleteUserModal = (intl: IntlShape, accountId: string, afterConfirm = () =
|
|||
}));
|
||||
};
|
||||
|
||||
const rejectUserModal = (intl: IntlShape, accountId: string, afterConfirm = () => {}) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
const state = getState();
|
||||
const acct = state.accounts.get(accountId)!.acct;
|
||||
const name = state.accounts.get(accountId)!.username;
|
||||
|
||||
dispatch(openModal('CONFIRM', {
|
||||
icon: require('@tabler/icons/user-off.svg'),
|
||||
heading: intl.formatMessage(messages.rejectUserHeading, { acct }),
|
||||
message: intl.formatMessage(messages.rejectUserPrompt, { acct }),
|
||||
confirm: intl.formatMessage(messages.rejectUserConfirm, { name }),
|
||||
onConfirm: () => {
|
||||
dispatch(deleteUsers([accountId]))
|
||||
.then(() => {
|
||||
afterConfirm();
|
||||
})
|
||||
.catch(() => {});
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
const toggleStatusSensitivityModal = (intl: IntlShape, statusId: string, sensitive: boolean, afterConfirm = () => {}) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
const state = getState();
|
||||
|
@ -157,6 +178,7 @@ const deleteStatusModal = (intl: IntlShape, statusId: string, afterConfirm = ()
|
|||
export {
|
||||
deactivateUserModal,
|
||||
deleteUserModal,
|
||||
rejectUserModal,
|
||||
toggleStatusSensitivityModal,
|
||||
deleteStatusModal,
|
||||
};
|
||||
|
|
|
@ -37,8 +37,8 @@ const subscribe = (registration: ServiceWorkerRegistration, getState: () => Root
|
|||
});
|
||||
|
||||
const unsubscribe = ({ registration, subscription }: {
|
||||
registration: ServiceWorkerRegistration
|
||||
subscription: PushSubscription | null
|
||||
registration: ServiceWorkerRegistration,
|
||||
subscription: PushSubscription | null,
|
||||
}) =>
|
||||
subscription ? subscription.unsubscribe().then(() => registration) : new Promise<ServiceWorkerRegistration>(r => r(registration));
|
||||
|
||||
|
@ -82,8 +82,8 @@ const register = () =>
|
|||
.then(getPushSubscription)
|
||||
// @ts-ignore
|
||||
.then(({ registration, subscription }: {
|
||||
registration: ServiceWorkerRegistration
|
||||
subscription: PushSubscription | null
|
||||
registration: ServiceWorkerRegistration,
|
||||
subscription: PushSubscription | null,
|
||||
}) => {
|
||||
if (subscription !== null) {
|
||||
// We have a subscription, check if it is still valid
|
||||
|
|
|
@ -4,7 +4,7 @@ import { openModal } from './modals';
|
|||
|
||||
import type { AxiosError } from 'axios';
|
||||
import type { AppDispatch, RootState } from 'soapbox/store';
|
||||
import type { Account, ChatMessage, Group, Status } from 'soapbox/types/entities';
|
||||
import type { Account, ChatMessage, Status } from 'soapbox/types/entities';
|
||||
|
||||
const REPORT_INIT = 'REPORT_INIT';
|
||||
const REPORT_CANCEL = 'REPORT_CANCEL';
|
||||
|
@ -20,29 +20,19 @@ const REPORT_BLOCK_CHANGE = 'REPORT_BLOCK_CHANGE';
|
|||
|
||||
const REPORT_RULE_CHANGE = 'REPORT_RULE_CHANGE';
|
||||
|
||||
enum ReportableEntities {
|
||||
ACCOUNT = 'ACCOUNT',
|
||||
CHAT_MESSAGE = 'CHAT_MESSAGE',
|
||||
GROUP = 'GROUP',
|
||||
STATUS = 'STATUS'
|
||||
}
|
||||
|
||||
type ReportedEntity = {
|
||||
status?: Status
|
||||
status?: Status,
|
||||
chatMessage?: ChatMessage
|
||||
group?: Group
|
||||
}
|
||||
|
||||
const initReport = (entityType: ReportableEntities, account: Account, entities?: ReportedEntity) => (dispatch: AppDispatch) => {
|
||||
const { status, chatMessage, group } = entities || {};
|
||||
const initReport = (account: Account, entities?: ReportedEntity) => (dispatch: AppDispatch) => {
|
||||
const { status, chatMessage } = entities || {};
|
||||
|
||||
dispatch({
|
||||
type: REPORT_INIT,
|
||||
entityType,
|
||||
account,
|
||||
status,
|
||||
chatMessage,
|
||||
group,
|
||||
});
|
||||
|
||||
return dispatch(openModal('REPORT'));
|
||||
|
@ -66,8 +56,7 @@ const submitReport = () =>
|
|||
return api(getState).post('/api/v1/reports', {
|
||||
account_id: reports.getIn(['new', 'account_id']),
|
||||
status_ids: reports.getIn(['new', 'status_ids']),
|
||||
message_ids: [reports.getIn(['new', 'chat_message', 'id'])].filter(Boolean),
|
||||
group_id: reports.getIn(['new', 'group', 'id']),
|
||||
message_ids: [reports.getIn(['new', 'chat_message', 'id'])],
|
||||
rule_ids: reports.getIn(['new', 'rule_ids']),
|
||||
comment: reports.getIn(['new', 'comment']),
|
||||
forward: reports.getIn(['new', 'forward']),
|
||||
|
@ -108,7 +97,6 @@ const changeReportRule = (ruleId: string) => ({
|
|||
});
|
||||
|
||||
export {
|
||||
ReportableEntities,
|
||||
REPORT_INIT,
|
||||
REPORT_CANCEL,
|
||||
REPORT_SUBMIT_REQUEST,
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
import { Map as ImmutableMap, List as ImmutableList, OrderedSet as ImmutableOrderedSet } from 'immutable';
|
||||
import { defineMessage } from 'react-intl';
|
||||
import { defineMessages } from 'react-intl';
|
||||
import { createSelector } from 'reselect';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
import { patchMe } from 'soapbox/actions/me';
|
||||
import messages from 'soapbox/locales/messages';
|
||||
import toast from 'soapbox/toast';
|
||||
import { isLoggedIn } from 'soapbox/utils/auth';
|
||||
|
||||
|
@ -19,10 +18,12 @@ const FE_NAME = 'soapbox_fe';
|
|||
/** Options when changing/saving settings. */
|
||||
type SettingOpts = {
|
||||
/** Whether to display an alert when settings are saved. */
|
||||
showAlert?: boolean
|
||||
showAlert?: boolean,
|
||||
}
|
||||
|
||||
const saveSuccessMessage = defineMessage({ id: 'settings.save.success', defaultMessage: 'Your preferences have been saved!' });
|
||||
const messages = defineMessages({
|
||||
saveSuccess: { id: 'settings.save.success', defaultMessage: 'Your preferences have been saved!' },
|
||||
});
|
||||
|
||||
const defaultSettings = ImmutableMap({
|
||||
onboarded: false,
|
||||
|
@ -39,7 +40,7 @@ const defaultSettings = ImmutableMap({
|
|||
defaultPrivacy: 'public',
|
||||
defaultContentType: 'text/plain',
|
||||
themeMode: 'system',
|
||||
locale: navigator.language || 'en',
|
||||
locale: navigator.language.split(/[-_]/)[0] || 'en',
|
||||
showExplanationBox: true,
|
||||
explanationBox: true,
|
||||
autoloadTimelines: true,
|
||||
|
@ -220,7 +221,7 @@ const saveSettingsImmediate = (opts?: SettingOpts) =>
|
|||
dispatch({ type: SETTING_SAVE });
|
||||
|
||||
if (opts?.showAlert) {
|
||||
toast.success(saveSuccessMessage);
|
||||
toast.success(messages.saveSuccess);
|
||||
}
|
||||
}).catch(error => {
|
||||
toast.showAlertForError(error);
|
||||
|
@ -230,12 +231,6 @@ const saveSettingsImmediate = (opts?: SettingOpts) =>
|
|||
const saveSettings = (opts?: SettingOpts) =>
|
||||
(dispatch: AppDispatch) => dispatch(saveSettingsImmediate(opts));
|
||||
|
||||
const getLocale = (state: RootState, fallback = 'en') => {
|
||||
const localeWithVariant = (getSettings(state).get('locale') as string).replace('_', '-');
|
||||
const locale = localeWithVariant.split('-')[0];
|
||||
return Object.keys(messages).includes(localeWithVariant) ? localeWithVariant : Object.keys(messages).includes(locale) ? locale : fallback;
|
||||
};
|
||||
|
||||
export {
|
||||
SETTING_CHANGE,
|
||||
SETTING_SAVE,
|
||||
|
@ -247,5 +242,4 @@ export {
|
|||
changeSetting,
|
||||
saveSettingsImmediate,
|
||||
saveSettings,
|
||||
getLocale,
|
||||
};
|
||||
|
|
|
@ -32,8 +32,8 @@ const getSoapboxConfig = createSelector([
|
|||
}
|
||||
|
||||
// If RGI reacts aren't supported, strip VS16s
|
||||
// https://git.pleroma.social/pleroma/pleroma/-/issues/2355
|
||||
if (features.emojiReactsNonRGI) {
|
||||
// // https://git.pleroma.social/pleroma/pleroma/-/issues/2355
|
||||
if (!features.emojiReactsRGI) {
|
||||
soapboxConfig.set('allowedEmoji', soapboxConfig.allowedEmoji.map(removeVS16s));
|
||||
}
|
||||
});
|
||||
|
|
|
@ -48,8 +48,6 @@ const STATUS_TRANSLATE_SUCCESS = 'STATUS_TRANSLATE_SUCCESS';
|
|||
const STATUS_TRANSLATE_FAIL = 'STATUS_TRANSLATE_FAIL';
|
||||
const STATUS_TRANSLATE_UNDO = 'STATUS_TRANSLATE_UNDO';
|
||||
|
||||
const STATUS_UNFILTER = 'STATUS_UNFILTER';
|
||||
|
||||
const statusExists = (getState: () => RootState, statusId: string) => {
|
||||
return (getState().statuses.get(statusId) || null) !== null;
|
||||
};
|
||||
|
@ -337,11 +335,6 @@ const undoStatusTranslation = (id: string) => ({
|
|||
id,
|
||||
});
|
||||
|
||||
const unfilterStatus = (id: string) => ({
|
||||
type: STATUS_UNFILTER,
|
||||
id,
|
||||
});
|
||||
|
||||
export {
|
||||
STATUS_CREATE_REQUEST,
|
||||
STATUS_CREATE_SUCCESS,
|
||||
|
@ -370,7 +363,6 @@ export {
|
|||
STATUS_TRANSLATE_SUCCESS,
|
||||
STATUS_TRANSLATE_FAIL,
|
||||
STATUS_TRANSLATE_UNDO,
|
||||
STATUS_UNFILTER,
|
||||
createStatus,
|
||||
editStatus,
|
||||
fetchStatus,
|
||||
|
@ -389,5 +381,4 @@ export {
|
|||
toggleStatusHidden,
|
||||
translateStatus,
|
||||
undoStatusTranslation,
|
||||
unfilterStatus,
|
||||
};
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import { getLocale, getSettings } from 'soapbox/actions/settings';
|
||||
import { getSettings } from 'soapbox/actions/settings';
|
||||
import messages from 'soapbox/locales/messages';
|
||||
import { ChatKeys, IChat, isLastMessage } from 'soapbox/queries/chats';
|
||||
import { queryClient } from 'soapbox/queries/client';
|
||||
import { getUnreadChatsCount, updateChatListItem, updateChatMessage } from 'soapbox/utils/chats';
|
||||
import { getUnreadChatsCount, updateChatListItem } from 'soapbox/utils/chats';
|
||||
import { removePageItem } from 'soapbox/utils/queries';
|
||||
import { play, soundCache } from 'soapbox/utils/sounds';
|
||||
|
||||
|
@ -34,6 +34,13 @@ import type { APIEntity, Chat } from 'soapbox/types/entities';
|
|||
const STREAMING_CHAT_UPDATE = 'STREAMING_CHAT_UPDATE';
|
||||
const STREAMING_FOLLOW_RELATIONSHIPS_UPDATE = 'STREAMING_FOLLOW_RELATIONSHIPS_UPDATE';
|
||||
|
||||
const validLocale = (locale: string) => Object.keys(messages).includes(locale);
|
||||
|
||||
const getLocale = (state: RootState) => {
|
||||
const locale = getSettings(state).get('locale') as string;
|
||||
return validLocale(locale) ? locale : 'en';
|
||||
};
|
||||
|
||||
const updateFollowRelationships = (relationships: APIEntity) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
const me = getState().me;
|
||||
|
@ -74,7 +81,7 @@ const updateChatQuery = (chat: IChat) => {
|
|||
};
|
||||
|
||||
interface StreamOpts {
|
||||
statContext?: IStatContext
|
||||
statContext?: IStatContext,
|
||||
}
|
||||
|
||||
const connectTimelineStream = (
|
||||
|
@ -163,9 +170,6 @@ const connectTimelineStream = (
|
|||
}
|
||||
});
|
||||
break;
|
||||
case 'chat_message.reaction': // TruthSocial
|
||||
updateChatMessage(JSON.parse(data.payload));
|
||||
break;
|
||||
case 'pleroma:follow_relationships_update':
|
||||
dispatch(updateFollowRelationships(JSON.parse(data.payload)));
|
||||
break;
|
||||
|
|
|
@ -31,14 +31,14 @@ const AGE: Challenge = 'age';
|
|||
export type Challenge = 'age' | 'sms' | 'email'
|
||||
|
||||
type Challenges = {
|
||||
email?: 0 | 1
|
||||
sms?: 0 | 1
|
||||
age?: 0 | 1
|
||||
email?: 0 | 1,
|
||||
sms?: 0 | 1,
|
||||
age?: 0 | 1,
|
||||
}
|
||||
|
||||
type Verification = {
|
||||
token?: string
|
||||
challenges?: Challenges
|
||||
token?: string,
|
||||
challenges?: Challenges,
|
||||
challengeTypes?: Array<'age' | 'sms' | 'email'>
|
||||
};
|
||||
|
||||
|
|
|
@ -23,12 +23,7 @@ export const getLinks = (response: AxiosResponse): LinkHeader => {
|
|||
|
||||
export const getNextLink = (response: AxiosResponse) => {
|
||||
const nextLink = new LinkHeader(response.headers?.link);
|
||||
return nextLink.refs.find(link => link.rel === 'next')?.uri;
|
||||
};
|
||||
|
||||
export const getPrevLink = (response: AxiosResponse) => {
|
||||
const prevLink = new LinkHeader(response.headers?.link);
|
||||
return prevLink.refs.find(link => link.rel === 'prev')?.uri;
|
||||
return nextLink.refs.find((ref) => ref.uri)?.uri;
|
||||
};
|
||||
|
||||
export const baseClient = (...params: any[]) => {
|
||||
|
|
|
@ -29,10 +29,6 @@ export const getNextLink = (response: AxiosResponse): string | undefined => {
|
|||
return getLinks(response).refs.find(link => link.rel === 'next')?.uri;
|
||||
};
|
||||
|
||||
export const getPrevLink = (response: AxiosResponse): string | undefined => {
|
||||
return getLinks(response).refs.find(link => link.rel === 'prev')?.uri;
|
||||
};
|
||||
|
||||
const getToken = (state: RootState, authType: string) => {
|
||||
return authType === 'app' ? getAppToken(state) : getAccessToken(state);
|
||||
};
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import React from 'react';
|
||||
|
||||
interface IInlineSVG {
|
||||
loader?: JSX.Element
|
||||
loader?: JSX.Element,
|
||||
}
|
||||
|
||||
const InlineSVG: React.FC<IInlineSVG> = ({ loader }): JSX.Element => {
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
import React from 'react';
|
||||
|
||||
import { render, screen } from '../../jest/test-helpers';
|
||||
import EmojiSelector from '../emoji-selector';
|
||||
|
||||
describe('<EmojiSelector />', () => {
|
||||
it('renders correctly', () => {
|
||||
const children = <EmojiSelector />;
|
||||
// @ts-ignore
|
||||
children.__proto__.addEventListener = () => {};
|
||||
|
||||
render(children);
|
||||
|
||||
expect(screen.queryAllByRole('button')).toHaveLength(6);
|
||||
});
|
||||
});
|
|
@ -1,4 +1,4 @@
|
|||
import clsx from 'clsx';
|
||||
import classNames from 'clsx';
|
||||
import React, { useState } from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
|
||||
|
@ -12,9 +12,9 @@ const messages = defineMessages({
|
|||
|
||||
interface IAccountSearch {
|
||||
/** Callback when a searched account is chosen. */
|
||||
onSelected: (accountId: string) => void
|
||||
onSelected: (accountId: string) => void,
|
||||
/** Override the default placeholder of the input. */
|
||||
placeholder?: string
|
||||
placeholder?: string,
|
||||
}
|
||||
|
||||
/** Input to search for accounts. */
|
||||
|
@ -72,17 +72,17 @@ const AccountSearch: React.FC<IAccountSearch> = ({ onSelected, ...rest }) => {
|
|||
<div
|
||||
role='button'
|
||||
tabIndex={0}
|
||||
className='absolute inset-y-0 right-0 flex cursor-pointer items-center px-3'
|
||||
className='absolute inset-y-0 right-0 px-3 flex items-center cursor-pointer'
|
||||
onClick={handleClear}
|
||||
>
|
||||
<SvgIcon
|
||||
src={require('@tabler/icons/search.svg')}
|
||||
className={clsx('h-4 w-4 text-gray-400', { hidden: !isEmpty() })}
|
||||
className={classNames('h-4 w-4 text-gray-400', { hidden: !isEmpty() })}
|
||||
/>
|
||||
|
||||
<SvgIcon
|
||||
src={require('@tabler/icons/x.svg')}
|
||||
className={clsx('h-4 w-4 text-gray-400', { hidden: isEmpty() })}
|
||||
className={classNames('h-4 w-4 text-gray-400', { hidden: isEmpty() })}
|
||||
aria-label={intl.formatMessage(messages.placeholder)}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import React, { useRef } from 'react';
|
||||
import React from 'react';
|
||||
import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
|
||||
import { Link, useHistory } from 'react-router-dom';
|
||||
|
||||
import HoverRefWrapper from 'soapbox/components/hover-ref-wrapper';
|
||||
import VerificationBadge from 'soapbox/components/verification-badge';
|
||||
import ActionButton from 'soapbox/features/ui/components/action-button';
|
||||
import { useAppSelector } from 'soapbox/hooks';
|
||||
import { useAppSelector, useOnScreen } from 'soapbox/hooks';
|
||||
import { getAcct } from 'soapbox/utils/accounts';
|
||||
import { displayFqn } from 'soapbox/utils/state';
|
||||
|
||||
|
@ -14,12 +14,11 @@ import RelativeTimestamp from './relative-timestamp';
|
|||
import { Avatar, Emoji, HStack, Icon, IconButton, Stack, Text } from './ui';
|
||||
|
||||
import type { StatusApprovalStatus } from 'soapbox/normalizers/status';
|
||||
import type { Account as AccountSchema } from 'soapbox/schemas';
|
||||
import type { Account as AccountEntity } from 'soapbox/types/entities';
|
||||
|
||||
interface IInstanceFavicon {
|
||||
account: AccountEntity | AccountSchema
|
||||
disabled?: boolean
|
||||
account: AccountEntity,
|
||||
disabled?: boolean,
|
||||
}
|
||||
|
||||
const messages = defineMessages({
|
||||
|
@ -44,17 +43,17 @@ const InstanceFavicon: React.FC<IInstanceFavicon> = ({ account, disabled }) => {
|
|||
|
||||
return (
|
||||
<button
|
||||
className='h-4 w-4 flex-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2'
|
||||
className='w-4 h-4 flex-none focus:ring-primary-500 focus:ring-2 focus:ring-offset-2'
|
||||
onClick={handleClick}
|
||||
disabled={disabled}
|
||||
>
|
||||
<img src={account.favicon} alt='' title={account.domain} className='max-h-full w-full' />
|
||||
<img src={account.favicon} alt='' title={account.domain} className='w-full max-h-full' />
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
interface IProfilePopper {
|
||||
condition: boolean
|
||||
condition: boolean,
|
||||
wrapper: (children: React.ReactNode) => React.ReactNode
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
@ -68,31 +67,30 @@ const ProfilePopper: React.FC<IProfilePopper> = ({ condition, wrapper, children
|
|||
};
|
||||
|
||||
export interface IAccount {
|
||||
account: AccountEntity | AccountSchema
|
||||
action?: React.ReactElement
|
||||
actionAlignment?: 'center' | 'top'
|
||||
actionIcon?: string
|
||||
actionTitle?: string
|
||||
account: AccountEntity,
|
||||
action?: React.ReactElement,
|
||||
actionAlignment?: 'center' | 'top',
|
||||
actionIcon?: string,
|
||||
actionTitle?: string,
|
||||
/** Override other actions for specificity like mute/unmute. */
|
||||
actionType?: 'muting' | 'blocking' | 'follow_request'
|
||||
avatarSize?: number
|
||||
hidden?: boolean
|
||||
hideActions?: boolean
|
||||
id?: string
|
||||
onActionClick?: (account: any) => void
|
||||
showProfileHoverCard?: boolean
|
||||
timestamp?: string
|
||||
timestampUrl?: string
|
||||
futureTimestamp?: boolean
|
||||
withAccountNote?: boolean
|
||||
withDate?: boolean
|
||||
withLinkToProfile?: boolean
|
||||
withRelationship?: boolean
|
||||
showEdit?: boolean
|
||||
approvalStatus?: StatusApprovalStatus
|
||||
emoji?: string
|
||||
emojiUrl?: string
|
||||
note?: string
|
||||
actionType?: 'muting' | 'blocking' | 'follow_request',
|
||||
avatarSize?: number,
|
||||
hidden?: boolean,
|
||||
hideActions?: boolean,
|
||||
id?: string,
|
||||
onActionClick?: (account: any) => void,
|
||||
showProfileHoverCard?: boolean,
|
||||
timestamp?: string,
|
||||
timestampUrl?: string,
|
||||
futureTimestamp?: boolean,
|
||||
withAccountNote?: boolean,
|
||||
withDate?: boolean,
|
||||
withLinkToProfile?: boolean,
|
||||
withRelationship?: boolean,
|
||||
showEdit?: boolean,
|
||||
approvalStatus?: StatusApprovalStatus,
|
||||
emoji?: string,
|
||||
note?: string,
|
||||
}
|
||||
|
||||
const Account = ({
|
||||
|
@ -117,17 +115,21 @@ const Account = ({
|
|||
showEdit = false,
|
||||
approvalStatus,
|
||||
emoji,
|
||||
emojiUrl,
|
||||
note,
|
||||
}: IAccount) => {
|
||||
const overflowRef = useRef<HTMLDivElement>(null);
|
||||
const actionRef = useRef<HTMLDivElement>(null);
|
||||
const overflowRef = React.useRef<HTMLDivElement>(null);
|
||||
const actionRef = React.useRef<HTMLDivElement>(null);
|
||||
// @ts-ignore
|
||||
const isOnScreen = useOnScreen(overflowRef);
|
||||
|
||||
const [style, setStyle] = React.useState<React.CSSProperties>({ visibility: 'hidden' });
|
||||
|
||||
const me = useAppSelector((state) => state.me);
|
||||
const username = useAppSelector((state) => account ? getAcct(account, displayFqn(state)) : null);
|
||||
|
||||
const handleAction = () => {
|
||||
onActionClick!(account);
|
||||
// @ts-ignore
|
||||
onActionClick(account);
|
||||
};
|
||||
|
||||
const renderAction = () => {
|
||||
|
@ -145,8 +147,8 @@ const Account = ({
|
|||
src={actionIcon}
|
||||
title={actionTitle}
|
||||
onClick={handleAction}
|
||||
className='bg-transparent text-gray-600 hover:text-gray-700 dark:text-gray-600 dark:hover:text-gray-500'
|
||||
iconClassName='h-4 w-4'
|
||||
className='bg-transparent text-gray-600 dark:text-gray-600 hover:text-gray-700 dark:hover:text-gray-500'
|
||||
iconClassName='w-4 h-4'
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -160,6 +162,19 @@ const Account = ({
|
|||
|
||||
const intl = useIntl();
|
||||
|
||||
React.useEffect(() => {
|
||||
const style: React.CSSProperties = {};
|
||||
const actionWidth = actionRef.current?.clientWidth || 0;
|
||||
|
||||
if (overflowRef.current) {
|
||||
style.maxWidth = overflowRef.current.clientWidth - 30 - avatarSize - actionWidth;
|
||||
} else {
|
||||
style.visibility = 'hidden';
|
||||
}
|
||||
|
||||
setStyle(style);
|
||||
}, [isOnScreen, overflowRef, actionRef]);
|
||||
|
||||
if (!account) {
|
||||
return null;
|
||||
}
|
||||
|
@ -178,9 +193,9 @@ const Account = ({
|
|||
const LinkEl: any = withLinkToProfile ? Link : 'div';
|
||||
|
||||
return (
|
||||
<div data-testid='account' className='group block w-full shrink-0' ref={overflowRef}>
|
||||
<div data-testid='account' className='flex-shrink-0 group block w-full' ref={overflowRef}>
|
||||
<HStack alignItems={actionAlignment} justifyContent='between'>
|
||||
<HStack alignItems={withAccountNote || note ? 'top' : 'center'} space={3} className='overflow-hidden'>
|
||||
<HStack alignItems={withAccountNote || note ? 'top' : 'center'} space={3}>
|
||||
<ProfilePopper
|
||||
condition={showProfileHoverCard}
|
||||
wrapper={(children) => <HoverRefWrapper className='relative' accountId={account.id} inline>{children}</HoverRefWrapper>}
|
||||
|
@ -193,15 +208,14 @@ const Account = ({
|
|||
<Avatar src={account.avatar} size={avatarSize} />
|
||||
{emoji && (
|
||||
<Emoji
|
||||
className='absolute bottom-0 -right-1.5 h-5 w-5'
|
||||
className='w-5 h-5 absolute -bottom-1.5 -right-1.5'
|
||||
emoji={emoji}
|
||||
src={emojiUrl}
|
||||
/>
|
||||
)}
|
||||
</LinkEl>
|
||||
</ProfilePopper>
|
||||
|
||||
<div className='grow overflow-hidden'>
|
||||
<div className='flex-grow'>
|
||||
<ProfilePopper
|
||||
condition={showProfileHoverCard}
|
||||
wrapper={(children) => <HoverRefWrapper accountId={account.id} inline>{children}</HoverRefWrapper>}
|
||||
|
@ -211,7 +225,7 @@ const Account = ({
|
|||
title={account.acct}
|
||||
onClick={(event: React.MouseEvent) => event.stopPropagation()}
|
||||
>
|
||||
<HStack space={1} alignItems='center' grow>
|
||||
<HStack space={1} alignItems='center' grow style={style}>
|
||||
<Text
|
||||
size='sm'
|
||||
weight='semibold'
|
||||
|
@ -227,7 +241,7 @@ const Account = ({
|
|||
</ProfilePopper>
|
||||
|
||||
<Stack space={withAccountNote || note ? 1 : 0}>
|
||||
<HStack alignItems='center' space={1}>
|
||||
<HStack alignItems='center' space={1} style={style}>
|
||||
<Text theme='muted' size='sm' direction='ltr' truncate>@{username}</Text>
|
||||
|
||||
{account.favicon && (
|
||||
|
|
|
@ -15,8 +15,8 @@ const obfuscatedCount = (count: number) => {
|
|||
};
|
||||
|
||||
interface IAnimatedNumber {
|
||||
value: number
|
||||
obfuscate?: boolean
|
||||
value: number;
|
||||
obfuscate?: boolean;
|
||||
}
|
||||
|
||||
const AnimatedNumber: React.FC<IAnimatedNumber> = ({ value, obfuscate }) => {
|
||||
|
@ -50,7 +50,7 @@ const AnimatedNumber: React.FC<IAnimatedNumber> = ({ value, obfuscate }) => {
|
|||
return (
|
||||
<TransitionMotion styles={styles} willEnter={willEnter} willLeave={willLeave}>
|
||||
{items => (
|
||||
<span className='relative inline-flex flex-col items-stretch overflow-hidden'>
|
||||
<span className='inline-flex flex-col items-stretch relative overflow-hidden'>
|
||||
{items.map(({ key, data, style }) => (
|
||||
<span key={key} style={{ position: (direction * style.y) > 0 ? 'absolute' : 'static', transform: `translateY(${style.y * 100}%)` }}>{obfuscate ? obfuscatedCount(data) : <FormattedNumber value={data} />}</span>
|
||||
))}
|
||||
|
|
|
@ -4,7 +4,7 @@ import { useHistory } from 'react-router-dom';
|
|||
import type { Announcement as AnnouncementEntity, Mention as MentionEntity } from 'soapbox/types/entities';
|
||||
|
||||
interface IAnnouncementContent {
|
||||
announcement: AnnouncementEntity
|
||||
announcement: AnnouncementEntity;
|
||||
}
|
||||
|
||||
const AnnouncementContent: React.FC<IAnnouncementContent> = ({ announcement }) => {
|
||||
|
|
|
@ -11,10 +11,10 @@ import type { Map as ImmutableMap } from 'immutable';
|
|||
import type { Announcement as AnnouncementEntity } from 'soapbox/types/entities';
|
||||
|
||||
interface IAnnouncement {
|
||||
announcement: AnnouncementEntity
|
||||
addReaction: (id: string, name: string) => void
|
||||
removeReaction: (id: string, name: string) => void
|
||||
emojiMap: ImmutableMap<string, ImmutableMap<string, string>>
|
||||
announcement: AnnouncementEntity;
|
||||
addReaction: (id: string, name: string) => void;
|
||||
removeReaction: (id: string, name: string) => void;
|
||||
emojiMap: ImmutableMap<string, ImmutableMap<string, string>>;
|
||||
}
|
||||
|
||||
const Announcement: React.FC<IAnnouncement> = ({ announcement, addReaction, removeReaction, emojiMap }) => {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import clsx from 'clsx';
|
||||
import classNames from 'clsx';
|
||||
import { List as ImmutableList, Map as ImmutableMap } from 'immutable';
|
||||
import React, { useState } from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
@ -52,7 +52,7 @@ const AnnouncementsPanel = () => {
|
|||
key={i}
|
||||
tabIndex={0}
|
||||
onClick={() => setIndex(i)}
|
||||
className={clsx({
|
||||
className={classNames({
|
||||
'w-2 h-2 rounded-full focus:ring-primary-600 focus:ring-2 focus:ring-offset-2': true,
|
||||
'bg-gray-200 hover:bg-gray-300': i !== index,
|
||||
'bg-primary-600': i === index,
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
import React from 'react';
|
||||
|
||||
import unicodeMapping from 'soapbox/features/emoji/mapping';
|
||||
import unicodeMapping from 'soapbox/features/emoji/emoji-unicode-mapping-light';
|
||||
import { useSettings } from 'soapbox/hooks';
|
||||
import { joinPublicPath } from 'soapbox/utils/static';
|
||||
|
||||
import type { Map as ImmutableMap } from 'immutable';
|
||||
|
||||
interface IEmoji {
|
||||
emoji: string
|
||||
emojiMap: ImmutableMap<string, ImmutableMap<string, string>>
|
||||
hovered: boolean
|
||||
emoji: string;
|
||||
emojiMap: ImmutableMap<string, ImmutableMap<string, string>>;
|
||||
hovered: boolean;
|
||||
}
|
||||
|
||||
const Emoji: React.FC<IEmoji> = ({ emoji, emojiMap, hovered }) => {
|
||||
|
@ -24,7 +24,7 @@ const Emoji: React.FC<IEmoji> = ({ emoji, emojiMap, hovered }) => {
|
|||
return (
|
||||
<img
|
||||
draggable='false'
|
||||
className='emojione m-0 block'
|
||||
className='emojione block m-0'
|
||||
alt={emoji}
|
||||
title={title}
|
||||
src={joinPublicPath(`packs/emoji/${filename}.svg`)}
|
||||
|
@ -37,7 +37,7 @@ const Emoji: React.FC<IEmoji> = ({ emoji, emojiMap, hovered }) => {
|
|||
return (
|
||||
<img
|
||||
draggable='false'
|
||||
className='emojione m-0 block'
|
||||
className='emojione block m-0'
|
||||
alt={shortCode}
|
||||
title={shortCode}
|
||||
src={filename as string}
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import clsx from 'clsx';
|
||||
import classNames from 'clsx';
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import AnimatedNumber from 'soapbox/components/animated-number';
|
||||
import unicodeMapping from 'soapbox/features/emoji/mapping';
|
||||
import unicodeMapping from 'soapbox/features/emoji/emoji-unicode-mapping-light';
|
||||
|
||||
import Emoji from './emoji';
|
||||
|
||||
|
@ -10,12 +10,12 @@ import type { Map as ImmutableMap } from 'immutable';
|
|||
import type { AnnouncementReaction } from 'soapbox/types/entities';
|
||||
|
||||
interface IReaction {
|
||||
announcementId: string
|
||||
reaction: AnnouncementReaction
|
||||
emojiMap: ImmutableMap<string, ImmutableMap<string, string>>
|
||||
addReaction: (id: string, name: string) => void
|
||||
removeReaction: (id: string, name: string) => void
|
||||
style: React.CSSProperties
|
||||
announcementId: string;
|
||||
reaction: AnnouncementReaction;
|
||||
emojiMap: ImmutableMap<string, ImmutableMap<string, string>>;
|
||||
addReaction: (id: string, name: string) => void;
|
||||
removeReaction: (id: string, name: string) => void;
|
||||
style: React.CSSProperties;
|
||||
}
|
||||
|
||||
const Reaction: React.FC<IReaction> = ({ announcementId, reaction, addReaction, removeReaction, emojiMap, style }) => {
|
||||
|
@ -43,7 +43,7 @@ const Reaction: React.FC<IReaction> = ({ announcementId, reaction, addReaction,
|
|||
|
||||
return (
|
||||
<button
|
||||
className={clsx('flex shrink-0 items-center gap-1.5 rounded-sm bg-gray-100 px-1.5 py-1 transition-colors dark:bg-primary-900', {
|
||||
className={classNames('flex shrink-0 items-center gap-1.5 bg-gray-100 dark:bg-primary-900 rounded-sm px-1.5 py-1 transition-colors', {
|
||||
'bg-gray-200 dark:bg-primary-800': hovered,
|
||||
'bg-primary-200 dark:bg-primary-500': reaction.me,
|
||||
})}
|
||||
|
|
|
@ -1,29 +1,30 @@
|
|||
import clsx from 'clsx';
|
||||
import classNames from 'clsx';
|
||||
import React from 'react';
|
||||
import { TransitionMotion, spring } from 'react-motion';
|
||||
|
||||
import EmojiPickerDropdown from 'soapbox/features/emoji/containers/emoji-picker-dropdown-container';
|
||||
import { Icon } from 'soapbox/components/ui';
|
||||
import EmojiPickerDropdown from 'soapbox/features/compose/components/emoji-picker/emoji-picker-dropdown';
|
||||
import { useSettings } from 'soapbox/hooks';
|
||||
|
||||
import Reaction from './reaction';
|
||||
|
||||
import type { List as ImmutableList, Map as ImmutableMap } from 'immutable';
|
||||
import type { Emoji, NativeEmoji } from 'soapbox/features/emoji';
|
||||
import type { Emoji } from 'soapbox/components/autosuggest-emoji';
|
||||
import type { AnnouncementReaction } from 'soapbox/types/entities';
|
||||
|
||||
interface IReactionsBar {
|
||||
announcementId: string
|
||||
reactions: ImmutableList<AnnouncementReaction>
|
||||
emojiMap: ImmutableMap<string, ImmutableMap<string, string>>
|
||||
addReaction: (id: string, name: string) => void
|
||||
removeReaction: (id: string, name: string) => void
|
||||
announcementId: string;
|
||||
reactions: ImmutableList<AnnouncementReaction>;
|
||||
emojiMap: ImmutableMap<string, ImmutableMap<string, string>>;
|
||||
addReaction: (id: string, name: string) => void;
|
||||
removeReaction: (id: string, name: string) => void;
|
||||
}
|
||||
|
||||
const ReactionsBar: React.FC<IReactionsBar> = ({ announcementId, reactions, addReaction, removeReaction, emojiMap }) => {
|
||||
const reduceMotion = useSettings().get('reduceMotion');
|
||||
|
||||
const handleEmojiPick = (data: Emoji) => {
|
||||
addReaction(announcementId, (data as NativeEmoji).native.replace(/:/g, ''));
|
||||
addReaction(announcementId, data.native.replace(/:/g, ''));
|
||||
};
|
||||
|
||||
const willEnter = () => ({ scale: reduceMotion ? 1 : 0 });
|
||||
|
@ -41,7 +42,7 @@ const ReactionsBar: React.FC<IReactionsBar> = ({ announcementId, reactions, addR
|
|||
return (
|
||||
<TransitionMotion styles={styles} willEnter={willEnter} willLeave={willLeave}>
|
||||
{items => (
|
||||
<div className={clsx('flex flex-wrap items-center gap-1', { 'reactions-bar--empty': visibleReactions.isEmpty() })}>
|
||||
<div className={classNames('flex flex-wrap items-center gap-1', { 'reactions-bar--empty': visibleReactions.isEmpty() })}>
|
||||
{items.map(({ key, data, style }) => (
|
||||
<Reaction
|
||||
key={key}
|
||||
|
@ -54,7 +55,7 @@ const ReactionsBar: React.FC<IReactionsBar> = ({ announcementId, reactions, addR
|
|||
/>
|
||||
))}
|
||||
|
||||
{visibleReactions.size < 8 && <EmojiPickerDropdown onPickEmoji={handleEmojiPick} />}
|
||||
{visibleReactions.size < 8 && <EmojiPickerDropdown onPickEmoji={handleEmojiPick} button={<Icon className='h-4 w-4 text-gray-400 hover:text-gray-600 dark:hover:text-white' src={require('@tabler/icons/plus.svg')} />} />}
|
||||
</div>
|
||||
)}
|
||||
</TransitionMotion>
|
||||
|
|
|
@ -1,139 +0,0 @@
|
|||
import clsx from 'clsx';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import { HStack, IconButton, Text } from 'soapbox/components/ui';
|
||||
|
||||
interface IAuthorizeRejectButtons {
|
||||
onAuthorize(): Promise<unknown> | unknown
|
||||
onReject(): Promise<unknown> | unknown
|
||||
countdown?: number
|
||||
}
|
||||
|
||||
/** Buttons to approve or reject a pending item, usually an account. */
|
||||
const AuthorizeRejectButtons: React.FC<IAuthorizeRejectButtons> = ({ onAuthorize, onReject, countdown }) => {
|
||||
const [state, setState] = useState<'authorizing' | 'rejecting' | 'authorized' | 'rejected' | 'pending'>('pending');
|
||||
const timeout = useRef<NodeJS.Timeout>();
|
||||
|
||||
function handleAction(
|
||||
present: 'authorizing' | 'rejecting',
|
||||
past: 'authorized' | 'rejected',
|
||||
action: () => Promise<unknown> | unknown,
|
||||
): void {
|
||||
if (state === present) {
|
||||
if (timeout.current) {
|
||||
clearTimeout(timeout.current);
|
||||
}
|
||||
setState('pending');
|
||||
} else {
|
||||
const doAction = async () => {
|
||||
try {
|
||||
await action();
|
||||
setState(past);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
};
|
||||
if (typeof countdown === 'number') {
|
||||
setState(present);
|
||||
timeout.current = setTimeout(doAction, countdown);
|
||||
} else {
|
||||
doAction();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleAuthorize = async () => handleAction('authorizing', 'authorized', onAuthorize);
|
||||
const handleReject = async () => handleAction('rejecting', 'rejected', onReject);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (timeout.current) {
|
||||
clearTimeout(timeout.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
switch (state) {
|
||||
case 'authorized':
|
||||
return (
|
||||
<ActionEmblem text={<FormattedMessage id='authorize.success' defaultMessage='Approved' />} />
|
||||
);
|
||||
case 'rejected':
|
||||
return (
|
||||
<ActionEmblem text={<FormattedMessage id='reject.success' defaultMessage='Rejected' />} />
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<HStack space={3} alignItems='center'>
|
||||
<AuthorizeRejectButton
|
||||
theme='danger'
|
||||
icon={require('@tabler/icons/x.svg')}
|
||||
action={handleReject}
|
||||
isLoading={state === 'rejecting'}
|
||||
disabled={state === 'authorizing'}
|
||||
/>
|
||||
<AuthorizeRejectButton
|
||||
theme='primary'
|
||||
icon={require('@tabler/icons/check.svg')}
|
||||
action={handleAuthorize}
|
||||
isLoading={state === 'authorizing'}
|
||||
disabled={state === 'rejecting'}
|
||||
/>
|
||||
</HStack>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
interface IActionEmblem {
|
||||
text: React.ReactNode
|
||||
}
|
||||
|
||||
const ActionEmblem: React.FC<IActionEmblem> = ({ text }) => {
|
||||
return (
|
||||
<div className='rounded-full bg-gray-100 px-4 py-2 dark:bg-gray-800'>
|
||||
<Text theme='muted' size='sm'>
|
||||
{text}
|
||||
</Text>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface IAuthorizeRejectButton {
|
||||
theme: 'primary' | 'danger'
|
||||
icon: string
|
||||
action(): void
|
||||
isLoading?: boolean
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
const AuthorizeRejectButton: React.FC<IAuthorizeRejectButton> = ({ theme, icon, action, isLoading, disabled }) => {
|
||||
return (
|
||||
<div className='relative'>
|
||||
<IconButton
|
||||
src={isLoading ? require('@tabler/icons/player-stop-filled.svg') : icon}
|
||||
onClick={action}
|
||||
theme='seamless'
|
||||
className={clsx('h-10 w-10 items-center justify-center border-2', {
|
||||
'border-primary-500/10 hover:border-primary-500': theme === 'primary',
|
||||
'border-danger-600/10 hover:border-danger-600': theme === 'danger',
|
||||
})}
|
||||
iconClassName={clsx('h-6 w-6', {
|
||||
'text-primary-500': theme === 'primary',
|
||||
'text-danger-600': theme === 'danger',
|
||||
})}
|
||||
disabled={disabled}
|
||||
/>
|
||||
{(isLoading) && (
|
||||
<div
|
||||
className={clsx('pointer-events-none absolute inset-0 h-10 w-10 animate-spin rounded-full border-2 border-transparent', {
|
||||
'border-t-primary-500': theme === 'primary',
|
||||
'border-t-danger-600': theme === 'danger',
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export { AuthorizeRejectButtons };
|
|
@ -12,16 +12,16 @@ import type { InputThemes } from 'soapbox/components/ui/input/input';
|
|||
const noOp = () => { };
|
||||
|
||||
interface IAutosuggestAccountInput {
|
||||
onChange: React.ChangeEventHandler<HTMLInputElement>
|
||||
onSelected: (accountId: string) => void
|
||||
autoFocus?: boolean
|
||||
value: string
|
||||
limit?: number
|
||||
className?: string
|
||||
autoSelect?: boolean
|
||||
menu?: Menu
|
||||
onKeyDown?: React.KeyboardEventHandler
|
||||
theme?: InputThemes
|
||||
onChange: React.ChangeEventHandler<HTMLInputElement>,
|
||||
onSelected: (accountId: string) => void,
|
||||
autoFocus?: boolean,
|
||||
value: string,
|
||||
limit?: number,
|
||||
className?: string,
|
||||
autoSelect?: boolean,
|
||||
menu?: Menu,
|
||||
onKeyDown?: React.KeyboardEventHandler,
|
||||
theme?: InputThemes,
|
||||
}
|
||||
|
||||
const AutosuggestAccountInput: React.FC<IAutosuggestAccountInput> = ({
|
||||
|
|
|
@ -1,30 +1,38 @@
|
|||
import React from 'react';
|
||||
|
||||
import { isCustomEmoji } from 'soapbox/features/emoji';
|
||||
import unicodeMapping from 'soapbox/features/emoji/mapping';
|
||||
import unicodeMapping from 'soapbox/features/emoji/emoji-unicode-mapping-light';
|
||||
import { joinPublicPath } from 'soapbox/utils/static';
|
||||
|
||||
import type { Emoji } from 'soapbox/features/emoji';
|
||||
export type Emoji = {
|
||||
id: string,
|
||||
custom: boolean,
|
||||
imageUrl: string,
|
||||
native: string,
|
||||
colons: string,
|
||||
}
|
||||
|
||||
type UnicodeMapping = {
|
||||
filename: string,
|
||||
}
|
||||
|
||||
interface IAutosuggestEmoji {
|
||||
emoji: Emoji
|
||||
emoji: Emoji,
|
||||
}
|
||||
|
||||
const AutosuggestEmoji: React.FC<IAutosuggestEmoji> = ({ emoji }) => {
|
||||
let url, alt;
|
||||
let url;
|
||||
|
||||
if (isCustomEmoji(emoji)) {
|
||||
if (emoji.custom) {
|
||||
url = emoji.imageUrl;
|
||||
alt = emoji.colons;
|
||||
} else {
|
||||
const mapping = unicodeMapping[emoji.native] || unicodeMapping[emoji.native.replace(/\uFE0F$/, '')];
|
||||
// @ts-ignore
|
||||
const mapping: UnicodeMapping = unicodeMapping[emoji.native] || unicodeMapping[emoji.native.replace(/\uFE0F$/, '')];
|
||||
|
||||
if (!mapping) {
|
||||
return null;
|
||||
}
|
||||
|
||||
url = joinPublicPath(`packs/emoji/${mapping.unified}.svg`);
|
||||
alt = emoji.native;
|
||||
url = joinPublicPath(`packs/emoji/${mapping.filename}.svg`);
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -32,7 +40,7 @@ const AutosuggestEmoji: React.FC<IAutosuggestEmoji> = ({ emoji }) => {
|
|||
<img
|
||||
className='emojione'
|
||||
src={url}
|
||||
alt={alt}
|
||||
alt={emoji.native || emoji.colons}
|
||||
/>
|
||||
|
||||
{emoji.colons}
|
||||
|
|
|
@ -1,39 +1,39 @@
|
|||
import clsx from 'clsx';
|
||||
import { Portal } from '@reach/portal';
|
||||
import classNames from 'clsx';
|
||||
import { List as ImmutableList } from 'immutable';
|
||||
import React from 'react';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
|
||||
import AutosuggestEmoji from 'soapbox/components/autosuggest-emoji';
|
||||
import AutosuggestEmoji, { Emoji } from 'soapbox/components/autosuggest-emoji';
|
||||
import Icon from 'soapbox/components/icon';
|
||||
import { Input, Portal } from 'soapbox/components/ui';
|
||||
import { Input } from 'soapbox/components/ui';
|
||||
import AutosuggestAccount from 'soapbox/features/compose/components/autosuggest-account';
|
||||
import { isRtl } from 'soapbox/rtl';
|
||||
import { textAtCursorMatchesToken } from 'soapbox/utils/suggestions';
|
||||
|
||||
import type { Menu, MenuItem } from 'soapbox/components/dropdown-menu';
|
||||
import type { InputThemes } from 'soapbox/components/ui/input/input';
|
||||
import type { Emoji } from 'soapbox/features/emoji';
|
||||
|
||||
export type AutoSuggestion = string | Emoji;
|
||||
|
||||
export interface IAutosuggestInput extends Pick<React.HTMLAttributes<HTMLInputElement>, 'onChange' | 'onKeyUp' | 'onKeyDown'> {
|
||||
value: string
|
||||
suggestions: ImmutableList<any>
|
||||
disabled?: boolean
|
||||
placeholder?: string
|
||||
onSuggestionSelected: (tokenStart: number, lastToken: string | null, suggestion: AutoSuggestion) => void
|
||||
onSuggestionsClearRequested: () => void
|
||||
onSuggestionsFetchRequested: (token: string) => void
|
||||
autoFocus: boolean
|
||||
autoSelect: boolean
|
||||
className?: string
|
||||
id?: string
|
||||
searchTokens: string[]
|
||||
maxLength?: number
|
||||
menu?: Menu
|
||||
renderSuggestion?: React.FC<{ id: string }>
|
||||
hidePortal?: boolean
|
||||
theme?: InputThemes
|
||||
value: string,
|
||||
suggestions: ImmutableList<any>,
|
||||
disabled?: boolean,
|
||||
placeholder?: string,
|
||||
onSuggestionSelected: (tokenStart: number, lastToken: string | null, suggestion: AutoSuggestion) => void,
|
||||
onSuggestionsClearRequested: () => void,
|
||||
onSuggestionsFetchRequested: (token: string) => void,
|
||||
autoFocus: boolean,
|
||||
autoSelect: boolean,
|
||||
className?: string,
|
||||
id?: string,
|
||||
searchTokens: string[],
|
||||
maxLength?: number,
|
||||
menu?: Menu,
|
||||
renderSuggestion?: React.FC<{ id: string }>,
|
||||
hidePortal?: boolean,
|
||||
theme?: InputThemes,
|
||||
}
|
||||
|
||||
export default class AutosuggestInput extends ImmutablePureComponent<IAutosuggestInput> {
|
||||
|
@ -199,7 +199,7 @@ export default class AutosuggestInput extends ImmutablePureComponent<IAutosugges
|
|||
tabIndex={0}
|
||||
key={key}
|
||||
data-index={i}
|
||||
className={clsx({
|
||||
className={classNames({
|
||||
'px-4 py-2.5 text-sm text-gray-700 dark:text-gray-500 hover:bg-gray-100 dark:hover:bg-gray-800 focus:bg-gray-100 dark:focus:bg-primary-800 group': true,
|
||||
'bg-gray-100 dark:bg-gray-800 hover:bg-gray-100 dark:hover:bg-gray-800': i === selectedSuggestion,
|
||||
})}
|
||||
|
@ -235,7 +235,7 @@ export default class AutosuggestInput extends ImmutablePureComponent<IAutosugges
|
|||
|
||||
return menu.map((item, i) => (
|
||||
<a
|
||||
className={clsx('flex cursor-pointer items-center space-x-2 px-4 py-2.5 text-sm text-gray-700 hover:bg-gray-100 focus:bg-gray-100 dark:text-gray-500 dark:hover:bg-gray-800 dark:focus:bg-primary-800', { selected: suggestions.size - selectedSuggestion === i })}
|
||||
className={classNames('flex items-center space-x-2 px-4 py-2.5 text-sm cursor-pointer text-gray-700 dark:text-gray-500 hover:bg-gray-100 dark:hover:bg-gray-800 focus:bg-gray-100 dark:focus:bg-primary-800', { selected: suggestions.size - selectedSuggestion === i })}
|
||||
href='#'
|
||||
role='button'
|
||||
tabIndex={0}
|
||||
|
@ -302,7 +302,7 @@ export default class AutosuggestInput extends ImmutablePureComponent<IAutosugges
|
|||
<Portal key='portal'>
|
||||
<div
|
||||
style={this.setPortalPosition()}
|
||||
className={clsx({
|
||||
className={classNames({
|
||||
'fixed w-full z-[1001] shadow bg-white dark:bg-gray-900 rounded-lg py-1 dark:ring-2 dark:ring-primary-700 focus:outline-none': true,
|
||||
hidden: !visible,
|
||||
block: visible,
|
||||
|
|
|
@ -19,7 +19,7 @@ export const ADDRESS_ICONS: Record<string, string> = {
|
|||
};
|
||||
|
||||
interface IAutosuggestLocation {
|
||||
id: string
|
||||
id: string,
|
||||
}
|
||||
|
||||
const AutosuggestLocation: React.FC<IAutosuggestLocation> = ({ id }) => {
|
||||
|
|
|
@ -1,36 +1,36 @@
|
|||
import clsx from 'clsx';
|
||||
import { Portal } from '@reach/portal';
|
||||
import classNames from 'clsx';
|
||||
import React from 'react';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import Textarea from 'react-textarea-autosize';
|
||||
|
||||
import { Portal } from 'soapbox/components/ui';
|
||||
import AutosuggestAccount from 'soapbox/features/compose/components/autosuggest-account';
|
||||
import { isRtl } from 'soapbox/rtl';
|
||||
import { textAtCursorMatchesToken } from 'soapbox/utils/suggestions';
|
||||
|
||||
import AutosuggestEmoji from './autosuggest-emoji';
|
||||
import AutosuggestAccount from '../features/compose/components/autosuggest-account';
|
||||
import { isRtl } from '../rtl';
|
||||
|
||||
import AutosuggestEmoji, { Emoji } from './autosuggest-emoji';
|
||||
|
||||
import type { List as ImmutableList } from 'immutable';
|
||||
import type { Emoji } from 'soapbox/features/emoji';
|
||||
|
||||
interface IAutosuggesteTextarea {
|
||||
id?: string
|
||||
value: string
|
||||
suggestions: ImmutableList<string>
|
||||
disabled: boolean
|
||||
placeholder: string
|
||||
onSuggestionSelected: (tokenStart: number, token: string | null, value: string | undefined) => void
|
||||
onSuggestionsClearRequested: () => void
|
||||
onSuggestionsFetchRequested: (token: string | number) => void
|
||||
onChange: React.ChangeEventHandler<HTMLTextAreaElement>
|
||||
onKeyUp?: React.KeyboardEventHandler<HTMLTextAreaElement>
|
||||
onKeyDown?: React.KeyboardEventHandler<HTMLTextAreaElement>
|
||||
onPaste: (files: FileList) => void
|
||||
autoFocus: boolean
|
||||
onFocus: () => void
|
||||
onBlur?: () => void
|
||||
condensed?: boolean
|
||||
children: React.ReactNode
|
||||
id?: string,
|
||||
value: string,
|
||||
suggestions: ImmutableList<string>,
|
||||
disabled: boolean,
|
||||
placeholder: string,
|
||||
onSuggestionSelected: (tokenStart: number, token: string | null, value: string | undefined) => void,
|
||||
onSuggestionsClearRequested: () => void,
|
||||
onSuggestionsFetchRequested: (token: string | number) => void,
|
||||
onChange: React.ChangeEventHandler<HTMLTextAreaElement>,
|
||||
onKeyUp?: React.KeyboardEventHandler<HTMLTextAreaElement>,
|
||||
onKeyDown?: React.KeyboardEventHandler<HTMLTextAreaElement>,
|
||||
onPaste: (files: FileList) => void,
|
||||
autoFocus: boolean,
|
||||
onFocus: () => void,
|
||||
onBlur?: () => void,
|
||||
condensed?: boolean,
|
||||
children: React.ReactNode,
|
||||
}
|
||||
|
||||
class AutosuggestTextarea extends ImmutablePureComponent<IAutosuggesteTextarea> {
|
||||
|
@ -201,7 +201,7 @@ class AutosuggestTextarea extends ImmutablePureComponent<IAutosuggesteTextarea>
|
|||
tabIndex={0}
|
||||
key={key}
|
||||
data-index={i}
|
||||
className={clsx({
|
||||
className={classNames({
|
||||
'px-4 py-2.5 text-sm text-gray-700 dark:text-gray-500 hover:bg-gray-100 dark:hover:bg-gray-800 focus:bg-gray-100 dark:focus:bg-primary-800 group': true,
|
||||
'bg-gray-100 dark:bg-gray-800 hover:bg-gray-100 dark:hover:bg-gray-800': i === selectedSuggestion,
|
||||
})}
|
||||
|
@ -244,7 +244,7 @@ class AutosuggestTextarea extends ImmutablePureComponent<IAutosuggesteTextarea>
|
|||
|
||||
<Textarea
|
||||
ref={this.setTextarea}
|
||||
className={clsx('w-full resize-none border-0 px-0 text-gray-800 transition-[min-height] placeholder:text-gray-600 focus:border-0 focus:shadow-none focus:ring-0 motion-reduce:transition-none dark:bg-transparent dark:text-white dark:placeholder:text-gray-600', {
|
||||
className={classNames('transition-[min-height] motion-reduce:transition-none dark:bg-transparent px-0 border-0 text-gray-800 dark:text-white placeholder:text-gray-600 dark:placeholder:text-gray-600 resize-none w-full focus:shadow-none focus:border-0 focus:ring-0', {
|
||||
'min-h-[40px]': condensed,
|
||||
'min-h-[100px]': !condensed,
|
||||
})}
|
||||
|
@ -271,7 +271,7 @@ class AutosuggestTextarea extends ImmutablePureComponent<IAutosuggesteTextarea>
|
|||
<Portal key='portal'>
|
||||
<div
|
||||
style={this.setPortalPosition()}
|
||||
className={clsx({
|
||||
className={classNames({
|
||||
'fixed z-1000 shadow bg-white dark:bg-gray-900 rounded-lg py-1 space-y-0 dark:ring-2 dark:ring-primary-700 focus:outline-none': true,
|
||||
hidden: suggestionsHidden || suggestions.isEmpty(),
|
||||
block: !suggestionsHidden && !suggestions.isEmpty(),
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import clsx from 'clsx';
|
||||
import classNames from 'clsx';
|
||||
import React from 'react';
|
||||
|
||||
interface IBadge {
|
||||
title: React.ReactNode
|
||||
slug: string
|
||||
title: React.ReactNode,
|
||||
slug: string,
|
||||
}
|
||||
/** Badge to display on a user's profile. */
|
||||
const Badge: React.FC<IBadge> = ({ title, slug }) => {
|
||||
|
@ -12,13 +12,13 @@ const Badge: React.FC<IBadge> = ({ title, slug }) => {
|
|||
return (
|
||||
<span
|
||||
data-testid='badge'
|
||||
className={clsx('inline-flex items-center rounded px-2 py-0.5 text-xs font-medium', {
|
||||
className={classNames('inline-flex items-center px-2 py-0.5 rounded text-xs font-medium', {
|
||||
'bg-fuchsia-700 text-white': slug === 'patron',
|
||||
'bg-emerald-800 text-white': slug === 'badge:donor',
|
||||
'bg-black text-white': slug === 'admin',
|
||||
'bg-cyan-600 text-white': slug === 'moderator',
|
||||
'bg-gray-100 dark:bg-gray-800 text-gray-900 dark:text-gray-100': fallback,
|
||||
'bg-white/75 text-gray-900': slug === 'opaque',
|
||||
'bg-white bg-opacity-75 text-gray-900': slug === 'opaque',
|
||||
})}
|
||||
>
|
||||
{title}
|
||||
|
|
|
@ -15,9 +15,9 @@ const messages = defineMessages({
|
|||
});
|
||||
|
||||
interface IBirthdayInput {
|
||||
value?: string
|
||||
onChange: (value: string) => void
|
||||
required?: boolean
|
||||
value?: string,
|
||||
onChange: (value: string) => void,
|
||||
required?: boolean,
|
||||
}
|
||||
|
||||
const BirthdayInput: React.FC<IBirthdayInput> = ({ value, onChange, required }) => {
|
||||
|
@ -56,15 +56,15 @@ const BirthdayInput: React.FC<IBirthdayInput> = ({ value, onChange, required })
|
|||
nextYearButtonDisabled,
|
||||
date,
|
||||
}: {
|
||||
decreaseMonth(): void
|
||||
increaseMonth(): void
|
||||
prevMonthButtonDisabled: boolean
|
||||
nextMonthButtonDisabled: boolean
|
||||
decreaseYear(): void
|
||||
increaseYear(): void
|
||||
prevYearButtonDisabled: boolean
|
||||
nextYearButtonDisabled: boolean
|
||||
date: Date
|
||||
decreaseMonth(): void,
|
||||
increaseMonth(): void,
|
||||
prevMonthButtonDisabled: boolean,
|
||||
nextMonthButtonDisabled: boolean,
|
||||
decreaseYear(): void,
|
||||
increaseYear(): void,
|
||||
prevYearButtonDisabled: boolean,
|
||||
nextYearButtonDisabled: boolean,
|
||||
date: Date,
|
||||
}) => {
|
||||
return (
|
||||
<div className='flex flex-col gap-2'>
|
||||
|
@ -113,7 +113,7 @@ const BirthdayInput: React.FC<IBirthdayInput> = ({ value, onChange, required })
|
|||
const handleChange = (date: Date) => onChange(date ? new Date(date.getTime() - (date.getTimezoneOffset() * 60000)).toISOString().slice(0, 10) : '');
|
||||
|
||||
return (
|
||||
<div className='relative mt-1 rounded-md shadow-sm'>
|
||||
<div className='mt-1 relative rounded-md shadow-sm'>
|
||||
<BundleContainer fetchComponent={DatePicker}>
|
||||
{Component => (<Component
|
||||
selected={selected}
|
||||
|
|
|
@ -3,18 +3,18 @@ import React, { useRef, useEffect } from 'react';
|
|||
|
||||
interface IBlurhash {
|
||||
/** Hash to render */
|
||||
hash: string | null | undefined
|
||||
hash: string | null | undefined,
|
||||
/** Width of the blurred region in pixels. Defaults to 32. */
|
||||
width?: number
|
||||
width?: number,
|
||||
/** Height of the blurred region in pixels. Defaults to width. */
|
||||
height?: number
|
||||
height?: number,
|
||||
/**
|
||||
* Whether dummy mode is enabled. If enabled, nothing is rendered
|
||||
* and canvas left untouched.
|
||||
*/
|
||||
dummy?: boolean
|
||||
dummy?: boolean,
|
||||
/** className of the canvas element. */
|
||||
className?: string
|
||||
className?: string,
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -5,7 +5,7 @@ import { Button, HStack, Input } from './ui';
|
|||
|
||||
interface ICopyableInput {
|
||||
/** Text to be copied. */
|
||||
value: string
|
||||
value: string,
|
||||
}
|
||||
|
||||
/** An input with copy abilities. */
|
||||
|
@ -29,7 +29,7 @@ const CopyableInput: React.FC<ICopyableInput> = ({ value }) => {
|
|||
type='text'
|
||||
value={value}
|
||||
className='rounded-r-none rtl:rounded-l-none rtl:rounded-r-lg'
|
||||
outerClassName='grow'
|
||||
outerClassName='flex-grow'
|
||||
onClick={selectInput}
|
||||
readOnly
|
||||
/>
|
||||
|
|
|
@ -5,6 +5,8 @@ import { useSoapboxConfig } from 'soapbox/hooks';
|
|||
|
||||
import { getAcct } from '../utils/accounts';
|
||||
|
||||
import Icon from './icon';
|
||||
import RelativeTimestamp from './relative-timestamp';
|
||||
import { HStack, Text } from './ui';
|
||||
import VerificationBadge from './verification-badge';
|
||||
|
||||
|
@ -13,12 +15,20 @@ import type { Account } from 'soapbox/types/entities';
|
|||
interface IDisplayName {
|
||||
account: Account
|
||||
withSuffix?: boolean
|
||||
withDate?: boolean
|
||||
children?: React.ReactNode
|
||||
}
|
||||
|
||||
const DisplayName: React.FC<IDisplayName> = ({ account, children, withSuffix = true }) => {
|
||||
const DisplayName: React.FC<IDisplayName> = ({ account, children, withSuffix = true, withDate = false }) => {
|
||||
const { displayFqn = false } = useSoapboxConfig();
|
||||
const { verified } = account;
|
||||
const { created_at: createdAt, verified } = account;
|
||||
|
||||
const joinedAt = createdAt ? (
|
||||
<div className='account__joined-at'>
|
||||
<Icon src={require('@tabler/icons/clock.svg')} />
|
||||
<RelativeTimestamp timestamp={createdAt} />
|
||||
</div>
|
||||
) : null;
|
||||
|
||||
const displayName = (
|
||||
<HStack space={1} alignItems='center' grow>
|
||||
|
@ -30,6 +40,7 @@ const DisplayName: React.FC<IDisplayName> = ({ account, children, withSuffix = t
|
|||
/>
|
||||
|
||||
{verified && <VerificationBadge />}
|
||||
{withDate && joinedAt}
|
||||
</HStack>
|
||||
);
|
||||
|
||||
|
|
|
@ -12,7 +12,7 @@ const messages = defineMessages({
|
|||
});
|
||||
|
||||
interface IDomain {
|
||||
domain: string
|
||||
domain: string,
|
||||
}
|
||||
|
||||
const Domain: React.FC<IDomain> = ({ domain }) => {
|
||||
|
|
|
@ -0,0 +1,420 @@
|
|||
import classNames from 'clsx';
|
||||
import { supportsPassiveEvents } from 'detect-passive-events';
|
||||
import React from 'react';
|
||||
import { spring } from 'react-motion';
|
||||
// @ts-ignore: TODO: upgrade react-overlays. v3.1 and above have TS definitions
|
||||
import Overlay from 'react-overlays/lib/Overlay';
|
||||
import { withRouter, RouteComponentProps } from 'react-router-dom';
|
||||
|
||||
import { Counter, IconButton } from 'soapbox/components/ui';
|
||||
import SvgIcon from 'soapbox/components/ui/icon/svg-icon';
|
||||
import Motion from 'soapbox/features/ui/util/optional-motion';
|
||||
|
||||
import type { Status } from 'soapbox/types/entities';
|
||||
|
||||
const listenerOptions = supportsPassiveEvents ? { passive: true } : false;
|
||||
let id = 0;
|
||||
|
||||
export interface MenuItem {
|
||||
action?: React.EventHandler<React.KeyboardEvent | React.MouseEvent>,
|
||||
middleClick?: React.EventHandler<React.MouseEvent>,
|
||||
text: string,
|
||||
href?: string,
|
||||
to?: string,
|
||||
newTab?: boolean,
|
||||
isLogout?: boolean,
|
||||
icon?: string,
|
||||
count?: number,
|
||||
destructive?: boolean,
|
||||
meta?: string,
|
||||
active?: boolean,
|
||||
}
|
||||
|
||||
export type Menu = Array<MenuItem | null>;
|
||||
|
||||
interface IDropdownMenu extends RouteComponentProps {
|
||||
items: Menu,
|
||||
onClose: () => void,
|
||||
style?: React.CSSProperties,
|
||||
placement?: DropdownPlacement,
|
||||
arrowOffsetLeft?: string,
|
||||
arrowOffsetTop?: string,
|
||||
openedViaKeyboard: boolean,
|
||||
}
|
||||
|
||||
interface IDropdownMenuState {
|
||||
mounted: boolean,
|
||||
}
|
||||
|
||||
class DropdownMenu extends React.PureComponent<IDropdownMenu, IDropdownMenuState> {
|
||||
|
||||
static defaultProps: Partial<IDropdownMenu> = {
|
||||
style: {},
|
||||
placement: 'bottom',
|
||||
};
|
||||
|
||||
state = {
|
||||
mounted: false,
|
||||
};
|
||||
|
||||
node: HTMLDivElement | null = null;
|
||||
focusedItem: HTMLAnchorElement | null = null;
|
||||
|
||||
handleDocumentClick = (e: Event) => {
|
||||
if (this.node && !this.node.contains(e.target as Node)) {
|
||||
this.props.onClose();
|
||||
}
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
document.addEventListener('click', this.handleDocumentClick, false);
|
||||
document.addEventListener('keydown', this.handleKeyDown, false);
|
||||
document.addEventListener('touchend', this.handleDocumentClick, listenerOptions);
|
||||
if (this.focusedItem && this.props.openedViaKeyboard) {
|
||||
this.focusedItem.focus({ preventScroll: true });
|
||||
}
|
||||
this.setState({ mounted: true });
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
document.removeEventListener('click', this.handleDocumentClick);
|
||||
document.removeEventListener('keydown', this.handleKeyDown);
|
||||
document.removeEventListener('touchend', this.handleDocumentClick);
|
||||
}
|
||||
|
||||
setRef: React.RefCallback<HTMLDivElement> = c => {
|
||||
this.node = c;
|
||||
};
|
||||
|
||||
setFocusRef: React.RefCallback<HTMLAnchorElement> = c => {
|
||||
this.focusedItem = c;
|
||||
};
|
||||
|
||||
handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (!this.node) return;
|
||||
|
||||
const items = Array.from(this.node.getElementsByTagName('a'));
|
||||
const index = items.indexOf(document.activeElement as any);
|
||||
let element = null;
|
||||
|
||||
switch (e.key) {
|
||||
case 'ArrowDown':
|
||||
element = items[index + 1] || items[0];
|
||||
break;
|
||||
case 'ArrowUp':
|
||||
element = items[index - 1] || items[items.length - 1];
|
||||
break;
|
||||
case 'Tab':
|
||||
if (e.shiftKey) {
|
||||
element = items[index - 1] || items[items.length - 1];
|
||||
} else {
|
||||
element = items[index + 1] || items[0];
|
||||
}
|
||||
break;
|
||||
case 'Home':
|
||||
element = items[0];
|
||||
break;
|
||||
case 'End':
|
||||
element = items[items.length - 1];
|
||||
break;
|
||||
case 'Escape':
|
||||
this.props.onClose();
|
||||
break;
|
||||
}
|
||||
|
||||
if (element) {
|
||||
element.focus();
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
};
|
||||
|
||||
handleItemKeyPress: React.EventHandler<React.KeyboardEvent> = e => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
this.handleClick(e);
|
||||
}
|
||||
};
|
||||
|
||||
handleClick: React.EventHandler<React.MouseEvent | React.KeyboardEvent> = e => {
|
||||
const i = Number(e.currentTarget.getAttribute('data-index'));
|
||||
const item = this.props.items[i];
|
||||
if (!item) return;
|
||||
const { action, to } = item;
|
||||
|
||||
this.props.onClose();
|
||||
|
||||
e.stopPropagation();
|
||||
|
||||
if (to) {
|
||||
e.preventDefault();
|
||||
this.props.history.push(to);
|
||||
} else if (typeof action === 'function') {
|
||||
e.preventDefault();
|
||||
action(e);
|
||||
}
|
||||
};
|
||||
|
||||
handleMiddleClick: React.EventHandler<React.MouseEvent> = e => {
|
||||
const i = Number(e.currentTarget.getAttribute('data-index'));
|
||||
const item = this.props.items[i];
|
||||
if (!item) return;
|
||||
const { middleClick } = item;
|
||||
|
||||
this.props.onClose();
|
||||
|
||||
if (e.button === 1 && typeof middleClick === 'function') {
|
||||
e.preventDefault();
|
||||
middleClick(e);
|
||||
}
|
||||
};
|
||||
|
||||
handleAuxClick: React.EventHandler<React.MouseEvent> = e => {
|
||||
if (e.button === 1) {
|
||||
this.handleMiddleClick(e);
|
||||
}
|
||||
};
|
||||
|
||||
renderItem(option: MenuItem | null, i: number): JSX.Element {
|
||||
if (option === null) {
|
||||
return <li key={`sep-${i}`} className='dropdown-menu__separator' />;
|
||||
}
|
||||
|
||||
const { text, href, to, newTab, isLogout, icon, count, destructive } = option;
|
||||
|
||||
return (
|
||||
<li className={classNames('dropdown-menu__item truncate', { destructive })} key={`${text}-${i}`}>
|
||||
<a
|
||||
href={href || to || '#'}
|
||||
role='button'
|
||||
tabIndex={0}
|
||||
ref={i === 0 ? this.setFocusRef : null}
|
||||
onClick={this.handleClick}
|
||||
onAuxClick={this.handleAuxClick}
|
||||
onKeyPress={this.handleItemKeyPress}
|
||||
data-index={i}
|
||||
target={newTab ? '_blank' : undefined}
|
||||
data-method={isLogout ? 'delete' : undefined}
|
||||
title={text}
|
||||
>
|
||||
{icon && <SvgIcon src={icon} className='mr-3 rtl:ml-3 rtl:mr-0 h-5 w-5 flex-none' />}
|
||||
|
||||
<span className='truncate'>{text}</span>
|
||||
|
||||
{count ? (
|
||||
<span className='ml-auto h-5 w-5 flex-none'>
|
||||
<Counter count={count} />
|
||||
</span>
|
||||
) : null}
|
||||
</a>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { items, style, placement, arrowOffsetLeft, arrowOffsetTop } = this.props;
|
||||
const { mounted } = this.state;
|
||||
return (
|
||||
<Motion defaultStyle={{ opacity: 0, scaleX: 1, scaleY: 1 }} style={{ opacity: spring(1, { damping: 35, stiffness: 400 }), scaleX: spring(1, { damping: 35, stiffness: 400 }), scaleY: spring(1, { damping: 35, stiffness: 400 }) }}>
|
||||
{({ opacity, scaleX, scaleY }) => (
|
||||
// It should not be transformed when mounting because the resulting
|
||||
// size will be used to determine the coordinate of the menu by
|
||||
// react-overlays
|
||||
<div
|
||||
className={`dropdown-menu ${placement}`}
|
||||
style={{ ...style, opacity: opacity, transform: mounted ? `scale(${scaleX}, ${scaleY})` : undefined }}
|
||||
ref={this.setRef}
|
||||
data-testid='dropdown-menu'
|
||||
>
|
||||
<div className={`dropdown-menu__arrow ${placement}`} style={{ left: arrowOffsetLeft, top: arrowOffsetTop }} />
|
||||
<ul>
|
||||
{items.map((option, i) => this.renderItem(option, i))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</Motion>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const RouterDropdownMenu = withRouter(DropdownMenu);
|
||||
|
||||
export interface IDropdown extends RouteComponentProps {
|
||||
icon?: string,
|
||||
src?: string,
|
||||
items: Menu,
|
||||
size?: number,
|
||||
active?: boolean,
|
||||
pressed?: boolean,
|
||||
title?: string,
|
||||
disabled?: boolean,
|
||||
status?: Status,
|
||||
isUserTouching?: () => boolean,
|
||||
isModalOpen?: boolean,
|
||||
onOpen?: (
|
||||
id: number,
|
||||
onItemClick: React.EventHandler<React.MouseEvent | React.KeyboardEvent>,
|
||||
dropdownPlacement: DropdownPlacement,
|
||||
keyboard: boolean,
|
||||
) => void,
|
||||
onClose?: (id: number) => void,
|
||||
dropdownPlacement?: string,
|
||||
openDropdownId?: number | null,
|
||||
openedViaKeyboard?: boolean,
|
||||
text?: string,
|
||||
onShiftClick?: React.EventHandler<React.MouseEvent | React.KeyboardEvent>,
|
||||
children?: JSX.Element,
|
||||
dropdownMenuStyle?: React.CSSProperties,
|
||||
}
|
||||
|
||||
interface IDropdownState {
|
||||
id: number,
|
||||
open: boolean,
|
||||
}
|
||||
|
||||
export type DropdownPlacement = 'top' | 'bottom';
|
||||
|
||||
class Dropdown extends React.PureComponent<IDropdown, IDropdownState> {
|
||||
|
||||
static defaultProps: Partial<IDropdown> = {
|
||||
title: 'Menu',
|
||||
};
|
||||
|
||||
state = {
|
||||
id: id++,
|
||||
open: false,
|
||||
};
|
||||
|
||||
target: HTMLButtonElement | null = null;
|
||||
activeElement: Element | null = null;
|
||||
|
||||
handleClick: React.EventHandler<React.MouseEvent<HTMLButtonElement> | React.KeyboardEvent<HTMLButtonElement>> = e => {
|
||||
const { onOpen, onShiftClick, openDropdownId } = this.props;
|
||||
e.stopPropagation();
|
||||
|
||||
if (onShiftClick && e.shiftKey) {
|
||||
e.preventDefault();
|
||||
onShiftClick(e);
|
||||
} else if (this.state.id === openDropdownId) {
|
||||
this.handleClose();
|
||||
} else if (onOpen) {
|
||||
const { top } = e.currentTarget.getBoundingClientRect();
|
||||
const placement: DropdownPlacement = top * 2 < innerHeight ? 'bottom' : 'top';
|
||||
|
||||
onOpen(this.state.id, this.handleItemClick, placement, e.type !== 'click');
|
||||
}
|
||||
};
|
||||
|
||||
handleClose = () => {
|
||||
if (this.activeElement && this.activeElement === this.target) {
|
||||
(this.activeElement as HTMLButtonElement).focus();
|
||||
this.activeElement = null;
|
||||
}
|
||||
|
||||
if (this.props.onClose) {
|
||||
this.props.onClose(this.state.id);
|
||||
}
|
||||
};
|
||||
|
||||
handleMouseDown: React.EventHandler<React.MouseEvent | React.KeyboardEvent> = () => {
|
||||
if (!this.state.open) {
|
||||
this.activeElement = document.activeElement;
|
||||
}
|
||||
};
|
||||
|
||||
handleButtonKeyDown: React.EventHandler<React.KeyboardEvent> = (e) => {
|
||||
switch (e.key) {
|
||||
case ' ':
|
||||
case 'Enter':
|
||||
this.handleMouseDown(e);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
handleKeyPress: React.EventHandler<React.KeyboardEvent<HTMLButtonElement>> = (e) => {
|
||||
switch (e.key) {
|
||||
case ' ':
|
||||
case 'Enter':
|
||||
this.handleClick(e);
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
handleItemClick: React.EventHandler<React.MouseEvent> = e => {
|
||||
const i = Number(e.currentTarget.getAttribute('data-index'));
|
||||
const item = this.props.items[i];
|
||||
if (!item) return;
|
||||
|
||||
const { action, to } = item;
|
||||
|
||||
this.handleClose();
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
if (typeof action === 'function') {
|
||||
action(e);
|
||||
} else if (to) {
|
||||
this.props.history?.push(to);
|
||||
}
|
||||
};
|
||||
|
||||
setTargetRef: React.RefCallback<HTMLButtonElement> = c => {
|
||||
this.target = c;
|
||||
};
|
||||
|
||||
findTarget = () => {
|
||||
return this.target;
|
||||
};
|
||||
|
||||
componentWillUnmount = () => {
|
||||
if (this.state.id === this.props.openDropdownId) {
|
||||
this.handleClose();
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { src = require('@tabler/icons/dots.svg'), items, title, disabled, dropdownPlacement, openDropdownId, openedViaKeyboard = false, pressed, text, children, dropdownMenuStyle } = this.props;
|
||||
const open = this.state.id === openDropdownId;
|
||||
|
||||
return (
|
||||
<>
|
||||
{children ? (
|
||||
React.cloneElement(children, {
|
||||
disabled,
|
||||
onClick: this.handleClick,
|
||||
onMouseDown: this.handleMouseDown,
|
||||
onKeyDown: this.handleButtonKeyDown,
|
||||
onKeyPress: this.handleKeyPress,
|
||||
ref: this.setTargetRef,
|
||||
})
|
||||
) : (
|
||||
<IconButton
|
||||
disabled={disabled}
|
||||
className={classNames({
|
||||
'text-gray-600 hover:text-gray-700 dark:hover:text-gray-500': true,
|
||||
'text-gray-700 dark:text-gray-500': open,
|
||||
})}
|
||||
title={title}
|
||||
src={src}
|
||||
aria-pressed={pressed}
|
||||
text={text}
|
||||
onClick={this.handleClick}
|
||||
onMouseDown={this.handleMouseDown}
|
||||
onKeyDown={this.handleButtonKeyDown}
|
||||
onKeyPress={this.handleKeyPress}
|
||||
ref={this.setTargetRef}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Overlay show={open} placement={dropdownPlacement} target={this.findTarget}>
|
||||
<RouterDropdownMenu items={items} onClose={this.handleClose} openedViaKeyboard={openedViaKeyboard} style={dropdownMenuStyle} />
|
||||
</Overlay>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default withRouter(Dropdown);
|
|
@ -1,109 +0,0 @@
|
|||
import clsx from 'clsx';
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
|
||||
import { Counter, Icon } from '../ui';
|
||||
|
||||
export interface MenuItem {
|
||||
action?: React.EventHandler<React.KeyboardEvent | React.MouseEvent>
|
||||
active?: boolean
|
||||
count?: number
|
||||
destructive?: boolean
|
||||
href?: string
|
||||
icon?: string
|
||||
meta?: string
|
||||
middleClick?(event: React.MouseEvent): void
|
||||
target?: React.HTMLAttributeAnchorTarget
|
||||
text: string
|
||||
to?: string
|
||||
}
|
||||
|
||||
interface IDropdownMenuItem {
|
||||
index: number
|
||||
item: MenuItem | null
|
||||
onClick?(): void
|
||||
}
|
||||
|
||||
const DropdownMenuItem = ({ index, item, onClick }: IDropdownMenuItem) => {
|
||||
const history = useHistory();
|
||||
|
||||
const itemRef = useRef<HTMLAnchorElement>(null);
|
||||
|
||||
const handleClick: React.EventHandler<React.MouseEvent | React.KeyboardEvent> = (event) => {
|
||||
event.stopPropagation();
|
||||
|
||||
if (!item) return;
|
||||
if (onClick) onClick();
|
||||
|
||||
|
||||
if (item.to) {
|
||||
event.preventDefault();
|
||||
history.push(item.to);
|
||||
} else if (typeof item.action === 'function') {
|
||||
event.preventDefault();
|
||||
item.action(event);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAuxClick: React.EventHandler<React.MouseEvent> = (event) => {
|
||||
if (!item) return;
|
||||
if (onClick) onClick();
|
||||
|
||||
if (event.button === 1 && item.middleClick) {
|
||||
item.middleClick(event);
|
||||
}
|
||||
};
|
||||
|
||||
const handleItemKeyPress: React.EventHandler<React.KeyboardEvent> = (event) => {
|
||||
if (event.key === 'Enter' || event.key === ' ') {
|
||||
handleClick(event);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const firstItem = index === 0;
|
||||
|
||||
if (itemRef.current && firstItem) {
|
||||
itemRef.current.focus({ preventScroll: true });
|
||||
}
|
||||
}, [itemRef.current, index]);
|
||||
|
||||
if (item === null) {
|
||||
return <li className='my-1 mx-2 h-[2px] bg-gray-100 dark:bg-gray-800' />;
|
||||
}
|
||||
|
||||
return (
|
||||
<li className='truncate focus-visible:ring-2 focus-visible:ring-primary-500'>
|
||||
<a
|
||||
href={item.href || item.to || '#'}
|
||||
role='button'
|
||||
tabIndex={0}
|
||||
ref={itemRef}
|
||||
data-index={index}
|
||||
onClick={handleClick}
|
||||
onAuxClick={handleAuxClick}
|
||||
onKeyPress={handleItemKeyPress}
|
||||
target={item.target}
|
||||
title={item.text}
|
||||
className={
|
||||
clsx({
|
||||
'flex px-4 py-2.5 text-sm text-gray-700 dark:text-gray-500 hover:bg-gray-100 dark:hover:bg-gray-800 focus:outline-none cursor-pointer': true,
|
||||
'text-danger-600 dark:text-danger-400': item.destructive,
|
||||
})
|
||||
}
|
||||
>
|
||||
{item.icon && <Icon src={item.icon} className='mr-3 h-5 w-5 flex-none rtl:ml-3 rtl:mr-0' />}
|
||||
|
||||
<span className='truncate'>{item.text}</span>
|
||||
|
||||
{item.count ? (
|
||||
<span className='ml-auto h-5 w-5 flex-none'>
|
||||
<Counter count={item.count} />
|
||||
</span>
|
||||
) : null}
|
||||
</a>
|
||||
</li>
|
||||
);
|
||||
};
|
||||
|
||||
export default DropdownMenuItem;
|
|
@ -1,346 +0,0 @@
|
|||
import { offset, Placement, useFloating, flip, arrow } from '@floating-ui/react';
|
||||
import clsx from 'clsx';
|
||||
import { supportsPassiveEvents } from 'detect-passive-events';
|
||||
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
|
||||
import {
|
||||
closeDropdownMenu as closeDropdownMenuRedux,
|
||||
openDropdownMenu,
|
||||
} from 'soapbox/actions/dropdown-menu';
|
||||
import { closeModal, openModal } from 'soapbox/actions/modals';
|
||||
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
|
||||
import { isUserTouching } from 'soapbox/is-mobile';
|
||||
|
||||
import { IconButton, Portal } from '../ui';
|
||||
|
||||
import DropdownMenuItem, { MenuItem } from './dropdown-menu-item';
|
||||
|
||||
import type { Status } from 'soapbox/types/entities';
|
||||
|
||||
export type Menu = Array<MenuItem | null>;
|
||||
|
||||
interface IDropdownMenu {
|
||||
children?: React.ReactElement
|
||||
disabled?: boolean
|
||||
items: Menu
|
||||
onClose?: () => void
|
||||
onOpen?: () => void
|
||||
onShiftClick?: React.EventHandler<React.MouseEvent | React.KeyboardEvent>
|
||||
placement?: Placement
|
||||
src?: string
|
||||
status?: Status
|
||||
title?: string
|
||||
}
|
||||
|
||||
const listenerOptions = supportsPassiveEvents ? { passive: true } : false;
|
||||
|
||||
const DropdownMenu = (props: IDropdownMenu) => {
|
||||
const {
|
||||
children,
|
||||
disabled,
|
||||
items,
|
||||
onClose,
|
||||
onOpen,
|
||||
onShiftClick,
|
||||
placement: initialPlacement = 'top',
|
||||
src = require('@tabler/icons/dots.svg'),
|
||||
title = 'Menu',
|
||||
...filteredProps
|
||||
} = props;
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
const history = useHistory();
|
||||
|
||||
const [isOpen, setIsOpen] = useState<boolean>(false);
|
||||
const isOpenRedux = useAppSelector(state => state.dropdown_menu.isOpen);
|
||||
|
||||
const arrowRef = useRef<HTMLDivElement>(null);
|
||||
const activeElement = useRef<Element | null>(null);
|
||||
|
||||
const isOnMobile = isUserTouching();
|
||||
|
||||
const { x, y, strategy, refs, middlewareData, placement } = useFloating<HTMLButtonElement>({
|
||||
placement: initialPlacement,
|
||||
middleware: [
|
||||
offset(12),
|
||||
flip(),
|
||||
arrow({
|
||||
element: arrowRef,
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
const handleClick: React.EventHandler<
|
||||
React.MouseEvent<HTMLButtonElement> | React.KeyboardEvent<HTMLButtonElement>
|
||||
> = (event) => {
|
||||
event.stopPropagation();
|
||||
|
||||
if (onShiftClick && event.shiftKey) {
|
||||
event.preventDefault();
|
||||
|
||||
onShiftClick(event);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isOpen) {
|
||||
handleClose();
|
||||
} else {
|
||||
handleOpen();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* On mobile screens, let's replace the Popper dropdown with a Modal.
|
||||
*/
|
||||
const handleOpen = () => {
|
||||
if (isOnMobile) {
|
||||
dispatch(
|
||||
openModal('ACTIONS', {
|
||||
status: filteredProps.status,
|
||||
actions: items,
|
||||
onClick: handleItemClick,
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
dispatch(openDropdownMenu());
|
||||
setIsOpen(true);
|
||||
}
|
||||
|
||||
if (onOpen) {
|
||||
onOpen();
|
||||
}
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
if (activeElement.current && activeElement.current === refs.reference.current) {
|
||||
(activeElement.current as any).focus();
|
||||
activeElement.current = null;
|
||||
}
|
||||
|
||||
if (isOnMobile) {
|
||||
dispatch(closeModal('ACTIONS'));
|
||||
} else {
|
||||
closeDropdownMenu();
|
||||
setIsOpen(false);
|
||||
}
|
||||
|
||||
if (onClose) {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
const closeDropdownMenu = () => {
|
||||
if (isOpenRedux) {
|
||||
dispatch(closeDropdownMenuRedux());
|
||||
}
|
||||
};
|
||||
|
||||
const handleMouseDown: React.EventHandler<React.MouseEvent | React.KeyboardEvent> = () => {
|
||||
if (!isOpen) {
|
||||
activeElement.current = document.activeElement;
|
||||
}
|
||||
};
|
||||
|
||||
const handleButtonKeyDown: React.EventHandler<React.KeyboardEvent> = (event) => {
|
||||
switch (event.key) {
|
||||
case ' ':
|
||||
case 'Enter':
|
||||
handleMouseDown(event);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyPress: React.EventHandler<React.KeyboardEvent<HTMLButtonElement>> = (event) => {
|
||||
switch (event.key) {
|
||||
case ' ':
|
||||
case 'Enter':
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
handleClick(event);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
const handleItemClick: React.EventHandler<React.MouseEvent> = (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
const i = Number(event.currentTarget.getAttribute('data-index'));
|
||||
const item = items[i];
|
||||
if (!item) return;
|
||||
|
||||
const { action, to } = item;
|
||||
|
||||
handleClose();
|
||||
|
||||
if (typeof action === 'function') {
|
||||
action(event);
|
||||
} else if (to) {
|
||||
history.push(to);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDocumentClick = (event: Event) => {
|
||||
if (refs.floating.current && !refs.floating.current.contains(event.target as Node)) {
|
||||
handleClose();
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (!refs.floating.current) return;
|
||||
|
||||
const items = Array.from(refs.floating.current.getElementsByTagName('a'));
|
||||
const index = items.indexOf(document.activeElement as any);
|
||||
|
||||
let element = null;
|
||||
|
||||
switch (e.key) {
|
||||
case 'ArrowDown':
|
||||
element = items[index + 1] || items[0];
|
||||
break;
|
||||
case 'ArrowUp':
|
||||
element = items[index - 1] || items[items.length - 1];
|
||||
break;
|
||||
case 'Tab':
|
||||
if (e.shiftKey) {
|
||||
element = items[index - 1] || items[items.length - 1];
|
||||
} else {
|
||||
element = items[index + 1] || items[0];
|
||||
}
|
||||
break;
|
||||
case 'Home':
|
||||
element = items[0];
|
||||
break;
|
||||
case 'End':
|
||||
element = items[items.length - 1];
|
||||
break;
|
||||
case 'Escape':
|
||||
handleClose();
|
||||
break;
|
||||
}
|
||||
|
||||
if (element) {
|
||||
element.focus();
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
};
|
||||
|
||||
const arrowProps: React.CSSProperties = useMemo(() => {
|
||||
if (middlewareData.arrow) {
|
||||
const { x, y } = middlewareData.arrow;
|
||||
|
||||
const staticPlacement = {
|
||||
top: 'bottom',
|
||||
right: 'left',
|
||||
bottom: 'top',
|
||||
left: 'right',
|
||||
}[placement.split('-')[0]];
|
||||
|
||||
return {
|
||||
left: x !== null ? `${x}px` : '',
|
||||
top: y !== null ? `${y}px` : '',
|
||||
// Ensure the static side gets unset when
|
||||
// flipping to other placements' axes.
|
||||
right: '',
|
||||
bottom: '',
|
||||
[staticPlacement as string]: `${(-(arrowRef.current?.offsetWidth || 0)) / 2}px`,
|
||||
transform: 'rotate(45deg)',
|
||||
};
|
||||
}
|
||||
|
||||
return {};
|
||||
}, [middlewareData.arrow, placement]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
closeDropdownMenu();
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener('click', handleDocumentClick, false);
|
||||
document.addEventListener('keydown', handleKeyDown, false);
|
||||
document.addEventListener('touchend', handleDocumentClick, listenerOptions);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('click', handleDocumentClick);
|
||||
document.removeEventListener('keydown', handleKeyDown);
|
||||
document.removeEventListener('touchend', handleDocumentClick);
|
||||
};
|
||||
}, [refs.floating.current]);
|
||||
|
||||
if (items.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{children ? (
|
||||
React.cloneElement(children, {
|
||||
disabled,
|
||||
onClick: handleClick,
|
||||
onMouseDown: handleMouseDown,
|
||||
onKeyDown: handleButtonKeyDown,
|
||||
onKeyPress: handleKeyPress,
|
||||
ref: refs.setReference,
|
||||
})
|
||||
) : (
|
||||
<IconButton
|
||||
disabled={disabled}
|
||||
className={clsx({
|
||||
'text-gray-600 hover:text-gray-700 dark:hover:text-gray-500': true,
|
||||
'text-gray-700 dark:text-gray-500': isOpen,
|
||||
})}
|
||||
title={title}
|
||||
src={src}
|
||||
onClick={handleClick}
|
||||
onMouseDown={handleMouseDown}
|
||||
onKeyDown={handleButtonKeyDown}
|
||||
onKeyPress={handleKeyPress}
|
||||
ref={refs.setReference}
|
||||
/>
|
||||
)}
|
||||
|
||||
{isOpen ? (
|
||||
<Portal>
|
||||
<div
|
||||
data-testid='dropdown-menu'
|
||||
ref={refs.setFloating}
|
||||
className={
|
||||
clsx('z-[1001] w-56 rounded-md bg-white py-1 shadow-lg transition-opacity duration-100 focus:outline-none dark:bg-gray-900 dark:ring-2 dark:ring-primary-700', {
|
||||
'opacity-0 pointer-events-none': !isOpen,
|
||||
})
|
||||
}
|
||||
style={{
|
||||
position: strategy,
|
||||
top: y ?? 0,
|
||||
left: x ?? 0,
|
||||
}}
|
||||
>
|
||||
<ul>
|
||||
{items.map((item, idx) => (
|
||||
<DropdownMenuItem
|
||||
key={idx}
|
||||
item={item}
|
||||
index={idx}
|
||||
onClick={handleClose}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
{/* Arrow */}
|
||||
<div
|
||||
ref={arrowRef}
|
||||
style={arrowProps}
|
||||
className='pointer-events-none absolute z-[-1] h-3 w-3 bg-white dark:bg-gray-900'
|
||||
/>
|
||||
</div>
|
||||
</Portal>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default DropdownMenu;
|
|
@ -1,3 +0,0 @@
|
|||
export { default } from './dropdown-menu';
|
||||
export type { Menu } from './dropdown-menu';
|
||||
export type { MenuItem } from './dropdown-menu-item';
|
|
@ -1,19 +1,21 @@
|
|||
import classNames from 'clsx';
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { usePopper } from 'react-popper';
|
||||
|
||||
import { simpleEmojiReact } from 'soapbox/actions/emoji-reacts';
|
||||
import { openModal } from 'soapbox/actions/modals';
|
||||
import { EmojiSelector, Portal } from 'soapbox/components/ui';
|
||||
import { EmojiSelector } from 'soapbox/components/ui';
|
||||
import { useAppDispatch, useAppSelector, useOwnAccount, useSoapboxConfig } from 'soapbox/hooks';
|
||||
import { isUserTouching } from 'soapbox/is-mobile';
|
||||
import { getReactForStatus } from 'soapbox/utils/emoji-reacts';
|
||||
|
||||
interface IStatusReactionWrapper {
|
||||
statusId: string
|
||||
children: JSX.Element
|
||||
interface IEmojiButtonWrapper {
|
||||
statusId: string,
|
||||
children: JSX.Element,
|
||||
}
|
||||
|
||||
/** Provides emoji reaction functionality to the underlying button component */
|
||||
const StatusReactionWrapper: React.FC<IStatusReactionWrapper> = ({ statusId, children }): JSX.Element | null => {
|
||||
const EmojiButtonWrapper: React.FC<IEmojiButtonWrapper> = ({ statusId, children }): JSX.Element | null => {
|
||||
const dispatch = useAppDispatch();
|
||||
const ownAccount = useOwnAccount();
|
||||
const status = useAppSelector(state => state.statuses.get(statusId));
|
||||
|
@ -21,8 +23,24 @@ const StatusReactionWrapper: React.FC<IStatusReactionWrapper> = ({ statusId, chi
|
|||
|
||||
const timeout = useRef<NodeJS.Timeout>();
|
||||
const [visible, setVisible] = useState(false);
|
||||
// const [focused, setFocused] = useState(false);
|
||||
|
||||
// `useRef` won't trigger a re-render, while `useState` does.
|
||||
// https://popper.js.org/react-popper/v2/
|
||||
const [referenceElement, setReferenceElement] = useState<HTMLDivElement | null>(null);
|
||||
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
|
||||
|
||||
const { styles, attributes } = usePopper(referenceElement, popperElement, {
|
||||
placement: 'top-start',
|
||||
modifiers: [
|
||||
{
|
||||
name: 'offset',
|
||||
options: {
|
||||
offset: [-10, 0],
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
|
@ -60,9 +78,9 @@ const StatusReactionWrapper: React.FC<IStatusReactionWrapper> = ({ statusId, chi
|
|||
}
|
||||
};
|
||||
|
||||
const handleReact = (emoji: string, custom?: string): void => {
|
||||
const handleReact = (emoji: string): void => {
|
||||
if (ownAccount) {
|
||||
dispatch(simpleEmojiReact(status, emoji, custom));
|
||||
dispatch(simpleEmojiReact(status, emoji));
|
||||
} else {
|
||||
handleUnauthorized();
|
||||
}
|
||||
|
@ -71,7 +89,7 @@ const StatusReactionWrapper: React.FC<IStatusReactionWrapper> = ({ statusId, chi
|
|||
};
|
||||
|
||||
const handleClick: React.EventHandler<React.MouseEvent> = e => {
|
||||
const meEmojiReact = getReactForStatus(status, soapboxConfig.allowedEmoji)?.get('name') || '👍';
|
||||
const meEmojiReact = getReactForStatus(status, soapboxConfig.allowedEmoji) || '👍';
|
||||
|
||||
if (isUserTouching()) {
|
||||
if (ownAccount) {
|
||||
|
@ -98,6 +116,28 @@ const StatusReactionWrapper: React.FC<IStatusReactionWrapper> = ({ statusId, chi
|
|||
}));
|
||||
};
|
||||
|
||||
// const handleUnfocus: React.EventHandler<React.KeyboardEvent> = () => {
|
||||
// setFocused(false);
|
||||
// };
|
||||
|
||||
const selector = (
|
||||
<div
|
||||
className={classNames('z-50 transition-opacity duration-100', {
|
||||
'opacity-0 pointer-events-none': !visible,
|
||||
})}
|
||||
ref={setPopperElement}
|
||||
style={styles.popper}
|
||||
{...attributes.popper}
|
||||
>
|
||||
<EmojiSelector
|
||||
emojis={soapboxConfig.allowedEmoji}
|
||||
onReact={handleReact}
|
||||
// focused={focused}
|
||||
// onUnfocus={handleUnfocus}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className='relative' onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave}>
|
||||
{React.cloneElement(children, {
|
||||
|
@ -105,19 +145,9 @@ const StatusReactionWrapper: React.FC<IStatusReactionWrapper> = ({ statusId, chi
|
|||
ref: setReferenceElement,
|
||||
})}
|
||||
|
||||
{visible && (
|
||||
<Portal>
|
||||
<EmojiSelector
|
||||
placement='top-start'
|
||||
referenceElement={referenceElement}
|
||||
onReact={handleReact}
|
||||
visible={visible}
|
||||
onClose={() => setVisible(false)}
|
||||
/>
|
||||
</Portal>
|
||||
)}
|
||||
{selector}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default StatusReactionWrapper;
|
||||
export default EmojiButtonWrapper;
|
|
@ -0,0 +1,142 @@
|
|||
// import classNames from 'clsx';
|
||||
import React from 'react';
|
||||
import { HotKeys } from 'react-hotkeys';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { getSoapboxConfig } from 'soapbox/actions/soapbox';
|
||||
import { EmojiSelector as RealEmojiSelector } from 'soapbox/components/ui';
|
||||
|
||||
import type { List as ImmutableList } from 'immutable';
|
||||
import type { RootState } from 'soapbox/store';
|
||||
|
||||
const mapStateToProps = (state: RootState) => ({
|
||||
allowedEmoji: getSoapboxConfig(state).allowedEmoji,
|
||||
});
|
||||
|
||||
interface IEmojiSelector {
|
||||
allowedEmoji: ImmutableList<string>,
|
||||
onReact: (emoji: string) => void,
|
||||
onUnfocus: () => void,
|
||||
visible: boolean,
|
||||
focused?: boolean,
|
||||
}
|
||||
|
||||
class EmojiSelector extends ImmutablePureComponent<IEmojiSelector> {
|
||||
|
||||
static defaultProps: Partial<IEmojiSelector> = {
|
||||
onReact: () => { },
|
||||
onUnfocus: () => { },
|
||||
visible: false,
|
||||
};
|
||||
|
||||
node?: HTMLDivElement = undefined;
|
||||
|
||||
handleBlur: React.FocusEventHandler<HTMLDivElement> = e => {
|
||||
const { focused, onUnfocus } = this.props;
|
||||
|
||||
if (focused && (!e.currentTarget || !e.currentTarget.classList.contains('emoji-react-selector__emoji'))) {
|
||||
onUnfocus();
|
||||
}
|
||||
};
|
||||
|
||||
_selectPreviousEmoji = (i: number): void => {
|
||||
if (!this.node) return;
|
||||
|
||||
if (i !== 0) {
|
||||
const button: HTMLButtonElement | null = this.node.querySelector(`.emoji-react-selector__emoji:nth-child(${i})`);
|
||||
button?.focus();
|
||||
} else {
|
||||
const button: HTMLButtonElement | null = this.node.querySelector('.emoji-react-selector__emoji:last-child');
|
||||
button?.focus();
|
||||
}
|
||||
};
|
||||
|
||||
_selectNextEmoji = (i: number) => {
|
||||
if (!this.node) return;
|
||||
|
||||
if (i !== this.props.allowedEmoji.size - 1) {
|
||||
const button: HTMLButtonElement | null = this.node.querySelector(`.emoji-react-selector__emoji:nth-child(${i + 2})`);
|
||||
button?.focus();
|
||||
} else {
|
||||
const button: HTMLButtonElement | null = this.node.querySelector('.emoji-react-selector__emoji:first-child');
|
||||
button?.focus();
|
||||
}
|
||||
};
|
||||
|
||||
handleKeyDown = (i: number): React.KeyboardEventHandler => e => {
|
||||
const { onUnfocus } = this.props;
|
||||
|
||||
switch (e.key) {
|
||||
case 'Tab':
|
||||
e.preventDefault();
|
||||
if (e.shiftKey) this._selectPreviousEmoji(i);
|
||||
else this._selectNextEmoji(i);
|
||||
break;
|
||||
case 'Left':
|
||||
case 'ArrowLeft':
|
||||
this._selectPreviousEmoji(i);
|
||||
break;
|
||||
case 'Right':
|
||||
case 'ArrowRight':
|
||||
this._selectNextEmoji(i);
|
||||
break;
|
||||
case 'Escape':
|
||||
onUnfocus();
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
handleReact = (emoji: string) => (): void => {
|
||||
const { onReact, focused, onUnfocus } = this.props;
|
||||
|
||||
onReact(emoji);
|
||||
|
||||
if (focused) {
|
||||
onUnfocus();
|
||||
}
|
||||
};
|
||||
|
||||
handlers = {
|
||||
open: () => { },
|
||||
};
|
||||
|
||||
setRef = (c: HTMLDivElement): void => {
|
||||
this.node = c;
|
||||
};
|
||||
|
||||
render() {
|
||||
const { visible, focused, allowedEmoji, onReact } = this.props;
|
||||
|
||||
return (
|
||||
<HotKeys handlers={this.handlers}>
|
||||
{/*<div
|
||||
className={classNames('flex absolute bg-white dark:bg-gray-500 px-2 py-3 rounded-full shadow-md opacity-0 pointer-events-none duration-100 w-max', { 'opacity-100 pointer-events-auto z-[999]': visible || focused })}
|
||||
onBlur={this.handleBlur}
|
||||
ref={this.setRef}
|
||||
>
|
||||
{allowedEmoji.map((emoji, i) => (
|
||||
<button
|
||||
key={i}
|
||||
className='emoji-react-selector__emoji'
|
||||
onClick={this.handleReact(emoji)}
|
||||
onKeyDown={this.handleKeyDown(i)}
|
||||
tabIndex={(visible || focused) ? 0 : -1}
|
||||
>
|
||||
<Emoji emoji={emoji} />
|
||||
</button>
|
||||
))}
|
||||
</div>*/}
|
||||
<RealEmojiSelector
|
||||
emojis={allowedEmoji.toArray()}
|
||||
onReact={onReact}
|
||||
visible={visible}
|
||||
focused={focused}
|
||||
/>
|
||||
</HotKeys>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(EmojiSelector);
|
|
@ -31,10 +31,10 @@ interface Props extends ReturnType<typeof mapStateToProps> {
|
|||
}
|
||||
|
||||
type State = {
|
||||
hasError: boolean
|
||||
error: any
|
||||
componentStack: any
|
||||
browser?: Bowser.Parser.Parser
|
||||
hasError: boolean,
|
||||
error: any,
|
||||
componentStack: any,
|
||||
browser?: Bowser.Parser.Parser,
|
||||
}
|
||||
|
||||
class ErrorBoundary extends React.PureComponent<Props, State> {
|
||||
|
@ -113,17 +113,17 @@ class ErrorBoundary extends React.PureComponent<Props, State> {
|
|||
const errorText = this.getErrorText();
|
||||
|
||||
return (
|
||||
<div className='flex h-screen flex-col bg-white pt-16 pb-12 dark:bg-primary-900'>
|
||||
<main className='mx-auto flex w-full max-w-7xl grow flex-col justify-center px-4 sm:px-6 lg:px-8'>
|
||||
<div className='flex shrink-0 justify-center'>
|
||||
<div className='h-screen pt-16 pb-12 flex flex-col bg-white dark:bg-primary-900'>
|
||||
<main className='flex-grow flex flex-col justify-center max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8'>
|
||||
<div className='flex-shrink-0 flex justify-center'>
|
||||
<a href='/' className='inline-flex'>
|
||||
<SiteLogo alt='Logo' className='h-12 w-auto cursor-pointer' />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div className='py-8'>
|
||||
<div className='mx-auto max-w-xl space-y-2 text-center'>
|
||||
<h1 className='text-3xl font-extrabold tracking-tight text-gray-900 dark:text-gray-500 sm:text-4xl'>
|
||||
<div className='text-center max-w-xl mx-auto space-y-2'>
|
||||
<h1 className='text-3xl font-extrabold text-gray-900 dark:text-gray-500 tracking-tight sm:text-4xl'>
|
||||
<FormattedMessage id='alert.unexpected.message' defaultMessage='Something went wrong.' />
|
||||
</h1>
|
||||
<p className='text-lg text-gray-700 dark:text-gray-600'>
|
||||
|
@ -132,7 +132,7 @@ class ErrorBoundary extends React.PureComponent<Props, State> {
|
|||
defaultMessage="We're sorry for the interruption. If the problem persists, please reach out to our support team. You may also try to {clearCookies} (this will log you out)."
|
||||
values={{
|
||||
clearCookies: (
|
||||
<a href='/' onClick={this.clearCookies} className='text-primary-600 hover:underline dark:text-accent-blue'>
|
||||
<a href='/' onClick={this.clearCookies} className='text-primary-600 dark:text-accent-blue hover:underline'>
|
||||
<FormattedMessage
|
||||
id='alert.unexpected.clear_cookies'
|
||||
defaultMessage='clear cookies and browser data'
|
||||
|
@ -150,7 +150,7 @@ class ErrorBoundary extends React.PureComponent<Props, State> {
|
|||
</Text>
|
||||
|
||||
<div className='mt-10'>
|
||||
<a href='/' className='text-base font-medium text-primary-600 hover:underline dark:text-accent-blue'>
|
||||
<a href='/' className='text-base font-medium text-primary-600 dark:text-accent-blue hover:underline'>
|
||||
<FormattedMessage id='alert.unexpected.return_home' defaultMessage='Return Home' />
|
||||
<span aria-hidden='true'> →</span>
|
||||
</a>
|
||||
|
@ -158,11 +158,11 @@ class ErrorBoundary extends React.PureComponent<Props, State> {
|
|||
</div>
|
||||
|
||||
{!isProduction && (
|
||||
<div className='mx-auto max-w-lg space-y-4 py-16'>
|
||||
<div className='py-16 max-w-lg mx-auto space-y-4'>
|
||||
{errorText && (
|
||||
<textarea
|
||||
ref={this.setTextareaRef}
|
||||
className='block h-48 w-full rounded-md border-gray-300 bg-gray-100 p-4 font-mono text-gray-900 shadow-sm focus:border-primary-500 focus:ring-2 focus:ring-primary-500 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100 sm:text-sm'
|
||||
className='h-48 p-4 shadow-sm bg-gray-100 text-gray-900 dark:text-gray-100 dark:bg-gray-800 focus:ring-2 focus:ring-primary-500 focus:border-primary-500 block w-full sm:text-sm border-gray-300 dark:border-gray-700 rounded-md font-mono'
|
||||
value={errorText}
|
||||
onClick={this.handleCopy}
|
||||
readOnly
|
||||
|
@ -180,11 +180,11 @@ class ErrorBoundary extends React.PureComponent<Props, State> {
|
|||
</div>
|
||||
</main>
|
||||
|
||||
<footer className='mx-auto w-full max-w-7xl shrink-0 px-4 sm:px-6 lg:px-8'>
|
||||
<footer className='flex-shrink-0 max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8'>
|
||||
<HStack justifyContent='center' space={4} element='nav'>
|
||||
{links.get('status') && (
|
||||
<>
|
||||
<a href={links.get('status')} className='text-sm font-medium text-gray-700 hover:underline dark:text-gray-600'>
|
||||
<a href={links.get('status')} className='text-sm font-medium text-gray-700 dark:text-gray-600 hover:underline'>
|
||||
<FormattedMessage id='alert.unexpected.links.status' defaultMessage='Status' />
|
||||
</a>
|
||||
</>
|
||||
|
@ -193,7 +193,7 @@ class ErrorBoundary extends React.PureComponent<Props, State> {
|
|||
{links.get('help') && (
|
||||
<>
|
||||
<span className='inline-block border-l border-gray-300' aria-hidden='true' />
|
||||
<a href={links.get('help')} className='text-sm font-medium text-gray-700 hover:underline dark:text-gray-600'>
|
||||
<a href={links.get('help')} className='text-sm font-medium text-gray-700 dark:text-gray-600 hover:underline'>
|
||||
<FormattedMessage id='alert.unexpected.links.help' defaultMessage='Help Center' />
|
||||
</a>
|
||||
</>
|
||||
|
@ -202,7 +202,7 @@ class ErrorBoundary extends React.PureComponent<Props, State> {
|
|||
{links.get('support') && (
|
||||
<>
|
||||
<span className='inline-block border-l border-gray-300' aria-hidden='true' />
|
||||
<a href={links.get('support')} className='text-sm font-medium text-gray-700 hover:underline dark:text-gray-600'>
|
||||
<a href={links.get('support')} className='text-sm font-medium text-gray-700 dark:text-gray-600 hover:underline'>
|
||||
<FormattedMessage id='alert.unexpected.links.support' defaultMessage='Support' />
|
||||
</a>
|
||||
</>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import clsx from 'clsx';
|
||||
import classNames from 'clsx';
|
||||
import React from 'react';
|
||||
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||
|
||||
|
@ -51,11 +51,11 @@ const EventPreview: React.FC<IEventPreview> = ({ status, className, hideAction,
|
|||
));
|
||||
|
||||
return (
|
||||
<div className={clsx('relative w-full overflow-hidden rounded-lg bg-gray-100 dark:bg-primary-800', className)}>
|
||||
<div className={classNames('w-full rounded-lg bg-gray-100 dark:bg-primary-800 relative overflow-hidden', className)}>
|
||||
<div className='absolute top-28 right-3'>
|
||||
{floatingAction && action}
|
||||
</div>
|
||||
<div className='h-40 bg-primary-200 dark:bg-gray-600'>
|
||||
<div className='bg-primary-200 dark:bg-gray-600 h-40'>
|
||||
{banner && <img className='h-full w-full object-cover' src={banner.url} alt={intl.formatMessage(messages.eventBanner)} />}
|
||||
</div>
|
||||
<Stack className='p-2.5' space={2}>
|
||||
|
@ -65,7 +65,7 @@ const EventPreview: React.FC<IEventPreview> = ({ status, className, hideAction,
|
|||
{!floatingAction && action}
|
||||
</HStack>
|
||||
|
||||
<div className='flex flex-wrap gap-y-1 gap-x-2 text-gray-700 dark:text-gray-600'>
|
||||
<div className='flex gap-y-1 gap-x-2 flex-wrap text-gray-700 dark:text-gray-600'>
|
||||
<HStack alignItems='center' space={2}>
|
||||
<Icon src={require('@tabler/icons/user.svg')} />
|
||||
<HStack space={1} alignItems='center' grow>
|
||||
|
|
|
@ -3,14 +3,14 @@ import React, { useEffect, useRef } from 'react';
|
|||
import { isIOS } from 'soapbox/is-mobile';
|
||||
|
||||
interface IExtendedVideoPlayer {
|
||||
src: string
|
||||
alt?: string
|
||||
width?: number
|
||||
height?: number
|
||||
time?: number
|
||||
controls?: boolean
|
||||
muted?: boolean
|
||||
onClick?: () => void
|
||||
src: string,
|
||||
alt?: string,
|
||||
width?: number,
|
||||
height?: number,
|
||||
time?: number,
|
||||
controls?: boolean,
|
||||
muted?: boolean,
|
||||
onClick?: () => void,
|
||||
}
|
||||
|
||||
const ExtendedVideoPlayer: React.FC<IExtendedVideoPlayer> = ({ src, alt, time, controls, muted, onClick }) => {
|
||||
|
|
|
@ -5,13 +5,13 @@
|
|||
* @see soapbox/components/icon
|
||||
*/
|
||||
|
||||
import clsx from 'clsx';
|
||||
import classNames from 'clsx';
|
||||
import React from 'react';
|
||||
|
||||
export interface IForkAwesomeIcon extends React.HTMLAttributes<HTMLLIElement> {
|
||||
id: string
|
||||
className?: string
|
||||
fixedWidth?: boolean
|
||||
id: string,
|
||||
className?: string,
|
||||
fixedWidth?: boolean,
|
||||
}
|
||||
|
||||
const ForkAwesomeIcon: React.FC<IForkAwesomeIcon> = ({ id, className, fixedWidth, ...rest }) => {
|
||||
|
@ -25,7 +25,7 @@ const ForkAwesomeIcon: React.FC<IForkAwesomeIcon> = ({ id, className, fixedWidth
|
|||
<i
|
||||
role='img'
|
||||
// alt={alt}
|
||||
className={clsx('fa', `fa-${id}`, className, { 'fa-fw': fixedWidth })}
|
||||
className={classNames('fa', `fa-${id}`, className, { 'fa-fw': fixedWidth })}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import clsx from 'clsx';
|
||||
import classNames from 'clsx';
|
||||
import React, { useState } from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
|
@ -30,8 +30,8 @@ const GdprBanner: React.FC = () => {
|
|||
}
|
||||
|
||||
return (
|
||||
<Banner theme='opaque' className={clsx('transition-transform', { 'translate-y-full': slideout })}>
|
||||
<div className='flex flex-col space-y-4 rtl:space-x-reverse lg:flex-row lg:items-center lg:justify-between lg:space-y-0 lg:space-x-4'>
|
||||
<Banner theme='opaque' className={classNames('transition-transform', { 'translate-y-full': slideout })}>
|
||||
<div className='flex flex-col space-y-4 lg:space-y-0 lg:space-x-4 rtl:space-x-reverse lg:flex-row lg:items-center lg:justify-between'>
|
||||
<Stack space={2}>
|
||||
<Text size='xl' weight='bold'>
|
||||
<FormattedMessage id='gdpr.title' defaultMessage='{siteTitle} uses cookies' values={{ siteTitle: instance.title }} />
|
||||
|
|
|
@ -1,12 +1,7 @@
|
|||
import React from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { FormattedMessage, defineMessages, useIntl } from 'react-intl';
|
||||
|
||||
import GroupMemberCount from 'soapbox/features/group/components/group-member-count';
|
||||
import GroupPrivacy from 'soapbox/features/group/components/group-privacy';
|
||||
import GroupRelationship from 'soapbox/features/group/components/group-relationship';
|
||||
|
||||
import GroupAvatar from './groups/group-avatar';
|
||||
import { HStack, Stack, Text } from './ui';
|
||||
import { Avatar, HStack, Icon, Stack, Text } from './ui';
|
||||
|
||||
import type { Group as GroupEntity } from 'soapbox/types/entities';
|
||||
|
||||
|
@ -22,42 +17,43 @@ const GroupCard: React.FC<IGroupCard> = ({ group }) => {
|
|||
const intl = useIntl();
|
||||
|
||||
return (
|
||||
<Stack
|
||||
className='relative h-[240px] rounded-lg border border-solid border-gray-300 bg-white dark:border-primary-800 dark:bg-primary-900'
|
||||
data-testid='group-card'
|
||||
>
|
||||
{/* Group Cover Image */}
|
||||
<Stack grow className='relative basis-1/2 rounded-t-lg bg-primary-100 dark:bg-gray-800'>
|
||||
{group.header && (
|
||||
<img
|
||||
className='absolute inset-0 h-full w-full rounded-t-lg object-cover'
|
||||
src={group.header} alt={intl.formatMessage(messages.groupHeader)}
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
{/* Group Avatar */}
|
||||
<div className='absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2'>
|
||||
<GroupAvatar group={group} size={64} withRing />
|
||||
</div>
|
||||
|
||||
{/* Group Info */}
|
||||
<Stack alignItems='center' justifyContent='end' grow className='basis-1/2 py-4' space={0.5}>
|
||||
<HStack alignItems='center' space={1.5}>
|
||||
<div className='overflow-hidden'>
|
||||
<Stack className='bg-white dark:bg-primary-900 border border-solid border-gray-300 dark:border-primary-800 rounded-lg sm:rounded-xl'>
|
||||
<div className='bg-primary-100 dark:bg-gray-800 h-[120px] relative -m-[1px] mb-0 rounded-t-lg sm:rounded-t-xl'>
|
||||
{group.header && <img className='h-full w-full object-cover rounded-t-lg sm:rounded-t-xl' src={group.header} alt={intl.formatMessage(messages.groupHeader)} />}
|
||||
<div className='absolute left-1/2 bottom-0 -translate-x-1/2 translate-y-1/2'>
|
||||
<Avatar className='ring-2 ring-white dark:ring-primary-900' src={group.avatar} size={64} />
|
||||
</div>
|
||||
</div>
|
||||
<Stack className='p-3 pt-9' alignItems='center' space={3}>
|
||||
<Text size='lg' weight='bold' dangerouslySetInnerHTML={{ __html: group.display_name_html }} />
|
||||
|
||||
{group.relationship?.pending_requests && (
|
||||
<div className='h-2 w-2 rounded-full bg-secondary-500' />
|
||||
)}
|
||||
</HStack>
|
||||
|
||||
<HStack className='text-gray-700 dark:text-gray-600' space={2} wrap>
|
||||
<GroupRelationship group={group} />
|
||||
<GroupPrivacy group={group} />
|
||||
<GroupMemberCount group={group} />
|
||||
</HStack>
|
||||
<HStack className='text-gray-700 dark:text-gray-600' space={3} wrap>
|
||||
{group.relationship?.role === 'admin' ? (
|
||||
<HStack space={1} alignItems='center'>
|
||||
<Icon className='h-4 w-4' src={require('@tabler/icons/users.svg')} />
|
||||
<span><FormattedMessage id='group.role.admin' defaultMessage='Admin' /></span>
|
||||
</HStack>
|
||||
) : group.relationship?.role === 'moderator' && (
|
||||
<HStack space={1} alignItems='center'>
|
||||
<Icon className='h-4 w-4' src={require('@tabler/icons/gavel.svg')} />
|
||||
<span><FormattedMessage id='group.role.moderator' defaultMessage='Moderator' /></span>
|
||||
</HStack>
|
||||
)}
|
||||
{group.locked ? (
|
||||
<HStack space={1} alignItems='center'>
|
||||
<Icon className='h-4 w-4' src={require('@tabler/icons/lock.svg')} />
|
||||
<span><FormattedMessage id='group.privacy.locked' defaultMessage='Private' /></span>
|
||||
</HStack>
|
||||
) : (
|
||||
<HStack space={1} alignItems='center'>
|
||||
<Icon className='h-4 w-4' src={require('@tabler/icons/world.svg')} />
|
||||
<span><FormattedMessage id='group.privacy.public' defaultMessage='Public' /></span>
|
||||
</HStack>
|
||||
)}
|
||||
</HStack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -1,37 +0,0 @@
|
|||
import clsx from 'clsx';
|
||||
import React from 'react';
|
||||
|
||||
import { GroupRoles } from 'soapbox/schemas/group-member';
|
||||
|
||||
import { Avatar } from '../ui';
|
||||
|
||||
import type { Group } from 'soapbox/schemas';
|
||||
|
||||
interface IGroupAvatar {
|
||||
group: Group
|
||||
size: number
|
||||
withRing?: boolean
|
||||
}
|
||||
|
||||
const GroupAvatar = (props: IGroupAvatar) => {
|
||||
const { group, size, withRing = false } = props;
|
||||
|
||||
const isOwner = group.relationship?.role === GroupRoles.OWNER;
|
||||
|
||||
return (
|
||||
<Avatar
|
||||
className={
|
||||
clsx('relative rounded-full', {
|
||||
'shadow-[0_0_0_2px_theme(colors.primary.600),0_0_0_4px_theme(colors.white)]': isOwner && withRing,
|
||||
'dark:shadow-[0_0_0_2px_theme(colors.primary.600),0_0_0_4px_theme(colors.gray.800)]': isOwner && withRing,
|
||||
'shadow-[0_0_0_2px_theme(colors.primary.600)]': isOwner && !withRing,
|
||||
'shadow-[0_0_0_2px_theme(colors.white)] dark:shadow-[0_0_0_2px_theme(colors.gray.800)]': !isOwner && withRing,
|
||||
})
|
||||
}
|
||||
src={group.avatar}
|
||||
size={size}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default GroupAvatar;
|
|
@ -1,99 +0,0 @@
|
|||
import React from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { Button, Divider, HStack, Popover, Stack, Text } from 'soapbox/components/ui';
|
||||
import GroupMemberCount from 'soapbox/features/group/components/group-member-count';
|
||||
import GroupPrivacy from 'soapbox/features/group/components/group-privacy';
|
||||
|
||||
import GroupAvatar from '../group-avatar';
|
||||
|
||||
import type { Group } from 'soapbox/schemas';
|
||||
|
||||
interface IGroupPopoverContainer {
|
||||
children: React.ReactElement<any, string | React.JSXElementConstructor<any>>
|
||||
isEnabled: boolean
|
||||
group: Group
|
||||
}
|
||||
|
||||
const messages = defineMessages({
|
||||
title: { id: 'group.popover.title', defaultMessage: 'Membership required' },
|
||||
summary: { id: 'group.popover.summary', defaultMessage: 'You must be a member of the group in order to reply to this status.' },
|
||||
action: { id: 'group.popover.action', defaultMessage: 'View Group' },
|
||||
});
|
||||
|
||||
const GroupPopover = (props: IGroupPopoverContainer) => {
|
||||
const { children, group, isEnabled } = props;
|
||||
|
||||
const intl = useIntl();
|
||||
|
||||
if (!isEnabled) {
|
||||
return children;
|
||||
}
|
||||
|
||||
return (
|
||||
<Popover
|
||||
interaction='click'
|
||||
referenceElementClassName='cursor-pointer'
|
||||
content={
|
||||
<Stack space={4} className='w-80'>
|
||||
<Stack
|
||||
className='relative h-60 rounded-lg bg-white dark:border-primary-800 dark:bg-primary-900'
|
||||
data-testid='group-card'
|
||||
>
|
||||
{/* Group Cover Image */}
|
||||
<Stack grow className='relative basis-1/2 rounded-t-lg bg-primary-100 dark:bg-gray-800'>
|
||||
{group.header && (
|
||||
<img
|
||||
className='absolute inset-0 h-full w-full rounded-t-lg object-cover'
|
||||
src={group.header}
|
||||
alt=''
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
{/* Group Avatar */}
|
||||
<div className='absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2'>
|
||||
<GroupAvatar group={group} size={64} withRing />
|
||||
</div>
|
||||
|
||||
{/* Group Info */}
|
||||
<Stack alignItems='center' justifyContent='end' grow className='basis-1/2 py-4' space={0.5}>
|
||||
<Text size='lg' weight='bold' dangerouslySetInnerHTML={{ __html: group.display_name_html }} />
|
||||
|
||||
<HStack className='text-gray-700 dark:text-gray-600' space={2} wrap>
|
||||
<GroupPrivacy group={group} />
|
||||
<GroupMemberCount group={group} />
|
||||
</HStack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
|
||||
<Divider />
|
||||
|
||||
<Stack space={0.5} className='px-4'>
|
||||
<Text weight='semibold'>
|
||||
{intl.formatMessage(messages.title)}
|
||||
</Text>
|
||||
<Text theme='muted'>
|
||||
{intl.formatMessage(messages.summary)}
|
||||
</Text>
|
||||
</Stack>
|
||||
|
||||
<div className='px-4 pb-4'>
|
||||
<Link to={`/groups/${group.id}`}>
|
||||
<Button type='button' theme='secondary' block>
|
||||
{intl.formatMessage(messages.action)}
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</Stack>
|
||||
}
|
||||
isFlush
|
||||
children={
|
||||
<div className='inline-block'>{children}</div>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default GroupPopover;
|
|
@ -10,7 +10,7 @@ import { HStack, Stack, Text } from './ui';
|
|||
import type { Tag } from 'soapbox/types/entities';
|
||||
|
||||
interface IHashtag {
|
||||
hashtag: Tag
|
||||
hashtag: Tag,
|
||||
}
|
||||
|
||||
const Hashtag: React.FC<IHashtag> = ({ hashtag }) => {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import clsx from 'clsx';
|
||||
import classNames from 'clsx';
|
||||
import debounce from 'lodash/debounce';
|
||||
import React, { useRef } from 'react';
|
||||
|
||||
|
@ -15,10 +15,10 @@ const showProfileHoverCard = debounce((dispatch, ref, accountId) => {
|
|||
}, 600);
|
||||
|
||||
interface IHoverRefWrapper {
|
||||
accountId: string
|
||||
inline?: boolean
|
||||
className?: string
|
||||
children: React.ReactNode
|
||||
accountId: string,
|
||||
inline?: boolean,
|
||||
className?: string,
|
||||
children: React.ReactNode,
|
||||
}
|
||||
|
||||
/** Makes a profile hover card appear when the wrapped element is hovered. */
|
||||
|
@ -47,7 +47,7 @@ export const HoverRefWrapper: React.FC<IHoverRefWrapper> = ({ accountId, childre
|
|||
return (
|
||||
<Elem
|
||||
ref={ref}
|
||||
className={clsx('hover-ref-wrapper', className)}
|
||||
className={classNames('hover-ref-wrapper', className)}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
onClick={handleClick}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import clsx from 'clsx';
|
||||
import classNames from 'clsx';
|
||||
import debounce from 'lodash/debounce';
|
||||
import React, { useRef } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
@ -14,10 +14,10 @@ const showStatusHoverCard = debounce((dispatch, ref, statusId) => {
|
|||
}, 300);
|
||||
|
||||
interface IHoverStatusWrapper {
|
||||
statusId: any
|
||||
inline: boolean
|
||||
className?: string
|
||||
children: React.ReactNode
|
||||
statusId: any,
|
||||
inline: boolean,
|
||||
className?: string,
|
||||
children: React.ReactNode,
|
||||
}
|
||||
|
||||
/** Makes a status hover card appear when the wrapped element is hovered. */
|
||||
|
@ -45,7 +45,7 @@ export const HoverStatusWrapper: React.FC<IHoverStatusWrapper> = ({ statusId, ch
|
|||
return (
|
||||
<Elem
|
||||
ref={ref}
|
||||
className={clsx('hover-status-wrapper', className)}
|
||||
className={classNames('hover-status-wrapper', className)}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
onClick={handleClick}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import clsx from 'clsx';
|
||||
import classNames from 'clsx';
|
||||
import React from 'react';
|
||||
|
||||
import Icon from 'soapbox/components/icon';
|
||||
|
@ -66,7 +66,7 @@ const IconButton: React.FC<IIconButton> = ({
|
|||
}
|
||||
};
|
||||
|
||||
const classes = clsx(className, 'icon-button', {
|
||||
const classes = classNames(className, 'icon-button', {
|
||||
active,
|
||||
disabled,
|
||||
});
|
||||
|
|
|
@ -4,10 +4,10 @@ import Icon, { IIcon } from 'soapbox/components/icon';
|
|||
import { Counter } from 'soapbox/components/ui';
|
||||
|
||||
interface IIconWithCounter extends React.HTMLAttributes<HTMLDivElement> {
|
||||
count: number
|
||||
count: number,
|
||||
countMax?: number
|
||||
icon?: string
|
||||
src?: string
|
||||
icon?: string;
|
||||
src?: string;
|
||||
}
|
||||
|
||||
const IconWithCounter: React.FC<IIconWithCounter> = ({ icon, count, countMax, ...rest }) => {
|
||||
|
|
|
@ -3,24 +3,21 @@
|
|||
* @module soapbox/components/icon
|
||||
*/
|
||||
|
||||
import clsx from 'clsx';
|
||||
import classNames from 'clsx';
|
||||
import React from 'react';
|
||||
import InlineSVG from 'react-inlinesvg'; // eslint-disable-line no-restricted-imports
|
||||
|
||||
export interface IIcon extends React.HTMLAttributes<HTMLDivElement> {
|
||||
src: string
|
||||
id?: string
|
||||
alt?: string
|
||||
className?: string
|
||||
src: string,
|
||||
id?: string,
|
||||
alt?: string,
|
||||
className?: string,
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use the UI Icon component directly.
|
||||
*/
|
||||
const Icon: React.FC<IIcon> = ({ src, alt, className, ...rest }) => {
|
||||
return (
|
||||
<div
|
||||
className={clsx('svg-icon', className)}
|
||||
className={classNames('svg-icon', className)}
|
||||
{...rest}
|
||||
>
|
||||
<InlineSVG src={src} title={alt} loader={<></>} />
|
||||
|
|
|
@ -2,7 +2,7 @@ import React from 'react';
|
|||
|
||||
/** Fullscreen gradient used as a backdrop to public pages. */
|
||||
const LandingGradient: React.FC = () => (
|
||||
<div className='fixed h-screen w-full bg-gradient-to-tr from-primary-50 via-white to-gradient-end/10 dark:from-primary-900/50 dark:via-primary-900 dark:to-primary-800/50' />
|
||||
<div className='fixed h-screen w-full bg-gradient-to-tr from-primary-50 dark:from-primary-900/50 via-white dark:via-primary-900 to-gradient-end/10 dark:to-primary-800/50' />
|
||||
);
|
||||
|
||||
export default LandingGradient;
|
||||
|
|
|
@ -4,7 +4,7 @@ import { Link as Comp, LinkProps } from 'react-router-dom';
|
|||
const Link = (props: LinkProps) => (
|
||||
<Comp
|
||||
{...props}
|
||||
className='text-primary-600 hover:underline dark:text-accent-blue'
|
||||
className='text-primary-600 dark:text-accent-blue hover:underline'
|
||||
/>
|
||||
);
|
||||
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
import clsx from 'clsx';
|
||||
import classNames from 'clsx';
|
||||
import React from 'react';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { SelectDropdown } from '../features/forms';
|
||||
|
||||
import { Icon, HStack, Select } from './ui';
|
||||
import Icon from './icon';
|
||||
import { HStack, Select } from './ui';
|
||||
|
||||
interface IList {
|
||||
children: React.ReactNode
|
||||
|
@ -15,9 +16,9 @@ const List: React.FC<IList> = ({ children }) => (
|
|||
);
|
||||
|
||||
interface IListItem {
|
||||
label: React.ReactNode
|
||||
hint?: React.ReactNode
|
||||
onClick?(): void
|
||||
label: React.ReactNode,
|
||||
hint?: React.ReactNode,
|
||||
onClick?(): void,
|
||||
onSelect?(): void
|
||||
isSelected?: boolean
|
||||
children?: React.ReactNode
|
||||
|
@ -44,7 +45,7 @@ const ListItem: React.FC<IListItem> = ({ label, hint, children, onClick, onSelec
|
|||
|
||||
return React.cloneElement(child, {
|
||||
id: domId,
|
||||
className: clsx({
|
||||
className: classNames({
|
||||
'w-auto': isSelect,
|
||||
}, child.props.className),
|
||||
});
|
||||
|
@ -56,14 +57,14 @@ const ListItem: React.FC<IListItem> = ({ label, hint, children, onClick, onSelec
|
|||
|
||||
return (
|
||||
<Comp
|
||||
className={clsx({
|
||||
'flex items-center justify-between px-4 py-2 first:rounded-t-lg last:rounded-b-lg bg-gradient-to-r from-gradient-start/20 to-gradient-end/20 dark:from-gradient-start/10 dark:to-gradient-end/10': true,
|
||||
'cursor-pointer hover:from-gradient-start/30 hover:to-gradient-end/30 dark:hover:from-gradient-start/5 dark:hover:to-gradient-end/5': typeof onClick !== 'undefined' || typeof onSelect !== 'undefined',
|
||||
className={classNames({
|
||||
'flex items-center justify-between px-3 py-2 first:rounded-t-lg last:rounded-b-lg bg-gradient-to-r from-gradient-start/10 to-gradient-end/10': true,
|
||||
'cursor-pointer hover:from-gradient-start/20 hover:to-gradient-end/20 dark:hover:from-gradient-start/5 dark:hover:to-gradient-end/5': typeof onClick !== 'undefined' || typeof onSelect !== 'undefined',
|
||||
})}
|
||||
{...linkProps}
|
||||
>
|
||||
<div className='flex flex-col py-1.5 pr-4 rtl:pl-4 rtl:pr-0'>
|
||||
<LabelComp className='font-medium text-gray-900 dark:text-gray-100' htmlFor={domId}>{label}</LabelComp>
|
||||
<LabelComp className='text-gray-900 dark:text-gray-100' htmlFor={domId}>{label}</LabelComp>
|
||||
|
||||
{hint ? (
|
||||
<span className='text-sm text-gray-700 dark:text-gray-600'>{hint}</span>
|
||||
|
@ -82,26 +83,9 @@ const ListItem: React.FC<IListItem> = ({ label, hint, children, onClick, onSelec
|
|||
<div className='flex flex-row items-center text-gray-700 dark:text-gray-600'>
|
||||
{children}
|
||||
|
||||
<div
|
||||
className={
|
||||
clsx({
|
||||
'flex h-6 w-6 items-center justify-center rounded-full border-2 border-solid border-primary-500 dark:border-primary-400 transition': true,
|
||||
'bg-primary-500 dark:bg-primary-400': isSelected,
|
||||
'bg-transparent': !isSelected,
|
||||
})
|
||||
}
|
||||
>
|
||||
<Icon
|
||||
src={require('@tabler/icons/check.svg')}
|
||||
className={
|
||||
clsx({
|
||||
'h-4 w-4 text-white dark:text-white transition-all duration-500': true,
|
||||
'opacity-0 scale-50': !isSelected,
|
||||
'opacity-100 scale-100': isSelected,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
{isSelected ? (
|
||||
<Icon src={require('@tabler/icons/check.svg')} className='ml-1 text-primary-500 dark:text-primary-400' />
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
|
|
|
@ -8,9 +8,9 @@ const messages = defineMessages({
|
|||
});
|
||||
|
||||
interface ILoadGap {
|
||||
disabled?: boolean
|
||||
maxId: string
|
||||
onClick: (id: string) => void
|
||||
disabled?: boolean,
|
||||
maxId: string,
|
||||
onClick: (id: string) => void,
|
||||
}
|
||||
|
||||
const LoadGap: React.FC<ILoadGap> = ({ disabled, maxId, onClick }) => {
|
||||
|
|
|
@ -4,19 +4,18 @@ import { FormattedMessage } from 'react-intl';
|
|||
import { Button } from 'soapbox/components/ui';
|
||||
|
||||
interface ILoadMore {
|
||||
onClick: React.MouseEventHandler
|
||||
disabled?: boolean
|
||||
visible?: boolean
|
||||
className?: string
|
||||
onClick: React.MouseEventHandler,
|
||||
disabled?: boolean,
|
||||
visible?: Boolean,
|
||||
}
|
||||
|
||||
const LoadMore: React.FC<ILoadMore> = ({ onClick, disabled, visible = true, className }) => {
|
||||
const LoadMore: React.FC<ILoadMore> = ({ onClick, disabled, visible = true }) => {
|
||||
if (!visible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Button className={className} theme='primary' block disabled={disabled || !visible} onClick={onClick}>
|
||||
<Button theme='primary' block disabled={disabled || !visible} onClick={onClick}>
|
||||
<FormattedMessage id='status.load_more' defaultMessage='Load more' />
|
||||
</Button>
|
||||
);
|
||||
|
|
|
@ -9,7 +9,7 @@ const LoadingScreen: React.FC = () => {
|
|||
<div className='fixed h-screen w-screen'>
|
||||
<LandingGradient />
|
||||
|
||||
<div className='d-screen fixed z-10 flex w-screen items-center justify-center'>
|
||||
<div className='fixed d-screen w-screen flex items-center justify-center z-10'>
|
||||
<div className='p-4'>
|
||||
<Spinner size={40} withText={false} />
|
||||
</div>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import clsx from 'clsx';
|
||||
import classNames from 'clsx';
|
||||
import { OrderedSet as ImmutableOrderedSet } from 'immutable';
|
||||
import throttle from 'lodash/throttle';
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
@ -18,7 +18,7 @@ const messages = defineMessages({
|
|||
});
|
||||
|
||||
interface ILocationSearch {
|
||||
onSelected: (locationId: string) => void
|
||||
onSelected: (locationId: string) => void,
|
||||
}
|
||||
|
||||
const LocationSearch: React.FC<ILocationSearch> = ({ onSelected }) => {
|
||||
|
@ -100,8 +100,8 @@ const LocationSearch: React.FC<ILocationSearch> = ({ onSelected }) => {
|
|||
renderSuggestion={AutosuggestLocation}
|
||||
/>
|
||||
<div role='button' tabIndex={0} className='search__icon' onClick={handleClear}>
|
||||
<Icon src={require('@tabler/icons/search.svg')} className={clsx('svg-icon--search', { active: isEmpty() })} />
|
||||
<Icon src={require('@tabler/icons/backspace.svg')} className={clsx('svg-icon--backspace', { active: !isEmpty() })} aria-label={intl.formatMessage(messages.placeholder)} />
|
||||
<Icon src={require('@tabler/icons/search.svg')} className={classNames('svg-icon--search', { active: isEmpty() })} />
|
||||
<Icon src={require('@tabler/icons/backspace.svg')} className={classNames('svg-icon--backspace', { active: !isEmpty() })} aria-label={intl.formatMessage(messages.placeholder)} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import clsx from 'clsx';
|
||||
import classNames from 'clsx';
|
||||
import React, { useState, useRef, useLayoutEffect } from 'react';
|
||||
|
||||
import Blurhash from 'soapbox/components/blurhash';
|
||||
|
@ -19,21 +19,21 @@ const ATTACHMENT_LIMIT = 4;
|
|||
const MAX_FILENAME_LENGTH = 45;
|
||||
|
||||
interface Dimensions {
|
||||
w: Property.Width | number
|
||||
h: Property.Height | number
|
||||
t?: Property.Top
|
||||
r?: Property.Right
|
||||
b?: Property.Bottom
|
||||
l?: Property.Left
|
||||
float?: Property.Float
|
||||
pos?: Property.Position
|
||||
w: Property.Width | number,
|
||||
h: Property.Height | number,
|
||||
t?: Property.Top,
|
||||
r?: Property.Right,
|
||||
b?: Property.Bottom,
|
||||
l?: Property.Left,
|
||||
float?: Property.Float,
|
||||
pos?: Property.Position,
|
||||
}
|
||||
|
||||
interface SizeData {
|
||||
style: React.CSSProperties
|
||||
itemsDimensions: Dimensions[]
|
||||
size: number
|
||||
width: number
|
||||
style: React.CSSProperties,
|
||||
itemsDimensions: Dimensions[],
|
||||
size: number,
|
||||
width: number,
|
||||
}
|
||||
|
||||
const withinLimits = (aspectRatio: number) => {
|
||||
|
@ -48,16 +48,16 @@ const shouldLetterbox = (attachment: Attachment): boolean => {
|
|||
};
|
||||
|
||||
interface IItem {
|
||||
attachment: Attachment
|
||||
standalone?: boolean
|
||||
index: number
|
||||
size: number
|
||||
onClick: (index: number) => void
|
||||
displayWidth?: number
|
||||
visible: boolean
|
||||
dimensions: Dimensions
|
||||
last?: boolean
|
||||
total: number
|
||||
attachment: Attachment,
|
||||
standalone?: boolean,
|
||||
index: number,
|
||||
size: number,
|
||||
onClick: (index: number) => void,
|
||||
displayWidth?: number,
|
||||
visible: boolean,
|
||||
dimensions: Dimensions,
|
||||
last?: boolean,
|
||||
total: number,
|
||||
}
|
||||
|
||||
const Item: React.FC<IItem> = ({
|
||||
|
@ -152,14 +152,7 @@ const Item: React.FC<IItem> = ({
|
|||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx('media-gallery__item', {
|
||||
standalone,
|
||||
'rounded-md': total > 1,
|
||||
})}
|
||||
key={attachment.id}
|
||||
style={{ position, float, left, top, right, bottom, height, width: `${width}%` }}
|
||||
>
|
||||
<div className={classNames('media-gallery__item', { standalone })} key={attachment.id} style={{ position, float, left, top, right, bottom, height, width: `${width}%` }}>
|
||||
<a className='media-gallery__item-thumbnail' href={attachment.url} target='_blank' style={{ cursor: 'pointer' }}>
|
||||
<Blurhash hash={attachment.blurhash} className='media-gallery__preview' />
|
||||
<span className='media-gallery__item__icons'>{attachmentIcon}</span>
|
||||
|
@ -178,7 +171,7 @@ const Item: React.FC<IItem> = ({
|
|||
target='_blank'
|
||||
>
|
||||
<StillImage
|
||||
className='h-full w-full'
|
||||
className='w-full h-full'
|
||||
src={mediaPreview ? attachment.preview_url : attachment.url}
|
||||
alt={attachment.description}
|
||||
letterboxed={letterboxed}
|
||||
|
@ -196,7 +189,7 @@ const Item: React.FC<IItem> = ({
|
|||
}
|
||||
|
||||
thumbnail = (
|
||||
<div className={clsx('media-gallery__gifv', { autoplay: autoPlayGif })}>
|
||||
<div className={classNames('media-gallery__gifv', { autoplay: autoPlayGif })}>
|
||||
<video
|
||||
className='media-gallery__item-gifv-thumbnail'
|
||||
aria-label={attachment.description}
|
||||
|
@ -218,7 +211,7 @@ const Item: React.FC<IItem> = ({
|
|||
const ext = attachment.url.split('.').pop()?.toUpperCase();
|
||||
thumbnail = (
|
||||
<a
|
||||
className={clsx('media-gallery__item-thumbnail')}
|
||||
className={classNames('media-gallery__item-thumbnail')}
|
||||
href={attachment.url}
|
||||
onClick={handleClick}
|
||||
target='_blank'
|
||||
|
@ -232,7 +225,7 @@ const Item: React.FC<IItem> = ({
|
|||
const ext = attachment.url.split('.').pop()?.toUpperCase();
|
||||
thumbnail = (
|
||||
<a
|
||||
className={clsx('media-gallery__item-thumbnail')}
|
||||
className={classNames('media-gallery__item-thumbnail')}
|
||||
href={attachment.url}
|
||||
onClick={handleClick}
|
||||
target='_blank'
|
||||
|
@ -252,14 +245,7 @@ const Item: React.FC<IItem> = ({
|
|||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx('media-gallery__item', `media-gallery__item--${attachment.type}`, {
|
||||
standalone,
|
||||
'rounded-md': total > 1,
|
||||
})}
|
||||
key={attachment.id}
|
||||
style={{ position, float, left, top, right, bottom, height, width: `${width}%` }}
|
||||
>
|
||||
<div className={classNames('media-gallery__item', `media-gallery__item--${attachment.type}`, { standalone })} key={attachment.id} style={{ position, float, left, top, right, bottom, height, width: `${width}%` }}>
|
||||
{last && total > ATTACHMENT_LIMIT && (
|
||||
<div className='media-gallery__item-overflow'>
|
||||
+{total - ATTACHMENT_LIMIT + 1}
|
||||
|
@ -274,25 +260,23 @@ const Item: React.FC<IItem> = ({
|
|||
);
|
||||
};
|
||||
|
||||
export interface IMediaGallery {
|
||||
sensitive?: boolean
|
||||
media: ImmutableList<Attachment>
|
||||
height?: number
|
||||
onOpenMedia: (media: ImmutableList<Attachment>, index: number) => void
|
||||
defaultWidth?: number
|
||||
cacheWidth?: (width: number) => void
|
||||
visible?: boolean
|
||||
onToggleVisibility?: () => void
|
||||
displayMedia?: string
|
||||
compact?: boolean
|
||||
className?: string
|
||||
interface IMediaGallery {
|
||||
sensitive?: boolean,
|
||||
media: ImmutableList<Attachment>,
|
||||
height?: number,
|
||||
onOpenMedia: (media: ImmutableList<Attachment>, index: number) => void,
|
||||
defaultWidth?: number,
|
||||
cacheWidth?: (width: number) => void,
|
||||
visible?: boolean,
|
||||
onToggleVisibility?: () => void,
|
||||
displayMedia?: string,
|
||||
compact: boolean,
|
||||
}
|
||||
|
||||
const MediaGallery: React.FC<IMediaGallery> = (props) => {
|
||||
const {
|
||||
media,
|
||||
defaultWidth = 0,
|
||||
className,
|
||||
onOpenMedia,
|
||||
cacheWidth,
|
||||
compact,
|
||||
|
@ -562,11 +546,7 @@ const MediaGallery: React.FC<IMediaGallery> = (props) => {
|
|||
}, [node.current]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx(className, 'media-gallery', { 'media-gallery--compact': compact })}
|
||||
style={sizeData.style}
|
||||
ref={node}
|
||||
>
|
||||
<div className={classNames('media-gallery', { 'media-gallery--compact': compact })} style={sizeData.style} ref={node}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import clsx from 'clsx';
|
||||
import classNames from 'clsx';
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import 'wicg-inert';
|
||||
import { FormattedMessage, defineMessages, useIntl } from 'react-intl';
|
||||
|
@ -39,10 +39,10 @@ export const checkEventComposeContent = (compose?: ReturnType<typeof ReducerComp
|
|||
};
|
||||
|
||||
interface IModalRoot {
|
||||
onCancel?: () => void
|
||||
onClose: (type?: ModalType) => void
|
||||
type: ModalType
|
||||
children: React.ReactNode
|
||||
onCancel?: () => void,
|
||||
onClose: (type?: ModalType) => void,
|
||||
type: ModalType,
|
||||
children: React.ReactNode,
|
||||
}
|
||||
|
||||
const ModalRoot: React.FC<IModalRoot> = ({ children, onCancel, onClose, type }) => {
|
||||
|
@ -232,7 +232,7 @@ const ModalRoot: React.FC<IModalRoot> = ({ children, onCancel, onClose, type })
|
|||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={clsx({
|
||||
className={classNames({
|
||||
'fixed top-0 left-0 z-[100] w-full h-full overflow-x-hidden overflow-y-auto': true,
|
||||
'pointer-events-none': !visible,
|
||||
})}
|
||||
|
@ -241,13 +241,13 @@ const ModalRoot: React.FC<IModalRoot> = ({ children, onCancel, onClose, type })
|
|||
<div
|
||||
role='presentation'
|
||||
id='modal-overlay'
|
||||
className='fixed inset-0 bg-gray-500/90 backdrop-blur dark:bg-gray-700/90'
|
||||
className='fixed inset-0 bg-gray-500/90 dark:bg-gray-700/90 backdrop-blur'
|
||||
onClick={handleOnClose}
|
||||
/>
|
||||
|
||||
<div
|
||||
role='dialog'
|
||||
className={clsx({
|
||||
className={classNames({
|
||||
'my-2 mx-auto relative pointer-events-none flex items-center min-h-[calc(100%-3.5rem)]': true,
|
||||
'p-4 md:p-0': type !== 'MEDIA',
|
||||
})}
|
||||
|
|
|
@ -1,16 +1,16 @@
|
|||
import clsx from 'clsx';
|
||||
import classNames from 'clsx';
|
||||
import React from 'react';
|
||||
|
||||
interface IOutlineBox extends React.HTMLAttributes<HTMLDivElement> {
|
||||
children: React.ReactNode
|
||||
className?: string
|
||||
children: React.ReactNode,
|
||||
className?: string,
|
||||
}
|
||||
|
||||
/** Wraps children in a container with an outline. */
|
||||
const OutlineBox: React.FC<IOutlineBox> = ({ children, className, ...rest }) => {
|
||||
return (
|
||||
<div
|
||||
className={clsx('rounded-lg border border-solid border-gray-300 p-4 dark:border-gray-800', className)}
|
||||
className={classNames('p-4 rounded-lg border border-solid border-gray-300 dark:border-gray-800', className)}
|
||||
{...rest}
|
||||
>
|
||||
{children}
|
||||
|
@ -18,4 +18,4 @@ const OutlineBox: React.FC<IOutlineBox> = ({ children, className, ...rest }) =>
|
|||
);
|
||||
};
|
||||
|
||||
export default OutlineBox;
|
||||
export default OutlineBox;
|
|
@ -1,54 +0,0 @@
|
|||
import clsx from 'clsx';
|
||||
import React from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { HStack, Icon, Text } from 'soapbox/components/ui';
|
||||
|
||||
interface IPendingItemsRow {
|
||||
/** Path to navigate the user when clicked. */
|
||||
to: string
|
||||
/** Number of pending items. */
|
||||
count: number
|
||||
/** Size of the icon. */
|
||||
size?: 'md' | 'lg'
|
||||
}
|
||||
|
||||
const PendingItemsRow: React.FC<IPendingItemsRow> = ({ to, count, size = 'md' }) => {
|
||||
return (
|
||||
<Link to={to} className='group' data-testid='pending-items-row'>
|
||||
<HStack alignItems='center' justifyContent='between'>
|
||||
<HStack alignItems='center' space={2}>
|
||||
<div className={clsx('rounded-full bg-primary-200 text-primary-500 dark:bg-primary-800 dark:text-primary-200', {
|
||||
'p-3': size === 'lg',
|
||||
'p-2.5': size === 'md',
|
||||
})}
|
||||
>
|
||||
<Icon
|
||||
src={require('@tabler/icons/exclamation-circle.svg')}
|
||||
className={clsx({
|
||||
'h-5 w-5': size === 'md',
|
||||
'h-7 w-7': size === 'lg',
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Text weight='bold' size='md'>
|
||||
<FormattedMessage
|
||||
id='groups.pending.count'
|
||||
defaultMessage='{number, plural, one {# pending request} other {# pending requests}}'
|
||||
values={{ number: count }}
|
||||
/>
|
||||
</Text>
|
||||
</HStack>
|
||||
|
||||
<Icon
|
||||
src={require('@tabler/icons/chevron-right.svg')}
|
||||
className='h-5 w-5 text-gray-600 transition-colors group-hover:text-gray-700 dark:text-gray-600 dark:group-hover:text-gray-500'
|
||||
/>
|
||||
</HStack>
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
||||
export { PendingItemsRow };
|
|
@ -16,9 +16,9 @@ const messages = defineMessages({
|
|||
});
|
||||
|
||||
interface IPollFooter {
|
||||
poll: PollEntity
|
||||
showResults: boolean
|
||||
selected: Selected
|
||||
poll: PollEntity,
|
||||
showResults: boolean,
|
||||
selected: Selected,
|
||||
}
|
||||
|
||||
const PollFooter: React.FC<IPollFooter> = ({ poll, showResults, selected }): JSX.Element => {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import clsx from 'clsx';
|
||||
import classNames from 'clsx';
|
||||
import React from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { Motion, presets, spring } from 'react-motion';
|
||||
|
@ -20,7 +20,7 @@ const PollPercentageBar: React.FC<{ percent: number, leading: boolean }> = ({ pe
|
|||
<Motion defaultStyle={{ width: 0 }} style={{ width: spring(percent, { ...presets.gentle, precision: 0.1 }) }}>
|
||||
{({ width }) => (
|
||||
<span
|
||||
className='absolute inset-0 inline-block h-full rounded-l-md bg-primary-100 dark:bg-primary-500'
|
||||
className='absolute inset-0 h-full inline-block bg-primary-100 dark:bg-primary-500 rounded-l-md'
|
||||
style={{ width: `${width}%` }}
|
||||
/>
|
||||
)}
|
||||
|
@ -29,7 +29,7 @@ const PollPercentageBar: React.FC<{ percent: number, leading: boolean }> = ({ pe
|
|||
};
|
||||
|
||||
interface IPollOptionText extends IPollOption {
|
||||
percent: number
|
||||
percent: number,
|
||||
}
|
||||
|
||||
const PollOptionText: React.FC<IPollOptionText> = ({ poll, option, index, active, onToggle }) => {
|
||||
|
@ -46,7 +46,7 @@ const PollOptionText: React.FC<IPollOptionText> = ({ poll, option, index, active
|
|||
return (
|
||||
<label
|
||||
className={
|
||||
clsx('relative flex cursor-pointer rounded-3xl border border-solid bg-white p-2 hover:bg-primary-50 dark:bg-primary-900 dark:hover:bg-primary-800/50', {
|
||||
classNames('flex relative p-2 bg-white dark:bg-primary-900 cursor-pointer rounded-3xl border border-solid hover:bg-primary-50 dark:hover:bg-primary-800/50', {
|
||||
'border-primary-600 ring-1 ring-primary-600 bg-primary-50 dark:bg-primary-800/50 dark:border-primary-300 dark:ring-primary-300': active,
|
||||
'border-primary-300 dark:border-primary-500': !active,
|
||||
})
|
||||
|
@ -61,8 +61,8 @@ const PollOptionText: React.FC<IPollOptionText> = ({ poll, option, index, active
|
|||
onChange={handleOptionChange}
|
||||
/>
|
||||
|
||||
<div className='grid w-full items-center'>
|
||||
<div className='col-start-1 row-start-1 ml-4 mr-6 justify-self-center'>
|
||||
<div className='grid items-center w-full'>
|
||||
<div className='col-start-1 row-start-1 justify-self-center ml-4 mr-6'>
|
||||
<div className='text-primary-600 dark:text-white'>
|
||||
<Text
|
||||
theme='inherit'
|
||||
|
@ -72,9 +72,9 @@ const PollOptionText: React.FC<IPollOptionText> = ({ poll, option, index, active
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div className='col-start-1 row-start-1 flex items-center justify-self-end'>
|
||||
<div className='col-start-1 row-start-1 justify-self-end flex items-center'>
|
||||
<span
|
||||
className={clsx('flex h-6 w-6 flex-none items-center justify-center rounded-full border border-solid', {
|
||||
className={classNames('flex items-center justify-center w-6 h-6 flex-none border border-solid rounded-full', {
|
||||
'bg-primary-600 border-primary-600 dark:bg-primary-300 dark:border-primary-300': active,
|
||||
'border-primary-300 bg-white dark:bg-primary-900 dark:border-primary-500': !active,
|
||||
})}
|
||||
|
@ -85,7 +85,7 @@ const PollOptionText: React.FC<IPollOptionText> = ({ poll, option, index, active
|
|||
aria-label={option.title}
|
||||
>
|
||||
{active && (
|
||||
<Icon src={require('@tabler/icons/check.svg')} className='h-4 w-4 text-white dark:text-primary-900' />
|
||||
<Icon src={require('@tabler/icons/check.svg')} className='text-white dark:text-primary-900 w-4 h-4' />
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
|
@ -95,12 +95,12 @@ const PollOptionText: React.FC<IPollOptionText> = ({ poll, option, index, active
|
|||
};
|
||||
|
||||
interface IPollOption {
|
||||
poll: PollEntity
|
||||
option: PollOptionEntity
|
||||
index: number
|
||||
showResults?: boolean
|
||||
active: boolean
|
||||
onToggle: (value: number) => void
|
||||
poll: PollEntity,
|
||||
option: PollOptionEntity,
|
||||
index: number,
|
||||
showResults?: boolean,
|
||||
active: boolean,
|
||||
onToggle: (value: number) => void,
|
||||
}
|
||||
|
||||
const PollOption: React.FC<IPollOption> = (props): JSX.Element | null => {
|
||||
|
@ -123,7 +123,7 @@ const PollOption: React.FC<IPollOption> = (props): JSX.Element | null => {
|
|||
<HStack
|
||||
justifyContent='between'
|
||||
alignItems='center'
|
||||
className='relative w-full overflow-hidden rounded-md bg-white p-2 dark:bg-primary-800'
|
||||
className='relative p-2 w-full bg-white dark:bg-primary-800 rounded-md overflow-hidden'
|
||||
>
|
||||
<PollPercentageBar percent={percent} leading={leading} />
|
||||
|
||||
|
@ -141,7 +141,7 @@ const PollOption: React.FC<IPollOption> = (props): JSX.Element | null => {
|
|||
<Icon
|
||||
src={require('@tabler/icons/circle-check.svg')}
|
||||
alt={intl.formatMessage(messages.voted)}
|
||||
className='h-4 w-4 text-primary-600 dark:fill-white dark:text-primary-800'
|
||||
className='text-primary-600 dark:text-primary-800 dark:fill-white w-4 h-4'
|
||||
/>
|
||||
) : (
|
||||
<div className='svg-icon' />
|
||||
|
|
|
@ -13,8 +13,8 @@ import PollOption from './poll-option';
|
|||
export type Selected = Record<number, boolean>;
|
||||
|
||||
interface IPoll {
|
||||
id: string
|
||||
status?: string
|
||||
id: string,
|
||||
status?: string,
|
||||
}
|
||||
|
||||
const messages = defineMessages({
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import clsx from 'clsx';
|
||||
import classNames from 'clsx';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useIntl, FormattedMessage } from 'react-intl';
|
||||
import { usePopper } from 'react-popper';
|
||||
|
@ -54,7 +54,7 @@ const handleMouseLeave = (dispatch: AppDispatch): React.MouseEventHandler => {
|
|||
};
|
||||
|
||||
interface IProfileHoverCard {
|
||||
visible: boolean
|
||||
visible: boolean,
|
||||
}
|
||||
|
||||
/** Popup profile preview that appears when hovering avatars and display names. */
|
||||
|
@ -95,7 +95,7 @@ export const ProfileHoverCard: React.FC<IProfileHoverCard> = ({ visible = true }
|
|||
|
||||
return (
|
||||
<div
|
||||
className={clsx({
|
||||
className={classNames({
|
||||
'absolute transition-opacity w-[320px] z-[101] top-0 left-0': true,
|
||||
'opacity-100': visible,
|
||||
'opacity-0 pointer-events-none': !visible,
|
||||
|
@ -123,7 +123,7 @@ export const ProfileHoverCard: React.FC<IProfileHoverCard> = ({ visible = true }
|
|||
<HStack alignItems='center' space={0.5}>
|
||||
<Icon
|
||||
src={require('@tabler/icons/calendar.svg')}
|
||||
className='h-4 w-4 text-gray-800 dark:text-gray-200'
|
||||
className='w-4 h-4 text-gray-800 dark:text-gray-200'
|
||||
/>
|
||||
|
||||
<Text size='sm'>
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import clsx from 'clsx';
|
||||
import classNames from 'clsx';
|
||||
import React from 'react';
|
||||
|
||||
interface IProgressCircle {
|
||||
progress: number
|
||||
radius?: number
|
||||
stroke?: number
|
||||
title?: string
|
||||
progress: number,
|
||||
radius?: number,
|
||||
stroke?: number,
|
||||
title?: string,
|
||||
}
|
||||
|
||||
const ProgressCircle: React.FC<IProgressCircle> = ({ progress, radius = 12, stroke = 4, title }) => {
|
||||
|
@ -30,7 +30,7 @@ const ProgressCircle: React.FC<IProgressCircle> = ({ progress, radius = 12, stro
|
|||
strokeWidth={stroke}
|
||||
/>
|
||||
<circle
|
||||
className={clsx('stroke-primary-500', {
|
||||
className={classNames('stroke-primary-500', {
|
||||
'stroke-secondary-500': progress > 1,
|
||||
})}
|
||||
style={{
|
||||
|
|
|
@ -4,10 +4,10 @@ import PTRComponent from 'react-simple-pull-to-refresh';
|
|||
import { Spinner } from 'soapbox/components/ui';
|
||||
|
||||
interface IPullToRefresh {
|
||||
onRefresh?: () => Promise<any>
|
||||
refreshingContent?: JSX.Element | string
|
||||
pullingContent?: JSX.Element | string
|
||||
children: React.ReactNode
|
||||
onRefresh?: () => Promise<any>;
|
||||
refreshingContent?: JSX.Element | string;
|
||||
pullingContent?: JSX.Element | string;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -3,7 +3,7 @@ import React from 'react';
|
|||
import PullToRefresh from './pull-to-refresh';
|
||||
|
||||
interface IPullable {
|
||||
children: React.ReactNode
|
||||
children: React.ReactNode,
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import clsx from 'clsx';
|
||||
import classNames from 'clsx';
|
||||
import React, { MouseEventHandler, useEffect, useRef, useState } from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
|
@ -23,11 +23,11 @@ const messages = defineMessages({
|
|||
|
||||
interface IQuotedStatus {
|
||||
/** The quoted status entity. */
|
||||
status?: StatusEntity
|
||||
status?: StatusEntity,
|
||||
/** Callback when cancelled (during compose). */
|
||||
onCancel?: Function
|
||||
onCancel?: Function,
|
||||
/** Whether the status is shown in the post composer. */
|
||||
compose?: boolean
|
||||
compose?: boolean,
|
||||
}
|
||||
|
||||
/** Status embedded in a quote post. */
|
||||
|
@ -94,7 +94,7 @@ const QuotedStatus: React.FC<IQuotedStatus> = ({ status, onCancel, compose }) =>
|
|||
return (
|
||||
<OutlineBox
|
||||
data-testid='quoted-status'
|
||||
className={clsx('cursor-pointer', {
|
||||
className={classNames('cursor-pointer', {
|
||||
'hover:bg-gray-100 dark:hover:bg-gray-800': !compose,
|
||||
})}
|
||||
>
|
||||
|
@ -133,7 +133,7 @@ const QuotedStatus: React.FC<IQuotedStatus> = ({ status, onCancel, compose }) =>
|
|||
collapsable
|
||||
/>
|
||||
|
||||
{status.media_attachments.size > 0 && (
|
||||
{(status.card || status.media_attachments.size > 0) && (
|
||||
<StatusMedia
|
||||
status={status}
|
||||
muted={compose}
|
||||
|
|
|
@ -16,11 +16,11 @@ const RadioGroup = ({ onChange, children }: IRadioGroup) => {
|
|||
};
|
||||
|
||||
interface IRadioItem {
|
||||
label: React.ReactNode
|
||||
hint?: React.ReactNode
|
||||
value: string
|
||||
checked: boolean
|
||||
onChange?: React.ChangeEventHandler
|
||||
label: React.ReactNode,
|
||||
hint?: React.ReactNode,
|
||||
value: string,
|
||||
checked: boolean,
|
||||
onChange?: React.ChangeEventHandler,
|
||||
}
|
||||
|
||||
const RadioItem: React.FC<IRadioItem> = ({ label, hint, checked = false, onChange, value }) => {
|
||||
|
|
|
@ -113,14 +113,14 @@ const timeRemainingString = (intl: IntlShape, date: Date, now: number) => {
|
|||
};
|
||||
|
||||
interface RelativeTimestampProps extends IText {
|
||||
intl: IntlShape
|
||||
timestamp: string
|
||||
year?: number
|
||||
futureDate?: boolean
|
||||
intl: IntlShape,
|
||||
timestamp: string,
|
||||
year?: number,
|
||||
futureDate?: boolean,
|
||||
}
|
||||
|
||||
interface RelativeTimestampState {
|
||||
now: number
|
||||
now: number,
|
||||
}
|
||||
|
||||
/** Displays a timestamp compared to the current time, eg "1m" for one minute ago. */
|
||||
|
|
|
@ -2,13 +2,13 @@ import React, { useCallback, useEffect, useRef, useState } from 'react';
|
|||
|
||||
interface ISafeEmbed {
|
||||
/** Styles for the outer frame element. */
|
||||
className?: string
|
||||
className?: string,
|
||||
/** Space-separate list of restrictions to ALLOW for the iframe. */
|
||||
sandbox?: string
|
||||
sandbox?: string,
|
||||
/** Unique title for the iframe. */
|
||||
title: string
|
||||
title: string,
|
||||
/** HTML body to embed. */
|
||||
html?: string
|
||||
html?: string,
|
||||
}
|
||||
|
||||
/** Safely embeds arbitrary HTML content on the page (by putting it in an iframe). */
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Ładowanie…
Reference in New Issue