diff --git a/app/soapbox/actions/compose.js b/app/soapbox/actions/compose.js
index 04b1c3cd7..3361188b6 100644
--- a/app/soapbox/actions/compose.js
+++ b/app/soapbox/actions/compose.js
@@ -219,7 +219,7 @@ export function submitCompose(routerHistory, force = false) {
const status = state.getIn(['compose', 'text'], '');
const media = state.getIn(['compose', 'media_attachments']);
- let to = state.getIn(['compose', 'to'], null);
+ let to = state.getIn(['compose', 'to']);
if (!validateSchedule(state)) {
dispatch(snackbar.error(messages.scheduleError));
diff --git a/app/soapbox/components/modal_root.js b/app/soapbox/components/modal_root.js
index 9cda3b890..99b6b1c0e 100644
--- a/app/soapbox/components/modal_root.js
+++ b/app/soapbox/components/modal_root.js
@@ -161,7 +161,7 @@ class ModalRoot extends React.PureComponent {
if (this.unlistenHistory) {
this.unlistenHistory();
}
- if (!['FAVOURITES', 'MENTIONS', 'REACTIONS', 'REBLOGS'].includes(type)) {
+ if (!['FAVOURITES', 'MENTIONS', 'REACTIONS', 'REBLOGS', 'MEDIA'].includes(type)) {
const { state } = this.history.location;
if (state && state.soapboxModalKey === this._modalHistoryKey) {
this.history.goBack();
diff --git a/app/soapbox/features/compose/components/reply_mentions.js b/app/soapbox/features/compose/components/reply_mentions.js
index a6ebbbc7e..5bc6615ac 100644
--- a/app/soapbox/features/compose/components/reply_mentions.js
+++ b/app/soapbox/features/compose/components/reply_mentions.js
@@ -11,6 +11,7 @@ class ReplyMentions extends ImmutablePureComponent {
onOpenMentionsModal: PropTypes.func.isRequired,
explicitAddressing: PropTypes.bool,
to: ImmutablePropTypes.orderedSet,
+ parentTo: ImmutablePropTypes.orderedSet,
isReply: PropTypes.bool,
};
@@ -21,12 +22,23 @@ class ReplyMentions extends ImmutablePureComponent {
}
render() {
- const { explicitAddressing, to, isReply } = this.props;
+ const { explicitAddressing, to, parentTo, isReply } = this.props;
- if (!explicitAddressing || !isReply || !to || to.size === 0) {
+ if (!explicitAddressing || !isReply || !to || (parentTo.size === 0)) {
return null;
}
+ if (to.size === 0) {
+ return (
+
+
+
+ );
+ }
+
return (
{
}
const to = state.getIn(['compose', 'to']);
+ const me = state.get('me');
+ const account = state.getIn(['accounts', me]);
+
+ const parentTo = statusToMentionsAccountIdsArray(state, status, account);
+
return {
to,
+ parentTo,
isReply: true,
explicitAddressing: true,
};
diff --git a/app/soapbox/normalizers/__tests__/instance-test.js b/app/soapbox/normalizers/__tests__/instance-test.js
new file mode 100644
index 000000000..28eea3191
--- /dev/null
+++ b/app/soapbox/normalizers/__tests__/instance-test.js
@@ -0,0 +1,101 @@
+import { Map as ImmutableMap, fromJS } from 'immutable';
+
+import { normalizeInstance } from '../instance';
+
+describe('normalizeInstance()', () => {
+ it('normalizes an empty Map', () => {
+ const expected = ImmutableMap({
+ description_limit: 1500,
+ configuration: ImmutableMap({
+ statuses: ImmutableMap({
+ max_characters: 500,
+ max_media_attachments: 4,
+ }),
+ polls: ImmutableMap({
+ max_options: 4,
+ max_characters_per_option: 25,
+ min_expiration: 300,
+ max_expiration: 2629746,
+ }),
+ }),
+ version: '0.0.0',
+ });
+
+ const result = normalizeInstance(ImmutableMap());
+ expect(result).toEqual(expected);
+ });
+
+ it('normalizes Pleroma instance with Mastodon configuration format', () => {
+ const instance = fromJS(require('soapbox/__fixtures__/pleroma-instance.json'));
+
+ const expected = {
+ configuration: {
+ statuses: {
+ max_characters: 5000,
+ max_media_attachments: Infinity,
+ },
+ polls: {
+ max_options: 20,
+ max_characters_per_option: 200,
+ min_expiration: 0,
+ max_expiration: 31536000,
+ },
+ },
+ };
+
+ const result = normalizeInstance(instance);
+ expect(result.toJS()).toMatchObject(expected);
+ });
+
+ it('normalizes Mastodon instance with retained configuration', () => {
+ const instance = fromJS(require('soapbox/__fixtures__/mastodon-instance.json'));
+
+ const expected = {
+ configuration: {
+ statuses: {
+ max_characters: 500,
+ max_media_attachments: 4,
+ characters_reserved_per_url: 23,
+ },
+ media_attachments: {
+ image_size_limit: 10485760,
+ image_matrix_limit: 16777216,
+ video_size_limit: 41943040,
+ video_frame_rate_limit: 60,
+ video_matrix_limit: 2304000,
+ },
+ polls: {
+ max_options: 4,
+ max_characters_per_option: 50,
+ min_expiration: 300,
+ max_expiration: 2629746,
+ },
+ },
+ };
+
+ const result = normalizeInstance(instance);
+ expect(result.toJS()).toMatchObject(expected);
+ });
+
+ it('normalizes Mastodon 3.0.0 instance with default configuration', () => {
+ const instance = fromJS(require('soapbox/__fixtures__/mastodon-3.0.0-instance.json'));
+
+ const expected = {
+ configuration: {
+ statuses: {
+ max_characters: 500,
+ max_media_attachments: 4,
+ },
+ polls: {
+ max_options: 4,
+ max_characters_per_option: 25,
+ min_expiration: 300,
+ max_expiration: 2629746,
+ },
+ },
+ };
+
+ const result = normalizeInstance(instance);
+ expect(result.toJS()).toMatchObject(expected);
+ });
+});
diff --git a/app/soapbox/normalizers/instance.js b/app/soapbox/normalizers/instance.js
new file mode 100644
index 000000000..d95990670
--- /dev/null
+++ b/app/soapbox/normalizers/instance.js
@@ -0,0 +1,65 @@
+import { Map as ImmutableMap } from 'immutable';
+
+import { parseVersion, PLEROMA } from 'soapbox/utils/features';
+import { isNumber } from 'soapbox/utils/numbers';
+
+// Use Mastodon defaults
+const baseInstance = ImmutableMap({
+ description_limit: 1500,
+ configuration: ImmutableMap({
+ statuses: ImmutableMap({
+ max_characters: 500,
+ max_media_attachments: 4,
+ }),
+ polls: ImmutableMap({
+ max_options: 4,
+ max_characters_per_option: 25,
+ min_expiration: 300,
+ max_expiration: 2629746,
+ }),
+ }),
+ version: '0.0.0',
+});
+
+// Build Mastodon configuration from Pleroma instance
+const pleromaToMastodonConfig = instance => {
+ return ImmutableMap({
+ statuses: ImmutableMap({
+ max_characters: instance.get('max_toot_chars'),
+ }),
+ polls: ImmutableMap({
+ max_options: instance.getIn(['poll_limits', 'max_options']),
+ max_characters_per_option: instance.getIn(['poll_limits', 'max_option_chars']),
+ min_expiration: instance.getIn(['poll_limits', 'min_expiration']),
+ max_expiration: instance.getIn(['poll_limits', 'max_expiration']),
+ }),
+ });
+};
+
+// Use new value only if old value is undefined
+const mergeDefined = (oldVal, newVal) => oldVal === undefined ? newVal : oldVal;
+
+// Get the software's default attachment limit
+const getAttachmentLimit = software => software === PLEROMA ? Infinity : 4;
+
+// Normalize instance (Pleroma, Mastodon, etc.) to Mastodon's format
+export const normalizeInstance = instance => {
+ const { software } = parseVersion(instance.get('version'));
+ const mastodonConfig = pleromaToMastodonConfig(instance);
+
+ return instance.withMutations(instance => {
+ // Merge configuration
+ instance.update('configuration', ImmutableMap(), configuration => (
+ configuration.mergeDeepWith(mergeDefined, mastodonConfig)
+ ));
+
+ // If max attachments isn't set, check the backend software
+ instance.updateIn(['configuration', 'statuses', 'max_media_attachments'], value => {
+ return isNumber(value) ? value : getAttachmentLimit(software);
+ });
+
+ // Merge defaults & cleanup
+ instance.mergeDeepWith(mergeDefined, baseInstance);
+ instance.deleteAll(['max_toot_chars', 'poll_limits']);
+ });
+};
diff --git a/app/soapbox/reducers/compose.js b/app/soapbox/reducers/compose.js
index c0ee4af0b..57796af63 100644
--- a/app/soapbox/reducers/compose.js
+++ b/app/soapbox/reducers/compose.js
@@ -428,7 +428,7 @@ export default function compose(state = initialState, action) {
case REDRAFT:
return state.withMutations(map => {
map.set('text', action.raw_text || unescapeHTML(expandMentions(action.status)));
- map.set('to', action.explicitAddressing ? getExplicitMentions(action.status.get('account', 'id'), action.status) : null);
+ map.set('to', action.explicitAddressing ? getExplicitMentions(action.status.get('account', 'id'), action.status) : undefined);
map.set('in_reply_to', action.status.get('in_reply_to_id'));
map.set('privacy', action.status.get('visibility'));
// TODO: Actually fix this rather than just removing it
diff --git a/app/soapbox/reducers/instance.js b/app/soapbox/reducers/instance.js
index 8a79a2869..ac28e88ee 100644
--- a/app/soapbox/reducers/instance.js
+++ b/app/soapbox/reducers/instance.js
@@ -2,10 +2,9 @@ import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable';
import { ADMIN_CONFIG_UPDATE_REQUEST, ADMIN_CONFIG_UPDATE_SUCCESS } from 'soapbox/actions/admin';
import { PLEROMA_PRELOAD_IMPORT } from 'soapbox/actions/preload';
+import { normalizeInstance } from 'soapbox/normalizers/instance';
import KVStore from 'soapbox/storage/kv_store';
import { ConfigDB } from 'soapbox/utils/config_db';
-import { parseVersion, PLEROMA } from 'soapbox/utils/features';
-import { isNumber } from 'soapbox/utils/numbers';
import {
INSTANCE_REMEMBER_SUCCESS,
@@ -14,6 +13,8 @@ import {
NODEINFO_FETCH_SUCCESS,
} from '../actions/instance';
+const initialState = normalizeInstance(ImmutableMap());
+
const nodeinfoToInstance = nodeinfo => {
// Match Pleroma's develop branch
return ImmutableMap({
@@ -30,67 +31,6 @@ const nodeinfoToInstance = nodeinfo => {
});
};
-// Use Mastodon defaults
-const initialState = ImmutableMap({
- description_limit: 1500,
- configuration: ImmutableMap({
- statuses: ImmutableMap({
- max_characters: 500,
- max_media_attachments: 4,
- }),
- polls: ImmutableMap({
- max_options: 4,
- max_characters_per_option: 25,
- min_expiration: 300,
- max_expiration: 2629746,
- }),
- }),
- version: '0.0.0',
-});
-
-// Build Mastodon configuration from Pleroma instance
-const pleromaToMastodonConfig = instance => {
- return ImmutableMap({
- statuses: ImmutableMap({
- max_characters: instance.get('max_toot_chars'),
- }),
- polls: ImmutableMap({
- max_options: instance.getIn(['poll_limits', 'max_options']),
- max_characters_per_option: instance.getIn(['poll_limits', 'max_option_chars']),
- min_expiration: instance.getIn(['poll_limits', 'min_expiration']),
- max_expiration: instance.getIn(['poll_limits', 'max_expiration']),
- }),
- });
-};
-
-// Use new value only if old value is undefined
-const mergeDefined = (oldVal, newVal) => oldVal === undefined ? newVal : oldVal;
-
-// Get the software's default attachment limit
-const getAttachmentLimit = software => software === PLEROMA ? Infinity : 4;
-
-// Normalize instance (Pleroma, Mastodon, etc.) to Mastodon's format
-const normalizeInstance = instance => {
- const { software } = parseVersion(instance.get('version'));
- const mastodonConfig = pleromaToMastodonConfig(instance);
-
- return instance.withMutations(instance => {
- // Merge configuration
- instance.update('configuration', ImmutableMap(), configuration => (
- configuration.mergeDeepWith(mergeDefined, mastodonConfig)
- ));
-
- // If max attachments isn't set, check the backend software
- instance.updateIn(['configuration', 'statuses', 'max_media_attachments'], value => {
- return isNumber(value) ? value : getAttachmentLimit(software);
- });
-
- // Merge defaults & cleanup
- instance.mergeDeepWith(mergeDefined, initialState);
- instance.deleteAll(['max_toot_chars', 'poll_limits']);
- });
-};
-
const importInstance = (state, instance) => {
return normalizeInstance(instance);
};