diff --git a/app/javascript/flavours/glitch/blurhash.js b/app/javascript/flavours/glitch/blurhash.js
new file mode 100644
index 0000000000..5adcc3e770
--- /dev/null
+++ b/app/javascript/flavours/glitch/blurhash.js
@@ -0,0 +1,112 @@
+const DIGIT_CHARACTERS = [
+ '0',
+ '1',
+ '2',
+ '3',
+ '4',
+ '5',
+ '6',
+ '7',
+ '8',
+ '9',
+ 'A',
+ 'B',
+ 'C',
+ 'D',
+ 'E',
+ 'F',
+ 'G',
+ 'H',
+ 'I',
+ 'J',
+ 'K',
+ 'L',
+ 'M',
+ 'N',
+ 'O',
+ 'P',
+ 'Q',
+ 'R',
+ 'S',
+ 'T',
+ 'U',
+ 'V',
+ 'W',
+ 'X',
+ 'Y',
+ 'Z',
+ 'a',
+ 'b',
+ 'c',
+ 'd',
+ 'e',
+ 'f',
+ 'g',
+ 'h',
+ 'i',
+ 'j',
+ 'k',
+ 'l',
+ 'm',
+ 'n',
+ 'o',
+ 'p',
+ 'q',
+ 'r',
+ 's',
+ 't',
+ 'u',
+ 'v',
+ 'w',
+ 'x',
+ 'y',
+ 'z',
+ '#',
+ '$',
+ '%',
+ '*',
+ '+',
+ ',',
+ '-',
+ '.',
+ ':',
+ ';',
+ '=',
+ '?',
+ '@',
+ '[',
+ ']',
+ '^',
+ '_',
+ '{',
+ '|',
+ '}',
+ '~',
+];
+
+export const decode83 = (str) => {
+ let value = 0;
+ let c, digit;
+
+ for (let i = 0; i < str.length; i++) {
+ c = str[i];
+ digit = DIGIT_CHARACTERS.indexOf(c);
+ value = value * 83 + digit;
+ }
+
+ return value;
+};
+
+export const intToRGB = int => ({
+ r: Math.max(0, (int >> 16)),
+ g: Math.max(0, (int >> 8) & 255),
+ b: Math.max(0, (int & 255)),
+});
+
+export const getAverageFromBlurhash = blurhash => {
+ if (!blurhash) {
+ return null;
+ }
+
+ return intToRGB(decode83(blurhash.slice(2, 6)));
+};
diff --git a/app/javascript/flavours/glitch/components/modal_root.js b/app/javascript/flavours/glitch/components/modal_root.js
index 13a8e8702d..913234d325 100644
--- a/app/javascript/flavours/glitch/components/modal_root.js
+++ b/app/javascript/flavours/glitch/components/modal_root.js
@@ -2,6 +2,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import 'wicg-inert';
import { createBrowserHistory } from 'history';
+import { multiply } from 'color-blend';
export default class ModalRoot extends React.PureComponent {
static contextTypes = {
@@ -11,6 +12,11 @@ export default class ModalRoot extends React.PureComponent {
static propTypes = {
children: PropTypes.node,
onClose: PropTypes.func.isRequired,
+ backgroundColor: PropTypes.shape({
+ r: PropTypes.number,
+ g: PropTypes.number,
+ b: PropTypes.number,
+ }),
noEsc: PropTypes.bool,
};
@@ -68,9 +74,7 @@ export default class ModalRoot extends React.PureComponent {
Promise.resolve().then(() => {
this.activeElement.focus({ preventScroll: true });
this.activeElement = null;
- }).catch((error) => {
- console.error(error);
- });
+ }).catch(console.error);
this.handleModalClose();
}
@@ -120,10 +124,16 @@ export default class ModalRoot extends React.PureComponent {
);
}
+ let backgroundColor = null;
+
+ if (this.props.backgroundColor) {
+ backgroundColor = multiply({ ...this.props.backgroundColor, a: 1 }, { r: 0, g: 0, b: 0, a: 0.7 });
+ }
+
return (
diff --git a/app/javascript/flavours/glitch/components/status.js b/app/javascript/flavours/glitch/components/status.js
index fcbf4be8c3..782fd918ee 100644
--- a/app/javascript/flavours/glitch/components/status.js
+++ b/app/javascript/flavours/glitch/components/status.js
@@ -378,22 +378,26 @@ class Status extends ImmutablePureComponent {
}
};
- handleOpenVideo = (media, options) => {
- this.props.onOpenVideo(media, options);
+ handleOpenVideo = (options) => {
+ const { status } = this.props;
+ this.props.onOpenVideo(status.get('id'), status.getIn(['media_attachments', 0]), options);
+ }
+
+ handleOpenMedia = (media, index) => {
+ this.props.onOpenMedia(this.props.status.get('id'), media, index);
}
handleHotkeyOpenMedia = e => {
const { status, onOpenMedia, onOpenVideo } = this.props;
+ const statusId = status.get('id');
e.preventDefault();
if (status.get('media_attachments').size > 0) {
- if (status.getIn(['media_attachments', 0, 'type']) === 'audio') {
- // TODO: toggle play/paused?
- } else if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
- onOpenVideo(status.getIn(['media_attachments', 0]), { startTime: 0 });
+ if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
+ onOpenVideo(statusId, status.getIn(['media_attachments', 0]), { startTime: 0 });
} else {
- onOpenMedia(status.get('media_attachments'), 0);
+ onOpenMedia(statusId, status.get('media_attachments'), 0);
}
}
}
@@ -657,7 +661,7 @@ class Status extends ImmutablePureComponent {
letterbox={settings.getIn(['media', 'letterbox'])}
fullwidth={settings.getIn(['media', 'fullwidth'])}
hidden={isCollapsed || !isExpanded}
- onOpenMedia={this.props.onOpenMedia}
+ onOpenMedia={this.handleOpenMedia}
cacheWidth={this.props.cacheMediaWidth}
defaultWidth={this.props.cachedMediaWidth}
visible={this.state.showMedia}
@@ -675,7 +679,7 @@ class Status extends ImmutablePureComponent {
} else if (status.get('card') && settings.get('inline_preview_cards')) {
media = (
{
@@ -39,20 +41,32 @@ export default class MediaContainer extends PureComponent {
this.setState({ media, index });
}
- handleOpenVideo = (video, time) => {
- const media = ImmutableList([video]);
+ handleOpenVideo = (options) => {
+ const { components } = this.props;
+ const { media } = JSON.parse(components[options.componetIndex].getAttribute('data-props'));
+ const mediaList = fromJS(media);
document.body.classList.add('with-modals--active');
document.documentElement.style.marginRight = `${getScrollbarWidth()}px`;
- this.setState({ media, time });
+ this.setState({ media: mediaList, options });
}
handleCloseMedia = () => {
document.body.classList.remove('with-modals--active');
document.documentElement.style.marginRight = 0;
- this.setState({ media: null, index: null, time: null });
+ this.setState({
+ media: null,
+ index: null,
+ time: null,
+ backgroundColor: null,
+ options: null,
+ });
+ }
+
+ setBackgroundColor = color => {
+ this.setState({ backgroundColor: color });
}
render () {
@@ -73,6 +87,7 @@ export default class MediaContainer extends PureComponent {
...(hashtag ? { hashtag: fromJS(hashtag) } : {}),
...(componentName === 'Video' ? {
+ componetIndex: i,
onOpenVideo: this.handleOpenVideo,
} : {
onOpenMedia: this.handleOpenMedia,
@@ -85,13 +100,16 @@ export default class MediaContainer extends PureComponent {
);
})}
-
+
{this.state.media && (
)}
diff --git a/app/javascript/flavours/glitch/containers/status_container.js b/app/javascript/flavours/glitch/containers/status_container.js
index 6461bf8050..bc3c43d856 100644
--- a/app/javascript/flavours/glitch/containers/status_container.js
+++ b/app/javascript/flavours/glitch/containers/status_container.js
@@ -177,12 +177,12 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({
dispatch(mentionCompose(account, router));
},
- onOpenMedia (media, index) {
- dispatch(openModal('MEDIA', { media, index }));
+ onOpenMedia (statusId, media, index) {
+ dispatch(openModal('MEDIA', { statusId, media, index }));
},
- onOpenVideo (media, options) {
- dispatch(openModal('VIDEO', { media, options }));
+ onOpenVideo (statusId, media, options) {
+ dispatch(openModal('VIDEO', { statusId, media, options }));
},
onBlock (status) {
diff --git a/app/javascript/flavours/glitch/features/account_gallery/index.js b/app/javascript/flavours/glitch/features/account_gallery/index.js
index 81203e3f8a..2a43d1ed22 100644
--- a/app/javascript/flavours/glitch/features/account_gallery/index.js
+++ b/app/javascript/flavours/glitch/features/account_gallery/index.js
@@ -114,15 +114,18 @@ class AccountGallery extends ImmutablePureComponent {
}
handleOpenMedia = attachment => {
+ const { dispatch } = this.props;
+ const statusId = attachment.getIn(['status', 'id']);
+
if (attachment.get('type') === 'video') {
- this.props.dispatch(openModal('VIDEO', { media: attachment, status: attachment.get('status'), options: { autoPlay: true } }));
+ dispatch(openModal('VIDEO', { media: attachment, statusId, options: { autoPlay: true } }));
} else if (attachment.get('type') === 'audio') {
- this.props.dispatch(openModal('AUDIO', { media: attachment, status: attachment.get('status'), options: { autoPlay: true } }));
+ dispatch(openModal('AUDIO', { media: attachment, statusId, options: { autoPlay: true } }));
} 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, status: attachment.get('status') }));
+ dispatch(openModal('MEDIA', { media, index, statusId }));
}
}
diff --git a/app/javascript/flavours/glitch/features/picture_in_picture/components/footer.js b/app/javascript/flavours/glitch/features/picture_in_picture/components/footer.js
index d8989ec61b..fcb2df5278 100644
--- a/app/javascript/flavours/glitch/features/picture_in_picture/components/footer.js
+++ b/app/javascript/flavours/glitch/features/picture_in_picture/components/footer.js
@@ -23,6 +23,7 @@ const messages = defineMessages({
favourite: { id: 'status.favourite', defaultMessage: 'Favourite' },
replyConfirm: { id: 'confirmations.reply.confirm', defaultMessage: 'Reply' },
replyMessage: { id: 'confirmations.reply.message', defaultMessage: 'Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?' },
+ open: { id: 'status.open', defaultMessage: 'Expand this status' },
});
const makeMapStateToProps = () => {
@@ -52,11 +53,19 @@ class Footer extends ImmutablePureComponent {
dispatch: PropTypes.func.isRequired,
askReplyConfirmation: PropTypes.bool,
showReplyCount: PropTypes.bool,
+ withOpenButton: PropTypes.bool,
+ onClose: PropTypes.func,
};
_performReply = () => {
- const { dispatch, status } = this.props;
- dispatch(replyCompose(status, this.context.router.history));
+ const { dispatch, status, onClose } = this.props;
+ const { router } = this.context;
+
+ if (onClose) {
+ onClose();
+ }
+
+ dispatch(replyCompose(status, router.history));
};
handleReplyClick = () => {
@@ -100,8 +109,20 @@ class Footer extends ImmutablePureComponent {
}
};
+ handleOpenClick = e => {
+ const { router } = this.context;
+
+ if (e.button !== 0 || !router) {
+ return;
+ }
+
+ const { status } = this.props;
+
+ router.history.push(`/statuses/${status.get('id')}`);
+ }
+
render () {
- const { status, intl, showReplyCount } = this.props;
+ const { status, intl, showReplyCount, withOpenButton } = this.props;
const publicStatus = ['public', 'unlisted'].includes(status.get('visibility'));
const reblogPrivate = status.getIn(['account', 'id']) === me && status.get('visibility') === 'private';
@@ -156,6 +177,7 @@ class Footer extends ImmutablePureComponent {
{replyButton}
+ {withOpenButton && }
);
}
diff --git a/app/javascript/flavours/glitch/features/status/components/detailed_status.js b/app/javascript/flavours/glitch/features/status/components/detailed_status.js
index 40bf370f31..4cc1d1af50 100644
--- a/app/javascript/flavours/glitch/features/status/components/detailed_status.js
+++ b/app/javascript/flavours/glitch/features/status/components/detailed_status.js
@@ -68,8 +68,8 @@ export default class DetailedStatus extends ImmutablePureComponent {
e.stopPropagation();
}
- handleOpenVideo = (media, options) => {
- this.props.onOpenVideo(media, options);
+ handleOpenVideo = (options) => {
+ this.props.onOpenVideo(this.props.status.getIn(['media_attachments', 0]), options);
}
_measureHeight (heightJustChanged) {
diff --git a/app/javascript/flavours/glitch/features/status/index.js b/app/javascript/flavours/glitch/features/status/index.js
index 21e4414070..513a6227ff 100644
--- a/app/javascript/flavours/glitch/features/status/index.js
+++ b/app/javascript/flavours/glitch/features/status/index.js
@@ -316,11 +316,11 @@ class Status extends ImmutablePureComponent {
}
handleOpenMedia = (media, index) => {
- this.props.dispatch(openModal('MEDIA', { media, index }));
+ this.props.dispatch(openModal('MEDIA', { statusId: this.props.status.get('id'), media, index }));
}
handleOpenVideo = (media, options) => {
- this.props.dispatch(openModal('VIDEO', { media, options }));
+ this.props.dispatch(openModal('VIDEO', { statusId: this.props.status.get('id'), media, options }));
}
handleHotkeyOpenMedia = e => {
@@ -329,9 +329,7 @@ class Status extends ImmutablePureComponent {
e.preventDefault();
if (status.get('media_attachments').size > 0) {
- if (status.getIn(['media_attachments', 0, 'type']) === 'audio') {
- // TODO: toggle play/paused?
- } else if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
+ if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
this.handleOpenVideo(status.getIn(['media_attachments', 0]), { startTime: 0 });
} else {
this.handleOpenMedia(status.get('media_attachments'), 0);
diff --git a/app/javascript/flavours/glitch/features/ui/components/audio_modal.js b/app/javascript/flavours/glitch/features/ui/components/audio_modal.js
index f9d4bb2f30..fc98cc6af1 100644
--- a/app/javascript/flavours/glitch/features/ui/components/audio_modal.js
+++ b/app/javascript/flavours/glitch/features/ui/components/audio_modal.js
@@ -4,12 +4,10 @@ import PropTypes from 'prop-types';
import Audio from 'flavours/glitch/features/audio';
import { connect } from 'react-redux';
import ImmutablePureComponent from 'react-immutable-pure-component';
-import { FormattedMessage } from 'react-intl';
-import classNames from 'classnames';
-import Icon from 'flavours/glitch/components/icon';
+import Footer from 'flavours/glitch/features/picture_in_picture/components/footer';
-const mapStateToProps = (state, { status }) => ({
- account: state.getIn(['accounts', status.get('account')]),
+const mapStateToProps = (state, { statusId }) => ({
+ accountStaticAvatar: state.getIn(['accounts', state.getIn(['statuses', statusId, 'account']), 'avatar_static']),
});
export default @connect(mapStateToProps)
@@ -17,27 +15,21 @@ class AudioModal extends ImmutablePureComponent {
static propTypes = {
media: ImmutablePropTypes.map.isRequired,
- status: ImmutablePropTypes.map,
+ statusId: PropTypes.string.isRequired,
+ accountStaticAvatar: PropTypes.string.isRequired,
options: PropTypes.shape({
autoPlay: PropTypes.bool,
}),
- account: ImmutablePropTypes.map,
onClose: PropTypes.func.isRequired,
+ onChangeBackgroundColor: PropTypes.func.isRequired,
};
static contextTypes = {
router: PropTypes.object,
};
- 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, status, account } = this.props;
+ const { media, accountStaticAvatar, statusId, onClose } = this.props;
const options = this.props.options || {};
return (
@@ -48,7 +40,7 @@ class AudioModal extends ImmutablePureComponent {
alt={media.get('description')}
duration={media.getIn(['meta', 'original', 'duration'], 0)}
height={150}
- poster={media.get('preview_url') || account.get('avatar_static')}
+ poster={media.get('preview_url') || accountStaticAvatar}
backgroundColor={media.getIn(['meta', 'colors', 'background'])}
foregroundColor={media.getIn(['meta', 'colors', 'foreground'])}
accentColor={media.getIn(['meta', 'colors', 'accent'])}
@@ -56,11 +48,9 @@ class AudioModal extends ImmutablePureComponent {
/>
- {status && (
-
- )}
+
+ {statusId && }
+
);
}
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 e37df72083..a8cbb837e3 100644
--- a/app/javascript/flavours/glitch/features/ui/components/media_modal.js
+++ b/app/javascript/flavours/glitch/features/ui/components/media_modal.js
@@ -4,12 +4,14 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types';
import Video from 'flavours/glitch/features/video';
import classNames from 'classnames';
-import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
+import { defineMessages, injectIntl } from 'react-intl';
import IconButton from 'flavours/glitch/components/icon_button';
import ImmutablePureComponent from 'react-immutable-pure-component';
import ImageLoader from './image_loader';
import Icon from 'flavours/glitch/components/icon';
import GIFV from 'flavours/glitch/components/gifv';
+import Footer from 'flavours/glitch/features/picture_in_picture/components/footer';
+import { getAverageFromBlurhash } from 'flavours/glitch/blurhash';
const messages = defineMessages({
close: { id: 'lightbox.close', defaultMessage: 'Close' },
@@ -26,10 +28,14 @@ class MediaModal extends ImmutablePureComponent {
static propTypes = {
media: ImmutablePropTypes.list.isRequired,
- status: ImmutablePropTypes.map,
+ statusId: PropTypes.string,
index: PropTypes.number.isRequired,
onClose: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
+ onChangeBackgroundColor: PropTypes.func.isRequired,
+ currentTime: PropTypes.number,
+ autoPlay: PropTypes.bool,
+ volume: PropTypes.number,
};
state = {
@@ -64,6 +70,7 @@ class MediaModal extends ImmutablePureComponent {
handleChangeIndex = (e) => {
const index = Number(e.currentTarget.getAttribute('data-index'));
+
this.setState({
index: index % this.props.media.size,
zoomButtonHidden: true,
@@ -87,10 +94,12 @@ class MediaModal extends ImmutablePureComponent {
componentDidMount () {
window.addEventListener('keydown', this.handleKeyDown, false);
+ this._sendBackgroundColor();
}
componentWillUnmount () {
window.removeEventListener('keydown', this.handleKeyDown);
+ this.props.onChangeBackgroundColor(null);
}
getIndex () {
@@ -106,30 +115,38 @@ 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')}`);
+ this.context.router.history.push(`/statuses/${this.props.statusId}`);
+ }
+
+ this._sendBackgroundColor();
+ }
+
+ componentDidUpdate (prevProps, prevState) {
+ if (prevState.index !== this.state.index) {
+ this._sendBackgroundColor();
+ }
+ }
+
+ _sendBackgroundColor () {
+ const { media, onChangeBackgroundColor } = this.props;
+ const index = this.getIndex();
+ const blurhash = media.getIn([index, 'blurhash']);
+
+ if (blurhash) {
+ const backgroundColor = getAverageFromBlurhash(blurhash);
+ onChangeBackgroundColor(backgroundColor);
}
}
render () {
- const { media, status, intl, onClose } = this.props;
+ const { media, statusId, intl, onClose } = this.props;
const { navigationHidden } = this.state;
const index = this.getIndex();
- let pagination = [];
const leftNav = media.size > 1 && ;
const rightNav = media.size > 1 && ;
- if (media.size > 1) {
- pagination = media.map((item, i) => {
- const classes = ['media-modal__button'];
- if (i === index) {
- classes.push('media-modal__button--active');
- }
- return ();
- });
- }
-
const content = media.map((image) => {
const width = image.getIn(['meta', 'original', 'width']) || null;
const height = image.getIn(['meta', 'original', 'height']) || null;
@@ -148,7 +165,7 @@ class MediaModal extends ImmutablePureComponent {
/>
);
} else if (image.get('type') === 'video') {
- const { time } = this.props;
+ const { currentTime, autoPlay, volume } = this.props;
return (