experimental-a2 0.20.0 → 0.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +55 -0
- package/README.md +1 -1
- package/dist/{actor-C7b-1arn.d.ts → actor-CqibLgjQ.d.ts} +15 -7
- package/dist/actor-CqibLgjQ.d.ts.map +1 -0
- package/dist/actor-client.d.ts +1 -1
- package/dist/actor-client.js +2 -2
- package/dist/actor-react.d.ts +3 -3
- package/dist/actor-react.js +5 -5
- package/dist/{actor-shared-t7w8xIKP.js → actor-shared-DFET5q16.js} +3 -3
- package/dist/actor-shared-DFET5q16.js.map +1 -0
- package/dist/actor.d.ts +1 -1
- package/dist/actor.js +54 -43
- package/dist/actor.js.map +1 -1
- package/dist/{ai-Cz3hqVhk.d.ts → ai-DAu77CDr.d.ts} +8 -8
- package/dist/{ai-Cz3hqVhk.d.ts.map → ai-DAu77CDr.d.ts.map} +1 -1
- package/dist/{ai-mMiWul1T.js → ai-EEfKyc-z.js} +9 -9
- package/dist/ai-EEfKyc-z.js.map +1 -0
- package/dist/ai-client.d.ts +2 -2
- package/dist/ai-client.js +2 -2
- package/dist/ai-fx.d.ts +4 -2
- package/dist/ai-fx.d.ts.map +1 -1
- package/dist/ai-fx.js +214 -67
- package/dist/ai-fx.js.map +1 -1
- package/dist/{ai-harness-CV6rXbB3.d.ts → ai-harness-DTjkAr2T.d.ts} +2 -2
- package/dist/{ai-harness-CV6rXbB3.d.ts.map → ai-harness-DTjkAr2T.d.ts.map} +1 -1
- package/dist/{ai-harness-Clbg5ei7.js → ai-harness-dQeJaR1t.js} +7 -3
- package/dist/ai-harness-dQeJaR1t.js.map +1 -0
- package/dist/ai-pi.d.ts +1 -1
- package/dist/ai-pi.js +3 -3
- package/dist/ai-pi.js.map +1 -1
- package/dist/ai-react.d.ts +5 -5
- package/dist/ai-react.js +2 -2
- package/dist/ai-server.d.ts +14 -17
- package/dist/ai-server.d.ts.map +1 -1
- package/dist/ai-server.js +86 -73
- package/dist/ai-server.js.map +1 -1
- package/dist/ai.d.ts +2 -2
- package/dist/ai.js +2 -2
- package/dist/{client-BhHLoNKF.js → client-BQS4i_V9.js} +29 -6
- package/dist/client-BQS4i_V9.js.map +1 -0
- package/dist/{client-h_DKFAjj.d.ts → client-CUMbS_38.d.ts} +3 -3
- package/dist/{client-h_DKFAjj.d.ts.map → client-CUMbS_38.d.ts.map} +1 -1
- package/dist/client.d.ts +3 -3
- package/dist/client.js +1 -1
- package/dist/{contract-BYKASlCV.js → contract-CWukt01d.js} +2 -2
- package/dist/contract-CWukt01d.js.map +1 -0
- package/dist/index.d.ts +5 -5
- package/dist/index.js +3 -3
- package/dist/{initial-snapshot-Bx-c0pDP.d.ts → initial-snapshot-D3Lve43o.d.ts} +4 -4
- package/dist/initial-snapshot-D3Lve43o.d.ts.map +1 -0
- package/dist/{internal-CscSY7K_.js → internal-CvsrprnU.js} +40 -16
- package/dist/internal-CvsrprnU.js.map +1 -0
- package/dist/otel.d.ts +1 -1
- package/dist/otel.js +1 -1
- package/dist/otel.js.map +1 -1
- package/dist/{react-BBXgHWFc.d.ts → react-BoHvC3v9.d.ts} +4 -4
- package/dist/{react-BBXgHWFc.d.ts.map → react-BoHvC3v9.d.ts.map} +1 -1
- package/dist/react.d.ts +1 -1
- package/dist/react.js +1 -1
- package/dist/{reducer-CqA59Q6U.d.ts → reducer-CYPHsSmd.d.ts} +2 -2
- package/dist/{reducer-CqA59Q6U.d.ts.map → reducer-CYPHsSmd.d.ts.map} +1 -1
- package/dist/{reducer-BAXc8JyF.js → reducer-CjmvZSAb.js} +2 -2
- package/dist/{reducer-BAXc8JyF.js.map → reducer-CjmvZSAb.js.map} +1 -1
- package/dist/runtime-consumer-CIeL33wj.js +12 -0
- package/dist/runtime-consumer-CIeL33wj.js.map +1 -0
- package/dist/scheduler-qstash.d.ts +2 -2
- package/dist/scheduler-qstash.js +3 -3
- package/dist/scheduler-task-X1SToDjP.js.map +1 -1
- package/dist/scheduler-vercel.d.ts +2 -2
- package/dist/scheduler-vercel.js +3 -3
- package/dist/scheduler-vercel.js.map +1 -1
- package/dist/{server-qyb5aJJG.js → server-DtFrWxi3.js} +412 -116
- package/dist/server-DtFrWxi3.js.map +1 -0
- package/dist/{server-DYAATFuT.d.ts → server-qgez0Xny.d.ts} +108 -41
- package/dist/server-qgez0Xny.d.ts.map +1 -0
- package/dist/server.browser.js +1 -1
- package/dist/server.browser.js.map +1 -1
- package/dist/server.d.ts +4 -4
- package/dist/server.js +2 -2
- package/dist/{session-clear-C-QwTXV1.d.ts → session-clear-D1PFe7xl.d.ts} +2 -2
- package/dist/session-clear-D1PFe7xl.d.ts.map +1 -0
- package/dist/{session-clear-DGI4jTQ5.js → session-clear-wHd5GCpZ.js} +5 -4
- package/dist/session-clear-wHd5GCpZ.js.map +1 -0
- package/dist/store-D0MhBvg6.d.ts +532 -0
- package/dist/store-D0MhBvg6.d.ts.map +1 -0
- package/dist/store-N8PXxDAS.js.map +1 -1
- package/dist/store-codec-DwrWlgQx.js +65 -0
- package/dist/store-codec-DwrWlgQx.js.map +1 -0
- package/dist/store-memory.d.ts +1 -1
- package/dist/store-memory.d.ts.map +1 -1
- package/dist/store-memory.js +406 -225
- package/dist/store-memory.js.map +1 -1
- package/dist/{store-polling-7WJV0YWP.js → store-polling-BZt2Rf3D.js} +2 -2
- package/dist/{store-polling-7WJV0YWP.js.map → store-polling-BZt2Rf3D.js.map} +1 -1
- package/dist/store-postgres.d.ts +1 -1
- package/dist/store-postgres.d.ts.map +1 -1
- package/dist/store-postgres.js +1598 -587
- package/dist/store-postgres.js.map +1 -1
- package/dist/store-presence-polling-JuAy2Ck2.js +111 -0
- package/dist/store-presence-polling-JuAy2Ck2.js.map +1 -0
- package/dist/store-redis-http.d.ts +1 -1
- package/dist/store-redis-http.js +4 -4
- package/dist/store-redis-notify-CsJw_hfD.js +3060 -0
- package/dist/store-redis-notify-CsJw_hfD.js.map +1 -0
- package/dist/store-redis.d.ts +1 -1
- package/dist/store-redis.d.ts.map +1 -1
- package/dist/store-redis.js +4 -5
- package/dist/store-redis.js.map +1 -1
- package/dist/store-sqlite.d.ts +1 -1
- package/dist/store-sqlite.d.ts.map +1 -1
- package/dist/store-sqlite.js +527 -299
- package/dist/store-sqlite.js.map +1 -1
- package/dist/stream-buffer-DrTyiIuo.js +57 -0
- package/dist/stream-buffer-DrTyiIuo.js.map +1 -0
- package/dist/{telemetry-Cyfqo6Qt.d.ts → telemetry-CWwsh-Gx.d.ts} +3 -3
- package/dist/telemetry-CWwsh-Gx.d.ts.map +1 -0
- package/dist/{wire-BKxH1vbP.js → wire-CV-Z7wz-.js} +6 -5
- package/dist/wire-CV-Z7wz-.js.map +1 -0
- package/docs/01-quickstart.mdx +21 -15
- package/docs/02-examples.mdx +1 -1
- package/docs/actors/01-introduction.mdx +13 -2
- package/docs/actors/02-concurrency.mdx +1 -1
- package/docs/agents/01-introduction.mdx +1 -1
- package/docs/agents/02-tools.mdx +12 -11
- package/docs/agents/03-conversations.mdx +36 -25
- package/docs/agents/04-harnesses.mdx +14 -7
- package/docs/client/03-presence.mdx +18 -14
- package/docs/contract/01-events.mdx +2 -2
- package/docs/contract/02-state.mdx +18 -14
- package/docs/contract/03-sessions.mdx +32 -25
- package/docs/index.mdx +29 -24
- package/docs/reference/01-core.mdx +7 -6
- package/docs/reference/02-server.mdx +250 -191
- package/docs/reference/04-ai.mdx +33 -23
- package/docs/reference/05-telemetry.mdx +27 -13
- package/docs/reference/06-errors.mdx +25 -19
- package/docs/reference/meta.ts +1 -1
- package/docs/schedulers/01-overview.mdx +8 -8
- package/docs/schedulers/02-vercel-queues.mdx +2 -2
- package/docs/schedulers/03-qstash.mdx +4 -4
- package/docs/server/01-consumers.mdx +418 -0
- package/docs/server/02-timers.mdx +31 -21
- package/docs/server/03-cancellation.mdx +52 -44
- package/docs/server/04-routes.mdx +4 -1
- package/docs/server/05-sql-projections.mdx +391 -0
- package/docs/server/06-guarantees.mdx +61 -51
- package/docs/stores/01-overview.mdx +3 -3
- package/docs/stores/02-redis.mdx +24 -10
- package/docs/stores/03-redis-http.mdx +8 -6
- package/docs/stores/04-postgres.mdx +65 -14
- package/docs/stores/05-sqlite.mdx +5 -3
- package/docs/upgrades/01-overview.mdx +31 -0
- package/docs/upgrades/78-0-22.mdx +115 -0
- package/docs/upgrades/79-0-21.mdx +165 -0
- package/docs/upgrades/80-0-20.mdx +43 -0
- package/docs/upgrades/81-0-19.mdx +33 -0
- package/docs/upgrades/82-0-18.mdx +33 -0
- package/docs/upgrades/83-0-17.mdx +69 -0
- package/docs/upgrades/meta.ts +3 -0
- package/examples/playground/DEPLOY.md +39 -7
- package/examples/playground/app/a0/chat.test.tsx +2 -2
- package/examples/playground/app/a0/sandbox-log.ts +17 -15
- package/examples/playground/app/a0/sandbox.ts +58 -13
- package/examples/playground/app/a0/server.ts +4 -4
- package/examples/playground/app/a2slack/a2slack.css +1067 -0
- package/examples/playground/app/a2slack/c/[channel]/page.tsx +20 -0
- package/examples/playground/app/a2slack/consumers/agent.ts +63 -0
- package/examples/playground/app/a2slack/consumers/feed.ts +164 -0
- package/examples/playground/app/a2slack/consumers/notify.ts +116 -0
- package/examples/playground/app/a2slack/consumers/search.ts +37 -0
- package/examples/playground/app/a2slack/consumers/triage.ts +31 -0
- package/examples/playground/app/a2slack/consumers/welcome.ts +25 -0
- package/examples/playground/app/a2slack/db/a2.ts +209 -0
- package/examples/playground/app/a2slack/db/index.ts +73 -0
- package/examples/playground/app/a2slack/db/migrations/0000_extensions.sql +4 -0
- package/examples/playground/app/a2slack/db/migrations/0001_workspace.sql +74 -0
- package/examples/playground/app/a2slack/db/migrations/0002_a2.sql +215 -0
- package/examples/playground/app/a2slack/db/migrations/0003_a2_batch.sql +67 -0
- package/examples/playground/app/a2slack/db/migrations/0004_a2_session_event_ids.sql +2 -0
- package/examples/playground/app/a2slack/db/migrations/meta/0000_snapshot.json +18 -0
- package/examples/playground/app/a2slack/db/migrations/meta/0001_snapshot.json +500 -0
- package/examples/playground/app/a2slack/db/migrations/meta/0002_snapshot.json +1159 -0
- package/examples/playground/app/a2slack/db/migrations/meta/0003_snapshot.json +1159 -0
- package/examples/playground/app/a2slack/db/migrations/meta/0004_snapshot.json +1165 -0
- package/examples/playground/app/a2slack/db/migrations/meta/_journal.json +41 -0
- package/examples/playground/app/a2slack/db/queries.ts +263 -0
- package/examples/playground/app/a2slack/db/schema.ts +145 -0
- package/examples/playground/app/a2slack/drills/a2slack.drill.ts +708 -0
- package/examples/playground/app/a2slack/drills/harness.ts +244 -0
- package/examples/playground/app/a2slack/events/route.ts +36 -0
- package/examples/playground/app/a2slack/identity.ts +44 -0
- package/examples/playground/app/a2slack/inbox/route.ts +8 -0
- package/examples/playground/app/a2slack/lag/data/route.ts +8 -0
- package/examples/playground/app/a2slack/lag/page.tsx +101 -0
- package/examples/playground/app/a2slack/lag.ts +73 -0
- package/examples/playground/app/a2slack/layout.tsx +43 -0
- package/examples/playground/app/a2slack/messages/route.ts +17 -0
- package/examples/playground/app/a2slack/model.test.ts +208 -0
- package/examples/playground/app/a2slack/model.ts +369 -0
- package/examples/playground/app/a2slack/models.test.ts +72 -0
- package/examples/playground/app/a2slack/page.tsx +10 -0
- package/examples/playground/app/a2slack/scheduler/route.ts +5 -0
- package/examples/playground/app/a2slack/search/route.ts +40 -0
- package/examples/playground/app/a2slack/search-index.ts +108 -0
- package/examples/playground/app/a2slack/server.test.ts +315 -0
- package/examples/playground/app/a2slack/server.ts +55 -0
- package/examples/playground/app/a2slack/sessions.ts +50 -0
- package/examples/playground/app/a2slack/thread/route.ts +8 -0
- package/examples/playground/app/a2slack/ui/ChannelView.tsx +87 -0
- package/examples/playground/app/a2slack/ui/Composer.tsx +117 -0
- package/examples/playground/app/a2slack/ui/DotsShadow.tsx +42 -0
- package/examples/playground/app/a2slack/ui/Feed.tsx +157 -0
- package/examples/playground/app/a2slack/ui/InboxContext.tsx +63 -0
- package/examples/playground/app/a2slack/ui/InboxView.tsx +82 -0
- package/examples/playground/app/a2slack/ui/Menu.tsx +128 -0
- package/examples/playground/app/a2slack/ui/MessageRow.tsx +314 -0
- package/examples/playground/app/a2slack/ui/NameGate.tsx +35 -0
- package/examples/playground/app/a2slack/ui/SearchContext.tsx +146 -0
- package/examples/playground/app/a2slack/ui/SearchResults.tsx +97 -0
- package/examples/playground/app/a2slack/ui/Shell.tsx +34 -0
- package/examples/playground/app/a2slack/ui/Sidebar.tsx +199 -0
- package/examples/playground/app/a2slack/ui/ThreadPanel.tsx +134 -0
- package/examples/playground/app/a2slack/ui/useInbox.ts +71 -0
- package/examples/playground/app/agent/server.ts +3 -3
- package/examples/playground/app/chat/[chatId]/chat-client.tsx +1 -1
- package/examples/playground/app/chat/server.ts +45 -35
- package/examples/playground/app/components/demo-icons.tsx +7 -0
- package/examples/playground/app/components/demos.tsx +7 -0
- package/examples/playground/app/orders/[orderId]/order-client.tsx +2 -2
- package/examples/playground/app/orders/page.tsx +1 -1
- package/examples/playground/app/orders/server.ts +49 -30
- package/examples/playground/app/recovery/[recoveryId]/recovery-client.tsx +14 -15
- package/examples/playground/app/recovery/page.tsx +4 -4
- package/examples/playground/app/recovery/server.ts +23 -32
- package/examples/playground/drizzle.config.ts +14 -0
- package/examples/playground/lib/store.ts +1 -1
- package/examples/playground/next.config.ts +6 -1
- package/examples/playground/package.json +12 -1
- package/examples/playground/scripts/migrate.mjs +31 -0
- package/examples/playground/vercel.json +9 -0
- package/examples/playground/vitest.drills.config.ts +32 -0
- package/package.json +5 -2
- package/sql/postgres/drizzle-schema.ts +209 -0
- package/sql/postgres/schema.sql +227 -67
- package/src/actor-shared.ts +10 -2
- package/src/actor.ts +101 -71
- package/src/ai-agent.ts +162 -111
- package/src/ai-control-server.ts +8 -8
- package/src/ai-fx.ts +324 -82
- package/src/ai-harness.ts +17 -2
- package/src/ai-pi.ts +2 -2
- package/src/ai-server.ts +2 -2
- package/src/ai.ts +7 -7
- package/src/client.ts +19 -2
- package/src/contract.ts +1 -1
- package/src/deterministic-id.ts +27 -4
- package/src/errors.ts +8 -5
- package/src/idempotent-replay.ts +0 -1
- package/src/index.ts +1 -1
- package/src/internal.ts +57 -16
- package/src/otel.ts +1 -1
- package/src/postgres-schema.ts +98 -57
- package/src/presence-recovery.ts +18 -11
- package/src/presence-tombstones.ts +52 -0
- package/src/push-envelope.ts +20 -0
- package/src/runtime-consumer.ts +10 -0
- package/src/scheduler-task.ts +2 -2
- package/src/scheduler-vercel.ts +1 -1
- package/src/server.browser.ts +1 -1
- package/src/server.ts +819 -218
- package/src/session-socket.ts +1 -1
- package/src/store-codec.ts +66 -0
- package/src/store-memory.ts +604 -286
- package/src/store-postgres.ts +2006 -857
- package/src/store-presence-polling.ts +59 -34
- package/src/store-redis-core.ts +1961 -1333
- package/src/store-redis-notify.ts +76 -26
- package/src/store-redis.ts +2 -3
- package/src/store-sqlite.ts +915 -516
- package/src/store.ts +241 -102
- package/src/telemetry.ts +2 -1
- package/src/wire.ts +1 -0
- package/dist/actor-C7b-1arn.d.ts.map +0 -1
- package/dist/actor-shared-t7w8xIKP.js.map +0 -1
- package/dist/ai-harness-Clbg5ei7.js.map +0 -1
- package/dist/ai-mMiWul1T.js.map +0 -1
- package/dist/client-BhHLoNKF.js.map +0 -1
- package/dist/contract-BYKASlCV.js.map +0 -1
- package/dist/idempotent-replay-JdgMQRRm.js +0 -21
- package/dist/idempotent-replay-JdgMQRRm.js.map +0 -1
- package/dist/initial-snapshot-Bx-c0pDP.d.ts.map +0 -1
- package/dist/internal-CscSY7K_.js.map +0 -1
- package/dist/server-DYAATFuT.d.ts.map +0 -1
- package/dist/server-qyb5aJJG.js.map +0 -1
- package/dist/session-clear-C-QwTXV1.d.ts.map +0 -1
- package/dist/session-clear-DGI4jTQ5.js.map +0 -1
- package/dist/store-CyLUy86v.d.ts +0 -404
- package/dist/store-CyLUy86v.d.ts.map +0 -1
- package/dist/store-codec-DTG0Ftek.js +0 -10
- package/dist/store-codec-DTG0Ftek.js.map +0 -1
- package/dist/store-presence-polling-B1ixeqIS.js +0 -94
- package/dist/store-presence-polling-B1ixeqIS.js.map +0 -1
- package/dist/store-redis-notify-CM0AfLY0.js +0 -2544
- package/dist/store-redis-notify-CM0AfLY0.js.map +0 -1
- package/dist/stream-buffer-Cx7AAPGq.js +0 -23
- package/dist/stream-buffer-Cx7AAPGq.js.map +0 -1
- package/dist/telemetry-Cyfqo6Qt.d.ts.map +0 -1
- package/dist/wire-BKxH1vbP.js.map +0 -1
- package/docs/server/01-handlers.mdx +0 -227
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
import { useEffect, useRef, useState, type ReactNode } from 'react'
|
|
3
|
+
import { useIdentity } from '../identity'
|
|
4
|
+
import { useWorkspace } from '../sessions'
|
|
5
|
+
|
|
6
|
+
const TYPING_IDLE_MS = 3000
|
|
7
|
+
|
|
8
|
+
export function Composer({
|
|
9
|
+
channel,
|
|
10
|
+
threadId,
|
|
11
|
+
placeholder,
|
|
12
|
+
autoFocus = false,
|
|
13
|
+
compact = false,
|
|
14
|
+
}: {
|
|
15
|
+
channel: string
|
|
16
|
+
threadId?: string
|
|
17
|
+
placeholder: string
|
|
18
|
+
autoFocus?: boolean
|
|
19
|
+
compact?: boolean
|
|
20
|
+
}): ReactNode {
|
|
21
|
+
const identity = useIdentity()
|
|
22
|
+
const { push, presence, setPresence } = useWorkspace()
|
|
23
|
+
const [text, setText] = useState('')
|
|
24
|
+
const areaRef = useRef<HTMLTextAreaElement>(null)
|
|
25
|
+
const idleTimer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined)
|
|
26
|
+
// The workspace session outlives this channel, so leaving it while typing
|
|
27
|
+
// has to say so: nothing else would clear the field.
|
|
28
|
+
const stopRef = useRef<(() => void) | null>(null)
|
|
29
|
+
|
|
30
|
+
const stopTyping = (): void => {
|
|
31
|
+
clearTimeout(idleTimer.current)
|
|
32
|
+
stopRef.current = null
|
|
33
|
+
setPresence({ typing: null })
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
useEffect(
|
|
37
|
+
() => () => {
|
|
38
|
+
clearTimeout(idleTimer.current)
|
|
39
|
+
stopRef.current?.()
|
|
40
|
+
},
|
|
41
|
+
[],
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
const el = areaRef.current
|
|
46
|
+
if (!el) return
|
|
47
|
+
el.style.height = 'auto'
|
|
48
|
+
el.style.height = `${Math.min(el.scrollHeight, 160)}px`
|
|
49
|
+
}, [text])
|
|
50
|
+
|
|
51
|
+
// Everyone typing in this channel but you.
|
|
52
|
+
const typing = Object.entries(presence)
|
|
53
|
+
.filter(([participant]) => participant !== identity?.id)
|
|
54
|
+
.flatMap(([, fields]) => {
|
|
55
|
+
const value = fields.typing?.value
|
|
56
|
+
return value?.channel === channel ? [value.name] : []
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
const canSend = identity !== null && text.trim().length > 0
|
|
60
|
+
|
|
61
|
+
const send = (): void => {
|
|
62
|
+
const trimmed = text.trim()
|
|
63
|
+
if (!identity || !trimmed) return
|
|
64
|
+
push({
|
|
65
|
+
type: 'message.posted',
|
|
66
|
+
payload: {
|
|
67
|
+
channel,
|
|
68
|
+
author: identity,
|
|
69
|
+
text: trimmed,
|
|
70
|
+
...(threadId !== undefined ? { threadId } : {}),
|
|
71
|
+
},
|
|
72
|
+
}).catch(() => {})
|
|
73
|
+
setText('')
|
|
74
|
+
stopTyping()
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
<div className="a2slack-composer" data-compact={compact || undefined}>
|
|
79
|
+
<div className="a2slack-composer-box">
|
|
80
|
+
<textarea
|
|
81
|
+
className="a2slack-composer-textarea"
|
|
82
|
+
ref={areaRef}
|
|
83
|
+
autoFocus={autoFocus}
|
|
84
|
+
rows={1}
|
|
85
|
+
maxLength={2000}
|
|
86
|
+
placeholder={identity ? placeholder : 'pick a name first'}
|
|
87
|
+
disabled={!identity}
|
|
88
|
+
value={text}
|
|
89
|
+
onChange={(e) => {
|
|
90
|
+
setText(e.target.value)
|
|
91
|
+
if (!identity) return
|
|
92
|
+
setPresence({ typing: { name: identity.name, channel } })
|
|
93
|
+
stopRef.current = () => setPresence({ typing: null })
|
|
94
|
+
clearTimeout(idleTimer.current)
|
|
95
|
+
idleTimer.current = setTimeout(stopTyping, TYPING_IDLE_MS)
|
|
96
|
+
}}
|
|
97
|
+
onKeyDown={(e) => {
|
|
98
|
+
if (e.key === 'Enter' && !e.shiftKey) {
|
|
99
|
+
e.preventDefault()
|
|
100
|
+
send()
|
|
101
|
+
}
|
|
102
|
+
}}
|
|
103
|
+
/>
|
|
104
|
+
<div className="a2slack-composer-footer">
|
|
105
|
+
<span className="a2slack-typing">
|
|
106
|
+
{!compact &&
|
|
107
|
+
typing.length > 0 &&
|
|
108
|
+
`${typing.join(', ')} ${typing.length === 1 ? 'is' : 'are'} typing…`}
|
|
109
|
+
</span>
|
|
110
|
+
<button className="a2slack-send" disabled={!canSend} onClick={send}>
|
|
111
|
+
send ⏎
|
|
112
|
+
</button>
|
|
113
|
+
</div>
|
|
114
|
+
</div>
|
|
115
|
+
</div>
|
|
116
|
+
)
|
|
117
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
import { DotGrid } from '@paper-design/shaders-react'
|
|
3
|
+
import { useEffect, useState, type ReactNode } from 'react'
|
|
4
|
+
|
|
5
|
+
function useShaderColors(): { background: string; foreground: string } {
|
|
6
|
+
const [dark, setDark] = useState(
|
|
7
|
+
() => window.matchMedia('(prefers-color-scheme: dark)').matches,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
const query = window.matchMedia('(prefers-color-scheme: dark)')
|
|
12
|
+
const onChange = (): void => setDark(query.matches)
|
|
13
|
+
query.addEventListener('change', onChange)
|
|
14
|
+
return () => query.removeEventListener('change', onChange)
|
|
15
|
+
}, [])
|
|
16
|
+
|
|
17
|
+
return {
|
|
18
|
+
background: dark ? '#04040495' : '#fafafa95',
|
|
19
|
+
foreground: dark ? '#404040' : '#d9d9d9',
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function DotsShadow(): ReactNode {
|
|
24
|
+
const colors = useShaderColors()
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<div className="a2slack-dots" aria-hidden>
|
|
28
|
+
<DotGrid
|
|
29
|
+
colorBack={colors.background}
|
|
30
|
+
colorFill={colors.foreground}
|
|
31
|
+
gapX={16}
|
|
32
|
+
gapY={16}
|
|
33
|
+
opacityRange={0}
|
|
34
|
+
scale={0.2}
|
|
35
|
+
shape="circle"
|
|
36
|
+
size={5}
|
|
37
|
+
sizeRange={0.6}
|
|
38
|
+
style={{ width: '100%', height: '100%' }}
|
|
39
|
+
/>
|
|
40
|
+
</div>
|
|
41
|
+
)
|
|
42
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
import {
|
|
3
|
+
useLayoutEffect,
|
|
4
|
+
useMemo,
|
|
5
|
+
useRef,
|
|
6
|
+
useState,
|
|
7
|
+
type ReactNode,
|
|
8
|
+
} from 'react'
|
|
9
|
+
import {
|
|
10
|
+
liveMessages,
|
|
11
|
+
type ChatMessage,
|
|
12
|
+
type MessagePage,
|
|
13
|
+
} from '@/app/a2slack/model'
|
|
14
|
+
import { useWorkspace } from '../sessions'
|
|
15
|
+
import { MessageRow } from './MessageRow'
|
|
16
|
+
|
|
17
|
+
const dayOf = (at: string): string =>
|
|
18
|
+
new Date(at).toLocaleDateString([], {
|
|
19
|
+
month: 'short',
|
|
20
|
+
day: 'numeric',
|
|
21
|
+
year: 'numeric',
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The channel's messages: the pages read from `/a2slack/messages` while
|
|
26
|
+
* scrolling back, then the newest page, each kept live from the workspace's
|
|
27
|
+
* events. Only the newest page gains new messages.
|
|
28
|
+
*/
|
|
29
|
+
export function Feed({
|
|
30
|
+
channel,
|
|
31
|
+
newest,
|
|
32
|
+
more,
|
|
33
|
+
onOpenThread,
|
|
34
|
+
}: {
|
|
35
|
+
channel: string
|
|
36
|
+
/** The newest page, already live. */
|
|
37
|
+
newest: ChatMessage[]
|
|
38
|
+
/** Older messages exist before the newest page. */
|
|
39
|
+
more: boolean
|
|
40
|
+
onOpenThread: (rootId: string) => void
|
|
41
|
+
}): ReactNode {
|
|
42
|
+
const { events } = useWorkspace()
|
|
43
|
+
// Pages loaded by scrolling back, oldest first.
|
|
44
|
+
const [older, setOlder] = useState<MessagePage[]>([])
|
|
45
|
+
const [loading, setLoading] = useState(false)
|
|
46
|
+
const [failed, setFailed] = useState(false)
|
|
47
|
+
|
|
48
|
+
const feed = useMemo(
|
|
49
|
+
() => [
|
|
50
|
+
...older.flatMap((page) =>
|
|
51
|
+
liveMessages(page, events, { channel, append: false }),
|
|
52
|
+
),
|
|
53
|
+
...newest,
|
|
54
|
+
],
|
|
55
|
+
[older, events, channel, newest],
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
const scrollRef = useRef<HTMLDivElement>(null)
|
|
59
|
+
const pinnedRef = useRef(true)
|
|
60
|
+
// Where the viewport stood when an older page arrived, to keep it there.
|
|
61
|
+
const anchorRef = useRef<{ height: number; top: number } | null>(null)
|
|
62
|
+
const lastId = feed.at(-1)?.id
|
|
63
|
+
|
|
64
|
+
// Follow new messages while pinned to the bottom.
|
|
65
|
+
useLayoutEffect(() => {
|
|
66
|
+
const el = scrollRef.current
|
|
67
|
+
if (el && pinnedRef.current) el.scrollTop = el.scrollHeight
|
|
68
|
+
}, [lastId])
|
|
69
|
+
|
|
70
|
+
// Keep the viewport anchored when an older page prepends above it.
|
|
71
|
+
useLayoutEffect(() => {
|
|
72
|
+
const el = scrollRef.current
|
|
73
|
+
const anchor = anchorRef.current
|
|
74
|
+
if (!el || !anchor) return
|
|
75
|
+
anchorRef.current = null
|
|
76
|
+
el.scrollTop = anchor.top + el.scrollHeight - anchor.height
|
|
77
|
+
}, [older])
|
|
78
|
+
|
|
79
|
+
const oldest = older[0]?.messages[0] ?? newest[0]
|
|
80
|
+
const hasMore = older[0]?.more ?? more
|
|
81
|
+
|
|
82
|
+
const loadEarlier = async (): Promise<void> => {
|
|
83
|
+
if (oldest === undefined || loading) return
|
|
84
|
+
setLoading(true)
|
|
85
|
+
setFailed(false)
|
|
86
|
+
pinnedRef.current = false
|
|
87
|
+
try {
|
|
88
|
+
const response = await fetch(
|
|
89
|
+
`/a2slack/messages?channel=${encodeURIComponent(channel)}&before=${oldest.index}`,
|
|
90
|
+
)
|
|
91
|
+
if (!response.ok) throw new Error(`messages: ${response.status}`)
|
|
92
|
+
const page = (await response.json()) as MessagePage
|
|
93
|
+
const el = scrollRef.current
|
|
94
|
+
if (el) anchorRef.current = { height: el.scrollHeight, top: el.scrollTop }
|
|
95
|
+
setOlder((pages) => [page, ...pages])
|
|
96
|
+
} catch {
|
|
97
|
+
setFailed(true)
|
|
98
|
+
} finally {
|
|
99
|
+
setLoading(false)
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return (
|
|
104
|
+
<div
|
|
105
|
+
ref={scrollRef}
|
|
106
|
+
className="a2slack-feed"
|
|
107
|
+
onScroll={(e) => {
|
|
108
|
+
const el = e.currentTarget
|
|
109
|
+
pinnedRef.current =
|
|
110
|
+
el.scrollHeight - el.scrollTop - el.clientHeight < 40
|
|
111
|
+
}}
|
|
112
|
+
>
|
|
113
|
+
{hasMore && (
|
|
114
|
+
<button
|
|
115
|
+
className="a2slack-older"
|
|
116
|
+
disabled={loading}
|
|
117
|
+
onClick={() => void loadEarlier()}
|
|
118
|
+
>
|
|
119
|
+
{loading
|
|
120
|
+
? 'loading…'
|
|
121
|
+
: failed
|
|
122
|
+
? "↑ earlier messages (didn't load, try again)"
|
|
123
|
+
: '↑ earlier messages'}
|
|
124
|
+
</button>
|
|
125
|
+
)}
|
|
126
|
+
{feed.length === 0 && (
|
|
127
|
+
<p className="a2slack-feed-empty">no messages yet — say something</p>
|
|
128
|
+
)}
|
|
129
|
+
{feed.map((message, i) => {
|
|
130
|
+
const prev = feed[i - 1]
|
|
131
|
+
const newDay =
|
|
132
|
+
prev === undefined || dayOf(prev.at) !== dayOf(message.at)
|
|
133
|
+
const grouped =
|
|
134
|
+
!newDay &&
|
|
135
|
+
prev !== undefined &&
|
|
136
|
+
!prev.deleted &&
|
|
137
|
+
prev.author.id === message.author.id &&
|
|
138
|
+
Date.parse(message.at) - Date.parse(prev.at) < 5 * 60 * 1000
|
|
139
|
+
return (
|
|
140
|
+
<div key={message.id}>
|
|
141
|
+
{newDay && (
|
|
142
|
+
<div className="a2slack-day">
|
|
143
|
+
<span>{dayOf(message.at)}</span>
|
|
144
|
+
<span className="a2slack-day-line" />
|
|
145
|
+
</div>
|
|
146
|
+
)}
|
|
147
|
+
<MessageRow
|
|
148
|
+
message={message}
|
|
149
|
+
grouped={grouped}
|
|
150
|
+
onOpenThread={onOpenThread}
|
|
151
|
+
/>
|
|
152
|
+
</div>
|
|
153
|
+
)
|
|
154
|
+
})}
|
|
155
|
+
</div>
|
|
156
|
+
)
|
|
157
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The inbox, shared by the sidebar (its row and unread badge), the main
|
|
3
|
+
* column (the list), and the thread panel (which marks what it shows read).
|
|
4
|
+
* One page read serves all three.
|
|
5
|
+
*/
|
|
6
|
+
'use client'
|
|
7
|
+
import {
|
|
8
|
+
createContext,
|
|
9
|
+
useContext,
|
|
10
|
+
useRef,
|
|
11
|
+
useState,
|
|
12
|
+
type ReactNode,
|
|
13
|
+
} from 'react'
|
|
14
|
+
import { useIdentity } from '../identity'
|
|
15
|
+
import { useWorkspace } from '../sessions'
|
|
16
|
+
import { useInbox, type Inbox } from './useInbox'
|
|
17
|
+
|
|
18
|
+
export type InboxPanel = Inbox & {
|
|
19
|
+
/** The list shows in the main column. */
|
|
20
|
+
open: boolean
|
|
21
|
+
setOpen: (open: boolean) => void
|
|
22
|
+
/** Marks a thread's notifications read, through its latest one. */
|
|
23
|
+
markRead: (threadId: string) => void
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const InboxContext = createContext<InboxPanel | null>(null)
|
|
27
|
+
|
|
28
|
+
export function useInboxPanel(): InboxPanel {
|
|
29
|
+
const inbox = useContext(InboxContext)
|
|
30
|
+
if (inbox === null) throw new Error('useInboxPanel needs an InboxProvider')
|
|
31
|
+
return inbox
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function InboxProvider({
|
|
35
|
+
children,
|
|
36
|
+
}: {
|
|
37
|
+
children: ReactNode
|
|
38
|
+
}): ReactNode {
|
|
39
|
+
const identity = useIdentity()
|
|
40
|
+
const { push } = useWorkspace()
|
|
41
|
+
const inbox = useInbox(identity?.id ?? null)
|
|
42
|
+
const [open, setOpen] = useState(false)
|
|
43
|
+
// What this tab has already marked, so a re-render before the read lands
|
|
44
|
+
// doesn't send it again.
|
|
45
|
+
const marked = useRef(new Map<string, number>())
|
|
46
|
+
|
|
47
|
+
const markRead = (threadId: string): void => {
|
|
48
|
+
const item = inbox.items.find((other) => other.threadId === threadId)
|
|
49
|
+
if (!identity || !item || item.unread === 0) return
|
|
50
|
+
if ((marked.current.get(threadId) ?? -1) >= item.index) return
|
|
51
|
+
marked.current.set(threadId, item.index)
|
|
52
|
+
push({
|
|
53
|
+
type: 'inbox.read',
|
|
54
|
+
payload: { by: identity.id, threadId, through: item.index },
|
|
55
|
+
}).catch(() => {})
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<InboxContext value={{ ...inbox, open, setOpen, markRead }}>
|
|
60
|
+
{children}
|
|
61
|
+
</InboxContext>
|
|
62
|
+
)
|
|
63
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
import type { ReactNode } from 'react'
|
|
3
|
+
import Link from 'next/link'
|
|
4
|
+
import type { InboxItem } from '@/app/a2slack/model'
|
|
5
|
+
import { useInboxPanel } from './InboxContext'
|
|
6
|
+
|
|
7
|
+
const whenOf = (at: string): string =>
|
|
8
|
+
new Date(at).toLocaleString([], {
|
|
9
|
+
month: 'short',
|
|
10
|
+
day: 'numeric',
|
|
11
|
+
hour: '2-digit',
|
|
12
|
+
minute: '2-digit',
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
const REASON: Record<InboxItem['reason'], string> = {
|
|
16
|
+
mention: 'mentioned you',
|
|
17
|
+
reply: "replied in a thread you're in",
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** The main column while the inbox is open: one row per thread with news. */
|
|
21
|
+
export function InboxView(): ReactNode {
|
|
22
|
+
const inbox = useInboxPanel()
|
|
23
|
+
if (!inbox.open) return null
|
|
24
|
+
|
|
25
|
+
const status = inbox.loading
|
|
26
|
+
? 'loading…'
|
|
27
|
+
: inbox.error !== null
|
|
28
|
+
? 'unavailable'
|
|
29
|
+
: `${inbox.unread} unread`
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<section
|
|
33
|
+
className="a2slack-results"
|
|
34
|
+
aria-label="inbox"
|
|
35
|
+
onKeyDown={(e) => {
|
|
36
|
+
if (e.key === 'Escape') inbox.setOpen(false)
|
|
37
|
+
}}
|
|
38
|
+
>
|
|
39
|
+
<header className="a2slack-header">
|
|
40
|
+
<span className="a2slack-header-name">inbox</span>
|
|
41
|
+
<span className="a2slack-status" role="status">
|
|
42
|
+
{status}
|
|
43
|
+
</span>
|
|
44
|
+
</header>
|
|
45
|
+
<div className="a2slack-results-list">
|
|
46
|
+
{inbox.error !== null ? (
|
|
47
|
+
<p className="a2slack-results-hint">{inbox.error}</p>
|
|
48
|
+
) : (
|
|
49
|
+
!inbox.loading &&
|
|
50
|
+
inbox.items.length === 0 && (
|
|
51
|
+
<p className="a2slack-results-hint">
|
|
52
|
+
nothing yet — mentions and replies in your threads land here
|
|
53
|
+
</p>
|
|
54
|
+
)
|
|
55
|
+
)}
|
|
56
|
+
{inbox.items.map((item) => (
|
|
57
|
+
<Link
|
|
58
|
+
key={item.threadId}
|
|
59
|
+
href={`/a2slack/c/${item.channel}?thread=${encodeURIComponent(item.threadId)}`}
|
|
60
|
+
className="a2slack-result"
|
|
61
|
+
data-unread={item.unread > 0 || undefined}
|
|
62
|
+
onClick={() => {
|
|
63
|
+
inbox.markRead(item.threadId)
|
|
64
|
+
inbox.setOpen(false)
|
|
65
|
+
}}
|
|
66
|
+
>
|
|
67
|
+
<div className="a2slack-meta">
|
|
68
|
+
<span className="a2slack-result-channel">#{item.channel}</span>
|
|
69
|
+
<span className="a2slack-author">{item.from.name}</span>
|
|
70
|
+
<span className="a2slack-time">{REASON[item.reason]}</span>
|
|
71
|
+
<span className="a2slack-time">{whenOf(item.at)}</span>
|
|
72
|
+
{item.unread > 0 && (
|
|
73
|
+
<span className="a2slack-unread">{item.unread}</span>
|
|
74
|
+
)}
|
|
75
|
+
</div>
|
|
76
|
+
<p className="a2slack-result-text">{item.text}</p>
|
|
77
|
+
</Link>
|
|
78
|
+
))}
|
|
79
|
+
</div>
|
|
80
|
+
</section>
|
|
81
|
+
)
|
|
82
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
import { Menu as BaseMenu } from '@base-ui/react/menu'
|
|
3
|
+
import { ContextMenu as BaseContextMenu } from '@base-ui/react/context-menu'
|
|
4
|
+
import { useCallback, useState, type ReactNode } from 'react'
|
|
5
|
+
import { DotsShadow } from './DotsShadow'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Menus portal into the page root, not the body, so they keep a2slack's
|
|
9
|
+
* tokens. Returns a ref for the trigger and the root it found.
|
|
10
|
+
*/
|
|
11
|
+
function usePageRoot(): [
|
|
12
|
+
(element: Element | null) => void,
|
|
13
|
+
HTMLElement | null,
|
|
14
|
+
] {
|
|
15
|
+
const [root, setRoot] = useState<HTMLElement | null>(null)
|
|
16
|
+
const ref = useCallback((element: Element | null) => {
|
|
17
|
+
if (element) setRoot(element.closest<HTMLElement>('.a2slack-page'))
|
|
18
|
+
}, [])
|
|
19
|
+
return [ref, root]
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function Popup({ children }: { children: ReactNode }): ReactNode {
|
|
23
|
+
return (
|
|
24
|
+
<div className="a2slack-menu-wrapper">
|
|
25
|
+
<BaseMenu.Popup className="a2slack-menu-popup">{children}</BaseMenu.Popup>
|
|
26
|
+
<DotsShadow />
|
|
27
|
+
</div>
|
|
28
|
+
)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function Menu({
|
|
32
|
+
trigger,
|
|
33
|
+
children,
|
|
34
|
+
onOpenChange,
|
|
35
|
+
triggerClassName = 'a2slack-menu-trigger',
|
|
36
|
+
}: {
|
|
37
|
+
trigger: ReactNode
|
|
38
|
+
children: ReactNode
|
|
39
|
+
onOpenChange?: (open: boolean) => void
|
|
40
|
+
/** Replaces the trigger's own look. */
|
|
41
|
+
triggerClassName?: string
|
|
42
|
+
}): ReactNode {
|
|
43
|
+
const [ref, root] = usePageRoot()
|
|
44
|
+
return (
|
|
45
|
+
<BaseMenu.Root {...(onOpenChange ? { onOpenChange } : {})}>
|
|
46
|
+
<BaseMenu.Trigger ref={ref} className={triggerClassName}>
|
|
47
|
+
{trigger}
|
|
48
|
+
</BaseMenu.Trigger>
|
|
49
|
+
<BaseMenu.Portal container={root}>
|
|
50
|
+
<BaseMenu.Positioner
|
|
51
|
+
className="a2slack-menu-positioner"
|
|
52
|
+
align="end"
|
|
53
|
+
sideOffset={4}
|
|
54
|
+
>
|
|
55
|
+
<Popup>{children}</Popup>
|
|
56
|
+
</BaseMenu.Positioner>
|
|
57
|
+
</BaseMenu.Portal>
|
|
58
|
+
</BaseMenu.Root>
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function ContextMenuArea({
|
|
63
|
+
menu,
|
|
64
|
+
children,
|
|
65
|
+
onOpenChange,
|
|
66
|
+
}: {
|
|
67
|
+
menu: ReactNode
|
|
68
|
+
children: ReactNode
|
|
69
|
+
onOpenChange?: (open: boolean) => void
|
|
70
|
+
}): ReactNode {
|
|
71
|
+
const [ref, root] = usePageRoot()
|
|
72
|
+
return (
|
|
73
|
+
<BaseContextMenu.Root {...(onOpenChange ? { onOpenChange } : {})}>
|
|
74
|
+
<BaseContextMenu.Trigger ref={ref}>{children}</BaseContextMenu.Trigger>
|
|
75
|
+
<BaseContextMenu.Portal container={root}>
|
|
76
|
+
<BaseContextMenu.Positioner className="a2slack-menu-positioner">
|
|
77
|
+
<Popup>{menu}</Popup>
|
|
78
|
+
</BaseContextMenu.Positioner>
|
|
79
|
+
</BaseContextMenu.Portal>
|
|
80
|
+
</BaseContextMenu.Root>
|
|
81
|
+
)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function MenuItem({
|
|
85
|
+
onClick,
|
|
86
|
+
danger,
|
|
87
|
+
children,
|
|
88
|
+
}: {
|
|
89
|
+
onClick: () => void
|
|
90
|
+
danger?: boolean
|
|
91
|
+
children: ReactNode
|
|
92
|
+
}): ReactNode {
|
|
93
|
+
return (
|
|
94
|
+
<BaseMenu.Item
|
|
95
|
+
className="a2slack-menu-item"
|
|
96
|
+
{...(danger === true ? { 'data-danger': '' } : {})}
|
|
97
|
+
onClick={onClick}
|
|
98
|
+
>
|
|
99
|
+
{children}
|
|
100
|
+
</BaseMenu.Item>
|
|
101
|
+
)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function MenuSeparator(): ReactNode {
|
|
105
|
+
return <BaseMenu.Separator className="a2slack-menu-separator" />
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function MenuEmojiRow({
|
|
109
|
+
emojis,
|
|
110
|
+
onPick,
|
|
111
|
+
}: {
|
|
112
|
+
emojis: readonly string[]
|
|
113
|
+
onPick: (emoji: string) => void
|
|
114
|
+
}): ReactNode {
|
|
115
|
+
return (
|
|
116
|
+
<div className="a2slack-emoji-row">
|
|
117
|
+
{emojis.map((emoji) => (
|
|
118
|
+
<BaseMenu.Item
|
|
119
|
+
key={emoji}
|
|
120
|
+
className="a2slack-menu-item a2slack-emoji-item"
|
|
121
|
+
onClick={() => onPick(emoji)}
|
|
122
|
+
>
|
|
123
|
+
{emoji}
|
|
124
|
+
</BaseMenu.Item>
|
|
125
|
+
))}
|
|
126
|
+
</div>
|
|
127
|
+
)
|
|
128
|
+
}
|