mirror of
https://github.com/duckietm/Nitro-V3.git
synced 2026-06-19 23:16:21 +00:00
feat(chat): improve command autocomplete and command alerts
This commit is contained in:
+68
-5
@@ -1,5 +1,5 @@
|
||||
import { FC, useState } from 'react';
|
||||
import { LocalizeText, NotificationAlertItem, NotificationAlertType, OpenUrl } from '../../../../api';
|
||||
import { FC, useMemo, useState } from 'react';
|
||||
import { DispatchUiEvent, LocalizeText, NotificationAlertItem, NotificationAlertType, OpenUrl, RoomWidgetUpdateChatInputContentEvent } from '../../../../api';
|
||||
import { Button, Column, Flex, LayoutNotificationAlertView, LayoutNotificationAlertViewProps } from '../../../../common';
|
||||
|
||||
interface NotificationDefaultAlertViewProps extends LayoutNotificationAlertViewProps
|
||||
@@ -7,11 +7,57 @@ interface NotificationDefaultAlertViewProps extends LayoutNotificationAlertViewP
|
||||
item: NotificationAlertItem;
|
||||
}
|
||||
|
||||
const COMMAND_LINE_PATTERN = /^\s*:[\w.-]+(?:\s.*)?$/;
|
||||
|
||||
interface CommandTemplateEntry
|
||||
{
|
||||
command: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export const NotificationDefaultAlertView: FC<NotificationDefaultAlertViewProps> = props =>
|
||||
{
|
||||
const { item = null, title = ((props.item && props.item.title) || ''), onClose = null, ...rest } = props;
|
||||
const { item = null, title = ((props.item && props.item.title) || ''), onClose = null, classNames = [], ...rest } = props;
|
||||
const [ imageFailed, setImageFailed ] = useState<boolean>(false);
|
||||
|
||||
const alertLines = useMemo(() => item.messages.flatMap(message => message.split(/\r\n|\r|\n/g)), [ item.messages ]);
|
||||
const hasCommandTemplate = useMemo(() =>
|
||||
{
|
||||
const commandLines = alertLines.filter(line => COMMAND_LINE_PATTERN.test(line));
|
||||
|
||||
return commandLines.length >= 4 || alertLines.some(line => /^Your Commands\(\d+\):?/i.test(line.trim()));
|
||||
}, [ alertLines ]);
|
||||
const commandTemplateContent = useMemo(() =>
|
||||
{
|
||||
const intro: string[] = [];
|
||||
const commands: CommandTemplateEntry[] = [];
|
||||
|
||||
for(const rawLine of alertLines)
|
||||
{
|
||||
const text = rawLine.trim();
|
||||
|
||||
if(!text.length) continue;
|
||||
|
||||
if(COMMAND_LINE_PATTERN.test(text))
|
||||
{
|
||||
commands.push({ command: text, description: '' });
|
||||
continue;
|
||||
}
|
||||
|
||||
if(commands.length)
|
||||
{
|
||||
const lastCommand = commands[commands.length - 1];
|
||||
|
||||
lastCommand.description = lastCommand.description ? `${ lastCommand.description } ${ text }` : text;
|
||||
continue;
|
||||
}
|
||||
|
||||
intro.push(text);
|
||||
}
|
||||
|
||||
return { intro, commands };
|
||||
}, [ alertLines ]);
|
||||
|
||||
const visitUrl = () =>
|
||||
{
|
||||
OpenUrl(item.clickUrl);
|
||||
@@ -19,10 +65,18 @@ export const NotificationDefaultAlertView: FC<NotificationDefaultAlertViewProps>
|
||||
onClose();
|
||||
};
|
||||
|
||||
const copyCommandToChatInput = (command: string) =>
|
||||
{
|
||||
const chatValue = command.endsWith(' ') ? command : `${ command } `;
|
||||
|
||||
DispatchUiEvent(new RoomWidgetUpdateChatInputContentEvent(RoomWidgetUpdateChatInputContentEvent.TEXT, chatValue));
|
||||
};
|
||||
|
||||
const hasFrank = (item.alertType === NotificationAlertType.DEFAULT);
|
||||
const alertClassNames = hasCommandTemplate ? [ ...classNames, 'nitro-alert-command-list' ] : classNames;
|
||||
|
||||
return (
|
||||
<LayoutNotificationAlertView title={ title } onClose={ onClose } { ...rest } type={ hasFrank ? NotificationAlertType.DEFAULT : item.alertType }>
|
||||
<LayoutNotificationAlertView title={ title } onClose={ onClose } classNames={ alertClassNames } { ...rest } type={ hasFrank ? NotificationAlertType.DEFAULT : item.alertType }>
|
||||
<Flex fullHeight gap={ hasFrank || (item.imageUrl && !imageFailed) ? 2 : 0 } overflow="auto">
|
||||
{ hasFrank && !item.imageUrl && <div className="notification-frank shrink-0" /> }
|
||||
{ item.imageUrl && !imageFailed && <img alt={ item.title } className="align-self-baseline" src={ item.imageUrl } onError={ () =>
|
||||
@@ -30,7 +84,16 @@ export const NotificationDefaultAlertView: FC<NotificationDefaultAlertViewProps>
|
||||
setImageFailed(true);
|
||||
} } /> }
|
||||
<div className={ [ 'notification-text overflow-y-auto flex flex-col w-full', (item.clickUrl && !hasFrank) ? 'justify-center' : '' ].join(' ') }>
|
||||
{ (item.messages.length > 0) && item.messages.map((message, index) =>
|
||||
{ hasCommandTemplate && <div className="notification-command-template">
|
||||
{ commandTemplateContent.intro.map((text, index) =>
|
||||
<div key={ index } className={ index === 0 ? 'notification-command-heading' : 'notification-command-copy' }>{ text }</div>) }
|
||||
{ commandTemplateContent.commands.map((entry, index) =>
|
||||
<button key={ `${ entry.command }-${ index }` } className="notification-command-row" type="button" onClick={ () => copyCommandToChatInput(entry.command) }>
|
||||
<span className="notification-command-name">{ entry.command }</span>
|
||||
{ entry.description && <span className="notification-command-description">{ entry.description }</span> }
|
||||
</button>) }
|
||||
</div> }
|
||||
{ !hasCommandTemplate && (item.messages.length > 0) && item.messages.map((message, index) =>
|
||||
{
|
||||
const htmlText = message.replace(/\r\n|\r|\n/g, '<br />');
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { FC, useEffect, useRef } from 'react';
|
||||
import { CommandDefinition } from '../../../../api';
|
||||
import type { CommandDefinition } from '../../../../api';
|
||||
import type { RankedCommandDefinition } from '../../../../hooks/rooms/widgets/useChatCommandSelector.helpers';
|
||||
|
||||
interface ChatInputCommandSelectorViewProps
|
||||
{
|
||||
commands: CommandDefinition[];
|
||||
commands: RankedCommandDefinition[];
|
||||
selectedIndex: number;
|
||||
onSelect: (command: CommandDefinition) => void;
|
||||
onHover: (index: number) => void;
|
||||
@@ -24,17 +25,18 @@ export const ChatInputCommandSelectorView: FC<ChatInputCommandSelectorViewProps>
|
||||
}, [ selectedIndex ]);
|
||||
|
||||
return (
|
||||
<div ref={ listRef } className="absolute bottom-full left-0 w-full bg-[#e8e8e8] border-2 border-black border-b-0 rounded-t-lg max-h-[240px] overflow-y-auto z-[1070]">
|
||||
<div ref={ listRef } className="absolute bottom-full left-0 z-[1070] max-h-[238px] w-full overflow-y-auto rounded-t-[8px] border-2 border-b-0 border-black bg-[#f2f2eb] shadow-[0_-4px_14px_rgba(0,0,0,0.22)]">
|
||||
{ commands.map((cmd, index) => (
|
||||
<div
|
||||
<button
|
||||
key={ cmd.key }
|
||||
className={ `px-3 py-1.5 cursor-pointer text-sm flex items-center gap-2 ${ index === selectedIndex ? 'bg-[#283F5D] text-white' : 'hover:bg-gray-300' }` }
|
||||
className={ `flex min-h-[34px] w-full cursor-pointer items-center gap-2 border-b border-[#c6c6bd] px-3 py-1.5 text-left last:border-b-0 ${ index === selectedIndex ? 'bg-[#255d72] text-white' : 'text-black hover:bg-[#dceaf0]' }` }
|
||||
type="button"
|
||||
onClick={ () => onSelect(cmd) }
|
||||
onMouseEnter={ () => onHover(index) }
|
||||
>
|
||||
<span className="font-bold">:{ cmd.key }</span>
|
||||
<span className={ `text-xs ${ index === selectedIndex ? 'text-gray-300' : 'text-gray-500' }` }>{ cmd.description }</span>
|
||||
</div>
|
||||
<span className={ `shrink-0 rounded-[4px] border px-1.5 py-[1px] font-bold ${ index === selectedIndex ? 'border-white/60 bg-white/15' : 'border-[#8ca6b1] bg-white text-[#123b4c]' }` }>:{ cmd.key }</span>
|
||||
<span className={ `min-w-0 flex-1 truncate text-[12px] ${ index === selectedIndex ? 'text-white/85' : 'text-[#525252]' }` }>{ cmd.description }</span>
|
||||
</button>
|
||||
)) }
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -8,7 +8,7 @@ import { ChatInputCommandSelectorView } from './ChatInputCommandSelectorView';
|
||||
import { ChatInputEmojiSelectorView } from './ChatInputEmojiSelectorView';
|
||||
import { ChatInputStyleSelectorView } from './ChatInputStyleSelectorView';
|
||||
|
||||
export const ChatInputView: FC<{}> = props =>
|
||||
export const ChatInputView: FC = () =>
|
||||
{
|
||||
const [ chatValue, setChatValue ] = useState<string>('');
|
||||
const { chatStyleId = 0, updateChatStyleId = null } = useSessionInfo();
|
||||
@@ -42,6 +42,23 @@ export const ChatInputView: FC<{}> = props =>
|
||||
inputRef.current.setSelectionRange((inputRef.current.value.length * 2), (inputRef.current.value.length * 2));
|
||||
}, [ inputRef ]);
|
||||
|
||||
const setChatInputValue = useCallback((value: string, markTyping: boolean = true) =>
|
||||
{
|
||||
setChatValue(value);
|
||||
|
||||
if(markTyping)
|
||||
{
|
||||
setIsTyping(!!value.length);
|
||||
setIsIdle(!!value.length);
|
||||
}
|
||||
|
||||
requestAnimationFrame(() =>
|
||||
{
|
||||
inputRef.current?.focus();
|
||||
inputRef.current?.setSelectionRange(value.length, value.length);
|
||||
});
|
||||
}, [ setIsTyping, setIsIdle ]);
|
||||
|
||||
const checkSpecialKeywordForInput = useCallback(() =>
|
||||
{
|
||||
setChatValue(prevValue =>
|
||||
@@ -157,7 +174,7 @@ export const ChatInputView: FC<{}> = props =>
|
||||
if(selected)
|
||||
{
|
||||
event.preventDefault();
|
||||
setChatValue(':' + selected.key + ' ');
|
||||
setChatInputValue(':' + selected.key + ' ');
|
||||
return;
|
||||
}
|
||||
break;
|
||||
@@ -194,12 +211,15 @@ export const ChatInputView: FC<{}> = props =>
|
||||
return;
|
||||
}
|
||||
|
||||
}, [ floodBlocked, inputRef, chatModeIdWhisper, anotherInputHasFocus, setInputFocus, checkSpecialKeywordForInput, sendChatValue, commandSelectorVisible, moveUp, moveDown, selectCurrent, closeCommandSelector ]);
|
||||
}, [ floodBlocked, inputRef, chatModeIdWhisper, anotherInputHasFocus, setInputFocus, checkSpecialKeywordForInput, sendChatValue, commandSelectorVisible, moveUp, moveDown, selectCurrent, setChatInputValue, closeCommandSelector ]);
|
||||
|
||||
useUiEvent<RoomWidgetUpdateChatInputContentEvent>(RoomWidgetUpdateChatInputContentEvent.CHAT_INPUT_CONTENT, event =>
|
||||
{
|
||||
switch(event.chatMode)
|
||||
{
|
||||
case RoomWidgetUpdateChatInputContentEvent.TEXT:
|
||||
setChatInputValue(event.userName);
|
||||
return;
|
||||
case RoomWidgetUpdateChatInputContentEvent.WHISPER: {
|
||||
setChatValue(`${ chatModeIdWhisper } ${ event.userName } `);
|
||||
return;
|
||||
@@ -286,7 +306,7 @@ export const ChatInputView: FC<{}> = props =>
|
||||
selectedIndex={ selectedIndex }
|
||||
onSelect={ (cmd) =>
|
||||
{
|
||||
setChatValue(':' + cmd.key + ' '); inputRef.current?.focus();
|
||||
setChatInputValue(':' + cmd.key + ' ');
|
||||
} }
|
||||
onHover={ setSelectedIndex }
|
||||
/> }
|
||||
|
||||
Reference in New Issue
Block a user