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: retract vote in poll #828

Merged
merged 5 commits into from
Apr 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -13,6 +13,7 @@ import { SaveMessageAction } from './SaveMessageAction';
import { useGetUser } from '@/hooks/useGetUser';
import { ShareAction } from './ShareAction';
import { EmojiAction } from './EmojiAction';
import { RetractVoteAction } from './RetractVoteAction';
import MessagePreview from './MessagePreview';

interface MessageActionModalProps {
Expand Down Expand Up @@ -77,13 +78,19 @@ export const MessageActionModal = ({ selectedMessage, onDismiss }: MessageAction
<IonIcon slot="start" icon={returnDownBackOutline} />
<IonLabel className='font-semibold'>Reply</IonLabel>
</IonItem> */}
<ShareAction message={selectedMessage} onSuccess={onDismiss} />
<CopyAction message={selectedMessage} onSuccess={onDismiss} />

{selectedMessage.message_type === 'Poll' &&
<RetractVoteAction message={selectedMessage} onSuccess={onDismiss} />}
{selectedMessage.message_type !== 'Poll' &&
<ShareAction message={selectedMessage} onSuccess={onDismiss} />}

{selectedMessage.message_type !== 'Poll' &&
<CopyAction message={selectedMessage} onSuccess={onDismiss} />}

<SaveMessageAction message={selectedMessage} onSuccess={onDismiss} />

{isOwnMessage &&
<DeleteAction message={selectedMessage} onSuccess={onDismiss} />
}
<DeleteAction message={selectedMessage} onSuccess={onDismiss} />}
{/* <IonItem className='py-1'>
<IonIcon slot="start" icon={documentAttachOutline} />
<IonLabel className='font-semibold'>Link to document</IonLabel>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Message } from '../../../../../../types/Messaging/Message'
import { Message, PollMessage } from '../../../../../../types/Messaging/Message'
import { UserFields } from '@/utils/users/UserListProvider'
import { MessageSenderAvatar } from '../chat-view/MessageBlock'
import { IonText } from '@ionic/react'
Expand All @@ -25,6 +25,8 @@ const MessagePreview = ({ message, user }: MessagePreview) => {
</div>}
{message.message_type === 'File' && <p
className='text-sm font-semibold'>📎 &nbsp;{message.file?.split('/')[3]}</p>}
{message.message_type === 'Poll' && <p className="text-sm font-semibold line-clamp-2 flex items-center">
📊 &nbsp;Poll: {(message as PollMessage).content?.split("\n")?.[0]}</p>}
</div>
</div>
)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { useFrappeGetCall, useFrappePostCall } from 'frappe-react-sdk'
import { ActionIcon, ActionItem, ActionLabel, ActionProps } from './common'
import { arrowUndoOutline } from 'ionicons/icons'
import { useIonToast } from '@ionic/react'
import { Poll } from '../chat-view/MessageBlock'

export const RetractVoteAction = ({ message, onSuccess }: ActionProps) => {

const [present] = useIonToast()

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

const { call } = useFrappePostCall('raven.api.raven_poll.retract_vote')
const onRetractVote = () => {
return call({
poll_id: message?.poll_id,
}).then(() => {
present({
position: 'bottom',
color: 'success',
duration: 600,
message: 'Vote retracted',
})
onSuccess()
}).catch(() => {
present({
color: 'danger',
duration: 600,
message: "Error: Could not retract vote",
})
})
}

if (data && data.message?.current_user_votes.length > 0)
return (
<ActionItem onClick={onRetractVote}>
<ActionIcon icon={arrowUndoOutline} />
<ActionLabel label='Retract vote' />
</ActionItem>
)

return null
}
Original file line number Diff line number Diff line change
Expand Up @@ -301,7 +301,7 @@ const ReplyBlock = ({ message }: { message: Message }) => {
</div>
}

interface Poll {
export interface Poll {
'poll': RavenPoll,
'current_user_votes': { 'option': string }[]
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { HiReply } from 'react-icons/hi'
import { FrappeConfig, FrappeContext } from 'frappe-react-sdk'
import { useMessageCopy } from './useMessageCopy'
import { useToast } from '@/hooks/useToast'
import { RetractVote } from './RetractVote'

export interface MessageContextMenuProps {
message?: Message | null,
Expand All @@ -25,6 +26,9 @@ export const MessageContextMenu = ({ message, onDelete, onEdit, onReply }: Messa
return (
<ContextMenu.Content>
{message ? <>

{message && message.message_type === 'Poll' && <RetractVote message={message} />}

<ContextMenu.Item onClick={onReply}>
<Flex gap='2'>
<HiReply size='18' />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { ContextMenu, Flex } from '@radix-ui/themes'
import { useFrappeGetCall, useFrappePostCall } from 'frappe-react-sdk'
import { TiArrowBackOutline } from 'react-icons/ti'
import { Poll } from '../Renderers/PollMessage'
import { Message } from '../../../../../../../types/Messaging/Message'
import { toast } from '@/hooks/useToast'

interface RetractVoteProps {
message: Message
}

export const RetractVote = ({ message }: RetractVoteProps) => {

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

const { call } = useFrappePostCall('raven.api.raven_poll.retract_vote')
const onRetractVote = () => {
return call({
poll_id: message?.poll_id,
}).then(() => {
toast({
title: 'Vote retracted',
variant: 'accent',
duration: 800,
})
}).catch(() => {
toast({
title: 'Could not retract vote',
variant: 'destructive',
duration: 800,
})
})
}

if (data && data.message?.current_user_votes.length > 0)
return (
<>
<ContextMenu.Item onClick={onRetractVote} disabled={data.message?.poll.is_disabled ? true : false}>
<Flex gap='2'>
<TiArrowBackOutline size='18' />
Retract vote
</Flex>
</ContextMenu.Item>
<ContextMenu.Separator />
</>
)

return null
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
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 { 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 { useFrappeDocumentEventListener, useFrappeGetCall, useFrappePostCall } from "frappe-react-sdk"
import { RavenPoll } from "@/types/RavenMessaging/RavenPoll"
import { ErrorBanner } from "@/components/layout/AlertBanner"
import { RavenPollOption } from "@/types/RavenMessaging/RavenPollOption"
Expand All @@ -14,12 +14,12 @@ interface PollMessageBlockProps extends BoxProps {
user?: UserFields,
}

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

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

// fetch poll data using message_id
const { data, error, mutate } = useFrappeGetCall<{ message: Poll }>('raven.api.raven_poll.get_poll', {
Expand All @@ -40,7 +40,7 @@ export const PollMessageBlock = memo(({ message, user, ...props }: PollMessageBl
{data && <PollMessageBox data={data.message} messageID={message.name} />}
</Box>
)
})
}

const PollMessageBox = ({ data, messageID }: { data: Poll, messageID: string }) => {
return (
Expand Down Expand Up @@ -132,15 +132,13 @@ const PollOption = ({ data, option }: { data: Poll, option: RavenPollOption }) =

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',
Expand Down Expand Up @@ -168,7 +166,6 @@ const SingleChoicePoll = ({ data, messageID }: { data: Poll, messageID: string }
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) => {
Expand All @@ -185,7 +182,6 @@ const MultiChoicePoll = ({ data, messageID }: { data: Poll, messageID: string })
'message_id': messageID,
'option_id': selectedOptions
}).then(() => {
mutate(`poll_data_${data.poll.name}`)
toast({
title: "Your vote has been submitted!",
variant: 'success',
Expand Down
24 changes: 12 additions & 12 deletions raven/api/chat.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,18 +19,18 @@ def get_channel_members(channel_id):
if frappe.get_cached_value("Raven Channel", channel_id, "type") == "Open":
# select all users, if channel member exists, get is_admin
member_query = (
frappe.qb.from_(user)
.join(channel_member, JoinType.left)
.on((user.name == channel_member.user_id) & (channel_member.channel_id == channel_id))
.select(
user.name,
user.full_name,
user.user_image,
user.first_name,
user.type,
channel_member.is_admin,
)
.orderby(channel_member.creation, order=Order.desc)
frappe.qb.from_(user)
.join(channel_member, JoinType.left)
.on((user.name == channel_member.user_id) & (channel_member.channel_id == channel_id))
.select(
user.name,
user.full_name,
user.user_image,
user.first_name,
user.type,
channel_member.is_admin,
)
.orderby(channel_member.creation, order=Order.desc)
)
else:
member_query = (
Expand Down
14 changes: 14 additions & 0 deletions raven/api/raven_poll.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,3 +113,17 @@ def add_vote(message_id, option_id):
).insert()

return "Vote added successfully."


@frappe.whitelist(methods=["POST"])
def retract_vote(poll_id):
# delete all votes by the user for the poll (this takes care of the case where the user has voted for multiple options in the same poll)
user = frappe.session.user
votes = frappe.get_all(
"Raven Poll Vote", filters={"poll_id": poll_id, "user_id": user}, fields=["name"]
)
if not votes:
frappe.throw(_("You have not voted for any option in this poll."))
else:
for vote in votes:
frappe.delete_doc("Raven Poll Vote", vote.name)
2 changes: 1 addition & 1 deletion raven/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ def raven_poll_vote_has_permission(doc, user=None, ptype=None):
Allowed users can add a vote to a poll and read votes (if the poll is not anonymous)
"""

if ptype in ["read", "create"]:
if ptype in ["read", "create", "delete"]:
if doc.owner == user:
return True
elif user == "Administrator":
Expand Down
Loading
Loading