mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-01-20 22:42:53 -05:00
Improved grouped notifications UI
ref https://linear.app/ghost/issue/AP-625/implement-notification-grouping-for-follows-and-likes - Improved handling for notification clicks of various types: single follower notification opens that follower in the drawer, multiple followers expands the followers list, liked post opens the article in the wide drawer, liked note opens the note in the narrow drawer - Improved hover and click states for profile names, usernames and avatars. Now it's more obvious what's clickable, and clicking on any of these elements in any context opens that profile in the drawer. - Created a handleProfileClick utility since we're using it in a lot of places. - Removed unnecessary types - Made the HTML structure more semantic
This commit is contained in:
parent
de6efba68a
commit
4597abddff
10 changed files with 113 additions and 102 deletions
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@tryghost/admin-x-activitypub",
|
||||
"version": "0.3.35",
|
||||
"version": "0.3.36",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
|
|
@ -9,14 +9,13 @@ import ArticleModal from './feed/ArticleModal';
|
|||
import MainNavigation from './navigation/MainNavigation';
|
||||
import NotificationItem from './activities/NotificationItem';
|
||||
import Separator from './global/Separator';
|
||||
import ViewProfileModal from './modals/ViewProfileModal';
|
||||
|
||||
import getUsername from '../utils/get-username';
|
||||
import stripHtml from '../utils/strip-html';
|
||||
import truncate from '../utils/truncate';
|
||||
import {GET_ACTIVITIES_QUERY_KEY_NOTIFICATIONS, useActivitiesForUser} from '../hooks/useActivityPubQueries';
|
||||
import {type NotificationType} from './activities/NotificationIcon';
|
||||
import {useRouting} from '@tryghost/admin-x-framework/routing';
|
||||
import {handleProfileClick} from '../utils/handle-profile-click';
|
||||
|
||||
interface ActivitiesProps {}
|
||||
|
||||
|
@ -39,15 +38,15 @@ const getExtendedDescription = (activity: GroupedActivity): JSX.Element | null =
|
|||
if (Boolean(activity.type === ACTIVITY_TYPE.CREATE && activity.object?.inReplyTo)) {
|
||||
return (
|
||||
<div
|
||||
dangerouslySetInnerHTML={{__html: activity.object?.content || ''}}
|
||||
className='mt-1 line-clamp-2 text-pretty text-grey-700'
|
||||
dangerouslySetInnerHTML={{__html: stripHtml(activity.object?.content || '')}}
|
||||
className='ap-note-content mt-1 line-clamp-2 text-pretty text-grey-700'
|
||||
/>
|
||||
);
|
||||
} else if (activity.type === ACTIVITY_TYPE.LIKE && !activity.object?.name && activity.object?.content) {
|
||||
return (
|
||||
<div
|
||||
dangerouslySetInnerHTML={{__html: activity.object?.content || ''}}
|
||||
className='mt-1 line-clamp-2 text-pretty text-grey-700'
|
||||
dangerouslySetInnerHTML={{__html: stripHtml(activity.object?.content || '')}}
|
||||
className='ap-note-content mt-1 line-clamp-2 text-pretty text-grey-700'
|
||||
></div>
|
||||
);
|
||||
}
|
||||
|
@ -114,53 +113,39 @@ const groupActivities = (activities: Activity[]): GroupedActivity[] => {
|
|||
};
|
||||
|
||||
const getGroupDescription = (group: GroupedActivity): JSX.Element => {
|
||||
const actorNames = group.actors.map(actor => actor.name);
|
||||
const [firstActor, secondActor, ...otherActors] = actorNames;
|
||||
const [firstActor, secondActor, ...otherActors] = group.actors;
|
||||
const hasOthers = otherActors.length > 0;
|
||||
|
||||
let actorText = <></>;
|
||||
const actorClass = 'cursor-pointer font-semibold hover:underline';
|
||||
|
||||
const actorText = (
|
||||
<>
|
||||
<span
|
||||
className={actorClass}
|
||||
onClick={e => handleProfileClick(firstActor, e)}
|
||||
>{firstActor.name}</span>
|
||||
{secondActor && (
|
||||
<>
|
||||
{hasOthers ? ', ' : ' and '}
|
||||
<span
|
||||
className={actorClass}
|
||||
onClick={e => handleProfileClick(secondActor, e)}
|
||||
>{secondActor.name}</span>
|
||||
</>
|
||||
)}
|
||||
{hasOthers && ' and others'}
|
||||
</>
|
||||
);
|
||||
|
||||
switch (group.type) {
|
||||
case ACTIVITY_TYPE.FOLLOW:
|
||||
actorText = (
|
||||
<>
|
||||
<span className='font-semibold'>{firstActor}</span>
|
||||
{secondActor && ` and `}
|
||||
{secondActor && <span className='font-semibold'>{secondActor}</span>}
|
||||
{hasOthers && ' and others'}
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{actorText} started following you
|
||||
</>
|
||||
);
|
||||
return <>{actorText} started following you</>;
|
||||
case ACTIVITY_TYPE.LIKE:
|
||||
const postType = group.object?.type === 'Article' ? 'post' : 'note';
|
||||
actorText = (
|
||||
<>
|
||||
<span className='font-semibold'>{firstActor}</span>
|
||||
{secondActor && (
|
||||
<>
|
||||
{hasOthers ? ', ' : ' and '}
|
||||
<span className='font-semibold'>{secondActor}</span>
|
||||
</>
|
||||
)}
|
||||
{hasOthers && ' and others'}
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{actorText} liked your {postType}{' '}
|
||||
<span className='font-semibold'>{group.object?.name || ''}</span>
|
||||
</>
|
||||
);
|
||||
return <>{actorText} liked your post <span className='font-semibold'>{group.object?.name || ''}</span></>;
|
||||
case ACTIVITY_TYPE.CREATE:
|
||||
if (group.object?.inReplyTo && typeof group.object?.inReplyTo !== 'string') {
|
||||
const content = stripHtml(group.object.inReplyTo.name);
|
||||
return <><span className='font-semibold'>{group.actors[0].name}</span> replied to your post <span className='font-semibold'>{truncate(content, 80)}</span></>;
|
||||
return <>{actorText} replied to your post <span className='font-semibold'>{truncate(content, 80)}</span></>;
|
||||
}
|
||||
}
|
||||
return <></>;
|
||||
|
@ -179,8 +164,6 @@ const Activities: React.FC<ActivitiesProps> = ({}) => {
|
|||
|
||||
const maxAvatars = 5;
|
||||
|
||||
const {updateRoute} = useRouting();
|
||||
|
||||
const {getActivitiesQuery} = useActivitiesForUser({
|
||||
handle: user,
|
||||
includeOwn: true,
|
||||
|
@ -190,6 +173,7 @@ const Activities: React.FC<ActivitiesProps> = ({}) => {
|
|||
},
|
||||
key: GET_ACTIVITIES_QUERY_KEY_NOTIFICATIONS
|
||||
});
|
||||
|
||||
const {data, fetchNextPage, hasNextPage, isFetchingNextPage, isLoading} = getActivitiesQuery;
|
||||
const groupedActivities = (data?.pages.flatMap((page) => {
|
||||
const filtered = page.data.filter((activity, index, self) => index === self.findIndex(a => a.id === activity.id));
|
||||
|
@ -222,7 +206,7 @@ const Activities: React.FC<ActivitiesProps> = ({}) => {
|
|||
};
|
||||
}, [hasNextPage, isFetchingNextPage, fetchNextPage]);
|
||||
|
||||
const handleActivityClick = (group: GroupedActivity) => {
|
||||
const handleActivityClick = (group: GroupedActivity, index: number) => {
|
||||
switch (group.type) {
|
||||
case ACTIVITY_TYPE.CREATE:
|
||||
NiceModal.show(ArticleModal, {
|
||||
|
@ -238,16 +222,14 @@ const Activities: React.FC<ActivitiesProps> = ({}) => {
|
|||
activityId: group.id,
|
||||
object: group.object,
|
||||
actor: group.object.attributedTo as ActorProperties,
|
||||
width: 'wide'
|
||||
width: group.object?.type === 'Article' ? 'wide' : 'narrow'
|
||||
});
|
||||
break;
|
||||
case ACTIVITY_TYPE.FOLLOW:
|
||||
if (group.actors.length > 1) {
|
||||
updateRoute('profile');
|
||||
toggleOpen(group.id || `${group.type}_${index}`);
|
||||
} else {
|
||||
NiceModal.show(ViewProfileModal, {
|
||||
profile: getUsername(group.actors[0])
|
||||
});
|
||||
handleProfileClick(group.actors[0]);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
@ -278,7 +260,7 @@ const Activities: React.FC<ActivitiesProps> = ({}) => {
|
|||
<React.Fragment key={group.id || `${group.type}_${index}`}>
|
||||
<NotificationItem
|
||||
className='hover:bg-gray-100'
|
||||
onClick={() => handleActivityClick(group)}
|
||||
onClick={() => handleActivityClick(group, index)}
|
||||
>
|
||||
<NotificationItem.Icon type={getActivityBadge(group)} />
|
||||
<NotificationItem.Avatars>
|
||||
|
@ -301,7 +283,7 @@ const Activities: React.FC<ActivitiesProps> = ({}) => {
|
|||
|
||||
{group.actors.length > 1 && (
|
||||
<Button
|
||||
className={`transition-color flex h-9 items-center rounded-full bg-transparent text-grey-700 ${openStates[group.id || `${group.type}_${index}`] ? 'w-full justify-start pl-1' : '-ml-2 w-9 justify-center'}`}
|
||||
className={`transition-color flex h-9 items-center rounded-full bg-transparent text-grey-700 hover:opacity-60 ${openStates[group.id || `${group.type}_${index}`] ? 'w-full justify-start pl-1' : '-ml-2 w-9 justify-center'}`}
|
||||
hideLabel={!openStates[group.id || `${group.type}_${index}`]}
|
||||
icon='chevron-down'
|
||||
iconColorClass={`w-[12px] h-[12px] ${openStates[group.id || `${group.type}_${index}`] ? 'rotate-180' : ''}`}
|
||||
|
@ -313,11 +295,15 @@ const Activities: React.FC<ActivitiesProps> = ({}) => {
|
|||
}}/>
|
||||
)}
|
||||
</div>
|
||||
<div className={`overflow-hidden transition-all duration-300 ease-in-out ${openStates[group.id || `${group.type}_${index}`] ? 'mb-2 max-h-[500px] opacity-100' : 'max-h-0 opacity-0'}`}>
|
||||
<div className={`overflow-hidden transition-all duration-300 ease-in-out ${openStates[group.id || `${group.type}_${index}`] ? 'mb-2 max-h-[1384px] opacity-100' : 'max-h-0 opacity-0'}`}>
|
||||
{openStates[group.id || `${group.type}_${index}`] && group.actors.length > 1 && (
|
||||
<div className='flex flex-col gap-2 pt-4'>
|
||||
{group.actors.map(actor => (
|
||||
<div key={actor.id} className='flex items-center'>
|
||||
<div
|
||||
key={actor.id}
|
||||
className='flex items-center hover:opacity-80'
|
||||
onClick={e => handleProfileClick(actor, e)}
|
||||
>
|
||||
<APAvatar author={actor} size='xs' />
|
||||
<span className='ml-2 text-base font-semibold'>{actor.name}</span>
|
||||
<span className='ml-1 text-base text-grey-700'>{getUsername(actor)}</span>
|
||||
|
|
|
@ -7,7 +7,6 @@ import NewPostModal from './modals/NewPostModal';
|
|||
import NiceModal from '@ebay/nice-modal-react';
|
||||
import React, {useEffect, useRef} from 'react';
|
||||
import Separator from './global/Separator';
|
||||
import ViewProfileModal from './modals/ViewProfileModal';
|
||||
import getName from '../utils/get-name';
|
||||
import getUsername from '../utils/get-username';
|
||||
import {ActorProperties} from '@tryghost/admin-x-framework/api/activitypub';
|
||||
|
@ -19,6 +18,7 @@ import {
|
|||
useSuggestedProfiles,
|
||||
useUserDataForUser
|
||||
} from '../hooks/useActivityPubQueries';
|
||||
import {handleProfileClick} from '../utils/handle-profile-click';
|
||||
import {handleViewContent} from '../utils/content-handlers';
|
||||
import {useRouting} from '@tryghost/admin-x-framework/routing';
|
||||
|
||||
|
@ -149,9 +149,9 @@ const Inbox: React.FC<InboxProps> = ({layout}) => {
|
|||
return (
|
||||
<React.Fragment key={actor.id}>
|
||||
<li key={actor.id}>
|
||||
<ActivityItem url={actor.url} onClick={() => NiceModal.show(ViewProfileModal, {
|
||||
profile: getUsername(actor)
|
||||
})}>
|
||||
<ActivityItem
|
||||
onClick={() => handleProfileClick(actor)}
|
||||
>
|
||||
<APAvatar author={actor} />
|
||||
<div className='flex min-w-0 flex-col'>
|
||||
<span className='block w-full truncate font-bold text-black'>{getName(actor)}</span>
|
||||
|
|
|
@ -194,7 +194,6 @@ const FollowingTab: React.FC = () => {
|
|||
<React.Fragment key={item.id}>
|
||||
<ActivityItem
|
||||
key={item.id}
|
||||
url={item.url}
|
||||
onClick={() => handleUserClick(item)}
|
||||
>
|
||||
<APAvatar author={item} />
|
||||
|
@ -231,7 +230,6 @@ const FollowersTab: React.FC = () => {
|
|||
<React.Fragment key={item.id}>
|
||||
<ActivityItem
|
||||
key={item.id}
|
||||
url={item.url}
|
||||
onClick={() => handleUserClick(item)}
|
||||
>
|
||||
<APAvatar author={item} />
|
||||
|
|
|
@ -4,7 +4,7 @@ import {Icon} from '@tryghost/admin-x-design-system';
|
|||
export type NotificationType = 'like' | 'follow' | 'reply';
|
||||
|
||||
interface NotificationIconProps {
|
||||
notificationType: 'like' | 'follow' | 'reply';
|
||||
notificationType: NotificationType;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
|
@ -32,7 +32,7 @@ const NotificationIcon: React.FC<NotificationIconProps> = ({notificationType, cl
|
|||
}
|
||||
|
||||
return (
|
||||
<div className={`flex h-10 w-10 items-center justify-center rounded-full ${badgeColor} ${className}`}>
|
||||
<div className={`flex h-10 w-10 items-center justify-center rounded-full ${badgeColor} ${className && className}`}>
|
||||
<Icon colorClass={iconColor} name={icon} size='sm' />
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -20,9 +20,12 @@ interface NotificationItemProps {
|
|||
const NotificationItem = ({children, onClick, url, className}: NotificationItemProps) => {
|
||||
return (
|
||||
<NotificationContext.Provider value={{onClick, url}}>
|
||||
<button className={`relative -mx-4 -my-px grid grid-cols-[auto_1fr] gap-x-3 gap-y-2 rounded-lg p-4 text-left hover:bg-grey-75 ${className}`} type='button' onClick={onClick}>
|
||||
<div className={`relative -mx-4 -my-px grid cursor-pointer grid-cols-[auto_1fr] gap-x-3 gap-y-2 rounded-lg p-4 text-left hover:bg-grey-75 ${className}`}
|
||||
role='button'
|
||||
onClick={onClick}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
</div>
|
||||
</NotificationContext.Provider>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -10,6 +10,7 @@ import getReadingTime from '../../utils/get-reading-time';
|
|||
import getRelativeTimestamp from '../../utils/get-relative-timestamp';
|
||||
import getUsername from '../../utils/get-username';
|
||||
import stripHtml from '../../utils/strip-html';
|
||||
import {handleProfileClick} from '../../utils/handle-profile-click';
|
||||
import {renderTimestamp} from '../../utils/render-timestamp';
|
||||
|
||||
function getAttachment(object: ObjectProperties) {
|
||||
|
@ -241,9 +242,18 @@ const FeedItem: React.FC<FeedItemProps> = ({actor, object, layout, type, comment
|
|||
<div className='relative z-30 flex min-w-0 items-center gap-3'>
|
||||
<APAvatar author={author}/>
|
||||
<div className='flex min-w-0 flex-col gap-0.5'>
|
||||
<span className='min-w-0 truncate break-all font-semibold leading-[normal]' data-test-activity-heading>{author.name}</span>
|
||||
<span className='min-w-0 truncate break-all font-semibold leading-[normal] hover:underline'
|
||||
data-test-activity-heading
|
||||
onClick={e => handleProfileClick(actor, e)}
|
||||
>
|
||||
{author.name}
|
||||
</span>
|
||||
<div className='flex w-full text-grey-700'>
|
||||
<span className='truncate leading-tight'>{getUsername(author)}</span>
|
||||
<span className='truncate leading-tight hover:underline'
|
||||
onClick={e => handleProfileClick(actor, e)}
|
||||
>
|
||||
{getUsername(author)}
|
||||
</span>
|
||||
<div className='ml-1 leading-tight before:mr-1 before:content-["·"]' title={`${timestamp}`}>{renderTimestamp(object)}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -400,7 +410,12 @@ const FeedItem: React.FC<FeedItemProps> = ({actor, object, layout, type, comment
|
|||
<div className='min-w-0'>
|
||||
<div className='z-10 mb-1.5 flex w-full min-w-0 items-center gap-1.5 text-sm group-hover/article:border-transparent'>
|
||||
<APAvatar author={author} size='2xs'/>
|
||||
<span className='min-w-0 truncate break-all font-semibold text-grey-900' title={getUsername(author)} data-test-activity-heading>{author.name}</span>
|
||||
<span className='min-w-0 truncate break-all font-semibold text-grey-900 hover:underline'
|
||||
title={getUsername(author)}
|
||||
data-test-activity-heading
|
||||
onClick={e => handleProfileClick(actor, e)}
|
||||
>{author.name}
|
||||
</span>
|
||||
<span className='shrink-0 whitespace-nowrap text-grey-600 before:mr-1 before:content-["·"]' title={`${timestamp}`}>{getRelativeTimestamp(date)}</span>
|
||||
</div>
|
||||
<div className='flex'>
|
||||
|
|
|
@ -1,40 +1,30 @@
|
|||
import NiceModal from '@ebay/nice-modal-react';
|
||||
import React, {useEffect, useState} from 'react';
|
||||
import ViewProfileModal from '../modals/ViewProfileModal';
|
||||
import clsx from 'clsx';
|
||||
import getUsername from '../../utils/get-username';
|
||||
import {ActorProperties} from '@tryghost/admin-x-framework/api/activitypub';
|
||||
import {Icon} from '@tryghost/admin-x-design-system';
|
||||
|
||||
type AvatarSize = '2xs' | 'xs' | 'sm' | 'lg' | 'notification';
|
||||
export type AvatarBadge = 'user-fill' | 'heart-fill' | 'comment-fill' | undefined;
|
||||
|
||||
interface APAvatarProps {
|
||||
author?: ActorProperties;
|
||||
author: ActorProperties | undefined;
|
||||
size?: AvatarSize;
|
||||
badge?: AvatarBadge;
|
||||
}
|
||||
|
||||
const APAvatar: React.FC<APAvatarProps> = ({author, size, badge}) => {
|
||||
const APAvatar: React.FC<APAvatarProps> = ({author, size}) => {
|
||||
let iconSize = 18;
|
||||
let containerClass = 'shrink-0 items-center justify-center relative z-10 flex';
|
||||
let containerClass = `shrink-0 items-center justify-center relative cursor-pointer z-10 flex ${size === 'lg' ? '' : 'hover:opacity-80'}`;
|
||||
let imageClass = 'z-10 rounded-md w-10 h-10 object-cover';
|
||||
const badgeClass = `w-6 h-6 z-20 rounded-full absolute -bottom-2 -right-[0.6rem] border-2 border-white content-box flex items-center justify-center`;
|
||||
let badgeColor = '';
|
||||
const [iconUrl, setIconUrl] = useState(author?.icon?.url);
|
||||
|
||||
useEffect(() => {
|
||||
setIconUrl(author?.icon?.url);
|
||||
}, [author?.icon?.url]);
|
||||
|
||||
switch (badge) {
|
||||
case 'user-fill':
|
||||
badgeColor = 'bg-blue-500';
|
||||
break;
|
||||
case 'heart-fill':
|
||||
badgeColor = 'bg-red-500';
|
||||
break;
|
||||
case 'comment-fill':
|
||||
badgeColor = 'bg-purple-500';
|
||||
break;
|
||||
if (!author) {
|
||||
return null;
|
||||
}
|
||||
|
||||
switch (size) {
|
||||
|
@ -69,37 +59,42 @@ const APAvatar: React.FC<APAvatarProps> = ({author, size, badge}) => {
|
|||
containerClass = clsx(containerClass, 'bg-grey-100');
|
||||
}
|
||||
|
||||
const BadgeElement = badge && (
|
||||
<div className={clsx(badgeClass, badgeColor)}>
|
||||
<Icon
|
||||
colorClass='text-white'
|
||||
name={badge}
|
||||
size='xs'
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
const onClick = (e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
NiceModal.show(ViewProfileModal, {
|
||||
profile: getUsername(author as ActorProperties)
|
||||
});
|
||||
};
|
||||
|
||||
const title = `${author?.name} ${getUsername(author as ActorProperties)}`;
|
||||
|
||||
if (iconUrl) {
|
||||
return (
|
||||
<a className={containerClass} href={author?.url} rel='noopener noreferrer' target='_blank' title={`${author?.name} ${getUsername(author as ActorProperties)}`}>
|
||||
<div
|
||||
className={containerClass}
|
||||
title={title}
|
||||
onClick={onClick}
|
||||
>
|
||||
<img
|
||||
className={imageClass}
|
||||
src={iconUrl}
|
||||
onError={() => setIconUrl(undefined)}
|
||||
/>
|
||||
{BadgeElement}
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={containerClass} title={author?.name}>
|
||||
<div
|
||||
className={containerClass}
|
||||
title={title}
|
||||
onClick={onClick}
|
||||
>
|
||||
<Icon
|
||||
colorClass='text-grey-600'
|
||||
name='user'
|
||||
size={iconSize}
|
||||
/>
|
||||
{BadgeElement}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -16,6 +16,7 @@ import FollowButton from '../global/FollowButton';
|
|||
import Separator from '../global/Separator';
|
||||
import getName from '../../utils/get-name';
|
||||
import getUsername from '../../utils/get-username';
|
||||
import {handleProfileClick} from '../../utils/handle-profile-click';
|
||||
|
||||
const noop = () => {};
|
||||
|
||||
|
@ -83,7 +84,9 @@ const ActorList: React.FC<ActorListProps> = ({
|
|||
{actors.map(({actor, isFollowing}, index) => {
|
||||
return (
|
||||
<React.Fragment key={actor.id}>
|
||||
<ActivityItem key={actor.id} url={actor.url}>
|
||||
<ActivityItem key={actor.id}
|
||||
onClick={() => handleProfileClick(actor)}
|
||||
>
|
||||
<APAvatar author={actor} />
|
||||
<div>
|
||||
<div className='text-grey-600'>
|
||||
|
|
11
apps/admin-x-activitypub/src/utils/handle-profile-click.ts
Normal file
11
apps/admin-x-activitypub/src/utils/handle-profile-click.ts
Normal file
|
@ -0,0 +1,11 @@
|
|||
import NiceModal from '@ebay/nice-modal-react';
|
||||
import ViewProfileModal from '../components/modals/ViewProfileModal';
|
||||
import getUsername from './get-username';
|
||||
import {ActorProperties} from '@tryghost/admin-x-framework/api/activitypub';
|
||||
|
||||
export const handleProfileClick = (actor: ActorProperties, e?: React.MouseEvent) => {
|
||||
e?.stopPropagation();
|
||||
NiceModal.show(ViewProfileModal, {
|
||||
profile: getUsername(actor)
|
||||
});
|
||||
};
|
Loading…
Add table
Reference in a new issue