1import format from 'date-fns/format'; 2import { Text } from 'expo-dev-client-components'; 3import { gql } from 'graphql-request'; 4import * as React from 'react'; 5import { Platform } from 'react-native'; 6import { useInfiniteQuery } from 'react-query'; 7 8import { apiClient } from '../apiClient'; 9import { Toasts } from '../components/Toasts'; 10import { queryClient, useQueryOptions } from '../providers/QueryProvider'; 11import { useToastStack } from '../providers/ToastStackProvider'; 12import { useUpdatesConfig } from '../providers/UpdatesConfigProvider'; 13import { primeCacheWithUpdates, Update } from './useUpdatesForBranch'; 14 15const query = gql` 16 query getBranches( 17 $appId: String! 18 $offset: Int! 19 $limit: Int! 20 $runtimeVersion: String! 21 $platform: AppPlatform! 22 ) { 23 app { 24 byId(appId: $appId) { 25 updateBranches(offset: $offset, limit: $limit) { 26 id 27 name 28 29 compatibleUpdates: updates( 30 offset: 0 31 limit: 1 32 filter: { runtimeVersions: [$runtimeVersion], platform: $platform } 33 ) { 34 id 35 } 36 37 updates: updates(offset: 0, limit: $limit, filter: { platform: $platform }) { 38 id 39 message 40 runtimeVersion 41 createdAt 42 manifestPermalink 43 } 44 } 45 } 46 } 47 } 48`; 49 50export type Branch = { 51 id: string; 52 name: string; 53 updates: Update[]; 54}; 55 56async function getBranchesAsync({ 57 appId, 58 page = 1, 59 runtimeVersion, 60 pageSize, 61}: { 62 appId: string; 63 page?: number; 64 runtimeVersion: string; 65 pageSize: number; 66}) { 67 if (appId !== '') { 68 const offset = (page - 1) * pageSize; 69 const variables = { 70 appId, 71 offset, 72 limit: pageSize, 73 runtimeVersion, 74 platform: Platform.OS.toUpperCase(), 75 }; 76 77 const branches: Branch[] = []; 78 const incompatibleBranches: Branch[] = []; 79 80 const response = await apiClient.request(query, variables); 81 const updateBranches = response.app.byId.updateBranches; 82 updateBranches.forEach((updateBranch) => { 83 const branch: Branch = { 84 id: updateBranch.id, 85 name: updateBranch.name, 86 updates: updateBranch.updates.map((update) => { 87 return { 88 ...update, 89 createdAt: format(new Date(update.createdAt), 'MMMM d, yyyy, h:mma'), 90 }; 91 }), 92 }; 93 94 const hasNoUpdates = updateBranch.updates.length === 0; 95 const isCompatible = hasNoUpdates || updateBranch.compatibleUpdates.length > 0; 96 97 if (isCompatible) { 98 branches.push(branch); 99 } else { 100 incompatibleBranches.push(branch); 101 } 102 103 // side-effect: prime the cache with branches 104 primeCacheWithBranch(appId, branch); 105 106 // side-effect: prime the cache with the first paginated updates for a branch 107 primeCacheWithUpdates(appId, branch.name, branch.updates); 108 }); 109 110 return { 111 branches, 112 incompatibleBranches, 113 page, 114 }; 115 } 116 117 return { 118 branches: [], 119 incompatibleBranches: [], 120 page: 1, 121 }; 122} 123 124export function useBranchesForApp(appId: string, isAuthenticated: boolean) { 125 const { runtimeVersion } = useUpdatesConfig(); 126 const toastStack = useToastStack(); 127 const { queryOptions } = useQueryOptions(); 128 const isEnabled = appId != null && isAuthenticated; 129 130 const query = useInfiniteQuery( 131 ['branches', appId, queryOptions.pageSize], 132 ({ pageParam }) => { 133 return getBranchesAsync({ 134 appId, 135 page: pageParam, 136 runtimeVersion, 137 pageSize: queryOptions.pageSize, 138 }); 139 }, 140 { 141 retry: 3, 142 refetchOnMount: false, 143 enabled: !!isEnabled, 144 getNextPageParam: (lastPage) => { 145 const totalBranches = lastPage.incompatibleBranches.length + lastPage.branches.length; 146 147 if (totalBranches < queryOptions.pageSize) { 148 return undefined; 149 } 150 151 return lastPage?.page + 1; 152 }, 153 } 154 ); 155 156 React.useEffect(() => { 157 if (query.error && isAuthenticated) { 158 const doesNotHaveErrorShowing = 159 toastStack.getItems().filter((i) => i.status === 'pushing' || i.status === 'settled') 160 .length === 0; 161 162 // @ts-ignore 163 const errorMessage = query.error.message; 164 165 if (doesNotHaveErrorShowing) { 166 toastStack.push(() => ( 167 <Toasts.Error> 168 <Text color="error" size="small"> 169 {errorMessage || `Something went wrong trying to fetch branches for this app`} 170 </Text> 171 </Toasts.Error> 172 )); 173 } 174 } 175 }, [query.error, isAuthenticated]); 176 177 const branches = 178 query.data?.pages 179 .flatMap((page) => page.branches) 180 .filter((branch) => branch.updates.length > 0) ?? []; 181 182 // incompatible branches are branches that have no compatible updates with the current runtimeVersion 183 const incompatibleBranches = 184 query?.data?.pages.flatMap((page) => page.incompatibleBranches) ?? []; 185 186 // emptyBranches are branches that have no updates and have been created recently 187 const emptyBranches = 188 query?.data?.pages[0].branches.filter((branch) => branch.updates.length === 0) ?? []; 189 190 return { 191 ...query, 192 data: branches, 193 emptyBranches, 194 incompatibleBranches, 195 isRefreshing: query.isRefetching && !query.isFetchingNextPage, 196 isFetchingNextPage: !query.isLoading && query.isFetchingNextPage, 197 }; 198} 199 200export function prefetchBranchesForApp(appId: string, runtimeVersion: string, pageSize: number) { 201 return queryClient.prefetchInfiniteQuery(['branches', appId, pageSize], ({ pageParam = 1 }) => 202 getBranchesAsync({ page: pageParam, appId, runtimeVersion, pageSize }) 203 ); 204} 205 206export function primeCacheWithBranch(appId: string, branch: Branch) { 207 return queryClient.setQueryData(['branches', appId, branch.name], branch); 208} 209 210export function resetBranchQueries() { 211 return queryClient.resetQueries(['branches']); 212} 213