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