mirror of
https://github.com/mastodon/mastodon.git
synced 2024-12-26 18:44:13 +00:00
Merge branch 'master' into glitch-soc/merge-upstream
This commit is contained in:
commit
a16175645d
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
112
app/javascript/mastodon/blurhash.js
Normal file
112
app/javascript/mastodon/blurhash.js
Normal 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)));
|
||||||
|
};
|
|
@ -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 (
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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') {
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 () {
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 = () => {
|
||||||
|
|
|
@ -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) => {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in a new issue