Files
Compass/web/components/comments/comment-input.tsx
Okechi Jones-Williams 549eba0bbe [User functionality] Messages UI page and messaging helpers added for end-to-end scenarios (#53)
* Added Database checks to the onboarding flow

* Added compatibility page setup
Added more compatibility questions

* Finished up the onboarding flow suite
Added compatibility question tests and verifications
Updated tests to cover Keywords and Headline changes recently made
Updated tests to cover all of the big5 personality traits

* .

* Fix: Merge conflict

* .

* Fix: Added fix for None discriptive error issue #36
Updated signUp.spec.ts to use new fixture
Updated Account information variable names
Deleted "deleteUserFixture.ts" as it was incorporated into the "base.ts" file

* Linting and Prettier

* Minor cleaning

* Added Google account to the Onboarding flow

* Added account cleanup for google accounts

* Started work on Sign-in tests
Updated seedDatabase.ts to throw an error if the user already exists, to also add display names and usernames so they seedUser func acts like a normal basic user
Some organising of the google auth code

* Linting and Prettier

* Added checks to the deleteUser func to check if the accout exists
Added account deletion checks

* Linting and Prettier

* Formatting update, fixed homePage locator for signin

* .

* .

* .

* Coderabbitai fix's

* Fix

* Improve test utilities and stabilize onboarding flow tests

* Changes requested

* Changed POM/Fixture structure to use an app class to instantiate the page objects

* Apply suggestion from @MartinBraquet

* Delete .vscode/settings.json

* Apply suggestion from @MartinBraquet

* Apply suggestion from @MartinBraquet

* Apply suggestion from @MartinBraquet

* Linting and Prettier

* Updated People page

* Fix app.ts

* Updated peoplePage.ts: continued adding functions to use filters
Updated filters.tsx: added data testid

* Coderabbitai fix's

* .

* Updated People page
Added data test attributes to search.tsx and profile-grid.tsx

* Lint and Prettier

* .

* Continued work on filter tests
Added testid attributes to people page so the info from the results can be accessed

* Added more filter tests

* Added tests for hiding profiles

* Added a context manager to test with multiple accounts interacting with each other
Added an option to verify the users email when creating an account

* Added Tests for sending/recieving messages
Added tests for staring/favoriting profiles
Added testIds where necessary
Added messagesPage.ts
Updated peoplePage.ts

* Linting and Prettier

* Coderabbit suggestions

* CodeRabbit suggestion #2

* Fix #1

* Minor fixes

* TC fix

---------

Co-authored-by: MartinBraquet <martin.braquet@gmail.com>
2026-05-29 01:00:57 +02:00

246 lines
7.2 KiB
TypeScript

import {PaperAirplaneIcon} from '@heroicons/react/24/solid'
import {Editor} from '@tiptap/react'
import clsx from 'clsx'
import {MAX_COMMENT_LENGTH, ReplyToUserInfo} from 'common/comment'
import {User} from 'common/user'
import {useEffect, useState} from 'react'
import toast from 'react-hot-toast'
import {BiRepost} from 'react-icons/bi'
import {Tooltip} from 'web/components/widgets/tooltip'
import {useEvent} from 'web/hooks/use-event'
import {useUser} from 'web/hooks/use-user'
import {firebaseLogin} from 'web/lib/firebase/users'
import {useT} from 'web/lib/locale'
import {track} from 'web/lib/service/analytics'
import {safeLocalStorage} from 'web/lib/util/local'
import {Row} from '../layout/row'
import {Avatar} from '../widgets/avatar'
import {TextEditor, useTextEditor} from '../widgets/editor'
import {LoadingIndicator} from '../widgets/loading-indicator'
export function CommentInput(props: {
replyToUserInfo?: ReplyToUserInfo
// Reply to another comment
parentCommentId?: string
onSubmitComment: (editor: Editor, type: CommentType) => Promise<void>
// unique id for autosave
pageId: string
className?: string
blocked?: boolean
placeholder?: string
commentTypes: CommentType[]
onClearInput?: () => void
}) {
const {
parentCommentId,
replyToUserInfo,
onSubmitComment,
pageId,
className,
blocked,
placeholder = 'Write a comment...',
commentTypes,
onClearInput,
} = props
const user = useUser()
const key = `comment ${pageId} ${parentCommentId ?? ''}`
const [isSubmitting, setIsSubmitting] = useState(false)
const editor = useTextEditor({
key,
size: 'sm',
max: MAX_COMMENT_LENGTH,
placeholder,
className: isSubmitting ? '!text-ink-400' : '',
})
const submitComment = useEvent(async (type: CommentType) => {
if (!editor || editor.isEmpty || isSubmitting) return
setIsSubmitting(true)
if (!user) {
track('sign in to comment')
await firebaseLogin()
setIsSubmitting(false)
return
}
editor.commands.focus('end')
// if last item is text, try to linkify it by adding and deleting a space
if (editor.state.selection.empty) {
editor.commands.insertContent(' ')
const endPos = editor.state.selection.from
editor.commands.deleteRange({from: endPos - 1, to: endPos})
}
try {
await onSubmitComment?.(editor, type)
editor.commands.clearContent(true)
// force clear save, because it can fail if editor unrenders
safeLocalStorage?.removeItem(`text ${key}`)
onClearInput?.()
} catch (e) {
console.error(e)
toast.error('Error submitting. Try again?')
} finally {
setIsSubmitting(false)
}
})
if (user?.isBannedFromPosting) return <></>
return blocked ? (
<div className={'text-ink-500 mb-3 text-sm'}>
You blocked the creator or they blocked you, so you can't comment.
</div>
) : (
<Row className={clsx(className, 'mb-2 w-full gap-1 sm:gap-2')}>
<Avatar avatarUrl={user?.avatarUrl} username={user?.username} size="sm" />
<CommentInputTextArea
editor={editor}
replyTo={replyToUserInfo}
user={user}
submit={submitComment}
isSubmitting={isSubmitting}
commentTypes={commentTypes}
/>
</Row>
)
}
const emojiMenuActive = (view: {state: any}) => {
const regex = /^emoji\$.*$/ // emoji$ can have random numbers following it....❤️ tiptap
let active = false
for (const key in view.state) {
if (regex.test(key)) {
active = (view.state as any)[key].active
if (active) break
}
}
return active
}
export type CommentType = 'comment' | 'repost'
export function CommentInputTextArea(props: {
user: User | undefined | null
replyTo?: {id: string; username: string}
editor: Editor | null
submit?: (type: CommentType) => void
cancelEditing?: () => void
isSubmitting: boolean
submitOnEnter?: boolean
isDisabled?: boolean
isEditing?: boolean
commentTypes?: CommentType[]
maxHeight?: string
}) {
const {
user,
submitOnEnter,
editor,
submit,
isSubmitting,
isEditing,
replyTo,
commentTypes = ['comment'],
cancelEditing,
isDisabled,
maxHeight,
} = props
const t = useT()
useEffect(() => {
editor?.setEditable(!isSubmitting)
}, [isSubmitting, editor])
useEffect(() => {
if (!editor) return
// Submit on ctrl+enter or mod+enter key
editor.setOptions({
editorProps: {
handleKeyDown: (view, event) => {
if (
event.key === 'Enter' &&
!event.shiftKey &&
(!submitOnEnter ? event.ctrlKey || event.metaKey : true) &&
// mention list is closed
!(view.state as any).mention$.active &&
// emoji list is closed
!emojiMenuActive(view) &&
!isDisabled
) {
// console.log('handleKeyDown')
submit?.(commentTypes[0])
event.preventDefault()
return true
}
return false
},
},
})
}, [editor, submit, submitOnEnter, commentTypes])
useEffect(() => {
if (!editor) return
// insert at mention and focus
if (replyTo && editor.isEmpty) {
editor
.chain()
.setContent({
type: 'mention',
attrs: {label: replyTo.username, id: replyTo.id},
})
.insertContent(' ')
.focus(undefined, {scrollIntoView: false})
.run()
}
}, [replyTo, editor])
return (
<TextEditor editor={editor} maxHeight={maxHeight} simple hideEmbed>
<Row className={''}>
{user && !isSubmitting && submit && commentTypes.includes('repost') && (
<Tooltip text={'Post question & comment to your followers'} className={'mt-2'}>
<button
disabled={isDisabled || !editor || editor.isEmpty}
className="text-ink-500 hover:text-ink-700 active:bg-ink-300 disabled:text-ink-300 px-2 transition-colors"
onClick={() => submit('repost')}
>
<BiRepost className="h-7 w-7" />
</button>
</Tooltip>
)}
{isEditing && (
<Row className="border-ink-200 bg-canvas-50 text-ink-600 items-center justify-between gap-2 border-t px-3 py-2 text-sm">
{/*<span>Editing message</span>*/}
<button
type="button"
className="text-primary-600 hover:underline"
onClick={cancelEditing}
>
{t('comment.cancel', 'Cancel')}
</button>
</Row>
)}
{!isSubmitting && !isDisabled && submit && commentTypes.includes('comment') && (
<button
data-testid="conversation-message-submit"
className="text-ink-500 hover:text-ink-700 active:bg-ink-300 disabled:text-ink-300 px-4 transition-colors"
disabled={isDisabled || !editor || editor.isEmpty}
onClick={() => submit('comment')}
>
<PaperAirplaneIcon className="m-0 h-[25px] w-[22px] p-0" />
</button>
)}
{submit && isSubmitting && (
<LoadingIndicator size={'md'} className={'px-4'} spinnerClassName="border-ink-500" />
)}
</Row>
</TextEditor>
)
}