diff --git a/.env.production.sample b/.env.production.sample index 3dd66abae4..1faaf5b57c 100644 --- a/.env.production.sample +++ b/.env.production.sample @@ -50,7 +50,7 @@ OTP_SECRET= # Must be available (and set to same values) for all server processes # These are private/secret values, do not share outside hosting environment # Use `bin/rails db:encryption:init` to generate fresh secrets -# Do not change these secrets once in use, as this would cause data loss and other issues +# Do NOT change these secrets once in use, as this would cause data loss and other issues # ------------------ # ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY= # ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT= diff --git a/Gemfile.lock b/Gemfile.lock index 80c733d202..c67516feb2 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -94,20 +94,20 @@ GEM ast (2.4.2) attr_required (1.0.2) aws-eventstream (1.3.0) - aws-partitions (1.1038.0) - aws-sdk-core (3.216.0) + aws-partitions (1.1032.0) + aws-sdk-core (3.214.1) aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.992.0) aws-sigv4 (~> 1.9) jmespath (~> 1, >= 1.6.1) - aws-sdk-kms (1.97.0) - aws-sdk-core (~> 3, >= 3.216.0) + aws-sdk-kms (1.96.0) + aws-sdk-core (~> 3, >= 3.210.0) aws-sigv4 (~> 1.5) - aws-sdk-s3 (1.178.0) - aws-sdk-core (~> 3, >= 3.216.0) + aws-sdk-s3 (1.177.0) + aws-sdk-core (~> 3, >= 3.210.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.5) - aws-sigv4 (1.11.0) + aws-sigv4 (1.10.1) aws-eventstream (~> 1, >= 1.0.2) azure-blob (0.5.4) rexml @@ -283,7 +283,7 @@ GEM hashie (5.0.0) hcaptcha (7.1.0) json - highline (3.1.1) + highline (3.1.2) reline hiredis (0.6.3) hkdf (0.3.0) @@ -302,7 +302,7 @@ GEM httplog (1.7.0) rack (>= 2.0) rainbow (>= 2.0.0) - i18n (1.14.6) + i18n (1.14.7) concurrent-ruby (~> 1.0) i18n-tasks (1.0.14) activesupport (>= 4.0.2) @@ -319,7 +319,8 @@ GEM activesupport (>= 3.0) nokogiri (>= 1.6) io-console (0.8.0) - irb (1.14.3) + irb (1.15.1) + pp (>= 0.6.0) rdoc (>= 4.0.0) reline (>= 0.4.2) jd-paperclip-azure (3.0.0) @@ -384,7 +385,7 @@ GEM llhttp-ffi (0.5.0) ffi-compiler (~> 1.0) rake (~> 13.0) - logger (1.6.4) + logger (1.6.5) lograge (0.14.0) actionpack (>= 4) activesupport (>= 4) @@ -406,7 +407,7 @@ GEM mime-types (3.6.0) logger mime-types-data (~> 3.2015) - mime-types-data (3.2024.1203) + mime-types-data (3.2025.0107) mini_mime (1.1.5) mini_portile2 (2.8.8) minitest (5.25.4) @@ -415,7 +416,7 @@ GEM mutex_m (0.3.0) net-http (0.6.0) uri - net-imap (0.5.4) + net-imap (0.5.5) date net-protocol net-ldap (0.19.0) @@ -426,7 +427,7 @@ GEM net-smtp (0.5.0) net-protocol nio4r (2.7.4) - nokogiri (1.18.1) + nokogiri (1.18.2) mini_portile2 (~> 2.8.2) racc (~> 1.4) oj (3.16.9) @@ -559,7 +560,7 @@ GEM ox (2.14.20) bigdecimal (>= 3.0) parallel (1.26.3) - parser (3.3.6.0) + parser (3.3.7.0) ast (~> 2.4.1) racc parslet (2.0.0) @@ -568,6 +569,8 @@ GEM pg (1.5.9) pghero (3.6.1) activerecord (>= 6.1) + pp (0.6.2) + prettyprint premailer (1.27.0) addressable css_parser (>= 1.19.0) @@ -576,12 +579,13 @@ GEM actionmailer (>= 3) net-smtp premailer (~> 1.7, >= 1.7.9) + prettyprint (0.2.0) propshaft (1.1.0) actionpack (>= 7.0.0) activesupport (>= 7.0.0) rack railties (>= 7.0.0) - psych (5.2.2) + psych (5.2.3) date stringio public_suffix (6.0.1) @@ -658,7 +662,7 @@ GEM link_header (~> 0.0, >= 0.0.8) rdf-normalize (0.7.0) rdf (~> 3.3) - rdoc (6.10.0) + rdoc (6.11.0) psych (>= 4.0.0) redcarpet (3.6.0) redis (4.8.1) @@ -732,7 +736,7 @@ GEM rack (>= 1.1) rubocop (>= 1.52.0, < 2.0) rubocop-ast (>= 1.31.1, < 2.0) - rubocop-rspec (3.3.0) + rubocop-rspec (3.4.0) rubocop (~> 1.61) rubocop-rspec_rails (2.30.0) rubocop (~> 1.61) @@ -757,7 +761,7 @@ GEM activerecord (>= 4.0.0) railties (>= 4.0.0) securerandom (0.4.1) - selenium-webdriver (4.27.0) + selenium-webdriver (4.28.0) base64 (~> 0.2) logger (~> 1.4) rexml (~> 3.2, >= 3.2.5) @@ -833,7 +837,7 @@ GEM unf (~> 0.1.0) tzinfo (2.0.6) concurrent-ruby (~> 1.0) - tzinfo-data (1.2024.2) + tzinfo-data (1.2025.1) tzinfo (>= 1.0.0) unf (0.1.4) unf_ext @@ -871,7 +875,8 @@ GEM semantic_range (>= 2.3.0) webrick (1.9.1) websocket (1.2.11) - websocket-driver (0.7.6) + websocket-driver (0.7.7) + base64 websocket-extensions (>= 0.1.0) websocket-extensions (0.1.5) wisper (2.0.1) @@ -1038,4 +1043,4 @@ RUBY VERSION ruby 3.4.1p0 BUNDLED WITH - 2.6.2 + 2.6.3 diff --git a/app/controllers/api/v1/accounts/credentials_controller.rb b/app/controllers/api/v1/accounts/credentials_controller.rb index 7488fdec7c..032e42e9d2 100644 --- a/app/controllers/api/v1/accounts/credentials_controller.rb +++ b/app/controllers/api/v1/accounts/credentials_controller.rb @@ -34,6 +34,7 @@ class Api::V1::Accounts::CredentialsController < Api::BaseController :searchability, :hide_collections, :indexable, + attribution_domains: [], fields_attributes: [:name, :value] ) end diff --git a/app/controllers/api/v1/push/subscriptions_controller.rb b/app/controllers/api/v1/push/subscriptions_controller.rb index d74b5d958f..f2c52f2846 100644 --- a/app/controllers/api/v1/push/subscriptions_controller.rb +++ b/app/controllers/api/v1/push/subscriptions_controller.rb @@ -56,12 +56,12 @@ class Api::V1::Push::SubscriptionsController < Api::BaseController end def subscription_params - params.require(:subscription).permit(:endpoint, :standard, keys: [:auth, :p256dh]) + params.expect(subscription: [:endpoint, :standard, keys: [:auth, :p256dh]]) end def data_params return {} if params[:data].blank? - params.require(:data).permit(:policy, alerts: Notification::TYPES) + params.expect(data: [:policy, alerts: Notification::TYPES]) end end diff --git a/app/controllers/api/web/push_subscriptions_controller.rb b/app/controllers/api/web/push_subscriptions_controller.rb index 7eb51c6846..2711071b4a 100644 --- a/app/controllers/api/web/push_subscriptions_controller.rb +++ b/app/controllers/api/web/push_subscriptions_controller.rb @@ -66,7 +66,7 @@ class Api::Web::PushSubscriptionsController < Api::Web::BaseController end def subscription_params - @subscription_params ||= params.require(:subscription).permit(:standard, :endpoint, keys: [:auth, :p256dh]) + @subscription_params ||= params.expect(subscription: [:standard, :endpoint, keys: [:auth, :p256dh]]) end def web_push_subscription_params @@ -82,6 +82,6 @@ class Api::Web::PushSubscriptionsController < Api::Web::BaseController end def data_params - @data_params ||= params.require(:data).permit(:policy, alerts: Notification::TYPES) + @data_params ||= params.expect(data: [:policy, alerts: Notification::TYPES]) end end diff --git a/app/controllers/auth/setup_controller.rb b/app/controllers/auth/setup_controller.rb index 376a30c16f..5e7b14646a 100644 --- a/app/controllers/auth/setup_controller.rb +++ b/app/controllers/auth/setup_controller.rb @@ -35,6 +35,6 @@ class Auth::SetupController < ApplicationController end def user_params - params.require(:user).permit(:email) + params.expect(user: [:email]) end end diff --git a/app/controllers/disputes/appeals_controller.rb b/app/controllers/disputes/appeals_controller.rb index 98b58d2117..797f31cf78 100644 --- a/app/controllers/disputes/appeals_controller.rb +++ b/app/controllers/disputes/appeals_controller.rb @@ -21,6 +21,6 @@ class Disputes::AppealsController < Disputes::BaseController end def appeal_params - params.require(:appeal).permit(:text) + params.expect(appeal: [:text]) end end diff --git a/app/controllers/filters_controller.rb b/app/controllers/filters_controller.rb index 7746db049f..5c3968dea7 100644 --- a/app/controllers/filters_controller.rb +++ b/app/controllers/filters_controller.rb @@ -48,7 +48,7 @@ class FiltersController < ApplicationController end def resource_params - params.require(:custom_filter).permit(:title, :expires_in, :filter_action, :exclude_follows, :exclude_localusers, :exclude_quote, :exclude_profile, context: [], keywords_attributes: [:id, :keyword, :whole_word, :_destroy]) + params.expect(custom_filter: [:title, :expires_in, :filter_action, :exclude_follows, :exclude_localusers, :exclude_quote, :exclude_profile, context: [], keywords_attributes: [:id, :keyword, :whole_word, :_destroy]]) end def set_cache_headers diff --git a/app/controllers/invites_controller.rb b/app/controllers/invites_controller.rb index 070852695e..c4c52cce11 100644 --- a/app/controllers/invites_controller.rb +++ b/app/controllers/invites_controller.rb @@ -43,7 +43,7 @@ class InvitesController < ApplicationController end def resource_params - params.require(:invite).permit(:max_uses, :expires_in, :autofollow, :comment) + params.expect(invite: [:max_uses, :expires_in, :autofollow, :comment]) end def set_cache_headers diff --git a/app/controllers/settings/aliases_controller.rb b/app/controllers/settings/aliases_controller.rb index a421b8ede3..c21d43eeb3 100644 --- a/app/controllers/settings/aliases_controller.rb +++ b/app/controllers/settings/aliases_controller.rb @@ -30,7 +30,7 @@ class Settings::AliasesController < Settings::BaseController private def resource_params - params.require(:account_alias).permit(:acct) + params.expect(account_alias: [:acct]) end def set_alias diff --git a/app/controllers/settings/deletes_controller.rb b/app/controllers/settings/deletes_controller.rb index 16c201b6b3..815d95ad83 100644 --- a/app/controllers/settings/deletes_controller.rb +++ b/app/controllers/settings/deletes_controller.rb @@ -21,7 +21,7 @@ class Settings::DeletesController < Settings::BaseController private def resource_params - params.require(:form_delete_confirmation).permit(:password, :username) + params.expect(form_delete_confirmation: [:password, :username]) end def require_not_suspended! diff --git a/app/controllers/settings/featured_tags_controller.rb b/app/controllers/settings/featured_tags_controller.rb index 7e29dd1d29..0f352e1913 100644 --- a/app/controllers/settings/featured_tags_controller.rb +++ b/app/controllers/settings/featured_tags_controller.rb @@ -44,6 +44,6 @@ class Settings::FeaturedTagsController < Settings::BaseController end def featured_tag_params - params.require(:featured_tag).permit(:name) + params.expect(featured_tag: [:name]) end end diff --git a/app/controllers/settings/imports_controller.rb b/app/controllers/settings/imports_controller.rb index 5346a448a3..be1699315f 100644 --- a/app/controllers/settings/imports_controller.rb +++ b/app/controllers/settings/imports_controller.rb @@ -90,7 +90,7 @@ class Settings::ImportsController < Settings::BaseController private def import_params - params.require(:form_import).permit(:data, :type, :mode) + params.expect(form_import: [:data, :type, :mode]) end def set_bulk_import diff --git a/app/controllers/settings/migration/redirects_controller.rb b/app/controllers/settings/migration/redirects_controller.rb index 6d469f3842..d850e05e94 100644 --- a/app/controllers/settings/migration/redirects_controller.rb +++ b/app/controllers/settings/migration/redirects_controller.rb @@ -33,6 +33,6 @@ class Settings::Migration::RedirectsController < Settings::BaseController private def resource_params - params.require(:form_redirect).permit(:acct, :current_password, :current_username) + params.expect(form_redirect: [:acct, :current_password, :current_username]) end end diff --git a/app/controllers/settings/migrations_controller.rb b/app/controllers/settings/migrations_controller.rb index 62603aba81..92e3611fd9 100644 --- a/app/controllers/settings/migrations_controller.rb +++ b/app/controllers/settings/migrations_controller.rb @@ -27,7 +27,7 @@ class Settings::MigrationsController < Settings::BaseController private def resource_params - params.require(:account_migration).permit(:acct, :current_password, :current_username) + params.expect(account_migration: [:acct, :current_password, :current_username]) end def set_migrations diff --git a/app/controllers/settings/privacy_controller.rb b/app/controllers/settings/privacy_controller.rb index 1102c89fad..a5bb3b884f 100644 --- a/app/controllers/settings/privacy_controller.rb +++ b/app/controllers/settings/privacy_controller.rb @@ -18,7 +18,7 @@ class Settings::PrivacyController < Settings::BaseController private def account_params - params.require(:account).permit(:discoverable, :unlocked, :indexable, :show_collections, settings: UserSettings.keys) + params.expect(account: [:discoverable, :unlocked, :indexable, :show_collections, settings: UserSettings.keys]) end def set_account diff --git a/app/controllers/settings/profiles_controller.rb b/app/controllers/settings/profiles_controller.rb index dc759a060b..99a647336a 100644 --- a/app/controllers/settings/profiles_controller.rb +++ b/app/controllers/settings/profiles_controller.rb @@ -20,7 +20,7 @@ class Settings::ProfilesController < Settings::BaseController private def account_params - params.require(:account).permit(:display_name, :note, :bio_markdown, :avatar, :header, :bot, :my_actor_type, fields_attributes: [:name, :value]) + params.expect(account: [:display_name, :note, :bio_markdown, :avatar, :header, :bot, :my_actor_type, fields_attributes: [[:name, :value]]]) end def set_account diff --git a/app/controllers/settings/verifications_controller.rb b/app/controllers/settings/verifications_controller.rb index 4e0663253c..9cc60ba2e8 100644 --- a/app/controllers/settings/verifications_controller.rb +++ b/app/controllers/settings/verifications_controller.rb @@ -18,7 +18,9 @@ class Settings::VerificationsController < Settings::BaseController private def account_params - params.require(:account).permit(:attribution_domains_as_text) + params.require(:account).permit(:attribution_domains).tap do |params| + params[:attribution_domains] = params[:attribution_domains].split if params[:attribution_domains] + end end def set_account diff --git a/app/controllers/statuses_cleanup_controller.rb b/app/controllers/statuses_cleanup_controller.rb index 4db02051cc..583254ec27 100644 --- a/app/controllers/statuses_cleanup_controller.rb +++ b/app/controllers/statuses_cleanup_controller.rb @@ -15,8 +15,6 @@ class StatusesCleanupController < ApplicationController else render :show end - rescue ActionController::ParameterMissing - # Do nothing end def require_functional! @@ -30,7 +28,7 @@ class StatusesCleanupController < ApplicationController end def resource_params - params.require(:account_statuses_cleanup_policy).permit(:enabled, :min_status_age, :keep_direct, :keep_pinned, :keep_polls, :keep_media, :keep_self_fav, :keep_self_bookmark, :keep_self_emoji, :min_favs, :min_reblogs, :min_emojis) + params.expect(account_statuses_cleanup_policy: [:enabled, :min_status_age, :keep_direct, :keep_pinned, :keep_polls, :keep_media, :keep_self_fav, :keep_self_bookmark, :keep_self_emoji, :min_favs, :min_reblogs, :min_emojis]) end def set_cache_headers diff --git a/app/javascript/images/reticle.png b/app/javascript/images/reticle.png deleted file mode 100644 index a724ac0bcd..0000000000 Binary files a/app/javascript/images/reticle.png and /dev/null differ diff --git a/app/javascript/mastodon/actions/compose.js b/app/javascript/mastodon/actions/compose.js index 6059fe0e7e..9a92528f3a 100644 --- a/app/javascript/mastodon/actions/compose.js +++ b/app/javascript/mastodon/actions/compose.js @@ -441,7 +441,7 @@ export function initMediaEditModal(id) { dispatch(openModal({ modalType: 'FOCAL_POINT', - modalProps: { id }, + modalProps: { mediaId: id }, })); }; } diff --git a/app/javascript/mastodon/actions/compose_typed.ts b/app/javascript/mastodon/actions/compose_typed.ts new file mode 100644 index 0000000000..97f0d68c51 --- /dev/null +++ b/app/javascript/mastodon/actions/compose_typed.ts @@ -0,0 +1,70 @@ +import type { List as ImmutableList, Map as ImmutableMap } from 'immutable'; + +import { apiUpdateMedia } from 'mastodon/api/compose'; +import type { ApiMediaAttachmentJSON } from 'mastodon/api_types/media_attachments'; +import type { MediaAttachment } from 'mastodon/models/media_attachment'; +import { createDataLoadingThunk } from 'mastodon/store/typed_functions'; + +type SimulatedMediaAttachmentJSON = ApiMediaAttachmentJSON & { + unattached?: boolean; +}; + +const simulateModifiedApiResponse = ( + media: MediaAttachment, + params: { description?: string; focus?: string }, +): SimulatedMediaAttachmentJSON => { + const [x, y] = (params.focus ?? '').split(','); + + const data = { + ...media.toJS(), + ...params, + meta: { + focus: { + x: parseFloat(x ?? '0'), + y: parseFloat(y ?? '0'), + }, + }, + } as unknown as SimulatedMediaAttachmentJSON; + + return data; +}; + +export const changeUploadCompose = createDataLoadingThunk( + 'compose/changeUpload', + async ( + { + id, + ...params + }: { + id: string; + description: string; + focus: string; + }, + { getState }, + ) => { + const media = ( + (getState().compose as ImmutableMap).get( + 'media_attachments', + ) as ImmutableList + ).find((item) => item.get('id') === id); + + // Editing already-attached media is deferred to editing the post itself. + // For simplicity's sake, fake an API reply. + if (media && !media.get('unattached')) { + return new Promise((resolve) => { + resolve(simulateModifiedApiResponse(media, params)); + }); + } + + return apiUpdateMedia(id, params); + }, + (media: SimulatedMediaAttachmentJSON) => { + return { + media, + attached: typeof media.unattached !== 'undefined' && !media.unattached, + }; + }, + { + useLoadingBar: false, + }, +); diff --git a/app/javascript/mastodon/actions/modal.ts b/app/javascript/mastodon/actions/modal.ts index ab03e46765..49af176a11 100644 --- a/app/javascript/mastodon/actions/modal.ts +++ b/app/javascript/mastodon/actions/modal.ts @@ -9,6 +9,7 @@ export type ModalType = keyof typeof MODAL_COMPONENTS; interface OpenModalPayload { modalType: ModalType; modalProps: ModalProps; + previousModalProps?: ModalProps; } export const openModal = createAction('MODAL_OPEN'); diff --git a/app/javascript/mastodon/api/compose.ts b/app/javascript/mastodon/api/compose.ts new file mode 100644 index 0000000000..757e9961c9 --- /dev/null +++ b/app/javascript/mastodon/api/compose.ts @@ -0,0 +1,7 @@ +import { apiRequestPut } from 'mastodon/api'; +import type { ApiMediaAttachmentJSON } from 'mastodon/api_types/media_attachments'; + +export const apiUpdateMedia = ( + id: string, + params?: { description?: string; focus?: string }, +) => apiRequestPut(`v1/media/${id}`, params); diff --git a/app/javascript/mastodon/components/button.tsx b/app/javascript/mastodon/components/button.tsx index b349a83f2b..a527468f65 100644 --- a/app/javascript/mastodon/components/button.tsx +++ b/app/javascript/mastodon/components/button.tsx @@ -7,6 +7,7 @@ interface BaseProps extends Omit, 'children'> { block?: boolean; secondary?: boolean; + compact?: boolean; dangerous?: boolean; } @@ -27,6 +28,7 @@ export const Button: React.FC = ({ disabled, block, secondary, + compact, dangerous, className, title, @@ -47,6 +49,7 @@ export const Button: React.FC = ({ - - - {({ props }) => ( -
-
-
-

-
- -
-
{isSelf ? : }
-
@{username}@{domain}
-
- -
-
-
- -
-
-

{isSelf ? : }

-
-
- -
-
- -
-
-

{isSelf ? : }

-
-
-
- -

{isSelf ? }} /> : }} />}

- - {expanded && ( - <> -

-

- - )} -
- )} -
- - ); -}; - -DomainPill.propTypes = { - username: PropTypes.string.isRequired, - domain: PropTypes.string.isRequired, - isSelf: PropTypes.bool, -}; diff --git a/app/javascript/mastodon/features/account/components/domain_pill.tsx b/app/javascript/mastodon/features/account/components/domain_pill.tsx new file mode 100644 index 0000000000..956afa19c3 --- /dev/null +++ b/app/javascript/mastodon/features/account/components/domain_pill.tsx @@ -0,0 +1,197 @@ +import { useState, useRef, useCallback } from 'react'; + +import { FormattedMessage } from 'react-intl'; + +import classNames from 'classnames'; + +import Overlay from 'react-overlays/Overlay'; + +import AlternateEmailIcon from '@/material-icons/400-24px/alternate_email.svg?react'; +import BadgeIcon from '@/material-icons/400-24px/badge.svg?react'; +import GlobeIcon from '@/material-icons/400-24px/globe.svg?react'; +import { Icon } from 'mastodon/components/icon'; + +export const DomainPill: React.FC<{ + domain: string; + username: string; + isSelf: boolean; +}> = ({ domain, username, isSelf }) => { + const [open, setOpen] = useState(false); + const [expanded, setExpanded] = useState(false); + const triggerRef = useRef(null); + + const handleClick = useCallback(() => { + setOpen(!open); + }, [open, setOpen]); + + const handleExpandClick = useCallback(() => { + setExpanded(!expanded); + }, [expanded, setExpanded]); + + return ( + <> + + + + {({ props }) => ( +
+
+
+ +
+

+ +

+
+ +
+
+ {isSelf ? ( + + ) : ( + + )} +
+
+ @{username}@{domain} +
+
+ +
+
+
+ +
+ +
+
+ +
+

+ {isSelf ? ( + + ) : ( + + )} +

+
+
+ +
+
+ +
+ +
+
+ +
+

+ {isSelf ? ( + + ) : ( + + )} +

+
+
+
+ +

+ {isSelf ? ( + ( + + ), + }} + /> + ) : ( + ( + + ), + }} + /> + )} +

+ + {expanded && ( + <> +

+ +

+

+ +

+ + )} +
+ )} +
+ + ); +}; diff --git a/app/javascript/mastodon/features/alt_text_modal/index.tsx b/app/javascript/mastodon/features/alt_text_modal/index.tsx new file mode 100644 index 0000000000..88ffb7c477 --- /dev/null +++ b/app/javascript/mastodon/features/alt_text_modal/index.tsx @@ -0,0 +1,531 @@ +import { + useState, + useCallback, + useRef, + useEffect, + useImperativeHandle, + forwardRef, +} from 'react'; + +import { FormattedMessage, useIntl, defineMessages } from 'react-intl'; + +import classNames from 'classnames'; + +import type { List as ImmutableList, Map as ImmutableMap } from 'immutable'; + +import Textarea from 'react-textarea-autosize'; +import { length } from 'stringz'; +// eslint-disable-next-line import/extensions +import tesseractWorkerPath from 'tesseract.js/dist/worker.min.js'; +// eslint-disable-next-line import/no-extraneous-dependencies +import tesseractCorePath from 'tesseract.js-core/tesseract-core.wasm.js'; + +import { showAlertForError } from 'mastodon/actions/alerts'; +import { uploadThumbnail } from 'mastodon/actions/compose'; +import { changeUploadCompose } from 'mastodon/actions/compose_typed'; +import { Button } from 'mastodon/components/button'; +import { GIFV } from 'mastodon/components/gifv'; +import { LoadingIndicator } from 'mastodon/components/loading_indicator'; +import { Skeleton } from 'mastodon/components/skeleton'; +import Audio from 'mastodon/features/audio'; +import { CharacterCounter } from 'mastodon/features/compose/components/character_counter'; +import { Tesseract as fetchTesseract } from 'mastodon/features/ui/util/async-components'; +import Video, { getPointerPosition } from 'mastodon/features/video'; +import { me } from 'mastodon/initial_state'; +import type { MediaAttachment } from 'mastodon/models/media_attachment'; +import { useAppSelector, useAppDispatch } from 'mastodon/store'; +import { assetHost } from 'mastodon/utils/config'; + +const messages = defineMessages({ + placeholderVisual: { + id: 'alt_text_modal.describe_for_people_with_visual_impairments', + defaultMessage: 'Describe this for people with visual impairments…', + }, + placeholderHearing: { + id: 'alt_text_modal.describe_for_people_with_hearing_impairments', + defaultMessage: 'Describe this for people with hearing impairments…', + }, + discardMessage: { + id: 'confirmations.discard_edit_media.message', + defaultMessage: + 'You have unsaved changes to the media description or preview, discard them anyway?', + }, + discardConfirm: { + id: 'confirmations.discard_edit_media.confirm', + defaultMessage: 'Discard', + }, +}); + +const MAX_LENGTH = 1500; + +type FocalPoint = [number, number]; + +const UploadButton: React.FC<{ + children: React.ReactNode; + onSelectFile: (arg0: File) => void; + mimeTypes: string; +}> = ({ children, onSelectFile, mimeTypes }) => { + const fileRef = useRef(null); + + const handleClick = useCallback(() => { + fileRef.current?.click(); + }, []); + + const handleChange = useCallback( + (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + + if (file) { + onSelectFile(file); + } + }, + [onSelectFile], + ); + + return ( + + ); +}; + +const Preview: React.FC<{ + mediaId: string; + position: FocalPoint; + onPositionChange: (arg0: FocalPoint) => void; +}> = ({ mediaId, position, onPositionChange }) => { + const media = useAppSelector((state) => + ( + (state.compose as ImmutableMap).get( + 'media_attachments', + ) as ImmutableList + ).find((x) => x.get('id') === mediaId), + ); + const account = useAppSelector((state) => + me ? state.accounts.get(me) : undefined, + ); + + const [dragging, setDragging] = useState(false); + const [x, y] = position; + const nodeRef = useRef(null); + const draggingRef = useRef(false); + + const setRef = useCallback( + (e: HTMLImageElement | HTMLVideoElement | null) => { + nodeRef.current = e; + }, + [], + ); + + const handleMouseDown = useCallback( + (e: React.MouseEvent) => { + if (e.button !== 0) { + return; + } + + const { x, y } = getPointerPosition(nodeRef.current, e); + setDragging(true); + draggingRef.current = true; + onPositionChange([x, y]); + }, + [setDragging, onPositionChange], + ); + + const handleTouchStart = useCallback( + (e: React.TouchEvent) => { + const { x, y } = getPointerPosition(nodeRef.current, e); + setDragging(true); + draggingRef.current = true; + onPositionChange([x, y]); + }, + [setDragging, onPositionChange], + ); + + useEffect(() => { + const handleMouseUp = () => { + setDragging(false); + draggingRef.current = false; + }; + + const handleMouseMove = (e: MouseEvent) => { + if (draggingRef.current) { + const { x, y } = getPointerPosition(nodeRef.current, e); + onPositionChange([x, y]); + } + }; + + const handleTouchEnd = () => { + setDragging(false); + draggingRef.current = false; + }; + + const handleTouchMove = (e: TouchEvent) => { + if (draggingRef.current) { + const { x, y } = getPointerPosition(nodeRef.current, e); + onPositionChange([x, y]); + } + }; + + document.addEventListener('mouseup', handleMouseUp); + document.addEventListener('mousemove', handleMouseMove); + document.addEventListener('touchend', handleTouchEnd); + document.addEventListener('touchmove', handleTouchMove); + + return () => { + document.removeEventListener('mouseup', handleMouseUp); + document.removeEventListener('mousemove', handleMouseMove); + document.removeEventListener('touchend', handleTouchEnd); + document.removeEventListener('touchmove', handleTouchMove); + }; + }, [setDragging, onPositionChange]); + + if (!media) { + return null; + } + + if (media.get('type') === 'image') { + return ( +
+ +
+
+ ); + } else if (media.get('type') === 'gifv') { + return ( +
+ +
+
+ ); + } else if (media.get('type') === 'video') { + return ( +