feat: manually mirror opencoze's code from bytedance
Change-Id: I09a73aadda978ad9511264a756b2ce51f5761adf
This commit is contained in:
@@ -0,0 +1,210 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type RefObject, useEffect, useRef } from 'react';
|
||||
|
||||
import { useLatest, useKeyPress } from 'ahooks';
|
||||
import {
|
||||
type AudioRecordEvents,
|
||||
type AudioRecordOptions,
|
||||
} from '@coze-common/chat-uikit-shared';
|
||||
import { useEventCallback } from '@coze-common/chat-hooks';
|
||||
|
||||
type EventType = MouseEvent | TouchEvent | KeyboardEvent;
|
||||
|
||||
export interface UseAudioRecordInteractionProps {
|
||||
target: RefObject<HTMLElement>;
|
||||
events: AudioRecordEvents;
|
||||
options?: AudioRecordOptions;
|
||||
}
|
||||
|
||||
const touchSupported: boolean = 'ontouchstart' in window;
|
||||
|
||||
const isTouchEvent = (eventType: unknown): eventType is TouchEvent =>
|
||||
'TouchEvent' in window && eventType instanceof TouchEvent;
|
||||
|
||||
const getClientPosition = (event: TouchEvent | MouseEvent) => {
|
||||
if (isTouchEvent(event)) {
|
||||
return {
|
||||
clientX: event.touches[0]?.clientX ?? 0,
|
||||
clientY: event.touches[0]?.clientY ?? 0,
|
||||
};
|
||||
}
|
||||
return event;
|
||||
};
|
||||
|
||||
// eslint-disable-next-line max-lines-per-function, @coze-arch/max-line-per-function
|
||||
export const useAudioRecordInteraction = ({
|
||||
target,
|
||||
events,
|
||||
options = {},
|
||||
}: UseAudioRecordInteractionProps) => {
|
||||
const { onStart, onEnd, onMoveEnter, onMoveLeave } = events;
|
||||
const {
|
||||
shortcutKey = () => false,
|
||||
getIsShortcutKeyDisabled,
|
||||
enabled = true,
|
||||
getActiveZoneTarget,
|
||||
} = options;
|
||||
const onStartRef = useLatest(onStart);
|
||||
const onEndRef = useLatest(onEnd);
|
||||
const onMoveEnterRef = useLatest(onMoveEnter);
|
||||
const onMoveLeaveRef = useLatest(onMoveLeave);
|
||||
const isKeydown = useRef(false);
|
||||
const isMouseOrTouchDown = useRef(false);
|
||||
const isMoveLeave = useRef(false);
|
||||
|
||||
const onKeyDown = (eventType: KeyboardEvent) => {
|
||||
if (!enabled) {
|
||||
return;
|
||||
}
|
||||
if (getIsShortcutKeyDisabled?.()) {
|
||||
return onKeyUp(eventType);
|
||||
}
|
||||
if (isKeydown.current) {
|
||||
return;
|
||||
}
|
||||
isKeydown.current = true;
|
||||
onStartRef.current?.(eventType);
|
||||
};
|
||||
|
||||
const onKeyUp = (eventType: KeyboardEvent | undefined) => {
|
||||
if (!enabled) {
|
||||
return;
|
||||
}
|
||||
if (!isKeydown.current) {
|
||||
return;
|
||||
}
|
||||
isKeydown.current = false;
|
||||
onEndRef.current?.(eventType);
|
||||
};
|
||||
|
||||
const onPointerMove = useEventCallback(
|
||||
(eventType: TouchEvent | MouseEvent) => {
|
||||
eventType.preventDefault();
|
||||
const activeZoneElement = getActiveZoneTarget?.() || target.current;
|
||||
|
||||
if (!isMouseOrTouchDown.current || !activeZoneElement) {
|
||||
return;
|
||||
}
|
||||
const { clientX, clientY } = getClientPosition(eventType);
|
||||
// 获取元素的边界信息
|
||||
const rect = activeZoneElement.getBoundingClientRect();
|
||||
const isOutRect =
|
||||
clientX < rect.left ||
|
||||
clientX > rect.right ||
|
||||
clientY < rect.top ||
|
||||
clientY > rect.bottom;
|
||||
|
||||
// 判断触摸点是否在元素范围内
|
||||
if (isOutRect && !isMoveLeave.current) {
|
||||
isMoveLeave.current = true;
|
||||
onMoveLeaveRef.current?.();
|
||||
}
|
||||
|
||||
if (!isOutRect && isMoveLeave.current) {
|
||||
isMoveLeave.current = false;
|
||||
onMoveEnterRef.current?.();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
const onClickOrTouchStart = useEventCallback((eventType: EventType) => {
|
||||
isMoveLeave.current = false;
|
||||
|
||||
if (isMouseOrTouchDown.current) {
|
||||
return;
|
||||
}
|
||||
isMouseOrTouchDown.current = true;
|
||||
|
||||
if (isTouchEvent(eventType)) {
|
||||
eventType.preventDefault();
|
||||
document.addEventListener('touchmove', onPointerMove);
|
||||
} else {
|
||||
document.addEventListener('mousemove', onPointerMove);
|
||||
}
|
||||
|
||||
onStartRef.current?.(eventType);
|
||||
});
|
||||
|
||||
const onClickOrTouchEnd = useEventCallback((eventType: EventType) => {
|
||||
if (!isMouseOrTouchDown.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
document.removeEventListener('mousemove', onPointerMove);
|
||||
document.removeEventListener('touchmove', onPointerMove);
|
||||
|
||||
isMouseOrTouchDown.current = false;
|
||||
onEndRef.current?.(eventType);
|
||||
});
|
||||
|
||||
useKeyPress(shortcutKey, onKeyDown, {
|
||||
exactMatch: true,
|
||||
events: ['keydown'],
|
||||
});
|
||||
|
||||
useKeyPress(shortcutKey, onKeyUp, {
|
||||
exactMatch: false,
|
||||
events: ['keyup'],
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const onWindowBlur = () => {
|
||||
onKeyUp(undefined);
|
||||
};
|
||||
window.addEventListener('blur', onWindowBlur);
|
||||
return () => {
|
||||
window.removeEventListener('blur', onWindowBlur);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const element = target.current;
|
||||
|
||||
if (!element || !enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!touchSupported) {
|
||||
element.addEventListener('mousedown', onClickOrTouchStart);
|
||||
// ! caution document
|
||||
document.addEventListener('mouseup', onClickOrTouchEnd);
|
||||
} else {
|
||||
element.addEventListener('touchstart', onClickOrTouchStart);
|
||||
element.addEventListener('touchend', onClickOrTouchEnd);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (!touchSupported) {
|
||||
element.removeEventListener('mousedown', onClickOrTouchStart);
|
||||
// ! caution document
|
||||
document.removeEventListener('mouseup', onClickOrTouchEnd);
|
||||
} else {
|
||||
element.removeEventListener('touchstart', onClickOrTouchStart);
|
||||
element.removeEventListener('touchend', onClickOrTouchEnd);
|
||||
}
|
||||
};
|
||||
});
|
||||
useEffect(
|
||||
() => () => {
|
||||
// 避免异常情况下事件无法卸载
|
||||
document.removeEventListener('mousemove', onPointerMove);
|
||||
document.removeEventListener('touchmove', onPointerMove);
|
||||
},
|
||||
[],
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { useEffect, type RefObject } from 'react';
|
||||
|
||||
import { useDebounceFn } from 'ahooks';
|
||||
import {
|
||||
UIKitEvents,
|
||||
useUiKitEventCenter,
|
||||
} from '@coze-common/chat-uikit-shared';
|
||||
|
||||
export const useObserveCardContainer = ({
|
||||
messageId,
|
||||
cardContainerRef,
|
||||
onResize,
|
||||
}: {
|
||||
messageId: string | null;
|
||||
cardContainerRef: RefObject<HTMLDivElement>;
|
||||
onResize: () => void;
|
||||
}) => {
|
||||
const eventCenter = useUiKitEventCenter();
|
||||
|
||||
/** 30s 内没变化则自动清除 observer */
|
||||
const debouncedDisconnect = useDebounceFn(
|
||||
(getResizeObserver: () => ResizeObserver | null) => {
|
||||
const resizeObserver = getResizeObserver();
|
||||
resizeObserver?.disconnect();
|
||||
},
|
||||
{
|
||||
wait: 30000,
|
||||
},
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!eventCenter) {
|
||||
return;
|
||||
}
|
||||
|
||||
let resizeObserver: ResizeObserver | null = null;
|
||||
|
||||
const onAfterCardRender = ({
|
||||
messageId: renderCardMessageId,
|
||||
}: {
|
||||
messageId: string;
|
||||
}) => {
|
||||
if (!cardContainerRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (renderCardMessageId !== messageId) {
|
||||
return;
|
||||
}
|
||||
|
||||
resizeObserver = new ResizeObserver(() => {
|
||||
debouncedDisconnect.run(() => resizeObserver);
|
||||
onResize();
|
||||
});
|
||||
|
||||
resizeObserver.observe(cardContainerRef.current);
|
||||
};
|
||||
|
||||
eventCenter.on(UIKitEvents.AFTER_CARD_RENDER, onAfterCardRender);
|
||||
|
||||
return () => {
|
||||
eventCenter.off(UIKitEvents.AFTER_CARD_RENDER, onAfterCardRender);
|
||||
resizeObserver?.disconnect();
|
||||
};
|
||||
}, []);
|
||||
};
|
||||
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { useState } from 'react';
|
||||
|
||||
import type { LocalCacheKey, StoreStruct } from '../utils/local-cache/type';
|
||||
import { useLocalCache } from '../context/local-cache';
|
||||
|
||||
export const useStateWithLocalCache = <K extends LocalCacheKey>(
|
||||
key: K,
|
||||
init: StoreStruct[K],
|
||||
) => {
|
||||
const { readLocalStoreValue, writeLocalStoreValue } = useLocalCache();
|
||||
const readVal = readLocalStoreValue(key, init);
|
||||
const [state, setState] = useState(readVal);
|
||||
return {
|
||||
state,
|
||||
setState: (val: StoreStruct[K]) => {
|
||||
setState(val);
|
||||
writeLocalStoreValue(key, val);
|
||||
},
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user