Only animate typing when viewing conversation

This commit is contained in:
ayumi-signal 2023-10-02 16:18:28 -04:00 committed by GitHub
parent c9af8d3ce2
commit 69c0cad14c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 59 additions and 26 deletions

View file

@ -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%;

View file

@ -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}

View file

@ -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",