-
-
- {' '}
-
-
+
{mentionsPlaceholder}
diff --git a/app/javascript/flavours/glitch/components/status_prepend.jsx b/app/javascript/flavours/glitch/components/status_prepend.jsx
index b93aba5a2a..501a60e270 100644
--- a/app/javascript/flavours/glitch/components/status_prepend.jsx
+++ b/app/javascript/flavours/glitch/components/status_prepend.jsx
@@ -66,7 +66,7 @@ export default class StatusPrepend extends PureComponent {
return (
);
@@ -82,7 +82,7 @@ export default class StatusPrepend extends PureComponent {
return (
);
@@ -162,11 +162,13 @@ export default class StatusPrepend extends PureComponent {
return !type ? null : (
diff --git a/app/javascript/flavours/glitch/components/status_reactions.jsx b/app/javascript/flavours/glitch/components/status_reactions.jsx
index 81443d2055..d750b5f233 100644
--- a/app/javascript/flavours/glitch/components/status_reactions.jsx
+++ b/app/javascript/flavours/glitch/components/status_reactions.jsx
@@ -21,9 +21,9 @@ export default class StatusReactions extends ImmutablePureComponent {
statusId: PropTypes.string.isRequired,
reactions: ImmutablePropTypes.list.isRequired,
numVisible: PropTypes.number,
- addReaction: PropTypes.func.isRequired,
+ addReaction: PropTypes.func,
canReact: PropTypes.bool.isRequired,
- removeReaction: PropTypes.func.isRequired,
+ removeReaction: PropTypes.func,
};
willEnter() {
@@ -78,8 +78,8 @@ class Reaction extends ImmutablePureComponent {
static propTypes = {
statusId: PropTypes.string,
reaction: ImmutablePropTypes.map.isRequired,
- addReaction: PropTypes.func.isRequired,
- removeReaction: PropTypes.func.isRequired,
+ addReaction: PropTypes.func,
+ removeReaction: PropTypes.func,
canReact: PropTypes.bool.isRequired,
style: PropTypes.object,
};
@@ -91,9 +91,9 @@ class Reaction extends ImmutablePureComponent {
handleClick = () => {
const { reaction, statusId, addReaction, removeReaction } = this.props;
- if (reaction.get('me')) {
+ if (reaction.get('me') && removeReaction) {
removeReaction(statusId, reaction.get('name'));
- } else {
+ } else if (addReaction) {
addReaction(statusId, reaction.get('name'));
}
};
diff --git a/app/javascript/flavours/glitch/components/status_thread_label.tsx b/app/javascript/flavours/glitch/components/status_thread_label.tsx
new file mode 100644
index 0000000000..054156efaa
--- /dev/null
+++ b/app/javascript/flavours/glitch/components/status_thread_label.tsx
@@ -0,0 +1,50 @@
+import { FormattedMessage } from 'react-intl';
+
+import ReplyIcon from '@/material-icons/400-24px/reply.svg?react';
+import { Icon } from 'flavours/glitch/components/icon';
+import { DisplayedName } from 'flavours/glitch/features/notifications_v2/components/displayed_name';
+import { useAppSelector } from 'flavours/glitch/store';
+
+export const StatusThreadLabel: React.FC<{
+ accountId: string;
+ inReplyToAccountId: string;
+}> = ({ accountId, inReplyToAccountId }) => {
+ const inReplyToAccount = useAppSelector((state) =>
+ state.accounts.get(inReplyToAccountId),
+ );
+
+ let label;
+
+ if (accountId === inReplyToAccountId) {
+ label = (
+
+ );
+ } else if (inReplyToAccount) {
+ label = (
+
}}
+ />
+ );
+ } else {
+ label = (
+
+ );
+ }
+
+ return (
+
+ );
+};
diff --git a/app/javascript/flavours/glitch/containers/status_container.js b/app/javascript/flavours/glitch/containers/status_container.js
index cf01d67060..011444c390 100644
--- a/app/javascript/flavours/glitch/containers/status_container.js
+++ b/app/javascript/flavours/glitch/containers/status_container.js
@@ -36,8 +36,6 @@ import Status from 'flavours/glitch/components/status';
import { deleteModal } from 'flavours/glitch/initial_state';
import { makeGetStatus, makeGetPictureInPicture } from 'flavours/glitch/selectors';
-import { showAlertForError } from '../actions/alerts';
-
const makeMapStateToProps = () => {
const getStatus = makeGetStatus();
const getPictureInPicture = makeGetPictureInPicture();
@@ -121,10 +119,7 @@ const mapDispatchToProps = (dispatch, { contextType }) => ({
onEmbed (status) {
dispatch(openModal({
modalType: 'EMBED',
- modalProps: {
- id: status.get('id'),
- onError: error => dispatch(showAlertForError(error)),
- },
+ modalProps: { id: status.get('id') },
}));
},
diff --git a/app/javascript/flavours/glitch/entrypoints/embed.tsx b/app/javascript/flavours/glitch/entrypoints/embed.tsx
new file mode 100644
index 0000000000..12eb4a413f
--- /dev/null
+++ b/app/javascript/flavours/glitch/entrypoints/embed.tsx
@@ -0,0 +1,74 @@
+import { createRoot } from 'react-dom/client';
+
+import '@/entrypoints/public-path';
+
+import { start } from 'flavours/glitch/common';
+import { Status } from 'flavours/glitch/features/standalone/status';
+import { afterInitialRender } from 'flavours/glitch/hooks/useRenderSignal';
+import { loadPolyfills } from 'flavours/glitch/polyfills';
+import ready from 'flavours/glitch/ready';
+
+start();
+
+function loaded() {
+ const mountNode = document.getElementById('mastodon-status');
+
+ if (mountNode) {
+ const attr = mountNode.getAttribute('data-props');
+
+ if (!attr) return;
+
+ const props = JSON.parse(attr) as { id: string; locale: string };
+ const root = createRoot(mountNode);
+
+ root.render(
);
+ }
+}
+
+function main() {
+ ready(loaded).catch((error: unknown) => {
+ console.error(error);
+ });
+}
+
+loadPolyfills()
+ .then(main)
+ .catch((error: unknown) => {
+ console.error(error);
+ });
+
+interface SetHeightMessage {
+ type: 'setHeight';
+ id: string;
+ height: number;
+}
+
+function isSetHeightMessage(data: unknown): data is SetHeightMessage {
+ if (
+ data &&
+ typeof data === 'object' &&
+ 'type' in data &&
+ data.type === 'setHeight'
+ )
+ return true;
+ else return false;
+}
+
+window.addEventListener('message', (e) => {
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- typings are not correct, it can be null in very rare cases
+ if (!e.data || !isSetHeightMessage(e.data) || !window.parent) return;
+
+ const data = e.data;
+
+ // We use a timeout to allow for the React page to render before calculating the height
+ afterInitialRender(() => {
+ window.parent.postMessage(
+ {
+ type: 'setHeight',
+ id: data.id,
+ height: document.getElementsByTagName('html')[0]?.scrollHeight,
+ },
+ '*',
+ );
+ });
+});
diff --git a/app/javascript/flavours/glitch/entrypoints/public.tsx b/app/javascript/flavours/glitch/entrypoints/public.tsx
index 44afc9d825..41c0e34396 100644
--- a/app/javascript/flavours/glitch/entrypoints/public.tsx
+++ b/app/javascript/flavours/glitch/entrypoints/public.tsx
@@ -37,43 +37,6 @@ const messages = defineMessages({
},
});
-interface SetHeightMessage {
- type: 'setHeight';
- id: string;
- height: number;
-}
-
-function isSetHeightMessage(data: unknown): data is SetHeightMessage {
- if (
- data &&
- typeof data === 'object' &&
- 'type' in data &&
- data.type === 'setHeight'
- )
- return true;
- else return false;
-}
-
-window.addEventListener('message', (e) => {
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- typings are not correct, it can be null in very rare cases
- if (!e.data || !isSetHeightMessage(e.data) || !window.parent) return;
-
- const data = e.data;
-
- ready(() => {
- window.parent.postMessage(
- {
- type: 'setHeight',
- id: data.id,
- height: document.getElementsByTagName('html')[0]?.scrollHeight,
- },
- '*',
- );
- }).catch((e: unknown) => {
- console.error('Error in setHeightMessage postMessage', e);
- });
-});
-
function loaded() {
const { messages: localeData } = getLocale();
diff --git a/app/javascript/flavours/glitch/features/account/navigation.jsx b/app/javascript/flavours/glitch/features/account/navigation.jsx
index 4be00c49f2..9505c48d5f 100644
--- a/app/javascript/flavours/glitch/features/account/navigation.jsx
+++ b/app/javascript/flavours/glitch/features/account/navigation.jsx
@@ -43,10 +43,7 @@ class AccountNavigation extends PureComponent {
}
return (
- <>
-
-
- >
+
);
}
diff --git a/app/javascript/flavours/glitch/features/account_gallery/components/media_item.jsx b/app/javascript/flavours/glitch/features/account_gallery/components/media_item.jsx
deleted file mode 100644
index c709e58db1..0000000000
--- a/app/javascript/flavours/glitch/features/account_gallery/components/media_item.jsx
+++ /dev/null
@@ -1,158 +0,0 @@
-import PropTypes from 'prop-types';
-
-import classNames from 'classnames';
-
-import ImmutablePropTypes from 'react-immutable-proptypes';
-import ImmutablePureComponent from 'react-immutable-pure-component';
-
-import AudiotrackIcon from '@/material-icons/400-24px/music_note.svg?react';
-import PlayArrowIcon from '@/material-icons/400-24px/play_arrow.svg?react';
-import VisibilityOffIcon from '@/material-icons/400-24px/visibility_off.svg?react';
-import { Blurhash } from 'flavours/glitch/components/blurhash';
-import { Icon } from 'flavours/glitch/components/icon';
-import { autoPlayGif, displayMedia, useBlurhash } from 'flavours/glitch/initial_state';
-
-export default class MediaItem extends ImmutablePureComponent {
-
- static propTypes = {
- attachment: ImmutablePropTypes.map.isRequired,
- displayWidth: PropTypes.number.isRequired,
- onOpenMedia: PropTypes.func.isRequired,
- };
-
- state = {
- visible: displayMedia !== 'hide_all' && !this.props.attachment.getIn(['status', 'sensitive']) || displayMedia === 'show_all',
- loaded: false,
- };
-
- handleImageLoad = () => {
- this.setState({ loaded: true });
- };
-
- handleMouseEnter = e => {
- if (this.hoverToPlay()) {
- e.target.play();
- }
- };
-
- handleMouseLeave = e => {
- if (this.hoverToPlay()) {
- e.target.pause();
- e.target.currentTime = 0;
- }
- };
-
- hoverToPlay () {
- return !autoPlayGif && ['gifv', 'video'].indexOf(this.props.attachment.get('type')) !== -1;
- }
-
- handleClick = e => {
- if (e.button === 0 && !(e.ctrlKey || e.metaKey)) {
- e.preventDefault();
-
- if (this.state.visible) {
- this.props.onOpenMedia(this.props.attachment);
- } else {
- this.setState({ visible: true });
- }
- }
- };
-
- render () {
- const { attachment, displayWidth } = this.props;
- const { visible, loaded } = this.state;
-
- const width = `${Math.floor((displayWidth - 4) / 3) - 4}px`;
- const height = width;
- const status = attachment.get('status');
- const title = status.get('spoiler_text') || attachment.get('description');
-
- let thumbnail, label, icon, content;
-
- if (!visible) {
- icon = (
-
-
-
- );
- } else {
- if (['audio', 'video'].includes(attachment.get('type'))) {
- content = (
-
- );
-
- if (attachment.get('type') === 'audio') {
- label =
;
- } else {
- label =
;
- }
- } else if (attachment.get('type') === 'image') {
- const focusX = attachment.getIn(['meta', 'focus', 'x']) || 0;
- const focusY = attachment.getIn(['meta', 'focus', 'y']) || 0;
- const x = ((focusX / 2) + .5) * 100;
- const y = ((focusY / -2) + .5) * 100;
-
- content = (
-
- );
- } else if (attachment.get('type') === 'gifv') {
- content = (
-
- );
-
- label = 'GIF';
- }
-
- thumbnail = (
-
- {content}
-
- {label && (
-
- {label}
-
- )}
-
- );
- }
-
- return (
-
- );
- }
-
-}
diff --git a/app/javascript/flavours/glitch/features/account_gallery/components/media_item.tsx b/app/javascript/flavours/glitch/features/account_gallery/components/media_item.tsx
new file mode 100644
index 0000000000..e7983c503c
--- /dev/null
+++ b/app/javascript/flavours/glitch/features/account_gallery/components/media_item.tsx
@@ -0,0 +1,203 @@
+import { useState, useCallback } from 'react';
+
+import classNames from 'classnames';
+
+import HeadphonesIcon from '@/material-icons/400-24px/headphones-fill.svg?react';
+import MovieIcon from '@/material-icons/400-24px/movie-fill.svg?react';
+import VisibilityOffIcon from '@/material-icons/400-24px/visibility_off.svg?react';
+import { AltTextBadge } from 'flavours/glitch/components/alt_text_badge';
+import { Blurhash } from 'flavours/glitch/components/blurhash';
+import { Icon } from 'flavours/glitch/components/icon';
+import { formatTime } from 'flavours/glitch/features/video';
+import {
+ autoPlayGif,
+ displayMedia,
+ useBlurhash,
+} from 'flavours/glitch/initial_state';
+import type { Status, MediaAttachment } from 'flavours/glitch/models/status';
+
+export const MediaItem: React.FC<{
+ attachment: MediaAttachment;
+ onOpenMedia: (arg0: MediaAttachment) => void;
+}> = ({ attachment, onOpenMedia }) => {
+ const [visible, setVisible] = useState(
+ (displayMedia !== 'hide_all' &&
+ !attachment.getIn(['status', 'sensitive'])) ||
+ displayMedia === 'show_all',
+ );
+ const [loaded, setLoaded] = useState(false);
+
+ const handleImageLoad = useCallback(() => {
+ setLoaded(true);
+ }, [setLoaded]);
+
+ const handleMouseEnter = useCallback(
+ (e: React.MouseEvent
) => {
+ if (e.target instanceof HTMLVideoElement) {
+ void e.target.play();
+ }
+ },
+ [],
+ );
+
+ const handleMouseLeave = useCallback(
+ (e: React.MouseEvent) => {
+ if (e.target instanceof HTMLVideoElement) {
+ e.target.pause();
+ e.target.currentTime = 0;
+ }
+ },
+ [],
+ );
+
+ const handleClick = useCallback(
+ (e: React.MouseEvent) => {
+ if (e.button === 0 && !(e.ctrlKey || e.metaKey)) {
+ e.preventDefault();
+
+ if (visible) {
+ onOpenMedia(attachment);
+ } else {
+ setVisible(true);
+ }
+ }
+ },
+ [attachment, visible, onOpenMedia, setVisible],
+ );
+
+ const status = attachment.get('status') as Status;
+ const description = (attachment.getIn(['translation', 'description']) ||
+ attachment.get('description')) as string | undefined;
+ const previewUrl = attachment.get('preview_url') as string;
+ const fullUrl = attachment.get('url') as string;
+ const avatarUrl = status.getIn(['account', 'avatar_static']) as string;
+ const lang = status.get('language') as string;
+ const blurhash = attachment.get('blurhash') as string;
+ const statusUrl = status.get('url') as string;
+ const type = attachment.get('type') as string;
+
+ let thumbnail;
+
+ const badges = [];
+
+ if (description && description.length > 0) {
+ badges.push();
+ }
+
+ if (!visible) {
+ thumbnail = (
+
+
+
+ );
+ } else if (type === 'audio') {
+ thumbnail = (
+ <>
+
+
+
+
+
+ >
+ );
+ } else if (type === 'image') {
+ const focusX = (attachment.getIn(['meta', 'focus', 'x']) || 0) as number;
+ const focusY = (attachment.getIn(['meta', 'focus', 'y']) || 0) as number;
+ const x = (focusX / 2 + 0.5) * 100;
+ const y = (focusY / -2 + 0.5) * 100;
+
+ thumbnail = (
+
+ );
+ } else if (['video', 'gifv'].includes(type)) {
+ const duration = attachment.getIn([
+ 'meta',
+ 'original',
+ 'duration',
+ ]) as number;
+
+ thumbnail = (
+
+
+
+ {type === 'video' && (
+
+
+
+ )}
+
+ );
+
+ if (type === 'gifv') {
+ badges.push(
+
+ GIF
+ ,
+ );
+ } else {
+ badges.push(
+
+ {formatTime(Math.floor(duration))}
+ ,
+ );
+ }
+ }
+
+ return (
+
+ );
+};
diff --git a/app/javascript/flavours/glitch/features/account_gallery/index.jsx b/app/javascript/flavours/glitch/features/account_gallery/index.jsx
index d3f845ddc4..284713f93d 100644
--- a/app/javascript/flavours/glitch/features/account_gallery/index.jsx
+++ b/app/javascript/flavours/glitch/features/account_gallery/index.jsx
@@ -20,7 +20,7 @@ import { expandAccountMediaTimeline } from '../../actions/timelines';
import HeaderContainer from '../account_timeline/containers/header_container';
import Column from '../ui/components/column';
-import MediaItem from './components/media_item';
+import { MediaItem } from './components/media_item';
const mapStateToProps = (state, { params: { acct, id } }) => {
const accountId = id || state.getIn(['accounts_map', normalizeForLookup(acct)]);
diff --git a/app/javascript/flavours/glitch/features/compose/components/language_dropdown.jsx b/app/javascript/flavours/glitch/features/compose/components/language_dropdown.jsx
index b759b2dbb6..4b6d8c1f18 100644
--- a/app/javascript/flavours/glitch/features/compose/components/language_dropdown.jsx
+++ b/app/javascript/flavours/glitch/features/compose/components/language_dropdown.jsx
@@ -240,7 +240,6 @@ class LanguageDropdown extends PureComponent {
frequentlyUsedLanguages: PropTypes.arrayOf(PropTypes.string),
intl: PropTypes.object.isRequired,
onChange: PropTypes.func,
- onClose: PropTypes.func,
};
state = {
@@ -257,14 +256,11 @@ class LanguageDropdown extends PureComponent {
};
handleClose = () => {
- const { value, onClose } = this.props;
-
if (this.state.open && this.activeElement) {
this.activeElement.focus({ preventScroll: true });
}
this.setState({ open: false });
- onClose(value);
};
handleChange = value => {
diff --git a/app/javascript/flavours/glitch/features/compose/components/sensitive_button.jsx b/app/javascript/flavours/glitch/features/compose/components/sensitive_button.jsx
index deb401bfb7..de7d033ed0 100644
--- a/app/javascript/flavours/glitch/features/compose/components/sensitive_button.jsx
+++ b/app/javascript/flavours/glitch/features/compose/components/sensitive_button.jsx
@@ -21,11 +21,11 @@ const messages = defineMessages({
export const SensitiveButton = () => {
const intl = useIntl();
- const spoilersAlwaysOn = useAppSelector((state) => state.getIn(['local_settings', 'always_show_spoilers_field']));
- const spoilerText = useAppSelector((state) => state.getIn(['compose', 'spoiler_text']));
- const sensitive = useAppSelector((state) => state.getIn(['compose', 'sensitive']));
- const spoiler = useAppSelector((state) => state.getIn(['compose', 'spoiler']));
- const mediaCount = useAppSelector((state) => state.getIn(['compose', 'media_attachments']).size);
+ const spoilersAlwaysOn = useAppSelector((state) => state.local_settings.getIn(['always_show_spoilers_field']));
+ const spoilerText = useAppSelector((state) => state.compose.get('spoiler_text'));
+ const sensitive = useAppSelector((state) => state.compose.get('sensitive'));
+ const spoiler = useAppSelector((state) => state.compose.get('spoiler'));
+ const mediaCount = useAppSelector((state) => state.compose.get('media_attachments').size);
const disabled = spoilersAlwaysOn ? (spoilerText && spoilerText.length > 0) : spoiler;
const active = sensitive || (spoilersAlwaysOn && spoilerText && spoilerText.length > 0);
diff --git a/app/javascript/flavours/glitch/features/compose/components/upload.jsx b/app/javascript/flavours/glitch/features/compose/components/upload.jsx
deleted file mode 100644
index 790d76264d..0000000000
--- a/app/javascript/flavours/glitch/features/compose/components/upload.jsx
+++ /dev/null
@@ -1,81 +0,0 @@
-import PropTypes from 'prop-types';
-import { useCallback } from 'react';
-
-import { FormattedMessage } from 'react-intl';
-
-import classNames from 'classnames';
-
-import { useDispatch, useSelector } from 'react-redux';
-
-import spring from 'react-motion/lib/spring';
-
-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 { Blurhash } from 'flavours/glitch/components/blurhash';
-import { Icon } from 'flavours/glitch/components/icon';
-import Motion from 'flavours/glitch/features/ui/util/optional_motion';
-
-export const Upload = ({ id, onDragStart, onDragEnter, onDragEnd }) => {
- const dispatch = useDispatch();
- const media = useSelector(state => state.getIn(['compose', 'media_attachments']).find(item => item.get('id') === id));
- const sensitive = useSelector(state => state.getIn(['compose', 'sensitive']));
-
- const handleUndoClick = useCallback(() => {
- dispatch(undoUploadCompose(id));
- }, [dispatch, id]);
-
- const handleFocalPointClick = useCallback(() => {
- dispatch(initMediaEditModal(id));
- }, [dispatch, id]);
-
- const handleDragStart = useCallback(() => {
- onDragStart(id);
- }, [onDragStart, id]);
-
- const handleDragEnter = useCallback(() => {
- onDragEnter(id);
- }, [onDragEnter, id]);
-
- if (!media) {
- return null;
- }
-
- const focusX = media.getIn(['meta', 'focus', 'x']);
- const focusY = media.getIn(['meta', 'focus', 'y']);
- const x = ((focusX / 2) + .5) * 100;
- const y = ((focusY / -2) + .5) * 100;
- const missingDescription = (media.get('description') || '').length === 0;
-
- return (
-
-
- {({ scale }) => (
-
- {sensitive &&
}
-
-
-
-
-
-
-
-
-
-
- )}
-
-
- );
-};
-
-Upload.propTypes = {
- id: PropTypes.string,
- onDragEnter: PropTypes.func,
- onDragStart: PropTypes.func,
- onDragEnd: PropTypes.func,
-};
diff --git a/app/javascript/flavours/glitch/features/compose/components/upload.tsx b/app/javascript/flavours/glitch/features/compose/components/upload.tsx
new file mode 100644
index 0000000000..a583a8d81d
--- /dev/null
+++ b/app/javascript/flavours/glitch/features/compose/components/upload.tsx
@@ -0,0 +1,130 @@
+import { useCallback } from 'react';
+
+import { FormattedMessage } from 'react-intl';
+
+import classNames from 'classnames';
+
+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 { Blurhash } from 'flavours/glitch/components/blurhash';
+import { Icon } from 'flavours/glitch/components/icon';
+import type { MediaAttachment } from 'flavours/glitch/models/media_attachment';
+import { useAppDispatch, useAppSelector } from 'flavours/glitch/store';
+
+export const Upload: React.FC<{
+ id: string;
+ dragging?: boolean;
+ overlay?: boolean;
+ tall?: boolean;
+ 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 sensitive = useAppSelector(
+ (state) => state.compose.get('sensitive') as boolean, // eslint-disable-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
+ );
+
+ const handleUndoClick = useCallback(() => {
+ dispatch(undoUploadCompose(id));
+ }, [dispatch, id]);
+
+ const handleFocalPointClick = useCallback(() => {
+ dispatch(initMediaEditModal(id));
+ }, [dispatch, id]);
+
+ const { attributes, listeners, setNodeRef, transform, transition } =
+ useSortable({ id });
+
+ if (!media) {
+ return null;
+ }
+
+ const focusX = media.getIn(['meta', 'focus', 'x']) as number;
+ const focusY = media.getIn(['meta', 'focus', 'y']) as number;
+ const x = (focusX / 2 + 0.5) * 100;
+ const y = (focusY / -2 + 0.5) * 100;
+ const missingDescription =
+ ((media.get('description') as string | undefined) ?? '').length === 0;
+
+ const style = {
+ transform: CSS.Transform.toString(transform),
+ transition,
+ };
+
+ return (
+
+
+ {sensitive && (
+
+ )}
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/app/javascript/flavours/glitch/features/compose/components/upload_form.jsx b/app/javascript/flavours/glitch/features/compose/components/upload_form.jsx
deleted file mode 100644
index 2b26735f5e..0000000000
--- a/app/javascript/flavours/glitch/features/compose/components/upload_form.jsx
+++ /dev/null
@@ -1,56 +0,0 @@
-import { useRef, useCallback } from 'react';
-
-import { useSelector, useDispatch } from 'react-redux';
-
-import { changeMediaOrder } from 'flavours/glitch/actions/compose';
-
-import { SensitiveButton } from './sensitive_button';
-import { Upload } from './upload';
-import { UploadProgress } from './upload_progress';
-
-export const UploadForm = () => {
- const dispatch = useDispatch();
- const mediaIds = useSelector(state => state.getIn(['compose', 'media_attachments']).map(item => item.get('id')));
- const active = useSelector(state => state.getIn(['compose', 'is_uploading']));
- const progress = useSelector(state => state.getIn(['compose', 'progress']));
- const isProcessing = useSelector(state => state.getIn(['compose', 'is_processing']));
-
- const dragItem = useRef();
- const dragOverItem = useRef();
-
- const handleDragStart = useCallback(id => {
- dragItem.current = id;
- }, [dragItem]);
-
- const handleDragEnter = useCallback(id => {
- dragOverItem.current = id;
- }, [dragOverItem]);
-
- const handleDragEnd = useCallback(() => {
- dispatch(changeMediaOrder(dragItem.current, dragOverItem.current));
- dragItem.current = null;
- dragOverItem.current = null;
- }, [dispatch, dragItem, dragOverItem]);
-
- return (
- <>
-
-
- {mediaIds.size > 0 && (
-
- {mediaIds.map(id => (
-
- ))}
-
- )}
-
- {!mediaIds.isEmpty() && }
- >
- );
-};
diff --git a/app/javascript/flavours/glitch/features/compose/components/upload_form.tsx b/app/javascript/flavours/glitch/features/compose/components/upload_form.tsx
new file mode 100644
index 0000000000..44bfaa8f38
--- /dev/null
+++ b/app/javascript/flavours/glitch/features/compose/components/upload_form.tsx
@@ -0,0 +1,188 @@
+import { useState, useCallback, useMemo } from 'react';
+
+import { useIntl, defineMessages } from 'react-intl';
+
+import type { List } from 'immutable';
+
+import type {
+ DragStartEvent,
+ DragEndEvent,
+ UniqueIdentifier,
+ Announcements,
+ ScreenReaderInstructions,
+} from '@dnd-kit/core';
+import {
+ DndContext,
+ closestCenter,
+ KeyboardSensor,
+ PointerSensor,
+ useSensor,
+ useSensors,
+ DragOverlay,
+} from '@dnd-kit/core';
+import {
+ SortableContext,
+ sortableKeyboardCoordinates,
+ rectSortingStrategy,
+} from '@dnd-kit/sortable';
+
+import { changeMediaOrder } from 'flavours/glitch/actions/compose';
+import type { MediaAttachment } from 'flavours/glitch/models/media_attachment';
+import { useAppSelector, useAppDispatch } from 'flavours/glitch/store';
+
+import { SensitiveButton } from './sensitive_button';
+import { Upload } from './upload';
+import { UploadProgress } from './upload_progress';
+
+const messages = defineMessages({
+ screenReaderInstructions: {
+ id: 'upload_form.drag_and_drop.instructions',
+ defaultMessage:
+ '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.',
+ },
+ onDragStart: {
+ id: 'upload_form.drag_and_drop.on_drag_start',
+ defaultMessage: 'Picked up media attachment {item}.',
+ },
+ onDragOver: {
+ id: 'upload_form.drag_and_drop.on_drag_over',
+ defaultMessage: 'Media attachment {item} was moved.',
+ },
+ onDragEnd: {
+ id: 'upload_form.drag_and_drop.on_drag_end',
+ defaultMessage: 'Media attachment {item} was dropped.',
+ },
+ onDragCancel: {
+ id: 'upload_form.drag_and_drop.on_drag_cancel',
+ defaultMessage:
+ 'Dragging was cancelled. Media attachment {item} was dropped.',
+ },
+});
+
+export const UploadForm: React.FC = () => {
+ const dispatch = useAppDispatch();
+ 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, // eslint-disable-line @typescript-eslint/no-unsafe-member-access
+ );
+ 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
+ );
+ const progress = useAppSelector(
+ (state) => state.compose.get('progress') as number, // eslint-disable-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
+ );
+ 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
+ );
+ const [activeId, setActiveId] = useState(null);
+ const sensors = useSensors(
+ useSensor(PointerSensor, {
+ activationConstraint: {
+ distance: 5,
+ },
+ }),
+ useSensor(KeyboardSensor, {
+ coordinateGetter: sortableKeyboardCoordinates,
+ }),
+ );
+
+ const handleDragStart = useCallback(
+ (e: DragStartEvent) => {
+ const { active } = e;
+
+ setActiveId(active.id);
+ },
+ [setActiveId],
+ );
+
+ const handleDragEnd = useCallback(
+ (e: DragEndEvent) => {
+ const { active, over } = e;
+
+ if (over && active.id !== over.id) {
+ dispatch(changeMediaOrder(active.id, over.id));
+ }
+
+ setActiveId(null);
+ },
+ [dispatch, setActiveId],
+ );
+
+ const accessibility: {
+ screenReaderInstructions: ScreenReaderInstructions;
+ announcements: Announcements;
+ } = useMemo(
+ () => ({
+ screenReaderInstructions: {
+ draggable: intl.formatMessage(messages.screenReaderInstructions),
+ },
+
+ announcements: {
+ onDragStart({ active }) {
+ return intl.formatMessage(messages.onDragStart, { item: active.id });
+ },
+
+ onDragOver({ active }) {
+ return intl.formatMessage(messages.onDragOver, { item: active.id });
+ },
+
+ onDragEnd({ active }) {
+ return intl.formatMessage(messages.onDragEnd, { item: active.id });
+ },
+
+ onDragCancel({ active }) {
+ return intl.formatMessage(messages.onDragCancel, { item: active.id });
+ },
+ },
+ }),
+ [intl],
+ );
+
+ return (
+ <>
+
+
+ {mediaIds.size > 0 && (
+
+
+
+ {mediaIds.map((id, idx) => (
+
+ ))}
+
+
+
+ {activeId ? : null}
+
+
+
+ )}
+
+ {!mediaIds.isEmpty() && }
+ >
+ );
+};
diff --git a/app/javascript/flavours/glitch/features/compose/containers/language_dropdown_container.js b/app/javascript/flavours/glitch/features/compose/containers/language_dropdown_container.js
index 9388cb0059..7d1b1e8d42 100644
--- a/app/javascript/flavours/glitch/features/compose/containers/language_dropdown_container.js
+++ b/app/javascript/flavours/glitch/features/compose/containers/language_dropdown_container.js
@@ -4,7 +4,6 @@ import { connect } from 'react-redux';
import { changeComposeLanguage } from 'flavours/glitch/actions/compose';
-import { useLanguage } from 'flavours/glitch/actions/languages';
import LanguageDropdown from '../components/language_dropdown';
@@ -28,11 +27,6 @@ const mapDispatchToProps = dispatch => ({
dispatch(changeComposeLanguage(value));
},
- onClose (value) {
- // eslint-disable-next-line react-hooks/rules-of-hooks -- this is not a react hook
- dispatch(useLanguage(value));
- },
-
});
export default connect(mapStateToProps, mapDispatchToProps)(LanguageDropdown);
diff --git a/app/javascript/flavours/glitch/features/direct_timeline/components/conversation.jsx b/app/javascript/flavours/glitch/features/direct_timeline/components/conversation.jsx
index 0f63c7a2e2..649786ecc5 100644
--- a/app/javascript/flavours/glitch/features/direct_timeline/components/conversation.jsx
+++ b/app/javascript/flavours/glitch/features/direct_timeline/components/conversation.jsx
@@ -197,7 +197,7 @@ export const Conversation = ({ conversation, scrollKey, onMoveUp, onMoveDown })
return (
-
+
diff --git a/app/javascript/flavours/glitch/features/directory/index.tsx b/app/javascript/flavours/glitch/features/directory/index.tsx
index 743543f9a6..150adee94e 100644
--- a/app/javascript/flavours/glitch/features/directory/index.tsx
+++ b/app/javascript/flavours/glitch/features/directory/index.tsx
@@ -1,24 +1,34 @@
-import type {ChangeEventHandler} from 'react';
-import {useCallback, useEffect, useRef, useState} from 'react';
+import type { ChangeEventHandler } from 'react';
+import { useCallback, useEffect, useRef } from 'react';
-import {defineMessages, useIntl} from 'react-intl';
+import { defineMessages, useIntl } from 'react-intl';
-import {Helmet} from 'react-helmet';
+import { Helmet } from 'react-helmet';
-import {List as ImmutableList} from 'immutable';
+import { List as ImmutableList } from 'immutable';
import PeopleIcon from '@/material-icons/400-24px/group.svg?react';
-import {addColumn, changeColumnParams, moveColumn, removeColumn,} from 'flavours/glitch/actions/columns';
-import {expandDirectory, fetchDirectory,} from 'flavours/glitch/actions/directory';
+import {
+ addColumn,
+ removeColumn,
+ moveColumn,
+ changeColumnParams,
+} from 'flavours/glitch/actions/columns';
+import {
+ fetchDirectory,
+ expandDirectory,
+} from 'flavours/glitch/actions/directory';
import Column from 'flavours/glitch/components/column';
-import {ColumnHeader} from 'flavours/glitch/components/column_header';
-import {LoadMore} from 'flavours/glitch/components/load_more';
-import {LoadingIndicator} from 'flavours/glitch/components/loading_indicator';
-import {RadioButton} from 'flavours/glitch/components/radio_button';
+import { ColumnHeader } from 'flavours/glitch/components/column_header';
+import { LoadMore } from 'flavours/glitch/components/load_more';
+import { LoadingIndicator } from 'flavours/glitch/components/loading_indicator';
+import { RadioButton } from 'flavours/glitch/components/radio_button';
import ScrollContainer from 'flavours/glitch/containers/scroll_container';
-import {useAppDispatch, useAppSelector} from 'flavours/glitch/store';
+import { useAppDispatch, useAppSelector } from 'flavours/glitch/store';
-import {AccountCard} from './components/account_card';
+import { useSearchParam } from '../../hooks/useSearchParam';
+
+import { AccountCard } from './components/account_card';
const messages = defineMessages({
title: { id: 'column.directory', defaultMessage: 'Browse profiles' },
@@ -42,18 +52,19 @@ export const Directory: React.FC<{
const intl = useIntl();
const dispatch = useAppDispatch();
- const [state, setState] = useState<{
- order: string | null;
- local: boolean | null;
- }>({
- order: null,
- local: null,
- });
-
const column = useRef
(null);
- const order = state.order ?? params?.order ?? 'active';
- const local = state.local ?? params?.local ?? false;
+ const [orderParam, setOrderParam] = useSearchParam('order');
+ const [localParam, setLocalParam] = useSearchParam('local');
+
+ let localParamBool: boolean | undefined;
+
+ if (localParam === 'false') {
+ localParamBool = false;
+ }
+
+ const order = orderParam ?? params?.order ?? 'active';
+ const local = localParamBool ?? params?.local ?? true;
const handlePin = useCallback(() => {
if (columnId) {
@@ -96,10 +107,10 @@ export const Directory: React.FC<{
if (columnId) {
dispatch(changeColumnParams(columnId, ['order'], e.target.value));
} else {
- setState((s) => ({ order: e.target.value, local: s.local }));
+ setOrderParam(e.target.value);
}
},
- [dispatch, columnId],
+ [dispatch, columnId, setOrderParam],
);
const handleChangeLocal = useCallback>(
@@ -108,11 +119,13 @@ export const Directory: React.FC<{
dispatch(
changeColumnParams(columnId, ['local'], e.target.value === '1'),
);
+ } else if (e.target.value === '1') {
+ setLocalParam('true');
} else {
- setState((s) => ({ local: e.target.value === '1', order: s.order }));
+ setLocalParam('false');
}
},
- [dispatch, columnId],
+ [dispatch, columnId, setLocalParam],
);
const handleLoadMore = useCallback(() => {
diff --git a/app/javascript/flavours/glitch/features/getting_started/index.jsx b/app/javascript/flavours/glitch/features/getting_started/index.jsx
index 2d13d3d584..af6dc313ad 100644
--- a/app/javascript/flavours/glitch/features/getting_started/index.jsx
+++ b/app/javascript/flavours/glitch/features/getting_started/index.jsx
@@ -12,11 +12,12 @@ import { connect } from 'react-redux';
import BookmarksIcon from '@/material-icons/400-24px/bookmarks-fill.svg?react';
import ExploreIcon from '@/material-icons/400-24px/explore.svg?react';
+import ModerationIcon from '@/material-icons/400-24px/gavel.svg?react';
import PeopleIcon from '@/material-icons/400-24px/group.svg?react';
import HomeIcon from '@/material-icons/400-24px/home-fill.svg?react';
import ListAltIcon from '@/material-icons/400-24px/list_alt.svg?react';
import MailIcon from '@/material-icons/400-24px/mail.svg?react';
-import ManufacturingIcon from '@/material-icons/400-24px/manufacturing.svg?react';
+import AdministrationIcon from '@/material-icons/400-24px/manufacturing.svg?react';
import MenuIcon from '@/material-icons/400-24px/menu.svg?react';
import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react';
import NotificationsIcon from '@/material-icons/400-24px/notifications.svg?react';
@@ -29,9 +30,9 @@ import { openModal } from 'flavours/glitch/actions/modal';
import Column from 'flavours/glitch/features/ui/components/column';
import LinkFooter from 'flavours/glitch/features/ui/components/link_footer';
import { identityContextPropShape, withIdentity } from 'flavours/glitch/identity_context';
+import { canManageReports, canViewAdminDashboard } from 'flavours/glitch/permissions';
import { preferencesLink } from 'flavours/glitch/utils/backend_links';
-
import { me, showTrends } from '../../initial_state';
import { NavigationBar } from '../compose/components/navigation_bar';
import ColumnLink from '../ui/components/column_link';
@@ -51,6 +52,8 @@ const messages = defineMessages({
direct: { id: 'navigation_bar.direct', defaultMessage: 'Private mentions' },
bookmarks: { id: 'navigation_bar.bookmarks', defaultMessage: 'Bookmarks' },
preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' },
+ administration: { id: 'navigation_bar.administration', defaultMessage: 'Administration' },
+ moderation: { id: 'navigation_bar.moderation', defaultMessage: 'Moderation' },
settings: { id: 'navigation_bar.app_settings', defaultMessage: 'App settings' },
follow_requests: { id: 'navigation_bar.follow_requests', defaultMessage: 'Follow requests' },
lists: { id: 'navigation_bar.lists', defaultMessage: 'Lists' },
@@ -131,7 +134,7 @@ class GettingStarted extends ImmutablePureComponent {
render () {
const { intl, myAccount, columns, multiColumn, unreadFollowRequests, unreadNotifications, lists, openSettings } = this.props;
- const { signedIn } = this.props.identity;
+ const { signedIn, permissions } = this.props.identity;
const navItems = [];
let listItems = [];
@@ -196,7 +199,9 @@ class GettingStarted extends ImmutablePureComponent {
{listItems}
{ preferencesLink !== undefined && }
-
+
+ {canManageReports(permissions) && }
+ {canViewAdminDashboard(permissions) && }
>
)}
diff --git a/app/javascript/flavours/glitch/features/interaction_modal/index.jsx b/app/javascript/flavours/glitch/features/interaction_modal/index.jsx
index 71cdf8409c..23244f06ca 100644
--- a/app/javascript/flavours/glitch/features/interaction_modal/index.jsx
+++ b/app/javascript/flavours/glitch/features/interaction_modal/index.jsx
@@ -131,7 +131,7 @@ class LoginForm extends React.PureComponent {
try {
new URL(url);
return true;
- } catch(_) {
+ } catch {
return false;
}
};
diff --git a/app/javascript/flavours/glitch/features/notifications/components/column_settings.jsx b/app/javascript/flavours/glitch/features/notifications/components/column_settings.jsx
index 50ab43f481..7afd205899 100644
--- a/app/javascript/flavours/glitch/features/notifications/components/column_settings.jsx
+++ b/app/javascript/flavours/glitch/features/notifications/components/column_settings.jsx
@@ -35,7 +35,6 @@ class ColumnSettings extends PureComponent {
const { settings, pushSettings, onChange, onClear, alertsEnabled, browserSupport, browserPermission, onRequestNotificationPermission } = this.props;
const unreadMarkersShowStr =
;
- const groupingShowStr =
;
const filterBarShowStr =
;
const filterAdvancedStr =
;
const alertStr =
;
@@ -78,16 +77,6 @@ class ColumnSettings extends PureComponent {
-
-
diff --git a/app/javascript/flavours/glitch/features/notifications/components/filtered_notifications_banner.tsx b/app/javascript/flavours/glitch/features/notifications/components/filtered_notifications_banner.tsx
index d0f136a54d..bcb0ac10d9 100644
--- a/app/javascript/flavours/glitch/features/notifications/components/filtered_notifications_banner.tsx
+++ b/app/javascript/flavours/glitch/features/notifications/components/filtered_notifications_banner.tsx
@@ -31,7 +31,7 @@ export const FilteredNotificationsIconButton: React.FC<{
history.push('/notifications/requests');
}, [history]);
- if (policy === null || policy.summary.pending_notifications_count === 0) {
+ if (policy === null || policy.summary.pending_requests_count <= 0) {
return null;
}
@@ -70,7 +70,7 @@ export const FilteredNotificationsBanner: React.FC = () => {
};
}, [dispatch]);
- if (policy === null || policy.summary.pending_notifications_count === 0) {
+ if (policy === null || policy.summary.pending_requests_count <= 0) {
return null;
}
diff --git a/app/javascript/flavours/glitch/features/notifications/components/notification.jsx b/app/javascript/flavours/glitch/features/notifications/components/notification.jsx
index db64519b20..6c567fdeae 100644
--- a/app/javascript/flavours/glitch/features/notifications/components/notification.jsx
+++ b/app/javascript/flavours/glitch/features/notifications/components/notification.jsx
@@ -410,7 +410,7 @@ class Notification extends ImmutablePureComponent {
{
- dispatch(dismissNotificationRequest(id));
+ dispatch(dismissNotificationRequest({ id }));
}, [dispatch, id]);
const handleAccept = useCallback(() => {
- dispatch(acceptNotificationRequest(id));
+ dispatch(acceptNotificationRequest({ id }));
}, [dispatch, id]);
const handleMute = useCallback(() => {
diff --git a/app/javascript/flavours/glitch/features/notifications/request.jsx b/app/javascript/flavours/glitch/features/notifications/request.jsx
index 3dd186f2bf..f33c92a205 100644
--- a/app/javascript/flavours/glitch/features/notifications/request.jsx
+++ b/app/javascript/flavours/glitch/features/notifications/request.jsx
@@ -10,7 +10,13 @@ import { useSelector, useDispatch } from 'react-redux';
import DeleteIcon from '@/material-icons/400-24px/delete.svg?react';
import DoneIcon from '@/material-icons/400-24px/done.svg?react';
import InventoryIcon from '@/material-icons/400-24px/inventory_2.svg?react';
-import { fetchNotificationRequest, fetchNotificationsForRequest, expandNotificationsForRequest, acceptNotificationRequest, dismissNotificationRequest } from 'flavours/glitch/actions/notifications';
+import {
+ fetchNotificationRequest,
+ fetchNotificationsForRequest,
+ expandNotificationsForRequest,
+ acceptNotificationRequest,
+ dismissNotificationRequest,
+} from 'flavours/glitch/actions/notification_requests';
import Column from 'flavours/glitch/components/column';
import ColumnHeader from 'flavours/glitch/components/column_header';
import { IconButton } from 'flavours/glitch/components/icon_button';
@@ -44,28 +50,28 @@ export const NotificationRequest = ({ multiColumn, params: { id } }) => {
const columnRef = useRef();
const intl = useIntl();
const dispatch = useDispatch();
- const notificationRequest = useSelector(state => state.getIn(['notificationRequests', 'current', 'item', 'id']) === id ? state.getIn(['notificationRequests', 'current', 'item']) : null);
- const accountId = notificationRequest?.get('account');
+ const notificationRequest = useSelector(state => state.notificationRequests.current.item?.id === id ? state.notificationRequests.current.item : null);
+ const accountId = notificationRequest?.account_id;
const account = useSelector(state => state.getIn(['accounts', accountId]));
- const notifications = useSelector(state => state.getIn(['notificationRequests', 'current', 'notifications', 'items']));
- const isLoading = useSelector(state => state.getIn(['notificationRequests', 'current', 'notifications', 'isLoading']));
- const hasMore = useSelector(state => !!state.getIn(['notificationRequests', 'current', 'notifications', 'next']));
- const removed = useSelector(state => state.getIn(['notificationRequests', 'current', 'removed']));
+ const notifications = useSelector(state => state.notificationRequests.current.notifications.items);
+ const isLoading = useSelector(state => state.notificationRequests.current.notifications.isLoading);
+ const hasMore = useSelector(state => !!state.notificationRequests.current.notifications.next);
+ const removed = useSelector(state => state.notificationRequests.current.removed);
const handleHeaderClick = useCallback(() => {
columnRef.current?.scrollTop();
}, [columnRef]);
const handleLoadMore = useCallback(() => {
- dispatch(expandNotificationsForRequest());
- }, [dispatch]);
+ dispatch(expandNotificationsForRequest({ accountId }));
+ }, [dispatch, accountId]);
const handleDismiss = useCallback(() => {
- dispatch(dismissNotificationRequest(id));
+ dispatch(dismissNotificationRequest({ id }));
}, [dispatch, id]);
const handleAccept = useCallback(() => {
- dispatch(acceptNotificationRequest(id));
+ dispatch(acceptNotificationRequest({ id }));
}, [dispatch, id]);
const handleMoveUp = useCallback(id => {
@@ -79,12 +85,12 @@ export const NotificationRequest = ({ multiColumn, params: { id } }) => {
}, [columnRef, notifications]);
useEffect(() => {
- dispatch(fetchNotificationRequest(id));
+ dispatch(fetchNotificationRequest({ id }));
}, [dispatch, id]);
useEffect(() => {
if (accountId) {
- dispatch(fetchNotificationsForRequest(accountId));
+ dispatch(fetchNotificationsForRequest({ accountId }));
}
}, [dispatch, accountId]);
diff --git a/app/javascript/flavours/glitch/features/notifications/requests.jsx b/app/javascript/flavours/glitch/features/notifications/requests.jsx
index aa39805aac..87b5727555 100644
--- a/app/javascript/flavours/glitch/features/notifications/requests.jsx
+++ b/app/javascript/flavours/glitch/features/notifications/requests.jsx
@@ -11,7 +11,12 @@ import ArrowDropDownIcon from '@/material-icons/400-24px/arrow_drop_down.svg?rea
import InventoryIcon from '@/material-icons/400-24px/inventory_2.svg?react';
import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react';
import { openModal } from 'flavours/glitch/actions/modal';
-import { fetchNotificationRequests, expandNotificationRequests, acceptNotificationRequests, dismissNotificationRequests } from 'flavours/glitch/actions/notifications';
+import {
+ fetchNotificationRequests,
+ expandNotificationRequests,
+ acceptNotificationRequests,
+ dismissNotificationRequests,
+} from 'flavours/glitch/actions/notification_requests';
import { changeSetting } from 'flavours/glitch/actions/settings';
import { CheckBox } from 'flavours/glitch/components/check_box';
import Column from 'flavours/glitch/components/column';
@@ -84,7 +89,7 @@ const SelectRow = ({selectAllChecked, toggleSelectAll, selectedItems, selectionM
message: intl.formatMessage(messages.confirmAcceptMultipleMessage, { count: selectedItems.length }),
confirm: intl.formatMessage(messages.confirmAcceptMultipleButton, { count: selectedItems.length}),
onConfirm: () =>
- dispatch(acceptNotificationRequests(selectedItems)),
+ dispatch(acceptNotificationRequests({ ids: selectedItems })),
},
}));
}, [dispatch, intl, selectedItems]);
@@ -97,7 +102,7 @@ const SelectRow = ({selectAllChecked, toggleSelectAll, selectedItems, selectionM
message: intl.formatMessage(messages.confirmDismissMultipleMessage, { count: selectedItems.length }),
confirm: intl.formatMessage(messages.confirmDismissMultipleButton, { count: selectedItems.length}),
onConfirm: () =>
- dispatch(dismissNotificationRequests(selectedItems)),
+ dispatch(dismissNotificationRequests({ ids: selectedItems })),
},
}));
}, [dispatch, intl, selectedItems]);
@@ -161,9 +166,9 @@ export const NotificationRequests = ({ multiColumn }) => {
const columnRef = useRef();
const intl = useIntl();
const dispatch = useDispatch();
- const isLoading = useSelector(state => state.getIn(['notificationRequests', 'isLoading']));
- const notificationRequests = useSelector(state => state.getIn(['notificationRequests', 'items']));
- const hasMore = useSelector(state => !!state.getIn(['notificationRequests', 'next']));
+ const isLoading = useSelector(state => state.notificationRequests.isLoading);
+ const notificationRequests = useSelector(state => state.notificationRequests.items);
+ const hasMore = useSelector(state => !!state.notificationRequests.next);
const [selectionMode, setSelectionMode] = useState(false);
const [checkedRequestIds, setCheckedRequestIds] = useState([]);
@@ -182,7 +187,7 @@ export const NotificationRequests = ({ multiColumn }) => {
else
ids.push(id);
- setSelectAllChecked(ids.length === notificationRequests.size);
+ setSelectAllChecked(ids.length === notificationRequests.length);
return [...ids];
});
@@ -193,7 +198,7 @@ export const NotificationRequests = ({ multiColumn }) => {
if(checked)
setCheckedRequestIds([]);
else
- setCheckedRequestIds(notificationRequests.map(request => request.get('id')).toArray());
+ setCheckedRequestIds(notificationRequests.map(request => request.id));
return !checked;
});
@@ -217,7 +222,7 @@ export const NotificationRequests = ({ multiColumn }) => {
multiColumn={multiColumn}
showBackButton
appendContent={
- notificationRequests.size > 0 && (
+ notificationRequests.length > 0 && (
)}
>
@@ -236,12 +241,12 @@ export const NotificationRequests = ({ multiColumn }) => {
>
{notificationRequests.map(request => (
))}
diff --git a/app/javascript/flavours/glitch/features/notifications_v2/components/embedded_status.tsx b/app/javascript/flavours/glitch/features/notifications_v2/components/embedded_status.tsx
index c67ec5dba8..6a67b5c849 100644
--- a/app/javascript/flavours/glitch/features/notifications_v2/components/embedded_status.tsx
+++ b/app/javascript/flavours/glitch/features/notifications_v2/components/embedded_status.tsx
@@ -1,20 +1,22 @@
-import {useCallback, useRef} from 'react';
+import { useCallback, useRef } from 'react';
-import {FormattedMessage} from 'react-intl';
+import { FormattedMessage } from 'react-intl';
-import {useHistory} from 'react-router-dom';
+import { useHistory } from 'react-router-dom';
-import type {List as ImmutableList, RecordOf} from 'immutable';
+import type { List as ImmutableList, RecordOf } from 'immutable';
import BarChart4BarsIcon from '@/material-icons/400-24px/bar_chart_4_bars.svg?react';
import PhotoLibraryIcon from '@/material-icons/400-24px/photo_library.svg?react';
-import {Avatar} from 'flavours/glitch/components/avatar';
-import {DisplayName} from 'flavours/glitch/components/display_name';
-import {Icon} from 'flavours/glitch/components/icon';
-import type {Status} from 'flavours/glitch/models/status';
-import {useAppSelector} from 'flavours/glitch/store';
+import { toggleStatusSpoilers } from 'flavours/glitch/actions/statuses';
+import { Avatar } from 'flavours/glitch/components/avatar';
+import { ContentWarning } from 'flavours/glitch/components/content_warning';
+import { DisplayName } from 'flavours/glitch/components/display_name';
+import { Icon } from 'flavours/glitch/components/icon';
+import type { Status } from 'flavours/glitch/models/status';
+import { useAppSelector, useAppDispatch } from 'flavours/glitch/store';
-import {EmbeddedStatusContent} from './embedded_status_content';
+import { EmbeddedStatusContent } from './embedded_status_content';
export type Mention = RecordOf<{ url: string; acct: string }>;
@@ -23,6 +25,7 @@ export const EmbeddedStatus: React.FC<{ statusId: string }> = ({
}) => {
const history = useHistory();
const clickCoordinatesRef = useRef<[number, number] | null>();
+ const dispatch = useAppDispatch();
const status = useAppSelector(
(state) => state.statuses.get(statusId) as Status | undefined,
@@ -96,15 +99,21 @@ export const EmbeddedStatus: React.FC<{ statusId: string }> = ({
[],
);
+ const handleContentWarningClick = useCallback(() => {
+ dispatch(toggleStatusSpoilers(statusId));
+ }, [dispatch, statusId]);
+
if (!status) {
return null;
}
// Assign status attributes to variables with a forced type, as status is not yet properly typed
const contentHtml = status.get('contentHtml') as string;
+ const contentWarning = status.get('spoilerHtml') as string;
const poll = status.get('poll');
const language = status.get('language') as string;
const mentions = status.get('mentions') as ImmutableList;
+ const expanded = !status.get('hidden') || !contentWarning;
const mediaAttachmentsSize = (
status.get('media_attachments') as ImmutableList
).size;
@@ -124,14 +133,24 @@ export const EmbeddedStatus: React.FC<{ statusId: string }> = ({
-
{!!poll && (
<>
diff --git a/app/javascript/flavours/glitch/features/notifications_v2/components/notification_admin_report.tsx b/app/javascript/flavours/glitch/features/notifications_v2/components/notification_admin_report.tsx
index a79f7a92d7..18092e758b 100644
--- a/app/javascript/flavours/glitch/features/notifications_v2/components/notification_admin_report.tsx
+++ b/app/javascript/flavours/glitch/features/notifications_v2/components/notification_admin_report.tsx
@@ -42,19 +42,11 @@ export const NotificationAdminReport: React.FC<{
if (!account || !targetAccount) return null;
+ const domain = account.acct.split('@')[1];
+
const values = {
- name: (
-
- ),
- target: (
-
- ),
+ name:
{domain ?? `@${account.acct}`},
+ target:
@{targetAccount.acct},
category: intl.formatMessage(messages[report.category]),
count: report.status_ids.length,
};
diff --git a/app/javascript/flavours/glitch/features/notifications_v2/components/notification_favourite.tsx b/app/javascript/flavours/glitch/features/notifications_v2/components/notification_favourite.tsx
index c364b6f824..173b0065b0 100644
--- a/app/javascript/flavours/glitch/features/notifications_v2/components/notification_favourite.tsx
+++ b/app/javascript/flavours/glitch/features/notifications_v2/components/notification_favourite.tsx
@@ -14,7 +14,7 @@ const labelRenderer: LabelRenderer = (displayedName, total, seeMoreHref) => {
return (
);
diff --git a/app/javascript/flavours/glitch/features/notifications_v2/components/notification_group_with_status.tsx b/app/javascript/flavours/glitch/features/notifications_v2/components/notification_group_with_status.tsx
index 233caaf65e..99045fa53b 100644
--- a/app/javascript/flavours/glitch/features/notifications_v2/components/notification_group_with_status.tsx
+++ b/app/javascript/flavours/glitch/features/notifications_v2/components/notification_group_with_status.tsx
@@ -9,7 +9,7 @@ import { navigateToStatus } from 'flavours/glitch/actions/statuses';
import type { IconProp } from 'flavours/glitch/components/icon';
import { Icon } from 'flavours/glitch/components/icon';
import { RelativeTimestamp } from 'flavours/glitch/components/relative_timestamp';
-import { useAppDispatch } from 'flavours/glitch/store';
+import { useAppSelector, useAppDispatch } from 'flavours/glitch/store';
import { AvatarGroup } from './avatar_group';
import { DisplayedName } from './displayed_name';
@@ -60,6 +60,10 @@ export const NotificationGroupWithStatus: React.FC<{
[labelRenderer, accountIds, count, labelSeeMoreHref],
);
+ const isPrivateMention = useAppSelector(
+ (state) => state.statuses.getIn([statusId, 'visibility']) === 'direct',
+ );
+
const handlers = useMemo(
() => ({
open: () => {
@@ -79,7 +83,10 @@ export const NotificationGroupWithStatus: React.FC<{
role='button'
className={classNames(
`notification-group focusable notification-group--${type}`,
- { 'notification-group--unread': unread },
+ {
+ 'notification-group--unread': unread,
+ 'notification-group--direct': isPrivateMention,
+ },
)}
tabIndex={0}
>
diff --git a/app/javascript/flavours/glitch/features/notifications_v2/components/notification_mention.tsx b/app/javascript/flavours/glitch/features/notifications_v2/components/notification_mention.tsx
index 7c7d949374..276e2d3cc1 100644
--- a/app/javascript/flavours/glitch/features/notifications_v2/components/notification_mention.tsx
+++ b/app/javascript/flavours/glitch/features/notifications_v2/components/notification_mention.tsx
@@ -1,5 +1,7 @@
import { FormattedMessage } from 'react-intl';
+import { isEqual } from 'lodash';
+
import AlternateEmailIcon from '@/material-icons/400-24px/alternate_email.svg?react';
import ReplyIcon from '@/material-icons/400-24px/reply-fill.svg?react';
import { me } from 'flavours/glitch/initial_state';
@@ -47,7 +49,7 @@ export const NotificationMention: React.FC<{
status.get('visibility') === 'direct',
status.get('in_reply_to_account_id') === me,
] as const;
- });
+ }, isEqual);
let labelRenderer = mentionLabelRenderer;
diff --git a/app/javascript/flavours/glitch/features/notifications_v2/components/notification_reblog.tsx b/app/javascript/flavours/glitch/features/notifications_v2/components/notification_reblog.tsx
index 981ba1c3eb..fcbe18b8c6 100644
--- a/app/javascript/flavours/glitch/features/notifications_v2/components/notification_reblog.tsx
+++ b/app/javascript/flavours/glitch/features/notifications_v2/components/notification_reblog.tsx
@@ -14,7 +14,7 @@ const labelRenderer: LabelRenderer = (displayedName, total, seeMoreHref) => {
return (
);
diff --git a/app/javascript/flavours/glitch/features/notifications_v2/index.tsx b/app/javascript/flavours/glitch/features/notifications_v2/index.tsx
index f5bf7adda3..b51d2cc1fd 100644
--- a/app/javascript/flavours/glitch/features/notifications_v2/index.tsx
+++ b/app/javascript/flavours/glitch/features/notifications_v2/index.tsx
@@ -4,6 +4,7 @@ import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import { Helmet } from 'react-helmet';
+import { isEqual } from 'lodash';
import { useDebouncedCallback } from 'use-debounce';
import DoneAllIcon from '@/material-icons/400-24px/done_all.svg?react';
@@ -62,7 +63,7 @@ export const Notifications: React.FC<{
multiColumn?: boolean;
}> = ({ columnId, multiColumn }) => {
const intl = useIntl();
- const notifications = useAppSelector(selectNotificationGroups);
+ const notifications = useAppSelector(selectNotificationGroups, isEqual);
const dispatch = useAppDispatch();
const isLoading = useAppSelector((s) => s.notificationGroups.isLoading);
const hasMore = notifications.at(-1)?.type === 'gap';
diff --git a/app/javascript/flavours/glitch/features/notifications_wrapper.jsx b/app/javascript/flavours/glitch/features/notifications_wrapper.jsx
index 15ab3367cc..e19aa9d07d 100644
--- a/app/javascript/flavours/glitch/features/notifications_wrapper.jsx
+++ b/app/javascript/flavours/glitch/features/notifications_wrapper.jsx
@@ -1,12 +1,8 @@
-import Notifications from 'flavours/glitch/features/notifications';
import Notifications_v2 from 'flavours/glitch/features/notifications_v2';
-import { useAppSelector } from 'flavours/glitch/store';
export const NotificationsWrapper = (props) => {
- const optedInGroupedNotifications = useAppSelector((state) => state.getIn(['settings', 'notifications', 'groupingBeta'], false));
-
return (
- optedInGroupedNotifications ?
:
+
);
};
diff --git a/app/javascript/flavours/glitch/features/onboarding/share.jsx b/app/javascript/flavours/glitch/features/onboarding/share.jsx
index 0c7d579c10..c985c798aa 100644
--- a/app/javascript/flavours/glitch/features/onboarding/share.jsx
+++ b/app/javascript/flavours/glitch/features/onboarding/share.jsx
@@ -10,8 +10,8 @@ import { Link } from 'react-router-dom';
import SwipeableViews from 'react-swipeable-views';
import ArrowRightAltIcon from '@/material-icons/400-24px/arrow_right_alt.svg?react';
-import ContentCopyIcon from '@/material-icons/400-24px/content_copy.svg?react';
import { ColumnBackButton } from 'flavours/glitch/components/column_back_button';
+import { CopyPasteText } from 'flavours/glitch/components/copy_paste_text';
import { Icon } from 'flavours/glitch/components/icon';
import { me, domain } from 'flavours/glitch/initial_state';
import { useAppSelector } from 'flavours/glitch/store';
@@ -20,67 +20,6 @@ const messages = defineMessages({
shareableMessage: { id: 'onboarding.share.message', defaultMessage: 'I\'m {username} on #Mastodon! Come follow me at {url}' },
});
-class CopyPasteText extends PureComponent {
-
- static propTypes = {
- value: PropTypes.string,
- };
-
- state = {
- copied: false,
- focused: false,
- };
-
- setRef = c => {
- this.input = c;
- };
-
- handleInputClick = () => {
- this.setState({ copied: false });
- this.input.focus();
- this.input.select();
- this.input.setSelectionRange(0, this.props.value.length);
- };
-
- handleButtonClick = e => {
- e.stopPropagation();
-
- const { value } = this.props;
- navigator.clipboard.writeText(value);
- this.input.blur();
- this.setState({ copied: true });
- this.timeout = setTimeout(() => this.setState({ copied: false }), 700);
- };
-
- handleFocus = () => {
- this.setState({ focused: true });
- };
-
- handleBlur = () => {
- this.setState({ focused: false });
- };
-
- componentWillUnmount () {
- if (this.timeout) clearTimeout(this.timeout);
- }
-
- render () {
- const { value } = this.props;
- const { copied, focused } = this.state;
-
- return (
-
-
-
-
-
- );
- }
-
-}
-
class TipCarousel extends PureComponent {
static propTypes = {
diff --git a/app/javascript/flavours/glitch/features/standalone/status/index.tsx b/app/javascript/flavours/glitch/features/standalone/status/index.tsx
new file mode 100644
index 0000000000..280b8fbb09
--- /dev/null
+++ b/app/javascript/flavours/glitch/features/standalone/status/index.tsx
@@ -0,0 +1,94 @@
+/* eslint-disable @typescript-eslint/no-unsafe-return,
+ @typescript-eslint/no-explicit-any,
+ @typescript-eslint/no-unsafe-assignment */
+
+import { useEffect, useCallback } from 'react';
+
+import { Provider } from 'react-redux';
+
+import {
+ fetchStatus,
+ toggleStatusSpoilers,
+} from 'flavours/glitch/actions/statuses';
+import { hydrateStore } from 'flavours/glitch/actions/store';
+import { Router } from 'flavours/glitch/components/router';
+import { DetailedStatus } from 'flavours/glitch/features/status/components/detailed_status';
+import { useRenderSignal } from 'flavours/glitch/hooks/useRenderSignal';
+import initialState from 'flavours/glitch/initial_state';
+import { IntlProvider } from 'flavours/glitch/locales';
+import {
+ makeGetStatus,
+ makeGetPictureInPicture,
+} from 'flavours/glitch/selectors';
+import { store, useAppSelector, useAppDispatch } from 'flavours/glitch/store';
+
+const getStatus = makeGetStatus() as unknown as (arg0: any, arg1: any) => any;
+const getPictureInPicture = makeGetPictureInPicture() as unknown as (
+ arg0: any,
+ arg1: any,
+) => any;
+
+const Embed: React.FC<{ id: string }> = ({ id }) => {
+ const status = useAppSelector((state) => getStatus(state, { id }));
+ const pictureInPicture = useAppSelector((state) =>
+ getPictureInPicture(state, { id }),
+ );
+ const domain = useAppSelector((state) => state.meta.get('domain'));
+ const dispatch = useAppDispatch();
+ const dispatchRenderSignal = useRenderSignal();
+
+ useEffect(() => {
+ dispatch(fetchStatus(id, false, false));
+ }, [dispatch, id]);
+
+ const handleToggleHidden = useCallback(() => {
+ dispatch(toggleStatusSpoilers(id));
+ }, [dispatch, id]);
+
+ // This allows us to calculate the correct page height for embeds
+ if (status) {
+ dispatchRenderSignal();
+ }
+
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
+ const permalink = status?.get('url') as string;
+
+ return (
+
+ );
+};
+
+export const Status: React.FC<{ id: string }> = ({ id }) => {
+ useEffect(() => {
+ if (initialState) {
+ store.dispatch(hydrateStore(initialState));
+ }
+ }, []);
+
+ return (
+
+
+
+
+
+
+
+ );
+};
diff --git a/app/javascript/flavours/glitch/features/status/components/action_bar.jsx b/app/javascript/flavours/glitch/features/status/components/action_bar.jsx
index 999910d6f5..d1319090c3 100644
--- a/app/javascript/flavours/glitch/features/status/components/action_bar.jsx
+++ b/app/javascript/flavours/glitch/features/status/components/action_bar.jsx
@@ -52,7 +52,7 @@ const messages = defineMessages({
share: { id: 'status.share', defaultMessage: 'Share' },
pin: { id: 'status.pin', defaultMessage: 'Pin on profile' },
unpin: { id: 'status.unpin', defaultMessage: 'Unpin from profile' },
- embed: { id: 'status.embed', defaultMessage: 'Embed' },
+ embed: { id: 'status.embed', defaultMessage: 'Get embed code' },
admin_account: { id: 'status.admin_account', defaultMessage: 'Open moderation interface for @{name}' },
admin_status: { id: 'status.admin_status', defaultMessage: 'Open this post in the moderation interface' },
admin_domain: { id: 'status.admin_domain', defaultMessage: 'Open moderation interface for {domain}' },
diff --git a/app/javascript/flavours/glitch/features/status/components/detailed_status.jsx b/app/javascript/flavours/glitch/features/status/components/detailed_status.jsx
deleted file mode 100644
index dd572bf99e..0000000000
--- a/app/javascript/flavours/glitch/features/status/components/detailed_status.jsx
+++ /dev/null
@@ -1,349 +0,0 @@
-import PropTypes from 'prop-types';
-
-import {FormattedDate, FormattedMessage} from 'react-intl';
-
-import classNames from 'classnames';
-import {Link, withRouter} from 'react-router-dom';
-
-import ImmutablePropTypes from 'react-immutable-proptypes';
-import ImmutablePureComponent from 'react-immutable-pure-component';
-
-import {AnimatedNumber} from 'flavours/glitch/components/animated_number';
-import AttachmentList from 'flavours/glitch/components/attachment_list';
-import EditedTimestamp from 'flavours/glitch/components/edited_timestamp';
-import {getHashtagBarForStatus} from 'flavours/glitch/components/hashtag_bar';
-import PictureInPicturePlaceholder from 'flavours/glitch/components/picture_in_picture_placeholder';
-import {VisibilityIcon} from 'flavours/glitch/components/visibility_icon';
-import PollContainer from 'flavours/glitch/containers/poll_container';
-import {identityContextPropShape, withIdentity} from 'flavours/glitch/identity_context';
-import {WithRouterPropTypes} from 'flavours/glitch/utils/react_router';
-
-import {Avatar} from '../../../components/avatar';
-import {DisplayName} from '../../../components/display_name';
-import MediaGallery from '../../../components/media_gallery';
-import StatusContent from '../../../components/status_content';
-import StatusReactions from '../../../components/status_reactions';
-import {visibleReactions} from '../../../initial_state';
-import Audio from '../../audio';
-import scheduleIdleTask from '../../ui/util/schedule_idle_task';
-import Video from '../../video';
-
-import Card from './card';
-
-class DetailedStatus extends ImmutablePureComponent {
- static propTypes = {
- identity: identityContextPropShape,
- status: ImmutablePropTypes.map,
- settings: ImmutablePropTypes.map.isRequired,
- onOpenMedia: PropTypes.func.isRequired,
- onOpenVideo: PropTypes.func.isRequired,
- onToggleHidden: PropTypes.func,
- onTranslate: PropTypes.func.isRequired,
- expanded: PropTypes.bool,
- measureHeight: PropTypes.bool,
- onHeightChange: PropTypes.func,
- domain: PropTypes.string.isRequired,
- compact: PropTypes.bool,
- showMedia: PropTypes.bool,
- pictureInPicture: ImmutablePropTypes.contains({
- inUse: PropTypes.bool,
- available: PropTypes.bool,
- }),
- onToggleMediaVisibility: PropTypes.func,
- onReactionAdd: PropTypes.func.isRequired,
- onReactionRemove: PropTypes.func.isRequired,
- ...WithRouterPropTypes,
- };
-
- state = {
- height: null,
- };
-
- handleAccountClick = (e) => {
- if (e.button === 0 && !(e.ctrlKey || e.altKey || e.metaKey) && this.props.history) {
- e.preventDefault();
- this.props.history.push(`/@${this.props.status.getIn(['account', 'acct'])}`);
- }
-
- e.stopPropagation();
- };
-
- parseClick = (e, destination) => {
- if (e.button === 0 && !(e.ctrlKey || e.altKey || e.metaKey) && this.props.history) {
- e.preventDefault();
- this.props.history.push(destination);
- }
-
- e.stopPropagation();
- };
-
- handleOpenVideo = (options) => {
- this.props.onOpenVideo(this.props.status.getIn(['media_attachments', 0]), options);
- };
-
- _measureHeight (heightJustChanged) {
- if (this.props.measureHeight && this.node) {
- scheduleIdleTask(() => this.node && this.setState({ height: Math.ceil(this.node.scrollHeight) + 1 }));
-
- if (this.props.onHeightChange && heightJustChanged) {
- this.props.onHeightChange();
- }
- }
- }
-
- setRef = c => {
- this.node = c;
- this._measureHeight();
- };
-
- componentDidUpdate (prevProps, prevState) {
- this._measureHeight(prevState.height !== this.state.height);
- }
-
- handleChildUpdate = () => {
- this._measureHeight();
- };
-
- handleModalLink = e => {
- e.preventDefault();
-
- let href;
-
- if (e.target.nodeName !== 'A') {
- href = e.target.parentNode.href;
- } else {
- href = e.target.href;
- }
-
- window.open(href, 'mastodon-intent', 'width=445,height=600,resizable=no,menubar=no,status=no,scrollbars=yes');
- };
-
- handleTranslate = () => {
- const { onTranslate, status } = this.props;
- onTranslate(status);
- };
-
- render () {
- const status = (this.props.status && this.props.status.get('reblog')) ? this.props.status.get('reblog') : this.props.status;
- const outerStyle = { boxSizing: 'border-box' };
- const { compact, pictureInPicture, expanded, onToggleHidden, settings } = this.props;
-
- if (!status) {
- return null;
- }
-
- let applicationLink = '';
- let reblogLink = '';
- let favouriteLink = '';
-
- // Depending on user settings, some media are considered as parts of the
- // contents (affected by CW) while other will be displayed outside of the
- // CW.
- let contentMedia = [];
- let contentMediaIcons = [];
- let extraMedia = [];
- let extraMediaIcons = [];
- let media = contentMedia;
- let mediaIcons = contentMediaIcons;
-
- if (settings.getIn(['content_warnings', 'media_outside'])) {
- media = extraMedia;
- mediaIcons = extraMediaIcons;
- }
-
- if (this.props.measureHeight) {
- outerStyle.height = `${this.state.height}px`;
- }
-
- const language = status.getIn(['translation', 'language']) || status.get('language');
-
- if (pictureInPicture.get('inUse')) {
- media.push(
);
- mediaIcons.push('video-camera');
- } else if (status.get('media_attachments').size > 0) {
- if (status.get('media_attachments').some(item => item.get('type') === 'unknown')) {
- media.push(
);
- } else if (status.getIn(['media_attachments', 0, 'type']) === 'audio') {
- const attachment = status.getIn(['media_attachments', 0]);
- const description = attachment.getIn(['translation', 'description']) || attachment.get('description');
-
- media.push(
-
,
- );
- mediaIcons.push('music');
- } else if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
- const attachment = status.getIn(['media_attachments', 0]);
- const description = attachment.getIn(['translation', 'description']) || attachment.get('description');
-
- media.push(
-
,
- );
- mediaIcons.push('video-camera');
- } else {
- media.push(
-
,
- );
- mediaIcons.push('picture-o');
- }
- } else if (status.get('card')) {
- media.push(
);
- mediaIcons.push('link');
- }
-
- if (status.get('poll')) {
- contentMedia.push(
);
- contentMediaIcons.push('tasks');
- }
-
- if (status.get('application')) {
- applicationLink = <>·
{status.getIn(['application', 'name'])}>;
- }
-
- const visibilityLink = <>·
>;
-
- if (!['unlisted', 'public'].includes(status.get('visibility'))) {
- reblogLink = null;
- } else if (this.props.history) {
- reblogLink = (
-
-
-
-
-
-
- );
- } else {
- reblogLink = (
-
-
-
-
-
-
- );
- }
-
- if (this.props.history) {
- favouriteLink = (
-
-
-
-
-
-
- );
- } else {
- favouriteLink = (
-
-
-
-
-
-
- );
- }
-
- const {statusContentProps, hashtagBar} = getHashtagBarForStatus(status);
- contentMedia.push(hashtagBar);
-
- return (
-
-
-
-
-
-
-
-
-
- {visibleReactions > 0 && (
)}
-
-
-
-
-
-
-
- {visibilityLink}
-
- {applicationLink}
-
-
- {status.get('edited_at') &&
}
-
-
- {reblogLink}
- {reblogLink && <>·>}
- {favouriteLink}
-
-
-
-
- );
- }
-
-}
-
-export default withRouter(withIdentity(DetailedStatus));
diff --git a/app/javascript/flavours/glitch/features/status/components/detailed_status.tsx b/app/javascript/flavours/glitch/features/status/components/detailed_status.tsx
new file mode 100644
index 0000000000..1cfb77610f
--- /dev/null
+++ b/app/javascript/flavours/glitch/features/status/components/detailed_status.tsx
@@ -0,0 +1,463 @@
+/* eslint-disable @typescript-eslint/no-unsafe-member-access,
+ @typescript-eslint/no-unsafe-call,
+ @typescript-eslint/no-explicit-any,
+ @typescript-eslint/no-unsafe-assignment */
+
+import type { CSSProperties } from 'react';
+import { useState, useRef, useCallback } from 'react';
+
+import { FormattedDate, FormattedMessage } from 'react-intl';
+
+import classNames from 'classnames';
+import { Link } from 'react-router-dom';
+
+import { AnimatedNumber } from 'flavours/glitch/components/animated_number';
+import AttachmentList from 'flavours/glitch/components/attachment_list';
+import EditedTimestamp from 'flavours/glitch/components/edited_timestamp';
+import type { StatusLike } from 'flavours/glitch/components/hashtag_bar';
+import { getHashtagBarForStatus } from 'flavours/glitch/components/hashtag_bar';
+import { IconLogo } from 'flavours/glitch/components/logo';
+import { Permalink } from 'flavours/glitch/components/permalink';
+import PictureInPicturePlaceholder from 'flavours/glitch/components/picture_in_picture_placeholder';
+import { useAppHistory } from 'flavours/glitch/components/router';
+import { VisibilityIcon } from 'flavours/glitch/components/visibility_icon';
+import PollContainer from 'flavours/glitch/containers/poll_container';
+import { useIdentity } from 'flavours/glitch/identity_context';
+import { useAppSelector } from 'flavours/glitch/store';
+
+import { Avatar } from '../../../components/avatar';
+import { DisplayName } from '../../../components/display_name';
+import MediaGallery from '../../../components/media_gallery';
+import StatusContent from '../../../components/status_content';
+import StatusReactions from '../../../components/status_reactions';
+import { visibleReactions } from '../../../initial_state';
+import Audio from '../../audio';
+import scheduleIdleTask from '../../ui/util/schedule_idle_task';
+import Video from '../../video';
+
+import Card from './card';
+
+interface VideoModalOptions {
+ startTime: number;
+ autoPlay?: boolean;
+ defaultVolume: number;
+ componentIndex: number;
+}
+
+export const DetailedStatus: React.FC<{
+ status: any;
+ onOpenMedia?: (status: any, index: number, lang: string) => void;
+ onOpenVideo?: (status: any, lang: string, options: VideoModalOptions) => void;
+ onTranslate?: (status: any) => void;
+ measureHeight?: boolean;
+ onHeightChange?: () => void;
+ domain: string;
+ showMedia?: boolean;
+ withLogo?: boolean;
+ pictureInPicture: any;
+ onToggleHidden?: (status: any) => void;
+ onToggleMediaVisibility?: () => void;
+ onReactionAdd?: (status: any, name: string, url: string) => void;
+ onReactionRemove?: (status: any, name: string) => void;
+ expanded: boolean;
+}> = ({
+ status,
+ onOpenMedia,
+ onOpenVideo,
+ onTranslate,
+ measureHeight,
+ onHeightChange,
+ domain,
+ showMedia,
+ withLogo,
+ pictureInPicture,
+ onToggleMediaVisibility,
+ onToggleHidden,
+ onReactionAdd,
+ onReactionRemove,
+ expanded,
+}) => {
+ const properStatus = status?.get('reblog') ?? status;
+ const [height, setHeight] = useState(0);
+ const nodeRef = useRef
();
+ const history = useAppHistory();
+ const { signedIn } = useIdentity();
+
+ const rewriteMentions = useAppSelector(
+ (state) => state.local_settings.get('rewrite_mentions', false) as boolean,
+ );
+ const tagMisleadingLinks = useAppSelector(
+ (state) =>
+ state.local_settings.get('tag_misleading_links', false) as boolean,
+ );
+ const mediaOutsideCW = useAppSelector(
+ (state) =>
+ state.local_settings.getIn(
+ ['content_warnings', 'media_outside'],
+ false,
+ ) as boolean,
+ );
+ const letterboxMedia = useAppSelector(
+ (state) =>
+ state.local_settings.getIn(['media', 'letterbox'], false) as boolean,
+ );
+ const fullwidthMedia = useAppSelector(
+ (state) =>
+ state.local_settings.getIn(['media', 'fullwidth'], false) as boolean,
+ );
+
+ const handleOpenVideo = useCallback(
+ (options: VideoModalOptions) => {
+ const lang = (status.getIn(['translation', 'language']) ||
+ status.get('language')) as string;
+ if (onOpenVideo)
+ onOpenVideo(status.getIn(['media_attachments', 0]), lang, options);
+ },
+ [onOpenVideo, status],
+ );
+
+ const _measureHeight = useCallback(
+ (heightJustChanged?: boolean) => {
+ if (measureHeight && nodeRef.current) {
+ scheduleIdleTask(() => {
+ if (nodeRef.current)
+ setHeight(Math.ceil(nodeRef.current.scrollHeight) + 1);
+ });
+
+ if (onHeightChange && heightJustChanged) {
+ onHeightChange();
+ }
+ }
+ },
+ [onHeightChange, measureHeight, setHeight],
+ );
+
+ const handleRef = useCallback(
+ (c: HTMLDivElement) => {
+ nodeRef.current = c;
+ _measureHeight();
+ },
+ [_measureHeight],
+ );
+
+ const handleChildUpdate = useCallback(() => {
+ _measureHeight();
+ }, [_measureHeight]);
+
+ const handleTranslate = useCallback(() => {
+ if (onTranslate) onTranslate(status);
+ }, [onTranslate, status]);
+
+ const parseClick = useCallback(
+ (e: React.MouseEvent, destination: string) => {
+ if (e.button === 0 && !(e.ctrlKey || e.altKey || e.metaKey)) {
+ e.preventDefault();
+ history.push(destination);
+ }
+
+ e.stopPropagation();
+ },
+ [history],
+ );
+
+ if (!properStatus) {
+ return null;
+ }
+
+ let applicationLink;
+ let reblogLink;
+
+ // Depending on user settings, some media are considered as parts of the
+ // contents (affected by CW) while other will be displayed outside of the
+ // CW.
+ const contentMedia: React.ReactNode[] = [];
+ const contentMediaIcons: string[] = [];
+ const extraMedia: React.ReactNode[] = [];
+ const extraMediaIcons: string[] = [];
+ let media = contentMedia;
+ let mediaIcons: string[] = contentMediaIcons;
+
+ if (mediaOutsideCW) {
+ media = extraMedia;
+ mediaIcons = extraMediaIcons;
+ }
+
+ const outerStyle = { boxSizing: 'border-box' } as CSSProperties;
+
+ if (measureHeight) {
+ outerStyle.height = height;
+ }
+
+ const language =
+ status.getIn(['translation', 'language']) || status.get('language');
+
+ if (pictureInPicture.get('inUse')) {
+ media.push();
+ mediaIcons.push('video-camera');
+ } else if (status.get('media_attachments').size > 0) {
+ if (
+ status
+ .get('media_attachments')
+ .some(
+ (item: Immutable.Map) => item.get('type') === 'unknown',
+ )
+ ) {
+ media.push();
+ } else if (
+ ['image', 'gifv'].includes(
+ status.getIn(['media_attachments', 0, 'type']) as string,
+ ) ||
+ status.get('media_attachments').size > 1
+ ) {
+ media.push(
+ ,
+ );
+ mediaIcons.push('picture-o');
+ } else if (status.getIn(['media_attachments', 0, 'type']) === 'audio') {
+ const attachment = status.getIn(['media_attachments', 0]);
+ const description =
+ attachment.getIn(['translation', 'description']) ||
+ attachment.get('description');
+
+ media.push(
+ ,
+ );
+ mediaIcons.push('music');
+ } else if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
+ const attachment = status.getIn(['media_attachments', 0]);
+ const description =
+ attachment.getIn(['translation', 'description']) ||
+ attachment.get('description');
+
+ media.push(
+ ,
+ );
+ mediaIcons.push('video-camera');
+ }
+ } else if (status.get('spoiler_text').length === 0) {
+ media.push(
+ ,
+ );
+ mediaIcons.push('link');
+ }
+
+ if (status.get('poll')) {
+ contentMedia.push(
+ ,
+ );
+ contentMediaIcons.push('tasks');
+ }
+
+ if (status.get('application')) {
+ applicationLink = (
+ <>
+ ·
+
+ {status.getIn(['application', 'name'])}
+
+ >
+ );
+ }
+
+ const visibilityLink = (
+ <>
+ ·
+ >
+ );
+
+ if (['private', 'direct'].includes(status.get('visibility') as string)) {
+ reblogLink = '';
+ } else {
+ reblogLink = (
+
+
+
+
+
+
+ );
+ }
+
+ const favouriteLink = (
+
+
+
+
+
+
+ );
+
+ const { statusContentProps, hashtagBar } = getHashtagBarForStatus(
+ status as StatusLike,
+ );
+ contentMedia.push(hashtagBar);
+
+ return (
+
+
+
+
+
+ {withLogo && (
+ <>
+
+
+ >
+ )}
+
+
+
+
+ {visibleReactions && visibleReactions > 0 && (
+
+ )}
+
+
+
+
+
+
+
+ {visibilityLink}
+ {applicationLink}
+
+
+ {status.get('edited_at') && (
+
+
+
+ )}
+
+
+ {reblogLink}
+ {reblogLink && <>·>}
+ {favouriteLink}
+
+
+
+
+ );
+};
diff --git a/app/javascript/flavours/glitch/features/status/containers/detailed_status_container.js b/app/javascript/flavours/glitch/features/status/containers/detailed_status_container.js
deleted file mode 100644
index c48d56fabe..0000000000
--- a/app/javascript/flavours/glitch/features/status/containers/detailed_status_container.js
+++ /dev/null
@@ -1,121 +0,0 @@
-import {injectIntl} from 'react-intl';
-
-import {connect} from 'react-redux';
-
-import {showAlertForError} from '../../../actions/alerts';
-import {initBlockModal} from '../../../actions/blocks';
-import {directCompose, mentionCompose, replyCompose,} from '../../../actions/compose';
-import {pin, toggleFavourite, toggleReblog, unpin,} from '../../../actions/interactions';
-import {openModal} from '../../../actions/modal';
-import {initMuteModal} from '../../../actions/mutes';
-import {initReport} from '../../../actions/reports';
-import {deleteStatus, muteStatus, unmuteStatus,} from '../../../actions/statuses';
-import {deleteModal} from '../../../initial_state';
-import {makeGetStatus} from '../../../selectors';
-import DetailedStatus from '../components/detailed_status';
-
-const makeMapStateToProps = () => {
- const getStatus = makeGetStatus();
-
- const mapStateToProps = (state, props) => ({
- status: getStatus(state, props),
- domain: state.getIn(['meta', 'domain']),
- settings: state.get('local_settings'),
- });
-
- return mapStateToProps;
-};
-
-const mapDispatchToProps = (dispatch) => ({
-
- onReply (status) {
- dispatch((_, getState) => {
- let state = getState();
- if (state.getIn(['compose', 'text']).trim().length !== 0) {
- dispatch(openModal({ modalType: 'CONFIRM_REPLY', modalProps: { status } }));
- } else {
- dispatch(replyCompose(status));
- }
- });
- },
-
- onReblog (status, e) {
- dispatch(toggleReblog(status.get('id'), e.shiftKey));
- },
-
- onFavourite (status, e) {
- dispatch(toggleFavourite(status.get('id'), e.shiftKey));
- },
-
- onPin (status) {
- if (status.get('pinned')) {
- dispatch(unpin(status));
- } else {
- dispatch(pin(status));
- }
- },
-
- onEmbed (status) {
- dispatch(openModal({
- modalType: 'EMBED',
- modalProps: {
- id: status.get('id'),
- onError: error => dispatch(showAlertForError(error)),
- },
- }));
- },
-
- onDelete (status, withRedraft = false) {
- if (!deleteModal) {
- dispatch(deleteStatus(status.get('id'), withRedraft));
- } else {
- dispatch(openModal({ modalType: 'CONFIRM_DELETE_STATUS', modalProps: { statusId: status.get('id'), withRedraft } }));
- }
- },
-
- onDirect (account) {
- dispatch(directCompose(account));
- },
-
- onMention (account) {
- dispatch(mentionCompose(account));
- },
-
- onOpenMedia (media, index, lang) {
- dispatch(openModal({
- modalType: 'MEDIA',
- modalProps: { media, index, lang },
- }));
- },
-
- onOpenVideo (media, lang, options) {
- dispatch(openModal({
- modalType: 'VIDEO',
- modalProps: { media, lang, options },
- }));
- },
-
- onBlock (status) {
- const account = status.get('account');
- dispatch(initBlockModal(account));
- },
-
- onReport (status) {
- dispatch(initReport(status.get('account'), status));
- },
-
- onMute (account) {
- dispatch(initMuteModal(account));
- },
-
- onMuteConversation (status) {
- if (status.get('muted')) {
- dispatch(unmuteStatus(status.get('id')));
- } else {
- dispatch(muteStatus(status.get('id')));
- }
- },
-
-});
-
-export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(DetailedStatus));
diff --git a/app/javascript/flavours/glitch/features/status/index.jsx b/app/javascript/flavours/glitch/features/status/index.jsx
index 90920513a0..982ee8e08b 100644
--- a/app/javascript/flavours/glitch/features/status/index.jsx
+++ b/app/javascript/flavours/glitch/features/status/index.jsx
@@ -65,7 +65,7 @@ import Column from '../ui/components/column';
import { attachFullscreenListener, detachFullscreenListener, isFullscreen } from '../ui/util/fullscreen';
import ActionBar from './components/action_bar';
-import DetailedStatus from './components/detailed_status';
+import { DetailedStatus } from './components/detailed_status';
const messages = defineMessages({
diff --git a/app/javascript/flavours/glitch/features/ui/components/block_modal.jsx b/app/javascript/flavours/glitch/features/ui/components/block_modal.jsx
index fa772b067d..2d835eb8ca 100644
--- a/app/javascript/flavours/glitch/features/ui/components/block_modal.jsx
+++ b/app/javascript/flavours/glitch/features/ui/components/block_modal.jsx
@@ -99,7 +99,7 @@ export const BlockModal = ({ accountId, acct }) => {
-
diff --git a/app/javascript/flavours/glitch/features/ui/components/boost_modal.tsx b/app/javascript/flavours/glitch/features/ui/components/boost_modal.tsx
index 452a022a7d..9d1cf8545f 100644
--- a/app/javascript/flavours/glitch/features/ui/components/boost_modal.tsx
+++ b/app/javascript/flavours/glitch/features/ui/components/boost_modal.tsx
@@ -1,28 +1,17 @@
-import type { MouseEventHandler } from 'react';
import { useCallback, useState } from 'react';
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import classNames from 'classnames';
-import { useHistory } from 'react-router';
-
-import type Immutable from 'immutable';
import RepeatIcon from '@/material-icons/400-24px/repeat.svg?react';
-import AttachmentList from 'flavours/glitch/components/attachment_list';
+import { Button } from 'flavours/glitch/components/button';
import { Icon } from 'flavours/glitch/components/icon';
-import { VisibilityIcon } from 'flavours/glitch/components/visibility_icon';
import PrivacyDropdown from 'flavours/glitch/features/compose/components/privacy_dropdown';
-import type { Account } from 'flavours/glitch/models/account';
+import { EmbeddedStatus } from 'flavours/glitch/features/notifications_v2/components/embedded_status';
import type { Status, StatusVisibility } from 'flavours/glitch/models/status';
import { useAppSelector } from 'flavours/glitch/store';
-import { Avatar } from '../../../components/avatar';
-import { Button } from '../../../components/button';
-import { DisplayName } from '../../../components/display_name';
-import { RelativeTimestamp } from '../../../components/relative_timestamp';
-import StatusContent from '../../../components/status_content';
-
const messages = defineMessages({
cancel_reblog: {
id: 'status.cancel_reblog_private',
@@ -38,18 +27,17 @@ export const BoostModal: React.FC<{
missingMediaDescription?: boolean;
}> = ({ status, onReblog, onClose, missingMediaDescription }) => {
const intl = useIntl();
- const history = useHistory();
- const default_privacy = 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,
);
- const account = status.get('account') as Account;
+ const statusId = status.get('id') as string;
const statusVisibility = status.get('visibility') as StatusVisibility;
const [privacy, setPrivacy] = useState