mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-01-27 22:49:56 -05:00
Added open reply notification in sidebar functionality (#21014)
no refs Added logic to open the notification in the sidebar when the notification is clicked if it is a reply notification
This commit is contained in:
parent
5903dd7fb4
commit
12710eaefa
2 changed files with 53 additions and 30 deletions
|
@ -1,9 +1,12 @@
|
||||||
|
import NiceModal from '@ebay/nice-modal-react';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import {Button, NoValueLabel} from '@tryghost/admin-x-design-system';
|
||||||
|
import {ObjectProperties} from '@tryghost/admin-x-framework/api/activitypub';
|
||||||
|
|
||||||
import APAvatar, {AvatarBadge} from './global/APAvatar';
|
import APAvatar, {AvatarBadge} from './global/APAvatar';
|
||||||
import ActivityItem from './activities/ActivityItem';
|
import ActivityItem, {type Activity} from './activities/ActivityItem';
|
||||||
|
import ArticleModal from './feed/ArticleModal';
|
||||||
import MainNavigation from './navigation/MainNavigation';
|
import MainNavigation from './navigation/MainNavigation';
|
||||||
import {Button, NoValueLabel} from '@tryghost/admin-x-design-system';
|
|
||||||
|
|
||||||
import getUsername from '../utils/get-username';
|
import getUsername from '../utils/get-username';
|
||||||
import {useBrowseInboxForUser, useBrowseOutboxForUser, useFollowersForUser} from '../MainContent';
|
import {useBrowseInboxForUser, useBrowseOutboxForUser, useFollowersForUser} from '../MainContent';
|
||||||
|
@ -18,28 +21,7 @@ enum ACTVITY_TYPE {
|
||||||
FOLLOW = 'Follow'
|
FOLLOW = 'Follow'
|
||||||
}
|
}
|
||||||
|
|
||||||
type Actor = {
|
const getActivityDescription = (activity: Activity, activityObjectsMap: Map<string, ObjectProperties>): string => {
|
||||||
id: string
|
|
||||||
name: string
|
|
||||||
preferredUsername: string
|
|
||||||
url: string
|
|
||||||
}
|
|
||||||
|
|
||||||
type ActivityObject = {
|
|
||||||
name: string
|
|
||||||
url: string
|
|
||||||
inReplyTo: string | null
|
|
||||||
content: string
|
|
||||||
}
|
|
||||||
|
|
||||||
type Activity = {
|
|
||||||
id: string
|
|
||||||
type: ACTVITY_TYPE
|
|
||||||
object?: ActivityObject
|
|
||||||
actor: Actor
|
|
||||||
}
|
|
||||||
|
|
||||||
const getActivityDescription = (activity: Activity, activityObjectsMap: Map<string, ActivityObject>): string => {
|
|
||||||
switch (activity.type) {
|
switch (activity.type) {
|
||||||
case ACTVITY_TYPE.CREATE:
|
case ACTVITY_TYPE.CREATE:
|
||||||
const object = activityObjectsMap.get(activity.object?.inReplyTo || '');
|
const object = activityObjectsMap.get(activity.object?.inReplyTo || '');
|
||||||
|
@ -76,7 +58,7 @@ const getExtendedDescription = (activity: Activity): JSX.Element | null => {
|
||||||
|
|
||||||
const getActivityUrl = (activity: Activity): string | null => {
|
const getActivityUrl = (activity: Activity): string | null => {
|
||||||
if (activity.object) {
|
if (activity.object) {
|
||||||
return activity.object.url;
|
return activity.object.url || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
@ -119,7 +101,7 @@ const Activities: React.FC<ActivitiesProps> = ({}) => {
|
||||||
// This allows us to quickly look up an object associated with an activity
|
// This allows us to quickly look up an object associated with an activity
|
||||||
// We could just make a http request to get the object, but this is more
|
// We could just make a http request to get the object, but this is more
|
||||||
// efficient seeming though we already have the data in the inbox and outbox
|
// efficient seeming though we already have the data in the inbox and outbox
|
||||||
const activityObjectsMap = new Map<string, ActivityObject>();
|
const activityObjectsMap = new Map<string, ObjectProperties>();
|
||||||
|
|
||||||
outboxActivities.forEach((activity) => {
|
outboxActivities.forEach((activity) => {
|
||||||
if (activity.object) {
|
if (activity.object) {
|
||||||
|
@ -161,6 +143,24 @@ const Activities: React.FC<ActivitiesProps> = ({}) => {
|
||||||
// to show the most recent activities first
|
// to show the most recent activities first
|
||||||
.reverse();
|
.reverse();
|
||||||
|
|
||||||
|
// Create a map of activity comments, grouping them by the parent activity
|
||||||
|
// This allows us to quickly look up all comments for a given activity
|
||||||
|
const commentsMap = new Map<string, Activity[]>();
|
||||||
|
|
||||||
|
for (const activity of activities) {
|
||||||
|
if (activity.type === ACTVITY_TYPE.CREATE && activity.object?.inReplyTo) {
|
||||||
|
const comments = commentsMap.get(activity.object.inReplyTo) ?? [];
|
||||||
|
|
||||||
|
comments.push(activity);
|
||||||
|
|
||||||
|
commentsMap.set(activity.object.inReplyTo, comments.reverse());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getCommentsForObject = (id: string) => {
|
||||||
|
return commentsMap.get(id) ?? [];
|
||||||
|
};
|
||||||
|
|
||||||
// Retrieve followers for the user
|
// Retrieve followers for the user
|
||||||
const {data: followers = []} = useFollowersForUser(user);
|
const {data: followers = []} = useFollowersForUser(user);
|
||||||
|
|
||||||
|
@ -182,7 +182,20 @@ const Activities: React.FC<ActivitiesProps> = ({}) => {
|
||||||
{activities.length > 0 && (
|
{activities.length > 0 && (
|
||||||
<div className='mt-8 flex w-full max-w-[560px] flex-col'>
|
<div className='mt-8 flex w-full max-w-[560px] flex-col'>
|
||||||
{activities?.map(activity => (
|
{activities?.map(activity => (
|
||||||
<ActivityItem key={activity.id} url={getActivityUrl(activity) || getActorUrl(activity)}>
|
<ActivityItem
|
||||||
|
key={activity.id}
|
||||||
|
url={getActivityUrl(activity) || getActorUrl(activity)}
|
||||||
|
onClick={
|
||||||
|
activity.type === ACTVITY_TYPE.CREATE ? () => {
|
||||||
|
NiceModal.show(ArticleModal, {
|
||||||
|
object: activity.object,
|
||||||
|
actor: activity.actor,
|
||||||
|
comments: getCommentsForObject(activity.object.id),
|
||||||
|
allComments: commentsMap
|
||||||
|
});
|
||||||
|
} : undefined
|
||||||
|
}
|
||||||
|
>
|
||||||
<APAvatar author={activity.actor} badge={getActivityBadge(activity)} />
|
<APAvatar author={activity.actor} badge={getActivityBadge(activity)} />
|
||||||
<div className='pt-[2px]'>
|
<div className='pt-[2px]'>
|
||||||
<div className='text-grey-600'>
|
<div className='text-grey-600'>
|
||||||
|
|
|
@ -13,13 +13,18 @@ export type Activity = {
|
||||||
interface ActivityItemProps {
|
interface ActivityItemProps {
|
||||||
children?: ReactNode;
|
children?: ReactNode;
|
||||||
url?: string | null;
|
url?: string | null;
|
||||||
|
onClick?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ActivityItem: React.FC<ActivityItemProps> = ({children, url = null}) => {
|
const ActivityItem: React.FC<ActivityItemProps> = ({children, url = null, onClick}) => {
|
||||||
const childrenArray = React.Children.toArray(children);
|
const childrenArray = React.Children.toArray(children);
|
||||||
|
|
||||||
const Item = (
|
const Item = (
|
||||||
<div className='flex w-full max-w-[560px] flex-col hover:bg-grey-75'>
|
<div className='flex w-full max-w-[560px] flex-col hover:bg-grey-75' onClick={() => {
|
||||||
|
if (!url && onClick) {
|
||||||
|
onClick();
|
||||||
|
}
|
||||||
|
}}>
|
||||||
<div className='flex w-full gap-4 border-b border-grey-100 px-2 py-4'>
|
<div className='flex w-full gap-4 border-b border-grey-100 px-2 py-4'>
|
||||||
{childrenArray[0]}
|
{childrenArray[0]}
|
||||||
{childrenArray[1]}
|
{childrenArray[1]}
|
||||||
|
@ -30,7 +35,12 @@ const ActivityItem: React.FC<ActivityItemProps> = ({children, url = null}) => {
|
||||||
|
|
||||||
if (url) {
|
if (url) {
|
||||||
return (
|
return (
|
||||||
<a href={url} rel='noreferrer' target='_blank'>
|
<a href={url} rel='noreferrer' target='_blank' onClick={(e) => {
|
||||||
|
if (onClick) {
|
||||||
|
e.preventDefault();
|
||||||
|
onClick();
|
||||||
|
}
|
||||||
|
}}>
|
||||||
{Item}
|
{Item}
|
||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
|
|
Loading…
Add table
Reference in a new issue