[Glitch] Change embedded posts to use web UI
Port 3d46f47817
to glitch-soc
Co-authored-by: Claire <claire.github-309c@sitedethib.com>
Signed-off-by: Claire <claire.github-309c@sitedethib.com>
pull/2843/head
parent
3465d39494
commit
e705ec13db
|
@ -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) => {
|
return (dispatch, getState) => {
|
||||||
const skipLoading = !forceFetch && getState().getIn(['statuses', id], null) !== null;
|
const skipLoading = !forceFetch && getState().getIn(['statuses', id], null) !== null;
|
||||||
|
|
||||||
dispatch(fetchContext(id));
|
if (alsoFetchContext) {
|
||||||
|
dispatch(fetchContext(id));
|
||||||
|
}
|
||||||
|
|
||||||
if (skipLoading) {
|
if (skipLoading) {
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -7,6 +7,13 @@ export const WordmarkLogo: React.FC = () => (
|
||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const IconLogo: React.FC = () => (
|
||||||
|
<svg viewBox='0 0 79 79' className='logo logo--icon' role='img'>
|
||||||
|
<title>Mastodon</title>
|
||||||
|
<use xlinkHref='#logo-symbol-icon' />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
export const SymbolLogo: React.FC = () => (
|
export const SymbolLogo: React.FC = () => (
|
||||||
<img src={logo} alt='Mastodon' className='logo logo--icon' />
|
<img src={logo} alt='Mastodon' className='logo logo--icon' />
|
||||||
);
|
);
|
||||||
|
|
|
@ -2,14 +2,12 @@ import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
|
import { IconLogo } from 'flavours/glitch/components/logo';
|
||||||
import { AuthorLink } from 'flavours/glitch/features/explore/components/author_link';
|
import { AuthorLink } from 'flavours/glitch/features/explore/components/author_link';
|
||||||
|
|
||||||
export const MoreFromAuthor = ({ accountId }) => (
|
export const MoreFromAuthor = ({ accountId }) => (
|
||||||
<div className='more-from-author'>
|
<div className='more-from-author'>
|
||||||
<svg viewBox='0 0 79 79' className='logo logo--icon' role='img'>
|
<IconLogo />
|
||||||
<use xlinkHref='#logo-symbol-icon' />
|
|
||||||
</svg>
|
|
||||||
|
|
||||||
<FormattedMessage id='link_preview.more_from_author' defaultMessage='More from {name}' values={{ name: <AuthorLink accountId={accountId} /> }} />
|
<FormattedMessage id='link_preview.more_from_author' defaultMessage='More from {name}' values={{ name: <AuthorLink accountId={accountId} /> }} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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(<Status {...props} />);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
'*',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
|
@ -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() {
|
function loaded() {
|
||||||
const { messages: localeData } = getLocale();
|
const { messages: localeData } = getLocale();
|
||||||
|
|
||||||
|
|
|
@ -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 (
|
||||||
|
<div className='embed'>
|
||||||
|
<DetailedStatus
|
||||||
|
status={status}
|
||||||
|
domain={domain}
|
||||||
|
pictureInPicture={pictureInPicture}
|
||||||
|
onToggleHidden={handleToggleHidden}
|
||||||
|
expanded={false}
|
||||||
|
withLogo
|
||||||
|
/>
|
||||||
|
|
||||||
|
<a
|
||||||
|
className='embed__overlay'
|
||||||
|
href={permalink}
|
||||||
|
target='_blank'
|
||||||
|
rel='noreferrer noopener'
|
||||||
|
aria-label=''
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Status: React.FC<{ id: string }> = ({ id }) => {
|
||||||
|
useEffect(() => {
|
||||||
|
if (initialState) {
|
||||||
|
store.dispatch(hydrateStore(initialState));
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<IntlProvider>
|
||||||
|
<Provider store={store}>
|
||||||
|
<Router>
|
||||||
|
<Embed id={id} />
|
||||||
|
</Router>
|
||||||
|
</Provider>
|
||||||
|
</IntlProvider>
|
||||||
|
);
|
||||||
|
};
|
|
@ -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(<PictureInPicturePlaceholder />);
|
|
||||||
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(<AttachmentList media={status.get('media_attachments')} />);
|
|
||||||
} 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(
|
|
||||||
<Audio
|
|
||||||
src={attachment.get('url')}
|
|
||||||
alt={description}
|
|
||||||
lang={language}
|
|
||||||
duration={attachment.getIn(['meta', 'original', 'duration'], 0)}
|
|
||||||
poster={attachment.get('preview_url') || status.getIn(['account', 'avatar_static'])}
|
|
||||||
backgroundColor={attachment.getIn(['meta', 'colors', 'background'])}
|
|
||||||
foregroundColor={attachment.getIn(['meta', 'colors', 'foreground'])}
|
|
||||||
accentColor={attachment.getIn(['meta', 'colors', 'accent'])}
|
|
||||||
sensitive={status.get('sensitive')}
|
|
||||||
visible={this.props.showMedia}
|
|
||||||
blurhash={attachment.get('blurhash')}
|
|
||||||
height={150}
|
|
||||||
onToggleVisibility={this.props.onToggleMediaVisibility}
|
|
||||||
/>,
|
|
||||||
);
|
|
||||||
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(
|
|
||||||
<Video
|
|
||||||
preview={attachment.get('preview_url')}
|
|
||||||
frameRate={attachment.getIn(['meta', 'original', 'frame_rate'])}
|
|
||||||
blurhash={attachment.get('blurhash')}
|
|
||||||
src={attachment.get('url')}
|
|
||||||
alt={description}
|
|
||||||
lang={language}
|
|
||||||
inline
|
|
||||||
sensitive={status.get('sensitive')}
|
|
||||||
letterbox={settings.getIn(['media', 'letterbox'])}
|
|
||||||
fullwidth={settings.getIn(['media', 'fullwidth'])}
|
|
||||||
preventPlayback={!expanded}
|
|
||||||
onOpenVideo={this.handleOpenVideo}
|
|
||||||
autoplay
|
|
||||||
visible={this.props.showMedia}
|
|
||||||
onToggleVisibility={this.props.onToggleMediaVisibility}
|
|
||||||
/>,
|
|
||||||
);
|
|
||||||
mediaIcons.push('video-camera');
|
|
||||||
} else {
|
|
||||||
media.push(
|
|
||||||
<MediaGallery
|
|
||||||
standalone
|
|
||||||
sensitive={status.get('sensitive')}
|
|
||||||
media={status.get('media_attachments')}
|
|
||||||
lang={language}
|
|
||||||
letterbox={settings.getIn(['media', 'letterbox'])}
|
|
||||||
fullwidth={settings.getIn(['media', 'fullwidth'])}
|
|
||||||
hidden={!expanded}
|
|
||||||
onOpenMedia={this.props.onOpenMedia}
|
|
||||||
visible={this.props.showMedia}
|
|
||||||
onToggleVisibility={this.props.onToggleMediaVisibility}
|
|
||||||
/>,
|
|
||||||
);
|
|
||||||
mediaIcons.push('picture-o');
|
|
||||||
}
|
|
||||||
} else if (status.get('card')) {
|
|
||||||
media.push(<Card sensitive={status.get('sensitive')} onOpenMedia={this.props.onOpenMedia} card={status.get('card')} />);
|
|
||||||
mediaIcons.push('link');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (status.get('poll')) {
|
|
||||||
contentMedia.push(<PollContainer pollId={status.get('poll')} lang={status.get('language')} />);
|
|
||||||
contentMediaIcons.push('tasks');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (status.get('application')) {
|
|
||||||
applicationLink = <>·<a className='detailed-status__application' href={status.getIn(['application', 'website'])} target='_blank' rel='noopener noreferrer'>{status.getIn(['application', 'name'])}</a></>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const visibilityLink = <>·<VisibilityIcon visibility={status.get('visibility')} /></>;
|
|
||||||
|
|
||||||
if (!['unlisted', 'public'].includes(status.get('visibility'))) {
|
|
||||||
reblogLink = null;
|
|
||||||
} else if (this.props.history) {
|
|
||||||
reblogLink = (
|
|
||||||
<Link to={`/@${status.getIn(['account', 'acct'])}/${status.get('id')}/reblogs`} className='detailed-status__link'>
|
|
||||||
<span className='detailed-status__reblogs'>
|
|
||||||
<AnimatedNumber value={status.get('reblogs_count')} />
|
|
||||||
</span>
|
|
||||||
<FormattedMessage id='status.reblogs' defaultMessage='{count, plural, one {boost} other {boosts}}' values={{ count: status.get('reblogs_count') }} />
|
|
||||||
</Link>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
reblogLink = (
|
|
||||||
<a href={`/interact/${status.get('id')}?type=reblog`} className='detailed-status__link' onClick={this.handleModalLink}>
|
|
||||||
<span className='detailed-status__reblogs'>
|
|
||||||
<AnimatedNumber value={status.get('reblogs_count')} />
|
|
||||||
</span>
|
|
||||||
<FormattedMessage id='status.reblogs' defaultMessage='{count, plural, one {boost} other {boosts}}' values={{ count: status.get('reblogs_count') }} />
|
|
||||||
</a>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.props.history) {
|
|
||||||
favouriteLink = (
|
|
||||||
<Link to={`/@${status.getIn(['account', 'acct'])}/${status.get('id')}/favourites`} className='detailed-status__link'>
|
|
||||||
<span className='detailed-status__favorites'>
|
|
||||||
<AnimatedNumber value={status.get('favourites_count')} />
|
|
||||||
</span>
|
|
||||||
<FormattedMessage id='status.favourites' defaultMessage='{count, plural, one {favorite} other {favorites}}' values={{ count: status.get('favourites_count') }} />
|
|
||||||
</Link>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
favouriteLink = (
|
|
||||||
<a href={`/interact/${status.get('id')}?type=favourite`} className='detailed-status__link' onClick={this.handleModalLink}>
|
|
||||||
<span className='detailed-status__favorites'>
|
|
||||||
<AnimatedNumber value={status.get('favourites_count')} />
|
|
||||||
</span>
|
|
||||||
<FormattedMessage id='status.favourites' defaultMessage='{count, plural, one {favorite} other {favorites}}' values={{ count: status.get('favourites_count') }} />
|
|
||||||
</a>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const {statusContentProps, hashtagBar} = getHashtagBarForStatus(status);
|
|
||||||
contentMedia.push(hashtagBar);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div style={outerStyle}>
|
|
||||||
<div ref={this.setRef} className={classNames('detailed-status', `detailed-status-${status.get('visibility')}`, { compact })} data-status-by={status.getIn(['account', 'acct'])}>
|
|
||||||
<a href={status.getIn(['account', 'url'])} data-hover-card-account={status.getIn(['account', 'id'])} onClick={this.handleAccountClick} className='detailed-status__display-name'>
|
|
||||||
<div className='detailed-status__display-avatar'><Avatar account={status.get('account')} size={48} /></div>
|
|
||||||
<DisplayName account={status.get('account')} localDomain={this.props.domain} />
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<StatusContent
|
|
||||||
status={status}
|
|
||||||
media={contentMedia}
|
|
||||||
extraMedia={extraMedia}
|
|
||||||
mediaIcons={contentMediaIcons}
|
|
||||||
expanded={expanded}
|
|
||||||
collapsed={false}
|
|
||||||
onExpandedToggle={onToggleHidden}
|
|
||||||
onTranslate={this.handleTranslate}
|
|
||||||
parseClick={this.parseClick}
|
|
||||||
onUpdate={this.handleChildUpdate}
|
|
||||||
tagLinks={settings.get('tag_misleading_links')}
|
|
||||||
rewriteMentions={settings.get('rewrite_mentions')}
|
|
||||||
disabled
|
|
||||||
{...statusContentProps}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className='detailed-status__meta'>
|
|
||||||
<div className='detailed-status__meta__line'>
|
|
||||||
<a className='detailed-status__datetime' href={status.get('url')} target='_blank' rel='noopener noreferrer'>
|
|
||||||
<FormattedDate value={new Date(status.get('created_at'))} year='numeric' month='short' day='2-digit' hour='2-digit' minute='2-digit' />
|
|
||||||
</a>
|
|
||||||
|
|
||||||
{visibilityLink}
|
|
||||||
|
|
||||||
{applicationLink}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{status.get('edited_at') && <div className='detailed-status__meta__line'><EditedTimestamp statusId={status.get('id')} timestamp={status.get('edited_at')} /></div>}
|
|
||||||
|
|
||||||
<div className='detailed-status__meta__line'>
|
|
||||||
{reblogLink}
|
|
||||||
{reblogLink && <>·</>}
|
|
||||||
{favouriteLink}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
export default withRouter(DetailedStatus);
|
|
|
@ -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<HTMLDivElement>();
|
||||||
|
|
||||||
|
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(<PictureInPicturePlaceholder />);
|
||||||
|
mediaIcons.push('video-camera');
|
||||||
|
} else if (status.get('media_attachments').size > 0) {
|
||||||
|
if (
|
||||||
|
status
|
||||||
|
.get('media_attachments')
|
||||||
|
.some(
|
||||||
|
(item: Immutable.Map<string, any>) => item.get('type') === 'unknown',
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
media.push(<AttachmentList media={status.get('media_attachments')} />);
|
||||||
|
} 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(
|
||||||
|
<Audio
|
||||||
|
src={attachment.get('url')}
|
||||||
|
alt={description}
|
||||||
|
lang={language}
|
||||||
|
duration={attachment.getIn(['meta', 'original', 'duration'], 0)}
|
||||||
|
poster={
|
||||||
|
attachment.get('preview_url') ||
|
||||||
|
status.getIn(['account', 'avatar_static'])
|
||||||
|
}
|
||||||
|
backgroundColor={attachment.getIn(['meta', 'colors', 'background'])}
|
||||||
|
foregroundColor={attachment.getIn(['meta', 'colors', 'foreground'])}
|
||||||
|
accentColor={attachment.getIn(['meta', 'colors', 'accent'])}
|
||||||
|
sensitive={status.get('sensitive')}
|
||||||
|
visible={showMedia}
|
||||||
|
blurhash={attachment.get('blurhash')}
|
||||||
|
height={150}
|
||||||
|
onToggleVisibility={onToggleMediaVisibility}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
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(
|
||||||
|
<Video
|
||||||
|
preview={attachment.get('preview_url')}
|
||||||
|
frameRate={attachment.getIn(['meta', 'original', 'frame_rate'])}
|
||||||
|
aspectRatio={`${attachment.getIn(['meta', 'original', 'width'])} / ${attachment.getIn(['meta', 'original', 'height'])}`}
|
||||||
|
blurhash={attachment.get('blurhash')}
|
||||||
|
src={attachment.get('url')}
|
||||||
|
alt={description}
|
||||||
|
lang={language}
|
||||||
|
width={300}
|
||||||
|
height={150}
|
||||||
|
onOpenVideo={handleOpenVideo}
|
||||||
|
sensitive={status.get('sensitive')}
|
||||||
|
visible={showMedia}
|
||||||
|
onToggleVisibility={onToggleMediaVisibility}
|
||||||
|
letterbox={letterboxMedia}
|
||||||
|
fullwidth={fullwidthMedia}
|
||||||
|
preventPlayback={!expanded}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
mediaIcons.push('video-camera');
|
||||||
|
} else {
|
||||||
|
media.push(
|
||||||
|
<MediaGallery
|
||||||
|
standalone
|
||||||
|
sensitive={status.get('sensitive')}
|
||||||
|
media={status.get('media_attachments')}
|
||||||
|
lang={language}
|
||||||
|
height={300}
|
||||||
|
letterbox={letterboxMedia}
|
||||||
|
fullwidth={fullwidthMedia}
|
||||||
|
hidden={!expanded}
|
||||||
|
onOpenMedia={onOpenMedia}
|
||||||
|
visible={showMedia}
|
||||||
|
onToggleVisibility={onToggleMediaVisibility}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
mediaIcons.push('picture-o');
|
||||||
|
}
|
||||||
|
} else if (status.get('spoiler_text').length === 0) {
|
||||||
|
media.push(
|
||||||
|
<Card
|
||||||
|
sensitive={status.get('sensitive')}
|
||||||
|
onOpenMedia={onOpenMedia}
|
||||||
|
card={status.get('card', null)}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
mediaIcons.push('link');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status.get('application')) {
|
||||||
|
applicationLink = (
|
||||||
|
<>
|
||||||
|
·
|
||||||
|
<a
|
||||||
|
className='detailed-status__application'
|
||||||
|
href={status.getIn(['application', 'website'])}
|
||||||
|
target='_blank'
|
||||||
|
rel='noopener noreferrer'
|
||||||
|
>
|
||||||
|
{status.getIn(['application', 'name'])}
|
||||||
|
</a>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const visibilityLink = (
|
||||||
|
<>
|
||||||
|
·<VisibilityIcon visibility={status.get('visibility')} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (['private', 'direct'].includes(status.get('visibility') as string)) {
|
||||||
|
reblogLink = '';
|
||||||
|
} else {
|
||||||
|
reblogLink = (
|
||||||
|
<Link
|
||||||
|
to={`/@${status.getIn(['account', 'acct'])}/${status.get('id')}/reblogs`}
|
||||||
|
className='detailed-status__link'
|
||||||
|
>
|
||||||
|
<span className='detailed-status__reblogs'>
|
||||||
|
<AnimatedNumber value={status.get('reblogs_count')} />
|
||||||
|
</span>
|
||||||
|
<FormattedMessage
|
||||||
|
id='status.reblogs'
|
||||||
|
defaultMessage='{count, plural, one {boost} other {boosts}}'
|
||||||
|
values={{ count: status.get('reblogs_count') }}
|
||||||
|
/>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const favouriteLink = (
|
||||||
|
<Link
|
||||||
|
to={`/@${status.getIn(['account', 'acct'])}/${status.get('id')}/favourites`}
|
||||||
|
className='detailed-status__link'
|
||||||
|
>
|
||||||
|
<span className='detailed-status__favorites'>
|
||||||
|
<AnimatedNumber value={status.get('favourites_count')} />
|
||||||
|
</span>
|
||||||
|
<FormattedMessage
|
||||||
|
id='status.favourites'
|
||||||
|
defaultMessage='{count, plural, one {favorite} other {favorites}}'
|
||||||
|
values={{ count: status.get('favourites_count') }}
|
||||||
|
/>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
|
||||||
|
const { statusContentProps, hashtagBar } = getHashtagBarForStatus(
|
||||||
|
status as StatusLike,
|
||||||
|
);
|
||||||
|
contentMedia.push(hashtagBar);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={outerStyle}>
|
||||||
|
<div
|
||||||
|
ref={handleRef}
|
||||||
|
className={classNames(
|
||||||
|
'detailed-status',
|
||||||
|
`detailed-status-${status.get('visibility')}`,
|
||||||
|
)}
|
||||||
|
data-status-by={status.getIn(['account', 'acct'])}
|
||||||
|
>
|
||||||
|
<Permalink
|
||||||
|
to={`/@${status.getIn(['account', 'acct'])}`}
|
||||||
|
href={status.getIn(['account', 'url'])}
|
||||||
|
data-hover-card-account={status.getIn(['account', 'id'])}
|
||||||
|
className='detailed-status__display-name'
|
||||||
|
>
|
||||||
|
<div className='detailed-status__display-avatar'>
|
||||||
|
<Avatar account={status.get('account')} size={46} />
|
||||||
|
</div>
|
||||||
|
<DisplayName account={status.get('account')} localDomain={domain} />
|
||||||
|
{withLogo && (
|
||||||
|
<>
|
||||||
|
<div className='spacer' />
|
||||||
|
<IconLogo />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Permalink>
|
||||||
|
|
||||||
|
{/* TODO: parseClick={this.parseClick} */}
|
||||||
|
<StatusContent
|
||||||
|
status={status}
|
||||||
|
media={contentMedia}
|
||||||
|
extraMedia={extraMedia}
|
||||||
|
mediaIcons={contentMediaIcons}
|
||||||
|
expanded={expanded}
|
||||||
|
collapsed={false}
|
||||||
|
onExpandedToggle={onToggleHidden}
|
||||||
|
onTranslate={handleTranslate}
|
||||||
|
onUpdate={handleChildUpdate}
|
||||||
|
tagLinks={tagMisleadingLinks}
|
||||||
|
rewriteMentions={rewriteMentions}
|
||||||
|
{...(statusContentProps as any)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className='detailed-status__meta'>
|
||||||
|
<div className='detailed-status__meta__line'>
|
||||||
|
<a
|
||||||
|
className='detailed-status__datetime'
|
||||||
|
href={`/@${status.getIn(['account', 'acct'])}/${status.get('id')}`}
|
||||||
|
target='_blank'
|
||||||
|
rel='noopener noreferrer'
|
||||||
|
>
|
||||||
|
<FormattedDate
|
||||||
|
value={new Date(status.get('created_at') as string)}
|
||||||
|
year='numeric'
|
||||||
|
month='short'
|
||||||
|
day='2-digit'
|
||||||
|
hour='2-digit'
|
||||||
|
minute='2-digit'
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
{visibilityLink}
|
||||||
|
{applicationLink}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{status.get('edited_at') && (
|
||||||
|
<div className='detailed-status__meta__line'>
|
||||||
|
<EditedTimestamp
|
||||||
|
statusId={status.get('id')}
|
||||||
|
timestamp={status.get('edited_at')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className='detailed-status__meta__line'>
|
||||||
|
{reblogLink}
|
||||||
|
{reblogLink && <>·</>}
|
||||||
|
{favouriteLink}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
|
@ -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));
|
|
|
@ -63,7 +63,7 @@ import Column from '../ui/components/column';
|
||||||
import { attachFullscreenListener, detachFullscreenListener, isFullscreen } from '../ui/util/fullscreen';
|
import { attachFullscreenListener, detachFullscreenListener, isFullscreen } from '../ui/util/fullscreen';
|
||||||
|
|
||||||
import ActionBar from './components/action_bar';
|
import ActionBar from './components/action_bar';
|
||||||
import DetailedStatus from './components/detailed_status';
|
import { DetailedStatus } from './components/detailed_status';
|
||||||
|
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
|
|
|
@ -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();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
|
@ -11,7 +11,6 @@
|
||||||
@import 'widgets';
|
@import 'widgets';
|
||||||
@import 'forms';
|
@import 'forms';
|
||||||
@import 'accounts';
|
@import 'accounts';
|
||||||
@import 'statuses';
|
|
||||||
@import 'components';
|
@import 'components';
|
||||||
@import 'polls';
|
@import 'polls';
|
||||||
@import 'modal';
|
@import 'modal';
|
||||||
|
|
|
@ -1849,18 +1849,6 @@ body > [data-popper-placement] {
|
||||||
padding: 14px 10px; // glitch: reduced padding
|
padding: 14px 10px; // glitch: reduced padding
|
||||||
border-top: 1px solid var(--background-border-color);
|
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 {
|
.status__content {
|
||||||
font-size: 19px;
|
font-size: 19px;
|
||||||
line-height: 24px;
|
line-height: 24px;
|
||||||
|
@ -1887,6 +1875,29 @@ body > [data-popper-placement] {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin-bottom: 16px;
|
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 {
|
.scrollable > div:first-child .detailed-status {
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in New Issue