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', }; } };