diff --git a/app/soapbox/components/sidebar-navigation.tsx b/app/soapbox/components/sidebar-navigation.tsx
index eede8c871..bb8f47e4d 100644
--- a/app/soapbox/components/sidebar-navigation.tsx
+++ b/app/soapbox/components/sidebar-navigation.tsx
@@ -81,12 +81,12 @@ const SidebarNavigation = () => {
});
}
- if (features.localTimeline || features.publicTimeline) {
+ if (features.publicTimeline) {
menu.push(null);
}
}
- if (features.localTimeline) {
+ if (features.publicTimeline) {
menu.push({
to: '/timeline/local',
icon: features.federating ? require('@tabler/icons/icons/users.svg') : require('@tabler/icons/icons/world.svg'),
@@ -94,7 +94,7 @@ const SidebarNavigation = () => {
});
}
- if (features.localTimeline && features.federating) {
+ if (features.publicTimeline && features.federating) {
menu.push({
to: '/timeline/fediverse',
icon: require('icons/fediverse.svg'),
diff --git a/app/soapbox/components/sidebar_menu.tsx b/app/soapbox/components/sidebar_menu.tsx
index e7cec5d95..2de745577 100644
--- a/app/soapbox/components/sidebar_menu.tsx
+++ b/app/soapbox/components/sidebar_menu.tsx
@@ -192,27 +192,25 @@ const SidebarMenu: React.FC = (): JSX.Element | null => {
onClick={onClose}
/>
- {(features.localTimeline || features.publicTimeline) && (
+ {features.publicTimeline && <>
- )}
- {features.localTimeline && (
}
onClick={onClose}
/>
- )}
- {(features.publicTimeline && features.federating) && (
- }
- onClick={onClose}
- />
- )}
+ {features.federating && (
+ }
+ onClick={onClose}
+ />
+ )}
+ >}
diff --git a/app/soapbox/utils/features.ts b/app/soapbox/utils/features.ts
index 27982f15d..947dc4d12 100644
--- a/app/soapbox/utils/features.ts
+++ b/app/soapbox/utils/features.ts
@@ -1,4 +1,4 @@
-// Detect backend features to conditionally render elements
+/* eslint sort-keys: "error" */
import { List as ImmutableList, Map as ImmutableMap } from 'immutable';
import { createSelector } from 'reselect';
import gte from 'semver/functions/gte';
@@ -8,152 +8,444 @@ import { custom } from 'soapbox/custom';
import type { Instance } from 'soapbox/types/entities';
-// Import custom overrides, if exists
+/** Import custom overrides, if exists */
const overrides = custom('features');
-// Truthy array convenience function
+/** Truthy array convenience function */
const any = (arr: Array): boolean => arr.some(Boolean);
-// For uglification
-export const MASTODON = 'Mastodon';
-export const PLEROMA = 'Pleroma';
-export const MITRA = 'Mitra';
-export const TRUTHSOCIAL = 'TruthSocial';
-export const PIXELFED = 'Pixelfed';
+/**
+ * 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}
+ */
+export const MITRA = 'Mitra';
+
+/**
+ * Pixelfed, a federated image sharing platform.
+ * @see {@link https://pixelfed.org/}
+ */
+export const PIXELFED = 'Pixelfed';
+
+/**
+ * Truth Social, the Mastodon fork powering truthsocial.com
+ * @see {@link https://help.truthsocial.com/open-source}
+ */
+export const TRUTHSOCIAL = 'TruthSocial';
+
+/** Parse features for the given instance */
const getInstanceFeatures = (instance: Instance) => {
const v = parseVersion(instance.version);
const features = instance.pleroma.getIn(['metadata', 'features'], ImmutableList()) as ImmutableList;
const federation = instance.pleroma.getIn(['metadata', 'federation'], ImmutableMap()) as ImmutableMap;
return {
- media: true,
- privacyScopes: v.software !== TRUTHSOCIAL,
- spoilers: v.software !== TRUTHSOCIAL,
- filters: v.software !== TRUTHSOCIAL,
- polls: any([
- v.software === MASTODON && gte(v.version, '2.8.0'),
- v.software === PLEROMA,
+ /**
+ * Can view and manage ActivityPub aliases through the API.
+ * @see GET /api/pleroma/aliases
+ * @see PATCH /api/v1/accounts/update_credentials
+ */
+ accountAliasesAPI: v.software === PLEROMA,
+
+ /**
+ * The accounts API allows an acct instead of an ID.
+ * @see GET /api/v1/accounts/:acct_or_id
+ */
+ accountByUsername: v.software === PLEROMA,
+
+ /**
+ * Ability to pin other accounts on one's profile.
+ * @see POST /api/v1/accounts/:id/pin
+ * @see POST /api/v1/accounts/:id/unpin
+ * @see GET /api/v1/pleroma/accounts/:id/endorsements
+ */
+ accountEndorsements: v.software === PLEROMA && gte(v.version, '2.4.50'),
+
+ /**
+ * Ability to set one's location on their profile.
+ * @see PATCH /api/v1/accounts/update_credentials
+ */
+ accountLocation: v.software === TRUTHSOCIAL,
+
+ /**
+ * Look up an account by the acct.
+ * @see GET /api/v1/accounts/lookup
+ */
+ accountLookup: any([
+ v.software === MASTODON && gte(v.compatVersion, '3.4.0'),
+ v.software === PLEROMA && gte(v.version, '2.4.50'),
]),
- scheduledStatuses: any([
- v.software === MASTODON && gte(v.version, '2.7.0'),
- v.software === PLEROMA,
+
+ /**
+ * Move followers to a different ActivityPub account.
+ * @see POST /api/pleroma/move_account
+ */
+ accountMoving: v.software === PLEROMA && gte(v.version, '2.4.50'),
+
+ /**
+ * Ability to subscribe to notifications every time an account posts.
+ * @see POST /api/v1/accounts/:id/follow
+ */
+ accountNotifies: any([
+ v.software === MASTODON && gte(v.compatVersion, '3.3.0'),
+ v.software === PLEROMA && gte(v.version, '2.4.50'),
]),
+
+ /**
+ * Ability to subscribe to notifications every time an account posts.
+ * @see POST /api/v1/pleroma/accounts/:id/subscribe
+ * @see POST /api/v1/pleroma/accounts/:id/unsubscribe
+ */
+ accountSubscriptions: v.software === PLEROMA && gte(v.version, '1.0.0'),
+
+ /**
+ * Ability to set one's website on their profile.
+ * @see PATCH /api/v1/accounts/update_credentials
+ */
+ accountWebsite: v.software === TRUTHSOCIAL,
+
+ /**
+ * Set your birthday and view upcoming birthdays.
+ * @see GET /api/v1/pleroma/birthdays
+ * @see POST /api/v1/accounts
+ * @see PATCH /api/v1/accounts/update_credentials
+ */
+ birthdays: v.software === PLEROMA && gte(v.version, '2.4.50'),
+
+ /** Whether people who blocked you are visible through the API. */
+ blockersVisible: features.includes('blockers_visible'),
+
+ /**
+ * Can bookmark statuses.
+ * @see POST /api/v1/statuses/:id/bookmark
+ * @see GET /api/v1/bookmarks
+ */
bookmarks: any([
v.software === MASTODON && gte(v.compatVersion, '3.1.0'),
v.software === PLEROMA && gte(v.version, '0.9.9'),
v.software === PIXELFED,
]),
- lists: any([
- v.software === MASTODON && gte(v.compatVersion, '2.1.0'),
- v.software === PLEROMA && gte(v.version, '0.9.9'),
- ]),
- suggestions: any([
- v.software === MASTODON && gte(v.compatVersion, '2.4.3'),
- v.software === TRUTHSOCIAL,
- features.includes('v2_suggestions'),
- ]),
- suggestionsV2: any([
- v.software === MASTODON && gte(v.compatVersion, '3.4.0'),
- v.software === TRUTHSOCIAL,
- features.includes('v2_suggestions'),
- ]),
- blockersVisible: features.includes('blockers_visible'),
- trends: any([
- v.software === MASTODON && gte(v.compatVersion, '3.0.0'),
- v.software === TRUTHSOCIAL,
- ]),
- mediaV2: any([
- v.software === MASTODON && gte(v.compatVersion, '3.1.3'),
- // Even though Pleroma supports these endpoints, it has disadvantages
- // v.software === PLEROMA && gte(v.version, '2.1.0'),
- ]),
- localTimeline: any([
- v.software === MASTODON,
- v.software === PLEROMA,
- ]),
- publicTimeline: any([
- v.software === MASTODON,
- v.software === PLEROMA,
- ]),
- directTimeline: any([
- v.software === MASTODON && lt(v.compatVersion, '3.0.0'),
- v.software === PLEROMA && gte(v.version, '0.9.9'),
- ]),
+
+ /**
+ * Pleroma chats API.
+ * @see {@link https://docs.pleroma.social/backend/development/API/chats/}
+ */
+ chats: v.software === PLEROMA && gte(v.version, '2.1.0'),
+
+ /**
+ * Paginated chats API.
+ * @see GET /api/v2/chats
+ */
+ chatsV2: v.software === PLEROMA && gte(v.version, '2.3.0'),
+
+ /**
+ * Mastodon's newer solution for direct messaging.
+ * @see {@link https://docs.joinmastodon.org/methods/timelines/conversations/}
+ */
conversations: any([
v.software === MASTODON && gte(v.compatVersion, '2.6.0'),
v.software === PLEROMA && gte(v.version, '0.9.9'),
v.software === PIXELFED,
]),
- emojiReacts: v.software === PLEROMA && gte(v.version, '2.0.0'),
- emojiReactsRGI: v.software === PLEROMA && gte(v.version, '2.2.49'),
- focalPoint: v.software === MASTODON && gte(v.compatVersion, '2.3.0'),
- importAPI: v.software === PLEROMA,
- importMutes: v.software === PLEROMA && gte(v.version, '2.2.0'),
- emailList: features.includes('email_list'),
- chats: v.software === PLEROMA && gte(v.version, '2.1.0'),
- chatsV2: v.software === PLEROMA && gte(v.version, '2.3.0'),
- scopes: v.software === PLEROMA ? 'read write follow push admin' : 'read write follow push',
- federating: federation.get('enabled', true) === true, // Assume true unless explicitly false
- richText: v.software === PLEROMA,
- securityAPI: any([
- v.software === PLEROMA,
- v.software === TRUTHSOCIAL,
- ]),
- settingsStore: any([
- v.software === PLEROMA,
- v.software === TRUTHSOCIAL,
- ]),
- accountAliasesAPI: v.software === PLEROMA,
- resetPasswordAPI: v.software === PLEROMA,
- exposableReactions: features.includes('exposable_reactions'),
- accountSubscriptions: v.software === PLEROMA && gte(v.version, '1.0.0'),
- accountNotifies: any([
- v.software === MASTODON && gte(v.compatVersion, '3.3.0'),
- v.software === PLEROMA && gte(v.version, '2.4.50'),
- ]),
- unrestrictedLists: v.software === PLEROMA,
- accountByUsername: v.software === PLEROMA,
- profileDirectory: any([
- v.software === MASTODON && gte(v.compatVersion, '3.0.0'),
- features.includes('profile_directory'),
- ]),
- accountLookup: any([
- v.software === MASTODON && gte(v.compatVersion, '3.4.0'),
- v.software === PLEROMA && gte(v.version, '2.4.50'),
- ]),
- remoteInteractionsAPI: v.software === PLEROMA && gte(v.version, '2.4.50'),
- explicitAddressing: any([
- v.software === PLEROMA && gte(v.version, '1.0.0'),
- v.software === TRUTHSOCIAL,
- ]),
- accountEndorsements: v.software === PLEROMA && gte(v.version, '2.4.50'),
- quotePosts: any([
- v.software === PLEROMA && gte(v.version, '2.4.50'),
- instance.feature_quote === true,
- ]),
- birthdays: v.software === PLEROMA && gte(v.version, '2.4.50'),
- ethereumLogin: v.software === MITRA,
- accountMoving: v.software === PLEROMA && gte(v.version, '2.4.50'),
- notes: any([
- v.software === MASTODON && gte(v.compatVersion, '3.2.0'),
- v.software === PLEROMA && gte(v.version, '2.4.50'),
- ]),
- trendingTruths: v.software === TRUTHSOCIAL,
- trendingStatuses: v.software === MASTODON && gte(v.compatVersion, '3.5.0'),
- pepe: v.software === TRUTHSOCIAL,
- accountLocation: v.software === TRUTHSOCIAL,
- accountWebsite: v.software === TRUTHSOCIAL,
- frontendConfigurations: v.software === PLEROMA,
// FIXME: long-term this shouldn't be a feature,
// but for now we want it to be overrideable in the build
darkMode: true,
+
+ /**
+ * Legacy DMs timeline where messages are displayed chronologically without groupings.
+ * @see GET /api/v1/timelines/direct
+ */
+ directTimeline: any([
+ v.software === MASTODON && lt(v.compatVersion, '3.0.0'),
+ v.software === PLEROMA && gte(v.version, '0.9.9'),
+ ]),
+
+ /**
+ * Soapbox email list.
+ * @see POST /api/v1/accounts
+ * @see PATCH /api/v1/accounts/update_credentials
+ * @see GET /api/v1/pleroma/admin/email_list/subscribers.csv
+ * @see GET /api/v1/pleroma/admin/email_list/unsubscribers.csv
+ * @see GET /api/v1/pleroma/admin/email_list/combined.csv
+ */
+ emailList: features.includes('email_list'),
+
+ /**
+ * Ability to add emoji reactions to a status.
+ * @see PUT /api/v1/pleroma/statuses/:id/reactions/:emoji
+ * @see GET /api/v1/pleroma/statuses/:id/reactions/:emoji?
+ * @see DELETE /api/v1/pleroma/statuses/:id/reactions/:emoji
+ */
+ emojiReacts: v.software === PLEROMA && gte(v.version, '2.0.0'),
+
+ /**
+ * The backend allows only RGI ("Recommended for General Interchange") emoji reactions.
+ * @see PUT /api/v1/pleroma/statuses/:id/reactions/:emoji
+ */
+ emojiReactsRGI: v.software === PLEROMA && gte(v.version, '2.2.49'),
+
+ /**
+ * Sign in with an Ethereum wallet.
+ * @see POST /oauth/token
+ */
+ ethereumLogin: v.software === MITRA,
+
+ /**
+ * Ability to address recipients of a status explicitly (with `to`).
+ * @see POST /api/v1/statuses
+ */
+ explicitAddressing: any([
+ v.software === PLEROMA && gte(v.version, '1.0.0'),
+ v.software === TRUTHSOCIAL,
+ ]),
+
+ /** Whether the accounts who favourited or emoji-reacted to a status can be viewed through the API. */
+ exposableReactions: features.includes('exposable_reactions'),
+
+ /** Whether the instance federates. */
+ federating: federation.get('enabled', true) === true, // Assume true unless explicitly false
+
+ /**
+ * Can edit and manage timeline filters (aka "muted words").
+ * @see {@link https://docs.joinmastodon.org/methods/accounts/filters/}
+ */
+ filters: v.software !== TRUTHSOCIAL,
+
+ /**
+ * Allows setting the focal point of a media attachment.
+ * @see {@link https://docs.joinmastodon.org/methods/statuses/media/}
+ */
+ focalPoint: v.software === MASTODON && gte(v.compatVersion, '2.3.0'),
+
+ /**
+ * Whether client settings can be retrieved from the API.
+ * @see GET /api/pleroma/frontend_configurations
+ */
+ frontendConfigurations: v.software === PLEROMA,
+
+ /**
+ * Pleroma import API.
+ * @see POST /api/pleroma/follow_import
+ * @see POST /api/pleroma/blocks_import
+ * @see POST /api/pleroma/mutes_import
+ */
+ importAPI: v.software === PLEROMA,
+
+ /**
+ * Pleroma import mutes API.
+ * @see POST /api/pleroma/mutes_import
+ */
+ importMutes: v.software === PLEROMA && gte(v.version, '2.2.0'),
+
+ /**
+ * Can create, view, and manage lists.
+ * @see {@link https://docs.joinmastodon.org/methods/timelines/lists/}
+ * @see GET /api/v1/timelines/list/:list_id
+ */
+ lists: any([
+ v.software === MASTODON && gte(v.compatVersion, '2.1.0'),
+ v.software === PLEROMA && gte(v.version, '0.9.9'),
+ ]),
+
+ /**
+ * Can upload media attachments to statuses.
+ * @see POST /api/v1/media
+ * @see POST /api/v1/statuses
+ */
+ media: true,
+
+ /**
+ * Supports V2 media uploads.
+ * @see POST /api/v2/media
+ */
+ mediaV2: any([
+ v.software === MASTODON && gte(v.compatVersion, '3.1.3'),
+ // Even though Pleroma supports these endpoints, it has disadvantages
+ // v.software === PLEROMA && gte(v.version, '2.1.0'),
+ ]),
+
+ /**
+ * Add private notes to accounts.
+ * @see POST /api/v1/accounts/:id/note
+ * @see GET /api/v1/accounts/relationships
+ */
+ notes: any([
+ v.software === MASTODON && gte(v.compatVersion, '3.2.0'),
+ v.software === PLEROMA && gte(v.version, '2.4.50'),
+ ]),
+
+ /** Truth Social account registration API. */
+ pepe: v.software === TRUTHSOCIAL,
+
+ /**
+ * Can add polls to statuses.
+ * @see POST /api/v1/statuses
+ */
+ polls: any([
+ v.software === MASTODON && gte(v.version, '2.8.0'),
+ v.software === PLEROMA,
+ ]),
+
+ /**
+ * Can set privacy scopes on statuses.
+ * @see POST /api/v1/statuses
+ */
+ privacyScopes: v.software !== TRUTHSOCIAL,
+
+ /**
+ * A directory of discoverable profiles from the instance.
+ * @see {@link https://docs.joinmastodon.org/methods/instance/directory/}
+ */
+ profileDirectory: any([
+ v.software === MASTODON && gte(v.compatVersion, '3.0.0'),
+ features.includes('profile_directory'),
+ ]),
+
+ /**
+ * Can display a timeline of all known public statuses.
+ * Local and Fediverse timelines both use this feature.
+ * @see GET /api/v1/timelines/public
+ */
+ publicTimeline: any([
+ v.software === MASTODON,
+ v.software === PLEROMA,
+ ]),
+
+ /**
+ * Ability to quote posts in statuses.
+ * @see POST /api/v1/statuses
+ */
+ quotePosts: any([
+ v.software === PLEROMA && gte(v.version, '2.4.50'),
+ instance.feature_quote === true,
+ ]),
+
+ /**
+ * Interact with statuses from another instance while logged-out.
+ * @see POST /api/v1/pleroma/remote_interaction
+ */
+ remoteInteractionsAPI: v.software === PLEROMA && gte(v.version, '2.4.50'),
+
+ /**
+ * Can request a password reset email through the API.
+ * @see POST /auth/password
+ */
+ resetPasswordAPI: v.software === PLEROMA,
+
+ /**
+ * Ability to post statuses in Markdown, BBCode, and HTML.
+ * @see POST /api/v1/statuses
+ */
+ richText: v.software === PLEROMA,
+
+ /**
+ * Can schedule statuses to be posted at a later time.
+ * @see POST /api/v1/statuses
+ * @see {@link https://docs.joinmastodon.org/methods/statuses/scheduled_statuses/}
+ */
+ scheduledStatuses: any([
+ v.software === MASTODON && gte(v.version, '2.7.0'),
+ 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 manage account security settings.
+ * @see POST /api/pleroma/change_password
+ * @see POST /api/pleroma/change_email
+ * @see POST /api/pleroma/delete_account
+ */
+ securityAPI: any([
+ v.software === PLEROMA,
+ v.software === TRUTHSOCIAL,
+ ]),
+
+ /**
+ * Can store client settings in the database.
+ * @see PATCH /api/v1/accounts/update_credentials
+ */
+ settingsStore: any([
+ v.software === PLEROMA,
+ v.software === TRUTHSOCIAL,
+ ]),
+
+ /**
+ * Can set content warnings on statuses.
+ * @see POST /api/v1/statuses
+ */
+ spoilers: v.software !== TRUTHSOCIAL,
+
+ /**
+ * Can display suggested accounts.
+ * @see {@link https://docs.joinmastodon.org/methods/accounts/suggestions/}
+ */
+ suggestions: any([
+ v.software === MASTODON && gte(v.compatVersion, '2.4.3'),
+ v.software === TRUTHSOCIAL,
+ features.includes('v2_suggestions'),
+ ]),
+
+ /**
+ * Supports V2 suggested accounts.
+ * @see GET /api/v2/suggestions
+ */
+ suggestionsV2: any([
+ v.software === MASTODON && gte(v.compatVersion, '3.4.0'),
+ v.software === TRUTHSOCIAL,
+ features.includes('v2_suggestions'),
+ ]),
+
+ /**
+ * Trending statuses.
+ * @see GET /api/v1/trends/statuses
+ */
+ trendingStatuses: v.software === MASTODON && gte(v.compatVersion, '3.5.0'),
+
+ /**
+ * Truth Social trending statuses API.
+ * @see GET /api/v1/truth/trending/truths
+ */
+ trendingTruths: v.software === TRUTHSOCIAL,
+
+ /**
+ * Can display trending hashtags.
+ * @see GET /api/v1/trends
+ */
+ trends: any([
+ v.software === MASTODON && gte(v.compatVersion, '3.0.0'),
+ v.software === TRUTHSOCIAL,
+ ]),
+
+ /**
+ * Whether the backend allows adding users you don't follow to lists.
+ * @see POST /api/v1/lists/:id/accounts
+ */
+ unrestrictedLists: v.software === PLEROMA,
};
};
+/** Features available from a backend */
export type Features = ReturnType;
+/** Detect backend features to conditionally render elements */
export const getFeatures = createSelector([
(instance: Instance) => instance,
], (instance): Features => {
@@ -161,29 +453,34 @@ export const getFeatures = createSelector([
return Object.assign(features, overrides) as Features;
});
+/** Fediverse backend */
interface Backend {
+ /** Name of the software */
software: string | null,
+ /** API version number */
version: string,
+ /** Mastodon API version this backend is compatible with */
compatVersion: string,
}
+/** Get information about the software from its version string */
export const parseVersion = (version: string): Backend => {
const regex = /^([\w.]*)(?: \(compatible; ([\w]*) (.*)\))?$/;
const match = regex.exec(version);
if (match) {
return {
+ compatVersion: match[1],
software: match[2] || MASTODON,
version: match[3] || match[1],
- compatVersion: match[1],
};
} else {
// If we can't parse the version, this is a new and exotic backend.
// Fall back to minimal featureset.
return {
+ compatVersion: '0.0.0',
software: null,
version: '0.0.0',
- compatVersion: '0.0.0',
};
}
};