diff --git a/LICENSE b/LICENSE index b52cfae194..ce0968e726 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2013-2023 Ghost Foundation +Copyright (c) 2013-2024 Ghost Foundation Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation diff --git a/README.md b/README.md index abc822bbe1..7fa794ca2c 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ Twitter

- Downloads + Downloads Latest release @@ -82,7 +82,7 @@ For anyone wishing to contribute to Ghost or to hack/customize core files we rec # Ghost sponsors -We'd like to extend big thanks to our sponsors and partners who make Ghost possible. If you're interested in sponsoring Ghost and supporting the project, please check out our profile on [GitHub sponsors](https://github.com/sponsors/TryGhost) :heart: +A big thanks to our sponsors and partners who make Ghost possible. If you're interested in sponsoring Ghost and supporting the project, please check out our profile on [GitHub sponsors](https://github.com/sponsors/TryGhost) :heart: **[DigitalOcean](https://m.do.co/c/9ff29836d717)** • **[Fastly](https://www.fastly.com/)** @@ -90,12 +90,13 @@ We'd like to extend big thanks to our sponsors and partners who make Ghost possi # Getting help -You can find answers to a huge variety of questions, along with a large community of helpful developers over on the [Ghost forum](https://forum.ghost.org/) - replies are generally very quick. **Ghost(Pro)** customers also have access to 24/7 email support. +Everyone can get help and support from a large community of developers over on the [Ghost forum](https://forum.ghost.org/). **Ghost(Pro)** customers have access to 24/7 email support. -To stay up to date with all the latest news and product updates, make sure you [subscribe to our blog](https://ghost.org/blog/) — or you can always follow us [on Twitter](https://twitter.com/Ghost), if you prefer your updates bite-sized and facetious. :saxophone::turtle: +To stay up to date with all the latest news and product updates, make sure you [subscribe to our changelog newsletter](https://ghost.org/changelog/) — or follow us [on Twitter](https://twitter.com/Ghost), if you prefer your updates bite-sized and facetious. :saxophone::turtle:   # Copyright & license -Copyright (c) 2013-2023 Ghost Foundation - Released under the [MIT license](LICENSE). Ghost and the Ghost Logo are trademarks of Ghost Foundation Ltd. Please see our [trademark policy](https://ghost.org/trademark/) for info on acceptable usage. +Copyright (c) 2013-2024 Ghost Foundation - Released under the [MIT license](LICENSE). +Ghost and the Ghost Logo are trademarks of Ghost Foundation Ltd. Please see our [trademark policy](https://ghost.org/trademark/) for info on acceptable usage. diff --git a/apps/admin-x-activitypub/package.json b/apps/admin-x-activitypub/package.json index 8431203950..1911c4b190 100644 --- a/apps/admin-x-activitypub/package.json +++ b/apps/admin-x-activitypub/package.json @@ -36,10 +36,13 @@ "@testing-library/react": "14.1.0", "@tryghost/admin-x-design-system": "0.0.0", "@tryghost/admin-x-framework": "0.0.0", + "@types/jest": "29.5.12", "@types/react": "18.3.3", "@types/react-dom": "18.3.0", + "jest": "29.7.0", "react": "18.3.1", - "react-dom": "18.3.1" + "react-dom": "18.3.1", + "ts-jest": "29.1.5" }, "nx": { "targets": { diff --git a/apps/admin-x-activitypub/src/assets/images/ap-welcome.png b/apps/admin-x-activitypub/src/assets/images/ap-welcome.png new file mode 100644 index 0000000000..189768bcc5 Binary files /dev/null and b/apps/admin-x-activitypub/src/assets/images/ap-welcome.png differ diff --git a/apps/admin-x-activitypub/src/components/FollowSite.tsx b/apps/admin-x-activitypub/src/components/FollowSite.tsx index c137e9f870..c76fb6c64a 100644 --- a/apps/admin-x-activitypub/src/components/FollowSite.tsx +++ b/apps/admin-x-activitypub/src/components/FollowSite.tsx @@ -1,5 +1,6 @@ import NiceModal from '@ebay/nice-modal-react'; import {Modal, TextField, showToast} from '@tryghost/admin-x-design-system'; +import {useBrowseSite} from '@tryghost/admin-x-framework/api/site'; import {useFollow} from '@tryghost/admin-x-framework/api/activitypub'; import {useQueryClient} from '@tryghost/admin-x-framework'; import {useRouting} from '@tryghost/admin-x-framework/routing'; @@ -16,6 +17,9 @@ const FollowSite = NiceModal.create(() => { const modal = NiceModal.useModal(); const mutation = useFollow(); const client = useQueryClient(); + const site = useBrowseSite(); + const siteData = site.data?.site; + const siteUrl = siteData?.url ?? window.location.origin; // mutation.isPending // mutation.isError @@ -30,8 +34,11 @@ const FollowSite = NiceModal.create(() => { const handleFollow = async () => { try { + const url = new URL(`.ghost/activitypub/actions/follow/${profileName}`, siteUrl); + await fetch(url, { + method: 'POST' + }); // Perform the mutation - await mutation.mutateAsync({username: profileName}); // If successful, set the success state to true // setSuccess(true); showToast({ diff --git a/apps/admin-x-activitypub/src/components/ListIndex.tsx b/apps/admin-x-activitypub/src/components/ListIndex.tsx index 76c2b3e401..9776a53ab5 100644 --- a/apps/admin-x-activitypub/src/components/ListIndex.tsx +++ b/apps/admin-x-activitypub/src/components/ListIndex.tsx @@ -1,9 +1,9 @@ // import NiceModal from '@ebay/nice-modal-react'; -import React, {useState} from 'react'; +import ActivityPubWelcomeImage from '../assets/images/ap-welcome.png'; +import React, {useEffect, useRef, useState} from 'react'; import articleBodyStyles from './articleBodyStyles'; -import getUsername from '../utils/get-username'; import {ActorProperties, ObjectProperties, useBrowseFollowersForUser, useBrowseFollowingForUser, useBrowseInboxForUser} from '@tryghost/admin-x-framework/api/activitypub'; -import {Avatar, Button, Heading, List, ListItem, Page, SettingValue, ViewContainer, ViewTab} from '@tryghost/admin-x-design-system'; +import {Avatar, Button, ButtonGroup, Heading, List, ListItem, Page, SelectOption, SettingValue, ViewContainer, ViewTab} from '@tryghost/admin-x-design-system'; import {useBrowseSite} from '@tryghost/admin-x-framework/api/site'; import {useRouting} from '@tryghost/admin-x-framework/routing'; @@ -16,7 +16,7 @@ const ActivityPubComponent: React.FC = () => { const {updateRoute} = useRouting(); // TODO: Replace with actual user ID - const {data: {orderedItems: activities = []} = {}} = useBrowseInboxForUser('index'); + const {data: {items: activities = []} = {}} = useBrowseInboxForUser('index'); const {data: {totalItems: followingCount = 0} = {}} = useBrowseFollowingForUser('index'); const {data: {totalItems: followersCount = 0} = {}} = useBrowseFollowersForUser('index'); @@ -32,20 +32,30 @@ const ActivityPubComponent: React.FC = () => { setArticleContent(null); }; + const [selectedOption, setSelectedOption] = useState({label: 'Inbox', value: 'inbox'}); + const [selectedTab, setSelectedTab] = useState('inbox'); const tabs: ViewTab[] = [ { id: 'inbox', title: 'Inbox', - contents:
-
    - {activities && activities.slice().reverse().map(activity => ( + contents:
    +
      + {activities && activities.some(activity => activity.type === 'Create' && activity.object.type === 'Article') ? (activities.slice().reverse().map(activity => ( activity.type === 'Create' && activity.object.type === 'Article' &&
    • handleViewContent(activity.object, activity.actor)}> - +
    • - ))} + ))) :
      +
      + Ghost site logos + Welcome to ActivityPub +

      We’re so glad to have you on board! At the moment, you can follow other Ghost sites and enjoy their content right here inside Ghost.

      +

      You can see all of the users on the right—find your favorite ones and give them a follow.

      +
      +
      }
    @@ -53,9 +63,9 @@ const ActivityPubComponent: React.FC = () => { { id: 'activity', title: 'Activity', - contents:
    + contents:
    {activities && activities.slice().reverse().map(activity => ( - activity.type === 'Like' && } id='list-item' title={
    {activity.actor.name} liked your post {activity.object.name}
    }>
    + activity.type === 'Like' && } id='list-item' title={
    {activity.actor.name} liked your post {activity.object.name}
    }>
    ))}
    @@ -64,12 +74,12 @@ const ActivityPubComponent: React.FC = () => { { id: 'likes', title: 'Likes', - contents:
    + contents:
      {activities && activities.slice().reverse().map(activity => ( activity.type === 'Create' && activity.object.type === 'Article' &&
    • handleViewContent(activity.object, activity.actor)}> - +
    • ))}
    @@ -82,6 +92,25 @@ const ActivityPubComponent: React.FC = () => { {!articleContent ? ( { + setSelectedOption({label: 'Inbox', value: 'inbox'}); + } + + }, + { + icon: 'cardview', + size: 'sm', + iconColorClass: selectedOption.value === 'feed' ? 'text-black' : 'text-grey-500', + onClick: () => { + setSelectedOption({label: 'Feed', value: 'feed'}); + } + } + ]} clearBg={true} link outlineOnMobile />]} firstOnPage={true} primaryAction={{ title: 'Follow', @@ -93,9 +122,10 @@ const ActivityPubComponent: React.FC = () => { selectedTab={selectedTab} stickyHeader={true} tabs={tabs} - toolbarBorder={false} - type='page' - onTabChange={setSelectedTab} + title='ActivityPub' + toolbarBorder={true} + type='page' + onTabChange={setSelectedTab} > @@ -108,33 +138,46 @@ const ActivityPubComponent: React.FC = () => { }; const Sidebar: React.FC<{followingCount: number, followersCount: number, updateRoute: (route: string) => void}> = ({followingCount, followersCount, updateRoute}) => ( -
    -
    -
    -
    updateRoute('/view-following')}> - {followingCount} - Following -
    -
    updateRoute('/view-followers')}> - {followersCount} - Followers +
    +
    +
    +
    +
    updateRoute('/view-following')}> + {followingCount} + Following +
    +
    updateRoute('/view-followers')}> + {followersCount} + Followers +
    +
    +
    + Explore +
    + + {}} />} avatar={} detail='829 followers' hideActions={true} title='404 Media' /> + {}} />} avatar={} detail='791 followers' hideActions={true} title='The Browser' /> + {}} />} avatar={} detail='854 followers' hideActions={true} title='Welcome to Hell World' /> + +
    ); const ArticleBody: React.FC<{heading: string, image: string|undefined, html: string}> = ({heading, image, html}) => { - // const dangerouslySetInnerHTML = {__html: html}; - // const cssFile = '../index.css'; const site = useBrowseSite(); const siteData = site.data?.site; + const iframeRef = useRef(null); + const cssContent = articleBodyStyles(siteData?.url.replace(/\/$/, '')); const htmlContent = ` - ${cssContent} + ${cssContent}
    @@ -152,20 +195,29 @@ ${image && `; + useEffect(() => { + const iframe = iframeRef.current; + if (iframe) { + iframe.srcdoc = htmlContent; + } + }, [htmlContent]); + return ( - +
    + +
    ); }; -const ObjectContentDisplay: React.FC<{actor: ActorProperties, object: ObjectProperties }> = ({actor, object}) => { +const ObjectContentDisplay: React.FC<{actor: ActorProperties, object: ObjectProperties, layout: string }> = ({actor, object, layout}) => { const parser = new DOMParser(); const doc = parser.parseFromString(object.content || '', 'text/html'); @@ -183,37 +235,75 @@ const ObjectContentDisplay: React.FC<{actor: ActorProperties, object: ObjectProp setTimeout(() => setIsClicked(false), 300); // Reset the animation class after 300ms }; - return ( - <> - {object && ( -
    -
    - - {actor.name} - {getUsername(actor)} - {timestamp} -
    -
    -
    -
    - {object.name} -
    -

    {plainTextContent}

    -
    -
    +
    +
    +
    + {/*
    */}
    -
    - {/*
    */} -
    - )} - - ); + )} + + ); + } else if (layout === 'inbox') { + return ( + <> + {object && ( +
    +
    + + {actor.name} + {/* {getUsername(actor)} */} + {timestamp} +
    +
    +
    +
    + {object.name} +
    +

    {object.preview?.content}

    +
    +
    +
    + {object.image &&
    + +
    } +
    +
    + {/*
    */} +
    + )} + + ); + } }; const ViewArticle: React.FC = ({object, onBackToList}) => { @@ -241,7 +331,7 @@ const ViewArticle: React.FC = ({object, onBackToList}) => {
    -
    +