Skip to content

Commit

Permalink
Implement in-app update notifications
Browse files Browse the repository at this point in the history
CommanderRedYT committed Jan 3, 2024
1 parent 59d792a commit 7a33eb5
Showing 9 changed files with 189 additions and 43 deletions.
30 changes: 26 additions & 4 deletions src/github/FetchHandler.tsx
Original file line number Diff line number Diff line change
@@ -3,11 +3,15 @@ import type { Release } from '@octokit/webhooks-types';
import type { FC } from 'react';
import { useEffect } from 'react';

import { setLatestRelease, setReleases } from '@/slices/github';
import {
setLatestAppRelease,
setLatestRelease,
setReleases,
} from '@/slices/github';

import ago from '@/utils/ago';

import { GithubBaseConfig, useGithub } from '@/github/index';
import { AppGithubBaseConfig, OpenDTUGithubBaseConfig, useGithub } from '@/github/index';
import { useAppDispatch, useAppSelector } from '@/store';

const FetchHandler: FC = () => {
@@ -26,6 +30,12 @@ const FetchHandler: FC = () => {
: true,
);

const latestAppReleaseRefetchOk = useAppSelector(state =>
state.github.latestAppRelease.lastUpdate
? ago(state.github.latestAppRelease.lastUpdate) > 1000 * 60 * 10 // 10 minutes
: true,
);

const githubApi = useGithub();

useEffect(() => {
@@ -38,7 +48,7 @@ const FetchHandler: FC = () => {
if (latestReleaseRefetchOk) {
const latestRelease = await githubApi.request(
'GET /repos/{owner}/{repo}/releases/latest',
GithubBaseConfig,
OpenDTUGithubBaseConfig,
);

dispatch(setLatestRelease({ latest: latestRelease.data as Release }));
@@ -49,13 +59,24 @@ const FetchHandler: FC = () => {
if (allReleasesRefetchOk) {
const releases = await githubApi.request(
'GET /repos/{owner}/{repo}/releases',
GithubBaseConfig,
OpenDTUGithubBaseConfig,
);

dispatch(setReleases({ releases: releases.data as Release[] }));
} else {
console.log('SKIP allReleasesRefetchOk');
}

if (latestAppReleaseRefetchOk) {
const appRelease = await githubApi.request(
'GET /repos/{owner}/{repo}/releases/latest',
AppGithubBaseConfig,
);

dispatch(setLatestAppRelease({ latest: appRelease.data as Release }));
} else {
console.log('SKIP latestAppReleaseRefetchOk');
}
} catch (e) {
console.warn('GITHUB FETCH ERROR', e);
}
@@ -68,6 +89,7 @@ const FetchHandler: FC = () => {
githubApi,
latestReleaseRefetchOk,
allReleasesRefetchOk,
latestAppReleaseRefetchOk,
]);

return null;
10 changes: 9 additions & 1 deletion src/github/index.tsx
Original file line number Diff line number Diff line change
@@ -5,14 +5,22 @@ import { createContext, useContext, useMemo } from 'react';

export const GithubContext = createContext<Octokit | undefined>(undefined);

export const GithubBaseConfig = {
export const OpenDTUGithubBaseConfig = {
owner: 'tbnobody',
repo: 'OpenDTU',
headers: {
'X-GitHub-Api-Version': '2022-11-28',
},
};

export const AppGithubBaseConfig = {
owner: 'OpenDTU-App',
repo: 'opendtu-react-native',
headers: {
'X-GitHub-Api-Version': '2022-11-28',
},
};

export const GithubProvider: FC<PropsWithChildren<unknown>> = ({
children,
}) => {
26 changes: 26 additions & 0 deletions src/hooks/useHasNewAppVersion.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import packageJson from '@root/package.json';
import { compare } from 'compare-versions';

import { useMemo } from 'react';

import { useAppSelector } from '@/store';

const useHasNewAppVersion = () => {
const appRelease = useAppSelector(
state => state.github.latestAppRelease?.data,
);

return useMemo(() => {
if (!appRelease) return [false, null] as const;

const newAppVersionAvailable = compare(
appRelease?.tag_name,
packageJson.version,
'>',
);

return [newAppVersionAvailable, appRelease] as const;
}, [appRelease]);
};

export default useHasNewAppVersion;
37 changes: 36 additions & 1 deletion src/slices/github.ts
Original file line number Diff line number Diff line change
@@ -15,6 +15,10 @@ const initialState: GithubState = {
data: null,
lastUpdate: null,
},
latestAppRelease: {
data: null,
lastUpdate: null,
},
};

const githubSlice = createSlice({
@@ -33,10 +37,41 @@ const githubSlice = createSlice({
lastUpdate: new Date(),
};
},
setLatestAppRelease: (state, action: SetGithubLatestReleaseAction) => {
state.latestAppRelease = {
data: action.payload.latest,
lastUpdate: new Date(),
};
},
clearReleases: state => {
state.releases = {
data: [],
lastUpdate: null,
};
},
clearLatestRelease: state => {
state.latestRelease = {
data: null,
lastUpdate: null,
};
},
clearLatestAppRelease: state => {
state.latestAppRelease = {
data: null,
lastUpdate: null,
};
},
},
});

export const { setReleases, setLatestRelease } = githubSlice.actions;
export const {
setReleases,
setLatestRelease,
setLatestAppRelease,
clearReleases,
clearLatestRelease,
clearLatestAppRelease,
} = githubSlice.actions;

export const { reducer: GithubReducer } = githubSlice;

2 changes: 1 addition & 1 deletion src/translations/translation-files
Submodule translation-files updated 2 files
+10 −2 de.json
+10 −2 en.json
3 changes: 3 additions & 0 deletions src/types/opendtu/github.ts
Original file line number Diff line number Diff line change
@@ -17,4 +17,7 @@ export interface WithTimestamp<T> {
export interface GithubState {
latestRelease: WithTimestamp<Release | null>;
releases: WithTimestamp<Release[]>;

// Maybe at a later point: https://docs.github.com/en/rest/releases/releases?apiVersion=2022-11-28#generate-release-notes-content-for-a-release
latestAppRelease: WithTimestamp<Release | null>;
}
18 changes: 5 additions & 13 deletions src/views/navigation/NavigationTabs.tsx
Original file line number Diff line number Diff line change
@@ -6,6 +6,8 @@ import {
useTheme,
} from 'react-native-paper';

import useHasNewAppVersion from '@/hooks/useHasNewAppVersion';

import LivedataTab from '@/views/navigation/tabs/LivedataTab';
import MainSettingsTab from '@/views/navigation/tabs/MainSettingsTab';

@@ -22,18 +24,7 @@ const BottomNavigation: FC = () => {
const { t } = useTranslation();
const [index, setIndex] = useState<number>(0);

/*const [routes] = useState<BaseRoutes>([
{
key: 'livedata',
title: 'Livedata',
focusedIcon: 'solar-power',
},
{
key: 'settings',
title: 'Settings',
focusedIcon: 'cog',
},
]);*/
const [hasNewAppVersion] = useHasNewAppVersion();

const routes = useMemo<BaseRoutes>(
() => [
@@ -46,9 +37,10 @@ const BottomNavigation: FC = () => {
key: 'settings',
title: t('navigation.settings'),
focusedIcon: 'cog',
badge: hasNewAppVersion,
},
],
[t],
[t, hasNewAppVersion],
);

const renderScene = BottomNavigationPaper.SceneMap({
95 changes: 73 additions & 22 deletions src/views/navigation/screens/AboutSettingsScreen.tsx
Original file line number Diff line number Diff line change
@@ -5,19 +5,22 @@ import type { Licenses } from 'npm-license-crawler';
import packageJson from 'package.json';

import type { FC } from 'react';
import { useCallback } from 'react';
import { useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { Linking, ScrollView } from 'react-native';
import { Box } from 'react-native-flex-layout';
import {
Appbar,
Badge,
Button,
Divider,
List,
Text,
useTheme,
} from 'react-native-paper';

import useHasNewAppVersion from '@/hooks/useHasNewAppVersion';

import { StyledSafeAreaView } from '@/style';

const AboutSettingsScreen: FC = () => {
@@ -30,6 +33,16 @@ const AboutSettingsScreen: FC = () => {
navigation.goBack();
}, [navigation]);

const [hasNewAppVersion, releaseInfo] = useHasNewAppVersion();

const prettyTagName = useMemo(() => {
if (!releaseInfo?.tag_name) {
return '';
}

return releaseInfo.tag_name.replace(/^v/, '');
}, [releaseInfo]);

return (
<>
<Appbar.Header>
@@ -59,30 +72,68 @@ const AboutSettingsScreen: FC = () => {
</Box>
</Box>
</Box>
{hasNewAppVersion ? (
<>
<Divider />
<Box p={8}>
<Box
style={{
display: 'flex',
flexDirection: 'row',
justifyContent: 'center',
gap: 4,
}}
>
<Text variant="titleLarge" style={{ textAlign: 'center' }}>
{t('aboutApp.newVersionAvailable')}
</Text>
<Badge style={{ alignSelf: 'center' }}>
{prettyTagName}
</Badge>
</Box>
<Box mt={16} mb={8}>
<Button
buttonColor="#24292e"
textColor="#ffffff"
icon="github"
onPress={() =>
Linking.openURL(releaseInfo?.html_url || '')
}
disabled={!releaseInfo?.html_url}
>
{t('aboutApp.viewMore')}
</Button>
</Box>
</Box>
</>
) : null}
<Divider />
{Object.entries(licenses as unknown as Licenses).map(
([key, { licenses, repository, licenseUrl }]) => {
const [name, version] = key.rsplit('@', 1);
<List.Section>
<List.Subheader>{t('aboutApp.licenses')}</List.Subheader>
{Object.entries(licenses as unknown as Licenses).map(
([key, { licenses, repository, licenseUrl }]) => {
const [name, version] = key.rsplit('@', 1);

return (
<List.Item
key={key}
title={name}
description={`${licenses} \u2022 ${version}`}
onPress={
repository || licenseUrl
? async () => {
const url = repository || licenseUrl;
if (await Linking.canOpenURL(url)) {
await Linking.openURL(url);
return (
<List.Item
key={key}
title={name}
description={`${licenses} \u2022 ${version}`}
onPress={
repository || licenseUrl
? async () => {
const url = repository || licenseUrl;
if (await Linking.canOpenURL(url)) {
await Linking.openURL(url);
}
}
}
: undefined
}
/>
);
},
)}
: undefined
}
/>
);
},
)}
</List.Section>
</ScrollView>
</Box>
</StyledSafeAreaView>
11 changes: 10 additions & 1 deletion src/views/navigation/tabs/MainSettingsTab.tsx
Original file line number Diff line number Diff line change
@@ -6,11 +6,12 @@ import { useCallback, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { ScrollView } from 'react-native';
import { Box } from 'react-native-flex-layout';
import { List, useTheme } from 'react-native-paper';
import { Badge, List, useTheme } from 'react-native-paper';

import ChangeLanguageModal from '@/components/modals/ChangeLanguageModal';
import ChangeThemeModal from '@/components/modals/ChangeThemeModal';

import useHasNewAppVersion from '@/hooks/useHasNewAppVersion';
import useIsConnected from '@/hooks/useIsConnected';

import { StyledSafeAreaView } from '@/style';
@@ -34,6 +35,7 @@ const MainSettingsTab: FC = () => {
const closeChangeLanguageModal = () => setShowChangeLanguageModal(false);

const websocketConnected = useIsConnected();
const [hasNewAppVersion] = useHasNewAppVersion();

const handleAbout = useCallback(() => {
navigation.navigate('AboutSettingsScreen');
@@ -77,6 +79,13 @@ const MainSettingsTab: FC = () => {
title={t('settings.aboutApp')}
description={t('settings.aboutDescription')}
left={props => <List.Icon {...props} icon="information" />}
right={props =>
hasNewAppVersion ? (
<Badge visible={true} style={{ marginTop: 8 }} {...props}>
{t('settings.newAppRelease')}
</Badge>
) : null
}
onPress={handleAbout}
/>
</List.Section>

0 comments on commit 7a33eb5

Please sign in to comment.