Skip to content

Commit

Permalink
fix: Fix rare mobile bug causing out of window scrolls and remove med…
Browse files Browse the repository at this point in the history
…ia controls from notifications tray
  • Loading branch information
tjtanjin committed Mar 21, 2024
1 parent 03f72f4 commit 51c27cf
Show file tree
Hide file tree
Showing 3 changed files with 100 additions and 31 deletions.
19 changes: 15 additions & 4 deletions src/components/ChatBotBody/ChatBotBody.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,6 @@ const ChatBotBody = ({
}

if (botOptions.chatWindow?.autoJumpToBottom || !isScrolling) {
console.log(!isScrolling);
chatBodyRef.current.scrollTop = chatBodyRef.current.scrollHeight;
if (botOptions.isOpen) {
setUnreadCount(0);
Expand All @@ -111,7 +110,14 @@ const ChatBotBody = ({
setUnreadCount(0);
}
}
}, [chatBodyRef.current?.scrollHeight, isScrolling]);
}, [chatBodyRef.current?.scrollHeight]);

// sets unread count to 0 if not scrolling
useEffect(() => {
if (!isScrolling) {
setUnreadCount(0);
}
}, [isScrolling]);

/**
* Checks and updates whether a user is scrolling in chat window.
Expand All @@ -120,11 +126,16 @@ const ChatBotBody = ({
if (!chatBodyRef.current) {
return;
}

const { scrollTop, clientHeight, scrollHeight } = chatBodyRef.current;
setIsScrolling(
scrollTop + clientHeight < scrollHeight - (botOptions.chatWindow?.messagePromptOffset || 30)
);

// workaround to ensure user never truly scrolls to bottom by introducing a 1 pixel offset
// this is necessary to prevent unexpected scroll behaviors of the chat window when user reaches the bottom
if (!isScrolling && scrollTop + clientHeight >= scrollHeight - 1) {
chatBodyRef.current.scrollTop = scrollHeight - clientHeight - 1;
}
};

/**
Expand All @@ -150,7 +161,7 @@ const ChatBotBody = ({
</>
);
};

/**
* Renders message from the bot.
*
Expand Down
45 changes: 37 additions & 8 deletions src/components/ChatBotBody/ChatMessagePrompt/ChatMessagePrompt.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,15 +52,44 @@ const ChatMessagePrompt = ({
};

/**
* Handles scrolling to the bottom of the chat window.
* Handles scrolling to the bottom of the chat window with specified duration.
*/
const scrollToBottom = () => {
if (chatBodyRef.current) {
chatBodyRef.current.scrollTo({
top: chatBodyRef.current.scrollHeight,
behavior: "smooth"
});
function scrollToBottom(duration: number) {
if (!chatBodyRef.current) {
return;
}

const start = chatBodyRef.current.scrollTop;
const end = chatBodyRef.current.scrollHeight - chatBodyRef.current.clientHeight;
const change = end - start;
const increment = 20;
let currentTime = 0;

function animateScroll() {
if (!chatBodyRef.current) {
return;
}
currentTime += increment;
const val = easeInOutQuad(currentTime, start, change, duration);
chatBodyRef.current.scrollTop = val;
if (currentTime < duration) {
requestAnimationFrame(animateScroll);
} else {
setIsScrolling(false);
}
}

animateScroll();
}

/**
* Helper function for custom scrolling.
*/
const easeInOutQuad = (t: number, b: number, c: number, d: number) => {
t /= d / 2;
if (t < 1) return c / 2 * t * t + b;
t--;
return -c / 2 * (t * (t - 2) - 1) + b;
};

/**
Expand All @@ -82,7 +111,7 @@ const ChatMessagePrompt = ({
style={isHovered ? chatMessagePromptHoveredStyle : botOptions.chatMessagePromptStyle}
onMouseDown={(event: MouseEvent) => {
event.preventDefault();
scrollToBottom();
scrollToBottom(600);
}}
className="rcb-message-prompt-text"
>
Expand Down
67 changes: 48 additions & 19 deletions src/components/ChatBotContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,9 @@ const ChatBotContainer = ({ flow }: { flow: Flow }) => {
const keepVoiceOnRef = useRef<boolean>(false);

// audio to play for notifications
const notificationAudio = useRef<HTMLAudioElement>();
const audioContextRef = useRef<AudioContext | null>(null);
const audioBufferRef = useRef<AudioBuffer>();
const gainNodeRef = useRef<AudioNode | null>(null);

// tracks if user has interacted with page
const [hasInteracted, setHasInteracted] = useState<boolean>(false);
Expand Down Expand Up @@ -265,25 +267,30 @@ const ChatBotContainer = ({ flow }: { flow: Flow }) => {
/**
* Sets up the notifications feature (initial toggle status and sound).
*/
const setUpNotifications = () => {
const setUpNotifications = async () => {
setNotificationToggledOn(botOptions.notification?.defaultToggledOn as boolean);

let notificationSound = botOptions.notification?.sound;

Check failure on line 273 in src/components/ChatBotContainer.tsx

View workflow job for this annotation

GitHub Actions / lint

'notificationSound' is never reassigned. Use 'const' instead
audioContextRef.current = new AudioContext();
const gainNode = audioContextRef.current.createGain();
gainNode.gain.value = botOptions.notification?.volume || 0.2;
gainNodeRef.current = gainNode;

// convert data uri to url if it is base64, true in production
let audioSource;
if (notificationSound?.startsWith("data:audio")) {
const binaryString = atob(notificationSound.split(",")[1]);
const arrayBuffer = new ArrayBuffer(binaryString.length);
const uint8Array = new Uint8Array(arrayBuffer);
for (let i = 0; i < binaryString.length; i++) {
uint8Array[i] = binaryString.charCodeAt(i);
}
const blob = new Blob([uint8Array], { type: "audio/wav" });
notificationSound = URL.createObjectURL(blob);
audioSource = arrayBuffer;
} else {
const response = await fetch(notificationSound as string);
audioSource = await response.arrayBuffer();
}

notificationAudio.current = new Audio(notificationSound);
notificationAudio.current.volume = botOptions.notification?.volume as number;
audioBufferRef.current = await audioContextRef.current.decodeAudioData(audioSource);
}

/**
Expand All @@ -292,9 +299,6 @@ const ChatBotContainer = ({ flow }: { flow: Flow }) => {
const handleFirstInteraction = () => {
setHasInteracted(true);

// load audio on first user interaction
notificationAudio.current?.load();

// workaround for getting audio to play on mobile
const utterance = new SpeechSynthesisUtterance();
utterance.text = "";
Expand All @@ -319,21 +323,46 @@ const ChatBotContainer = ({ flow }: { flow: Flow }) => {
* Handles notification count update and notification sound.
*/
const handleNotifications = () => {
// if embedded, or no message found, no need for notifications
if (botOptions.theme?.embedded || messages.length == 0) {
// if no audio context or no messages, return
if (!audioContextRef.current || messages.length === 0) {
return;
}

const message = messages[messages.length - 1]
if (message != null && message?.sender !== "user" && !isBotTyping
&& (!botOptions.isOpen || document.visibilityState !== "visible"
|| (botOptions.isOpen && isScrolling))) {
setUnreadCount(prev => prev + 1);
if (!botOptions.notification?.disabled && notificationToggledOn && hasInteracted) {
notificationAudio.current?.play();
}
// if message is null or sent by user or is bot typing, return
if (message == null || message.sender === "user" || isBotTyping) {
return;
}

// if chat is open but user is not scrolling, return
if (botOptions.isOpen && !isScrolling) {
return;
}

setUnreadCount(prev => prev + 1);
if (!botOptions.notification?.disabled && notificationToggledOn && hasInteracted && audioBufferRef.current) {
const source = audioContextRef.current.createBufferSource();
source.buffer = audioBufferRef.current;
source.connect(gainNodeRef.current as AudioNode).connect(audioContextRef.current.destination);
source.start();
}
}

/**
* Helps check if audio should be played.
*/
const shouldPlayAudio = () => {

Check failure on line 354 in src/components/ChatBotContainer.tsx

View workflow job for this annotation

GitHub Actions / lint

'shouldPlayAudio' is assigned a value but never used
if (!audioBufferRef.current || !audioContextRef.current) {
return false;

Check failure on line 356 in src/components/ChatBotContainer.tsx

View workflow job for this annotation

GitHub Actions / lint

Mixed spaces and tabs
}

Check failure on line 358 in src/components/ChatBotContainer.tsx

View workflow job for this annotation

GitHub Actions / lint

Mixed spaces and tabs
const message = messages[messages.length - 1];
const isUserMessage = message?.sender === "user";
const isBotTypingOrInvisible = isBotTyping || !botOptions.isOpen || document.visibilityState !== "visible" || (botOptions.isOpen && isScrolling);

Check failure on line 361 in src/components/ChatBotContainer.tsx

View workflow job for this annotation

GitHub Actions / lint

This line has a length of 153. Maximum allowed is 120

Check failure on line 362 in src/components/ChatBotContainer.tsx

View workflow job for this annotation

GitHub Actions / lint

Mixed spaces and tabs
return !botOptions.theme?.embedded && messages.length > 0 && !isUserMessage && !isBotTypingOrInvisible && !botOptions.notification?.disabled && notificationToggledOn && hasInteracted;

Check failure on line 363 in src/components/ChatBotContainer.tsx

View workflow job for this annotation

GitHub Actions / lint

This line has a length of 191. Maximum allowed is 120
};

/**
* Retrieves current path for user.
*/
Expand Down

0 comments on commit 51c27cf

Please sign in to comment.