Skip to content

Commit

Permalink
Change embedded posts to use web UI (mastodon#31766)
Browse files Browse the repository at this point in the history
Co-authored-by: Claire <claire.github-309c@sitedethib.com>
  • Loading branch information
Gargron and ClearlyClaire authored Sep 12, 2024
1 parent f2a92c2 commit 3d46f47
Show file tree
Hide file tree
Showing 115 changed files with 710 additions and 1,928 deletions.
8 changes: 0 additions & 8 deletions app/helpers/accounts_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,6 @@ def acct(account)
end
end

def account_action_button(account)
return if account.memorial? || account.moved?

link_to ActivityPub::TagManager.instance.url_for(account), class: 'button logo-button', target: '_new' do
safe_join([logo_as_symbol, t('accounts.follow')])
end
end

def account_formatted_stat(value)
number_to_human(value, precision: 3, strip_insignificant_zeros: true)
end
Expand Down
36 changes: 0 additions & 36 deletions app/helpers/media_component_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -57,26 +57,6 @@ def render_media_gallery_component(status, **options)
end
end

def render_card_component(status, **options)
component_params = {
sensitive: sensitive_viewer?(status, current_account),
card: serialize_status_card(status).as_json,
}.merge(**options)

react_component :card, component_params
end

def render_poll_component(status, **options)
component_params = {
disabled: true,
poll: serialize_status_poll(status).as_json,
}.merge(**options)

react_component :poll, component_params do
render partial: 'statuses/poll', locals: { status: status, poll: status.preloadable_poll, autoplay: prefers_autoplay? }
end
end

private

def serialize_media_attachment(attachment)
Expand All @@ -86,22 +66,6 @@ def serialize_media_attachment(attachment)
)
end

def serialize_status_card(status)
ActiveModelSerializers::SerializableResource.new(
status.preview_card,
serializer: REST::PreviewCardSerializer
)
end

def serialize_status_poll(status)
ActiveModelSerializers::SerializableResource.new(
status.preloadable_poll,
serializer: REST::PollSerializer,
scope: current_user,
scope_name: :current_user
)
end

def sensitive_viewer?(status, account)
if !account.nil? && account.id == status.account_id
status.sensitive
Expand Down
74 changes: 74 additions & 0 deletions app/javascript/entrypoints/embed.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import './public-path';
import { createRoot } from 'react-dom/client';

import { afterInitialRender } from 'mastodon/../hooks/useRenderSignal';

import { start } from '../mastodon/common';
import { Status } from '../mastodon/features/standalone/status';
import { loadPolyfills } from '../mastodon/polyfills';
import ready from '../mastodon/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 changes: 0 additions & 37 deletions app/javascript/entrypoints/public.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down
32 changes: 32 additions & 0 deletions app/javascript/hooks/useRenderSignal.ts
Original file line number Diff line number Diff line change
@@ -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();
});
}
};
};
6 changes: 4 additions & 2 deletions app/javascript/mastodon/actions/statuses.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
7 changes: 7 additions & 0 deletions app/javascript/mastodon/components/logo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@ export const WordmarkLogo: React.FC = () => (
</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 = () => (
<img src={logo} alt='Mastodon' className='logo logo--icon' />
);
6 changes: 2 additions & 4 deletions app/javascript/mastodon/components/more_from_author.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,12 @@ import PropTypes from 'prop-types';

import { FormattedMessage } from 'react-intl';

import { IconLogo } from 'mastodon/components/logo';
import { AuthorLink } from 'mastodon/features/explore/components/author_link';

export const MoreFromAuthor = ({ accountId }) => (
<div className='more-from-author'>
<svg viewBox='0 0 79 79' className='logo logo--icon' role='img'>
<use xlinkHref='#logo-symbol-icon' />
</svg>

<IconLogo />
<FormattedMessage id='link_preview.more_from_author' defaultMessage='More from {name}' values={{ name: <AuthorLink accountId={accountId} /> }} />
</div>
);
Expand Down
87 changes: 87 additions & 0 deletions app/javascript/mastodon/features/standalone/status/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/* 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 { useRenderSignal } from 'mastodon/../hooks/useRenderSignal';
import { fetchStatus, toggleStatusSpoilers } from 'mastodon/actions/statuses';
import { hydrateStore } from 'mastodon/actions/store';
import { Router } from 'mastodon/components/router';
import { DetailedStatus } from 'mastodon/features/status/components/detailed_status';
import initialState from 'mastodon/initial_state';
import { IntlProvider } from 'mastodon/locales';
import { makeGetStatus, makeGetPictureInPicture } from 'mastodon/selectors';
import { store, useAppSelector, useAppDispatch } from 'mastodon/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}
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>
);
};
Loading

0 comments on commit 3d46f47

Please sign in to comment.