Merge remote-tracking branch 'soapbox/develop' into mastodon-groups

Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
marcin mikołajczak
2023-01-27 23:04:42 +01:00
443 changed files with 10101 additions and 30568 deletions

View File

@@ -4,7 +4,8 @@ import type { RootState } from 'soapbox/store';
export const validId = (id: any) => typeof id === 'string' && id !== 'null' && id !== 'undefined';
export const isURL = (url: string) => {
export const isURL = (url?: string | null) => {
if (typeof url !== 'string') return false;
try {
new URL(url);
return true;
@@ -30,11 +31,11 @@ export const isLoggedIn = (getState: () => RootState) => {
return validId(getState().me);
};
export const getAppToken = (state: RootState) => state.auth.getIn(['app', 'access_token']) as string;
export const getAppToken = (state: RootState) => state.auth.app.access_token as string;
export const getUserToken = (state: RootState, accountId?: string | false | null) => {
const accountUrl = state.accounts.getIn([accountId, 'url']);
return state.auth.getIn(['users', accountUrl, 'access_token']) as string;
const accountUrl = state.accounts.getIn([accountId, 'url']) as string;
return state.auth.users.get(accountUrl)?.access_token as string;
};
export const getAccessToken = (state: RootState) => {
@@ -43,24 +44,23 @@ export const getAccessToken = (state: RootState) => {
};
export const getAuthUserId = (state: RootState) => {
const me = state.auth.get('me');
const me = state.auth.me;
return ImmutableList([
state.auth.getIn(['users', me, 'id']),
state.auth.users.get(me!)?.id,
me,
]).find(validId);
].filter(id => id)).find(validId);
};
export const getAuthUserUrl = (state: RootState) => {
const me = state.auth.get('me');
const me = state.auth.me;
return ImmutableList([
state.auth.getIn(['users', me, 'url']),
state.auth.users.get(me!)?.url,
me,
]).find(isURL);
].filter(url => url)).find(isURL);
};
/** Get the VAPID public key. */
export const getVapidKey = (state: RootState) => {
return state.auth.getIn(['app', 'vapid_key']) || state.instance.getIn(['pleroma', 'vapid_public_key']);
};
export const getVapidKey = (state: RootState) =>
(state.auth.app.vapid_key || state.instance.pleroma.get('vapid_public_key')) as string;

View File

@@ -1,6 +1,7 @@
/* eslint sort-keys: "error" */
import { List as ImmutableList, Map as ImmutableMap } from 'immutable';
import { createSelector } from 'reselect';
import semverCoerce from 'semver/functions/coerce';
import gte from 'semver/functions/gte';
import lt from 'semver/functions/lt';
import semverParse from 'semver/functions/parse';
@@ -15,18 +16,18 @@ const overrides = custom('features');
/** Truthy array convenience function */
const any = (arr: Array<any>): boolean => arr.some(Boolean);
/**
* Friendica, decentralized social platform implementing multiple federation protocols.
* @see {@link https://friendi.ca/}
*/
export const FRIENDICA = 'Friendica';
/**
* Mastodon, the software upon which this is all based.
* @see {@link https://joinmastodon.org/}
*/
export const MASTODON = 'Mastodon';
/**
* Pleroma, a feature-rich alternative written in Elixir.
* @see {@link https://pleroma.social/}
*/
export const PLEROMA = 'Pleroma';
/**
* Mitra, a Rust backend with deep Ethereum integrations.
* @see {@link https://codeberg.org/silverpill/mitra}
@@ -39,6 +40,18 @@ export const MITRA = 'Mitra';
*/
export const PIXELFED = 'Pixelfed';
/**
* Pleroma, a feature-rich alternative written in Elixir.
* @see {@link https://pleroma.social/}
*/
export const PLEROMA = 'Pleroma';
/**
* Takahē, backend with support for serving multiple domains.
* @see {@link https://jointakahe.org/}
*/
export const TAKAHE = 'Takahe';
/**
* Truth Social, the Mastodon fork powering truthsocial.com
* @see {@link https://help.truthsocial.com/open-source}
@@ -46,11 +59,15 @@ export const PIXELFED = 'Pixelfed';
export const TRUTHSOCIAL = 'TruthSocial';
/**
* Rebased, the recommended backend for Soapbox.
* @see {@link https://gitlab.com/soapbox-pub/rebased}
* Wildebeest, backend running on top of Cloudflare Pages.
*/
// NOTE: Rebased is named 'soapbox' for legacy reasons.
export const REBASED = 'soapbox';
export const WILDEBEEST = 'Wildebeest';
/**
* Akkoma, a Pleroma fork.
* @see {@link https://akkoma.dev/AkkomaGang/akkoma}
*/
export const AKKOMA = 'akkoma';
/**
* glitch-soc, fork of Mastodon with a number of experimental features.
@@ -59,10 +76,11 @@ export const REBASED = 'soapbox';
export const GLITCH = 'glitch';
/**
* Akkoma, a Pleroma fork.
* @see {@link https://akkoma.dev/AkkomaGang/akkoma}
* Rebased, the recommended backend for Soapbox.
* @see {@link https://gitlab.com/soapbox-pub/rebased}
*/
export const AKKOMA = 'akkoma';
// NOTE: Rebased is named 'soapbox' for legacy reasons.
export const REBASED = 'soapbox';
/** Parse features for the given instance */
const getInstanceFeatures = (instance: Instance) => {
@@ -88,10 +106,7 @@ const getInstanceFeatures = (instance: Instance) => {
* Ability to create accounts.
* @see POST /api/v1/accounts
*/
accountCreation: any([
v.software === MASTODON,
v.software === PLEROMA,
]),
accountCreation: v.software !== TRUTHSOCIAL,
/**
* Ability to pin other accounts on one's profile.
@@ -117,6 +132,7 @@ const getInstanceFeatures = (instance: Instance) => {
accountLookup: any([
v.software === MASTODON && gte(v.compatVersion, '3.4.0'),
v.software === PLEROMA && gte(v.version, '2.4.50'),
v.software === TAKAHE && gte(v.version, '0.6.1'),
v.software === TRUTHSOCIAL,
]),
@@ -174,6 +190,13 @@ const getInstanceFeatures = (instance: Instance) => {
*/
announcementsReactions: v.software === MASTODON && gte(v.compatVersion, '3.1.0'),
/**
* Pleroma backups.
* @see GET /api/v1/pleroma/backups
* @see POST /api/v1/pleroma/backups
*/
backups: v.software === PLEROMA,
/**
* Set your birthday and view upcoming birthdays.
* @see GET /api/v1/pleroma/birthdays
@@ -191,6 +214,7 @@ const getInstanceFeatures = (instance: Instance) => {
* @see GET /api/v1/bookmarks
*/
bookmarks: any([
v.software === FRIENDICA,
v.software === MASTODON && gte(v.compatVersion, '3.1.0'),
v.software === PLEROMA && gte(v.version, '0.9.9'),
v.software === PIXELFED,
@@ -268,7 +292,7 @@ const getInstanceFeatures = (instance: Instance) => {
/**
* Paginated chats API.
* @see GET /api/v2/chats
* @see GET /api/v2/pleroma/chats
*/
chatsV2: any([
v.software === TRUTHSOCIAL,
@@ -285,9 +309,11 @@ const getInstanceFeatures = (instance: Instance) => {
* @see {@link https://docs.joinmastodon.org/methods/timelines/conversations/}
*/
conversations: any([
v.software === FRIENDICA,
v.software === MASTODON && gte(v.compatVersion, '2.6.0'),
v.software === PLEROMA && gte(v.version, '0.9.9'),
v.software === PIXELFED,
v.software === TAKAHE,
]),
/**
@@ -295,10 +321,26 @@ const getInstanceFeatures = (instance: Instance) => {
* @see GET /api/v1/timelines/direct
*/
directTimeline: any([
v.software === FRIENDICA,
v.software === MASTODON && lt(v.compatVersion, '3.0.0'),
v.software === PLEROMA && gte(v.version, '0.9.9'),
]),
/**
* Ability to edit profile information.
* @see PATCH /api/v1/accounts/update_credentials
*/
editProfile: any([
v.software === FRIENDICA,
v.software === MASTODON,
v.software === MITRA,
v.software === PIXELFED,
v.software === PLEROMA,
v.software === TAKAHE && gte(v.version, '0.7.0'),
v.software === TRUTHSOCIAL,
v.software === WILDEBEEST,
]),
editStatuses: any([
v.software === MASTODON && gte(v.version, '3.5.0'),
features.includes('editing'),
@@ -357,7 +399,7 @@ const getInstanceFeatures = (instance: Instance) => {
* @see GET /api/v1/pleroma/events/:id/ics
* @see GET /api/v1/pleroma/search/location
*/
events: v.software === PLEROMA && v.build === REBASED && gte(v.version, '2.4.50'),
events: features.includes('events'),
/**
* Ability to address recipients of a status explicitly (with `to`).
@@ -368,9 +410,13 @@ const getInstanceFeatures = (instance: Instance) => {
v.software === TRUTHSOCIAL,
]),
/** Whether to allow exporting follows/blocks/mutes to CSV by paginating the API. */
exportData: true,
/** Whether the accounts who favourited or emoji-reacted to a status can be viewed through the API. */
exposableReactions: any([
v.software === MASTODON,
v.software === TAKAHE && gte(v.version, '0.6.1'),
v.software === TRUTHSOCIAL,
features.includes('exposable_reactions'),
]),
@@ -379,7 +425,10 @@ const getInstanceFeatures = (instance: Instance) => {
* Can see accounts' followers you know
* @see GET /api/v1/accounts/familiar_followers
*/
familiarFollowers: v.software === MASTODON && gte(v.version, '3.5.0'),
familiarFollowers: any([
v.software === MASTODON && gte(v.version, '3.5.0'),
v.software === TAKAHE,
]),
/** Whether the instance federates. */
federating: federation.get('enabled', true) === true, // Assume true unless explicitly false
@@ -440,7 +489,7 @@ const getInstanceFeatures = (instance: Instance) => {
* @see POST /api/v1/admin/groups/:group_id/unsuspend
* @see DELETE /api/v1/admin/groups/:group_id
*/
groups: v.software === MASTODON && gte(v.compatVersion, '3.5.3'), // '4.1.0' ?
groups: false,
/**
* Can hide follows/followers lists and counts.
@@ -470,6 +519,7 @@ const getInstanceFeatures = (instance: Instance) => {
* @see GET /api/v1/timelines/list/:list_id
*/
lists: any([
v.software === FRIENDICA,
v.software === MASTODON && gte(v.compatVersion, '2.1.0'),
v.software === PLEROMA && gte(v.version, '0.9.9'),
]),
@@ -501,6 +551,7 @@ const getInstanceFeatures = (instance: Instance) => {
*/
mediaV2: any([
v.software === MASTODON && gte(v.compatVersion, '3.1.3'),
v.software === WILDEBEEST,
// Even though Pleroma supports these endpoints, it has disadvantages
// v.software === PLEROMA && gte(v.version, '2.1.0'),
]),
@@ -537,6 +588,7 @@ const getInstanceFeatures = (instance: Instance) => {
notificationsIncludeTypes: any([
v.software === MASTODON && gte(v.compatVersion, '3.5.0'),
v.software === PLEROMA && gte(v.version, '2.4.50'),
v.software === TAKAHE && gte(v.version, '0.6.2'),
]),
/**
@@ -581,6 +633,7 @@ const getInstanceFeatures = (instance: Instance) => {
* @see {@link https://docs.joinmastodon.org/methods/instance/directory/}
*/
profileDirectory: any([
v.software === FRIENDICA,
v.software === MASTODON && gte(v.compatVersion, '3.0.0'),
features.includes('profile_directory'),
]),
@@ -600,8 +653,11 @@ const getInstanceFeatures = (instance: Instance) => {
* @see GET /api/v1/timelines/public
*/
publicTimeline: any([
v.software === FRIENDICA,
v.software === MASTODON,
v.software === PLEROMA,
v.software === TAKAHE,
v.software === WILDEBEEST,
]),
/**
@@ -665,13 +721,6 @@ const getInstanceFeatures = (instance: Instance) => {
v.software === PLEROMA,
]),
/**
* List of OAuth scopes supported by both Soapbox and the backend.
* @see POST /api/v1/apps
* @see POST /oauth/token
*/
scopes: v.software === PLEROMA ? 'read write follow push admin' : 'read write follow push',
/**
* Ability to search statuses from the given account.
* @see {@link https://docs.joinmastodon.org/methods/search/}
@@ -730,6 +779,7 @@ const getInstanceFeatures = (instance: Instance) => {
* @see GET /api/v2/suggestions
*/
suggestionsV2: any([
v.software === FRIENDICA,
v.software === MASTODON && gte(v.compatVersion, '3.4.0'),
v.software === TRUTHSOCIAL,
features.includes('v2_suggestions'),
@@ -810,8 +860,9 @@ export const parseVersion = (version: string): Backend => {
const regex = /^([\w+.]*)(?: \(compatible; ([\w]*) (.*)\))?$/;
const match = regex.exec(version);
const semver = match ? semverParse(match[3] || match[1]) : null;
const compat = match ? semverParse(match[1]) : null;
const semverString = match && (match[3] || match[1]);
const semver = match ? semverParse(semverString) || semverCoerce(semverString) : null;
const compat = match ? semverParse(match[1]) || semverCoerce(match[1]) : null;
if (match && semver && compat) {
return {

View File

@@ -1,6 +1,6 @@
/** Convert HTML to a plaintext representation, preserving whitespace. */
// NB: This function can still return unsafe HTML
export const unescapeHTML = (html: string): string => {
export const unescapeHTML = (html: string = ''): string => {
const wrapper = document.createElement('div');
wrapper.innerHTML = html.replace(/<br\s*\/?>/g, '\n').replace(/<\/p><[^>]*>/g, '\n\n').replace(/<[^>]*>/g, '');
return wrapper.textContent || '';

View File

@@ -57,28 +57,33 @@ enum VideoProviders {
RUMBLE = 'rumble.com'
}
/** Try adding autoplay to an iframe embed for platforms such as YouTube. */
const addAutoPlay = (html: string): string => {
const document = domParser.parseFromString(html, 'text/html').documentElement;
const iframe = document.querySelector('iframe');
if (iframe) {
const url = new URL(iframe.src);
const provider = new URL(iframe.src).host;
if (provider === VideoProviders.RUMBLE) {
url.searchParams.append('pub', '7a20');
url.searchParams.append('autoplay', '2');
} else {
url.searchParams.append('autoplay', '1');
url.searchParams.append('auto_play', '1');
iframe.allow = 'autoplay';
try {
const document = domParser.parseFromString(html, 'text/html').documentElement;
const iframe = document.querySelector('iframe');
if (iframe) {
const url = new URL(iframe.src);
const provider = new URL(iframe.src).host;
if (provider === VideoProviders.RUMBLE) {
url.searchParams.append('pub', '7a20');
url.searchParams.append('autoplay', '2');
} else {
url.searchParams.append('autoplay', '1');
url.searchParams.append('auto_play', '1');
iframe.allow = 'autoplay';
}
iframe.src = url.toString();
// DOM parser creates html/body elements around original HTML fragment,
// so we need to get innerHTML out of the body and not the entire document
return (document.querySelector('body') as HTMLBodyElement).innerHTML;
}
iframe.src = url.toString();
// DOM parser creates html/body elements around original HTML fragment,
// so we need to get innerHTML out of the body and not the entire document
return (document.querySelector('body') as HTMLBodyElement).innerHTML;
} catch (e) {
return html;
}
return html;

View File

@@ -1,9 +1,10 @@
import { Location } from 'history';
import { useEffect } from 'react';
import type { Location } from 'soapbox/types/history';
const LOCAL_STORAGE_REDIRECT_KEY = 'soapbox:redirect-uri';
const cacheCurrentUrl = (location: Location<unknown>) => {
const cacheCurrentUrl = (location: Location) => {
const actualUrl = encodeURIComponent(`${location.pathname}${location.search}`);
localStorage.setItem(LOCAL_STORAGE_REDIRECT_KEY, actualUrl);
return actualUrl;

View File

@@ -0,0 +1,31 @@
import { PLEROMA, parseVersion } from './features';
import type { RootState } from 'soapbox/store';
import type { Instance } from 'soapbox/types/entities';
/**
* Get the OAuth scopes to use for login & signup.
* Mastodon will refuse scopes it doesn't know, so care is needed.
*/
const getInstanceScopes = (instance: Instance) => {
const v = parseVersion(instance.version);
switch (v.software) {
case PLEROMA:
return 'read write follow push admin';
default:
return 'read write follow push';
}
};
/** Convenience function to get scopes from instance in store. */
const getScopes = (state: RootState) => {
return getInstanceScopes(state.instance);
};
export {
getInstanceScopes,
getScopes,
};

View File

@@ -1,9 +0,0 @@
import PropTypes from 'prop-types';
export default {
me: PropTypes.oneOfType([
PropTypes.string,
PropTypes.oneOf([false, null]),
]),
meLoggedIn: PropTypes.string,
};