Merge commit '16c41e035bdd6e08927c950aeeb5332adbe8b6d5' into glitch-soc/merge-upstream
Conflicts: - `lib/mastodon/version.rb`: Upstream bumped the Mastodon API version, glitch-soc has an extra glitch version. Bumped the Mastodon API version as upstream did.
This commit is contained in:
@@ -26,9 +26,9 @@ class ActivityPub::InboxesController < ActivityPub::BaseController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def unknown_affected_account?
|
def unknown_affected_account?
|
||||||
json = Oj.load(body, mode: :strict)
|
json = JSON.parse(body)
|
||||||
json.is_a?(Hash) && %w(Delete Update).include?(json['type']) && json['actor'].present? && json['actor'] == value_or_id(json['object']) && !Account.exists?(uri: json['actor'])
|
json.is_a?(Hash) && %w(Delete Update).include?(json['type']) && json['actor'].present? && json['actor'] == value_or_id(json['object']) && !Account.exists?(uri: json['actor'])
|
||||||
rescue Oj::ParseError
|
rescue JSON::ParserError
|
||||||
false
|
false
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ class Api::V1::AccountsController < Api::BaseController
|
|||||||
|
|
||||||
headers.merge!(response.headers)
|
headers.merge!(response.headers)
|
||||||
|
|
||||||
self.response_body = Oj.dump(response.body)
|
self.response_body = response.body.to_json
|
||||||
self.status = response.status
|
self.status = response.status
|
||||||
rescue ActiveRecord::RecordInvalid => e
|
rescue ActiveRecord::RecordInvalid => e
|
||||||
render json: ValidationErrorFormatter.new(e, 'account.username': :username, 'invite_request.text': :reason).as_json, status: 422
|
render json: ValidationErrorFormatter.new(e, 'account.username': :username, 'invite_request.text': :reason).as_json, status: 422
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ class Api::V1::DonationCampaignsController < Api::BaseController
|
|||||||
return if key.blank?
|
return if key.blank?
|
||||||
|
|
||||||
campaign = Rails.cache.read("donation_campaign:#{key}", raw: true)
|
campaign = Rails.cache.read("donation_campaign:#{key}", raw: true)
|
||||||
Oj.load(campaign) if campaign.present?
|
JSON.parse(campaign) if campaign.present?
|
||||||
end
|
end
|
||||||
|
|
||||||
def save_to_cache!(campaign)
|
def save_to_cache!(campaign)
|
||||||
@@ -44,7 +44,7 @@ class Api::V1::DonationCampaignsController < Api::BaseController
|
|||||||
Rails.cache.write_multi(
|
Rails.cache.write_multi(
|
||||||
{
|
{
|
||||||
request_key => campaign_key(campaign),
|
request_key => campaign_key(campaign),
|
||||||
"donation_campaign:#{campaign_key(campaign)}" => Oj.dump(campaign),
|
"donation_campaign:#{campaign_key(campaign)}" => campaign.to_json,
|
||||||
},
|
},
|
||||||
expires_in: 1.hour,
|
expires_in: 1.hour,
|
||||||
raw: true
|
raw: true
|
||||||
@@ -57,7 +57,7 @@ class Api::V1::DonationCampaignsController < Api::BaseController
|
|||||||
url.query_values = { platform: 'web', seed: seed, locale: locale, environment: Rails.configuration.x.donation_campaigns.environment }.compact
|
url.query_values = { platform: 'web', seed: seed, locale: locale, environment: Rails.configuration.x.donation_campaigns.environment }.compact
|
||||||
|
|
||||||
Request.new(:get, url.to_s).perform do |res|
|
Request.new(:get, url.to_s).perform do |res|
|
||||||
return Oj.load(res.body_with_limit, mode: :strict) if res.code == 200
|
return JSON.parse(res.body_with_limit) if res.code == 200
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
rescue *Mastodon::HTTP_CONNECTION_ERRORS, Oj::ParseError
|
rescue *Mastodon::HTTP_CONNECTION_ERRORS, Oj::ParseError
|
||||||
|
|||||||
@@ -25,7 +25,9 @@ class Api::V1::ProfilesController < Api::BaseController
|
|||||||
:display_name,
|
:display_name,
|
||||||
:note,
|
:note,
|
||||||
:avatar,
|
:avatar,
|
||||||
|
:avatar_description,
|
||||||
:header,
|
:header,
|
||||||
|
:header_description,
|
||||||
:locked,
|
:locked,
|
||||||
:bot,
|
:bot,
|
||||||
:discoverable,
|
:discoverable,
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ class Api::V1::Statuses::PinsController < Api::V1::Statuses::BaseController
|
|||||||
adapter: ActivityPub::Adapter
|
adapter: ActivityPub::Adapter
|
||||||
).as_json
|
).as_json
|
||||||
|
|
||||||
ActivityPub::RawDistributionWorker.perform_async(Oj.dump(json), current_account.id)
|
ActivityPub::RawDistributionWorker.perform_async(json.to_json, current_account.id)
|
||||||
end
|
end
|
||||||
|
|
||||||
def distribute_remove_activity!
|
def distribute_remove_activity!
|
||||||
@@ -40,6 +40,6 @@ class Api::V1::Statuses::PinsController < Api::V1::Statuses::BaseController
|
|||||||
adapter: ActivityPub::Adapter
|
adapter: ActivityPub::Adapter
|
||||||
).as_json
|
).as_json
|
||||||
|
|
||||||
ActivityPub::RawDistributionWorker.perform_async(Oj.dump(json), current_account.id)
|
ActivityPub::RawDistributionWorker.perform_async(json.to_json, current_account.id)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -309,7 +309,7 @@ module JsonLdHelper
|
|||||||
end
|
end
|
||||||
|
|
||||||
def body_to_json(body, compare_id: nil)
|
def body_to_json(body, compare_id: nil)
|
||||||
json = body.is_a?(String) ? Oj.load(body, mode: :strict) : body
|
json = body.is_a?(String) ? JSON.parse(body) : body
|
||||||
|
|
||||||
return if compare_id.present? && json['id'] != compare_id
|
return if compare_id.present? && json['id'] != compare_id
|
||||||
|
|
||||||
|
|||||||
@@ -273,3 +273,7 @@ export const quoteComposeCancel = createAction('compose/quoteComposeCancel');
|
|||||||
export const setComposeQuotePolicy = createAction<ApiQuotePolicy>(
|
export const setComposeQuotePolicy = createAction<ApiQuotePolicy>(
|
||||||
'compose/setQuotePolicy',
|
'compose/setQuotePolicy',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const setDragUploadEnabled = createAction<boolean>(
|
||||||
|
'compose/setDragUploadEnabled',
|
||||||
|
);
|
||||||
|
|||||||
@@ -67,5 +67,11 @@ export const apiGetFamiliarFollowers = (id: string) =>
|
|||||||
|
|
||||||
export const apiGetProfile = () => apiRequestGet<ApiProfileJSON>('v1/profile');
|
export const apiGetProfile = () => apiRequestGet<ApiProfileJSON>('v1/profile');
|
||||||
|
|
||||||
export const apiPatchProfile = (params: ApiProfileUpdateParams) =>
|
export const apiPatchProfile = (params: ApiProfileUpdateParams | FormData) =>
|
||||||
apiRequestPatch<ApiProfileJSON>('v1/profile', params);
|
apiRequestPatch<ApiProfileJSON>('v1/profile', params);
|
||||||
|
|
||||||
|
export const apiDeleteProfileAvatar = () =>
|
||||||
|
apiRequestDelete('v1/profile/avatar');
|
||||||
|
|
||||||
|
export const apiDeleteProfileHeader = () =>
|
||||||
|
apiRequestDelete('v1/profile/header');
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ export interface ApiProfileJSON {
|
|||||||
export type ApiProfileUpdateParams = Partial<
|
export type ApiProfileUpdateParams = Partial<
|
||||||
Pick<
|
Pick<
|
||||||
ApiProfileJSON,
|
ApiProfileJSON,
|
||||||
|
| 'avatar_description'
|
||||||
|
| 'header_description'
|
||||||
| 'display_name'
|
| 'display_name'
|
||||||
| 'note'
|
| 'note'
|
||||||
| 'locked'
|
| 'locked'
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ export const CharacterCounter = polymorphicForwardRef<
|
|||||||
maxLength,
|
maxLength,
|
||||||
as: Component = 'span',
|
as: Component = 'span',
|
||||||
recommended = false,
|
recommended = false,
|
||||||
|
className,
|
||||||
...props
|
...props
|
||||||
},
|
},
|
||||||
ref,
|
ref,
|
||||||
@@ -39,6 +40,7 @@ export const CharacterCounter = polymorphicForwardRef<
|
|||||||
{...props}
|
{...props}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
|
className,
|
||||||
classes.counter,
|
classes.counter,
|
||||||
currentLength > maxLength && !recommended && classes.counterError,
|
currentLength > maxLength && !recommended && classes.counterError,
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import type { Meta, StoryObj } from '@storybook/react-vite';
|
||||||
|
|
||||||
|
import { Details } from './index';
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
component: Details,
|
||||||
|
title: 'Components/Details',
|
||||||
|
args: {
|
||||||
|
summary: 'Here is the summary title',
|
||||||
|
children: (
|
||||||
|
<p>
|
||||||
|
And here are the details that are hidden until you click the summary.
|
||||||
|
</p>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
render(props) {
|
||||||
|
return (
|
||||||
|
<div style={{ width: '400px' }}>
|
||||||
|
<Details {...props} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
} satisfies Meta<typeof Details>;
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
|
||||||
|
type Story = StoryObj<typeof meta>;
|
||||||
|
|
||||||
|
export const Plain: Story = {};
|
||||||
35
app/javascript/mastodon/components/details/index.tsx
Normal file
35
app/javascript/mastodon/components/details/index.tsx
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { forwardRef } from 'react';
|
||||||
|
import type { ComponentPropsWithoutRef, ReactNode } from 'react';
|
||||||
|
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
import ExpandArrowIcon from '@/material-icons/400-24px/expand_more.svg?react';
|
||||||
|
|
||||||
|
import { Icon } from '../icon';
|
||||||
|
|
||||||
|
import classes from './styles.module.scss';
|
||||||
|
|
||||||
|
export const Details = forwardRef<
|
||||||
|
HTMLDetailsElement,
|
||||||
|
{
|
||||||
|
summary: ReactNode;
|
||||||
|
children: ReactNode;
|
||||||
|
className?: string;
|
||||||
|
} & ComponentPropsWithoutRef<'details'>
|
||||||
|
>(({ summary, children, className, ...rest }, ref) => {
|
||||||
|
return (
|
||||||
|
<details
|
||||||
|
ref={ref}
|
||||||
|
className={classNames(classes.details, className)}
|
||||||
|
{...rest}
|
||||||
|
>
|
||||||
|
<summary>
|
||||||
|
{summary}
|
||||||
|
<Icon icon={ExpandArrowIcon} id='arrow' />
|
||||||
|
</summary>
|
||||||
|
|
||||||
|
{children}
|
||||||
|
</details>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
Details.displayName = 'Details';
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
.details {
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
font-size: 13px;
|
||||||
|
margin-top: 8px;
|
||||||
|
|
||||||
|
summary {
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 600;
|
||||||
|
list-style: none;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
text-decoration: underline;
|
||||||
|
text-decoration-style: dotted;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.icon) {
|
||||||
|
width: 1.4em;
|
||||||
|
height: 1.4em;
|
||||||
|
vertical-align: middle;
|
||||||
|
transition: transform 0.2s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
&[open] :global(.icon) {
|
||||||
|
transform: rotate(-180deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,11 +12,9 @@ import { openModal } from '@/mastodon/actions/modal';
|
|||||||
import { Dropdown } from '@/mastodon/components/dropdown_menu';
|
import { Dropdown } from '@/mastodon/components/dropdown_menu';
|
||||||
import { IconButton } from '@/mastodon/components/icon_button';
|
import { IconButton } from '@/mastodon/components/icon_button';
|
||||||
import type { MenuItem } from '@/mastodon/models/dropdown_menu';
|
import type { MenuItem } from '@/mastodon/models/dropdown_menu';
|
||||||
import {
|
import type { ImageLocation } from '@/mastodon/reducers/slices/profile_edit';
|
||||||
createAppSelector,
|
import { selectImageInfo } from '@/mastodon/reducers/slices/profile_edit';
|
||||||
useAppDispatch,
|
import { useAppDispatch, useAppSelector } from '@/mastodon/store';
|
||||||
useAppSelector,
|
|
||||||
} from '@/mastodon/store';
|
|
||||||
import AddIcon from '@/material-icons/400-24px/add.svg?react';
|
import AddIcon from '@/material-icons/400-24px/add.svg?react';
|
||||||
import DeleteIcon from '@/material-icons/400-24px/delete.svg?react';
|
import DeleteIcon from '@/material-icons/400-24px/delete.svg?react';
|
||||||
import EditIcon from '@/material-icons/400-24px/edit.svg?react';
|
import EditIcon from '@/material-icons/400-24px/edit.svg?react';
|
||||||
@@ -50,36 +48,15 @@ const messages = defineMessages({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export type ImageLocation = 'avatar' | 'header';
|
|
||||||
|
|
||||||
const selectImageInfo = createAppSelector(
|
|
||||||
[
|
|
||||||
(state) => state.profileEdit.profile,
|
|
||||||
(_, location: ImageLocation) => location,
|
|
||||||
],
|
|
||||||
(profile, location) => {
|
|
||||||
if (!profile) {
|
|
||||||
return {
|
|
||||||
hasImage: false,
|
|
||||||
hasAlt: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
hasImage: !!profile[`${location}Static`],
|
|
||||||
hasAlt: !!profile[`${location}Description`],
|
|
||||||
};
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
export const AccountImageEdit: FC<{
|
export const AccountImageEdit: FC<{
|
||||||
className?: string;
|
className?: string;
|
||||||
location: ImageLocation;
|
location: ImageLocation;
|
||||||
}> = ({ className, location }) => {
|
}> = ({ className, location }) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const { hasAlt, hasImage } = useAppSelector((state) =>
|
const { alt, src } = useAppSelector((state) =>
|
||||||
selectImageInfo(state, location),
|
selectImageInfo(state, location),
|
||||||
);
|
);
|
||||||
|
const hasAlt = !!alt;
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
const handleModal = useCallback(
|
const handleModal = useCallback(
|
||||||
@@ -125,7 +102,7 @@ export const AccountImageEdit: FC<{
|
|||||||
|
|
||||||
const iconClassName = classNames(classes.imageButton, className);
|
const iconClassName = classNames(classes.imageButton, className);
|
||||||
|
|
||||||
if (!hasImage) {
|
if (!src) {
|
||||||
return (
|
return (
|
||||||
<IconButton
|
<IconButton
|
||||||
title={intl.formatMessage(messages.add)}
|
title={intl.formatMessage(messages.add)}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { useHistory } from 'react-router-dom';
|
|||||||
|
|
||||||
import type { ModalType } from '@/mastodon/actions/modal';
|
import type { ModalType } from '@/mastodon/actions/modal';
|
||||||
import { openModal } from '@/mastodon/actions/modal';
|
import { openModal } from '@/mastodon/actions/modal';
|
||||||
|
import { AccountBio } from '@/mastodon/components/account_bio';
|
||||||
import { Avatar } from '@/mastodon/components/avatar';
|
import { Avatar } from '@/mastodon/components/avatar';
|
||||||
import { Button } from '@/mastodon/components/button';
|
import { Button } from '@/mastodon/components/button';
|
||||||
import { DismissibleCallout } from '@/mastodon/components/callout/dismissible';
|
import { DismissibleCallout } from '@/mastodon/components/callout/dismissible';
|
||||||
@@ -201,7 +202,11 @@ export const AccountEdit: FC = () => {
|
|||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<EmojiHTML htmlString={profile.bio} {...htmlHandlers} />
|
<AccountBio
|
||||||
|
showDropdown
|
||||||
|
accountId={profile.id}
|
||||||
|
className={classes.bio}
|
||||||
|
/>
|
||||||
</AccountEditSection>
|
</AccountEditSection>
|
||||||
|
|
||||||
<AccountEditSection
|
<AccountEditSection
|
||||||
|
|||||||
@@ -1,11 +1,147 @@
|
|||||||
import type { FC } from 'react';
|
import type { ChangeEventHandler, FC } from 'react';
|
||||||
|
import { useCallback, useState } from 'react';
|
||||||
|
|
||||||
import { DialogModal } from '../../ui/components/dialog_modal';
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
|
import { CharacterCounter } from '@/mastodon/components/character_counter';
|
||||||
|
import { Details } from '@/mastodon/components/details';
|
||||||
|
import { TextAreaField } from '@/mastodon/components/form_fields';
|
||||||
|
import { LoadingIndicator } from '@/mastodon/components/loading_indicator';
|
||||||
|
import { patchProfile } from '@/mastodon/reducers/slices/profile_edit';
|
||||||
|
import type { ImageLocation } from '@/mastodon/reducers/slices/profile_edit';
|
||||||
|
import { useAppDispatch, useAppSelector } from '@/mastodon/store';
|
||||||
|
|
||||||
|
import { ConfirmationModal } from '../../ui/components/confirmation_modals';
|
||||||
import type { DialogModalProps } from '../../ui/components/dialog_modal';
|
import type { DialogModalProps } from '../../ui/components/dialog_modal';
|
||||||
import type { ImageLocation } from '../components/image_edit';
|
|
||||||
|
import classes from './styles.module.scss';
|
||||||
|
|
||||||
export const ImageAltModal: FC<
|
export const ImageAltModal: FC<
|
||||||
DialogModalProps & { location: ImageLocation }
|
DialogModalProps & { location: ImageLocation }
|
||||||
> = ({ onClose }) => {
|
> = ({ onClose, location }) => {
|
||||||
return <DialogModal title='TODO' onClose={onClose} />;
|
const { profile, isPending } = useAppSelector((state) => state.profileEdit);
|
||||||
|
|
||||||
|
const initialAlt = profile?.[`${location}Description`];
|
||||||
|
const imageSrc = profile?.[`${location}Static`];
|
||||||
|
|
||||||
|
const [altText, setAltText] = useState(initialAlt ?? '');
|
||||||
|
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const handleSave = useCallback(() => {
|
||||||
|
void dispatch(
|
||||||
|
patchProfile({
|
||||||
|
[`${location}_description`]: altText,
|
||||||
|
}),
|
||||||
|
).then(onClose);
|
||||||
|
}, [altText, dispatch, location, onClose]);
|
||||||
|
|
||||||
|
if (!imageSrc) {
|
||||||
|
return <LoadingIndicator />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ConfirmationModal
|
||||||
|
title={
|
||||||
|
initialAlt ? (
|
||||||
|
<FormattedMessage
|
||||||
|
id='account_edit.image_alt_modal.edit_title'
|
||||||
|
defaultMessage='Edit alt text'
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<FormattedMessage
|
||||||
|
id='account_edit.image_alt_modal.add_title'
|
||||||
|
defaultMessage='Add alt text'
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
onClose={onClose}
|
||||||
|
onConfirm={handleSave}
|
||||||
|
confirm={
|
||||||
|
<FormattedMessage
|
||||||
|
id='account_edit.upload_modal.done'
|
||||||
|
defaultMessage='Done'
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
updating={isPending}
|
||||||
|
>
|
||||||
|
<div className={classes.wrapper}>
|
||||||
|
<ImageAltTextField
|
||||||
|
imageSrc={imageSrc}
|
||||||
|
altText={altText}
|
||||||
|
onChange={setAltText}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</ConfirmationModal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ImageAltTextField: FC<{
|
||||||
|
imageSrc: string;
|
||||||
|
altText: string;
|
||||||
|
onChange: (altText: string) => void;
|
||||||
|
}> = ({ imageSrc, altText, onChange }) => {
|
||||||
|
const altLimit = useAppSelector(
|
||||||
|
(state) =>
|
||||||
|
state.server.getIn(
|
||||||
|
['server', 'configuration', 'media_attachments', 'description_limit'],
|
||||||
|
150,
|
||||||
|
) as number,
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleChange: ChangeEventHandler<HTMLTextAreaElement> = useCallback(
|
||||||
|
(event) => {
|
||||||
|
onChange(event.currentTarget.value);
|
||||||
|
},
|
||||||
|
[onChange],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<img src={imageSrc} alt='' className={classes.altImage} />
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<TextAreaField
|
||||||
|
label={
|
||||||
|
<FormattedMessage
|
||||||
|
id='account_edit.image_alt_modal.text_label'
|
||||||
|
defaultMessage='Alt text'
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
hint={
|
||||||
|
<FormattedMessage
|
||||||
|
id='account_edit.image_alt_modal.text_hint'
|
||||||
|
defaultMessage='Alt text helps screen reader users to understand your content.'
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
onChange={handleChange}
|
||||||
|
value={altText}
|
||||||
|
/>
|
||||||
|
<CharacterCounter
|
||||||
|
currentString={altText}
|
||||||
|
maxLength={altLimit}
|
||||||
|
className={classes.altCounter}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Details
|
||||||
|
summary={
|
||||||
|
<FormattedMessage
|
||||||
|
id='account_edit.image_alt_modal.details_title'
|
||||||
|
defaultMessage='Tips: Alt text for profile photos'
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
className={classes.altHint}
|
||||||
|
>
|
||||||
|
<FormattedMessage
|
||||||
|
id='account_edit.image_alt_modal.details_content'
|
||||||
|
defaultMessage='DO: <ul> <li>Describe yourself as pictured</li> <li>Use third person language (e.g. “Alex” instead of “me”)</li> <li>Be succinct – a few words is often enough</li> </ul> DON’T: <ul> <li>Start with “Photo of” – it’s redundant for screen readers</li> </ul> EXAMPLE: <ul> <li>“Alex wearing a green shirt and glasses”</li> </ul>'
|
||||||
|
values={{
|
||||||
|
ul: (chunks) => <ul>{chunks}</ul>,
|
||||||
|
li: (chunks) => <li>{chunks}</li>,
|
||||||
|
}}
|
||||||
|
tagName='div'
|
||||||
|
/>
|
||||||
|
</Details>
|
||||||
|
</>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,11 +1,48 @@
|
|||||||
|
import { useCallback } from 'react';
|
||||||
import type { FC } from 'react';
|
import type { FC } from 'react';
|
||||||
|
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
|
import { Button } from '@/mastodon/components/button';
|
||||||
|
import { deleteImage } from '@/mastodon/reducers/slices/profile_edit';
|
||||||
|
import type { ImageLocation } from '@/mastodon/reducers/slices/profile_edit';
|
||||||
|
import { useAppDispatch, useAppSelector } from '@/mastodon/store';
|
||||||
|
|
||||||
import { DialogModal } from '../../ui/components/dialog_modal';
|
import { DialogModal } from '../../ui/components/dialog_modal';
|
||||||
import type { DialogModalProps } from '../../ui/components/dialog_modal';
|
import type { DialogModalProps } from '../../ui/components/dialog_modal';
|
||||||
import type { ImageLocation } from '../components/image_edit';
|
|
||||||
|
|
||||||
export const ImageDeleteModal: FC<
|
export const ImageDeleteModal: FC<
|
||||||
DialogModalProps & { location: ImageLocation }
|
DialogModalProps & { location: ImageLocation }
|
||||||
> = ({ onClose }) => {
|
> = ({ onClose, location }) => {
|
||||||
return <DialogModal title='TODO' onClose={onClose} />;
|
const isPending = useAppSelector((state) => state.profileEdit.isPending);
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const handleDelete = useCallback(() => {
|
||||||
|
void dispatch(deleteImage({ location })).then(onClose);
|
||||||
|
}, [dispatch, location, onClose]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DialogModal
|
||||||
|
onClose={onClose}
|
||||||
|
title={
|
||||||
|
<FormattedMessage
|
||||||
|
id='account_edit.image_delete_modal.title'
|
||||||
|
defaultMessage='Delete image?'
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
buttons={
|
||||||
|
<Button dangerous onClick={handleDelete} disabled={isPending}>
|
||||||
|
<FormattedMessage
|
||||||
|
id='account_edit.image_delete_modal.delete_button'
|
||||||
|
defaultMessage='Delete'
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<FormattedMessage
|
||||||
|
id='account_edit.image_delete_modal.confirm'
|
||||||
|
defaultMessage='Are you sure you want to delete this image? This action can’t be undone.'
|
||||||
|
tagName='p'
|
||||||
|
/>
|
||||||
|
</DialogModal>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,11 +1,438 @@
|
|||||||
import type { FC } from 'react';
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
|
import type { ChangeEventHandler, FC } from 'react';
|
||||||
|
|
||||||
|
import { defineMessage, FormattedMessage, useIntl } from 'react-intl';
|
||||||
|
|
||||||
|
import type { Area } from 'react-easy-crop';
|
||||||
|
import Cropper from 'react-easy-crop';
|
||||||
|
|
||||||
|
import { setDragUploadEnabled } from '@/mastodon/actions/compose_typed';
|
||||||
|
import { Button } from '@/mastodon/components/button';
|
||||||
|
import { RangeInput } from '@/mastodon/components/form_fields/range_input_field';
|
||||||
|
import {
|
||||||
|
selectImageInfo,
|
||||||
|
uploadImage,
|
||||||
|
} from '@/mastodon/reducers/slices/profile_edit';
|
||||||
|
import type { ImageLocation } from '@/mastodon/reducers/slices/profile_edit';
|
||||||
|
import { useAppDispatch, useAppSelector } from '@/mastodon/store';
|
||||||
|
|
||||||
import { DialogModal } from '../../ui/components/dialog_modal';
|
import { DialogModal } from '../../ui/components/dialog_modal';
|
||||||
import type { DialogModalProps } from '../../ui/components/dialog_modal';
|
import type { DialogModalProps } from '../../ui/components/dialog_modal';
|
||||||
import type { ImageLocation } from '../components/image_edit';
|
|
||||||
|
import { ImageAltTextField } from './image_alt';
|
||||||
|
import classes from './styles.module.scss';
|
||||||
|
|
||||||
|
import 'react-easy-crop/react-easy-crop.css';
|
||||||
|
|
||||||
export const ImageUploadModal: FC<
|
export const ImageUploadModal: FC<
|
||||||
DialogModalProps & { location: ImageLocation }
|
DialogModalProps & { location: ImageLocation }
|
||||||
> = ({ onClose }) => {
|
> = ({ onClose, location }) => {
|
||||||
return <DialogModal title='TODO' onClose={onClose} />;
|
const { src: oldSrc } = useAppSelector((state) =>
|
||||||
|
selectImageInfo(state, location),
|
||||||
|
);
|
||||||
|
const hasImage = !!oldSrc;
|
||||||
|
const [step, setStep] = useState<'select' | 'crop' | 'alt'>('select');
|
||||||
|
|
||||||
|
// State for individual steps.
|
||||||
|
const [imageSrc, setImageSrc] = useState<string | null>(null);
|
||||||
|
const [imageBlob, setImageBlob] = useState<Blob | null>(null);
|
||||||
|
|
||||||
|
const handleFile = useCallback((file: File) => {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.addEventListener('load', () => {
|
||||||
|
const result = reader.result;
|
||||||
|
if (typeof result === 'string' && result.length > 0) {
|
||||||
|
setImageSrc(result);
|
||||||
|
setStep('crop');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleCrop = useCallback(
|
||||||
|
(crop: Area) => {
|
||||||
|
if (!imageSrc) {
|
||||||
|
setStep('select');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
void calculateCroppedImage(imageSrc, crop).then((blob) => {
|
||||||
|
setImageBlob(blob);
|
||||||
|
setStep('alt');
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[imageSrc],
|
||||||
|
);
|
||||||
|
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const handleSave = useCallback(
|
||||||
|
(altText: string) => {
|
||||||
|
if (!imageBlob) {
|
||||||
|
setStep('crop');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
void dispatch(uploadImage({ location, imageBlob, altText })).then(
|
||||||
|
onClose,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[dispatch, imageBlob, location, onClose],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleCancel = useCallback(() => {
|
||||||
|
switch (step) {
|
||||||
|
case 'crop':
|
||||||
|
setImageSrc(null);
|
||||||
|
setStep('select');
|
||||||
|
break;
|
||||||
|
case 'alt':
|
||||||
|
setImageBlob(null);
|
||||||
|
setStep('crop');
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
}, [onClose, step]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DialogModal
|
||||||
|
title={
|
||||||
|
hasImage ? (
|
||||||
|
<FormattedMessage
|
||||||
|
id='account_edit.upload_modal.title_replace'
|
||||||
|
defaultMessage='Replace profile photo'
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<FormattedMessage
|
||||||
|
id='account_edit.upload_modal.title_add'
|
||||||
|
defaultMessage='Add profile photo'
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
onClose={onClose}
|
||||||
|
wrapperClassName={classes.uploadWrapper}
|
||||||
|
noCancelButton
|
||||||
|
>
|
||||||
|
{step === 'select' && (
|
||||||
|
<StepUpload location={location} onFile={handleFile} />
|
||||||
|
)}
|
||||||
|
{step === 'crop' && imageSrc && (
|
||||||
|
<StepCrop
|
||||||
|
src={imageSrc}
|
||||||
|
location={location}
|
||||||
|
onCancel={handleCancel}
|
||||||
|
onComplete={handleCrop}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{step === 'alt' && imageBlob && (
|
||||||
|
<StepAlt
|
||||||
|
imageBlob={imageBlob}
|
||||||
|
onCancel={handleCancel}
|
||||||
|
onComplete={handleSave}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</DialogModal>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Taken from app/models/concerns/account/header.rb and app/models/concerns/account/avatar.rb
|
||||||
|
const ALLOWED_MIME_TYPES = [
|
||||||
|
'image/jpeg',
|
||||||
|
'image/png',
|
||||||
|
'image/gif',
|
||||||
|
'image/webp',
|
||||||
|
];
|
||||||
|
|
||||||
|
const StepUpload: FC<{
|
||||||
|
location: ImageLocation;
|
||||||
|
onFile: (file: File) => void;
|
||||||
|
}> = ({ location, onFile }) => {
|
||||||
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
|
const handleUploadClick = useCallback(() => {
|
||||||
|
inputRef.current?.click();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleFileChange: ChangeEventHandler<HTMLInputElement> = useCallback(
|
||||||
|
(event) => {
|
||||||
|
const file = event.currentTarget.files?.[0];
|
||||||
|
if (!file || !ALLOWED_MIME_TYPES.includes(file.type)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
onFile(file);
|
||||||
|
},
|
||||||
|
[onFile],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Handle drag and drop
|
||||||
|
const [isDragging, setDragging] = useState(false);
|
||||||
|
|
||||||
|
const handleDragOver = useCallback((event: DragEvent) => {
|
||||||
|
event.preventDefault();
|
||||||
|
if (!event.dataTransfer?.types.includes('Files')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const items = Array.from(event.dataTransfer.items);
|
||||||
|
if (
|
||||||
|
!items.some(
|
||||||
|
(item) =>
|
||||||
|
item.kind === 'file' && ALLOWED_MIME_TYPES.includes(item.type),
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setDragging(true);
|
||||||
|
}, []);
|
||||||
|
const handleDragDrop = useCallback(
|
||||||
|
(event: DragEvent) => {
|
||||||
|
event.preventDefault();
|
||||||
|
setDragging(false);
|
||||||
|
|
||||||
|
if (!event.dataTransfer?.files) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const file = Array.from(event.dataTransfer.files).find((f) =>
|
||||||
|
ALLOWED_MIME_TYPES.includes(f.type),
|
||||||
|
);
|
||||||
|
if (!file) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
onFile(file);
|
||||||
|
},
|
||||||
|
[onFile],
|
||||||
|
);
|
||||||
|
const handleDragLeave = useCallback((event: DragEvent) => {
|
||||||
|
event.preventDefault();
|
||||||
|
setDragging(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
useEffect(() => {
|
||||||
|
dispatch(setDragUploadEnabled(false));
|
||||||
|
document.addEventListener('dragover', handleDragOver);
|
||||||
|
document.addEventListener('drop', handleDragDrop);
|
||||||
|
document.addEventListener('dragleave', handleDragLeave);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('dragover', handleDragOver);
|
||||||
|
document.removeEventListener('drop', handleDragDrop);
|
||||||
|
document.removeEventListener('dragleave', handleDragLeave);
|
||||||
|
dispatch(setDragUploadEnabled(true));
|
||||||
|
};
|
||||||
|
}, [handleDragLeave, handleDragDrop, handleDragOver, dispatch]);
|
||||||
|
|
||||||
|
if (isDragging) {
|
||||||
|
return (
|
||||||
|
<div className={classes.uploadStepSelect}>
|
||||||
|
<FormattedMessage
|
||||||
|
id='account_edit.upload_modal.step_upload.dragging'
|
||||||
|
defaultMessage='Drop to upload'
|
||||||
|
tagName='h2'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={classes.uploadStepSelect}>
|
||||||
|
<FormattedMessage
|
||||||
|
id='account_edit.upload_modal.step_upload.header'
|
||||||
|
defaultMessage='Choose an image'
|
||||||
|
tagName='h2'
|
||||||
|
/>
|
||||||
|
<FormattedMessage
|
||||||
|
id='account_edit.upload_modal.step_upload.hint'
|
||||||
|
defaultMessage='WEBP, PNG, GIF or JPG format, up to {limit}MB.{br}Image will be scaled to {width}x{height}px.'
|
||||||
|
description='Guideline for avatar and header images.'
|
||||||
|
values={{
|
||||||
|
br: <br />,
|
||||||
|
limit: 8,
|
||||||
|
width: location === 'avatar' ? 400 : 1500,
|
||||||
|
height: location === 'avatar' ? 400 : 500,
|
||||||
|
}}
|
||||||
|
tagName='p'
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
onClick={handleUploadClick}
|
||||||
|
// eslint-disable-next-line jsx-a11y/no-autofocus -- This is the main input, so auto-focus on it.
|
||||||
|
autoFocus
|
||||||
|
>
|
||||||
|
<FormattedMessage
|
||||||
|
id='account_edit.upload_modal.step_upload.button'
|
||||||
|
defaultMessage='Browse files'
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<input
|
||||||
|
hidden
|
||||||
|
type='file'
|
||||||
|
ref={inputRef}
|
||||||
|
accept={ALLOWED_MIME_TYPES.join(',')}
|
||||||
|
onChange={handleFileChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const zoomLabel = defineMessage({
|
||||||
|
id: 'account_edit.upload_modal.step_crop.zoom',
|
||||||
|
defaultMessage: 'Zoom',
|
||||||
|
});
|
||||||
|
|
||||||
|
const StepCrop: FC<{
|
||||||
|
src: string;
|
||||||
|
location: ImageLocation;
|
||||||
|
onCancel: () => void;
|
||||||
|
onComplete: (crop: Area) => void;
|
||||||
|
}> = ({ src, location, onCancel, onComplete }) => {
|
||||||
|
const [crop, setCrop] = useState({ x: 0, y: 0 });
|
||||||
|
const [croppedArea, setCroppedArea] = useState<Area | null>(null);
|
||||||
|
const [zoom, setZoom] = useState(1);
|
||||||
|
const intl = useIntl();
|
||||||
|
|
||||||
|
const handleZoomChange: ChangeEventHandler<HTMLInputElement> = useCallback(
|
||||||
|
(event) => {
|
||||||
|
setZoom(event.currentTarget.valueAsNumber);
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
const handleCropComplete = useCallback((_: Area, croppedAreaPixels: Area) => {
|
||||||
|
setCroppedArea(croppedAreaPixels);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleNext = useCallback(() => {
|
||||||
|
if (croppedArea) {
|
||||||
|
onComplete(croppedArea);
|
||||||
|
}
|
||||||
|
}, [croppedArea, onComplete]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className={classes.cropContainer}>
|
||||||
|
<Cropper
|
||||||
|
image={src}
|
||||||
|
crop={crop}
|
||||||
|
zoom={zoom}
|
||||||
|
onCropChange={setCrop}
|
||||||
|
onCropComplete={handleCropComplete}
|
||||||
|
aspect={location === 'avatar' ? 1 : 3 / 1}
|
||||||
|
disableAutomaticStylesInjection
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={classes.cropActions}>
|
||||||
|
<RangeInput
|
||||||
|
min={1}
|
||||||
|
max={3}
|
||||||
|
step={0.1}
|
||||||
|
value={zoom}
|
||||||
|
onChange={handleZoomChange}
|
||||||
|
className={classes.zoomControl}
|
||||||
|
aria-label={intl.formatMessage(zoomLabel)}
|
||||||
|
/>
|
||||||
|
<Button onClick={onCancel} secondary>
|
||||||
|
<FormattedMessage
|
||||||
|
id='account_edit.upload_modal.back'
|
||||||
|
defaultMessage='Back'
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleNext} disabled={!croppedArea}>
|
||||||
|
<FormattedMessage
|
||||||
|
id='account_edit.upload_modal.next'
|
||||||
|
defaultMessage='Next'
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const StepAlt: FC<{
|
||||||
|
imageBlob: Blob;
|
||||||
|
onCancel: () => void;
|
||||||
|
onComplete: (altText: string) => void;
|
||||||
|
}> = ({ imageBlob, onCancel, onComplete }) => {
|
||||||
|
const [altText, setAltText] = useState('');
|
||||||
|
|
||||||
|
const handleComplete = useCallback(() => {
|
||||||
|
onComplete(altText);
|
||||||
|
}, [altText, onComplete]);
|
||||||
|
|
||||||
|
const imageSrc = useMemo(() => URL.createObjectURL(imageBlob), [imageBlob]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ImageAltTextField
|
||||||
|
imageSrc={imageSrc}
|
||||||
|
altText={altText}
|
||||||
|
onChange={setAltText}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className={classes.cropActions}>
|
||||||
|
<Button onClick={onCancel} secondary>
|
||||||
|
<FormattedMessage
|
||||||
|
id='account_edit.upload_modal.back'
|
||||||
|
defaultMessage='Back'
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button onClick={handleComplete}>
|
||||||
|
<FormattedMessage
|
||||||
|
id='account_edit.upload_modal.done'
|
||||||
|
defaultMessage='Done'
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
async function calculateCroppedImage(
|
||||||
|
imageSrc: string,
|
||||||
|
crop: Area,
|
||||||
|
): Promise<Blob> {
|
||||||
|
const image = await dataUriToImage(imageSrc);
|
||||||
|
const canvas = new OffscreenCanvas(crop.width, crop.height);
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
if (!ctx) {
|
||||||
|
throw new Error('Failed to get canvas context');
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.imageSmoothingQuality = 'high';
|
||||||
|
|
||||||
|
// Draw the image
|
||||||
|
ctx.drawImage(
|
||||||
|
image,
|
||||||
|
crop.x,
|
||||||
|
crop.y,
|
||||||
|
crop.width,
|
||||||
|
crop.height,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
crop.width,
|
||||||
|
crop.height,
|
||||||
|
);
|
||||||
|
|
||||||
|
return canvas.convertToBlob({
|
||||||
|
quality: 0.7,
|
||||||
|
type: 'image/jpeg',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function dataUriToImage(dataUri: string) {
|
||||||
|
return new Promise<HTMLImageElement>((resolve, reject) => {
|
||||||
|
const image = new Image();
|
||||||
|
image.addEventListener('load', () => {
|
||||||
|
resolve(image);
|
||||||
|
});
|
||||||
|
image.addEventListener('error', (event) => {
|
||||||
|
if (event.error instanceof Error) {
|
||||||
|
reject(event.error);
|
||||||
|
} else {
|
||||||
|
reject(new Error('Failed to load image'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
image.src = dataUri;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
export * from './bio_modal';
|
export * from './bio_modal';
|
||||||
export * from './fields_modals';
|
export * from './fields_modals';
|
||||||
export * from './fields_reorder_modal';
|
export * from './fields_reorder_modal';
|
||||||
export * from './image_alt';
|
export { ImageAltModal } from './image_alt';
|
||||||
export * from './image_delete';
|
export * from './image_delete';
|
||||||
export * from './image_upload';
|
export * from './image_upload';
|
||||||
export * from './name_modal';
|
export * from './name_modal';
|
||||||
|
|||||||
@@ -80,6 +80,66 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.uploadWrapper {
|
||||||
|
min-height: min(400px, 70vh);
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.uploadStepSelect {
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.cropContainer {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 300px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cropActions {
|
||||||
|
margin-top: 8px; // 16px gap from DialogModal, plus 8px = 24px to look like normal action buttons.
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
|
||||||
|
.zoomControl {
|
||||||
|
width: min(100%, 200px);
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.altImage {
|
||||||
|
max-height: 150px;
|
||||||
|
object-fit: contain;
|
||||||
|
align-self: flex-start;
|
||||||
|
border: 1px solid var(--color-border-primary);
|
||||||
|
border-radius: var(--avatar-border-radius);
|
||||||
|
}
|
||||||
|
|
||||||
|
.altCounter {
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.altHint {
|
||||||
|
ul {
|
||||||
|
padding-left: 1em;
|
||||||
|
list-style: disc;
|
||||||
|
margin-bottom: 1em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.verifiedSteps {
|
.verifiedSteps {
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
|
|
||||||
@@ -108,29 +168,3 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.details {
|
|
||||||
color: var(--color-text-secondary);
|
|
||||||
font-size: 13px;
|
|
||||||
margin-top: 8px;
|
|
||||||
|
|
||||||
summary {
|
|
||||||
cursor: pointer;
|
|
||||||
font-weight: 600;
|
|
||||||
list-style: none;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
text-decoration: underline;
|
|
||||||
text-decoration-style: dotted;
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(.icon) {
|
|
||||||
width: 1.4em;
|
|
||||||
height: 1.4em;
|
|
||||||
vertical-align: middle;
|
|
||||||
transition: transform 0.2s ease-in-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
&[open] :global(.icon) {
|
|
||||||
transform: rotate(-180deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -2,10 +2,9 @@ import type { FC } from 'react';
|
|||||||
|
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
|
import { Details } from '@/mastodon/components/details';
|
||||||
import { CopyLinkField } from '@/mastodon/components/form_fields/copy_link_field';
|
import { CopyLinkField } from '@/mastodon/components/form_fields/copy_link_field';
|
||||||
import { Icon } from '@/mastodon/components/icon';
|
|
||||||
import { createAppSelector, useAppSelector } from '@/mastodon/store';
|
import { createAppSelector, useAppSelector } from '@/mastodon/store';
|
||||||
import ExpandArrowIcon from '@/material-icons/400-24px/expand_more.svg?react';
|
|
||||||
|
|
||||||
import type { DialogModalProps } from '../../ui/components/dialog_modal';
|
import type { DialogModalProps } from '../../ui/components/dialog_modal';
|
||||||
import { DialogModal } from '../../ui/components/dialog_modal';
|
import { DialogModal } from '../../ui/components/dialog_modal';
|
||||||
@@ -53,20 +52,20 @@ export const VerifiedModal: FC<DialogModalProps> = ({ onClose }) => {
|
|||||||
}
|
}
|
||||||
value={`<a rel="me" href="${accountUrl}">Mastodon</a>`}
|
value={`<a rel="me" href="${accountUrl}">Mastodon</a>`}
|
||||||
/>
|
/>
|
||||||
<details className={classes.details}>
|
<Details
|
||||||
<summary>
|
summary={
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id='account_edit.verified_modal.invisible_link.summary'
|
id='account_edit.verified_modal.invisible_link.summary'
|
||||||
defaultMessage='How do I make the link invisible?'
|
defaultMessage='How do I make the link invisible?'
|
||||||
/>
|
/>
|
||||||
<Icon icon={ExpandArrowIcon} id='arrow' />
|
}
|
||||||
</summary>
|
>
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id='account_edit.verified_modal.invisible_link.details'
|
id='account_edit.verified_modal.invisible_link.details'
|
||||||
defaultMessage='Add the link to your header. The important part is rel="me" which prevents impersonation on websites with user-generated content. You can even use a link tag in the header of the page instead of {tag}, but the HTML must be accessible without executing JavaScript.'
|
defaultMessage='Add the link to your header. The important part is rel="me" which prevents impersonation on websites with user-generated content. You can even use a link tag in the header of the page instead of {tag}, but the HTML must be accessible without executing JavaScript.'
|
||||||
values={{ tag: <code><a></code> }}
|
values={{ tag: <code><a></code> }}
|
||||||
/>
|
/>
|
||||||
</details>
|
</Details>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
|
|||||||
@@ -40,6 +40,19 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.bio {
|
||||||
|
unicode-bidi: plaintext;
|
||||||
|
|
||||||
|
p:not(:last-child) {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.field {
|
.field {
|
||||||
padding: 12px 0;
|
padding: 12px 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -211,6 +211,7 @@ export const AccountHeader: React.FC<{
|
|||||||
))}
|
))}
|
||||||
|
|
||||||
<AccountBio
|
<AccountBio
|
||||||
|
showDropdown
|
||||||
accountId={accountId}
|
accountId={accountId}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'account__header__content',
|
'account__header__content',
|
||||||
|
|||||||
@@ -103,7 +103,11 @@ const mapStateToProps = state => ({
|
|||||||
layout: state.getIn(['meta', 'layout']),
|
layout: state.getIn(['meta', 'layout']),
|
||||||
isComposing: state.getIn(['compose', 'is_composing']),
|
isComposing: state.getIn(['compose', 'is_composing']),
|
||||||
hasComposingContents: state.getIn(['compose', 'text']).trim().length !== 0 || state.getIn(['compose', 'media_attachments']).size > 0 || state.getIn(['compose', 'poll']) !== null || state.getIn(['compose', 'quoted_status_id']) !== null,
|
hasComposingContents: state.getIn(['compose', 'text']).trim().length !== 0 || state.getIn(['compose', 'media_attachments']).size > 0 || state.getIn(['compose', 'poll']) !== null || state.getIn(['compose', 'quoted_status_id']) !== null,
|
||||||
canUploadMore: !state.getIn(['compose', 'media_attachments']).some(x => ['audio', 'video'].includes(x.get('type'))) && state.getIn(['compose', 'media_attachments']).size < state.getIn(['server', 'server', 'configuration', 'statuses', 'max_media_attachments']),
|
canUploadMore:
|
||||||
|
!state.getIn(['compose', 'media_attachments']).some(x => ['audio', 'video'].includes(x.get('type')))
|
||||||
|
&& state.getIn(['compose', 'media_attachments']).size < state.getIn(['server', 'server', 'configuration', 'statuses', 'max_media_attachments']),
|
||||||
|
isUploadEnabled:
|
||||||
|
state.getIn(['compose', 'isDragDisabled']) !== true,
|
||||||
firstLaunch: state.getIn(['settings', 'introductionVersion'], 0) < INTRODUCTION_VERSION,
|
firstLaunch: state.getIn(['settings', 'introductionVersion'], 0) < INTRODUCTION_VERSION,
|
||||||
newAccount: !state.getIn(['accounts', me, 'note']) && !state.getIn(['accounts', me, 'bot']) && state.getIn(['accounts', me, 'following_count'], 0) === 0 && state.getIn(['accounts', me, 'statuses_count'], 0) === 0,
|
newAccount: !state.getIn(['accounts', me, 'note']) && !state.getIn(['accounts', me, 'bot']) && state.getIn(['accounts', me, 'following_count'], 0) === 0 && state.getIn(['accounts', me, 'statuses_count'], 0) === 0,
|
||||||
username: state.getIn(['accounts', me, 'username']),
|
username: state.getIn(['accounts', me, 'username']),
|
||||||
@@ -324,6 +328,9 @@ class UI extends PureComponent {
|
|||||||
};
|
};
|
||||||
|
|
||||||
handleDragEnter = (e) => {
|
handleDragEnter = (e) => {
|
||||||
|
if (!this.props.isUploadEnabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
if (!this.dragTargets) {
|
if (!this.dragTargets) {
|
||||||
@@ -340,6 +347,9 @@ class UI extends PureComponent {
|
|||||||
};
|
};
|
||||||
|
|
||||||
handleDragOver = (e) => {
|
handleDragOver = (e) => {
|
||||||
|
if (!this.props.isUploadEnabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (this.dataTransferIsText(e.dataTransfer)) return false;
|
if (this.dataTransferIsText(e.dataTransfer)) return false;
|
||||||
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -355,6 +365,9 @@ class UI extends PureComponent {
|
|||||||
};
|
};
|
||||||
|
|
||||||
handleDrop = (e) => {
|
handleDrop = (e) => {
|
||||||
|
if (!this.props.isUploadEnabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (this.dataTransferIsText(e.dataTransfer)) return;
|
if (this.dataTransferIsText(e.dataTransfer)) return;
|
||||||
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -429,7 +442,6 @@ class UI extends PureComponent {
|
|||||||
document.addEventListener('dragover', this.handleDragOver, false);
|
document.addEventListener('dragover', this.handleDragOver, false);
|
||||||
document.addEventListener('drop', this.handleDrop, false);
|
document.addEventListener('drop', this.handleDrop, false);
|
||||||
document.addEventListener('dragleave', this.handleDragLeave, false);
|
document.addEventListener('dragleave', this.handleDragLeave, false);
|
||||||
document.addEventListener('dragend', this.handleDragEnd, false);
|
|
||||||
|
|
||||||
if ('serviceWorker' in navigator) {
|
if ('serviceWorker' in navigator) {
|
||||||
navigator.serviceWorker.addEventListener('message', this.handleServiceWorkerPostMessage);
|
navigator.serviceWorker.addEventListener('message', this.handleServiceWorkerPostMessage);
|
||||||
@@ -456,7 +468,6 @@ class UI extends PureComponent {
|
|||||||
document.removeEventListener('dragover', this.handleDragOver);
|
document.removeEventListener('dragover', this.handleDragOver);
|
||||||
document.removeEventListener('drop', this.handleDrop);
|
document.removeEventListener('drop', this.handleDrop);
|
||||||
document.removeEventListener('dragleave', this.handleDragLeave);
|
document.removeEventListener('dragleave', this.handleDragLeave);
|
||||||
document.removeEventListener('dragend', this.handleDragEnd);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setRef = c => {
|
setRef = c => {
|
||||||
|
|||||||
@@ -184,6 +184,15 @@
|
|||||||
"account_edit.field_reorder_modal.drag_start": "Picked up field \"{item}\".",
|
"account_edit.field_reorder_modal.drag_start": "Picked up field \"{item}\".",
|
||||||
"account_edit.field_reorder_modal.handle_label": "Drag field \"{item}\"",
|
"account_edit.field_reorder_modal.handle_label": "Drag field \"{item}\"",
|
||||||
"account_edit.field_reorder_modal.title": "Rearrange fields",
|
"account_edit.field_reorder_modal.title": "Rearrange fields",
|
||||||
|
"account_edit.image_alt_modal.add_title": "Add alt text",
|
||||||
|
"account_edit.image_alt_modal.details_content": "DO: <ul> <li>Describe yourself as pictured</li> <li>Use third person language (e.g. “Alex” instead of “me”)</li> <li>Be succinct – a few words is often enough</li> </ul> DON’T: <ul> <li>Start with “Photo of” – it’s redundant for screen readers</li> </ul> EXAMPLE: <ul> <li>“Alex wearing a green shirt and glasses”</li> </ul>",
|
||||||
|
"account_edit.image_alt_modal.details_title": "Tips: Alt text for profile photos",
|
||||||
|
"account_edit.image_alt_modal.edit_title": "Edit alt text",
|
||||||
|
"account_edit.image_alt_modal.text_hint": "Alt text helps screen reader users to understand your content.",
|
||||||
|
"account_edit.image_alt_modal.text_label": "Alt text",
|
||||||
|
"account_edit.image_delete_modal.confirm": "Are you sure you want to delete this image? This action can’t be undone.",
|
||||||
|
"account_edit.image_delete_modal.delete_button": "Delete",
|
||||||
|
"account_edit.image_delete_modal.title": "Delete image?",
|
||||||
"account_edit.image_edit.add_button": "Add image",
|
"account_edit.image_edit.add_button": "Add image",
|
||||||
"account_edit.image_edit.alt_add_button": "Add alt text",
|
"account_edit.image_edit.alt_add_button": "Add alt text",
|
||||||
"account_edit.image_edit.alt_edit_button": "Edit alt text",
|
"account_edit.image_edit.alt_edit_button": "Edit alt text",
|
||||||
@@ -203,6 +212,16 @@
|
|||||||
"account_edit.profile_tab.subtitle": "Customize the tabs on your profile and what they display.",
|
"account_edit.profile_tab.subtitle": "Customize the tabs on your profile and what they display.",
|
||||||
"account_edit.profile_tab.title": "Profile tab settings",
|
"account_edit.profile_tab.title": "Profile tab settings",
|
||||||
"account_edit.save": "Save",
|
"account_edit.save": "Save",
|
||||||
|
"account_edit.upload_modal.back": "Back",
|
||||||
|
"account_edit.upload_modal.done": "Done",
|
||||||
|
"account_edit.upload_modal.next": "Next",
|
||||||
|
"account_edit.upload_modal.step_crop.zoom": "Zoom",
|
||||||
|
"account_edit.upload_modal.step_upload.button": "Browse files",
|
||||||
|
"account_edit.upload_modal.step_upload.dragging": "Drop to upload",
|
||||||
|
"account_edit.upload_modal.step_upload.header": "Choose an image",
|
||||||
|
"account_edit.upload_modal.step_upload.hint": "WEBP, PNG, GIF or JPG format, up to {limit}MB.{br}Image will be scaled to {width}x{height}px.",
|
||||||
|
"account_edit.upload_modal.title_add": "Add profile photo",
|
||||||
|
"account_edit.upload_modal.title_replace": "Replace profile photo",
|
||||||
"account_edit.verified_modal.details": "Add credibility to your Mastodon profile by verifying links to personal websites. Here’s how it works:",
|
"account_edit.verified_modal.details": "Add credibility to your Mastodon profile by verifying links to personal websites. Here’s how it works:",
|
||||||
"account_edit.verified_modal.invisible_link.details": "Add the link to your header. The important part is rel=\"me\" which prevents impersonation on websites with user-generated content. You can even use a link tag in the header of the page instead of {tag}, but the HTML must be accessible without executing JavaScript.",
|
"account_edit.verified_modal.invisible_link.details": "Add the link to your header. The important part is rel=\"me\" which prevents impersonation on websites with user-generated content. You can even use a link tag in the header of the page instead of {tag}, but the HTML must be accessible without executing JavaScript.",
|
||||||
"account_edit.verified_modal.invisible_link.summary": "How do I make the link invisible?",
|
"account_edit.verified_modal.invisible_link.summary": "How do I make the link invisible?",
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
setComposeQuotePolicy,
|
setComposeQuotePolicy,
|
||||||
pasteLinkCompose,
|
pasteLinkCompose,
|
||||||
cancelPasteLinkCompose,
|
cancelPasteLinkCompose,
|
||||||
|
setDragUploadEnabled,
|
||||||
} from '@/mastodon/actions/compose_typed';
|
} from '@/mastodon/actions/compose_typed';
|
||||||
import { timelineDelete } from 'mastodon/actions/timelines_typed';
|
import { timelineDelete } from 'mastodon/actions/timelines_typed';
|
||||||
|
|
||||||
@@ -75,6 +76,7 @@ const initialState = ImmutableMap({
|
|||||||
is_submitting: false,
|
is_submitting: false,
|
||||||
is_changing_upload: false,
|
is_changing_upload: false,
|
||||||
is_uploading: false,
|
is_uploading: false,
|
||||||
|
isDragDisabled: false,
|
||||||
should_redirect_to_compose_page: false,
|
should_redirect_to_compose_page: false,
|
||||||
progress: 0,
|
progress: 0,
|
||||||
isUploadingThumbnail: false,
|
isUploadingThumbnail: false,
|
||||||
@@ -132,6 +134,7 @@ function clearAll(state) {
|
|||||||
map.set('idempotencyKey', uuid());
|
map.set('idempotencyKey', uuid());
|
||||||
map.set('quoted_status_id', null);
|
map.set('quoted_status_id', null);
|
||||||
map.set('quote_policy', state.get('default_quote_policy'));
|
map.set('quote_policy', state.get('default_quote_policy'));
|
||||||
|
map.set('isDragDisabled', false);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -359,6 +362,8 @@ export const composeReducer = (state = initialState, action) => {
|
|||||||
return action.meta.requestId === state.get('fetching_link') ? state.set('fetching_link', null) : state;
|
return action.meta.requestId === state.get('fetching_link') ? state.set('fetching_link', null) : state;
|
||||||
} else if (cancelPasteLinkCompose.match(action)) {
|
} else if (cancelPasteLinkCompose.match(action)) {
|
||||||
return state.set('fetching_link', null);
|
return state.set('fetching_link', null);
|
||||||
|
} else if (setDragUploadEnabled.match(action)) {
|
||||||
|
return state.set('isDragDisabled', !action.payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
switch(action.type) {
|
switch(action.type) {
|
||||||
|
|||||||
@@ -3,8 +3,11 @@ import { createSlice } from '@reduxjs/toolkit';
|
|||||||
|
|
||||||
import { debounce } from 'lodash';
|
import { debounce } from 'lodash';
|
||||||
|
|
||||||
|
import { fetchAccount } from '@/mastodon/actions/accounts';
|
||||||
import {
|
import {
|
||||||
apiDeleteFeaturedTag,
|
apiDeleteFeaturedTag,
|
||||||
|
apiDeleteProfileAvatar,
|
||||||
|
apiDeleteProfileHeader,
|
||||||
apiGetCurrentFeaturedTags,
|
apiGetCurrentFeaturedTags,
|
||||||
apiGetProfile,
|
apiGetProfile,
|
||||||
apiGetTagSuggestions,
|
apiGetTagSuggestions,
|
||||||
@@ -109,6 +112,27 @@ const profileEditSlice = createSlice({
|
|||||||
state.isPending = false;
|
state.isPending = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
builder.addCase(uploadImage.pending, (state) => {
|
||||||
|
state.isPending = true;
|
||||||
|
});
|
||||||
|
builder.addCase(uploadImage.rejected, (state) => {
|
||||||
|
state.isPending = false;
|
||||||
|
});
|
||||||
|
builder.addCase(uploadImage.fulfilled, (state, action) => {
|
||||||
|
state.profile = action.payload;
|
||||||
|
state.isPending = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
builder.addCase(deleteImage.pending, (state) => {
|
||||||
|
state.isPending = true;
|
||||||
|
});
|
||||||
|
builder.addCase(deleteImage.rejected, (state) => {
|
||||||
|
state.isPending = false;
|
||||||
|
});
|
||||||
|
builder.addCase(deleteImage.fulfilled, (state) => {
|
||||||
|
state.isPending = false;
|
||||||
|
});
|
||||||
|
|
||||||
builder.addCase(addFeaturedTag.pending, (state) => {
|
builder.addCase(addFeaturedTag.pending, (state) => {
|
||||||
state.isPending = true;
|
state.isPending = true;
|
||||||
});
|
});
|
||||||
@@ -220,7 +244,10 @@ export const fetchProfile = createDataLoadingThunk(
|
|||||||
export const patchProfile = createDataLoadingThunk(
|
export const patchProfile = createDataLoadingThunk(
|
||||||
`${profileEditSlice.name}/patchProfile`,
|
`${profileEditSlice.name}/patchProfile`,
|
||||||
(params: Partial<ApiProfileUpdateParams>) => apiPatchProfile(params),
|
(params: Partial<ApiProfileUpdateParams>) => apiPatchProfile(params),
|
||||||
transformProfile,
|
(response, { dispatch }) => {
|
||||||
|
dispatch(fetchAccount(response.id));
|
||||||
|
return transformProfile(response);
|
||||||
|
},
|
||||||
{
|
{
|
||||||
useLoadingBar: false,
|
useLoadingBar: false,
|
||||||
condition(_, { getState }) {
|
condition(_, { getState }) {
|
||||||
@@ -229,6 +256,67 @@ export const patchProfile = createDataLoadingThunk(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export type ImageLocation = 'avatar' | 'header';
|
||||||
|
|
||||||
|
export const selectImageInfo = createAppSelector(
|
||||||
|
[
|
||||||
|
(state) => state.profileEdit.profile,
|
||||||
|
(_, location: ImageLocation) => location,
|
||||||
|
],
|
||||||
|
(profile, location) => {
|
||||||
|
if (!profile) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
src: profile[location],
|
||||||
|
static: profile[`${location}Static`],
|
||||||
|
alt: profile[`${location}Description`],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export const uploadImage = createDataLoadingThunk(
|
||||||
|
`${profileEditSlice.name}/uploadImage`,
|
||||||
|
(arg: { location: ImageLocation; imageBlob: Blob; altText: string }) => {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append(arg.location, arg.imageBlob);
|
||||||
|
if (arg.altText) {
|
||||||
|
formData.append(`${arg.location}_description`, arg.altText);
|
||||||
|
}
|
||||||
|
|
||||||
|
return apiPatchProfile(formData);
|
||||||
|
},
|
||||||
|
(response, { dispatch }) => {
|
||||||
|
dispatch(fetchAccount(response.id));
|
||||||
|
return transformProfile(response);
|
||||||
|
},
|
||||||
|
{
|
||||||
|
useLoadingBar: false,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export const deleteImage = createDataLoadingThunk(
|
||||||
|
`${profileEditSlice.name}/deleteImage`,
|
||||||
|
(arg: { location: ImageLocation }) => {
|
||||||
|
if (arg.location === 'avatar') {
|
||||||
|
return apiDeleteProfileAvatar();
|
||||||
|
} else {
|
||||||
|
return apiDeleteProfileHeader();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async (_, { dispatch, getState }) => {
|
||||||
|
await dispatch(fetchProfile());
|
||||||
|
const accountId = getState().profileEdit.profile?.id;
|
||||||
|
if (accountId) {
|
||||||
|
dispatch(fetchAccount(accountId));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
useLoadingBar: false,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
export const selectFieldById = createAppSelector(
|
export const selectFieldById = createAppSelector(
|
||||||
[(state) => state.profileEdit.profile?.fields, (_, id?: string) => id],
|
[(state) => state.profileEdit.profile?.fields, (_, id?: string) => id],
|
||||||
(fields, fieldId) => {
|
(fields, fieldId) => {
|
||||||
|
|||||||
@@ -24,6 +24,6 @@ module AccessTokenExtension
|
|||||||
end
|
end
|
||||||
|
|
||||||
def push_to_streaming_api
|
def push_to_streaming_api
|
||||||
redis.publish("timeline:access_token:#{id}", Oj.dump(event: :kill)) if revoked? || destroyed?
|
redis.publish("timeline:access_token:#{id}", { event: :kill }.to_json) if revoked? || destroyed?
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ class ActivityPub::Activity::Follow < ActivityPub::Activity
|
|||||||
end
|
end
|
||||||
|
|
||||||
def reject_follow_request!(target_account)
|
def reject_follow_request!(target_account)
|
||||||
json = Oj.dump(serialize_payload(FollowRequest.new(account: @account, target_account: target_account, uri: @json['id']), ActivityPub::RejectFollowSerializer))
|
json = serialize_payload(FollowRequest.new(account: @account, target_account: target_account, uri: @json['id']), ActivityPub::RejectFollowSerializer).to_json
|
||||||
ActivityPub::DeliveryWorker.perform_async(json, target_account.id, @account.inbox_url)
|
ActivityPub::DeliveryWorker.perform_async(json, target_account.id, @account.inbox_url)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ class ActivityPub::Activity::QuoteRequest < ActivityPub::Activity
|
|||||||
status.quote.update!(activity_uri: @json['id'])
|
status.quote.update!(activity_uri: @json['id'])
|
||||||
status.quote.accept!
|
status.quote.accept!
|
||||||
|
|
||||||
json = Oj.dump(serialize_payload(status.quote, ActivityPub::AcceptQuoteRequestSerializer))
|
json = serialize_payload(status.quote, ActivityPub::AcceptQuoteRequestSerializer).to_json
|
||||||
ActivityPub::DeliveryWorker.perform_async(json, quoted_status.account_id, @account.inbox_url)
|
ActivityPub::DeliveryWorker.perform_async(json, quoted_status.account_id, @account.inbox_url)
|
||||||
|
|
||||||
# Ensure the user is notified
|
# Ensure the user is notified
|
||||||
@@ -60,7 +60,7 @@ class ActivityPub::Activity::QuoteRequest < ActivityPub::Activity
|
|||||||
account: @account,
|
account: @account,
|
||||||
activity_uri: @json['id']
|
activity_uri: @json['id']
|
||||||
)
|
)
|
||||||
json = Oj.dump(serialize_payload(quote, ActivityPub::RejectQuoteRequestSerializer))
|
json = serialize_payload(quote, ActivityPub::RejectQuoteRequestSerializer).to_json
|
||||||
ActivityPub::DeliveryWorker.perform_async(json, quoted_status.account_id, @account.inbox_url)
|
ActivityPub::DeliveryWorker.perform_async(json, quoted_status.account_id, @account.inbox_url)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ class Admin::Metrics::Dimension::SoftwareVersionsDimension < Admin::Metrics::Dim
|
|||||||
|
|
||||||
def ffmpeg_version
|
def ffmpeg_version
|
||||||
version_output = Terrapin::CommandLine.new(Rails.configuration.x.ffprobe_binary, '-show_program_version -v 0 -of json').run
|
version_output = Terrapin::CommandLine.new(Rails.configuration.x.ffprobe_binary, '-show_program_version -v 0 -of json').run
|
||||||
version = Oj.load(version_output, mode: :strict, symbol_keys: true).dig(:program_version, :version)
|
version = JSON.parse(version_output, symbolize_names: true).dig(:program_version, :version)
|
||||||
|
|
||||||
{
|
{
|
||||||
key: 'ffmpeg',
|
key: 'ffmpeg',
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ module ApplicationExtension
|
|||||||
|
|
||||||
def close_streaming_sessions(resource_owner = nil)
|
def close_streaming_sessions(resource_owner = nil)
|
||||||
# TODO: #28793 Combine into a single topic
|
# TODO: #28793 Combine into a single topic
|
||||||
payload = Oj.dump(event: :kill)
|
payload = { event: :kill }.to_json
|
||||||
scope = access_tokens
|
scope = access_tokens
|
||||||
scope = scope.where(resource_owner_id: resource_owner.id) unless resource_owner.nil?
|
scope = scope.where(resource_owner_id: resource_owner.id) unless resource_owner.nil?
|
||||||
scope.in_batches do |tokens|
|
scope.in_batches do |tokens|
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ class FeedManager
|
|||||||
def unpush_from_home(account, status, update: false)
|
def unpush_from_home(account, status, update: false)
|
||||||
return false unless remove_from_feed(:home, account.id, status, aggregate_reblogs: account.user&.aggregates_reblogs?)
|
return false unless remove_from_feed(:home, account.id, status, aggregate_reblogs: account.user&.aggregates_reblogs?)
|
||||||
|
|
||||||
redis.publish("timeline:#{account.id}", Oj.dump(event: :delete, payload: status.id.to_s)) unless update
|
redis.publish("timeline:#{account.id}", { event: :delete, payload: status.id.to_s }.to_json) unless update
|
||||||
true
|
true
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -119,7 +119,7 @@ class FeedManager
|
|||||||
def unpush_from_list(list, status, update: false)
|
def unpush_from_list(list, status, update: false)
|
||||||
return false unless remove_from_feed(:list, list.id, status, aggregate_reblogs: list.account.user&.aggregates_reblogs?)
|
return false unless remove_from_feed(:list, list.id, status, aggregate_reblogs: list.account.user&.aggregates_reblogs?)
|
||||||
|
|
||||||
redis.publish("timeline:list:#{list.id}", Oj.dump(event: :delete, payload: status.id.to_s)) unless update
|
redis.publish("timeline:list:#{list.id}", { event: :delete, payload: status.id.to_s }.to_json) unless update
|
||||||
true
|
true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ class LinkDetailsExtractor
|
|||||||
end
|
end
|
||||||
|
|
||||||
def json
|
def json
|
||||||
@json ||= root_array(Oj.load(@data)).compact.find { |obj| SUPPORTED_TYPES.include?(obj['@type']) } || {}
|
@json ||= root_array(JSON.parse(@data)).compact.find { |obj| SUPPORTED_TYPES.include?(obj['@type']) } || {}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -265,7 +265,7 @@ class LinkDetailsExtractor
|
|||||||
next unless structured_data.valid?
|
next unless structured_data.valid?
|
||||||
|
|
||||||
structured_data
|
structured_data
|
||||||
rescue Oj::ParseError, EncodingError
|
rescue JSON::ParserError, EncodingError
|
||||||
Rails.logger.debug { "Invalid JSON-LD in #{@original_url}" }
|
Rails.logger.debug { "Invalid JSON-LD in #{@original_url}" }
|
||||||
next
|
next
|
||||||
end.first
|
end.first
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ class TranslationService::DeepL < TranslationService
|
|||||||
|
|
||||||
def fetch_languages(type)
|
def fetch_languages(type)
|
||||||
request(:get, "/v2/languages?type=#{type}") do |res|
|
request(:get, "/v2/languages?type=#{type}") do |res|
|
||||||
Oj.load(res.body_with_limit).map { |language| normalize_language(language['language']) }
|
JSON.parse(res.body_with_limit).map { |language| normalize_language(language['language']) }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -68,7 +68,7 @@ class TranslationService::DeepL < TranslationService
|
|||||||
end
|
end
|
||||||
|
|
||||||
def transform_response(json)
|
def transform_response(json)
|
||||||
data = Oj.load(json, mode: :strict)
|
data = JSON.parse(json)
|
||||||
raise UnexpectedResponseError unless data.is_a?(Hash)
|
raise UnexpectedResponseError unless data.is_a?(Hash)
|
||||||
|
|
||||||
data['translations'].map do |translation|
|
data['translations'].map do |translation|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ class TranslationService::LibreTranslate < TranslationService
|
|||||||
end
|
end
|
||||||
|
|
||||||
def translate(texts, source_language, target_language)
|
def translate(texts, source_language, target_language)
|
||||||
body = Oj.dump(q: texts, source: source_language.presence || 'auto', target: target_language, format: 'html', api_key: @api_key)
|
body = { q: texts, source: source_language.presence || 'auto', target: target_language, format: 'html', api_key: @api_key }.to_json
|
||||||
request(:post, '/translate', body: body) do |res|
|
request(:post, '/translate', body: body) do |res|
|
||||||
transform_response(res.body_with_limit, source_language)
|
transform_response(res.body_with_limit, source_language)
|
||||||
end
|
end
|
||||||
@@ -17,7 +17,7 @@ class TranslationService::LibreTranslate < TranslationService
|
|||||||
|
|
||||||
def languages
|
def languages
|
||||||
request(:get, '/languages') do |res|
|
request(:get, '/languages') do |res|
|
||||||
languages = Oj.load(res.body_with_limit).to_h do |language|
|
languages = JSON.parse(res.body_with_limit).to_h do |language|
|
||||||
[language['code'], language['targets'].without(language['code'])]
|
[language['code'], language['targets'].without(language['code'])]
|
||||||
end
|
end
|
||||||
languages[nil] = languages.values.flatten.uniq.sort
|
languages[nil] = languages.values.flatten.uniq.sort
|
||||||
@@ -45,7 +45,7 @@ class TranslationService::LibreTranslate < TranslationService
|
|||||||
end
|
end
|
||||||
|
|
||||||
def transform_response(json, source_language)
|
def transform_response(json, source_language)
|
||||||
data = Oj.load(json, mode: :strict)
|
data = JSON.parse(json)
|
||||||
raise UnexpectedResponseError unless data.is_a?(Hash)
|
raise UnexpectedResponseError unless data.is_a?(Hash)
|
||||||
|
|
||||||
data['translatedText'].map.with_index do |text, index|
|
data['translatedText'].map.with_index do |text, index|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ class UserSettingsSerializer
|
|||||||
if value.blank?
|
if value.blank?
|
||||||
{}
|
{}
|
||||||
else
|
else
|
||||||
Oj.load(value, symbol_keys: true)
|
JSON.parse(value, symbolize_names: true)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -14,6 +14,6 @@ class UserSettingsSerializer
|
|||||||
end
|
end
|
||||||
|
|
||||||
def self.dump(value)
|
def self.dump(value)
|
||||||
Oj.dump(value.as_json)
|
JSON.generate(value.as_json)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ class VideoMetadataExtractor
|
|||||||
|
|
||||||
def initialize(path)
|
def initialize(path)
|
||||||
@path = path
|
@path = path
|
||||||
@metadata = Oj.load(ffmpeg_command_output, mode: :strict, symbol_keys: true)
|
@metadata = JSON.parse(ffmpeg_command_output, symbolize_names: true)
|
||||||
|
|
||||||
parse_metadata
|
parse_metadata
|
||||||
rescue Terrapin::ExitStatusError, Oj::ParseError
|
rescue Terrapin::ExitStatusError, Oj::ParseError
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ class Webfinger
|
|||||||
|
|
||||||
def initialize(uri, body)
|
def initialize(uri, body)
|
||||||
@uri = uri
|
@uri = uri
|
||||||
@json = Oj.load(body, mode: :strict)
|
@json = JSON.parse(body)
|
||||||
|
|
||||||
validate_response!
|
validate_response!
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ class Webhooks::PayloadRenderer
|
|||||||
/iox
|
/iox
|
||||||
|
|
||||||
def initialize(json)
|
def initialize(json)
|
||||||
@document = DocumentTraverser.new(Oj.load(json))
|
@document = DocumentTraverser.new(JSON.parse(json))
|
||||||
end
|
end
|
||||||
|
|
||||||
def render(template)
|
def render(template)
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ module Account::Suspensions
|
|||||||
|
|
||||||
# This terminates all connections for the given account with the streaming
|
# This terminates all connections for the given account with the streaming
|
||||||
# server:
|
# server:
|
||||||
redis.publish("timeline:system:#{id}", Oj.dump(event: :kill)) if local?
|
redis.publish("timeline:system:#{id}", { event: :kill }.to_json) if local?
|
||||||
end
|
end
|
||||||
|
|
||||||
def unsuspend!
|
def unsuspend!
|
||||||
|
|||||||
@@ -115,8 +115,8 @@ class CustomFilter < ApplicationRecord
|
|||||||
@should_invalidate_cache = false
|
@should_invalidate_cache = false
|
||||||
|
|
||||||
Rails.cache.delete("filters:v3:#{account_id}")
|
Rails.cache.delete("filters:v3:#{account_id}")
|
||||||
redis.publish("timeline:#{account_id}", Oj.dump(event: :filters_changed))
|
redis.publish("timeline:#{account_id}", { event: :filters_changed }.to_json)
|
||||||
redis.publish("timeline:system:#{account_id}", Oj.dump(event: :filters_changed))
|
redis.publish("timeline:system:#{account_id}", { event: :filters_changed }.to_json)
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ class Relay < ApplicationRecord
|
|||||||
|
|
||||||
def enable!
|
def enable!
|
||||||
activity_id = ActivityPub::TagManager.instance.generate_uri_for(nil)
|
activity_id = ActivityPub::TagManager.instance.generate_uri_for(nil)
|
||||||
payload = Oj.dump(follow_activity(activity_id))
|
payload = follow_activity(activity_id).to_json
|
||||||
|
|
||||||
update!(state: :pending, follow_activity_id: activity_id)
|
update!(state: :pending, follow_activity_id: activity_id)
|
||||||
reset_delivery_tracker
|
reset_delivery_tracker
|
||||||
@@ -40,7 +40,7 @@ class Relay < ApplicationRecord
|
|||||||
|
|
||||||
def disable!
|
def disable!
|
||||||
activity_id = ActivityPub::TagManager.instance.generate_uri_for(nil)
|
activity_id = ActivityPub::TagManager.instance.generate_uri_for(nil)
|
||||||
payload = Oj.dump(unfollow_activity(activity_id))
|
payload = unfollow_activity(activity_id).to_json
|
||||||
|
|
||||||
update!(state: :idle, follow_activity_id: nil)
|
update!(state: :idle, follow_activity_id: nil)
|
||||||
reset_delivery_tracker
|
reset_delivery_tracker
|
||||||
|
|||||||
@@ -173,7 +173,7 @@ class User < ApplicationRecord
|
|||||||
|
|
||||||
# This terminates all connections for the given account with the streaming
|
# This terminates all connections for the given account with the streaming
|
||||||
# server:
|
# server:
|
||||||
redis.publish("timeline:system:#{account.id}", Oj.dump(event: :kill))
|
redis.publish("timeline:system:#{account.id}", { event: :kill }.to_json)
|
||||||
end
|
end
|
||||||
|
|
||||||
def enable!
|
def enable!
|
||||||
@@ -347,7 +347,7 @@ class User < ApplicationRecord
|
|||||||
# Revoke each access token for the Streaming API, since `update_all``
|
# Revoke each access token for the Streaming API, since `update_all``
|
||||||
# doesn't trigger ActiveRecord Callbacks:
|
# doesn't trigger ActiveRecord Callbacks:
|
||||||
# TODO: #28793 Combine into a single topic
|
# TODO: #28793 Combine into a single topic
|
||||||
payload = Oj.dump(event: :kill)
|
payload = { event: :kill }.to_json
|
||||||
redis.pipelined do |pipeline|
|
redis.pipelined do |pipeline|
|
||||||
batch.ids.each do |id|
|
batch.ids.each do |id|
|
||||||
pipeline.publish("timeline:access_token:#{id}", payload)
|
pipeline.publish("timeline:access_token:#{id}", payload)
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ class ActivityPub::ProcessCollectionService < BaseService
|
|||||||
|
|
||||||
def call(body, actor, **options)
|
def call(body, actor, **options)
|
||||||
@account = actor
|
@account = actor
|
||||||
@json = original_json = Oj.load(body, mode: :strict)
|
@json = original_json = JSON.parse(body)
|
||||||
@options = options
|
@options = options
|
||||||
|
|
||||||
return unless @json.is_a?(Hash)
|
return unless @json.is_a?(Hash)
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ class ActivityPub::SynchronizeFollowersService < BaseService
|
|||||||
end
|
end
|
||||||
|
|
||||||
def build_undo_follow_json(follow)
|
def build_undo_follow_json(follow)
|
||||||
Oj.dump(serialize_payload(follow, ActivityPub::UndoFollowSerializer))
|
serialize_payload(follow, ActivityPub::UndoFollowSerializer).to_json
|
||||||
end
|
end
|
||||||
|
|
||||||
# Only returns true if the whole collection has been processed
|
# Only returns true if the whole collection has been processed
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ class AfterBlockDomainFromAccountService < BaseService
|
|||||||
|
|
||||||
return unless follow.account.activitypub?
|
return unless follow.account.activitypub?
|
||||||
|
|
||||||
ActivityPub::DeliveryWorker.perform_async(Oj.dump(serialize_payload(follow, ActivityPub::RejectFollowSerializer)), @account.id, follow.account.inbox_url)
|
ActivityPub::DeliveryWorker.perform_async(serialize_payload(follow, ActivityPub::RejectFollowSerializer).to_json, @account.id, follow.account.inbox_url)
|
||||||
end
|
end
|
||||||
|
|
||||||
def notify_of_severed_relationships!
|
def notify_of_severed_relationships!
|
||||||
|
|||||||
@@ -22,6 +22,6 @@ class AuthorizeFollowService < BaseService
|
|||||||
end
|
end
|
||||||
|
|
||||||
def build_json(follow_request)
|
def build_json(follow_request)
|
||||||
Oj.dump(serialize_payload(follow_request, ActivityPub::AcceptFollowSerializer))
|
serialize_payload(follow_request, ActivityPub::AcceptFollowSerializer).to_json
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ class BackupService < BaseService
|
|||||||
skeleton = serialize(collection_presenter, ActivityPub::CollectionSerializer)
|
skeleton = serialize(collection_presenter, ActivityPub::CollectionSerializer)
|
||||||
skeleton[:@context] = full_context
|
skeleton[:@context] = full_context
|
||||||
skeleton[:orderedItems] = ['!PLACEHOLDER!']
|
skeleton[:orderedItems] = ['!PLACEHOLDER!']
|
||||||
skeleton = Oj.dump(skeleton)
|
skeleton = JSON.generate(skeleton)
|
||||||
prepend, append = skeleton.split('"!PLACEHOLDER!"')
|
prepend, append = skeleton.split('"!PLACEHOLDER!"')
|
||||||
add_comma = false
|
add_comma = false
|
||||||
|
|
||||||
@@ -44,7 +44,7 @@ class BackupService < BaseService
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
Oj.dump(item)
|
JSON.generate(item)
|
||||||
end.join(','))
|
end.join(','))
|
||||||
|
|
||||||
GC.start
|
GC.start
|
||||||
@@ -107,7 +107,7 @@ class BackupService < BaseService
|
|||||||
download_to_zip(zipfile, account.avatar, "avatar#{File.extname(account.avatar.path)}") if account.avatar.exists?
|
download_to_zip(zipfile, account.avatar, "avatar#{File.extname(account.avatar.path)}") if account.avatar.exists?
|
||||||
download_to_zip(zipfile, account.header, "header#{File.extname(account.header.path)}") if account.header.exists?
|
download_to_zip(zipfile, account.header, "header#{File.extname(account.header.path)}") if account.header.exists?
|
||||||
|
|
||||||
json = Oj.dump(actor)
|
json = JSON.generate(actor)
|
||||||
|
|
||||||
zipfile.get_output_stream('actor.json') do |io|
|
zipfile.get_output_stream('actor.json') do |io|
|
||||||
io.write(json)
|
io.write(json)
|
||||||
@@ -118,7 +118,7 @@ class BackupService < BaseService
|
|||||||
skeleton = serialize(ActivityPub::CollectionPresenter.new(id: 'likes.json', type: :ordered, size: 0, items: []), ActivityPub::CollectionSerializer)
|
skeleton = serialize(ActivityPub::CollectionPresenter.new(id: 'likes.json', type: :ordered, size: 0, items: []), ActivityPub::CollectionSerializer)
|
||||||
skeleton.delete(:totalItems)
|
skeleton.delete(:totalItems)
|
||||||
skeleton[:orderedItems] = ['!PLACEHOLDER!']
|
skeleton[:orderedItems] = ['!PLACEHOLDER!']
|
||||||
skeleton = Oj.dump(skeleton)
|
skeleton = JSON.generate(skeleton)
|
||||||
prepend, append = skeleton.split('"!PLACEHOLDER!"')
|
prepend, append = skeleton.split('"!PLACEHOLDER!"')
|
||||||
|
|
||||||
zipfile.get_output_stream('likes.json') do |io|
|
zipfile.get_output_stream('likes.json') do |io|
|
||||||
@@ -131,7 +131,7 @@ class BackupService < BaseService
|
|||||||
add_comma = true
|
add_comma = true
|
||||||
|
|
||||||
io.write(statuses.map do |status|
|
io.write(statuses.map do |status|
|
||||||
Oj.dump(ActivityPub::TagManager.instance.uri_for(status))
|
JSON.generate(ActivityPub::TagManager.instance.uri_for(status))
|
||||||
end.join(','))
|
end.join(','))
|
||||||
|
|
||||||
GC.start
|
GC.start
|
||||||
@@ -145,7 +145,7 @@ class BackupService < BaseService
|
|||||||
skeleton = serialize(ActivityPub::CollectionPresenter.new(id: 'bookmarks.json', type: :ordered, size: 0, items: []), ActivityPub::CollectionSerializer)
|
skeleton = serialize(ActivityPub::CollectionPresenter.new(id: 'bookmarks.json', type: :ordered, size: 0, items: []), ActivityPub::CollectionSerializer)
|
||||||
skeleton.delete(:totalItems)
|
skeleton.delete(:totalItems)
|
||||||
skeleton[:orderedItems] = ['!PLACEHOLDER!']
|
skeleton[:orderedItems] = ['!PLACEHOLDER!']
|
||||||
skeleton = Oj.dump(skeleton)
|
skeleton = JSON.generate(skeleton)
|
||||||
prepend, append = skeleton.split('"!PLACEHOLDER!"')
|
prepend, append = skeleton.split('"!PLACEHOLDER!"')
|
||||||
|
|
||||||
zipfile.get_output_stream('bookmarks.json') do |io|
|
zipfile.get_output_stream('bookmarks.json') do |io|
|
||||||
@@ -157,7 +157,7 @@ class BackupService < BaseService
|
|||||||
add_comma = true
|
add_comma = true
|
||||||
|
|
||||||
io.write(statuses.map do |status|
|
io.write(statuses.map do |status|
|
||||||
Oj.dump(ActivityPub::TagManager.instance.uri_for(status))
|
JSON.generate(ActivityPub::TagManager.instance.uri_for(status))
|
||||||
end.join(','))
|
end.join(','))
|
||||||
|
|
||||||
GC.start
|
GC.start
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ class BatchedRemoveStatusService < BaseService
|
|||||||
def unpush_from_public_timelines(status, pipeline)
|
def unpush_from_public_timelines(status, pipeline)
|
||||||
return unless status.public_visibility? && status.id > @status_id_cutoff
|
return unless status.public_visibility? && status.id > @status_id_cutoff
|
||||||
|
|
||||||
payload = Oj.dump(event: :delete, payload: status.id.to_s)
|
payload = { event: :delete, payload: status.id.to_s }.to_json
|
||||||
|
|
||||||
pipeline.publish('timeline:public', payload)
|
pipeline.publish('timeline:public', payload)
|
||||||
pipeline.publish(status.local? ? 'timeline:public:local' : 'timeline:public:remote', payload)
|
pipeline.publish(status.local? ? 'timeline:public:local' : 'timeline:public:remote', payload)
|
||||||
|
|||||||
@@ -26,6 +26,6 @@ class BlockService < BaseService
|
|||||||
end
|
end
|
||||||
|
|
||||||
def build_json(block)
|
def build_json(block)
|
||||||
Oj.dump(serialize_payload(block, ActivityPub::BlockSerializer))
|
serialize_payload(block, ActivityPub::BlockSerializer).to_json
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -26,6 +26,6 @@ class CreateFeaturedTagService < BaseService
|
|||||||
private
|
private
|
||||||
|
|
||||||
def build_json(featured_tag)
|
def build_json(featured_tag)
|
||||||
Oj.dump(serialize_payload(featured_tag, ActivityPub::AddHashtagSerializer, signer: @account))
|
serialize_payload(featured_tag, ActivityPub::AddHashtagSerializer, signer: @account).to_json
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ class DeleteAccountService < BaseService
|
|||||||
# we have to force it to unfollow them.
|
# we have to force it to unfollow them.
|
||||||
|
|
||||||
ActivityPub::DeliveryWorker.push_bulk(Follow.where(account: @account)) do |follow|
|
ActivityPub::DeliveryWorker.push_bulk(Follow.where(account: @account)) do |follow|
|
||||||
[Oj.dump(serialize_payload(follow, ActivityPub::RejectFollowSerializer)), follow.target_account_id, @account.inbox_url]
|
[serialize_payload(follow, ActivityPub::RejectFollowSerializer).to_json, follow.target_account_id, @account.inbox_url]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -126,7 +126,7 @@ class DeleteAccountService < BaseService
|
|||||||
# if the remote account gets un-suspended.
|
# if the remote account gets un-suspended.
|
||||||
|
|
||||||
ActivityPub::DeliveryWorker.push_bulk(Follow.where(target_account: @account)) do |follow|
|
ActivityPub::DeliveryWorker.push_bulk(Follow.where(target_account: @account)) do |follow|
|
||||||
[Oj.dump(serialize_payload(follow, ActivityPub::UndoFollowSerializer)), follow.account_id, @account.inbox_url]
|
[serialize_payload(follow, ActivityPub::UndoFollowSerializer).to_json, follow.account_id, @account.inbox_url]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -285,7 +285,7 @@ class DeleteAccountService < BaseService
|
|||||||
end
|
end
|
||||||
|
|
||||||
def delete_actor_json
|
def delete_actor_json
|
||||||
@delete_actor_json ||= Oj.dump(serialize_payload(@account, ActivityPub::DeleteActorSerializer, signer: @account, always_sign: true))
|
@delete_actor_json ||= serialize_payload(@account, ActivityPub::DeleteActorSerializer, signer: @account, always_sign: true).to_json
|
||||||
end
|
end
|
||||||
|
|
||||||
def delivery_inboxes
|
def delivery_inboxes
|
||||||
|
|||||||
@@ -179,10 +179,10 @@ class FanOutOnWriteService < BaseService
|
|||||||
end
|
end
|
||||||
|
|
||||||
def anonymous_payload
|
def anonymous_payload
|
||||||
@anonymous_payload ||= Oj.dump(
|
@anonymous_payload ||= JSON.generate({
|
||||||
event: update? ? :'status.update' : :update,
|
event: update? ? :'status.update' : :update,
|
||||||
payload: rendered_status
|
payload: rendered_status,
|
||||||
)
|
}.as_json)
|
||||||
end
|
end
|
||||||
|
|
||||||
def rendered_status
|
def rendered_status
|
||||||
|
|||||||
@@ -42,6 +42,6 @@ class FavouriteService < BaseService
|
|||||||
end
|
end
|
||||||
|
|
||||||
def build_json(favourite)
|
def build_json(favourite)
|
||||||
Oj.dump(serialize_payload(favourite, ActivityPub::LikeSerializer))
|
serialize_payload(favourite, ActivityPub::LikeSerializer).to_json
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ class FollowService < BaseService
|
|||||||
end
|
end
|
||||||
|
|
||||||
def build_json(follow_request)
|
def build_json(follow_request)
|
||||||
Oj.dump(serialize_payload(follow_request, ActivityPub::FollowSerializer))
|
serialize_payload(follow_request, ActivityPub::FollowSerializer).to_json
|
||||||
end
|
end
|
||||||
|
|
||||||
def follow_options
|
def follow_options
|
||||||
|
|||||||
@@ -259,7 +259,7 @@ class NotifyService < BaseService
|
|||||||
end
|
end
|
||||||
|
|
||||||
def push_to_streaming_api!
|
def push_to_streaming_api!
|
||||||
redis.publish("timeline:#{@recipient.id}:notifications", Oj.dump(event: :notification, payload: InlineRenderer.render(@notification, @recipient, :notification)))
|
redis.publish("timeline:#{@recipient.id}:notifications", { event: :notification, payload: InlineRenderer.render(@notification, @recipient, :notification) }.to_json)
|
||||||
end
|
end
|
||||||
|
|
||||||
def subscribed_to_streaming_api?
|
def subscribed_to_streaming_api?
|
||||||
|
|||||||
@@ -17,6 +17,6 @@ class RejectFollowService < BaseService
|
|||||||
end
|
end
|
||||||
|
|
||||||
def build_json(follow_request)
|
def build_json(follow_request)
|
||||||
Oj.dump(serialize_payload(follow_request, ActivityPub::RejectFollowSerializer))
|
serialize_payload(follow_request, ActivityPub::RejectFollowSerializer).to_json
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -18,6 +18,6 @@ class RemoveDomainsFromFollowersService < BaseService
|
|||||||
end
|
end
|
||||||
|
|
||||||
def build_json(follow)
|
def build_json(follow)
|
||||||
Oj.dump(serialize_payload(follow, ActivityPub::RejectFollowSerializer))
|
serialize_payload(follow, ActivityPub::RejectFollowSerializer).to_json
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -26,6 +26,6 @@ class RemoveFeaturedTagService < BaseService
|
|||||||
private
|
private
|
||||||
|
|
||||||
def build_json(featured_tag)
|
def build_json(featured_tag)
|
||||||
Oj.dump(serialize_payload(featured_tag, ActivityPub::RemoveHashtagSerializer, signer: @account))
|
serialize_payload(featured_tag, ActivityPub::RemoveHashtagSerializer, signer: @account).to_json
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -18,6 +18,6 @@ class RemoveFromFollowersService < BaseService
|
|||||||
end
|
end
|
||||||
|
|
||||||
def build_json(follow)
|
def build_json(follow)
|
||||||
Oj.dump(serialize_payload(follow, ActivityPub::RejectFollowSerializer))
|
serialize_payload(follow, ActivityPub::RejectFollowSerializer).to_json
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ class RemoveStatusService < BaseService
|
|||||||
# @option [Boolean] :original_removed
|
# @option [Boolean] :original_removed
|
||||||
# @option [Boolean] :skip_streaming
|
# @option [Boolean] :skip_streaming
|
||||||
def call(status, **options)
|
def call(status, **options)
|
||||||
@payload = Oj.dump(event: :delete, payload: status.id.to_s)
|
@payload = { event: :delete, payload: status.id.to_s }.to_json
|
||||||
@status = status
|
@status = status
|
||||||
@account = status.account
|
@account = status.account
|
||||||
@options = options
|
@options = options
|
||||||
@@ -105,7 +105,7 @@ class RemoveStatusService < BaseService
|
|||||||
end
|
end
|
||||||
|
|
||||||
def signed_activity_json
|
def signed_activity_json
|
||||||
@signed_activity_json ||= Oj.dump(serialize_payload(@status, @status.reblog? ? ActivityPub::UndoAnnounceSerializer : ActivityPub::DeleteNoteSerializer, signer: @account, always_sign: true))
|
@signed_activity_json ||= serialize_payload(@status, @status.reblog? ? ActivityPub::UndoAnnounceSerializer : ActivityPub::DeleteNoteSerializer, signer: @account, always_sign: true).to_json
|
||||||
end
|
end
|
||||||
|
|
||||||
def remove_reblogs
|
def remove_reblogs
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ class ReportService < BaseService
|
|||||||
end
|
end
|
||||||
|
|
||||||
def payload
|
def payload
|
||||||
Oj.dump(serialize_payload(@report, ActivityPub::FlagSerializer, account: some_local_account))
|
serialize_payload(@report, ActivityPub::FlagSerializer, account: some_local_account).to_json
|
||||||
end
|
end
|
||||||
|
|
||||||
def some_local_account
|
def some_local_account
|
||||||
|
|||||||
@@ -19,6 +19,6 @@ class RevokeCollectionItemService < BaseService
|
|||||||
end
|
end
|
||||||
|
|
||||||
def signed_activity_json
|
def signed_activity_json
|
||||||
@signed_activity_json ||= Oj.dump(serialize_payload(@collection_item, ActivityPub::DeleteFeatureAuthorizationSerializer, signer: @account, always_sign: true))
|
@signed_activity_json ||= serialize_payload(@collection_item, ActivityPub::DeleteFeatureAuthorizationSerializer, signer: @account, always_sign: true).to_json
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -39,6 +39,6 @@ class RevokeQuoteService < BaseService
|
|||||||
end
|
end
|
||||||
|
|
||||||
def signed_activity_json
|
def signed_activity_json
|
||||||
@signed_activity_json ||= Oj.dump(serialize_payload(@quote, ActivityPub::DeleteQuoteAuthorizationSerializer, signer: @account, always_sign: true, force_approval_id: true))
|
@signed_activity_json ||= serialize_payload(@quote, ActivityPub::DeleteQuoteAuthorizationSerializer, signer: @account, always_sign: true, force_approval_id: true).to_json
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ class SoftwareUpdateCheckService < BaseService
|
|||||||
|
|
||||||
def fetch_update_notices
|
def fetch_update_notices
|
||||||
Request.new(:get, "#{api_url}?version=#{version}").add_headers('Accept' => 'application/json', 'User-Agent' => 'Mastodon update checker').perform do |res|
|
Request.new(:get, "#{api_url}?version=#{version}").add_headers('Accept' => 'application/json', 'User-Agent' => 'Mastodon update checker').perform do |res|
|
||||||
return Oj.load(res.body_with_limit, mode: :strict) if res.code == 200
|
return JSON.parse(res.body_with_limit) if res.code == 200
|
||||||
end
|
end
|
||||||
rescue *Mastodon::HTTP_CONNECTION_ERRORS, Oj::ParseError
|
rescue *Mastodon::HTTP_CONNECTION_ERRORS, Oj::ParseError
|
||||||
nil
|
nil
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ class SuspendAccountService < BaseService
|
|||||||
|
|
||||||
Follow.where(account: @account).find_in_batches do |follows|
|
Follow.where(account: @account).find_in_batches do |follows|
|
||||||
ActivityPub::DeliveryWorker.push_bulk(follows) do |follow|
|
ActivityPub::DeliveryWorker.push_bulk(follows) do |follow|
|
||||||
[Oj.dump(serialize_payload(follow, ActivityPub::RejectFollowSerializer)), follow.target_account_id, @account.inbox_url]
|
[serialize_payload(follow, ActivityPub::RejectFollowSerializer).to_json, follow.target_account_id, @account.inbox_url]
|
||||||
end
|
end
|
||||||
|
|
||||||
follows.each(&:destroy)
|
follows.each(&:destroy)
|
||||||
@@ -72,6 +72,6 @@ class SuspendAccountService < BaseService
|
|||||||
end
|
end
|
||||||
|
|
||||||
def signed_activity_json
|
def signed_activity_json
|
||||||
@signed_activity_json ||= Oj.dump(serialize_payload(@account, ActivityPub::UpdateActorSerializer, signer: @account))
|
@signed_activity_json ||= serialize_payload(@account, ActivityPub::UpdateActorSerializer, signer: @account).to_json
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -18,6 +18,6 @@ class UnblockService < BaseService
|
|||||||
end
|
end
|
||||||
|
|
||||||
def build_json(unblock)
|
def build_json(unblock)
|
||||||
Oj.dump(serialize_payload(unblock, ActivityPub::UndoBlockSerializer))
|
serialize_payload(unblock, ActivityPub::UndoBlockSerializer).to_json
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -18,6 +18,6 @@ class UnfavouriteService < BaseService
|
|||||||
end
|
end
|
||||||
|
|
||||||
def build_json(favourite)
|
def build_json(favourite)
|
||||||
Oj.dump(serialize_payload(favourite, ActivityPub::UndoLikeSerializer))
|
serialize_payload(favourite, ActivityPub::UndoLikeSerializer).to_json
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -68,10 +68,10 @@ class UnfollowService < BaseService
|
|||||||
end
|
end
|
||||||
|
|
||||||
def build_json(follow)
|
def build_json(follow)
|
||||||
Oj.dump(serialize_payload(follow, ActivityPub::UndoFollowSerializer))
|
serialize_payload(follow, ActivityPub::UndoFollowSerializer).to_json
|
||||||
end
|
end
|
||||||
|
|
||||||
def build_reject_json(follow)
|
def build_reject_json(follow)
|
||||||
Oj.dump(serialize_payload(follow, ActivityPub::RejectFollowSerializer))
|
serialize_payload(follow, ActivityPub::RejectFollowSerializer).to_json
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -63,6 +63,6 @@ class UnsuspendAccountService < BaseService
|
|||||||
end
|
end
|
||||||
|
|
||||||
def signed_activity_json
|
def signed_activity_json
|
||||||
@signed_activity_json ||= Oj.dump(serialize_payload(@account, ActivityPub::UpdateActorSerializer, signer: @account))
|
@signed_activity_json ||= serialize_payload(@account, ActivityPub::UpdateActorSerializer, signer: @account).to_json
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ class VoteService < BaseService
|
|||||||
end
|
end
|
||||||
|
|
||||||
def build_json(vote)
|
def build_json(vote)
|
||||||
Oj.dump(serialize_payload(vote, ActivityPub::VoteSerializer))
|
serialize_payload(vote, ActivityPub::VoteSerializer).to_json
|
||||||
end
|
end
|
||||||
|
|
||||||
def increment_voters_count!
|
def increment_voters_count!
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ class WebhookService < BaseService
|
|||||||
end
|
end
|
||||||
|
|
||||||
def serialize_event
|
def serialize_event
|
||||||
Oj.dump(ActiveModelSerializers::SerializableResource.new(@event, serializer: REST::Admin::WebhookEventSerializer, scope: nil, scope_name: :current_user).as_json)
|
ActiveModelSerializers::SerializableResource
|
||||||
|
.new(@event, serializer: REST::Admin::WebhookEventSerializer, scope: nil, scope_name: :current_user)
|
||||||
|
.to_json
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class ReactionValidator < ActiveModel::Validator
|
class ReactionValidator < ActiveModel::Validator
|
||||||
SUPPORTED_EMOJIS = Oj.load_file(Rails.root.join('app', 'javascript', 'mastodon', 'features', 'emoji', 'emoji_map.json').to_s).keys.freeze
|
SUPPORTED_EMOJIS = JSON.load_file(Rails.root.join('app', 'javascript', 'mastodon', 'features', 'emoji', 'emoji_map.json').to_s).keys.freeze
|
||||||
|
|
||||||
LIMIT = 8
|
LIMIT = 8
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
= render_initial_state
|
= render_initial_state
|
||||||
= flavoured_vite_typescript_tag 'application.ts', crossorigin: 'anonymous'
|
= flavoured_vite_typescript_tag 'application.ts', crossorigin: 'anonymous'
|
||||||
|
|
||||||
.notranslate.app-holder#mastodon{ data: { props: Oj.dump(default_props) } }
|
.notranslate.app-holder#mastodon{ data: { props: default_props.to_json } }
|
||||||
%noscript
|
%noscript
|
||||||
= image_tag frontend_asset_path('images/logo.svg'), alt: 'Mastodon'
|
= image_tag frontend_asset_path('images/logo.svg'), alt: 'Mastodon'
|
||||||
|
|
||||||
|
|||||||
@@ -2,4 +2,4 @@
|
|||||||
= render_initial_state
|
= render_initial_state
|
||||||
= flavoured_vite_typescript_tag 'share.tsx', crossorigin: 'anonymous'
|
= flavoured_vite_typescript_tag 'share.tsx', crossorigin: 'anonymous'
|
||||||
|
|
||||||
#mastodon-compose{ data: { props: Oj.dump(default_props) } }
|
#mastodon-compose{ data: { props: default_props.to_json } }
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
#mastodon-status{ data: { props: Oj.dump(default_props.merge(id: @status.id.to_s)) } }
|
#mastodon-status{ data: { props: default_props.merge(id: @status.id.to_s).to_json } }
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ class ActivityPub::DistributePollUpdateWorker
|
|||||||
end
|
end
|
||||||
|
|
||||||
def payload
|
def payload
|
||||||
@payload ||= Oj.dump(serialize_payload(@status, ActivityPub::UpdatePollSerializer, signer: @account))
|
@payload ||= serialize_payload(@status, ActivityPub::UpdatePollSerializer, signer: @account).to_json
|
||||||
end
|
end
|
||||||
|
|
||||||
def relay!
|
def relay!
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ class ActivityPub::DistributionWorker < ActivityPub::RawDistributionWorker
|
|||||||
end
|
end
|
||||||
|
|
||||||
def payload
|
def payload
|
||||||
@payload ||= Oj.dump(serialize_payload(@status, activity_serializer, serializer_options.merge(signer: @account)))
|
@payload ||= serialize_payload(@status, activity_serializer, serializer_options.merge(signer: @account)).to_json
|
||||||
end
|
end
|
||||||
|
|
||||||
def activity_serializer
|
def activity_serializer
|
||||||
|
|||||||
@@ -17,6 +17,6 @@ class ActivityPub::FeatureRequestWorker < ActivityPub::RawDistributionWorker
|
|||||||
end
|
end
|
||||||
|
|
||||||
def payload
|
def payload
|
||||||
@payload ||= Oj.dump(serialize_payload(@collection_item, ActivityPub::FeatureRequestSerializer, signer: @account))
|
@payload ||= serialize_payload(@collection_item, ActivityPub::FeatureRequestSerializer, signer: @account).to_json
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -28,6 +28,6 @@ class ActivityPub::MoveDistributionWorker
|
|||||||
end
|
end
|
||||||
|
|
||||||
def signed_payload
|
def signed_payload
|
||||||
@signed_payload ||= Oj.dump(serialize_payload(@migration, ActivityPub::MoveSerializer, signer: @account))
|
@signed_payload ||= serialize_payload(@migration, ActivityPub::MoveSerializer, signer: @account).to_json
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -17,6 +17,6 @@ class ActivityPub::QuoteRequestWorker < ActivityPub::RawDistributionWorker
|
|||||||
end
|
end
|
||||||
|
|
||||||
def payload
|
def payload
|
||||||
@payload ||= Oj.dump(serialize_payload(@quote, ActivityPub::QuoteRequestSerializer, signer: @account, allow_post_inlining: true))
|
@payload ||= serialize_payload(@quote, ActivityPub::QuoteRequestSerializer, signer: @account, allow_post_inlining: true).to_json
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -23,6 +23,6 @@ class ActivityPub::UpdateDistributionWorker < ActivityPub::RawDistributionWorker
|
|||||||
end
|
end
|
||||||
|
|
||||||
def payload
|
def payload
|
||||||
@payload ||= Oj.dump(serialize_payload(@account, ActivityPub::UpdateActorSerializer, signer: @account, sign_with: @options[:sign_with]))
|
@payload ||= serialize_payload(@account, ActivityPub::UpdateActorSerializer, signer: @account, sign_with: @options[:sign_with]).to_json
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ class PublishAnnouncementReactionWorker
|
|||||||
reaction ||= announcement.announcement_reactions.new(name: name)
|
reaction ||= announcement.announcement_reactions.new(name: name)
|
||||||
|
|
||||||
payload = InlineRenderer.render(reaction, nil, :reaction).tap { |h| h[:announcement_id] = announcement_id.to_s }
|
payload = InlineRenderer.render(reaction, nil, :reaction).tap { |h| h[:announcement_id] = announcement_id.to_s }
|
||||||
payload = Oj.dump(event: :'announcement.reaction', payload: payload)
|
payload = { event: :'announcement.reaction', payload: payload }
|
||||||
|
|
||||||
FeedManager.instance.with_active_accounts do |account|
|
FeedManager.instance.with_active_accounts do |account|
|
||||||
redis.publish("timeline:#{account.id}", payload) if redis.exists?("subscribed:timeline:#{account.id}")
|
redis.publish("timeline:#{account.id}", payload) if redis.exists?("subscribed:timeline:#{account.id}")
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ class PublishScheduledAnnouncementWorker
|
|||||||
@announcement.publish! unless @announcement.published?
|
@announcement.publish! unless @announcement.published?
|
||||||
|
|
||||||
payload = InlineRenderer.render(@announcement, nil, :announcement)
|
payload = InlineRenderer.render(@announcement, nil, :announcement)
|
||||||
payload = Oj.dump(event: :announcement, payload: payload)
|
payload = { event: :announcement, payload: payload }.to_json
|
||||||
|
|
||||||
FeedManager.instance.with_active_accounts do |account|
|
FeedManager.instance.with_active_accounts do |account|
|
||||||
redis.publish("timeline:#{account.id}", payload) if redis.exists?("subscribed:timeline:#{account.id}")
|
redis.publish("timeline:#{account.id}", payload) if redis.exists?("subscribed:timeline:#{account.id}")
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ class PushConversationWorker
|
|||||||
message = InlineRenderer.render(conversation, conversation.account, :conversation)
|
message = InlineRenderer.render(conversation, conversation.account, :conversation)
|
||||||
timeline_id = "timeline:direct:#{conversation.account_id}"
|
timeline_id = "timeline:direct:#{conversation.account_id}"
|
||||||
|
|
||||||
redis.publish(timeline_id, Oj.dump(event: :conversation, payload: message))
|
redis.publish(timeline_id, { event: :conversation, payload: message }.to_json)
|
||||||
rescue ActiveRecord::RecordNotFound
|
rescue ActiveRecord::RecordNotFound
|
||||||
true
|
true
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -23,10 +23,10 @@ class PushUpdateWorker
|
|||||||
end
|
end
|
||||||
|
|
||||||
def message
|
def message
|
||||||
Oj.dump(
|
JSON.generate({
|
||||||
event: update? ? :'status.update' : :update,
|
event: update? ? :'status.update' : :update,
|
||||||
payload: @payload
|
payload: @payload,
|
||||||
)
|
}.as_json)
|
||||||
end
|
end
|
||||||
|
|
||||||
def publish!
|
def publish!
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ class UnfilterNotificationsWorker
|
|||||||
end
|
end
|
||||||
|
|
||||||
def push_streaming_event!
|
def push_streaming_event!
|
||||||
redis.publish("timeline:#{@recipient.id}:notifications", Oj.dump(event: :notifications_merged, payload: '1'))
|
redis.publish("timeline:#{@recipient.id}:notifications", { event: :notifications_merged, payload: '1' }.to_json)
|
||||||
end
|
end
|
||||||
|
|
||||||
def subscribed_to_streaming_api?
|
def subscribed_to_streaming_api?
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ class UnpublishAnnouncementWorker
|
|||||||
include Redisable
|
include Redisable
|
||||||
|
|
||||||
def perform(announcement_id)
|
def perform(announcement_id)
|
||||||
payload = Oj.dump(event: :'announcement.delete', payload: announcement_id.to_s)
|
payload = { event: :'announcement.delete', payload: announcement_id.to_s }.to_json
|
||||||
|
|
||||||
FeedManager.instance.with_active_accounts do |account|
|
FeedManager.instance.with_active_accounts do |account|
|
||||||
redis.publish("timeline:#{account.id}", payload) if redis.exists?("subscribed:timeline:#{account.id}")
|
redis.publish("timeline:#{account.id}", payload) if redis.exists?("subscribed:timeline:#{account.id}")
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ class Web::PushNotificationWorker
|
|||||||
|
|
||||||
def push_notification_json
|
def push_notification_json
|
||||||
I18n.with_locale(@subscription.locale.presence || I18n.default_locale) do
|
I18n.with_locale(@subscription.locale.presence || I18n.default_locale) do
|
||||||
Oj.dump(serialized_notification.as_json)
|
serialized_notification.to_json
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -214,7 +214,7 @@ module Mastodon::CLI
|
|||||||
|
|
||||||
def stats_to_json(stats)
|
def stats_to_json(stats)
|
||||||
stats.compact!
|
stats.compact!
|
||||||
say(Oj.dump(stats))
|
say(stats.to_json)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ module Mastodon
|
|||||||
|
|
||||||
def api_versions
|
def api_versions
|
||||||
{
|
{
|
||||||
mastodon: 8,
|
mastodon: 9,
|
||||||
glitch: 1,
|
glitch: 1,
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -93,6 +93,7 @@
|
|||||||
"punycode": "^2.3.0",
|
"punycode": "^2.3.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
|
"react-easy-crop": "^5.5.6",
|
||||||
"react-helmet": "^6.1.0",
|
"react-helmet": "^6.1.0",
|
||||||
"react-immutable-proptypes": "^2.2.0",
|
"react-immutable-proptypes": "^2.2.0",
|
||||||
"react-immutable-pure-component": "^2.2.2",
|
"react-immutable-pure-component": "^2.2.2",
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ RSpec.describe ActivityPub::Activity::Announce do
|
|||||||
context 'when sender is followed by a local account' do
|
context 'when sender is followed by a local account' do
|
||||||
before do
|
before do
|
||||||
Fabricate(:account).follow!(sender)
|
Fabricate(:account).follow!(sender)
|
||||||
stub_request(:get, 'https://example.com/actor/hello-world').to_return(body: Oj.dump(unknown_object_json), headers: { 'Content-Type': 'application/activity+json' })
|
stub_request(:get, 'https://example.com/actor/hello-world').to_return(body: JSON.generate(unknown_object_json), headers: { 'Content-Type': 'application/activity+json' })
|
||||||
subject.perform
|
subject.perform
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -120,7 +120,7 @@ RSpec.describe ActivityPub::Activity::Announce do
|
|||||||
let(:object_json) { 'https://example.com/actor/hello-world' }
|
let(:object_json) { 'https://example.com/actor/hello-world' }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
stub_request(:get, 'https://example.com/actor/hello-world').to_return(body: Oj.dump(unknown_object_json), headers: { 'Content-Type': 'application/activity+json' })
|
stub_request(:get, 'https://example.com/actor/hello-world').to_return(body: JSON.generate(unknown_object_json), headers: { 'Content-Type': 'application/activity+json' })
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when the relay is enabled' do
|
context 'when the relay is enabled' do
|
||||||
|
|||||||
@@ -1079,7 +1079,7 @@ RSpec.describe ActivityPub::Activity::Create do
|
|||||||
end
|
end
|
||||||
|
|
||||||
before do
|
before do
|
||||||
stub_request(:get, approval_uri).to_return(headers: { 'Content-Type': 'application/activity+json' }, body: Oj.dump({
|
stub_request(:get, approval_uri).to_return(headers: { 'Content-Type': 'application/activity+json' }, body: JSON.generate({
|
||||||
'@context': [
|
'@context': [
|
||||||
'https://www.w3.org/ns/activitystreams',
|
'https://www.w3.org/ns/activitystreams',
|
||||||
{
|
{
|
||||||
@@ -1135,7 +1135,7 @@ RSpec.describe ActivityPub::Activity::Create do
|
|||||||
end
|
end
|
||||||
|
|
||||||
before do
|
before do
|
||||||
stub_request(:get, approval_uri).to_return(headers: { 'Content-Type': 'application/activity+json' }, body: Oj.dump({
|
stub_request(:get, approval_uri).to_return(headers: { 'Content-Type': 'application/activity+json' }, body: JSON.generate({
|
||||||
'@context': [
|
'@context': [
|
||||||
'https://www.w3.org/ns/activitystreams',
|
'https://www.w3.org/ns/activitystreams',
|
||||||
{
|
{
|
||||||
@@ -1267,7 +1267,7 @@ RSpec.describe ActivityPub::Activity::Create do
|
|||||||
before do
|
before do
|
||||||
stub_request(:get, object_json[:id])
|
stub_request(:get, object_json[:id])
|
||||||
.with(headers: { Authorization: "Bearer #{token}" })
|
.with(headers: { Authorization: "Bearer #{token}" })
|
||||||
.to_return(body: Oj.dump(object_json), headers: { 'Content-Type': 'application/activity+json' })
|
.to_return(body: JSON.generate(object_json), headers: { 'Content-Type': 'application/activity+json' })
|
||||||
|
|
||||||
subject.perform
|
subject.perform
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -84,6 +84,23 @@ RSpec.describe ActivityPub::Activity::Follow do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when recipient blocks sender' do
|
||||||
|
before { Fabricate :block, account: recipient, target_account: sender }
|
||||||
|
|
||||||
|
it 'sends a reject and does not follow' do
|
||||||
|
subject.perform
|
||||||
|
|
||||||
|
expect(sender.requested?(recipient))
|
||||||
|
.to be false
|
||||||
|
expect(ActivityPub::DeliveryWorker)
|
||||||
|
.to have_enqueued_sidekiq_job(
|
||||||
|
match_json_values(type: 'Reject', object: include(type: 'Follow')),
|
||||||
|
recipient.id,
|
||||||
|
anything
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
context 'when a follow relationship already exists' do
|
context 'when a follow relationship already exists' do
|
||||||
before do
|
before do
|
||||||
sender.active_relationships.create!(target_account: recipient, uri: 'bar')
|
sender.active_relationships.create!(target_account: recipient, uri: 'bar')
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ RSpec.describe ActivityPub::Activity::QuoteRequest do
|
|||||||
|
|
||||||
context 'when trying to quote a quotable local status' do
|
context 'when trying to quote a quotable local status' do
|
||||||
before do
|
before do
|
||||||
stub_request(:get, 'https://example.com/unknown-status').to_return(status: 200, body: Oj.dump(status_json), headers: { 'Content-Type': 'application/activity+json' })
|
stub_request(:get, 'https://example.com/unknown-status').to_return(status: 200, body: JSON.generate(status_json), headers: { 'Content-Type': 'application/activity+json' })
|
||||||
quoted_post.update(quote_approval_policy: InteractionPolicy::POLICY_FLAGS[:public] << 16)
|
quoted_post.update(quote_approval_policy: InteractionPolicy::POLICY_FLAGS[:public] << 16)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ RSpec.describe ActivityPub::Activity do
|
|||||||
before do
|
before do
|
||||||
sender.update(uri: ActivityPub::TagManager.instance.uri_for(sender))
|
sender.update(uri: ActivityPub::TagManager.instance.uri_for(sender))
|
||||||
|
|
||||||
stub_request(:get, approval_uri).to_return(headers: { 'Content-Type': 'application/activity+json' }, body: Oj.dump(approval_payload))
|
stub_request(:get, approval_uri).to_return(headers: { 'Content-Type': 'application/activity+json' }, body: JSON.generate(approval_payload))
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when getting them in order' do
|
context 'when getting them in order' do
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ RSpec.describe ActivityPub::Dereferencer do
|
|||||||
let(:uri) { nil }
|
let(:uri) { nil }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
stub_request(:get, 'https://example.com/foo').to_return(body: Oj.dump(object), headers: { 'Content-Type' => 'application/activity+json' })
|
stub_request(:get, 'https://example.com/foo').to_return(body: JSON.generate(object), headers: { 'Content-Type' => 'application/activity+json' })
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'with a URI' do
|
context 'with a URI' do
|
||||||
|
|||||||
@@ -54,8 +54,8 @@ RSpec.describe ActivityPub::Forwarder do
|
|||||||
|
|
||||||
it 'correctly forwards to expected remote followers' do
|
it 'correctly forwards to expected remote followers' do
|
||||||
expect { subject.forward! }
|
expect { subject.forward! }
|
||||||
.to enqueue_sidekiq_job(ActivityPub::LowPriorityDeliveryWorker).with(Oj.dump(payload), anything, eve.preferred_inbox_url)
|
.to enqueue_sidekiq_job(ActivityPub::LowPriorityDeliveryWorker).with(JSON.generate(payload), anything, eve.preferred_inbox_url)
|
||||||
.and enqueue_sidekiq_job(ActivityPub::LowPriorityDeliveryWorker).with(Oj.dump(payload), anything, mallory.preferred_inbox_url)
|
.and enqueue_sidekiq_job(ActivityPub::LowPriorityDeliveryWorker).with(JSON.generate(payload), anything, mallory.preferred_inbox_url)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user