From a5da59f140a2a8fb2d3f480cdd87964d0beff103 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Thu, 2 May 2019 08:34:32 +0200 Subject: [Glitch] Change account gallery in web UI Port 3f143606faa6181ff2745b6bd29ac8ea075088bf to glitch-soc Signed-off-by: Thibaut Girka --- .../account_gallery/components/media_item.js | 160 +++++++++++++++------ .../glitch/features/account_gallery/index.js | 67 +++++---- 2 files changed, 158 insertions(+), 69 deletions(-) (limited to 'app/javascript/flavours/glitch/features/account_gallery') diff --git a/app/javascript/flavours/glitch/features/account_gallery/components/media_item.js b/app/javascript/flavours/glitch/features/account_gallery/components/media_item.js index 89778e123..cc35097a7 100644 --- a/app/javascript/flavours/glitch/features/account_gallery/components/media_item.js +++ b/app/javascript/flavours/glitch/features/account_gallery/components/media_item.js @@ -1,69 +1,141 @@ import React from 'react'; +import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePureComponent from 'react-immutable-pure-component'; -import Permalink from 'flavours/glitch/components/permalink'; -import { displayMedia } from 'flavours/glitch/util/initial_state'; +import { autoPlayGif, 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 { static propTypes = { - media: ImmutablePropTypes.map.isRequired, + attachment: ImmutablePropTypes.map.isRequired, + displayWidth: PropTypes.number.isRequired, + onOpenMedia: PropTypes.func.isRequired, }; 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 = () => { - if (!this.state.visible) { - this.setState({ visible: true }); - return true; + componentDidMount () { + if (this.props.attachment.get('blurhash')) { + this._decode(); } + } - return false; + componentDidUpdate (prevProps) { + if (prevProps.attachment.get('blurhash') !== this.props.attachment.get('blurhash') && this.props.attachment.get('blurhash')) { + this._decode(); + } } - render () { - const { media } = this.props; - const { visible } = this.state; - const status = media.get('status'); - const focusX = media.getIn(['meta', 'focus', 'x']); - const focusY = media.getIn(['meta', 'focus', 'y']); - const x = ((focusX / 2) + .5) * 100; - const y = ((focusY / -2) + .5) * 100; - const style = {}; - - let label, icon, title; - - if (media.get('type') === 'gifv') { - label = GIF; + _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 () { + const { attachment, displayWidth } = this.props; + const { visible, loaded } = this.state; + + const width = `${Math.floor((displayWidth - 4) / 3) - 4}px`; + 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 y = ((focusY / -2) + .5) * 100; + + thumbnail = ( + {attachment.get('description')} + ); + } else if (['gifv', 'video'].indexOf(attachment.get('type')) !== -1) { + const autoPlay = !isIOS() && autoPlayGif; - if (visible) { - style.backgroundImage = `url(${media.get('preview_url')})`; - style.backgroundPosition = `${x}% ${y}%`; - title = media.get('description'); - } else { - icon = ( - - - + thumbnail = ( +
+
); - title = status.get('spoiler_text') || media.get('description'); } return ( -
- - {icon} - {label} - +
+ + + {visible && thumbnail} +
); } diff --git a/app/javascript/flavours/glitch/features/account_gallery/index.js b/app/javascript/flavours/glitch/features/account_gallery/index.js index 3b1af108f..264aff261 100644 --- a/app/javascript/flavours/glitch/features/account_gallery/index.js +++ b/app/javascript/flavours/glitch/features/account_gallery/index.js @@ -14,12 +14,13 @@ import HeaderContainer from 'flavours/glitch/features/account_timeline/container import { ScrollContainer } from 'react-router-scroll-4'; import LoadMore from 'flavours/glitch/components/load_more'; import MissingIndicator from 'flavours/glitch/components/missing_indicator'; +import { openModal } from 'flavours/glitch/actions/modal'; const mapStateToProps = (state, props) => ({ 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']), - hasMore: state.getIn(['timelines', `account:${props.params.accountId}:media`, 'hasMore']), + hasMore: state.getIn(['timelines', `account:${props.params.accountId}:media`, 'hasMore']), }); class LoadMoreMedia extends ImmutablePureComponent { @@ -50,12 +51,16 @@ export default class AccountGallery extends ImmutablePureComponent { static propTypes = { params: PropTypes.object.isRequired, dispatch: PropTypes.func.isRequired, - medias: ImmutablePropTypes.list.isRequired, + attachments: ImmutablePropTypes.list.isRequired, isLoading: PropTypes.bool, hasMore: PropTypes.bool, isAccount: PropTypes.bool, }; + state = { + width: 323, + }; + componentDidMount () { this.props.dispatch(fetchAccount(this.props.params.accountId)); this.props.dispatch(expandAccountMediaTimeline(this.props.params.accountId)); @@ -74,11 +79,11 @@ export default class AccountGallery extends ImmutablePureComponent { handleScrollToBottom = () => { 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 offset = scrollHeight - scrollTop - clientHeight; @@ -91,7 +96,7 @@ export default class AccountGallery extends ImmutablePureComponent { this.props.dispatch(expandAccountMediaTimeline(this.props.params.accountId, { maxId })); }; - handleLoadOlder = (e) => { + handleLoadOlder = e => { e.preventDefault(); this.handleScrollToBottom(); } @@ -101,12 +106,30 @@ export default class AccountGallery extends ImmutablePureComponent { return !(location.state && location.state.mastodonModalOpen); } - setRef = c => { + setColumnRef = 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 () { - const { medias, isLoading, hasMore, isAccount } = this.props; + const { attachments, isLoading, hasMore, isAccount } = this.props; + const { width } = this.state; if (!isAccount) { return ( @@ -116,9 +139,7 @@ export default class AccountGallery extends ImmutablePureComponent { ); } - let loadOlder = null; - - if (!medias && isLoading) { + if (!attachments && isLoading) { return ( @@ -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 = ; } return ( - +
-
- {medias.map((media, index) => media === null ? ( - 0 ? medias.getIn(index - 1, 'id') : null} - onLoadMore={this.handleLoadMore} - /> +
+ {attachments.map((attachment, index) => attachment === null ? ( + 0 ? attachments.getIn(index - 1, 'id') : null} onLoadMore={this.handleLoadMore} /> ) : ( - + ))} + {loadOlder}
- {isLoading && medias.size === 0 && ( + {isLoading && attachments.size === 0 && (
-- cgit From bc97fd641f1f075e45f920fdc214f355ce16f53d Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Fri, 3 May 2019 16:16:30 +0200 Subject: [Glitch] Add button to view context to media modal Port eb63217210b0ab85ff1fcca9506d5e7931382a56 to glitch-soc Signed-off-by: Thibaut Girka --- .../glitch/features/account_gallery/index.js | 4 +-- .../glitch/features/ui/components/media_modal.js | 25 +++++++++++-- .../glitch/features/ui/components/video_modal.js | 18 +++++++++- .../flavours/glitch/features/video/index.js | 9 +++-- .../flavours/glitch/styles/components/media.scss | 42 ++++++++++++++++++++++ 5 files changed, 90 insertions(+), 8 deletions(-) (limited to 'app/javascript/flavours/glitch/features/account_gallery') diff --git a/app/javascript/flavours/glitch/features/account_gallery/index.js b/app/javascript/flavours/glitch/features/account_gallery/index.js index 264aff261..3e4421306 100644 --- a/app/javascript/flavours/glitch/features/account_gallery/index.js +++ b/app/javascript/flavours/glitch/features/account_gallery/index.js @@ -112,12 +112,12 @@ export default class AccountGallery extends ImmutablePureComponent { handleOpenMedia = attachment => { if (attachment.get('type') === 'video') { - this.props.dispatch(openModal('VIDEO', { media: attachment })); + this.props.dispatch(openModal('VIDEO', { media: attachment, status: attachment.get('status') })); } 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 })); + this.props.dispatch(openModal('MEDIA', { media, index, status: attachment.get('status') })); } } diff --git a/app/javascript/flavours/glitch/features/ui/components/media_modal.js b/app/javascript/flavours/glitch/features/ui/components/media_modal.js index 39386ee1c..ce6660480 100644 --- a/app/javascript/flavours/glitch/features/ui/components/media_modal.js +++ b/app/javascript/flavours/glitch/features/ui/components/media_modal.js @@ -5,7 +5,7 @@ import PropTypes from 'prop-types'; import Video from 'flavours/glitch/features/video'; import ExtendedVideoPlayer from 'flavours/glitch/components/extended_video_player'; import classNames from 'classnames'; -import { defineMessages, injectIntl } from 'react-intl'; +import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import IconButton from 'flavours/glitch/components/icon_button'; import ImmutablePureComponent from 'react-immutable-pure-component'; import ImageLoader from './image_loader'; @@ -19,8 +19,13 @@ const messages = defineMessages({ @injectIntl export default class MediaModal extends ImmutablePureComponent { + static contextTypes = { + router: PropTypes.object, + }; + static propTypes = { media: ImmutablePropTypes.list.isRequired, + status: ImmutablePropTypes.map, index: PropTypes.number.isRequired, onClose: PropTypes.func.isRequired, intl: PropTypes.object.isRequired, @@ -81,8 +86,15 @@ export default class MediaModal extends ImmutablePureComponent { })); }; + handleStatusClick = e => { + if (e.button === 0 && !(e.ctrlKey || e.metaKey)) { + e.preventDefault(); + this.context.router.history.push(`/statuses/${this.props.status.get('id')}`); + } + } + render () { - const { media, intl, onClose } = this.props; + const { media, status, intl, onClose } = this.props; const { navigationHidden } = this.state; const index = this.getIndex(); @@ -186,10 +198,19 @@ export default class MediaModal extends ImmutablePureComponent { {content}
+
+ {leftNav} {rightNav} + + {status && ( +
1 })}> + +
+ )} +
    {pagination}
diff --git a/app/javascript/flavours/glitch/features/ui/components/video_modal.js b/app/javascript/flavours/glitch/features/ui/components/video_modal.js index 8c74d5a13..3f742c260 100644 --- a/app/javascript/flavours/glitch/features/ui/components/video_modal.js +++ b/app/javascript/flavours/glitch/features/ui/components/video_modal.js @@ -3,17 +3,32 @@ import ImmutablePropTypes from 'react-immutable-proptypes'; import PropTypes from 'prop-types'; import Video from 'flavours/glitch/features/video'; import ImmutablePureComponent from 'react-immutable-pure-component'; +import { FormattedMessage } from 'react-intl'; export default class VideoModal extends ImmutablePureComponent { + static contextTypes = { + router: PropTypes.object, + }; + static propTypes = { media: ImmutablePropTypes.map.isRequired, + status: ImmutablePropTypes.map, time: PropTypes.number, onClose: PropTypes.func.isRequired, }; + handleStatusClick = e => { + if (e.button === 0 && !(e.ctrlKey || e.metaKey)) { + e.preventDefault(); + this.context.router.history.push(`/statuses/${this.props.status.get('id')}`); + } + } + render () { - const { media, time, onClose } = this.props; + const { media, status, time, onClose } = this.props; + + const link = status && ; return (
@@ -24,6 +39,7 @@ export default class VideoModal extends ImmutablePureComponent { src={media.get('url')} startTime={time} onCloseVideo={onClose} + link={link} detailed alt={media.get('description')} /> diff --git a/app/javascript/flavours/glitch/features/video/index.js b/app/javascript/flavours/glitch/features/video/index.js index aad24b1d9..381485802 100644 --- a/app/javascript/flavours/glitch/features/video/index.js +++ b/app/javascript/flavours/glitch/features/video/index.js @@ -106,6 +106,7 @@ export default class Video extends React.PureComponent { intl: PropTypes.object.isRequired, cacheWidth: PropTypes.func, blurhash: PropTypes.string, + link: PropTypes.node, }; state = { @@ -384,7 +385,7 @@ export default class Video extends React.PureComponent { } render () { - const { preview, src, inline, startTime, onOpenVideo, onCloseVideo, intl, alt, letterbox, fullwidth, detailed, sensitive } = this.props; + const { preview, src, inline, startTime, onOpenVideo, onCloseVideo, intl, alt, letterbox, fullwidth, detailed, sensitive, link } = this.props; const { containerWidth, currentTime, duration, volume, buffer, dragging, paused, fullscreen, hovered, muted, revealed } = this.state; const progress = (currentTime / duration) * 100; const playerStyle = {}; @@ -487,13 +488,15 @@ export default class Video extends React.PureComponent { />
- {(detailed || fullscreen) && + {(detailed || fullscreen) && ( {formatTime(currentTime)} / {formatTime(duration)} - } + )} + + {link && {link}}
diff --git a/app/javascript/flavours/glitch/styles/components/media.scss b/app/javascript/flavours/glitch/styles/components/media.scss index bc241de14..e5927057e 100644 --- a/app/javascript/flavours/glitch/styles/components/media.scss +++ b/app/javascript/flavours/glitch/styles/components/media.scss @@ -270,6 +270,31 @@ pointer-events: none; } +.media-modal__meta { + text-align: center; + position: absolute; + left: 0; + bottom: 20px; + width: 100%; + pointer-events: none; + + &--shifted { + bottom: 62px; + } + + a { + text-decoration: none; + font-weight: 500; + color: $ui-secondary-color; + + &:hover, + &:focus, + &:active { + text-decoration: underline; + } + } +} + .media-modal__page-dot { display: inline-block; } @@ -519,6 +544,23 @@ } } + &__link { + padding: 2px 10px; + + a { + text-decoration: none; + font-size: 14px; + font-weight: 500; + color: $white; + + &:hover, + &:active, + &:focus { + text-decoration: underline; + } + } + } + &__seek { cursor: pointer; height: 24px; -- cgit From b7f69beebe2a4f7b94bc39772e7e9714540220b7 Mon Sep 17 00:00:00 2001 From: ThibG Date: Sat, 4 May 2019 17:36:43 +0200 Subject: [Glitch] Make the cursor icon consistant across media types in account media gallery --- .../flavours/glitch/features/account_gallery/components/media_item.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'app/javascript/flavours/glitch/features/account_gallery') diff --git a/app/javascript/flavours/glitch/features/account_gallery/components/media_item.js b/app/javascript/flavours/glitch/features/account_gallery/components/media_item.js index cc35097a7..1eae552f6 100644 --- a/app/javascript/flavours/glitch/features/account_gallery/components/media_item.js +++ b/app/javascript/flavours/glitch/features/account_gallery/components/media_item.js @@ -132,7 +132,7 @@ export default class MediaItem extends ImmutablePureComponent { return (
- + {visible && thumbnail} -- cgit From cbda1b8b66270a02a3d06d1cafe0c6396466c50d Mon Sep 17 00:00:00 2001 From: Thibaut Girka Date: Sat, 4 May 2019 20:03:37 +0200 Subject: Add back description on hover --- .../flavours/glitch/features/account_gallery/components/media_item.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'app/javascript/flavours/glitch/features/account_gallery') diff --git a/app/javascript/flavours/glitch/features/account_gallery/components/media_item.js b/app/javascript/flavours/glitch/features/account_gallery/components/media_item.js index 1eae552f6..f2a661862 100644 --- a/app/javascript/flavours/glitch/features/account_gallery/components/media_item.js +++ b/app/javascript/flavours/glitch/features/account_gallery/components/media_item.js @@ -88,6 +88,7 @@ export default class MediaItem extends ImmutablePureComponent { const width = `${Math.floor((displayWidth - 4) / 3) - 4}px`; const height = width; const status = attachment.get('status'); + const title = status.get('spoiler_text') || attachment.get('description'); let thumbnail = ''; @@ -132,7 +133,7 @@ export default class MediaItem extends ImmutablePureComponent { return (
- + {visible && thumbnail} -- cgit From b1ab4d5ebe3ccb7d91e55eedd0ad08226358c446 Mon Sep 17 00:00:00 2001 From: Thibaut Girka Date: Sat, 4 May 2019 20:06:17 +0200 Subject: Add visibility icon back in media gallery --- .../glitch/features/account_gallery/components/media_item.js | 8 +++++++- app/javascript/flavours/glitch/styles/components/accounts.scss | 8 ++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) (limited to 'app/javascript/flavours/glitch/features/account_gallery') diff --git a/app/javascript/flavours/glitch/features/account_gallery/components/media_item.js b/app/javascript/flavours/glitch/features/account_gallery/components/media_item.js index f2a661862..026136b2c 100644 --- a/app/javascript/flavours/glitch/features/account_gallery/components/media_item.js +++ b/app/javascript/flavours/glitch/features/account_gallery/components/media_item.js @@ -131,11 +131,17 @@ export default class MediaItem extends ImmutablePureComponent { ); } + const icon = ( + + + + ); + return ( ); diff --git a/app/javascript/flavours/glitch/styles/components/accounts.scss b/app/javascript/flavours/glitch/styles/components/accounts.scss index 518eea5fa..c0340e3f8 100644 --- a/app/javascript/flavours/glitch/styles/components/accounts.scss +++ b/app/javascript/flavours/glitch/styles/components/accounts.scss @@ -343,6 +343,14 @@ border-radius: 4px; overflow: hidden; margin: 2px; + + &__icons { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + font-size: 24px; + } } .notification__filter-bar, -- cgit