diff --git a/app/controllers/accounts_controller.rb b/app/controllers/accounts_controller.rb
index 052394ab4a..dfe94af7d9 100644
--- a/app/controllers/accounts_controller.rb
+++ b/app/controllers/accounts_controller.rb
@@ -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
diff --git a/app/controllers/admin/statuses_controller.rb b/app/controllers/admin/statuses_controller.rb
index 6501950346..d7c192f0d6 100644
--- a/app/controllers/admin/statuses_controller.rb
+++ b/app/controllers/admin/statuses_controller.rb
@@ -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
diff --git a/app/javascript/flavours/glitch/actions/markers.js b/app/javascript/flavours/glitch/actions/markers.js
index c0e7a93af6..a086def979 100644
--- a/app/javascript/flavours/glitch/actions/markers.js
+++ b/app/javascript/flavours/glitch/actions/markers.js
@@ -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;
};
diff --git a/app/javascript/flavours/glitch/features/list_timeline/index.js b/app/javascript/flavours/glitch/features/list_timeline/index.js
index 70e530baee..d826c8ccd5 100644
--- a/app/javascript/flavours/glitch/features/list_timeline/index.js
+++ b/app/javascript/flavours/glitch/features/list_timeline/index.js
@@ -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 {
- { ['no_replies', 'list_replies', 'all_replies'].map(policy => (
+ { ['none', 'list', 'followed'].map(policy => (
))}
diff --git a/app/javascript/flavours/glitch/features/ui/components/columns_area.js b/app/javascript/flavours/glitch/features/ui/components/columns_area.js
index 729ade2128..640be19aba 100644
--- a/app/javascript/flavours/glitch/features/ui/components/columns_area.js
+++ b/app/javascript/flavours/glitch/features/ui/components/columns_area.js
@@ -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 () {
diff --git a/app/javascript/mastodon/actions/markers.js b/app/javascript/mastodon/actions/markers.js
index c4b61effdd..16a3df8f63 100644
--- a/app/javascript/mastodon/actions/markers.js
+++ b/app/javascript/mastodon/actions/markers.js
@@ -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;
};
diff --git a/app/javascript/mastodon/blurhash.js b/app/javascript/mastodon/blurhash.js
new file mode 100644
index 0000000000..5adcc3e770
--- /dev/null
+++ b/app/javascript/mastodon/blurhash.js
@@ -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)));
+};
diff --git a/app/javascript/mastodon/components/modal_root.js b/app/javascript/mastodon/components/modal_root.js
index 9bfc0e49ff..26344528ea 100644
--- a/app/javascript/mastodon/components/modal_root.js
+++ b/app/javascript/mastodon/components/modal_root.js
@@ -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 (
diff --git a/app/javascript/mastodon/components/status.js b/app/javascript/mastodon/components/status.js
index 27d96e588d..295e83f581 100644
--- a/app/javascript/mastodon/components/status.js
+++ b/app/javascript/mastodon/components/status.js
@@ -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 ;
}
- 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 = ;
} 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}
/>
)}
@@ -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 = (
{
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;
diff --git a/app/javascript/mastodon/features/list_timeline/index.js b/app/javascript/mastodon/features/list_timeline/index.js
index a3be8fbea3..02b0182473 100644
--- a/app/javascript/mastodon/features/list_timeline/index.js
+++ b/app/javascript/mastodon/features/list_timeline/index.js
@@ -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 {
- { ['no_replies', 'list_replies', 'all_replies'].map(policy => (
+ { ['none', 'list', 'followed'].map(policy => (
))}
diff --git a/app/javascript/mastodon/features/status/components/detailed_status.js b/app/javascript/mastodon/features/status/components/detailed_status.js
index cd29b54895..043a749ede 100644
--- a/app/javascript/mastodon/features/status/components/detailed_status.js
+++ b/app/javascript/mastodon/features/status/components/detailed_status.js
@@ -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 = ;
} else if (status.get('media_attachments').size > 0) {
if (status.getIn(['media_attachments', 0, 'type']) === 'audio') {
diff --git a/app/javascript/mastodon/features/status/containers/detailed_status_container.js b/app/javascript/mastodon/features/status/containers/detailed_status_container.js
index 6d5c33240e..0ac4519c89 100644
--- a/app/javascript/mastodon/features/status/containers/detailed_status_container.js
+++ b/app/javascript/mastodon/features/status/containers/detailed_status_container.js
@@ -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;
diff --git a/app/javascript/mastodon/features/status/index.js b/app/javascript/mastodon/features/status/index.js
index c5e7ba7762..09822f372a 100644
--- a/app/javascript/mastodon/features/status/index.js
+++ b/app/javascript/mastodon/features/status/index.js
@@ -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}
/>
({
- 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 {
/>
- {status && (
-
- )}
+
+ {statusId && }
+
);
}
diff --git a/app/javascript/mastodon/features/ui/components/columns_area.js b/app/javascript/mastodon/features/ui/components/columns_area.js
index 36a84fcbf4..6837450eb5 100644
--- a/app/javascript/mastodon/features/ui/components/columns_area.js
+++ b/app/javascript/mastodon/features/ui/components/columns_area.js
@@ -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 () {
diff --git a/app/javascript/mastodon/features/ui/components/media_modal.js b/app/javascript/mastodon/features/ui/components/media_modal.js
index 58cef1e9da..7fe7ed0947 100644
--- a/app/javascript/mastodon/features/ui/components/media_modal.js
+++ b/app/javascript/mastodon/features/ui/components/media_modal.js
@@ -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);
}
}
diff --git a/app/javascript/mastodon/features/ui/components/video_modal.js b/app/javascript/mastodon/features/ui/components/video_modal.js
index 2c3c026c8f..2f13a175a1 100644
--- a/app/javascript/mastodon/features/ui/components/video_modal.js
+++ b/app/javascript/mastodon/features/ui/components/video_modal.js
@@ -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')}
/>
+
+
+ {statusId && }
+
);
}
diff --git a/app/javascript/mastodon/features/video/index.js b/app/javascript/mastodon/features/video/index.js
index 46eaebd9b9..a2dccdfc04 100644
--- a/app/javascript/mastodon/features/video/index.js
+++ b/app/javascript/mastodon/features/video/index.js
@@ -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 = () => {
diff --git a/app/javascript/mastodon/selectors/index.js b/app/javascript/mastodon/selectors/index.js
index fd3b72f96c..1e19db65d0 100644
--- a/app/javascript/mastodon/selectors/index.js
+++ b/app/javascript/mastodon/selectors/index.js
@@ -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) => {
diff --git a/app/lib/feed_manager.rb b/app/lib/feed_manager.rb
index 3c1f8d6e25..ebd25b398e 100644
--- a/app/lib/feed_manager.rb
+++ b/app/lib/feed_manager.rb
@@ -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
diff --git a/app/models/account.rb b/app/models/account.rb
index 87b89df512..b70978d2be 100644
--- a/app/models/account.rb
+++ b/app/models/account.rb
@@ -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
diff --git a/app/models/form/account_batch.rb b/app/models/form/account_batch.rb
index 882770d7c8..26d6d3abfc 100644
--- a/app/models/form/account_batch.rb
+++ b/app/models/form/account_batch.rb
@@ -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
diff --git a/app/models/list.rb b/app/models/list.rb
index 8493046e58..655d55ff6f 100644
--- a/app/models/list.rb
+++ b/app/models/list.rb
@@ -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
diff --git a/app/serializers/rest/account_featured_tag_serializer.rb b/app/serializers/rest/account_featured_tag_serializer.rb
index d8d5fd68c5..84bef2e629 100644
--- a/app/serializers/rest/account_featured_tag_serializer.rb
+++ b/app/serializers/rest/account_featured_tag_serializer.rb
@@ -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
diff --git a/spec/lib/feed_manager_spec.rb b/spec/lib/feed_manager_spec.rb
index 78563ee94b..2217ad272e 100644
--- a/spec/lib/feed_manager_spec.rb
+++ b/spec/lib/feed_manager_spec.rb
@@ -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