From c976850c456c9402809713135b7aad3725485b11 Mon Sep 17 00:00:00 2001 From: Sam Date: Fri, 29 Mar 2024 10:57:17 +0200 Subject: [PATCH] Add chat link and unread count --- src/components/activity/AccountActivity.tsx | 32 +++--------- src/components/activity/style.module.sass | 14 +---- src/components/chat/ChatButton.tsx | 31 +++++++++++ src/components/chat/ChatFloatingModal.tsx | 2 +- src/components/chat/ChatIframe.module.sass | 37 +++++++++++++- .../chat/ChatLinkButtonWithCounter.tsx | 51 +++++++++++++++++++ src/components/chat/CreateChatModal.tsx | 9 ---- src/components/chat/UnhideChatButton.tsx | 46 +++++++++++++++++ src/components/spaces/ViewSpace.tsx | 38 ++++++-------- src/components/utils/HiddenButton/index.tsx | 23 +++++++-- 10 files changed, 209 insertions(+), 74 deletions(-) create mode 100644 src/components/chat/ChatButton.tsx create mode 100644 src/components/chat/ChatLinkButtonWithCounter.tsx create mode 100644 src/components/chat/UnhideChatButton.tsx diff --git a/src/components/activity/AccountActivity.tsx b/src/components/activity/AccountActivity.tsx index da1dc76d5..4560af83e 100644 --- a/src/components/activity/AccountActivity.tsx +++ b/src/components/activity/AccountActivity.tsx @@ -1,5 +1,5 @@ import { PostWithSomeDetails, SpaceData } from '@subsocial/api/types' -import { Button, Tabs } from 'antd' +import { Tabs } from 'antd' import clsx from 'clsx' import { useRouter } from 'next/router' import { useEffect, useState } from 'react' @@ -12,19 +12,14 @@ import { // useGetFollowActivities, useGetPostActivities, } from 'src/graphql/hooks' -import { - useFetchPosts, - useSelectPost, - useSelectSpace, - useSetChatEntityConfig, - useSetChatOpen, -} from 'src/rtk/app/hooks' +import { useFetchPosts, useSelectPost, useSelectSpace } from 'src/rtk/app/hooks' import { useIsMyAddress } from '../auth/MyAccountsContext' import WriteSomething from '../posts/WriteSomething' // import { PostPreviewsOnSpace } from '../spaces/helpers' import { Loading } from '../utils' import { createLoadMorePosts, FeedActivities } from './FeedActivities' // import { createLoadMoreActivities, NotifActivities } from './Notifications' +import ChatLinkButtonWithCounter from '../chat/ChatLinkButtonWithCounter' import { OnchainAccountActivity } from './OnchainAccountActivity' import { SpaceActivities } from './SpaceActivities' import styles from './style.module.sass' @@ -105,6 +100,7 @@ const CommentActivities = (props: BaseActivityProps) => { const PostActivities = (props: BaseActivityProps) => { const getPostActivities = useGetPostActivities() const loadMorePosts = createLoadMorePosts(getPostActivities) + return ( (initialActiveTab) const [counts, setCounts] = useState() - const setChatConfig = useSetChatEntityConfig() - const setChatOpen = useSetChatOpen() const space = useSelectSpace(spaceId) const chatId = (space?.content?.chats as any[])?.[0]?.id as string | undefined - console.log('ChatId', chatId, space?.content) - useFetchPosts(chatId ? [chatId] : []) const post = useSelectPost(chatId) @@ -213,20 +205,8 @@ const OffchainAccountActivity = ({ renderTabBar={(props, DefaultTabBar) => { return (
- {chatId && post && ( - + {chatId && post && !post.post.struct.hidden && ( + )}
diff --git a/src/components/activity/style.module.sass b/src/components/activity/style.module.sass index 1a6cb22f4..e8067260b 100644 --- a/src/components/activity/style.module.sass +++ b/src/components/activity/style.module.sass @@ -21,18 +21,8 @@ :global(.ant-tabs-nav) margin: 0 !important - :global(.ant-tabs-nav) &::before border-bottom: none !important - .ChatButton - position: relative - display: inline-flex - align-items: center - margin: 0 32px 0 0 - padding: 12px 0 - font-size: 14px - background: transparent - border: 0 - outline: none - cursor: pointer + :global(.ant-tabs-tab) + margin: 0 19px 0 0 !important diff --git a/src/components/chat/ChatButton.tsx b/src/components/chat/ChatButton.tsx new file mode 100644 index 000000000..bde3905d3 --- /dev/null +++ b/src/components/chat/ChatButton.tsx @@ -0,0 +1,31 @@ +import { isEmptyArray } from '@subsocial/utils' +import { useSelectPost, useSelectSpace } from 'src/rtk/app/hooks' +import CreateChatModalButton from './CreateChatModal' +import UnhideChatButton from './UnhideChatButton' + +type ChatButtonProps = { + spaceId?: string +} + +const ChatButton = ({ spaceId }: ChatButtonProps) => { + const space = useSelectSpace(spaceId) + + const spaceContent = space?.content + + const chats = spaceContent?.chats + const chat = chats?.[0] + + const post = useSelectPost(chat.id) + + const isPostHidden = !!post?.post.struct.hidden + + if (spaceContent && !isEmptyArray(chats) && !isPostHidden) return null + + return isPostHidden ? ( + + ) : ( + + ) +} + +export default ChatButton diff --git a/src/components/chat/ChatFloatingModal.tsx b/src/components/chat/ChatFloatingModal.tsx index ef92bc005..5ef145494 100644 --- a/src/components/chat/ChatFloatingModal.tsx +++ b/src/components/chat/ChatFloatingModal.tsx @@ -93,7 +93,7 @@ export default function ChatFloatingModal() { } type Entity = NonNullable -function getUnreadCount(entity: Entity) { +export function getUnreadCount(entity: Entity) { return parseInt(localStorage.getItem(`unreadCount:${entity.type}:${entity.data.id}`) ?? '') ?? 0 } diff --git a/src/components/chat/ChatIframe.module.sass b/src/components/chat/ChatIframe.module.sass index fbe569dc4..dd70cf45d 100644 --- a/src/components/chat/ChatIframe.module.sass +++ b/src/components/chat/ChatIframe.module.sass @@ -1,6 +1,41 @@ +@import 'src/styles/subsocial-vars.scss' + .ChatIframe transition: opacity 150ms ease-out opacity: 1 &.ChatIframeLoading - opacity: 0 \ No newline at end of file + opacity: 0 + +.ButtonWrapper + display: flex + align-items: center + gap: $space_tiny + align-items: center + margin: 0 19px 0 0 + padding: 12px 0 + +.ChatButton + height: auto + position: relative + display: inline-flex + font-size: 14px + background: transparent + align-items: center + padding: 0 + border: 0 + outline: none + cursor: pointer + +.ChatUnreadCount + font-size: 10px + padding: 2px 3px + display: flex + align-items: center + justify-content: center + height: 14px + min-width: 14px + font-weight: 500 + background: #EB2F95 + border-radius: 32px + color: white diff --git a/src/components/chat/ChatLinkButtonWithCounter.tsx b/src/components/chat/ChatLinkButtonWithCounter.tsx new file mode 100644 index 000000000..cedb3fd2b --- /dev/null +++ b/src/components/chat/ChatLinkButtonWithCounter.tsx @@ -0,0 +1,51 @@ +import { PostData } from '@subsocial/api/types' +import { Button } from 'antd' +import clsx from 'clsx' +import { useEffect, useState } from 'react' +import { useSetChatEntityConfig, useSetChatOpen } from 'src/rtk/app/hooks' +import { useAppSelector } from 'src/rtk/app/store' +import { getUnreadCount } from './ChatFloatingModal' +import styles from './ChatIframe.module.sass' + +type ChatLinkButtonWithCounterProps = { + post: PostData +} + +const ChatLinkButtonWithCounter = ({ post }: ChatLinkButtonWithCounterProps) => { + const setChatConfig = useSetChatEntityConfig() + const setChatOpen = useSetChatOpen() + + const entity = useAppSelector(state => state.chat.entity) + + const [unreadCount, setUnreadCount] = useState(0) + + useEffect(() => { + if (!entity) return + + const unreadCountFromStorage = getUnreadCount(entity) + if (unreadCountFromStorage && !isNaN(unreadCountFromStorage)) { + setUnreadCount(unreadCountFromStorage) + } + }, [entity]) + + return ( + + + {!!unreadCount &&
{unreadCount}
} +
+ ) +} + +export default ChatLinkButtonWithCounter diff --git a/src/components/chat/CreateChatModal.tsx b/src/components/chat/CreateChatModal.tsx index 1bad8e95f..8c84cbd15 100644 --- a/src/components/chat/CreateChatModal.tsx +++ b/src/components/chat/CreateChatModal.tsx @@ -2,7 +2,6 @@ import { Button } from 'antd' import { useRouter } from 'next/router' import { useEffect, useRef, useState } from 'react' import { createPortal } from 'react-dom' -// import { useSelectSpace } from 'src/rtk/app/hooks' import { getCurrentUrlOrigin } from 'src/utils/url' function parseMessage(data: string) { @@ -26,12 +25,6 @@ const CreateChatModalButton = ({ size }: CreateChatModalButtonProps) => { const [openModal, setOpenModal] = useState(false) const router = useRouter() - // const space = useSelectSpace(spaceId) - - // const spaceContent = space?.content - - // const chats = spaceContent?.chats - useEffect(() => { window.onmessage = event => { const message = parseMessage(event.data + '') @@ -53,8 +46,6 @@ const CreateChatModalButton = ({ size }: CreateChatModalButtonProps) => { } }, []) - // if (spaceContent && !isEmptyArray(chats)) return null - return ( { diff --git a/src/components/chat/UnhideChatButton.tsx b/src/components/chat/UnhideChatButton.tsx new file mode 100644 index 000000000..5b564b12e --- /dev/null +++ b/src/components/chat/UnhideChatButton.tsx @@ -0,0 +1,46 @@ +import { PostUpdate } from '@subsocial/api/substrate/wrappers' +import { PostStruct } from '@subsocial/api/types' +import { useAppDispatch } from 'src/rtk/app/store' +import { fetchPost } from 'src/rtk/features/posts/postsSlice' +import { DataSourceTypes } from 'src/types' +import { useSubsocialApi } from '../substrate' +import HiddenButton from '../utils/HiddenButton' + +type UnhideChatButtonProps = { + post?: PostStruct +} + +const UnhideChatButton = ({ post }: UnhideChatButtonProps) => { + const dispatch = useAppDispatch() + const { subsocial } = useSubsocialApi() + + if (!post) return null + + const newTxParams = () => { + const update = PostUpdate({ + hidden: false, + }) + return [post.id, update] + } + + const onTxSuccess = () => { + dispatch( + fetchPost({ api: subsocial, id: post.id, dataSource: DataSourceTypes.CHAIN, reload: true }), + ) + } + + return ( + + ) +} + +export default UnhideChatButton diff --git a/src/components/spaces/ViewSpace.tsx b/src/components/spaces/ViewSpace.tsx index 26e22ac0a..fd3c68b44 100644 --- a/src/components/spaces/ViewSpace.tsx +++ b/src/components/spaces/ViewSpace.tsx @@ -2,7 +2,7 @@ import { EditOutlined } from '@ant-design/icons' import { isEmptyStr, newLogger, nonEmptyStr } from '@subsocial/utils' import clsx from 'clsx' import dynamic from 'next/dynamic' -import React, { MouseEvent, useCallback, useMemo, useState } from 'react' +import React, { MouseEvent, useCallback, useState } from 'react' import { ButtonLink } from 'src/components/utils/CustomLinks' import { Segment } from 'src/components/utils/Segment' import { LARGE_AVATAR_SIZE } from 'src/config/Size.config' @@ -17,6 +17,7 @@ import { useSelectProfileSpace } from '../../rtk/features/profiles/profilesHooks import { useSelectSpace } from '../../rtk/features/spaces/spacesHooks' import { AccountActivity } from '../activity/AccountActivity' import { useMyAddress } from '../auth/MyAccountsContext' +import ChatButton from '../chat/ChatButton' import MobileActiveStakingSection from '../creators/MobileActiveStakingSection' import WriteSomething from '../posts/WriteSomething' import MakeAsProfileModal from '../profiles/address-views/utils/MakeAsProfileModal' @@ -49,7 +50,6 @@ import { ViewSpaceOptsProps, ViewSpaceProps } from './ViewSpaceProps' const log = newLogger('ViewSpace') const FollowSpaceButton = dynamic(() => import('../utils/FollowSpaceButton'), { ssr: false }) -const CreateChatModalButton = dynamic(() => import('../chat/CreateChatModal'), { ssr: false }) type Props = ViewSpaceProps @@ -136,28 +136,23 @@ export const InnerViewSpace = (props: Props) => { ) }, [spaceData, imageSize]) - // const setChatConfig = useSetChatEntityConfig() - // const setChatOpen = useSetChatOpen() - - // const { isCreatorSpace } = useIsCreatorSpace(spaceData?.id) - const isMySpace = useIsMySpace(spaceData?.struct) - const { filteredPostIds, filteredPosts } = useMemo(() => { - if (isMySpace) return { filteredPosts: posts, filteredPostIds: postIds } + // const { filteredPostIds, filteredPosts } = useMemo(() => { + // if (isMySpace) return { filteredPosts: posts, filteredPostIds: postIds } - const hiddenPosts = new Set() + // const hiddenPosts = new Set() - const filteredPosts = posts.filter(post => { - if (isHidden(post.post.struct)) { - hiddenPosts.add(post.post.id) - return false - } - return true - }) + // const filteredPosts = posts.filter(post => { + // if (isHidden(post.post.struct)) { + // hiddenPosts.add(post.post.id) + // return false + // } + // return true + // }) - const filteredPostIds = postIds.filter(id => !hiddenPosts.has(id)) - return { filteredPosts, filteredPostIds } - }, [posts, postIds]) + // const filteredPostIds = postIds.filter(id => !hiddenPosts.has(id)) + // return { filteredPosts, filteredPostIds } + // }, [posts, postIds]) // We do not return 404 page here, because this component could be used to render a space in list. if (!spaceData) return null @@ -167,7 +162,7 @@ export const InnerViewSpace = (props: Props) => { const contactInfo = { email, links } const spaceName = renderSpaceName(spaceData) - const chatButton = + const chatButton = const primaryClass = `ProfileDetails ${isMy && 'MySpace'} d-flex` @@ -365,7 +360,6 @@ export const InnerViewSpace = (props: Props) => {
{isProfileSpace ? ( void + buttonType?: 'text' | 'link' | 'ghost' | 'default' | 'primary' | 'dashed' | undefined + ghost?: boolean + size?: SizeType } export function HiddenButton(props: Props) { - const { struct, newTxParams, label, type, asLink, setVisibility, onTxSuccess } = props + const { + struct, + newTxParams, + label, + type, + asLink, + setVisibility, + onTxSuccess, + buttonType, + ghost, + size = 'small', + } = props const { hidden } = struct const extrinsic = type === 'space' ? 'spaces.updateSpace' : 'posts.updatePost' @@ -31,9 +46,11 @@ export function HiddenButton(props: Props) { return (