Newer
Older
mobile.raikyakun.app / node / src / app / page.tsx
nutrition 14 hours ago 11 KB 利便性向上
'use client'
import { useEffect, useMemo, useState } from 'react'
import Image from 'next/image'
import styles from './page.module.css'
import { Box, Alert, Button, TextField, Typography, InputAdornment, IconButton, FormControl, InputLabel, OutlinedInput, FormHelperText,
	Divider, Skeleton
 } from '@mui/material'
import { ContentCopy as ContentCopyIcon } from '@mui/icons-material'
import { getMobileOS, getBrowser } from '@/utils'
import useWebpush from './useWebpush'
import LockIcon from '@mui/icons-material/Lock';
import { useIndexedDB } from '@/hooks'
import dynamic from 'next/dynamic'
import { QRCodeSVG } from 'qrcode.react'
import { diffTimeCalc } from '@/utils/date'


// ITPについい
// https://webkit.org/tracking-prevention/#intelligent-tracking-prevention-itp
const URL = (process.env.NEXT_PUBLIC_BASE_URL ?? 'https://mobile.raikyakun.app') + '/'

export default function Home() {
	const [os, setOS] = useState('Other')
	const [browser, setBrowser] = useState('Other')
	const [osDetected, setOsDetected] = useState(false)

	const [installPrompt, setInstallPrompt] = useState<BeforeInstallPromptEvent | null>(null)
	const [isInstalled, setIsInstalled] = useState(false)
	const [isStandalone, setIsStandalone] = useState(false)
	const [now, setNow] = useState<Date>(new Date())
	const [isLatestCallActive, setIsLatestCallActive] = useState(false)
	const { latestCall, callList, update } = useIndexedDB()

	
	useEffect(()=> {
		const os = getMobileOS()
		const browser = getBrowser()
		setOS(os)
		setBrowser(browser)
		setOsDetected(true)

		setIsStandalone(window.matchMedia('(display-mode: standalone)').matches)

		navigator.getInstalledRelatedApps?.().then(apps => {
			setIsInstalled(apps.length > 0)
		})

		const handleBeforeInstallPrompt = (e: Event) => {
			e.preventDefault()
			setInstallPrompt(e as BeforeInstallPromptEvent)
		}
		window.addEventListener('beforeinstallprompt', handleBeforeInstallPrompt)
		return () => window.removeEventListener('beforeinstallprompt', handleBeforeInstallPrompt)
	}, [])

	useEffect(() => {
		update()

		setInterval(()=> {
			setNow(new Date())
		}, 5000)

		const handleVisibilityChange = () => {
			if (document.visibilityState === 'visible') {
				update()
			}
		}
		document.addEventListener('visibilitychange', handleVisibilityChange)
		return () => {
		  document.removeEventListener('visibilitychange', handleVisibilityChange)
		}
	}, [])

	useEffect(()=>{
		if(!latestCall || latestCall.responder) return
		
		// ISO 8601形式の日付文字列をDateオブジェクトに変換
		const callDate = new Date(latestCall.createdAt)
			if (isNaN(callDate.getTime())) {
			console.error('Invalid date format')
			return
		}

		// 両者の差をミリ秒単位で計算し、秒単位に変換
		const now = new Date()
		const durationInSeconds = (now.getTime() - callDate.getTime()) / 1000
		
		// 現在時刻から60秒以内の場合はページを切り替え
		
		if(durationInSeconds >= 60) return
		if(latestCall.responder == null){
			setIsLatestCallActive(true)
		} else {
			location.href = `/actions?data=${JSON.stringify(latestCall)}`
		}
	}, [latestCall])

	const copyToClipboard = async () => {
		await global.navigator.clipboard.writeText(`${URL}${accessKey ? `#accessKey=${accessKey}` : ''}`)
	}
	
	const registerMode = isStandalone || (!installPrompt && !isInstalled) ? 'allowed'
		: installPrompt ? 'install-required'
		: 'hidden'
	const { Subscribe, errorMessage, accessKey, isSubscribed, canSubscribe, isLoading } = useWebpush({ registerMode, os, browser, isStandalone })
	

	
	return (
		<main className={styles.main}>
			<div style={{padding: '2rem'}}>
				<Typography sx={{textAlign: 'center'}} variant="h5" component="h1">らいきゃくん通知{os != 'Other' ? <><br />for {os}</> : ''}</Typography>
			</div>
			{(!osDetected || isLoading) && <Box sx={{width: '100%', px: 2}}>
				<Skeleton variant="rounded" height={56} sx={{mb: 2}} />
				<Skeleton variant="rounded" height={56} sx={{mb: 2}} />
				<Skeleton variant="rounded" height={100} />
			</Box>}
			{osDetected && !isLoading && <>
			{latestCall && 
			<Box 
				sx={{
					mt: 2, 
					border: `2px solid ${isLatestCallActive?'royalblue':'lightgray'}`, 
					width: '80%', 
					display: 'flex', 
					justifyContent: "space-between", 
					borderRadius: 2, 
					padding: 3, 
					boxShadow: `0px 0px 6px ${isLatestCallActive?'lightskyblue':'gray'} inset`
				}}
				onClick={()=>{if(isLatestCallActive) location.href = `/actions?data=${JSON.stringify(latestCall)}`}}
			>
				<Typography variant="h5">
					<span style={{fontWeight: 'bold', fontSize: '80%'}}>最後の呼び出し</span><br />
					<span style={{fontSize: '50%'}}>宛 先:</span>[{latestCall.dst.group}] {latestCall.dst.name}
					{latestCall.visitor && <><br /><span style={{fontSize: '50%'}}>来訪者:</span> {latestCall.visitor}</>}
					{latestCall.responder && <><br /><span style={{fontSize: '50%'}}>対応者: {latestCall.responder}</span></>}
				</Typography>
				<Box sx={{color: 'gray'}}>
					{diffTimeCalc(now, new Date(latestCall.createdAt))}
				</Box>
			</Box>}
			{(os === 'Other' || (os === 'Android' && browser !== 'Chrome' && browser !== 'WebView')) && <div style={{padding: '2rem'}}>
				<div>
					{os == 'Other' && <Alert severity="warning">このページを
					スマートフォンで開いてください</Alert>}
					{os == 'Android' && browser != 'Chrome' && browser != 'WebView' && <Alert severity="warning">このページはChromeで開いてください</Alert>}
					</div>
				<div style={{textAlign: 'center'}}>
					{os === 'Other' && <QRCodeSVG style={{margin: '1rem auto', display: 'block'}} value={`${URL}${accessKey ? `#accessKey=${accessKey}` : ''}`} size={180} />}
					{os === 'Other' && <FormControl
						fullWidth
						variant="outlined"
					>
						<InputLabel htmlFor="url">URLをコピー</InputLabel>
						<OutlinedInput
							id="url"
							value={`${URL}${accessKey ? `#accessKey=${accessKey}` : ''}`}
							endAdornment={
								<InputAdornment position="end">
									<IconButton
										edge="end"
										onClick={copyToClipboard}
									>
										<ContentCopyIcon />
									</IconButton>
								</InputAdornment>
							}
							label="URLをコピー"
						/>
					</FormControl>}
				</div>
			</div>}
			<div style={{width: '100%'}}>{ Subscribe }</div>
			{!isStandalone && isInstalled && <Alert severity="success" sx={{mx: 2, mb: 1}}>
				Webアプリとしてインストール済みです。ホーム画面のアイコンから起動してください。
			</Alert>}
			{!isStandalone && !isInstalled && installPrompt && <div style={{padding: '0 1rem 1rem'}}>
				<Button fullWidth variant="outlined" onClick={async () => {
					await installPrompt.prompt()
					const { outcome } = await installPrompt.userChoice
					if (outcome === 'accepted') setInstallPrompt(null)
				}}>Webアプリとしてインストール</Button>
			</div>}
			{errorMessage != '' && <Alert severity="error">{errorMessage}</Alert>}
			{errorMessage != '' && os == 'iOS' && browser == 'Safari' && <FormHelperText sx={{margin: 2, padding: 2, border: '1px solid gray', borderRadius: 5}}>
				通知許可ダイアログで「許可しない」を選択してしまった場合は<br />
				<span style={{verticalAlign: 'middle'}}>「<img src="/images/iphone-settings-icon.png" width={20} height={20} style={{verticalAlign: 'middle'}} />設定」アプリ
				>「<img src="/images/iphone-bell-icon.png" width={20} height={20} style={{verticalAlign: 'middle'}} />通知」>「通知スタイル」
				>「<img src="/images/maskable_icon_x144.png" width={20} height={20} style={{verticalAlign: 'middle'}} />らいきゃくん通知」>「通知を許可」から許可してください</span>
				<a target="_blank" href="https://support.apple.com/ja-jp/guide/iphone/iph7c3d96bab/ios" ><Button size="small">詳しくはこちら</Button></a>
			</FormHelperText>}
			{errorMessage != '' && os == 'Android' && browser == 'Chrome' && <FormHelperText sx={{margin: 2, padding: 2, border: '1px solid gray', borderRadius: 5}}>
				<span style={{verticalAlign: 'middle'}}>通知許可ダイアログで「ブロック」を選択してしまった場合は<br />
				アドレスバーの左にある<img src="/images/android-chrome.png" width={20} height={20} style={{verticalAlign: 'middle'}} />アイコンをクリックして権限をご確認ください</span>
			</FormHelperText>}
			{errorMessage != '' && os == 'Other' && browser == 'Chrome' && <FormHelperText sx={{margin: 2, padding: 2, border: '1px solid gray', borderRadius: 5}}>
				<span style={{verticalAlign: 'middle'}}>通知許可ダイアログで「ブロック」を選択してしまった場合は<br />
				アドレスバーの左にある<LockIcon sx={{verticalAlign: 'middle'}}/>鍵アイコンをクリックして通知設定をご確認ください</span>
			</FormHelperText>}
			{errorMessage != '' && os == 'Other' && browser == 'Safari' && <FormHelperText sx={{margin: 2, padding: 2, border: '1px solid gray', borderRadius: 5}}>
				通知許可ダイアログで「許可しない」を選択してしまった場合は<br />
				左上メニューバーから「Safari」>「設定…」>「Webサイト」>一般「通知」と進み「mobile.raikyakun.app」を許可に設定してください
			</FormHelperText>}

			{callList.length > 0 && <><Typography variant="h4" sx={{mt: 7}}>通知履歴</Typography>
			<FormHelperText>受付履歴とは異なります</FormHelperText></>}
			{callList.map(call => 
				<Box key={call.callId} sx={{width: '100%', display: 'flex', justifyContent: "space-between", mb: 2, p: 2, borderBottom: '1px solid lightgray'}}>
				<Typography variant="h5">
					<span style={{fontSize: '50%'}}>宛 先:</span>[{call.dst.group}] {call.dst.name}
					{call.visitor && <><br /><span style={{fontSize: '50%'}}>来訪者:</span> {call.visitor}</>}
					{call.responder && <><br /><span style={{fontSize: '50%'}}>対応者: {call.responder}</span></>}
				</Typography>
				<Box sx={{color: 'gray'}}>
					{diffTimeCalc(now, new Date(call.createdAt))}
				</Box>
			</Box>)}

		{os === 'iOS' && browser === 'Safari' && !isStandalone && <img style={{width:'100%'}} src="/images/install_for_ios.png" />}

		{os === 'Android' && (isStandalone || isInstalled) && <FormHelperText sx={{margin: 2, padding: 2, border: '1px solid gray', borderRadius: 5}}>
			<strong>通知が届かなくなった場合</strong><br />
			Androidの「使用していないアプリを管理する」機能により、しばらく起動しないと通知権限が自動で削除されることがあります。<br />
			「設定」>「アプリ」>「らいきゃくん通知」から「使用していないアプリを管理する」をオフにすることをおすすめします。
		</FormHelperText>}

		</>	}

		</main>
	)
}


const ServiceWorkerStatus: React.FC = () => {
	const [registrations, setRegistrations] = useState<string[]>(['確認中...']);
  
	useEffect(() => {
	  if ('serviceWorker' in navigator) {
		navigator.serviceWorker.getRegistrations().then(regs => {
		  if (regs.length === 0) {
			setRegistrations(['サービスワーカーは登録されていません。']);
		  } else {
			const swInfo = regs.map((reg, index) => 
			  `(${index + 1}) スコープ: ${reg.scope}, 状態: ${reg.active ? 'アクティブ' : '非アクティブ'}`
			);
			setRegistrations(swInfo);
		  }
		});
	  } else {
		setRegistrations(['このブラウザはサービスワーカーをサポートしていません。']);
	  }
	}, []);
  
	return (
	  <div>
		<h1>Service Workerの状態</h1>
		<ul>
		  {registrations.map((reg, index) => (
			<li key={index}>{reg}</li>
		  ))}
		</ul>
	  </div>
	);
  };