Merge pull request #1472 from ThibG/glitch-soc/merge-upstream

Merge upstream changes
main
ThibG 2020-12-09 19:06:13 +01:00 committed by GitHub
commit b27d11dd33
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 257 additions and 224 deletions

View File

@ -82,7 +82,7 @@ class AccountsController < ApplicationController
end
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
def no_replies_scope

View File

@ -14,7 +14,7 @@ module Admin
@statuses = @account.statuses.where(visibility: [:public, :unlisted])
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))
end

View File

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

View File

@ -19,9 +19,9 @@ import RadioButton from 'flavours/glitch/components/radio_button';
const messages = defineMessages({
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' },
all_replies: { id: 'lists.replies_policy.all_replies', defaultMessage: 'Any followed user' },
no_replies: { id: 'lists.replies_policy.no_replies', defaultMessage: 'No one' },
list_replies: { id: 'lists.replies_policy.list_replies', defaultMessage: 'Members of the list' },
followed: { id: 'lists.replies_policy.followed', defaultMessage: 'Any followed user' },
none: { id: 'lists.replies_policy.none', defaultMessage: 'No one' },
list: { id: 'lists.replies_policy.list', defaultMessage: 'Members of the list' },
});
const mapStateToProps = (state, props) => ({
@ -193,7 +193,7 @@ class ListTimeline extends React.PureComponent {
<FormattedMessage id='lists.replies_policy.title' defaultMessage='Show replies to:' />
</span>
<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} />
))}
</div>

View File

@ -75,7 +75,9 @@ class ColumnsArea extends ImmutablePureComponent {
}
componentWillReceiveProps() {
this.setState({ shouldAnimate: false });
if (typeof this.pendingIndex !== 'number' && this.lastIndex !== getIndex(this.context.router.history.location.pathname)) {
this.setState({ shouldAnimate: false });
}
}
componentDidMount() {
@ -99,8 +101,13 @@ class ColumnsArea extends ImmutablePureComponent {
if (this.props.singleColumn !== prevProps.singleColumn && !this.props.singleColumn) {
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 () {

View File

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

View File

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

View File

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

View File

@ -20,9 +20,9 @@ import RadioButton from 'mastodon/components/radio_button';
const messages = defineMessages({
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' },
all_replies: { id: 'lists.replies_policy.all_replies', defaultMessage: 'Any followed user' },
no_replies: { id: 'lists.replies_policy.no_replies', defaultMessage: 'No one' },
list_replies: { id: 'lists.replies_policy.list_replies', defaultMessage: 'Members of the list' },
followed: { id: 'lists.replies_policy.followed', defaultMessage: 'Any followed user' },
none: { id: 'lists.replies_policy.none', defaultMessage: 'No one' },
list: { id: 'lists.replies_policy.list', defaultMessage: 'Members of the list' },
});
const mapStateToProps = (state, props) => ({
@ -193,7 +193,7 @@ class ListTimeline extends React.PureComponent {
<FormattedMessage id='lists.replies_policy.title' defaultMessage='Show replies to:' />
</span>
<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} />
))}
</div>

View File

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

View File

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

View File

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

View File

@ -4,13 +4,11 @@ import PropTypes from 'prop-types';
import Audio from 'mastodon/features/audio';
import { connect } from 'react-redux';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { FormattedMessage } from 'react-intl';
import { previewState } from './video_modal';
import classNames from 'classnames';
import Icon from 'mastodon/components/icon';
import Footer from 'mastodon/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)
@ -18,12 +16,13 @@ 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 = {
@ -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 () {
const { media, status, account } = this.props;
const { media, accountStaticAvatar, statusId, onClose } = this.props;
const options = this.props.options || {};
return (
@ -71,7 +63,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'])}
@ -79,11 +71,9 @@ class AudioModal extends ImmutablePureComponent {
/>
</div>
{status && (
<div className={classNames('media-modal__meta')}>
<a href={status.get('url')} onClick={this.handleStatusClick}><Icon id='comments' /> <FormattedMessage id='lightbox.view_context' defaultMessage='View context' /></a>
</div>
)}
<div className='media-modal__overlay'>
{statusId && <Footer statusId={statusId} withOpenButton onClose={onClose} />}
</div>
</div>
);
}

View File

@ -75,7 +75,9 @@ class ColumnsArea extends ImmutablePureComponent {
}
componentWillReceiveProps() {
this.setState({ shouldAnimate: false });
if (typeof this.pendingIndex !== 'number' && this.lastIndex !== getIndex(this.context.router.history.location.pathname)) {
this.setState({ shouldAnimate: false });
}
}
componentDidMount() {
@ -99,8 +101,13 @@ class ColumnsArea extends ImmutablePureComponent {
if (this.props.singleColumn !== prevProps.singleColumn && !this.props.singleColumn) {
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 () {

View File

@ -12,6 +12,7 @@ import Icon from 'mastodon/components/icon';
import GIFV from 'mastodon/components/gifv';
import { disableSwiping } from 'mastodon/initial_state';
import Footer from 'mastodon/features/picture_in_picture/components/footer';
import { getAverageFromBlurhash } from 'mastodon/blurhash';
const messages = defineMessages({
close: { id: 'lightbox.close', defaultMessage: 'Close' },
@ -21,111 +22,6 @@ const messages = defineMessages({
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
class MediaModal extends ImmutablePureComponent {
@ -224,7 +120,7 @@ class MediaModal extends ImmutablePureComponent {
const blurhash = media.getIn([index, 'blurhash']);
if (blurhash) {
const backgroundColor = decodeRGB(decode83(blurhash.slice(2, 6)));
const backgroundColor = getAverageFromBlurhash(blurhash);
onChangeBackgroundColor(backgroundColor);
}
}

View File

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

View File

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

View File

@ -1,5 +1,5 @@
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';
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');
export const getAlerts = createSelector([getAlertsBase], (base) => {

View File

@ -403,8 +403,8 @@ class FeedManager
def filter_from_list?(status, list)
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 &&= !list.show_all_replies?
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_followed?
should_filter &&= !(list.show_list? && ListAccount.where(list_id: list.id, account_id: status.in_reply_to_account_id).exists?)
return !!should_filter
end

View File

@ -445,7 +445,7 @@ class Account < ApplicationRecord
end
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)
end

View File

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

View File

@ -8,7 +8,7 @@
# title :string default(""), not null
# created_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
@ -16,7 +16,7 @@ class List < ApplicationRecord
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

View File

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

View File

@ -342,7 +342,7 @@ RSpec.describe FeedManager do
context 'when replies policy is set to no replies' do
before do
list.replies_policy = :no_replies
list.replies_policy = :none
end
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
before do
list.replies_policy = :list_replies
list.replies_policy = :list
end
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
before do
list.replies_policy = :all_replies
list.replies_policy = :followed
end
it 'pushes statuses that are not replies' do