web/front/UserPage.tsx
import { useRouter } from 'next/router'
import Link from 'next/link'
import React from 'react'
import pluralize from 'pluralize'
import { formatDate, formatNumberApprox } from 'ourbigbook'
import {
ArticleIcon,
ChildrenIcon,
CommentIcon,
IncomingIcon,
DiscussionIcon,
LikeIcon,
LargestIcon,
ListIcon,
MyHead,
SettingsIcon,
StarIcon,
UserIcon,
TagIcon,
orderToPageTitle,
useEEdit,
TimeIcon,
TreeIcon,
FollowIcon,
ArrowRightIcon,
ArrowLeftIcon,
AlphabeticalOrderTabTitle,
AnnounceIcon,
LockIcon,
DirectoryIcon,
FileIcon,
} from 'front'
import ArticleList from 'front/ArticleList'
import CommentList from 'front/CommentList'
import { cant } from 'front/cant'
import config from 'front/config'
import CustomLink from 'front/CustomLink'
import CustomImage from 'front/CustomImage'
import LoadingSpinner from 'front/LoadingSpinner'
import LogoutButton from 'front/LogoutButton'
import FollowUserButton from 'front/FollowUserButton'
import { DisplayAndUsername, displayAndUsernameText } from 'front/user'
import routes from 'front/routes'
import Article from 'front/Article'
import UserList from 'front/UserList'
import { DirectoryEntries, FileList, UnlistedFilesNotice } from 'front/DirPage'
import { UploadEntryType, UploadIndexType } from 'front/types/UploadType'
import { UploadDirectoryEntryType } from 'front/types/UploadDirectoryType'
import { ArticleType, ArticleLinkType } from 'front/types/ArticleType'
import { CommentType } from 'front/types/CommentType'
import { CommonPropsType } from 'front/types/CommonPropsType'
import { IssueType } from 'front/types/IssueType'
import { TopicType } from 'front/types/TopicType'
import { UserType } from 'front/types/UserType'
import LockUserButton from 'front/LockUserButton'
import BlacklistSignupIpButton from './BlacklistSignupIpButton'
import SpammerButton from './SpammerButton'
import UnlistAllContentButton from './UnlistAllContentButton'
export interface UserPageProps extends CommonPropsType {
ancestors?: ArticleLinkType[];
article?: ArticleType & IssueType;
articles?: (ArticleType & IssueType & TopicType)[];
articlesCount?: number;
articlesInSamePage?: ArticleType[];
articlesInSamePageCount?: number;
articlesInSamePageForToc?: ArticleType[];
articlesInSamePageForTocCount?: number;
commentCountByLoggedInUser?: number;
comments?: CommentType[];
commentsCount?: number;
hasListedContent?: boolean;
hasLocked?: boolean;
hasUnlisted?: boolean;
incomingLinks?: ArticleLinkType[];
issuesCount?: number;
itemType?: 'article' | 'comment' | 'discussion' | 'file' | 'like'| 'topic' | 'user';
files?: UploadIndexType[];
filesCount?: number;
childDirectories?: UploadDirectoryEntryType[];
childFiles?: UploadEntryType[];
latestIssues?: IssueType[];
list: boolean,
locked?: boolean,
order: string;
orderAscDesc: string;
page: number;
// For when listed articles are relative to another article,
// e.g. tagged by, incoming links or children.
parentArticle?: ArticleType;
// Always false if its not admin querying.
signupIpIsBlacklisted: boolean;
synonymLinks?: ArticleLinkType[];
tagged?: ArticleLinkType[];
topIssues?: IssueType[];
totalArticlesByUser: number;
totalCommentsByUser: number;
totalDiscussionsByUser: number;
user: UserType;
users?: UserType[];
usersCount?: number;
what:
'followed' |
'followed-articles' |
'followed-discussions' |
'follows' |
'home' |
'liked' |
'liked-discussions' |
'likes' |
'likes-discussions' |
'user-articles' |
'user-child-articles' |
'user-incoming-articles' |
'user-tagged-articles' |
'user-comments' |
'user-files' |
'user-files-tree' |
'user-issues'
;
}
export default function UserPage({
article,
articles,
articlesCount,
articlesInSamePage,
articlesInSamePageCount,
articlesInSamePageForToc,
articlesInSamePageForTocCount,
ancestors,
comments,
commentsCount,
files,
filesCount,
childDirectories,
childFiles,
commentCountByLoggedInUser,
hasListedContent,
hasLocked,
hasUnlisted,
incomingLinks,
issuesCount,
itemType,
latestIssues,
list,
locked,
loggedInUser,
order,
orderAscDesc,
page,
parentArticle,
signupIpIsBlacklisted,
synonymLinks,
tagged,
topIssues,
totalArticlesByUser,
totalCommentsByUser,
totalDiscussionsByUser,
user,
users,
usersCount,
what,
}: UserPageProps) {
const router = useRouter();
const username = user?.username
const isCurrentUser = loggedInUser && username === loggedInUser?.username
const canEdit = loggedInUser && loggedInUser?.username === username
useEEdit(canEdit, article?.slug)
// Following state.
const [following, setFollowing] = React.useState(false)
const [followerCount, setFollowerCount] = React.useState(user?.followerCount)
const [moderationLocked, setModerationLocked] = React.useState(user.locked)
const [moderationIpBlacklisted, setModerationIpBlacklisted] = React.useState(signupIpIsBlacklisted)
const [moderationContentUnlisted, setModerationContentUnlisted] = React.useState(!hasListedContent)
React.useEffect(() => {
setFollowing(user?.following)
setFollowerCount(user?.followerCount)
}, [
user?.following,
user?.followerCount,
])
React.useEffect(() => {
setModerationLocked(user.locked)
setModerationIpBlacklisted(signupIpIsBlacklisted)
setModerationContentUnlisted(!hasListedContent)
}, [hasListedContent, signupIpIsBlacklisted, user.locked])
// title
const displayAndUsername = displayAndUsernameText(user)
let title
let parentArticlePref
if (parentArticle) {
switch (what) {
case 'user-child-articles':
parentArticlePref = 'Children'
break
case 'user-incoming-articles':
parentArticlePref = 'Incoming links'
break
case 'user-tagged-articles':
parentArticlePref = 'Tagged'
break
}
title = `${parentArticlePref} - ${parentArticle.titleRenderPlaintext} - ${displayAndUsername}`
} else {
let title2
switch (what) {
case 'user-files':
title2 = `${order === 'size' ? 'Largest' : orderToPageTitle(order)} files`
break
case 'user-files-tree':
title2 = 'Files'
break
case 'followed':
title2 = 'Newly followed by'
break;
case 'follows':
title2 = 'New follows'
break;
case 'liked':
title2 = `New received likes`
break;
case 'likes':
case 'likes-discussions':
title2 = `Newly liked ${pluralize(itemType)}`
break;
case 'followed-articles':
case 'followed-discussions':
title2 = `Newly followed ${pluralize(itemType)}`
break;
default:
if (itemType) {
title2 = `${orderToPageTitle(order)} ${pluralize(itemType)}`
}
}
title = `${displayAndUsername}${title2 ? ` - ${title2}` : ''}`
}
const handleShortFragmentSkipOnce = React.useRef(false)
if (router.isFallback) { return <LoadingSpinner />; }
const statuses = []
if (user.admin) {
statuses.push(<a href={`${config.docsAdminUrl}`}><StarIcon /> Admin</a>)
}
if (user.locked) {
statuses.push(<a href={`${config.docsAccountLockingUrl}`}><LockIcon /> Locked</a>)
}
return <>
<MyHead previewImage={user.effectiveImage} title={title} />
<div className="profile-page">
<div className="user-info content-not-ourbigbook">
<div className="name-and-image">
<div className="no-image">
<h1>
<a href={routes.user(username)}>
<DisplayAndUsername user={user} showParenthesis={false} />
</a>
</h1>
{statuses.length !== 0 && <div className="statuses">{
statuses.map((status, i) => <div className="h2 inline" key={i}>{status}</div>)}
</div>}
<div className="user-actions">
<span className="pill"><TimeIcon /> Joined {formatDate(user.createdAt)}</span>
<FollowUserButton {...{ loggedInUser, user, showUsername: false }}/>
<CustomLink className="btn" href={routes.issueNew(`${username}`)}>
<DiscussionIcon /> Message
</CustomLink>
{!cant.viewUserSettings(loggedInUser, user) &&
<CustomLink
href={routes.userEdit(username)}
className="btn btn-sm btn-outline-secondary action-btn"
>
<SettingsIcon /> Settings
</CustomLink>
}
{!cant.setUserLimits(loggedInUser, user) &&
<SpammerButton {...{
username,
on: moderationLocked && moderationIpBlacklisted && moderationContentUnlisted,
onSuccess: () => {
setModerationLocked(true)
setModerationIpBlacklisted(true)
setModerationContentUnlisted(true)
},
}} />
}
{!cant.setUserLimits(loggedInUser, user) &&
<LockUserButton {...{
username,
on: moderationLocked,
onChange: setModerationLocked,
}} />
}
{(!cant.setUserLimits(loggedInUser, user) && user.ip) &&
<BlacklistSignupIpButton {...{
ip: user.ip,
on: moderationIpBlacklisted,
onChange: setModerationIpBlacklisted,
}} />
}
{!cant.setUserLimits(loggedInUser, user) &&
<UnlistAllContentButton {...{ username, on: moderationContentUnlisted }} />
}
{isCurrentUser &&
<LogoutButton />
}
</div>
</div>
<a href={routes.user(username)}>
<CustomImage
src={user.effectiveImage}
alt="User's profile image"
className="user-img"
/>
</a>
</div>
{parentArticle
? <div className="parent-article">
<h2>{
what === 'user-child-articles' ? <ChildrenIcon /> :
what === 'user-incoming-articles' ? <IncomingIcon /> :
what === 'user-tagged-articles' ? <TagIcon /> :
(() => { throw new Error("TODO shit's bugged") })()
}{' '}{parentArticlePref}:
{' '}
<Link href={routes.article(parentArticle.slug)}>
<span
className="ourbigbook-title title"
dangerouslySetInnerHTML={{ __html: parentArticle.titleRenderWithScope }}
/>
</Link>
</h2>
</div>
: <>
<div className="tab-list">
<CustomLink
href={routes.userArticles(username, { sort: 'created' })}
className={`tab-item${itemType === 'article' ? ' active' : ''}`}
>
<ArticleIcon /> Articles<span className="mobile-hide"> ({ formatNumberApprox(totalArticlesByUser) })</span>
</CustomLink>
<CustomLink
href={routes.userIssues(username, { sort: 'created' })}
className={`tab-item${itemType === 'discussion' ? ' active' : ''}`}
>
<DiscussionIcon /> Discussions<span className="mobile-hide"> ({ formatNumberApprox(totalDiscussionsByUser) })</span>
</CustomLink>
<CustomLink
href={routes.userComments(username, { sort: 'created' })}
className={`tab-item${itemType === 'comment' ? ' active' : ''}`}
>
<CommentIcon /> Comments<span className="mobile-hide"> ({ formatNumberApprox(totalCommentsByUser) })</span>
</CustomLink>
<CustomLink
href={routes.userFollows(username)}
className={`tab-item${itemType === 'user' ? ' active' : ''}`}
>
<UserIcon /> Follows
</CustomLink>
<CustomLink
href={routes.userLiked(username)}
className={`tab-item${what === 'liked' ? ' active' : ''}`}
>
<LikeIcon /> Received<span className="mobile-hide"> likes</span>
</CustomLink>
<CustomLink
href={routes.dir(username)}
className={`tab-item${itemType === 'file' ? ' active' : ''}`}
>
<DirectoryIcon /> Files<span className="mobile-hide"> ({ formatNumberApprox(user.fileCount) })</span>
</CustomLink>
{false &&
// TODO https://github.com/ourbigbook/ourbigbook/issues/313
<CustomLink
href={routes.userLikedDiscussions(username)}
className={`tab-item${what === 'liked-discussions' ? ' active' : ''}`}
>
<LikeIcon /><DiscussionIcon /> Received<span className="mobile-hide"> discussion likes</span>
</CustomLink>
}
</div>
<div className="tab-list">
{itemType === 'file' && <>
<CustomLink href={routes.dir(username)} className={`tab-item${what === 'user-files-tree' ? ' active' : ''}`}>
<TreeIcon /> Tree
</CustomLink>
<CustomLink href={routes.userFiles(username)} className={`tab-item${what === 'user-files' ? ' active' : ''}`}>
<ListIcon /> List
</CustomLink>
</>}
{itemType === 'article' && <>
<CustomLink
href={routes.userArticles(username, { sort: 'created' })}
className={`tab-item${what === 'user-articles' && order === 'createdAt' ? ' active' : ''}`}
>
<TimeIcon /> New
</CustomLink>
<CustomLink
href={routes.userArticles(username, { sort: 'updated' })}
className={`tab-item${what === 'user-articles' && order === 'updatedAt' ? ' active' : ''}`}
>
<TimeIcon /> Updated
</CustomLink>
<CustomLink
href={routes.userArticles(username, { sort: 'score' })}
className={`tab-item${what === 'user-articles' && order === 'score' ? ' active' : ''}`}
>
<StarIcon /> Top
</CustomLink>
<CustomLink
href={routes.userArticles(username, { sort: 'announced' })}
className={`tab-item${what === 'user-articles' && order === 'announcedAt' ? ' active' : ''}`}
>
<AnnounceIcon /> Announced
</CustomLink>
<CustomLink
className={`tab-item${order === 'topicId' ? ' active' : ''}`}
href={routes.userArticles(username, { sort: 'id' })}
>
<ArticleIcon /> <AlphabeticalOrderTabTitle />
</CustomLink>
<CustomLink
href={routes.userLikes(username)}
className={`tab-item${what === 'likes' ? ' active' : ''}`}
>
<LikeIcon /> Liked
</CustomLink>
<CustomLink
href={routes.userFollowsArticles(username)}
className={`tab-item${what === 'followed-articles' ? ' active' : ''}`}
>
<FollowIcon /> Followed
</CustomLink>
</>}
{itemType === 'discussion' && <>
<CustomLink
href={routes.userIssues(username, { sort: 'created' })}
className={`tab-item${what === 'user-issues' && order === 'createdAt' ? ' active' : ''}`}
>
<TimeIcon /> New
</CustomLink>
<CustomLink
href={routes.userIssues(username, { sort: 'updated' })}
className={`tab-item${what === 'user-issues' && order === 'updatedAt' ? ' active' : ''}`}
>
<TimeIcon /> Updated
</CustomLink>
<CustomLink
href={routes.userIssues(username, { sort: 'score' })}
className={`tab-item${what === 'user-issues' && order === 'score' ? ' active' : ''}`}
>
<StarIcon /> Top
</CustomLink>
<CustomLink
href={routes.userLikesDiscussions(username)}
className={`tab-item${what === 'likes-discussions' ? ' active' : ''}`}
>
<LikeIcon /> Liked
</CustomLink>
<CustomLink
href={routes.userFollowsDiscussions(username)}
className={`tab-item${what === 'followed-discussions' ? ' active' : ''}`}
>
<FollowIcon /> Followed
</CustomLink>
</>}
{itemType === 'user' && <>
<CustomLink
href={routes.userFollows(username)}
className={`tab-item${what === 'follows' ? ' active' : ''}`}
>
<ArrowRightIcon /> Follows
</CustomLink>
<CustomLink
href={routes.userFollowed(username)}
className={`tab-item${what === 'followed' ? ' active' : ''}`}
>
<ArrowLeftIcon /> Followed by
</CustomLink>
</>}
</div>
</>
}
</div>
{what === 'home' &&
<Article {...{
ancestors,
article,
articlesInSamePage,
articlesInSamePageCount,
articlesInSamePageForToc,
articlesInSamePageForTocCount,
comments,
commentCountByLoggedInUser,
handleShortFragmentSkipOnce,
incomingLinks,
isIndex: true,
issuesCount,
latestIssues,
loggedInUser,
synonymLinks,
tagged,
topIssues,
}}/>
}
</div>
{what === 'user-files-tree' && <div className="dir-page content-not-ourbigbook">
<DirectoryEntries author={user} {...{ childDirectories, childFiles }} />
<UnlistedFilesNotice {...{ list, hasUnlisted }} />
</div>}
{what === 'user-files' && <>
<div className="tab-list content-not-ourbigbook">
{[
['created', 'createdAt', 'New'], ['updated', 'updatedAt', 'Updated'], ['size', 'size', 'Largest'],
].map(([sort, column, label]) => <CustomLink key={sort} href={routes.userFiles(username, { sort })}
className={`tab-item${order === column ? ' active' : ''}`}>
{sort === 'size' ? <LargestIcon /> : <TimeIcon />} {label}
</CustomLink>)}
<CustomLink className={`tab-item${order === 'path' ? ' active' : ''}`} href={routes.userFiles(username, { sort: 'path' })}>
<FileIcon /> <AlphabeticalOrderTabTitle />
</CustomLink>
</div>
<FileList {...{ files, filesCount, page, username, list, hasUnlisted }} />
</>}
{(itemType === 'article' || itemType === 'discussion' || itemType === 'like') &&
<ArticleList {...{
articles,
articlesCount,
handleShortFragmentSkipOnce,
hasUnlisted,
itemType,
list,
loggedInUser,
page,
showAuthor: what === 'likes' || what === 'followed-articles',
what,
}}/>
}
{itemType === 'comment' &&
<CommentList {...{
comments,
commentsCount,
hasUnlisted,
list,
loggedInUser,
page,
showAuthor: false,
}}/>
}
{itemType === 'user' &&
<UserList {...{
hasLocked,
locked,
loggedInUser,
page,
users,
usersCount,
}}/>
}
</>
}