diff --git a/app/soapbox/actions/consumer-auth.ts b/app/soapbox/actions/consumer-auth.ts new file mode 100644 index 000000000..b669c6393 --- /dev/null +++ b/app/soapbox/actions/consumer-auth.ts @@ -0,0 +1,55 @@ +import axios from 'axios'; + +import * as BuildConfig from 'soapbox/build_config'; +import { isURL } from 'soapbox/utils/auth'; +import sourceCode from 'soapbox/utils/code'; +import { getFeatures } from 'soapbox/utils/features'; + +import { createApp } from './apps'; + +import type { AppDispatch, RootState } from 'soapbox/store'; + +const createProviderApp = () => { + return async(dispatch: AppDispatch, getState: () => RootState) => { + const state = getState(); + const { scopes } = getFeatures(state.instance); + + const params = { + client_name: sourceCode.displayName, + redirect_uris: `${window.location.origin}/login/external`, + website: sourceCode.homepage, + scopes, + }; + + return dispatch(createApp(params)); + }; +}; + +export const prepareRequest = (provider: string) => { + return async(dispatch: AppDispatch, getState: () => RootState) => { + const baseURL = isURL(BuildConfig.BACKEND_URL) ? BuildConfig.BACKEND_URL : ''; + + const state = getState(); + const { scopes } = getFeatures(state.instance); + const app = await dispatch(createProviderApp()); + const { client_id, redirect_uri } = app; + + localStorage.setItem('soapbox:external:app', JSON.stringify(app)); + localStorage.setItem('soapbox:external:baseurl', baseURL); + localStorage.setItem('soapbox:external:scopes', scopes); + + const params = { + provider, + authorization: { + client_id, + redirect_uri, + scope: scopes, + }, + }; + + const formdata = axios.toFormData(params); + const query = new URLSearchParams(formdata as any); + + location.href = `${baseURL}/oauth/prepare_request?${query.toString()}`; + }; +}; diff --git a/app/soapbox/base_polyfills.ts b/app/soapbox/base_polyfills.ts index 53146d222..a6e92bb3c 100644 --- a/app/soapbox/base_polyfills.ts +++ b/app/soapbox/base_polyfills.ts @@ -37,7 +37,7 @@ if (!HTMLCanvasElement.prototype.toBlob) { const dataURL = this.toDataURL(type, quality); let data; - if (dataURL.indexOf(BASE64_MARKER) >= 0) { + if (dataURL.includes(BASE64_MARKER)) { const [, base64] = dataURL.split(BASE64_MARKER); data = decodeBase64(base64); } else { diff --git a/app/soapbox/components/autosuggest_input.tsx b/app/soapbox/components/autosuggest_input.tsx index e6a4af6d6..54f126a23 100644 --- a/app/soapbox/components/autosuggest_input.tsx +++ b/app/soapbox/components/autosuggest_input.tsx @@ -30,7 +30,7 @@ const textAtCursorMatchesToken = (str: string, caretPosition: number, searchToke word = str.slice(left, right + caretPosition); } - if (!word || word.trim().length < 3 || searchTokens.indexOf(word[0]) === -1) { + if (!word || word.trim().length < 3 || !searchTokens.includes(word[0])) { return [null, null]; } diff --git a/app/soapbox/components/autosuggest_textarea.tsx b/app/soapbox/components/autosuggest_textarea.tsx index b5d54b670..a475e5ce2 100644 --- a/app/soapbox/components/autosuggest_textarea.tsx +++ b/app/soapbox/components/autosuggest_textarea.tsx @@ -23,7 +23,7 @@ const textAtCursorMatchesToken = (str: string, caretPosition: number) => { word = str.slice(left, right + caretPosition); } - if (!word || word.trim().length < 3 || ['@', ':', '#'].indexOf(word[0]) === -1) { + if (!word || word.trim().length < 3 || !['@', ':', '#'].includes(word[0])) { return [null, null]; } diff --git a/app/soapbox/components/status-action-bar.tsx b/app/soapbox/components/status-action-bar.tsx index a8b590709..8e8cf17bf 100644 --- a/app/soapbox/components/status-action-bar.tsx +++ b/app/soapbox/components/status-action-bar.tsx @@ -442,7 +442,7 @@ const StatusActionBar: React.FC = ({ action: handleChatClick, icon: require('@tabler/icons/messages.svg'), }); - } else { + } else if (features.privacyScopes) { menu.push({ text: intl.formatMessage(messages.direct, { name: username }), action: handleDirectClick, diff --git a/app/soapbox/components/status.tsx b/app/soapbox/components/status.tsx index 3b27b4ad9..5b3fb15a8 100644 --- a/app/soapbox/components/status.tsx +++ b/app/soapbox/components/status.tsx @@ -48,6 +48,7 @@ export interface IStatus { hideActionBar?: boolean, hoverable?: boolean, variant?: 'default' | 'rounded', + withDismiss?: boolean, } const Status: React.FC = (props) => { @@ -65,6 +66,7 @@ const Status: React.FC = (props) => { group, hideActionBar, variant = 'rounded', + withDismiss, } = props; const intl = useIntl(); const history = useHistory(); @@ -181,7 +183,7 @@ const Status: React.FC = (props) => { }; if (!status) return null; - let prepend, rebloggedByText, reblogElement, reblogElementMobile; + let rebloggedByText, reblogElement, reblogElementMobile; if (hidden) { return ( @@ -207,20 +209,6 @@ const Status: React.FC = (props) => { ); } - if (featured) { - prepend = ( -
- - - - - - - -
- ); - } - if (status.reblog && typeof status.reblog === 'object') { const displayNameHtml = { __html: String(status.getIn(['account', 'display_name_html'])) }; @@ -318,7 +306,17 @@ const Status: React.FC = (props) => { onClick={() => history.push(statusUrl)} role='link' > - {prepend} + {featured && ( +
+ + + + + + + +
+ )} = (props) => { {!hideActionBar && (
- +
)} diff --git a/app/soapbox/components/status_list.tsx b/app/soapbox/components/status_list.tsx index db9695b87..c45216e03 100644 --- a/app/soapbox/components/status_list.tsx +++ b/app/soapbox/components/status_list.tsx @@ -122,7 +122,6 @@ const StatusList: React.FC = ({ const renderStatus = (statusId: string) => { return ( - // @ts-ignore = ({ if (!featuredStatusIds) return []; return featuredStatusIds.toArray().map(statusId => ( - // @ts-ignore { /** Text to display next ot the button. */ text?: string, /** Don't render a background behind the icon. */ - transparent?: boolean + transparent?: boolean, + /** Predefined styles to display for the button. */ + theme?: 'seamless' | 'outlined', } /** A clickable icon. */ const IconButton = React.forwardRef((props: IIconButton, ref: React.ForwardedRef): JSX.Element => { - const { src, className, iconClassName, text, transparent = false, ...filteredProps } = props; + const { src, className, iconClassName, text, transparent = false, theme = 'seamless', ...filteredProps } = props; return ( - + + + ); }; diff --git a/app/soapbox/features/emoji/emoji_mart_search_light.js b/app/soapbox/features/emoji/emoji_mart_search_light.js index 89e25785f..f16918ada 100644 --- a/app/soapbox/features/emoji/emoji_mart_search_light.js +++ b/app/soapbox/features/emoji/emoji_mart_search_light.js @@ -85,8 +85,8 @@ export function search(value, { emojisToShowFilter, maxResults, include, exclude pool = {}; data.categories.forEach(category => { - const isIncluded = include && include.length ? include.indexOf(category.name.toLowerCase()) > -1 : true; - const isExcluded = exclude && exclude.length ? exclude.indexOf(category.name.toLowerCase()) > -1 : false; + const isIncluded = include && include.length ? include.includes(category.name.toLowerCase()) : true; + const isExcluded = exclude && exclude.length ? exclude.includes(category.name.toLowerCase()) : false; if (!isIncluded || isExcluded) { return; } @@ -95,8 +95,8 @@ export function search(value, { emojisToShowFilter, maxResults, include, exclude }); if (custom.length) { - const customIsIncluded = include && include.length ? include.indexOf('custom') > -1 : true; - const customIsExcluded = exclude && exclude.length ? exclude.indexOf('custom') > -1 : false; + const customIsIncluded = include && include.length ? include.includes('custom') : true; + const customIsExcluded = exclude && exclude.length ? exclude.includes('custom') : false; if (customIsIncluded && !customIsExcluded) { addCustomToPool(custom, pool); } diff --git a/app/soapbox/features/emoji/emoji_utils.js b/app/soapbox/features/emoji/emoji_utils.js index 1f4629edf..ad0319598 100644 --- a/app/soapbox/features/emoji/emoji_utils.js +++ b/app/soapbox/features/emoji/emoji_utils.js @@ -15,7 +15,7 @@ const buildSearch = (data) => { (split ? string.split(/[-|_|\s]+/) : [string]).forEach((s) => { s = s.toLowerCase(); - if (search.indexOf(s) === -1) { + if (!search.includes(s)) { search.push(s); } }); @@ -190,7 +190,7 @@ function getData(emoji, skin, set) { function uniq(arr) { return arr.reduce((acc, item) => { - if (acc.indexOf(item) === -1) { + if (!acc.includes(item)) { acc.push(item); } return acc; @@ -201,7 +201,7 @@ function intersect(a, b) { const uniqA = uniq(a); const uniqB = uniq(b); - return uniqA.filter(item => uniqB.indexOf(item) >= 0); + return uniqA.filter(item => uniqB.includes(item)); } function deepMerge(a, b) { diff --git a/app/soapbox/features/feed-filtering/feed-carousel.tsx b/app/soapbox/features/feed-filtering/feed-carousel.tsx index 7a2b64b2c..02b30e7a1 100644 --- a/app/soapbox/features/feed-filtering/feed-carousel.tsx +++ b/app/soapbox/features/feed-filtering/feed-carousel.tsx @@ -3,7 +3,7 @@ import React, { useEffect, useState } from 'react'; import { FormattedMessage } from 'react-intl'; import { replaceHomeTimeline } from 'soapbox/actions/timelines'; -import { useAppDispatch, useAppSelector, useDimensions, useFeatures } from 'soapbox/hooks'; +import { useAppDispatch, useAppSelector, useDimensions } from 'soapbox/hooks'; import useCarouselAvatars from 'soapbox/queries/carousels'; import { Card, HStack, Icon, Stack, Text } from '../../components/ui'; @@ -59,8 +59,6 @@ const CarouselItem = ({ avatar }: { avatar: any }) => { }; const FeedCarousel = () => { - const features = useFeatures(); - const { data: avatars, isFetching, isError } = useCarouselAvatars(); const [cardRef, setCardRef, { width }] = useDimensions(); @@ -83,10 +81,6 @@ const FeedCarousel = () => { } }, [width, widthPerAvatar]); - if (!features.feedUserFiltering) { - return null; - } - if (isError) { return ( diff --git a/app/soapbox/features/landing_page/index.tsx b/app/soapbox/features/landing_page/index.tsx index dcb9afd36..1e3b01f80 100644 --- a/app/soapbox/features/landing_page/index.tsx +++ b/app/soapbox/features/landing_page/index.tsx @@ -1,12 +1,15 @@ import * as React from 'react'; import { FormattedMessage } from 'react-intl'; +import { prepareRequest } from 'soapbox/actions/consumer-auth'; import { Button, Card, CardBody, Stack, Text } from 'soapbox/components/ui'; import VerificationBadge from 'soapbox/components/verification_badge'; import RegistrationForm from 'soapbox/features/auth_login/components/registration_form'; -import { useAppSelector, useFeatures, useSoapboxConfig } from 'soapbox/hooks'; +import { useAppDispatch, useAppSelector, useFeatures, useSoapboxConfig } from 'soapbox/hooks'; +import { capitalize } from 'soapbox/utils/strings'; const LandingPage = () => { + const dispatch = useAppDispatch(); const features = useFeatures(); const soapboxConfig = useSoapboxConfig(); const pepeEnabled = soapboxConfig.getIn(['extensions', 'pepe', 'enabled']) === true; @@ -40,6 +43,29 @@ const LandingPage = () => { return ; }; + /** Display login button for external provider. */ + const renderProvider = () => { + const { authProvider } = soapboxConfig; + + return ( + + + + + + + + + + ); + }; + /** Pepe API registrations are open */ const renderPepe = () => { return ( @@ -47,18 +73,26 @@ const LandingPage = () => { - Let's get started! - Social Media Without Discrimination + + + + + + - + ); }; // Render registration flow depending on features const renderBody = () => { - if (pepeEnabled && pepeOpen) { + if (soapboxConfig.authProvider) { + return renderProvider(); + } else if (pepeEnabled && pepeOpen) { return renderPepe(); } else if (features.accountCreation && instance.registrations) { return renderOpen(); diff --git a/app/soapbox/features/notifications/components/notification.tsx b/app/soapbox/features/notifications/components/notification.tsx index 642b92a7f..b7e528ef5 100644 --- a/app/soapbox/features/notifications/components/notification.tsx +++ b/app/soapbox/features/notifications/components/notification.tsx @@ -18,7 +18,7 @@ import { makeGetNotification } from 'soapbox/selectors'; import { NotificationType, validType } from 'soapbox/utils/notification'; import type { ScrollPosition } from 'soapbox/components/status'; -import type { Account, Status, Notification as NotificationEntity } from 'soapbox/types/entities'; +import type { Account, Status as StatusEntity, Notification as NotificationEntity } from 'soapbox/types/entities'; const getNotification = makeGetNotification(); @@ -104,7 +104,7 @@ const messages: Record = defineMessages({ }, update: { id: 'notification.update', - defaultMessage: '{name} edited a post', + defaultMessage: '{name} edited a post you interacted with', }, }); @@ -143,7 +143,7 @@ interface INotificaton { notification: NotificationEntity, onMoveUp?: (notificationId: string) => void, onMoveDown?: (notificationId: string) => void, - onReblog?: (status: Status, e?: KeyboardEvent) => void, + onReblog?: (status: StatusEntity, e?: KeyboardEvent) => void, getScrollPosition?: () => ScrollPosition | undefined, updateScrollBottom?: (bottom: number) => void, } @@ -216,7 +216,7 @@ const Notification: React.FC = (props) => { if (e?.shiftKey || !boostModal) { dispatch(reblog(status)); } else { - dispatch(openModal('BOOST', { status, onReblog: (status: Status) => { + dispatch(openModal('BOOST', { status, onReblog: (status: StatusEntity) => { dispatch(reblog(status)); } })); } @@ -303,16 +303,12 @@ const Notification: React.FC = (props) => { case 'update': case 'pleroma:emoji_reaction': return status && typeof status === 'object' ? ( - // @ts-ignore )} - + {features.feedUserFiltering && } {children} diff --git a/app/soapbox/pages/status_page.js b/app/soapbox/pages/status_page.js deleted file mode 100644 index e4ff9955e..000000000 --- a/app/soapbox/pages/status_page.js +++ /dev/null @@ -1,69 +0,0 @@ -import React from 'react'; -import ImmutablePureComponent from 'react-immutable-pure-component'; -import { connect } from 'react-redux'; - -import LinkFooter from 'soapbox/features/ui/components/link_footer'; -import { - WhoToFollowPanel, - TrendsPanel, - SignUpPanel, - CtaBanner, -} from 'soapbox/features/ui/util/async-components'; -// import GroupSidebarPanel from '../features/groups/sidebar_panel'; -import { getFeatures } from 'soapbox/utils/features'; - -import { Layout } from '../components/ui'; -import BundleContainer from '../features/ui/containers/bundle_container'; - -const mapStateToProps = state => { - const me = state.get('me'); - const features = getFeatures(state.get('instance')); - - return { - me, - showTrendsPanel: features.trends, - showWhoToFollowPanel: features.suggestions, - }; -}; - -export default @connect(mapStateToProps) -class StatusPage extends ImmutablePureComponent { - - render() { - const { me, children, showTrendsPanel, showWhoToFollowPanel } = this.props; - - return ( - <> - - {children} - - {!me && ( - - {Component => } - - )} - - - - {!me && ( - - {Component => } - - )} - {showTrendsPanel && ( - - {Component => } - - )} - {showWhoToFollowPanel && ( - - {Component => } - - )} - - - - ); - } - -} diff --git a/app/soapbox/pages/status_page.tsx b/app/soapbox/pages/status_page.tsx new file mode 100644 index 000000000..2c35947ad --- /dev/null +++ b/app/soapbox/pages/status_page.tsx @@ -0,0 +1,57 @@ +import React from 'react'; + +import LinkFooter from 'soapbox/features/ui/components/link_footer'; +import { + WhoToFollowPanel, + TrendsPanel, + SignUpPanel, + CtaBanner, +} from 'soapbox/features/ui/util/async-components'; +import { useAppSelector, useFeatures } from 'soapbox/hooks'; + +import { Layout } from '../components/ui'; +import BundleContainer from '../features/ui/containers/bundle_container'; + +interface IStatusPage { + children: React.ReactNode, +} + +const StatusPage: React.FC = ({ children }) => { + const me = useAppSelector(state => state.me); + const features = useFeatures(); + + return ( + <> + + {children} + + {!me && ( + + {Component => } + + )} + + + + {!me && ( + + {Component => } + + )} + {features.trends && ( + + {Component => } + + )} + {features.suggestions && ( + + {Component => } + + )} + + + + ); +}; + +export default StatusPage; diff --git a/app/soapbox/selectors/index.ts b/app/soapbox/selectors/index.ts index c1ab55532..2a026afe1 100644 --- a/app/soapbox/selectors/index.ts +++ b/app/soapbox/selectors/index.ts @@ -93,7 +93,7 @@ const toServerSideType = (columnType: string): string => { case 'thread': return columnType; default: - if (columnType.indexOf('list:') > -1) { + if (columnType.includes('list:')) { return 'home'; } else { return 'public'; // community, account, hashtag diff --git a/app/soapbox/service_worker/web_push_notifications.ts b/app/soapbox/service_worker/web_push_notifications.ts index eefeb2b1b..fe3d8e652 100644 --- a/app/soapbox/service_worker/web_push_notifications.ts +++ b/app/soapbox/service_worker/web_push_notifications.ts @@ -146,7 +146,7 @@ const handlePush = (event: PushEvent) => { timestamp: notification.created_at && Number(new Date(notification.created_at)), tag: notification.id, image: notification.status?.media_attachments[0]?.preview_url, - data: { access_token, preferred_locale, id: notification.status ? notification.status.id : notification.account.id, url: notification.status ? `/@${notification.account.username}/posts/${notification.status.id}` : `/@${notification.account.username}` }, + data: { access_token, preferred_locale, id: notification.status ? notification.status.id : notification.account.id, url: notification.status ? `/@${notification.account.acct}/posts/${notification.status.id}` : `/@${notification.account.acct}` }, }; if (notification.status?.spoiler_text || notification.status?.sensitive) { diff --git a/app/soapbox/utils/strings.ts b/app/soapbox/utils/strings.ts new file mode 100644 index 000000000..c1c8e08bc --- /dev/null +++ b/app/soapbox/utils/strings.ts @@ -0,0 +1,7 @@ +/** Capitalize the first letter of a string. */ +// https://stackoverflow.com/a/1026087 +function capitalize(str: string) { + return str.charAt(0).toUpperCase() + str.slice(1); +} + +export { capitalize }; diff --git a/app/styles/components/modal.scss b/app/styles/components/modal.scss index f31680d20..6d4e3d611 100644 --- a/app/styles/components/modal.scss +++ b/app/styles/components/modal.scss @@ -304,7 +304,7 @@ li:not(:empty) { a, button { - @apply flex items-center px-4 py-3 text-gray-600 dark:text-gray-300 no-underline hover:bg-gray-100 dark:bg-gray-800 hover:text-gray-800 dark:hover:text-gray-200; + @apply flex items-center px-4 py-3 text-gray-600 dark:text-gray-300 no-underline hover:bg-gray-100 dark:bg-gray-800 hover:text-gray-800 dark:hover:text-gray-200 text-left; &.destructive { @apply text-danger-600; @@ -325,7 +325,7 @@ } button[type="button"] { - @apply w-full justify-center; + @apply w-full justify-center text-center; } } } diff --git a/custom/modules/.gitkeep b/custom/modules/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/webpack/rules/assets.js b/webpack/rules/assets.js index 1b291ae0c..e0a14c250 100644 --- a/webpack/rules/assets.js +++ b/webpack/rules/assets.js @@ -66,7 +66,10 @@ module.exports = [{ }, { test: /\.svg$/, type: 'asset/resource', - include: resolve('node_modules', '@tabler'), + include: [ + resolve('node_modules', '@tabler'), + resolve('custom', 'modules', '@tabler'), + ], generator: { filename: 'packs/icons/[name]-[contenthash:8][ext]', }, diff --git a/webpack/shared.js b/webpack/shared.js index fb8308ee4..326981b8b 100644 --- a/webpack/shared.js +++ b/webpack/shared.js @@ -141,6 +141,7 @@ module.exports = { resolve: { extensions: settings.extensions, modules: [ + resolve('custom', 'modules'), resolve(settings.source_path), 'node_modules', ],