Skip to content

Commit d6380a5

Browse files
committed
Added Activity app
1 parent 25b18ff commit d6380a5

14 files changed

Lines changed: 695 additions & 40 deletions

CLAUDE.md

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -43,25 +43,31 @@ Single-page React 19 app built with Vite 8. No routing, no state management libr
4343
`src/App.jsx` owns the top-level state and renders:
4444

4545
```
46-
┌──────────────────────── TitleBar ───────────────────────┐
47-
│ NavRail │ ChatList │ ChatView (+ optional right rails) │
48-
└─────────┴──────────┴──────────────────────────────────────┘
46+
┌──────────────────────── TitleBar ─────────────────────────────────────┐
47+
│ NavRail │ ChatList │ ChatView (+ optional right rails) │
48+
│ │ or │ │
49+
│ │ ActivityList (when the bell icon is selected) │
50+
└─────────┴──────────┴──────────────────────────────────────────────────┘
4951
```
5052

53+
`NavRail` is a controlled component driving `activeView` (`'chat' | 'activity'`). When `activity` is active, `ChatList` is swapped for `ActivityList` and `ChatView` renders the chat the selected activity event points at (with that event's source message highlighted).
54+
5155
`ChatView` can render up to two right-hand rails alongside the main canvas:
5256
- **SessionsRail** — list of past agent sessions for the active agent chat (agent chats only)
5357
- **AgentsRail** — agents in the current conversation + per-agent private chat thread
54-
- **ChannelThreadRail** — a channel post's replies thread (channel chats only; opens when a reply indicator is clicked)
58+
- **ChannelThreadRail** — a channel post's replies thread (channel chats only; opens when a reply indicator is clicked, or when an Activity event points at a channel reply)
5559

5660
### State lives in `App.jsx`
5761

5862
Hoisted so rails and the canvas stay in sync:
5963

64+
- `activeView``'chat' | 'activity'`, driven by NavRail
6065
- `activeChatId` — which chat is open
6166
- `readChatIds` (`Set`) — chats the user has visited; dismisses `bold` + unread-dot in `ChatList`
6267
- `sessions` — per-agent list of past sessions (starts from `data/sessions.js`, augmented at runtime)
6368
- `dynamicSessionMessages` — messages for sessions created during the prototype session
64-
- `navIntent` — one-shot signal to open a specific session when cross-navigating between chats
69+
- `activityEvents` / `activeActivityId` — notification feed state backing the Activity view
70+
- `navIntent` — one-shot signal used when cross-navigating between chats. Shape: `{ chatId, sessionId?, channelThreadPostId?, highlightMessageId? }`. ChatView consumes it on the next render to open the sessions rail, auto-open a channel thread, or scroll + flash a specific message
6571

6672
`ChatView` owns local state for compose input, which rail is open, per-chat ephemeral flags, scripted/demo flow progress, etc. When `activeChatId` changes, the chat-level state is reset via the render-phase state-adjustment pattern (not a `useEffect`).
6773

@@ -76,6 +82,7 @@ All mocks live in `src/data/` and are re-exported by `src/data.js` (the barrel).
7682
- `sessions.js``agentSessions`, the initial per-agent session lists
7783
- `sessionMessages.js` — message history keyed by session id
7884
- `promptSuggestions.js` — the 2×3 grid of prompt cards shown in the empty state for a new agent session, keyed by agent contact id
85+
- `activityEvents.js``activityEvents`, the chronological notification feed (reactions, replies, mentions) referenced by contact/message/post ids
7986

8087
### Contact types
8188

@@ -95,6 +102,7 @@ Channels are additionally grouped into teams via the `teams` export. Each team h
95102
- `src/components/common/` — shared primitives meant for reuse across surfaces (`Avatar`, `LinkCard`, `IconButton`, `Icon` library, `PrivateDisclaimer`). Add a new reusable primitive here; don't inline SVGs or hand-roll a button when a common one fits.
96103
- `src/components/` — feature components for the current surface:
97104
- `TitleBar`, `NavRail`, `ChatList` — chrome
105+
- `ActivityList` — left pane when the bell icon is selected; chronological feed of reactions/replies/mentions that target the current user. Clicking an event routes `ChatView` to the source chat (and opens a channel thread or flashes an anchor message when relevant)
98106
- `ChatView` — the main canvas router; delegates to `ChatHeader`, `Compose`, and the rails
99107
- `ChatHeader`, `Compose` — split out from `ChatView` to keep it focused on state + routing
100108
- `MessageRow` — one message bubble (reactions, thread reply badge, link cards, Adaptive Cards)

DESIGN_GUIDE.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,8 +106,10 @@ Before adding an icon inline, check `src/components/common/Icon.jsx` — the sha
106106

107107
### App Bar (Nav Rail) Icons
108108

109+
- **Size:** 24×24 SVG inside a 28×28 wrap (larger than Teams' typical 20px — the icons read more clearly at this size and the prototype skews the detail up slightly)
109110
- **Inactive state:** Outlined icons, `#666666`
110-
- **Active state:** Filled icons, Teams purple (`#5B5FC7`); same color drives the 3px left-edge active indicator and an 8% tinted background fill (`rgba(91, 95, 199, 0.08)`)
111+
- **Active state:** Filled icons, Teams purple (`#5B5FC7`); same color drives a 3px left-edge active indicator spanning the **full height of the nav-item row** (flush top to bottom, not capped) and an 8% tinted background fill (`rgba(91, 95, 199, 0.08)`)
112+
- **Clickable surfaces:** `chat` and `activity` toggle the left pane (ChatList ↔ ActivityList). Others are decorative for now.
111113

112114
## Message Patterns
113115

src/App.jsx

Lines changed: 42 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,24 @@
11
import { useState, useCallback } from 'react'
2-
import { agentSessions as initialSessions } from './data/sessions'
2+
import { agentSessions as initialSessions, activityEvents as seedActivityEvents } from './data'
33
import NavRail from './components/NavRail'
44
import ChatList from './components/ChatList'
55
import ChatView from './components/ChatView'
6+
import ActivityList from './components/ActivityList'
67
import TitleBar from './components/TitleBar'
78
import './App.css'
89

910
export default function App() {
11+
const [activeView, setActiveView] = useState('chat') // 'chat' | 'activity'
1012
const [activeChatId, setActiveChatId] = useState(1)
1113
const [readChatIds, setReadChatIds] = useState(() => new Set([1]))
1214
const [sessions, setSessions] = useState(initialSessions)
1315
const [dynamicSessionMessages, setDynamicSessionMessages] = useState({})
14-
// When navigating to an agent chat, optionally open sessions rail with a specific session
16+
// Activity feed: persist which events the user has opened so unread decorations clear.
17+
const [activityEvents, setActivityEvents] = useState(seedActivityEvents)
18+
const [activeActivityId, setActiveActivityId] = useState(null)
19+
// When navigating to a chat, optionally tell ChatView to open a specific
20+
// session (sessions rail), open a specific channel thread, or flash a
21+
// specific message so the user can see where a notification landed.
1522
const [navIntent, setNavIntent] = useState(null)
1623

1724
const selectChat = useCallback((chatId) => {
@@ -49,16 +56,44 @@ export default function App() {
4956
setDynamicSessionMessages(prev => ({ ...prev, [sessionId]: messages }))
5057
}, [])
5158

59+
const selectActivity = useCallback((event) => {
60+
setActiveActivityId(event.id)
61+
setActivityEvents(prev =>
62+
prev.map(e => (e.id === event.id && e.unread ? { ...e, unread: false } : e))
63+
)
64+
setActiveChatId(event.chatId)
65+
setReadChatIds(prev => (prev.has(event.chatId) ? prev : new Set(prev).add(event.chatId)))
66+
setNavIntent({
67+
chatId: event.chatId,
68+
channelThreadPostId: event.postId || null,
69+
highlightMessageId: event.messageId || null,
70+
})
71+
}, [])
72+
73+
const activityUnreadCount = activityEvents.reduce((n, e) => n + (e.unread ? 1 : 0), 0)
74+
5275
return (
5376
<div className="app">
5477
<TitleBar />
5578
<div className="app-body">
56-
<NavRail />
57-
<ChatList
58-
activeChatId={activeChatId}
59-
onSelectChat={selectChat}
60-
readChatIds={readChatIds}
79+
<NavRail
80+
activeView={activeView}
81+
onSelectView={setActiveView}
82+
activityUnreadCount={activityUnreadCount}
6183
/>
84+
{activeView === 'activity' ? (
85+
<ActivityList
86+
events={activityEvents}
87+
activeEventId={activeActivityId}
88+
onSelectEvent={selectActivity}
89+
/>
90+
) : (
91+
<ChatList
92+
activeChatId={activeChatId}
93+
onSelectChat={selectChat}
94+
readChatIds={readChatIds}
95+
/>
96+
)}
6297
<ChatView
6398
activeChatId={activeChatId}
6499
onSelectChat={navigateToChat}

src/components/ActivityList.css

Lines changed: 242 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,242 @@
1+
.activity-rail {
2+
width: 390px;
3+
flex-shrink: 0;
4+
background: #F5F5F5;
5+
border-right: 1px solid #E8E8E8;
6+
display: flex;
7+
flex-direction: column;
8+
min-width: 0;
9+
}
10+
11+
/* ── Header ── */
12+
.activity-rail-header {
13+
display: flex;
14+
align-items: center;
15+
justify-content: space-between;
16+
padding: 14px 20px 10px;
17+
}
18+
19+
.activity-rail-title {
20+
font-size: 18px;
21+
font-weight: 700;
22+
color: #242424;
23+
}
24+
25+
/* ── Filter pills ── */
26+
.activity-rail-filters {
27+
display: flex;
28+
align-items: center;
29+
gap: 8px;
30+
padding: 4px 20px 10px;
31+
}
32+
33+
.activity-filter-pill {
34+
background: none;
35+
border: 1px solid #D1D1D1;
36+
border-radius: 16px;
37+
padding: 3px 12px;
38+
font-size: 12px;
39+
font-weight: 400;
40+
color: #424242;
41+
cursor: pointer;
42+
line-height: 18px;
43+
font-family: inherit;
44+
}
45+
46+
.activity-filter-pill:hover {
47+
background: #EBEBEB;
48+
border-color: #C0C0C0;
49+
}
50+
51+
.activity-filter-pill-active,
52+
.activity-filter-pill-active:hover {
53+
background: #E8EBFA;
54+
border-color: #B8C0F0;
55+
color: #242424;
56+
font-weight: 600;
57+
}
58+
59+
.activity-rail-divider {
60+
height: 0;
61+
border-top: 1px solid #E0E0E0;
62+
flex-shrink: 0;
63+
}
64+
65+
/* ── List ── */
66+
.activity-rail-list {
67+
flex: 1;
68+
overflow-y: auto;
69+
scrollbar-width: none;
70+
padding: 4px 0;
71+
}
72+
73+
.activity-rail-list::-webkit-scrollbar {
74+
display: none;
75+
}
76+
77+
.activity-rail-empty {
78+
padding: 24px 20px;
79+
text-align: center;
80+
font-size: 12px;
81+
color: #616161;
82+
line-height: 16px;
83+
}
84+
85+
.activity-item {
86+
position: relative;
87+
display: flex;
88+
gap: 12px;
89+
align-items: flex-start;
90+
width: calc(100% - 16px);
91+
margin: 0 8px;
92+
padding: 10px 12px 10px 10px;
93+
border: none;
94+
background: none;
95+
border-radius: 4px;
96+
cursor: pointer;
97+
font-family: inherit;
98+
text-align: left;
99+
color: #242424;
100+
}
101+
102+
.activity-item:hover {
103+
background: #FCFBFA;
104+
}
105+
106+
.activity-item-selected,
107+
.activity-item-selected:hover {
108+
background: #FCFBFA;
109+
}
110+
111+
/* ── Avatar + action badge ── */
112+
.activity-avatar-wrap {
113+
position: relative;
114+
flex-shrink: 0;
115+
width: 32px;
116+
height: 32px;
117+
margin-top: 1px;
118+
}
119+
120+
.activity-badge {
121+
position: absolute;
122+
right: -4px;
123+
bottom: -4px;
124+
display: inline-flex;
125+
align-items: center;
126+
justify-content: center;
127+
width: 18px;
128+
height: 18px;
129+
border-radius: 50%;
130+
background: #fff;
131+
box-shadow: 0 0 0 1.5px #F5F5F5;
132+
font-size: 10px;
133+
line-height: 1;
134+
}
135+
136+
.activity-item:hover .activity-badge,
137+
.activity-item-selected .activity-badge {
138+
box-shadow: 0 0 0 1.5px #FCFBFA;
139+
}
140+
141+
.activity-badge-emoji {
142+
background: #fff;
143+
font-size: 11px;
144+
}
145+
146+
.activity-badge-reply {
147+
background: #5B5FC7;
148+
color: #fff;
149+
}
150+
151+
.activity-badge-mention {
152+
background: #5B5FC7;
153+
color: #fff;
154+
font-size: 11px;
155+
font-weight: 700;
156+
}
157+
158+
/* ── Body (text) ── */
159+
.activity-body {
160+
flex: 1;
161+
min-width: 0;
162+
display: flex;
163+
flex-direction: column;
164+
gap: 2px;
165+
}
166+
167+
/* Title sits 2px above the other lines. Unread events bold the whole
168+
title (not just the actor) so the hierarchy between unread/read reads
169+
clearly at a glance. */
170+
.activity-title {
171+
font-size: 14px;
172+
line-height: 20px;
173+
color: #242424;
174+
font-weight: 400;
175+
word-break: break-word;
176+
}
177+
178+
.activity-item-unread .activity-title {
179+
font-weight: 700;
180+
}
181+
182+
.activity-actor,
183+
.activity-action {
184+
color: inherit;
185+
font-weight: inherit;
186+
}
187+
188+
.activity-action {
189+
color: #424242;
190+
}
191+
192+
.activity-item-unread .activity-action {
193+
color: #242424;
194+
}
195+
196+
.activity-preview {
197+
font-size: 12px;
198+
line-height: 16px;
199+
color: #616161;
200+
display: -webkit-box;
201+
-webkit-line-clamp: 2;
202+
-webkit-box-orient: vertical;
203+
overflow: hidden;
204+
word-break: break-word;
205+
}
206+
207+
.activity-meta {
208+
display: flex;
209+
align-items: center;
210+
gap: 6px;
211+
font-size: 12px;
212+
line-height: 16px;
213+
color: #999;
214+
margin-top: 2px;
215+
min-width: 0;
216+
}
217+
218+
.activity-location {
219+
color: #616161;
220+
white-space: nowrap;
221+
overflow: hidden;
222+
text-overflow: ellipsis;
223+
}
224+
225+
.activity-meta-sep {
226+
flex-shrink: 0;
227+
}
228+
229+
.activity-time {
230+
flex-shrink: 0;
231+
}
232+
233+
.activity-unread-dot {
234+
position: absolute;
235+
top: 18px;
236+
right: 12px;
237+
width: 8px;
238+
height: 8px;
239+
border-radius: 50%;
240+
background: #5B5FC7;
241+
flex-shrink: 0;
242+
}

0 commit comments

Comments
 (0)