Sfoglia il codice sorgente

add follow/unfollow functionality (#224)

* add follow/unfollow button, saving follows to local store

* clean channel view

* fix localStorage on channelview, add queries

* change types

* clean in localStorageClient

* add follow/unfollow channel resolvers

* fix follow channel resolver

* fix resolver and mutation query

* rebase personalisation

* add followedChannels state, edit button styling, add types to mutation

* change reducer

* add button styling fix, mutation types fix, unfollow resolver fix

* add local caching for following a channel
mikkio 4 anni fa
parent
commit
17ef0779f5

+ 22 - 0
src/api/queries/__generated__/followChannel.ts

@@ -0,0 +1,22 @@
+/* tslint:disable */
+/* eslint-disable */
+// @generated
+// This file was automatically generated and should not be edited.
+
+// ====================================================
+// GraphQL mutation operation: FollowChannel
+// ====================================================
+
+export interface FollowChannel_followChannel {
+  __typename: "ChannelFollowsInfo";
+  id: string;
+  follows: number;
+}
+
+export interface FollowChannel {
+  followChannel: FollowChannel_followChannel;
+}
+
+export interface FollowChannelVariables {
+  channelId: string;
+}

+ 22 - 0
src/api/queries/__generated__/unfollowChannel.ts

@@ -0,0 +1,22 @@
+/* tslint:disable */
+/* eslint-disable */
+// @generated
+// This file was automatically generated and should not be edited.
+
+// ====================================================
+// GraphQL mutation operation: UnfollowChannel
+// ====================================================
+
+export interface UnfollowChannel_unfollowChannel {
+  __typename: "ChannelFollowsInfo";
+  id: string;
+  follows: number;
+}
+
+export interface UnfollowChannel {
+  unfollowChannel: UnfollowChannel_unfollowChannel;
+}
+
+export interface UnfollowChannelVariables {
+  channelId: string;
+}

+ 17 - 0
src/api/queries/channels.ts

@@ -45,3 +45,20 @@ export const GET_CHANNEL = gql`
   }
   ${allChannelFieldsFragment}
 `
+
+export const FOLLOW_CHANNEL = gql`
+  mutation FollowChannel($channelId: ID!) {
+    followChannel(channelId: $channelId) {
+      id
+      follows
+    }
+  }
+`
+export const UNFOLLOW_CHANNEL = gql`
+  mutation UnfollowChannel($channelId: ID!) {
+    unfollowChannel(channelId: $channelId) {
+      id
+      follows
+    }
+  }
+`

+ 2 - 3
src/hooks/usePersonalData/localStorageClient/localStorageClient.tsx

@@ -92,11 +92,10 @@ const setChannelFollowing = async (id: string, follow = true) => {
   if (isFollowing) {
     newFollowedChannels = follow
       ? currentFollowedChannels
-      : currentFollowedChannels.filter((channel) => channel.id === id)
+      : currentFollowedChannels.filter((channel) => channel.id !== id)
   } else {
-    newFollowedChannels = follow ? [...currentFollowedChannels, id] : currentFollowedChannels
+    newFollowedChannels = follow ? [...currentFollowedChannels, { id }] : currentFollowedChannels
   }
-
   writeToLocalStorage('followedChannels', newFollowedChannels)
 }
 

+ 4 - 0
src/mocking/server/index.ts

@@ -16,6 +16,8 @@ import {
   videosResolver,
   videoViewsResolver,
   channelFollowsResolver,
+  followChannelResolver,
+  unfollowChannelResolver,
 } from './resolvers'
 import {
   MOCK_ORION_GRAPHQL_URL,
@@ -49,6 +51,8 @@ createServer({
         },
         Mutation: {
           addVideoView: addVideoViewResolver,
+          followChannel: followChannelResolver,
+          unfollowChannel: unfollowChannelResolver,
         },
       },
     })

+ 32 - 0
src/mocking/server/resolvers.ts

@@ -115,6 +115,38 @@ export const channelFollowsResolver: QueryResolver<ChannelFollowsArgs> = (obj, a
   return mirageGraphQLFieldResolver(obj, { id: args.channelId }, context, info)
 }
 
+export const followChannelResolver: QueryResolver<ChannelFollowsArgs> = (obj, args, context, info) => {
+  const channelInfo = context.mirageSchema.channelFollowsInfos.find(args.channelId)
+  if (!channelInfo) {
+    const channelInfo = context.mirageSchema.channelFollowsInfos.create({
+      id: args.channelId,
+      follows: 1,
+    })
+
+    return channelInfo
+  }
+  channelInfo.update({
+    follows: channelInfo.follows + 1,
+  })
+  return channelInfo
+}
+
+export const unfollowChannelResolver: QueryResolver<ChannelFollowsArgs> = (obj, args, context, info) => {
+  const channelInfo = context.mirageSchema.channelFollowsInfos.find(args.channelId)
+  if (!channelInfo) {
+    const channelInfo = context.mirageSchema.channelFollowsInfos.create({
+      id: args.channelId,
+      follows: 0,
+    })
+
+    return channelInfo
+  }
+  channelInfo.update({
+    follows: Math.max(0, channelInfo.follows - 1),
+  })
+  return channelInfo
+}
+
 type VideoModel = { attrs: VideoFields }
 type ChannelModel = { attrs: AllChannelFields }
 type SearchResolverResult = Omit<Search_search, 'item'> & { item: VideoModel | ChannelModel }

+ 11 - 1
src/views/ChannelView/ChannelView.style.tsx

@@ -1,6 +1,6 @@
 import styled from '@emotion/styled'
 import { fluidRange } from 'polished'
-import { Placeholder, Text } from '@/shared/components'
+import { Placeholder, Text, Button } from '@/shared/components'
 import ChannelLink from '@/components/ChannelLink'
 import { breakpoints, colors, sizes, typography, zIndex } from '@/shared/theme'
 
@@ -158,6 +158,7 @@ export const TitleSection = styled.div`
 `
 export const TitleContainer = styled.div`
   max-width: 100%;
+  overflow: hidden;
   @media screen and (min-width: ${breakpoints.medium}) {
     max-width: 60%;
   }
@@ -216,3 +217,12 @@ export const SubTitlePlaceholder = styled(Placeholder)`
     height: ${SUBTITLE_HEIGHT};
   }
 `
+export const StyledButtonContainer = styled.div`
+  margin-top: ${sizes(2)};
+  @media screen and (min-width: ${breakpoints.small}) {
+    margin-top: 0;
+    padding-left: ${sizes(6)};
+    margin-left: auto;
+    align-self: center;
+  }
+`

+ 74 - 3
src/views/ChannelView/ChannelView.tsx

@@ -1,9 +1,12 @@
-import React from 'react'
+import React, { useState, useEffect } from 'react'
 import { RouteComponentProps, useParams } from '@reach/router'
-import { useQuery } from '@apollo/client'
+import { useQuery, useMutation } from '@apollo/client'
 
-import { GET_CHANNEL } from '@/api/queries/channels'
+import { GET_CHANNEL, FOLLOW_CHANNEL, UNFOLLOW_CHANNEL } from '@/api/queries/channels'
 import { GetChannel, GetChannelVariables } from '@/api/queries/__generated__/GetChannel'
+import { FollowChannel, FollowChannelVariables } from '@/api/queries/__generated__/followChannel'
+import { UnfollowChannel, UnfollowChannelVariables } from '@/api/queries/__generated__/unfollowChannel'
+import { usePersonalData } from '@/hooks'
 
 import {
   CoverImage,
@@ -18,18 +21,81 @@ import {
   VideoSection,
   SubTitle,
   SubTitlePlaceholder,
+  StyledButtonContainer,
 } from './ChannelView.style'
 import { BackgroundPattern, InfiniteVideoGrid } from '@/components'
+import { Button } from '@/shared/components'
 import { CSSTransition, TransitionGroup } from 'react-transition-group'
 import { transitions } from '@/shared/theme'
 import { formatNumberShort } from '@/utils/number'
 
+type FollowedChannel = {
+  id: string
+}
+
 const ChannelView: React.FC<RouteComponentProps> = () => {
   const { id } = useParams()
   const { data, loading, error } = useQuery<GetChannel, GetChannelVariables>(GET_CHANNEL, {
     variables: { id },
   })
+  const [followChannel] = useMutation<FollowChannel, FollowChannelVariables>(FOLLOW_CHANNEL, {
+    variables: {
+      channelId: id,
+    },
+    update: (cache, mutationResult) => {
+      cache.modify({
+        id: cache.identify({
+          __typename: 'Channel',
+          id,
+        }),
+        fields: {
+          follows: () => mutationResult.data?.followChannel.follows,
+        },
+      })
+    },
+  })
+  const [unfollowChannel] = useMutation<UnfollowChannel, UnfollowChannelVariables>(UNFOLLOW_CHANNEL, {
+    variables: {
+      channelId: id,
+    },
+    update: (cache, mutationResult) => {
+      cache.modify({
+        id: cache.identify({
+          __typename: 'Channel',
+          id,
+        }),
+        fields: {
+          follows: () => mutationResult.data?.unfollowChannel.follows,
+        },
+      })
+    },
+  })
+  const {
+    state: { followedChannels },
+    updateChannelFollowing,
+  } = usePersonalData()
+  const [isFollowing, setFollowing] = useState<boolean>()
+
+  useEffect(() => {
+    const isFollowing = followedChannels.some((channel) => channel.id === id)
+    setFollowing(isFollowing)
+  }, [followedChannels, id])
 
+  const handleFollow = () => {
+    try {
+      if (isFollowing) {
+        updateChannelFollowing(id, false)
+        unfollowChannel()
+        setFollowing(false)
+      } else {
+        updateChannelFollowing(id, true)
+        followChannel()
+        setFollowing(true)
+      }
+    } catch (error) {
+      console.warn('Failed to update Channel following', { error })
+    }
+  }
   if (error) {
     throw error
   }
@@ -71,6 +137,11 @@ const ChannelView: React.FC<RouteComponentProps> = () => {
               </>
             )}
           </TitleContainer>
+          <StyledButtonContainer>
+            <Button variant={isFollowing ? 'secondary' : 'primary'} onClick={handleFollow}>
+              {isFollowing ? 'Unfollow' : 'Follow'}
+            </Button>
+          </StyledButtonContainer>
         </TitleSection>
       </Header>
       <VideoSection>