mirror of
https://github.com/glitch-soc/mastodon.git
synced 2025-02-02 21:13:02 -05:00
Merge commit '3dcf5e12b1ffb8e57a7c1e1f3f1145c9bae477e8' into glitch-soc/merge-upstream
Conflicts: - `app/javascript/styles/mastodon/components.scss`: Conflict because one of the removed lines was modified to accomodate for glitch-soc's slightly different build system. Removed the lines as upstream did.
This commit is contained in:
commit
05e5db91c7
Binary file not shown.
Before Width: | Height: | Size: 1.4 KiB |
@ -416,7 +416,7 @@ export function initMediaEditModal(id) {
|
|||||||
|
|
||||||
dispatch(openModal({
|
dispatch(openModal({
|
||||||
modalType: 'FOCAL_POINT',
|
modalType: 'FOCAL_POINT',
|
||||||
modalProps: { id },
|
modalProps: { mediaId: id },
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
70
app/javascript/mastodon/actions/compose_typed.ts
Normal file
70
app/javascript/mastodon/actions/compose_typed.ts
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
import type { List as ImmutableList, Map as ImmutableMap } from 'immutable';
|
||||||
|
|
||||||
|
import { apiUpdateMedia } from 'mastodon/api/compose';
|
||||||
|
import type { ApiMediaAttachmentJSON } from 'mastodon/api_types/media_attachments';
|
||||||
|
import type { MediaAttachment } from 'mastodon/models/media_attachment';
|
||||||
|
import { createDataLoadingThunk } from 'mastodon/store/typed_functions';
|
||||||
|
|
||||||
|
type SimulatedMediaAttachmentJSON = ApiMediaAttachmentJSON & {
|
||||||
|
unattached?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const simulateModifiedApiResponse = (
|
||||||
|
media: MediaAttachment,
|
||||||
|
params: { description?: string; focus?: string },
|
||||||
|
): SimulatedMediaAttachmentJSON => {
|
||||||
|
const [x, y] = (params.focus ?? '').split(',');
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
...media.toJS(),
|
||||||
|
...params,
|
||||||
|
meta: {
|
||||||
|
focus: {
|
||||||
|
x: parseFloat(x ?? '0'),
|
||||||
|
y: parseFloat(y ?? '0'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as unknown as SimulatedMediaAttachmentJSON;
|
||||||
|
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const changeUploadCompose = createDataLoadingThunk(
|
||||||
|
'compose/changeUpload',
|
||||||
|
async (
|
||||||
|
{
|
||||||
|
id,
|
||||||
|
...params
|
||||||
|
}: {
|
||||||
|
id: string;
|
||||||
|
description: string;
|
||||||
|
focus: string;
|
||||||
|
},
|
||||||
|
{ getState },
|
||||||
|
) => {
|
||||||
|
const media = (
|
||||||
|
(getState().compose as ImmutableMap<string, unknown>).get(
|
||||||
|
'media_attachments',
|
||||||
|
) as ImmutableList<MediaAttachment>
|
||||||
|
).find((item) => item.get('id') === id);
|
||||||
|
|
||||||
|
// Editing already-attached media is deferred to editing the post itself.
|
||||||
|
// For simplicity's sake, fake an API reply.
|
||||||
|
if (media && !media.get('unattached')) {
|
||||||
|
return new Promise<SimulatedMediaAttachmentJSON>((resolve) => {
|
||||||
|
resolve(simulateModifiedApiResponse(media, params));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return apiUpdateMedia(id, params);
|
||||||
|
},
|
||||||
|
(media: SimulatedMediaAttachmentJSON) => {
|
||||||
|
return {
|
||||||
|
media,
|
||||||
|
attached: typeof media.unattached !== 'undefined' && !media.unattached,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
{
|
||||||
|
useLoadingBar: false,
|
||||||
|
},
|
||||||
|
);
|
@ -9,6 +9,7 @@ export type ModalType = keyof typeof MODAL_COMPONENTS;
|
|||||||
interface OpenModalPayload {
|
interface OpenModalPayload {
|
||||||
modalType: ModalType;
|
modalType: ModalType;
|
||||||
modalProps: ModalProps;
|
modalProps: ModalProps;
|
||||||
|
previousModalProps?: ModalProps;
|
||||||
}
|
}
|
||||||
export const openModal = createAction<OpenModalPayload>('MODAL_OPEN');
|
export const openModal = createAction<OpenModalPayload>('MODAL_OPEN');
|
||||||
|
|
||||||
|
7
app/javascript/mastodon/api/compose.ts
Normal file
7
app/javascript/mastodon/api/compose.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { apiRequestPut } from 'mastodon/api';
|
||||||
|
import type { ApiMediaAttachmentJSON } from 'mastodon/api_types/media_attachments';
|
||||||
|
|
||||||
|
export const apiUpdateMedia = (
|
||||||
|
id: string,
|
||||||
|
params?: { description?: string; focus?: string },
|
||||||
|
) => apiRequestPut<ApiMediaAttachmentJSON>(`v1/media/${id}`, params);
|
@ -7,6 +7,7 @@ interface BaseProps
|
|||||||
extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'children'> {
|
extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'children'> {
|
||||||
block?: boolean;
|
block?: boolean;
|
||||||
secondary?: boolean;
|
secondary?: boolean;
|
||||||
|
compact?: boolean;
|
||||||
dangerous?: boolean;
|
dangerous?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -27,6 +28,7 @@ export const Button: React.FC<Props> = ({
|
|||||||
disabled,
|
disabled,
|
||||||
block,
|
block,
|
||||||
secondary,
|
secondary,
|
||||||
|
compact,
|
||||||
dangerous,
|
dangerous,
|
||||||
className,
|
className,
|
||||||
title,
|
title,
|
||||||
@ -47,6 +49,7 @@ export const Button: React.FC<Props> = ({
|
|||||||
<button
|
<button
|
||||||
className={classNames('button', className, {
|
className={classNames('button', className, {
|
||||||
'button-secondary': secondary,
|
'button-secondary': secondary,
|
||||||
|
'button--compact': compact,
|
||||||
'button--block': block,
|
'button--block': block,
|
||||||
'button--dangerous': dangerous,
|
'button--dangerous': dangerous,
|
||||||
})}
|
})}
|
||||||
|
@ -2,6 +2,8 @@ import { useCallback, useEffect } from 'react';
|
|||||||
|
|
||||||
import { useIntl, defineMessages } from 'react-intl';
|
import { useIntl, defineMessages } from 'react-intl';
|
||||||
|
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
import { useIdentity } from '@/mastodon/identity_context';
|
import { useIdentity } from '@/mastodon/identity_context';
|
||||||
import { fetchRelationships, followAccount } from 'mastodon/actions/accounts';
|
import { fetchRelationships, followAccount } from 'mastodon/actions/accounts';
|
||||||
import { openModal } from 'mastodon/actions/modal';
|
import { openModal } from 'mastodon/actions/modal';
|
||||||
@ -20,7 +22,8 @@ const messages = defineMessages({
|
|||||||
|
|
||||||
export const FollowButton: React.FC<{
|
export const FollowButton: React.FC<{
|
||||||
accountId?: string;
|
accountId?: string;
|
||||||
}> = ({ accountId }) => {
|
compact?: boolean;
|
||||||
|
}> = ({ accountId, compact }) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const { signedIn } = useIdentity();
|
const { signedIn } = useIdentity();
|
||||||
@ -89,7 +92,9 @@ export const FollowButton: React.FC<{
|
|||||||
href='/settings/profile'
|
href='/settings/profile'
|
||||||
target='_blank'
|
target='_blank'
|
||||||
rel='noopener'
|
rel='noopener'
|
||||||
className='button button-secondary'
|
className={classNames('button button-secondary', {
|
||||||
|
'button--compact': compact,
|
||||||
|
})}
|
||||||
>
|
>
|
||||||
{label}
|
{label}
|
||||||
</a>
|
</a>
|
||||||
@ -106,6 +111,7 @@ export const FollowButton: React.FC<{
|
|||||||
(account?.suspended || !!account?.moved))
|
(account?.suspended || !!account?.moved))
|
||||||
}
|
}
|
||||||
secondary={following}
|
secondary={following}
|
||||||
|
compact={compact}
|
||||||
className={following ? 'button--destructive' : undefined}
|
className={following ? 'button--destructive' : undefined}
|
||||||
>
|
>
|
||||||
{label}
|
{label}
|
||||||
|
@ -1,70 +1,70 @@
|
|||||||
import { useCallback, useState } from 'react';
|
import { useCallback, useState, forwardRef } from 'react';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
src: string;
|
src: string;
|
||||||
key: string;
|
|
||||||
alt?: string;
|
alt?: string;
|
||||||
lang?: string;
|
lang?: string;
|
||||||
width: number;
|
width?: number;
|
||||||
height: number;
|
height?: number;
|
||||||
onClick?: () => void;
|
onClick?: React.MouseEventHandler;
|
||||||
|
onMouseDown?: React.MouseEventHandler;
|
||||||
|
onTouchStart?: React.TouchEventHandler;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const GIFV: React.FC<Props> = ({
|
export const GIFV = forwardRef<HTMLVideoElement, Props>(
|
||||||
src,
|
(
|
||||||
alt,
|
{ src, alt, lang, width, height, onClick, onMouseDown, onTouchStart },
|
||||||
lang,
|
ref,
|
||||||
width,
|
) => {
|
||||||
height,
|
const [loading, setLoading] = useState(true);
|
||||||
onClick,
|
|
||||||
}) => {
|
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
|
|
||||||
const handleLoadedData: React.ReactEventHandler<HTMLVideoElement> =
|
const handleLoadedData = useCallback(() => {
|
||||||
useCallback(() => {
|
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}, [setLoading]);
|
}, [setLoading]);
|
||||||
|
|
||||||
const handleClick: React.MouseEventHandler = useCallback(
|
const handleClick = useCallback(
|
||||||
(e) => {
|
(e: React.MouseEvent) => {
|
||||||
if (onClick) {
|
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
onClick();
|
onClick?.(e);
|
||||||
}
|
},
|
||||||
},
|
[onClick],
|
||||||
[onClick],
|
);
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='gifv' style={{ position: 'relative' }}>
|
<div className='gifv'>
|
||||||
{loading && (
|
{loading && (
|
||||||
<canvas
|
<canvas
|
||||||
width={width}
|
role='button'
|
||||||
height={height}
|
tabIndex={0}
|
||||||
|
aria-label={alt}
|
||||||
|
title={alt}
|
||||||
|
lang={lang}
|
||||||
|
onClick={handleClick}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<video
|
||||||
|
ref={ref}
|
||||||
|
src={src}
|
||||||
role='button'
|
role='button'
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
aria-label={alt}
|
aria-label={alt}
|
||||||
title={alt}
|
title={alt}
|
||||||
lang={lang}
|
lang={lang}
|
||||||
|
width={width}
|
||||||
|
height={height}
|
||||||
|
muted
|
||||||
|
loop
|
||||||
|
autoPlay
|
||||||
|
playsInline
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
|
onLoadedData={handleLoadedData}
|
||||||
|
onMouseDown={onMouseDown}
|
||||||
|
onTouchStart={onTouchStart}
|
||||||
/>
|
/>
|
||||||
)}
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
<video
|
GIFV.displayName = 'GIFV';
|
||||||
src={src}
|
|
||||||
role='button'
|
|
||||||
tabIndex={0}
|
|
||||||
aria-label={alt}
|
|
||||||
title={alt}
|
|
||||||
lang={lang}
|
|
||||||
muted
|
|
||||||
loop
|
|
||||||
autoPlay
|
|
||||||
playsInline
|
|
||||||
onClick={handleClick}
|
|
||||||
onLoadedData={handleLoadedData}
|
|
||||||
style={{ position: loading ? 'absolute' : 'static', top: 0, left: 0 }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
531
app/javascript/mastodon/features/alt_text_modal/index.tsx
Normal file
531
app/javascript/mastodon/features/alt_text_modal/index.tsx
Normal file
@ -0,0 +1,531 @@
|
|||||||
|
import {
|
||||||
|
useState,
|
||||||
|
useCallback,
|
||||||
|
useRef,
|
||||||
|
useEffect,
|
||||||
|
useImperativeHandle,
|
||||||
|
forwardRef,
|
||||||
|
} from 'react';
|
||||||
|
|
||||||
|
import { FormattedMessage, useIntl, defineMessages } from 'react-intl';
|
||||||
|
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
import type { List as ImmutableList, Map as ImmutableMap } from 'immutable';
|
||||||
|
|
||||||
|
import Textarea from 'react-textarea-autosize';
|
||||||
|
import { length } from 'stringz';
|
||||||
|
// eslint-disable-next-line import/extensions
|
||||||
|
import tesseractWorkerPath from 'tesseract.js/dist/worker.min.js';
|
||||||
|
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||||
|
import tesseractCorePath from 'tesseract.js-core/tesseract-core.wasm.js';
|
||||||
|
|
||||||
|
import { showAlertForError } from 'mastodon/actions/alerts';
|
||||||
|
import { uploadThumbnail } from 'mastodon/actions/compose';
|
||||||
|
import { changeUploadCompose } from 'mastodon/actions/compose_typed';
|
||||||
|
import { Button } from 'mastodon/components/button';
|
||||||
|
import { GIFV } from 'mastodon/components/gifv';
|
||||||
|
import { LoadingIndicator } from 'mastodon/components/loading_indicator';
|
||||||
|
import { Skeleton } from 'mastodon/components/skeleton';
|
||||||
|
import Audio from 'mastodon/features/audio';
|
||||||
|
import { CharacterCounter } from 'mastodon/features/compose/components/character_counter';
|
||||||
|
import { Tesseract as fetchTesseract } from 'mastodon/features/ui/util/async-components';
|
||||||
|
import Video, { getPointerPosition } from 'mastodon/features/video';
|
||||||
|
import { me } from 'mastodon/initial_state';
|
||||||
|
import type { MediaAttachment } from 'mastodon/models/media_attachment';
|
||||||
|
import { useAppSelector, useAppDispatch } from 'mastodon/store';
|
||||||
|
import { assetHost } from 'mastodon/utils/config';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
placeholderVisual: {
|
||||||
|
id: 'alt_text_modal.describe_for_people_with_visual_impairments',
|
||||||
|
defaultMessage: 'Describe this for people with visual impairments…',
|
||||||
|
},
|
||||||
|
placeholderHearing: {
|
||||||
|
id: 'alt_text_modal.describe_for_people_with_hearing_impairments',
|
||||||
|
defaultMessage: 'Describe this for people with hearing impairments…',
|
||||||
|
},
|
||||||
|
discardMessage: {
|
||||||
|
id: 'confirmations.discard_edit_media.message',
|
||||||
|
defaultMessage:
|
||||||
|
'You have unsaved changes to the media description or preview, discard them anyway?',
|
||||||
|
},
|
||||||
|
discardConfirm: {
|
||||||
|
id: 'confirmations.discard_edit_media.confirm',
|
||||||
|
defaultMessage: 'Discard',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const MAX_LENGTH = 1500;
|
||||||
|
|
||||||
|
type FocalPoint = [number, number];
|
||||||
|
|
||||||
|
const UploadButton: React.FC<{
|
||||||
|
children: React.ReactNode;
|
||||||
|
onSelectFile: (arg0: File) => void;
|
||||||
|
mimeTypes: string;
|
||||||
|
}> = ({ children, onSelectFile, mimeTypes }) => {
|
||||||
|
const fileRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
const handleClick = useCallback(() => {
|
||||||
|
fileRef.current?.click();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleChange = useCallback(
|
||||||
|
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const file = e.target.files?.[0];
|
||||||
|
|
||||||
|
if (file) {
|
||||||
|
onSelectFile(file);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[onSelectFile],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<label>
|
||||||
|
<Button onClick={handleClick}>{children}</Button>
|
||||||
|
|
||||||
|
<input
|
||||||
|
id='upload-modal__thumbnail'
|
||||||
|
ref={fileRef}
|
||||||
|
type='file'
|
||||||
|
accept={mimeTypes}
|
||||||
|
onChange={handleChange}
|
||||||
|
style={{ display: 'none' }}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const Preview: React.FC<{
|
||||||
|
mediaId: string;
|
||||||
|
position: FocalPoint;
|
||||||
|
onPositionChange: (arg0: FocalPoint) => void;
|
||||||
|
}> = ({ mediaId, position, onPositionChange }) => {
|
||||||
|
const media = useAppSelector((state) =>
|
||||||
|
(
|
||||||
|
(state.compose as ImmutableMap<string, unknown>).get(
|
||||||
|
'media_attachments',
|
||||||
|
) as ImmutableList<MediaAttachment>
|
||||||
|
).find((x) => x.get('id') === mediaId),
|
||||||
|
);
|
||||||
|
const account = useAppSelector((state) =>
|
||||||
|
me ? state.accounts.get(me) : undefined,
|
||||||
|
);
|
||||||
|
|
||||||
|
const [dragging, setDragging] = useState(false);
|
||||||
|
const [x, y] = position;
|
||||||
|
const nodeRef = useRef<HTMLImageElement | HTMLVideoElement | null>(null);
|
||||||
|
const draggingRef = useRef<boolean>(false);
|
||||||
|
|
||||||
|
const setRef = useCallback(
|
||||||
|
(e: HTMLImageElement | HTMLVideoElement | null) => {
|
||||||
|
nodeRef.current = e;
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleMouseDown = useCallback(
|
||||||
|
(e: React.MouseEvent) => {
|
||||||
|
if (e.button !== 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { x, y } = getPointerPosition(nodeRef.current, e);
|
||||||
|
setDragging(true);
|
||||||
|
draggingRef.current = true;
|
||||||
|
onPositionChange([x, y]);
|
||||||
|
},
|
||||||
|
[setDragging, onPositionChange],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleTouchStart = useCallback(
|
||||||
|
(e: React.TouchEvent) => {
|
||||||
|
const { x, y } = getPointerPosition(nodeRef.current, e);
|
||||||
|
setDragging(true);
|
||||||
|
draggingRef.current = true;
|
||||||
|
onPositionChange([x, y]);
|
||||||
|
},
|
||||||
|
[setDragging, onPositionChange],
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleMouseUp = () => {
|
||||||
|
setDragging(false);
|
||||||
|
draggingRef.current = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseMove = (e: MouseEvent) => {
|
||||||
|
if (draggingRef.current) {
|
||||||
|
const { x, y } = getPointerPosition(nodeRef.current, e);
|
||||||
|
onPositionChange([x, y]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTouchEnd = () => {
|
||||||
|
setDragging(false);
|
||||||
|
draggingRef.current = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTouchMove = (e: TouchEvent) => {
|
||||||
|
if (draggingRef.current) {
|
||||||
|
const { x, y } = getPointerPosition(nodeRef.current, e);
|
||||||
|
onPositionChange([x, y]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('mouseup', handleMouseUp);
|
||||||
|
document.addEventListener('mousemove', handleMouseMove);
|
||||||
|
document.addEventListener('touchend', handleTouchEnd);
|
||||||
|
document.addEventListener('touchmove', handleTouchMove);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('mouseup', handleMouseUp);
|
||||||
|
document.removeEventListener('mousemove', handleMouseMove);
|
||||||
|
document.removeEventListener('touchend', handleTouchEnd);
|
||||||
|
document.removeEventListener('touchmove', handleTouchMove);
|
||||||
|
};
|
||||||
|
}, [setDragging, onPositionChange]);
|
||||||
|
|
||||||
|
if (!media) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (media.get('type') === 'image') {
|
||||||
|
return (
|
||||||
|
<div className={classNames('focal-point', { dragging })}>
|
||||||
|
<img
|
||||||
|
ref={setRef}
|
||||||
|
draggable={false}
|
||||||
|
src={media.get('url') as string}
|
||||||
|
alt=''
|
||||||
|
role='presentation'
|
||||||
|
onMouseDown={handleMouseDown}
|
||||||
|
onTouchStart={handleTouchStart}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className='focal-point__reticle'
|
||||||
|
style={{ top: `${y * 100}%`, left: `${x * 100}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else if (media.get('type') === 'gifv') {
|
||||||
|
return (
|
||||||
|
<div className={classNames('focal-point', { dragging })}>
|
||||||
|
<GIFV
|
||||||
|
ref={setRef}
|
||||||
|
src={media.get('url') as string}
|
||||||
|
alt=''
|
||||||
|
onMouseDown={handleMouseDown}
|
||||||
|
onTouchStart={handleTouchStart}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className='focal-point__reticle'
|
||||||
|
style={{ top: `${y * 100}%`, left: `${x * 100}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else if (media.get('type') === 'video') {
|
||||||
|
return (
|
||||||
|
<Video
|
||||||
|
preview={media.get('preview_url') as string}
|
||||||
|
frameRate={media.getIn(['meta', 'original', 'frame_rate']) as string}
|
||||||
|
blurhash={media.get('blurhash') as string}
|
||||||
|
src={media.get('url') as string}
|
||||||
|
detailed
|
||||||
|
inline
|
||||||
|
editable
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else if (media.get('type') === 'audio') {
|
||||||
|
return (
|
||||||
|
<Audio
|
||||||
|
src={media.get('url') as string}
|
||||||
|
duration={media.getIn(['meta', 'original', 'duration'], 0) as number}
|
||||||
|
poster={
|
||||||
|
(media.get('preview_url') as string | undefined) ??
|
||||||
|
account?.avatar_static
|
||||||
|
}
|
||||||
|
backgroundColor={
|
||||||
|
media.getIn(['meta', 'colors', 'background']) as string
|
||||||
|
}
|
||||||
|
foregroundColor={
|
||||||
|
media.getIn(['meta', 'colors', 'foreground']) as string
|
||||||
|
}
|
||||||
|
accentColor={media.getIn(['meta', 'colors', 'accent']) as string}
|
||||||
|
editable
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
interface RestoreProps {
|
||||||
|
previousDescription: string;
|
||||||
|
previousPosition: FocalPoint;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
mediaId: string;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ConfirmationMessage {
|
||||||
|
message: string;
|
||||||
|
confirm: string;
|
||||||
|
props?: RestoreProps;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ModalRef {
|
||||||
|
getCloseConfirmationMessage: () => null | ConfirmationMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AltTextModal = forwardRef<ModalRef, Props & Partial<RestoreProps>>(
|
||||||
|
({ mediaId, previousDescription, previousPosition, onClose }, ref) => {
|
||||||
|
const intl = useIntl();
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const media = useAppSelector((state) =>
|
||||||
|
(
|
||||||
|
(state.compose as ImmutableMap<string, unknown>).get(
|
||||||
|
'media_attachments',
|
||||||
|
) as ImmutableList<MediaAttachment>
|
||||||
|
).find((x) => x.get('id') === mediaId),
|
||||||
|
);
|
||||||
|
const lang = useAppSelector(
|
||||||
|
(state) =>
|
||||||
|
(state.compose as ImmutableMap<string, unknown>).get('lang') as string,
|
||||||
|
);
|
||||||
|
const focusX =
|
||||||
|
(media?.getIn(['meta', 'focus', 'x'], 0) as number | undefined) ?? 0;
|
||||||
|
const focusY =
|
||||||
|
(media?.getIn(['meta', 'focus', 'y'], 0) as number | undefined) ?? 0;
|
||||||
|
const [description, setDescription] = useState(
|
||||||
|
previousDescription ??
|
||||||
|
(media?.get('description') as string | undefined) ??
|
||||||
|
'',
|
||||||
|
);
|
||||||
|
const [position, setPosition] = useState<FocalPoint>(
|
||||||
|
previousPosition ?? [focusX / 2 + 0.5, focusY / -2 + 0.5],
|
||||||
|
);
|
||||||
|
const [isDetecting, setIsDetecting] = useState(false);
|
||||||
|
const [isSaving, setIsSaving] = useState(false);
|
||||||
|
const dirtyRef = useRef(
|
||||||
|
previousDescription || previousPosition ? true : false,
|
||||||
|
);
|
||||||
|
const type = media?.get('type') as string;
|
||||||
|
const valid = length(description) <= MAX_LENGTH;
|
||||||
|
|
||||||
|
const handleDescriptionChange = useCallback(
|
||||||
|
(e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||||
|
setDescription(e.target.value);
|
||||||
|
dirtyRef.current = true;
|
||||||
|
},
|
||||||
|
[setDescription],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleThumbnailChange = useCallback(
|
||||||
|
(file: File) => {
|
||||||
|
dispatch(uploadThumbnail(mediaId, file));
|
||||||
|
},
|
||||||
|
[dispatch, mediaId],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handlePositionChange = useCallback(
|
||||||
|
(position: FocalPoint) => {
|
||||||
|
setPosition(position);
|
||||||
|
dirtyRef.current = true;
|
||||||
|
},
|
||||||
|
[setPosition],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleSubmit = useCallback(() => {
|
||||||
|
setIsSaving(true);
|
||||||
|
|
||||||
|
dispatch(
|
||||||
|
changeUploadCompose({
|
||||||
|
id: mediaId,
|
||||||
|
description,
|
||||||
|
focus: `${((position[0] - 0.5) * 2).toFixed(2)},${((position[1] - 0.5) * -2).toFixed(2)}`,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.then(() => {
|
||||||
|
setIsSaving(false);
|
||||||
|
dirtyRef.current = false;
|
||||||
|
onClose();
|
||||||
|
return '';
|
||||||
|
})
|
||||||
|
.catch((err: unknown) => {
|
||||||
|
setIsSaving(false);
|
||||||
|
dispatch(showAlertForError(err));
|
||||||
|
});
|
||||||
|
}, [dispatch, setIsSaving, mediaId, onClose, position, description]);
|
||||||
|
|
||||||
|
const handleKeyUp = useCallback(
|
||||||
|
(e: React.KeyboardEvent) => {
|
||||||
|
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
if (valid) {
|
||||||
|
handleSubmit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[handleSubmit, valid],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleDetectClick = useCallback(() => {
|
||||||
|
setIsDetecting(true);
|
||||||
|
|
||||||
|
fetchTesseract()
|
||||||
|
.then(async ({ createWorker }) => {
|
||||||
|
const worker = await createWorker('eng', 1, {
|
||||||
|
workerPath: tesseractWorkerPath as string,
|
||||||
|
corePath: tesseractCorePath as string,
|
||||||
|
langPath: `${assetHost}/ocr/lang-data`,
|
||||||
|
cacheMethod: 'write',
|
||||||
|
});
|
||||||
|
|
||||||
|
const image = URL.createObjectURL(media?.get('file') as File);
|
||||||
|
const result = await worker.recognize(image);
|
||||||
|
|
||||||
|
setDescription(result.data.text);
|
||||||
|
setIsDetecting(false);
|
||||||
|
|
||||||
|
await worker.terminate();
|
||||||
|
|
||||||
|
return '';
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
setIsDetecting(false);
|
||||||
|
});
|
||||||
|
}, [setDescription, setIsDetecting, media]);
|
||||||
|
|
||||||
|
useImperativeHandle(
|
||||||
|
ref,
|
||||||
|
() => ({
|
||||||
|
getCloseConfirmationMessage: () => {
|
||||||
|
if (dirtyRef.current) {
|
||||||
|
return {
|
||||||
|
message: intl.formatMessage(messages.discardMessage),
|
||||||
|
confirm: intl.formatMessage(messages.discardConfirm),
|
||||||
|
props: {
|
||||||
|
previousDescription: description,
|
||||||
|
previousPosition: position,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
[intl, description, position],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='modal-root__modal dialog-modal'>
|
||||||
|
<div className='dialog-modal__header'>
|
||||||
|
<Button onClick={handleSubmit} disabled={!valid}>
|
||||||
|
{isSaving ? (
|
||||||
|
<LoadingIndicator />
|
||||||
|
) : (
|
||||||
|
<FormattedMessage
|
||||||
|
id='alt_text_modal.done'
|
||||||
|
defaultMessage='Done'
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<span className='dialog-modal__header__title'>
|
||||||
|
<FormattedMessage
|
||||||
|
id='alt_text_modal.add_alt_text'
|
||||||
|
defaultMessage='Add alt text'
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<Button secondary onClick={onClose}>
|
||||||
|
<FormattedMessage
|
||||||
|
id='alt_text_modal.cancel'
|
||||||
|
defaultMessage='Cancel'
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='dialog-modal__content'>
|
||||||
|
<div className='dialog-modal__content__preview'>
|
||||||
|
<Preview
|
||||||
|
mediaId={mediaId}
|
||||||
|
position={position}
|
||||||
|
onPositionChange={handlePositionChange}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{(type === 'audio' || type === 'video') && (
|
||||||
|
<UploadButton
|
||||||
|
onSelectFile={handleThumbnailChange}
|
||||||
|
mimeTypes='image/jpeg,image/png,image/gif,image/heic,image/heif,image/webp,image/avif'
|
||||||
|
>
|
||||||
|
<FormattedMessage
|
||||||
|
id='alt_text_modal.change_thumbnail'
|
||||||
|
defaultMessage='Change thumbnail'
|
||||||
|
/>
|
||||||
|
</UploadButton>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form
|
||||||
|
className='dialog-modal__content__form simple_form'
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
>
|
||||||
|
<div className='input'>
|
||||||
|
<div className='label_input'>
|
||||||
|
<Textarea
|
||||||
|
id='description'
|
||||||
|
value={isDetecting ? ' ' : description}
|
||||||
|
onChange={handleDescriptionChange}
|
||||||
|
onKeyUp={handleKeyUp}
|
||||||
|
lang={lang}
|
||||||
|
placeholder={intl.formatMessage(
|
||||||
|
type === 'audio'
|
||||||
|
? messages.placeholderHearing
|
||||||
|
: messages.placeholderVisual,
|
||||||
|
)}
|
||||||
|
minRows={3}
|
||||||
|
disabled={isDetecting}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{isDetecting && (
|
||||||
|
<div className='label_input__loading-indicator'>
|
||||||
|
<Skeleton width='100%' />
|
||||||
|
<Skeleton width='100%' />
|
||||||
|
<Skeleton width='61%' />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='input__toolbar'>
|
||||||
|
<button
|
||||||
|
className='link-button'
|
||||||
|
onClick={handleDetectClick}
|
||||||
|
disabled={type !== 'image' || isDetecting}
|
||||||
|
>
|
||||||
|
<FormattedMessage
|
||||||
|
id='alt_text_modal.add_text_from_image'
|
||||||
|
defaultMessage='Add text from image'
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<CharacterCounter
|
||||||
|
max={MAX_LENGTH}
|
||||||
|
text={isDetecting ? '' : description}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
AltTextModal.displayName = 'AltTextModal';
|
@ -581,10 +581,14 @@ class Audio extends PureComponent {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='video-player__buttons right'>
|
<div className='video-player__buttons right'>
|
||||||
{!editable && <button type='button' title={intl.formatMessage(messages.hide)} aria-label={intl.formatMessage(messages.hide)} className='player-button' onClick={this.toggleReveal}><Icon id='eye-slash' icon={VisibilityOffIcon} /></button>}
|
{!editable && (
|
||||||
<a title={intl.formatMessage(messages.download)} aria-label={intl.formatMessage(messages.download)} className='video-player__download__icon player-button' href={this.props.src} download>
|
<>
|
||||||
<Icon id={'download'} icon={DownloadIcon} />
|
<button type='button' title={intl.formatMessage(messages.hide)} aria-label={intl.formatMessage(messages.hide)} className='player-button' onClick={this.toggleReveal}><Icon id='eye-slash' icon={VisibilityOffIcon} /></button>
|
||||||
</a>
|
<a title={intl.formatMessage(messages.download)} aria-label={intl.formatMessage(messages.download)} className='video-player__download__icon player-button' href={this.props.src} download>
|
||||||
|
<Icon id='download' icon={DownloadIcon} />
|
||||||
|
</a>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,18 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
import { length } from 'stringz';
|
|
||||||
|
|
||||||
export const CharacterCounter = ({ text, max }) => {
|
|
||||||
const diff = max - length(text);
|
|
||||||
|
|
||||||
if (diff < 0) {
|
|
||||||
return <span className='character-counter character-counter--over'>{diff}</span>;
|
|
||||||
}
|
|
||||||
|
|
||||||
return <span className='character-counter'>{diff}</span>;
|
|
||||||
};
|
|
||||||
|
|
||||||
CharacterCounter.propTypes = {
|
|
||||||
text: PropTypes.string.isRequired,
|
|
||||||
max: PropTypes.number.isRequired,
|
|
||||||
};
|
|
@ -0,0 +1,16 @@
|
|||||||
|
import { length } from 'stringz';
|
||||||
|
|
||||||
|
export const CharacterCounter: React.FC<{
|
||||||
|
text: string;
|
||||||
|
max: number;
|
||||||
|
}> = ({ text, max }) => {
|
||||||
|
const diff = max - length(text);
|
||||||
|
|
||||||
|
if (diff < 0) {
|
||||||
|
return (
|
||||||
|
<span className='character-counter character-counter--over'>{diff}</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <span className='character-counter'>{diff}</span>;
|
||||||
|
};
|
@ -301,6 +301,7 @@ class ComposeForm extends ImmutablePureComponent {
|
|||||||
<div className='compose-form__submit'>
|
<div className='compose-form__submit'>
|
||||||
<Button
|
<Button
|
||||||
type='submit'
|
type='submit'
|
||||||
|
compact
|
||||||
text={intl.formatMessage(this.props.isEditing ? messages.saveChanges : (this.props.isInReply ? messages.reply : messages.publish))}
|
text={intl.formatMessage(this.props.isEditing ? messages.saveChanges : (this.props.isInReply ? messages.reply : messages.publish))}
|
||||||
disabled={!this.canSubmit()}
|
disabled={!this.canSubmit()}
|
||||||
/>
|
/>
|
||||||
|
@ -4,16 +4,16 @@ import { FormattedMessage } from 'react-intl';
|
|||||||
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
import type { Map as ImmutableMap, List as ImmutableList } from 'immutable';
|
||||||
|
|
||||||
import { useSortable } from '@dnd-kit/sortable';
|
import { useSortable } from '@dnd-kit/sortable';
|
||||||
import { CSS } from '@dnd-kit/utilities';
|
import { CSS } from '@dnd-kit/utilities';
|
||||||
|
|
||||||
import CloseIcon from '@/material-icons/400-20px/close.svg?react';
|
import CloseIcon from '@/material-icons/400-20px/close.svg?react';
|
||||||
import EditIcon from '@/material-icons/400-24px/edit.svg?react';
|
import EditIcon from '@/material-icons/400-24px/edit.svg?react';
|
||||||
import WarningIcon from '@/material-icons/400-24px/warning.svg?react';
|
import WarningIcon from '@/material-icons/400-24px/warning.svg?react';
|
||||||
import {
|
import { undoUploadCompose } from 'mastodon/actions/compose';
|
||||||
undoUploadCompose,
|
import { openModal } from 'mastodon/actions/modal';
|
||||||
initMediaEditModal,
|
|
||||||
} from 'mastodon/actions/compose';
|
|
||||||
import { Blurhash } from 'mastodon/components/blurhash';
|
import { Blurhash } from 'mastodon/components/blurhash';
|
||||||
import { Icon } from 'mastodon/components/icon';
|
import { Icon } from 'mastodon/components/icon';
|
||||||
import type { MediaAttachment } from 'mastodon/models/media_attachment';
|
import type { MediaAttachment } from 'mastodon/models/media_attachment';
|
||||||
@ -27,16 +27,15 @@ export const Upload: React.FC<{
|
|||||||
wide?: boolean;
|
wide?: boolean;
|
||||||
}> = ({ id, dragging, overlay, tall, wide }) => {
|
}> = ({ id, dragging, overlay, tall, wide }) => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const media = useAppSelector(
|
const media = useAppSelector((state) =>
|
||||||
(state) =>
|
(
|
||||||
state.compose // eslint-disable-line @typescript-eslint/no-unsafe-call
|
(state.compose as ImmutableMap<string, unknown>).get(
|
||||||
.get('media_attachments') // eslint-disable-line @typescript-eslint/no-unsafe-member-access
|
'media_attachments',
|
||||||
.find((item: MediaAttachment) => item.get('id') === id) as // eslint-disable-line @typescript-eslint/no-unsafe-member-access
|
) as ImmutableList<MediaAttachment>
|
||||||
| MediaAttachment
|
).find((item) => item.get('id') === id),
|
||||||
| undefined,
|
|
||||||
);
|
);
|
||||||
const sensitive = useAppSelector(
|
const sensitive = useAppSelector(
|
||||||
(state) => state.compose.get('spoiler') as boolean, // eslint-disable-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
|
(state) => state.compose.get('spoiler') as boolean,
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleUndoClick = useCallback(() => {
|
const handleUndoClick = useCallback(() => {
|
||||||
@ -44,7 +43,9 @@ export const Upload: React.FC<{
|
|||||||
}, [dispatch, id]);
|
}, [dispatch, id]);
|
||||||
|
|
||||||
const handleFocalPointClick = useCallback(() => {
|
const handleFocalPointClick = useCallback(() => {
|
||||||
dispatch(initMediaEditModal(id));
|
dispatch(
|
||||||
|
openModal({ modalType: 'FOCAL_POINT', modalProps: { mediaId: id } }),
|
||||||
|
);
|
||||||
}, [dispatch, id]);
|
}, [dispatch, id]);
|
||||||
|
|
||||||
const { attributes, listeners, setNodeRef, transform, transition } =
|
const { attributes, listeners, setNodeRef, transform, transition } =
|
||||||
|
@ -2,7 +2,11 @@ import { useState, useCallback, useMemo } from 'react';
|
|||||||
|
|
||||||
import { useIntl, defineMessages } from 'react-intl';
|
import { useIntl, defineMessages } from 'react-intl';
|
||||||
|
|
||||||
import type { List } from 'immutable';
|
import type {
|
||||||
|
List,
|
||||||
|
Map as ImmutableMap,
|
||||||
|
List as ImmutableList,
|
||||||
|
} from 'immutable';
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
DragStartEvent,
|
DragStartEvent,
|
||||||
@ -63,18 +67,20 @@ export const UploadForm: React.FC = () => {
|
|||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const mediaIds = useAppSelector(
|
const mediaIds = useAppSelector(
|
||||||
(state) =>
|
(state) =>
|
||||||
state.compose // eslint-disable-line @typescript-eslint/no-unsafe-call
|
(
|
||||||
.get('media_attachments') // eslint-disable-line @typescript-eslint/no-unsafe-member-access
|
(state.compose as ImmutableMap<string, unknown>).get(
|
||||||
.map((item: MediaAttachment) => item.get('id')) as List<string>, // eslint-disable-line @typescript-eslint/no-unsafe-member-access
|
'media_attachments',
|
||||||
|
) as ImmutableList<MediaAttachment>
|
||||||
|
).map((item: MediaAttachment) => item.get('id')) as List<string>,
|
||||||
);
|
);
|
||||||
const active = useAppSelector(
|
const active = useAppSelector(
|
||||||
(state) => state.compose.get('is_uploading') as boolean, // eslint-disable-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
|
(state) => state.compose.get('is_uploading') as boolean,
|
||||||
);
|
);
|
||||||
const progress = useAppSelector(
|
const progress = useAppSelector(
|
||||||
(state) => state.compose.get('progress') as number, // eslint-disable-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
|
(state) => state.compose.get('progress') as number,
|
||||||
);
|
);
|
||||||
const isProcessing = useAppSelector(
|
const isProcessing = useAppSelector(
|
||||||
(state) => state.compose.get('is_processing') as boolean, // eslint-disable-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
|
(state) => state.compose.get('is_processing') as boolean,
|
||||||
);
|
);
|
||||||
const [activeId, setActiveId] = useState<UniqueIdentifier | null>(null);
|
const [activeId, setActiveId] = useState<UniqueIdentifier | null>(null);
|
||||||
const sensors = useSensors(
|
const sensors = useSensors(
|
||||||
|
@ -65,7 +65,9 @@ export const NotificationFollow: React.FC<{
|
|||||||
const account = notification.sampleAccountIds[0];
|
const account = notification.sampleAccountIds[0];
|
||||||
|
|
||||||
if (account) {
|
if (account) {
|
||||||
actions = <FollowButton accountId={notification.sampleAccountIds[0]} />;
|
actions = (
|
||||||
|
<FollowButton compact accountId={notification.sampleAccountIds[0]} />
|
||||||
|
);
|
||||||
additionalContent = <FollowerCount accountId={account} />;
|
additionalContent = <FollowerCount accountId={account} />;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -28,7 +28,6 @@ export const BoostModal: React.FC<{
|
|||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
|
||||||
const defaultPrivacy = useAppSelector(
|
const defaultPrivacy = useAppSelector(
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
|
|
||||||
(state) => state.compose.get('default_privacy') as StatusVisibility,
|
(state) => state.compose.get('default_privacy') as StatusVisibility,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -1,438 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import { PureComponent } from 'react';
|
|
||||||
|
|
||||||
import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
|
|
||||||
|
|
||||||
import classNames from 'classnames';
|
|
||||||
|
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
|
|
||||||
import Textarea from 'react-textarea-autosize';
|
|
||||||
import { length } from 'stringz';
|
|
||||||
// eslint-disable-next-line import/extensions
|
|
||||||
import tesseractWorkerPath from 'tesseract.js/dist/worker.min.js';
|
|
||||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
|
||||||
import tesseractCorePath from 'tesseract.js-core/tesseract-core.wasm.js';
|
|
||||||
|
|
||||||
import CloseIcon from '@/material-icons/400-24px/close.svg?react';
|
|
||||||
import { Button } from 'mastodon/components/button';
|
|
||||||
import { GIFV } from 'mastodon/components/gifv';
|
|
||||||
import { IconButton } from 'mastodon/components/icon_button';
|
|
||||||
import Audio from 'mastodon/features/audio';
|
|
||||||
import { CharacterCounter } from 'mastodon/features/compose/components/character_counter';
|
|
||||||
import { UploadProgress } from 'mastodon/features/compose/components/upload_progress';
|
|
||||||
import { Tesseract as fetchTesseract } from 'mastodon/features/ui/util/async-components';
|
|
||||||
import { me } from 'mastodon/initial_state';
|
|
||||||
import { assetHost } from 'mastodon/utils/config';
|
|
||||||
|
|
||||||
import { changeUploadCompose, uploadThumbnail, onChangeMediaDescription, onChangeMediaFocus } from '../../../actions/compose';
|
|
||||||
import Video, { getPointerPosition } from '../../video';
|
|
||||||
|
|
||||||
const messages = defineMessages({
|
|
||||||
close: { id: 'lightbox.close', defaultMessage: 'Close' },
|
|
||||||
apply: { id: 'upload_modal.apply', defaultMessage: 'Apply' },
|
|
||||||
applying: { id: 'upload_modal.applying', defaultMessage: 'Applying…' },
|
|
||||||
placeholder: { id: 'upload_modal.description_placeholder', defaultMessage: 'A quick brown fox jumps over the lazy dog' },
|
|
||||||
chooseImage: { id: 'upload_modal.choose_image', defaultMessage: 'Choose image' },
|
|
||||||
discardMessage: { id: 'confirmations.discard_edit_media.message', defaultMessage: 'You have unsaved changes to the media description or preview, discard them anyway?' },
|
|
||||||
discardConfirm: { id: 'confirmations.discard_edit_media.confirm', defaultMessage: 'Discard' },
|
|
||||||
});
|
|
||||||
|
|
||||||
const mapStateToProps = (state, { id }) => ({
|
|
||||||
media: state.getIn(['compose', 'media_attachments']).find(item => item.get('id') === id),
|
|
||||||
account: state.getIn(['accounts', me]),
|
|
||||||
isUploadingThumbnail: state.getIn(['compose', 'isUploadingThumbnail']),
|
|
||||||
description: state.getIn(['compose', 'media_modal', 'description']),
|
|
||||||
lang: state.getIn(['compose', 'language']),
|
|
||||||
focusX: state.getIn(['compose', 'media_modal', 'focusX']),
|
|
||||||
focusY: state.getIn(['compose', 'media_modal', 'focusY']),
|
|
||||||
dirty: state.getIn(['compose', 'media_modal', 'dirty']),
|
|
||||||
is_changing_upload: state.getIn(['compose', 'is_changing_upload']),
|
|
||||||
});
|
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch, { id }) => ({
|
|
||||||
|
|
||||||
onSave: (description, x, y) => {
|
|
||||||
dispatch(changeUploadCompose(id, { description, focus: `${x.toFixed(2)},${y.toFixed(2)}` }));
|
|
||||||
},
|
|
||||||
|
|
||||||
onChangeDescription: (description) => {
|
|
||||||
dispatch(onChangeMediaDescription(description));
|
|
||||||
},
|
|
||||||
|
|
||||||
onChangeFocus: (focusX, focusY) => {
|
|
||||||
dispatch(onChangeMediaFocus(focusX, focusY));
|
|
||||||
},
|
|
||||||
|
|
||||||
onSelectThumbnail: files => {
|
|
||||||
dispatch(uploadThumbnail(id, files[0]));
|
|
||||||
},
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
const removeExtraLineBreaks = str => str.replace(/\n\n/g, '******')
|
|
||||||
.replace(/\n/g, ' ')
|
|
||||||
.replace(/\*\*\*\*\*\*/g, '\n\n');
|
|
||||||
|
|
||||||
class ImageLoader extends PureComponent {
|
|
||||||
|
|
||||||
static propTypes = {
|
|
||||||
src: PropTypes.string.isRequired,
|
|
||||||
width: PropTypes.number,
|
|
||||||
height: PropTypes.number,
|
|
||||||
};
|
|
||||||
|
|
||||||
state = {
|
|
||||||
loading: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
const image = new Image();
|
|
||||||
image.addEventListener('load', () => this.setState({ loading: false }));
|
|
||||||
image.src = this.props.src;
|
|
||||||
}
|
|
||||||
|
|
||||||
render () {
|
|
||||||
const { loading } = this.state;
|
|
||||||
|
|
||||||
if (loading) {
|
|
||||||
return <canvas width={this.props.width} height={this.props.height} />;
|
|
||||||
} else {
|
|
||||||
return <img {...this.props} alt='' />;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
class FocalPointModal extends ImmutablePureComponent {
|
|
||||||
|
|
||||||
static propTypes = {
|
|
||||||
media: ImmutablePropTypes.map.isRequired,
|
|
||||||
account: ImmutablePropTypes.record.isRequired,
|
|
||||||
isUploadingThumbnail: PropTypes.bool,
|
|
||||||
onSave: PropTypes.func.isRequired,
|
|
||||||
onChangeDescription: PropTypes.func.isRequired,
|
|
||||||
onChangeFocus: PropTypes.func.isRequired,
|
|
||||||
onSelectThumbnail: PropTypes.func.isRequired,
|
|
||||||
onClose: PropTypes.func.isRequired,
|
|
||||||
intl: PropTypes.object.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
state = {
|
|
||||||
dragging: false,
|
|
||||||
dirty: false,
|
|
||||||
progress: 0,
|
|
||||||
loading: true,
|
|
||||||
ocrStatus: '',
|
|
||||||
};
|
|
||||||
|
|
||||||
componentWillUnmount () {
|
|
||||||
document.removeEventListener('mousemove', this.handleMouseMove);
|
|
||||||
document.removeEventListener('mouseup', this.handleMouseUp);
|
|
||||||
}
|
|
||||||
|
|
||||||
handleMouseDown = e => {
|
|
||||||
document.addEventListener('mousemove', this.handleMouseMove);
|
|
||||||
document.addEventListener('mouseup', this.handleMouseUp);
|
|
||||||
|
|
||||||
this.updatePosition(e);
|
|
||||||
this.setState({ dragging: true });
|
|
||||||
};
|
|
||||||
|
|
||||||
handleTouchStart = e => {
|
|
||||||
document.addEventListener('touchmove', this.handleMouseMove);
|
|
||||||
document.addEventListener('touchend', this.handleTouchEnd);
|
|
||||||
|
|
||||||
this.updatePosition(e);
|
|
||||||
this.setState({ dragging: true });
|
|
||||||
};
|
|
||||||
|
|
||||||
handleMouseMove = e => {
|
|
||||||
this.updatePosition(e);
|
|
||||||
};
|
|
||||||
|
|
||||||
handleMouseUp = () => {
|
|
||||||
document.removeEventListener('mousemove', this.handleMouseMove);
|
|
||||||
document.removeEventListener('mouseup', this.handleMouseUp);
|
|
||||||
|
|
||||||
this.setState({ dragging: false });
|
|
||||||
};
|
|
||||||
|
|
||||||
handleTouchEnd = () => {
|
|
||||||
document.removeEventListener('touchmove', this.handleMouseMove);
|
|
||||||
document.removeEventListener('touchend', this.handleTouchEnd);
|
|
||||||
|
|
||||||
this.setState({ dragging: false });
|
|
||||||
};
|
|
||||||
|
|
||||||
updatePosition = e => {
|
|
||||||
const { x, y } = getPointerPosition(this.node, e);
|
|
||||||
const focusX = (x - .5) * 2;
|
|
||||||
const focusY = (y - .5) * -2;
|
|
||||||
|
|
||||||
this.props.onChangeFocus(focusX, focusY);
|
|
||||||
};
|
|
||||||
|
|
||||||
handleChange = e => {
|
|
||||||
this.props.onChangeDescription(e.target.value);
|
|
||||||
};
|
|
||||||
|
|
||||||
handleKeyDown = (e) => {
|
|
||||||
if (e.keyCode === 13 && (e.ctrlKey || e.metaKey)) {
|
|
||||||
this.props.onChangeDescription(e.target.value);
|
|
||||||
this.handleSubmit(e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
handleSubmit = (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
this.props.onSave(this.props.description, this.props.focusX, this.props.focusY);
|
|
||||||
};
|
|
||||||
|
|
||||||
getCloseConfirmationMessage = () => {
|
|
||||||
const { intl, dirty } = this.props;
|
|
||||||
|
|
||||||
if (dirty) {
|
|
||||||
return {
|
|
||||||
message: intl.formatMessage(messages.discardMessage),
|
|
||||||
confirm: intl.formatMessage(messages.discardConfirm),
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
setRef = c => {
|
|
||||||
this.node = c;
|
|
||||||
};
|
|
||||||
|
|
||||||
handleTextDetection = () => {
|
|
||||||
this._detectText();
|
|
||||||
};
|
|
||||||
|
|
||||||
_detectText = (refreshCache = false) => {
|
|
||||||
const { media } = this.props;
|
|
||||||
|
|
||||||
this.setState({ detecting: true });
|
|
||||||
|
|
||||||
fetchTesseract().then(({ createWorker }) => {
|
|
||||||
const worker = createWorker({
|
|
||||||
workerPath: tesseractWorkerPath,
|
|
||||||
corePath: tesseractCorePath,
|
|
||||||
langPath: `${assetHost}/ocr/lang-data`,
|
|
||||||
logger: ({ status, progress }) => {
|
|
||||||
if (status === 'recognizing text') {
|
|
||||||
this.setState({ ocrStatus: 'detecting', progress });
|
|
||||||
} else {
|
|
||||||
this.setState({ ocrStatus: 'preparing', progress });
|
|
||||||
}
|
|
||||||
},
|
|
||||||
cacheMethod: refreshCache ? 'refresh' : 'write',
|
|
||||||
});
|
|
||||||
|
|
||||||
let media_url = media.get('url');
|
|
||||||
|
|
||||||
if (window.URL && URL.createObjectURL) {
|
|
||||||
try {
|
|
||||||
media_url = URL.createObjectURL(media.get('file'));
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (async () => {
|
|
||||||
await worker.load();
|
|
||||||
await worker.loadLanguage('eng');
|
|
||||||
await worker.initialize('eng');
|
|
||||||
const { data: { text } } = await worker.recognize(media_url);
|
|
||||||
this.setState({ detecting: false });
|
|
||||||
this.props.onChangeDescription(removeExtraLineBreaks(text));
|
|
||||||
await worker.terminate();
|
|
||||||
})().catch((e) => {
|
|
||||||
if (refreshCache) {
|
|
||||||
throw e;
|
|
||||||
} else {
|
|
||||||
this._detectText(true);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}).catch((e) => {
|
|
||||||
console.error(e);
|
|
||||||
this.setState({ detecting: false });
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
handleThumbnailChange = e => {
|
|
||||||
if (e.target.files.length > 0) {
|
|
||||||
this.props.onSelectThumbnail(e.target.files);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
setFileInputRef = c => {
|
|
||||||
this.fileInput = c;
|
|
||||||
};
|
|
||||||
|
|
||||||
handleFileInputClick = () => {
|
|
||||||
this.fileInput.click();
|
|
||||||
};
|
|
||||||
|
|
||||||
render () {
|
|
||||||
const { media, intl, account, onClose, isUploadingThumbnail, description, lang, focusX, focusY, dirty, is_changing_upload } = this.props;
|
|
||||||
const { dragging, detecting, progress, ocrStatus } = this.state;
|
|
||||||
const x = (focusX / 2) + .5;
|
|
||||||
const y = (focusY / -2) + .5;
|
|
||||||
|
|
||||||
const width = media.getIn(['meta', 'original', 'width']) || null;
|
|
||||||
const height = media.getIn(['meta', 'original', 'height']) || null;
|
|
||||||
const focals = ['image', 'gifv'].includes(media.get('type'));
|
|
||||||
const thumbnailable = ['audio', 'video'].includes(media.get('type'));
|
|
||||||
|
|
||||||
const previewRatio = 16/9;
|
|
||||||
const previewWidth = 200;
|
|
||||||
const previewHeight = previewWidth / previewRatio;
|
|
||||||
|
|
||||||
let descriptionLabel = null;
|
|
||||||
|
|
||||||
if (media.get('type') === 'audio') {
|
|
||||||
descriptionLabel = <FormattedMessage id='upload_form.audio_description' defaultMessage='Describe for people who are hard of hearing' />;
|
|
||||||
} else if (media.get('type') === 'video') {
|
|
||||||
descriptionLabel = <FormattedMessage id='upload_form.video_description' defaultMessage='Describe for people who are deaf, hard of hearing, blind or have low vision' />;
|
|
||||||
} else {
|
|
||||||
descriptionLabel = <FormattedMessage id='upload_form.description' defaultMessage='Describe for people who are blind or have low vision' />;
|
|
||||||
}
|
|
||||||
|
|
||||||
let ocrMessage = '';
|
|
||||||
if (ocrStatus === 'detecting') {
|
|
||||||
ocrMessage = <FormattedMessage id='upload_modal.analyzing_picture' defaultMessage='Analyzing picture…' />;
|
|
||||||
} else {
|
|
||||||
ocrMessage = <FormattedMessage id='upload_modal.preparing_ocr' defaultMessage='Preparing OCR…' />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='modal-root__modal report-modal' style={{ maxWidth: 960 }}>
|
|
||||||
<div className='report-modal__target'>
|
|
||||||
<IconButton className='report-modal__close' title={intl.formatMessage(messages.close)} icon='times' iconComponent={CloseIcon} onClick={onClose} size={20} />
|
|
||||||
<FormattedMessage id='upload_modal.edit_media' defaultMessage='Edit media' />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='report-modal__container'>
|
|
||||||
<form className='report-modal__comment' onSubmit={this.handleSubmit} >
|
|
||||||
{focals && <p><FormattedMessage id='upload_modal.hint' defaultMessage='Click or drag the circle on the preview to choose the focal point which will always be in view on all thumbnails.' /></p>}
|
|
||||||
|
|
||||||
{thumbnailable && (
|
|
||||||
<>
|
|
||||||
<label className='setting-text-label' htmlFor='upload-modal__thumbnail'><FormattedMessage id='upload_form.thumbnail' defaultMessage='Change thumbnail' /></label>
|
|
||||||
|
|
||||||
<Button disabled={isUploadingThumbnail || !media.get('unattached')} text={intl.formatMessage(messages.chooseImage)} onClick={this.handleFileInputClick} />
|
|
||||||
|
|
||||||
<label>
|
|
||||||
<span style={{ display: 'none' }}>{intl.formatMessage(messages.chooseImage)}</span>
|
|
||||||
|
|
||||||
<input
|
|
||||||
id='upload-modal__thumbnail'
|
|
||||||
ref={this.setFileInputRef}
|
|
||||||
type='file'
|
|
||||||
accept='image/png,image/jpeg'
|
|
||||||
onChange={this.handleThumbnailChange}
|
|
||||||
style={{ display: 'none' }}
|
|
||||||
disabled={isUploadingThumbnail || is_changing_upload}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<hr className='setting-divider' />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<label className='setting-text-label' htmlFor='upload-modal__description'>
|
|
||||||
{descriptionLabel}
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<div className='setting-text__wrapper'>
|
|
||||||
<Textarea
|
|
||||||
id='upload-modal__description'
|
|
||||||
className='setting-text light'
|
|
||||||
value={detecting ? '…' : description}
|
|
||||||
lang={lang}
|
|
||||||
onChange={this.handleChange}
|
|
||||||
onKeyDown={this.handleKeyDown}
|
|
||||||
disabled={detecting || is_changing_upload}
|
|
||||||
autoFocus
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className='setting-text__modifiers'>
|
|
||||||
<UploadProgress progress={progress * 100} active={detecting} icon='file-text-o' message={ocrMessage} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='setting-text__toolbar'>
|
|
||||||
<button
|
|
||||||
type='button'
|
|
||||||
disabled={detecting || media.get('type') !== 'image' || is_changing_upload}
|
|
||||||
className='link-button'
|
|
||||||
onClick={this.handleTextDetection}
|
|
||||||
>
|
|
||||||
<FormattedMessage id='upload_modal.detect_text' defaultMessage='Detect text from picture' />
|
|
||||||
</button>
|
|
||||||
<CharacterCounter max={1500} text={detecting ? '' : description} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
type='submit'
|
|
||||||
disabled={!dirty || detecting || isUploadingThumbnail || length(description) > 1500 || is_changing_upload}
|
|
||||||
text={intl.formatMessage(is_changing_upload ? messages.applying : messages.apply)}
|
|
||||||
/>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<div className='focal-point-modal__content'>
|
|
||||||
{focals && (
|
|
||||||
<div className={classNames('focal-point', { dragging })} ref={this.setRef} onMouseDown={this.handleMouseDown} onTouchStart={this.handleTouchStart}>
|
|
||||||
{media.get('type') === 'image' && <ImageLoader src={media.get('url')} width={width} height={height} alt='' />}
|
|
||||||
{media.get('type') === 'gifv' && <GIFV src={media.get('url')} key={media.get('url')} width={width} height={height} />}
|
|
||||||
|
|
||||||
<div className='focal-point__preview'>
|
|
||||||
<strong><FormattedMessage id='upload_modal.preview_label' defaultMessage='Preview ({ratio})' values={{ ratio: '16:9' }} /></strong>
|
|
||||||
<div style={{ width: previewWidth, height: previewHeight, backgroundImage: `url(${media.get('preview_url')})`, backgroundSize: 'cover', backgroundPosition: `${x * 100}% ${y * 100}%` }} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='focal-point__reticle' style={{ top: `${y * 100}%`, left: `${x * 100}%` }} />
|
|
||||||
<div className='focal-point__overlay' />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{media.get('type') === 'video' && (
|
|
||||||
<Video
|
|
||||||
preview={media.get('preview_url')}
|
|
||||||
frameRate={media.getIn(['meta', 'original', 'frame_rate'])}
|
|
||||||
blurhash={media.get('blurhash')}
|
|
||||||
src={media.get('url')}
|
|
||||||
detailed
|
|
||||||
inline
|
|
||||||
editable
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{media.get('type') === 'audio' && (
|
|
||||||
<Audio
|
|
||||||
src={media.get('url')}
|
|
||||||
duration={media.getIn(['meta', 'original', 'duration'], 0)}
|
|
||||||
height={150}
|
|
||||||
poster={media.get('preview_url') || account.get('avatar_static')}
|
|
||||||
backgroundColor={media.getIn(['meta', 'colors', 'background'])}
|
|
||||||
foregroundColor={media.getIn(['meta', 'colors', 'foreground'])}
|
|
||||||
accentColor={media.getIn(['meta', 'colors', 'accent'])}
|
|
||||||
editable
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps, null, {
|
|
||||||
forwardRef: true,
|
|
||||||
})(injectIntl(FocalPointModal, { forwardRef: true }));
|
|
@ -4,6 +4,7 @@ import { PureComponent } from 'react';
|
|||||||
import { Helmet } from 'react-helmet';
|
import { Helmet } from 'react-helmet';
|
||||||
|
|
||||||
import Base from 'mastodon/components/modal_root';
|
import Base from 'mastodon/components/modal_root';
|
||||||
|
import { AltTextModal } from 'mastodon/features/alt_text_modal';
|
||||||
import {
|
import {
|
||||||
MuteModal,
|
MuteModal,
|
||||||
BlockModal,
|
BlockModal,
|
||||||
@ -37,7 +38,6 @@ import {
|
|||||||
ConfirmLogOutModal,
|
ConfirmLogOutModal,
|
||||||
ConfirmFollowToListModal,
|
ConfirmFollowToListModal,
|
||||||
} from './confirmation_modals';
|
} from './confirmation_modals';
|
||||||
import FocalPointModal from './focal_point_modal';
|
|
||||||
import ImageModal from './image_modal';
|
import ImageModal from './image_modal';
|
||||||
import MediaModal from './media_modal';
|
import MediaModal from './media_modal';
|
||||||
import { ModalPlaceholder } from './modal_placeholder';
|
import { ModalPlaceholder } from './modal_placeholder';
|
||||||
@ -64,7 +64,7 @@ export const MODAL_COMPONENTS = {
|
|||||||
'REPORT': ReportModal,
|
'REPORT': ReportModal,
|
||||||
'ACTIONS': () => Promise.resolve({ default: ActionsModal }),
|
'ACTIONS': () => Promise.resolve({ default: ActionsModal }),
|
||||||
'EMBED': EmbedModal,
|
'EMBED': EmbedModal,
|
||||||
'FOCAL_POINT': () => Promise.resolve({ default: FocalPointModal }),
|
'FOCAL_POINT': () => Promise.resolve({ default: AltTextModal }),
|
||||||
'LIST_ADDER': ListAdder,
|
'LIST_ADDER': ListAdder,
|
||||||
'COMPARE_HISTORY': CompareHistoryModal,
|
'COMPARE_HISTORY': CompareHistoryModal,
|
||||||
'FILTER': FilterModal,
|
'FILTER': FilterModal,
|
||||||
@ -139,8 +139,7 @@ export default class ModalRoot extends PureComponent {
|
|||||||
<>
|
<>
|
||||||
<BundleContainer fetchComponent={MODAL_COMPONENTS[type]} loading={this.renderLoading} error={this.renderError} renderDelay={200}>
|
<BundleContainer fetchComponent={MODAL_COMPONENTS[type]} loading={this.renderLoading} error={this.renderError} renderDelay={200}>
|
||||||
{(SpecificComponent) => {
|
{(SpecificComponent) => {
|
||||||
const ref = typeof SpecificComponent !== 'function' ? this.setModalRef : undefined;
|
return <SpecificComponent {...props} onChangeBackgroundColor={this.setBackgroundColor} onClose={this.handleClose} ref={this.setModalRef} />;
|
||||||
return <SpecificComponent {...props} onChangeBackgroundColor={this.setBackgroundColor} onClose={this.handleClose} ref={ref} />;
|
|
||||||
}}
|
}}
|
||||||
</BundleContainer>
|
</BundleContainer>
|
||||||
|
|
||||||
|
@ -16,6 +16,7 @@ const mapDispatchToProps = dispatch => ({
|
|||||||
if (confirmationMessage) {
|
if (confirmationMessage) {
|
||||||
dispatch(
|
dispatch(
|
||||||
openModal({
|
openModal({
|
||||||
|
previousModalProps: confirmationMessage.props,
|
||||||
modalType: 'CONFIRM',
|
modalType: 'CONFIRM',
|
||||||
modalProps: {
|
modalProps: {
|
||||||
message: confirmationMessage.message,
|
message: confirmationMessage.message,
|
||||||
@ -24,7 +25,8 @@ const mapDispatchToProps = dispatch => ({
|
|||||||
modalType: undefined,
|
modalType: undefined,
|
||||||
ignoreFocus: { ignoreFocus },
|
ignoreFocus: { ignoreFocus },
|
||||||
})),
|
})),
|
||||||
} }),
|
},
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
dispatch(closeModal({
|
dispatch(closeModal({
|
||||||
|
@ -86,6 +86,13 @@
|
|||||||
"alert.unexpected.message": "An unexpected error occurred.",
|
"alert.unexpected.message": "An unexpected error occurred.",
|
||||||
"alert.unexpected.title": "Oops!",
|
"alert.unexpected.title": "Oops!",
|
||||||
"alt_text_badge.title": "Alt text",
|
"alt_text_badge.title": "Alt text",
|
||||||
|
"alt_text_modal.add_alt_text": "Add alt text",
|
||||||
|
"alt_text_modal.add_text_from_image": "Add text from image",
|
||||||
|
"alt_text_modal.cancel": "Cancel",
|
||||||
|
"alt_text_modal.change_thumbnail": "Change thumbnail",
|
||||||
|
"alt_text_modal.describe_for_people_with_hearing_impairments": "Describe this for people with hearing impairments…",
|
||||||
|
"alt_text_modal.describe_for_people_with_visual_impairments": "Describe this for people with visual impairments…",
|
||||||
|
"alt_text_modal.done": "Done",
|
||||||
"announcement.announcement": "Announcement",
|
"announcement.announcement": "Announcement",
|
||||||
"annual_report.summary.archetype.booster": "The cool-hunter",
|
"annual_report.summary.archetype.booster": "The cool-hunter",
|
||||||
"annual_report.summary.archetype.lurker": "The lurker",
|
"annual_report.summary.archetype.lurker": "The lurker",
|
||||||
@ -875,26 +882,12 @@
|
|||||||
"upload_button.label": "Add images, a video or an audio file",
|
"upload_button.label": "Add images, a video or an audio file",
|
||||||
"upload_error.limit": "File upload limit exceeded.",
|
"upload_error.limit": "File upload limit exceeded.",
|
||||||
"upload_error.poll": "File upload not allowed with polls.",
|
"upload_error.poll": "File upload not allowed with polls.",
|
||||||
"upload_form.audio_description": "Describe for people who are deaf or hard of hearing",
|
|
||||||
"upload_form.description": "Describe for people who are blind or have low vision",
|
|
||||||
"upload_form.drag_and_drop.instructions": "To pick up a media attachment, press space or enter. While dragging, use the arrow keys to move the media attachment in any given direction. Press space or enter again to drop the media attachment in its new position, or press escape to cancel.",
|
"upload_form.drag_and_drop.instructions": "To pick up a media attachment, press space or enter. While dragging, use the arrow keys to move the media attachment in any given direction. Press space or enter again to drop the media attachment in its new position, or press escape to cancel.",
|
||||||
"upload_form.drag_and_drop.on_drag_cancel": "Dragging was cancelled. Media attachment {item} was dropped.",
|
"upload_form.drag_and_drop.on_drag_cancel": "Dragging was cancelled. Media attachment {item} was dropped.",
|
||||||
"upload_form.drag_and_drop.on_drag_end": "Media attachment {item} was dropped.",
|
"upload_form.drag_and_drop.on_drag_end": "Media attachment {item} was dropped.",
|
||||||
"upload_form.drag_and_drop.on_drag_over": "Media attachment {item} was moved.",
|
"upload_form.drag_and_drop.on_drag_over": "Media attachment {item} was moved.",
|
||||||
"upload_form.drag_and_drop.on_drag_start": "Picked up media attachment {item}.",
|
"upload_form.drag_and_drop.on_drag_start": "Picked up media attachment {item}.",
|
||||||
"upload_form.edit": "Edit",
|
"upload_form.edit": "Edit",
|
||||||
"upload_form.thumbnail": "Change thumbnail",
|
|
||||||
"upload_form.video_description": "Describe for people who are deaf, hard of hearing, blind or have low vision",
|
|
||||||
"upload_modal.analyzing_picture": "Analyzing picture…",
|
|
||||||
"upload_modal.apply": "Apply",
|
|
||||||
"upload_modal.applying": "Applying…",
|
|
||||||
"upload_modal.choose_image": "Choose image",
|
|
||||||
"upload_modal.description_placeholder": "A quick brown fox jumps over the lazy dog",
|
|
||||||
"upload_modal.detect_text": "Detect text from picture",
|
|
||||||
"upload_modal.edit_media": "Edit media",
|
|
||||||
"upload_modal.hint": "Click or drag the circle on the preview to choose the focal point which will always be in view on all thumbnails.",
|
|
||||||
"upload_modal.preparing_ocr": "Preparing OCR…",
|
|
||||||
"upload_modal.preview_label": "Preview ({ratio})",
|
|
||||||
"upload_progress.label": "Uploading...",
|
"upload_progress.label": "Uploading...",
|
||||||
"upload_progress.processing": "Processing…",
|
"upload_progress.processing": "Processing…",
|
||||||
"username.taken": "That username is taken. Try another",
|
"username.taken": "That username is taken. Try another",
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { Map as ImmutableMap, List as ImmutableList, OrderedSet as ImmutableOrderedSet, fromJS } from 'immutable';
|
import { Map as ImmutableMap, List as ImmutableList, OrderedSet as ImmutableOrderedSet, fromJS } from 'immutable';
|
||||||
|
|
||||||
|
import { changeUploadCompose } from 'mastodon/actions/compose_typed';
|
||||||
import { timelineDelete } from 'mastodon/actions/timelines_typed';
|
import { timelineDelete } from 'mastodon/actions/timelines_typed';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -36,17 +37,11 @@ import {
|
|||||||
COMPOSE_LANGUAGE_CHANGE,
|
COMPOSE_LANGUAGE_CHANGE,
|
||||||
COMPOSE_COMPOSING_CHANGE,
|
COMPOSE_COMPOSING_CHANGE,
|
||||||
COMPOSE_EMOJI_INSERT,
|
COMPOSE_EMOJI_INSERT,
|
||||||
COMPOSE_UPLOAD_CHANGE_REQUEST,
|
|
||||||
COMPOSE_UPLOAD_CHANGE_SUCCESS,
|
|
||||||
COMPOSE_UPLOAD_CHANGE_FAIL,
|
|
||||||
COMPOSE_RESET,
|
COMPOSE_RESET,
|
||||||
COMPOSE_POLL_ADD,
|
COMPOSE_POLL_ADD,
|
||||||
COMPOSE_POLL_REMOVE,
|
COMPOSE_POLL_REMOVE,
|
||||||
COMPOSE_POLL_OPTION_CHANGE,
|
COMPOSE_POLL_OPTION_CHANGE,
|
||||||
COMPOSE_POLL_SETTINGS_CHANGE,
|
COMPOSE_POLL_SETTINGS_CHANGE,
|
||||||
INIT_MEDIA_EDIT_MODAL,
|
|
||||||
COMPOSE_CHANGE_MEDIA_DESCRIPTION,
|
|
||||||
COMPOSE_CHANGE_MEDIA_FOCUS,
|
|
||||||
COMPOSE_CHANGE_MEDIA_ORDER,
|
COMPOSE_CHANGE_MEDIA_ORDER,
|
||||||
COMPOSE_SET_STATUS,
|
COMPOSE_SET_STATUS,
|
||||||
COMPOSE_FOCUS,
|
COMPOSE_FOCUS,
|
||||||
@ -87,13 +82,6 @@ const initialState = ImmutableMap({
|
|||||||
resetFileKey: Math.floor((Math.random() * 0x10000)),
|
resetFileKey: Math.floor((Math.random() * 0x10000)),
|
||||||
idempotencyKey: null,
|
idempotencyKey: null,
|
||||||
tagHistory: ImmutableList(),
|
tagHistory: ImmutableList(),
|
||||||
media_modal: ImmutableMap({
|
|
||||||
id: null,
|
|
||||||
description: '',
|
|
||||||
focusX: 0,
|
|
||||||
focusY: 0,
|
|
||||||
dirty: false,
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const initialPoll = ImmutableMap({
|
const initialPoll = ImmutableMap({
|
||||||
@ -294,7 +282,24 @@ const updatePoll = (state, index, value, maxOptions) => state.updateIn(['poll',
|
|||||||
return tmp;
|
return tmp;
|
||||||
});
|
});
|
||||||
|
|
||||||
export default function compose(state = initialState, action) {
|
/** @type {import('@reduxjs/toolkit').Reducer<typeof initialState>} */
|
||||||
|
export const composeReducer = (state = initialState, action) => {
|
||||||
|
if (changeUploadCompose.fulfilled.match(action)) {
|
||||||
|
return state
|
||||||
|
.set('is_changing_upload', false)
|
||||||
|
.update('media_attachments', list => list.map(item => {
|
||||||
|
if (item.get('id') === action.payload.media.id) {
|
||||||
|
return fromJS(action.payload.media).set('unattached', !action.payload.attached);
|
||||||
|
}
|
||||||
|
|
||||||
|
return item;
|
||||||
|
}));
|
||||||
|
} else if (changeUploadCompose.pending.match(action)) {
|
||||||
|
return state.set('is_changing_upload', true);
|
||||||
|
} else if (changeUploadCompose.rejected.match(action)) {
|
||||||
|
return state.set('is_changing_upload', false);
|
||||||
|
}
|
||||||
|
|
||||||
switch(action.type) {
|
switch(action.type) {
|
||||||
case STORE_HYDRATE:
|
case STORE_HYDRATE:
|
||||||
return hydrate(state, action.state.get('compose'));
|
return hydrate(state, action.state.get('compose'));
|
||||||
@ -369,16 +374,13 @@ export default function compose(state = initialState, action) {
|
|||||||
});
|
});
|
||||||
case COMPOSE_SUBMIT_REQUEST:
|
case COMPOSE_SUBMIT_REQUEST:
|
||||||
return state.set('is_submitting', true);
|
return state.set('is_submitting', true);
|
||||||
case COMPOSE_UPLOAD_CHANGE_REQUEST:
|
|
||||||
return state.set('is_changing_upload', true);
|
|
||||||
case COMPOSE_REPLY_CANCEL:
|
case COMPOSE_REPLY_CANCEL:
|
||||||
case COMPOSE_RESET:
|
case COMPOSE_RESET:
|
||||||
case COMPOSE_SUBMIT_SUCCESS:
|
case COMPOSE_SUBMIT_SUCCESS:
|
||||||
return clearAll(state);
|
return clearAll(state);
|
||||||
case COMPOSE_SUBMIT_FAIL:
|
case COMPOSE_SUBMIT_FAIL:
|
||||||
return state.set('is_submitting', false);
|
return state.set('is_submitting', false);
|
||||||
case COMPOSE_UPLOAD_CHANGE_FAIL:
|
|
||||||
return state.set('is_changing_upload', false);
|
|
||||||
case COMPOSE_UPLOAD_REQUEST:
|
case COMPOSE_UPLOAD_REQUEST:
|
||||||
return state.set('is_uploading', true).update('pending_media_attachments', n => n + 1);
|
return state.set('is_uploading', true).update('pending_media_attachments', n => n + 1);
|
||||||
case COMPOSE_UPLOAD_PROCESSING:
|
case COMPOSE_UPLOAD_PROCESSING:
|
||||||
@ -407,20 +409,6 @@ export default function compose(state = initialState, action) {
|
|||||||
|
|
||||||
return item;
|
return item;
|
||||||
}));
|
}));
|
||||||
case INIT_MEDIA_EDIT_MODAL: {
|
|
||||||
const media = state.get('media_attachments').find(item => item.get('id') === action.id);
|
|
||||||
return state.set('media_modal', ImmutableMap({
|
|
||||||
id: action.id,
|
|
||||||
description: media.get('description') || '',
|
|
||||||
focusX: media.getIn(['meta', 'focus', 'x'], 0),
|
|
||||||
focusY: media.getIn(['meta', 'focus', 'y'], 0),
|
|
||||||
dirty: false,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
case COMPOSE_CHANGE_MEDIA_DESCRIPTION:
|
|
||||||
return state.setIn(['media_modal', 'description'], action.description).setIn(['media_modal', 'dirty'], true);
|
|
||||||
case COMPOSE_CHANGE_MEDIA_FOCUS:
|
|
||||||
return state.setIn(['media_modal', 'focusX'], action.focusX).setIn(['media_modal', 'focusY'], action.focusY).setIn(['media_modal', 'dirty'], true);
|
|
||||||
case COMPOSE_MENTION:
|
case COMPOSE_MENTION:
|
||||||
return state.withMutations(map => {
|
return state.withMutations(map => {
|
||||||
map.update('text', text => [text.trim(), `@${action.account.get('acct')} `].filter((str) => str.length !== 0).join(' '));
|
map.update('text', text => [text.trim(), `@${action.account.get('acct')} `].filter((str) => str.length !== 0).join(' '));
|
||||||
@ -458,17 +446,6 @@ export default function compose(state = initialState, action) {
|
|||||||
}
|
}
|
||||||
case COMPOSE_EMOJI_INSERT:
|
case COMPOSE_EMOJI_INSERT:
|
||||||
return insertEmoji(state, action.position, action.emoji, action.needsSpace);
|
return insertEmoji(state, action.position, action.emoji, action.needsSpace);
|
||||||
case COMPOSE_UPLOAD_CHANGE_SUCCESS:
|
|
||||||
return state
|
|
||||||
.set('is_changing_upload', false)
|
|
||||||
.setIn(['media_modal', 'dirty'], false)
|
|
||||||
.update('media_attachments', list => list.map(item => {
|
|
||||||
if (item.get('id') === action.media.id) {
|
|
||||||
return fromJS(action.media).set('unattached', !action.attached);
|
|
||||||
}
|
|
||||||
|
|
||||||
return item;
|
|
||||||
}));
|
|
||||||
case REDRAFT:
|
case REDRAFT:
|
||||||
return state.withMutations(map => {
|
return state.withMutations(map => {
|
||||||
map.set('text', action.raw_text || unescapeHTML(expandMentions(action.status)));
|
map.set('text', action.raw_text || unescapeHTML(expandMentions(action.status)));
|
||||||
@ -550,4 +527,4 @@ export default function compose(state = initialState, action) {
|
|||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
@ -7,7 +7,7 @@ import { accountsReducer } from './accounts';
|
|||||||
import accounts_map from './accounts_map';
|
import accounts_map from './accounts_map';
|
||||||
import alerts from './alerts';
|
import alerts from './alerts';
|
||||||
import announcements from './announcements';
|
import announcements from './announcements';
|
||||||
import compose from './compose';
|
import { composeReducer } from './compose';
|
||||||
import contexts from './contexts';
|
import contexts from './contexts';
|
||||||
import conversations from './conversations';
|
import conversations from './conversations';
|
||||||
import custom_emojis from './custom_emojis';
|
import custom_emojis from './custom_emojis';
|
||||||
@ -59,7 +59,7 @@ const reducers = {
|
|||||||
push_notifications,
|
push_notifications,
|
||||||
server,
|
server,
|
||||||
contexts,
|
contexts,
|
||||||
compose,
|
compose: composeReducer,
|
||||||
search: searchReducer,
|
search: searchReducer,
|
||||||
media_attachments,
|
media_attachments,
|
||||||
notifications,
|
notifications,
|
||||||
|
@ -3,7 +3,6 @@ import { Record as ImmutableRecord, Stack } from 'immutable';
|
|||||||
|
|
||||||
import { timelineDelete } from 'mastodon/actions/timelines_typed';
|
import { timelineDelete } from 'mastodon/actions/timelines_typed';
|
||||||
|
|
||||||
import { COMPOSE_UPLOAD_CHANGE_SUCCESS } from '../actions/compose';
|
|
||||||
import type { ModalType } from '../actions/modal';
|
import type { ModalType } from '../actions/modal';
|
||||||
import { openModal, closeModal } from '../actions/modal';
|
import { openModal, closeModal } from '../actions/modal';
|
||||||
|
|
||||||
@ -53,12 +52,36 @@ const pushModal = (
|
|||||||
state: State,
|
state: State,
|
||||||
modalType: ModalType,
|
modalType: ModalType,
|
||||||
modalProps: ModalProps,
|
modalProps: ModalProps,
|
||||||
|
previousModalProps?: ModalProps,
|
||||||
): State => {
|
): State => {
|
||||||
return state.withMutations((record) => {
|
return state.withMutations((record) => {
|
||||||
record.set('ignoreFocus', false);
|
record.set('ignoreFocus', false);
|
||||||
record.update('stack', (stack) =>
|
record.update('stack', (stack) => {
|
||||||
stack.unshift(Modal({ modalType, modalProps })),
|
let tmp = stack;
|
||||||
);
|
|
||||||
|
// With this option, we update the previously opened modal, so that when the
|
||||||
|
// current (new) modal is closed, the previous modal is re-opened with different
|
||||||
|
// props. Specifically, this is useful for the confirmation modal.
|
||||||
|
if (previousModalProps) {
|
||||||
|
const previousModal = tmp.first() as Modal | undefined;
|
||||||
|
|
||||||
|
if (previousModal) {
|
||||||
|
tmp = tmp.shift().unshift(
|
||||||
|
Modal({
|
||||||
|
modalType: previousModal.modalType,
|
||||||
|
modalProps: {
|
||||||
|
...previousModal.modalProps,
|
||||||
|
...previousModalProps,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tmp = tmp.unshift(Modal({ modalType, modalProps }));
|
||||||
|
|
||||||
|
return tmp;
|
||||||
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -68,11 +91,10 @@ export const modalReducer: Reducer<State> = (state = initialState, action) => {
|
|||||||
state,
|
state,
|
||||||
action.payload.modalType,
|
action.payload.modalType,
|
||||||
action.payload.modalProps,
|
action.payload.modalProps,
|
||||||
|
action.payload.previousModalProps,
|
||||||
);
|
);
|
||||||
else if (closeModal.match(action)) return popModal(state, action.payload);
|
else if (closeModal.match(action)) return popModal(state, action.payload);
|
||||||
// TODO: type those actions
|
// TODO: type those actions
|
||||||
else if (action.type === COMPOSE_UPLOAD_CHANGE_SUCCESS)
|
|
||||||
return popModal(state, { modalType: 'FOCAL_POINT', ignoreFocus: false });
|
|
||||||
else if (timelineDelete.match(action))
|
else if (timelineDelete.match(action))
|
||||||
return state.update('stack', (stack) =>
|
return state.update('stack', (stack) =>
|
||||||
stack.filterNot(
|
stack.filterNot(
|
||||||
|
@ -127,9 +127,7 @@
|
|||||||
.actions-modal ul li:not(:empty) a:focus button,
|
.actions-modal ul li:not(:empty) a:focus button,
|
||||||
.actions-modal ul li:not(:empty) a:hover,
|
.actions-modal ul li:not(:empty) a:hover,
|
||||||
.actions-modal ul li:not(:empty) a:hover button,
|
.actions-modal ul li:not(:empty) a:hover button,
|
||||||
.simple_form .block-button,
|
.simple_form button:not(.button, .link-button) {
|
||||||
.simple_form .button,
|
|
||||||
.simple_form button {
|
|
||||||
color: $white;
|
color: $white;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -142,6 +140,11 @@
|
|||||||
border-top-color: lighten($ui-base-color, 4%);
|
border-top-color: lighten($ui-base-color, 4%);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dialog-modal__content__preview {
|
||||||
|
background: #fff;
|
||||||
|
border-bottom: 1px solid var(--modal-border-color);
|
||||||
|
}
|
||||||
|
|
||||||
.reactions-bar__item:hover,
|
.reactions-bar__item:hover,
|
||||||
.reactions-bar__item:focus,
|
.reactions-bar__item:focus,
|
||||||
.reactions-bar__item:active {
|
.reactions-bar__item:active {
|
||||||
|
@ -85,6 +85,14 @@
|
|||||||
outline: $ui-button-icon-focus-outline;
|
outline: $ui-button-icon-focus-outline;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&--compact {
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
padding: 5px 12px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
&--dangerous {
|
&--dangerous {
|
||||||
background-color: var(--error-background-color);
|
background-color: var(--error-background-color);
|
||||||
color: var(--on-error-color);
|
color: var(--on-error-color);
|
||||||
@ -3734,58 +3742,6 @@ $ui-header-logo-wordmark-width: 99px;
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.setting-text {
|
|
||||||
display: block;
|
|
||||||
box-sizing: border-box;
|
|
||||||
margin: 0;
|
|
||||||
color: $primary-text-color;
|
|
||||||
background: $ui-base-color;
|
|
||||||
padding: 7px 10px;
|
|
||||||
font-family: inherit;
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 22px;
|
|
||||||
border-radius: 4px;
|
|
||||||
border: 1px solid var(--background-border-color);
|
|
||||||
|
|
||||||
&:focus {
|
|
||||||
outline: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__wrapper {
|
|
||||||
background: $ui-base-color;
|
|
||||||
border: 1px solid var(--background-border-color);
|
|
||||||
margin-bottom: 10px;
|
|
||||||
border-radius: 4px;
|
|
||||||
|
|
||||||
.setting-text {
|
|
||||||
border: 0;
|
|
||||||
margin-bottom: 0;
|
|
||||||
border-radius: 0;
|
|
||||||
|
|
||||||
&:focus {
|
|
||||||
border: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__modifiers {
|
|
||||||
color: $inverted-text-color;
|
|
||||||
font-family: inherit;
|
|
||||||
font-size: 14px;
|
|
||||||
background: $white;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__toolbar {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media screen and (width <= 600px) {
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-card {
|
.status-card {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@ -6104,6 +6060,35 @@ a.status-card {
|
|||||||
gap: 16px;
|
gap: 16px;
|
||||||
padding: 24px;
|
padding: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__preview {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 24px;
|
||||||
|
background: #000;
|
||||||
|
|
||||||
|
img {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
img,
|
||||||
|
.gifv video {
|
||||||
|
outline: 1px solid var(--media-outline-color);
|
||||||
|
outline-offset: -1px;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
img,
|
||||||
|
.gifv video,
|
||||||
|
.video-player,
|
||||||
|
.audio-player {
|
||||||
|
max-width: 360px;
|
||||||
|
max-height: 45vh;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.copy-paste-text {
|
.copy-paste-text {
|
||||||
@ -6450,62 +6435,6 @@ a.status-card {
|
|||||||
margin-bottom: 29px;
|
margin-bottom: 29px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.report-modal__comment {
|
|
||||||
padding: 20px;
|
|
||||||
border-inline-end: 1px solid var(--background-border-color);
|
|
||||||
max-width: 320px;
|
|
||||||
|
|
||||||
p {
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 20px;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.setting-text-label {
|
|
||||||
display: block;
|
|
||||||
color: $secondary-text-color;
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 500;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.setting-text {
|
|
||||||
width: 100%;
|
|
||||||
resize: none;
|
|
||||||
min-height: 100px;
|
|
||||||
max-height: 50vh;
|
|
||||||
border: 0;
|
|
||||||
|
|
||||||
@media screen and (height <= 600px) {
|
|
||||||
max-height: 20vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media screen and (max-width: $no-columns-breakpoint) {
|
|
||||||
max-height: 20vh;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.setting-toggle {
|
|
||||||
margin-top: 20px;
|
|
||||||
margin-bottom: 24px;
|
|
||||||
|
|
||||||
&__label {
|
|
||||||
color: $inverted-text-color;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media screen and (width <= 480px) {
|
|
||||||
padding: 10px;
|
|
||||||
max-width: 100%;
|
|
||||||
order: 2;
|
|
||||||
|
|
||||||
.setting-toggle {
|
|
||||||
margin-bottom: 4px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.actions-modal {
|
.actions-modal {
|
||||||
max-height: 80vh;
|
max-height: 80vh;
|
||||||
max-width: 80vw;
|
max-width: 80vw;
|
||||||
@ -6998,11 +6927,6 @@ a.status-card {
|
|||||||
outline: 1px solid var(--media-outline-color);
|
outline: 1px solid var(--media-outline-color);
|
||||||
outline-offset: -1px;
|
outline-offset: -1px;
|
||||||
|
|
||||||
&.editable {
|
|
||||||
border-radius: 0;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.inactive {
|
&.inactive {
|
||||||
audio,
|
audio,
|
||||||
.video-player__controls {
|
.video-player__controls {
|
||||||
@ -7071,11 +6995,6 @@ a.status-card {
|
|||||||
outline-offset: -1px;
|
outline-offset: -1px;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
|
|
||||||
&.editable {
|
|
||||||
border-radius: 0;
|
|
||||||
height: 100% !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
video {
|
video {
|
||||||
display: block;
|
display: block;
|
||||||
z-index: -2;
|
z-index: -2;
|
||||||
@ -7381,6 +7300,14 @@ a.status-card {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.gifv {
|
.gifv {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
canvas {
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
video {
|
video {
|
||||||
max-width: 100vw;
|
max-width: 100vw;
|
||||||
max-height: 80vh;
|
max-height: 80vh;
|
||||||
@ -7686,24 +7613,14 @@ noscript {
|
|||||||
|
|
||||||
.focal-point {
|
.focal-point {
|
||||||
position: relative;
|
position: relative;
|
||||||
cursor: move;
|
cursor: grab;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
height: 100%;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background: $base-shadow-color;
|
|
||||||
|
|
||||||
img,
|
&.dragging {
|
||||||
video,
|
cursor: grabbing;
|
||||||
canvas {
|
|
||||||
display: block;
|
|
||||||
max-height: 80vh;
|
|
||||||
width: 100%;
|
|
||||||
height: auto;
|
|
||||||
margin: 0;
|
|
||||||
object-fit: contain;
|
|
||||||
background: $base-shadow-color;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&__reticle {
|
&__reticle {
|
||||||
@ -7711,54 +7628,10 @@ noscript {
|
|||||||
width: 100px;
|
width: 100px;
|
||||||
height: 100px;
|
height: 100px;
|
||||||
transform: translate(-50%, -50%);
|
transform: translate(-50%, -50%);
|
||||||
background: url('~images/reticle.png') no-repeat 0 0;
|
border: 2px solid #fff;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
box-shadow: 0 0 0 9999em rgba($base-shadow-color, 0.35);
|
box-shadow: 0 0 0 9999em rgba($base-shadow-color, 0.35);
|
||||||
}
|
pointer-events: none;
|
||||||
|
|
||||||
&__overlay {
|
|
||||||
position: absolute;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
top: 0;
|
|
||||||
inset-inline-start: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__preview {
|
|
||||||
position: absolute;
|
|
||||||
bottom: 10px;
|
|
||||||
inset-inline-end: 10px;
|
|
||||||
z-index: 2;
|
|
||||||
cursor: move;
|
|
||||||
transition: opacity 0.1s ease;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
opacity: 0.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
strong {
|
|
||||||
color: $primary-text-color;
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 500;
|
|
||||||
display: block;
|
|
||||||
margin-bottom: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
div {
|
|
||||||
border-radius: 4px;
|
|
||||||
box-shadow: 0 0 14px rgba($base-shadow-color, 0.2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media screen and (width <= 480px) {
|
|
||||||
img,
|
|
||||||
video {
|
|
||||||
max-height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__preview {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -10468,12 +10341,7 @@ noscript {
|
|||||||
.compose-form__actions {
|
.compose-form__actions {
|
||||||
.button {
|
.button {
|
||||||
display: block; // Otherwise text-ellipsis doesn't work
|
display: block; // Otherwise text-ellipsis doesn't work
|
||||||
font-size: 14px;
|
|
||||||
line-height: normal;
|
|
||||||
font-weight: 700;
|
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
padding: 5px 12px;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -76,6 +76,18 @@ code {
|
|||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__toolbar {
|
||||||
|
margin-top: 16px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
&.hidden {
|
&.hidden {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
@ -540,6 +552,7 @@ code {
|
|||||||
.actions {
|
.actions {
|
||||||
margin-top: 30px;
|
margin-top: 30px;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
|
||||||
&.actions--top {
|
&.actions--top {
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
@ -552,9 +565,7 @@ code {
|
|||||||
margin-bottom: 15px;
|
margin-bottom: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
button,
|
button:not(.button, .link-button) {
|
||||||
.button,
|
|
||||||
.block-button {
|
|
||||||
display: block;
|
display: block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border: 0;
|
border: 0;
|
||||||
@ -629,6 +640,18 @@ code {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.label_input {
|
.label_input {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&__loading-indicator {
|
||||||
|
box-sizing: border-box;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
inset-inline-start: 0;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
padding: 10px 16px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
&__wrapper {
|
&__wrapper {
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
3
app/javascript/types/tesseract.d.ts
vendored
Normal file
3
app/javascript/types/tesseract.d.ts
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
declare module 'tesseract.js-core/tesseract-core.wasm.js';
|
||||||
|
|
||||||
|
declare module 'tesseract.js/dist/worker.min.js';
|
@ -34,6 +34,7 @@ class NotifyService < BaseService
|
|||||||
@sender = notification.from_account
|
@sender = notification.from_account
|
||||||
@notification = notification
|
@notification = notification
|
||||||
@policy = NotificationPolicy.find_or_initialize_by(account: @recipient)
|
@policy = NotificationPolicy.find_or_initialize_by(account: @recipient)
|
||||||
|
@from_staff = @sender.local? && @sender.user.present? && @sender.user_role&.bypass_block?(@recipient.user_role)
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
@ -63,6 +64,14 @@ class NotifyService < BaseService
|
|||||||
@sender.silenced? && not_following?
|
@sender.silenced? && not_following?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def message?
|
||||||
|
@notification.type == :mention
|
||||||
|
end
|
||||||
|
|
||||||
|
def from_staff?
|
||||||
|
@from_staff
|
||||||
|
end
|
||||||
|
|
||||||
def private_mention_not_in_response?
|
def private_mention_not_in_response?
|
||||||
@notification.type == :mention && @notification.target_status.direct_visibility? && !response_to_recipient?
|
@notification.type == :mention && @notification.target_status.direct_visibility? && !response_to_recipient?
|
||||||
end
|
end
|
||||||
@ -129,14 +138,6 @@ class NotifyService < BaseService
|
|||||||
FeedManager.instance.filter?(:mentions, @notification.target_status, @recipient)
|
FeedManager.instance.filter?(:mentions, @notification.target_status, @recipient)
|
||||||
end
|
end
|
||||||
|
|
||||||
def message?
|
|
||||||
@notification.type == :mention
|
|
||||||
end
|
|
||||||
|
|
||||||
def from_staff?
|
|
||||||
@sender.local? && @sender.user.present? && @sender.user_role&.bypass_block?(@recipient.user_role)
|
|
||||||
end
|
|
||||||
|
|
||||||
def from_self?
|
def from_self?
|
||||||
@recipient.id == @sender.id
|
@recipient.id == @sender.id
|
||||||
end
|
end
|
||||||
@ -174,6 +175,7 @@ class NotifyService < BaseService
|
|||||||
def filter?
|
def filter?
|
||||||
return false unless filterable_type?
|
return false unless filterable_type?
|
||||||
return false if override_for_sender?
|
return false if override_for_sender?
|
||||||
|
return false if message? && from_staff?
|
||||||
|
|
||||||
filtered_by_limited_accounts_policy? ||
|
filtered_by_limited_accounts_policy? ||
|
||||||
filtered_by_not_following_policy? ||
|
filtered_by_not_following_policy? ||
|
||||||
|
@ -39,7 +39,7 @@
|
|||||||
|
|
||||||
.actions
|
.actions
|
||||||
%button.button= t('admin.accounts.search')
|
%button.button= t('admin.accounts.search')
|
||||||
= link_to t('admin.accounts.reset'), admin_accounts_path, class: 'button negative'
|
= link_to t('admin.accounts.reset'), admin_accounts_path, class: 'button button--dangerous'
|
||||||
|
|
||||||
%hr.spacer/
|
%hr.spacer/
|
||||||
|
|
||||||
|
@ -35,7 +35,7 @@
|
|||||||
|
|
||||||
.actions
|
.actions
|
||||||
%button.button= t('admin.accounts.search')
|
%button.button= t('admin.accounts.search')
|
||||||
= link_to t('admin.accounts.reset'), admin_custom_emojis_path, class: 'button negative'
|
= link_to t('admin.accounts.reset'), admin_custom_emojis_path, class: 'button button--dangerous'
|
||||||
|
|
||||||
= form_with model: @form, url: batch_admin_custom_emojis_path do |f|
|
= form_with model: @form, url: batch_admin_custom_emojis_path do |f|
|
||||||
= hidden_field_tag :page, params[:page] || 1
|
= hidden_field_tag :page, params[:page] || 1
|
||||||
|
@ -19,4 +19,4 @@
|
|||||||
|
|
||||||
.actions
|
.actions
|
||||||
= link_to t('.cancel'), admin_instances_path, class: 'button button-tertiary'
|
= link_to t('.cancel'), admin_instances_path, class: 'button button-tertiary'
|
||||||
= f.button :submit, t('.confirm'), class: 'button negative', name: :confirm
|
= f.button :submit, t('.confirm'), class: 'button button--dangerous', name: :confirm
|
||||||
|
@ -42,7 +42,7 @@
|
|||||||
|
|
||||||
.actions
|
.actions
|
||||||
%button.button= t('admin.accounts.search')
|
%button.button= t('admin.accounts.search')
|
||||||
= link_to t('admin.accounts.reset'), admin_instances_path, class: 'button negative'
|
= link_to t('admin.accounts.reset'), admin_instances_path, class: 'button button--dangerous'
|
||||||
|
|
||||||
%hr.spacer/
|
%hr.spacer/
|
||||||
|
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
.simple_form
|
.simple_form
|
||||||
%p.hint= t('admin.relays.description_html')
|
%p.hint= t('admin.relays.description_html')
|
||||||
= link_to @relays.empty? ? t('admin.relays.setup') : t('admin.relays.add_new'), new_admin_relay_path, class: 'block-button'
|
= link_to @relays.empty? ? t('admin.relays.setup') : t('admin.relays.add_new'), new_admin_relay_path, class: 'button button--block'
|
||||||
|
|
||||||
- unless @relays.empty?
|
- unless @relays.empty?
|
||||||
%hr.spacer
|
%hr.spacer
|
||||||
|
@ -28,7 +28,7 @@
|
|||||||
|
|
||||||
.actions
|
.actions
|
||||||
%button.button= t('admin.accounts.search')
|
%button.button= t('admin.accounts.search')
|
||||||
= link_to t('admin.accounts.reset'), admin_reports_path, class: 'button negative'
|
= link_to t('admin.accounts.reset'), admin_reports_path, class: 'button button--dangerous'
|
||||||
|
|
||||||
- @reports.group_by(&:target_account_id).each_value do |reports|
|
- @reports.group_by(&:target_account_id).each_value do |reports|
|
||||||
- target_account = reports.first.target_account
|
- target_account = reports.first.target_account
|
||||||
|
@ -25,7 +25,7 @@
|
|||||||
|
|
||||||
.actions
|
.actions
|
||||||
%button.button= t('admin.tags.search')
|
%button.button= t('admin.tags.search')
|
||||||
= link_to t('admin.tags.reset'), admin_tags_path, class: 'button negative'
|
= link_to t('admin.tags.reset'), admin_tags_path, class: 'button button--dangerous'
|
||||||
|
|
||||||
%hr.spacer/
|
%hr.spacer/
|
||||||
|
|
||||||
|
@ -6,4 +6,4 @@
|
|||||||
|
|
||||||
%hr.spacer/
|
%hr.spacer/
|
||||||
|
|
||||||
= link_to t('otp_authentication.setup'), settings_otp_authentication_path, data: { method: :post }, class: 'block-button'
|
= link_to t('otp_authentication.setup'), settings_otp_authentication_path, data: { method: :post }, class: 'button button--block'
|
||||||
|
@ -14,4 +14,4 @@
|
|||||||
%hr.spacer/
|
%hr.spacer/
|
||||||
|
|
||||||
.simple_form
|
.simple_form
|
||||||
= link_to t('webauthn_credentials.add'), new_settings_webauthn_credential_path, class: 'block-button'
|
= link_to t('webauthn_credentials.add'), new_settings_webauthn_credential_path, class: 'button button--block'
|
||||||
|
@ -38,4 +38,4 @@
|
|||||||
%hr.spacer/
|
%hr.spacer/
|
||||||
|
|
||||||
.simple_form
|
.simple_form
|
||||||
= link_to t('two_factor_authentication.generate_recovery_codes'), settings_two_factor_authentication_recovery_codes_path, data: { method: :post }, class: 'block-button'
|
= link_to t('two_factor_authentication.generate_recovery_codes'), settings_two_factor_authentication_recovery_codes_path, data: { method: :post }, class: 'button button--block'
|
||||||
|
@ -1,9 +1,8 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
test: [
|
test: [
|
||||||
/tesseract\.js\/dist\/worker\.min\.js$/,
|
/tesseract\.js\/dist\/worker\.min\.js$/,
|
||||||
/tesseract\.js\/dist\/worker\.min\.js.map$/,
|
/tesseract\.js\/dist\/worker\.min\.js\.map$/,
|
||||||
/tesseract\.js-core\/tesseract-core\.wasm$/,
|
/tesseract\.js-core\/tesseract-core\.wasm\.js$/,
|
||||||
/tesseract\.js-core\/tesseract-core\.wasm.js$/,
|
|
||||||
],
|
],
|
||||||
use: {
|
use: {
|
||||||
loader: 'file-loader',
|
loader: 'file-loader',
|
||||||
|
@ -126,7 +126,7 @@
|
|||||||
"stringz": "^2.1.0",
|
"stringz": "^2.1.0",
|
||||||
"substring-trie": "^1.0.2",
|
"substring-trie": "^1.0.2",
|
||||||
"terser-webpack-plugin": "^4.2.3",
|
"terser-webpack-plugin": "^4.2.3",
|
||||||
"tesseract.js": "^2.1.5",
|
"tesseract.js": "^6.0.0",
|
||||||
"tiny-queue": "^0.2.1",
|
"tiny-queue": "^0.2.1",
|
||||||
"twitter-text": "3.1.0",
|
"twitter-text": "3.1.0",
|
||||||
"use-debounce": "^10.0.0",
|
"use-debounce": "^10.0.0",
|
||||||
|
@ -319,6 +319,16 @@ RSpec.describe NotifyService do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when sender is a moderator' do
|
||||||
|
let(:sender_role) { Fabricate(:user_role, highlighted: true, permissions: UserRole::FLAGS[:manage_users]) }
|
||||||
|
let(:sender) { Fabricate(:user, role: sender_role).account }
|
||||||
|
let(:activity) { Fabricate(:mention, status: Fabricate(:status, account: sender)) }
|
||||||
|
|
||||||
|
it 'returns false' do
|
||||||
|
expect(subject.filter?).to be false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
context 'when sender is followed by recipient' do
|
context 'when sender is followed by recipient' do
|
||||||
before do
|
before do
|
||||||
notification.account.follow!(notification.from_account)
|
notification.account.follow!(notification.from_account)
|
||||||
|
@ -26,8 +26,8 @@ RSpec.describe 'OCR', :attachment_processing, :inline_jobs, :js, :streaming do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
click_on('Detect text from picture')
|
click_on('Add text from image')
|
||||||
|
|
||||||
expect(page).to have_css('#upload-modal__description', text: /Hello Mastodon\s*/, wait: 10)
|
expect(page).to have_css('#description', text: /Hello Mastodon\s*/, wait: 10)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
121
yarn.lock
121
yarn.lock
@ -2961,7 +2961,7 @@ __metadata:
|
|||||||
stylelint-config-standard-scss: "npm:^14.0.0"
|
stylelint-config-standard-scss: "npm:^14.0.0"
|
||||||
substring-trie: "npm:^1.0.2"
|
substring-trie: "npm:^1.0.2"
|
||||||
terser-webpack-plugin: "npm:^4.2.3"
|
terser-webpack-plugin: "npm:^4.2.3"
|
||||||
tesseract.js: "npm:^2.1.5"
|
tesseract.js: "npm:^6.0.0"
|
||||||
tiny-queue: "npm:^0.2.1"
|
tiny-queue: "npm:^0.2.1"
|
||||||
twitter-text: "npm:3.1.0"
|
twitter-text: "npm:3.1.0"
|
||||||
typescript: "npm:^5.0.4"
|
typescript: "npm:^5.0.4"
|
||||||
@ -5590,13 +5590,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"blueimp-load-image@npm:^3.0.0":
|
|
||||||
version: 3.0.0
|
|
||||||
resolution: "blueimp-load-image@npm:3.0.0"
|
|
||||||
checksum: 10c0/e860da4113afd8e58bc026fb17240007e15dc155287a70fb57b3048fc8f0aa5f7dbd052efed8bff19d1208eeab4d058dc6788684a721c50ccd08b68d836a8d18
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"blurhash@npm:^2.0.5":
|
"blurhash@npm:^2.0.5":
|
||||||
version: 2.0.5
|
version: 2.0.5
|
||||||
resolution: "blurhash@npm:2.0.5"
|
resolution: "blurhash@npm:2.0.5"
|
||||||
@ -6334,13 +6327,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"colors@npm:^1.4.0":
|
|
||||||
version: 1.4.0
|
|
||||||
resolution: "colors@npm:1.4.0"
|
|
||||||
checksum: 10c0/9af357c019da3c5a098a301cf64e3799d27549d8f185d86f79af23069e4f4303110d115da98483519331f6fb71c8568d5688fa1c6523600044fd4a54e97c4efb
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"combined-stream@npm:^1.0.8":
|
"combined-stream@npm:^1.0.8":
|
||||||
version: 1.0.8
|
version: 1.0.8
|
||||||
resolution: "combined-stream@npm:1.0.8"
|
resolution: "combined-stream@npm:1.0.8"
|
||||||
@ -8690,13 +8676,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"file-type@npm:^12.4.1":
|
|
||||||
version: 12.4.2
|
|
||||||
resolution: "file-type@npm:12.4.2"
|
|
||||||
checksum: 10c0/26a307262a2a0b41ea83136550fbe83d8b502d080778b6577e0336fbfe9e919e1f871a286a6eb59f668425f60ebb19402fcb6c0443af58446d33c63362554e1d
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"file-uri-to-path@npm:1.0.0":
|
"file-uri-to-path@npm:1.0.0":
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
resolution: "file-uri-to-path@npm:1.0.0"
|
resolution: "file-uri-to-path@npm:1.0.0"
|
||||||
@ -9728,10 +9707,10 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"idb-keyval@npm:^3.2.0":
|
"idb-keyval@npm:^6.2.0":
|
||||||
version: 3.2.0
|
version: 6.2.1
|
||||||
resolution: "idb-keyval@npm:3.2.0"
|
resolution: "idb-keyval@npm:6.2.1"
|
||||||
checksum: 10c0/9b1f65d5f08630ef444a89334370c394175b1543f157621b36a3bc5e5208946f3f0ab5d5e24c74e81f2ef54b55b742b4e5b439c561f62695ffb69a06b0bce8e1
|
checksum: 10c0/9f0c83703a365e00bd0b4ed6380ce509a06dedfc6ec39b2ba5740085069fd2f2ff5c14ba19356488e3612a2f9c49985971982d836460a982a5d0b4019eeba48a
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
@ -10189,13 +10168,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"is-electron@npm:^2.2.0":
|
|
||||||
version: 2.2.2
|
|
||||||
resolution: "is-electron@npm:2.2.2"
|
|
||||||
checksum: 10c0/327bb373f7be01b16cdff3998b5ddaa87d28f576092affaa7fe0659571b3306fdd458afbf0683a66841e7999af13f46ad0e1b51647b469526cd05a4dd736438a
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"is-extendable@npm:^0.1.0, is-extendable@npm:^0.1.1":
|
"is-extendable@npm:^0.1.0, is-extendable@npm:^0.1.1":
|
||||||
version: 0.1.1
|
version: 0.1.1
|
||||||
resolution: "is-extendable@npm:0.1.1"
|
resolution: "is-extendable@npm:0.1.1"
|
||||||
@ -11169,28 +11141,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"jpeg-autorotate@npm:^7.1.1":
|
|
||||||
version: 7.1.1
|
|
||||||
resolution: "jpeg-autorotate@npm:7.1.1"
|
|
||||||
dependencies:
|
|
||||||
colors: "npm:^1.4.0"
|
|
||||||
glob: "npm:^7.1.6"
|
|
||||||
jpeg-js: "npm:^0.4.2"
|
|
||||||
piexifjs: "npm:^1.0.6"
|
|
||||||
yargs-parser: "npm:^20.2.1"
|
|
||||||
bin:
|
|
||||||
jpeg-autorotate: src/cli.js
|
|
||||||
checksum: 10c0/75328e15b7abcaf8b36c980495cb0b37ffabeb8921e8576312deac8139a9e8a66f85d9196f314120e4633c76623d1b595e65ca7a87b679511ffb804f880a1644
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"jpeg-js@npm:^0.4.2":
|
|
||||||
version: 0.4.4
|
|
||||||
resolution: "jpeg-js@npm:0.4.4"
|
|
||||||
checksum: 10c0/4d0d5097f8e55d8bbce6f1dc32ffaf3f43f321f6222e4e6490734fdc6d005322e3bd6fb992c2df7f5b587343b1441a1c333281dc3285bc9116e369fd2a2b43a7
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"js-tokens@npm:^3.0.0 || ^4.0.0, js-tokens@npm:^4.0.0":
|
"js-tokens@npm:^3.0.0 || ^4.0.0, js-tokens@npm:^4.0.0":
|
||||||
version: 4.0.0
|
version: 4.0.0
|
||||||
resolution: "js-tokens@npm:4.0.0"
|
resolution: "js-tokens@npm:4.0.0"
|
||||||
@ -12431,9 +12381,9 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"node-fetch@npm:^2.6.0":
|
"node-fetch@npm:^2.6.9":
|
||||||
version: 2.6.11
|
version: 2.7.0
|
||||||
resolution: "node-fetch@npm:2.6.11"
|
resolution: "node-fetch@npm:2.7.0"
|
||||||
dependencies:
|
dependencies:
|
||||||
whatwg-url: "npm:^5.0.0"
|
whatwg-url: "npm:^5.0.0"
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@ -12441,7 +12391,7 @@ __metadata:
|
|||||||
peerDependenciesMeta:
|
peerDependenciesMeta:
|
||||||
encoding:
|
encoding:
|
||||||
optional: true
|
optional: true
|
||||||
checksum: 10c0/3ec847ca43f678d07b80abfd85bdf06523c2554ee9a494c992c5fc61f5d9cde9f9f16aa33ff09a62f19eee9d54813b8850d7f054cdfee8b2daf789c57f8eeaea
|
checksum: 10c0/b55786b6028208e6fbe594ccccc213cab67a72899c9234eb59dba51062a299ea853210fcf526998eaa2867b0963ad72338824450905679ff0fa304b8c5093ae8
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
@ -12810,7 +12760,7 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"opencollective-postinstall@npm:^2.0.2":
|
"opencollective-postinstall@npm:^2.0.3":
|
||||||
version: 2.0.3
|
version: 2.0.3
|
||||||
resolution: "opencollective-postinstall@npm:2.0.3"
|
resolution: "opencollective-postinstall@npm:2.0.3"
|
||||||
bin:
|
bin:
|
||||||
@ -13293,13 +13243,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"piexifjs@npm:^1.0.6":
|
|
||||||
version: 1.0.6
|
|
||||||
resolution: "piexifjs@npm:1.0.6"
|
|
||||||
checksum: 10c0/69a10fe09c08f1e67e653844ac79e720324a7fa34689b020359d60d98b3a601c070e1759df8f2d97d022298bd2f5b79eed4c92de86c5f215300c8a63adf947b1
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"pify@npm:^2.0.0":
|
"pify@npm:^2.0.0":
|
||||||
version: 2.3.0
|
version: 2.3.0
|
||||||
resolution: "pify@npm:2.3.0"
|
resolution: "pify@npm:2.3.0"
|
||||||
@ -17124,31 +17067,27 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"tesseract.js-core@npm:^2.2.0":
|
"tesseract.js-core@npm:^6.0.0":
|
||||||
version: 2.2.0
|
version: 6.0.0
|
||||||
resolution: "tesseract.js-core@npm:2.2.0"
|
resolution: "tesseract.js-core@npm:6.0.0"
|
||||||
checksum: 10c0/9ef569529f1ee96f8bf18388ef086d4940d3f02d28b4252df133a9bd36f6a9d140085e77a12ff1963cf9b4cd85bd1c644b61eca266ecfc51bb83adb30a1f11e3
|
checksum: 10c0/c04be8bbaa296be658664496754f21e857bdffff84113f08adf02f03a1f84596d68b3542ed2fda4a6dc138abb84b09b30ab07c04ee5950879e780876d343955f
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"tesseract.js@npm:^2.1.5":
|
"tesseract.js@npm:^6.0.0":
|
||||||
version: 2.1.5
|
version: 6.0.0
|
||||||
resolution: "tesseract.js@npm:2.1.5"
|
resolution: "tesseract.js@npm:6.0.0"
|
||||||
dependencies:
|
dependencies:
|
||||||
blueimp-load-image: "npm:^3.0.0"
|
|
||||||
bmp-js: "npm:^0.1.0"
|
bmp-js: "npm:^0.1.0"
|
||||||
file-type: "npm:^12.4.1"
|
idb-keyval: "npm:^6.2.0"
|
||||||
idb-keyval: "npm:^3.2.0"
|
|
||||||
is-electron: "npm:^2.2.0"
|
|
||||||
is-url: "npm:^1.2.4"
|
is-url: "npm:^1.2.4"
|
||||||
jpeg-autorotate: "npm:^7.1.1"
|
node-fetch: "npm:^2.6.9"
|
||||||
node-fetch: "npm:^2.6.0"
|
opencollective-postinstall: "npm:^2.0.3"
|
||||||
opencollective-postinstall: "npm:^2.0.2"
|
|
||||||
regenerator-runtime: "npm:^0.13.3"
|
regenerator-runtime: "npm:^0.13.3"
|
||||||
resolve-url: "npm:^0.2.1"
|
tesseract.js-core: "npm:^6.0.0"
|
||||||
tesseract.js-core: "npm:^2.2.0"
|
wasm-feature-detect: "npm:^1.2.11"
|
||||||
zlibjs: "npm:^0.3.1"
|
zlibjs: "npm:^0.3.1"
|
||||||
checksum: 10c0/b3aaee9189f3bc7f4217b83e110d0dd4d9afcafc3045b842f72b7ca9beb00bec732bc6b4b00eca14167c16b014c437fcf83dd272a640c9c8b5e1e9b55ea00ff5
|
checksum: 10c0/f65b816eabc16266bfa74ea61db73afa2d21ce0f57041b87b96abdff8954e042ee16637edea20aaf752227bc075052ca12021f4f68d5d25d52f062ebc4c644e1
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
@ -17994,6 +17933,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"wasm-feature-detect@npm:^1.2.11":
|
||||||
|
version: 1.8.0
|
||||||
|
resolution: "wasm-feature-detect@npm:1.8.0"
|
||||||
|
checksum: 10c0/2cb43e91bbf7aa7c121bc76b3133de3ab6dc4f482acc1d2dc46c528e8adb7a51c72df5c2aacf1d219f113c04efd1706f18274d5790542aa5dd49e0644e3ee665
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"watchpack-chokidar2@npm:^2.0.1":
|
"watchpack-chokidar2@npm:^2.0.1":
|
||||||
version: 2.0.1
|
version: 2.0.1
|
||||||
resolution: "watchpack-chokidar2@npm:2.0.1"
|
resolution: "watchpack-chokidar2@npm:2.0.1"
|
||||||
@ -18846,13 +18792,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"yargs-parser@npm:^20.2.1":
|
|
||||||
version: 20.2.9
|
|
||||||
resolution: "yargs-parser@npm:20.2.9"
|
|
||||||
checksum: 10c0/0685a8e58bbfb57fab6aefe03c6da904a59769bd803a722bb098bd5b0f29d274a1357762c7258fb487512811b8063fb5d2824a3415a0a4540598335b3b086c72
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"yargs-parser@npm:^21.1.1":
|
"yargs-parser@npm:^21.1.1":
|
||||||
version: 21.1.1
|
version: 21.1.1
|
||||||
resolution: "yargs-parser@npm:21.1.1"
|
resolution: "yargs-parser@npm:21.1.1"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user