diff --git a/mobile/src/components/features/chat-space/MessageActions/MessageActionModal.tsx b/mobile/src/components/features/chat-space/MessageActions/MessageActionModal.tsx index ca9c1dd9b..859de3554 100644 --- a/mobile/src/components/features/chat-space/MessageActions/MessageActionModal.tsx +++ b/mobile/src/components/features/chat-space/MessageActions/MessageActionModal.tsx @@ -13,8 +13,8 @@ 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'; +import { PollActions } from './PollActions'; interface MessageActionModalProps { selectedMessage?: Message, @@ -79,8 +79,8 @@ export const MessageActionModal = ({ selectedMessage, onDismiss }: MessageAction Reply */} - {selectedMessage.message_type === 'Poll' && - } + {selectedMessage.message_type === 'Poll' && } + {selectedMessage.message_type !== 'Poll' && } diff --git a/mobile/src/components/features/chat-space/MessageActions/PollActions.tsx b/mobile/src/components/features/chat-space/MessageActions/PollActions.tsx new file mode 100644 index 000000000..667d98814 --- /dev/null +++ b/mobile/src/components/features/chat-space/MessageActions/PollActions.tsx @@ -0,0 +1,47 @@ +import { useFrappeGetCall } from 'frappe-react-sdk' +import { ActionIcon, ActionItem, ActionLabel, ActionProps } from './common' +import { Poll } from '../chat-view/MessageBlock' +import { useState } from 'react' +import { peopleOutline } from 'ionicons/icons' +import { RetractVoteAction } from './RetractVoteAction' +import { ViewPollVotes } from '../../polls/ViewPollVotes' + +export const PollActions = ({ message, onSuccess }: ActionProps) => { + + // 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 + }) + + if (data && data.message) { + return <> + {data.message.current_user_votes.length > 0 && } + {data.message.poll.is_anonymous !== 1 && } + + } + + return null +} + +const ViewPollVotesAction = ({ poll }: { poll: Poll }) => { + + const [isOpen, setIsOpen] = useState(false) + + return ( + <> + setIsOpen(true)}> + + + + setIsOpen(false)} + poll={poll} + /> + + ) +} \ No newline at end of file diff --git a/mobile/src/components/features/chat-space/MessageActions/RetractVoteAction.tsx b/mobile/src/components/features/chat-space/MessageActions/RetractVoteAction.tsx index bede8f480..050238455 100644 --- a/mobile/src/components/features/chat-space/MessageActions/RetractVoteAction.tsx +++ b/mobile/src/components/features/chat-space/MessageActions/RetractVoteAction.tsx @@ -1,22 +1,12 @@ -import { useFrappeGetCall, useFrappePostCall } from 'frappe-react-sdk' +import { 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({ @@ -38,13 +28,10 @@ export const RetractVoteAction = ({ message, onSuccess }: ActionProps) => { }) } - if (data && data.message?.current_user_votes.length > 0) - return ( - - - - - ) - - return null + return ( + + + + + ) } \ No newline at end of file diff --git a/mobile/src/components/features/chat-space/chat-view/MessageBlock.tsx b/mobile/src/components/features/chat-space/chat-view/MessageBlock.tsx index a93fd44d0..4b5f2302e 100644 --- a/mobile/src/components/features/chat-space/chat-view/MessageBlock.tsx +++ b/mobile/src/components/features/chat-space/chat-view/MessageBlock.tsx @@ -13,7 +13,7 @@ import { useLongPress } from "@uidotdev/usehooks"; import MessageReactions from './components/MessageReactions' import parse from 'html-react-parser'; import clsx from 'clsx' -import { Avatar, Badge, Box, Button, Checkbox, Flex, RadioGroup, Text, Theme } from '@radix-ui/themes' +import { Avatar, Badge, Box, Button, Checkbox, Flex, RadioGroup, Separator, Text, Theme } from '@radix-ui/themes' import { useGetUser } from '@/hooks/useGetUser' import { generateAvatarColor, getInitials } from '@/components/common/UserAvatar' import { RiRobot2Fill } from 'react-icons/ri' @@ -21,6 +21,7 @@ import { useFrappeDocumentEventListener, useFrappeGetCall, useFrappePostCall, us import { RavenPoll } from '@/types/RavenMessaging/RavenPoll' import { RavenPollOption } from '@/types/RavenMessaging/RavenPollOption' import { MdOutlineBarChart } from 'react-icons/md' +import { ViewPollVotes } from '../../polls/ViewPollVotes' type Props = { message: Message, @@ -80,8 +81,10 @@ export const NonContinuationMessageBlock = ({ message, onMessageSelect, isScroll const { user, isActive } = useGetUserDetails(message.is_bot_message && message.bot ? message.bot : message.owner) + const [disableLongPress, setDisableLongPress] = useState(false) const longPressEvent = useLongPress((e) => { if (isScrolling) return + if (disableLongPress) return Haptics.impact({ style: ImpactStyle.Medium }) @@ -100,7 +103,11 @@ export const NonContinuationMessageBlock = ({ message, onMessageSelect, isScroll {isBot && Bot} {DateObjectToTimeString(message.creation)} - + setDisableLongPress(true)} + onLongPressEnabled={() => setDisableLongPress(false)} + onReplyMessageClick={onReplyMessageClick} /> {message.is_edited === 1 && (edited)} @@ -149,8 +156,10 @@ interface ContinuationMessageBlockProps { } const ContinuationMessageBlock = ({ message, onMessageSelect, isScrolling, isHighlighted, onReplyMessageClick }: ContinuationMessageBlockProps) => { + const [disableLongPress, setDisableLongPress] = useState(false) const longPressEvent = useLongPress((e) => { if (isScrolling) return + if (disableLongPress) return Haptics.impact({ style: ImpactStyle.Medium }) @@ -163,7 +172,11 @@ const ContinuationMessageBlock = ({ message, onMessageSelect, isScrolling, isHig
- + setDisableLongPress(true)} + onLongPressEnabled={() => setDisableLongPress(false)} + onReplyMessageClick={onReplyMessageClick} /> {message.is_edited === 1 && (edited)}
@@ -175,7 +188,13 @@ const ContinuationMessageBlock = ({ message, onMessageSelect, isScrolling, isHig } -const MessageContent = ({ message, onReplyMessageClick }: { message: Message, onReplyMessageClick: (messageID: string) => void }) => { +interface MessageContentProps { + message: Message, + onReplyMessageClick: (messageID: string) => void + onLongPressDisabled: () => void + onLongPressEnabled: () => void +} +const MessageContent = ({ message, onReplyMessageClick, onLongPressDisabled, onLongPressEnabled }: MessageContentProps) => { const scrollToMessage = () => { if (message.linked_message) { Haptics.impact({ @@ -193,7 +212,10 @@ const MessageContent = ({ message, onReplyMessageClick }: { message: Message, on {message.text &&
} {message.message_type === 'Image' && } {message.message_type === 'File' && } - {message.message_type === 'Poll' && } + {message.message_type === 'Poll' && } } @@ -306,31 +328,50 @@ export interface Poll { 'current_user_votes': { 'option': string }[] } -const PollMessageBlock = ({ message }: { message: PollMessage }) => { +const PollMessageBlock = ({ message, onModalClose, onModalOpen }: { message: PollMessage, onModalOpen: VoidFunction, onModalClose: VoidFunction }) => { + const { mutate: globalMutate } = useSWRConfig() // 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 + revalidateOnReconnect: false, + revalidateOnMount: true }) useFrappeDocumentEventListener('Raven Poll', message.poll_id, () => { mutate() + globalMutate(`poll_votes_${message.poll_id}`) }) return (
- {data && } + {data && }
) } -const PollMessageBox = ({ data, messageID }: { data: Poll, messageID: string }) => { +const PollMessageBox = ({ data, messageID, onModalClose, onModalOpen }: { data: Poll, messageID: string, onModalOpen: VoidFunction, onModalClose: VoidFunction }) => { + + const [isOpen, setOpen] = useState(false) + const onViewClick: React.MouseEventHandler = (e) => { + setOpen(true) + onModalOpen() + } + + const closeModal = () => { + setOpen(false) + onModalClose() + } return ( - - - - {data.poll.question} + + + + {data.poll.question} + {data.poll.is_anonymous ? Anonymous : null} {data.current_user_votes.length > 0 ? @@ -356,6 +399,17 @@ const PollMessageBox = ({ data, messageID }: { data: Poll, messageID: string }) } {data.poll.is_disabled ? Poll is now closed : null} + {data.poll.is_anonymous ? null : + <> + + + + + + + } ) } diff --git a/mobile/src/components/features/polls/ViewPollVotes.tsx b/mobile/src/components/features/polls/ViewPollVotes.tsx new file mode 100644 index 000000000..8d26ac615 --- /dev/null +++ b/mobile/src/components/features/polls/ViewPollVotes.tsx @@ -0,0 +1,103 @@ +import { UserAvatar } from '@/components/common/UserAvatar' +import { IonAvatar, IonContent, IonHeader, IonItem, IonLabel, IonList, IonModal } from '@ionic/react' +import { Button, Theme, Text } from '@radix-ui/themes' +import { useRef } from 'react' +import { Poll } from '../chat-space/chat-view/MessageBlock' +import { useFrappeGetCall } from 'frappe-react-sdk' +import { useGetUser } from '@/hooks/useGetUser' + +interface ViewPollVotesProps { + isOpen: boolean, + onDismiss: VoidFunction, + poll: Poll +} + +type VoteData = { + users: string[] + count: number + percentage: number +} + +type PollVotesResponse = Record + +export const ViewPollVotes = ({ isOpen, onDismiss, poll }: ViewPollVotesProps) => { + + const modal = useRef(null) + + // fetch poll votes using poll_id + const { data, error } = useFrappeGetCall<{ message: PollVotesResponse }>('raven.api.raven_poll.get_all_votes', { + 'poll_id': poll.poll.name, + }, `poll_votes_${poll.poll.name}`, { + revalidateOnFocus: false, + revalidateOnReconnect: false, + }) + + return ( + + + +
+
+ +
+
+
+ Poll Votes +
+ {poll.poll.total_votes} vote{poll.poll.total_votes > 1 ? 's' : ''} +
+
+
+
+
+ + + +
+ {poll.poll.question} +
+
+ {data?.message && Object.keys(data.message).map((opt) => { + const option = data.message[opt] + const optionName = poll.poll.options.find(o => o.name === opt)?.option + return ( +
+
+
+ {optionName} + - {option.percentage}% +
+ {option.count} vote{option.count > 1 ? 's' : ''} +
+ + + {option.users.map(user => ( + + ))} + +
+ ) + })} +
+
+
+
+ ) +} + +const UserVote = ({ user_id }: { user_id: string }) => { + + const user = useGetUser(user_id) + + return + + + + +
+ {user?.full_name} + {user?.name} +
+
+
+} \ No newline at end of file diff --git a/raven/api/raven_poll.py b/raven/api/raven_poll.py index dc395f41c..7863b7890 100644 --- a/raven/api/raven_poll.py +++ b/raven/api/raven_poll.py @@ -127,3 +127,48 @@ def retract_vote(poll_id): else: for vote in votes: frappe.delete_doc("Raven Poll Vote", vote.name) + + +@frappe.whitelist() +def get_all_votes(poll_id): + + # Check if the current user has access to the poll + if not frappe.has_permission(doctype="Raven Poll", doc=poll_id, ptype="read"): + frappe.throw(_("You do not have permission to access this poll"), frappe.PermissionError) + + # Check if the poll is anonymous + is_anonymous = frappe.get_cached_value("Raven Poll", poll_id, "is_anonymous") + + if is_anonymous: + frappe.throw(_("This poll is anonymous. You do not have permission to access the votes."), frappe.PermissionError) + else: + # Get all votes for this poll + votes = frappe.get_all( + "Raven Poll Vote", + filters={"poll_id": poll_id}, + fields=["name", "option", "user_id"] + ) + + # Initialize results dictionary + results = {} + + # Process votes + for vote in votes: + option = vote['option'] + if option not in results: + results[option] = { + 'users': [vote['user_id']], + 'count': 1 + } + else: + results[option]['users'].append(vote['user_id']) + results[option]['count'] += 1 + + # Calculate total votes + total_votes = sum(result['count'] for result in results.values()) + + # Calculate percentages + for result in results.values(): + result['percentage'] = (result['count'] / total_votes) * 100 + + return results \ No newline at end of file