Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Change aspect ratios on link previews in web UI #26250

Merged
merged 1 commit into from Jul 30, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
82 changes: 29 additions & 53 deletions app/javascript/mastodon/features/status/components/card.jsx
Expand Up @@ -5,7 +5,7 @@ import { PureComponent } from 'react';

import { FormattedMessage } from 'react-intl';

import classnames from 'classnames';
import classNames from 'classnames';

import Immutable from 'immutable';
import ImmutablePropTypes from 'react-immutable-proptypes';
Expand Down Expand Up @@ -71,6 +71,7 @@ export default class Card extends PureComponent {
if (!Immutable.is(this.props.card, nextProps.card)) {
this.setState({ embedded: false, previewLoaded: false });
}

if (this.props.sensitive !== nextProps.sensitive) {
this.setState({ revealed: !nextProps.sensitive });
}
Expand All @@ -84,35 +85,8 @@ export default class Card extends PureComponent {
window.removeEventListener('resize', this.handleResize);
}

handlePhotoClick = () => {
const { card, onOpenMedia } = this.props;

onOpenMedia(
Immutable.fromJS([
{
type: 'image',
url: card.get('embed_url'),
description: card.get('title'),
meta: {
original: {
width: card.get('width'),
height: card.get('height'),
},
},
},
]),
0,
);
};

handleEmbedClick = () => {
const { card } = this.props;

if (card.get('type') === 'photo') {
this.handlePhotoClick();
} else {
this.setState({ embedded: true });
}
this.setState({ embedded: true });
};

setRef = c => {
Expand All @@ -130,15 +104,15 @@ export default class Card extends PureComponent {
};

renderVideo () {
const { card } = this.props;
const content = { __html: addAutoPlay(card.get('html')) };
const { card } = this.props;
const content = { __html: addAutoPlay(card.get('html')) };

return (
<div
ref={this.setRef}
className='status-card__image status-card-video'
dangerouslySetInnerHTML={content}
style={{ aspectRatio: `${card.get('width')} / ${card.get('height')}` }}
style={{ aspectRatio: '16 / 9' }}
/>
);
}
Expand All @@ -152,30 +126,40 @@ export default class Card extends PureComponent {
}

const provider = card.get('provider_name').length === 0 ? decodeIDNA(getHostname(card.get('url'))) : card.get('provider_name');
const interactive = card.get('type') !== 'link';
const interactive = card.get('type') === 'video';
const language = card.get('language') || '';
const largeImage = (card.get('image')?.length > 0 && card.get('width') > card.get('height')) || interactive;

const description = (
<div className='status-card__content'>
<span className='status-card__host'>
<span lang={language}>{provider}</span>
{card.get('published_at') && <> · <RelativeTimestamp timestamp={card.get('published_at')} /></>}
</span>
</span>

<strong className='status-card__title' title={card.get('title')} lang={language}>{card.get('title')}</strong>
{card.get('author_name').length > 0 && <span className='status-card__author'><FormattedMessage id='link_preview.author' defaultMessage='By {name}' values={{ name: <strong>{card.get('author_name')}</strong> }} /></span>}

{card.get('author_name').length > 0 ? <span className='status-card__author'><FormattedMessage id='link_preview.author' defaultMessage='By {name}' values={{ name: <strong>{card.get('author_name')}</strong> }} /></span> : <span className='status-card__description'>{card.get('description')}</span>}
</div>
);

const thumbnailStyle = {
visibility: revealed ? null : 'hidden',
aspectRatio: `${card.get('width')} / ${card.get('height')}`
};

if (largeImage && card.get('type') === 'video') {
thumbnailStyle.aspectRatio = `16 / 9`;
} else if (largeImage) {
thumbnailStyle.aspectRatio = '1.91 / 1';
} else {
thumbnailStyle.aspectRatio = 1;
}

let embed;

let canvas = (
<Blurhash
className={classnames('status-card__image-preview', {
className={classNames('status-card__image-preview', {
'status-card__image-preview--hidden': revealed && this.state.previewLoaded,
})}
hash={card.get('blurhash')}
Expand All @@ -195,7 +179,7 @@ export default class Card extends PureComponent {
);

spoilerButton = (
<div className={classnames('spoiler-button', { 'spoiler-button--minified': revealed })}>
<div className={classNames('spoiler-button', { 'spoiler-button--minified': revealed })}>
{spoilerButton}
</div>
);
Expand All @@ -204,33 +188,25 @@ export default class Card extends PureComponent {
if (embedded) {
embed = this.renderVideo();
} else {
let iconVariant = 'play';

if (card.get('type') === 'photo') {
iconVariant = 'search-plus';
}

embed = (
<div className='status-card__image'>
{canvas}
{thumbnail}

{revealed && (
<div className='status-card__actions'>
{revealed ? (
<div className='status-card__actions' onClick={this.handleEmbedClick} role='none'>
<div>
<button type='button' onClick={this.handleEmbedClick}><Icon id={iconVariant} /></button>
<button type='button' onClick={this.handleEmbedClick}><Icon id='play' /></button>
<a href={card.get('url')} target='_blank' rel='noopener noreferrer'><Icon id='external-link' /></a>
</div>
</div>
)}

{!revealed && spoilerButton}
) : spoilerButton}
</div>
);
}

return (
<div className='status-card' ref={this.setRef} onClick={revealed ? null : this.handleReveal} role={revealed ? 'button' : null}>
<div className={classNames('status-card', { expanded: largeImage })} ref={this.setRef} onClick={revealed ? null : this.handleReveal} role={revealed ? 'button' : null}>
{embed}
<a href={card.get('url')} target='_blank' rel='noopener noreferrer'>{description}</a>
</div>
Expand All @@ -244,14 +220,14 @@ export default class Card extends PureComponent {
);
} else {
embed = (
<div className='status-card__image' style={{ aspectRatio: '1.9 / 1' }}>
<div className='status-card__image'>
<Icon id='file-text' />
</div>
);
}

return (
<a href={card.get('url')} className='status-card' target='_blank' rel='noopener noreferrer' ref={this.setRef}>
<a href={card.get('url')} className={classNames('status-card', { expanded: largeImage })} target='_blank' rel='noopener noreferrer' ref={this.setRef}>
{embed}
{description}
</a>
Expand Down
76 changes: 68 additions & 8 deletions app/javascript/styles/mastodon/components.scss
Expand Up @@ -3510,13 +3510,16 @@ button.icon-button.active i.fa-retweet {
}

.status-card {
display: block;
display: flex;
align-items: center;
position: relative;
font-size: 14px;
color: $darker-text-color;
margin-top: 14px;
text-decoration: none;
overflow: hidden;
border: 1px solid lighten($ui-base-color, 8%);
border-radius: 8px;

&__actions {
bottom: 0;
Expand All @@ -3527,11 +3530,13 @@ button.icon-button.active i.fa-retweet {
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;

& > div {
background: rgba($base-shadow-color, 0.6);
border-radius: 8px;
padding: 12px 9px;
backdrop-filter: blur(10px) saturate(180%) contrast(75%) brightness(70%);
flex: 0 0 auto;
display: flex;
justify-content: center;
Expand Down Expand Up @@ -3572,7 +3577,8 @@ a.status-card {
&:active {
.status-card__title,
.status-card__host,
.status-card__author {
.status-card__author,
.status-card__description {
color: $highlight-text-color;
}
}
Expand All @@ -3587,7 +3593,8 @@ a.status-card {
&:active {
.status-card__title,
.status-card__host,
.status-card__author {
.status-card__author,
.status-card__description {
color: $highlight-text-color;
}
}
Expand Down Expand Up @@ -3620,37 +3627,64 @@ a.status-card {
line-height: 24px;
color: $primary-text-color;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}

.status-card.expanded .status-card__title {
white-space: normal;
-webkit-line-clamp: 2;
}

.status-card__content {
flex: 1 1 auto;
overflow: hidden;
padding: 15px 0;
padding-bottom: 0;
padding: 15px;
box-sizing: border-box;
max-width: 100%;
}

.status-card__host {
display: block;
font-size: 14px;
margin-bottom: 8px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}

.status-card__author {
display: block;
margin-top: 8px;
font-size: 14px;
color: $primary-text-color;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;

strong {
font-weight: 500;
}
}

.status-card__description {
display: block;
margin-top: 8px;
font-size: 14px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}

.status-card__image {
width: 100%;
flex: 0 0 auto;
width: 120px;
aspect-ratio: 1;
background: lighten($ui-base-color, 8%);
position: relative;
border-radius: 8px;
border-start-end-radius: 0;
border-end-end-radius: 0;

& > .fa {
font-size: 21px;
Expand All @@ -3664,6 +3698,8 @@ a.status-card {

.status-card__image-image {
border-radius: 8px;
border-start-end-radius: 0;
border-end-end-radius: 0;
display: block;
margin: 0;
width: 100%;
Expand All @@ -3675,6 +3711,8 @@ a.status-card {

.status-card__image-preview {
border-radius: 8px;
border-start-end-radius: 0;
border-end-end-radius: 0;
display: block;
margin: 0;
width: 100%;
Expand All @@ -3691,6 +3729,28 @@ a.status-card {
}
}

.status-card.expanded {
flex-direction: column;
align-items: flex-start;
}

.status-card.expanded .status-card__image {
width: 100%;
aspect-ratio: auto;
}

.status-card.expanded .status-card__image,
.status-card.expanded .status-card__image-image,
.status-card.expanded .status-card__image-preview {
border-start-end-radius: 8px;
border-end-end-radius: 0;
border-end-start-radius: 0;
}

.status-card.expanded > a {
width: 100%;
}

.load-more {
display: block;
color: $dark-text-color;
Expand Down Expand Up @@ -4902,7 +4962,7 @@ a.status-card {
width: 100%;
background: $ui-base-color;
border-radius: 0 0 4px 4px;
box-shadow: 4px 4px 6px rgba($base-shadow-color, 0.4);
box-shadow: var(--dropdown-shadow);
z-index: 99;
font-size: 13px;
padding: 15px 5px;
Expand Down Expand Up @@ -8218,7 +8278,7 @@ noscript {
flex: 0 0 auto;
position: relative;
width: 120px;
height: 120px;
aspect-ratio: 1;

.skeleton {
width: 100%;
Expand Down