ソースを参照

last channel view tweaks

eldiegod 3 年 前
コミット
942df32f65

+ 61 - 13
src/api/client/resolvers.ts

@@ -17,12 +17,18 @@ import {
   TransformOrionFollowsField,
   TransformOrionViewsField,
 } from './transforms'
-import { ORION_CHANNEL_VIEWS_QUERY_NAME, TransformOrionChannelViewsField } from './transforms/orionViews'
+import {
+  ORION_BATCHED_CHANNEL_VIEWS_QUERY_NAME,
+  ORION_CHANNEL_VIEWS_QUERY_NAME,
+  TransformBatchedChannelOrionViewsField,
+  TransformOrionChannelViewsField,
+} from './transforms/orionViews'
 import { RemoveQueryNodeChannelViewsField } from './transforms/queryNodeViews'
 
 import { Channel, ChannelEdge, Video, VideoEdge } from '../queries'
 
 const BATCHED_VIDEO_VIEWS_QUERY_NAME = 'GetBatchedVideoViews'
+const BATCHED_CHANNEL_VIEWS_QUERY_NAME = 'GetBatchedChannelViews'
 const BATCHED_FOLLOWS_VIEWS_QUERY_NAME = 'GetBatchedChannelFollows'
 
 const createResolverWithTransforms = (
@@ -111,9 +117,31 @@ export const queryNodeStitchingResolvers = (
           context,
           info
         )
-
         const followsLookup = createLookup<{ id: string; follows: number }>(batchedChannelFollows || [])
-        return channels.map((channel: Channel) => ({ ...channel, follows: followsLookup[channel.id]?.follows || 0 }))
+
+        const batchedChannelViewsResolver = createResolverWithTransforms(
+          orionSchema,
+          ORION_BATCHED_CHANNEL_VIEWS_QUERY_NAME,
+          [TransformBatchedChannelOrionViewsField],
+          // operationName has to be manually kept in sync with the query name used
+          BATCHED_CHANNEL_VIEWS_QUERY_NAME
+        )
+        const batchedChannelViews = await batchedChannelViewsResolver(
+          parent,
+          {
+            channelIdList: channels.map((channel: Channel) => channel.id),
+          },
+          context,
+          info
+        )
+
+        const viewsLookup = createLookup<{ id: string; views: number }>(batchedChannelViews || [])
+
+        return channels.map((channel: Channel) => ({
+          ...channel,
+          follows: followsLookup[channel.id]?.follows || 0,
+          views: viewsLookup[channel.id]?.views || 0,
+        }))
       } catch (error) {
         Logger.warn('Failed to resolve channels field', { error })
         return null
@@ -190,20 +218,22 @@ export const queryNodeStitchingResolvers = (
       if (parent.views != null) {
         return parent.views
       }
+      const orionViewsResolver = createResolverWithTransforms(
+        orionSchema,
+        ORION_CHANNEL_VIEWS_QUERY_NAME,
+        [TransformOrionChannelViewsField],
+        // operationName has to be manually kept in sync with the query name used
+        'GetChannelViews'
+      )
       try {
-        return await delegateToSchema({
-          schema: orionSchema,
-          operation: 'query',
-          // operationName has to be manually kept in sync with the query name used
-          operationName: 'GetChannelViews',
-          fieldName: ORION_CHANNEL_VIEWS_QUERY_NAME,
-          args: {
+        return await orionViewsResolver(
+          parent,
+          {
             channelId: parent.id,
           },
           context,
-          info,
-          transforms: [TransformOrionChannelViewsField],
-        })
+          info
+        )
       } catch (error) {
         Logger.warn('Failed to resolve views field', { error })
         return null
@@ -252,13 +282,31 @@ export const queryNodeStitchingResolvers = (
         info
       )
 
+      const batchedChannelViewsResolver = createResolverWithTransforms(
+        orionSchema,
+        ORION_BATCHED_CHANNEL_VIEWS_QUERY_NAME,
+        [TransformBatchedChannelOrionViewsField],
+        // operationName has to be manually kept in sync with the query name used
+        BATCHED_CHANNEL_VIEWS_QUERY_NAME
+      )
+      const batchedChannelViews = await batchedChannelViewsResolver(
+        parent,
+        {
+          channelIdList: parent.edges.map((edge: ChannelEdge) => edge.node.id),
+        },
+        context,
+        info
+      )
+
       const followsLookup = createLookup<{ id: string; views: number }>(batchedChannelFollows || [])
+      const viewsLookup = createLookup<{ id: string; views: number }>(batchedChannelViews || [])
 
       return parent.edges.map((edge: ChannelEdge) => ({
         ...edge,
         node: {
           ...edge.node,
           follows: followsLookup[edge.node.id]?.views || 0,
+          views: viewsLookup[edge.node.id]?.views || 0,
         },
       }))
     },

+ 4 - 0
src/api/client/transforms/index.ts

@@ -9,6 +9,10 @@ export {
   ORION_VIEWS_QUERY_NAME,
   TransformBatchedOrionViewsField,
   TransformOrionViewsField,
+  ORION_BATCHED_CHANNEL_VIEWS_QUERY_NAME,
+  ORION_CHANNEL_VIEWS_QUERY_NAME,
+  TransformBatchedChannelOrionViewsField,
+  TransformOrionChannelViewsField,
 } from './orionViews'
 export { RemoveQueryNodeFollowsField } from './queryNodeFollows'
 export { RemoveQueryNodeViewsField } from './queryNodeViews'

+ 42 - 4
src/api/client/transforms/orionViews.ts

@@ -10,7 +10,7 @@ class OrionError extends Error {
   }
 }
 
-const VIDEO_INFO_SELECTION_SET: SelectionSetNode = {
+const INFO_SELECTION_SET: SelectionSetNode = {
   kind: 'SelectionSet',
   selections: [
     {
@@ -47,7 +47,7 @@ export const TransformOrionViewsField: Transform = {
                 if (selection.kind === 'Field' && selection.name.value === ORION_VIEWS_QUERY_NAME) {
                   return {
                     ...selection,
-                    selectionSet: VIDEO_INFO_SELECTION_SET,
+                    selectionSet: INFO_SELECTION_SET,
                   }
                 }
                 return selection
@@ -91,7 +91,7 @@ export const TransformBatchedOrionViewsField: Transform = {
                 if (selection.kind === 'Field' && selection.name.value === ORION_BATCHED_VIEWS_QUERY_NAME) {
                   return {
                     ...selection,
-                    selectionSet: VIDEO_INFO_SELECTION_SET,
+                    selectionSet: INFO_SELECTION_SET,
                   }
                 }
                 return selection
@@ -128,7 +128,7 @@ export const TransformOrionChannelViewsField: Transform = {
                 if (selection.kind === 'Field' && selection.name.value === ORION_CHANNEL_VIEWS_QUERY_NAME) {
                   return {
                     ...selection,
-                    selectionSet: VIDEO_INFO_SELECTION_SET,
+                    selectionSet: INFO_SELECTION_SET,
                   }
                 }
                 return selection
@@ -154,3 +154,41 @@ export const TransformOrionChannelViewsField: Transform = {
     return { data }
   },
 }
+
+export const ORION_BATCHED_CHANNEL_VIEWS_QUERY_NAME = 'batchedChannelsViews'
+
+export const TransformBatchedChannelOrionViewsField: Transform = {
+  transformRequest(request) {
+    request.document = {
+      ...request.document,
+      definitions: request.document.definitions.map((definition) => {
+        if (definition.kind === 'OperationDefinition') {
+          return {
+            ...definition,
+            selectionSet: {
+              ...definition.selectionSet,
+              selections: definition.selectionSet.selections.map((selection) => {
+                if (selection.kind === 'Field' && selection.name.value === ORION_BATCHED_CHANNEL_VIEWS_QUERY_NAME) {
+                  return {
+                    ...selection,
+                    selectionSet: INFO_SELECTION_SET,
+                  }
+                }
+                return selection
+              }),
+            },
+          }
+        }
+        return definition
+      }),
+    }
+
+    return request
+  },
+  transformResult(result) {
+    if (result.errors) {
+      throw new OrionError(result.errors)
+    }
+    return result
+  },
+}

+ 56 - 0
src/api/queries/__generated__/channels.generated.tsx

@@ -114,6 +114,15 @@ export type GetChannelViewsQuery = {
   channelViews?: Types.Maybe<{ __typename?: 'EntityViewsInfo'; id: string; views: number }>
 }
 
+export type GetBatchedChannelViewsQueryVariables = Types.Exact<{
+  channelIdList: Array<Types.Scalars['ID']> | Types.Scalars['ID']
+}>
+
+export type GetBatchedChannelViewsQuery = {
+  __typename?: 'Query'
+  batchedChannelsViews: Array<Types.Maybe<{ __typename?: 'EntityViewsInfo'; id: string; views: number }>>
+}
+
 export type FollowChannelMutationVariables = Types.Exact<{
   channelId: Types.Scalars['ID']
 }>
@@ -509,6 +518,53 @@ export function useGetChannelViewsLazyQuery(
 export type GetChannelViewsQueryHookResult = ReturnType<typeof useGetChannelViewsQuery>
 export type GetChannelViewsLazyQueryHookResult = ReturnType<typeof useGetChannelViewsLazyQuery>
 export type GetChannelViewsQueryResult = Apollo.QueryResult<GetChannelViewsQuery, GetChannelViewsQueryVariables>
+export const GetBatchedChannelViewsDocument = gql`
+  query GetBatchedChannelViews($channelIdList: [ID!]!) {
+    batchedChannelsViews(channelIdList: $channelIdList) {
+      id
+      views
+    }
+  }
+`
+
+/**
+ * __useGetBatchedChannelViewsQuery__
+ *
+ * To run a query within a React component, call `useGetBatchedChannelViewsQuery` and pass it any options that fit your needs.
+ * When your component renders, `useGetBatchedChannelViewsQuery` returns an object from Apollo Client that contains loading, error, and data properties
+ * you can use to render your UI.
+ *
+ * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
+ *
+ * @example
+ * const { data, loading, error } = useGetBatchedChannelViewsQuery({
+ *   variables: {
+ *      channelIdList: // value for 'channelIdList'
+ *   },
+ * });
+ */
+export function useGetBatchedChannelViewsQuery(
+  baseOptions: Apollo.QueryHookOptions<GetBatchedChannelViewsQuery, GetBatchedChannelViewsQueryVariables>
+) {
+  return Apollo.useQuery<GetBatchedChannelViewsQuery, GetBatchedChannelViewsQueryVariables>(
+    GetBatchedChannelViewsDocument,
+    baseOptions
+  )
+}
+export function useGetBatchedChannelViewsLazyQuery(
+  baseOptions?: Apollo.LazyQueryHookOptions<GetBatchedChannelViewsQuery, GetBatchedChannelViewsQueryVariables>
+) {
+  return Apollo.useLazyQuery<GetBatchedChannelViewsQuery, GetBatchedChannelViewsQueryVariables>(
+    GetBatchedChannelViewsDocument,
+    baseOptions
+  )
+}
+export type GetBatchedChannelViewsQueryHookResult = ReturnType<typeof useGetBatchedChannelViewsQuery>
+export type GetBatchedChannelViewsLazyQueryHookResult = ReturnType<typeof useGetBatchedChannelViewsLazyQuery>
+export type GetBatchedChannelViewsQueryResult = Apollo.QueryResult<
+  GetBatchedChannelViewsQuery,
+  GetBatchedChannelViewsQueryVariables
+>
 export const FollowChannelDocument = gql`
   mutation FollowChannel($channelId: ID!) {
     followChannel(channelId: $channelId) {

+ 7 - 0
src/api/queries/channels.graphql

@@ -96,6 +96,13 @@ query GetChannelViews($channelId: ID!) {
   }
 }
 
+query GetBatchedChannelViews($channelIdList: [ID!]!) {
+  batchedChannelsViews(channelIdList: $channelIdList) {
+    id
+    views
+  }
+}
+
 mutation FollowChannel($channelId: ID!) {
   followChannel(channelId: $channelId) {
     id

+ 1 - 1
src/shared/components/ChannelCardBase/ChannelCardBase.tsx

@@ -74,7 +74,7 @@ export const ChannelCardBase: React.FC<ChannelCardBaseProps> = ({
                       timeout={parseInt(transitions.timings.loading) * 0.5}
                       classNames={transitions.names.fade}
                     >
-                      <VideoCount variant="subtitle2">{`${videoCount} Uploads`}</VideoCount>
+                      <VideoCount variant="subtitle2">{`${videoCount ?? ''} Uploads`}</VideoCount>
                     </CSSTransition>
                   )}
                 </VideoCountContainer>

+ 1 - 1
src/shared/components/Tabs/Tabs.styles.tsx

@@ -41,7 +41,7 @@ export const Tab = styled.div<TabProps>`
   color: ${(props) => (props.selected ? colors.white : colors.gray[300])};
   text-transform: capitalize;
   text-align: center;
-  border-bottom: ${(props) => (props.selected ? `4px solid ${colors.blue[500]}` : 'none')};
+  border-bottom: ${(props) => (props.selected ? `4px solid ${colors.blue[500]}` : '4px solid transparent')};
 
   :hover,
   :focus {

+ 1 - 1
src/views/viewer/ChannelView/ChannelAbout.tsx

@@ -57,7 +57,7 @@ export const ChannelAbout = () => {
           <Text variant="caption" secondary>
             Num. of views
           </Text>
-          <Text variant="body1">{channel?.views ? formatNumberShort(channel.views) : ''}</Text>
+          <Text variant="body1">{typeof channel?.views === 'number' ? formatNumberShort(channel.views) : ''}</Text>
         </Details>
 
         <Details>

+ 86 - 60
src/views/viewer/ChannelView/ChannelView.tsx

@@ -1,5 +1,5 @@
 import { subMonths } from 'date-fns'
-import React, { useEffect, useMemo, useRef, useState } from 'react'
+import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
 import { useParams, useSearchParams } from 'react-router-dom'
 
 import {
@@ -59,15 +59,13 @@ export const ChannelView: React.FC = () => {
   const { channel, loading, error } = useChannel(id)
   const {
     searchVideos,
-    handleSearchInputKeyPress,
     loadingSearch,
     isSearchInputOpen,
     setIsSearchingInputOpen,
-    searchQuery,
-    setSearchQuery,
     isSearching,
     setIsSearching,
     searchInputRef,
+    search,
     errorSearch,
   } = useSearchVideos({ id })
   const { followChannel } = useFollowChannel()
@@ -164,7 +162,6 @@ export const ChannelView: React.FC = () => {
     if (TABS[tab] === 'Videos' && isSearching) {
       setIsSearchingInputOpen(false)
       searchInputRef.current?.blur()
-      setSearchQuery('')
     }
     setIsSearching(false)
     setSearchParams({ 'tab': TABS[tab] }, { replace: true })
@@ -286,27 +283,13 @@ export const ChannelView: React.FC = () => {
             onSelectTab={handleSetCurrentTab}
           />
           {currentTabName === 'Videos' && (
-            <SearchContainer>
-              <StyledTextField
-                ref={searchInputRef}
-                isOpen={isSearchInputOpen}
-                value={searchQuery}
-                onChange={(e) => setSearchQuery(e.target.value)}
-                onKeyDown={handleSearchInputKeyPress}
-                placeholder="Search"
-                type="search"
-                helperText={null}
-              />
-              <SearchButton
-                onClick={() => {
-                  setIsSearchingInputOpen(true)
-                  searchInputRef.current?.focus()
-                }}
-                variant="tertiary"
-              >
-                <SvgGlyphSearch />
-              </SearchButton>
-            </SearchContainer>
+            <Search
+              searchInputRef={searchInputRef}
+              isSearchInputOpen={isSearchInputOpen}
+              setIsSearchingInputOpen={setIsSearchingInputOpen}
+              setIsSearching={setIsSearching}
+              search={search}
+            />
           )}
           {currentTabName === 'Videos' && !isSearching && (
             <SortContainer>
@@ -346,40 +329,26 @@ type UseSearchVideosParams = {
 }
 const useSearchVideos = ({ id }: UseSearchVideosParams) => {
   const [isSearchInputOpen, setIsSearchingInputOpen] = useState(false)
-  const [searchQuery, setSearchQuery] = useState('')
   const [isSearching, setIsSearching] = useState(false)
   const [searchVideo, { loading: loadingSearch, data: searchData, error: errorSearch }] = useSearchLazyQuery()
   const searchInputRef = useRef<HTMLInputElement>(null)
-  const handleSearchInputKeyPress = (event: React.KeyboardEvent<HTMLInputElement>) => {
-    if (event.key === 'Enter' || event.key === 'NumpadEnter') {
-      if (searchQuery.trim() === '') {
-        setSearchQuery('')
-        setIsSearching(false)
-      } else {
-        search()
-        setIsSearching(true)
-      }
-    }
-    if (event.key === 'Escape' || event.key === 'Esc') {
-      setIsSearchingInputOpen(false)
-      searchInputRef.current?.blur()
-      setSearchQuery('')
-    }
-  }
-  const search = () => {
-    searchVideo({
-      variables: {
-        text: searchQuery,
-        whereVideo: {
-          isPublic_eq: true,
-          mediaAvailability_eq: AssetAvailability.Accepted,
-          thumbnailPhotoAvailability_eq: AssetAvailability.Accepted,
-          channelId_eq: id,
+  const search = useCallback(
+    (searchQuery: string) => {
+      searchVideo({
+        variables: {
+          text: searchQuery,
+          whereVideo: {
+            isPublic_eq: true,
+            mediaAvailability_eq: AssetAvailability.Accepted,
+            thumbnailPhotoAvailability_eq: AssetAvailability.Accepted,
+            channelId_eq: id,
+          },
+          limit: 100,
         },
-        limit: 100,
-      },
-    })
-  }
+      })
+    },
+    [id, searchVideo]
+  )
 
   const { searchVideos } = useMemo(() => getVideosFromSearch(loadingSearch, searchData?.search), [
     loadingSearch,
@@ -392,12 +361,69 @@ const useSearchVideos = ({ id }: UseSearchVideosParams) => {
     loadingSearch,
     isSearchInputOpen,
     setIsSearchingInputOpen,
-    searchQuery,
-    setSearchQuery,
+    errorSearch,
     isSearching,
     setIsSearching,
     searchInputRef,
-    errorSearch,
-    handleSearchInputKeyPress,
   }
 }
+
+type SearchProps = {
+  searchInputRef: React.RefObject<HTMLInputElement>
+  isSearchInputOpen: boolean
+  setIsSearchingInputOpen: (isOpen: boolean) => void
+  setIsSearching: (isOpen: boolean) => void
+  search: (searchQuery: string) => void
+}
+const Search: React.FC<SearchProps> = ({
+  searchInputRef,
+  isSearchInputOpen,
+  setIsSearching,
+  search,
+  setIsSearchingInputOpen,
+}) => {
+  const [searchQuery, setSearchQuery] = useState('')
+  const handleSearchInputKeyPress = useCallback(
+    (event: React.KeyboardEvent<HTMLInputElement>) => {
+      if (event.key === 'Enter' || event.key === 'NumpadEnter') {
+        if (searchQuery.trim() === '') {
+          setSearchQuery('')
+          setIsSearching(false)
+        } else {
+          search(searchQuery)
+          setIsSearching(true)
+        }
+      }
+      if (event.key === 'Escape' || event.key === 'Esc') {
+        setIsSearchingInputOpen(false)
+        searchInputRef.current?.blur()
+        setSearchQuery('')
+      }
+    },
+    [search, searchInputRef, searchQuery, setIsSearching, setIsSearchingInputOpen, setSearchQuery]
+  )
+
+  return (
+    <SearchContainer>
+      <StyledTextField
+        ref={searchInputRef}
+        isOpen={isSearchInputOpen}
+        value={searchQuery}
+        onChange={(e) => setSearchQuery(e.target.value)}
+        onKeyDown={handleSearchInputKeyPress}
+        placeholder="Search"
+        type="search"
+        helperText={null}
+      />
+      <SearchButton
+        onClick={() => {
+          setIsSearchingInputOpen(true)
+          searchInputRef.current?.focus()
+        }}
+        variant="tertiary"
+      >
+        <SvgGlyphSearch />
+      </SearchButton>
+    </SearchContainer>
+  )
+}

+ 0 - 1
src/views/viewer/SearchOverlayView/SearchResults/SearchResults.tsx

@@ -26,7 +26,6 @@ export const SearchResults: React.FC<SearchResultsProps> = ({ query }) => {
       thumbnailPhotoAvailability_eq: AssetAvailability.Accepted,
     },
     whereChannel: {},
-    limit: 100,
   })
 
   const getChannelsAndVideos = (loading: boolean, data: SearchQuery['search'] | undefined) => {