Skip to content

Commit 9fe19b6

Browse files
committed
Initial commit of state context
1 parent 85d7da8 commit 9fe19b6

3 files changed

Lines changed: 138 additions & 21 deletions

File tree

packages/component/src/Attachment/Text/private/ActivityCopyButton.tsx

Lines changed: 9 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
import { hooks } from 'botframework-webchat-api';
22
import { validateProps } from 'botframework-webchat-react-valibot';
33
import classNames from 'classnames';
4-
import React, { memo, useCallback, useEffect, useRef, useState } from 'react';
4+
import React, { memo, useCallback, useEffect, useRef } from 'react';
5+
import { wrapWith } from 'react-wrap-with';
56
import { instance, nullable, object, optional, pipe, readonly, string, type InferInput } from 'valibot';
67

78
import useStyleSet from '../../../hooks/useStyleSet';
9+
import ClipboardWritePermissionComposer, {
10+
useClipboardWritePermissionHooks
11+
} from '../../../providers/ClipboardWritePermission/ClipboardWritePermissionComposer';
812
import { useQueueStaticElement } from '../../../providers/LiveRegionTwin';
913
import refObject from '../../../types/internal/refObject';
1014
import ActivityButton from './ActivityButton';
@@ -23,11 +27,11 @@ type ActivityCopyButtonProps = InferInput<typeof activityCopyButtonPropsSchema>;
2327

2428
const COPY_ICON_URL = `data:image/svg+xml;utf8,${encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" width="21" height="20" viewBox="0 0 21 20" fill="none"><path d="M8.5 2C7.39543 2 6.5 2.89543 6.5 4V14C6.5 15.1046 7.39543 16 8.5 16H14.5C15.6046 16 16.5 15.1046 16.5 14V4C16.5 2.89543 15.6046 2 14.5 2H8.5ZM7.5 4C7.5 3.44772 7.94772 3 8.5 3H14.5C15.0523 3 15.5 3.44772 15.5 4V14C15.5 14.5523 15.0523 15 14.5 15H8.5C7.94772 15 7.5 14.5523 7.5 14V4ZM4.5 6.00001C4.5 5.25973 4.9022 4.61339 5.5 4.26758V14.5C5.5 15.8807 6.61929 17 8 17H14.2324C13.8866 17.5978 13.2403 18 12.5 18H8C6.067 18 4.5 16.433 4.5 14.5V6.00001Z" fill="#000000"/></svg>')}`;
2529

26-
const ActivityCopyButton = (props: ActivityCopyButtonProps) => {
30+
function ActivityCopyButton(props: ActivityCopyButtonProps) {
2731
const { className, targetRef } = validateProps(activityCopyButtonPropsSchema, props);
2832

2933
const [{ activityButton, activityCopyButton }] = useStyleSet();
30-
const [permissionGranted, setPermissionGranted] = useState(false);
34+
const [permissionGranted] = useClipboardWritePermissionHooks().usePermissionGranted();
3135
const [uiState] = useUIState();
3236
const buttonRef = useRef<HTMLButtonElement>(null);
3337
const localize = useLocalizer();
@@ -73,20 +77,6 @@ const ActivityCopyButton = (props: ActivityCopyButtonProps) => {
7377
queueStaticElement(<div className="webchat__activity-copy-button__copy-announcement">{copiedText}</div>);
7478
}, [buttonRef, copiedText, queueStaticElement, targetRef]);
7579

76-
useEffect(() => {
77-
let unmounted = false;
78-
79-
(async function () {
80-
if ((await navigator.permissions.query({ name: 'clipboard-write' as any })).state === 'granted') {
81-
unmounted || setPermissionGranted(true);
82-
}
83-
})();
84-
85-
return () => {
86-
unmounted = true;
87-
};
88-
}, [setPermissionGranted]);
89-
9080
return (
9181
<ActivityButton
9282
className={classNames(
@@ -106,9 +96,7 @@ const ActivityCopyButton = (props: ActivityCopyButtonProps) => {
10696
<span className="webchat__activity-copy-button__copied-text">{copiedText}</span>
10797
</ActivityButton>
10898
);
109-
};
110-
111-
ActivityCopyButton.displayName = 'ActivityCopyButton';
99+
}
112100

113-
export default memo(ActivityCopyButton);
101+
export default memo(wrapWith(ClipboardWritePermissionComposer)(ActivityCopyButton));
114102
export { activityCopyButtonPropsSchema, type ActivityCopyButtonProps };
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
// import { reactNode, validateProps } from 'botframework-webchat-react-valibot';
2+
import { validateProps } from 'botframework-webchat-api/internal';
3+
import React, { Fragment, memo, useCallback, useEffect, useMemo } from 'react';
4+
import { wrapWith } from 'react-wrap-with';
5+
import { object, optional, pipe, readonly, type InferInput } from 'valibot';
6+
7+
import reactNode from '../../types/internal/reactNode';
8+
import createStateContextWithHook from './private/createStateContextWithHook';
9+
10+
const clipboardWritePermissionComposerPropsSchema = pipe(
11+
object({
12+
children: optional(reactNode())
13+
}),
14+
readonly()
15+
);
16+
17+
type ClipboardWritePermissionComposerProps = InferInput<typeof clipboardWritePermissionComposerPropsSchema>;
18+
19+
const { Composer: PermissionGrantedComposer, useValue: useRawPermissionGranted } =
20+
createStateContextWithHook<boolean>(false);
21+
22+
function useClipboardWritePermissionHooks(): Readonly<{
23+
usePermissionGranted: () => readonly [boolean];
24+
}> {
25+
const [permissionGranted] = useRawPermissionGranted();
26+
27+
const usePermissionGranted = useCallback(() => Object.freeze([permissionGranted] as const), [permissionGranted]);
28+
29+
const hooks = useMemo(() => Object.freeze({ usePermissionGranted }), [usePermissionGranted]);
30+
31+
return hooks;
32+
}
33+
34+
function ClipboardWritePermissionComposer_(props: ClipboardWritePermissionComposerProps) {
35+
const { children } = validateProps(clipboardWritePermissionComposerPropsSchema, props);
36+
37+
const [_, setPermissionGranted] = useRawPermissionGranted();
38+
39+
useEffect(() => {
40+
let unmounted = false;
41+
42+
(async function () {
43+
if ((await navigator.permissions.query({ name: 'clipboard-write' as any })).state === 'granted') {
44+
unmounted || setPermissionGranted(true);
45+
}
46+
})();
47+
48+
return () => {
49+
unmounted = true;
50+
};
51+
}, [setPermissionGranted]);
52+
53+
return <Fragment>{children}</Fragment>;
54+
}
55+
56+
const ClipboardWritePermissionComposer = wrapWith(PermissionGrantedComposer)(memo(ClipboardWritePermissionComposer_));
57+
58+
export default memo(ClipboardWritePermissionComposer);
59+
export { useClipboardWritePermissionHooks };
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
// import { reactNode, validateProps } from 'botframework-webchat-react-valibot';
2+
import { validateProps } from 'botframework-webchat-api/internal';
3+
import React, {
4+
createContext,
5+
memo,
6+
useContext,
7+
useMemo,
8+
useState,
9+
type ComponentType,
10+
type Dispatch,
11+
type ReactNode,
12+
type SetStateAction
13+
} from 'react';
14+
import { object, optional, pipe, readonly, type InferInput } from 'valibot';
15+
import reactNode from '../../../types/internal/reactNode';
16+
17+
type GenericContextType<T> = Readonly<{
18+
valueState: readonly [T, Dispatch<SetStateAction<T>>];
19+
}>;
20+
21+
type GenericComposerProps<T> = Readonly<{
22+
children?: ReactNode | undefined;
23+
defaultValue: T;
24+
}>;
25+
26+
export default function createStateContextWithHook<T>(defaultValue: T): Readonly<{
27+
Composer: ComponentType<GenericComposerProps<T>>;
28+
useValue(): readonly [T, Dispatch<SetStateAction<T>>];
29+
'~types': {
30+
props: GenericComposerProps<T>;
31+
};
32+
}> {
33+
type ContextType = GenericContextType<T>;
34+
35+
const Context = createContext<ContextType>(
36+
new Proxy({} as ContextType, {
37+
get() {
38+
throw new Error('botframework-webchat: This hook can only be used under its corresponding context.');
39+
}
40+
})
41+
);
42+
43+
const composerPropsSchema = pipe(
44+
object({
45+
children: optional(reactNode())
46+
}),
47+
readonly()
48+
);
49+
50+
type ComposerProps = InferInput<typeof composerPropsSchema>;
51+
52+
function Composer(props: ComposerProps) {
53+
// const { children, defaultValue } = validateProps(composerPropsSchema, props);
54+
const { children } = validateProps(composerPropsSchema, props);
55+
56+
const valueState = useState<ContextType['valueState'][0]>(() => defaultValue as any);
57+
58+
const context = useMemo<ContextType>(() => Object.freeze({ valueState }), [valueState]);
59+
60+
return <Context.Provider value={context}>{children}</Context.Provider>;
61+
}
62+
63+
return Object.freeze({
64+
Composer: memo(Composer),
65+
useValue: () => useContext(Context).valueState,
66+
'~types': {
67+
props: {} as any
68+
}
69+
});
70+
}

0 commit comments

Comments
 (0)