[251021] fix: VoiceInputOverlay API wating message
🕐 커밋 시간: 2025. 10. 21. 10:48:07 📊 변경 통계: • 총 파일: 7개 • 추가: +319줄 • 삭제: -53줄 📝 수정된 파일: ~ com.twin.app.shoptime/src/views/SearchPanel/SearchPanel.new.jsx ~ com.twin.app.shoptime/src/views/SearchPanel/TInput/TInput.jsx ~ com.twin.app.shoptime/src/views/SearchPanel/TInput/TInput.module.less ~ com.twin.app.shoptime/src/views/SearchPanel/VoiceInputOverlay/VoiceInputOverlay.jsx ~ com.twin.app.shoptime/src/views/SearchPanel/VoiceInputOverlay/VoiceInputOverlay.module.less ~ com.twin.app.shoptime/src/views/SearchPanel/VoiceInputOverlay/modes/VoiceResponse.jsx ~ com.twin.app.shoptime/src/views/SearchPanel/VoiceInputOverlay/modes/VoiceResponse.module.less 🔧 함수 변경 내용: 📄 com.twin.app.shoptime/src/views/SearchPanel/TInput/TInput.jsx (javascript): 🔄 Modified: Spottable() 📄 com.twin.app.shoptime/src/views/SearchPanel/VoiceInputOverlay/VoiceInputOverlay.jsx (javascript): 🔄 Modified: clearAllTimers() 📄 com.twin.app.shoptime/src/views/SearchPanel/VoiceInputOverlay/modes/VoiceResponse.jsx (javascript): ✅ Added: typeNextChar() 🔄 Modified: SpotlightContainerDecorator()
This commit is contained in:
@@ -330,7 +330,7 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
|
||||
// ShopperHouse 검색 결과가 들어왔을 때 TInput으로 포커스 이동
|
||||
const focusTimer = setTimeout(() => {
|
||||
Spotlight.focus(SPOTLIGHT_IDS.SEARCH_INPUT_BOX);
|
||||
}, 600); // VoiceInputOverlay 닫히는 시간(500ms) + 여유(100ms)
|
||||
}, 300); // VoiceInputOverlay 닫히는 시간(200ms) + 여유(100ms)
|
||||
|
||||
return () => {
|
||||
clearTimeout(focusTimer);
|
||||
@@ -661,6 +661,9 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
|
||||
onFocus={_onFocus}
|
||||
onBlur={_onBlur}
|
||||
onInputModeChange={handleInputModeChange}
|
||||
displayMode={false}
|
||||
allowModeToggle={true}
|
||||
placeholder="Search"
|
||||
/>
|
||||
</div>
|
||||
<SpottableMicButton
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { useCallback, useState } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { InputField } from '@enact/sandstone/Input';
|
||||
import Spotlight from '@enact/spotlight';
|
||||
@@ -29,6 +30,10 @@ export default function TInput({
|
||||
onFocus,
|
||||
onBlur,
|
||||
onInputModeChange, // 입력 모드 변경 콜백
|
||||
displayMode: initialDisplayMode = false, // Display Mode 활성화 여부
|
||||
allowModeToggle = true, // Enter/Click으로 모드 토글 허용 여부
|
||||
onModeChange, // 모드 변경 콜백 (display ↔ input)
|
||||
placeholder,
|
||||
...rest
|
||||
}) {
|
||||
const { handleScrollReset, handleStopScrolling } = useScrollReset(scrollTop);
|
||||
@@ -36,13 +41,49 @@ export default function TInput({
|
||||
// 입력 가능 모드 상태 (webOS 키보드가 뜨는 상태)
|
||||
const [isInputActive, setIsInputActive] = useState(false);
|
||||
|
||||
const handleContainerClick = useCallback((e) => {
|
||||
if (e.key === 'ArrrowLeft' || e.key === 'Left') {
|
||||
Spotlight.focus('spotlight_search');
|
||||
} else {
|
||||
Spotlight.focus('input-field-box');
|
||||
// 내부 모드 상태 (Display Mode = true, Input Mode = false)
|
||||
const [isDisplayMode, setIsDisplayMode] = useState(initialDisplayMode);
|
||||
|
||||
// Display → Input 모드 전환
|
||||
const switchToInputMode = useCallback(() => {
|
||||
if (!allowModeToggle) return;
|
||||
|
||||
console.log('[TInput] Display → Input 모드 전환');
|
||||
setIsDisplayMode(false);
|
||||
|
||||
// 부모 컴포넌트에 모드 변경 알림
|
||||
if (onModeChange) {
|
||||
onModeChange('input');
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Input에 포커스를 주어 키보드 활성화
|
||||
setTimeout(() => {
|
||||
const inputElement = document.querySelector('[data-spotlight-id="input-field-box"] input');
|
||||
if (inputElement) {
|
||||
inputElement.focus();
|
||||
}
|
||||
}, 100);
|
||||
}, [allowModeToggle, onModeChange]);
|
||||
|
||||
const handleContainerClick = useCallback(
|
||||
(e) => {
|
||||
// Display Mode에서 클릭 시 Input Mode로 전환
|
||||
if (isDisplayMode) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
switchToInputMode();
|
||||
return;
|
||||
}
|
||||
|
||||
// Input Mode에서는 기존 동작
|
||||
if (e.key === 'ArrrowLeft' || e.key === 'Left') {
|
||||
Spotlight.focus('spotlight_search');
|
||||
} else {
|
||||
Spotlight.focus('input-field-box');
|
||||
}
|
||||
},
|
||||
[isDisplayMode, switchToInputMode]
|
||||
);
|
||||
|
||||
const handleButtonKeyDown = useCallback((e) => {
|
||||
if (e.key === 'ArrowDown' || e.key === 'Down') {
|
||||
@@ -56,18 +97,35 @@ export default function TInput({
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleBoxKeyDown = useCallback((e) => {
|
||||
if (e.key === 'ArrowLeft' || e.key === 'Left') {
|
||||
const currentElement = e.currentTarget;
|
||||
const inputElement = currentElement.querySelector('input');
|
||||
|
||||
if (inputElement.value.length === 0) {
|
||||
const handleBoxKeyDown = useCallback(
|
||||
(e) => {
|
||||
// Display Mode에서 Enter 키 처리
|
||||
if (isDisplayMode && (e.key === 'Enter' || e.keyCode === 13)) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
Spotlight.focus('spotlight_search');
|
||||
switchToInputMode();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Input Mode에서 기존 동작
|
||||
if (e.key === 'ArrowLeft' || e.key === 'Left') {
|
||||
const currentElement = e.currentTarget;
|
||||
const inputElement = currentElement.querySelector('input');
|
||||
|
||||
if (inputElement && inputElement.value.length === 0) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
Spotlight.focus('spotlight_search');
|
||||
}
|
||||
}
|
||||
|
||||
// 부모 컴포넌트의 onKeyDown 호출
|
||||
if (onKeyDown) {
|
||||
onKeyDown(e);
|
||||
}
|
||||
},
|
||||
[isDisplayMode, switchToInputMode, onKeyDown]
|
||||
);
|
||||
|
||||
//focus,blur 추가
|
||||
const _onFocus = useCallback(() => {
|
||||
@@ -103,33 +161,49 @@ export default function TInput({
|
||||
}
|
||||
}, [onInputModeChange]);
|
||||
|
||||
console.log('[TInput Render] isInputActive:', isInputActive);
|
||||
console.log('[TInput Render] isDisplayMode:', isDisplayMode, 'isInputActive:', isInputActive);
|
||||
|
||||
return (
|
||||
<Container
|
||||
className={classNames(
|
||||
css.container,
|
||||
className ? className : null,
|
||||
isDisplayMode && css.displayMode,
|
||||
isInputActive && css.containerActive
|
||||
)}
|
||||
spotlightId={spotlightId}
|
||||
onClick={handleContainerClick}
|
||||
onKeyDown={handleBoxKeyDown}
|
||||
>
|
||||
<InputField
|
||||
{...rest}
|
||||
spotlightDisabled={spotlightDisabled}
|
||||
className={classNames(css.input, isInputActive && css.inputActive)}
|
||||
autoFocus
|
||||
onFocus={_onFocus}
|
||||
onBlur={_onBlur}
|
||||
onActivate={_onActivate}
|
||||
onDeactivate={_onDeactivate}
|
||||
onKeyDown={onKeyDown}
|
||||
spotlightId={'input-field-box'}
|
||||
aria-label="Keyword edit box"
|
||||
dismissOnEnter
|
||||
/>
|
||||
{isDisplayMode ? (
|
||||
// Display Mode: 읽기 전용 div
|
||||
<div
|
||||
className={classNames(css.input, css.displayModeText)}
|
||||
data-spotlight-id="input-display-box"
|
||||
data-placeholder={placeholder || ''}
|
||||
aria-label="Search text display (press Enter to edit)"
|
||||
aria-readonly="true"
|
||||
>
|
||||
{rest.value || ''}
|
||||
</div>
|
||||
) : (
|
||||
// Input Mode: 실제 InputField
|
||||
<InputField
|
||||
{...rest}
|
||||
spotlightDisabled={spotlightDisabled}
|
||||
className={classNames(css.input, isInputActive && css.inputActive)}
|
||||
autoFocus
|
||||
onFocus={_onFocus}
|
||||
onBlur={_onBlur}
|
||||
onActivate={_onActivate}
|
||||
onDeactivate={_onDeactivate}
|
||||
onKeyDown={handleBoxKeyDown}
|
||||
spotlightId={'input-field-box'}
|
||||
aria-label="Keyword edit box"
|
||||
dismissOnEnter
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
)}
|
||||
|
||||
{kind === 'withIcon' && (
|
||||
<TIconButton
|
||||
@@ -144,4 +218,24 @@ export default function TInput({
|
||||
);
|
||||
}
|
||||
|
||||
TInput.propTypes = {
|
||||
kind: PropTypes.string,
|
||||
icon: PropTypes.string,
|
||||
className: PropTypes.string,
|
||||
spotlightDisabled: PropTypes.bool,
|
||||
spotlightId: PropTypes.string,
|
||||
onKeyDown: PropTypes.func,
|
||||
scrollTop: PropTypes.number,
|
||||
onIconClick: PropTypes.func,
|
||||
forcedSpotlight: PropTypes.string,
|
||||
onFocus: PropTypes.func,
|
||||
onBlur: PropTypes.func,
|
||||
onInputModeChange: PropTypes.func,
|
||||
displayMode: PropTypes.bool,
|
||||
allowModeToggle: PropTypes.bool,
|
||||
onModeChange: PropTypes.func,
|
||||
placeholder: PropTypes.string,
|
||||
value: PropTypes.string,
|
||||
};
|
||||
|
||||
export { BORDER, COLOR, ICONS, KINDS };
|
||||
|
||||
@@ -71,4 +71,45 @@
|
||||
background-position: center bottom;
|
||||
}
|
||||
}
|
||||
|
||||
// Display Mode 스타일
|
||||
&.displayMode {
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.95;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: 2px solid #C70850;
|
||||
outline-offset: 2px;
|
||||
border-radius: 50px;
|
||||
}
|
||||
}
|
||||
|
||||
// Display Mode 텍스트 영역
|
||||
.displayModeText {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 15px 40px 15px 50px !important; // 위아래 패딩 축소 (20px → 15px)
|
||||
min-height: 90px; // 높이 축소 (100px → 90px)
|
||||
color: @COLOR_GRAY07 !important;
|
||||
font-family: @baseFont;
|
||||
font-size: 24px;
|
||||
font-weight: 700; // Bold 폰트
|
||||
line-height: 1.5;
|
||||
background-color: #fff !important;
|
||||
border-radius: 50px;
|
||||
border: none !important;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
// placeholder 효과 (텍스트가 없을 때)
|
||||
&:empty::before {
|
||||
content: attr(data-placeholder);
|
||||
color: rgba(136, 136, 136, 0.6); // @COLOR_GRAY07의 투명도 버전
|
||||
font-weight: 400; // placeholder는 normal weight
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -378,7 +378,7 @@ const VoiceInputOverlay = ({
|
||||
// 약간의 지연 후 닫기 (사용자가 결과를 인지할 수 있도록)
|
||||
closeTimerRef.current = setTimeout(() => {
|
||||
onClose();
|
||||
}, 500);
|
||||
}, 200);
|
||||
}
|
||||
|
||||
return () => {
|
||||
@@ -764,14 +764,24 @@ const VoiceInputOverlay = ({
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Input 창 포커스 핸들러 - VoiceInputOverlay 닫고 SearchPanel 화면 보이기
|
||||
const handleInputFocus = useCallback(() => {
|
||||
if (DEBUG_MODE) {
|
||||
console.log('⌨️ [DEBUG] handleInputFocus called - closing overlay to show SearchPanel');
|
||||
}
|
||||
// VoiceInputOverlay 닫기 - SearchPanel이 보이게 됨
|
||||
handleClose();
|
||||
}, [handleClose]);
|
||||
// Input 모드 변경 핸들러 - Input 모드로 전환되면 VoiceInputOverlay 닫기
|
||||
const handleInputModeChange = useCallback(
|
||||
(mode) => {
|
||||
if (DEBUG_MODE) {
|
||||
console.log('[VoiceInputOverlay] TInput 모드 변경:', mode);
|
||||
}
|
||||
|
||||
if (mode === 'input') {
|
||||
// Input 모드로 전환되면 Overlay 닫기
|
||||
// SearchPanel의 TInput으로 자연스럽게 전환됨
|
||||
if (DEBUG_MODE) {
|
||||
console.log('⌨️ [DEBUG] Input 모드로 전환됨 - VoiceInputOverlay 닫고 SearchPanel로 이동');
|
||||
}
|
||||
handleClose();
|
||||
}
|
||||
},
|
||||
[handleClose]
|
||||
);
|
||||
|
||||
// ⛔ TALK AGAIN 버튼 제거됨 - 더 이상 사용하지 않음
|
||||
// const handleTalkAgain = useCallback(() => { ... }, []);
|
||||
@@ -816,10 +826,12 @@ const VoiceInputOverlay = ({
|
||||
if (DEBUG_MODE) {
|
||||
console.log(
|
||||
'💬 [DEBUG][VoiceInputOverlay] MODE = RESPONSE | Rendering VoiceResponse with text:',
|
||||
sttResponseText
|
||||
sttResponseText,
|
||||
'isLoading:',
|
||||
!shopperHouseData
|
||||
);
|
||||
}
|
||||
return <VoiceResponse responseText={sttResponseText} />;
|
||||
return <VoiceResponse responseText={sttResponseText} isLoading={!shopperHouseData} />;
|
||||
case VOICE_MODES.NOINIT:
|
||||
if (DEBUG_MODE) {
|
||||
console.log('⚠️ [DEBUG][VoiceInputOverlay] MODE = NOINIT | Rendering VoiceNotRecognized');
|
||||
@@ -1151,7 +1163,10 @@ const VoiceInputOverlay = ({
|
||||
onKeyDown={handleInputKeyDown}
|
||||
onIconClick={handleSearchSubmit}
|
||||
spotlightId={INPUT_SPOTLIGHT_ID}
|
||||
onFocus={handleInputFocus}
|
||||
displayMode={true}
|
||||
allowModeToggle={true}
|
||||
onModeChange={handleInputModeChange}
|
||||
placeholder="Search"
|
||||
/>
|
||||
|
||||
{/* voiceVersion에 따라 하나의 마이크만 표시 */}
|
||||
|
||||
@@ -41,8 +41,8 @@
|
||||
// 입력창과 마이크 버튼 영역 - SearchPanel.inputContainer와 동일 (210px 높이)
|
||||
.inputWrapper {
|
||||
width: 100%;
|
||||
padding-top: 55px;
|
||||
padding-bottom: 55px;
|
||||
padding-top: 60px; // 위쪽 패딩 증가 (55px → 60px) - 포커스 테두리 여유 공간
|
||||
padding-bottom: 60px; // 아래쪽 패딩 증가 (55px → 60px) - 포커스 테두리 여유 공간
|
||||
padding-left: 60px;
|
||||
padding-right: 60px;
|
||||
display: flex;
|
||||
@@ -52,6 +52,7 @@
|
||||
z-index: 1003;
|
||||
position: relative;
|
||||
pointer-events: all; // 입력 영역은 클릭 가능
|
||||
overflow: visible; // 포커스 테두리가 잘리지 않도록
|
||||
|
||||
.searchInputWrapper {
|
||||
height: 100px;
|
||||
@@ -60,6 +61,8 @@
|
||||
align-items: center;
|
||||
min-height: 100px;
|
||||
max-height: 100px;
|
||||
overflow: visible; // 포커스 테두리가 잘리지 않도록
|
||||
padding: 5px 0; // 위아래 여유 공간
|
||||
|
||||
> * {
|
||||
margin-right: 15px;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// src/views/SearchPanel/VoiceInputOverlay/modes/VoiceResponse.jsx
|
||||
import React from 'react';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Spottable from '@enact/spotlight/Spottable';
|
||||
import SpotlightContainerDecorator from '@enact/spotlight/SpotlightContainerDecorator';
|
||||
@@ -15,7 +15,14 @@ const ResponseContainer = SpotlightContainerDecorator(
|
||||
'div'
|
||||
);
|
||||
|
||||
const VoiceResponse = ({ responseText = '', onTalkAgain }) => {
|
||||
const VoiceResponse = ({ responseText = '', isLoading = true }) => {
|
||||
// 타이핑 애니메이션 상태
|
||||
const [typedText, setTypedText] = useState('');
|
||||
const typingTimerRef = useRef(null);
|
||||
|
||||
const LOADING_MESSAGE = 'Just a moment, finding the best matches for you...';
|
||||
const TYPING_SPEED = 50; // ms per character
|
||||
|
||||
// 각 문장의 첫 글자를 대문자로 변환
|
||||
const capitalizeSentences = (text) => {
|
||||
if (!text || text.length === 0) return text;
|
||||
@@ -38,12 +45,63 @@ const VoiceResponse = ({ responseText = '', onTalkAgain }) => {
|
||||
|
||||
const displayText = capitalizeSentences(responseText);
|
||||
|
||||
// 타이핑 애니메이션 효과 (반복)
|
||||
useEffect(() => {
|
||||
if (!isLoading) {
|
||||
// 로딩이 끝나면 타이핑 애니메이션 정리
|
||||
if (typingTimerRef.current) {
|
||||
clearTimeout(typingTimerRef.current);
|
||||
typingTimerRef.current = null;
|
||||
}
|
||||
setTypedText('');
|
||||
return;
|
||||
}
|
||||
|
||||
// 로딩 중일 때 타이핑 애니메이션 시작 (반복)
|
||||
let currentIndex = 0;
|
||||
|
||||
const typeNextChar = () => {
|
||||
if (currentIndex < LOADING_MESSAGE.length) {
|
||||
setTypedText(LOADING_MESSAGE.substring(0, currentIndex + 1));
|
||||
currentIndex++;
|
||||
typingTimerRef.current = setTimeout(typeNextChar, TYPING_SPEED);
|
||||
} else {
|
||||
// 타이핑이 끝나면 1초 대기 후 처음부터 다시 시작
|
||||
typingTimerRef.current = setTimeout(() => {
|
||||
currentIndex = 0;
|
||||
setTypedText('');
|
||||
// 200ms 후 다시 타이핑 시작
|
||||
typingTimerRef.current = setTimeout(typeNextChar, 200);
|
||||
}, 1000);
|
||||
}
|
||||
};
|
||||
|
||||
// 초기화 및 시작
|
||||
setTypedText('');
|
||||
typingTimerRef.current = setTimeout(typeNextChar, 300); // 300ms 지연 후 시작
|
||||
|
||||
// Cleanup
|
||||
return () => {
|
||||
if (typingTimerRef.current) {
|
||||
clearTimeout(typingTimerRef.current);
|
||||
typingTimerRef.current = null;
|
||||
}
|
||||
};
|
||||
}, [isLoading]);
|
||||
|
||||
return (
|
||||
<ResponseContainer className={css.container} spotlightId="voice-response-container">
|
||||
<div className={css.responseContainer}>
|
||||
<SpottableBubble className={css.bubbleMessage} spotlightId="voice-response-text">
|
||||
<div className={css.bubbleText}>{displayText}</div>
|
||||
</SpottableBubble>
|
||||
{/* 입력받은 텍스트 (위쪽, 작게 표시) */}
|
||||
<div className={css.userQuery}>{displayText}</div>
|
||||
|
||||
{/* 로딩 메시지 (타이핑 애니메이션) */}
|
||||
{isLoading && (
|
||||
<div className={css.loadingMessage}>
|
||||
<div className={css.typingText}>{typedText}</div>
|
||||
<span className={css.cursor}>|</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</ResponseContainer>
|
||||
);
|
||||
@@ -51,12 +109,12 @@ const VoiceResponse = ({ responseText = '', onTalkAgain }) => {
|
||||
|
||||
VoiceResponse.propTypes = {
|
||||
responseText: PropTypes.string,
|
||||
onTalkAgain: PropTypes.func,
|
||||
isLoading: PropTypes.bool,
|
||||
};
|
||||
|
||||
VoiceResponse.defaultProps = {
|
||||
responseText: '',
|
||||
onTalkAgain: null,
|
||||
isLoading: true,
|
||||
};
|
||||
|
||||
export default VoiceResponse;
|
||||
|
||||
@@ -16,10 +16,60 @@
|
||||
.responseContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
padding: 0 40px;
|
||||
gap: 40px;
|
||||
}
|
||||
|
||||
// 사용자 입력 텍스트 (위쪽, 작게 표시)
|
||||
.userQuery {
|
||||
text-align: center;
|
||||
color: #ffffff;
|
||||
font-size: 32px;
|
||||
font-family: "LG Smart UI";
|
||||
font-weight: 700;
|
||||
line-height: 38px;
|
||||
margin-top: 20px;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
// 로딩 메시지 컨테이너
|
||||
.loadingMessage {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
// 타이핑 애니메이션 텍스트
|
||||
.typingText {
|
||||
color: rgba(234, 234, 234, 0.85);
|
||||
font-size: 34px;
|
||||
font-family: "LG Smart UI";
|
||||
font-weight: 400;
|
||||
line-height: 40px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
// 깜빡이는 커서
|
||||
.cursor {
|
||||
color: rgba(234, 234, 234, 0.85);
|
||||
font-size: 34px;
|
||||
font-family: "LG Smart UI";
|
||||
font-weight: 400;
|
||||
animation: blink 1s infinite;
|
||||
}
|
||||
|
||||
@keyframes blink {
|
||||
0%, 49% {
|
||||
opacity: 1;
|
||||
}
|
||||
50%, 100% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.talkAgainButton {
|
||||
|
||||
Reference in New Issue
Block a user