mirror of
https://github.com/glitch-soc/mastodon.git
synced 2025-02-03 05:23:18 -05:00
[Glitch] Change design of edit media modal in web UI
Port 11786f1114f9dfb5a17810d14477a94162f94064 to glitch-soc Signed-off-by: Claire <claire.github-309c@sitedethib.com>
This commit is contained in:
parent
05e5db91c7
commit
5e7c079787
@ -448,7 +448,7 @@ export function initMediaEditModal(id) {
|
||||
|
||||
dispatch(openModal({
|
||||
modalType: 'FOCAL_POINT',
|
||||
modalProps: { id },
|
||||
modalProps: { mediaId: id },
|
||||
}));
|
||||
};
|
||||
}
|
||||
|
70
app/javascript/flavours/glitch/actions/compose_typed.ts
Normal file
70
app/javascript/flavours/glitch/actions/compose_typed.ts
Normal file
@ -0,0 +1,70 @@
|
||||
import type { List as ImmutableList, Map as ImmutableMap } from 'immutable';
|
||||
|
||||
import { apiUpdateMedia } from 'flavours/glitch/api/compose';
|
||||
import type { ApiMediaAttachmentJSON } from 'flavours/glitch/api_types/media_attachments';
|
||||
import type { MediaAttachment } from 'flavours/glitch/models/media_attachment';
|
||||
import { createDataLoadingThunk } from 'flavours/glitch/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 {
|
||||
modalType: ModalType;
|
||||
modalProps: ModalProps;
|
||||
previousModalProps?: ModalProps;
|
||||
}
|
||||
export const openModal = createAction<OpenModalPayload>('MODAL_OPEN');
|
||||
|
||||
|
7
app/javascript/flavours/glitch/api/compose.ts
Normal file
7
app/javascript/flavours/glitch/api/compose.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import { apiRequestPut } from 'flavours/glitch/api';
|
||||
import type { ApiMediaAttachmentJSON } from 'flavours/glitch/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'> {
|
||||
block?: boolean;
|
||||
secondary?: boolean;
|
||||
compact?: boolean;
|
||||
dangerous?: boolean;
|
||||
}
|
||||
|
||||
@ -27,6 +28,7 @@ export const Button: React.FC<Props> = ({
|
||||
disabled,
|
||||
block,
|
||||
secondary,
|
||||
compact,
|
||||
dangerous,
|
||||
className,
|
||||
title,
|
||||
@ -47,6 +49,7 @@ export const Button: React.FC<Props> = ({
|
||||
<button
|
||||
className={classNames('button', className, {
|
||||
'button-secondary': secondary,
|
||||
'button--compact': compact,
|
||||
'button--block': block,
|
||||
'button--dangerous': dangerous,
|
||||
})}
|
||||
|
@ -2,6 +2,8 @@ import { useCallback, useEffect } from 'react';
|
||||
|
||||
import { useIntl, defineMessages } from 'react-intl';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { useIdentity } from '@/flavours/glitch/identity_context';
|
||||
import {
|
||||
fetchRelationships,
|
||||
@ -22,7 +24,8 @@ const messages = defineMessages({
|
||||
|
||||
export const FollowButton: React.FC<{
|
||||
accountId?: string;
|
||||
}> = ({ accountId }) => {
|
||||
compact?: boolean;
|
||||
}> = ({ accountId, compact }) => {
|
||||
const intl = useIntl();
|
||||
const dispatch = useAppDispatch();
|
||||
const { signedIn } = useIdentity();
|
||||
@ -89,7 +92,9 @@ export const FollowButton: React.FC<{
|
||||
href='/settings/profile'
|
||||
target='_blank'
|
||||
rel='noopener'
|
||||
className='button button-secondary'
|
||||
className={classNames('button button-secondary', {
|
||||
'button--compact': compact,
|
||||
})}
|
||||
>
|
||||
{label}
|
||||
</a>
|
||||
@ -106,6 +111,7 @@ export const FollowButton: React.FC<{
|
||||
(account?.suspended || !!account?.moved))
|
||||
}
|
||||
secondary={following}
|
||||
compact={compact}
|
||||
className={following ? 'button--destructive' : undefined}
|
||||
>
|
||||
{label}
|
||||
|
@ -1,46 +1,39 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
import { useCallback, useState, forwardRef } from 'react';
|
||||
|
||||
interface Props {
|
||||
src: string;
|
||||
key: string;
|
||||
alt?: string;
|
||||
lang?: string;
|
||||
width: number;
|
||||
height: number;
|
||||
onClick?: () => void;
|
||||
width?: number;
|
||||
height?: number;
|
||||
onClick?: React.MouseEventHandler;
|
||||
onMouseDown?: React.MouseEventHandler;
|
||||
onTouchStart?: React.TouchEventHandler;
|
||||
}
|
||||
|
||||
export const GIFV: React.FC<Props> = ({
|
||||
src,
|
||||
alt,
|
||||
lang,
|
||||
width,
|
||||
height,
|
||||
onClick,
|
||||
}) => {
|
||||
export const GIFV = forwardRef<HTMLVideoElement, Props>(
|
||||
(
|
||||
{ src, alt, lang, width, height, onClick, onMouseDown, onTouchStart },
|
||||
ref,
|
||||
) => {
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
const handleLoadedData: React.ReactEventHandler<HTMLVideoElement> =
|
||||
useCallback(() => {
|
||||
const handleLoadedData = useCallback(() => {
|
||||
setLoading(false);
|
||||
}, [setLoading]);
|
||||
|
||||
const handleClick: React.MouseEventHandler = useCallback(
|
||||
(e) => {
|
||||
if (onClick) {
|
||||
const handleClick = useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
onClick();
|
||||
}
|
||||
onClick?.(e);
|
||||
},
|
||||
[onClick],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className='gifv' style={{ position: 'relative' }}>
|
||||
<div className='gifv'>
|
||||
{loading && (
|
||||
<canvas
|
||||
width={width}
|
||||
height={height}
|
||||
role='button'
|
||||
tabIndex={0}
|
||||
aria-label={alt}
|
||||
@ -51,20 +44,27 @@ export const GIFV: React.FC<Props> = ({
|
||||
)}
|
||||
|
||||
<video
|
||||
ref={ref}
|
||||
src={src}
|
||||
role='button'
|
||||
tabIndex={0}
|
||||
aria-label={alt}
|
||||
title={alt}
|
||||
lang={lang}
|
||||
width={width}
|
||||
height={height}
|
||||
muted
|
||||
loop
|
||||
autoPlay
|
||||
playsInline
|
||||
onClick={handleClick}
|
||||
onLoadedData={handleLoadedData}
|
||||
style={{ position: loading ? 'absolute' : 'static', top: 0, left: 0 }}
|
||||
onMouseDown={onMouseDown}
|
||||
onTouchStart={onTouchStart}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
GIFV.displayName = 'GIFV';
|
||||
|
531
app/javascript/flavours/glitch/features/alt_text_modal/index.tsx
Normal file
531
app/javascript/flavours/glitch/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 'flavours/glitch/actions/alerts';
|
||||
import { uploadThumbnail } from 'flavours/glitch/actions/compose';
|
||||
import { changeUploadCompose } from 'flavours/glitch/actions/compose_typed';
|
||||
import { Button } from 'flavours/glitch/components/button';
|
||||
import { GIFV } from 'flavours/glitch/components/gifv';
|
||||
import { LoadingIndicator } from 'flavours/glitch/components/loading_indicator';
|
||||
import { Skeleton } from 'flavours/glitch/components/skeleton';
|
||||
import Audio from 'flavours/glitch/features/audio';
|
||||
import { CharacterCounter } from 'flavours/glitch/features/compose/components/character_counter';
|
||||
import { Tesseract as fetchTesseract } from 'flavours/glitch/features/ui/util/async-components';
|
||||
import Video, { getPointerPosition } from 'flavours/glitch/features/video';
|
||||
import { me } from 'flavours/glitch/initial_state';
|
||||
import type { MediaAttachment } from 'flavours/glitch/models/media_attachment';
|
||||
import { useAppSelector, useAppDispatch } from 'flavours/glitch/store';
|
||||
import { assetHost } from 'flavours/glitch/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';
|
@ -587,10 +587,14 @@ class Audio extends PureComponent {
|
||||
</div>
|
||||
|
||||
<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 && (
|
||||
<>
|
||||
<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 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} />
|
||||
<Icon id='download' icon={DownloadIcon} />
|
||||
</a>
|
||||
</>
|
||||
)}
|
||||
</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>;
|
||||
};
|
@ -335,6 +335,7 @@ class ComposeForm extends ImmutablePureComponent {
|
||||
/>
|
||||
<Button
|
||||
type='submit'
|
||||
compact
|
||||
text={intl.formatMessage(this.props.isEditing ? messages.saveChanges : (this.props.isInReply ? messages.reply : messages.publish))}
|
||||
disabled={!this.canSubmit()}
|
||||
/>
|
||||
|
@ -4,16 +4,16 @@ import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import type { Map as ImmutableMap, List as ImmutableList } from 'immutable';
|
||||
|
||||
import { useSortable } from '@dnd-kit/sortable';
|
||||
import { CSS } from '@dnd-kit/utilities';
|
||||
|
||||
import CloseIcon from '@/material-icons/400-20px/close.svg?react';
|
||||
import EditIcon from '@/material-icons/400-24px/edit.svg?react';
|
||||
import WarningIcon from '@/material-icons/400-24px/warning.svg?react';
|
||||
import {
|
||||
undoUploadCompose,
|
||||
initMediaEditModal,
|
||||
} from 'flavours/glitch/actions/compose';
|
||||
import { undoUploadCompose } from 'flavours/glitch/actions/compose';
|
||||
import { openModal } from 'flavours/glitch/actions/modal';
|
||||
import { Blurhash } from 'flavours/glitch/components/blurhash';
|
||||
import { Icon } from 'flavours/glitch/components/icon';
|
||||
import type { MediaAttachment } from 'flavours/glitch/models/media_attachment';
|
||||
@ -27,16 +27,15 @@ export const Upload: React.FC<{
|
||||
wide?: boolean;
|
||||
}> = ({ id, dragging, overlay, tall, wide }) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const media = useAppSelector(
|
||||
(state) =>
|
||||
state.compose // eslint-disable-line @typescript-eslint/no-unsafe-call
|
||||
.get('media_attachments') // eslint-disable-line @typescript-eslint/no-unsafe-member-access
|
||||
.find((item: MediaAttachment) => item.get('id') === id) as // eslint-disable-line @typescript-eslint/no-unsafe-member-access
|
||||
| MediaAttachment
|
||||
| undefined,
|
||||
const media = useAppSelector((state) =>
|
||||
(
|
||||
(state.compose as ImmutableMap<string, unknown>).get(
|
||||
'media_attachments',
|
||||
) as ImmutableList<MediaAttachment>
|
||||
).find((item) => item.get('id') === id),
|
||||
);
|
||||
const sensitive = useAppSelector(
|
||||
(state) => state.compose.get('sensitive') as boolean, // eslint-disable-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
|
||||
(state) => state.compose.get('sensitive') as boolean,
|
||||
);
|
||||
|
||||
const handleUndoClick = useCallback(() => {
|
||||
@ -44,7 +43,9 @@ export const Upload: React.FC<{
|
||||
}, [dispatch, id]);
|
||||
|
||||
const handleFocalPointClick = useCallback(() => {
|
||||
dispatch(initMediaEditModal(id));
|
||||
dispatch(
|
||||
openModal({ modalType: 'FOCAL_POINT', modalProps: { mediaId: id } }),
|
||||
);
|
||||
}, [dispatch, id]);
|
||||
|
||||
const { attributes, listeners, setNodeRef, transform, transition } =
|
||||
|
@ -2,7 +2,11 @@ import { useState, useCallback, useMemo } from 'react';
|
||||
|
||||
import { useIntl, defineMessages } from 'react-intl';
|
||||
|
||||
import type { List } from 'immutable';
|
||||
import type {
|
||||
List,
|
||||
Map as ImmutableMap,
|
||||
List as ImmutableList,
|
||||
} from 'immutable';
|
||||
|
||||
import type {
|
||||
DragStartEvent,
|
||||
@ -64,18 +68,20 @@ export const UploadForm: React.FC = () => {
|
||||
const intl = useIntl();
|
||||
const mediaIds = useAppSelector(
|
||||
(state) =>
|
||||
state.compose // eslint-disable-line @typescript-eslint/no-unsafe-call
|
||||
.get('media_attachments') // eslint-disable-line @typescript-eslint/no-unsafe-member-access
|
||||
.map((item: MediaAttachment) => item.get('id')) as List<string>, // eslint-disable-line @typescript-eslint/no-unsafe-member-access
|
||||
(
|
||||
(state.compose as ImmutableMap<string, unknown>).get(
|
||||
'media_attachments',
|
||||
) as ImmutableList<MediaAttachment>
|
||||
).map((item: MediaAttachment) => item.get('id')) as List<string>,
|
||||
);
|
||||
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(
|
||||
(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(
|
||||
(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 sensors = useSensors(
|
||||
|
@ -65,7 +65,9 @@ export const NotificationFollow: React.FC<{
|
||||
const account = notification.sampleAccountIds[0];
|
||||
|
||||
if (account) {
|
||||
actions = <FollowButton accountId={notification.sampleAccountIds[0]} />;
|
||||
actions = (
|
||||
<FollowButton compact accountId={notification.sampleAccountIds[0]} />
|
||||
);
|
||||
additionalContent = <FollowerCount accountId={account} />;
|
||||
}
|
||||
}
|
||||
|
@ -29,7 +29,6 @@ export const BoostModal: React.FC<{
|
||||
const intl = useIntl();
|
||||
|
||||
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,
|
||||
);
|
||||
|
||||
|
@ -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 'flavours/glitch/components/button';
|
||||
import { GIFV } from 'flavours/glitch/components/gifv';
|
||||
import { IconButton } from 'flavours/glitch/components/icon_button';
|
||||
import Audio from 'flavours/glitch/features/audio';
|
||||
import { CharacterCounter } from 'flavours/glitch/features/compose/components/character_counter';
|
||||
import { UploadProgress } from 'flavours/glitch/features/compose/components/upload_progress';
|
||||
import { Tesseract as fetchTesseract } from 'flavours/glitch/features/ui/util/async-components';
|
||||
import { me } from 'flavours/glitch/initial_state';
|
||||
import { assetHost } from 'flavours/glitch/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 Base from 'flavours/glitch/components/modal_root';
|
||||
import { AltTextModal } from 'flavours/glitch/features/alt_text_modal';
|
||||
import {
|
||||
MuteModal,
|
||||
BlockModal,
|
||||
@ -41,7 +42,6 @@ import {
|
||||
import DeprecatedSettingsModal from './deprecated_settings_modal';
|
||||
import DoodleModal from './doodle_modal';
|
||||
import { FavouriteModal } from './favourite_modal';
|
||||
import FocalPointModal from './focal_point_modal';
|
||||
import ImageModal from './image_modal';
|
||||
import MediaModal from './media_modal';
|
||||
import { ModalPlaceholder } from './modal_placeholder';
|
||||
@ -72,7 +72,7 @@ export const MODAL_COMPONENTS = {
|
||||
'DEPRECATED_SETTINGS': () => Promise.resolve({ default: DeprecatedSettingsModal }),
|
||||
'ACTIONS': () => Promise.resolve({ default: ActionsModal }),
|
||||
'EMBED': EmbedModal,
|
||||
'FOCAL_POINT': () => Promise.resolve({ default: FocalPointModal }),
|
||||
'FOCAL_POINT': () => Promise.resolve({ default: AltTextModal }),
|
||||
'LIST_ADDER': ListAdder,
|
||||
'COMPARE_HISTORY': CompareHistoryModal,
|
||||
'FILTER': FilterModal,
|
||||
@ -146,8 +146,7 @@ export default class ModalRoot extends PureComponent {
|
||||
<>
|
||||
<BundleContainer fetchComponent={MODAL_COMPONENTS[type]} loading={this.renderLoading} error={this.renderError} renderDelay={200}>
|
||||
{(SpecificComponent) => {
|
||||
const ref = typeof SpecificComponent !== 'function' ? this.setModalRef : undefined;
|
||||
return <SpecificComponent {...props} onChangeBackgroundColor={this.setBackgroundColor} onClose={this.handleClose} ref={ref} />;
|
||||
return <SpecificComponent {...props} onChangeBackgroundColor={this.setBackgroundColor} onClose={this.handleClose} ref={this.setModalRef} />;
|
||||
}}
|
||||
</BundleContainer>
|
||||
|
||||
|
@ -16,6 +16,7 @@ const mapDispatchToProps = dispatch => ({
|
||||
if (confirmationMessage) {
|
||||
dispatch(
|
||||
openModal({
|
||||
previousModalProps: confirmationMessage.props,
|
||||
modalType: 'CONFIRM',
|
||||
modalProps: {
|
||||
message: confirmationMessage.message,
|
||||
@ -24,7 +25,8 @@ const mapDispatchToProps = dispatch => ({
|
||||
modalType: undefined,
|
||||
ignoreFocus: { ignoreFocus },
|
||||
})),
|
||||
} }),
|
||||
},
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
dispatch(closeModal({
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { Map as ImmutableMap, List as ImmutableList, OrderedSet as ImmutableOrderedSet, fromJS } from 'immutable';
|
||||
|
||||
import { changeUploadCompose } from 'flavours/glitch/actions/compose_typed';
|
||||
import { timelineDelete } from 'flavours/glitch/actions/timelines_typed';
|
||||
|
||||
import {
|
||||
@ -38,18 +39,12 @@ import {
|
||||
COMPOSE_COMPOSING_CHANGE,
|
||||
COMPOSE_CONTENT_TYPE_CHANGE,
|
||||
COMPOSE_EMOJI_INSERT,
|
||||
COMPOSE_UPLOAD_CHANGE_REQUEST,
|
||||
COMPOSE_UPLOAD_CHANGE_SUCCESS,
|
||||
COMPOSE_UPLOAD_CHANGE_FAIL,
|
||||
COMPOSE_DOODLE_SET,
|
||||
COMPOSE_RESET,
|
||||
COMPOSE_POLL_ADD,
|
||||
COMPOSE_POLL_REMOVE,
|
||||
COMPOSE_POLL_OPTION_CHANGE,
|
||||
COMPOSE_POLL_SETTINGS_CHANGE,
|
||||
INIT_MEDIA_EDIT_MODAL,
|
||||
COMPOSE_CHANGE_MEDIA_DESCRIPTION,
|
||||
COMPOSE_CHANGE_MEDIA_FOCUS,
|
||||
COMPOSE_CHANGE_MEDIA_ORDER,
|
||||
COMPOSE_SET_STATUS,
|
||||
COMPOSE_FOCUS,
|
||||
@ -102,13 +97,6 @@ const initialState = ImmutableMap({
|
||||
resetFileKey: Math.floor((Math.random() * 0x10000)),
|
||||
idempotencyKey: null,
|
||||
tagHistory: ImmutableList(),
|
||||
media_modal: ImmutableMap({
|
||||
id: null,
|
||||
description: '',
|
||||
focusX: 0,
|
||||
focusY: 0,
|
||||
dirty: false,
|
||||
}),
|
||||
doodle: ImmutableMap({
|
||||
fg: 'rgb( 0, 0, 0)',
|
||||
bg: 'rgb(255, 255, 255)',
|
||||
@ -370,7 +358,24 @@ const updatePoll = (state, index, value, maxOptions) => state.updateIn(['poll',
|
||||
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) {
|
||||
case STORE_HYDRATE:
|
||||
return hydrate(state, action.state.get('compose'));
|
||||
@ -476,14 +481,10 @@ export default function compose(state = initialState, action) {
|
||||
});
|
||||
case COMPOSE_SUBMIT_REQUEST:
|
||||
return state.set('is_submitting', true);
|
||||
case COMPOSE_UPLOAD_CHANGE_REQUEST:
|
||||
return state.set('is_changing_upload', true);
|
||||
case COMPOSE_SUBMIT_SUCCESS:
|
||||
return action.status && state.getIn(['advanced_options', 'threaded_mode']) ? continueThread(state, action.status) : clearAll(state);
|
||||
case COMPOSE_SUBMIT_FAIL:
|
||||
return state.set('is_submitting', false);
|
||||
case COMPOSE_UPLOAD_CHANGE_FAIL:
|
||||
return state.set('is_changing_upload', false);
|
||||
case COMPOSE_UPLOAD_REQUEST:
|
||||
return state.set('is_uploading', true).update('pending_media_attachments', n => n + 1);
|
||||
case COMPOSE_UPLOAD_PROCESSING:
|
||||
@ -512,20 +513,6 @@ export default function compose(state = initialState, action) {
|
||||
|
||||
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:
|
||||
return state.withMutations(map => {
|
||||
map.update('text', text => [text.trim(), `@${action.account.get('acct')} `].filter((str) => str.length !== 0).join(' '));
|
||||
@ -563,17 +550,6 @@ export default function compose(state = initialState, action) {
|
||||
}
|
||||
case COMPOSE_EMOJI_INSERT:
|
||||
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 COMPOSE_DOODLE_SET:
|
||||
return state.mergeIn(['doodle'], action.options);
|
||||
case REDRAFT: {
|
||||
@ -671,4 +647,4 @@ export default function compose(state = initialState, action) {
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -7,7 +7,7 @@ import { accountsReducer } from './accounts';
|
||||
import accounts_map from './accounts_map';
|
||||
import alerts from './alerts';
|
||||
import announcements from './announcements';
|
||||
import compose from './compose';
|
||||
import { composeReducer } from './compose';
|
||||
import contexts from './contexts';
|
||||
import conversations from './conversations';
|
||||
import custom_emojis from './custom_emojis';
|
||||
@ -61,7 +61,7 @@ const reducers = {
|
||||
push_notifications,
|
||||
server,
|
||||
contexts,
|
||||
compose,
|
||||
compose: composeReducer,
|
||||
search: searchReducer,
|
||||
media_attachments,
|
||||
notifications,
|
||||
|
@ -3,7 +3,6 @@ import { Record as ImmutableRecord, Stack } from 'immutable';
|
||||
|
||||
import { timelineDelete } from 'flavours/glitch/actions/timelines_typed';
|
||||
|
||||
import { COMPOSE_UPLOAD_CHANGE_SUCCESS } from '../actions/compose';
|
||||
import type { ModalType } from '../actions/modal';
|
||||
import { openModal, closeModal } from '../actions/modal';
|
||||
|
||||
@ -53,12 +52,36 @@ const pushModal = (
|
||||
state: State,
|
||||
modalType: ModalType,
|
||||
modalProps: ModalProps,
|
||||
previousModalProps?: ModalProps,
|
||||
): State => {
|
||||
return state.withMutations((record) => {
|
||||
record.set('ignoreFocus', false);
|
||||
record.update('stack', (stack) =>
|
||||
stack.unshift(Modal({ modalType, modalProps })),
|
||||
record.update('stack', (stack) => {
|
||||
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,
|
||||
action.payload.modalType,
|
||||
action.payload.modalProps,
|
||||
action.payload.previousModalProps,
|
||||
);
|
||||
else if (closeModal.match(action)) return popModal(state, action.payload);
|
||||
// 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))
|
||||
return state.update('stack', (stack) =>
|
||||
stack.filterNot(
|
||||
|
@ -85,6 +85,14 @@
|
||||
outline: $ui-button-icon-focus-outline;
|
||||
}
|
||||
|
||||
&--compact {
|
||||
font-size: 14px;
|
||||
line-height: normal;
|
||||
font-weight: 700;
|
||||
padding: 5px 12px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
&--dangerous {
|
||||
background-color: var(--error-background-color);
|
||||
color: var(--on-error-color);
|
||||
@ -3811,58 +3819,6 @@ input.glitch-setting-text {
|
||||
}
|
||||
}
|
||||
|
||||
.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 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@ -6442,6 +6398,35 @@ a.status-card {
|
||||
gap: 16px;
|
||||
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 {
|
||||
@ -6790,62 +6775,6 @@ a.status-card {
|
||||
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 {
|
||||
max-height: 80vh;
|
||||
max-width: 80vw;
|
||||
@ -7388,11 +7317,6 @@ img.modal-warning {
|
||||
outline: 1px solid var(--media-outline-color);
|
||||
outline-offset: -1px;
|
||||
|
||||
&.editable {
|
||||
border-radius: 0;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
&.inactive {
|
||||
audio,
|
||||
.video-player__controls {
|
||||
@ -7461,11 +7385,6 @@ img.modal-warning {
|
||||
outline-offset: -1px;
|
||||
z-index: 2;
|
||||
|
||||
&.editable {
|
||||
border-radius: 0;
|
||||
height: 100% !important;
|
||||
}
|
||||
|
||||
.detailed-status & {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
@ -7788,6 +7707,14 @@ img.modal-warning {
|
||||
}
|
||||
|
||||
.gifv {
|
||||
position: relative;
|
||||
|
||||
canvas {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
video {
|
||||
max-width: 100vw;
|
||||
max-height: 80vh;
|
||||
@ -8091,24 +8018,14 @@ noscript {
|
||||
|
||||
.focal-point {
|
||||
position: relative;
|
||||
cursor: move;
|
||||
cursor: grab;
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background: $base-shadow-color;
|
||||
|
||||
img,
|
||||
video,
|
||||
canvas {
|
||||
display: block;
|
||||
max-height: 80vh;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
margin: 0;
|
||||
object-fit: contain;
|
||||
background: $base-shadow-color;
|
||||
&.dragging {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
&__reticle {
|
||||
@ -8116,54 +8033,10 @@ noscript {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
transform: translate(-50%, -50%);
|
||||
background: url('~images/reticle.png') no-repeat 0 0;
|
||||
border: 2px solid #fff;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 0 0 9999em rgba($base-shadow-color, 0.35);
|
||||
}
|
||||
|
||||
&__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;
|
||||
}
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
@ -10885,12 +10758,7 @@ noscript {
|
||||
.compose-form__actions {
|
||||
.button {
|
||||
display: block; // Otherwise text-ellipsis doesn't work
|
||||
font-size: 14px;
|
||||
line-height: normal;
|
||||
font-weight: 700;
|
||||
flex: 1 1 auto;
|
||||
padding: 5px 12px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -76,6 +76,18 @@ code {
|
||||
margin-bottom: 16px;
|
||||
overflow: hidden;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
&__toolbar {
|
||||
margin-top: 16px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
&.hidden {
|
||||
margin: 0;
|
||||
}
|
||||
@ -541,6 +553,7 @@ code {
|
||||
.actions {
|
||||
margin-top: 30px;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
|
||||
&.actions--top {
|
||||
margin-top: 0;
|
||||
@ -553,9 +566,7 @@ code {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
button,
|
||||
.button,
|
||||
.block-button {
|
||||
button:not(.button, .link-button) {
|
||||
display: block;
|
||||
width: 100%;
|
||||
border: 0;
|
||||
@ -630,6 +641,18 @@ code {
|
||||
}
|
||||
|
||||
.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 {
|
||||
position: relative;
|
||||
}
|
||||
|
@ -127,9 +127,7 @@
|
||||
.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 button,
|
||||
.simple_form .block-button,
|
||||
.simple_form .button,
|
||||
.simple_form button {
|
||||
.simple_form button:not(.button, .link-button) {
|
||||
color: $white;
|
||||
}
|
||||
|
||||
@ -142,6 +140,11 @@
|
||||
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:focus,
|
||||
.reactions-bar__item:active {
|
||||
|
Loading…
x
Reference in New Issue
Block a user