Merge remote-tracking branch 'soapbox/develop' into mastodon-groups
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 || '';
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
31
app/soapbox/utils/scopes.ts
Normal file
31
app/soapbox/utils/scopes.ts
Normal 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,
|
||||
};
|
||||
@@ -1,9 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
export default {
|
||||
me: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.oneOf([false, null]),
|
||||
]),
|
||||
meLoggedIn: PropTypes.string,
|
||||
};
|
||||
Reference in New Issue
Block a user