feat: manually mirror opencoze's code from bytedance
Change-Id: I09a73aadda978ad9511264a756b2ce51f5761adf
This commit is contained in:
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* 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 MutableRefObject,
|
||||
useEffect,
|
||||
useState,
|
||||
type RefObject,
|
||||
} from 'react';
|
||||
|
||||
import { defer } from 'lodash-es';
|
||||
import { type GrabPosition } from '@coze-common/text-grab';
|
||||
|
||||
import { type MenuListRef } from '../custom-components/menu-list';
|
||||
|
||||
export const useAutoGetMaxPosition = ({
|
||||
position,
|
||||
messageRef,
|
||||
floatMenuRef,
|
||||
}: {
|
||||
position: GrabPosition | null;
|
||||
messageRef: MutableRefObject<Element | null>;
|
||||
floatMenuRef: RefObject<MenuListRef>;
|
||||
}) => {
|
||||
const [maxPositionX, setMaxPositionX] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
const maxX = messageRef.current?.getBoundingClientRect().right ?? 0;
|
||||
setMaxPositionX(maxX);
|
||||
defer(() => floatMenuRef.current?.refreshOpacity());
|
||||
}, [position, messageRef.current, floatMenuRef.current]);
|
||||
|
||||
return { maxPositionX };
|
||||
};
|
||||
@@ -0,0 +1,240 @@
|
||||
/*
|
||||
* 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, useRef, useState } from 'react';
|
||||
|
||||
import { defer, delay } from 'lodash-es';
|
||||
import {
|
||||
CONTENT_ATTRIBUTE_NAME,
|
||||
getAncestorAttributeNode,
|
||||
type GrabPosition,
|
||||
} from '@coze-common/text-grab';
|
||||
import { NO_MESSAGE_ID_MARK } from '@coze-common/chat-uikit';
|
||||
import { useEventCallback } from '@coze-common/chat-hooks';
|
||||
import {
|
||||
type OnLinkElementContext,
|
||||
type useWriteablePlugin,
|
||||
} from '@coze-common/chat-area';
|
||||
|
||||
import { getMouseNearbyRect } from '../utils/get-mouse-nearby-rect';
|
||||
import {
|
||||
EventNames,
|
||||
type GrabPluginBizContext,
|
||||
} from '../types/plugin-biz-context';
|
||||
import { type MenuListRef } from '../custom-components/menu-list';
|
||||
|
||||
const DELAY_DISAPPEAR_TIME = 100;
|
||||
const TIMEOUT = 100;
|
||||
// eslint-disable-next-line max-lines-per-function, @coze-arch/max-line-per-function
|
||||
export const useFloatMenuListener = ({
|
||||
plugin,
|
||||
}: {
|
||||
plugin: ReturnType<typeof useWriteablePlugin<GrabPluginBizContext>>;
|
||||
}) => {
|
||||
const floatMenuRef = useRef<MenuListRef>(null);
|
||||
|
||||
const [visible, setVisible] = useState(false);
|
||||
|
||||
const targetElement = useRef<HTMLElement | null>(null);
|
||||
const targetInfo = useRef<{
|
||||
source: string;
|
||||
type: 'image' | 'link';
|
||||
text: string;
|
||||
} | null>(null);
|
||||
|
||||
/**
|
||||
* 是否在 Scrolling 中
|
||||
*/
|
||||
const [isScrolling, setIsScrolling] = useState(false);
|
||||
|
||||
/**
|
||||
* Scrolling 计时器
|
||||
*/
|
||||
const scrollingTimer = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
const { pluginBizContext } = plugin;
|
||||
const { eventCenter } = pluginBizContext;
|
||||
const { on, off } = eventCenter;
|
||||
|
||||
const isMouseInMenu = useRef(false);
|
||||
const [position, setPosition] = useState<GrabPosition | null>(null);
|
||||
const timer = useRef<number | null>(null);
|
||||
|
||||
const mouseInfo = useRef<GrabPosition>({ x: 0, y: 0 });
|
||||
|
||||
const handleMenuMouseEnter = useEventCallback(() => {
|
||||
isMouseInMenu.current = true;
|
||||
if (timer.current) {
|
||||
clearTimeout(timer.current);
|
||||
}
|
||||
});
|
||||
|
||||
const handleMenuMouseLeave = useEventCallback(() => {
|
||||
isMouseInMenu.current = false;
|
||||
handleCardLinkElementMouseLeave();
|
||||
});
|
||||
|
||||
const isMessageFinished = () => {
|
||||
const target = getAncestorAttributeNode(
|
||||
targetElement.current,
|
||||
CONTENT_ATTRIBUTE_NAME,
|
||||
);
|
||||
|
||||
const messageId = target?.attributes.getNamedItem(
|
||||
CONTENT_ATTRIBUTE_NAME,
|
||||
)?.value;
|
||||
|
||||
const isSpecialMessage = messageId === NO_MESSAGE_ID_MARK;
|
||||
|
||||
if (isSpecialMessage) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!messageId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const { is_finish } =
|
||||
plugin.chatAreaPluginContext.readonlyAPI.message.findMessage(messageId) ??
|
||||
{};
|
||||
|
||||
return is_finish;
|
||||
};
|
||||
|
||||
const handleCardLinkElementMouseEnter = useEventCallback(
|
||||
(ctx: OnLinkElementContext & { type: 'link' | 'image' }) => {
|
||||
if (timer.current) {
|
||||
clearTimeout(timer.current);
|
||||
timer.current = null;
|
||||
}
|
||||
targetElement.current = ctx.element;
|
||||
targetInfo.current = {
|
||||
source: ctx.link,
|
||||
type: ctx.type,
|
||||
text: ctx.element.textContent ?? '',
|
||||
};
|
||||
|
||||
const isFinished = isMessageFinished();
|
||||
|
||||
if (!isFinished) {
|
||||
return;
|
||||
}
|
||||
|
||||
setVisible(true);
|
||||
handleViewScroll();
|
||||
|
||||
defer(() => {
|
||||
floatMenuRef.current?.refreshOpacity();
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
const handleCardLinkElementMouseLeave = useEventCallback(() => {
|
||||
timer.current = delay(() => {
|
||||
if (isMouseInMenu.current) {
|
||||
return;
|
||||
}
|
||||
targetElement.current = null;
|
||||
setVisible(false);
|
||||
}, DELAY_DISAPPEAR_TIME);
|
||||
});
|
||||
|
||||
const handleViewScroll = useEventCallback(() => {
|
||||
const menuRef = floatMenuRef.current?.getRef();
|
||||
|
||||
if (
|
||||
!targetElement.current ||
|
||||
!menuRef ||
|
||||
!menuRef.current ||
|
||||
!targetInfo.current
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const target = targetElement.current;
|
||||
const targetRect = target.getBoundingClientRect();
|
||||
const menuRect = menuRef.current.getBoundingClientRect();
|
||||
let x = 0;
|
||||
let y = 0;
|
||||
if (targetInfo.current.type === 'image') {
|
||||
x = targetRect.right;
|
||||
y = targetRect.bottom - targetRect.height / 2 - menuRect.height / 2;
|
||||
} else {
|
||||
const targetRects = target.getClientRects();
|
||||
|
||||
const nearbyRect =
|
||||
getMouseNearbyRect(Array.from(targetRects), mouseInfo.current) ??
|
||||
targetRect;
|
||||
|
||||
x = mouseInfo.current.x;
|
||||
y = nearbyRect.bottom;
|
||||
}
|
||||
|
||||
setPosition({ x, y });
|
||||
});
|
||||
|
||||
const handleMouseMove = useEventCallback((event: MouseEvent) => {
|
||||
const [x, y] = [event.clientX, event.clientY];
|
||||
mouseInfo.current = { x, y };
|
||||
});
|
||||
|
||||
const handleSmartScreenChange = useEventCallback(() => {
|
||||
if (scrollingTimer.current) {
|
||||
clearTimeout(scrollingTimer.current);
|
||||
}
|
||||
|
||||
setIsScrolling(true);
|
||||
scrollingTimer.current = setTimeout(() => {
|
||||
handleViewScroll();
|
||||
setIsScrolling(false);
|
||||
}, TIMEOUT);
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
on(EventNames.OnLinkElementMouseEnter, handleCardLinkElementMouseEnter);
|
||||
on(EventNames.OnLinkElementMouseLeave, handleCardLinkElementMouseLeave);
|
||||
on(EventNames.OnViewScroll, handleSmartScreenChange);
|
||||
on(EventNames.OnMessageUpdate, handleSmartScreenChange);
|
||||
window.addEventListener('mousemove', handleMouseMove);
|
||||
window.addEventListener('resize', handleSmartScreenChange);
|
||||
window.addEventListener('wheel', handleSmartScreenChange);
|
||||
window.addEventListener('scroll', handleSmartScreenChange);
|
||||
|
||||
return () => {
|
||||
off(EventNames.OnLinkElementMouseEnter, handleCardLinkElementMouseEnter);
|
||||
off(EventNames.OnLinkElementMouseLeave, handleCardLinkElementMouseLeave);
|
||||
off(EventNames.OnViewScroll, handleSmartScreenChange);
|
||||
off(EventNames.OnMessageUpdate, handleSmartScreenChange);
|
||||
window.removeEventListener('mousemove', handleMouseMove);
|
||||
window.removeEventListener('resize', handleSmartScreenChange);
|
||||
window.removeEventListener('wheel', handleSmartScreenChange);
|
||||
window.removeEventListener('scroll', handleSmartScreenChange);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return {
|
||||
handleMenuMouseEnter,
|
||||
handleMenuMouseLeave,
|
||||
targetElement,
|
||||
targetInfo,
|
||||
position,
|
||||
floatMenuRef,
|
||||
visible,
|
||||
setVisible,
|
||||
computePosition: handleSmartScreenChange,
|
||||
isScrolling,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* 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 { useMemo, useState } from 'react';
|
||||
|
||||
import { type MessageSource } from '@coze-common/chat-area';
|
||||
|
||||
import { type Scene, createGrabPlugin } from '../create';
|
||||
|
||||
interface Params {
|
||||
onQuote?: ({
|
||||
botId,
|
||||
source,
|
||||
}: {
|
||||
botId: string;
|
||||
source: MessageSource | undefined;
|
||||
}) => void;
|
||||
// 目前只需要区分出 store 的场景
|
||||
scene?: Scene;
|
||||
}
|
||||
export const useCreateGrabPlugin = (params?: Params) => {
|
||||
const { onQuote, scene = 'other' } = params ?? {};
|
||||
const [grabEnableUpload, setGrabEnableUpload] = useState(true);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention -- 符合预期的命名
|
||||
const { grabPlugin: GrabPlugin, grabPluginId } = useMemo(
|
||||
() =>
|
||||
createGrabPlugin({
|
||||
preference: {
|
||||
enableGrab: true,
|
||||
},
|
||||
onQuote,
|
||||
onQuoteChange: ({ isEmpty }) => {
|
||||
setGrabEnableUpload(isEmpty);
|
||||
},
|
||||
scene,
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
||||
return { grabEnableUpload, GrabPlugin, grabPluginId };
|
||||
};
|
||||
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
* 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 MutableRefObject, useEffect, useRef, useState } from 'react';
|
||||
|
||||
import {
|
||||
CONTENT_ATTRIBUTE_NAME,
|
||||
MESSAGE_SOURCE_ATTRIBUTE_NAME,
|
||||
getAncestorAttributeNode,
|
||||
} from '@coze-common/text-grab';
|
||||
|
||||
import { FILTER_MESSAGE_SOURCE } from '../constants/filter-message';
|
||||
|
||||
interface HideQuoteProps<T> {
|
||||
containerRef?: MutableRefObject<T | null>;
|
||||
}
|
||||
|
||||
export const useHideQuote = <T extends Element>(props?: HideQuoteProps<T>) => {
|
||||
const containerRef = useRef<T | null>(null);
|
||||
const targetRef = useRef<Element | null>(null);
|
||||
|
||||
const usedContainerRef = props?.containerRef?.current
|
||||
? props.containerRef
|
||||
: containerRef;
|
||||
|
||||
const [forceHidden, setForceHidden] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const target = getAncestorAttributeNode(
|
||||
usedContainerRef.current,
|
||||
CONTENT_ATTRIBUTE_NAME,
|
||||
);
|
||||
|
||||
const messageSource = target?.attributes.getNamedItem(
|
||||
MESSAGE_SOURCE_ATTRIBUTE_NAME,
|
||||
)?.value;
|
||||
|
||||
if (FILTER_MESSAGE_SOURCE.includes(Number(messageSource))) {
|
||||
setForceHidden(true);
|
||||
}
|
||||
|
||||
targetRef.current = target;
|
||||
|
||||
return () => {
|
||||
setForceHidden(false);
|
||||
targetRef.current = null;
|
||||
};
|
||||
}, [usedContainerRef.current]);
|
||||
|
||||
return { targetRef, containerRef: usedContainerRef, forceHidden };
|
||||
};
|
||||
Reference in New Issue
Block a user