Merge branch 'master' into glitch-soc/merge-upstream

rebase/4.0.0rc2
Claire 2020-12-09 14:42:33 +01:00
commit a16175645d
23 changed files with 235 additions and 212 deletions

View File

@ -82,7 +82,7 @@ class AccountsController < ApplicationController
end end
def account_media_status_ids def account_media_status_ids
@account.media_attachments.attached.reorder(nil).select(:status_id).distinct @account.media_attachments.attached.reorder(nil).select(:status_id).group(:status_id)
end end
def no_replies_scope def no_replies_scope

View File

@ -14,7 +14,7 @@ module Admin
@statuses = @account.statuses.where(visibility: [:public, :unlisted]) @statuses = @account.statuses.where(visibility: [:public, :unlisted])
if params[:media] if params[:media]
account_media_status_ids = @account.media_attachments.attached.reorder(nil).select(:status_id).distinct account_media_status_ids = @account.media_attachments.attached.reorder(nil).select(:status_id).group(:status_id)
@statuses.merge!(Status.where(id: account_media_status_ids)) @statuses.merge!(Status.where(id: account_media_status_ids))
end end

View File

@ -1,7 +1,6 @@
import api from '../api'; import api from '../api';
import { debounce } from 'lodash'; import { debounce } from 'lodash';
import compareId from '../compare_id'; import compareId from '../compare_id';
import { showAlertForError } from './alerts';
export const MARKERS_FETCH_REQUEST = 'MARKERS_FETCH_REQUEST'; export const MARKERS_FETCH_REQUEST = 'MARKERS_FETCH_REQUEST';
export const MARKERS_FETCH_SUCCESS = 'MARKERS_FETCH_SUCCESS'; export const MARKERS_FETCH_SUCCESS = 'MARKERS_FETCH_SUCCESS';
@ -29,15 +28,19 @@ export const synchronouslySubmitMarkers = () => (dispatch, getState) => {
}, },
body: JSON.stringify(params), body: JSON.stringify(params),
}); });
return; return;
} else if (navigator && navigator.sendBeacon) { } else if (navigator && navigator.sendBeacon) {
// Failing that, we can use sendBeacon, but we have to encode the data as // Failing that, we can use sendBeacon, but we have to encode the data as
// FormData for DoorKeeper to recognize the token. // FormData for DoorKeeper to recognize the token.
const formData = new FormData(); const formData = new FormData();
formData.append('bearer_token', accessToken); formData.append('bearer_token', accessToken);
for (const [id, value] of Object.entries(params)) { for (const [id, value] of Object.entries(params)) {
formData.append(`${id}[last_read_id]`, value.last_read_id); formData.append(`${id}[last_read_id]`, value.last_read_id);
} }
if (navigator.sendBeacon('/api/v1/markers', formData)) { if (navigator.sendBeacon('/api/v1/markers', formData)) {
return; return;
} }
@ -85,11 +88,9 @@ const debouncedSubmitMarkers = debounce((dispatch, getState) => {
return; return;
} }
api().post('/api/v1/markers', params).then(() => { api(getState).post('/api/v1/markers', params).then(() => {
dispatch(submitMarkersSuccess(params)); dispatch(submitMarkersSuccess(params));
}).catch(error => { }).catch(() => {});
dispatch(showAlertForError(error));
});
}, 300000, { leading: true, trailing: true }); }, 300000, { leading: true, trailing: true });
export function submitMarkersSuccess({ home, notifications }) { export function submitMarkersSuccess({ home, notifications }) {
@ -102,9 +103,11 @@ export function submitMarkersSuccess({ home, notifications }) {
export function submitMarkers(params = {}) { export function submitMarkers(params = {}) {
const result = (dispatch, getState) => debouncedSubmitMarkers(dispatch, getState); const result = (dispatch, getState) => debouncedSubmitMarkers(dispatch, getState);
if (params.immediate === true) { if (params.immediate === true) {
debouncedSubmitMarkers.flush(); debouncedSubmitMarkers.flush();
} }
return result; return result;
}; };

View File

@ -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)));
};

View File

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import 'wicg-inert'; import 'wicg-inert';
import { normal } from 'color-blend'; import { multiply } from 'color-blend';
export default class ModalRoot extends React.PureComponent { export default class ModalRoot extends React.PureComponent {
@ -98,7 +98,7 @@ export default class ModalRoot extends React.PureComponent {
let backgroundColor = null; let backgroundColor = null;
if (this.props.backgroundColor) { if (this.props.backgroundColor) {
backgroundColor = normal({ ...this.props.backgroundColor, a: 1 }, { r: 0, g: 0, b: 0, a: 0.3 }); backgroundColor = multiply({ ...this.props.backgroundColor, a: 1 }, { r: 0, g: 0, b: 0, a: 0.7 });
} }
return ( return (

View File

@ -97,7 +97,7 @@ class Status extends ImmutablePureComponent {
cachedMediaWidth: PropTypes.number, cachedMediaWidth: PropTypes.number,
scrollKey: PropTypes.string, scrollKey: PropTypes.string,
deployPictureInPicture: PropTypes.func, deployPictureInPicture: PropTypes.func,
pictureInPicture: PropTypes.shape({ pictureInPicture: ImmutablePropTypes.contains({
inUse: PropTypes.bool, inUse: PropTypes.bool,
available: PropTypes.bool, available: PropTypes.bool,
}), }),
@ -192,8 +192,9 @@ class Status extends ImmutablePureComponent {
return <div className='audio-player' style={{ height: '110px' }} />; return <div className='audio-player' style={{ height: '110px' }} />;
} }
handleOpenVideo = (media, options) => { handleOpenVideo = (options) => {
this.props.onOpenVideo(this._properStatus().get('id'), media, options); const status = this._properStatus();
this.props.onOpenVideo(status.get('id'), status.getIn(['media_attachments', 0]), options);
} }
handleOpenMedia = (media, index) => { handleOpenMedia = (media, index) => {
@ -202,15 +203,15 @@ class Status extends ImmutablePureComponent {
handleHotkeyOpenMedia = e => { handleHotkeyOpenMedia = e => {
const { onOpenMedia, onOpenVideo } = this.props; const { onOpenMedia, onOpenVideo } = this.props;
const statusId = this._properStatus().get('id'); const status = this._properStatus();
e.preventDefault(); e.preventDefault();
if (status.get('media_attachments').size > 0) { if (status.get('media_attachments').size > 0) {
if (status.getIn(['media_attachments', 0, 'type']) === 'video') { if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
onOpenVideo(statusId, status.getIn(['media_attachments', 0]), { startTime: 0 }); onOpenVideo(status.get('id'), status.getIn(['media_attachments', 0]), { startTime: 0 });
} else { } else {
onOpenMedia(statusId, status.get('media_attachments'), 0); onOpenMedia(status.get('id'), status.get('media_attachments'), 0);
} }
} }
} }
@ -353,7 +354,7 @@ class Status extends ImmutablePureComponent {
status = status.get('reblog'); status = status.get('reblog');
} }
if (pictureInPicture.inUse) { if (pictureInPicture.get('inUse')) {
media = <PictureInPicturePlaceholder width={this.props.cachedMediaWidth} />; media = <PictureInPicturePlaceholder width={this.props.cachedMediaWidth} />;
} else if (status.get('media_attachments').size > 0) { } else if (status.get('media_attachments').size > 0) {
if (this.props.muted) { if (this.props.muted) {
@ -380,7 +381,7 @@ class Status extends ImmutablePureComponent {
width={this.props.cachedMediaWidth} width={this.props.cachedMediaWidth}
height={110} height={110}
cacheWidth={this.props.cacheMediaWidth} cacheWidth={this.props.cacheMediaWidth}
deployPictureInPicture={pictureInPicture.available ? this.handleDeployPictureInPicture : undefined} deployPictureInPicture={pictureInPicture.get('available') ? this.handleDeployPictureInPicture : undefined}
/> />
)} )}
</Bundle> </Bundle>
@ -403,7 +404,7 @@ class Status extends ImmutablePureComponent {
sensitive={status.get('sensitive')} sensitive={status.get('sensitive')}
onOpenVideo={this.handleOpenVideo} onOpenVideo={this.handleOpenVideo}
cacheWidth={this.props.cacheMediaWidth} cacheWidth={this.props.cacheMediaWidth}
deployPictureInPicture={pictureInPicture.available ? this.handleDeployPictureInPicture : undefined} deployPictureInPicture={pictureInPicture.get('available') ? this.handleDeployPictureInPicture : undefined}
visible={this.state.showMedia} visible={this.state.showMedia}
onToggleVisibility={this.handleToggleMediaVisibility} onToggleVisibility={this.handleToggleMediaVisibility}
/> />
@ -431,7 +432,7 @@ class Status extends ImmutablePureComponent {
} else if (status.get('spoiler_text').length === 0 && status.get('card')) { } else if (status.get('spoiler_text').length === 0 && status.get('card')) {
media = ( media = (
<Card <Card
onOpenMedia={this.props.onOpenMedia} onOpenMedia={this.handleOpenMedia}
card={status.get('card')} card={status.get('card')}
compact compact
cacheWidth={this.props.cacheMediaWidth} cacheWidth={this.props.cacheMediaWidth}

View File

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import Status from '../components/status'; import Status from '../components/status';
import { makeGetStatus } from '../selectors'; import { makeGetStatus, makeGetPictureInPicture } from '../selectors';
import { import {
replyCompose, replyCompose,
mentionCompose, mentionCompose,
@ -54,14 +54,11 @@ const messages = defineMessages({
const makeMapStateToProps = () => { const makeMapStateToProps = () => {
const getStatus = makeGetStatus(); const getStatus = makeGetStatus();
const getPictureInPicture = makeGetPictureInPicture();
const mapStateToProps = (state, props) => ({ const mapStateToProps = (state, props) => ({
status: getStatus(state, props), status: getStatus(state, props),
pictureInPicture: getPictureInPicture(state, props),
pictureInPicture: {
inUse: state.getIn(['meta', 'layout']) !== 'mobile' && state.get('picture_in_picture').statusId === props.id,
available: state.getIn(['meta', 'layout']) !== 'mobile',
},
}); });
return mapStateToProps; return mapStateToProps;

View File

@ -20,9 +20,9 @@ import RadioButton from 'mastodon/components/radio_button';
const messages = defineMessages({ const messages = defineMessages({
deleteMessage: { id: 'confirmations.delete_list.message', defaultMessage: 'Are you sure you want to permanently delete this list?' }, deleteMessage: { id: 'confirmations.delete_list.message', defaultMessage: 'Are you sure you want to permanently delete this list?' },
deleteConfirm: { id: 'confirmations.delete_list.confirm', defaultMessage: 'Delete' }, deleteConfirm: { id: 'confirmations.delete_list.confirm', defaultMessage: 'Delete' },
all_replies: { id: 'lists.replies_policy.all_replies', defaultMessage: 'Any followed user' }, followed: { id: 'lists.replies_policy.followed', defaultMessage: 'Any followed user' },
no_replies: { id: 'lists.replies_policy.no_replies', defaultMessage: 'No one' }, none: { id: 'lists.replies_policy.none', defaultMessage: 'No one' },
list_replies: { id: 'lists.replies_policy.list_replies', defaultMessage: 'Members of the list' }, list: { id: 'lists.replies_policy.list', defaultMessage: 'Members of the list' },
}); });
const mapStateToProps = (state, props) => ({ const mapStateToProps = (state, props) => ({
@ -193,7 +193,7 @@ class ListTimeline extends React.PureComponent {
<FormattedMessage id='lists.replies_policy.title' defaultMessage='Show replies to:' /> <FormattedMessage id='lists.replies_policy.title' defaultMessage='Show replies to:' />
</span> </span>
<div className='column-settings__row'> <div className='column-settings__row'>
{ ['no_replies', 'list_replies', 'all_replies'].map(policy => ( { ['none', 'list', 'followed'].map(policy => (
<RadioButton name='order' value={policy} label={intl.formatMessage(messages[policy])} checked={replies_policy === policy} onChange={this.handleRepliesPolicyChange} /> <RadioButton name='order' value={policy} label={intl.formatMessage(messages[policy])} checked={replies_policy === policy} onChange={this.handleRepliesPolicyChange} />
))} ))}
</div> </div>

View File

@ -41,7 +41,10 @@ class DetailedStatus extends ImmutablePureComponent {
domain: PropTypes.string.isRequired, domain: PropTypes.string.isRequired,
compact: PropTypes.bool, compact: PropTypes.bool,
showMedia: PropTypes.bool, showMedia: PropTypes.bool,
usingPiP: PropTypes.bool, pictureInPicture: ImmutablePropTypes.contains({
inUse: PropTypes.bool,
available: PropTypes.bool,
}),
onToggleMediaVisibility: PropTypes.func, onToggleMediaVisibility: PropTypes.func,
}; };
@ -58,8 +61,8 @@ class DetailedStatus extends ImmutablePureComponent {
e.stopPropagation(); e.stopPropagation();
} }
handleOpenVideo = (media, options) => { handleOpenVideo = (options) => {
this.props.onOpenVideo(media, options); this.props.onOpenVideo(this.props.status.getIn(['media_attachments', 0]), options);
} }
handleExpandedToggle = () => { handleExpandedToggle = () => {
@ -102,7 +105,7 @@ class DetailedStatus extends ImmutablePureComponent {
render () { render () {
const status = (this.props.status && this.props.status.get('reblog')) ? this.props.status.get('reblog') : this.props.status; const status = (this.props.status && this.props.status.get('reblog')) ? this.props.status.get('reblog') : this.props.status;
const outerStyle = { boxSizing: 'border-box' }; const outerStyle = { boxSizing: 'border-box' };
const { intl, compact, usingPiP } = this.props; const { intl, compact, pictureInPicture } = this.props;
if (!status) { if (!status) {
return null; return null;
@ -118,7 +121,7 @@ class DetailedStatus extends ImmutablePureComponent {
outerStyle.height = `${this.state.height}px`; outerStyle.height = `${this.state.height}px`;
} }
if (usingPiP) { if (pictureInPicture.get('inUse')) {
media = <PictureInPicturePlaceholder />; media = <PictureInPicturePlaceholder />;
} else if (status.get('media_attachments').size > 0) { } else if (status.get('media_attachments').size > 0) {
if (status.getIn(['media_attachments', 0, 'type']) === 'audio') { if (status.getIn(['media_attachments', 0, 'type']) === 'audio') {

View File

@ -1,6 +1,6 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import DetailedStatus from '../components/detailed_status'; import DetailedStatus from '../components/detailed_status';
import { makeGetStatus } from '../../../selectors'; import { makeGetStatus, makeGetPictureInPicture } from '../../../selectors';
import { import {
replyCompose, replyCompose,
mentionCompose, mentionCompose,
@ -40,10 +40,12 @@ const messages = defineMessages({
const makeMapStateToProps = () => { const makeMapStateToProps = () => {
const getStatus = makeGetStatus(); const getStatus = makeGetStatus();
const getPictureInPicture = makeGetPictureInPicture();
const mapStateToProps = (state, props) => ({ const mapStateToProps = (state, props) => ({
status: getStatus(state, props), status: getStatus(state, props),
domain: state.getIn(['meta', 'domain']), domain: state.getIn(['meta', 'domain']),
pictureInPicture: getPictureInPicture(state, props),
}); });
return mapStateToProps; return mapStateToProps;

View File

@ -43,7 +43,7 @@ import {
import { initMuteModal } from '../../actions/mutes'; import { initMuteModal } from '../../actions/mutes';
import { initBlockModal } from '../../actions/blocks'; import { initBlockModal } from '../../actions/blocks';
import { initReport } from '../../actions/reports'; import { initReport } from '../../actions/reports';
import { makeGetStatus } from '../../selectors'; import { makeGetStatus, makeGetPictureInPicture } from '../../selectors';
import { ScrollContainer } from 'react-router-scroll-4'; import { ScrollContainer } from 'react-router-scroll-4';
import ColumnBackButton from '../../components/column_back_button'; import ColumnBackButton from '../../components/column_back_button';
import ColumnHeader from '../../components/column_header'; import ColumnHeader from '../../components/column_header';
@ -72,6 +72,7 @@ const messages = defineMessages({
const makeMapStateToProps = () => { const makeMapStateToProps = () => {
const getStatus = makeGetStatus(); const getStatus = makeGetStatus();
const getPictureInPicture = makeGetPictureInPicture();
const getAncestorsIds = createSelector([ const getAncestorsIds = createSelector([
(_, { id }) => id, (_, { id }) => id,
@ -129,11 +130,12 @@ const makeMapStateToProps = () => {
const mapStateToProps = (state, props) => { const mapStateToProps = (state, props) => {
const status = getStatus(state, { id: props.params.statusId }); const status = getStatus(state, { id: props.params.statusId });
let ancestorsIds = Immutable.List();
let ancestorsIds = Immutable.List();
let descendantsIds = Immutable.List(); let descendantsIds = Immutable.List();
if (status) { if (status) {
ancestorsIds = getAncestorsIds(state, { id: status.get('in_reply_to_id') }); ancestorsIds = getAncestorsIds(state, { id: status.get('in_reply_to_id') });
descendantsIds = getDescendantsIds(state, { id: status.get('id') }); descendantsIds = getDescendantsIds(state, { id: status.get('id') });
} }
@ -143,7 +145,7 @@ const makeMapStateToProps = () => {
descendantsIds, descendantsIds,
askReplyConfirmation: state.getIn(['compose', 'text']).trim().length !== 0, askReplyConfirmation: state.getIn(['compose', 'text']).trim().length !== 0,
domain: state.getIn(['meta', 'domain']), domain: state.getIn(['meta', 'domain']),
usingPiP: state.get('picture_in_picture').statusId === props.params.statusId, pictureInPicture: getPictureInPicture(state, { id: props.params.statusId }),
}; };
}; };
@ -168,7 +170,10 @@ class Status extends ImmutablePureComponent {
askReplyConfirmation: PropTypes.bool, askReplyConfirmation: PropTypes.bool,
multiColumn: PropTypes.bool, multiColumn: PropTypes.bool,
domain: PropTypes.string.isRequired, domain: PropTypes.string.isRequired,
usingPiP: PropTypes.bool, pictureInPicture: ImmutablePropTypes.contains({
inUse: PropTypes.bool,
available: PropTypes.bool,
}),
}; };
state = { state = {
@ -492,7 +497,7 @@ class Status extends ImmutablePureComponent {
render () { render () {
let ancestors, descendants; let ancestors, descendants;
const { shouldUpdateScroll, status, ancestorsIds, descendantsIds, intl, domain, multiColumn, usingPiP } = this.props; const { shouldUpdateScroll, status, ancestorsIds, descendantsIds, intl, domain, multiColumn, pictureInPicture } = this.props;
const { fullscreen } = this.state; const { fullscreen } = this.state;
if (status === null) { if (status === null) {
@ -550,7 +555,7 @@ class Status extends ImmutablePureComponent {
domain={domain} domain={domain}
showMedia={this.state.showMedia} showMedia={this.state.showMedia}
onToggleMediaVisibility={this.handleToggleMediaVisibility} onToggleMediaVisibility={this.handleToggleMediaVisibility}
usingPiP={usingPiP} pictureInPicture={pictureInPicture}
/> />
<ActionBar <ActionBar

View File

@ -4,13 +4,11 @@ import PropTypes from 'prop-types';
import Audio from 'mastodon/features/audio'; import Audio from 'mastodon/features/audio';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePureComponent from 'react-immutable-pure-component';
import { FormattedMessage } from 'react-intl';
import { previewState } from './video_modal'; import { previewState } from './video_modal';
import classNames from 'classnames'; import Footer from 'mastodon/features/picture_in_picture/components/footer';
import Icon from 'mastodon/components/icon';
const mapStateToProps = (state, { status }) => ({ const mapStateToProps = (state, { statusId }) => ({
account: state.getIn(['accounts', status.get('account')]), accountStaticAvatar: state.getIn(['accounts', state.getIn(['statuses', statusId, 'account']), 'avatar_static']),
}); });
export default @connect(mapStateToProps) export default @connect(mapStateToProps)
@ -18,12 +16,13 @@ class AudioModal extends ImmutablePureComponent {
static propTypes = { static propTypes = {
media: ImmutablePropTypes.map.isRequired, media: ImmutablePropTypes.map.isRequired,
status: ImmutablePropTypes.map, statusId: PropTypes.string.isRequired,
accountStaticAvatar: PropTypes.string.isRequired,
options: PropTypes.shape({ options: PropTypes.shape({
autoPlay: PropTypes.bool, autoPlay: PropTypes.bool,
}), }),
account: ImmutablePropTypes.map,
onClose: PropTypes.func.isRequired, onClose: PropTypes.func.isRequired,
onChangeBackgroundColor: PropTypes.func.isRequired,
}; };
static contextTypes = { static contextTypes = {
@ -52,15 +51,8 @@ class AudioModal 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 () { render () {
const { media, status, account } = this.props; const { media, accountStaticAvatar, statusId, onClose } = this.props;
const options = this.props.options || {}; const options = this.props.options || {};
return ( return (
@ -71,7 +63,7 @@ class AudioModal extends ImmutablePureComponent {
alt={media.get('description')} alt={media.get('description')}
duration={media.getIn(['meta', 'original', 'duration'], 0)} duration={media.getIn(['meta', 'original', 'duration'], 0)}
height={150} height={150}
poster={media.get('preview_url') || account.get('avatar_static')} poster={media.get('preview_url') || accountStaticAvatar}
backgroundColor={media.getIn(['meta', 'colors', 'background'])} backgroundColor={media.getIn(['meta', 'colors', 'background'])}
foregroundColor={media.getIn(['meta', 'colors', 'foreground'])} foregroundColor={media.getIn(['meta', 'colors', 'foreground'])}
accentColor={media.getIn(['meta', 'colors', 'accent'])} accentColor={media.getIn(['meta', 'colors', 'accent'])}
@ -79,11 +71,9 @@ class AudioModal extends ImmutablePureComponent {
/> />
</div> </div>
{status && ( <div className='media-modal__overlay'>
<div className={classNames('media-modal__meta')}> {statusId && <Footer statusId={statusId} withOpenButton onClose={onClose} />}
<a href={status.get('url')} onClick={this.handleStatusClick}><Icon id='comments' /> <FormattedMessage id='lightbox.view_context' defaultMessage='View context' /></a> </div>
</div>
)}
</div> </div>
); );
} }

View File

@ -75,7 +75,9 @@ class ColumnsArea extends ImmutablePureComponent {
} }
componentWillReceiveProps() { componentWillReceiveProps() {
this.setState({ shouldAnimate: false }); if (typeof this.pendingIndex !== 'number' && this.lastIndex !== getIndex(this.context.router.history.location.pathname)) {
this.setState({ shouldAnimate: false });
}
} }
componentDidMount() { componentDidMount() {
@ -99,8 +101,13 @@ class ColumnsArea extends ImmutablePureComponent {
if (this.props.singleColumn !== prevProps.singleColumn && !this.props.singleColumn) { if (this.props.singleColumn !== prevProps.singleColumn && !this.props.singleColumn) {
this.node.addEventListener('wheel', this.handleWheel, supportsPassiveEvents ? { passive: true } : false); this.node.addEventListener('wheel', this.handleWheel, supportsPassiveEvents ? { passive: true } : false);
} }
this.lastIndex = getIndex(this.context.router.history.location.pathname);
this.setState({ shouldAnimate: true }); const newIndex = getIndex(this.context.router.history.location.pathname);
if (this.lastIndex !== newIndex) {
this.lastIndex = newIndex;
this.setState({ shouldAnimate: true });
}
} }
componentWillUnmount () { componentWillUnmount () {

View File

@ -12,6 +12,7 @@ import Icon from 'mastodon/components/icon';
import GIFV from 'mastodon/components/gifv'; import GIFV from 'mastodon/components/gifv';
import { disableSwiping } from 'mastodon/initial_state'; import { disableSwiping } from 'mastodon/initial_state';
import Footer from 'mastodon/features/picture_in_picture/components/footer'; import Footer from 'mastodon/features/picture_in_picture/components/footer';
import { getAverageFromBlurhash } from 'mastodon/blurhash';
const messages = defineMessages({ const messages = defineMessages({
close: { id: 'lightbox.close', defaultMessage: 'Close' }, close: { id: 'lightbox.close', defaultMessage: 'Close' },
@ -21,111 +22,6 @@ const messages = defineMessages({
export const previewState = 'previewMediaModal'; export const previewState = 'previewMediaModal';
const digitCharacters = [
'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',
'#',
'$',
'%',
'*',
'+',
',',
'-',
'.',
':',
';',
'=',
'?',
'@',
'[',
']',
'^',
'_',
'{',
'|',
'}',
'~',
];
const decode83 = (str) => {
let value = 0;
let c, digit;
for (let i = 0; i < str.length; i++) {
c = str[i];
digit = digitCharacters.indexOf(c);
value = value * 83 + digit;
}
return value;
};
const decodeRGB = int => ({
r: Math.max(0, (int >> 16)),
g: Math.max(0, (int >> 8) & 255),
b: Math.max(0, (int & 255)),
});
export default @injectIntl export default @injectIntl
class MediaModal extends ImmutablePureComponent { class MediaModal extends ImmutablePureComponent {
@ -224,7 +120,7 @@ class MediaModal extends ImmutablePureComponent {
const blurhash = media.getIn([index, 'blurhash']); const blurhash = media.getIn([index, 'blurhash']);
if (blurhash) { if (blurhash) {
const backgroundColor = decodeRGB(decode83(blurhash.slice(2, 6))); const backgroundColor = getAverageFromBlurhash(blurhash);
onChangeBackgroundColor(backgroundColor); onChangeBackgroundColor(backgroundColor);
} }
} }

View File

@ -3,6 +3,8 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import Video from 'mastodon/features/video'; import Video from 'mastodon/features/video';
import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePureComponent from 'react-immutable-pure-component';
import Footer from 'mastodon/features/picture_in_picture/components/footer';
import { getAverageFromBlurhash } from 'mastodon/blurhash';
export const previewState = 'previewVideoModal'; export const previewState = 'previewVideoModal';
@ -17,6 +19,7 @@ export default class VideoModal extends ImmutablePureComponent {
defaultVolume: PropTypes.number, defaultVolume: PropTypes.number,
}), }),
onClose: PropTypes.func.isRequired, onClose: PropTypes.func.isRequired,
onChangeBackgroundColor: PropTypes.func.isRequired,
}; };
static contextTypes = { static contextTypes = {
@ -24,29 +27,35 @@ export default class VideoModal extends ImmutablePureComponent {
}; };
componentDidMount () { componentDidMount () {
if (this.context.router) { const { router } = this.context;
const history = this.context.router.history; const { media, onChangeBackgroundColor, onClose } = this.props;
history.push(history.location.pathname, previewState); if (router) {
router.history.push(router.history.location.pathname, previewState);
this.unlistenHistory = router.history.listen(() => onClose());
}
this.unlistenHistory = history.listen(() => { const backgroundColor = getAverageFromBlurhash(media.get('blurhash'));
this.props.onClose();
}); if (backgroundColor) {
onChangeBackgroundColor(backgroundColor);
} }
} }
componentWillUnmount () { componentWillUnmount () {
if (this.context.router) { const { router } = this.context;
if (router) {
this.unlistenHistory(); this.unlistenHistory();
if (this.context.router.history.location.state === previewState) { if (router.history.location.state === previewState) {
this.context.router.history.goBack(); router.history.goBack();
} }
} }
} }
render () { render () {
const { media, onClose } = this.props; const { media, statusId, onClose } = this.props;
const options = this.props.options || {}; const options = this.props.options || {};
return ( return (
@ -65,6 +74,10 @@ export default class VideoModal extends ImmutablePureComponent {
alt={media.get('description')} alt={media.get('description')}
/> />
</div> </div>
<div className='media-modal__overlay'>
{statusId && <Footer statusId={statusId} withOpenButton onClose={onClose} />}
</div>
</div> </div>
); );
} }

View File

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import { fromJS, is } from 'immutable'; import { is } from 'immutable';
import { throttle, debounce } from 'lodash'; import { throttle, debounce } from 'lodash';
import classNames from 'classnames'; import classNames from 'classnames';
import { isFullscreen, requestFullscreen, exitFullscreen } from '../ui/util/fullscreen'; import { isFullscreen, requestFullscreen, exitFullscreen } from '../ui/util/fullscreen';
@ -495,25 +495,13 @@ class Video extends React.PureComponent {
} }
handleOpenVideo = () => { handleOpenVideo = () => {
const { src, preview, width, height, alt } = this.props; this.video.pause();
const media = fromJS({ this.props.onOpenVideo({
type: 'video',
url: src,
preview_url: preview,
description: alt,
width,
height,
});
const options = {
startTime: this.video.currentTime, startTime: this.video.currentTime,
autoPlay: !this.state.paused, autoPlay: !this.state.paused,
defaultVolume: this.state.volume, defaultVolume: this.state.volume,
}; });
this.video.pause();
this.props.onOpenVideo(media, options);
} }
handleCloseVideo = () => { handleCloseVideo = () => {

View File

@ -1,5 +1,5 @@
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import { List as ImmutableList, is } from 'immutable'; import { List as ImmutableList, Map as ImmutableMap, is } from 'immutable';
import { me } from '../initial_state'; import { me } from '../initial_state';
const getAccountBase = (state, id) => state.getIn(['accounts', id], null); const getAccountBase = (state, id) => state.getIn(['accounts', id], null);
@ -121,6 +121,16 @@ export const makeGetStatus = () => {
); );
}; };
export const makeGetPictureInPicture = () => {
return createSelector([
(state, { id }) => state.get('picture_in_picture').statusId === id,
(state) => state.getIn(['meta', 'layout']) !== 'mobile',
], (inUse, available) => ImmutableMap({
inUse: inUse && available,
available,
}));
};
const getAlertsBase = state => state.get('alerts'); const getAlertsBase = state => state.get('alerts');
export const getAlerts = createSelector([getAlertsBase], (base) => { export const getAlerts = createSelector([getAlertsBase], (base) => {

View File

@ -403,8 +403,8 @@ class FeedManager
def filter_from_list?(status, list) def filter_from_list?(status, list)
if status.reply? && status.in_reply_to_account_id != status.account_id if status.reply? && status.in_reply_to_account_id != status.account_id
should_filter = status.in_reply_to_account_id != list.account_id should_filter = status.in_reply_to_account_id != list.account_id
should_filter &&= !list.show_all_replies? should_filter &&= !list.show_followed?
should_filter &&= !(list.show_list_replies? && ListAccount.where(list_id: list.id, account_id: status.in_reply_to_account_id).exists?) should_filter &&= !(list.show_list? && ListAccount.where(list_id: list.id, account_id: status.in_reply_to_account_id).exists?)
return !!should_filter return !!should_filter
end end

View File

@ -445,7 +445,7 @@ class Account < ApplicationRecord
end end
def inboxes def inboxes
urls = reorder(nil).where(protocol: :activitypub).pluck(Arel.sql("distinct coalesce(nullif(accounts.shared_inbox_url, ''), accounts.inbox_url)")) urls = reorder(nil).where(protocol: :activitypub).group(:preferred_inbox_url).pluck(Arel.sql("coalesce(nullif(accounts.shared_inbox_url, ''), accounts.inbox_url) AS preferred_inbox_url"))
DeliveryFailureTracker.without_unavailable(urls) DeliveryFailureTracker.without_unavailable(urls)
end end

View File

@ -51,7 +51,7 @@ class Form::AccountBatch
end end
def account_domains def account_domains
accounts.pluck(Arel.sql('distinct domain')).compact accounts.group(:domain).pluck(:domain).compact
end end
def accounts def accounts

View File

@ -8,7 +8,7 @@
# title :string default(""), not null # title :string default(""), not null
# created_at :datetime not null # created_at :datetime not null
# updated_at :datetime not null # updated_at :datetime not null
# replies_policy :integer default("list_replies"), not null # replies_policy :integer default("list"), not null
# #
class List < ApplicationRecord class List < ApplicationRecord
@ -16,7 +16,7 @@ class List < ApplicationRecord
PER_ACCOUNT_LIMIT = 50 PER_ACCOUNT_LIMIT = 50
enum replies_policy: [:list_replies, :all_replies, :no_replies], _prefix: :show enum replies_policy: [:list, :followed, :none], _prefix: :show
belongs_to :account, optional: true belongs_to :account, optional: true

View File

@ -9,10 +9,6 @@ class REST::AccountFeaturedTagSerializer < ActiveModel::Serializer
object.tag.id.to_s object.tag.id.to_s
end end
def name
"##{object.name}"
end
def url def url
short_account_tag_url(object.account, object.tag) short_account_tag_url(object.account, object.tag)
end end

View File

@ -342,7 +342,7 @@ RSpec.describe FeedManager do
context 'when replies policy is set to no replies' do context 'when replies policy is set to no replies' do
before do before do
list.replies_policy = :no_replies list.replies_policy = :none
end end
it 'pushes statuses that are not replies' do it 'pushes statuses that are not replies' do
@ -365,7 +365,7 @@ RSpec.describe FeedManager do
context 'when replies policy is set to list-only replies' do context 'when replies policy is set to list-only replies' do
before do before do
list.replies_policy = :list_replies list.replies_policy = :list
end end
it 'pushes statuses that are not replies' do it 'pushes statuses that are not replies' do
@ -394,7 +394,7 @@ RSpec.describe FeedManager do
context 'when replies policy is set to any reply' do context 'when replies policy is set to any reply' do
before do before do
list.replies_policy = :all_replies list.replies_policy = :followed
end end
it 'pushes statuses that are not replies' do it 'pushes statuses that are not replies' do