diff --git a/.gitignore b/.gitignore
index d2c20b8ef..1d36a4816 100644
--- a/.gitignore
+++ b/.gitignore
@@ -24,7 +24,7 @@ yarn-error.log*
!/custom/**/.gitkeep
# surge.sh
-CNAME
-AUTH
-CORS
-ROUTER
+/CNAME
+/AUTH
+/CORS
+/ROUTER
diff --git a/README.md b/README.md
index a3a4e8ddc..6068126d3 100644
--- a/README.md
+++ b/README.md
@@ -2,10 +2,14 @@

-**Soapbox FE** is a frontend for Pleroma with a focus on custom branding and ease of use.
+**Soapbox FE** is a frontend for Mastodon and Pleroma with a focus on custom branding and ease of use.
It's part of the [Soapbox](https://soapbox.pub) project.
-# :rocket: Deploy on Pleroma
+## Try it out
+
+Visit https://fe.soapbox.pub/ and point it to your favorite instance.
+
+## :rocket: Deploy on Pleroma
Installing Soapbox FE on an existing Pleroma server is extremely easy.
Just ssh into the server and download a .zip of the latest build:
diff --git a/app/images/avatar-missing.png b/app/images/avatar-missing.png
index b3e6b5709..6de33a5a4 100644
Binary files a/app/images/avatar-missing.png and b/app/images/avatar-missing.png differ
diff --git a/app/images/avatar-missing.svg b/app/images/avatar-missing.svg
index 7eb156089..d5833c90c 100644
--- a/app/images/avatar-missing.svg
+++ b/app/images/avatar-missing.svg
@@ -1,116 +1,30 @@
+ xmlns:dc="http://purl.org/dc/elements/1.1/">image/svg+xml
diff --git a/app/images/header-missing.png b/app/images/header-missing.png
new file mode 100644
index 000000000..26b59e75a
Binary files /dev/null and b/app/images/header-missing.png differ
diff --git a/app/soapbox/__fixtures__/mastodon-account.json b/app/soapbox/__fixtures__/mastodon-account.json
new file mode 100644
index 000000000..7a00340bf
--- /dev/null
+++ b/app/soapbox/__fixtures__/mastodon-account.json
@@ -0,0 +1,23 @@
+{
+ "id": "106801667066418367",
+ "username": "benis911",
+ "acct": "benis911",
+ "display_name": "",
+ "locked": false,
+ "bot": false,
+ "discoverable": null,
+ "group": false,
+ "created_at": "2021-08-22T00:00:00.000Z",
+ "note": "",
+ "url": "https://mastodon.social/@benis911",
+ "avatar": "https://mastodon.social/avatars/original/missing.png",
+ "avatar_static": "https://mastodon.social/avatars/original/missing.png",
+ "header": "https://mastodon.social/headers/original/missing.png",
+ "header_static": "https://mastodon.social/headers/original/missing.png",
+ "followers_count": 1,
+ "following_count": 0,
+ "statuses_count": 5,
+ "last_status_at": "2022-02-23",
+ "emojis": [],
+ "fields": []
+}
diff --git a/app/soapbox/__fixtures__/mastodon-instance-rc.json b/app/soapbox/__fixtures__/mastodon-instance-rc.json
new file mode 100644
index 000000000..277839d14
--- /dev/null
+++ b/app/soapbox/__fixtures__/mastodon-instance-rc.json
@@ -0,0 +1,123 @@
+{
+ "uri": "mastodon.social",
+ "title": "Mastodon",
+ "short_description": "Server run by the main developers of the project
It is not focused on any particular niche interest - everyone is welcome as long as you follow our code of conduct!",
+ "description": "Server run by the main developers of the project
It is not focused on any particular niche interest - everyone is welcome as long as you follow our code of conduct!",
+ "email": "staff@mastodon.social",
+ "version": "3.5.0rc1",
+ "urls": {
+ "streaming_api": "wss://mastodon.social"
+ },
+ "stats": {
+ "user_count": 635078,
+ "status_count": 34700866,
+ "domain_count": 21989
+ },
+ "thumbnail": "https://files.mastodon.social/site_uploads/files/000/000/001/original/vlcsnap-2018-08-27-16h43m11s127.png",
+ "languages": [
+ "en"
+ ],
+ "registrations": true,
+ "approval_required": false,
+ "invites_enabled": true,
+ "configuration": {
+ "statuses": {
+ "max_characters": 500,
+ "max_media_attachments": 4,
+ "characters_reserved_per_url": 23
+ },
+ "media_attachments": {
+ "supported_mime_types": [
+ "image/jpeg",
+ "image/png",
+ "image/gif",
+ "video/webm",
+ "video/mp4",
+ "video/quicktime",
+ "video/ogg",
+ "audio/wave",
+ "audio/wav",
+ "audio/x-wav",
+ "audio/x-pn-wave",
+ "audio/ogg",
+ "audio/vorbis",
+ "audio/mpeg",
+ "audio/mp3",
+ "audio/webm",
+ "audio/flac",
+ "audio/aac",
+ "audio/m4a",
+ "audio/x-m4a",
+ "audio/mp4",
+ "audio/3gpp",
+ "video/x-ms-asf"
+ ],
+ "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
+ }
+ },
+ "contact_account": {
+ "id": "1",
+ "username": "Gargron",
+ "acct": "Gargron",
+ "display_name": "Eugen",
+ "locked": false,
+ "bot": false,
+ "discoverable": true,
+ "group": false,
+ "created_at": "2016-03-16T00:00:00.000Z",
+ "note": "
-
+
+ {Component => ()}
+
diff --git a/app/soapbox/features/ui/util/async-components.js b/app/soapbox/features/ui/util/async-components.js
index 9c2218f8e..1622ca575 100644
--- a/app/soapbox/features/ui/util/async-components.js
+++ b/app/soapbox/features/ui/util/async-components.js
@@ -465,3 +465,7 @@ export function CreateApp() {
export function SettingsStore() {
return import(/* webpackChunkName: "features/developers" */'../../developers/settings_store');
}
+
+export function DatePicker() {
+ return import(/* webpackChunkName: "date_picker" */'../../birthdays/date_picker');
+}
diff --git a/app/soapbox/normalizers/__tests__/account-test.js b/app/soapbox/normalizers/__tests__/account-test.js
index 9211a1cf8..8d12a935d 100644
--- a/app/soapbox/normalizers/__tests__/account-test.js
+++ b/app/soapbox/normalizers/__tests__/account-test.js
@@ -3,25 +3,27 @@ import { Record as ImmutableRecord, fromJS } from 'immutable';
import { normalizeAccount } from '../account';
const AVATAR_MISSING = require('images/avatar-missing.png');
+const HEADER_MISSING = require('images/header-missing.png');
describe('normalizeAccount()', () => {
it('adds base fields', () => {
- const account = fromJS({});
+ const account = {};
const result = normalizeAccount(account);
expect(ImmutableRecord.isRecord(result)).toBe(true);
expect(result.acct).toEqual('');
expect(result.note).toEqual('');
expect(result.avatar).toEqual(AVATAR_MISSING);
+ expect(result.header_static).toEqual(HEADER_MISSING);
});
it('normalizes a mention', () => {
- const mention = fromJS({
+ const mention = {
acct: 'NEETzsche@iddqd.social',
id: '9v5bw7hEGBPc9nrpzc',
url: 'https://iddqd.social/users/NEETzsche',
username: 'NEETzsche',
- });
+ };
const result = normalizeAccount(mention);
expect(result.emojis).toEqual(fromJS([]));
@@ -32,21 +34,21 @@ describe('normalizeAccount()', () => {
});
it('normalizes Fedibird birthday', () => {
- const account = fromJS(require('soapbox/__fixtures__/fedibird-account.json'));
+ const account = require('soapbox/__fixtures__/fedibird-account.json');
const result = normalizeAccount(account);
expect(result.birthday).toEqual('1993-07-03');
});
it('normalizes Pleroma birthday', () => {
- const account = fromJS(require('soapbox/__fixtures__/pleroma-account.json'));
+ const account = require('soapbox/__fixtures__/pleroma-account.json');
const result = normalizeAccount(account);
expect(result.birthday).toEqual('1993-07-03');
});
it('normalizes Pleroma legacy fields', () => {
- const account = fromJS(require('soapbox/__fixtures__/pleroma-2.2.2-account.json'));
+ const account = require('soapbox/__fixtures__/pleroma-2.2.2-account.json');
const result = normalizeAccount(account);
expect(result.getIn(['pleroma', 'is_active'])).toBe(true);
@@ -57,7 +59,7 @@ describe('normalizeAccount()', () => {
});
it('prefers new Pleroma fields', () => {
- const account = fromJS(require('soapbox/__fixtures__/pleroma-account.json'));
+ const account = require('soapbox/__fixtures__/pleroma-account.json');
const result = normalizeAccount(account);
expect(result.getIn(['pleroma', 'is_active'])).toBe(true);
@@ -66,76 +68,82 @@ describe('normalizeAccount()', () => {
});
it('normalizes a verified Pleroma user', () => {
- const account = fromJS(require('soapbox/__fixtures__/mk.json'));
+ const account = require('soapbox/__fixtures__/mk.json');
const result = normalizeAccount(account);
expect(result.verified).toBe(true);
});
it('normalizes an unverified Pleroma user', () => {
- const account = fromJS(require('soapbox/__fixtures__/pleroma-account.json'));
+ const account = require('soapbox/__fixtures__/pleroma-account.json');
const result = normalizeAccount(account);
expect(result.verified).toBe(false);
});
it('normalizes a verified Truth Social user', () => {
- const account = fromJS(require('soapbox/__fixtures__/realDonaldTrump.json'));
+ const account = require('soapbox/__fixtures__/realDonaldTrump.json');
const result = normalizeAccount(account);
expect(result.verified).toBe(true);
});
it('normalizes Fedibird location', () => {
- const account = fromJS(require('soapbox/__fixtures__/fedibird-account.json'));
+ const account = require('soapbox/__fixtures__/fedibird-account.json');
const result = normalizeAccount(account);
expect(result.location).toBe('Texas, USA');
});
it('normalizes Truth Social location', () => {
- const account = fromJS(require('soapbox/__fixtures__/truthsocial-account.json'));
+ const account = require('soapbox/__fixtures__/truthsocial-account.json');
const result = normalizeAccount(account);
expect(result.location).toBe('Texas');
});
+ it('normalizes Truth Social website', () => {
+ const account = require('soapbox/__fixtures__/truthsocial-account.json');
+ const result = normalizeAccount(account);
+ expect(result.website).toBe('https://soapbox.pub');
+ });
+
it('sets display_name from username', () => {
- const account = fromJS({ username: 'alex' });
+ const account = { username: 'alex' };
const result = normalizeAccount(account);
expect(result.display_name).toBe('alex');
});
it('sets display_name from acct', () => {
- const account = fromJS({ acct: 'alex@gleasonator.com' });
+ const account = { acct: 'alex@gleasonator.com' };
const result = normalizeAccount(account);
expect(result.display_name).toBe('alex');
});
it('overrides a whitespace display_name', () => {
- const account = fromJS({ username: 'alex', display_name: ' ' });
+ const account = { username: 'alex', display_name: ' ' };
const result = normalizeAccount(account);
expect(result.display_name).toBe('alex');
});
it('emojifies display name as `display_name_html`', () => {
- const account = fromJS(require('soapbox/__fixtures__/account-with-emojis.json'));
+ const account = require('soapbox/__fixtures__/account-with-emojis.json');
const result = normalizeAccount(account);
const expected = 'Alex Gleason

';
expect(result.display_name_html).toBe(expected);
});
it('emojifies note as `note_emojified`', () => {
- const account = fromJS(require('soapbox/__fixtures__/account-with-emojis.json'));
+ const account = require('soapbox/__fixtures__/account-with-emojis.json');
const result = normalizeAccount(account);
const expected = 'I create Fediverse software that empowers people online.

I'm vegan btw
Note: If you have a question for me, please tag me publicly. This gives the opportunity for others to chime in, and bystanders to learn.';
expect(result.note_emojified).toBe(expected);
});
it('unescapes HTML note as `note_plain`', () => {
- const account = fromJS(require('soapbox/__fixtures__/account-with-emojis.json'));
+ const account = require('soapbox/__fixtures__/account-with-emojis.json');
const result = normalizeAccount(account);
const expected = 'I create Fediverse software that empowers people online. :soapbox:\n\nI\'m vegan btw\n\nNote: If you have a question for me, please tag me publicly. This gives the opportunity for others to chime in, and bystanders to learn.';
expect(result.note_plain).toBe(expected);
});
it('emojifies custom profile field', () => {
- const account = fromJS(require('soapbox/__fixtures__/account-with-emojis.json'));
+ const account = require('soapbox/__fixtures__/account-with-emojis.json');
const result = normalizeAccount(account);
const field = result.fields.get(1);
@@ -143,4 +151,21 @@ describe('normalizeAccount()', () => {
expect(field.value_emojified).toBe('
https://soapbox.pub 
');
expect(field.value_plain).toBe('https://soapbox.pub :soapbox:');
});
+
+ it('adds default avatar and banner to GoToSocial account', () => {
+ const account = require('soapbox/__fixtures__/gotosocial-account.json');
+ const result = normalizeAccount(account);
+
+ expect(result.avatar).toEqual(AVATAR_MISSING);
+ expect(result.avatar_static).toEqual(AVATAR_MISSING);
+ expect(result.header).toEqual(HEADER_MISSING);
+ expect(result.header_static).toEqual(HEADER_MISSING);
+ });
+
+ it('adds fqn to Mastodon account', () => {
+ const account = require('soapbox/__fixtures__/mastodon-account.json');
+ const result = normalizeAccount(account);
+
+ expect(result.fqn).toEqual('benis911@mastodon.social');
+ });
});
diff --git a/app/soapbox/normalizers/__tests__/attachment-test.js b/app/soapbox/normalizers/__tests__/attachment-test.js
index ecf3813e7..9647a55fa 100644
--- a/app/soapbox/normalizers/__tests__/attachment-test.js
+++ b/app/soapbox/normalizers/__tests__/attachment-test.js
@@ -1,10 +1,10 @@
-import { Record as ImmutableRecord, fromJS } from 'immutable';
+import { Record as ImmutableRecord } from 'immutable';
import { normalizeAttachment } from '../attachment';
describe('normalizeAttachment()', () => {
it('adds base fields', () => {
- const attachment = fromJS({});
+ const attachment = {};
const result = normalizeAttachment(attachment);
expect(ImmutableRecord.isRecord(result)).toBe(true);
@@ -13,7 +13,7 @@ describe('normalizeAttachment()', () => {
});
it('infers preview_url from url', () => {
- const attachment = fromJS({ url: 'https://site.fedi/123.png' });
+ const attachment = { url: 'https://site.fedi/123.png' };
const result = normalizeAttachment(attachment);
expect(result.preview_url).toEqual('https://site.fedi/123.png');
diff --git a/app/soapbox/normalizers/__tests__/card-test.js b/app/soapbox/normalizers/__tests__/card-test.js
index e8ac120b0..fc8d06221 100644
--- a/app/soapbox/normalizers/__tests__/card-test.js
+++ b/app/soapbox/normalizers/__tests__/card-test.js
@@ -1,10 +1,10 @@
-import { Record as ImmutableRecord, fromJS } from 'immutable';
+import { Record as ImmutableRecord } from 'immutable';
import { normalizeCard } from '../card';
describe('normalizeCard()', () => {
it('adds base fields', () => {
- const card = fromJS({});
+ const card = {};
const result = normalizeCard(card);
expect(ImmutableRecord.isRecord(result)).toBe(true);
diff --git a/app/soapbox/normalizers/__tests__/instance-test.js b/app/soapbox/normalizers/__tests__/instance-test.js
index 597fbbeee..f5fd4f6af 100644
--- a/app/soapbox/normalizers/__tests__/instance-test.js
+++ b/app/soapbox/normalizers/__tests__/instance-test.js
@@ -59,7 +59,7 @@ describe('normalizeInstance()', () => {
});
it('normalizes Pleroma instance with Mastodon configuration format', () => {
- const instance = fromJS(require('soapbox/__fixtures__/pleroma-instance.json'));
+ const instance = require('soapbox/__fixtures__/pleroma-instance.json');
const expected = {
configuration: {
@@ -81,7 +81,7 @@ describe('normalizeInstance()', () => {
});
it('normalizes Mastodon instance with retained configuration', () => {
- const instance = fromJS(require('soapbox/__fixtures__/mastodon-instance.json'));
+ const instance = require('soapbox/__fixtures__/mastodon-instance.json');
const expected = {
configuration: {
@@ -111,7 +111,7 @@ describe('normalizeInstance()', () => {
});
it('normalizes Mastodon 3.0.0 instance with default configuration', () => {
- const instance = fromJS(require('soapbox/__fixtures__/mastodon-3.0.0-instance.json'));
+ const instance = require('soapbox/__fixtures__/mastodon-3.0.0-instance.json');
const expected = {
configuration: {
@@ -133,18 +133,18 @@ describe('normalizeInstance()', () => {
});
it('normalizes Fedibird instance', () => {
- const instance = fromJS(require('soapbox/__fixtures__/fedibird-instance.json'));
+ const instance = require('soapbox/__fixtures__/fedibird-instance.json');
const result = normalizeInstance(instance);
// Sets description_limit
expect(result.description_limit).toEqual(1500);
// Preserves fedibird_capabilities
- expect(result.fedibird_capabilities).toEqual(instance.get('fedibird_capabilities'));
+ expect(result.fedibird_capabilities).toEqual(fromJS(instance.fedibird_capabilities));
});
it('normalizes Mitra instance', () => {
- const instance = fromJS(require('soapbox/__fixtures__/mitra-instance.json'));
+ const instance = require('soapbox/__fixtures__/mitra-instance.json');
const result = normalizeInstance(instance);
// Adds configuration and description_limit
@@ -153,7 +153,7 @@ describe('normalizeInstance()', () => {
});
it('normalizes GoToSocial instance', () => {
- const instance = fromJS(require('soapbox/__fixtures__/gotosocial-instance.json'));
+ const instance = require('soapbox/__fixtures__/gotosocial-instance.json');
const result = normalizeInstance(instance);
// Normalizes max_toot_chars
@@ -166,7 +166,7 @@ describe('normalizeInstance()', () => {
});
it('normalizes Friendica instance', () => {
- const instance = fromJS(require('soapbox/__fixtures__/friendica-instance.json'));
+ const instance = require('soapbox/__fixtures__/friendica-instance.json');
const result = normalizeInstance(instance);
// Normalizes max_toot_chars
@@ -177,4 +177,11 @@ describe('normalizeInstance()', () => {
expect(result.get('configuration') instanceof ImmutableMap).toBe(true);
expect(result.get('description_limit')).toBe(1500);
});
+
+ it('normalizes a Mastodon RC version', () => {
+ const instance = require('soapbox/__fixtures__/mastodon-instance-rc.json');
+ const result = normalizeInstance(instance);
+
+ expect(result.version).toEqual('3.5.0-rc1');
+ });
});
diff --git a/app/soapbox/normalizers/__tests__/mention-test.js b/app/soapbox/normalizers/__tests__/mention-test.js
index e429a03b1..03d712175 100644
--- a/app/soapbox/normalizers/__tests__/mention-test.js
+++ b/app/soapbox/normalizers/__tests__/mention-test.js
@@ -1,10 +1,10 @@
-import { Record as ImmutableRecord, fromJS } from 'immutable';
+import { Record as ImmutableRecord } from 'immutable';
import { normalizeMention } from '../mention';
describe('normalizeMention()', () => {
it('adds base fields', () => {
- const account = fromJS({});
+ const account = {};
const result = normalizeMention(account);
expect(ImmutableRecord.isRecord(result)).toBe(true);
@@ -15,7 +15,7 @@ describe('normalizeMention()', () => {
});
it('infers username from acct', () => {
- const account = fromJS({ acct: 'alex@gleasonator.com' });
+ const account = { acct: 'alex@gleasonator.com' };
const result = normalizeMention(account);
expect(result.username).toEqual('alex');
diff --git a/app/soapbox/normalizers/__tests__/notification-test.js b/app/soapbox/normalizers/__tests__/notification-test.js
index c90b5451e..b72f0d9aa 100644
--- a/app/soapbox/normalizers/__tests__/notification-test.js
+++ b/app/soapbox/normalizers/__tests__/notification-test.js
@@ -1,10 +1,10 @@
-import { Record as ImmutableRecord, fromJS } from 'immutable';
+import { Record as ImmutableRecord } from 'immutable';
import { normalizeNotification } from '../notification';
describe('normalizeNotification()', () => {
it('normalizes an empty map', () => {
- const notification = fromJS({});
+ const notification = {};
const result = normalizeNotification(notification);
expect(ImmutableRecord.isRecord(result)).toBe(true);
diff --git a/app/soapbox/normalizers/__tests__/poll-test.js b/app/soapbox/normalizers/__tests__/poll-test.js
index 4cc9dbbbc..31691f484 100644
--- a/app/soapbox/normalizers/__tests__/poll-test.js
+++ b/app/soapbox/normalizers/__tests__/poll-test.js
@@ -1,10 +1,10 @@
-import { Record as ImmutableRecord, fromJS } from 'immutable';
+import { Record as ImmutableRecord } from 'immutable';
import { normalizePoll } from '../poll';
describe('normalizePoll()', () => {
it('adds base fields', () => {
- const poll = fromJS({ options: [{ title: 'Apples' }] });
+ const poll = { options: [{ title: 'Apples' }] };
const result = normalizePoll(poll);
const expected = {
@@ -25,7 +25,7 @@ describe('normalizePoll()', () => {
});
it('normalizes a Pleroma logged-out poll', () => {
- const poll = fromJS(require('soapbox/__fixtures__/pleroma-status-with-poll.json')).get('poll');
+ const { poll } = require('soapbox/__fixtures__/pleroma-status-with-poll.json');
const result = normalizePoll(poll);
// Adds logged-in fields
@@ -34,7 +34,7 @@ describe('normalizePoll()', () => {
});
it('normalizes poll with emojis', () => {
- const poll = fromJS(require('soapbox/__fixtures__/pleroma-status-with-poll-with-emojis.json')).get('poll');
+ const { poll } = require('soapbox/__fixtures__/pleroma-status-with-poll-with-emojis.json');
const result = normalizePoll(poll);
// Emojifies poll options
diff --git a/app/soapbox/normalizers/__tests__/status-test.js b/app/soapbox/normalizers/__tests__/status-test.js
index 2824615cc..ebfcb3eee 100644
--- a/app/soapbox/normalizers/__tests__/status-test.js
+++ b/app/soapbox/normalizers/__tests__/status-test.js
@@ -4,7 +4,7 @@ import { normalizeStatus } from '../status';
describe('normalizeStatus()', () => {
it('adds base fields', () => {
- const status = fromJS({});
+ const status = {};
const result = normalizeStatus(status);
expect(ImmutableRecord.isRecord(result)).toBe(true);
@@ -17,7 +17,7 @@ describe('normalizeStatus()', () => {
});
it('fixes the order of mentions', () => {
- const status = fromJS(require('soapbox/__fixtures__/status-unordered-mentions.json'));
+ const status = require('soapbox/__fixtures__/status-unordered-mentions.json');
const expected = ['NEETzsche', 'alex', 'Lumeinshin', 'sneeden'];
@@ -30,7 +30,7 @@ describe('normalizeStatus()', () => {
});
it('adds mention to self in self-reply on Mastodon', () => {
- const status = fromJS(require('soapbox/__fixtures__/mastodon-reply-to-self.json'));
+ const status = require('soapbox/__fixtures__/mastodon-reply-to-self.json');
const expected = {
id: '106801667066418367',
@@ -48,7 +48,7 @@ describe('normalizeStatus()', () => {
});
it('normalizes mentions with only acct', () => {
- const status = fromJS({ mentions: [{ acct: 'alex@gleasonator.com' }] });
+ const status = { mentions: [{ acct: 'alex@gleasonator.com' }] };
const expected = [{
id: '',
@@ -63,7 +63,7 @@ describe('normalizeStatus()', () => {
});
it('normalizes Mitra attachments', () => {
- const status = fromJS(require('soapbox/__fixtures__/mitra-status-with-attachments.json'));
+ const status = require('soapbox/__fixtures__/mitra-status-with-attachments.json');
const expected = [{
id: '017eeb0e-e5df-30a4-77a7-a929145cb836',
@@ -97,7 +97,7 @@ describe('normalizeStatus()', () => {
});
it('leaves Pleroma attachments alone', () => {
- const status = fromJS(require('soapbox/__fixtures__/pleroma-status-with-attachments.json'));
+ const status = require('soapbox/__fixtures__/pleroma-status-with-attachments.json');
const result = normalizeStatus(status).media_attachments;
expect(result.size).toBe(4);
@@ -108,15 +108,15 @@ describe('normalizeStatus()', () => {
});
it('normalizes Pleroma quote post', () => {
- const status = fromJS(require('soapbox/__fixtures__/pleroma-quote-post.json'));
+ const status = require('soapbox/__fixtures__/pleroma-quote-post.json');
const result = normalizeStatus(status);
- expect(result.quote).toEqual(status.getIn(['pleroma', 'quote']));
+ expect(result.quote).toEqual(fromJS(status.pleroma.quote));
expect(result.pleroma.get('quote')).toBe(undefined);
});
it('normalizes GoToSocial status', () => {
- const status = fromJS(require('soapbox/__fixtures__/gotosocial-status.json'));
+ const status = require('soapbox/__fixtures__/gotosocial-status.json');
const result = normalizeStatus(status);
// Adds missing fields
@@ -132,7 +132,7 @@ describe('normalizeStatus()', () => {
});
it('normalizes Friendica status', () => {
- const status = fromJS(require('soapbox/__fixtures__/friendica-status.json'));
+ const status = require('soapbox/__fixtures__/friendica-status.json');
const result = normalizeStatus(status);
// Adds missing fields
@@ -145,7 +145,7 @@ describe('normalizeStatus()', () => {
});
it('normalizes poll and poll options', () => {
- const status = fromJS({ poll: { options: [{ title: 'Apples' }] } });
+ const status = { poll: { options: [{ title: 'Apples' }] } };
const result = normalizeStatus(status);
const expected = {
@@ -166,7 +166,7 @@ describe('normalizeStatus()', () => {
});
it('normalizes a Pleroma logged-out poll', () => {
- const status = fromJS(require('soapbox/__fixtures__/pleroma-status-with-poll.json'));
+ const status = require('soapbox/__fixtures__/pleroma-status-with-poll.json');
const result = normalizeStatus(status);
// Adds logged-in fields
@@ -175,7 +175,7 @@ describe('normalizeStatus()', () => {
});
it('normalizes poll with emojis', () => {
- const status = fromJS(require('soapbox/__fixtures__/pleroma-status-with-poll-with-emojis.json'));
+ const status = require('soapbox/__fixtures__/pleroma-status-with-poll-with-emojis.json');
const result = normalizeStatus(status);
// Emojifies poll options
@@ -188,7 +188,7 @@ describe('normalizeStatus()', () => {
});
it('normalizes a card', () => {
- const status = fromJS(require('soapbox/__fixtures__/status-with-card.json'));
+ const status = require('soapbox/__fixtures__/status-with-card.json');
const result = normalizeStatus(status);
expect(ImmutableRecord.isRecord(result.card)).toBe(true);
diff --git a/app/soapbox/normalizers/account.ts b/app/soapbox/normalizers/account.ts
index 7faa3282c..1c536ee4a 100644
--- a/app/soapbox/normalizers/account.ts
+++ b/app/soapbox/normalizers/account.ts
@@ -8,16 +8,18 @@ import {
Map as ImmutableMap,
List as ImmutableList,
Record as ImmutableRecord,
+ fromJS,
} from 'immutable';
import emojify from 'soapbox/features/emoji/emoji';
import { normalizeEmoji } from 'soapbox/normalizers/emoji';
import { IAccount } from 'soapbox/types';
+import { acctFull } from 'soapbox/utils/accounts';
import { unescapeHTML } from 'soapbox/utils/html';
import { mergeDefined, makeEmojiMap } from 'soapbox/utils/normalizers';
// https://docs.joinmastodon.org/entities/account/
-const AccountRecord = ImmutableRecord({
+export const AccountRecord = ImmutableRecord({
acct: '',
avatar: '',
avatar_static: '',
@@ -44,6 +46,7 @@ const AccountRecord = ImmutableRecord({
uri: '',
url: '',
username: '',
+ website: '',
verified: false,
// Internal fields
@@ -56,7 +59,7 @@ const AccountRecord = ImmutableRecord({
});
// https://docs.joinmastodon.org/entities/field/
-const FieldRecord = ImmutableRecord({
+export const FieldRecord = ImmutableRecord({
name: '',
value: '',
verified_at: null,
@@ -95,6 +98,18 @@ const normalizeAvatar = (account: ImmutableMap
) => {
});
};
+// Add header, if missing
+const normalizeHeader = (account: ImmutableMap) => {
+ const header = account.get('header');
+ const headerStatic = account.get('header_static');
+ const missing = require('images/header-missing.png');
+
+ return account.withMutations(account => {
+ account.set('header', header || headerStatic || missing);
+ account.set('header_static', headerStatic || header || missing);
+ });
+};
+
// Normalize custom fields
const normalizeFields = (account: ImmutableMap) => {
return account.update('fields', ImmutableList(), fields => fields.map(FieldRecord));
@@ -132,11 +147,12 @@ const normalizeVerified = (account: ImmutableMap) => {
});
};
-// Normalize Fedibird/Truth Social location
+// Normalize Fedibird/Truth Social/Pleroma location
const normalizeLocation = (account: ImmutableMap) => {
return account.update('location', location => {
return [
location,
+ account.getIn(['pleroma', 'location']),
account.getIn(['other_settings', 'location']),
].find(Boolean);
});
@@ -180,16 +196,22 @@ const addInternalFields = (account: ImmutableMap) => {
});
};
-export const normalizeAccount = (account: ImmutableMap): IAccount => {
+const normalizeFqn = (account: ImmutableMap) => {
+ return account.set('fqn', acctFull(account));
+};
+
+export const normalizeAccount = (account: Record): IAccount => {
return AccountRecord(
- account.withMutations(account => {
+ ImmutableMap(fromJS(account)).withMutations(account => {
normalizePleromaLegacyFields(account);
normalizeEmojis(account);
normalizeAvatar(account);
+ normalizeHeader(account);
normalizeFields(account);
normalizeVerified(account);
normalizeBirthday(account);
normalizeLocation(account);
+ normalizeFqn(account);
fixUsername(account);
fixDisplayName(account);
addInternalFields(account);
diff --git a/app/soapbox/normalizers/attachment.ts b/app/soapbox/normalizers/attachment.ts
index 9599fc1ce..26e616696 100644
--- a/app/soapbox/normalizers/attachment.ts
+++ b/app/soapbox/normalizers/attachment.ts
@@ -6,12 +6,13 @@
import {
Map as ImmutableMap,
Record as ImmutableRecord,
+ fromJS,
} from 'immutable';
import { mergeDefined } from 'soapbox/utils/normalizers';
// https://docs.joinmastodon.org/entities/attachment/
-const AttachmentRecord = ImmutableRecord({
+export const AttachmentRecord = ImmutableRecord({
blurhash: undefined,
description: '',
id: '',
@@ -29,7 +30,7 @@ const AttachmentRecord = ImmutableRecord({
});
// Ensure attachments have required fields
-export const normalizeAttachment = (attachment: ImmutableMap) => {
+const normalizeUrls = (attachment: ImmutableMap) => {
const url = [
attachment.get('url'),
attachment.get('preview_url'),
@@ -41,5 +42,11 @@ export const normalizeAttachment = (attachment: ImmutableMap) => {
preview_url: url,
});
- return AttachmentRecord(attachment.mergeWith(mergeDefined, base));
+ return attachment.mergeWith(mergeDefined, base);
+};
+
+export const normalizeAttachment = (attachment: Record) => {
+ return AttachmentRecord(
+ normalizeUrls(ImmutableMap(fromJS(attachment))),
+ );
};
diff --git a/app/soapbox/normalizers/card.ts b/app/soapbox/normalizers/card.ts
index c9ac76adb..169492647 100644
--- a/app/soapbox/normalizers/card.ts
+++ b/app/soapbox/normalizers/card.ts
@@ -3,10 +3,10 @@
* Converts API cards into our internal format.
* @see {@link https://docs.joinmastodon.org/entities/card/}
*/
-import { Record as ImmutableRecord, Map as ImmutableMap } from 'immutable';
+import { Record as ImmutableRecord, Map as ImmutableMap, fromJS } from 'immutable';
// https://docs.joinmastodon.org/entities/card/
-const CardRecord = ImmutableRecord({
+export const CardRecord = ImmutableRecord({
author_name: '',
author_url: '',
blurhash: null,
@@ -23,6 +23,8 @@ const CardRecord = ImmutableRecord({
width: 0,
});
-export const normalizeCard = (card: ImmutableMap) => {
- return CardRecord(card);
+export const normalizeCard = (card: Record) => {
+ return CardRecord(
+ ImmutableMap(fromJS(card)),
+ );
};
diff --git a/app/soapbox/normalizers/emoji.ts b/app/soapbox/normalizers/emoji.ts
index f450af253..8d973b175 100644
--- a/app/soapbox/normalizers/emoji.ts
+++ b/app/soapbox/normalizers/emoji.ts
@@ -3,10 +3,10 @@
* Converts API emojis into our internal format.
* @see {@link https://docs.joinmastodon.org/entities/emoji/}
*/
-import { Record as ImmutableRecord, Map as ImmutableMap } from 'immutable';
+import { Record as ImmutableRecord, Map as ImmutableMap, fromJS } from 'immutable';
// https://docs.joinmastodon.org/entities/emoji/
-const EmojiRecord = ImmutableRecord({
+export const EmojiRecord = ImmutableRecord({
category: '',
shortcode: '',
static_url: '',
@@ -14,6 +14,8 @@ const EmojiRecord = ImmutableRecord({
visible_in_picker: true,
});
-export const normalizeEmoji = (emoji: ImmutableMap) => {
- return EmojiRecord(emoji);
+export const normalizeEmoji = (emoji: Record) => {
+ return EmojiRecord(
+ ImmutableMap(fromJS(emoji)),
+ );
};
diff --git a/app/soapbox/normalizers/index.ts b/app/soapbox/normalizers/index.ts
new file mode 100644
index 000000000..c4a34d66c
--- /dev/null
+++ b/app/soapbox/normalizers/index.ts
@@ -0,0 +1,9 @@
+export { AccountRecord, FieldRecord, normalizeAccount } from './account';
+export { AttachmentRecord, normalizeAttachment } from './attachment';
+export { CardRecord, normalizeCard } from './card';
+export { EmojiRecord, normalizeEmoji } from './emoji';
+export { InstanceRecord, normalizeInstance } from './instance';
+export { MentionRecord, normalizeMention } from './mention';
+export { NotificationRecord, normalizeNotification } from './notification';
+export { PollRecord, PollOptionRecord, normalizePoll } from './poll';
+export { StatusRecord, normalizeStatus } from './status';
diff --git a/app/soapbox/normalizers/instance.ts b/app/soapbox/normalizers/instance.ts
index 8e0fe02a6..a33601bf9 100644
--- a/app/soapbox/normalizers/instance.ts
+++ b/app/soapbox/normalizers/instance.ts
@@ -7,6 +7,7 @@ import {
Map as ImmutableMap,
List as ImmutableList,
Record as ImmutableRecord,
+ fromJS,
} from 'immutable';
import { parseVersion, PLEROMA } from 'soapbox/utils/features';
@@ -15,7 +16,7 @@ import { isNumber } from 'soapbox/utils/numbers';
// Use Mastodon defaults
// https://docs.joinmastodon.org/entities/instance/
-const InstanceRecord = ImmutableRecord({
+export const InstanceRecord = ImmutableRecord({
approval_required: false,
contact_account: ImmutableMap(),
configuration: ImmutableMap({
@@ -83,13 +84,25 @@ const pleromaToMastodonConfig = (instance: ImmutableMap) => {
// Get the software's default attachment limit
const getAttachmentLimit = (software: string) => software === PLEROMA ? Infinity : 4;
-// Normalize instance (Pleroma, Mastodon, etc.) to Mastodon's format
-export const normalizeInstance = (instance: ImmutableMap) => {
- const { software } = parseVersion(instance.get('version'));
- const mastodonConfig = pleromaToMastodonConfig(instance);
+// Normalize version
+const normalizeVersion = (instance: ImmutableMap) => {
+ return instance.update('version', '0.0.0', version => {
+ // Handle Mastodon release candidates
+ if (new RegExp(/[0-9\.]+rc[0-9]+/g).test(version)) {
+ return version.split('rc').join('-rc');
+ } else {
+ return version;
+ }
+ });
+};
+// Normalize instance (Pleroma, Mastodon, etc.) to Mastodon's format
+export const normalizeInstance = (instance: Record) => {
return InstanceRecord(
- instance.withMutations(instance => {
+ ImmutableMap(fromJS(instance)).withMutations((instance: ImmutableMap) => {
+ const { software } = parseVersion(instance.get('version'));
+ const mastodonConfig = pleromaToMastodonConfig(instance);
+
// Merge configuration
instance.update('configuration', ImmutableMap(), configuration => (
configuration.mergeDeepWith(mergeDefined, mastodonConfig)
@@ -100,6 +113,9 @@ export const normalizeInstance = (instance: ImmutableMap) => {
return isNumber(value) ? value : getAttachmentLimit(software);
});
+ // Normalize version
+ normalizeVersion(instance);
+
// Merge defaults
instance.mergeDeepWith(mergeDefined, InstanceRecord());
}),
diff --git a/app/soapbox/normalizers/mention.ts b/app/soapbox/normalizers/mention.ts
index 998202065..5ff35ce16 100644
--- a/app/soapbox/normalizers/mention.ts
+++ b/app/soapbox/normalizers/mention.ts
@@ -3,22 +3,19 @@
* Converts API mentions into our internal format.
* @see {@link https://docs.joinmastodon.org/entities/mention/}
*/
-import {
- Map as ImmutableMap,
- Record as ImmutableRecord,
-} from 'immutable';
+import { Record as ImmutableRecord } from 'immutable';
import { normalizeAccount } from 'soapbox/normalizers/account';
// https://docs.joinmastodon.org/entities/mention/
-const MentionRecord = ImmutableRecord({
+export const MentionRecord = ImmutableRecord({
id: '',
acct: '',
username: '',
url: '',
});
-export const normalizeMention = (mention: ImmutableMap) => {
+export const normalizeMention = (mention: Record) => {
// Simply normalize it as an account then cast it as a mention Β―\_(γ)_/Β―
return MentionRecord(normalizeAccount(mention));
};
diff --git a/app/soapbox/normalizers/notification.ts b/app/soapbox/normalizers/notification.ts
index e0f466618..defd51215 100644
--- a/app/soapbox/normalizers/notification.ts
+++ b/app/soapbox/normalizers/notification.ts
@@ -6,10 +6,11 @@
import {
Map as ImmutableMap,
Record as ImmutableRecord,
+ fromJS,
} from 'immutable';
// https://docs.joinmastodon.org/entities/notification/
-const NotificationRecord = ImmutableRecord({
+export const NotificationRecord = ImmutableRecord({
account: null,
chat_message: null, // pleroma:chat_mention
created_at: new Date(),
@@ -20,6 +21,8 @@ const NotificationRecord = ImmutableRecord({
type: '',
});
-export const normalizeNotification = (notification: ImmutableMap) => {
- return NotificationRecord(notification);
+export const normalizeNotification = (notification: Record) => {
+ return NotificationRecord(
+ ImmutableMap(fromJS(notification)),
+ );
};
diff --git a/app/soapbox/normalizers/poll.ts b/app/soapbox/normalizers/poll.ts
index fa127702e..592f9aa65 100644
--- a/app/soapbox/normalizers/poll.ts
+++ b/app/soapbox/normalizers/poll.ts
@@ -8,6 +8,7 @@ import {
Map as ImmutableMap,
List as ImmutableList,
Record as ImmutableRecord,
+ fromJS,
} from 'immutable';
import emojify from 'soapbox/features/emoji/emoji';
@@ -15,7 +16,7 @@ import { normalizeEmoji } from 'soapbox/normalizers/emoji';
import { makeEmojiMap } from 'soapbox/utils/normalizers';
// https://docs.joinmastodon.org/entities/poll/
-const PollRecord = ImmutableRecord({
+export const PollRecord = ImmutableRecord({
emojis: ImmutableList(),
expired: false,
expires_at: new Date(),
@@ -29,7 +30,7 @@ const PollRecord = ImmutableRecord({
});
// Sub-entity of Poll
-const PollOptionRecord = ImmutableRecord({
+export const PollOptionRecord = ImmutableRecord({
title: '',
votes_count: 0,
@@ -76,9 +77,9 @@ const normalizePollVoted = (poll: ImmutableMap) => {
});
};
-export const normalizePoll = (poll: ImmutableMap) => {
+export const normalizePoll = (poll: Record) => {
return PollRecord(
- poll.withMutations((poll: ImmutableMap) => {
+ ImmutableMap(fromJS(poll)).withMutations((poll: ImmutableMap) => {
normalizeEmojis(poll);
normalizePollOptions(poll);
normalizePollOwnVotes(poll);
diff --git a/app/soapbox/normalizers/status.ts b/app/soapbox/normalizers/status.ts
index 2a1039f45..8db44ba39 100644
--- a/app/soapbox/normalizers/status.ts
+++ b/app/soapbox/normalizers/status.ts
@@ -7,6 +7,7 @@ import {
Map as ImmutableMap,
List as ImmutableList,
Record as ImmutableRecord,
+ fromJS,
} from 'immutable';
import { normalizeAttachment } from 'soapbox/normalizers/attachment';
@@ -17,7 +18,7 @@ import { normalizePoll } from 'soapbox/normalizers/poll';
import { IStatus } from 'soapbox/types';
// https://docs.joinmastodon.org/entities/status/
-const StatusRecord = ImmutableRecord({
+export const StatusRecord = ImmutableRecord({
account: null,
application: null,
bookmarked: false,
@@ -135,9 +136,9 @@ const fixQuote = (status: ImmutableMap) => {
});
};
-export const normalizeStatus = (status: ImmutableMap): IStatus => {
+export const normalizeStatus = (status: Record): IStatus => {
return StatusRecord(
- status.withMutations(status => {
+ ImmutableMap(fromJS(status)).withMutations(status => {
normalizeAttachments(status);
normalizeMentions(status);
normalizeEmojis(status);
diff --git a/app/soapbox/reducers/__tests__/compose-test.js b/app/soapbox/reducers/__tests__/compose-test.js
index 348739a6b..454c787ca 100644
--- a/app/soapbox/reducers/__tests__/compose-test.js
+++ b/app/soapbox/reducers/__tests__/compose-test.js
@@ -1,10 +1,11 @@
-import { Map as ImmutableMap } from 'immutable';
+import { Map as ImmutableMap, fromJS } from 'immutable';
import * as actions from 'soapbox/actions/compose';
import { ME_FETCH_SUCCESS, ME_PATCH_SUCCESS } from 'soapbox/actions/me';
import { SETTING_CHANGE } from 'soapbox/actions/settings';
-//import { REDRAFT } from 'soapbox/actions/statuses';
+import { REDRAFT } from 'soapbox/actions/statuses';
import { TIMELINE_DELETE } from 'soapbox/actions/timelines';
+import { normalizeStatus } from 'soapbox/normalizers/status';
import reducer from '../compose';
@@ -38,6 +39,29 @@ describe('compose reducer', () => {
expect(state.get('idempotencyKey').length === 36);
});
+ describe('REDRAFT', () => {
+ it('strips Pleroma integer attachments', () => {
+ const action = {
+ type: REDRAFT,
+ status: normalizeStatus(fromJS(require('soapbox/__fixtures__/pleroma-status-deleted.json'))),
+ v: { software: 'Pleroma' },
+ };
+
+ const result = reducer(undefined, action);
+ expect(result.get('media_attachments').isEmpty()).toBe(true);
+ });
+
+ it('leaves non-Pleroma integer attachments alone', () => {
+ const action = {
+ type: REDRAFT,
+ status: normalizeStatus(fromJS(require('soapbox/__fixtures__/pleroma-status-deleted.json'))),
+ };
+
+ const result = reducer(undefined, action);
+ expect(result.getIn(['media_attachments', 0, 'id'])).toEqual('508107650');
+ });
+ });
+
it('uses \'public\' scope as default', () => {
const action = {
type: actions.COMPOSE_REPLY,
@@ -325,30 +349,6 @@ describe('compose reducer', () => {
});
});
- // it('should handle COMPOSE_UPLOAD_UNDO', () => {
- // const state = ImmutableMap({
- // media_attachments: ImmutableList([
- // description: null,
- // id: '1375732379',
- // pleroma: {
- // mime_type: 'image/jpeg'
- // },
- // preview_url: 'https://media.gleasonator.com/media_attachments/files/000/853/856/original/7035d67937053e1d.jpg',
- // remote_url: 'https://media.gleasonator.com/media_attachments/files/000/853/856/original/7035d67937053e1d.jpg',
- // text_url: 'https://media.gleasonator.com/media_attachments/files/000/853/856/original/7035d67937053e1d.jpg',
- // type: 'image',
- // url: 'https://media.gleasonator.com/media_attachments/files/000/853/856/original/7035d67937053e1d.jpg'
- // ]),
- // });
- // const action = {
- // type: actions.COMPOSE_UPLOAD_UNDO,
- // mediaId: '1375732379',
- // };
- // expect(reducer(state, action)).toEqual({
- // media_attachments: [],
- // });
- // });
-
it('should handle COMPOSE_UPLOAD_PROGRESS', () => {
const state = ImmutableMap({ progress: 0 });
const action = {
@@ -361,203 +361,6 @@ describe('compose reducer', () => {
});
});
- // it('should handle COMPOSE_MENTION', () => {
- // const state = ImmutableMap({});
- // const account = {
- // '9w1HhmenIAKBHJiUs4': {
- // header_static: 'https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png',
- // display_name_html: 'Alex Gleason',
- // bot: false,
- // display_name: 'Alex Gleason',
- // created_at: '2020-06-12T21:47:28.000Z',
- // locked: false,
- // emojis: [],
- // header: 'https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png',
- // url: 'https://gleasonator.com/users/alex',
- // note: 'Fediverse developer. I come in peace. #vegan #freeculture #atheist #antiporn #gendercritical. Boosts β endorsements.',
- // acct: 'alex@gleasonator.com',
- // avatar_static: 'https://media.gleasonator.com/accounts/avatars/000/000/001/original/1a630e4c4c64c948.jpg',
- // username: 'alex',
- // avatar: 'https://media.gleasonator.com/accounts/avatars/000/000/001/original/1a630e4c4c64c948.jpg',
- // fields: [
- // {
- // name: 'Website',
- // value: 'https://alexgleason.me',
- // name_emojified: 'Website',
- // value_emojified: 'https://alexgleason.me',
- // value_plain: 'https://alexgleason.me'
- // },
- // {
- // name: 'Pleroma+Soapbox',
- // value: 'https://soapbox.pub',
- // name_emojified: 'Pleroma+Soapbox',
- // value_emojified: 'https://soapbox.pub',
- // value_plain: 'https://soapbox.pub'
- // },
- // {
- // name: 'Email',
- // value: 'alex@alexgleason.me',
- // name_emojified: 'Email',
- // value_emojified: 'alex@alexgleason.me',
- // value_plain: 'alex@alexgleason.me'
- // },
- // {
- // name: 'Gender identity',
- // value: 'Soyboy',
- // name_emojified: 'Gender identity',
- // value_emojified: 'Soyboy',
- // value_plain: 'Soyboy'
- // }
- // ],
- // pleroma: {
- // hide_follows: false,
- // hide_followers_count: false,
- // background_image: null,
- // confirmation_pending: false,
- // is_moderator: false,
- // hide_follows_count: false,
- // hide_followers: false,
- // relationship: {
- // showing_reblogs: true,
- // followed_by: false,
- // subscribing: false,
- // blocked_by: false,
- // requested: false,
- // domain_blocking: false,
- // following: false,
- // endorsed: false,
- // blocking: false,
- // muting: false,
- // id: '9w1HhmenIAKBHJiUs4',
- // muting_notifications: false
- // },
- // tags: [],
- // hide_favorites: true,
- // is_admin: false,
- // skip_thread_containment: false
- // },
- // source: {
- // fields: [],
- // note: 'Fediverse developer. I come in peace. #vegan #freeculture #atheist #antiporn #gendercritical. Boosts β endorsements.',
- // pleroma: {
- // actor_type: 'Person',
- // discoverable: false
- // },
- // sensitive: false
- // },
- // id: '9w1HhmenIAKBHJiUs4',
- // note_emojified: 'Fediverse developer. I come in peace. #vegan #freeculture #atheist #antiporn #gendercritical. Boosts β endorsements.'
- // },
- // };
- // const action = {
- // type: actions.COMPOSE_MENTION,
- // account: account,
- // };
- // expect(reducer(state, action).toJS()).toMatchObject({
- // text: '@alex@gleasonator.com',
- // caretPosition: null,
- // });
- // });
-
- // it('should handle COMPOSE_DIRECT', () => {
- // const state = ImmutableMap({});
- // const account = {
- // '9w1HhmenIAKBHJiUs4': {
- // header_static: 'https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png',
- // display_name_html: 'Alex Gleason',
- // bot: false,
- // display_name: 'Alex Gleason',
- // created_at: '2020-06-12T21:47:28.000Z',
- // locked: false,
- // emojis: [],
- // header: 'https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png',
- // url: 'https://gleasonator.com/users/alex',
- // note: 'Fediverse developer. I come in peace. #vegan #freeculture #atheist #antiporn #gendercritical. Boosts β endorsements.',
- // acct: 'alex@gleasonator.com',
- // avatar_static: 'https://media.gleasonator.com/accounts/avatars/000/000/001/original/1a630e4c4c64c948.jpg',
- // username: 'alex',
- // avatar: 'https://media.gleasonator.com/accounts/avatars/000/000/001/original/1a630e4c4c64c948.jpg',
- // fields: [
- // {
- // name: 'Website',
- // value: 'https://alexgleason.me',
- // name_emojified: 'Website',
- // value_emojified: 'https://alexgleason.me',
- // value_plain: 'https://alexgleason.me'
- // },
- // {
- // name: 'Pleroma+Soapbox',
- // value: 'https://soapbox.pub',
- // name_emojified: 'Pleroma+Soapbox',
- // value_emojified: 'https://soapbox.pub',
- // value_plain: 'https://soapbox.pub'
- // },
- // {
- // name: 'Email',
- // value: 'alex@alexgleason.me',
- // name_emojified: 'Email',
- // value_emojified: 'alex@alexgleason.me',
- // value_plain: 'alex@alexgleason.me'
- // },
- // {
- // name: 'Gender identity',
- // value: 'Soyboy',
- // name_emojified: 'Gender identity',
- // value_emojified: 'Soyboy',
- // value_plain: 'Soyboy'
- // }
- // ],
- // pleroma: {
- // hide_follows: false,
- // hide_followers_count: false,
- // background_image: null,
- // confirmation_pending: false,
- // is_moderator: false,
- // hide_follows_count: false,
- // hide_followers: false,
- // relationship: {
- // showing_reblogs: true,
- // followed_by: false,
- // subscribing: false,
- // blocked_by: false,
- // requested: false,
- // domain_blocking: false,
- // following: false,
- // endorsed: false,
- // blocking: false,
- // muting: false,
- // id: '9w1HhmenIAKBHJiUs4',
- // muting_notifications: false
- // },
- // tags: [],
- // hide_favorites: true,
- // is_admin: false,
- // skip_thread_containment: false
- // },
- // source: {
- // fields: [],
- // note: 'Fediverse developer. I come in peace. #vegan #freeculture #atheist #antiporn #gendercritical. Boosts β endorsements.',
- // pleroma: {
- // actor_type: 'Person',
- // discoverable: false
- // },
- // sensitive: false
- // },
- // id: '9w1HhmenIAKBHJiUs4',
- // note_emojified: 'Fediverse developer. I come in peace. #vegan #freeculture #atheist #antiporn #gendercritical. Boosts β endorsements.'
- // }
- // };
- // const action = {
- // type: actions.COMPOSE_DIRECT,
- // account: account,
- // };
- // expect(reducer(state, action).toJS()).toMatchObject({
- // text: '@alex@gleasonator.com',
- // caretPosition: null,
- // privacy: 'direct',
- // });
- // });
- //
it('should handle COMPOSE_SUGGESTIONS_CLEAR', () => {
const state = ImmutableMap({ });
const action = {
@@ -570,28 +373,6 @@ describe('compose reducer', () => {
});
});
- // it('should handle COMPOSE_SUGGESTIONS_READY', () => {
- // const state = ImmutableMap({ default_privacy: 'public', privacy: 'public'});
- // const action = {
- // type: actions.COMPOSE_SUGGESTIONS_READY,
- // };
- // expect(reducer(state, action).toJS()).toMatchObject({
- // default_privacy: 'unlisted',
- // privacy: 'public',
- // });
- // });
- //
- // it('should handle COMPOSE_SUGGESTION_SELECT', () => {
- // const state = ImmutableMap({ default_privacy: 'public', privacy: 'public'});
- // const action = {
- // type: actions.COMPOSE_SUGGESTION_SELECT,
- // };
- // expect(reducer(state, action).toJS()).toMatchObject({
- // default_privacy: 'unlisted',
- // privacy: 'public',
- // });
- // });
- //
it('should handle COMPOSE_SUGGESTION_TAGS_UPDATE', () => {
const state = ImmutableMap({ tagHistory: [ 'hashtag' ] });
const action = {
@@ -627,42 +408,6 @@ describe('compose reducer', () => {
});
});
- // it('should handle COMPOSE_EMOJI_INSERT', () => {
- // const state = ImmutableMap({ text: 'this is my' });
- // const action = {
- // type: actions.COMPOSE_EMOJI_INSERT,
- // position: 11,
- // emoji: [],
- // needsSpace, true,
- // };
- // expect(reducer(state, action).toJS()).toMatchObject({
- // text: 'this is my :emoji:',
- // caretPosition: 15,
- // });
- // });
- //
- // it('should handle COMPOSE_UPLOAD_CHANGE_SUCCESS', () => {
- // const state = ImmutableMap({ default_privacy: 'public' });
- // const action = {
- // type: actions.COMPOSE_UPLOAD_CHANGE_SUCCESS,
- // };
- // expect(reducer(state, action).toJS()).toMatchObject({
- // default_privacy: 'unlisted',
- // privacy: 'public',
- // });
- // });
- //
- // it('should handle REDRAFT', () => {
- // const state = ImmutableMap({ default_privacy: 'public' });
- // const action = {
- // type: REDRAFT,
- // };
- // expect(reducer(state, action).toJS()).toMatchObject({
- // default_privacy: 'unlisted',
- // privacy: 'public',
- // });
- // });
- //
it('should handle COMPOSE_POLL_ADD', () => {
const state = ImmutableMap({ poll: null });
const initialPoll = Object({
@@ -691,34 +436,6 @@ describe('compose reducer', () => {
});
});
- // it('should handle COMPOSE_POLL_OPTION_ADD', () => {
- // const initialPoll = Object({
- // options: [
- // 'option 1',
- // 'option 2',
- // ],
- // expires_in: 86400,
- // multiple: false
- // });
- // const state = ImmutableMap({ poll: initialPoll });
- // const action = {
- // type: actions.COMPOSE_POLL_OPTION_ADD,
- // title: 'option 3',
- // };
- // const updatedPoll = Object({
- // options: [
- // 'option 1',
- // 'option 2',
- // 'option 3',
- // ],
- // expires_in: 86400,
- // multiple: false,
- // });
- // expect(reducer(state, action).toJS()).toMatchObject({
- // poll: updatedPoll,
- // });
- // });
-
it('should handle COMPOSE_POLL_OPTION_CHANGE', () => {
const initialPoll = Object({
options: [
@@ -747,32 +464,6 @@ describe('compose reducer', () => {
});
});
- // it('should handle COMPOSE_POLL_OPTION_REMOVE', () => {
- // const initialPoll = Object({
- // options: [
- // 'option 1',
- // 'option 2',
- // ],
- // expires_in: 86400,
- // multiple: false,
- // });
- // const state = ImmutableMap({ poll: initialPoll });
- // const action = {
- // type: actions.COMPOSE_POLL_OPTION_REMOVE,
- // index: 1,
- // };
- // const updatedPoll = Object({
- // options: [
- // 'option 1',
- // ],
- // expires_in: 86400,
- // multiple: false,
- // });
- // expect(reducer(state, action).toJS()).toMatchObject({
- // poll: updatedPoll,
- // });
- // });
-
it('sets the post content-type', () => {
const action = {
type: actions.COMPOSE_TYPE_CHANGE,
diff --git a/app/soapbox/reducers/__tests__/notifications-test.js b/app/soapbox/reducers/__tests__/notifications-test.js
index 630aa61e3..aa5b7cd62 100644
--- a/app/soapbox/reducers/__tests__/notifications-test.js
+++ b/app/soapbox/reducers/__tests__/notifications-test.js
@@ -93,6 +93,22 @@ describe('notifications reducer', () => {
expect(result.items.size).toEqual(1);
expect(result.items.get('4').id).toEqual('4');
});
+
+ it('imports move notification', () => {
+ const action = {
+ type: NOTIFICATIONS_EXPAND_SUCCESS,
+ notifications: [
+ require('soapbox/__fixtures__/pleroma-notification-move.json'),
+ ],
+ next: null,
+ skipLoading: true,
+ };
+
+ const result = reducer(undefined, action).items.get('406814');
+
+ expect(result.account).toEqual('AFmHQ18XZ7Lco68MW8');
+ expect(result.target).toEqual('A5c5LK7EJTFR0u26Pg');
+ });
});
describe('NOTIFICATIONS_EXPAND_REQUEST', () => {
diff --git a/app/soapbox/reducers/accounts.js b/app/soapbox/reducers/accounts.js
index c601ed928..de7926feb 100644
--- a/app/soapbox/reducers/accounts.js
+++ b/app/soapbox/reducers/accounts.js
@@ -46,7 +46,7 @@ const minifyAccount = account => {
};
const fixAccount = (state, account) => {
- const normalized = minifyAccount(normalizeAccount(fromJS(account)));
+ const normalized = minifyAccount(normalizeAccount(account));
return state.set(account.id, normalized);
};
@@ -119,7 +119,7 @@ const removePermission = (state, accountIds, permissionGroup) => {
});
};
-const buildAccount = adminUser => normalizeAccount(fromJS({
+const buildAccount = adminUser => normalizeAccount({
id: adminUser.get('id'),
username: adminUser.get('nickname').split('@')[0],
acct: adminUser.get('nickname'),
@@ -142,7 +142,7 @@ const buildAccount = adminUser => normalizeAccount(fromJS({
},
},
should_refetch: true,
-}));
+});
const mergeAdminUser = (account, adminUser) => {
return account.withMutations(account => {
diff --git a/app/soapbox/reducers/compose.js b/app/soapbox/reducers/compose.js
index 2690b6637..43fd23154 100644
--- a/app/soapbox/reducers/compose.js
+++ b/app/soapbox/reducers/compose.js
@@ -1,6 +1,8 @@
import { Map as ImmutableMap, List as ImmutableList, OrderedSet as ImmutableOrderedSet, fromJS } from 'immutable';
import { tagHistory } from 'soapbox/settings';
+import { PLEROMA } from 'soapbox/utils/features';
+import { hasIntegerMediaIds } from 'soapbox/utils/status';
import {
COMPOSE_MOUNT,
@@ -431,13 +433,17 @@ export default function compose(state = initialState, action) {
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
- // map.set('media_attachments', action.status.get('media_attachments'));
map.set('focusDate', new Date());
map.set('caretPosition', null);
map.set('idempotencyKey', uuid());
map.set('content_type', action.content_type || 'text/plain');
+ if (action.v?.software === PLEROMA && hasIntegerMediaIds(action.status)) {
+ map.set('media_attachments', ImmutableList());
+ } else {
+ map.set('media_attachments', action.status.get('media_attachments'));
+ }
+
if (action.status.get('spoiler_text').length > 0) {
map.set('spoiler', true);
map.set('spoiler_text', action.status.get('spoiler_text'));
diff --git a/app/soapbox/reducers/index.js b/app/soapbox/reducers/index.js
index 989a1fa1f..743a622d4 100644
--- a/app/soapbox/reducers/index.js
+++ b/app/soapbox/reducers/index.js
@@ -116,7 +116,7 @@ const reducers = {
};
// Build a default state from all reducers: it has the key and `undefined`
-const StateRecord = ImmutableRecord(
+export const StateRecord = ImmutableRecord(
Object.keys(reducers).reduce((params, reducer) => {
params[reducer] = undefined;
return params;
diff --git a/app/soapbox/reducers/notifications.js b/app/soapbox/reducers/notifications.js
index 02ffa58e9..fad2563c3 100644
--- a/app/soapbox/reducers/notifications.js
+++ b/app/soapbox/reducers/notifications.js
@@ -56,6 +56,7 @@ const comparator = (a, b) => {
const minifyNotification = notification => {
return notification.mergeWith((o, n) => n || o, {
account: notification.getIn(['account', 'id']),
+ target: notification.getIn(['target', 'id']),
status: notification.getIn(['status', 'id']),
});
};
diff --git a/app/soapbox/reducers/polls.js b/app/soapbox/reducers/polls.js
index f1a06fdc1..f9f220edc 100644
--- a/app/soapbox/reducers/polls.js
+++ b/app/soapbox/reducers/polls.js
@@ -1,4 +1,4 @@
-import { Map as ImmutableMap, fromJS } from 'immutable';
+import { Map as ImmutableMap } from 'immutable';
import { POLLS_IMPORT } from 'soapbox/actions/importer';
import { normalizeStatus } from 'soapbox/normalizers/status';
@@ -6,7 +6,7 @@ import { normalizeStatus } from 'soapbox/normalizers/status';
// HOTFIX: Convert the poll into a fake status to normalize it...
// TODO: get rid of POLLS_IMPORT and use STATUS_IMPORT here.
const normalizePoll = poll => {
- const status = fromJS({ poll });
+ const status = { poll };
return normalizeStatus(status).poll;
};
diff --git a/app/soapbox/utils/__tests__/numbers-test.js b/app/soapbox/utils/__tests__/numbers-test.js
new file mode 100644
index 000000000..86923a781
--- /dev/null
+++ b/app/soapbox/utils/__tests__/numbers-test.js
@@ -0,0 +1,13 @@
+import { isIntegerId } from '../numbers';
+
+test('isIntegerId()', () => {
+ expect(isIntegerId('0')).toBe(true);
+ expect(isIntegerId('1')).toBe(true);
+ expect(isIntegerId('508107650')).toBe(true);
+ expect(isIntegerId('-1764036199')).toBe(true);
+ expect(isIntegerId('106801667066418367')).toBe(true);
+ expect(isIntegerId('9v5bmRalQvjOy0ECcC')).toBe(false);
+ expect(isIntegerId(null)).toBe(false);
+ expect(isIntegerId(undefined)).toBe(false);
+ expect(isIntegerId()).toBe(false);
+});
diff --git a/app/soapbox/utils/__tests__/status-test.js b/app/soapbox/utils/__tests__/status-test.js
new file mode 100644
index 000000000..0dcb3e78a
--- /dev/null
+++ b/app/soapbox/utils/__tests__/status-test.js
@@ -0,0 +1,12 @@
+import { fromJS } from 'immutable';
+
+import { normalizeStatus } from 'soapbox/normalizers/status';
+
+import { hasIntegerMediaIds } from '../status';
+
+describe('hasIntegerMediaIds()', () => {
+ it('returns true for a Pleroma deleted status', () => {
+ const status = normalizeStatus(fromJS(require('soapbox/__fixtures__/pleroma-status-deleted.json')));
+ expect(hasIntegerMediaIds(status)).toBe(true);
+ });
+});
diff --git a/app/soapbox/utils/accounts.ts b/app/soapbox/utils/accounts.ts
index e93138c86..a97f76338 100644
--- a/app/soapbox/utils/accounts.ts
+++ b/app/soapbox/utils/accounts.ts
@@ -10,14 +10,14 @@ const getDomainFromURL = (account: ImmutableMap): string => {
};
export const getDomain = (account: ImmutableMap): string => {
- const domain = account.get('acct').split('@')[1];
+ const domain = account.get('acct', '').split('@')[1];
return domain ? domain : getDomainFromURL(account);
};
export const guessFqn = (account: ImmutableMap): string => {
- const [user, domain] = account.get('acct').split('@');
+ const [user, domain] = account.get('acct', '').split('@');
if (!domain) return [user, getDomainFromURL(account)].join('@');
- return account.get('acct');
+ return account.get('acct', '');
};
export const getBaseURL = (account: ImmutableMap): string => {
@@ -31,7 +31,7 @@ export const getBaseURL = (account: ImmutableMap): string => {
// user@domain even for local users
export const acctFull = (account: ImmutableMap): string => (
- account.get('fqn') || guessFqn(account)
+ account.get('fqn') || guessFqn(account) || ''
);
export const getAcct = (account: ImmutableMap, displayFqn: boolean): string => (
diff --git a/app/soapbox/utils/numbers.js b/app/soapbox/utils/numbers.js
index 8191692b3..18f4d5019 100644
--- a/app/soapbox/utils/numbers.js
+++ b/app/soapbox/utils/numbers.js
@@ -12,3 +12,5 @@ export const shortNumberFormat = number => {
return K;
}
};
+
+export const isIntegerId = id => new RegExp(/^-?[0-9]+$/g).test(id);
diff --git a/app/soapbox/utils/status.js b/app/soapbox/utils/status.js
index 48554ced9..acd69dc5e 100644
--- a/app/soapbox/utils/status.js
+++ b/app/soapbox/utils/status.js
@@ -1,3 +1,5 @@
+import { isIntegerId } from 'soapbox/utils/numbers';
+
export const getFirstExternalLink = status => {
try {
// Pulled from Pleroma's media parser
@@ -13,3 +15,8 @@ export const getFirstExternalLink = status => {
export const shouldHaveCard = status => {
return Boolean(getFirstExternalLink(status));
};
+
+// https://gitlab.com/soapbox-pub/soapbox-fe/-/merge_requests/1087
+export const hasIntegerMediaIds = status => {
+ return status.media_attachments.some(({ id }) => isIntegerId(id));
+};
diff --git a/app/styles/chats.scss b/app/styles/chats.scss
index 418d0eca8..6060e882f 100644
--- a/app/styles/chats.scss
+++ b/app/styles/chats.scss
@@ -487,6 +487,7 @@
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
+ max-height: 19px;
a {
color: var(--highlight-text-color);
diff --git a/soapbox-screenshot.png b/soapbox-screenshot.png
index 4b3f82cbf..19c19e2c4 100644
Binary files a/soapbox-screenshot.png and b/soapbox-screenshot.png differ
diff --git a/yarn.lock b/yarn.lock
index 78299464e..a36b842ee 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3003,9 +3003,9 @@ caniuse-api@^3.0.0:
lodash.uniq "^4.5.0"
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001252, caniuse-lite@^1.0.30001254:
- version "1.0.30001257"
- resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001257.tgz#150aaf649a48bee531104cfeda57f92ce587f6e5"
- integrity sha512-JN49KplOgHSXpIsVSF+LUyhD8PUp6xPpAXeRrrcBh4KBeP7W864jHn6RvzJgDlrReyeVjMFJL3PLpPvKIxlIHA==
+ version "1.0.30001317"
+ resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001317.tgz"
+ integrity sha512-xIZLh8gBm4dqNX0gkzrBeyI86J2eCjWzYAs40q88smG844YIrN4tVQl/RhquHvKEKImWWFIVh1Lxe5n1G/N+GQ==
catharsis@^0.9.0:
version "0.9.0"