Browse Source

Merge branch 'dev'

Artem 1 year ago
parent
commit
bc02d79bd8
28 changed files with 1892 additions and 279 deletions
  1. 20 16
      packages/atlas/atlas.config.yml
  2. 3 1
      packages/atlas/package.json
  3. 5 1
      packages/atlas/src/.env
  4. 24 0
      packages/atlas/src/AnalyticsManager.tsx
  5. 19 10
      packages/atlas/src/CommonProviders.tsx
  6. 8 8
      packages/atlas/src/components/_nft/NftTile/NftTileDetails.tsx
  7. 6 7
      packages/atlas/src/components/_overlays/AdminModal/AdminModal.tsx
  8. 52 19
      packages/atlas/src/components/_overlays/ChangePriceDialog/ChangePriceDialog.tsx
  9. 5 3
      packages/atlas/src/components/_overlays/ContentTypeDialog/ContentTypeDialog.tsx
  10. 10 0
      packages/atlas/src/config/configSchema.ts
  11. 9 23
      packages/atlas/src/config/env.ts
  12. 6 2
      packages/atlas/src/hooks/useNftTransactions.tsx
  13. 20 0
      packages/atlas/src/hooks/useSegmentAnalytics.ts
  14. 1 0
      packages/atlas/src/index.html
  15. 9 6
      packages/atlas/src/providers/environment/store.ts
  16. 6 0
      packages/atlas/src/providers/nftActions/nftActions.provider.tsx
  17. 31 0
      packages/atlas/src/providers/segmentAnalytics/segment.provider.tsx
  18. 5 0
      packages/atlas/src/providers/segmentAnalytics/segment.types.ts
  19. 7 0
      packages/atlas/src/providers/segmentAnalytics/useSegmentAnalyticsContext.ts
  20. 31 27
      packages/atlas/src/providers/transactions/transactions.hooks.ts
  21. 22 0
      packages/atlas/src/utils/envVariables.ts
  22. 7 11
      packages/atlas/src/views/viewer/MemberView/ActivityItem.styles.ts
  23. 2 4
      packages/atlas/src/views/viewer/MemberView/ActivityItem.tsx
  24. 12 6
      packages/atlas/src/views/viewer/MemberView/MemberAbout.styles.ts
  25. 22 23
      packages/atlas/src/views/viewer/MemberView/MemberAbout.tsx
  26. 4 4
      packages/atlas/src/views/viewer/MemberView/MemberActivity.styles.ts
  27. 9 9
      packages/atlas/src/views/viewer/MemberView/MemberActivity.tsx
  28. 1537 99
      yarn.lock

+ 20 - 16
packages/atlas/atlas.config.yml

@@ -144,10 +144,6 @@ features:
 
         If you opt in to the Auto-sync feature you also agree to [YouTube's Terms of Service (https://www.youtube.com/t/terms)](https://www.youtube.com/t/terms) and [Google's Privacy Policy (https://www.google.com/policies/privacy).](https://www.google.com/policies/privacy)
 
-        ## Auto-sync service
-
-        In order to simplify the upload of content to $VITE_APP_NAME App, JS Genesis team has built a dedicated backend application, integrated with YouTube and $VITE_APP_NAME to facilitate the content upload. It is hosted on the JSGenesis operated infrastructure and operated by JS Genesis team.
-
         This requests access to the necessary information via YouTube API, namely "read_only" scope to your YouTube channel data. This happens during the authorisation flow as part of the YPP program onboarding. The App fetches only the information that YouTube exposes in the read_only scope and required to effectively operate the program via the open API.
 
         Account Info: email address.
@@ -182,6 +178,10 @@ features:
 
         License can be updated manually for each videos individually from the list of available options.
 
+        ## Auto-sync service
+
+        In order to simplify the upload of content to $VITE_APP_NAME App, JS Genesis team has built a dedicated backend application, integrated with YouTube and $VITE_APP_NAME to facilitate the content upload. It is hosted on the JSGenesis operated infrastructure and operated by JS Genesis team.
+
         ## Opt out
 
         Channel can opt out from auto-sync feature by choosing this option in the settings tab of the YPP dashboard. Opting out from Auto-sync would stop automated upload of all new content published to YouTube channel, but would keep the $VITE_APP_NAME channel in the YPP program, meaning that referral rewards can still be collected.
@@ -443,6 +443,8 @@ content:
       name: Thai
     - isoCode: tr
       name: Turkish
+    - isoCode: uk
+      name: Ukrainian
     - isoCode: ur
       name: Urdu
     - isoCode: vi
@@ -461,12 +463,16 @@ analytics:
     rootHostname: '$VITE_LIVESESSION_ROOT_HOSTNAME'
   usersnap: # Usersnap can be used to capture user feedback
     id: '$VITE_USERSNAP_ID'
+  GA: # Google Analytics
+    id: '$VITE_GA_ID'
+  segment: # Segment Analytics
+    id: '$VITE_SEGMENT_ID'
 
 legal:
   termsOfService: |
     # Terms of Service
 
-    Last updated on the 29th of May 2023
+    Last updated on the 9th of June 2023
 
     This Terms of Service ("Agreement") is a binding obligation between you ("User") and Jsgenesis AS ("Company", "We", "Us", "Our") for use of our Joystream Player interface ("$VITE_APP_NAME") hosted at play.joystream.org and all other products (collectively "Software") developed and published by Us.
 
@@ -486,7 +492,7 @@ legal:
 
     ## 3. Privacy Policy
 
-    Please see our Privacy Policy [(https://www.joystream.org/privacy-policy/)](https://www.joystream.org/privacy-policy/) for information regarding privacy.
+    Please see our Privacy Policy [(https://gleev.xyz/legal/privacyPolicy)](https://gleev.xyz/legal/privacyPolicy/) for information regarding privacy.
 
     ## 4. Membership
 
@@ -605,7 +611,7 @@ legal:
     Licenses supported may be updated at any time and full set of licenses that are available for selection in the App upon video upload or uploaded via Command Line interface are contained in [this file](https://github.com/Joystream/atlas/blob/master/packages/atlas/src/data/knownLicenses.json)
   privacyPolicy: |
     # 1. Privacy Policy
-    Last updated on the 23rd of February 2022
+    Last updated on the 9th of June 2023
 
     ## 1.1 Agreement to the Policy
 
@@ -617,7 +623,7 @@ legal:
 
     This Privacy Policy may be changed at the sole discretion of Company. If any material changes are made, the User will be notified in the Service that is used. Note that adding new products to be included in the term Software, e.g. a new User facing product replacing the App or a new tool for uploading Content, is not considered material as it will not affect Users unless they adopt the new product. Changing software names, terminology used in this Privacy Policy, and changing link locations are also examples of non-material changes.
 
-    ## 1.3 Information colleted
+    ## 1.3 Information collected
 
     All data written to the Blockchain, is implicitly collected not only by Company, but also anyone else in the world that is running the Full Node locally, or accessed via the App or a third party. This includes, but is not limited to, Content hashes, Membership profile, Memo field, and any other way a User can record data on the Blockchain. ‍
 
@@ -641,18 +647,16 @@ legal:
 
     We use cookies for the following purposes our Service:
 
-      - Provide Analytics
-      - Store preferences
-      - Persistant local storage of Keys and Membership.
+    Essential cookies for storing guest anonymous user session information and store preferences.
+    Non-essential cookies to provide Website Analytics.
 
     ## 2.3 Third-party Cookies
 
-    In addition to our own cookies, we also use various third-party cookies to report usage statistics of the Service, deliver advertisements on and through the Service, and so on. They include:
+    In addition to our own cookies, we also use various third-party cookies to report usage statistics of the Service. They include:
 
-      - Google Analytics
-      - Mailchimp (Only when signing up for any of our newsletters)
-      - Godaddy
-      - Sentry
+    - Google Analytics: collect information and report site usage statistics without personally identifying individual visitors to Google. '_ga', the main cookie used by Google Analytics, enables a service to distinguish one visitor from another.
+    - Segment CDP:  writes the user's IDs to the user's local storage and uses that as the Segment ID on the cookie whenever possible.
+    - Livesession: writes cookies to distinguish users from one another for the purpose of tracking website usage patterns.
 
     ## 2.4 Regarding Your Cookies
 

+ 3 - 1
packages/atlas/package.json

@@ -43,6 +43,7 @@
     "@livesession/sdk": "^1.1.4",
     "@loadable/component": "^5.15.2",
     "@lottiefiles/react-lottie-player": "^3.5.0",
+    "@segment/analytics-next": "^1.53.0",
     "@sentry/react": "^7.53.1",
     "@talismn/connect-wallets": "^1.2.1",
     "@tippyjs/react": "^4.2.6",
@@ -137,12 +138,13 @@
     "js-yaml": "^4.1.0",
     "madge": "^5.0.1",
     "postcss-syntax": "^0.36.2",
+    "react-ga": "^3.3.1",
     "react-hooks-testing-library": "^0.6.0",
     "rimraf": "^3.0.2",
     "rollup-plugin-visualizer": "^5.8.3",
     "storybook": "7.0.7",
     "style-dictionary": "^3.7.1",
-    "vite": "^4.0.1",
+    "vite": "^4.3.9",
     "vite-plugin-checker": "^0.5.2",
     "vitest": "^0.25.7"
   },

+ 5 - 1
packages/atlas/src/.env

@@ -2,11 +2,15 @@
 
 # should be set to "production" for production builds
 VITE_ENV=development
+VITE_ENV_SELECTION_ENABLED=true
+# default env in environments admin modal. Can be production, development, next or local. If not provided, VITE_ENV will be used
+VITE_DEFAULT_DATA_ENV=
+# forces maintenance screen. Set to true if Orion service is unavailable for a longer time 
+VITE_FORCE_MAINTENANCE=
 
 # App configuration
 VITE_APP_ID=4414-2
 VITE_APP_NAME=Atlas
-VITE_ENV_SELECTION_ENABLED=true
 
 VITE_AVATAR_SERVICE_URL=https://atlas-services.joystream.org/avatars
 VITE_GEOLOCATION_SERVICE_URL=https://geolocation.joystream.org

+ 24 - 0
packages/atlas/src/AnalyticsManager.tsx

@@ -1,5 +1,7 @@
 import ls from '@livesession/sdk'
 import { FC, useCallback, useEffect } from 'react'
+import ReactGA from 'react-ga'
+import { useLocation } from 'react-router-dom'
 
 import { atlasConfig } from '@/config'
 import { BUILD_ENV } from '@/config/env'
@@ -8,6 +10,7 @@ import { usePersonalDataStore } from '@/providers/personalData'
 export const AnalyticsManager: FC = () => {
   const cookiesAccepted = usePersonalDataStore((state) => state.cookiesAccepted)
   const analyticsEnabled = BUILD_ENV === 'production' && cookiesAccepted
+  const location = useLocation()
 
   const initUsersnap = useCallback(() => {
     if (!atlasConfig.analytics.usersnap?.id) return
@@ -36,6 +39,12 @@ export const AnalyticsManager: FC = () => {
     ls.newPageView()
   }, [])
 
+  const initGA = useCallback(() => {
+    if (!atlasConfig.analytics.GA?.id) return
+
+    ReactGA.initialize(atlasConfig.analytics.GA.id)
+  }, [])
+
   // initialize livesession
   useEffect(() => {
     if (!analyticsEnabled) return
@@ -50,5 +59,20 @@ export const AnalyticsManager: FC = () => {
     initUsersnap()
   }, [analyticsEnabled, initUsersnap])
 
+  //initialize Google Analytics
+  useEffect(() => {
+    if (!analyticsEnabled) return
+
+    initGA()
+  }, [analyticsEnabled, initGA])
+
+  //track pageview in GA
+  useEffect(() => {
+    if (!analyticsEnabled || !atlasConfig.analytics.GA?.id) {
+      return
+    }
+    ReactGA.pageview(location.pathname)
+  }, [location.pathname, analyticsEnabled])
+
   return null
 }

+ 19 - 10
packages/atlas/src/CommonProviders.tsx

@@ -10,9 +10,12 @@ import { AdminModal } from '@/components/_overlays/AdminModal'
 import { OperatorsContextProvider } from '@/providers/assets/assets.provider'
 import { ConfirmationModalProvider } from '@/providers/confirmationModal'
 import { OverlayManagerProvider } from '@/providers/overlayManager'
+import { SegmentAnalyticsProvider } from '@/providers/segmentAnalytics/segment.provider'
 import { UserProvider } from '@/providers/user/user.provider'
 import { GlobalStyles } from '@/styles'
 
+import { FORCE_MAINTENANCE } from './config/env'
+
 const queryClient = new QueryClient({
   defaultOptions: {
     queries: {
@@ -32,14 +35,16 @@ export const CommonProviders: FC<PropsWithChildren> = ({ children }) => {
         <QueryClientProvider client={queryClient}>
           <UserProvider>
             <OverlayManagerProvider>
-              <ConfirmationModalProvider>
-                <BrowserRouter>
-                  <AdminModal />
-                  <MaintenanceWrapper>
-                    <OperatorsContextProvider>{children}</OperatorsContextProvider>
-                  </MaintenanceWrapper>
-                </BrowserRouter>
-              </ConfirmationModalProvider>
+              <SegmentAnalyticsProvider>
+                <ConfirmationModalProvider>
+                  <BrowserRouter>
+                    <AdminModal />
+                    <MaintenanceWrapper>
+                      <OperatorsContextProvider>{children}</OperatorsContextProvider>
+                    </MaintenanceWrapper>
+                  </BrowserRouter>
+                </ConfirmationModalProvider>
+              </SegmentAnalyticsProvider>
             </OverlayManagerProvider>
           </UserProvider>
         </QueryClientProvider>
@@ -49,9 +54,13 @@ export const CommonProviders: FC<PropsWithChildren> = ({ children }) => {
 }
 
 const MaintenanceWrapper: FC<PropsWithChildren> = ({ children }) => {
-  const { isKilled, wasKilledLastTime, error, loading } = useGetKillSwitch({ context: { delay: 1000 } })
+  const isMaintenanceForced = FORCE_MAINTENANCE === 'true'
+  const { isKilled, wasKilledLastTime, error, loading } = useGetKillSwitch({
+    context: { delay: 1000 },
+    skip: isMaintenanceForced,
+  })
 
-  if (isKilled || (error && wasKilledLastTime) || (loading && wasKilledLastTime)) {
+  if (isKilled || (error && wasKilledLastTime) || (loading && wasKilledLastTime) || isMaintenanceForced) {
     return <Maintenance />
   } else {
     return <>{children}</>

+ 8 - 8
packages/atlas/src/components/_nft/NftTile/NftTileDetails.tsx

@@ -70,7 +70,6 @@ export const NftTileDetails: FC<NftTileDetailsProps> = memo(
     const [contentHovered, setContentHovered] = useState(false)
     const setOpenedContextMenuId = useMiscStore((state) => state.actions.setOpenedContextMenuId)
     const openedContexMenuId = useMiscStore((state) => state.openedContexMenuId)
-    const toggleContentHover = () => setContentHovered((prevState) => !prevState)
     const [tileSize, setTileSize] = useState<TileSize>()
     const { ref: contentRef } = useResizeObserver<HTMLAnchorElement>({
       box: 'border-box',
@@ -188,8 +187,8 @@ export const NftTileDetails: FC<NftTileDetailsProps> = memo(
         to={videoHref || ''}
         ref={contentRef}
         loading={loading}
-        onMouseEnter={toggleContentHover}
-        onMouseLeave={toggleContentHover}
+        onMouseEnter={() => setContentHovered(true)}
+        onMouseLeave={() => setContentHovered(false)}
         tileSize={tileSize}
         shouldHover={(contentHovered || hovered) && interactable}
       >
@@ -204,17 +203,18 @@ export const NftTileDetails: FC<NftTileDetailsProps> = memo(
             avatars={avatars}
           />
           {contextMenuItems && (
-            <div>
+            <div
+              onClick={(e) => {
+                e.stopPropagation()
+                e.preventDefault()
+              }}
+            >
               <KebabMenuButtonIcon
                 ref={ref}
                 icon={<SvgActionMore />}
                 variant="tertiary"
                 size="small"
                 isActive={!loading}
-                onClick={(e) => {
-                  e.stopPropagation()
-                  e.preventDefault()
-                }}
               />
               <ContextMenu
                 ref={contextMenuInstanceRef}

+ 6 - 7
packages/atlas/src/components/_overlays/AdminModal/AdminModal.tsx

@@ -14,7 +14,7 @@ import { Select } from '@/components/_inputs/Select'
 import { Switch } from '@/components/_inputs/Switch'
 import { DialogModal } from '@/components/_overlays/DialogModal'
 import { atlasConfig } from '@/config'
-import { ENV_SELECTION_ENABLED, NODE_URL, availableEnvs } from '@/config/env'
+import { ENV_SELECTION_ENABLED, NODE_URL } from '@/config/env'
 import { absoluteRoutes } from '@/config/routes'
 import { useConfirmationModal } from '@/providers/confirmationModal'
 import { useEnvironmentStore } from '@/providers/environment'
@@ -22,6 +22,7 @@ import { useSnackbar } from '@/providers/snackbars'
 import { useUserStore } from '@/providers/user/user.store'
 import { ActiveUserState } from '@/providers/user/user.types'
 import { useUserLocationStore } from '@/providers/userLocation'
+import { availableEnvs } from '@/utils/envVariables'
 import { SentryLogger } from '@/utils/logs'
 
 import {
@@ -35,8 +36,6 @@ const ENVIRONMENT_NAMES: Record<string, string> = {
   production: 'Joystream Mainnet',
   development: `${atlasConfig.general.appName} Dev Testnet`,
   next: `${atlasConfig.general.appName} Next Testnet`,
-  // todo for removal, created only for testing purposes
-  orion2test: `${atlasConfig.general.appName} Orion v2 production Testnet`,
   local: 'Local chain',
 }
 
@@ -133,9 +132,9 @@ export const AdminModal: FC = () => {
 
 const EnvTab: FC = () => {
   const {
-    targetDevEnv,
+    defaultDataEnv,
     nodeOverride,
-    actions: { setTargetDevEnv, setNodeOverride },
+    actions: { setDefaultDataEnv, setNodeOverride },
   } = useEnvironmentStore()
 
   const determinedNode = nodeOverride || NODE_URL
@@ -148,7 +147,7 @@ const EnvTab: FC = () => {
     if (!value) {
       return
     }
-    setTargetDevEnv(value)
+    setDefaultDataEnv(value)
     setNodeOverride(null)
     resetActiveUser()
 
@@ -184,7 +183,7 @@ const EnvTab: FC = () => {
         <Select
           items={environmentsItems}
           onChange={handleEnvironmentChange}
-          value={targetDevEnv}
+          value={defaultDataEnv}
           disabled={!ENV_SELECTION_ENABLED}
         />
       </FormField>

+ 52 - 19
packages/atlas/src/components/_overlays/ChangePriceDialog/ChangePriceDialog.tsx

@@ -1,9 +1,10 @@
 import styled from '@emotion/styled'
 import BN from 'bn.js'
-import { FC, useEffect, useState } from 'react'
+import { FC, useEffect } from 'react'
+import { Controller, useForm } from 'react-hook-form'
 
 import { Fee } from '@/components/Fee'
-import { Text } from '@/components/Text'
+import { FormField } from '@/components/_inputs/FormField'
 import { TokenInput } from '@/components/_inputs/TokenInput'
 import { DialogModal } from '@/components/_overlays/DialogModal'
 import { tokenNumberToHapiBn } from '@/joystream-lib/utils'
@@ -13,6 +14,7 @@ import { sizes } from '@/styles'
 type ChangePriceDialogProps = {
   onModalClose: () => void
   isOpen: boolean
+  currentPrice: number
   onChangePrice: (id: string, price: BN) => void
   nftId: string | null
   memberId: string | null
@@ -21,31 +23,46 @@ type ChangePriceDialogProps = {
 export const ChangePriceDialog: FC<ChangePriceDialogProps> = ({
   onModalClose,
   isOpen,
+  currentPrice,
   onChangePrice,
   nftId,
   memberId,
 }) => {
-  const [price, setPrice] = useState<number | null>(null)
-  const amountBn = tokenNumberToHapiBn(price || 0)
+  const {
+    reset,
+    handleSubmit,
+    watch,
+    control,
+    formState: { errors },
+  } = useForm<{ price: number }>({
+    defaultValues: {
+      price: currentPrice,
+    },
+  })
+  const amountBn = tokenNumberToHapiBn(watch('price') || 0)
   const { fullFee, loading: feeLoading } = useFee(
     'changeNftPriceTx',
     isOpen && memberId && nftId ? [memberId, nftId, amountBn.toString()] : undefined
   )
 
-  const handleSubmitPriceChange = () => {
-    if (!nftId || !price) {
-      return
-    }
-    setPrice(null)
-    onModalClose()
-    onChangePrice(nftId, tokenNumberToHapiBn(price))
-  }
+  useEffect(() => {
+    reset({ price: currentPrice })
+  }, [currentPrice, reset])
 
   useEffect(() => {
     if (!isOpen) {
-      setPrice(null)
+      reset({ price: currentPrice })
     }
-  }, [isOpen])
+  }, [currentPrice, isOpen, reset])
+
+  const handleSubmitPriceChange = () => {
+    handleSubmit((data) => {
+      if (!nftId) {
+        return
+      }
+      onChangePrice(nftId, tokenNumberToHapiBn(data.price))
+    })()
+  }
 
   return (
     <DialogModal
@@ -53,7 +70,6 @@ export const ChangePriceDialog: FC<ChangePriceDialogProps> = ({
       show={isOpen}
       primaryButton={{
         text: 'Change price',
-        disabled: !price,
         onClick: handleSubmitPriceChange,
       }}
       secondaryButton={{
@@ -64,10 +80,27 @@ export const ChangePriceDialog: FC<ChangePriceDialogProps> = ({
       additionalActionsNode={<Fee amount={fullFee} loading={feeLoading} variant="h200" />}
     >
       <>
-        <Text as="p" variant="t200" color="colorText">
-          You can update the price of this NFT anytime.
-        </Text>
-        <StyledTokenInput value={price} onChange={(value) => setPrice(value)} />
+        <Controller
+          control={control}
+          name="price"
+          rules={{
+            validate: {
+              valid: (val) => {
+                if (!val) {
+                  return 'Provide a price.'
+                }
+                if (val === currentPrice) {
+                  return 'Provide new price.'
+                }
+              },
+            },
+          }}
+          render={({ field: { onChange, value } }) => (
+            <FormField error={errors.price?.message}>
+              <StyledTokenInput value={value} onChange={(value) => onChange(value)} />
+            </FormField>
+          )}
+        />
       </>
     </DialogModal>
   )

+ 5 - 3
packages/atlas/src/components/_overlays/ContentTypeDialog/ContentTypeDialog.tsx

@@ -1,5 +1,6 @@
 import { FC } from 'react'
 import { Controller, useForm } from 'react-hook-form'
+import { Link } from 'react-router-dom'
 
 import discoverView from '@/assets/images/discover-view.webp'
 import { Text } from '@/components/Text'
@@ -45,11 +46,12 @@ export const ContentTypeDialog: FC<ContentTypeDialogProps> = ({ onClose, isOpen,
       <StyledImg src={discoverView} alt="Discover subpage" width={480} height={264} />
       <HeaderWrapper>
         <Text variant="h500" as="p" color="colorTextStrong">
-          Upload only {atlasConfig.general.appContentFocus} related content
+          {atlasConfig.general.appName} only supports {atlasConfig.general.appContentFocus} related content
         </Text>
         <Text variant="t200" as="p" color="colorText" margin={{ top: 2 }}>
-          Uploading any other type of content will result in taking down your channel. Please make sure that your
-          content falls under one of {atlasConfig.general.appName} categories before uploading.
+          Uploading content of any other topic will result in your channel excluded from {atlasConfig.general.appName}{' '}
+          by moderators. Joystream Network has multiple Apps connected, so please feel free to check{' '}
+          <Link to="https://joystream.org">joystream.org</Link> to find a suitable App for your channel.
         </Text>
       </HeaderWrapper>
       <CheckboxWrapper>

+ 10 - 0
packages/atlas/src/config/configSchema.ts

@@ -146,6 +146,16 @@ export const configSchema = z.object({
         id: z.string().nullable(),
       })
       .nullable(),
+    GA: z
+      .object({
+        id: z.string().nullable(),
+      })
+      .nullable(),
+    segment: z
+      .object({
+        id: z.string().nullable(),
+      })
+      .nullable(),
   }),
   legal: z.object({
     termsOfService: z.string(),

+ 9 - 23
packages/atlas/src/config/env.ts

@@ -1,13 +1,10 @@
 import { useEnvironmentStore } from '@/providers/environment'
+import { getEnvName } from '@/utils/envVariables'
 
 type BuildEnv = 'production' | 'development'
 
 export const ENV_PREFIX = 'VITE'
 
-export const getEnvName = (name: string) => {
-  return `${ENV_PREFIX}_${name}`
-}
-
 export const ENV_SELECTION_ENABLED: boolean = import.meta.env[getEnvName('ENV_SELECTION_ENABLED')] === 'true'
 
 export const BUILD_ENV = (import.meta.env[getEnvName('ENV')] || 'production') as BuildEnv
@@ -17,30 +14,15 @@ export const BUILD_ENV = (import.meta.env[getEnvName('ENV')] || 'production') as
 if (ENV_SELECTION_ENABLED === false) {
   const environmentState = useEnvironmentStore.getState()
 
-  if (environmentState.actions.getInitialState().targetDevEnv !== environmentState.targetDevEnv) {
+  if (environmentState.actions.getInitialState().defaultDataEnv !== environmentState.defaultDataEnv) {
     useEnvironmentStore.getState().actions.reset()
   }
 }
-export const availableEnvs = () => {
-  return Array.from(
-    new Set(
-      Object.keys(import.meta.env)
-        .filter(
-          (key) =>
-            key.startsWith(ENV_PREFIX) &&
-            !key.startsWith(`${ENV_PREFIX}_ENV`) &&
-            !key.startsWith(`${ENV_PREFIX}_VERCEL`)
-        )
-        .map((key) => {
-          return key.replace(ENV_PREFIX, '').split('_')[1].toLowerCase()
-        })
-    )
-  )
-}
+
 export const readEnv = (name: string, required = true, direct = false): string => {
   const fullName = direct
     ? getEnvName(name)
-    : getEnvName(`${useEnvironmentStore.getState().targetDevEnv.toUpperCase()}_${name}`)
+    : getEnvName(`${useEnvironmentStore.getState().defaultDataEnv.toUpperCase()}_${name}`)
   const value = import.meta.env[fullName]
   if (!value && required) {
     throw new Error(`Missing required env variable "${name}", tried access via "${fullName}"`)
@@ -50,12 +32,16 @@ export const readEnv = (name: string, required = true, direct = false): string =
   return value.toString()
 }
 
+// variables that depends on chosen environment
 export const ORION_GRAPHQL_URL = readEnv('ORION_URL')
 export const QUERY_NODE_GRAPHQL_SUBSCRIPTION_URL = readEnv('QUERY_NODE_SUBSCRIPTION_URL')
 export const NODE_URL = readEnv('NODE_URL')
 export const FAUCET_URL = readEnv('FAUCET_URL')
-export const GOOGLE_OAUTH_ENDPOINT = 'https://accounts.google.com/o/oauth2/v2/auth'
 
+// direct variables
+export const GOOGLE_OAUTH_ENDPOINT = 'https://accounts.google.com/o/oauth2/v2/auth'
+export const DEFAULT_DATA_ENV = readEnv('DEFAUL_DATA_ENV', false, true) || BUILD_ENV // if default data env is not provided use BUILD_ENV
 export const JOY_PRICE_SERVICE_URL = readEnv('PRICE_SERVICE_URL', false, true)
 export const USER_LOCATION_SERVICE_URL = readEnv('GEOLOCATION_SERVICE_URL', true, true)
 export const HCAPTCHA_SITE_KEY = readEnv('HCAPTCHA_SITE_KEY', false, true)
+export const FORCE_MAINTENANCE = readEnv('FORCE_MAINTENANCE', false, true)

+ 6 - 2
packages/atlas/src/hooks/useNftTransactions.tsx

@@ -105,8 +105,12 @@ export const useNftTransactions = () => {
 
       openModal({
         title: 'Remove from sale?',
-        description: 'Are you sure you want to remove this NFT from sale? You can put it back on sale anytime.',
-        type: 'warning',
+        description: `Are you sure you want to remove this NFT from sale? ${
+          saleType === 'buyNow'
+            ? 'You can put it back on sale anytime.'
+            : 'You may lose the bids that were already placed.'
+        } `,
+        type: 'destructive',
         primaryButton: {
           text: 'Remove',
           onClick: () => {

+ 20 - 0
packages/atlas/src/hooks/useSegmentAnalytics.ts

@@ -0,0 +1,20 @@
+import { useCallback } from 'react'
+
+import useSegmentAnalyticsContext from '@/providers/segmentAnalytics/useSegmentAnalyticsContext'
+
+const useAnalytics = () => {
+  const { analytics } = useSegmentAnalyticsContext()
+
+  const pageViewed = useCallback(
+    (name: string, category = 'App') => {
+      analytics.page(category, name)
+    },
+    [analytics]
+  )
+
+  return {
+    pageViewed,
+  }
+}
+
+export default useAnalytics

+ 1 - 0
packages/atlas/src/index.html

@@ -20,6 +20,7 @@
       href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@600;700&family=Roboto:wght@400&family=Inter:wght@400;500;600;700&display=swap"
       rel="stylesheet"
     />
+    <script src="https://www.googleoptimize.com/optimize.js?id=%VITE_OPTIMIZE_ID%"></script>
   </head>
   <body>
     <noscript>You need to enable JavaScript to run this app.</noscript>

+ 9 - 6
packages/atlas/src/providers/environment/store.ts

@@ -1,19 +1,22 @@
+import { getEnvName } from '@/utils/envVariables'
 import { createStore } from '@/utils/store'
 
 const LOCAL_STORAGE_KEY = 'environment'
 
 export type EnvironmentState = {
-  targetDevEnv: string
+  defaultDataEnv: string
   nodeOverride: string | null
 }
 
+export const ENV_PREFIX = 'VITE'
+
 const INITIAL_STATE: EnvironmentState = {
-  targetDevEnv: 'development',
+  defaultDataEnv: import.meta.env[getEnvName('DEFAULT_DATA_ENV')] || import.meta.env[getEnvName('ENV')] || 'production',
   nodeOverride: null,
 }
 
 export type EnvironmentStoreActions = {
-  setTargetDevEnv: (env: string) => void
+  setDefaultDataEnv: (env: string) => void
   setNodeOverride: (node: string | null) => void
   reset: () => void
   getInitialState: () => EnvironmentState
@@ -28,9 +31,9 @@ export const useEnvironmentStore = createStore<EnvironmentState, EnvironmentStor
           state.nodeOverride = node
         })
       },
-      setTargetDevEnv: (env) => {
+      setDefaultDataEnv: (env) => {
         set((state) => {
-          state.targetDevEnv = env
+          state.defaultDataEnv = env
         })
       },
       reset: () => {
@@ -43,7 +46,7 @@ export const useEnvironmentStore = createStore<EnvironmentState, EnvironmentStor
     persist: {
       key: LOCAL_STORAGE_KEY,
       version: 0,
-      whitelist: ['nodeOverride', 'targetDevEnv'],
+      whitelist: ['nodeOverride', 'defaultDataEnv'],
       migrate: () => null,
     },
   }

+ 6 - 0
packages/atlas/src/providers/nftActions/nftActions.provider.tsx

@@ -6,6 +6,7 @@ import { AcceptBidDialog } from '@/components/_overlays/AcceptBidDialog'
 import { ChangePriceDialog } from '@/components/_overlays/ChangePriceDialog'
 import { useNftState } from '@/hooks/useNftState'
 import { useNftTransactions } from '@/hooks/useNftTransactions'
+import { hapiBnToTokenNumber } from '@/joystream-lib/utils'
 import { useTokenPrice } from '@/providers/joystream/joystream.hooks'
 import { useUser } from '@/providers/user/user.hooks'
 
@@ -67,6 +68,10 @@ export const NftActionsProvider: FC<PropsWithChildren> = ({ children }) => {
     [closeNftAction, currentAction, currentNftId, isBuyNowClicked, transactions]
   )
 
+  const currentBuyNowPrice =
+    (nft?.transactionalStatus?.__typename === 'TransactionalStatusBuyNow' &&
+      hapiBnToTokenNumber(new BN(nft.transactionalStatus.price))) ||
+    0
   return (
     <NftActionsContext.Provider value={value}>
       <AcceptBidDialog
@@ -78,6 +83,7 @@ export const NftActionsProvider: FC<PropsWithChildren> = ({ children }) => {
         ownerId={nft?.owner.__typename === 'NftOwnerMember' ? nft.owner.member.id : nft?.owner.channel.ownerMember?.id}
       />
       <ChangePriceDialog
+        currentPrice={currentBuyNowPrice}
         isOpen={currentAction === 'change-price'}
         onModalClose={closeNftAction}
         onChangePrice={transactions.changeNftPrice}

+ 31 - 0
packages/atlas/src/providers/segmentAnalytics/segment.provider.tsx

@@ -0,0 +1,31 @@
+import { AnalyticsBrowser } from '@segment/analytics-next'
+import { FC, ReactNode, createContext, useMemo } from 'react'
+
+import { atlasConfig } from '@/config'
+import { BUILD_ENV } from '@/config/env'
+import { usePersonalDataStore } from '@/providers/personalData'
+
+import { AnalyticsContextProps } from './segment.types'
+
+interface AnalyticsProviderProps {
+  children: ReactNode
+}
+
+const defaultAnalyticsContext = {
+  analytics: new AnalyticsBrowser(),
+}
+
+export const SegmentAnalyticsContext = createContext<AnalyticsContextProps>(defaultAnalyticsContext)
+
+export const SegmentAnalyticsProvider: FC<AnalyticsProviderProps> = ({ children }) => {
+  const cookiesAccepted = usePersonalDataStore((state) => state.cookiesAccepted)
+  const analyticsEnabled = BUILD_ENV === 'production' && cookiesAccepted
+  const writeKey = (analyticsEnabled && atlasConfig.analytics.segment?.id) || ''
+
+  const segmentAnalytics: AnalyticsContextProps = useMemo(
+    () => ({ analytics: AnalyticsBrowser.load({ writeKey }) }),
+    [writeKey]
+  )
+
+  return <SegmentAnalyticsContext.Provider value={segmentAnalytics}>{children}</SegmentAnalyticsContext.Provider>
+}

+ 5 - 0
packages/atlas/src/providers/segmentAnalytics/segment.types.ts

@@ -0,0 +1,5 @@
+import { AnalyticsBrowser } from '@segment/analytics-next'
+
+export interface AnalyticsContextProps {
+  analytics: AnalyticsBrowser
+}

+ 7 - 0
packages/atlas/src/providers/segmentAnalytics/useSegmentAnalyticsContext.ts

@@ -0,0 +1,7 @@
+import { useContext } from 'react'
+
+import { SegmentAnalyticsContext } from './segment.provider'
+
+const useSegmentAnalyticsContext = () => useContext(SegmentAnalyticsContext)
+
+export default useSegmentAnalyticsContext

+ 31 - 27
packages/atlas/src/providers/transactions/transactions.hooks.ts

@@ -203,45 +203,49 @@ export const useTransaction = (): HandleTransactionFn => {
         let isAfterBlockCheck = false
         let isAfterMetaStatusCheck = false
         // if this is a metaprotocol transaction, we will also wait until we successfully query the transaction result from QN
-        const queryNodeSyncPromise = new Promise<void>((resolve, reject) => {
-          const syncCallback = async () => {
-            let status: MetaprotocolTransactionResultFieldsFragment | undefined = undefined
-            isAfterBlockCheck = true
-            try {
-              if (result.metaprotocol && result.transactionHash) {
-                status = await getMetaprotocolTxStatus(result.transactionHash)
+        const queryNodeSyncPromiseFactory = () =>
+          new Promise<void>((resolve, reject) => {
+            const syncCallback = async () => {
+              let status: MetaprotocolTransactionResultFieldsFragment | undefined = undefined
+              isAfterBlockCheck = true
+              try {
+                if (result.metaprotocol && result.transactionHash) {
+                  status = await getMetaprotocolTxStatus(result.transactionHash)
+                }
+              } catch (e) {
+                reject(e)
+                return
+              } finally {
+                isAfterMetaStatusCheck = true
               }
-            } catch (e) {
-              reject(e)
-              return
-            } finally {
-              isAfterMetaStatusCheck = true
-            }
 
-            if (onTxSync) {
-              try {
-                await onTxSync(result, status)
-              } catch (error) {
-                SentryLogger.error('Failed transaction sync callback', 'TransactionManager', error)
+              if (onTxSync) {
+                try {
+                  await onTxSync(result, status)
+                } catch (error) {
+                  SentryLogger.error('Failed transaction sync callback', 'TransactionManager', error)
+                }
               }
+              resolve()
             }
-            resolve()
-          }
 
-          if (disableQNSync) {
-            syncCallback()
-          } else {
-            addBlockAction({ callback: syncCallback, targetBlock: result.block })
-          }
-        })
+            if (disableQNSync) {
+              syncCallback()
+            } else {
+              addBlockAction({ callback: syncCallback, targetBlock: result.block })
+            }
+          })
 
-        await withTimeout(queryNodeSyncPromise, 20_000).catch((error) => {
+        await withTimeout(queryNodeSyncPromiseFactory(), 15_000).catch(async (error) => {
           SentryLogger.error('TEST: Processor sync promise timeout error', 'TransactionManager', {
             error,
             txResult: result,
             isAfterBlockCheck,
             isAfterMetaStatusCheck,
           })
+
+          disableQNSync = true
+          await queryNodeSyncPromiseFactory()
         })
 
         /* === transaction was successful, do necessary cleanup === */

+ 22 - 0
packages/atlas/src/utils/envVariables.ts

@@ -0,0 +1,22 @@
+export const ENV_PREFIX = 'VITE'
+
+export const getEnvName = (name: string) => {
+  return `${ENV_PREFIX}_${name}`
+}
+
+export const availableEnvs = () => {
+  return Array.from(
+    new Set(
+      Object.keys(import.meta.env)
+        .filter(
+          (key) =>
+            key.startsWith(ENV_PREFIX) &&
+            !key.startsWith(`${ENV_PREFIX}_ENV`) &&
+            !key.startsWith(`${ENV_PREFIX}_VERCEL`)
+        )
+        .map((key) => {
+          return key.replace(ENV_PREFIX, '').split('_')[1].toLowerCase()
+        })
+    )
+  )
+}

+ 7 - 11
packages/atlas/src/views/viewer/MemberView/ActivityItem.styles.ts

@@ -58,6 +58,9 @@ export const TitleAndDescriptionContainer = styled.div`
 `
 export const Title = styled(Text)`
   word-break: break-word;
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
 `
 export const Thumbnail = styled.img`
   height: 40px;
@@ -88,6 +91,10 @@ export const ThumbnailSkeletonLoader = styled(SkeletonLoader)`
 export const PillSkeletonLoader = styled(SkeletonLoader)`
   width: 40px;
   height: 20px;
+  align-self: flex-start;
+  ${media.sm} {
+    align-self: unset;
+  }
 `
 
 export const TitleSkeletonLoader = styled(SkeletonLoader)`
@@ -107,14 +114,3 @@ export const DescriptionSkeletonLoader = styled(SkeletonLoader)`
 export const DateText = styled(Text)`
   text-align: end;
 `
-
-export const DateRow = styled.span`
-  display: inline;
-
-  ${media.sm} {
-    display: block;
-  }
-  ${media.lg} {
-    display: inline;
-  }
-`

+ 2 - 4
packages/atlas/src/views/viewer/MemberView/ActivityItem.tsx

@@ -8,7 +8,6 @@ import { imageUrlValidation } from '@/utils/asset'
 
 import {
   ActivityItemContainer,
-  DateRow,
   DateText,
   DescriptionSkeletonLoader,
   PillAndDateContainer,
@@ -71,7 +70,7 @@ export const ActivityItem: FC<ActivityItemProps> = ({
         {loading ? (
           <TitleSkeletonLoader />
         ) : (
-          <Title as="h3" variant={getTitleTextVariant()} clampAfterLine={smMatch ? 2 : 1}>
+          <Title as="h3" variant={getTitleTextVariant()} title={title}>
             {title}
           </Title>
         )}
@@ -90,8 +89,7 @@ export const ActivityItem: FC<ActivityItemProps> = ({
           <Pill label={type} size="medium" />
           {date && (
             <DateText as="p" variant="t100" color="colorText">
-              <DateRow>{format(date, 'd MMM yyyy')},</DateRow>
-              <DateRow> {format(date, 'HH:mm')}</DateRow>
+              {format(date, 'd MMM yyyy')} at {format(date, 'HH:mm')}
             </DateText>
           )}
         </PillAndDateContainer>

+ 12 - 6
packages/atlas/src/views/viewer/MemberView/MemberAbout.styles.ts

@@ -3,7 +3,6 @@ import { HTMLProps } from 'react'
 
 import { LayoutGrid } from '@/components/LayoutGrid/LayoutGrid'
 import { Text } from '@/components/Text'
-import { ChannelCard } from '@/components/_channel/ChannelCard'
 import { cVar, media, sizes } from '@/styles'
 
 export const TextContainer = styled.div<{ withDivider?: boolean }>`
@@ -26,16 +25,23 @@ export const StyledLayoutGrid = styled(LayoutGrid)`
   margin-bottom: 50px;
 `
 
-export const ChannelsOwnedContainerGrid = styled(LayoutGrid)`
+export const ChannelsOwnedContainerGrid = styled.div`
   margin-top: ${sizes(4)};
+  display: grid;
+  grid-template-columns: 1fr;
+  gap: ${sizes(4)};
 
+  ${media.sm} {
+    grid-template-columns: repeat(2, 1fr);
+  }
   ${media.md} {
+    grid-template-columns: repeat(3, 1fr);
+    gap: ${sizes(6)};
     margin-top: ${sizes(6)};
   }
-`
-
-export const StyledChannelCard = styled(ChannelCard)`
-  min-width: 136px;
+  ${media.lg} {
+    grid-template-columns: repeat(4, 1fr);
+  }
 `
 
 export const Anchor = styled(Text)<HTMLProps<HTMLAnchorElement>>`

+ 22 - 23
packages/atlas/src/views/viewer/MemberView/MemberAbout.tsx

@@ -1,26 +1,28 @@
 import { useParams } from 'react-router'
 
 import { useMemberships } from '@/api/hooks/membership'
+import { EmptyFallback } from '@/components/EmptyFallback'
 import { GridItem } from '@/components/LayoutGrid/LayoutGrid'
 import { NumberFormat } from '@/components/NumberFormat'
 import { Text } from '@/components/Text'
+import { ChannelCard } from '@/components/_channel/ChannelCard'
 import { atlasConfig } from '@/config'
+import { createPlaceholderData } from '@/utils/data'
 import { formatDate } from '@/utils/time'
 
-import {
-  Anchor,
-  ChannelsOwnedContainerGrid,
-  Details,
-  StyledChannelCard,
-  StyledLayoutGrid,
-  TextContainer,
-} from './MemberAbout.styles'
+import { Anchor, ChannelsOwnedContainerGrid, Details, StyledLayoutGrid, TextContainer } from './MemberAbout.styles'
 
 export const MemberAbout = () => {
   const { handle } = useParams()
-  const { memberships } = useMemberships({ where: { handle_eq: handle } })
+  const { memberships, loading } = useMemberships({ where: { handle_eq: handle } })
   const member = memberships?.find((member) => member.handle === handle)
 
+  const placeholderItems = createPlaceholderData(2).map((_, idx) => <ChannelCard key={idx} loading={loading} />)
+
+  const channels = loading
+    ? placeholderItems
+    : member?.channels.map((channel) => <ChannelCard key={channel.id} withFollowButton={false} channel={channel} />)
+
   return (
     <StyledLayoutGrid>
       <GridItem colSpan={{ base: 12, sm: 8 }} rowStart={{ base: 2, sm: 1 }}>
@@ -34,20 +36,17 @@ export const MemberAbout = () => {
             </Text>
           </TextContainer>
         )}
-        {!!member?.channels.length && (
-          <div>
-            <Text as="h2" variant="h500">
-              Channels owned
-            </Text>
-            <ChannelsOwnedContainerGrid>
-              {member?.channels.map((channel) => (
-                <GridItem key={channel.id} colSpan={{ base: 6, lg: 3 }}>
-                  <StyledChannelCard withFollowButton={false} channel={channel} />
-                </GridItem>
-              ))}
-            </ChannelsOwnedContainerGrid>
-          </div>
-        )}
+
+        <div>
+          <Text as="h2" variant="h500">
+            Channels owned
+          </Text>
+          {channels?.length ? (
+            <ChannelsOwnedContainerGrid>{channels}</ChannelsOwnedContainerGrid>
+          ) : (
+            <EmptyFallback title="No channels" subtitle="This member hasn't created any channels yet." />
+          )}
+        </div>
       </GridItem>
       <GridItem colSpan={{ base: 12, sm: 3 }} colStart={{ sm: -4 }}>
         <Text as="h3" variant="h500" margin={{ bottom: 4 }}>

+ 4 - 4
packages/atlas/src/views/viewer/MemberView/MemberActivity.styles.ts

@@ -8,12 +8,11 @@ export const GridRowWrapper = styled.div`
   display: contents;
 `
 
-export const OverviewItem = styled.div<{ divider?: boolean }>`
+export const InfoListItem = styled.div<{ divider?: boolean }>`
   display: flex;
   align-items: center;
-  gap: ${sizes(4)};
+  gap: ${sizes(2)};
   padding-bottom: ${sizes(4)};
-  margin-bottom: ${sizes(4)};
   box-shadow: ${cVar('effectDividersBottom')};
 
   ${GridRowWrapper}:last-of-type > & {
@@ -40,12 +39,13 @@ export const StyledIconWrapper = styled(IconWrapper)`
 export const OverviewTextContainer = styled.div`
   display: grid;
   grid-auto-flow: row;
-  gap: ${sizes(2)};
+  gap: ${sizes(0.5)};
 `
 
 export const OverviewContainer = styled.div`
   margin-top: ${sizes(6)};
   display: grid;
+  row-gap: ${sizes(4)};
   grid-template-columns: 1fr 1fr;
 
   ${media.sm} {

+ 9 - 9
packages/atlas/src/views/viewer/MemberView/MemberActivity.tsx

@@ -15,8 +15,8 @@ import { ActivityItem } from './ActivityItem'
 import { ActivitiesRecord, useActivities } from './MemberActivity.hooks'
 import {
   GridRowWrapper,
+  InfoListItem,
   OverviewContainer,
-  OverviewItem,
   OverviewTextContainer,
   StyledIconWrapper,
   StyledLink,
@@ -227,7 +227,7 @@ export const MemberActivity: FC<MemberActivityProps> = ({
                 Overview
               </Text>
               <OverviewContainer>
-                <OverviewItem>
+                <InfoListItem>
                   <StyledIconWrapper icon={<SvgActionBuyNow />} size="large" />
                   <OverviewTextContainer>
                     <Text as="span" variant="t100" color="colorText">
@@ -237,8 +237,8 @@ export const MemberActivity: FC<MemberActivityProps> = ({
                       {activitiesTotalCounts.nftsBoughts}
                     </Text>
                   </OverviewTextContainer>
-                </OverviewItem>
-                <OverviewItem>
+                </InfoListItem>
+                <InfoListItem>
                   <StyledIconWrapper icon={<SvgActionSell />} size="large" />
                   <OverviewTextContainer>
                     <Text as="span" variant="t100" color="colorText">
@@ -248,9 +248,9 @@ export const MemberActivity: FC<MemberActivityProps> = ({
                       {activitiesTotalCounts.nftsSold}
                     </Text>
                   </OverviewTextContainer>
-                </OverviewItem>
+                </InfoListItem>
                 <GridRowWrapper>
-                  <OverviewItem>
+                  <InfoListItem>
                     <StyledIconWrapper icon={<SvgActionMint />} size="large" />
                     <OverviewTextContainer>
                       <Text as="span" variant="t100" color="colorText">
@@ -260,8 +260,8 @@ export const MemberActivity: FC<MemberActivityProps> = ({
                         {activitiesTotalCounts.nftsIssued}
                       </Text>
                     </OverviewTextContainer>
-                  </OverviewItem>
-                  <OverviewItem>
+                  </InfoListItem>
+                  <InfoListItem>
                     <StyledIconWrapper icon={<SvgActionBid />} size="large" />
                     <OverviewTextContainer>
                       <Text as="span" variant="t100" color="colorText">
@@ -271,7 +271,7 @@ export const MemberActivity: FC<MemberActivityProps> = ({
                         {activitiesTotalCounts.nftsBidded}
                       </Text>
                     </OverviewTextContainer>
-                  </OverviewItem>
+                  </InfoListItem>
                 </GridRowWrapper>
               </OverviewContainer>
             </GridItem>

File diff suppressed because it is too large
+ 1537 - 99
yarn.lock


Some files were not shown because too many files changed in this diff