Only animate typing when viewing conversation
This commit is contained in:
parent
c9af8d3ce2
commit
69c0cad14c
3 changed files with 59 additions and 26 deletions
|
@ -1384,6 +1384,11 @@ $message-padding-horizontal: 12px;
|
||||||
&--with-reactions {
|
&--with-reactions {
|
||||||
padding-bottom: 15px;
|
padding-bottom: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&--typing {
|
||||||
|
flex-direction: row-reverse;
|
||||||
|
overflow-y: clip;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.module-message__container-outer--typing-bubble {
|
.module-message__container-outer--typing-bubble {
|
||||||
|
@ -1391,16 +1396,8 @@ $message-padding-horizontal: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.module-message__typing-avatar-container {
|
.module-message__typing-avatar-container {
|
||||||
align-items: center;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row-reverse;
|
flex-direction: row-reverse;
|
||||||
justify-content: center;
|
|
||||||
margin-inline-end: 8px;
|
|
||||||
overflow-y: clip;
|
overflow-y: clip;
|
||||||
|
|
||||||
&--with-reactions {
|
|
||||||
padding-bottom: 15px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.module-message__typing-avatar {
|
.module-message__typing-avatar {
|
||||||
|
@ -2848,6 +2845,10 @@ button.module-image__border-overlay:focus {
|
||||||
padding-inline: 1px;
|
padding-inline: 1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.module-message__typing-animation-container .module-typing-animation {
|
||||||
|
width: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
.module-typing-animation__dot {
|
.module-typing-animation__dot {
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
|
|
||||||
|
|
|
@ -44,8 +44,8 @@ export type TypingBubblePropsType = {
|
||||||
|
|
||||||
const SPRING_CONFIG = {
|
const SPRING_CONFIG = {
|
||||||
mass: 1,
|
mass: 1,
|
||||||
tension: 986,
|
tension: 439,
|
||||||
friction: 64,
|
friction: 42,
|
||||||
precision: 0,
|
precision: 0,
|
||||||
velocity: 0,
|
velocity: 0,
|
||||||
};
|
};
|
||||||
|
@ -53,17 +53,15 @@ const SPRING_CONFIG = {
|
||||||
const AVATAR_ANIMATION_PROPS: Record<'visible' | 'hidden', object> = {
|
const AVATAR_ANIMATION_PROPS: Record<'visible' | 'hidden', object> = {
|
||||||
visible: {
|
visible: {
|
||||||
opacity: 1,
|
opacity: 1,
|
||||||
scale: 1,
|
|
||||||
width: '28px',
|
width: '28px',
|
||||||
x: '0px',
|
x: '0px',
|
||||||
top: '0px',
|
top: '0px',
|
||||||
},
|
},
|
||||||
hidden: {
|
hidden: {
|
||||||
opacity: 0.5,
|
opacity: 0.5,
|
||||||
scale: 0.5,
|
|
||||||
width: '4px', // Match value of module-message__typing-avatar margin-inline-start
|
width: '4px', // Match value of module-message__typing-avatar margin-inline-start
|
||||||
x: '14px',
|
x: '14px',
|
||||||
top: '30px',
|
top: '34px',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -71,6 +69,7 @@ function TypingBubbleAvatar({
|
||||||
conversationId,
|
conversationId,
|
||||||
contact,
|
contact,
|
||||||
visible,
|
visible,
|
||||||
|
shouldAnimate,
|
||||||
getPreferredBadge,
|
getPreferredBadge,
|
||||||
onContactExit,
|
onContactExit,
|
||||||
showContactModal,
|
showContactModal,
|
||||||
|
@ -80,6 +79,7 @@ function TypingBubbleAvatar({
|
||||||
conversationId: string;
|
conversationId: string;
|
||||||
contact: TypingContactType | undefined;
|
contact: TypingContactType | undefined;
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
|
shouldAnimate: boolean;
|
||||||
getPreferredBadge: PreferredBadgeSelectorType;
|
getPreferredBadge: PreferredBadgeSelectorType;
|
||||||
onContactExit: (id: string | undefined) => void;
|
onContactExit: (id: string | undefined) => void;
|
||||||
showContactModal: (contactId: string, conversationId?: string) => void;
|
showContactModal: (contactId: string, conversationId?: string) => void;
|
||||||
|
@ -89,7 +89,9 @@ function TypingBubbleAvatar({
|
||||||
const [springProps, springApi] = useSpring(
|
const [springProps, springApi] = useSpring(
|
||||||
{
|
{
|
||||||
config: SPRING_CONFIG,
|
config: SPRING_CONFIG,
|
||||||
from: AVATAR_ANIMATION_PROPS[visible ? 'hidden' : 'visible'],
|
from: shouldAnimate
|
||||||
|
? AVATAR_ANIMATION_PROPS[visible ? 'hidden' : 'visible']
|
||||||
|
: {},
|
||||||
to: AVATAR_ANIMATION_PROPS[visible ? 'visible' : 'hidden'],
|
to: AVATAR_ANIMATION_PROPS[visible ? 'visible' : 'hidden'],
|
||||||
onRest: () => {
|
onRest: () => {
|
||||||
if (!visible) {
|
if (!visible) {
|
||||||
|
@ -138,6 +140,7 @@ function TypingBubbleAvatar({
|
||||||
function TypingBubbleGroupAvatars({
|
function TypingBubbleGroupAvatars({
|
||||||
conversationId,
|
conversationId,
|
||||||
typingContactIds,
|
typingContactIds,
|
||||||
|
shouldAnimate,
|
||||||
getConversation,
|
getConversation,
|
||||||
getPreferredBadge,
|
getPreferredBadge,
|
||||||
showContactModal,
|
showContactModal,
|
||||||
|
@ -153,6 +156,7 @@ function TypingBubbleGroupAvatars({
|
||||||
| 'theme'
|
| 'theme'
|
||||||
> & {
|
> & {
|
||||||
typingContactIds: ReadonlyArray<string>;
|
typingContactIds: ReadonlyArray<string>;
|
||||||
|
shouldAnimate: boolean;
|
||||||
}): ReactElement {
|
}): ReactElement {
|
||||||
const [allContactsById, setAllContactsById] = useState<
|
const [allContactsById, setAllContactsById] = useState<
|
||||||
Map<string, TypingContactType>
|
Map<string, TypingContactType>
|
||||||
|
@ -195,7 +199,7 @@ function TypingBubbleGroupAvatars({
|
||||||
|
|
||||||
// Avatars are rendered Right-to-Left so the leftmost avatars can render on top.
|
// Avatars are rendered Right-to-Left so the leftmost avatars can render on top.
|
||||||
return (
|
return (
|
||||||
<div className="module-message__typing-avatar-container">
|
<div className="module-message__author-avatar-container module-message__author-avatar-container--typing">
|
||||||
{typingContactsOverflowCount > 0 && (
|
{typingContactsOverflowCount > 0 && (
|
||||||
<div
|
<div
|
||||||
className="module-message__typing-avatar module-message__typing-avatar--overflow-count
|
className="module-message__typing-avatar module-message__typing-avatar--overflow-count
|
||||||
|
@ -228,6 +232,7 @@ function TypingBubbleGroupAvatars({
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
theme={theme}
|
theme={theme}
|
||||||
visible={visibleContactIds.has(contactId)}
|
visible={visibleContactIds.has(contactId)}
|
||||||
|
shouldAnimate={shouldAnimate}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
@ -241,12 +246,10 @@ const OUTER_DIV_ANIMATION_PROPS: Record<'visible' | 'hidden', object> = {
|
||||||
const BUBBLE_ANIMATION_PROPS: Record<'visible' | 'hidden', object> = {
|
const BUBBLE_ANIMATION_PROPS: Record<'visible' | 'hidden', object> = {
|
||||||
visible: {
|
visible: {
|
||||||
opacity: 1,
|
opacity: 1,
|
||||||
scale: 1,
|
|
||||||
top: '0px',
|
top: '0px',
|
||||||
},
|
},
|
||||||
hidden: {
|
hidden: {
|
||||||
opacity: 0.5,
|
opacity: 0.5,
|
||||||
scale: 0.5,
|
|
||||||
top: '30px',
|
top: '30px',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -269,13 +272,17 @@ export function TypingBubble({
|
||||||
() => Object.keys(typingContactIdTimestamps),
|
() => Object.keys(typingContactIdTimestamps),
|
||||||
[typingContactIdTimestamps]
|
[typingContactIdTimestamps]
|
||||||
);
|
);
|
||||||
|
const [shouldAnimate, setShouldAnimate] = useState(false);
|
||||||
|
const prevTypingContactIds = React.useRef<
|
||||||
|
ReadonlyArray<string> | undefined
|
||||||
|
>();
|
||||||
const isSomeoneTyping = useMemo(
|
const isSomeoneTyping = useMemo(
|
||||||
() => typingContactIds.length > 0,
|
() => typingContactIds.length > 0,
|
||||||
[typingContactIds]
|
[typingContactIds]
|
||||||
);
|
);
|
||||||
|
|
||||||
const [outerDivStyle, outerDivSpringApi] = useSpring(
|
const [outerDivStyle, outerDivSpringApi] = useSpring(
|
||||||
{
|
{
|
||||||
from: OUTER_DIV_ANIMATION_PROPS[isSomeoneTyping ? 'hidden' : 'visible'],
|
|
||||||
to: OUTER_DIV_ANIMATION_PROPS[isSomeoneTyping ? 'visible' : 'hidden'],
|
to: OUTER_DIV_ANIMATION_PROPS[isSomeoneTyping ? 'visible' : 'hidden'],
|
||||||
config: SPRING_CONFIG,
|
config: SPRING_CONFIG,
|
||||||
},
|
},
|
||||||
|
@ -283,7 +290,6 @@ export function TypingBubble({
|
||||||
);
|
);
|
||||||
const [typingAnimationStyle, typingAnimationSpringApi] = useSpring(
|
const [typingAnimationStyle, typingAnimationSpringApi] = useSpring(
|
||||||
{
|
{
|
||||||
from: BUBBLE_ANIMATION_PROPS[isSomeoneTyping ? 'hidden' : 'visible'],
|
|
||||||
to: BUBBLE_ANIMATION_PROPS[isSomeoneTyping ? 'visible' : 'hidden'],
|
to: BUBBLE_ANIMATION_PROPS[isSomeoneTyping ? 'visible' : 'hidden'],
|
||||||
config: SPRING_CONFIG,
|
config: SPRING_CONFIG,
|
||||||
onRest: () => {
|
onRest: () => {
|
||||||
|
@ -336,6 +342,23 @@ export function TypingBubble({
|
||||||
typingContactIdTimestamps,
|
typingContactIdTimestamps,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
// Only animate when the user observes a change in typing contacts, not when first
|
||||||
|
// switching to a conversation.
|
||||||
|
useEffect(() => {
|
||||||
|
if (shouldAnimate) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!prevTypingContactIds.current) {
|
||||||
|
prevTypingContactIds.current = typingContactIds;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (prevTypingContactIds.current !== typingContactIds) {
|
||||||
|
setShouldAnimate(true);
|
||||||
|
}
|
||||||
|
}, [shouldAnimate, typingContactIds]);
|
||||||
|
|
||||||
if (!isVisible) {
|
if (!isVisible) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -360,6 +383,7 @@ export function TypingBubble({
|
||||||
<TypingBubbleGroupAvatars
|
<TypingBubbleGroupAvatars
|
||||||
conversationId={conversationId}
|
conversationId={conversationId}
|
||||||
typingContactIds={typingContactIds}
|
typingContactIds={typingContactIds}
|
||||||
|
shouldAnimate={shouldAnimate}
|
||||||
getConversation={getConversation}
|
getConversation={getConversation}
|
||||||
getPreferredBadge={getPreferredBadge}
|
getPreferredBadge={getPreferredBadge}
|
||||||
showContactModal={showContactModal}
|
showContactModal={showContactModal}
|
||||||
|
|
|
@ -2688,6 +2688,14 @@
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2022-11-03T14:21:47.456Z"
|
"updated": "2022-11-03T14:21:47.456Z"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"rule": "React-useRef",
|
||||||
|
"path": "ts/components/conversation/TypingBubble.tsx",
|
||||||
|
"line": " const prevTypingContactIds = React.useRef<",
|
||||||
|
"reasonCategory": "usageTrusted",
|
||||||
|
"updated": "2023-09-28T21:48:57.488Z",
|
||||||
|
"reasonDetail": "Used to track change of typing contacts while a conversation is actively viewed."
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"rule": "React-useRef",
|
"rule": "React-useRef",
|
||||||
"path": "ts/components/conversation/WaveformScrubber.tsx",
|
"path": "ts/components/conversation/WaveformScrubber.tsx",
|
||||||
|
@ -2804,6 +2812,13 @@
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2021-10-22T00:52:39.251Z"
|
"updated": "2021-10-22T00:52:39.251Z"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"rule": "React-useRef",
|
||||||
|
"path": "ts/hooks/useScrollLock.tsx",
|
||||||
|
"line": " const onUserInterruptRef = useRef(onUserInterrupt);",
|
||||||
|
"reasonCategory": "usageTrusted",
|
||||||
|
"updated": "2023-09-19T17:05:51.321Z"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"rule": "React-useRef",
|
"rule": "React-useRef",
|
||||||
"path": "ts/hooks/useSizeObserver.tsx",
|
"path": "ts/hooks/useSizeObserver.tsx",
|
||||||
|
@ -2853,13 +2868,6 @@
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2023-07-25T21:55:26.191Z"
|
"updated": "2023-07-25T21:55:26.191Z"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"rule": "React-useRef",
|
|
||||||
"path": "ts/hooks/useScrollLock.tsx",
|
|
||||||
"line": " const onUserInterruptRef = useRef(onUserInterrupt);",
|
|
||||||
"reasonCategory": "usageTrusted",
|
|
||||||
"updated": "2023-09-19T17:05:51.321Z"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"rule": "React-useRef",
|
"rule": "React-useRef",
|
||||||
"path": "ts/quill/formatting/menu.tsx",
|
"path": "ts/quill/formatting/menu.tsx",
|
||||||
|
|
Loading…
Reference in a new issue