From 970d0f33157edd74275ea6d86bf56be52dd07858 Mon Sep 17 00:00:00 2001 From: Tushar Ojha Date: Wed, 9 Aug 2023 12:41:23 +0530 Subject: [PATCH 01/17] added grill integration in polkaverse --- package.json | 3 +- .../chat/ChatFloatingModal.module.sass | 56 ++++++++ src/components/chat/ChatFloatingModal.tsx | 37 +++++ src/components/posts/view-post/PostPage.tsx | 19 ++- .../posts/view-post/ViewRegularPreview.tsx | 21 +-- .../posts/view-post/ViewSharedPreview.tsx | 21 +-- src/components/posts/view-post/helpers.tsx | 4 +- src/layout/ClientLayout.tsx | 2 + src/rtk/app/hooks.ts | 1 + src/rtk/app/rootReducer.ts | 2 + src/rtk/features/chat/chatHooks.ts | 41 ++++++ src/rtk/features/chat/chatSlice.ts | 30 ++++ yarn.lock | 134 ++++++++++-------- 13 files changed, 290 insertions(+), 81 deletions(-) create mode 100644 src/components/chat/ChatFloatingModal.module.sass create mode 100644 src/components/chat/ChatFloatingModal.tsx create mode 100644 src/rtk/features/chat/chatHooks.ts create mode 100644 src/rtk/features/chat/chatSlice.ts diff --git a/package.json b/package.json index 47417d418..dd8e9a189 100644 --- a/package.json +++ b/package.json @@ -61,7 +61,7 @@ "cypress": "^10.11.0", "cypress-network-idle": "^1.10.2", "cypress-wait-until": "^1.7.2", - "eslint": "^8.28.0", + "eslint": "^8.46.0", "eslint-config-prettier": "^8.5.0", "eslint-plugin-jsx-a11y": "^6.3.1", "eslint-plugin-prefer-object-spread": "^1.2.1", @@ -93,6 +93,7 @@ "@subsocial/api": "0.8.10", "@subsocial/definitions": "0.8.10", "@subsocial/elasticsearch": "0.8.10", + "@subsocial/grill-widget": "^0.0.8", "@subsocial/utils": "0.8.10", "@tiptap/extension-highlight": "^2.0.0-beta.33", "@tiptap/extension-image": "^2.0.0-beta.27", diff --git a/src/components/chat/ChatFloatingModal.module.sass b/src/components/chat/ChatFloatingModal.module.sass new file mode 100644 index 000000000..526c597fe --- /dev/null +++ b/src/components/chat/ChatFloatingModal.module.sass @@ -0,0 +1,56 @@ +@import 'src/styles/subsocial-vars.scss' + +.ChatFloatingModal + position: fixed + bottom: 30px + right: 30px + z-index: 1000 + display: flex + flex-direction: column + align-items: flex-end + gap: $space_normal + + &:has(.ChatFloatingIframeHidden) + // z-index: -1 !important + + .ChatFloatingButton + width: 60px + height: 60px + border-radius: 50% + color: white + font-size: 24px + display: flex + justify-content: center + align-items: center + padding: 0 + + img + display: block + width: 100% + height: 100% + + .ChatFloatingIframe + // 100px from the height of the button + offset to the bottom + gap + height: min(570px, 90vh - 100px) + // 60px from the offset left & right of the iframe (30px each) + width: min(400px, 100vw - 60px) + border-radius: $border_radius_large + overflow: hidden + box-shadow: 0 12px 50px -12px rgb(0 0 0 / 0.5) + transition-property: opacity, height, width, + transition-duration: .3s, 0s, 0s + transition-delay: 0s, 0s, 0s + opacity: 1 + + &.ChatFloatingIframeHidden + pointer-events: none + transition-delay: 0s, .3s, .3s !important + height: 0 + width: 0 + opacity: 0 + + iframe + border-radius: $border_radius_large + width: 100% + height: 100% + border: none diff --git a/src/components/chat/ChatFloatingModal.tsx b/src/components/chat/ChatFloatingModal.tsx new file mode 100644 index 000000000..bd9867e0c --- /dev/null +++ b/src/components/chat/ChatFloatingModal.tsx @@ -0,0 +1,37 @@ +import clsx from 'clsx' +import { useEffect, useRef } from 'react' +import { useChatWindowOpenState, useOpenCloseChatWindow } from 'src/rtk/app/hooks' +import styles from './ChatFloatingModal.module.sass' + +export default function ChatFloatingModal() { + const isOpen = useChatWindowOpenState() + const setChatWindowState = useOpenCloseChatWindow() + const ref = useRef(null) + + useEffect(() => { + const handleOutSideClick = (event: any) => { + console.log(isOpen) + if (!ref.current?.contains(event.target) && isOpen) { + setChatWindowState(false) + } + } + + window.addEventListener('mousedown', handleOutSideClick) + + return () => { + window.removeEventListener('mousedown', handleOutSideClick) + } + }, [ref]) + + return ( +
+ {isOpen && ( +
+ )} +
+ ) +} diff --git a/src/components/posts/view-post/PostPage.tsx b/src/components/posts/view-post/PostPage.tsx index 3cbf03e43..bbc227b39 100644 --- a/src/components/posts/view-post/PostPage.tsx +++ b/src/components/posts/view-post/PostPage.tsx @@ -15,7 +15,7 @@ import { return404 } from 'src/components/utils/next' import config from 'src/config' import { resolveIpfsUrl } from 'src/ipfs' import { getInitialPropsWithRedux, NextContextWithRedux } from 'src/rtk/app' -import { useSelectProfile } from 'src/rtk/app/hooks' +import { useOpenCloseChatWindow, useSelectProfile, useSetupGrillConfig } from 'src/rtk/app/hooks' import { useAppSelector } from 'src/rtk/app/store' import { fetchPost, fetchPosts, selectPost } from 'src/rtk/features/posts/postsSlice' import { useFetchMyReactionsByPostId } from 'src/rtk/features/reactions/myPostReactionsHooks' @@ -27,7 +27,6 @@ import { PostWithAllDetails, PostWithSomeDetails, } from 'src/types' -import { CommentSection } from '../../comments/CommentsSection' import { DfImage } from '../../utils/DfImage' import { DfMd } from '../../utils/DfMd' import Section from '../../utils/Section' @@ -61,11 +60,19 @@ const InnerPostPage: NextPage = props => { const { isNotMobile } = useResponsiveSize() useFetchMyReactionsByPostId(id) + const setGrillChatVisibility = useOpenCloseChatWindow() + const setupGrillConfig = useSetupGrillConfig() + const postData = useAppSelector(state => selectPost(state, { id })) || initialPostData const { post, space } = postData const { struct, content } = post + const openCommentSection = () => { + setupGrillConfig(space?.id ?? '', post.id, post.content?.title) + setGrillChatVisibility(true) + } + const profile = useSelectProfile(postData.post.struct.ownerId) const spaceId = space?.id const isSameProfileAndSpace = profile?.id === spaceId @@ -176,7 +183,11 @@ const InnerPostPage: NextPage = props => { )}
- + openCommentSection()} + />
@@ -190,8 +201,6 @@ const InnerPostPage: NextPage = props => { {!isSameProfileAndSpace && ( )} - -
diff --git a/src/components/posts/view-post/ViewRegularPreview.tsx b/src/components/posts/view-post/ViewRegularPreview.tsx index 26c640ba7..6cce3e5c7 100644 --- a/src/components/posts/view-post/ViewRegularPreview.tsx +++ b/src/components/posts/view-post/ViewRegularPreview.tsx @@ -1,6 +1,6 @@ -import { FC, useState } from 'react' +import { FC } from 'react' +import { useOpenCloseChatWindow, useSetupGrillConfig } from 'src/rtk/app/hooks' import { SpaceData } from 'src/types' -import { CommentSection } from '../../comments/CommentsSection' import { InfoPostPreview, PostActionsPanel, PostNotFound } from './helpers' import { PreviewProps } from './PostPreview' @@ -12,10 +12,18 @@ type ComponentType = FC /** Sloooooooow Regular Preview */ export const RegularPreview: ComponentType = props => { - const { postDetails, space, replies, withImage, withTags, withActions } = props - const [commentsSection, setCommentsSection] = useState(false) + const { postDetails, space, withImage, withTags, withActions } = props + + const setGrillChatVisibility = useOpenCloseChatWindow() + const setupGrillConfig = useSetupGrillConfig() + const { isSharedPost } = postDetails.post.struct + const openCommentSection = () => { + setupGrillConfig(space?.id ?? '', postDetails.id, postDetails.post.content?.title) + setGrillChatVisibility(true) + } + return !isSharedPost ? ( <> { setCommentsSection(!commentsSection)} + toogleCommentSection={() => openCommentSection()} preview /> )} - {commentsSection && ( - - )} ) : ( diff --git a/src/components/posts/view-post/ViewSharedPreview.tsx b/src/components/posts/view-post/ViewSharedPreview.tsx index 9c334b4a4..b09bd1f47 100644 --- a/src/components/posts/view-post/ViewSharedPreview.tsx +++ b/src/components/posts/view-post/ViewSharedPreview.tsx @@ -1,19 +1,25 @@ -import { FC, useState } from 'react' -import { useSelectPost } from 'src/rtk/app/hooks' +import { FC } from 'react' +import { useOpenCloseChatWindow, useSelectPost, useSetupGrillConfig } from 'src/rtk/app/hooks' import { asSharedPostStruct } from 'src/types' import { InnerPreviewProps } from '.' -import { CommentSection } from '../../comments/CommentsSection' import { PostActionsPanel, PostCreator, SharePostContent } from './helpers' import { PostDropDownMenu } from './PostDropDownMenu' type ComponentType = FC export const SharedPreview: ComponentType = props => { - const { postDetails, space, withActions, replies } = props - const [commentsSection, setCommentsSection] = useState(false) + const { postDetails, space, withActions } = props const sharedPostId = asSharedPostStruct(postDetails.post.struct).originalPostId postDetails.ext = useSelectPost(sharedPostId) + const setGrillChatVisibility = useOpenCloseChatWindow() + const setupGrillConfig = useSetupGrillConfig() + + const openCommentSection = () => { + setupGrillConfig(space?.id ?? '', postDetails.id, postDetails.post.content?.title) + setGrillChatVisibility(true) + } + return ( <>
@@ -27,13 +33,10 @@ export const SharedPreview: ComponentType = props => { setCommentsSection(!commentsSection)} + toogleCommentSection={() => openCommentSection()} preview /> )} - {commentsSection && ( - - )} ) } diff --git a/src/components/posts/view-post/helpers.tsx b/src/components/posts/view-post/helpers.tsx index b4b656629..f749d35c2 100644 --- a/src/components/posts/view-post/helpers.tsx +++ b/src/components/posts/view-post/helpers.tsx @@ -312,7 +312,7 @@ const Action: FC<{ onClick?: () => void; title?: string }> = ({ children, onClic ) export const PostActionsPanel: FC = props => { - const { postDetails, space, preview, withBorder } = props + const { postDetails, space, preview, withBorder, toogleCommentSection } = props const { post: { struct }, } = postDetails @@ -330,7 +330,7 @@ export const PostActionsPanel: FC = props => {
)} - {preview && } + {toogleCommentSection && } { {children} + diff --git a/src/rtk/app/hooks.ts b/src/rtk/app/hooks.ts index 33834f698..03a8bea2a 100644 --- a/src/rtk/app/hooks.ts +++ b/src/rtk/app/hooks.ts @@ -1,3 +1,4 @@ +export * from '../features/chat/chatHooks' export * from '../features/posts/postsHooks' export * from '../features/profiles/profilesHooks' export * from '../features/replies/repliesHooks' diff --git a/src/rtk/app/rootReducer.ts b/src/rtk/app/rootReducer.ts index 1f2728986..67be2b0c2 100644 --- a/src/rtk/app/rootReducer.ts +++ b/src/rtk/app/rootReducer.ts @@ -2,6 +2,7 @@ import { combineReducers } from '@reduxjs/toolkit' import myAccount from '../features/accounts/myAccountSlice' import spaceEditors from '../features/accounts/spaceEditorsSlice' import chainsInfo from '../features/chainsInfo/chainsInfoSlice' +import chat from '../features/chat/chatSlice' import enableConfirmation from '../features/confirmationPopup/enableConfirmationSlice' import contents from '../features/contents/contentsSlice' import domainByOwner from '../features/domains/domainsByOwnerSlice' @@ -43,6 +44,7 @@ const rootReducer = combineReducers({ chainsInfo, onBoarding, enableConfirmation, + chat, }) export type RootState = ReturnType diff --git a/src/rtk/features/chat/chatHooks.ts b/src/rtk/features/chat/chatHooks.ts new file mode 100644 index 000000000..3d64071d9 --- /dev/null +++ b/src/rtk/features/chat/chatHooks.ts @@ -0,0 +1,41 @@ +import grill, { GrillConfig } from '@subsocial/grill-widget' +import { useCallback } from 'react' +import { useAppDispatch, useAppSelector } from 'src/rtk/app/store' +import { setChatWindowVisibility, setCurrentConfig } from '../chat/chatSlice' + +export function useChatWindowOpenState() { + return useAppSelector(state => state.chat.isChatWindowOpen) +} + +export function useSetupGrillConfig() { + const dispatch = useAppDispatch() + return useCallback((spaceId: string, postId: string, title?: string) => { + const config: GrillConfig = { + hub: { + id: spaceId, + }, + channel: { + type: 'resource', + resource: { + toResourceId: () => postId, + }, + settings: { + enableLoginButton: false, + enableInputAutofocus: true, + }, + metadata: { + title: title ?? '', + }, + }, + } + grill.init(config) + dispatch(setCurrentConfig(config)) + }, []) +} + +export function useOpenCloseChatWindow() { + const dispatch = useAppDispatch() + return useCallback((state: boolean) => { + dispatch(setChatWindowVisibility(state)) + }, []) +} diff --git a/src/rtk/features/chat/chatSlice.ts b/src/rtk/features/chat/chatSlice.ts new file mode 100644 index 000000000..e76c15de8 --- /dev/null +++ b/src/rtk/features/chat/chatSlice.ts @@ -0,0 +1,30 @@ +import { createSlice, PayloadAction } from '@reduxjs/toolkit' +import { GrillConfig } from '@subsocial/grill-widget' + +const sliceName = 'chats' + +export interface chatEntity { + isChatWindowOpen: boolean + currentConfig?: GrillConfig +} + +const initialState: chatEntity = { + isChatWindowOpen: false, +} + +const slice = createSlice({ + name: sliceName, + initialState, + reducers: { + setChatWindowVisibility: (state, action: PayloadAction) => { + state.isChatWindowOpen = action.payload + }, + setCurrentConfig: (state, action: PayloadAction) => { + state.currentConfig = action.payload + }, + }, +}) + +export const { setChatWindowVisibility, setCurrentConfig } = slice.actions + +export default slice.reducer diff --git a/yarn.lock b/yarn.lock index 8d943a727..a10ba6853 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,6 +2,11 @@ # yarn lockfile v1 +"@aashutoshrathi/word-wrap@^1.2.3": + version "1.2.6" + resolved "https://registry.yarnpkg.com/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz#bd9154aec9983f77b3a034ecaa015c2e4201f6cf" + integrity sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA== + "@ampproject/remapping@^2.2.0": version "2.2.1" resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.2.1.tgz#99e8e11851128b8702cd57c33684f1d0f260b630" @@ -865,14 +870,19 @@ resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.5.0.tgz#f6f729b02feee2c749f57e334b7a1b5f40a81724" integrity sha512-vITaYzIcNmjn5tF5uxcZ/ft7/RXGrMUIS9HalWckEOF6ESiwXKoMzAQf2UW0aVd6rnOeExTJVd5hmWXucBKGXQ== -"@eslint/eslintrc@^2.0.2": - version "2.0.2" - resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-2.0.2.tgz#01575e38707add677cf73ca1589abba8da899a02" - integrity sha512-3W4f5tDUra+pA+FzgugqL2pRimUTDJWKr7BINqOpkZrC0uYI0NIc0/JFgBROCU07HR6GieA5m3/rsPIhDmCXTQ== +"@eslint-community/regexpp@^4.6.1": + version "4.6.2" + resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.6.2.tgz#1816b5f6948029c5eaacb0703b850ee0cb37d8f8" + integrity sha512-pPTNuaAG3QMH+buKyBIGJs3g/S5y0caxw0ygM3YyE6yJFySwiGGSzA+mM3KJ8QQvzeLh3blwgSonkFjgQdxzMw== + +"@eslint/eslintrc@^2.1.1": + version "2.1.1" + resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-2.1.1.tgz#18d635e24ad35f7276e8a49d135c7d3ca6a46f93" + integrity sha512-9t7ZA7NGGK8ckelF0PQCfcxIUzs1Md5rrO6U/c+FIQNanea5UZC0wqKXH4vHBccmu4ZJgZ2idtPeW7+Q2npOEA== dependencies: ajv "^6.12.4" debug "^4.3.2" - espree "^9.5.1" + espree "^9.6.0" globals "^13.19.0" ignore "^5.2.0" import-fresh "^3.2.1" @@ -880,10 +890,10 @@ minimatch "^3.1.2" strip-json-comments "^3.1.1" -"@eslint/js@8.38.0": - version "8.38.0" - resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.38.0.tgz#73a8a0d8aa8a8e6fe270431c5e72ae91b5337892" - integrity sha512-IoD2MfUnOV58ghIHCiil01PcohxjbYR/qCxsoC+xNgUwh1EY8jOOrYmu3d3a71+tJJ23uscEV4X2HJWMsPJu4g== +"@eslint/js@^8.46.0": + version "8.46.0" + resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.46.0.tgz#3f7802972e8b6fe3f88ed1aabc74ec596c456db6" + integrity sha512-a8TLtmPi8xzPkCbp/OGFUo5yhRkHM2Ko9kOWP4znJr0WAhWyThaw3PnwX4vOTWOAMsV2uRt32PPDcEz63esSaA== "@floating-ui/core@^1.2.6": version "1.2.6" @@ -929,10 +939,10 @@ dependencies: "@babel/runtime" "^7.17.9" -"@humanwhocodes/config-array@^0.11.8": - version "0.11.8" - resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.8.tgz#03595ac2075a4dc0f191cc2131de14fbd7d410b9" - integrity sha512-UybHIJzJnR5Qc/MsD9Kr+RpO2h+/P1GhOwdiLPXK5TWk5sgTdu88bTD9UP+CKbPPh5Rni1u0GjAdYQLemG8g+g== +"@humanwhocodes/config-array@^0.11.10": + version "0.11.10" + resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.10.tgz#5a3ffe32cc9306365fb3fd572596cd602d5e12d2" + integrity sha512-KVVjQmNUepDVGXNuoRRdmmEjruj0KfiGSbS8LVc12LMsWDQzRXJ0qdhN8L8uUigKpfEHRhlaQFY0ib1tnUbNeQ== dependencies: "@humanwhocodes/object-schema" "^1.2.1" debug "^4.1.1" @@ -2326,6 +2336,11 @@ "@elastic/elasticsearch" "7.4.0" "@subsocial/utils" latest +"@subsocial/grill-widget@^0.0.8": + version "0.0.8" + resolved "https://registry.yarnpkg.com/@subsocial/grill-widget/-/grill-widget-0.0.8.tgz#a8a584daffdac4edbb319ec59579a0bdd0004441" + integrity sha512-dRPBcn+LU4LLVzHmVCm3MuIVc6dRZ2vC80PAys05NBVTdak+Jz1RQbc1yozPg6ReHgzZhQhU5z+ErYsYRvF09Q== + "@subsocial/utils@0.8.10", "@subsocial/utils@latest": version "0.8.10" resolved "https://registry.yarnpkg.com/@subsocial/utils/-/utils-0.8.10.tgz#bfec93e089b0eecc58f35c7924e8efc30bc72127" @@ -3215,11 +3230,16 @@ acorn@^7.1.1: resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa" integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== -acorn@^8.0.4, acorn@^8.2.4, acorn@^8.8.0: +acorn@^8.0.4, acorn@^8.2.4: version "8.8.2" resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.2.tgz#1b2f25db02af965399b9776b0c2c391276d37c4a" integrity sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw== +acorn@^8.9.0: + version "8.10.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.10.0.tgz#8be5b3907a67221a81ab23c7889c4c5526b62ec5" + integrity sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw== + adblock-detect-react@^1.0.5: version "1.1.0" resolved "https://registry.yarnpkg.com/adblock-detect-react/-/adblock-detect-react-1.1.0.tgz#f34dc0ce6f92e011c17b1b94a9b98c3e737ff0c5" @@ -3245,7 +3265,7 @@ ajv-keywords@^3.5.2: resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d" integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ== -ajv@^6.10.0, ajv@^6.12.4, ajv@^6.12.5: +ajv@^6.12.4, ajv@^6.12.5: version "6.12.6" resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== @@ -6139,40 +6159,45 @@ eslint-scope@^5.1.1: esrecurse "^4.3.0" estraverse "^4.1.1" -eslint-scope@^7.1.1: - version "7.2.0" - resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-7.2.0.tgz#f21ebdafda02352f103634b96dd47d9f81ca117b" - integrity sha512-DYj5deGlHBfMt15J7rdtyKNq/Nqlv5KfU4iodrQ019XESsRnwXH9KAE0y3cwtUHDo2ob7CypAnCqefh6vioWRw== +eslint-scope@^7.2.2: + version "7.2.2" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-7.2.2.tgz#deb4f92563390f32006894af62a22dba1c46423f" + integrity sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg== dependencies: esrecurse "^4.3.0" estraverse "^5.2.0" -eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.0: +eslint-visitor-keys@^3.3.0: version "3.4.0" resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.0.tgz#c7f0f956124ce677047ddbc192a68f999454dedc" integrity sha512-HPpKPUBQcAsZOsHAFwTtIKcYlCje62XB7SEAcxjtmW6TD1WVpkS6i6/hOVtTZIl4zGj/mBqpFVGvaDneik+VoQ== -eslint@^8.28.0: - version "8.38.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.38.0.tgz#a62c6f36e548a5574dd35728ac3c6209bd1e2f1a" - integrity sha512-pIdsD2jwlUGf/U38Jv97t8lq6HpaU/G9NKbYmpWpZGw3LdTNhZLbJePqxOXGB5+JEKfOPU/XLxYxFh03nr1KTg== +eslint-visitor-keys@^3.4.1, eslint-visitor-keys@^3.4.2: + version "3.4.2" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.2.tgz#8c2095440eca8c933bedcadf16fefa44dbe9ba5f" + integrity sha512-8drBzUEyZ2llkpCA67iYrgEssKDUu68V8ChqqOfFupIaG/LCVPUT+CoGJpT77zJprs4T/W7p07LP7zAIMuweVw== + +eslint@^8.46.0: + version "8.46.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.46.0.tgz#a06a0ff6974e53e643acc42d1dcf2e7f797b3552" + integrity sha512-cIO74PvbW0qU8e0mIvk5IV3ToWdCq5FYG6gWPHHkx6gNdjlbAYvtfHmlCMXxjcoVaIdwy/IAt3+mDkZkfvb2Dg== dependencies: "@eslint-community/eslint-utils" "^4.2.0" - "@eslint-community/regexpp" "^4.4.0" - "@eslint/eslintrc" "^2.0.2" - "@eslint/js" "8.38.0" - "@humanwhocodes/config-array" "^0.11.8" + "@eslint-community/regexpp" "^4.6.1" + "@eslint/eslintrc" "^2.1.1" + "@eslint/js" "^8.46.0" + "@humanwhocodes/config-array" "^0.11.10" "@humanwhocodes/module-importer" "^1.0.1" "@nodelib/fs.walk" "^1.2.8" - ajv "^6.10.0" + ajv "^6.12.4" chalk "^4.0.0" cross-spawn "^7.0.2" debug "^4.3.2" doctrine "^3.0.0" escape-string-regexp "^4.0.0" - eslint-scope "^7.1.1" - eslint-visitor-keys "^3.4.0" - espree "^9.5.1" + eslint-scope "^7.2.2" + eslint-visitor-keys "^3.4.2" + espree "^9.6.1" esquery "^1.4.2" esutils "^2.0.2" fast-deep-equal "^3.1.3" @@ -6180,32 +6205,29 @@ eslint@^8.28.0: find-up "^5.0.0" glob-parent "^6.0.2" globals "^13.19.0" - grapheme-splitter "^1.0.4" + graphemer "^1.4.0" ignore "^5.2.0" - import-fresh "^3.0.0" imurmurhash "^0.1.4" is-glob "^4.0.0" is-path-inside "^3.0.3" - js-sdsl "^4.1.4" js-yaml "^4.1.0" json-stable-stringify-without-jsonify "^1.0.1" levn "^0.4.1" lodash.merge "^4.6.2" minimatch "^3.1.2" natural-compare "^1.4.0" - optionator "^0.9.1" + optionator "^0.9.3" strip-ansi "^6.0.1" - strip-json-comments "^3.1.0" text-table "^0.2.0" -espree@^9.5.1: - version "9.5.1" - resolved "https://registry.yarnpkg.com/espree/-/espree-9.5.1.tgz#4f26a4d5f18905bf4f2e0bd99002aab807e96dd4" - integrity sha512-5yxtHSZXRSW5pvv3hAlXM5+/Oswi1AUFqBmbibKb5s6bp3rGIDkyXU6xCoyuuLhijr4SFwPrXRoZjz0AZDN9tg== +espree@^9.6.0, espree@^9.6.1: + version "9.6.1" + resolved "https://registry.yarnpkg.com/espree/-/espree-9.6.1.tgz#a2a17b8e434690a5432f2f8018ce71d331a48c6f" + integrity sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ== dependencies: - acorn "^8.8.0" + acorn "^8.9.0" acorn-jsx "^5.3.2" - eslint-visitor-keys "^3.4.0" + eslint-visitor-keys "^3.4.1" esprima@^4.0.0, esprima@^4.0.1, esprima@~4.0.0: version "4.0.1" @@ -7029,6 +7051,11 @@ grapheme-splitter@^1.0.4: resolved "https://registry.yarnpkg.com/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz#9cf3a665c6247479896834af35cf1dbb4400767e" integrity sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ== +graphemer@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6" + integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag== + graphlib@^2.1.8: version "2.1.8" resolved "https://registry.yarnpkg.com/graphlib/-/graphlib-2.1.8.tgz#5761d414737870084c92ec7b5dbcb0592c9d35da" @@ -7413,7 +7440,7 @@ immutable@^4.0.0: resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.3.0.tgz#eb1738f14ffb39fd068b1dbe1296117484dd34be" integrity sha512-0AOCmOip+xgJwEVTQj1EfiDDOkPmuyllDuTuEX+DDXUgapLAsBIfkg3sxCYyCEA8mQqZrrxPUGjcOQ2JS3WLkg== -import-fresh@^3.0.0, import-fresh@^3.2.1: +import-fresh@^3.2.1: version "3.3.0" resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw== @@ -8601,11 +8628,6 @@ js-cookie@^2.2.1: resolved "https://registry.yarnpkg.com/js-cookie/-/js-cookie-2.2.1.tgz#69e106dc5d5806894562902aa5baec3744e9b2b8" integrity sha512-HvdH2LzI/EAZcUwA8+0nKNtWHqS+ZmijLA30RwZA0bo7ToCckjK5MkGhjED9KoRcXO6BaGI3I9UIzSA1FKFPOQ== -js-sdsl@^4.1.4: - version "4.4.0" - resolved "https://registry.yarnpkg.com/js-sdsl/-/js-sdsl-4.4.0.tgz#8b437dbe642daa95760400b602378ed8ffea8430" - integrity sha512-FfVSdx6pJ41Oa+CF7RDaFmTnCaFhua+SNYQX74riGOpl96x+2jQCqEfQ2bnXu/5DPCqlRuiqyvTJM0Qjz26IVg== - "js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" @@ -10218,17 +10240,17 @@ optionator@^0.8.1: type-check "~0.3.2" word-wrap "~1.2.3" -optionator@^0.9.1: - version "0.9.1" - resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.1.tgz#4f236a6373dae0566a6d43e1326674f50c291499" - integrity sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw== +optionator@^0.9.3: + version "0.9.3" + resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.3.tgz#007397d44ed1872fdc6ed31360190f81814e2c64" + integrity sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg== dependencies: + "@aashutoshrathi/word-wrap" "^1.2.3" deep-is "^0.1.3" fast-levenshtein "^2.0.6" levn "^0.4.1" prelude-ls "^1.2.1" type-check "^0.4.0" - word-wrap "^1.2.3" orderedmap@^2.0.0: version "2.1.0" @@ -12940,7 +12962,7 @@ strip-final-newline@^2.0.0: resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad" integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA== -strip-json-comments@^3.1.0, strip-json-comments@^3.1.1: +strip-json-comments@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== @@ -14078,7 +14100,7 @@ wolfy87-eventemitter@~5.1.0: resolved "https://registry.yarnpkg.com/wolfy87-eventemitter/-/wolfy87-eventemitter-5.1.0.tgz#35c1ac0dd1ac0c15e35d981508fc22084a13a011" integrity sha512-VakY4+17DbamV2VW4nZERrSuilclCRcYtfchPWe6jlma8k0AeLJxBR+C5OSFFtICArDFdXk0yw67HUGrTCdrEg== -word-wrap@^1.2.3, word-wrap@~1.2.3: +word-wrap@~1.2.3: version "1.2.3" resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ== From ba6e28f254467b448b27f9549611d21768881182 Mon Sep 17 00:00:00 2001 From: teodorus-nathaniel Date: Fri, 1 Sep 2023 23:18:03 +0700 Subject: [PATCH 02/17] Add side panel and floating for chat --- package.json | 3 +- public/images/grillchat.svg | 3 + .../chat/ChatFloatingModal.module.sass | 133 ++++++++++++------ src/components/chat/ChatFloatingModal.tsx | 88 +++++++++--- src/components/chat/ChatIframe.tsx | 76 ++++++++++ src/components/chat/ChatSidePanel.module.sass | 14 ++ src/components/chat/ChatSidePanel.tsx | 11 ++ src/components/main/PageWrapper.tsx | 2 +- src/components/posts/view-post/PostPage.tsx | 23 +-- .../posts/view-post/ViewRegularPreview.tsx | 18 +-- .../posts/view-post/ViewSharedPreview.tsx | 19 +-- .../responsive/ResponsiveContext.tsx | 3 + src/layout/ClientLayout.tsx | 6 +- src/layout/Navigation.tsx | 7 + src/rtk/features/chat/chatHooks.ts | 66 ++++----- src/rtk/features/chat/chatSlice.ts | 24 ++-- yarn.lock | 30 +++- 17 files changed, 364 insertions(+), 162 deletions(-) create mode 100644 public/images/grillchat.svg create mode 100644 src/components/chat/ChatIframe.tsx create mode 100644 src/components/chat/ChatSidePanel.module.sass create mode 100644 src/components/chat/ChatSidePanel.tsx diff --git a/package.json b/package.json index dd8e9a189..130c70132 100644 --- a/package.json +++ b/package.json @@ -93,7 +93,8 @@ "@subsocial/api": "0.8.10", "@subsocial/definitions": "0.8.10", "@subsocial/elasticsearch": "0.8.10", - "@subsocial/grill-widget": "^0.0.8", + "@subsocial/grill-widget": "^0.0.11", + "@subsocial/resource-discussions": "^0.0.3", "@subsocial/utils": "0.8.10", "@tiptap/extension-highlight": "^2.0.0-beta.33", "@tiptap/extension-image": "^2.0.0-beta.27", diff --git a/public/images/grillchat.svg b/public/images/grillchat.svg new file mode 100644 index 000000000..6d0e6ffd6 --- /dev/null +++ b/public/images/grillchat.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/components/chat/ChatFloatingModal.module.sass b/src/components/chat/ChatFloatingModal.module.sass index 526c597fe..f07b8efea 100644 --- a/src/components/chat/ChatFloatingModal.module.sass +++ b/src/components/chat/ChatFloatingModal.module.sass @@ -1,56 +1,109 @@ @import 'src/styles/subsocial-vars.scss' -.ChatFloatingModal +.ChatFloatingWrapper + z-index: 10 position: fixed - bottom: 30px - right: 30px - z-index: 1000 - display: flex - flex-direction: column - align-items: flex-end - gap: $space_normal + bottom: $space_huge + right: $space_normal - &:has(.ChatFloatingIframeHidden) - // z-index: -1 !important + .ChatUnreadCount + position: absolute + padding: 2px $space_tiny + top: 0 + right: 0 + font-size: $font_tiny + background: red + border-radius: 32px + transform: translate(25%, -25%) + color: white .ChatFloatingButton - width: 60px - height: 60px - border-radius: 50% - color: white - font-size: 24px + padding: $space_small $space_normal + border-radius: 32px + font-size: 1rem + color: white !important display: flex justify-content: center + height: auto align-items: center - padding: 0 + background: linear-gradient(95.39deg, #C43333 9.79%, #F9A11E 135.53%) !important + gap: $space_tiny + + &:hover, &:focus + filter: brightness(1.1) img display: block - width: 100% - height: 100% - - .ChatFloatingIframe - // 100px from the height of the button + offset to the bottom + gap - height: min(570px, 90vh - 100px) - // 60px from the offset left & right of the iframe (30px each) - width: min(400px, 100vw - 60px) - border-radius: $border_radius_large - overflow: hidden - box-shadow: 0 12px 50px -12px rgb(0 0 0 / 0.5) - transition-property: opacity, height, width, - transition-duration: .3s, 0s, 0s - transition-delay: 0s, 0s, 0s + height: 1.1rem + +.ChatContainer + z-index: 1010 + position: fixed + bottom: 0 + left: 0 + width: 100% + height: 100vh + display: flex + flex-direction: column + justify-content: flex-end + + .ChatOverlay + position: absolute + inset: 0 + width: 100% + height: 100% + background-color: rgba(0, 0, 0, .2) + transition: opacity 0.2s ease-out opacity: 1 - &.ChatFloatingIframeHidden - pointer-events: none - transition-delay: 0s, .3s, .3s !important - height: 0 - width: 0 + .ChatContent + height: 90vh + height: 90dvh + width: 100% + background: #F8FAFC + border-radius: $border_radius_huge + opacity: 1 + transform: translateY(0) + transition: transform 0.3s ease-in-out, opacity 0.2s ease-in-out + display: flex + flex-direction: column + + .ChatControl + display: flex + align-items: center + justify-content: center + padding-top: $space-tiny + + button + border-radius: 50% + background: #F0F1F9 + border: none + display: flex + align-items: center + justify-content: center + width: 2rem + height: 2rem + padding: 0 + font-size: 1rem + + .ChatIframe + flex: 1 + + iframe + display: block + border-radius: $border_radius_large + width: 100% + height: 100% + border: none + + &.ChatContainerHidden + pointer-events: none + + .ChatOverlay + opacity: 0 + + .ChatContent + transition: transform 0.3s ease-in-out, opacity 0.3s ease-in-out opacity: 0 + transform: translateY(100%) - iframe - border-radius: $border_radius_large - width: 100% - height: 100% - border: none diff --git a/src/components/chat/ChatFloatingModal.tsx b/src/components/chat/ChatFloatingModal.tsx index bd9867e0c..642d3c655 100644 --- a/src/components/chat/ChatFloatingModal.tsx +++ b/src/components/chat/ChatFloatingModal.tsx @@ -1,37 +1,81 @@ +import { Button } from 'antd' import clsx from 'clsx' -import { useEffect, useRef } from 'react' -import { useChatWindowOpenState, useOpenCloseChatWindow } from 'src/rtk/app/hooks' +import { useEffect, useRef, useState } from 'react' +import { createPortal } from 'react-dom' +import { HiChevronDown } from 'react-icons/hi2' +import { useResponsiveSize } from '../responsive' import styles from './ChatFloatingModal.module.sass' +import ChatIframe from './ChatIframe' +// import { useResponsiveSize } from '../responsive' +// import { useSendEvent } from '../providers/AnalyticContext' export default function ChatFloatingModal() { - const isOpen = useChatWindowOpenState() - const setChatWindowState = useOpenCloseChatWindow() - const ref = useRef(null) + const { isLargeDesktop } = useResponsiveSize() + // const sendEvent = useSendEvent() + + const [unreadCount, setUnreadCount] = useState(0) + const [isOpen, setIsOpen] = useState(false) useEffect(() => { - const handleOutSideClick = (event: any) => { - console.log(isOpen) - if (!ref.current?.contains(event.target) && isOpen) { - setChatWindowState(false) - } + const unreadCountFromStorage = parseInt(localStorage.getItem('unreadCount') ?? '') + if (unreadCountFromStorage && !isNaN(unreadCountFromStorage)) { + setUnreadCount(unreadCountFromStorage) + } + }, []) + + const hasOpened = useRef(false) + const toggleChat = () => { + // let event + if (isOpen) { + // event = 'close_grill_iframe' + } else { + // event = 'open_grill_iframe' + setUnreadCount(0) + localStorage.setItem('unreadCount', '0') } + // sendEvent(event) - window.addEventListener('mousedown', handleOutSideClick) + setIsOpen(prev => !prev) + hasOpened.current = true + } - return () => { - window.removeEventListener('mousedown', handleOutSideClick) + if (isLargeDesktop) { + return null + } + + const onUnreadCountChange = (count: number) => { + if (count > 0) { + setUnreadCount(count) + localStorage.setItem('unreadCount', count.toString()) } - }, [ref]) + } return ( -
- {isOpen && ( -
+ <> + {createPortal( +
+
setIsOpen(false)} /> +
+
+ +
+ +
+
, + document.body, + )} + {createPortal( +
+ + {!!unreadCount && {unreadCount}} +
, + document.body, )} -
+ ) } diff --git a/src/components/chat/ChatIframe.tsx b/src/components/chat/ChatIframe.tsx new file mode 100644 index 000000000..4e5b1bcac --- /dev/null +++ b/src/components/chat/ChatIframe.tsx @@ -0,0 +1,76 @@ +import grill, { GrillConfig } from '@subsocial/grill-widget' +import { Resource } from '@subsocial/resource-discussions' +import { summarize } from '@subsocial/utils' +import clsx from 'clsx' +import { ComponentProps, useEffect } from 'react' +import { useAppSelector } from 'src/rtk/app/store' +import { ChatEntity } from 'src/rtk/features/chat/chatSlice' + +export type ChatIframeProps = ComponentProps<'div'> & { + onUnreadCountChange?: (count: number) => void +} + +export default function ChatIframe({ onUnreadCountChange, ...props }: ChatIframeProps) { + const entity = useAppSelector(state => state.chat.entity) + // const sendEvent = useSendEvent() + + useEffect(() => { + if (!entity) return + const config = generateGrillConfig(entity) + if (!config) return + + const listener = onUnreadCountChange + ? (count: number) => { + console.log('unread count', count) + onUnreadCountChange(count) + } + : undefined + if (listener) { + grill.addUnreadCountListener(listener) + } + + grill.init(config) + + return () => { + if (listener) grill.removeUnreadCountListener(listener) + } + }, [entity]) + + return
+} + +function generateGrillConfig(entity: ChatEntity['entity']): GrillConfig | null { + if (!entity) return null + if (entity.type === 'post') { + const post = entity.data + const title = summarize(post.content?.title ?? post.content?.body ?? '', { limit: 50 }) + const body = summarize(post.content?.body ?? '', { limit: 50 }) + return { + hub: { + id: post.struct.spaceId ?? 'x', + }, + channel: { + type: 'resource', + resource: new Resource({ + schema: 'social', + app: 'polkaverse', + resourceType: 'post', + resourceValue: { + id: post.struct.id, + }, + }), + settings: { + enableLoginButton: true, + enableInputAutofocus: true, + }, + metadata: { + title, + body, + image: post.content?.image, + }, + }, + } + } + + return null +} diff --git a/src/components/chat/ChatSidePanel.module.sass b/src/components/chat/ChatSidePanel.module.sass new file mode 100644 index 000000000..f72947e85 --- /dev/null +++ b/src/components/chat/ChatSidePanel.module.sass @@ -0,0 +1,14 @@ +@import 'src/styles/subsocial-vars.scss' + +.ChatSidePanel + margin-top: -1rem + height: calc(100vh - 64px) + position: sticky + top: 64px + min-width: 350px + max-width: 525px + flex-grow: 1 + box-shadow: 0px 0px 12px 2px #EEECEC + + .ChatIframe + height: 100% diff --git a/src/components/chat/ChatSidePanel.tsx b/src/components/chat/ChatSidePanel.tsx new file mode 100644 index 000000000..44421a89f --- /dev/null +++ b/src/components/chat/ChatSidePanel.tsx @@ -0,0 +1,11 @@ +import clsx from 'clsx' +import ChatIframe from './ChatIframe' +import styles from './ChatSidePanel.module.sass' + +export default function ChatSidePanel() { + return ( +
+ +
+ ) +} diff --git a/src/components/main/PageWrapper.tsx b/src/components/main/PageWrapper.tsx index 965ae7a2d..e28891147 100644 --- a/src/components/main/PageWrapper.tsx +++ b/src/components/main/PageWrapper.tsx @@ -151,7 +151,7 @@ export const PageContent: FC = ({ {/* {isPanels &&
{rightPanel}
} */} {rightPanel} - {!rightPanel && withOnBoarding && showOnBoardingSidebar && myAddress && ( + {rightPanel === undefined && withOnBoarding && showOnBoardingSidebar && myAddress && (
setShowOnBoardingSidebar(false)} />
diff --git a/src/components/posts/view-post/PostPage.tsx b/src/components/posts/view-post/PostPage.tsx index bbc227b39..72fd334f7 100644 --- a/src/components/posts/view-post/PostPage.tsx +++ b/src/components/posts/view-post/PostPage.tsx @@ -3,7 +3,7 @@ import { getPostIdFromSlug } from '@subsocial/utils/slugify' import clsx from 'clsx' import { NextPage } from 'next' import router from 'next/router' -import { FC } from 'react' +import { FC, useEffect } from 'react' import { PageContent } from 'src/components/main/PageWrapper' import AuthorCard from 'src/components/profiles/address-views/AuthorCard' import { useResponsiveSize } from 'src/components/responsive' @@ -15,8 +15,9 @@ import { return404 } from 'src/components/utils/next' import config from 'src/config' import { resolveIpfsUrl } from 'src/ipfs' import { getInitialPropsWithRedux, NextContextWithRedux } from 'src/rtk/app' -import { useOpenCloseChatWindow, useSelectProfile, useSetupGrillConfig } from 'src/rtk/app/hooks' -import { useAppSelector } from 'src/rtk/app/store' +import { useSelectProfile } from 'src/rtk/app/hooks' +import { useAppDispatch, useAppSelector } from 'src/rtk/app/store' +import { setChatConfig } from 'src/rtk/features/chat/chatSlice' import { fetchPost, fetchPosts, selectPost } from 'src/rtk/features/posts/postsSlice' import { useFetchMyReactionsByPostId } from 'src/rtk/features/reactions/myPostReactionsHooks' import { @@ -60,18 +61,16 @@ const InnerPostPage: NextPage = props => { const { isNotMobile } = useResponsiveSize() useFetchMyReactionsByPostId(id) - const setGrillChatVisibility = useOpenCloseChatWindow() - const setupGrillConfig = useSetupGrillConfig() - const postData = useAppSelector(state => selectPost(state, { id })) || initialPostData const { post, space } = postData const { struct, content } = post - const openCommentSection = () => { - setupGrillConfig(space?.id ?? '', post.id, post.content?.title) - setGrillChatVisibility(true) - } + const dispatch = useAppDispatch() + useEffect(() => { + if (!post) return + dispatch(setChatConfig({ data: post, type: 'post' })) + }, []) const profile = useSelectProfile(postData.post.struct.ownerId) const spaceId = space?.id @@ -132,6 +131,8 @@ const InnerPostPage: NextPage = props => { }} withOnBoarding withVoteBanner + outerClassName='mx-auto' + rightPanel={null} >
@@ -186,7 +187,7 @@ const InnerPostPage: NextPage = props => { openCommentSection()} + // toogleCommentSection={() => openCommentSection()} />
diff --git a/src/components/posts/view-post/ViewRegularPreview.tsx b/src/components/posts/view-post/ViewRegularPreview.tsx index 6cce3e5c7..815de2bcd 100644 --- a/src/components/posts/view-post/ViewRegularPreview.tsx +++ b/src/components/posts/view-post/ViewRegularPreview.tsx @@ -1,5 +1,4 @@ import { FC } from 'react' -import { useOpenCloseChatWindow, useSetupGrillConfig } from 'src/rtk/app/hooks' import { SpaceData } from 'src/types' import { InfoPostPreview, PostActionsPanel, PostNotFound } from './helpers' import { PreviewProps } from './PostPreview' @@ -14,16 +13,8 @@ type ComponentType = FC export const RegularPreview: ComponentType = props => { const { postDetails, space, withImage, withTags, withActions } = props - const setGrillChatVisibility = useOpenCloseChatWindow() - const setupGrillConfig = useSetupGrillConfig() - const { isSharedPost } = postDetails.post.struct - const openCommentSection = () => { - setupGrillConfig(space?.id ?? '', postDetails.id, postDetails.post.content?.title) - setGrillChatVisibility(true) - } - return !isSharedPost ? ( <> { withTags={withTags} withMarginForCardType={!withActions} /> - {withActions && ( - openCommentSection()} - preview - /> - )} + {withActions && } ) : ( diff --git a/src/components/posts/view-post/ViewSharedPreview.tsx b/src/components/posts/view-post/ViewSharedPreview.tsx index b09bd1f47..a8d14f816 100644 --- a/src/components/posts/view-post/ViewSharedPreview.tsx +++ b/src/components/posts/view-post/ViewSharedPreview.tsx @@ -1,5 +1,5 @@ import { FC } from 'react' -import { useOpenCloseChatWindow, useSelectPost, useSetupGrillConfig } from 'src/rtk/app/hooks' +import { useSelectPost } from 'src/rtk/app/hooks' import { asSharedPostStruct } from 'src/types' import { InnerPreviewProps } from '.' import { PostActionsPanel, PostCreator, SharePostContent } from './helpers' @@ -12,14 +12,6 @@ export const SharedPreview: ComponentType = props => { const sharedPostId = asSharedPostStruct(postDetails.post.struct).originalPostId postDetails.ext = useSelectPost(sharedPostId) - const setGrillChatVisibility = useOpenCloseChatWindow() - const setupGrillConfig = useSetupGrillConfig() - - const openCommentSection = () => { - setupGrillConfig(space?.id ?? '', postDetails.id, postDetails.post.content?.title) - setGrillChatVisibility(true) - } - return ( <>
@@ -29,14 +21,7 @@ export const SharedPreview: ComponentType = props => {
- {withActions && ( - openCommentSection()} - preview - /> - )} + {withActions && } ) } diff --git a/src/components/responsive/ResponsiveContext.tsx b/src/components/responsive/ResponsiveContext.tsx index c28c0fdd6..ff70db91b 100644 --- a/src/components/responsive/ResponsiveContext.tsx +++ b/src/components/responsive/ResponsiveContext.tsx @@ -7,6 +7,7 @@ export type ResponsiveSizeState = { isMobile: boolean isTablet: boolean isDesktop: boolean + isLargeDesktop: boolean isNotMobile: boolean isNotDesktop: boolean } @@ -16,6 +17,7 @@ const contextStub: ResponsiveSizeState = { isMobile: false, isNotMobile: false, isSmallMobile: true, + isLargeDesktop: false, isTablet: false, isNotDesktop: false, } @@ -24,6 +26,7 @@ export const ResponsiveSizeContext = createContext(contextS export function ResponsiveSizeProvider(props: React.PropsWithChildren) { const value = { + isLargeDesktop: useMediaQuery({ minWidth: 1200 }), isDesktop: useMediaQuery({ minWidth: 992 }), isTablet: useMediaQuery({ minWidth: 768, maxWidth: 991 }), isMobile: useMediaQuery({ maxWidth: 767 }), diff --git a/src/layout/ClientLayout.tsx b/src/layout/ClientLayout.tsx index 471f93819..0d8e2ffc1 100644 --- a/src/layout/ClientLayout.tsx +++ b/src/layout/ClientLayout.tsx @@ -1,6 +1,6 @@ +import dynamic from 'next/dynamic' import React from 'react' import { NotifCounterProvider } from 'src/components/activity/NotifCounter' -import ChatFloatingModal from 'src/components/chat/ChatFloatingModal' import { LazyConnectionsProvider } from 'src/components/lazy-connection/LazyConnectionContext' import OnBoardingContextsWrapper from 'src/components/onboarding/contexts/OnBoardingContextsWrapper' import { ResponsiveSizeProvider } from 'src/components/responsive' @@ -12,6 +12,10 @@ import { SubstrateProvider, SubstrateWebConsole } from '../components/substrate' import SidebarCollapsedProvider from '../components/utils/SideBarCollapsedContext' import { Navigation } from './Navigation' +const ChatFloatingModal = dynamic(() => import('../components/chat/ChatFloatingModal'), { + ssr: false, +}) + initGa(config.ga) const ClientLayout: React.FunctionComponent = ({ children }) => { diff --git a/src/layout/Navigation.tsx b/src/layout/Navigation.tsx index b269ed19e..ec9d27a93 100644 --- a/src/layout/Navigation.tsx +++ b/src/layout/Navigation.tsx @@ -6,6 +6,8 @@ import styles from './Sider.module.sass' import clsx from 'clsx' import dynamic from 'next/dynamic' import { useRouter } from 'next/router' +import ChatSidePanel from 'src/components/chat/ChatSidePanel' +import { useResponsiveSize } from 'src/components/responsive' const TopMenu = dynamic(() => import('./TopMenu'), { ssr: false }) const Menu = dynamic(() => import('./SideMenu'), { ssr: false }) @@ -68,12 +70,16 @@ export const Navigation = (props: Props): JSX.Element => { const { state: { asDrawer }, } = useSidebarCollapsed() + const { isLargeDesktop } = useResponsiveSize() + const { pathname } = useRouter() const content = useMemo( () => {children}, [children], ) + const isPostPage = pathname === '/[spaceId]/[slug]' + return (
@@ -82,6 +88,7 @@ export const Navigation = (props: Props): JSX.Element => { {asDrawer ? : } {content} + {isLargeDesktop && isPostPage && } ) diff --git a/src/rtk/features/chat/chatHooks.ts b/src/rtk/features/chat/chatHooks.ts index 3d64071d9..4c7d458f8 100644 --- a/src/rtk/features/chat/chatHooks.ts +++ b/src/rtk/features/chat/chatHooks.ts @@ -1,41 +1,37 @@ -import grill, { GrillConfig } from '@subsocial/grill-widget' +import { PostData } from '@subsocial/api/types' import { useCallback } from 'react' -import { useAppDispatch, useAppSelector } from 'src/rtk/app/store' -import { setChatWindowVisibility, setCurrentConfig } from '../chat/chatSlice' - -export function useChatWindowOpenState() { - return useAppSelector(state => state.chat.isChatWindowOpen) -} +import { useAppDispatch } from 'src/rtk/app/store' +import { setChatConfig } from '../chat/chatSlice' export function useSetupGrillConfig() { const dispatch = useAppDispatch() - return useCallback((spaceId: string, postId: string, title?: string) => { - const config: GrillConfig = { - hub: { - id: spaceId, - }, - channel: { - type: 'resource', - resource: { - toResourceId: () => postId, - }, - settings: { - enableLoginButton: false, - enableInputAutofocus: true, - }, - metadata: { - title: title ?? '', - }, - }, - } - grill.init(config) - dispatch(setCurrentConfig(config)) - }, []) -} - -export function useOpenCloseChatWindow() { - const dispatch = useAppDispatch() - return useCallback((state: boolean) => { - dispatch(setChatWindowVisibility(state)) + return useCallback((post: PostData) => { + // const title = summarize(post.content?.title ?? post.content?.body ?? '', { limit: 50 }) + // const config: GrillConfig = { + // hub: { + // id: post.struct.spaceId ?? 'x', + // }, + // channel: { + // type: 'resource', + // resource: new Resource({ + // schema: 'social', + // app: 'polkaverse', + // resourceType: 'post', + // resourceValue: { + // id: post.struct.id, + // }, + // }), + // settings: { + // enableLoginButton: true, + // enableInputAutofocus: true, + // }, + // metadata: { + // title, + // body: post.content?.body, + // image: post.content?.image, + // }, + // }, + // } + dispatch(setChatConfig({ type: 'post', data: post })) }, []) } diff --git a/src/rtk/features/chat/chatSlice.ts b/src/rtk/features/chat/chatSlice.ts index e76c15de8..aca00ec21 100644 --- a/src/rtk/features/chat/chatSlice.ts +++ b/src/rtk/features/chat/chatSlice.ts @@ -1,30 +1,30 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit' -import { GrillConfig } from '@subsocial/grill-widget' +import { PostData } from '@subsocial/api/types' const sliceName = 'chats' -export interface chatEntity { - isChatWindowOpen: boolean - currentConfig?: GrillConfig +type Entity = { + type: 'post' + data: PostData +} | null +export interface ChatEntity { + entity: Entity } -const initialState: chatEntity = { - isChatWindowOpen: false, +const initialState: ChatEntity = { + entity: null, } const slice = createSlice({ name: sliceName, initialState, reducers: { - setChatWindowVisibility: (state, action: PayloadAction) => { - state.isChatWindowOpen = action.payload - }, - setCurrentConfig: (state, action: PayloadAction) => { - state.currentConfig = action.payload + setChatConfig: (state, action: PayloadAction) => { + state.entity = action.payload }, }, }) -export const { setChatWindowVisibility, setCurrentConfig } = slice.actions +export const { setChatConfig } = slice.actions export default slice.reducer diff --git a/yarn.lock b/yarn.lock index a10ba6853..16860826e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2336,10 +2336,17 @@ "@elastic/elasticsearch" "7.4.0" "@subsocial/utils" latest -"@subsocial/grill-widget@^0.0.8": - version "0.0.8" - resolved "https://registry.yarnpkg.com/@subsocial/grill-widget/-/grill-widget-0.0.8.tgz#a8a584daffdac4edbb319ec59579a0bdd0004441" - integrity sha512-dRPBcn+LU4LLVzHmVCm3MuIVc6dRZ2vC80PAys05NBVTdak+Jz1RQbc1yozPg6ReHgzZhQhU5z+ErYsYRvF09Q== +"@subsocial/grill-widget@^0.0.11": + version "0.0.11" + resolved "https://registry.yarnpkg.com/@subsocial/grill-widget/-/grill-widget-0.0.11.tgz#c77267b1c1344b5535fadfc5c1d7b062862f331f" + integrity sha512-2XOUbd467AeRtPkv2W3oYYfHGJCXAWEDMx6NpXdYs4AMOm1ZtHPjknrQ88/dcZCxDl26XcCRCSysu9n/6qPu2w== + +"@subsocial/resource-discussions@^0.0.3": + version "0.0.3" + resolved "https://registry.yarnpkg.com/@subsocial/resource-discussions/-/resource-discussions-0.0.3.tgz#18a13c8ad27b039328e8fc8ac330b7f923a271cc" + integrity sha512-XCrDFbypAyTibLgvEPYJkqI1uDoX+wmnyqis8Hrher6Y9QFGiG7gPnNk/Bmv5zmiEiz9HJ5k+C6IwbiS5B+KyQ== + dependencies: + graphology "^0.25.1" "@subsocial/utils@0.8.10", "@subsocial/utils@latest": version "0.8.10" @@ -6283,7 +6290,7 @@ eventemitter3@^5.0.0: resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-5.0.0.tgz#084eb7f5b5388df1451e63f4c2aafd71b217ccb3" integrity sha512-riuVbElZZNXLeLEoprfNYoDSwTBRR44X3mnhdI1YcnENpWTCsTTVZ2zFuqQcpoyqPQIUXdiPEU0ECAq0KQRaHg== -events@^3.0.0: +events@^3.0.0, events@^3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== @@ -7063,6 +7070,14 @@ graphlib@^2.1.8: dependencies: lodash "^4.17.15" +graphology@^0.25.1: + version "0.25.4" + resolved "https://registry.yarnpkg.com/graphology/-/graphology-0.25.4.tgz#e528a64555ac1f392a9d965321ada5b2b843efe1" + integrity sha512-33g0Ol9nkWdD6ulw687viS8YJQBxqG5LWII6FI6nul0pq6iM2t5EKquOTFDbyTblRB3O9I+7KX4xI8u5ffekAQ== + dependencies: + events "^3.3.0" + obliterator "^2.0.2" + graphql-tag@2.12.6, graphql-tag@^2.10.1, graphql-tag@^2.11.0, graphql-tag@^2.12.6: version "2.12.6" resolved "https://registry.yarnpkg.com/graphql-tag/-/graphql-tag-2.12.6.tgz#d441a569c1d2537ef10ca3d1633b48725329b5f1" @@ -10187,6 +10202,11 @@ object.values@^1.1.6: define-properties "^1.1.4" es-abstract "^1.20.4" +obliterator@^2.0.2: + version "2.0.4" + resolved "https://registry.yarnpkg.com/obliterator/-/obliterator-2.0.4.tgz#fa650e019b2d075d745e44f1effeb13a2adbe816" + integrity sha512-lgHwxlxV1qIg1Eap7LgIeoBWIMFibOjbrYPIPJZcI1mmGAI2m3lNYpK12Y+GBdPQ0U1hRwSord7GIaawz962qQ== + omit.js@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/omit.js/-/omit.js-1.0.2.tgz#91a14f0eba84066dfa015bf30e474c47f30bc858" From c0b4b6bf8cc4ce29c3229ceb8930527e39b54be2 Mon Sep 17 00:00:00 2001 From: teodorus-nathaniel Date: Mon, 4 Sep 2023 22:39:37 +0700 Subject: [PATCH 03/17] Add analytic and fix resource linking space issue --- .github/workflows/build-deploy.yml | 6 +- .github/workflows/feature-based.yaml | 7 +- ci.env | 1 + docker/Dockerfile | 5 +- export-env.js | 1 + package.json | 1 + src/components/chat/ChatFloatingModal.tsx | 13 ++- src/components/chat/ChatIframe.tsx | 20 ++++- src/config/env.ts | 1 + src/hooks/useWrapInRef.ts | 26 ++++++ src/pages/_app.js | 17 ++-- src/providers/AnalyticContext.tsx | 104 ++++++++++++++++++++++ yarn.lock | 59 ++++++++++++ 13 files changed, 237 insertions(+), 24 deletions(-) create mode 100644 src/hooks/useWrapInRef.ts create mode 100644 src/providers/AnalyticContext.tsx diff --git a/.github/workflows/build-deploy.yml b/.github/workflows/build-deploy.yml index 42b7d1f14..411d5ca78 100644 --- a/.github/workflows/build-deploy.yml +++ b/.github/workflows/build-deploy.yml @@ -5,8 +5,8 @@ on: - staging - main concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true jobs: front_build: @@ -72,6 +72,7 @@ jobs: GH_GA_ID=UA-204866512-2 GH_APP_KIND=polkaverse GH_HCAPTCHA_SITE_KEY=${{ secrets.PROD_HCAPTCHA_SITE_KEY }} + GH_AMP_ID=d9f7a97f3cdc1eb7b4298af4f17c202b GH_OFFCHAIN_SIGNER_URL=https://signer.subsocial.network GH_CONNECTION_KIND=main @@ -156,7 +157,6 @@ jobs: - name: Recreate HPA run: kubectl create -f $GITHUB_WORKSPACE/deployment/hpa/hpa.yaml - front-bk-prod-deploy: name: bk-prod-deploy needs: front_build diff --git a/.github/workflows/feature-based.yaml b/.github/workflows/feature-based.yaml index 9ec122ef3..351df3fe0 100644 --- a/.github/workflows/feature-based.yaml +++ b/.github/workflows/feature-based.yaml @@ -5,8 +5,8 @@ on: branches: - deploy/** concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true jobs: front_build: @@ -28,7 +28,7 @@ jobs: - name: Set up Docker context for buildx id: buildx-context run: | - docker context create builders + docker context create builders - name: Set up Docker Buildx id: buildx @@ -62,6 +62,7 @@ jobs: GH_GA_ID=fake GH_APP_KIND=polkaverse GH_HCAPTCHA_SITE_KEY=3beeddac-2dce-41cc-8e18-338118426c38 + GH_AMP_ID=a4dc29d356f19fd9788f24b493b462bf GH_OFFCHAIN_SIGNER_URL=https://signer.subsocial.network GH_CONNECTION_KIND=dev tags: | diff --git a/ci.env b/ci.env index 624f7cd57..39c4ecdfd 100644 --- a/ci.env +++ b/ci.env @@ -11,3 +11,4 @@ APP_KIND='$GH_APP_KIND' APP_BASE_URL='$GH_APP_BASE_URL' HCAPTCHA_SITE_KEY='$GH_HCAPTCHA_SITE_KEY' OFFCHAIN_SIGNER_URL='$GH_OFFCHAIN_SIGNER_URL' +AMP_ID='$GH_AMP_ID' diff --git a/docker/Dockerfile b/docker/Dockerfile index 035f21828..4e3e6a0e8 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -10,6 +10,7 @@ ARG GH_CONNECTION_KIND ARG GH_APP_KIND ARG GH_APP_BASE_URL ARG GH_HCAPTCHA_SITE_KEY +ARG GH_AMP_ID ARG GH_OFFCHAIN_SIGNER_URL ENV SUBSTRATE_URL=${GH_SUBSTRATE_URL} \ @@ -21,6 +22,7 @@ ENV SUBSTRATE_URL=${GH_SUBSTRATE_URL} \ APP_KIND=${GH_APP_KIND} \ APP_BASE_URL=${GH_APP_BASE_URL} \ HCAPTCHA_SITE_KEY=${GH_HCAPTCHA_SITE_KEY} \ + AMP_ID=${GH_AMP_ID} \ OFFCHAIN_SIGNER_URL=${GH_OFFCHAIN_SIGNER_URL} \ CONNECTION_KIND=${GH_CONNECTION_KIND} @@ -50,6 +52,7 @@ ARG GH_CONNECTION_KIND ARG GH_APP_KIND ARG GH_APP_BASE_URL ARG GH_HCAPTCHA_SITE_KEY +ARG GH_AMP_ID ARG GH_OFFCHAIN_SIGNER_URL ENV SUBSTRATE_URL=${GH_SUBSTRATE_URL} \ @@ -60,7 +63,7 @@ ENV SUBSTRATE_URL=${GH_SUBSTRATE_URL} \ GA_ID=${GH_GA_ID} \ APP_KIND=${GH_APP_KIND} \ APP_BASE_URL=${GH_APP_BASE_URL} \ - HCAPTCHA_SITE_KEY=${GH_HCAPTCHA_SITE_KEY} \ + AMP_ID=${GH_AMP_ID} \ OFFCHAIN_SIGNER_URL=${GH_OFFCHAIN_SIGNER_URL} \ CONNECTION_KIND=${GH_CONNECTION_KIND} diff --git a/export-env.js b/export-env.js index 67aa87e70..ff3a7d634 100755 --- a/export-env.js +++ b/export-env.js @@ -29,6 +29,7 @@ const varsToExport = [ 'MAINTENANCE_TEXT', 'GA_ID', + 'AMP_ID', 'HCAPTCHA_SITE_KEY', diff --git a/package.json b/package.json index 130c70132..a9d687ba0 100644 --- a/package.json +++ b/package.json @@ -81,6 +81,7 @@ "typescript": "^4.8.4" }, "dependencies": { + "@amplitude/analytics-browser": "^2.2.3", "@ant-design/icons": "^4.2.1", "@antv/g2plot": "^2.3.10", "@apollo/client": "^3.7.10", diff --git a/src/components/chat/ChatFloatingModal.tsx b/src/components/chat/ChatFloatingModal.tsx index 642d3c655..734fa1028 100644 --- a/src/components/chat/ChatFloatingModal.tsx +++ b/src/components/chat/ChatFloatingModal.tsx @@ -3,15 +3,14 @@ import clsx from 'clsx' import { useEffect, useRef, useState } from 'react' import { createPortal } from 'react-dom' import { HiChevronDown } from 'react-icons/hi2' +import { useSendEvent } from 'src/providers/AnalyticContext' import { useResponsiveSize } from '../responsive' import styles from './ChatFloatingModal.module.sass' import ChatIframe from './ChatIframe' -// import { useResponsiveSize } from '../responsive' -// import { useSendEvent } from '../providers/AnalyticContext' export default function ChatFloatingModal() { const { isLargeDesktop } = useResponsiveSize() - // const sendEvent = useSendEvent() + const sendEvent = useSendEvent() const [unreadCount, setUnreadCount] = useState(0) const [isOpen, setIsOpen] = useState(false) @@ -25,15 +24,15 @@ export default function ChatFloatingModal() { const hasOpened = useRef(false) const toggleChat = () => { - // let event + let event if (isOpen) { - // event = 'close_grill_iframe' + event = 'close_grill_iframe' } else { - // event = 'open_grill_iframe' + event = 'open_grill_iframe' setUnreadCount(0) localStorage.setItem('unreadCount', '0') } - // sendEvent(event) + sendEvent(event) setIsOpen(prev => !prev) hasOpened.current = true diff --git a/src/components/chat/ChatIframe.tsx b/src/components/chat/ChatIframe.tsx index 4e5b1bcac..56a29a523 100644 --- a/src/components/chat/ChatIframe.tsx +++ b/src/components/chat/ChatIframe.tsx @@ -3,6 +3,8 @@ import { Resource } from '@subsocial/resource-discussions' import { summarize } from '@subsocial/utils' import clsx from 'clsx' import { ComponentProps, useEffect } from 'react' +import useWrapInRef from 'src/hooks/useWrapInRef' +import { useSendEvent } from 'src/providers/AnalyticContext' import { useAppSelector } from 'src/rtk/app/store' import { ChatEntity } from 'src/rtk/features/chat/chatSlice' @@ -10,14 +12,26 @@ export type ChatIframeProps = ComponentProps<'div'> & { onUnreadCountChange?: (count: number) => void } +const COMMENTS_HUB_ID = '1032' + export default function ChatIframe({ onUnreadCountChange, ...props }: ChatIframeProps) { const entity = useAppSelector(state => state.chat.entity) - // const sendEvent = useSendEvent() + const sendEvent = useSendEvent() + const sendEventRef = useWrapInRef(sendEvent) useEffect(() => { if (!entity) return const config = generateGrillConfig(entity) if (!config) return + config.onWidgetCreated = iframe => { + iframe.onerror = () => { + sendEventRef.current('chat_widget_error') + } + iframe.onmouseenter = () => { + sendEventRef.current('chat_widget_mouse_enter') + } + return iframe + } const listener = onUnreadCountChange ? (count: number) => { @@ -34,7 +48,7 @@ export default function ChatIframe({ onUnreadCountChange, ...props }: ChatIframe return () => { if (listener) grill.removeUnreadCountListener(listener) } - }, [entity]) + }, [entity, sendEventRef]) return
} @@ -47,7 +61,7 @@ function generateGrillConfig(entity: ChatEntity['entity']): GrillConfig | null { const body = summarize(post.content?.body ?? '', { limit: 50 }) return { hub: { - id: post.struct.spaceId ?? 'x', + id: COMMENTS_HUB_ID, }, channel: { type: 'resource', diff --git a/src/config/env.ts b/src/config/env.ts index e59798f69..0a2216411 100644 --- a/src/config/env.ts +++ b/src/config/env.ts @@ -15,6 +15,7 @@ export const enableMaintenancePage = getEnvAsBool('ENABLE_MAINTENANCE_PAGE') export const maintenanceMsg = getEnv('MAINTENANCE_TEXT') export const gaId = getEnv('GA_ID') || '' +export const ampId = getEnv('AMP_ID') || '' export const hCaptchaSiteKey = getEnv('HCAPTCHA_SITE_KEY') || '' diff --git a/src/hooks/useWrapInRef.ts b/src/hooks/useWrapInRef.ts new file mode 100644 index 000000000..52ae4aa0d --- /dev/null +++ b/src/hooks/useWrapInRef.ts @@ -0,0 +1,26 @@ +import { useRef } from 'react' + +/** + * This function just wraps anything into a ref, so that it can be used in useEffect dependencies + * while using the latest version of the data. + * + * One example of this is when you want to use a callback in useEffect, + * but the function itself is using a state variable, and you want to use the latest version of the state variable. + * + * @example + * const [state, setState] = useState(0) + * const callback = () => { + * console.log(state) + * } + * const ref = useWrapInRef(callback) + * useEffect(() => { + * ref.current() // if you do not wrap it, you can remove the callback from the dependencies, but then it will use the old state and there will be a warning + * }, [ref]) + * @param data {T} + * @returns wrappedData {React.MutableRefObject} + */ +export default function useWrapInRef(data: T) { + const ref = useRef(data) + ref.current = data + return ref +} diff --git a/src/pages/_app.js b/src/pages/_app.js index 6ecbaa07c..145a87bf1 100644 --- a/src/pages/_app.js +++ b/src/pages/_app.js @@ -33,6 +33,7 @@ import config from 'src/config' import '@subsocial/definitions/interfaces/types-lookup' import '@subsocial/definitions/interfaces/augment-types' import '@subsocial/definitions/interfaces/augment-api' +import AnalyticProvider from 'src/providers/AnalyticContext' dayjs.extend(relativeTime) dayjs.extend(localizedFormat) @@ -70,13 +71,15 @@ function MyApp(props) { /> {/* */} - - - - - - - + + + + + + + + + ) diff --git a/src/providers/AnalyticContext.tsx b/src/providers/AnalyticContext.tsx new file mode 100644 index 000000000..6092d10bd --- /dev/null +++ b/src/providers/AnalyticContext.tsx @@ -0,0 +1,104 @@ +import { createInstance } from '@amplitude/analytics-browser' +import { BaseEvent, BrowserClient } from '@amplitude/analytics-types' +import React, { + createContext, + useCallback, + useContext, + useEffect, + useMemo, + useRef, + useState, +} from 'react' +import { ampId } from 'src/config/env' + +type AnalyticContextState = { + amp: BrowserClient | null + deviceId?: string +} + +const initialState: AnalyticContextState = { + amp: null, + deviceId: undefined, +} + +export type AnalyticContextProps = { + sendEvent: (name: string) => void +} + +const propsStub = { sendEvent: () => undefined } +export const AnalyticContext = createContext(propsStub) + +export async function createAmplitudeInstance() { + if (typeof window === 'undefined') return null + if (!ampId) return null + + try { + const amp = createInstance() + await amp.init(ampId, undefined, { identityStorage: 'localStorage' }).promise + return amp + } catch (e) { + console.error('Error initializing amplitude', e) + return null + } +} + +export function AnalyticProvider(props: React.PropsWithChildren<{}>) { + const [state, setState] = useState(initialState) + const [queuedEvents, setQueuedEvents] = useState([]) + const isInited = useRef(false) + + useEffect(() => { + if (isInited.current) return + isInited.current = true + + async function initAmp() { + const amp = await createAmplitudeInstance() + + let deviceId = localStorage.getItem('device_id') || undefined + if (!deviceId) { + deviceId = amp?.getDeviceId() + } + + setState({ amp, deviceId }) + queuedEvents.forEach(props => { + amp?.logEvent({ + ...props, + device_id: deviceId, + }) + }) + } + initAmp() + }, []) + + const contextValue: AnalyticContextProps = useMemo(() => { + return { + sendEvent: (name: string) => { + const eventProps = { + event_type: name, + device_id: state.deviceId, + } + if (!state.amp) { + setQueuedEvents(prev => [...prev, eventProps]) + return + } + state.amp.logEvent(eventProps) + }, + } + }, [state]) + + return {props.children} +} + +export function useSendEvent() { + return useContext(AnalyticContext).sendEvent +} + +export function useBuildSendEvent(eventName: string) { + const sendEvent = useSendEvent() + + return useCallback(() => { + sendEvent(eventName) + }, [eventName]) +} + +export default AnalyticProvider diff --git a/yarn.lock b/yarn.lock index 16860826e..fc9637749 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7,6 +7,65 @@ resolved "https://registry.yarnpkg.com/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz#bd9154aec9983f77b3a034ecaa015c2e4201f6cf" integrity sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA== +"@amplitude/analytics-browser@^2.2.3": + version "2.2.3" + resolved "https://registry.yarnpkg.com/@amplitude/analytics-browser/-/analytics-browser-2.2.3.tgz#7dbe4ad2ada7facfcf21aac4b47314f8e4c2903c" + integrity sha512-vuKG8/jqtsAFe0xK0ZGtXDxH7oPx989VIoCpUi97bkfxholySCNHjVMd5Q8D4Mqm/3eDH7YZhkFwEb0JFyCAfA== + dependencies: + "@amplitude/analytics-client-common" "^2.0.5" + "@amplitude/analytics-core" "^2.0.4" + "@amplitude/analytics-types" "^2.1.2" + "@amplitude/plugin-page-view-tracking-browser" "^2.0.9" + "@amplitude/plugin-web-attribution-browser" "^2.0.9" + tslib "^2.4.1" + +"@amplitude/analytics-client-common@^2.0.5": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@amplitude/analytics-client-common/-/analytics-client-common-2.0.5.tgz#164140e24529f6de3215db4f3e3c1bf3dbbe3267" + integrity sha512-5BrGl188h4Ayx4Z2e1x4I3Z8ykC+ap65cy8ShBByiaBBrR40gnXSuLZR7xeex3lvTp2b5lMBcVCqArdRbeZrgQ== + dependencies: + "@amplitude/analytics-connector" "^1.4.8" + "@amplitude/analytics-core" "^2.0.4" + "@amplitude/analytics-types" "^2.1.2" + tslib "^2.4.1" + +"@amplitude/analytics-connector@^1.4.8": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@amplitude/analytics-connector/-/analytics-connector-1.5.0.tgz#89a78b8c6463abe4de1d621db4af6c62f0d62b0a" + integrity sha512-T8mOYzB9RRxckzhL0NTHwdge9xuFxXEOplC8B1Y3UX3NHa3BLh7DlBUZlCOwQgMc2nxDfnSweDL5S3bhC+W90g== + +"@amplitude/analytics-core@^2.0.4": + version "2.0.4" + resolved "https://registry.yarnpkg.com/@amplitude/analytics-core/-/analytics-core-2.0.4.tgz#a4ea5c95e95ff75d760a6b57a2ed2be2c763f244" + integrity sha512-AM4g1ucaAJuFqaMBg7FiqwKHveyV2QpZ3yPxw3OxNCgZz2QmqeYE1bp47x4FlfzNsoGyuYqRKs1mCbmGobAYWA== + dependencies: + "@amplitude/analytics-types" "^2.1.2" + tslib "^2.4.1" + +"@amplitude/analytics-types@^2.1.2": + version "2.1.2" + resolved "https://registry.yarnpkg.com/@amplitude/analytics-types/-/analytics-types-2.1.2.tgz#7a2a7a22fba192639189bb03eda5dd8287487cf2" + integrity sha512-ASKwH9g+5gglTHr7h7miK8J/ofIzuEtGRDCjnZAtRbE6+laoOfCLYPPJXMYz0k1x+rIhLO/6I6WWjT7zchmpyA== + +"@amplitude/plugin-page-view-tracking-browser@^2.0.9": + version "2.0.9" + resolved "https://registry.yarnpkg.com/@amplitude/plugin-page-view-tracking-browser/-/plugin-page-view-tracking-browser-2.0.9.tgz#b7bb91b1369afff13ea20c519b7961413a5fa1a0" + integrity sha512-OjhAxvQ52lDcRap2sjUbYEcSM6bzeDa6SdBx6vCaeXswvjafcH9LeDPawLUHgaqvQJiAhA7lxrQ7ThfH0P6c0g== + dependencies: + "@amplitude/analytics-client-common" "^2.0.5" + "@amplitude/analytics-types" "^2.1.2" + tslib "^2.4.1" + +"@amplitude/plugin-web-attribution-browser@^2.0.9": + version "2.0.9" + resolved "https://registry.yarnpkg.com/@amplitude/plugin-web-attribution-browser/-/plugin-web-attribution-browser-2.0.9.tgz#0d24f2da764846601f59484a8651d58918ef89d7" + integrity sha512-QrNgieAEXEBbtnsxYzfeJl2U/5XwCCvO3Dg0hntAtnTdu1A3HlN5ItRtoHg0jGBbu4jpSbvag72UlkCotqJ+Yg== + dependencies: + "@amplitude/analytics-client-common" "^2.0.5" + "@amplitude/analytics-core" "^2.0.4" + "@amplitude/analytics-types" "^2.1.2" + tslib "^2.4.1" + "@ampproject/remapping@^2.2.0": version "2.2.1" resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.2.1.tgz#99e8e11851128b8702cd57c33684f1d0f260b630" From 595d7472efac4ad0278ce20696c160f653fe1ba3 Mon Sep 17 00:00:00 2001 From: teodorus-nathaniel Date: Mon, 4 Sep 2023 23:16:34 +0700 Subject: [PATCH 04/17] Add functionality for toggling chat open --- src/components/chat/ChatFloatingModal.tsx | 31 +++++++++--- src/components/posts/PostStats.tsx | 11 +++-- src/components/posts/view-post/PostPage.tsx | 24 +++++++--- src/components/posts/view-post/helpers.tsx | 34 ++------------ src/rtk/features/chat/chatHooks.ts | 52 ++++++++------------- src/rtk/features/chat/chatSlice.ts | 7 ++- 6 files changed, 76 insertions(+), 83 deletions(-) diff --git a/src/components/chat/ChatFloatingModal.tsx b/src/components/chat/ChatFloatingModal.tsx index 734fa1028..d24c34c09 100644 --- a/src/components/chat/ChatFloatingModal.tsx +++ b/src/components/chat/ChatFloatingModal.tsx @@ -4,6 +4,9 @@ import { useEffect, useRef, useState } from 'react' import { createPortal } from 'react-dom' import { HiChevronDown } from 'react-icons/hi2' import { useSendEvent } from 'src/providers/AnalyticContext' +import { useChatOpenState } from 'src/rtk/app/hooks' +import { useAppSelector } from 'src/rtk/app/store' +import { ChatEntity } from 'src/rtk/features/chat/chatSlice' import { useResponsiveSize } from '../responsive' import styles from './ChatFloatingModal.module.sass' import ChatIframe from './ChatIframe' @@ -11,16 +14,19 @@ import ChatIframe from './ChatIframe' export default function ChatFloatingModal() { const { isLargeDesktop } = useResponsiveSize() const sendEvent = useSendEvent() + const [isOpen, setIsOpen] = useChatOpenState() + const entity = useAppSelector(state => state.chat.entity) const [unreadCount, setUnreadCount] = useState(0) - const [isOpen, setIsOpen] = useState(false) useEffect(() => { - const unreadCountFromStorage = parseInt(localStorage.getItem('unreadCount') ?? '') + if (!entity) return + + const unreadCountFromStorage = getUnreadCount(entity) if (unreadCountFromStorage && !isNaN(unreadCountFromStorage)) { setUnreadCount(unreadCountFromStorage) } - }, []) + }, [entity]) const hasOpened = useRef(false) const toggleChat = () => { @@ -30,11 +36,11 @@ export default function ChatFloatingModal() { } else { event = 'open_grill_iframe' setUnreadCount(0) - localStorage.setItem('unreadCount', '0') + if (entity) saveUnreadCount(entity, 0) } sendEvent(event) - setIsOpen(prev => !prev) + setIsOpen(!isOpen) hasOpened.current = true } @@ -43,12 +49,14 @@ export default function ChatFloatingModal() { } const onUnreadCountChange = (count: number) => { - if (count > 0) { + if (count > 0 && entity) { setUnreadCount(count) - localStorage.setItem('unreadCount', count.toString()) + saveUnreadCount(entity, count) } } + if (!entity) return null + return ( <> {createPortal( @@ -78,3 +86,12 @@ export default function ChatFloatingModal() { ) } + +type Entity = NonNullable +function getUnreadCount(entity: Entity) { + return parseInt(localStorage.getItem(`unreadCount:${entity.type}:${entity.data.id}`) ?? '') ?? 0 +} + +function saveUnreadCount(entity: Entity, count: number) { + localStorage.setItem(`unreadCount:${entity.type}:${entity.data.id}`, count.toString() ?? '0') +} diff --git a/src/components/posts/PostStats.tsx b/src/components/posts/PostStats.tsx index 902830c94..7b2621fac 100644 --- a/src/components/posts/PostStats.tsx +++ b/src/components/posts/PostStats.tsx @@ -1,6 +1,7 @@ import { nonEmptyStr } from '@subsocial/utils' import clsx from 'clsx' import { useState } from 'react' +import { useSetChatOpen } from 'src/rtk/app/hooks' import { idToBn, PostStruct } from 'src/types' import { MutedSpan } from '../utils/MutedText' import { Pluralize } from '../utils/Plularize' @@ -14,16 +15,16 @@ type StatsProps = { export const StatsPanel = (props: StatsProps) => { const { post, goToCommentsId } = props - const [commentsSection, setCommentsSection] = useState(false) + const setChatOpen = useSetChatOpen() const [postVotersOpen, setPostVotersOpen] = useState(false) const { upvotesCount, downvotesCount, repliesCount, sharesCount, id } = post const reactionsCount = upvotesCount + downvotesCount const showReactionsModal = () => reactionsCount && setPostVotersOpen(true) - const toggleCommentsSection = goToCommentsId - ? undefined - : () => setCommentsSection(!commentsSection) + const toggleCommentsSection = () => { + setChatOpen(true) + } const comments = return ( @@ -36,7 +37,7 @@ export const StatsPanel = (props: StatsProps) => { {nonEmptyStr(goToCommentsId) ? ( - + {comments} ) : ( diff --git a/src/components/posts/view-post/PostPage.tsx b/src/components/posts/view-post/PostPage.tsx index 72fd334f7..0f1710252 100644 --- a/src/components/posts/view-post/PostPage.tsx +++ b/src/components/posts/view-post/PostPage.tsx @@ -15,9 +15,8 @@ import { return404 } from 'src/components/utils/next' import config from 'src/config' import { resolveIpfsUrl } from 'src/ipfs' import { getInitialPropsWithRedux, NextContextWithRedux } from 'src/rtk/app' -import { useSelectProfile } from 'src/rtk/app/hooks' -import { useAppDispatch, useAppSelector } from 'src/rtk/app/store' -import { setChatConfig } from 'src/rtk/features/chat/chatSlice' +import { useSelectProfile, useSetChatEntityConfig, useSetChatOpen } from 'src/rtk/app/hooks' +import { useAppSelector } from 'src/rtk/app/store' import { fetchPost, fetchPosts, selectPost } from 'src/rtk/features/posts/postsSlice' import { useFetchMyReactionsByPostId } from 'src/rtk/features/reactions/myPostReactionsHooks' import { @@ -66,10 +65,23 @@ const InnerPostPage: NextPage = props => { const { post, space } = postData const { struct, content } = post - const dispatch = useAppDispatch() + const setChatConfig = useSetChatEntityConfig() useEffect(() => { if (!post) return - dispatch(setChatConfig({ data: post, type: 'post' })) + setChatConfig({ data: post, type: 'post' }) + + return () => { + setChatConfig(null) + } + }, []) + + const setChatOpen = useSetChatOpen() + const goToCommentsId = 'comments' + useEffect(() => { + const hash = window.location.hash.substring(1) + if (hash === goToCommentsId) { + setChatOpen(true) + } }, []) const profile = useSelectProfile(postData.post.struct.ownerId) @@ -93,8 +105,6 @@ const InnerPostPage: NextPage = props => { body = parseTwitterTextToMarkdown(body) } - const goToCommentsId = 'comments' - const renderResponseTitle = (parentPost?: PostData) => { if (!parentPost || !parentPost.content) return null diff --git a/src/components/posts/view-post/helpers.tsx b/src/components/posts/view-post/helpers.tsx index f749d35c2..3a28baa88 100644 --- a/src/components/posts/view-post/helpers.tsx +++ b/src/components/posts/view-post/helpers.tsx @@ -1,8 +1,7 @@ -import { MessageOutlined } from '@ant-design/icons' import { BN } from '@polkadot/util' import { PostId } from '@subsocial/api/types/substrate' import { isEmptyObj, isEmptyStr } from '@subsocial/utils' -import { Alert, Button, Image, Tooltip } from 'antd' +import { Alert, Image, Tooltip } from 'antd' import clsx from 'clsx' import isEmpty from 'lodash.isempty' import Error from 'next/error' @@ -35,7 +34,7 @@ import { useSelectSpace } from '../../../rtk/features/spaces/spacesHooks' import { useIsMyAddress } from '../../auth/MyAccountsContext' import AuthorPreview from '../../profiles/address-views/AuthorPreview' import { SpaceNameAsLink } from '../../spaces/ViewSpace' -import { formatDate, IconWithLabel, isHidden, toShortUrl, useIsVisible } from '../../utils' +import { formatDate, isHidden, toShortUrl, useIsVisible } from '../../utils' import { SummarizeMd } from '../../utils/md/SummarizeMd' import ViewTags from '../../utils/ViewTags' import Embed from '../embed/Embed' @@ -285,34 +284,8 @@ type PostActionsPanelProps = { withBorder?: boolean } -const ShowCommentsAction = (props: PostActionsPanelProps) => { - const { postDetails, preview, toogleCommentSection } = props - const { - post: { - struct: { repliesCount }, - }, - } = postDetails - const title = 'Comment' - - return ( - - } - count={repliesCount || 0} - label={!preview ? title : undefined} - /> - - ) -} - -const Action: FC<{ onClick?: () => void; title?: string }> = ({ children, onClick, title }) => ( - -) - export const PostActionsPanel: FC = props => { - const { postDetails, space, preview, withBorder, toogleCommentSection } = props + const { postDetails, space, preview, withBorder } = props const { post: { struct }, } = postDetails @@ -330,7 +303,6 @@ export const PostActionsPanel: FC = props => {
)} - {toogleCommentSection && } { - // const title = summarize(post.content?.title ?? post.content?.body ?? '', { limit: 50 }) - // const config: GrillConfig = { - // hub: { - // id: post.struct.spaceId ?? 'x', - // }, - // channel: { - // type: 'resource', - // resource: new Resource({ - // schema: 'social', - // app: 'polkaverse', - // resourceType: 'post', - // resourceValue: { - // id: post.struct.id, - // }, - // }), - // settings: { - // enableLoginButton: true, - // enableInputAutofocus: true, - // }, - // metadata: { - // title, - // body: post.content?.body, - // image: post.content?.image, - // }, - // }, - // } - dispatch(setChatConfig({ type: 'post', data: post })) + return useCallback((...params: Parameters) => { + dispatch(setChatConfig(...params)) }, []) } + +export function useSetChatOpen() { + const dispatch = useAppDispatch() + const setIsOpen = useCallback((isOpen: boolean) => { + dispatch(setChatOpen(isOpen)) + }, []) + return setIsOpen +} + +export function useChatOpenState() { + const isOpen = useAppSelector(state => state.chat.isOpen) + const setIsOpen = useSetChatOpen() + + return [isOpen, setIsOpen] as const +} diff --git a/src/rtk/features/chat/chatSlice.ts b/src/rtk/features/chat/chatSlice.ts index aca00ec21..4b03385ae 100644 --- a/src/rtk/features/chat/chatSlice.ts +++ b/src/rtk/features/chat/chatSlice.ts @@ -8,10 +8,12 @@ type Entity = { data: PostData } | null export interface ChatEntity { + isOpen: boolean entity: Entity } const initialState: ChatEntity = { + isOpen: false, entity: null, } @@ -19,12 +21,15 @@ const slice = createSlice({ name: sliceName, initialState, reducers: { + setChatOpen: (state, action: PayloadAction) => { + state.isOpen = action.payload + }, setChatConfig: (state, action: PayloadAction) => { state.entity = action.payload }, }, }) -export const { setChatConfig } = slice.actions +export const { setChatConfig, setChatOpen } = slice.actions export default slice.reducer From a2e0e4869840fdc342c517e5790e49a157075905 Mon Sep 17 00:00:00 2001 From: teodorus-nathaniel Date: Mon, 4 Sep 2023 23:22:38 +0700 Subject: [PATCH 05/17] Remove border from chat button --- src/components/chat/ChatFloatingModal.module.sass | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/chat/ChatFloatingModal.module.sass b/src/components/chat/ChatFloatingModal.module.sass index f07b8efea..d7b17a01e 100644 --- a/src/components/chat/ChatFloatingModal.module.sass +++ b/src/components/chat/ChatFloatingModal.module.sass @@ -28,6 +28,7 @@ align-items: center background: linear-gradient(95.39deg, #C43333 9.79%, #F9A11E 135.53%) !important gap: $space_tiny + border: none &:hover, &:focus filter: brightness(1.1) From f5be6ed52776eb7cc7a7290cf32599feb2451ce3 Mon Sep 17 00:00:00 2001 From: teodorus-nathaniel Date: Tue, 5 Sep 2023 00:26:07 +0700 Subject: [PATCH 06/17] Setup theme and font size --- src/components/chat/ChatIframe.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/components/chat/ChatIframe.tsx b/src/components/chat/ChatIframe.tsx index 56a29a523..ba2d1ee09 100644 --- a/src/components/chat/ChatIframe.tsx +++ b/src/components/chat/ChatIframe.tsx @@ -63,6 +63,8 @@ function generateGrillConfig(entity: ChatEntity['entity']): GrillConfig | null { hub: { id: COMMENTS_HUB_ID, }, + theme: 'light', + rootFontSize: '1rem', channel: { type: 'resource', resource: new Resource({ From a0ac7987583c899e607767900c8531fea75c0c21 Mon Sep 17 00:00:00 2001 From: teodorus-nathaniel Date: Tue, 5 Sep 2023 22:45:52 +0700 Subject: [PATCH 07/17] Update comments count in post page --- package.json | 2 +- src/components/chat/ChatIframe.tsx | 13 ++++++++----- src/components/posts/PostStats.tsx | 6 ++++-- src/rtk/features/chat/chatHooks.ts | 9 ++++++++- src/rtk/features/chat/chatSlice.ts | 8 +++++++- yarn.lock | 8 ++++---- 6 files changed, 32 insertions(+), 14 deletions(-) diff --git a/package.json b/package.json index a9d687ba0..0c4fb1a0b 100644 --- a/package.json +++ b/package.json @@ -94,7 +94,7 @@ "@subsocial/api": "0.8.10", "@subsocial/definitions": "0.8.10", "@subsocial/elasticsearch": "0.8.10", - "@subsocial/grill-widget": "^0.0.11", + "@subsocial/grill-widget": "^0.0.12", "@subsocial/resource-discussions": "^0.0.3", "@subsocial/utils": "0.8.10", "@tiptap/extension-highlight": "^2.0.0-beta.33", diff --git a/src/components/chat/ChatIframe.tsx b/src/components/chat/ChatIframe.tsx index ba2d1ee09..4df29ceeb 100644 --- a/src/components/chat/ChatIframe.tsx +++ b/src/components/chat/ChatIframe.tsx @@ -1,10 +1,11 @@ -import grill, { GrillConfig } from '@subsocial/grill-widget' +import grill, { GrillConfig, GrillEventListener } from '@subsocial/grill-widget' import { Resource } from '@subsocial/resource-discussions' import { summarize } from '@subsocial/utils' import clsx from 'clsx' import { ComponentProps, useEffect } from 'react' import useWrapInRef from 'src/hooks/useWrapInRef' import { useSendEvent } from 'src/providers/AnalyticContext' +import { useSetChatTotalMessageCount } from 'src/rtk/app/hooks' import { useAppSelector } from 'src/rtk/app/store' import { ChatEntity } from 'src/rtk/features/chat/chatSlice' @@ -18,6 +19,7 @@ export default function ChatIframe({ onUnreadCountChange, ...props }: ChatIframe const entity = useAppSelector(state => state.chat.entity) const sendEvent = useSendEvent() const sendEventRef = useWrapInRef(sendEvent) + const setChatTotalMessageCount = useSetChatTotalMessageCount() useEffect(() => { if (!entity) return @@ -33,10 +35,11 @@ export default function ChatIframe({ onUnreadCountChange, ...props }: ChatIframe return iframe } - const listener = onUnreadCountChange - ? (count: number) => { - console.log('unread count', count) - onUnreadCountChange(count) + const listener: GrillEventListener | undefined = onUnreadCountChange + ? (name, value) => { + const parsedValue = parseInt(value) ?? 0 + if (name === 'unread') onUnreadCountChange(parsedValue) + else if (name === 'totalMessage') setChatTotalMessageCount(parsedValue) } : undefined if (listener) { diff --git a/src/components/posts/PostStats.tsx b/src/components/posts/PostStats.tsx index 7b2621fac..f455c3a95 100644 --- a/src/components/posts/PostStats.tsx +++ b/src/components/posts/PostStats.tsx @@ -2,6 +2,7 @@ import { nonEmptyStr } from '@subsocial/utils' import clsx from 'clsx' import { useState } from 'react' import { useSetChatOpen } from 'src/rtk/app/hooks' +import { useAppSelector } from 'src/rtk/app/store' import { idToBn, PostStruct } from 'src/types' import { MutedSpan } from '../utils/MutedText' import { Pluralize } from '../utils/Plularize' @@ -18,14 +19,15 @@ export const StatsPanel = (props: StatsProps) => { const setChatOpen = useSetChatOpen() const [postVotersOpen, setPostVotersOpen] = useState(false) - const { upvotesCount, downvotesCount, repliesCount, sharesCount, id } = post + const totalMessageCount = useAppSelector(state => state.chat.totalMessageCount) + const { upvotesCount, downvotesCount, sharesCount, id } = post const reactionsCount = upvotesCount + downvotesCount const showReactionsModal = () => reactionsCount && setPostVotersOpen(true) const toggleCommentsSection = () => { setChatOpen(true) } - const comments = + const comments = return ( <> diff --git a/src/rtk/features/chat/chatHooks.ts b/src/rtk/features/chat/chatHooks.ts index 2970afb74..60d3d52de 100644 --- a/src/rtk/features/chat/chatHooks.ts +++ b/src/rtk/features/chat/chatHooks.ts @@ -1,6 +1,13 @@ import { useCallback } from 'react' import { useAppDispatch, useAppSelector } from 'src/rtk/app/store' -import { setChatConfig, setChatOpen } from '../chat/chatSlice' +import { setChatConfig, setChatOpen, setTotalMessageCount } from '../chat/chatSlice' + +export function useSetChatTotalMessageCount() { + const dispatch = useAppDispatch() + return useCallback((...params: Parameters) => { + dispatch(setTotalMessageCount(...params)) + }, []) +} export function useSetChatEntityConfig() { const dispatch = useAppDispatch() diff --git a/src/rtk/features/chat/chatSlice.ts b/src/rtk/features/chat/chatSlice.ts index 4b03385ae..a635777c9 100644 --- a/src/rtk/features/chat/chatSlice.ts +++ b/src/rtk/features/chat/chatSlice.ts @@ -10,11 +10,13 @@ type Entity = { export interface ChatEntity { isOpen: boolean entity: Entity + totalMessageCount: number } const initialState: ChatEntity = { isOpen: false, entity: null, + totalMessageCount: 0, } const slice = createSlice({ @@ -26,10 +28,14 @@ const slice = createSlice({ }, setChatConfig: (state, action: PayloadAction) => { state.entity = action.payload + state.totalMessageCount = 0 + }, + setTotalMessageCount: (state, action: PayloadAction) => { + state.totalMessageCount = action.payload }, }, }) -export const { setChatConfig, setChatOpen } = slice.actions +export const { setChatConfig, setChatOpen, setTotalMessageCount } = slice.actions export default slice.reducer diff --git a/yarn.lock b/yarn.lock index fc9637749..1de09f57b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2395,10 +2395,10 @@ "@elastic/elasticsearch" "7.4.0" "@subsocial/utils" latest -"@subsocial/grill-widget@^0.0.11": - version "0.0.11" - resolved "https://registry.yarnpkg.com/@subsocial/grill-widget/-/grill-widget-0.0.11.tgz#c77267b1c1344b5535fadfc5c1d7b062862f331f" - integrity sha512-2XOUbd467AeRtPkv2W3oYYfHGJCXAWEDMx6NpXdYs4AMOm1ZtHPjknrQ88/dcZCxDl26XcCRCSysu9n/6qPu2w== +"@subsocial/grill-widget@^0.0.12": + version "0.0.12" + resolved "https://registry.yarnpkg.com/@subsocial/grill-widget/-/grill-widget-0.0.12.tgz#57384850a3adb5342a5505a3f0d67c258d7e3b0e" + integrity sha512-Xed+wN/NEZ+mQlKA1olVVcBviVIQOTyqaCYcl9pGsg8yDdIBsKVjqJUKty1vvBL7gY0A+1osdwBgX2xUsbHOBA== "@subsocial/resource-discussions@^0.0.3": version "0.0.3" From 826bffd431c13383af949f4bde7fc6215769375c Mon Sep 17 00:00:00 2001 From: teodorus-nathaniel Date: Tue, 5 Sep 2023 22:59:34 +0700 Subject: [PATCH 08/17] Update events to use ga events --- .../posts/view-post/PostDropDownMenu.tsx | 6 +-- src/components/search/SearchInput.tsx | 6 +-- .../spaces/helpers/SpaceDropdownMenu.tsx | 6 +-- .../substrate/SubstrateTxButton.tsx | 8 ++-- src/ga/events.ts | 37 ------------------- src/ga/index.ts | 1 - src/providers/AnalyticContext.tsx | 14 ++++--- 7 files changed, 22 insertions(+), 56 deletions(-) delete mode 100644 src/ga/events.ts diff --git a/src/components/posts/view-post/PostDropDownMenu.tsx b/src/components/posts/view-post/PostDropDownMenu.tsx index 0b18e4ddc..2fd248fec 100644 --- a/src/components/posts/view-post/PostDropDownMenu.tsx +++ b/src/components/posts/view-post/PostDropDownMenu.tsx @@ -7,7 +7,7 @@ import { editPostUrl } from 'src/components/urls' import { ViewOnIpfs } from 'src/components/utils' import { ButtonLink } from 'src/components/utils/CustomLinks' import { DropdownMenu } from 'src/components/utils/DropDownMenu' -import { useSendGaUserEvent } from 'src/ga' +import { useSendEvent } from 'src/providers/AnalyticContext' import { idToBn, PostData, SpaceStruct } from 'src/types' import { ReactionModal } from '.' import { useIsMyAddress, useMyAddress } from '../../auth/MyAccountsContext' @@ -26,7 +26,7 @@ const InnerPostDropDownMenu: FC = props => { const myAddress = useMyAddress() const { struct } = post const postId = struct.id - const sendGaEvent = useSendGaUserEvent() + const sendEvent = useSendEvent() const { canEditPost, canHidePost, canMovePost } = useCheckCanEditAndHideSpacePermission(props) @@ -36,7 +36,7 @@ const InnerPostDropDownMenu: FC = props => { } const buildMenuItems = useCallback(() => { - sendGaEvent('Open post dropdown menu') + sendEvent('Open post dropdown menu') return ( <> diff --git a/src/components/search/SearchInput.tsx b/src/components/search/SearchInput.tsx index 846d5d2be..730b571ce 100644 --- a/src/components/search/SearchInput.tsx +++ b/src/components/search/SearchInput.tsx @@ -4,7 +4,7 @@ import BN from 'bn.js' import clsx from 'clsx' import { useRouter } from 'next/router' import { useEffect, useState } from 'react' -import { useSendGaUserEvent } from 'src/ga' +import { useSendEvent } from 'src/providers/AnalyticContext' import { SpaceWithSomeDetails } from 'src/types' import { useSelectSpace } from '../../rtk/features/spaces/spacesHooks' import useSubsocialEffect from '../api/useSubsocialEffect' @@ -52,7 +52,7 @@ const SearchInput = () => { const [withSpaceFilter, setWithSpaceFilter] = useState(true) const [spaceId, setSpaceId] = useState() const isSearchPage = router.pathname.includes('search') - const sendGaEvent = useSendGaUserEvent() + const sendEvent = useSendEvent() const spaceIdOrHandle = router.query?.spaceId as string | undefined @@ -97,7 +97,7 @@ const SearchInput = () => { }, } - sendGaEvent(`Search for ${value}`) + sendEvent('Search', { value }) return nonEmptyStr(value) && router.replace(queryPath, queryPath) } diff --git a/src/components/spaces/helpers/SpaceDropdownMenu.tsx b/src/components/spaces/helpers/SpaceDropdownMenu.tsx index 4605f92f2..303e1338f 100644 --- a/src/components/spaces/helpers/SpaceDropdownMenu.tsx +++ b/src/components/spaces/helpers/SpaceDropdownMenu.tsx @@ -5,8 +5,8 @@ import { editSpaceUrl } from 'src/components/urls' import { isHidden, ViewOnIpfs } from 'src/components/utils' import { BasicDropDownMenuProps, DropdownMenu } from 'src/components/utils/DropDownMenu' import { showSuccessMessage } from 'src/components/utils/Message' -import { useSendGaUserEvent } from 'src/ga' import { useHasUserASpacePermission } from 'src/permissions/checkPermission' +import { useSendEvent } from 'src/providers/AnalyticContext' import { SpaceData } from 'src/types' import { useSelectProfile } from '../../../rtk/features/profiles/profilesHooks' import { useIsUsingEmail, useMyAddress } from '../../auth/MyAccountsContext' @@ -39,10 +39,10 @@ export const SpaceDropdownMenu = (props: SpaceDropDownProps) => { const showMakeAsProfileButton = isMySpace && (!profileSpaceId || profileSpaceId !== id) - const sendGaEvent = useSendGaUserEvent() + const sendEvent = useSendEvent() const buildMenuItems = () => { - sendGaEvent('Open space dropdown menu') + sendEvent('Open space dropdown menu') return ( <> diff --git a/src/components/substrate/SubstrateTxButton.tsx b/src/components/substrate/SubstrateTxButton.tsx index 101fd7f5f..856ff3631 100644 --- a/src/components/substrate/SubstrateTxButton.tsx +++ b/src/components/substrate/SubstrateTxButton.tsx @@ -14,10 +14,10 @@ import { isFunction } from '@polkadot/util' import type { Signer } from '@polkadot/api/types' import { VoidFn } from '@polkadot/api/types' import { isEmptyStr, newLogger } from '@subsocial/utils' -import { useCreateSendGaUserEvent } from 'src/ga' import useExternalStorage from 'src/hooks/useExternalStorage' import useSignerExternalStorage from 'src/hooks/useSignerExternalStorage' import messages from 'src/messages' +import { useBuildSendEvent } from 'src/providers/AnalyticContext' import { useOpenCloseOnBoardingModal } from 'src/rtk/features/onBoarding/onBoardingHooks' import { AnyAccountId } from 'src/types' import { useSubstrate } from '.' @@ -148,7 +148,7 @@ function TxButton({ const api = customNodeApi || subsocialApi - const sendGaEvent = useCreateSendGaUserEvent(`Create tx: ${tx}`) + const sendTxEvent = useBuildSendEvent('Create tx') const waitMessage = controlledMessage({ message: messages.waitingForTx, @@ -269,7 +269,7 @@ function TxButton({ onFailedHandler, ) waitMessage.open() - sendGaEvent() + sendTxEvent({ tx }) } const signWithExtension = async ( @@ -308,7 +308,7 @@ function TxButton({ unsub = await tx.send(onSuccessHandler) waitMessage.open() - sendGaEvent() + sendTxEvent({ tx }) } catch (err: any) { onFailedHandler(err instanceof Error ? err.message : err) } diff --git a/src/ga/events.ts b/src/ga/events.ts deleted file mode 100644 index 5fc21037e..000000000 --- a/src/ga/events.ts +++ /dev/null @@ -1,37 +0,0 @@ -import ga from 'react-ga' -import { useIsSignedIn } from 'src/components/auth/MyAccountsContext' -import categories from './category' - -type CreateEventProps = { - category: string - action: string -} - -export const sendGaEvent = (props: CreateEventProps) => { - ga.event(props) -} - -export const sendGuestGaEvent = (action: string) => - sendGaEvent({ - category: categories.user.guest, - action, - }) - -export const sendSignedInGaEvent = (action: string) => - sendGaEvent({ - category: categories.user.signin, - action, - }) - -export const useSendGaUserEvent = () => { - const isSignIn = useIsSignedIn() - const sendGAEvent = isSignIn ? sendSignedInGaEvent : sendGuestGaEvent - - return sendGAEvent -} - -export const useCreateSendGaUserEvent = (action: string) => { - const sendGaEvent = useSendGaUserEvent() - - return () => sendGaEvent(action) -} diff --git a/src/ga/index.ts b/src/ga/index.ts index 89277fad5..66dcb7714 100644 --- a/src/ga/index.ts +++ b/src/ga/index.ts @@ -1,5 +1,4 @@ export * from './category' -export * from './events' import ReactGA from 'react-ga' import { isServerSide } from 'src/components/utils' import { openCookiesNotification } from '../components/cookies' diff --git a/src/providers/AnalyticContext.tsx b/src/providers/AnalyticContext.tsx index 6092d10bd..ed4c322c9 100644 --- a/src/providers/AnalyticContext.tsx +++ b/src/providers/AnalyticContext.tsx @@ -22,7 +22,7 @@ const initialState: AnalyticContextState = { } export type AnalyticContextProps = { - sendEvent: (name: string) => void + sendEvent: (name: string, properties?: Record) => void } const propsStub = { sendEvent: () => undefined } @@ -72,10 +72,11 @@ export function AnalyticProvider(props: React.PropsWithChildren<{}>) { const contextValue: AnalyticContextProps = useMemo(() => { return { - sendEvent: (name: string) => { + sendEvent: (name: string, properties?: Record) => { const eventProps = { event_type: name, device_id: state.deviceId, + ...properties, } if (!state.amp) { setQueuedEvents(prev => [...prev, eventProps]) @@ -96,9 +97,12 @@ export function useSendEvent() { export function useBuildSendEvent(eventName: string) { const sendEvent = useSendEvent() - return useCallback(() => { - sendEvent(eventName) - }, [eventName]) + return useCallback( + (properties?: Record) => { + sendEvent(eventName, properties) + }, + [eventName], + ) } export default AnalyticProvider From 968d396a899d420ee1d4b034471415d47785bb3a Mon Sep 17 00:00:00 2001 From: teodorus-nathaniel Date: Wed, 6 Sep 2023 00:42:01 +0700 Subject: [PATCH 09/17] Change events to use snake case and change properties placement --- src/components/posts/view-post/PostDropDownMenu.tsx | 2 +- src/components/search/SearchInput.tsx | 2 +- src/components/spaces/helpers/SpaceDropdownMenu.tsx | 2 +- src/components/substrate/SubstrateTxButton.tsx | 2 +- src/providers/AnalyticContext.tsx | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/posts/view-post/PostDropDownMenu.tsx b/src/components/posts/view-post/PostDropDownMenu.tsx index 2fd248fec..979f6332a 100644 --- a/src/components/posts/view-post/PostDropDownMenu.tsx +++ b/src/components/posts/view-post/PostDropDownMenu.tsx @@ -36,7 +36,7 @@ const InnerPostDropDownMenu: FC = props => { } const buildMenuItems = useCallback(() => { - sendEvent('Open post dropdown menu') + sendEvent('open_post_dropdown_menu') return ( <> diff --git a/src/components/search/SearchInput.tsx b/src/components/search/SearchInput.tsx index 730b571ce..1e4635a17 100644 --- a/src/components/search/SearchInput.tsx +++ b/src/components/search/SearchInput.tsx @@ -97,7 +97,7 @@ const SearchInput = () => { }, } - sendEvent('Search', { value }) + sendEvent('search', { value }) return nonEmptyStr(value) && router.replace(queryPath, queryPath) } diff --git a/src/components/spaces/helpers/SpaceDropdownMenu.tsx b/src/components/spaces/helpers/SpaceDropdownMenu.tsx index 303e1338f..49f8a963a 100644 --- a/src/components/spaces/helpers/SpaceDropdownMenu.tsx +++ b/src/components/spaces/helpers/SpaceDropdownMenu.tsx @@ -42,7 +42,7 @@ export const SpaceDropdownMenu = (props: SpaceDropDownProps) => { const sendEvent = useSendEvent() const buildMenuItems = () => { - sendEvent('Open space dropdown menu') + sendEvent('open_space_dropdown_menu') return ( <> diff --git a/src/components/substrate/SubstrateTxButton.tsx b/src/components/substrate/SubstrateTxButton.tsx index 856ff3631..1563085ee 100644 --- a/src/components/substrate/SubstrateTxButton.tsx +++ b/src/components/substrate/SubstrateTxButton.tsx @@ -148,7 +148,7 @@ function TxButton({ const api = customNodeApi || subsocialApi - const sendTxEvent = useBuildSendEvent('Create tx') + const sendTxEvent = useBuildSendEvent('create_tx') const waitMessage = controlledMessage({ message: messages.waitingForTx, diff --git a/src/providers/AnalyticContext.tsx b/src/providers/AnalyticContext.tsx index ed4c322c9..f1f2dda8e 100644 --- a/src/providers/AnalyticContext.tsx +++ b/src/providers/AnalyticContext.tsx @@ -76,7 +76,7 @@ export function AnalyticProvider(props: React.PropsWithChildren<{}>) { const eventProps = { event_type: name, device_id: state.deviceId, - ...properties, + event_properties: properties, } if (!state.amp) { setQueuedEvents(prev => [...prev, eventProps]) From a597f04be4ad3f691033bfd9200ce548ba2bd1b3 Mon Sep 17 00:00:00 2001 From: teodorus-nathaniel Date: Wed, 6 Sep 2023 12:51:35 +0700 Subject: [PATCH 10/17] Remove call to cookies notif --- src/ga/index.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/ga/index.ts b/src/ga/index.ts index 66dcb7714..88eaf544a 100644 --- a/src/ga/index.ts +++ b/src/ga/index.ts @@ -1,7 +1,6 @@ export * from './category' import ReactGA from 'react-ga' import { isServerSide } from 'src/components/utils' -import { openCookiesNotification } from '../components/cookies' export type GaProps = { id: string @@ -18,5 +17,4 @@ export const initGa = (props?: GaProps) => { const { id, options } = props ReactGA.initialize(id, options) - openCookiesNotification() } From c28759d89f8bde20c589dfa268da11725081d3cd Mon Sep 17 00:00:00 2001 From: teodorus-nathaniel Date: Wed, 6 Sep 2023 12:55:43 +0700 Subject: [PATCH 11/17] Disable page scroll if user opens floating modal --- src/components/chat/ChatFloatingModal.tsx | 7 ++++++- src/utils/window.ts | 6 ++++++ 2 files changed, 12 insertions(+), 1 deletion(-) create mode 100644 src/utils/window.ts diff --git a/src/components/chat/ChatFloatingModal.tsx b/src/components/chat/ChatFloatingModal.tsx index d24c34c09..f885c21e5 100644 --- a/src/components/chat/ChatFloatingModal.tsx +++ b/src/components/chat/ChatFloatingModal.tsx @@ -7,6 +7,7 @@ import { useSendEvent } from 'src/providers/AnalyticContext' import { useChatOpenState } from 'src/rtk/app/hooks' import { useAppSelector } from 'src/rtk/app/store' import { ChatEntity } from 'src/rtk/features/chat/chatSlice' +import { disablePageScroll, enablePageScroll } from 'src/utils/window' import { useResponsiveSize } from '../responsive' import styles from './ChatFloatingModal.module.sass' import ChatIframe from './ChatIframe' @@ -16,6 +17,7 @@ export default function ChatFloatingModal() { const sendEvent = useSendEvent() const [isOpen, setIsOpen] = useChatOpenState() const entity = useAppSelector(state => state.chat.entity) + const totalMessageCount = useAppSelector(state => state.chat.totalMessageCount) const [unreadCount, setUnreadCount] = useState(0) @@ -40,6 +42,9 @@ export default function ChatFloatingModal() { } sendEvent(event) + if (!isOpen) disablePageScroll() + else enablePageScroll() + setIsOpen(!isOpen) hasOpened.current = true } @@ -77,7 +82,7 @@ export default function ChatFloatingModal() {
{!!unreadCount && {unreadCount}}
, diff --git a/src/utils/window.ts b/src/utils/window.ts new file mode 100644 index 000000000..488e1fb3d --- /dev/null +++ b/src/utils/window.ts @@ -0,0 +1,6 @@ +export function disablePageScroll() { + document.body.style.overflow = 'hidden' +} +export function enablePageScroll() { + document.body.style.overflow = 'auto' +} From cc6642a3bdf9f8253053e5c51b9939ff869fcdeb Mon Sep 17 00:00:00 2001 From: teodorus-nathaniel Date: Wed, 6 Sep 2023 17:35:17 +0700 Subject: [PATCH 12/17] Fix issue where event listeners not added --- src/components/chat/ChatIframe.tsx | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/components/chat/ChatIframe.tsx b/src/components/chat/ChatIframe.tsx index 4df29ceeb..0b1fc0354 100644 --- a/src/components/chat/ChatIframe.tsx +++ b/src/components/chat/ChatIframe.tsx @@ -35,13 +35,11 @@ export default function ChatIframe({ onUnreadCountChange, ...props }: ChatIframe return iframe } - const listener: GrillEventListener | undefined = onUnreadCountChange - ? (name, value) => { - const parsedValue = parseInt(value) ?? 0 - if (name === 'unread') onUnreadCountChange(parsedValue) - else if (name === 'totalMessage') setChatTotalMessageCount(parsedValue) - } - : undefined + const listener: GrillEventListener | undefined = (name, value) => { + const parsedValue = parseInt(value) ?? 0 + if (name === 'unread') onUnreadCountChange?.(parsedValue) + else if (name === 'totalMessage') setChatTotalMessageCount(parsedValue) + } if (listener) { grill.addUnreadCountListener(listener) } From a17a6bb0326fb956d4600995dce9d8c64000aeff Mon Sep 17 00:00:00 2001 From: Oleh Mell Date: Wed, 6 Sep 2023 19:20:10 +0300 Subject: [PATCH 13/17] Apply suggestions from code review --- .github/workflows/build-deploy.yml | 2 +- .github/workflows/feature-based.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-deploy.yml b/.github/workflows/build-deploy.yml index 411d5ca78..b3d324cea 100644 --- a/.github/workflows/build-deploy.yml +++ b/.github/workflows/build-deploy.yml @@ -72,7 +72,7 @@ jobs: GH_GA_ID=UA-204866512-2 GH_APP_KIND=polkaverse GH_HCAPTCHA_SITE_KEY=${{ secrets.PROD_HCAPTCHA_SITE_KEY }} - GH_AMP_ID=d9f7a97f3cdc1eb7b4298af4f17c202b + GH_AMP_ID=2eeca0e8a0163c89e3f023c971e426a6 GH_OFFCHAIN_SIGNER_URL=https://signer.subsocial.network GH_CONNECTION_KIND=main diff --git a/.github/workflows/feature-based.yaml b/.github/workflows/feature-based.yaml index 351df3fe0..4f07ba17c 100644 --- a/.github/workflows/feature-based.yaml +++ b/.github/workflows/feature-based.yaml @@ -62,7 +62,7 @@ jobs: GH_GA_ID=fake GH_APP_KIND=polkaverse GH_HCAPTCHA_SITE_KEY=3beeddac-2dce-41cc-8e18-338118426c38 - GH_AMP_ID=a4dc29d356f19fd9788f24b493b462bf + GH_AMP_ID=71bf5a46800fedba5e9a01243b988164 GH_OFFCHAIN_SIGNER_URL=https://signer.subsocial.network GH_CONNECTION_KIND=dev tags: | From e971e41954fba3001f89f311bf29e9bfa527cd52 Mon Sep 17 00:00:00 2001 From: teodorus-nathaniel Date: Wed, 6 Sep 2023 23:24:16 +0700 Subject: [PATCH 14/17] Move comments hub id into config --- src/components/chat/ChatIframe.tsx | 5 ++--- src/config/common.ts | 2 ++ 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/components/chat/ChatIframe.tsx b/src/components/chat/ChatIframe.tsx index 0b1fc0354..74bcc54a2 100644 --- a/src/components/chat/ChatIframe.tsx +++ b/src/components/chat/ChatIframe.tsx @@ -3,6 +3,7 @@ import { Resource } from '@subsocial/resource-discussions' import { summarize } from '@subsocial/utils' import clsx from 'clsx' import { ComponentProps, useEffect } from 'react' +import config from 'src/config' import useWrapInRef from 'src/hooks/useWrapInRef' import { useSendEvent } from 'src/providers/AnalyticContext' import { useSetChatTotalMessageCount } from 'src/rtk/app/hooks' @@ -13,8 +14,6 @@ export type ChatIframeProps = ComponentProps<'div'> & { onUnreadCountChange?: (count: number) => void } -const COMMENTS_HUB_ID = '1032' - export default function ChatIframe({ onUnreadCountChange, ...props }: ChatIframeProps) { const entity = useAppSelector(state => state.chat.entity) const sendEvent = useSendEvent() @@ -62,7 +61,7 @@ function generateGrillConfig(entity: ChatEntity['entity']): GrillConfig | null { const body = summarize(post.content?.body ?? '', { limit: 50 }) return { hub: { - id: COMMENTS_HUB_ID, + id: config.commentsHubId, }, theme: 'light', rootFontSize: '1rem', diff --git a/src/config/common.ts b/src/config/common.ts index 331f828cb..a4dc136f0 100644 --- a/src/config/common.ts +++ b/src/config/common.ts @@ -12,4 +12,6 @@ export default { // SEO-related settings: seoSitemapLastmod: '2020-11-21', seoSitemapPageSize: 100, + + commentsHubId: '1032', } From f0dcff85c394a3f933ec6b059b1def878e30c751 Mon Sep 17 00:00:00 2001 From: teodorus-nathaniel Date: Thu, 7 Sep 2023 19:29:59 +0700 Subject: [PATCH 15/17] Use summarizeMd for creating metadata of chat --- src/components/chat/ChatIframe.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/components/chat/ChatIframe.tsx b/src/components/chat/ChatIframe.tsx index 74bcc54a2..6c087d68e 100644 --- a/src/components/chat/ChatIframe.tsx +++ b/src/components/chat/ChatIframe.tsx @@ -1,6 +1,6 @@ import grill, { GrillConfig, GrillEventListener } from '@subsocial/grill-widget' import { Resource } from '@subsocial/resource-discussions' -import { summarize } from '@subsocial/utils' +import { summarizeMd } from '@subsocial/utils' import clsx from 'clsx' import { ComponentProps, useEffect } from 'react' import config from 'src/config' @@ -57,8 +57,10 @@ function generateGrillConfig(entity: ChatEntity['entity']): GrillConfig | null { if (!entity) return null if (entity.type === 'post') { const post = entity.data - const title = summarize(post.content?.title ?? post.content?.body ?? '', { limit: 50 }) - const body = summarize(post.content?.body ?? '', { limit: 50 }) + const title = summarizeMd(post.content?.title || post.content?.body || '', { + limit: 50, + }).summary + const body = summarizeMd(post.content?.body ?? '', { limit: 50 }).summary return { hub: { id: config.commentsHubId, From 39b55a231b3f3880c08bcbd181ece3dc4aab282a Mon Sep 17 00:00:00 2001 From: teodorus-nathaniel Date: Fri, 8 Sep 2023 20:27:14 +0700 Subject: [PATCH 16/17] Remove link in comments stats if desktop --- src/components/chat/ChatFloatingModal.tsx | 3 +-- src/components/posts/PostStats.tsx | 4 +++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/components/chat/ChatFloatingModal.tsx b/src/components/chat/ChatFloatingModal.tsx index f885c21e5..b0959c24b 100644 --- a/src/components/chat/ChatFloatingModal.tsx +++ b/src/components/chat/ChatFloatingModal.tsx @@ -17,7 +17,6 @@ export default function ChatFloatingModal() { const sendEvent = useSendEvent() const [isOpen, setIsOpen] = useChatOpenState() const entity = useAppSelector(state => state.chat.entity) - const totalMessageCount = useAppSelector(state => state.chat.totalMessageCount) const [unreadCount, setUnreadCount] = useState(0) @@ -82,7 +81,7 @@ export default function ChatFloatingModal() {
{!!unreadCount && {unreadCount}}
, diff --git a/src/components/posts/PostStats.tsx b/src/components/posts/PostStats.tsx index f455c3a95..07dd7a98d 100644 --- a/src/components/posts/PostStats.tsx +++ b/src/components/posts/PostStats.tsx @@ -4,6 +4,7 @@ import { useState } from 'react' import { useSetChatOpen } from 'src/rtk/app/hooks' import { useAppSelector } from 'src/rtk/app/store' import { idToBn, PostStruct } from 'src/types' +import { useResponsiveSize } from '../responsive' import { MutedSpan } from '../utils/MutedText' import { Pluralize } from '../utils/Plularize' import { ActiveVoters, PostVoters } from '../voting/ListVoters' @@ -15,6 +16,7 @@ type StatsProps = { export const StatsPanel = (props: StatsProps) => { const { post, goToCommentsId } = props + const { isLargeDesktop } = useResponsiveSize() const setChatOpen = useSetChatOpen() const [postVotersOpen, setPostVotersOpen] = useState(false) @@ -38,7 +40,7 @@ export const StatsPanel = (props: StatsProps) => { - {nonEmptyStr(goToCommentsId) ? ( + {!isLargeDesktop && nonEmptyStr(goToCommentsId) ? ( {comments} From d3bf915d51be9df22f5b13dafe4b34f1fef0a5a7 Mon Sep 17 00:00:00 2001 From: teodorus-nathaniel Date: Fri, 8 Sep 2023 20:30:35 +0700 Subject: [PATCH 17/17] Change offset --- src/components/chat/ChatFloatingModal.module.sass | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/chat/ChatFloatingModal.module.sass b/src/components/chat/ChatFloatingModal.module.sass index d7b17a01e..572ca194f 100644 --- a/src/components/chat/ChatFloatingModal.module.sass +++ b/src/components/chat/ChatFloatingModal.module.sass @@ -3,7 +3,7 @@ .ChatFloatingWrapper z-index: 10 position: fixed - bottom: $space_huge + bottom: $space_normal right: $space_normal .ChatUnreadCount @@ -19,6 +19,8 @@ .ChatFloatingButton padding: $space_small $space_normal + // To offset optical illusion from the grill icon + padding-left: $space_small border-radius: 32px font-size: 1rem color: white !important