Files
pezkuwi-ui/packages/react-qr/src/Display.tsx
T
Tarik Gul caa0ceea23 2025 (#822)
* 2025

* update License
2025-01-03 01:07:08 +02:00

154 lines
4.0 KiB
TypeScript

// Copyright 2017-2025 @polkadot/react-qr authors & contributors
// SPDX-License-Identifier: Apache-2.0
import React, { useEffect, useMemo, useRef, useState } from 'react';
import { objectSpread } from '@polkadot/util';
import { xxhashAsHex } from '@polkadot/util-crypto';
import { qrcode } from './qrcode.js';
import { styled } from './styled.js';
import { createFrames, createImgSize } from './util.js';
interface Props {
className?: string | undefined;
size?: string | number | undefined;
skipEncoding?: boolean;
style?: React.CSSProperties | undefined;
timerDelay?: number | undefined;
value: Uint8Array;
}
interface FrameState {
frames: Uint8Array[];
frameIdx: number;
image: string | null;
valueHash: string | null;
}
interface TimerState {
timerDelay: number;
timerId: ReturnType<typeof setTimeout> | null;
}
const DEFAULT_FRAME_DELAY = 2750;
const TIMER_INC = 500;
function getDataUrl (value: Uint8Array): string {
const qr = qrcode(0, 'M');
// HACK See our qrcode stringToBytes override as used internally. This
// will only work for the case where we actually pass `Bytes` in here
qr.addData(value as unknown as string, 'Byte');
qr.make();
return qr.createDataURL(16, 0);
}
function Display ({ className = '', size, skipEncoding, style = {}, timerDelay = DEFAULT_FRAME_DELAY, value }: Props): React.ReactElement<Props> | null {
const [{ image }, setFrameState] = useState<FrameState>({ frameIdx: 0, frames: [], image: null, valueHash: null });
const timerRef = useRef<TimerState>({ timerDelay, timerId: null });
const containerStyle = useMemo(
() => createImgSize(size),
[size]
);
// run on initial load to setup the global timer and provide and unsubscribe
useEffect((): () => void => {
const nextFrame = () => setFrameState((state): FrameState => {
// when we have a single frame, we only ever fire once
if (state.frames.length <= 1) {
return state;
}
let frameIdx = state.frameIdx + 1;
// when we overflow, skip to the first and slightly increase the delay between frames
if (frameIdx === state.frames.length) {
frameIdx = 0;
timerRef.current.timerDelay = timerRef.current.timerDelay + TIMER_INC;
}
// only encode the frames on demand, not above as part of the
// state derivation - in the case of large payloads, this should
// be slightly more responsive on initial load
const newState = objectSpread<FrameState>({}, state, {
frameIdx,
image: getDataUrl(state.frames[frameIdx])
});
// set the new timer last
timerRef.current.timerId = setTimeout(nextFrame, timerRef.current.timerDelay);
return newState;
});
timerRef.current.timerId = setTimeout(nextFrame, timerRef.current.timerDelay);
return (): void => {
// eslint-disable-next-line react-hooks/exhaustive-deps
timerRef.current.timerId && clearTimeout(timerRef.current.timerId);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect((): void => {
setFrameState((state): FrameState => {
const valueHash = xxhashAsHex(value);
if (valueHash === state.valueHash) {
return state;
}
const frames: Uint8Array[] = skipEncoding
? [value]
: createFrames(value);
// encode on demand
return {
frameIdx: 0,
frames,
image: getDataUrl(frames[0]),
valueHash
};
});
}, [skipEncoding, value]);
if (!image) {
return null;
}
return (
<StyledDiv
className={className}
style={containerStyle}
>
<div
className='ui--qr-Display'
style={style}
>
<img src={image} />
</div>
</StyledDiv>
);
}
const StyledDiv = styled.div`
.ui--qr-Display {
height: 100%;
width: 100%;
img,
svg {
background: white;
height: auto !important;
max-height: 100%;
max-width: 100%;
width: auto !important;
}
}
`;
export const QrDisplay = React.memo(Display);