feat: 增加安装应用按钮

pull/64/head
moonrailgun 2 years ago
parent 91fe01f247
commit dcbc148eeb

@ -33,6 +33,7 @@
"clsx": "^1.2.1",
"compressorjs": "^1.1.1",
"copy-to-clipboard": "^3.3.3",
"detect-browser": "^5.3.0",
"emoji-mart": "^3.0.1",
"immer": "^9.0.16",
"is-electron": "^2.2.1",

@ -0,0 +1,217 @@
// Fork from https://github.com/piro0919/use-pwa/blob/master/src/hooks/usePwa/index.ts
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { detect } from 'detect-browser';
type PromiseType<T extends Promise<any>> = T extends Promise<infer P>
? P
: never;
type BeforeInstallPromptEvent = Event & {
readonly platforms: Array<string>;
readonly userChoice: Promise<{
outcome: 'accepted' | 'dismissed';
platform: string;
}>;
prompt(): Promise<void>;
};
export type PwaData = {
appinstalled: boolean;
canInstallprompt: boolean;
enabledA2hs: boolean;
enabledPwa: boolean;
enabledUpdate: boolean;
isLoading: boolean;
isPwa: boolean;
showInstallPrompt: () => void;
unregister: () => Promise<boolean | undefined>;
userChoice?: PromiseType<BeforeInstallPromptEvent['userChoice']>;
};
export function usePwa(): PwaData {
const beforeinstallprompt = useRef<BeforeInstallPromptEvent>();
const [appinstalled, setAppinstalled] = useState(false);
const [canInstallprompt, setCanInstallprompt] = useState(false);
const [enabledA2hs, setEnabledA2hs] = useState(false);
const [enabledPwa, setEnabledPwa] = useState(false);
const [isPwa, setIsPwa] = useState(false);
const [enabledUpdate, setEnabledUpdate] = useState(false);
const [userChoice, setUserChoice] = useState<PwaData['userChoice']>();
const showInstallPrompt = useCallback(async () => {
if (!beforeinstallprompt.current) {
return;
}
await beforeinstallprompt.current.prompt();
if (!beforeinstallprompt.current) {
return;
}
const userChoice = await beforeinstallprompt.current.userChoice;
setUserChoice(userChoice);
}, []);
const unregister = useCallback(async () => {
if (!('serviceWorker' in window.navigator)) {
return;
}
const registration = await window.navigator.serviceWorker.getRegistration();
if (!registration) {
return;
}
const result = await registration.unregister();
return result;
}, []);
const handleBeforeInstallPrompt = useCallback(
(event: BeforeInstallPromptEvent) => {
beforeinstallprompt.current = event;
setCanInstallprompt(true);
},
[]
);
const handleAppinstalled = useCallback(() => {
setAppinstalled(true);
}, []);
const [completed, setCompleted] = useState({
appinstalled: false,
beforeinstallprompt: false,
enabledA2hs: false,
enabledPwa: false,
enabledUpdate: false,
isPwa: false,
});
const isLoading = useMemo(
() => !Object.values(completed).filter((value) => value).length,
[completed]
);
useEffect(() => {
window.addEventListener(
'beforeinstallprompt',
handleBeforeInstallPrompt as any
);
setCompleted((prevCompleted) => ({
...prevCompleted,
beforeinstallprompt: true,
}));
return () => {
window.removeEventListener(
'beforeinstallprompt',
handleBeforeInstallPrompt as any
);
};
}, [handleBeforeInstallPrompt]);
useEffect(() => {
window.addEventListener('appinstalled', handleAppinstalled);
setCompleted((prevCompleted) => ({
...prevCompleted,
appinstalled: true,
}));
return () => {
window.removeEventListener('appinstalled', handleAppinstalled);
};
}, [handleAppinstalled]);
useEffect(() => {
setEnabledPwa(
'serviceWorker' in window.navigator &&
'BeforeInstallPromptEvent' in window
);
setCompleted((prevCompleted) => ({
...prevCompleted,
enabledPwa: true,
}));
}, []);
useEffect(() => {
setIsPwa(
'standalone' in window.navigator ||
window.matchMedia('(display-mode: standalone)').matches
);
setCompleted((prevCompleted) => ({
...prevCompleted,
isPwa: true,
}));
}, []);
useEffect(() => {
try {
const browser = detect();
if (!browser) {
return;
}
const userAgent = window.navigator.userAgent.toLowerCase();
const isIos =
userAgent.indexOf('iphone') >= 0 ||
userAgent.indexOf('ipad') >= 0 ||
(userAgent.indexOf('macintosh') >= 0 && 'ontouchend' in document);
const { name } = browser;
setEnabledA2hs(isIos && name === 'ios');
} finally {
setCompleted((prevCompleted) => ({
...prevCompleted,
enabledA2hs: true,
}));
}
}, []);
useEffect(() => {
const callback = async () => {
try {
if (!('serviceWorker' in window.navigator)) {
return;
}
const registration =
await window.navigator.serviceWorker.getRegistration();
if (!registration) {
return;
}
registration.onupdatefound = async () => {
await registration.update();
setEnabledUpdate(true);
};
} finally {
setCompleted((prevCompleted) => ({
...prevCompleted,
enabledUpdate: true,
}));
}
};
callback();
}, []);
return {
appinstalled,
canInstallprompt,
enabledA2hs,
enabledUpdate,
enabledPwa,
isLoading,
isPwa,
showInstallPrompt,
unregister,
userChoice,
};
}

@ -0,0 +1,23 @@
import { usePwa } from '@/hooks/usePwa';
import React from 'react';
import { Icon } from 'tailchat-design';
/**
*
*/
export const InstallBtn: React.FC = React.memo(() => {
const { canInstallprompt, showInstallPrompt } = usePwa();
if (!canInstallprompt) {
return null;
}
return (
<Icon
className="text-3xl text-gray-600 dark:text-white cursor-pointer"
icon="mdi:download"
onClick={showInstallPrompt}
/>
);
});
InstallBtn.displayName = 'InstallBtn';

@ -6,6 +6,7 @@ import { Divider } from 'antd';
import { PersonalNav } from './PersonalNav';
import { DevContainer } from '@/components/DevContainer';
import { InboxNav } from './InboxNav';
import { InstallBtn } from './InstallBtn';
/**
*
@ -35,7 +36,14 @@ export const Navbar: React.FC = React.memo(() => {
<GroupNav />
</div>
<div data-tc-role="navbar-settings">
<div
data-tc-role="navbar-settings"
className="flex flex-col items-center space-y-2"
>
{/* 应用(PWA)安装按钮 */}
<InstallBtn />
{/* 设置按钮 */}
<SettingBtn />
</div>
</div>

@ -361,6 +361,7 @@ importers:
copy-webpack-plugin: ^11.0.0
cross-env: ^7.0.3
css-loader: ^5.2.7
detect-browser: ^5.3.0
dotenv: ^10.0.0
dts-generator: ^3.0.0
emoji-mart: ^3.0.1
@ -443,6 +444,7 @@ importers:
clsx: 1.2.1
compressorjs: 1.1.1
copy-to-clipboard: 3.3.3
detect-browser: 5.3.0
emoji-mart: 3.0.1_react@18.2.0
immer: 9.0.16
is-electron: 2.2.1
@ -16620,6 +16622,10 @@ packages:
dependencies:
repeat-string: 1.6.1
/detect-browser/5.3.0:
resolution: {integrity: sha512-53rsFbGdwMwlF7qvCt0ypLM5V5/Mbl0szB7GPN8y9NCcbknYOeVVXdrXEq+90IwAfrrzt6Hd+u2E2ntakICU8w==}
dev: false
/detect-file/1.0.0:
resolution: {integrity: sha512-DtCOLG98P007x7wiiOmfI0fi3eIKyWiLTGJ2MDnVi/E04lWGbf+JzrRHMm0rgIIZJGtHpKpbVgLWHrv8xXpc3Q==}
engines: {node: '>=0.10.0'}

Loading…
Cancel
Save