Merge pull request #1472 from ThibG/glitch-soc/merge-upstream
Merge upstream changes
This commit is contained in:
commit
b27d11dd33
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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;
|
||||
};
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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 () {
|
||||
|
@ -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;
|
||||
};
|
||||
|
||||
|
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 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 (
|
||||
|
@ -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}
|
||||
|
@ -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;
|
||||
|
@ -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>
|
||||
|
@ -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') {
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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 () {
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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 = () => {
|
||||
|
@ -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) => {
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user