diff --git a/app/soapbox/__fixtures__/pleroma-status-vertical-video-without-metadata.json b/app/soapbox/__fixtures__/pleroma-status-vertical-video-without-metadata.json new file mode 100644 index 000000000..edb24b9ef --- /dev/null +++ b/app/soapbox/__fixtures__/pleroma-status-vertical-video-without-metadata.json @@ -0,0 +1,108 @@ +{ + "account": { + "acct": "alex", + "avatar": "https://freespeechextremist.com/images/avi.png", + "avatar_static": "https://freespeechextremist.com/images/avi.png", + "bot": false, + "created_at": "2022-02-28T01:55:05.000Z", + "display_name": "Alex Gleason", + "emojis": [], + "fields": [], + "followers_count": 1, + "following_count": 0, + "header": "https://freespeechextremist.com/images/banner.png", + "header_static": "https://freespeechextremist.com/images/banner.png", + "id": "AGv8wCadU7DqWgMqNk", + "locked": false, + "note": "I'm testing out compatibility with an older Pleroma version", + "pleroma": { + "accepts_chat_messages": true, + "ap_id": "https://freespeechextremist.com/users/alex", + "background_image": null, + "confirmation_pending": false, + "favicon": null, + "hide_favorites": true, + "hide_followers": false, + "hide_followers_count": false, + "hide_follows": false, + "hide_follows_count": false, + "is_admin": false, + "is_moderator": false, + "relationship": {}, + "skip_thread_containment": false, + "tags": [] + }, + "source": { + "fields": [], + "note": "I'm testing out compatibility with an older Pleroma version", + "pleroma": { + "actor_type": "Person", + "discoverable": true + }, + "sensitive": false + }, + "statuses_count": 1, + "url": "https://freespeechextremist.com/users/alex", + "username": "alex" + }, + "application": { + "name": "Web", + "website": null + }, + "bookmarked": false, + "card": null, + "content": "
0f66e92f339705ccc03079b8f647048e15730adf2cc9eaa1071c7c7cf6884b1b.webm", + "created_at": "2022-04-14T19:42:48.000Z", + "emojis": [], + "favourited": false, + "favourites_count": 0, + "id": "AIRxLeIzncpCtsr2hs", + "in_reply_to_account_id": null, + "in_reply_to_id": null, + "language": null, + "media_attachments": [ + { + "description": "0f66e92f339705ccc03079b8f647048e15730adf2cc9eaa1071c7c7cf6884b1b.webm", + "id": "1142674091", + "pleroma": { + "mime_type": "video/webm" + }, + "preview_url": "https://freespeechextremist.com/media/3e34b808-1c84-4ef3-ba56-67cc86b7911a/0f66e92f339705ccc03079b8f647048e15730adf2cc9eaa1071c7c7cf6884b1b.webm?name=0f66e92f339705ccc03079b8f647048e15730adf2cc9eaa1071c7c7cf6884b1b.webm", + "remote_url": "https://freespeechextremist.com/media/3e34b808-1c84-4ef3-ba56-67cc86b7911a/0f66e92f339705ccc03079b8f647048e15730adf2cc9eaa1071c7c7cf6884b1b.webm?name=0f66e92f339705ccc03079b8f647048e15730adf2cc9eaa1071c7c7cf6884b1b.webm", + "text_url": "https://freespeechextremist.com/media/3e34b808-1c84-4ef3-ba56-67cc86b7911a/0f66e92f339705ccc03079b8f647048e15730adf2cc9eaa1071c7c7cf6884b1b.webm?name=0f66e92f339705ccc03079b8f647048e15730adf2cc9eaa1071c7c7cf6884b1b.webm", + "type": "video", + "url": "https://freespeechextremist.com/media/3e34b808-1c84-4ef3-ba56-67cc86b7911a/0f66e92f339705ccc03079b8f647048e15730adf2cc9eaa1071c7c7cf6884b1b.webm?name=0f66e92f339705ccc03079b8f647048e15730adf2cc9eaa1071c7c7cf6884b1b.webm" + } + ], + "mentions": [], + "muted": false, + "pinned": false, + "pleroma": { + "content": { + "text/plain": "0f66e92f339705ccc03079b8f647048e15730adf2cc9eaa1071c7c7cf6884b1b.webm" + }, + "conversation_id": 97191096, + "direct_conversation_id": null, + "emoji_reactions": [], + "expires_at": null, + "in_reply_to_account_acct": null, + "local": true, + "parent_visible": false, + "spoiler_text": { + "text/plain": "" + }, + "thread_muted": false + }, + "poll": null, + "reblog": null, + "reblogged": false, + "reblogs_count": 0, + "replies_count": 0, + "sensitive": false, + "spoiler_text": "", + "tags": [], + "text": null, + "uri": "https://freespeechextremist.com/objects/419b2cad-656a-4dbc-b2b5-94bb75e0afc8", + "url": "https://freespeechextremist.com/notice/AIRxLeIzncpCtsr2hs", + "visibility": "public" +} diff --git a/app/soapbox/components/dropdown_menu.tsx b/app/soapbox/components/dropdown_menu.tsx index 01a60948d..e28ea0f9e 100644 --- a/app/soapbox/components/dropdown_menu.tsx +++ b/app/soapbox/components/dropdown_menu.tsx @@ -18,7 +18,7 @@ let id = 0; export interface MenuItem { action?: React.EventHandler, middleClick?: React.EventHandler, - text: string, + text: string | JSX.Element, href?: string, to?: string, newTab?: boolean, diff --git a/app/soapbox/components/sidebar-navigation-link.tsx b/app/soapbox/components/sidebar-navigation-link.tsx index bd0567c03..fc63372e7 100644 --- a/app/soapbox/components/sidebar-navigation-link.tsx +++ b/app/soapbox/components/sidebar-navigation-link.tsx @@ -8,17 +8,29 @@ interface ISidebarNavigationLink { count?: number, icon: string, text: string | React.ReactElement, - to: string, + to?: string, + onClick?: React.EventHandler, } -const SidebarNavigationLink = ({ icon, text, to, count }: ISidebarNavigationLink) => { +const SidebarNavigationLink = React.forwardRef((props: ISidebarNavigationLink, ref: React.ForwardedRef): JSX.Element => { + const { icon, text, to = '', count, onClick } = props; const isActive = location.pathname === to; const withCounter = typeof count !== 'undefined'; + const handleClick: React.EventHandler = (e) => { + if (onClick) { + onClick(e); + e.preventDefault(); + e.stopPropagation(); + } + }; + return ( {text} ); -}; +}); export default SidebarNavigationLink; diff --git a/app/soapbox/components/sidebar-navigation.tsx b/app/soapbox/components/sidebar-navigation.tsx index 96411d6c3..3f33e6733 100644 --- a/app/soapbox/components/sidebar-navigation.tsx +++ b/app/soapbox/components/sidebar-navigation.tsx @@ -1,27 +1,110 @@ -import { Map as ImmutableMap } from 'immutable'; +import { Map as ImmutableMap, OrderedSet as ImmutableOrderedSet } from 'immutable'; import React from 'react'; import { FormattedMessage } from 'react-intl'; import { getSettings } from 'soapbox/actions/settings'; +import DropdownMenu from 'soapbox/containers/dropdown_menu_container'; import ComposeButton from 'soapbox/features/ui/components/compose-button'; -import { useAppSelector } from 'soapbox/hooks'; +import { useAppSelector, useOwnAccount } from 'soapbox/hooks'; import { getBaseURL } from 'soapbox/utils/accounts'; import { getFeatures } from 'soapbox/utils/features'; import SidebarNavigationLink from './sidebar-navigation-link'; +import type { Menu } from 'soapbox/components/dropdown_menu'; + const SidebarNavigation = () => { - const me = useAppSelector((state) => state.me); const instance = useAppSelector((state) => state.instance); const settings = useAppSelector((state) => getSettings(state)); - const account = useAppSelector((state) => state.accounts.get(me)); + const account = useOwnAccount(); const notificationCount = useAppSelector((state) => state.notifications.get('unread')); const chatsCount = useAppSelector((state) => state.chats.get('items').reduce((acc: any, curr: any) => acc + Math.min(curr.get('unread', 0), 1), 0)); - const dashboardCount = useAppSelector((state) => state.admin.openReports.count() + state.admin.awaitingApproval.count()); + const followRequestsCount = useAppSelector((state) => state.user_lists.getIn(['follow_requests', 'items'], ImmutableOrderedSet()).count()); + // const dashboardCount = useAppSelector((state) => state.admin.openReports.count() + state.admin.awaitingApproval.count()); - const baseURL = getBaseURL(ImmutableMap(account)); + const baseURL = account ? getBaseURL(ImmutableMap(account)) : ''; const features = getFeatures(instance); + const makeMenu = (): Menu => { + const menu: Menu = []; + + if (account?.locked || followRequestsCount > 0) { + menu.push({ + to: '/follow_requests', + text: , + icon: require('@tabler/icons/icons/user-plus.svg'), + // TODO: let menu items have a counter + // count: followRequestsCount, + }); + } + + if (features.bookmarks) { + menu.push({ + to: '/bookmarks', + text: , + icon: require('@tabler/icons/icons/bookmark.svg'), + }); + } + + if (features.lists) { + menu.push({ + to: '/lists', + text: , + icon: require('@tabler/icons/icons/list.svg'), + }); + } + + if (account && instance.invites_enabled) { + menu.push({ + to: `${baseURL}/invites`, + icon: require('@tabler/icons/icons/mailbox.svg'), + text: , + }); + } + + if (settings.get('isDeveloper')) { + menu.push({ + to: '/developers', + icon: require('@tabler/icons/icons/code.svg'), + text: , + }); + } + + if (account && account.staff) { + menu.push({ + to: '/admin', + icon: require('@tabler/icons/icons/dashboard.svg'), + text: , + // TODO: let menu items have a counter + // count: dashboardCount, + }); + } + + if (features.localTimeline || features.publicTimeline) { + menu.push(null); + } + + if (features.localTimeline) { + menu.push({ + to: '/timeline/local', + icon: features.federating ? require('@tabler/icons/icons/users.svg') : require('@tabler/icons/icons/world.svg'), + text: features.federating ? instance.title : , + }); + } + + if (features.localTimeline && features.federating) { + menu.push({ + to: '/timeline/fediverse', + icon: require('icons/fediverse.svg'), + text: , + }); + } + + return menu; + }; + + const menu = makeMenu(); + return (
@@ -71,49 +154,13 @@ const SidebarNavigation = () => { ) )} - {(account && account.staff) && ( - } - count={dashboardCount} - /> - )} - - {(account && instance.invites_enabled) && ( - } - /> - )} - - {(settings.get('isDeveloper')) && ( - } - /> - )} - - {(features.localTimeline || features.publicTimeline) && ( -
- )} - - {features.localTimeline && ( - } - /> - )} - - {(features.publicTimeline && features.federating) && ( - } - /> + {menu.length > 0 && ( + + } + /> + )}
diff --git a/app/soapbox/containers/soapbox.js b/app/soapbox/containers/soapbox.js index c1a7db84e..ad14c6e7e 100644 --- a/app/soapbox/containers/soapbox.js +++ b/app/soapbox/containers/soapbox.js @@ -86,6 +86,7 @@ const mapStateToProps = (state) => { locale: validLocale(locale) ? locale : 'en', themeCss: generateThemeCss(soapboxConfig), brandColor: soapboxConfig.get('brandColor'), + appleAppId: soapboxConfig.get('appleAppId'), themeMode: settings.get('themeMode'), singleUserMode, }; @@ -108,6 +109,7 @@ class SoapboxMount extends React.PureComponent { themeCss: PropTypes.string, themeMode: PropTypes.string, brandColor: PropTypes.string, + appleAppId: PropTypes.string, dispatch: PropTypes.func, singleUserMode: PropTypes.bool, }; @@ -171,6 +173,10 @@ class SoapboxMount extends React.PureComponent { {themeCss && } + + {this.props.appleAppId && ( + + )} diff --git a/app/soapbox/features/developers/developers_menu.tsx b/app/soapbox/features/developers/developers_menu.tsx index 6cabd36a0..545c0a998 100644 --- a/app/soapbox/features/developers/developers_menu.tsx +++ b/app/soapbox/features/developers/developers_menu.tsx @@ -47,6 +47,14 @@ const Developers = () => { + + + + + + + + diff --git a/app/soapbox/features/public_layout/components/pre_header.js b/app/soapbox/features/public_layout/components/pre_header.js deleted file mode 100644 index 5130f6804..000000000 --- a/app/soapbox/features/public_layout/components/pre_header.js +++ /dev/null @@ -1,59 +0,0 @@ -import * as React from 'react'; -import { defineMessages, useIntl } from 'react-intl'; -import { Link } from 'react-router-dom'; - -import { IconButton } from 'soapbox/components/ui'; -import { useAppSelector } from 'soapbox/hooks'; - -const messages = defineMessages({ - close: { id: 'pre_header.close', defaultMessage: 'Close' }, -}); - -export default () => { - const intl = useIntl(); - - const [hidden, setHidden] = React.useState(false); - const siteTitle = useAppSelector((state) => state.instance.title); - - const handleClose = () => { - localStorage.setItem('soapbox:welcome-banner', '0'); - setHidden(true); - }; - - React.useEffect(() => { - const shouldBeHidden = localStorage.getItem('soapbox:welcome-banner') === '0'; - - setHidden(shouldBeHidden); - }, []); - - if (hidden) { - return null; - } - - return ( -
-
-
-

- Welcome to {siteTitle} -

- - - Learn More - -
- -
- -
-
-
- ); -}; diff --git a/app/soapbox/features/test_timeline/index.tsx b/app/soapbox/features/test_timeline/index.tsx new file mode 100644 index 000000000..ace235b8f --- /dev/null +++ b/app/soapbox/features/test_timeline/index.tsx @@ -0,0 +1,53 @@ +import React from 'react'; +import { defineMessages, useIntl, FormattedMessage } from 'react-intl'; +import { useDispatch } from 'react-redux'; + +import { importFetchedStatuses } from 'soapbox/actions/importer'; +import { expandTimelineSuccess } from 'soapbox/actions/timelines'; +import SubNavigation from 'soapbox/components/sub_navigation'; + +import { Column } from '../../components/ui'; +import StatusListContainer from '../ui/containers/status_list_container'; + +const messages = defineMessages({ + title: { id: 'column.test', defaultMessage: 'Test timeline' }, +}); + +/** + * List of mock statuses to display in the timeline. + * These get embedded into the build, but only in this chunk, so it's okay. + */ +const MOCK_STATUSES: any[] = [ + require('soapbox/__fixtures__/pleroma-status-with-poll.json'), + require('soapbox/__fixtures__/pleroma-status-vertical-video-without-metadata.json'), + require('soapbox/__fixtures__/pleroma-status-with-poll-with-emojis.json'), + require('soapbox/__fixtures__/pleroma-quote-of-quote-post.json'), + require('soapbox/__fixtures__/truthsocial-status-with-external-video.json'), +]; + +const timelineId = 'test'; +const onlyMedia = false; + +const TestTimeline: React.FC = () => { + const intl = useIntl(); + const dispatch = useDispatch(); + + React.useEffect(() => { + dispatch(importFetchedStatuses(MOCK_STATUSES)); + dispatch(expandTimelineSuccess(timelineId, MOCK_STATUSES, null, false, false, false)); + }, []); + + return ( + + + } + divideType='space' + /> + + ); +}; + +export default TestTimeline; diff --git a/app/soapbox/features/ui/components/features_panel.js b/app/soapbox/features/ui/components/features_panel.js deleted file mode 100644 index f54ba36b5..000000000 --- a/app/soapbox/features/ui/components/features_panel.js +++ /dev/null @@ -1,74 +0,0 @@ -import { OrderedSet as ImmutableOrderedSet } from 'immutable'; -import PropTypes from 'prop-types'; -import React from 'react'; -import { injectIntl, defineMessages } from 'react-intl'; -import { connect } from 'react-redux'; -import { NavLink } from 'react-router-dom'; - -import Icon from 'soapbox/components/icon'; -import IconWithCounter from 'soapbox/components/icon_with_counter'; -import { getFeatures } from 'soapbox/utils/features'; - -const messages = defineMessages({ - edit_profile: { id: 'account.edit_profile', defaultMessage: 'Edit Profile' }, - preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' }, - security: { id: 'navigation_bar.security', defaultMessage: 'Security' }, - lists: { id: 'column.lists', defaultMessage: 'Lists' }, - bookmarks: { id: 'column.bookmarks', defaultMessage: 'Bookmarks' }, - follow_requests: { id: 'navigation_bar.follow_requests', defaultMessage: 'Follow requests' }, -}); - -const mapStateToProps = state => { - const me = state.get('me'); - - const instance = state.get('instance'); - const features = getFeatures(instance); - - return { - isLocked: state.getIn(['accounts', me, 'locked']), - followRequestsCount: state.getIn(['user_lists', 'follow_requests', 'items'], ImmutableOrderedSet()).count(), - features, - }; -}; - -export default @connect(mapStateToProps) -@injectIntl -class FeaturesPanel extends React.PureComponent { - - static propTypes = { - intl: PropTypes.object.isRequired, - isLocked: PropTypes.bool, - followRequestsCount: PropTypes.number, - features: PropTypes.object.isRequired, - }; - - render() { - const { intl, isLocked, followRequestsCount, features } = this.props; - - return ( -
-
- {(isLocked || followRequestsCount > 0) && - - {intl.formatMessage(messages.follow_requests)} - } - - {features.bookmarks && ( - - - {intl.formatMessage(messages.bookmarks)} - - )} - - {features.lists && ( - - - {intl.formatMessage(messages.lists)} - - )} -
-
- ); - } - -} diff --git a/app/soapbox/features/ui/index.js b/app/soapbox/features/ui/index.js index bc50ca114..af27d453a 100644 --- a/app/soapbox/features/ui/index.js +++ b/app/soapbox/features/ui/index.js @@ -41,7 +41,6 @@ import { expandNotifications } from '../../actions/notifications'; import { fetchScheduledStatuses } from '../../actions/scheduled_statuses'; import { connectUserStream } from '../../actions/streaming'; import { expandHomeTimeline } from '../../actions/timelines'; -// import PreHeader from '../../features/public_layout/components/pre_header'; // import GroupSidebarPanel from '../groups/sidebar_panel'; import BackgroundShapes from './components/background_shapes'; @@ -120,6 +119,7 @@ import { Developers, CreateApp, SettingsStore, + TestTimeline, } from './util/async-components'; import { WrappedRoute } from './util/react_router_helpers'; @@ -341,6 +341,7 @@ class SwitchingColumnsArea extends React.PureComponent { + @@ -727,7 +728,6 @@ class UI extends React.PureComponent {
- {/* */} diff --git a/app/soapbox/features/ui/util/async-components.js b/app/soapbox/features/ui/util/async-components.js index 69d3d401d..9f031adbc 100644 --- a/app/soapbox/features/ui/util/async-components.js +++ b/app/soapbox/features/ui/util/async-components.js @@ -338,10 +338,6 @@ export function UserPanel() { return import(/* webpackChunkName: "features/ui" */'../components/user_panel'); } -export function FeaturesPanel() { - return import(/* webpackChunkName: "features/ui" */'../components/features_panel'); -} - export function PromoPanel() { return import(/* webpackChunkName: "features/ui" */'../components/promo_panel'); } @@ -486,6 +482,10 @@ export function SettingsStore() { return import(/* webpackChunkName: "features/developers" */'../../developers/settings_store'); } +export function TestTimeline() { + return import(/* webpackChunkName: "features/test_timeline" */'../../test_timeline'); +} + export function DatePicker() { return import(/* webpackChunkName: "date_picker" */'../../birthdays/date_picker'); } diff --git a/app/soapbox/normalizers/soapbox/soapbox_config.ts b/app/soapbox/normalizers/soapbox/soapbox_config.ts index a56c5e0ce..f2e8cfef8 100644 --- a/app/soapbox/normalizers/soapbox/soapbox_config.ts +++ b/app/soapbox/normalizers/soapbox/soapbox_config.ts @@ -75,6 +75,7 @@ export const CryptoAddressRecord = ImmutableRecord({ }); export const SoapboxConfigRecord = ImmutableRecord({ + appleAppId: null, logo: '', banner: '', brandColor: '', // Empty diff --git a/app/soapbox/pages/remote_instance_page.js b/app/soapbox/pages/remote_instance_page.js index c922be6a5..2ff5e7c4f 100644 --- a/app/soapbox/pages/remote_instance_page.js +++ b/app/soapbox/pages/remote_instance_page.js @@ -7,7 +7,6 @@ import LinkFooter from 'soapbox/features/ui/components/link_footer'; import BundleContainer from 'soapbox/features/ui/containers/bundle_container'; import { PromoPanel, - FeaturesPanel, InstanceInfoPanel, InstanceModerationPanel, } from 'soapbox/features/ui/util/async-components'; @@ -30,7 +29,7 @@ export default @connect(mapStateToProps) class RemoteInstancePage extends ImmutablePureComponent { render() { - const { me, children, params: { instance: host }, disclosed, isAdmin } = this.props; + const { children, params: { instance: host }, disclosed, isAdmin } = this.props; return ( @@ -43,11 +42,6 @@ class RemoteInstancePage extends ImmutablePureComponent { - {me && ( - - {Component => } - - )} {Component => }