Skip to content

Commit 5dd5f18

Browse files
committed
feat: loading animation and style fixes
1 parent ffe2368 commit 5dd5f18

7 files changed

Lines changed: 95 additions & 15 deletions

File tree

packages/component/src/Activity/StackedLayout.module.css

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -108,12 +108,28 @@
108108
margin-block: calc(var(--webchat__padding--regular) + 1px);
109109
place-self: start;
110110
font-size: 1.1em;
111+
display: grid;
112+
grid-template-areas: "message-status-icon";
113+
114+
> * {
115+
grid-area: message-status-icon;
116+
}
111117
}
112118

113-
.stacked-layout__message-status-complete-icon {
119+
.stacked-layout__message-status-unset-icon,
120+
.stacked-layout__message-status-complete-icon,
121+
.stacked-layout__message-status-loader {
114122
visibility: hidden;
115123
}
116124

125+
.stacked-layout__message-status--unset .stacked-layout__message-status-unset-icon {
126+
visibility: unset;
127+
}
128+
129+
.stacked-layout__message-status--incomplete .stacked-layout__message-status-loader {
130+
visibility: unset;
131+
}
132+
117133
.stacked-layout__message-status--final .stacked-layout__message-status-complete-icon {
118134
visibility: unset;
119135
}
@@ -177,17 +193,23 @@
177193
.stacked-layout__bubble {
178194
grid-template: 'content' 1fr / 1fr;
179195
}
196+
180197
.stacked-layout__message-status {
181198
display: none;
182199
}
183-
&:has(.stacked-layout__message-status--final) .stacked-layout__bubble {
184-
grid-template:
185-
'status content'
186-
'status content'
187-
/ min-content auto;
188-
}
189-
&:has(.stacked-layout__message-status--final) .stacked-layout__message-status {
190-
display: flex;
200+
201+
&:has(.stacked-layout__message-status--final),
202+
&:has(.stacked-layout__message-status--incomplete) {
203+
.stacked-layout__bubble {
204+
grid-template:
205+
'status content'
206+
'status content'
207+
/ min-content auto;
208+
}
209+
210+
.stacked-layout__message-status {
211+
display: grid;
212+
}
191213
}
192214
}
193215
}

packages/component/src/Activity/StackedLayout.tsx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import { useGetLogicalGroupKey } from '../providers/ActivityLogicalGrouping';
2424

2525
import styles from './StackedLayout.module.css';
2626
import { ComponentIcon } from '../Icon';
27+
import MessageStatusLoader from './private/MessageStatusLoader';
2728

2829
const { useAvatarForBot, useAvatarForUser, useLocalizer, useGetKeyByActivity, useStyleOptions } = hooks;
2930

@@ -167,14 +168,23 @@ const StackedLayout = ({
167168
showStatus && (
168169
<div
169170
className={cx(classNames['stacked-layout__message-status'], {
170-
[classNames['stacked-layout__message-status--final']]: messageThing?.creativeWorkStatus === 'published'
171+
[classNames['stacked-layout__message-status--unset']]: !messageThing?.creativeWorkStatus,
172+
[classNames['stacked-layout__message-status--final']]: messageThing?.creativeWorkStatus === 'published',
173+
[classNames['stacked-layout__message-status--incomplete']]:
174+
messageThing?.creativeWorkStatus === 'incomplete'
171175
})}
172176
>
177+
<ComponentIcon
178+
appearance="text"
179+
className={classNames['stacked-layout__message-status-unset-icon']}
180+
icon="unchecked-circle"
181+
/>
173182
<ComponentIcon
174183
appearance="text"
175184
className={classNames['stacked-layout__message-status-complete-icon']}
176185
icon="checkmark-circle"
177186
/>
187+
<MessageStatusLoader className={classNames['stacked-layout__message-status-loader']} />
178188
</div>
179189
),
180190
[classNames, messageThing?.creativeWorkStatus, showStatus]
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
2+
:global(.webchat) .message-status-loader {
3+
--webchat__component-icon--mask: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'><circle cx='8' cy='8' r='3'/></svg>");
4+
animation: message-status-loader__pulse 1.5s ease-in-out infinite;
5+
}
6+
7+
@keyframes message-status-loader__pulse {
8+
0%, 100% {
9+
opacity: 0.3;
10+
}
11+
50% {
12+
opacity: 1;
13+
}
14+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { useStyles } from '@msinternal/botframework-webchat-styles/react';
2+
import cx from 'classnames';
3+
import React, { memo } from 'react';
4+
5+
import styles from './MessageStatusLoader.module.css';
6+
import { ComponentIcon } from '../../Icon';
7+
8+
function MessageStatusLoader({ className }: Readonly<{ className?: string | undefined }>) {
9+
const classNames = useStyles(styles);
10+
return <ComponentIcon appearance="text" className={cx(classNames['message-status-loader'], className)} />;
11+
}
12+
13+
export default memo(MessageStatusLoader);

packages/component/src/Icon/ComponentIcon.module.css

Lines changed: 4 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
:global(.webchat) .part-grouping-activity {
2+
.part-grouping-activity__activities {
3+
padding-block-start: 0;
4+
}
5+
}

packages/component/src/Middleware/ActivityGrouping/ui/PartGrouping/private/PartGroupingActivity.tsx

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { reactNode } from '@msinternal/botframework-webchat-react-valibot';
2+
import { useStyles } from '@msinternal/botframework-webchat-styles/react';
23
import { hooks } from 'botframework-webchat-api';
34
import { getOrgSchemaMessage, type WebChatActivity } from 'botframework-webchat-core';
45
import React, { memo, useCallback, useMemo, useState, type MouseEventHandler } from 'react';
@@ -37,6 +38,8 @@ import isZeroOrPositive from '../../../../../Utils/isZeroOrPositive';
3738
import { android } from '../../../../../Utils/detectBrowser';
3839
import TranscriptActivityList from '../../../../../Transcript/TranscriptFocus/TranscriptActivityList';
3940

41+
import styles from './PartGroupingActivity.module.css';
42+
4043
const { useAvatarForBot, useStyleOptions, useGetKeyByActivity } = hooks;
4144

4245
const partGroupingActivityPropsSchema = pipe(
@@ -65,8 +68,9 @@ const partGroupingFocusableActivityPropsSchema = pipe(
6568
transform(value => value as WebChatActivity),
6669
readonly()
6770
),
68-
groupKey: string(),
69-
children: optional(reactNode())
71+
children: optional(reactNode()),
72+
className: optional(string()),
73+
groupKey: string()
7074
}),
7175
readonly()
7276
);
@@ -75,7 +79,7 @@ type FocusablePartGroupingActivityProps = InferOutput<typeof partGroupingFocusab
7579

7680
function FocusablePartGroupingActivity(props: FocusablePartGroupingActivityProps) {
7781
const [activeDescendantId] = useActiveDescendantId();
78-
const { activity, children, groupKey } = parse(partGroupingFocusableActivityPropsSchema, props);
82+
const { activity, children, className, groupKey } = parse(partGroupingFocusableActivityPropsSchema, props);
7983

8084
const getGroupDescendantIdByActivityKey = useGetGroupDescendantIdByActivityKey();
8185
const focusByGroupKey = useFocusByGroupKey();
@@ -114,6 +118,7 @@ function FocusablePartGroupingActivity(props: FocusablePartGroupingActivityProps
114118

115119
return (
116120
<TranscriptFocusContent
121+
className={className}
117122
focused={isActiveDescendant}
118123
onMouseDownCapture={handleMouseDownCapture}
119124
ref={groupCallbackRef}
@@ -134,6 +139,7 @@ function FocusablePartGroupingActivity(props: FocusablePartGroupingActivityProps
134139
}
135140

136141
function PartGroupingActivity(props: PartGroupingActivityProps) {
142+
const classNames = useStyles(styles);
137143
const { activities, children } = parse(partGroupingActivityPropsSchema, props);
138144
const getKeyByActivity = useGetKeyByActivity();
139145
const [isGroupOpen, setIsGroupOpen] = useState(true);
@@ -175,7 +181,11 @@ function PartGroupingActivity(props: PartGroupingActivityProps) {
175181
const topAlignedCallout = isZeroOrPositive(bubbleNubOffset);
176182

177183
return (
178-
<FocusablePartGroupingActivity activity={firstActivity} groupKey={groupKey}>
184+
<FocusablePartGroupingActivity
185+
activity={firstActivity}
186+
className={classNames['part-grouping-activity']}
187+
groupKey={groupKey}
188+
>
179189
<StackedLayoutRoot
180190
hideAvatar={hasAvatar && !showAvatar}
181191
isGroup={true}
@@ -184,7 +194,9 @@ function PartGroupingActivity(props: PartGroupingActivityProps) {
184194
>
185195
<StackedLayoutMain avatar={showAvatar && renderAvatar && renderAvatar()}>
186196
<CollapsibleGrouping isOpen={isGroupOpen} onToggle={setIsGroupOpen} title={currentMessage?.abstract || ''}>
187-
<TranscriptActivityList>{children}</TranscriptActivityList>
197+
<TranscriptActivityList className={classNames['part-grouping-activity__activities']}>
198+
{children}
199+
</TranscriptActivityList>
188200
</CollapsibleGrouping>
189201
</StackedLayoutMain>
190202
{renderActivityStatus && <StackedLayoutStatus>{renderActivityStatus({ hideTimestamp })}</StackedLayoutStatus>}

0 commit comments

Comments
 (0)