[Glitch] Change account gallery in web UI

Port 3f143606fa to glitch-soc

Signed-off-by: Thibaut Girka <thib@sitedethib.com>
pull/1032/head
Eugen Rochko 2019-05-02 08:34:32 +02:00 committed by Thibaut Girka
parent 373dd1fdf1
commit a5da59f140
5 changed files with 166 additions and 119 deletions

View File

@ -177,7 +177,7 @@ class Item extends React.PureComponent {
if (attachment.get('type') === 'unknown') { if (attachment.get('type') === 'unknown') {
return ( return (
<div className={classNames('media-gallery__item', { standalone })} key={attachment.get('id')} style={{ left: left, top: top, right: right, bottom: bottom, width: `${width}%`, height: `${height}%` }}> <div className={classNames('media-gallery__item', { standalone })} key={attachment.get('id')} style={{ left: left, top: top, right: right, bottom: bottom, width: `${width}%`, height: `${height}%` }}>
<a className='media-gallery__item-thumbnail' href={attachment.get('remote_url')} target='_blank' style={{ cursor: 'pointer' }} > <a className='media-gallery__item-thumbnail' href={attachment.get('remote_url')} target='_blank' style={{ cursor: 'pointer' }}>
<canvas width={32} height={32} ref={this.setCanvasRef} className='media-gallery__preview' /> <canvas width={32} height={32} ref={this.setCanvasRef} className='media-gallery__preview' />
</a> </a>
</div> </div>

View File

@ -1,69 +1,141 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePureComponent from 'react-immutable-pure-component';
import Permalink from 'flavours/glitch/components/permalink'; import { autoPlayGif, displayMedia } from 'flavours/glitch/util/initial_state';
import { displayMedia } from 'flavours/glitch/util/initial_state'; import classNames from 'classnames';
import { decode } from 'blurhash';
import { isIOS } from 'flavours/glitch/util/is_mobile';
export default class MediaItem extends ImmutablePureComponent { export default class MediaItem extends ImmutablePureComponent {
static propTypes = { static propTypes = {
media: ImmutablePropTypes.map.isRequired, attachment: ImmutablePropTypes.map.isRequired,
displayWidth: PropTypes.number.isRequired,
onOpenMedia: PropTypes.func.isRequired,
}; };
state = { state = {
visible: displayMedia !== 'hide_all' && !this.props.media.getIn(['status', 'sensitive']) || displayMedia === 'show_all', visible: displayMedia !== 'hide_all' && !this.props.attachment.getIn(['status', 'sensitive']) || displayMedia === 'show_all',
loaded: false,
}; };
handleClick = () => { componentDidMount () {
if (!this.state.visible) { if (this.props.attachment.get('blurhash')) {
this.setState({ visible: true }); this._decode();
return true; }
} }
return false; componentDidUpdate (prevProps) {
if (prevProps.attachment.get('blurhash') !== this.props.attachment.get('blurhash') && this.props.attachment.get('blurhash')) {
this._decode();
}
}
_decode () {
const hash = this.props.attachment.get('blurhash');
const pixels = decode(hash, 32, 32);
if (pixels) {
const ctx = this.canvas.getContext('2d');
const imageData = new ImageData(pixels, 32, 32);
ctx.putImageData(imageData, 0, 0);
}
}
setCanvasRef = c => {
this.canvas = c;
}
handleImageLoad = () => {
this.setState({ loaded: true });
}
handleMouseEnter = e => {
if (this.hoverToPlay()) {
e.target.play();
}
}
handleMouseLeave = e => {
if (this.hoverToPlay()) {
e.target.pause();
e.target.currentTime = 0;
}
}
hoverToPlay () {
return !autoPlayGif && ['gifv', 'video'].indexOf(this.props.attachment.get('type')) !== -1;
}
handleClick = e => {
if (e.button === 0 && !(e.ctrlKey || e.metaKey)) {
e.preventDefault();
if (this.state.visible) {
this.props.onOpenMedia(this.props.attachment);
} else {
this.setState({ visible: true });
}
}
} }
render () { render () {
const { media } = this.props; const { attachment, displayWidth } = this.props;
const { visible } = this.state; const { visible, loaded } = this.state;
const status = media.get('status');
const focusX = media.getIn(['meta', 'focus', 'x']); const width = `${Math.floor((displayWidth - 4) / 3) - 4}px`;
const focusY = media.getIn(['meta', 'focus', 'y']); const height = width;
const status = attachment.get('status');
let thumbnail = '';
if (attachment.get('type') === 'unknown') {
// Skip
} else if (attachment.get('type') === 'image') {
const focusX = attachment.getIn(['meta', 'focus', 'x']) || 0;
const focusY = attachment.getIn(['meta', 'focus', 'y']) || 0;
const x = ((focusX / 2) + .5) * 100; const x = ((focusX / 2) + .5) * 100;
const y = ((focusY / -2) + .5) * 100; const y = ((focusY / -2) + .5) * 100;
const style = {};
let label, icon, title; thumbnail = (
<img
if (media.get('type') === 'gifv') { src={attachment.get('preview_url')}
label = <span className='media-gallery__gifv__label'>GIF</span>; alt={attachment.get('description')}
} title={attachment.get('description')}
style={{ objectPosition: `${x}% ${y}%` }}
if (visible) { onLoad={this.handleImageLoad}
style.backgroundImage = `url(${media.get('preview_url')})`; />
style.backgroundPosition = `${x}% ${y}%`; );
title = media.get('description'); } else if (['gifv', 'video'].indexOf(attachment.get('type')) !== -1) {
} else { const autoPlay = !isIOS() && autoPlayGif;
icon = (
<span className='account-gallery__item__icons'> thumbnail = (
<i className='fa fa-eye-slash' /> <div className={classNames('media-gallery__gifv', { autoplay: autoPlay })}>
</span> <video
className='media-gallery__item-gifv-thumbnail'
aria-label={attachment.get('description')}
title={attachment.get('description')}
role='application'
src={attachment.get('url')}
onMouseEnter={this.handleMouseEnter}
onMouseLeave={this.handleMouseLeave}
autoPlay={autoPlay}
loop
muted
/>
<span className='media-gallery__gifv__label'>GIF</span>
</div>
); );
title = status.get('spoiler_text') || media.get('description');
} }
return ( return (
<div className='account-gallery__item'> <div className='account-gallery__item' style={{ width, height }}>
<Permalink <a className='media-gallery__item-thumbnail' href={status.get('url')} target='_blank' style={{ cursor: 'pointer' }} onClick={this.handleClick}>
to={`/statuses/${status.get('id')}`} <canvas width={32} height={32} ref={this.setCanvasRef} className={classNames('media-gallery__preview', { 'media-gallery__preview--hidden': visible && loaded })} />
href={status.get('url')} {visible && thumbnail}
style={style} </a>
title={title}
onInterceptClick={this.handleClick}
>
{icon}
{label}
</Permalink>
</div> </div>
); );
} }

View File

@ -14,10 +14,11 @@ import HeaderContainer from 'flavours/glitch/features/account_timeline/container
import { ScrollContainer } from 'react-router-scroll-4'; import { ScrollContainer } from 'react-router-scroll-4';
import LoadMore from 'flavours/glitch/components/load_more'; import LoadMore from 'flavours/glitch/components/load_more';
import MissingIndicator from 'flavours/glitch/components/missing_indicator'; import MissingIndicator from 'flavours/glitch/components/missing_indicator';
import { openModal } from 'flavours/glitch/actions/modal';
const mapStateToProps = (state, props) => ({ const mapStateToProps = (state, props) => ({
isAccount: !!state.getIn(['accounts', props.params.accountId]), isAccount: !!state.getIn(['accounts', props.params.accountId]),
medias: getAccountGallery(state, props.params.accountId), attachments: getAccountGallery(state, props.params.accountId),
isLoading: state.getIn(['timelines', `account:${props.params.accountId}:media`, 'isLoading']), isLoading: state.getIn(['timelines', `account:${props.params.accountId}:media`, 'isLoading']),
hasMore: state.getIn(['timelines', `account:${props.params.accountId}:media`, 'hasMore']), hasMore: state.getIn(['timelines', `account:${props.params.accountId}:media`, 'hasMore']),
}); });
@ -50,12 +51,16 @@ export default class AccountGallery extends ImmutablePureComponent {
static propTypes = { static propTypes = {
params: PropTypes.object.isRequired, params: PropTypes.object.isRequired,
dispatch: PropTypes.func.isRequired, dispatch: PropTypes.func.isRequired,
medias: ImmutablePropTypes.list.isRequired, attachments: ImmutablePropTypes.list.isRequired,
isLoading: PropTypes.bool, isLoading: PropTypes.bool,
hasMore: PropTypes.bool, hasMore: PropTypes.bool,
isAccount: PropTypes.bool, isAccount: PropTypes.bool,
}; };
state = {
width: 323,
};
componentDidMount () { componentDidMount () {
this.props.dispatch(fetchAccount(this.props.params.accountId)); this.props.dispatch(fetchAccount(this.props.params.accountId));
this.props.dispatch(expandAccountMediaTimeline(this.props.params.accountId)); this.props.dispatch(expandAccountMediaTimeline(this.props.params.accountId));
@ -74,11 +79,11 @@ export default class AccountGallery extends ImmutablePureComponent {
handleScrollToBottom = () => { handleScrollToBottom = () => {
if (this.props.hasMore) { if (this.props.hasMore) {
this.handleLoadMore(this.props.medias.size > 0 ? this.props.medias.last().getIn(['status', 'id']) : undefined); this.handleLoadMore(this.props.attachments.size > 0 ? this.props.attachments.last().getIn(['status', 'id']) : undefined);
} }
} }
handleScroll = (e) => { handleScroll = e => {
const { scrollTop, scrollHeight, clientHeight } = e.target; const { scrollTop, scrollHeight, clientHeight } = e.target;
const offset = scrollHeight - scrollTop - clientHeight; const offset = scrollHeight - scrollTop - clientHeight;
@ -91,7 +96,7 @@ export default class AccountGallery extends ImmutablePureComponent {
this.props.dispatch(expandAccountMediaTimeline(this.props.params.accountId, { maxId })); this.props.dispatch(expandAccountMediaTimeline(this.props.params.accountId, { maxId }));
}; };
handleLoadOlder = (e) => { handleLoadOlder = e => {
e.preventDefault(); e.preventDefault();
this.handleScrollToBottom(); this.handleScrollToBottom();
} }
@ -101,12 +106,30 @@ export default class AccountGallery extends ImmutablePureComponent {
return !(location.state && location.state.mastodonModalOpen); return !(location.state && location.state.mastodonModalOpen);
} }
setRef = c => { setColumnRef = c => {
this.column = c; this.column = c;
} }
handleOpenMedia = attachment => {
if (attachment.get('type') === 'video') {
this.props.dispatch(openModal('VIDEO', { media: attachment }));
} else {
const media = attachment.getIn(['status', 'media_attachments']);
const index = media.findIndex(x => x.get('id') === attachment.get('id'));
this.props.dispatch(openModal('MEDIA', { media, index }));
}
}
handleRef = c => {
if (c) {
this.setState({ width: c.offsetWidth });
}
}
render () { render () {
const { medias, isLoading, hasMore, isAccount } = this.props; const { attachments, isLoading, hasMore, isAccount } = this.props;
const { width } = this.state;
if (!isAccount) { if (!isAccount) {
return ( return (
@ -116,9 +139,7 @@ export default class AccountGallery extends ImmutablePureComponent {
); );
} }
let loadOlder = null; if (!attachments && isLoading) {
if (!medias && isLoading) {
return ( return (
<Column> <Column>
<LoadingIndicator /> <LoadingIndicator />
@ -126,35 +147,31 @@ export default class AccountGallery extends ImmutablePureComponent {
); );
} }
if (hasMore && !(isLoading && medias.size === 0)) { let loadOlder = null;
if (hasMore && !(isLoading && attachments.size === 0)) {
loadOlder = <LoadMore visible={!isLoading} onClick={this.handleLoadOlder} />; loadOlder = <LoadMore visible={!isLoading} onClick={this.handleLoadOlder} />;
} }
return ( return (
<Column ref={this.setRef}> <Column ref={this.setColumnRef}>
<ProfileColumnHeader onClick={this.handleHeaderClick} /> <ProfileColumnHeader onClick={this.handleHeaderClick} />
<ScrollContainer scrollKey='account_gallery' shouldUpdateScroll={this.shouldUpdateScroll}> <ScrollContainer scrollKey='account_gallery' shouldUpdateScroll={this.shouldUpdateScroll}>
<div className='scrollable scrollable--flex' onScroll={this.handleScroll}> <div className='scrollable scrollable--flex' onScroll={this.handleScroll}>
<HeaderContainer accountId={this.props.params.accountId} /> <HeaderContainer accountId={this.props.params.accountId} />
<div role='feed' className='account-gallery__container'> <div role='feed' className='account-gallery__container' ref={this.handleRef}>
{medias.map((media, index) => media === null ? ( {attachments.map((attachment, index) => attachment === null ? (
<LoadMoreMedia <LoadMoreMedia key={'more:' + attachments.getIn(index + 1, 'id')} maxId={index > 0 ? attachments.getIn(index - 1, 'id') : null} onLoadMore={this.handleLoadMore} />
key={'more:' + medias.getIn(index + 1, 'id')}
maxId={index > 0 ? medias.getIn(index - 1, 'id') : null}
onLoadMore={this.handleLoadMore}
/>
) : ( ) : (
<MediaItem <MediaItem key={attachment.get('id')} attachment={attachment} displayWidth={width} onOpenMedia={this.handleOpenMedia} />
key={media.get('id')}
media={media}
/>
))} ))}
{loadOlder} {loadOlder}
</div> </div>
{isLoading && medias.size === 0 && ( {isLoading && attachments.size === 0 && (
<div className='scrollable__append'> <div className='scrollable__append'>
<LoadingIndicator /> <LoadingIndicator />
</div> </div>

View File

@ -331,62 +331,19 @@
.account-gallery__container { .account-gallery__container {
display: flex; display: flex;
justify-content: center;
flex-wrap: wrap; flex-wrap: wrap;
padding: 2px; justify-content: center;
padding: 4px 2px;
} }
.account-gallery__item { .account-gallery__item {
flex-grow: 1; border: none;
width: 50%; box-sizing: border-box;
overflow: hidden; display: block;
position: relative; position: relative;
border-radius: 4px;
&::before { overflow: hidden;
content: "";
display: block;
padding-top: 100%;
}
a {
display: block;
width: calc(100% - 4px);
height: calc(100% - 4px);
margin: 2px; margin: 2px;
top: 0;
left: 0;
background-color: $base-overlay-background;
background-size: cover;
background-position: center;
position: absolute;
color: $ui-primary-color;
text-decoration: none;
border-radius: 4px;
&:hover,
&:active,
&:focus {
outline: 0;
color: $ui-secondary-color;
&::before {
content: "";
display: block;
width: 100%;
height: 100%;
background: rgba($base-overlay-background, 0.3);
border-radius: 4px;
}
}
}
&__icons {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 24px;
}
} }
.notification__filter-bar, .notification__filter-bar,

View File

@ -58,6 +58,7 @@
pointer-events: none; pointer-events: none;
opacity: 0.9; opacity: 0.9;
transition: opacity 0.1s ease; transition: opacity 0.1s ease;
line-height: 18px;
} }
.media-gallery__gifv { .media-gallery__gifv {