Merge branch 'master' into glitch-soc/merge
This commit is contained in:
@ -84,8 +84,8 @@ export default class Status extends ImmutablePureComponent {
|
||||
return <div className='media-spoiler-video' style={{ height: '110px' }} />;
|
||||
}
|
||||
|
||||
handleOpenVideo = startTime => {
|
||||
this.props.onOpenVideo(this._properStatus().getIn(['media_attachments', 0]), startTime);
|
||||
handleOpenVideo = (media, startTime) => {
|
||||
this.props.onOpenVideo(media, startTime);
|
||||
}
|
||||
|
||||
handleHotkeyReply = e => {
|
||||
|
@ -1,59 +0,0 @@
|
||||
import React, { Fragment } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import PropTypes from 'prop-types';
|
||||
import { IntlProvider, addLocaleData } from 'react-intl';
|
||||
import { getLocale } from '../locales';
|
||||
import Card from '../features/status/components/card';
|
||||
import ModalRoot from '../components/modal_root';
|
||||
import MediaModal from '../features/ui/components/media_modal';
|
||||
import { fromJS } from 'immutable';
|
||||
|
||||
const { localeData, messages } = getLocale();
|
||||
addLocaleData(localeData);
|
||||
|
||||
export default class CardsContainer extends React.PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
locale: PropTypes.string,
|
||||
cards: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
state = {
|
||||
media: null,
|
||||
};
|
||||
|
||||
handleOpenCard = (media) => {
|
||||
document.body.classList.add('card-standalone__body');
|
||||
this.setState({ media });
|
||||
}
|
||||
|
||||
handleCloseCard = () => {
|
||||
document.body.classList.remove('card-standalone__body');
|
||||
this.setState({ media: null });
|
||||
}
|
||||
|
||||
render () {
|
||||
const { locale, cards } = this.props;
|
||||
|
||||
return (
|
||||
<IntlProvider locale={locale} messages={messages}>
|
||||
<Fragment>
|
||||
{[].map.call(cards, container => {
|
||||
const { card, ...props } = JSON.parse(container.getAttribute('data-props'));
|
||||
|
||||
return ReactDOM.createPortal(
|
||||
<Card card={fromJS(card)} onOpenMedia={this.handleOpenCard} {...props} />,
|
||||
container,
|
||||
);
|
||||
})}
|
||||
<ModalRoot onClose={this.handleCloseCard}>
|
||||
{this.state.media && (
|
||||
<MediaModal media={this.state.media} index={0} onClose={this.handleCloseCard} />
|
||||
)}
|
||||
</ModalRoot>
|
||||
</Fragment>
|
||||
</IntlProvider>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
90
app/javascript/mastodon/containers/media_container.js
Normal file
90
app/javascript/mastodon/containers/media_container.js
Normal file
@ -0,0 +1,90 @@
|
||||
import React, { PureComponent, Fragment } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import PropTypes from 'prop-types';
|
||||
import { IntlProvider, addLocaleData } from 'react-intl';
|
||||
import { getLocale } from '../locales';
|
||||
import MediaGallery from '../components/media_gallery';
|
||||
import Video from '../features/video';
|
||||
import Card from '../features/status/components/card';
|
||||
import ModalRoot from '../components/modal_root';
|
||||
import MediaModal from '../features/ui/components/media_modal';
|
||||
import { List as ImmutableList, fromJS } from 'immutable';
|
||||
|
||||
const { localeData, messages } = getLocale();
|
||||
addLocaleData(localeData);
|
||||
|
||||
const MEDIA_COMPONENTS = { MediaGallery, Video, Card };
|
||||
|
||||
export default class MediaContainer extends PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
locale: PropTypes.string.isRequired,
|
||||
components: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
state = {
|
||||
media: null,
|
||||
index: null,
|
||||
time: null,
|
||||
};
|
||||
|
||||
handleOpenMedia = (media, index) => {
|
||||
document.body.classList.add('media-standalone__body');
|
||||
this.setState({ media, index });
|
||||
}
|
||||
|
||||
handleOpenVideo = (video, time) => {
|
||||
const media = ImmutableList([video]);
|
||||
|
||||
document.body.classList.add('media-standalone__body');
|
||||
this.setState({ media, time });
|
||||
}
|
||||
|
||||
handleCloseMedia = () => {
|
||||
document.body.classList.remove('media-standalone__body');
|
||||
this.setState({ media: null, index: null, time: null });
|
||||
}
|
||||
|
||||
render () {
|
||||
const { locale, components } = this.props;
|
||||
|
||||
return (
|
||||
<IntlProvider locale={locale} messages={messages}>
|
||||
<Fragment>
|
||||
{[].map.call(components, (component, i) => {
|
||||
const componentName = component.getAttribute('data-component');
|
||||
const Component = MEDIA_COMPONENTS[componentName];
|
||||
const { media, card, ...props } = JSON.parse(component.getAttribute('data-props'));
|
||||
|
||||
Object.assign(props, {
|
||||
...(media ? { media: fromJS(media) } : {}),
|
||||
...(card ? { card: fromJS(card) } : {}),
|
||||
|
||||
...(componentName === 'Video' ? {
|
||||
onOpenVideo: this.handleOpenVideo,
|
||||
} : {
|
||||
onOpenMedia: this.handleOpenMedia,
|
||||
}),
|
||||
});
|
||||
|
||||
return ReactDOM.createPortal(
|
||||
<Component {...props} key={`media-${i}`} />,
|
||||
component,
|
||||
);
|
||||
})}
|
||||
<ModalRoot onClose={this.handleCloseMedia}>
|
||||
{this.state.media && (
|
||||
<MediaModal
|
||||
media={this.state.media}
|
||||
index={this.state.index || 0}
|
||||
time={this.state.time}
|
||||
onClose={this.handleCloseMedia}
|
||||
/>
|
||||
)}
|
||||
</ModalRoot>
|
||||
</Fragment>
|
||||
</IntlProvider>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
@ -1,68 +0,0 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import PropTypes from 'prop-types';
|
||||
import { IntlProvider, addLocaleData } from 'react-intl';
|
||||
import { getLocale } from '../locales';
|
||||
import MediaGallery from '../components/media_gallery';
|
||||
import ModalRoot from '../components/modal_root';
|
||||
import MediaModal from '../features/ui/components/media_modal';
|
||||
import { fromJS } from 'immutable';
|
||||
|
||||
const { localeData, messages } = getLocale();
|
||||
addLocaleData(localeData);
|
||||
|
||||
export default class MediaGalleriesContainer extends React.PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
locale: PropTypes.string.isRequired,
|
||||
galleries: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
state = {
|
||||
media: null,
|
||||
index: null,
|
||||
};
|
||||
|
||||
handleOpenMedia = (media, index) => {
|
||||
document.body.classList.add('media-gallery-standalone__body');
|
||||
this.setState({ media, index });
|
||||
}
|
||||
|
||||
handleCloseMedia = () => {
|
||||
document.body.classList.remove('media-gallery-standalone__body');
|
||||
this.setState({ media: null, index: null });
|
||||
}
|
||||
|
||||
render () {
|
||||
const { locale, galleries } = this.props;
|
||||
|
||||
return (
|
||||
<IntlProvider locale={locale} messages={messages}>
|
||||
<React.Fragment>
|
||||
{[].map.call(galleries, gallery => {
|
||||
const { media, ...props } = JSON.parse(gallery.getAttribute('data-props'));
|
||||
|
||||
return ReactDOM.createPortal(
|
||||
<MediaGallery
|
||||
{...props}
|
||||
media={fromJS(media)}
|
||||
onOpenMedia={this.handleOpenMedia}
|
||||
/>,
|
||||
gallery
|
||||
);
|
||||
})}
|
||||
<ModalRoot onClose={this.handleCloseMedia}>
|
||||
{this.state.media === null || this.state.index === null ? null : (
|
||||
<MediaModal
|
||||
media={this.state.media}
|
||||
index={this.state.index}
|
||||
onClose={this.handleCloseMedia}
|
||||
/>
|
||||
)}
|
||||
</ModalRoot>
|
||||
</React.Fragment>
|
||||
</IntlProvider>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
@ -1,26 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { IntlProvider, addLocaleData } from 'react-intl';
|
||||
import { getLocale } from '../locales';
|
||||
import Video from '../features/video';
|
||||
|
||||
const { localeData, messages } = getLocale();
|
||||
addLocaleData(localeData);
|
||||
|
||||
export default class VideoContainer extends React.PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
locale: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
render () {
|
||||
const { locale, ...props } = this.props;
|
||||
|
||||
return (
|
||||
<IntlProvider locale={locale} messages={messages}>
|
||||
<Video {...props} />
|
||||
</IntlProvider>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
@ -34,8 +34,8 @@ export default class DetailedStatus extends ImmutablePureComponent {
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
handleOpenVideo = startTime => {
|
||||
this.props.onOpenVideo(this.props.status.getIn(['media_attachments', 0]), startTime);
|
||||
handleOpenVideo = (media, startTime) => {
|
||||
this.props.onOpenVideo(media, startTime);
|
||||
}
|
||||
|
||||
handleExpandedToggle = () => {
|
||||
|
@ -2,6 +2,7 @@ import React from 'react';
|
||||
import ReactSwipeableViews from 'react-swipeable-views';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import PropTypes from 'prop-types';
|
||||
import Video from '../../video';
|
||||
import ExtendedVideoPlayer from '../../../components/extended_video_player';
|
||||
import classNames from 'classnames';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
@ -112,6 +113,22 @@ export default class MediaModal extends ImmutablePureComponent {
|
||||
onClick={this.toggleNavigation}
|
||||
/>
|
||||
);
|
||||
} else if (image.get('type') === 'video') {
|
||||
const { time } = this.props;
|
||||
|
||||
return (
|
||||
<Video
|
||||
preview={image.get('preview_url')}
|
||||
src={image.get('url')}
|
||||
width={image.get('width')}
|
||||
height={image.get('height')}
|
||||
startTime={time || 0}
|
||||
onCloseVideo={onClose}
|
||||
detailed
|
||||
description={image.get('description')}
|
||||
key={image.get('url')}
|
||||
/>
|
||||
);
|
||||
} else if (image.get('type') === 'gifv') {
|
||||
return (
|
||||
<ExtendedVideoPlayer
|
||||
|
@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||
import { fromJS } from 'immutable';
|
||||
import { throttle } from 'lodash';
|
||||
import classNames from 'classnames';
|
||||
import { isFullscreen, requestFullscreen, exitFullscreen } from '../ui/util/fullscreen';
|
||||
@ -131,6 +132,8 @@ export default class Video extends React.PureComponent {
|
||||
this.seek = c;
|
||||
}
|
||||
|
||||
handleClickRoot = e => e.stopPropagation();
|
||||
|
||||
handlePlay = () => {
|
||||
this.setState({ paused: false });
|
||||
}
|
||||
@ -244,8 +247,17 @@ export default class Video extends React.PureComponent {
|
||||
}
|
||||
|
||||
handleOpenVideo = () => {
|
||||
const { src, preview, width, height } = this.props;
|
||||
const media = fromJS({
|
||||
type: 'video',
|
||||
url: src,
|
||||
preview_url: preview,
|
||||
width,
|
||||
height,
|
||||
});
|
||||
|
||||
this.video.pause();
|
||||
this.props.onOpenVideo(this.video.currentTime);
|
||||
this.props.onOpenVideo(media, this.video.currentTime);
|
||||
}
|
||||
|
||||
handleCloseVideo = () => {
|
||||
@ -270,7 +282,16 @@ export default class Video extends React.PureComponent {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classNames('video-player', { inactive: !revealed, detailed, inline: inline && !fullscreen, fullscreen })} style={playerStyle} ref={this.setPlayerRef} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}>
|
||||
<div
|
||||
role='menuitem'
|
||||
className={classNames('video-player', { inactive: !revealed, detailed, inline: inline && !fullscreen, fullscreen })}
|
||||
style={playerStyle}
|
||||
ref={this.setPlayerRef}
|
||||
onMouseEnter={this.handleMouseEnter}
|
||||
onMouseLeave={this.handleMouseLeave}
|
||||
onClick={this.handleClickRoot}
|
||||
tabIndex={0}
|
||||
>
|
||||
<video
|
||||
ref={this.setVideoRef}
|
||||
src={src}
|
||||
|
@ -1870,4 +1870,4 @@
|
||||
],
|
||||
"path": "app/javascript/mastodon/features/video/index.json"
|
||||
}
|
||||
]
|
||||
]
|
@ -28,11 +28,10 @@ self.addEventListener('fetch', function(event) {
|
||||
const asyncResponse = fetchRoot();
|
||||
const asyncCache = openWebCache();
|
||||
|
||||
event.respondWith(asyncResponse.then(async response => {
|
||||
event.respondWith(asyncResponse.then(response => {
|
||||
if (response.ok) {
|
||||
const cache = await asyncCache;
|
||||
await cache.put('/', response);
|
||||
return response.clone();
|
||||
return asyncCache.then(cache => cache.put('/', response))
|
||||
.then(() => response.clone());
|
||||
}
|
||||
|
||||
throw null;
|
||||
@ -41,35 +40,38 @@ self.addEventListener('fetch', function(event) {
|
||||
const asyncResponse = fetch(event.request);
|
||||
const asyncCache = openWebCache();
|
||||
|
||||
event.respondWith(asyncResponse.then(async response => {
|
||||
event.respondWith(asyncResponse.then(response => {
|
||||
if (response.ok || response.type === 'opaqueredirect') {
|
||||
await Promise.all([
|
||||
return Promise.all([
|
||||
asyncCache.then(cache => cache.delete('/')),
|
||||
indexedDB.deleteDatabase('mastodon'),
|
||||
]);
|
||||
]).then(() => response);
|
||||
}
|
||||
|
||||
return response;
|
||||
}));
|
||||
} else if (process.env.CDN_HOST ? url.host === process.env.CDN_HOST : url.pathname.startsWith('/system/')) {
|
||||
event.respondWith(openSystemCache().then(async cache => {
|
||||
const cached = await cache.match(event.request.url);
|
||||
event.respondWith(openSystemCache().then(cache => {
|
||||
return cache.match(event.request.url).then(cached => {
|
||||
if (cached === undefined) {
|
||||
return fetch(event.request).then(fetched => {
|
||||
if (fetched.ok) {
|
||||
const put = cache.put(event.request.url, fetched.clone());
|
||||
|
||||
if (cached === undefined) {
|
||||
const fetched = await fetch(event.request);
|
||||
put.catch(() => freeStorage());
|
||||
|
||||
if (fetched.ok) {
|
||||
try {
|
||||
await cache.put(event.request.url, fetched.clone());
|
||||
} finally {
|
||||
freeStorage();
|
||||
}
|
||||
return put.then(() => {
|
||||
freeStorage();
|
||||
return fetched;
|
||||
});
|
||||
}
|
||||
|
||||
return fetched;
|
||||
});
|
||||
}
|
||||
|
||||
return fetched;
|
||||
}
|
||||
|
||||
return cached;
|
||||
return cached;
|
||||
});
|
||||
}));
|
||||
}
|
||||
});
|
||||
|
@ -182,7 +182,9 @@ export function putStatuses(records) {
|
||||
}
|
||||
|
||||
export function freeStorage() {
|
||||
return navigator.storage.estimate().then(({ quota, usage }) => {
|
||||
// navigator.storage is not present on:
|
||||
// Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.100 Safari/537.36 Edge/16.16299
|
||||
return 'storage' in navigator && navigator.storage.estimate().then(({ quota, usage }) => {
|
||||
if (usage + storageMargin < quota) {
|
||||
return null;
|
||||
}
|
||||
|
@ -6,7 +6,6 @@ function main() {
|
||||
const emojify = require('../mastodon/features/emoji/emoji').default;
|
||||
const { getLocale } = require('../mastodon/locales');
|
||||
const { localeData } = getLocale();
|
||||
const VideoContainer = require('../mastodon/containers/video_container').default;
|
||||
const React = require('react');
|
||||
const ReactDOM = require('react-dom');
|
||||
|
||||
@ -51,30 +50,16 @@ function main() {
|
||||
});
|
||||
});
|
||||
|
||||
[].forEach.call(document.querySelectorAll('[data-component="Video"]'), (content) => {
|
||||
const props = JSON.parse(content.getAttribute('data-props'));
|
||||
ReactDOM.render(<VideoContainer locale={locale} {...props} />, content);
|
||||
});
|
||||
const reactComponents = document.querySelectorAll('[data-component]');
|
||||
if (reactComponents.length > 0) {
|
||||
import(/* webpackChunkName: "containers/media_container" */ '../mastodon/containers/media_container')
|
||||
.then(({ default: MediaContainer }) => {
|
||||
const content = document.createElement('div');
|
||||
|
||||
const cards = document.querySelectorAll('[data-component="Card"]');
|
||||
|
||||
if (cards.length > 0) {
|
||||
import(/* webpackChunkName: "containers/cards_container" */ '../mastodon/containers/cards_container').then(({ default: CardsContainer }) => {
|
||||
const content = document.createElement('div');
|
||||
|
||||
ReactDOM.render(<CardsContainer locale={locale} cards={cards} />, content);
|
||||
document.body.appendChild(content);
|
||||
}).catch(error => console.error(error));
|
||||
}
|
||||
|
||||
const mediaGalleries = document.querySelectorAll('[data-component="MediaGallery"]');
|
||||
|
||||
if (mediaGalleries.length > 0) {
|
||||
const MediaGalleriesContainer = require('../mastodon/containers/media_galleries_container').default;
|
||||
const content = document.createElement('div');
|
||||
|
||||
ReactDOM.render(<MediaGalleriesContainer locale={locale} galleries={mediaGalleries} />, content);
|
||||
document.body.appendChild(content);
|
||||
ReactDOM.render(<MediaContainer locale={locale} components={reactComponents} />, content);
|
||||
document.body.appendChild(content);
|
||||
})
|
||||
.catch(error => console.error(error));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -484,19 +484,12 @@
|
||||
}
|
||||
|
||||
a.name-tag,
|
||||
.name-tag {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.name-tag,
|
||||
a.inline-name-tag,
|
||||
.inline-name-tag {
|
||||
text-decoration: none;
|
||||
color: $secondary-text-color;
|
||||
|
||||
.avatar {
|
||||
display: block;
|
||||
margin: 0;
|
||||
margin-right: 5px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.username {
|
||||
font-weight: 500;
|
||||
}
|
||||
@ -514,6 +507,26 @@ a.name-tag,
|
||||
}
|
||||
}
|
||||
|
||||
a.name-tag,
|
||||
.name-tag {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.avatar {
|
||||
display: block;
|
||||
margin: 0;
|
||||
margin-right: 5px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
&.suspended {
|
||||
.avatar {
|
||||
filter: grayscale(100%);
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.speech-bubble {
|
||||
margin-bottom: 20px;
|
||||
border-left: 4px solid $ui-highlight-color;
|
||||
|
@ -4432,6 +4432,10 @@ a.status-card {
|
||||
max-width: 100%;
|
||||
border-radius: 4px;
|
||||
|
||||
&:focus {
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
video {
|
||||
max-width: 100vw;
|
||||
max-height: 80vh;
|
||||
|
@ -60,8 +60,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
.card-standalone__body,
|
||||
.media-gallery-standalone__body {
|
||||
.media-standalone__body {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
|
@ -1,3 +1,9 @@
|
||||
@keyframes Swag {
|
||||
0% { background-position: 0% 0%; }
|
||||
50% { background-position: 100% 0%; }
|
||||
100% { background-position: 200% 0%; }
|
||||
}
|
||||
|
||||
.table {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
@ -187,6 +193,11 @@ a.table-action-link {
|
||||
|
||||
strong {
|
||||
font-weight: 700;
|
||||
background: linear-gradient(to right, orange , yellow, green, cyan, blue, violet,orange , yellow, green, cyan, blue, violet);
|
||||
background-size: 200% 100%;
|
||||
background-clip: text;
|
||||
color: transparent;
|
||||
animation: Swag 2s linear 0s infinite;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user