Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
2c189e4
feat: add an emoji picker
pauldambra Jul 14, 2025
49da1b4
Update UI snapshots for `chromium` (16)
github-actions[bot] Jul 14, 2025
1b97dd8
upgrde icons
pauldambra Jul 15, 2025
9dc174d
fix
pauldambra Jul 15, 2025
7708c37
fix
pauldambra Jul 15, 2025
7386c06
Update UI snapshots for `chromium` (16)
github-actions[bot] Jul 15, 2025
dd80c4e
Update UI snapshots for `chromium` (15)
github-actions[bot] Jul 15, 2025
2d7130b
fix
pauldambra Jul 15, 2025
c73d7a9
fix
pauldambra Jul 15, 2025
b80e88c
fix
pauldambra Jul 15, 2025
8482daf
Update UI snapshots for `chromium` (15)
github-actions[bot] Jul 15, 2025
cb96f7e
fix
pauldambra Jul 15, 2025
521f203
Update UI snapshots for `chromium` (16)
github-actions[bot] Jul 15, 2025
72484da
Merge branch 'master' into feat/pd/em-pi
pauldambra Jul 15, 2025
45d643b
Merge branch 'master' into feat/pd/em-pi
pauldambra Jul 15, 2025
33f4c54
fix
pauldambra Jul 15, 2025
08c2ce8
Update UI snapshots for `chromium` (16)
github-actions[bot] Jul 15, 2025
21f171f
fix
pauldambra Jul 15, 2025
5df56a4
Merge branch 'master' into feat/pd/em-pi
pauldambra Jul 15, 2025
4023963
fix
pauldambra Jul 15, 2025
688ff7c
fix
pauldambra Jul 15, 2025
fc9add5
fix
pauldambra Jul 15, 2025
4970527
fix
pauldambra Jul 15, 2025
4fd06b6
fix
pauldambra Jul 15, 2025
d1bc8f1
emoji picker in quick emoji row
pauldambra Jul 15, 2025
2159da0
new icons
pauldambra Jul 15, 2025
1aee986
fix
pauldambra Jul 15, 2025
0c344c4
Update UI snapshots for `chromium` (16)
github-actions[bot] Jul 15, 2025
cb38499
Update UI snapshots for `chromium` (15)
github-actions[bot] Jul 15, 2025
caea4fd
loading state
pauldambra Jul 15, 2025
7565cd2
put emoji where the cursor is
pauldambra Jul 15, 2025
959d541
Merge branch 'master' into feat/pd/em-pi
pauldambra Jul 15, 2025
5f36d2f
Merge branch 'master' into feat/pd/em-pi
pauldambra Jul 16, 2025
8dd1efd
Merge branch 'master' into feat/pd/em-pi
pauldambra Jul 16, 2025
87f4292
fix
pauldambra Jul 16, 2025
affb6a6
fix
pauldambra Jul 16, 2025
9502b7d
Update UI snapshots for `chromium` (2)
github-actions[bot] Jul 16, 2025
610bda8
Update UI snapshots for `chromium` (2)
github-actions[bot] Jul 16, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@
"fast-deep-equal": "^3.1.3",
"fastpriorityqueue": "^0.7.5",
"fflate": "^0.7.4",
"frimousse": "^0.3.0",
"fuse.js": "^6.6.2",
"heatmap.js": "^2.0.5",
"hls.js": "^1.5.15",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { Meta, StoryFn, StoryObj } from '@storybook/react'

import { EmojiPickerPopover } from 'lib/components/EmojiPicker/EmojiPickerPopover'

type Story = StoryObj<typeof EmojiPickerPopover>
const meta: Meta<typeof EmojiPickerPopover> = {
title: 'Lemon UI/Emoji Picker Popover',
component: EmojiPickerPopover,
tags: ['autodocs'],
parameters: {
docs: {
description: {
component: 'A component that opens a popover emoji picker.',
},
},
},
argTypes: {
onSelect: {
description: 'The function to run when a user chooses an emoji',
},
Comment thread
pauldambra marked this conversation as resolved.
defaultOpen: {
description: 'Whether to start with the popover open - defaults to false',
},
},
}
export default meta

const BasicTemplate: StoryFn<typeof EmojiPickerPopover> = (props) => {
return (
<div className="w-[325px] h-[370px] border rounded">
<EmojiPickerPopover {...props} />
</div>
)
}

export const Default: Story = BasicTemplate.bind({})
Default.args = {}

export const Open: Story = BasicTemplate.bind({})
Open.args = {
defaultOpen: true,
}
102 changes: 102 additions & 0 deletions frontend/src/lib/components/EmojiPicker/EmojiPickerPopover.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import {
EmojiPicker,
EmojiPickerListCategoryHeaderProps,
EmojiPickerListEmojiProps,
EmojiPickerListRowProps,
} from 'frimousse'
import { LemonButton } from 'lib/lemon-ui/LemonButton'
import { Popover } from 'lib/lemon-ui/Popover'
import { useState } from 'react'
import { IconEmojiAdd } from '@posthog/icons'

const EmojiPickerCategoryHeader = ({ category, ...props }: EmojiPickerListCategoryHeaderProps): JSX.Element => (
<div className="bg-bg-light px-3 pt-3 pb-1.5 font-medium text-neutral-600 text-sm" {...props}>
{category.label}
</div>
)

const EmojiPickerEmojiRow = ({ children, ...props }: EmojiPickerListRowProps): JSX.Element => (
<div className="scroll-my-1.5 px-1.5" {...props}>
{children}
</div>
)

const EmojiPickerEmojiButton = ({ emoji, ...props }: EmojiPickerListEmojiProps): JSX.Element => (
<button
data-attr="emoji-picker-button"
className="flex items-center justify-center rounded-md text-xl size-8 data-[active]:bg-secondary-3000-hover"
Comment thread
pauldambra marked this conversation as resolved.
{...props}
>
{emoji.emoji}
</button>
)

export interface EmojiPickerPopoverProps {
/**
* The action to take when a user selects an emoji
* receives the emoji as a string
*/
onSelect: (s: string) => void
/**
* Whether to start with the popover open or closed
* Defaults to false (closed)
*/
defaultOpen?: boolean
/**
* the data-attr to set on the button that opens and closes the popover
*/
'data-attr'?: string
}

export function EmojiPickerPopover({
onSelect,
defaultOpen = false,
'data-attr': dataAttr,
}: EmojiPickerPopoverProps): JSX.Element {
const [emojiPickerOpen, setEmojiPickerOpen] = useState(defaultOpen)

return (
<Popover
onClickOutside={() => setEmojiPickerOpen(false)}
// prefer the bottom, but will fall back to other positions based on space
placement="bottom-start"
visible={emojiPickerOpen}
overlay={
<EmojiPicker.Root
className="isolate flex h-[368px] w-fit flex-col bg-bg-light"
onEmojiSelect={({ emoji }) => {
onSelect(emoji)
setEmojiPickerOpen(false)
}}
>
<EmojiPicker.Search className="z-10 mx-2 mt-2 appearance-none rounded bg-fill-input px-2.5 py-2 text-sm border" />
<EmojiPicker.Viewport className="relative flex-1 outline-hidden">
<EmojiPicker.Loading className="absolute inset-0 flex items-center justify-center text-tertiary text-sm">
Loading…
</EmojiPicker.Loading>
<EmojiPicker.Empty className="absolute inset-0 flex items-center justify-center text-tertiary text-sm">
No emoji found.
</EmojiPicker.Empty>
<EmojiPicker.List
className="select-none pb-1.5"
components={{
CategoryHeader: EmojiPickerCategoryHeader,
Row: EmojiPickerEmojiRow,
Emoji: EmojiPickerEmojiButton,
Comment thread
pauldambra marked this conversation as resolved.
}}
/>
</EmojiPicker.Viewport>
</EmojiPicker.Root>
}
>
<LemonButton
data-attr={dataAttr}
icon={<IconEmojiAdd className="text-lg" />}
onClick={() => {
setEmojiPickerOpen(!emojiPickerOpen)
}}
size="small"
/>
</Popover>
)
}
2 changes: 1 addition & 1 deletion frontend/src/lib/lemon-ui/LemonTextArea/LemonTextArea.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ export const LemonTextArea = React.forwardRef<HTMLTextAreaElement, LemonTextArea
/>
{hasFooter ? (
<div className="flex flex-row gap-x-2 justify-between border-l border-r border-b rounded-b px-1">
<div className="flex flex-row gap-x-1 items-center">{actions}</div>
<div className="flex flex-row items-center">{actions}</div>
<div className="flex flex-row gap-x-1 items-center">
<div className="flex flex-row gap-x-1 justify-end flex-grow">
{rightFooter}
Expand Down
62 changes: 45 additions & 17 deletions frontend/src/lib/lemon-ui/LemonTextArea/LemonTextAreaMarkdown.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { useValues } from 'kea'
import { useActions, useValues } from 'kea'
import { TextContent } from 'lib/components/Cards/TextCard/TextCard'
import { useUploadFiles } from 'lib/hooks/useUploadFiles'
import { IconMarkdown, IconUploadFile } from 'lib/lemon-ui/icons'
import { IconMarkdown } from 'lib/lemon-ui/icons'
import { IconImage } from '@posthog/icons'
import { LemonFileInput } from 'lib/lemon-ui/LemonFileInput'
import { LemonTabs } from 'lib/lemon-ui/LemonTabs'
import { LemonTextArea, LemonTextAreaProps } from 'lib/lemon-ui/LemonTextArea/LemonTextArea'
Expand All @@ -10,10 +11,14 @@ import { Tooltip } from 'lib/lemon-ui/Tooltip'
import posthog from 'posthog-js'
import React, { useRef, useState } from 'react'
import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic'
import { EmojiPickerPopover } from 'lib/components/EmojiPicker/EmojiPickerPopover'
import { LemonButton } from 'lib/lemon-ui/LemonButton'
import { emojiUsageLogic } from 'lib/lemon-ui/LemonTextArea/emojiUsageLogic'

export const LemonTextAreaMarkdown = React.forwardRef<HTMLTextAreaElement, LemonTextAreaProps>(
function LemonTextAreaMarkdown({ value, onChange, className, ...editAreaProps }, ref): JSX.Element {
const { objectStorageAvailable } = useValues(preflightLogic)
const { emojiUsed } = useActions(emojiUsageLogic)

const [isPreviewShown, setIsPreviewShown] = useState(false)
const dropRef = useRef<HTMLDivElement>(null)
Expand Down Expand Up @@ -65,23 +70,46 @@ export const LemonTextAreaMarkdown = React.forwardRef<HTMLTextAreaElement, Lemon
loading={uploading}
value={filesToUpload}
callToAction={
objectStorageAvailable ? (
<Tooltip title="Click here or drag and drop to upload images">
<div className="rounded hover:bg-fill-button-tertiary-hover px-1 py-0.5">
{' '}
<IconUploadFile className="text-xl" />
</div>
</Tooltip>
) : (
<Tooltip title="Enable object storage to add images by dragging and dropping">
<div className="rounded px-1 py-0.5">
{' '}
<IconUploadFile className="text-xl" />
</div>
</Tooltip>
)
<LemonButton
size="small"
icon={<IconImage className="text-lg" />}
disabledReason={
objectStorageAvailable
? undefined
: 'Enable object storage to add images by dragging and dropping'
}
tooltip={
objectStorageAvailable
? 'Click here or drag and drop to upload images'
: null
}
/>
}
/>,
<EmojiPickerPopover
key="emoj-picker"
data-attr="lemon-text-area-markdown-emoji-popover"
onSelect={(emoji: string) => {
if (ref && 'current' in ref && ref.current) {
const textArea = ref.current
const cursorStart = textArea.selectionStart || 0
const cursorEnd = textArea.selectionEnd || 0
const textBefore = (value || '').slice(0, cursorStart)
const textAfter = (value || '').slice(cursorEnd)
const spaceBefore = textBefore.endsWith(' ') ? '' : ' '
const spaceAfter = textAfter.startsWith(' ') ? '' : ' '
const newValue =
textBefore + spaceBefore + emoji + spaceAfter + textAfter
onChange?.(newValue)
// Restore cursor position after the inserted emoji
setTimeout(() => {
textArea.selectionStart = textArea.selectionEnd =
cursorStart + spaceBefore.length + emoji.length
}, 0)
}
emojiUsed(emoji)
}}
/>,
]}
/>
</div>
Expand Down
70 changes: 70 additions & 0 deletions frontend/src/lib/lemon-ui/LemonTextArea/emojiUsageLogic.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { actions, kea, reducers, path, selectors } from 'kea'
import { now } from 'lib/dayjs'

import { permanentlyMount } from 'lib/utils/kea-logic-builders'

import type { emojiUsageLogicType } from './emojiUsageLogicType'

export const defaultQuickEmojis = ['💖', '👍', '🤔', '👎', '🌶️']

export const emojiUsageLogic = kea<emojiUsageLogicType>([
path(['lib', 'lemon-ui', 'LemonTextArea', 'emojiUsage', 'logic']),
actions({
emojiUsed: (emoji: string) => ({ emoji }),
}),
reducers({
usedEmojis: [
{} as Record<string, number[]>,
{ persist: true },
{
emojiUsed: (state, { emoji }) => {
const currentTime = now().valueOf()
const thirtyDaysAgo = currentTime - 30 * 24 * 60 * 60 * 1000

const newState = { ...state, [emoji]: state[emoji] || [] }
newState[emoji] = [...newState[emoji], currentTime]

newState[emoji] = newState[emoji].filter((timestamp: number) => timestamp > thirtyDaysAgo)

if (newState[emoji].length === 0) {
delete newState[emoji]
} else {
// Limit to max 10 timestamps per emoji to prevent memory issues
if (newState[emoji].length > 10) {
newState[emoji] = newState[emoji].slice(-10)
}
}

return newState
},
},
],
}),
selectors({
favouriteEmojis: [
(s) => [s.usedEmojis],
(usedEmojis): string[] => {
// Get user's favorite emojis sorted by usage count
const userFavorites = Object.entries(usedEmojis)
.map(([emoji, timestamps]) => ({
emoji,
count: timestamps.length,
}))
.sort((a, b) => b.count - a.count) // Sort by usage count descending
.slice(0, 5) // Take top 5
.map(({ emoji }) => emoji) // Extract just the emoji strings

// If we have fewer than 5 favorites, fill with quickEmojis (avoiding duplicates)
if (userFavorites.length < 5) {
const remainingSlots = 5 - userFavorites.length
const availableQuickEmojis = defaultQuickEmojis.filter((emoji) => !userFavorites.includes(emoji))
const fillEmojis = availableQuickEmojis.slice(0, remainingSlots)
return [...userFavorites, ...fillEmojis]
}

return userFavorites
},
],
}),
permanentlyMount(),
])
6 changes: 5 additions & 1 deletion frontend/src/lib/lemon-ui/icons/categories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ export const TECHNOLOGY = {
'IconChat',
'IconThoughtBubble',
'IconChatHelp',
'IconComment',
],
Hardware: [
'IconCompass',
Expand Down Expand Up @@ -167,6 +168,7 @@ export const TECHNOLOGY = {
'IconMouseScrollDown',
'IconDrag',
'IconPointer',
'IconImage',
],
}

Expand All @@ -191,6 +193,7 @@ export const ELEMENTS = {
'IconSort',
'IconSortAlpha',
'IconExternal',
'IconEmojiAdd',
],
Symbols: [
'IconLock',
Expand All @@ -209,6 +212,7 @@ export const ELEMENTS = {
'IconHide',
'IconStopFilled',
'IconListCheck',
'IconEmoji',
],
'Arrows & Shapes': [
'IconArrowLeft',
Expand Down Expand Up @@ -288,7 +292,7 @@ export const TEAMS_AND_COMPANIES = {
'Feature Success': ['IconFlask', 'IconTestTube', 'IconMultivariateTesting', 'IconSplitTesting', 'IconBalance'],
Pipeline: ['IconWebhooks', 'IconDecisionTree'],
'Product OS': ['IconNotebook', 'IconHogQL', 'IconDashboard', 'IconSupport'],
Logos: ['IconLogomark', 'IconGithub'],
Logos: ['IconLogomark', 'IconGithub', 'IconLinear'],
ErrorTracking: ['IconIssue'],
LLMObservability: ['IconLlmObservability', 'IconLlmPromptEvaluation', 'IconLlmPromptManagement'],
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@ import { notebookNodeLogic } from './notebookNodeLogic'
import { JSONContent, NotebookNodeProps, NotebookNodeAttributeProperties } from '../Notebook/utils'
import { SessionRecordingsPlaylist } from 'scenes/session-recordings/playlist/SessionRecordingsPlaylist'
import { sessionRecordingPlayerLogic } from 'scenes/session-recordings/player/sessionRecordingPlayerLogic'
import { IconComment } from 'lib/lemon-ui/icons'
import { sessionRecordingPlayerLogicType } from 'scenes/session-recordings/player/sessionRecordingPlayerLogicType'
import { RecordingsUniversalFiltersEmbed } from 'scenes/session-recordings/filters/RecordingsUniversalFiltersEmbed'
import { PostHogErrorBoundary } from 'posthog-js/react'
import { IconComment } from '@posthog/icons'

const Component = ({
attributes,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,8 @@ import { notebookNodeLogic } from './notebookNodeLogic'
import { LemonSwitch } from '@posthog/lemon-ui'
import { JSONContent, NotebookNodeProps, NotebookNodeAttributeProperties } from '../Notebook/utils'
import { asDisplay } from 'scenes/persons/person-utils'
import { IconComment } from 'lib/lemon-ui/icons'
import { NotFound } from 'lib/components/NotFound'
import { IconPerson } from '@posthog/icons'
import { IconComment, IconPerson } from '@posthog/icons'
import { UUID_REGEX_MATCH_GROUPS } from './utils'

const HEIGHT = 500
Expand Down
Loading
Loading