* feat: add rrweb web-extension package * refactor: make the extension suitable for manifest v3 * update tsconfig.json * use version_name rather than recorder_version in manifest.json * update manifest.json * enable to keep recording after changing tabs * enable to record between tabs and urls * fix CI error * try to fix CI error * feat: add pause and resume buttons * feat: add a link to new session after recording * improve session list * refactor: migrate session storage from chrome local storage to indexedDB * feat: add pagination to session list * fix: multiple recorders are started after pausing and resuming process * fix: can't stop recording on firefox browser * update type import of 'eventWithTime' * fix CI error * doc: add readme * Apply suggestions from Justin's code review Co-authored-by: Justin Halsall <Juice10@users.noreply.github.com> * refactor: make use of webNavigation API to implement recording consistent during page navigation * fix firefox compatibility issue and add title to pages * add mouseleave listener to enhance the recording liability * fix firefox compatibility issue and improve the experience of recording resume after closing tabs * update tsconfig * upgrade vite-plugin-web-extension config to fix some bugs on facebook web page * update import links * refactor: cross tab recording mechanism apply Justin's suggestion * refactor: slipt util/index.ts into multiple files * implement cross-origin iframe recording * fix: regression of issue: ShadowHost can't be a string (issue 941) * refactor shadow dom recording to make tests cover key code * Apply formatting changes * increase the node memory limitation to avoid CI failure * Create lovely-pears-cross.md * Apply formatting changes * Update packages/web-extension/package.json * Update .changeset/lovely-pears-cross.md * update change logs * delete duplicated property --------- Co-authored-by: Justin Halsall <Juice10@users.noreply.github.com>
250 lines
8.3 KiB
TypeScript
250 lines
8.3 KiB
TypeScript
import { useState, useEffect } from 'react';
|
|
import {
|
|
Box,
|
|
Flex,
|
|
IconButton,
|
|
Link,
|
|
Spacer,
|
|
Stack,
|
|
Text,
|
|
} from '@chakra-ui/react';
|
|
import { FiSettings, FiList, FiPause, FiPlay } from 'react-icons/fi';
|
|
import Channel from '~/utils/channel';
|
|
import {
|
|
LocalData,
|
|
LocalDataKey,
|
|
RecorderStatus,
|
|
ServiceName,
|
|
RecordStartedMessage,
|
|
RecordStoppedMessage,
|
|
Session,
|
|
EventName,
|
|
} from '~/types';
|
|
import Browser from 'webextension-polyfill';
|
|
import { CircleButton } from '~/components/CircleButton';
|
|
import { Timer } from './Timer';
|
|
import { pauseRecording, resumeRecording } from '~/utils/recording';
|
|
import { saveSession } from '~/utils/storage';
|
|
const RECORD_BUTTON_SIZE = 3;
|
|
|
|
const channel = new Channel();
|
|
|
|
export function App() {
|
|
const [status, setStatus] = useState<RecorderStatus>(RecorderStatus.IDLE);
|
|
const [errorMessage, setErrorMessage] = useState('');
|
|
const [startTime, setStartTime] = useState(0);
|
|
const [newSession, setNewSession] = useState<Session | null>(null);
|
|
|
|
useEffect(() => {
|
|
void Browser.storage.local.get(LocalDataKey.recorderStatus).then((data) => {
|
|
const localData = data as LocalData;
|
|
if (!localData || !localData[LocalDataKey.recorderStatus]) return;
|
|
const { status, startTimestamp, pausedTimestamp } = localData[
|
|
LocalDataKey.recorderStatus
|
|
];
|
|
setStatus(status);
|
|
if (startTimestamp && pausedTimestamp)
|
|
setStartTime(Date.now() - pausedTimestamp + startTimestamp || 0);
|
|
else if (startTimestamp) setStartTime(startTimestamp);
|
|
});
|
|
}, []);
|
|
|
|
return (
|
|
<Flex direction="column" w={300} padding="5%">
|
|
<Flex>
|
|
<Text fontSize="md" fontWeight="bold">
|
|
RRWeb Recorder
|
|
</Text>
|
|
<Spacer />
|
|
<Stack direction="row">
|
|
<IconButton
|
|
onClick={() => {
|
|
void Browser.tabs.create({ url: '/pages/index.html#/' });
|
|
}}
|
|
size="xs"
|
|
icon={<FiList />}
|
|
aria-label={'Session List'}
|
|
title="Session List"
|
|
></IconButton>
|
|
<IconButton
|
|
onClick={() => {
|
|
void Browser.runtime.openOptionsPage();
|
|
}}
|
|
size="xs"
|
|
icon={<FiSettings />}
|
|
aria-label={'Settings button'}
|
|
title="Settings"
|
|
></IconButton>
|
|
</Stack>
|
|
</Flex>
|
|
{status !== RecorderStatus.IDLE && startTime && (
|
|
<Timer
|
|
startTime={startTime}
|
|
ticking={status === RecorderStatus.RECORDING}
|
|
/>
|
|
)}
|
|
<Flex justify="center" gap="10" mt="5" mb="5">
|
|
{[RecorderStatus.IDLE, RecorderStatus.RECORDING].includes(status) && (
|
|
<CircleButton
|
|
diameter={RECORD_BUTTON_SIZE}
|
|
title={
|
|
status === RecorderStatus.IDLE
|
|
? 'Start Recording'
|
|
: 'Stop Recording'
|
|
}
|
|
onClick={() => {
|
|
if (status === RecorderStatus.RECORDING) {
|
|
// stop recording
|
|
setErrorMessage('');
|
|
void channel.getCurrentTabId().then((tabId) => {
|
|
if (tabId === -1) return;
|
|
void channel
|
|
.requestToTab(tabId, ServiceName.StopRecord, {})
|
|
.then(async (res: RecordStoppedMessage) => {
|
|
if (!res) return;
|
|
|
|
setStatus(RecorderStatus.IDLE);
|
|
const status: LocalData[LocalDataKey.recorderStatus] = {
|
|
status: RecorderStatus.IDLE,
|
|
activeTabId: tabId,
|
|
};
|
|
await Browser.storage.local.set({
|
|
[LocalDataKey.recorderStatus]: status,
|
|
});
|
|
if (res.session) {
|
|
setNewSession(res.session);
|
|
await saveSession(res.session, res.events).catch(
|
|
(e) => {
|
|
setErrorMessage((e as { message: string }).message);
|
|
},
|
|
);
|
|
channel.emit(EventName.SessionUpdated, {});
|
|
}
|
|
})
|
|
.catch((error: Error) => {
|
|
setErrorMessage(error.message);
|
|
});
|
|
});
|
|
} else {
|
|
// start recording
|
|
void channel.getCurrentTabId().then((tabId) => {
|
|
if (tabId === -1) return;
|
|
void channel
|
|
.requestToTab(tabId, ServiceName.StartRecord, {})
|
|
.then(async (res: RecordStartedMessage | undefined) => {
|
|
if (res) {
|
|
setStatus(RecorderStatus.RECORDING);
|
|
setStartTime(res.startTimestamp);
|
|
const status: LocalData[LocalDataKey.recorderStatus] = {
|
|
status: RecorderStatus.RECORDING,
|
|
activeTabId: tabId,
|
|
startTimestamp: res.startTimestamp,
|
|
};
|
|
await Browser.storage.local.set({
|
|
[LocalDataKey.recorderStatus]: status,
|
|
});
|
|
}
|
|
})
|
|
.catch((error: Error) => {
|
|
setErrorMessage(error.message);
|
|
});
|
|
});
|
|
}
|
|
}}
|
|
>
|
|
<Box
|
|
w={`${RECORD_BUTTON_SIZE}rem`}
|
|
h={`${RECORD_BUTTON_SIZE}rem`}
|
|
borderRadius={status === RecorderStatus.IDLE ? 9999 : 6}
|
|
margin="0"
|
|
bgColor="red.500"
|
|
/>
|
|
</CircleButton>
|
|
)}
|
|
{status !== RecorderStatus.IDLE && (
|
|
<CircleButton
|
|
diameter={RECORD_BUTTON_SIZE}
|
|
title={
|
|
status === RecorderStatus.RECORDING
|
|
? 'Pause Recording'
|
|
: 'Resume Recording'
|
|
}
|
|
onClick={() => {
|
|
if (status === RecorderStatus.RECORDING) {
|
|
void pauseRecording(channel, RecorderStatus.PAUSED).then(
|
|
(result) => {
|
|
if (!result) return;
|
|
setStatus(result?.status.status);
|
|
},
|
|
);
|
|
} else {
|
|
void channel.getCurrentTabId().then((tabId) => {
|
|
if (tabId === -1) return;
|
|
resumeRecording(channel, tabId)
|
|
.then((statusData) => {
|
|
if (!statusData) return;
|
|
setStatus(statusData.status);
|
|
if (statusData.startTimestamp)
|
|
setStartTime(statusData.startTimestamp);
|
|
})
|
|
.catch((error: Error) => {
|
|
setErrorMessage(error.message);
|
|
});
|
|
});
|
|
}
|
|
}}
|
|
>
|
|
<Box
|
|
w={`${RECORD_BUTTON_SIZE}rem`}
|
|
h={`${RECORD_BUTTON_SIZE}rem`}
|
|
borderRadius={9999}
|
|
margin="0"
|
|
color="gray.600"
|
|
>
|
|
{[RecorderStatus.PAUSED, RecorderStatus.PausedSwitch].includes(
|
|
status,
|
|
) && (
|
|
<FiPlay
|
|
style={{
|
|
paddingLeft: '0.5rem',
|
|
width: '100%',
|
|
height: '100%',
|
|
}}
|
|
/>
|
|
)}
|
|
{status === RecorderStatus.RECORDING && (
|
|
<FiPause
|
|
style={{
|
|
width: '100%',
|
|
height: '100%',
|
|
}}
|
|
/>
|
|
)}
|
|
</Box>
|
|
</CircleButton>
|
|
)}
|
|
</Flex>
|
|
{newSession && (
|
|
<Text>
|
|
<Text as="b">New Session: </Text>
|
|
<Link
|
|
href={Browser.runtime.getURL(
|
|
`pages/index.html#/session/${newSession.id}`,
|
|
)}
|
|
isExternal
|
|
>
|
|
{newSession.name}
|
|
</Link>
|
|
</Text>
|
|
)}
|
|
{errorMessage !== '' && (
|
|
<Text color="red.500" fontSize="md">
|
|
{errorMessage}
|
|
<br />
|
|
Maybe refresh your current tab.
|
|
</Text>
|
|
)}
|
|
</Flex>
|
|
);
|
|
}
|