Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: polls #776

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
ff48d4d
added doctypes for Poll, Poll Option and Poll Vote
janhvipatil Mar 22, 2024
d5a3688
added API to fetch poll and add vote
janhvipatil Mar 22, 2024
63c1bc9
remove votes on poll delete
janhvipatil Mar 22, 2024
70ddeee
Merge branch 'paginate-messages' into rav-128-setup-required-doctypes…
janhvipatil Mar 22, 2024
d58add2
chore: updated APIs to fetch poll ID
janhvipatil Mar 22, 2024
1b6fb34
added permissions
janhvipatil Mar 22, 2024
a12033a
added create poll modal
janhvipatil Mar 29, 2024
4293697
added API to create a poll
janhvipatil Mar 29, 2024
963cffc
updated styles for poll block, added total vote count
janhvipatil Mar 29, 2024
104e3de
UI for poll results
janhvipatil Mar 31, 2024
bff0d42
added functionality for multi choice polls
janhvipatil Apr 1, 2024
ab15acb
Merge branch 'develop' into rav-128-setup-required-doctypes-for-creat…
janhvipatil Apr 1, 2024
c7b0232
updated permission for anonymous poll
janhvipatil Apr 1, 2024
8b6e4b8
Merge branch 'develop' into rav-128-setup-required-doctypes-for-creat…
nikkothari22 Apr 2, 2024
d2958c5
fix: publish event after commit
nikkothari22 Apr 2, 2024
3ef26e3
fix: calculation of multi-select poll
nikkothari22 Apr 2, 2024
18a38ac
fix: fire realtime event when poll created
nikkothari22 Apr 2, 2024
84cd592
fix: disable add options button after 10 options
janhvipatil Apr 2, 2024
35f2d64
fix: added badge to show if poll is anonymous
janhvipatil Apr 2, 2024
852c392
Merge branch 'develop' into rav-128-setup-required-doctypes-for-creat…
nikkothari22 Apr 2, 2024
d70bbef
fix: formatting
nikkothari22 Apr 2, 2024
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
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { ToolbarFileProps } from './Tiptap'
import { Flex, IconButton, Inset, Popover, Separator } from '@radix-ui/themes'
import { Loader } from '@/components/common/Loader'
import { Suspense, lazy } from 'react'
import { CreatePoll } from '../../polls/CreatePoll'
import { HiOutlineGif } from "react-icons/hi2";
import { GIFPicker } from '@/components/common/GIFPicker/GIFPicker'

Expand All @@ -21,9 +22,10 @@ type RightToolbarButtonsProps = {
* Component to render the right toolbar buttons:
* 1. User Mention
* 2. Channel Mention
* 3. Emoji picker
* 4. File upload
* 5. Send button
* 3. Poll creation
* 4. Emoji picker
* 5. File upload
* 6. Send button
* @param props
* @returns
*/
Expand All @@ -32,6 +34,8 @@ export const RightToolbarButtons = ({ fileProps, ...sendProps }: RightToolbarBut
<Flex gap='2' align='center' px='1' py='1'>
<MentionButtons />
<Separator orientation='vertical' />
<CreatePollButton />
<Separator orientation='vertical' />
<Flex gap='3' align='center'>
<EmojiPickerButton />
<GIFPickerButton />
Expand Down Expand Up @@ -215,4 +219,13 @@ const SendButton = ({ sendMessage, messageSending, setContent }: {
<BiSolidSend {...ICON_PROPS} />
}
</IconButton>
}

const CreatePollButton = () => {

const { editor } = useCurrentEditor()

return <CreatePoll
buttonStyle={DEFAULT_BUTTON_STYLE}
isDisabled={editor?.isEditable === false} />
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export const MessageContextMenu = ({ message, onDelete, onEdit, onReply }: Messa
Reply
</Flex>
</ContextMenu.Item>
<ContextMenu.Separator />
{/* <ContextMenu.Separator /> */}
<ContextMenu.Group>
{message.message_type === 'Text' &&
<ContextMenu.Item onClick={copy}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { BsFillCircleFill } from 'react-icons/bs'
import { MessageReactions } from './MessageReactions'
import { ImageMessageBlock } from './Renderers/ImageMessage'
import { FileMessageBlock } from './Renderers/FileMessage'
import { PollMessageBlock } from './Renderers/PollMessage'
import { TiptapRenderer } from './Renderers/TiptapRenderer/TiptapRenderer'
import { QuickActions } from './MessageActions/QuickActions/QuickActions'
import { memo, useMemo, useState } from 'react'
Expand Down Expand Up @@ -76,7 +77,7 @@ export const MessageItem = ({ message, setDeleteMessage, isHighlighted, onReplyM
onMouseLeave={onMouseLeave}
// disabled={!isHoveredDebounced}
className={clsx(`group
hover:bg-gray-100
hover:bg-gray-2
hover:transition-all
hover:delay-100
dark:hover:bg-gray-3
Expand Down Expand Up @@ -244,5 +245,6 @@ export const MessageContent = ({ message, user, ...props }: MessageContentProps)
}} user={user} /> : null}
{message.message_type === 'Image' && <ImageMessageBlock message={message} user={user} />}
{message.message_type === 'File' && <FileMessageBlock message={message} user={user} />}
{message.message_type === 'Poll' && <PollMessageBlock message={message} user={user} />}
</Box>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
import { Box, Checkbox, Flex, Text, RadioGroup, Button, Badge } from "@radix-ui/themes"
import { BoxProps } from "@radix-ui/themes/dist/cjs/components/box"
import { memo, useEffect, useMemo, useState } from "react"
import { UserFields } from "../../../../../utils/users/UserListProvider"
import { PollMessage } from "../../../../../../../types/Messaging/Message"
import { useFrappeDocumentEventListener, useFrappeGetCall, useFrappePostCall, useSWRConfig } from "frappe-react-sdk"
import { RavenPoll } from "@/types/RavenMessaging/RavenPoll"
import { ErrorBanner } from "@/components/layout/AlertBanner"
import { RavenPollOption } from "@/types/RavenMessaging/RavenPollOption"
import { useToast } from "@/hooks/useToast"

interface PollMessageBlockProps extends BoxProps {
message: PollMessage,
user?: UserFields,
}

interface Poll {
'poll': RavenPoll,
'current_user_votes': { 'option': string }[]
}

export const PollMessageBlock = memo(({ message, user, ...props }: PollMessageBlockProps) => {

// fetch poll data using message_id
const { data, error, mutate } = useFrappeGetCall<{ message: Poll }>('raven.api.raven_poll.get_poll', {
'message_id': message.name,
}, `poll_data_${message.poll_id}`, {
revalidateOnFocus: false,
revalidateIfStale: false,
revalidateOnReconnect: false
})

useFrappeDocumentEventListener('Raven Poll', message.poll_id, () => {
mutate()
})

return (
<Box {...props} pt='1'>
<ErrorBanner error={error} />
{data && <PollMessageBox data={data.message} messageID={message.name} />}
</Box>
)
})

const PollMessageBox = ({ data, messageID }: { data: Poll, messageID: string }) => {
return (
<Flex align='center' gap='4' p='2' className="bg-gray-2
shadow-sm
dark:bg-gray-3
group-hover:bg-accent-a2
dark:group-hover:bg-gray-4
group-hover:transition-all
group-hover:delay-100
min-w-64
w-full
rounded-md">
<Flex direction='column' gap='2' p='2' className="w-full">
<Flex justify='between' align='center' gap='2'>
<Text size='2' weight={'medium'}>{data.poll.question}</Text>
{data.poll.is_anonymous ? <Badge color='blue' className={'w-fit'}>Anonymous</Badge> : null}
</Flex>
{data.current_user_votes.length > 0 ?
<PollResults data={data} /> :
<>
{data.poll.is_multi_choice ?
<MultiChoicePoll data={data} messageID={messageID} /> :
<SingleChoicePoll data={data} messageID={messageID} />
}
</>
}
{data.poll.is_disabled ? <Badge color="gray" className={'w-fit'}>Poll is now closed</Badge> : null}
</Flex>
</Flex>
)
}

const PollResults = ({ data }: { data: Poll }) => {
return (
<Flex direction='column' gap='2' className="w-full">
{data.poll.options.map(option => {
return <PollOption key={option.name} data={data} option={option} />
})}
<Text as='span' size='1' color='gray' className="px-2">{data.poll.total_votes} vote{data.poll.total_votes > 1 ? 's' : ''}</Text>
</Flex>
)
}

const PollOption = ({ data, option }: { data: Poll, option: RavenPollOption }) => {

const getPercentage = (votes: number) => {
if (data.poll.is_multi_choice) {
const totalVotes = data.poll.options.reduce((acc, opt) => acc + (opt.votes ?? 0), 0)
return (votes / totalVotes) * 100
} else return (votes / data.poll.total_votes) * 100
}

// State to track whether the animation should be triggered
const [triggerAnimation, setTriggerAnimation] = useState<boolean>(false)

// Use useEffect to trigger animation after the component is mounted
useEffect(() => {
setTriggerAnimation(true)
}, [])

const isCurrentUserVote = useMemo(() => {
return data.current_user_votes.some(vote => vote.option === option.name)
}, [data.current_user_votes, option.name])

const percentage = useMemo(() => {
return getPercentage(option.votes ?? 0)
}, [option.votes])

const width = `${percentage}%`

return (
<Flex key={option.name} justify='between' align='center' width='100%' className={'relative'}>
<Box position='absolute' top='0' left='0'
data-is-current-user-vote={isCurrentUserVote}
className={`bg-gray-5
dark:bg-gray-6
h-full
rounded-sm
data-[is-current-user-vote=true]:bg-accent-a5
dark:data-[is-current-user-vote=true]:bg-accent-a6`}
style={{ width: triggerAnimation ? width : 0, transition: 'width 0.5s ease-in-out' }}>
</Box>
<Text as='span' size='2' className="px-2 py-1 z-10" weight={isCurrentUserVote ? 'bold' : 'regular'}>{option.option}</Text>
<Text as='span' size='2' className="px-2 py-1 z-10" weight={isCurrentUserVote ? 'bold' : 'regular'}>{percentage.toFixed(1)}%</Text>
</Flex>
)
}

const SingleChoicePoll = ({ data, messageID }: { data: Poll, messageID: string }) => {

const { mutate } = useSWRConfig()
const { call } = useFrappePostCall('raven.api.raven_poll.add_vote')
const { toast } = useToast()
const onVoteSubmit = async (option: RavenPollOption) => {
return call({
'message_id': messageID,
'option_id': option.name
}).then(() => {
mutate(`poll_data_${data.poll.name}`)
toast({
title: "Your vote has been submitted!",
variant: 'success',
duration: 800
})
})
}

return (
<RadioGroup.Root>
{data.poll.options.map(option => (
<div key={option.name}>
<Text as="label" size="2">
<Flex gap="2" p='2' className="rounded-sm hover:bg-accent-a2 dark:hover:bg-gray-5">
<RadioGroup.Item disabled={data.poll.is_disabled ? true : false} value={option.name} onClick={() => onVoteSubmit(option)} />
{option.option}
</Flex>
</Text>
</div>
))}
</RadioGroup.Root>
)
}

const MultiChoicePoll = ({ data, messageID }: { data: Poll, messageID: string }) => {

const [selectedOptions, setSelectedOptions] = useState<string[]>([])
const { mutate } = useSWRConfig()
const { toast } = useToast()

const handleCheckboxChange = (name: string, value: boolean | string) => {
if (value) {
setSelectedOptions((opts) => [...opts, name])
} else {
setSelectedOptions((opts) => opts.filter(opt => opt !== name))
}
}

const { call } = useFrappePostCall('raven.api.raven_poll.add_vote')
const onVoteSubmit = async () => {
return call({
'message_id': messageID,
'option_id': selectedOptions
}).then(() => {
mutate(`poll_data_${data.poll.name}`)
toast({
title: "Your vote has been submitted!",
variant: 'success',
duration: 800
})
})
}

return (
<div>
{data.poll.options.map(option => (
<div key={option.name}>
<Text as="label" size="2">
<Flex gap="2" p='2' className="rounded-sm hover:bg-accent-a2 dark:hover:bg-gray-5">
<Checkbox disabled={data.poll.is_disabled ? true : false} value={option.name} onCheckedChange={(v) => handleCheckboxChange(option.name, v)} />
{option.option}
</Flex>
</Text>
</div>
))}
<Flex justify={'between'} align={'center'} gap={'2'}>
<Text size='1' className="text-gray-500">To view the poll results, please submit your choice(s)</Text>
<Button disabled={data.poll.is_disabled ? true : false} size={'1'} variant={'soft'} style={{ alignSelf: 'flex-end' }} onClick={onVoteSubmit}>Submit</Button>
</Flex>
</div>
)
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { FileMessage, Message, TextMessage } from "../../../../../../../types/Messaging/Message"
import { FileMessage, Message, PollMessage, TextMessage } from "../../../../../../../types/Messaging/Message"
import { Box, Flex, Separator, Text } from "@radix-ui/themes"
import { useGetUser } from "@/hooks/useGetUser"
import { DateMonthAtHourMinuteAmPm } from "@/utils/dateConversions"
Expand All @@ -7,6 +7,7 @@ import { getFileExtension, getFileName } from "@/utils/operations"
import { FlexProps } from "@radix-ui/themes/dist/cjs/components/flex"
import { clsx } from "clsx"
import parse from 'html-react-parser';
import { MdOutlineBarChart } from "react-icons/md"
interface ReplyMessageBoxProps extends FlexProps {
message: Partial<Message>
}
Expand All @@ -29,13 +30,18 @@ export const ReplyMessageBox = ({ message, children, className, ...props }: Repl
</Text>
</Flex>
<Box className="max-w-3xl">
{['File', 'Image'].includes(message.message_type ?? 'Text') ?
<Flex gap='2' align='center'>
{message.message_type === 'File' && message.file && <FileExtensionIcon ext={getFileExtension(message.file)} size='18' />}
{message.message_type === 'Image' && <img src={message.file} alt={`Image sent by ${message.owner}`} height='30' width='30' className="object-cover rounded-md" />}
<Text as='span' size='2'>{getFileName((message as FileMessage).file)}</Text>
</Flex>
: <Text as='span' size='2' className="line-clamp-2">{parse((message as TextMessage).content ?? '')}</Text>
{message.message_type === 'Poll' ? <Text as='span' size='2' className="line-clamp-2 flex items-center">
<MdOutlineBarChart size='14' className="inline mr-1" />
Poll: {(message as PollMessage).content?.split("\n")?.[0]}</Text>
:
['File', 'Image'].includes(message.message_type ?? 'Text') ?
<Flex gap='2' align='center'>
{message.message_type === 'File' && message.file && <FileExtensionIcon ext={getFileExtension(message.file)} size='18' />}
{message.message_type === 'Image' && <img src={message.file} alt={`Image sent by ${message.owner}`} height='30' width='30' className="object-cover rounded-md" />}

<Text as='span' size='2'>{getFileName((message as FileMessage).file)}</Text>
</Flex>
: <Text as='span' size='2' className="line-clamp-2">{parse((message as TextMessage).content ?? '')}</Text>
}
</Box>
</Flex>
Expand Down
Loading
Loading