diff --git a/CHANGELOG.md b/CHANGELOG.md index 684656a122..4e391ffdd7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,23 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [5.3.0] - 2024-06-04 + +### Added + +- Optimistic video updates + +### Fixed + +- Portfolio table actions +- Show correct potential revenue share on portfolio +- Small UI fixes + +### Changed + +- Make rewards drawers initially collapsed +- Seperate marketplace into 2 pages + ## [5.2.3] - 2024-05-24 ### Added diff --git a/packages/atlas/package.json b/packages/atlas/package.json index e73eae04cc..c2b1a3020b 100644 --- a/packages/atlas/package.json +++ b/packages/atlas/package.json @@ -1,7 +1,7 @@ { "name": "@joystream/atlas", "description": "UI for consuming Joystream - a user governed video platform", - "version": "5.2.3", + "version": "5.3.0", "license": "GPL-3.0", "scripts": { "start": "vite", diff --git a/packages/atlas/src/api/client/cache.ts b/packages/atlas/src/api/client/cache.ts index d9bd730a2e..b4d65ba2aa 100644 --- a/packages/atlas/src/api/client/cache.ts +++ b/packages/atlas/src/api/client/cache.ts @@ -4,6 +4,7 @@ import { FieldPolicy, FieldReadFunction } from '@apollo/client/cache/inmemory/po import { offsetLimitPagination, relayStylePagination } from '@apollo/client/utilities' import { parseISO } from 'date-fns' +import { QueryCommentReactionsConnectionArgs } from '../../../../atlas-meta-server/src/api/__generated__/sdk' import { Query, QueryChannelsConnectionArgs, @@ -119,7 +120,19 @@ const getCommentKeyArgs = ( const parentCommentId = args?.where?.parentComment?.id_eq const videoId = args?.where?.video?.id_eq ?? ctx.variables?.videoId const orderBy = args?.orderBy || [] - return `${parentCommentId}:${videoId}:${orderBy}` + return `parentId-${parentCommentId}-:id-${videoId}-:${orderBy}` +} + +const getCommentReactionsKeyArgs = ( + args: Partial | null, + ctx: { + variables?: Record + } +) => { + const memberId = args?.where?.member?.id_eq + const videoId = args?.where?.video?.id_eq ?? ctx.variables?.videoId + const orderBy = args?.orderBy || [] + return `memberId-${memberId}-:videoId-${videoId}-:${orderBy}` } const createDateHandler = () => ({ @@ -216,6 +229,9 @@ const queryCacheFields: CachePolicyFields = { return existing?.slice(offset, offset + limit) }, }, + commentReactions: { + ...offsetLimitPagination(getCommentReactionsKeyArgs), + }, commentsConnection: relayStylePagination(getCommentKeyArgs), channelById: (existing, { toReference, args }) => { return ( diff --git a/packages/atlas/src/api/client/index.ts b/packages/atlas/src/api/client/index.ts index 276fad64f1..f8e236cd6e 100644 --- a/packages/atlas/src/api/client/index.ts +++ b/packages/atlas/src/api/client/index.ts @@ -3,12 +3,20 @@ import { GraphQLWsLink } from '@apollo/client/link/subscriptions' import { getMainDefinition } from '@apollo/client/utilities' import { createClient } from 'graphql-ws' -import { ORION_GRAPHQL_URL, QUERY_NODE_GRAPHQL_SUBSCRIPTION_URL } from '@/config/env' +import { + FAUCET_URL, + ORION_AUTH_URL, + ORION_GRAPHQL_URL, + QUERY_NODE_GRAPHQL_SUBSCRIPTION_URL, + YPP_FAUCET_URL, +} from '@/config/env' import { useUserLocationStore } from '@/providers/userLocation' import { UserEventsLogger } from '@/utils/logs' import { cache } from './cache' +const followedRequests = [YPP_FAUCET_URL, ORION_GRAPHQL_URL, ORION_AUTH_URL, FAUCET_URL] + const initializePerformanceObserver = () => { try { const observer = new PerformanceObserver((list) => { @@ -17,6 +25,12 @@ const initializePerformanceObserver = () => { const queryString = entry.name.split('?')?.[1] const params = new URLSearchParams(queryString) const queryType = params.get('queryName') + + // Only follow requests to Atlas infra + if (!queryType && !followedRequests.some((allowedUrl) => entry.name.includes(allowedUrl))) { + return + } + UserEventsLogger.logUserEvent('request-response-time', { requestName: queryType ?? entry.name, timeToComplete: entry.duration, diff --git a/packages/atlas/src/api/hooks/comments.ts b/packages/atlas/src/api/hooks/comments.ts index 34167fb789..d1babea9f3 100644 --- a/packages/atlas/src/api/hooks/comments.ts +++ b/packages/atlas/src/api/hooks/comments.ts @@ -30,7 +30,7 @@ export const useComment = ( } } -export type UserCommentReactions = Record +export type UserCommentReactions = Record export const useUserCommentsReactions = (videoId?: string | null, memberId?: string | null) => { const { data } = useGetUserCommentsReactionsQuery({ variables: { @@ -42,9 +42,15 @@ export const useUserCommentsReactions = (videoId?: string | null, memberId?: str return useMemo( () => ({ - userReactions: data?.commentReactions.reduce>((acc, item) => { + userReactions: data?.commentReactions.reduce((acc, item) => { if (item) { - acc[item.comment.id] = [...(acc[item.comment.id] ? acc[item.comment.id] : []), item.reactionId] + acc[item.comment.id] = [ + ...(acc[item.comment.id] ? acc[item.comment.id] : []), + { + reactionId: item.reactionId, + reactionServerId: item.id, + }, + ] } return acc }, {}), diff --git a/packages/atlas/src/api/hooks/useCommentSectionComments.ts b/packages/atlas/src/api/hooks/useCommentSectionComments.ts new file mode 100644 index 0000000000..da5375529a --- /dev/null +++ b/packages/atlas/src/api/hooks/useCommentSectionComments.ts @@ -0,0 +1,38 @@ +import { QueryHookOptions } from '@apollo/client' + +import { UNCONFIRMED } from '@/hooks/useOptimisticActions' +import { createLookup } from '@/utils/data' + +import { + GetUserCommentsAndVideoCommentsConnectionQuery, + GetUserCommentsAndVideoCommentsConnectionQueryVariables, + useGetUserCommentsAndVideoCommentsConnectionQuery, +} from '../queries/__generated__/comments.generated' + +export const useCommentSectionComments = ( + variables?: GetUserCommentsAndVideoCommentsConnectionQueryVariables, + opts?: QueryHookOptions< + GetUserCommentsAndVideoCommentsConnectionQuery, + GetUserCommentsAndVideoCommentsConnectionQueryVariables + > +) => { + const { data, loading, ...rest } = useGetUserCommentsAndVideoCommentsConnectionQuery({ ...opts, variables }) + const userComments = data?.userComments + const userCommentLookup = data?.userComments && createLookup(data?.userComments) + const unconfirmedComments = data?.videoCommentsConnection.edges + .map((edge) => edge.node) + .filter((node) => node.id.includes(UNCONFIRMED)) + const unconfirmedCommentLookup = unconfirmedComments && createLookup(unconfirmedComments) + + const videoComments = data?.videoCommentsConnection?.edges + .map((edge) => edge.node) + .filter((comment) => userCommentLookup && !userCommentLookup[comment.id] && !unconfirmedCommentLookup?.[comment.id]) + + return { + userComments, + comments: data ? [...(unconfirmedComments || []), ...(userComments || []), ...(videoComments || [])] : undefined, + loading: loading, + pageInfo: data?.videoCommentsConnection?.pageInfo, + ...rest, + } +} diff --git a/packages/atlas/src/api/queries/__generated__/comments.generated.tsx b/packages/atlas/src/api/queries/__generated__/comments.generated.tsx index a96d8d784c..750487f1c2 100644 --- a/packages/atlas/src/api/queries/__generated__/comments.generated.tsx +++ b/packages/atlas/src/api/queries/__generated__/comments.generated.tsx @@ -257,6 +257,7 @@ export type GetUserCommentsReactionsQuery = { __typename?: 'Query' commentReactions: Array<{ __typename?: 'CommentReaction' + id: string reactionId: number comment: { __typename?: 'Comment'; id: string } }> @@ -528,6 +529,7 @@ export type GetUserCommentsAndVideoCommentsConnectionQueryResult = Apollo.QueryR export const GetUserCommentsReactionsDocument = gql` query GetUserCommentsReactions($memberId: String!, $videoId: String!) { commentReactions(where: { member: { id_eq: $memberId }, video: { id_eq: $videoId } }, limit: 1000) { + id reactionId comment { id diff --git a/packages/atlas/src/api/queries/comments.graphql b/packages/atlas/src/api/queries/comments.graphql index 45780bd7c4..432b93f60a 100644 --- a/packages/atlas/src/api/queries/comments.graphql +++ b/packages/atlas/src/api/queries/comments.graphql @@ -84,6 +84,7 @@ query GetUserCommentsAndVideoCommentsConnection( # CHANGE: ID is now `String` query GetUserCommentsReactions($memberId: String!, $videoId: String!) { commentReactions(where: { member: { id_eq: $memberId }, video: { id_eq: $videoId } }, limit: 1000) { + id reactionId comment { # CHANGE: `commentId` no longer exists, use `comment.id` diff --git a/packages/atlas/src/components/_comments/Comment/Comment.styles.ts b/packages/atlas/src/components/_comments/Comment/Comment.styles.ts index 37ab738543..34361dddf9 100644 --- a/packages/atlas/src/components/_comments/Comment/Comment.styles.ts +++ b/packages/atlas/src/components/_comments/Comment/Comment.styles.ts @@ -17,10 +17,11 @@ export const KebabMenuIconButton = styled(Button)<{ isActive: boolean }>` } ` -export const CommentWrapper = styled.div<{ shouldShowKebabButton: boolean }>` +export const CommentWrapper = styled.div<{ shouldShowKebabButton: boolean; isUnconfirmed?: boolean }>` display: grid; gap: ${sizes(3)}; align-items: start; + opacity: ${(props) => (props.isUnconfirmed ? '0.8' : 'unset')}; /* comment content, kebab button */ grid-template-columns: 1fr auto; diff --git a/packages/atlas/src/components/_comments/Comment/Comment.tsx b/packages/atlas/src/components/_comments/Comment/Comment.tsx index 6cef356b96..ba8af6c02a 100644 --- a/packages/atlas/src/components/_comments/Comment/Comment.tsx +++ b/packages/atlas/src/components/_comments/Comment/Comment.tsx @@ -1,7 +1,7 @@ import BN from 'bn.js' import { Dispatch, FC, SetStateAction, memo, useCallback, useRef, useState } from 'react' -import { useComment } from '@/api/hooks/comments' +import { UserCommentReactions, useComment } from '@/api/hooks/comments' import { CommentStatus } from '@/api/queries/__generated__/baseTypes.generated' import { CommentFieldsFragment, FullVideoFieldsFragment } from '@/api/queries/__generated__/fragments.generated' import { DialogModal } from '@/components/_overlays/DialogModal' @@ -25,7 +25,7 @@ import { CommentRowProps } from '../CommentRow' export type CommentProps = { commentId?: string video?: FullVideoFieldsFragment | null - userReactions?: number[] + userReactions?: UserCommentReactions[string] isReplyable?: boolean setHighlightedCommentId?: Dispatch> setRepliesOpen?: Dispatch> @@ -102,8 +102,10 @@ export const Comment: FC = memo( closeModal() isChannelOwner ? await moderateComment(comment.id, video?.channel.id, comment.author.handle, video?.id) - : await deleteComment(comment.id, video?.title || '', video?.id) - setIsCommentProcessing(false) + : await deleteComment(comment.id, video?.title || '', video?.id, { + onUnconfirmed: () => setIsCommentProcessing(false), + onTxSign: () => setIsCommentProcessing(false), + }) }, }, secondaryButton: { @@ -155,27 +157,25 @@ export const Comment: FC = memo( } setEditCommentInputIsProcessing(true) - const success = await updateComment({ + await updateComment({ videoId: video.id, commentBody: editCommentInputText ?? '', commentId: comment.id, videoTitle: video.title, + optimisticOpts: { + onTxSign: () => { + setEditCommentInputIsProcessing(false) + setEditCommentInputText('') + setHighlightedCommentId?.(comment?.id ?? null) + setIsEditingComment(false) + }, + onUnconfirmed: () => { + setEditCommentInputIsProcessing(false) + setEditCommentInputText('') + setIsEditingComment(false) + }, + }, }) - setEditCommentInputIsProcessing(false) - - if (success) { - setEditCommentInputText('') - setHighlightedCommentId?.(comment?.id ?? null) - setIsEditingComment(false) - } - } - const handleCommentReaction = async (commentId: string, reactionId: CommentReaction) => { - setProcessingReactionsIds((previous) => [...previous, reactionId]) - const fee = - reactionFee || - (await getReactToVideoCommentFee(memberId && comment?.id ? [memberId, comment.id, reactionId] : undefined)) - await reactToComment(commentId, video?.id || '', reactionId, comment?.author.handle || '', fee) - setProcessingReactionsIds((previous) => previous.filter((r) => r !== reactionId)) } const handleOnBoardingPopoverOpen = async (reactionId: number) => { @@ -190,19 +190,23 @@ export const Comment: FC = memo( } setReplyCommentInputIsProcessing(true) - const newCommentId = await addComment({ + await addComment({ videoId: video.id, commentBody: replyCommentInputText, parentCommentId: comment.id, videoTitle: video.title, commentAuthorHandle: comment.author.handle, + optimisticOpts: { + onTxSign: (newCommentId) => { + setReplyCommentInputIsProcessing(false) + setReplyCommentInputText('') + setHighlightedCommentId?.(newCommentId || null) + onReplyPosted?.(newCommentId || '') + setRepliesOpen?.(true) + setReplyInputOpen(false) + }, + }, }) - setReplyCommentInputIsProcessing(false) - setReplyCommentInputText('') - setHighlightedCommentId?.(newCommentId || null) - onReplyPosted?.(newCommentId || '') - setRepliesOpen?.(true) - setReplyInputOpen(false) } const handleReplyClick = () => { @@ -250,13 +254,28 @@ export const Comment: FC = memo( const reactions = (comment && getCommentReactions({ - userReactionsIds: userReactions, + userReactionsIds: userReactions?.map((uR) => uR.reactionId), reactionsCount: comment.reactionsCountByReactionId || [], processingReactionsIds, deleted: commentType === 'deleted', })) || undefined + const handleCommentReaction = async (commentId: string, reactionId: CommentReaction) => { + setProcessingReactionsIds((previous) => [...previous, reactionId]) + const fee = + reactionFee || + (await getReactToVideoCommentFee(memberId && comment?.id ? [memberId, comment.id, reactionId] : undefined)) + await reactToComment(commentId, video?.id || '', reactionId, comment?.author.handle || '', fee, { + prevReactionServerId: + userReactions?.find((reaction) => reaction.reactionId === reactionId)?.reactionServerId ?? '', + videoId: video?.id ?? '', + onUnconfirmedComment: () => setProcessingReactionsIds((previous) => previous.filter((r) => r !== reactionId)), + onTxSign: () => setProcessingReactionsIds((previous) => previous.filter((r) => r !== reactionId)), + }) + setProcessingReactionsIds((previous) => previous.filter((r) => r !== reactionId)) + } + if (isEditingComment) { return ( = ({ const [tempReactionId, setTempReactionId] = useState(null) const isDeleted = type === 'deleted' const isProcessing = type === 'processing' + const isUnconfirmed = commentId?.includes(UNCONFIRMED) const shouldShowKebabButton = type === 'options' && !loading && !isDeleted const popoverRef = useRef(null) @@ -191,7 +193,7 @@ export const InternalComment: FC = ({ onMouseEnter={() => setCommentHover(true)} onMouseLeave={() => setCommentHover(false)} > - + = ({ onDecline={handleOnboardingPopoverHide} trigger={ - {hasReactionsAndCommentIsNotDeleted && ( + {!isUnconfirmed && hasReactionsAndCommentIsNotDeleted && ( = ({ {repliesOpen ? 'Hide' : 'Show'} {repliesCount} {repliesCount === 1 ? 'reply' : 'replies'} )} - {onReplyClick && !isDeleted && !isProcessing && (commentHover || isTouchDevice) && ( - - - - Reply - - - - )} + {onReplyClick && + !isUnconfirmed && + !isDeleted && + !isProcessing && + (commentHover || isTouchDevice) && ( + + + + Reply + + + + )} } @@ -333,19 +339,21 @@ export const InternalComment: FC = ({ - } - variant="tertiary" - size="small" - isActive={shouldShowKebabButton} - /> - } - /> + {!isUnconfirmed ? ( + } + variant="tertiary" + size="small" + isActive={shouldShowKebabButton} + /> + } + /> + ) : null} ) diff --git a/packages/atlas/src/components/_crt/AmmTransactionsTable/AmmTransactionsTable.tsx b/packages/atlas/src/components/_crt/AmmTransactionsTable/AmmTransactionsTable.tsx index c5ef42b179..878129a6df 100644 --- a/packages/atlas/src/components/_crt/AmmTransactionsTable/AmmTransactionsTable.tsx +++ b/packages/atlas/src/components/_crt/AmmTransactionsTable/AmmTransactionsTable.tsx @@ -49,9 +49,10 @@ const tableEmptyState = { type AmmTransactionsTableProps = { data: FullAmmCurveFragment['transactions'] loading: boolean + symbol: string } -export const AmmTransactionsTable = ({ data, loading }: AmmTransactionsTableProps) => { +export const AmmTransactionsTable = ({ data, loading, symbol }: AmmTransactionsTableProps) => { const mappedData = useMemo( () => data.map((row) => ({ @@ -64,10 +65,12 @@ export const AmmTransactionsTable = ({ data, loading }: AmmTransactionsTableProp /> ), pricePerUnit: , - quantity: , + quantity: ( + + ), amount: , })), - [data] + [data, symbol] ) return ( diff --git a/packages/atlas/src/components/_crt/CrtPortfolioTable/CrtPortfolioTable.tsx b/packages/atlas/src/components/_crt/CrtPortfolioTable/CrtPortfolioTable.tsx index a0b572ad5f..4e8be4f796 100644 --- a/packages/atlas/src/components/_crt/CrtPortfolioTable/CrtPortfolioTable.tsx +++ b/packages/atlas/src/components/_crt/CrtPortfolioTable/CrtPortfolioTable.tsx @@ -209,7 +209,12 @@ export const TokenPortfolioUtils = ({ const [ref, setRef] = useState(null) return ( - + { + e.stopPropagation() + e.preventDefault() + }} + > - ) : ( - - ) - ) : ( - - )} - - - - - - - - - {hasAnotherUnsyncedChannel && selectedChannelTitle && ( - <> - Your channel "{selectedChannelTitle}" is already part of the YouTube Partner Program.{' '} - - Select a different channel - {' '} - to apply again. - - )} - {yppAtlasStatus !== 'ypp-signed' && 'It takes under 1 minute and is 100% free.'} - - - - - - - - - {items && items.length >= 7 && ( - - )} - - ) -} diff --git a/packages/atlas/src/views/global/YppLandingView/oldSections/YppRewardSection.styles.ts b/packages/atlas/src/views/global/YppLandingView/oldSections/YppRewardSection.styles.ts deleted file mode 100644 index 4f74261f00..0000000000 --- a/packages/atlas/src/views/global/YppLandingView/oldSections/YppRewardSection.styles.ts +++ /dev/null @@ -1,58 +0,0 @@ -import styled from '@emotion/styled' - -import { GridItem } from '@/components/LayoutGrid' -import { Button } from '@/components/_buttons/Button' -import { cVar, media, sizes } from '@/styles' -import { Anchor } from '@/views/global/YppLandingView/YppAuthorizationModal/YppAuthorizationModal.styles' - -export const BenefitsCardButton = styled(Button)` - border-radius: 999px; -` - -export const BenefitsCardsButtonsGroup = styled.div` - text-align: center; - display: grid; - overflow-x: auto; - white-space: nowrap; - margin: ${sizes(16)} 0 ${sizes(8)} 0; - - ::-webkit-scrollbar { - display: none; - } - - gap: ${sizes(2)}; - - ${media.sm} { - grid-template-columns: repeat(2, 1fr); - } - - ${media.md} { - grid-template-columns: repeat(3, 1fr); - } - - ${media.lg} { - grid-template-columns: repeat(6, 1fr); - } -` - -export const BenefitsCardsContainerGridItem = styled(GridItem)` - display: grid; - gap: ${sizes(2)}; -` - -export const ColorAnchor = styled(Anchor)` - color: ${cVar('colorTextPrimary')}; -` - -export const RewardsSubtitleGridItem = styled(GridItem)` - display: grid; - gap: ${sizes(4)}; - margin-top: ${sizes(8)}; -` - -export const RewardsSubtitleWrapper = styled.div` - display: flex; - flex-direction: row; - align-items: center; - justify-content: flex-end; -` diff --git a/packages/atlas/src/views/global/YppLandingView/oldSections/YppRewardSection.tsx b/packages/atlas/src/views/global/YppLandingView/oldSections/YppRewardSection.tsx deleted file mode 100644 index b1b2907e96..0000000000 --- a/packages/atlas/src/views/global/YppLandingView/oldSections/YppRewardSection.tsx +++ /dev/null @@ -1,134 +0,0 @@ -import { FC, useRef } from 'react' - -import { Information } from '@/components/Information' -import { FlexGridItem, GridItem, LayoutGrid } from '@/components/LayoutGrid' -import { Text } from '@/components/Text' -import { TooltipText } from '@/components/Tooltip/Tooltip.styles' -import { TierCard } from '@/components/_ypp/TierCard' -import { atlasConfig } from '@/config' -import { useMediaMatch } from '@/hooks/useMediaMatch' -import { getTierRewards } from '@/utils/ypp' -import { useSectionTextVariants } from '@/views/global/YppLandingView/sections/useSectionTextVariants' - -import { ColorAnchor } from './YppRewardSection.styles' - -import { - BackgroundContainer, - CenteredLayoutGrid, - RewardsSubText, - StyledLimitedWidthContainer, - TierCardWrapper, -} from '../YppLandingView.styles' - -export const calculateReward = ( - amount: number | number[] | { min: number | null; max: number } | null, - multiplier: number | number[], - tier: number -) => { - if (amount === null) { - return null - } else if (typeof amount === 'number') { - return { - type: 'number' as const, - amount: amount * (typeof multiplier === 'number' ? multiplier : multiplier[tier]), - } - } else if (Array.isArray(amount)) { - return { - type: 'number' as const, - amount: amount[tier], - } - } else { - return { type: 'range' as const, min: amount.min, max: amount.max } - } -} - -export const YppRewardSection: FC = () => { - const mdMatch = useMediaMatch('md') - const tiers = atlasConfig.features.ypp.tiersDefinition - const [titleVariant, subtitleVariant] = useSectionTextVariants() - const ref = useRef(null) - - if (!tiers?.length) { - return null - } - - return ( - - - - - - Rewards based on quality and popularity - - - - - Each participating channel is reviewed by the verification team and assigned to one of the reward tiers - below - - - - - - {tiers.map((tier) => { - const signupMultiplier = tier.tier === 'bronze' ? 1 : atlasConfig.features.ypp.tierBoostMultiplier || 1 - const referralMultiplier = atlasConfig.features.ypp.tierBoostMultiplier || 1 - const modifiedRewards = [ - tier.rewards[0] * signupMultiplier, - tier.rewards[1], - (getTierRewards('diamond')?.referral || 0) * referralMultiplier, - ] - return - })} - - *Referral rewards depend on the tier of the invited channel. - - - - - Payments are made in {atlasConfig.joystream.tokenTicker} tokens - - - {atlasConfig.joystream.tokenTicker} token is a native crypto asset of Joystream blockchain which - powers {atlasConfig.general.appName}. It is used for trading Creator Tokens, NFTs and covering - blockchain processing fees. It is also used for voting on proposals and partaking in council - elections.{' '} - - Purchase {atlasConfig.joystream.tokenTicker} - - - } - multiline - reference={ref.current} - /> - - - - - ) -} diff --git a/packages/atlas/src/views/global/YppLandingView/oldSections/YppSignupVideo.tsx b/packages/atlas/src/views/global/YppLandingView/oldSections/YppSignupVideo.tsx deleted file mode 100644 index d43a96c59f..0000000000 --- a/packages/atlas/src/views/global/YppLandingView/oldSections/YppSignupVideo.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import styled from '@emotion/styled' - -import { GridItem } from '@/components/LayoutGrid' -import { Text } from '@/components/Text' -import { SkeletonLoader } from '@/components/_loaders/SkeletonLoader' -import { useMediaMatch } from '@/hooks/useMediaMatch' -import { - BackgroundContainer, - CenteredLayoutGrid, - StyledLimitedWidthContainerVideo, -} from '@/views/global/YppLandingView/YppLandingView.styles' -import { useSectionTextVariants } from '@/views/global/YppLandingView/sections/useSectionTextVariants' - -export const YppSignupVideo = () => { - const mdMatch = useMediaMatch('md') - const [titleVariant, subtitleVariant] = useSectionTextVariants() - - return ( - - - - - - Sign up in 60 seconds - - - Watch the sign up demo by one of Joystream members. - - - - - - - - - - - ) -} - -const PlayerContainer = styled.div` - width: 100%; - position: relative; - aspect-ratio: 16/9; -` - -export const PlayerSkeletonLoader = styled(SkeletonLoader)` - position: absolute; - top: 0; -` - -const IframeVideo = styled.iframe` - border: none; - width: 640px; - height: 364px; - max-width: 100%; - max-height: 55vw; -` diff --git a/packages/atlas/src/views/global/YppLandingView/sections/YppHero.tsx b/packages/atlas/src/views/global/YppLandingView/sections/YppHero.tsx index 5b89c27aa4..98758fbc1d 100644 --- a/packages/atlas/src/views/global/YppLandingView/sections/YppHero.tsx +++ b/packages/atlas/src/views/global/YppLandingView/sections/YppHero.tsx @@ -158,6 +158,7 @@ export const YppHero: FC = ({ onSignUpClick, yppAtlasStatus, onVie fullWidth={!xsMatch} size={xxsMatch && !xsMatch ? 'large' : smMatch ? 'large' : 'medium'} variant="secondary" + id="rewards-new-channel-button" > Create New Channel diff --git a/packages/atlas/src/views/studio/CrtDashboard/tabs/CrtMarketTab.tsx b/packages/atlas/src/views/studio/CrtDashboard/tabs/CrtMarketTab.tsx index 9b1e35dc04..dc45cf9830 100644 --- a/packages/atlas/src/views/studio/CrtDashboard/tabs/CrtMarketTab.tsx +++ b/packages/atlas/src/views/studio/CrtDashboard/tabs/CrtMarketTab.tsx @@ -100,7 +100,11 @@ export const CrtMarketTab = ({ token }: CrtMarketTabProps) => { }} /> - + ) } diff --git a/packages/atlas/src/views/studio/YppDashboard/tabs/YppDashboardMainTab.tsx b/packages/atlas/src/views/studio/YppDashboard/tabs/YppDashboardMainTab.tsx index f1f4d57979..abe6917fd0 100644 --- a/packages/atlas/src/views/studio/YppDashboard/tabs/YppDashboardMainTab.tsx +++ b/packages/atlas/src/views/studio/YppDashboard/tabs/YppDashboardMainTab.tsx @@ -53,7 +53,7 @@ const benefitsMetadata = { }, twitterPost: { title: 'Post on X', - description: `Follow JoystreamDAO on X and post about why you signed up to ${atlasConfig.general.appName} using hashtag #${atlasConfig.general.appName}Web3Creators mentioning @JoystreamDAO to get a chance of weekly reward.`, + description: `Follow JoystreamDAO on X and post about why you signed up to ${atlasConfig.general.appName} using hashtag #${atlasConfig.general.appName}Web3Creators mentioning @JoystreamDAO and your ${atlasConfig.general.appName} Channel Name to get a chance of weekly reward.`, reward: '10 USD', actionLink: 'https://twitter.com/joystreamdao?lang=en', tooltipLink: @@ -491,7 +491,7 @@ export const YppDashboardMainTab: FC = () => { rewardNode={benefitsMetadata.shareNft.reward} description={ <> - Drop the link of your post to{' '} + Share NFT from Gleev on social media of your choice and drop the link of your post to{' '} #shared-NFTs on Discord to participate in rewards. @@ -518,7 +518,7 @@ export const YppDashboardMainTab: FC = () => { rewardNode={benefitsMetadata.shareToken.reward} description={ <> - Drop the link of your post to{' '} + Share your CRT page from Gleev on social media of your choice and drop the link of your post to{' '} #shared-CRTs on Discord to participate in rewards. @@ -595,7 +595,7 @@ const SilverTierWrapper = styled(TierWrapper)` export const BenefitsContainer = ({ children, title }: { children: ReactNode[] | ReactNode; title: string }) => { const drawer = useRef(null) - const [isDrawerActive, setDrawerActive] = useState(true) + const [isDrawerActive, setDrawerActive] = useState(false) return ( diff --git a/packages/atlas/src/views/viewer/ChannelView/ChannelView.tsx b/packages/atlas/src/views/viewer/ChannelView/ChannelView.tsx index 73b10e6035..401cbbfddd 100644 --- a/packages/atlas/src/views/viewer/ChannelView/ChannelView.tsx +++ b/packages/atlas/src/views/viewer/ChannelView/ChannelView.tsx @@ -7,6 +7,7 @@ import { useParams, useSearchParams } from 'react-router-dom' import { useChannelNftCollectors, useFullChannel } from '@/api/hooks/channel' import { useVideoCount } from '@/api/hooks/video' import { OwnedNftOrderByInput, VideoOrderByInput } from '@/api/queries/__generated__/baseTypes.generated' +import { useGetFullCreatorTokenQuery } from '@/api/queries/__generated__/creatorTokens.generated' import { useGetNftsCountQuery } from '@/api/queries/__generated__/nfts.generated' import { SvgActionCheck, SvgActionFilters, SvgActionFlag, SvgActionMore, SvgActionPlus } from '@/assets/icons' import { ChannelTitle } from '@/components/ChannelTitle' @@ -24,6 +25,7 @@ import { CollectorsBox } from '@/components/_channel/CollectorsBox' import { ContextMenu } from '@/components/_overlays/ContextMenu' import { ReportModal } from '@/components/_overlays/ReportModal' import { atlasConfig } from '@/config' +import { getPublicCryptoVideoFilter } from '@/config/contentFilter' import { absoluteRoutes } from '@/config/routes' import { NFT_SORT_OPTIONS, VIDEO_SORT_OPTIONS } from '@/config/sorting' import { useGetAssetUrl } from '@/hooks/useGetAssetUrl' @@ -91,21 +93,17 @@ export const ChannelView: FC = () => { const filteredTabs = TABS.filter((tab) => tab === 'Token' ? !!tab && (isChannelOwner || !!channel?.creatorToken?.token.id) : !!tab ) + const { data: tokenData } = useGetFullCreatorTokenQuery({ + variables: { id: channel?.creatorToken?.token.id ?? '' }, + skip: !channel?.creatorToken?.token.id, + }) const { videoCount } = useVideoCount({ - where: { + where: getPublicCryptoVideoFilter({ channel: { id_eq: id, }, - isPublic_eq: true, createdAt_lt: USER_TIMESTAMP, - isCensored_eq: false, - thumbnailPhoto: { - isAccepted_eq: true, - }, - media: { - isAccepted_eq: true, - }, - }, + }), }) const { data: nftCountData } = useGetNftsCountQuery({ variables: { @@ -220,7 +218,14 @@ export const ChannelView: FC = () => { const mappedTabs = filteredTabs.map((tab) => ({ name: tab, - pillText: tab === 'Videos' ? videoCount : tab === 'NFTs' ? nftCountData?.ownedNftsConnection.totalCount : undefined, + pillText: + tab === 'Videos' + ? videoCount + : tab === 'NFTs' + ? nftCountData?.ownedNftsConnection.totalCount + : tab === 'Token' + ? tokenData?.creatorTokenById?.symbol ?? undefined + : undefined, })) const getChannelContent = (tab: (typeof TABS)[number]) => { diff --git a/packages/atlas/src/views/viewer/ChannelView/ChannelViewTabs/ChannelVideos.tsx b/packages/atlas/src/views/viewer/ChannelView/ChannelViewTabs/ChannelVideos.tsx index f3d82771b7..6472716bb6 100644 --- a/packages/atlas/src/views/viewer/ChannelView/ChannelViewTabs/ChannelVideos.tsx +++ b/packages/atlas/src/views/viewer/ChannelView/ChannelViewTabs/ChannelVideos.tsx @@ -7,6 +7,7 @@ import { EmptyFallback } from '@/components/EmptyFallback' import { Grid } from '@/components/Grid' import { ViewErrorFallback } from '@/components/ViewErrorFallback' import { VideoTileViewer } from '@/components/_video/VideoTileViewer' +import { getPublicCryptoVideoFilter } from '@/config/contentFilter' import { transitions } from '@/styles' import { createPlaceholderData } from '@/utils/data' import { SentryLogger } from '@/utils/logs' @@ -49,21 +50,12 @@ export const ChannelVideos: FC = ({ orderBy: sortVideosBy, limit: tilesPerPage, offset: currentPage * tilesPerPage, - where: { + where: getPublicCryptoVideoFilter({ channel: { id_eq: channelId, }, - isPublic_eq: true, createdAt_lt: USER_TIMESTAMP, - isCensored_eq: false, - isShort_eq: false, - thumbnailPhoto: { - isAccepted_eq: true, - }, - media: { - isAccepted_eq: true, - }, - }, + }), }, }) diff --git a/packages/atlas/src/views/viewer/MarketplaceView/tabs/MarketplaceCrtTab.tsx b/packages/atlas/src/views/viewer/CrtMarketplaceView/CrtMarketplaceView.tsx similarity index 78% rename from packages/atlas/src/views/viewer/MarketplaceView/tabs/MarketplaceCrtTab.tsx rename to packages/atlas/src/views/viewer/CrtMarketplaceView/CrtMarketplaceView.tsx index 76714134c1..f11c726d1a 100644 --- a/packages/atlas/src/views/viewer/MarketplaceView/tabs/MarketplaceCrtTab.tsx +++ b/packages/atlas/src/views/viewer/CrtMarketplaceView/CrtMarketplaceView.tsx @@ -9,13 +9,15 @@ import { Section } from '@/components/Section/Section' import { TopEarningChannels } from '@/components/TopEarningChannels' import { AllTokensSection } from '@/components/_crt/AllTokensSection' import { CrtCard, CrtSaleTypes } from '@/components/_crt/CrtCard/CrtCard' +import { useHeadTags } from '@/hooks/useHeadTags' import { useMediaMatch } from '@/hooks/useMediaMatch' import { hapiBnToTokenNumber } from '@/joystream-lib/utils' -import { TableFullWitdhtWrapper } from '@/views/viewer/MarketplaceView/MarketplaceView.styles' +import { cVar, media, sizes } from '@/styles' -import { responsive } from '../FeaturedNftsSection/FeaturedNftsSection' +import { responsive } from '../NftMarketplaceView/FeaturedNftsSection/FeaturedNftsSection' -export const MarketplaceCrtTab = () => { +export const CrtMarketplaceView = () => { + const headTags = useHeadTags('CRT - Marketplace') const mdMatch = useMediaMatch('md') const { data, loading } = useGetBasicCreatorTokensQuery({ variables: { @@ -66,7 +68,8 @@ export const MarketplaceCrtTab = () => { ) ?? [] return ( - <> + + {headTags} {featuredCrts.length > 4 && ( @@ -91,10 +94,31 @@ export const MarketplaceCrtTab = () => { - + ) } const StyledCrtCard = styled(CrtCard)` min-height: 100%; ` + +const MarketplaceWrapper = styled.div` + padding: ${sizes(4)} 0; + display: grid; + gap: ${sizes(8)}; + ${media.md} { + padding: ${sizes(8)} 0; + gap: ${sizes(16)}; + } +` + +const TableFullWitdhtWrapper = styled.div` + width: calc(100% + var(--size-global-horizontal-padding) * 2); + margin-left: calc(var(--size-global-horizontal-padding) * -1); + background-color: ${cVar('colorBackgroundMuted')}; + padding: ${sizes(8)} var(--size-global-horizontal-padding); + + ${media.md} { + padding: ${sizes(16)} var(--size-global-horizontal-padding); + } +` diff --git a/packages/atlas/src/views/viewer/CrtMarketplaceView/index.ts b/packages/atlas/src/views/viewer/CrtMarketplaceView/index.ts new file mode 100644 index 0000000000..d0764ec3cd --- /dev/null +++ b/packages/atlas/src/views/viewer/CrtMarketplaceView/index.ts @@ -0,0 +1 @@ +export * from './CrtMarketplaceView' diff --git a/packages/atlas/src/views/viewer/MarketplaceView/MarketplaceView.styles.ts b/packages/atlas/src/views/viewer/MarketplaceView/MarketplaceView.styles.ts deleted file mode 100644 index cba5c97eb8..0000000000 --- a/packages/atlas/src/views/viewer/MarketplaceView/MarketplaceView.styles.ts +++ /dev/null @@ -1,24 +0,0 @@ -import styled from '@emotion/styled' - -import { cVar, media, sizes } from '@/styles' - -export const MarketplaceWrapper = styled.div` - padding: ${sizes(4)} 0; - display: grid; - gap: ${sizes(8)}; - ${media.md} { - padding: ${sizes(8)} 0; - gap: ${sizes(16)}; - } -` - -export const TableFullWitdhtWrapper = styled.div` - width: calc(100% + var(--size-global-horizontal-padding) * 2); - margin-left: calc(var(--size-global-horizontal-padding) * -1); - background-color: ${cVar('colorBackgroundMuted')}; - padding: ${sizes(8)} var(--size-global-horizontal-padding); - - ${media.md} { - padding: ${sizes(16)} var(--size-global-horizontal-padding); - } -` diff --git a/packages/atlas/src/views/viewer/MarketplaceView/MarketplaceView.tsx b/packages/atlas/src/views/viewer/MarketplaceView/MarketplaceView.tsx deleted file mode 100644 index 1c65cea9ad..0000000000 --- a/packages/atlas/src/views/viewer/MarketplaceView/MarketplaceView.tsx +++ /dev/null @@ -1,69 +0,0 @@ -import { FC, useCallback } from 'react' -import { useSearchParams } from 'react-router-dom' - -import { SvgActionCreatorToken, SvgActionPlay } from '@/assets/icons' -import { useMediaMatch } from '@/hooks/useMediaMatch' -import { useMountEffect } from '@/hooks/useMountEffect' -import { useSegmentAnalytics } from '@/hooks/useSegmentAnalytics' -import { StyledPageTabs } from '@/views/viewer/PortfolioView' - -import { MarketplaceWrapper } from './MarketplaceView.styles' -import { MarketplaceCrtTab } from './tabs/MarketplaceCrtTab' -import { MarketplaceNftTab } from './tabs/MarketplaceNftTab' - -const TABS = [ - { - name: 'Creator Tokens', - description: 'Discover channels you can invest in', - icon: , - }, - { - name: 'Video NFTs', - description: 'Explore offers of non-fungible tokens for popular videos', - icon: , - }, -] as const -type TabsNames = (typeof TABS)[number]['name'] - -const getTabIndex = (tabName: TabsNames, allTabs: typeof TABS): number => - allTabs.findIndex((tab) => tab.name === tabName) - -export const MarketplaceView: FC = () => { - const smMatch = useMediaMatch('sm') - const [searchParams, setSearchParams] = useSearchParams() - const { trackPageView } = useSegmentAnalytics() - const currentTabName = searchParams.get('tab') as (typeof TABS)[number]['name'] | null - const currentTab = currentTabName ? getTabIndex(currentTabName, TABS) : 0 - - useMountEffect(() => { - if (currentTab === -1) { - setSearchParams({ 'tab': '0' }, { replace: true }) - } else { - trackPageView(`${TABS[currentTab].name} Marketplace`) - } - }) - - const handleChangeTab = useCallback( - (idx: number) => { - trackPageView(`${TABS[idx].name} Marketplace`) - setSearchParams({ tab: TABS[idx].name }) - }, - [setSearchParams, trackPageView] - ) - - return ( - <> - (smMatch ? tab : { ...tab, description: '' }))} - onSelectTab={handleChangeTab} - selected={currentTab} - /> - - - {currentTab === 0 && } - {currentTab === 1 && } - - - ) -} diff --git a/packages/atlas/src/views/viewer/MarketplaceView/index.ts b/packages/atlas/src/views/viewer/MarketplaceView/index.ts deleted file mode 100644 index 532bfd5251..0000000000 --- a/packages/atlas/src/views/viewer/MarketplaceView/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './MarketplaceView' diff --git a/packages/atlas/src/views/viewer/MarketplaceView/FeaturedNftsSection/FeatureNftModal.tsx b/packages/atlas/src/views/viewer/NftMarketplaceView/FeaturedNftsSection/FeatureNftModal.tsx similarity index 100% rename from packages/atlas/src/views/viewer/MarketplaceView/FeaturedNftsSection/FeatureNftModal.tsx rename to packages/atlas/src/views/viewer/NftMarketplaceView/FeaturedNftsSection/FeatureNftModal.tsx diff --git a/packages/atlas/src/views/viewer/MarketplaceView/FeaturedNftsSection/FeaturedNftsSection.styles.ts b/packages/atlas/src/views/viewer/NftMarketplaceView/FeaturedNftsSection/FeaturedNftsSection.styles.ts similarity index 100% rename from packages/atlas/src/views/viewer/MarketplaceView/FeaturedNftsSection/FeaturedNftsSection.styles.ts rename to packages/atlas/src/views/viewer/NftMarketplaceView/FeaturedNftsSection/FeaturedNftsSection.styles.ts diff --git a/packages/atlas/src/views/viewer/MarketplaceView/FeaturedNftsSection/FeaturedNftsSection.tsx b/packages/atlas/src/views/viewer/NftMarketplaceView/FeaturedNftsSection/FeaturedNftsSection.tsx similarity index 100% rename from packages/atlas/src/views/viewer/MarketplaceView/FeaturedNftsSection/FeaturedNftsSection.tsx rename to packages/atlas/src/views/viewer/NftMarketplaceView/FeaturedNftsSection/FeaturedNftsSection.tsx diff --git a/packages/atlas/src/views/viewer/MarketplaceView/tabs/MarketplaceNftTab.tsx b/packages/atlas/src/views/viewer/NftMarketplaceView/NftMarketplaceView.tsx similarity index 54% rename from packages/atlas/src/views/viewer/MarketplaceView/tabs/MarketplaceNftTab.tsx rename to packages/atlas/src/views/viewer/NftMarketplaceView/NftMarketplaceView.tsx index 740b743539..2c9c6b9297 100644 --- a/packages/atlas/src/views/viewer/MarketplaceView/tabs/MarketplaceNftTab.tsx +++ b/packages/atlas/src/views/viewer/NftMarketplaceView/NftMarketplaceView.tsx @@ -1,19 +1,22 @@ +import styled from '@emotion/styled' + import { useFeaturedNftsVideos } from '@/api/hooks/nfts' import { AllNftSection } from '@/components/AllNftSection' import { LimitedWidthContainer } from '@/components/LimitedWidthContainer' import { MarketplaceCarousel } from '@/components/NftCarousel/MarketplaceCarousel' import { TopSellingChannelsTable } from '@/components/TopSellingChannelsTable' import { useHeadTags } from '@/hooks/useHeadTags' -import { FeaturedNftsSection } from '@/views/viewer/MarketplaceView/FeaturedNftsSection/FeaturedNftsSection' -import { TableFullWitdhtWrapper } from '@/views/viewer/MarketplaceView/MarketplaceView.styles' +import { cVar, media, sizes } from '@/styles' + +import { FeaturedNftsSection } from './FeaturedNftsSection/FeaturedNftsSection' -export const MarketplaceNftTab = () => { +export const NftMarketplaceView = () => { const headTags = useHeadTags('NFT - Marketplace') const { nfts, loading } = useFeaturedNftsVideos() return ( - <> + {headTags} @@ -25,6 +28,27 @@ export const MarketplaceNftTab = () => { - + ) } + +const MarketplaceWrapper = styled.div` + padding: ${sizes(4)} 0; + display: grid; + gap: ${sizes(8)}; + ${media.md} { + padding: ${sizes(8)} 0; + gap: ${sizes(16)}; + } +` + +const TableFullWitdhtWrapper = styled.div` + width: calc(100% + var(--size-global-horizontal-padding) * 2); + margin-left: calc(var(--size-global-horizontal-padding) * -1); + background-color: ${cVar('colorBackgroundMuted')}; + padding: ${sizes(8)} var(--size-global-horizontal-padding); + + ${media.md} { + padding: ${sizes(16)} var(--size-global-horizontal-padding); + } +` diff --git a/packages/atlas/src/views/viewer/NftMarketplaceView/index.ts b/packages/atlas/src/views/viewer/NftMarketplaceView/index.ts new file mode 100644 index 0000000000..36e729502b --- /dev/null +++ b/packages/atlas/src/views/viewer/NftMarketplaceView/index.ts @@ -0,0 +1 @@ +export * from './NftMarketplaceView' diff --git a/packages/atlas/src/views/viewer/PortfolioView/tabs/PortfolioNftTab.tsx b/packages/atlas/src/views/viewer/PortfolioView/tabs/PortfolioNftTab.tsx index 40df727aa4..ff27b5fb23 100644 --- a/packages/atlas/src/views/viewer/PortfolioView/tabs/PortfolioNftTab.tsx +++ b/packages/atlas/src/views/viewer/PortfolioView/tabs/PortfolioNftTab.tsx @@ -60,7 +60,7 @@ export const PortfolioNftTab = () => { title="You don’t own any NFTs yet" subtitle="When you buy any NFTs you will be able to manage them and view from this page." button={ - } diff --git a/packages/atlas/src/views/viewer/VideoView/CommentThread.tsx b/packages/atlas/src/views/viewer/VideoView/CommentThread.tsx index 324634dd69..96840a16d0 100644 --- a/packages/atlas/src/views/viewer/VideoView/CommentThread.tsx +++ b/packages/atlas/src/views/viewer/VideoView/CommentThread.tsx @@ -1,7 +1,7 @@ import styled from '@emotion/styled' import { FC, memo, useState } from 'react' -import { UserCommentReactions, useComment, useCommentRepliesConnection } from '@/api/hooks/comments' +import { UserCommentReactions, useCommentRepliesConnection } from '@/api/hooks/comments' import { SvgActionChevronB } from '@/assets/icons' import { TextButton } from '@/components/_buttons/Button' import { Comment, CommentProps } from '@/components/_comments/Comment' @@ -31,21 +31,16 @@ const _CommentThread: FC = ({ ...commentProps }) => { const [repliesOpen, setRepliesOpen] = useState(false) - const [newReplyId, setNewReplyId] = useState(null) const { replies, loading, fetchMore, pageInfo } = useCommentRepliesConnection({ - skip: !commentId || !video?.id || !repliesOpen || !hasAnyReplies, + skip: !commentId || !video?.id, variables: { first: INITIAL_REPLIES_COUNT, parentCommentId: commentId || '', }, - notifyOnNetworkStatusChange: true, + notifyOnNetworkStatusChange: false, }) - const { comment: newReply } = useComment({ commentId: newReplyId || '' }, { skip: !newReplyId }) - - const allRepliesContainNewReply = !!replies.find((r) => r.id === newReplyId) - const placeholderItems = loading ? createPlaceholderData(LOAD_MORE_REPLIES_COUNT) : [] const handleLoadMore = () => { @@ -69,7 +64,6 @@ const _CommentThread: FC = ({ userReactions={userReactionsLookup && commentId ? userReactionsLookup[commentId] : undefined} {...commentProps} isReplyable={true} - onReplyPosted={setNewReplyId} /> {linkedReplyId && !repliesOpen && ( = ({ Load more replies ) : null} - {newReplyId && !allRepliesContainNewReply ? ( - newReply ? ( - - ) : ( - // new reply is loading, display an empty skeleton Comment - ) - ) : null} )} diff --git a/packages/atlas/src/views/viewer/VideoView/CommentsSection.tsx b/packages/atlas/src/views/viewer/VideoView/CommentsSection.tsx index ec8ae53604..6cd6df3944 100644 --- a/packages/atlas/src/views/viewer/VideoView/CommentsSection.tsx +++ b/packages/atlas/src/views/viewer/VideoView/CommentsSection.tsx @@ -1,14 +1,13 @@ import { NetworkStatus } from '@apollo/client' -import { debounce } from 'lodash-es' -import { FC, useEffect, useMemo, useRef, useState } from 'react' +import { FC, ReactElement, useEffect, useMemo, useRef, useState } from 'react' import { useParams } from 'react-router-dom' -import { useComment, useCommentSectionComments, useUserCommentsReactions } from '@/api/hooks/comments' +import { useComment, useUserCommentsReactions } from '@/api/hooks/comments' +import { useCommentSectionComments } from '@/api/hooks/useCommentSectionComments' import { CommentOrderByInput } from '@/api/queries/__generated__/baseTypes.generated' import { FullVideoFieldsFragment } from '@/api/queries/__generated__/fragments.generated' import { EmptyFallback } from '@/components/EmptyFallback' import { Text } from '@/components/Text' -import { LoadMoreButton } from '@/components/_buttons/LoadMoreButton' import { Comment } from '@/components/_comments/Comment' import { CommentInput } from '@/components/_comments/CommentInput' import { Select } from '@/components/_inputs/Select' @@ -22,14 +21,10 @@ import { getMemberAvatar } from '@/providers/assets/assets.helpers' import { useFee } from '@/providers/joystream' import { useUser } from '@/providers/user/user.hooks' import { createPlaceholderData } from '@/utils/data' +import { InfiniteLoadingOffsets } from '@/utils/loading.contants' import { CommentThread } from './CommentThread' -import { - CommentWrapper, - CommentsSectionHeader, - CommentsSectionWrapper, - LoadMoreCommentsWrapper, -} from './VideoView.styles' +import { CommentsSectionHeader, CommentsSectionWrapper, CommentsStyledSection } from './VideoView.styles' type CommentsSectionProps = { disabled?: boolean @@ -47,10 +42,10 @@ export const CommentsSection: FC = ({ disabled, video, vid const [commentInputIsProcessing, setCommentInputIsProcessing] = useState(false) const [highlightedCommentId, setHighlightedCommentId] = useState(null) const [sortCommentsBy, setSortCommentsBy] = useState(COMMENTS_SORT_OPTIONS[0].value) - const [commentsOpen, setCommentsOpen] = useState(false) const [commentInputActive, setCommentInputActive] = useState(false) const commentIdQueryParam = useRouterQuery(QUERY_PARAMS.COMMENT_ID) const mdMatch = useMediaMatch('md') + const isConsideredMobile = !mdMatch const { id: videoId } = useParams() const { memberId, activeMembership, isLoggedIn } = useUser() const { isLoadingAsset: isMemberAvatarLoading, urls: memberAvatarUrls } = getMemberAvatar(activeMembership) @@ -71,14 +66,11 @@ export const CommentsSection: FC = ({ disabled, video, vid ) const commentsSectionHeaderRef = useRef(null) const commentSectionWrapperRef = useRef(null) - const mobileCommentsOpen = commentsOpen || mdMatch - const [numberOfComments, setNumberOfComments] = useState(mobileCommentsOpen ? INITIAL_COMMENTS : 1) const { comments, loading, fetchMore, pageInfo, networkStatus } = useCommentSectionComments( - { ...queryVariables, first: mobileCommentsOpen ? INITIAL_COMMENTS : 1 }, - { skip: disabled || !videoId, notifyOnNetworkStatusChange: true } + { ...queryVariables, first: isConsideredMobile ? INITIAL_COMMENTS : 1 }, + { skip: disabled || !videoId, notifyOnNetworkStatusChange: false } ) const { userReactions } = useUserCommentsReactions(videoId, memberId) - const { addComment } = useReactionTransactions() const { comment: commentFromUrl, loading: commentFromUrlLoading } = useComment( @@ -107,61 +99,26 @@ export const CommentsSection: FC = ({ disabled, video, vid } } - const setMoreComments = () => setNumberOfComments((prevState) => prevState + INITIAL_COMMENTS) - - // increase number of comments when user scrolls to the end of page - useEffect(() => { - if (!mobileCommentsOpen) { - return - } - const scrollHandler = debounce(() => { - if (!commentSectionWrapperRef.current) return - const scrolledToBottom = document.documentElement.scrollTop >= commentSectionWrapperRef.current.scrollHeight - if (scrolledToBottom && pageInfo?.hasNextPage && !commentsLoading) { - setMoreComments() - } - }, 100) - window.addEventListener('scroll', scrollHandler) - - return () => { - window.removeEventListener('scroll', scrollHandler) - } - }, [commentsLoading, mobileCommentsOpen, pageInfo?.hasNextPage]) - - // fetch more results when user scrolls to end of page - useEffect(() => { - if (pageInfo && numberOfComments !== INITIAL_COMMENTS && comments?.length !== numberOfComments) { - fetchMore({ variables: { ...queryVariables, first: numberOfComments } }) - } - }, [fetchMore, numberOfComments, pageInfo, queryVariables, comments?.length]) - const handleComment = async (parentCommentId?: string) => { if (!videoId || !commentInputText) { return } setCommentInputIsProcessing(true) - const newCommentId = await addComment({ + await addComment({ videoId, commentBody: commentInputText, parentCommentId, videoTitle: video?.title, + optimisticOpts: { + onTxSign: (newCommentId) => { + setCommentInputIsProcessing(false) + setCommentInputText('') + setHighlightedCommentId(newCommentId || null) + }, + }, }) - setCommentInputIsProcessing(false) trackCommentAdded(commentInputText, video?.id ?? 'no data') - - if (newCommentId) { - setCommentInputText('') - setHighlightedCommentId(newCommentId || null) - } - } - - const handleLoadMoreClick = () => { - if (!comments || !comments.length) { - return - } - setCommentsOpen(true) - setMoreComments() } const placeholderItems = commentsLoading ? createPlaceholderData(4) : [] @@ -208,6 +165,7 @@ export const CommentsSection: FC = ({ disabled, video, vid ) } + return ( @@ -243,41 +201,74 @@ export const CommentsSection: FC = ({ disabled, video, vid {comments && !comments.length && !commentsLoading && ( )} - - {displayedCommentFromUrl && ( - 0} - userReactionsLookup={userReactions} - highlightedCommentId={highlightedCommentId} - setHighlightedCommentId={setHighlightedCommentId} - linkedReplyId={parentCommentFromUrl ? commentFromUrl?.id : null} - repliesCount={displayedCommentFromUrl.repliesCount} - /> - )} - {commentsLoading && !isFetchingMore - ? mappedPlaceholders - : filteredComments - ?.map((comment) => ( - 0} - repliesCount={comment.repliesCount} - userReactionsLookup={userReactions} - highlightedCommentId={highlightedCommentId} - setHighlightedCommentId={setHighlightedCommentId} - /> - )) - .concat(isFetchingMore && commentsLoading ? mappedPlaceholders : [])} - - {!mobileCommentsOpen && !commentsLoading && comments && !!comments.length && pageInfo?.hasNextPage && ( - - - - )} + {comments?.length ? ( + + {displayedCommentFromUrl && ( + 0} + userReactionsLookup={userReactions} + highlightedCommentId={highlightedCommentId} + setHighlightedCommentId={setHighlightedCommentId} + linkedReplyId={parentCommentFromUrl ? commentFromUrl?.id : null} + repliesCount={displayedCommentFromUrl.repliesCount} + /> + )} + {commentsLoading && !isFetchingMore + ? mappedPlaceholders + : filteredComments + ?.map((comment) => ( + 0} + repliesCount={comment.repliesCount} + userReactionsLookup={userReactions} + highlightedCommentId={highlightedCommentId} + setHighlightedCommentId={setHighlightedCommentId} + /> + )) + .concat(isFetchingMore && commentsLoading ? mappedPlaceholders : [])} + + ) as unknown as ReactElement[], + }} + footerProps={ + isConsideredMobile + ? { + label: 'Load more comments', + handleLoadMore: async () => { + if (!loading) { + await fetchMore({ variables: { ...queryVariables, first: (comments?.length ?? 0) + 10 } }) + } + return + }, + type: 'link', + } + : { + reachedEnd: !pageInfo?.hasNextPage, + fetchMore: async () => { + if (!loading) { + await fetchMore({ variables: { ...queryVariables, first: (comments?.length ?? 0) + 10 } }) + } + return + }, + type: 'infinite', + loadingTriggerOffset: InfiniteLoadingOffsets.VideoTile, + } + } + /> + ) : null} ) } diff --git a/packages/atlas/src/views/viewer/VideoView/VideoView.styles.ts b/packages/atlas/src/views/viewer/VideoView/VideoView.styles.ts index ee8fb5548e..ebd3950d73 100644 --- a/packages/atlas/src/views/viewer/VideoView/VideoView.styles.ts +++ b/packages/atlas/src/views/viewer/VideoView/VideoView.styles.ts @@ -3,6 +3,7 @@ import styled from '@emotion/styled' import { GridItem, LayoutGrid } from '@/components/LayoutGrid' import { LimitedWidthContainer } from '@/components/LimitedWidthContainer' +import { Section } from '@/components/Section/Section' import { Text } from '@/components/Text' import { Button } from '@/components/_buttons/Button' import { CallToActionWrapper } from '@/components/_buttons/CallToActionButton' @@ -208,6 +209,17 @@ export const CommentWrapper = styled.div` } ` +export const CommentsStyledSection = styled(Section)` + display: grid; + margin-top: ${sizes(8)}; + gap: ${sizes(8)}; + margin-bottom: ${sizes(6)}; + + ${media.md} { + margin-bottom: 0; + } +` + export const LoadMoreCommentsWrapper = styled.div` margin-bottom: ${sizes(8)}; padding-bottom: ${sizes(8)}; diff --git a/packages/atlas/src/views/viewer/VideoView/VideoView.tsx b/packages/atlas/src/views/viewer/VideoView/VideoView.tsx index 73e96335fe..52b0689122 100644 --- a/packages/atlas/src/views/viewer/VideoView/VideoView.tsx +++ b/packages/atlas/src/views/viewer/VideoView/VideoView.tsx @@ -419,24 +419,29 @@ const DetailsItems = ({ if (video?.id) { setVideoReactionProcessing(true) const fee = reactionFee || (await getReactionFee([memberId || '', video?.id, reaction])) - const reacted = await likeOrDislikeVideo(video.id, reaction, video.title, fee) + const prevReaction = video.reactions.find((reaction) => reaction.member.id === memberId) + likeOrDislikeVideo(video.id, reaction, video.title, fee, { + prevReactionId: prevReaction?.id, + onTxSign: () => setVideoReactionProcessing(false), + isRemovingReaction: prevReaction?.reaction === reaction.toUpperCase(), + }) reaction === 'like' ? trackLikeAdded(video.id, memberId ?? 'no data') : trackDislikeAdded(video.id, memberId ?? 'no data') - setVideoReactionProcessing(false) - return reacted + return true } return false }, [ + video?.id, + video?.reactions, + video?.title, + reactionFee, getReactionFee, - likeOrDislikeVideo, memberId, - reactionFee, + likeOrDislikeVideo, trackLikeAdded, trackDislikeAdded, - video?.id, - video?.title, ] ) diff --git a/packages/atlas/src/views/viewer/ViewerLayout.tsx b/packages/atlas/src/views/viewer/ViewerLayout.tsx index 1d2bee806b..2069d02a3b 100644 --- a/packages/atlas/src/views/viewer/ViewerLayout.tsx +++ b/packages/atlas/src/views/viewer/ViewerLayout.tsx @@ -22,12 +22,8 @@ import { useUser } from '@/providers/user/user.hooks' import { media, transitions } from '@/styles' import { RoutingState } from '@/types/routing' -// Currently the newest version is at main file and the old one was moved to new file -const YppLandingViewTest = lazy(() => - import('@/views/global/YppLandingView').then((module) => ({ default: module.YppLandingView })) -) const YppLandingView = lazy(() => - import('@/views/global/YppLandingView/YppLandingViewOld').then((module) => ({ default: module.YppLandingViewOld })) + import('@/views/global/YppLandingView').then((module) => ({ default: module.YppLandingView })) ) const MemberNotificationsView = lazy(() => import('@/views/notifications').then((module) => ({ default: module.MemberNotificationsView })) @@ -36,7 +32,12 @@ const CategoryView = lazy(() => import('./CategoryView').then((module) => ({ def const ChannelView = lazy(() => import('./ChannelView').then((module) => ({ default: module.ChannelView }))) const ChannelsView = lazy(() => import('./ChannelsView').then((module) => ({ default: module.ChannelsView }))) const HomeView = lazy(() => import('./HomeView').then((module) => ({ default: module.HomeView }))) -const MarketplaceView = lazy(() => import('./MarketplaceView').then((module) => ({ default: module.MarketplaceView }))) +const NftMarketplaceView = lazy(() => + import('./NftMarketplaceView').then((module) => ({ default: module.NftMarketplaceView })) +) +const CrtMarketplaceView = lazy(() => + import('./CrtMarketplaceView').then((module) => ({ default: module.CrtMarketplaceView })) +) const MemberView = lazy(() => import('./MemberView').then((module) => ({ default: module.MemberView }))) const MembershipSettingsView = lazy(() => import('./MembershipSettingsView').then((module) => ({ default: module.MembershipSettingsView })) @@ -62,12 +63,10 @@ const viewerRoutes = [ { path: relativeRoutes.viewer.category(), element: }, { path: relativeRoutes.viewer.memberById(), element: }, { path: relativeRoutes.viewer.member(), element: }, - { path: relativeRoutes.viewer.marketplace(), element: }, + { path: relativeRoutes.viewer.crtMarketplace(), element: }, + { path: relativeRoutes.viewer.nftMarketplace(), element: }, ...(atlasConfig.features.ypp.googleConsoleClientId - ? [ - { path: relativeRoutes.viewer.ypp(), element: }, - { path: relativeRoutes.viewer.yppTest(), element: }, - ] + ? [{ path: relativeRoutes.viewer.ypp(), element: }] : []), { path: relativeRoutes.viewer.referrals(), element: }, ]