Skip to content

Commit

Permalink
Add option to notify user when processing is complete
Browse files Browse the repository at this point in the history
  • Loading branch information
bvaughn committed Jun 23, 2024
1 parent c88885f commit 07f5ba3
Show file tree
Hide file tree
Showing 6 changed files with 220 additions and 104 deletions.
60 changes: 60 additions & 0 deletions packages/replay-next/src/hooks/useNotification.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { useCallback, useEffect, useState } from "react";

export type Permission = NotificationPermission | PermissionState;
export type RequestPermission = () => Promise<boolean>;

export function useNotification(): {
permission: Permission;
requested: boolean;
requestPermission: RequestPermission;
supported: boolean;
} {
const [permission, setPermission] = useState<Permission>(Notification.permission);

useEffect(() => {
let permissionStatus: PermissionStatus;

const onChange = () => {
if (permissionStatus) {
setPermission(permissionStatus.state);
}
};

(async () => {
permissionStatus = await navigator.permissions.query({ name: "notifications" });
permissionStatus.addEventListener("change", onChange);
})();

return () => {
if (permissionStatus) {
permissionStatus.removeEventListener("change", onChange);
}
};
}, []);

const [requested, setRequested] = useState(false);

const requestPermission = useCallback(async () => {
if (!supported) {
return false;
}

setRequested(true);

let permission = Notification.permission;
if (permission !== "granted") {
permission = await Notification.requestPermission();
}

return permission === "granted";
}, []);

return {
permission,
requestPermission,
requested,
supported,
};
}

const supported = typeof window !== "undefined" && "Notification" in window;
19 changes: 19 additions & 0 deletions src/ui/components/DevToolsProcessingScreen.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
.SecondaryMessage {
color: var(--color-dim);
}

.NotifyMessage {
border: 1px solid var(--checkbox-border);
padding: 0.5rem 01rem;
border-radius: 2rem;
font-size: var(--font-size-regular);
cursor: pointer;
}
.NotifyMessage[data-disabled] {
opacity: 0.5;
cursor: not-allowed;
}

.Checkbox {
border-radius: 0.25rem;
}
83 changes: 79 additions & 4 deletions src/ui/components/DevToolsProcessingScreen.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,42 @@
import { useEffect, useState } from "react";
import { ChangeEvent, ReactNode, useEffect, useRef, useState } from "react";

// eslint-disable-next-line no-restricted-imports
import { client as protocolClient } from "protocol/socket";
import { useNotification } from "replay-next/src/hooks/useNotification";
import LoadingScreen from "ui/components/shared/LoadingScreen";
import { useGetRecording, useGetRecordingId } from "ui/hooks/recordings";
import { getProcessingProgress } from "ui/reducers/app";
import { useAppSelector } from "ui/setup/hooks";
import { useAppSelector, useAppStore } from "ui/setup/hooks";
import { formatEstimatedProcessingDuration } from "ui/utils/formatEstimatedProcessingDuration";

import styles from "./DevToolsProcessingScreen.module.css";

const SHOW_STALLED_MESSAGE_AFTER_MS = 30_000;

export function DevToolsProcessingScreen() {
const recordingId = useGetRecordingId();
const { recording } = useGetRecording(recordingId);

const store = useAppStore();

const {
permission: notificationPermission,
requestPermission: requestNotificationPermission,
requested: requestedNotificationPermission,
supported: notificationSupported,
} = useNotification();

// Sync latest permission values to a ref so we they can be checked during an unmount
const notificationPermissionStateRef = useRef({
permission: notificationPermission,
permissionsRequested: requestedNotificationPermission,
});
useEffect(() => {
const current = notificationPermissionStateRef.current;
current.permission = notificationPermission;
current.permissionsRequested = requestedNotificationPermission;
});

const processingProgress = useAppSelector(getProcessingProgress);

const [showStalledMessage, setShowStalledMessage] = useState(false);
Expand All @@ -30,8 +55,20 @@ export function DevToolsProcessingScreen() {
}
}, [processingProgress]);

useEffect(() => {
return () => {
const { permission, permissionsRequested } = notificationPermissionStateRef.current;

Check failure on line 60 in src/ui/components/DevToolsProcessingScreen.tsx

View workflow job for this annotation

GitHub Actions / Trunk Check

eslint(react-hooks/exhaustive-deps)

[new] The ref value 'notificationPermissionStateRef.current' will likely have changed by the time this effect cleanup function runs. If this ref points to a node rendered by React, copy 'notificationPermissionStateRef.current' to a variable inside the effect, and use that variable in the cleanup function.
if (permissionsRequested && permission === "granted" && recording) {
const progress = getProcessingProgress(store.getState());
if (progress === 100) {
new Notification(`"${recording.title}" has loaded`);
}
}
};
}, [protocolClient, recording, store]);

Check failure on line 68 in src/ui/components/DevToolsProcessingScreen.tsx

View workflow job for this annotation

GitHub Actions / Trunk Check

eslint(react-hooks/exhaustive-deps)

[new] React Hook useEffect has an unnecessary dependency: 'protocolClient'. Either exclude it or remove the dependency array. Outer scope values like 'protocolClient' aren't valid dependencies because mutating them doesn't re-render the component.

let message = "Processing...";
let secondaryMessage =
let secondaryMessage: ReactNode =
"This could take a while, depending on the complexity and length of the replay.";

if (showStalledMessage) {
Expand All @@ -45,5 +82,43 @@ export function DevToolsProcessingScreen() {
message = `Processing... (${Math.round(processingProgress)}%)`;
}

return <LoadingScreen message={message} secondaryMessage={secondaryMessage} />;
const onChange = async (event: ChangeEvent<HTMLInputElement>) => {
if (event.target.checked) {
const granted = await requestNotificationPermission();
if (!granted) {
event.target.checked = false;
event.target.blur();
}
}
};

return (
<LoadingScreen
message={
<>
<div className={styles.Message}>{message}</div>
<div className={styles.SecondaryMessage}>{secondaryMessage}</div>
{notificationSupported && (
<label
className={styles.NotifyMessage}
data-disabled={notificationPermission === "denied" || undefined}
title={
notificationPermission === "denied"
? "Notifications have been disabled in browser settings"
: undefined
}
>
<input
disabled={notificationPermission === "denied"}
className={styles.Checkbox}
onChange={onChange}
type="checkbox"
/>{" "}
Notify me when this tab loads
</label>
)}
</>
}
/>
);
}
55 changes: 18 additions & 37 deletions src/ui/components/shared/LoadingScreen.module.css
Original file line number Diff line number Diff line change
@@ -1,55 +1,36 @@
.loadingScreenWrapper {
.LoadingScreen {
width: 100%;
height: 100%;
position: relative;
display: flex;
width: 24rem;
flex-direction: column;
}

.hoverboardWrapper {
height: 8rem;
width: 8rem;
cursor: pointer;
}

.messageWrapper {
font-size: 0.875rem;
align-items: center;
text-align: center;
padding: 1rem;
gap: 1rem;
font-size: 0.875rem;
}

.messageWrapper a {
.LoadingScreen a {
text-decoration: underline;
}

.messageWrapper a:hover {
.LoadingScreen a:hover {
color: var(--primary-accent);
}

.message {
margin-bottom: 1rem;
}

.secondaryMessage {
color: var(--color-dim);
width: 18rem;
}

.viewportWrapper {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
padding: 1rem;
margin: 0.5rem;
border-radius: 0.5rem;
.Hoverboard {
height: 8rem;
width: 8rem;
}

.HighRiskWarning {
width: 40ch;
padding: 1rem 2rem;
border-radius: 1rem;
margin: 3rem 0;
padding: 0.5rem;
border-radius: 0.5rem;
text-align: center;
font-size: var(--font-size-regular);
background-color: var(--background-color-high-risk-setting);
color: var(--color-high-risk-setting);
}

.Spacer {
flex-grow: 1;
}
103 changes: 41 additions & 62 deletions src/ui/components/shared/LoadingScreen.tsx
Original file line number Diff line number Diff line change
@@ -1,86 +1,65 @@
import dynamic from "next/dynamic";
import { ReactNode, useCallback, useEffect, useState } from "react";
import { ConnectedProps, connect } from "react-redux";
import { ReactNode, useEffect, useState } from "react";

import { useHighRiskSettingCount } from "shared/user-data/GraphQL/useHighRiskSettingCount";
import { RecordingDocumentTitle } from "ui/components/RecordingDocumentTitle";
import { getAwaitingSourcemaps, getUploading } from "ui/reducers/app";
import { UIState } from "ui/state";
import { useAppSelector } from "ui/setup/hooks";

import { DefaultViewportWrapper } from "./Viewport";
import styles from "./LoadingScreen.module.css";

const colorOptions: Array<"blue" | "green" | "red"> = ["blue", "green", "red"];
export default function LoadingScreen({ message }: { message: ReactNode }) {
const isAwaitingSourceMaps = useAppSelector(getAwaitingSourcemaps);
const uploadingInfo = useAppSelector(getUploading);

const Hoverboard = dynamic(() => import("./Hoverboard"), {
ssr: false,
loading: () => <div />,
});
const showHighRiskWarning = useHighRiskSettingCount() > 0;

export function LoadingScreenTemplate({ children }: { children?: ReactNode }) {
const [hoverboardColor, setHoverboardColor] = useState(colorOptions[2]);

const changeHoverboardColor = useCallback(() => {
const randomIndex = Math.floor(Math.random() * colorOptions.length);
setHoverboardColor(colorOptions[randomIndex]);
}, []);
const [colorIndex, setColorIndex] = useState(0);
const color = colorOptions[colorIndex % colorOptions.length];

useEffect(() => {
const timeoutId = setInterval(changeHoverboardColor, 5000);
return () => clearInterval(timeoutId);
}, [changeHoverboardColor]);
const timeoutId = setInterval(() => {
setColorIndex(prevIndex => prevIndex + 1);
}, 5_000);

return (
<div className={styles.loadingScreenWrapper}>
<DefaultViewportWrapper>
<div className={styles.loadingScreenWrapper}>
<div className="flex flex-col items-center space-y-2">
<div className={styles.hoverboardWrapper} onClick={changeHoverboardColor}>
<Hoverboard color={hoverboardColor} />
</div>
{children}
</div>
</div>
</DefaultViewportWrapper>
</div>
);
}
return () => clearInterval(timeoutId);
}, []);

function LoadingScreen({
uploading,
awaitingSourcemaps,
message,
secondaryMessage,
}: PropsFromRedux & { message: string; secondaryMessage?: string }) {
const waitingForMessage =
awaitingSourcemaps || uploading ? (
<span>Uploading {Math.round(uploading?.amount ? Number(uploading.amount) : 0)}Mb</span>
) : (
<>
<div className={styles.message} dangerouslySetInnerHTML={{ __html: message }} />
{secondaryMessage && <div className={styles.secondaryMessage}>{secondaryMessage}</div>}
</>
let content: ReactNode;
if (isAwaitingSourceMaps || uploadingInfo) {
content = (
<span>
Uploading {Math.round(uploadingInfo?.amount ? Number(uploadingInfo.amount) : 0)}Mb
</span>
);

const showHighRiskWarning = useHighRiskSettingCount() > 0;
} else {
content = message;
}

return (
<LoadingScreenTemplate>
<>
<RecordingDocumentTitle />
<div className={styles.messageWrapper}>{waitingForMessage}</div>
{showHighRiskWarning && (
<div className={styles.HighRiskWarning}>
You have advanced settings enabled that may negatively affect performance
<DefaultViewportWrapper className={styles.LoadingScreen}>
<div className={styles.Spacer}></div>
<div className={styles.Hoverboard}>
<Hoverboard color={color} />
</div>
)}
</LoadingScreenTemplate>
{content}
<div className={styles.Spacer}></div>
{showHighRiskWarning && (
<div className={styles.HighRiskWarning}>
You have advanced settings enabled that may negatively affect performance
</div>
)}
</DefaultViewportWrapper>
</>
);
}

const connector = connect((state: UIState) => ({
uploading: getUploading(state),
awaitingSourcemaps: getAwaitingSourcemaps(state),
}));
type PropsFromRedux = ConnectedProps<typeof connector>;
const colorOptions: Array<"blue" | "green" | "red"> = ["blue", "green", "red"];

export default connector(LoadingScreen);
const Hoverboard = dynamic(() => import("./Hoverboard"), {
ssr: false,
loading: () => <div />,
});
Loading

0 comments on commit 07f5ba3

Please sign in to comment.