diff --git a/app/javascript/flavours/glitch/actions/statuses.js b/app/javascript/flavours/glitch/actions/statuses.js
index 2d83ae9926..2b6b589b37 100644
--- a/app/javascript/flavours/glitch/actions/statuses.js
+++ b/app/javascript/flavours/glitch/actions/statuses.js
@@ -49,11 +49,13 @@ export function fetchStatusRequest(id, skipLoading) {
};
}
-export function fetchStatus(id, forceFetch = false) {
+export function fetchStatus(id, forceFetch = false, alsoFetchContext = true) {
return (dispatch, getState) => {
const skipLoading = !forceFetch && getState().getIn(['statuses', id], null) !== null;
- dispatch(fetchContext(id));
+ if (alsoFetchContext) {
+ dispatch(fetchContext(id));
+ }
if (skipLoading) {
return;
diff --git a/app/javascript/flavours/glitch/components/logo.tsx b/app/javascript/flavours/glitch/components/logo.tsx
index b7f8bd6695..fe9680d0e3 100644
--- a/app/javascript/flavours/glitch/components/logo.tsx
+++ b/app/javascript/flavours/glitch/components/logo.tsx
@@ -7,6 +7,13 @@ export const WordmarkLogo: React.FC = () => (
);
+export const IconLogo: React.FC = () => (
+
+);
+
export const SymbolLogo: React.FC = () => (
);
diff --git a/app/javascript/flavours/glitch/components/more_from_author.jsx b/app/javascript/flavours/glitch/components/more_from_author.jsx
index 4f20ae76bf..f2377b450b 100644
--- a/app/javascript/flavours/glitch/components/more_from_author.jsx
+++ b/app/javascript/flavours/glitch/components/more_from_author.jsx
@@ -2,14 +2,12 @@ import PropTypes from 'prop-types';
import { FormattedMessage } from 'react-intl';
+import { IconLogo } from 'flavours/glitch/components/logo';
import { AuthorLink } from 'flavours/glitch/features/explore/components/author_link';
export const MoreFromAuthor = ({ accountId }) => (
-
-
+
}} />
);
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/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/detailed_status.jsx b/app/javascript/flavours/glitch/features/status/components/detailed_status.jsx
deleted file mode 100644
index 2db9fa6d3a..0000000000
--- a/app/javascript/flavours/glitch/features/status/components/detailed_status.jsx
+++ /dev/null
@@ -1,336 +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 { 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 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 = {
- 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,
- ...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 (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {visibilityLink}
-
- {applicationLink}
-
-
- {status.get('edited_at') &&
}
-
-
- {reblogLink}
- {reblogLink && <>·>}
- {favouriteLink}
-
-
-
-
- );
- }
-
-}
-
-export default withRouter(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..a1da973b49
--- /dev/null
+++ b/app/javascript/flavours/glitch/features/status/components/detailed_status.tsx
@@ -0,0 +1,413 @@
+/* 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 { VisibilityIcon } from 'flavours/glitch/components/visibility_icon';
+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 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;
+ expanded: boolean;
+}> = ({
+ status,
+ onOpenMedia,
+ onOpenVideo,
+ onTranslate,
+ measureHeight,
+ onHeightChange,
+ domain,
+ showMedia,
+ withLogo,
+ pictureInPicture,
+ onToggleMediaVisibility,
+ onToggleHidden,
+ expanded,
+}) => {
+ const properStatus = status?.get('reblog') ?? status;
+ const [height, setHeight] = useState(0);
+ const nodeRef = useRef();
+
+ 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]);
+
+ 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 (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('spoiler_text').length === 0) {
+ media.push(
+ ,
+ );
+ mediaIcons.push('link');
+ }
+
+ 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 && (
+ <>
+
+
+ >
+ )}
+
+
+ {/* TODO: parseClick={this.parseClick} */}
+
+
+
+
+
+
+
+
+ {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 34bde2fc6e..0000000000
--- a/app/javascript/flavours/glitch/features/status/containers/detailed_status_container.js
+++ /dev/null
@@ -1,134 +0,0 @@
-import { injectIntl } from 'react-intl';
-
-import { connect } from 'react-redux';
-
-import { showAlertForError } from '../../../actions/alerts';
-import { initBlockModal } from '../../../actions/blocks';
-import {
- replyCompose,
- mentionCompose,
- directCompose,
-} from '../../../actions/compose';
-import {
- toggleReblog,
- toggleFavourite,
- pin,
- unpin,
-} from '../../../actions/interactions';
-import { openModal } from '../../../actions/modal';
-import { initMuteModal } from '../../../actions/mutes';
-import { initReport } from '../../../actions/reports';
-import {
- muteStatus,
- unmuteStatus,
- deleteStatus,
-} 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 279d90b998..521db781b0 100644
--- a/app/javascript/flavours/glitch/features/status/index.jsx
+++ b/app/javascript/flavours/glitch/features/status/index.jsx
@@ -63,7 +63,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/hooks/useRenderSignal.ts b/app/javascript/flavours/glitch/hooks/useRenderSignal.ts
new file mode 100644
index 0000000000..740df4a35a
--- /dev/null
+++ b/app/javascript/flavours/glitch/hooks/useRenderSignal.ts
@@ -0,0 +1,32 @@
+// This hook allows a component to signal that it's done rendering in a way that
+// can be used by e.g. our embed code to determine correct iframe height
+
+let renderSignalReceived = false;
+
+type Callback = () => void;
+
+let onInitialRender: Callback;
+
+export const afterInitialRender = (callback: Callback) => {
+ if (renderSignalReceived) {
+ callback();
+ } else {
+ onInitialRender = callback;
+ }
+};
+
+export const useRenderSignal = () => {
+ return () => {
+ if (renderSignalReceived) {
+ return;
+ }
+
+ renderSignalReceived = true;
+
+ if (typeof onInitialRender !== 'undefined') {
+ window.requestAnimationFrame(() => {
+ onInitialRender();
+ });
+ }
+ };
+};
diff --git a/app/javascript/flavours/glitch/styles/application.scss b/app/javascript/flavours/glitch/styles/application.scss
index 6a0ed80b30..fd55960311 100644
--- a/app/javascript/flavours/glitch/styles/application.scss
+++ b/app/javascript/flavours/glitch/styles/application.scss
@@ -11,7 +11,6 @@
@import 'widgets';
@import 'forms';
@import 'accounts';
-@import 'statuses';
@import 'components';
@import 'polls';
@import 'modal';
diff --git a/app/javascript/flavours/glitch/styles/components.scss b/app/javascript/flavours/glitch/styles/components.scss
index c773b991ef..7c9a6ac480 100644
--- a/app/javascript/flavours/glitch/styles/components.scss
+++ b/app/javascript/flavours/glitch/styles/components.scss
@@ -1849,18 +1849,6 @@ body > [data-popper-placement] {
padding: 14px 10px; // glitch: reduced padding
border-top: 1px solid var(--background-border-color);
- &--flex {
- display: flex;
- flex-wrap: wrap;
- justify-content: space-between;
- align-items: flex-start;
-
- .status__content,
- .detailed-status__meta {
- flex: 100%;
- }
- }
-
.status__content {
font-size: 19px;
line-height: 24px;
@@ -1887,6 +1875,29 @@ body > [data-popper-placement] {
padding: 0;
margin-bottom: 16px;
}
+
+ .logo {
+ width: 40px;
+ height: 40px;
+ color: $dark-text-color;
+ }
+}
+
+.embed {
+ position: relative;
+
+ &__overlay {
+ display: block;
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ }
+
+ .detailed-status {
+ border-top: 0;
+ }
}
.scrollable > div:first-child .detailed-status {
diff --git a/app/javascript/flavours/glitch/styles/statuses.scss b/app/javascript/flavours/glitch/styles/statuses.scss
deleted file mode 100644
index cc509a6821..0000000000
--- a/app/javascript/flavours/glitch/styles/statuses.scss
+++ /dev/null
@@ -1,239 +0,0 @@
-.activity-stream {
- box-shadow: 0 0 15px rgba($base-shadow-color, 0.2);
- border-radius: 4px;
- overflow: hidden;
- margin-bottom: 10px;
-
- @media screen and (max-width: $no-gap-breakpoint) {
- margin-bottom: 0;
- border-radius: 0;
- box-shadow: none;
- }
-
- &--headless {
- border-radius: 0;
- margin: 0;
- box-shadow: none;
-
- .detailed-status,
- .status {
- border-radius: 0 !important;
- }
- }
-
- div[data-component] {
- width: 100%;
- }
-
- .entry {
- background: $ui-base-color;
-
- .detailed-status,
- .status,
- .load-more {
- animation: none;
- }
-
- &:last-child {
- .detailed-status,
- .status,
- .load-more {
- border-bottom: 0;
- border-radius: 0 0 4px 4px;
- }
- }
-
- &:first-child {
- .detailed-status,
- .status,
- .load-more {
- border-radius: 4px 4px 0 0;
- }
-
- &:last-child {
- .detailed-status,
- .status,
- .load-more {
- border-radius: 4px;
- }
- }
- }
-
- @media screen and (width <= 740px) {
- .detailed-status,
- .status,
- .load-more {
- border-radius: 0 !important;
- }
- }
- }
-
- &--highlighted .entry {
- background: lighten($ui-base-color, 8%);
- }
-}
-
-.button.logo-button svg {
- width: 20px;
- height: auto;
- vertical-align: middle;
- margin-inline-end: 5px;
- fill: $primary-text-color;
-
- @media screen and (max-width: $no-gap-breakpoint) {
- display: none;
- }
-}
-
-.embed {
- .status__content[data-spoiler='folded'] {
- .e-content {
- display: none;
- }
-
- p:first-child {
- margin-bottom: 0;
- }
- }
-
- .detailed-status {
- padding: 15px;
-
- .detailed-status__display-avatar .account__avatar {
- width: 48px;
- height: 48px;
- }
- }
-
- .status {
- padding: 15px;
- padding-inline-start: (48px + 15px * 2);
- min-height: 48px + 2px;
-
- &__avatar {
- inset-inline-start: 15px;
- top: 17px;
-
- .account__avatar {
- width: 48px;
- height: 48px;
- }
- }
-
- &__content {
- padding-top: 5px;
- }
-
- &__prepend {
- padding: 8px 0;
- padding-bottom: 2px;
- margin: initial;
- margin-inline-start: 48px + 15px * 2;
- padding-top: 15px;
- }
-
- &__prepend-icon-wrapper {
- position: absolute;
- margin: initial;
- float: initial;
- width: auto;
- inset-inline-start: -32px;
- }
-
- .media-gallery,
- &__action-bar,
- .video-player {
- margin-top: 10px;
- }
-
- &__action-bar-button {
- font-size: 18px;
- width: 23.1429px;
- height: 23.1429px;
- line-height: 23.15px;
- }
- }
-}
-
-// Styling from upstream's WebUI, as public pages use the same layout
-.embed {
- .status {
- .status__info {
- font-size: 15px;
- display: initial;
- }
-
- .status__relative-time {
- color: $dark-text-color;
- float: right;
- font-size: 14px;
- width: auto;
- margin: initial;
- padding: initial;
- padding-bottom: 1px;
- }
-
- .status__visibility-icon {
- padding: 0 4px;
-
- .icon {
- width: 1em;
- height: 1em;
- margin-bottom: -2px;
- }
- }
-
- .status__info .status__display-name {
- display: block;
- max-width: 100%;
- padding: 6px 0;
- padding-right: 25px;
- margin: initial;
- }
-
- .status__avatar {
- height: 48px;
- position: absolute;
- width: 48px;
- margin: initial;
- }
- }
-}
-
-.rtl {
- .embed {
- .status {
- padding-left: 10px;
- padding-right: 68px;
-
- .status__info .status__display-name {
- padding-left: 25px;
- padding-right: 0;
- }
-
- .status__relative-time,
- .status__visibility-icon {
- float: left;
- }
- }
- }
-}
-
-.status__content__read-more-button,
-.status__content__translate-button {
- display: flex;
- align-items: center;
- font-size: 15px;
- line-height: 20px;
- color: $highlight-text-color;
- border: 0;
- background: transparent;
- padding: 0;
- padding-top: 16px;
- text-decoration: none;
-
- &:hover,
- &:active {
- text-decoration: underline;
- }
-}