diff --git a/mobile/package.json b/mobile/package.json index 9a5b19400..b20ca3d6a 100644 --- a/mobile/package.json +++ b/mobile/package.json @@ -1,7 +1,7 @@ { "name": "mobile", "private": true, - "version": "1.4.4", + "version": "1.5.0", "type": "module", "scripts": { "dev": "vite", @@ -79,4 +79,4 @@ "@types/turndown": "^5.0.4", "typescript": "^4.9.3" } -} +} \ No newline at end of file diff --git a/mobile/src/components/auth/ForgotPasswordButton.tsx b/mobile/src/components/auth/ForgotPasswordButton.tsx new file mode 100644 index 000000000..e8fe0c506 --- /dev/null +++ b/mobile/src/components/auth/ForgotPasswordButton.tsx @@ -0,0 +1,17 @@ +import { Link } from '@radix-ui/themes' +import { ActiveScreenProps } from "../layout/AuthContainer" + +export const ForgotPasswordButton = ({setActiveScreen}: ActiveScreenProps) => { + return( +
+ setActiveScreen({ login: false, loginWithEmail: false, signup: false, forgotPassword: true })} + > + Forgot Password? + +
+ ) +} \ No newline at end of file diff --git a/mobile/src/components/auth/SocialProviders.tsx b/mobile/src/components/auth/SocialProviders.tsx index 0136bf7c7..b350101b9 100644 --- a/mobile/src/components/auth/SocialProviders.tsx +++ b/mobile/src/components/auth/SocialProviders.tsx @@ -53,7 +53,7 @@ export const EmailLoginProvider = ({ isSubmitting, setActiveScreen }: EmailLogin type="button" color='gray' size='3' - onClick={() => setActiveScreen({ login: false, loginWithEmail: true, signup: false })} + onClick={() => setActiveScreen({ login: false, loginWithEmail: true, signup: false, forgotPassword: false })} className='cursor-pointer' >
diff --git a/mobile/src/components/features/chat-input/ChatInput.tsx b/mobile/src/components/features/chat-input/ChatInput.tsx index 066266f82..f95275afe 100644 --- a/mobile/src/components/features/chat-input/ChatInput.tsx +++ b/mobile/src/components/features/chat-input/ChatInput.tsx @@ -4,6 +4,7 @@ import { FileUploadModal } from './FileUploadModal'; import { Tiptap } from './Tiptap'; import { AiOutlinePaperClip } from 'react-icons/ai'; import { Haptics, ImpactStyle } from '@capacitor/haptics'; +import { CreatePoll } from '../polls/CreatePoll'; type Props = { channelID: string, @@ -54,11 +55,20 @@ export const ChatInput = ({ channelID, allChannels, allMembers }: Props) => { setFiles(f => [...f, ...newFiles]) } + const [isPollOpen, setIsPollOpen] = useState(false) + return (
- + setIsPollOpen(true)} + onGetFiles={getFiles} + fileRef={fileInputRef} />
+ {/* setIsPollOpen(false)} /> */}
) diff --git a/mobile/src/components/features/chat-input/MessageInputActions.tsx b/mobile/src/components/features/chat-input/MessageInputActions.tsx index 2d24b00c6..6eaa75a66 100644 --- a/mobile/src/components/features/chat-input/MessageInputActions.tsx +++ b/mobile/src/components/features/chat-input/MessageInputActions.tsx @@ -3,6 +3,8 @@ import { useCurrentEditor } from '@tiptap/react' import { BiAt, BiHash, BiPaperclip } from 'react-icons/bi' import { HiOutlineGif } from 'react-icons/hi2' import { MdOutlineBarChart } from 'react-icons/md' +import { CreatePoll } from '../polls/CreatePoll' +import { useRef, useState } from 'react' export const ICON_PROPS = { size: '22' @@ -18,16 +20,17 @@ const DEFAULT_PROPS: Partial = { } type Props = { - onFileClick?: () => void + onFileClick?: () => void, + onPollCreate: () => void } -const MessageInputActions = ({ onFileClick }: Props) => { +const MessageInputActions = ({ onFileClick, onPollCreate }: Props) => { return ( - {/* */} + {/* */} {/* */} ) @@ -111,11 +114,9 @@ const PollButton = (props: IconButtonProps) => { return + {...props}> - } export default MessageInputActions \ No newline at end of file diff --git a/mobile/src/components/features/chat-input/Tiptap.tsx b/mobile/src/components/features/chat-input/Tiptap.tsx index e8b099b9f..fca43b63f 100644 --- a/mobile/src/components/features/chat-input/Tiptap.tsx +++ b/mobile/src/components/features/chat-input/Tiptap.tsx @@ -44,7 +44,8 @@ type TiptapEditorProps = { defaultText?: string, onPickFiles?: () => void, onGetFiles?: (e: React.ChangeEvent) => void, - fileRef?: React.RefObject + fileRef?: React.RefObject, + onPollCreate: VoidFunction } const UserMention = Mention.extend({ @@ -66,7 +67,7 @@ const ChannelMention = Mention.extend({ pluginKey: new PluginKey('channelMention'), } }) -export const Tiptap = ({ onMessageSend, messageSending, defaultText = '', onPickFiles, onGetFiles, fileRef }: TiptapEditorProps) => { +export const Tiptap = ({ onMessageSend, messageSending, defaultText = '', onPickFiles, onGetFiles, fileRef, onPollCreate }: TiptapEditorProps) => { const { enabledUsers } = useContext(UserListContext) @@ -361,6 +362,7 @@ export const Tiptap = ({ onMessageSend, messageSending, defaultText = '', onPick {focused &&
{/* Reply */} - - + + {selectedMessage.message_type === 'Poll' && } + + {selectedMessage.message_type !== 'Poll' && + } + + {selectedMessage.message_type !== 'Poll' && + } + {isOwnMessage && - - } + } {/* Link to document diff --git a/mobile/src/components/features/chat-space/MessageActions/MessagePreview.tsx b/mobile/src/components/features/chat-space/MessageActions/MessagePreview.tsx index 3060a8bec..383888bb4 100644 --- a/mobile/src/components/features/chat-space/MessageActions/MessagePreview.tsx +++ b/mobile/src/components/features/chat-space/MessageActions/MessagePreview.tsx @@ -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' @@ -25,6 +25,8 @@ const MessagePreview = ({ message, user }: MessagePreview) => {
} {message.message_type === 'File' &&

📎  {message.file?.split('/')[3]}

} + {message.message_type === 'Poll' &&

+ 📊  Poll: {(message as PollMessage).content?.split("\n")?.[0]}

}
) 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 new file mode 100644 index 000000000..050238455 --- /dev/null +++ b/mobile/src/components/features/chat-space/MessageActions/RetractVoteAction.tsx @@ -0,0 +1,37 @@ +import { useFrappePostCall } from 'frappe-react-sdk' +import { ActionIcon, ActionItem, ActionLabel, ActionProps } from './common' +import { arrowUndoOutline } from 'ionicons/icons' +import { useIonToast } from '@ionic/react' + +export const RetractVoteAction = ({ message, onSuccess }: ActionProps) => { + + const [present] = useIonToast() + + 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", + }) + }) + } + + 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 324c77c4e..2716d271e 100644 --- a/mobile/src/components/features/chat-space/chat-view/MessageBlock.tsx +++ b/mobile/src/components/features/chat-space/chat-view/MessageBlock.tsx @@ -1,5 +1,5 @@ -import { memo, useContext, useMemo } from 'react' -import { FileMessage, ImageMessage, Message, TextMessage } from '../../../../../../types/Messaging/Message' +import { memo, useContext, useEffect, useMemo, useState } from 'react' +import { FileMessage, ImageMessage, Message, TextMessage, PollMessage } from '../../../../../../types/Messaging/Message' import { IonIcon, IonSkeletonText, IonText } from '@ionic/react' import { MarkdownRenderer } from '@/components/common/MarkdownRenderer' import { UserFields } from '@/utils/users/UserListProvider' @@ -13,10 +13,15 @@ import { useLongPress } from "@uidotdev/usehooks"; import MessageReactions from './components/MessageReactions' import parse from 'html-react-parser'; import clsx from 'clsx' -import { Avatar, Badge, 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' +import { useFrappeDocumentEventListener, useFrappeGetCall, useFrappePostCall, useSWRConfig } from 'frappe-react-sdk' +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, @@ -76,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 }) @@ -96,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)} @@ -145,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 }) @@ -159,7 +172,11 @@ const ContinuationMessageBlock = ({ message, onMessageSelect, isScrolling, isHig
- + setDisableLongPress(true)} + onLongPressEnabled={() => setDisableLongPress(false)} + onReplyMessageClick={onReplyMessageClick} /> {message.is_edited === 1 && (edited)}
@@ -171,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({ @@ -189,6 +212,10 @@ const MessageContent = ({ message, onReplyMessageClick }: { message: Message, on {message.text &&
} {message.message_type === 'Image' && } {message.message_type === 'File' && } + {message.message_type === 'Poll' && } } @@ -209,8 +236,28 @@ const options = { const ImageMessageBlock = ({ message }: { message: ImageMessage }) => { const { ref, inView } = useInView(options); - const height = `${message.thumbnail_height ?? 200}px` - const width = `${message.thumbnail_width ?? 300}px` + const { width, height } = useMemo(() => { + let height = message.thumbnail_height ?? 200 + let width = message.thumbnail_width ?? 300 + + // Max width is 280px, so we need to adjust the height accordingly + const aspectRatio = width / height + + if (width > 280) { + width = 280 + height = width / aspectRatio + } + + return { + height: `${height}px`, + width: `${width}px` + } + + }, [message]) + + + // const height = `${message.thumbnail_height ?? 200}px` + // const width = `${message.thumbnail_width ?? 300}px` return
{ style={{ width: width, height: height, - maxWidth: '280px' }} /> : }
@@ -275,8 +320,6 @@ const ReplyBlock = ({ message }: { message: Message }) => { } }, [message]) - - const date = message ? new Date(message?.creation) : null return
{message &&
@@ -291,7 +334,228 @@ const ReplyBlock = ({ message }: { message: Message }) => {
} {message.message_type === 'File' &&

📎  {message.file?.split('/')[3]}

} + {message.message_type === 'Poll' &&

+ + Poll: {(message as PollMessage).content?.split("\n")?.[0]}

} +
} + +} + +export interface Poll { + 'poll': RavenPoll, + 'current_user_votes': { 'option': string }[] +} + +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, + revalidateOnMount: true + }) + + useFrappeDocumentEventListener('Raven Poll', message.poll_id, () => { + mutate() + globalMutate(`poll_votes_${message.poll_id}`) + }) + + return ( +
+ {data && }
+ ) +} + +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.is_anonymous ? Anonymous : null} + + {data.current_user_votes.length > 0 ? + : + <> + {data.poll.is_multi_choice ? + : + + } + + } + {data.poll.is_disabled ? Poll is now closed : null} + + {data.poll.is_anonymous ? null : + <> + + + + + + + } + + ) +} + +const PollResults = ({ data }: { data: Poll }) => { + return ( + + {data.poll.options.map(option => { + return + })} + {data.poll.total_votes} vote{data.poll.total_votes > 1 ? 's' : ''} + + ) +} + +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(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 ( + + + + {option.option} + {percentage.toFixed(1)}% + + ) +} + +const SingleChoicePoll = ({ data, messageID }: { data: Poll, messageID: string }) => { + + const { mutate } = useSWRConfig() + const { call } = useFrappePostCall('raven.api.raven_poll.add_vote') + const onVoteSubmit = async (option: RavenPollOption) => { + return call({ + 'message_id': messageID, + 'option_id': option.name + }).then(() => { + mutate(`poll_data_${data.poll.name}`) + }) + } + + return ( + + {data.poll.options.map(option => ( +
+ + + onVoteSubmit(option)} /> + {option.option} + + +
+ ))} +
+ ) +} + +const MultiChoicePoll = ({ data, messageID }: { data: Poll, messageID: string }) => { + + const [selectedOptions, setSelectedOptions] = useState([]) + const { mutate } = useSWRConfig() + + 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}`) + }) + } + + return ( +
+ {data.poll.options.map(option => ( +
+ + + handleCheckboxChange(option.name, v)} /> + {option.option} + + +
+ ))} + + To view the poll results, please submit your choice(s) + + +
+ ) } \ No newline at end of file diff --git a/mobile/src/components/features/polls/CreatePoll.tsx b/mobile/src/components/features/polls/CreatePoll.tsx new file mode 100644 index 000000000..2c5f5834e --- /dev/null +++ b/mobile/src/components/features/polls/CreatePoll.tsx @@ -0,0 +1,238 @@ +import { IonContent, ToastOptions, IonModal, useIonToast, IonHeader, IonToggle, IonCheckbox, IonItem, IonList } from "@ionic/react"; +import { useFrappePostCall } from "frappe-react-sdk"; +import { useRef } from "react"; +import { useFieldArray, useForm } from "react-hook-form"; +import { useParams } from "react-router-dom"; +import { ErrorBanner } from "../../layout"; +import { FormField, FormItem, FormLabel, FormControl, FormDescription, FormMessage, Form } from "@/components/ui/form"; +import { Button, Checkbox, Flex, IconButton, Text, TextField, Theme } from "@radix-ui/themes"; +import { RavenPoll } from "@/types/RavenMessaging/RavenPoll"; +import { BiPlus, BiTrash } from "react-icons/bi"; + +interface CreatePollProps { + presentingElement?: HTMLElement, + isOpen: boolean, + onDismiss: VoidFunction +} + +export const CreatePoll = ({ presentingElement, isOpen, onDismiss }: CreatePollProps) => { + + const modal = useRef(null) + const methods = useForm({ + // Initialize the form with 2 option fields by default + defaultValues: { + options: [{ + name: '', + creation: '', + modified: '', + owner: '', + modified_by: '', + docstatus: 0, + option: '' + }, { + name: '', + creation: '', + modified: '', + owner: '', + modified_by: '', + docstatus: 0, + option: '' + }], + } + }) + + const { register, handleSubmit, control, reset } = methods + const { fields, remove, append } = useFieldArray({ + control: control, + name: 'options' + }) + + const optionPlaceholders = ['Cersei Lannister', 'Jon Snow', 'Daenerys Targaryen', 'Tyrion Lannister', 'Night King', 'Arya Stark', 'Sansa Stark', 'Jaime Lannister', 'Bran Stark', 'The Hound'] + + const handleAddOption = () => { + // limit the number of options to 10 + if (fields.length >= 10) { + return + } else { + append({ + name: '', + creation: '', + modified: '', + owner: '', + modified_by: '', + docstatus: 0, + option: '' + }) + } + } + + const handleRemoveOption = (index: number) => { + // Do not remove the last 2 options + if (fields.length === 2) { + return + } + remove(index) + } + + const { call: createPoll, error } = useFrappePostCall('raven.api.raven_poll.create_poll') + const { channelID } = useParams<{ channelID: string }>() + + const [present] = useIonToast() + + const presentToast = (message: string, color: ToastOptions['color']) => { + present({ + message, + duration: 1500, + color, + position: 'bottom', + }) + } + + const onSubmit = (data: RavenPoll) => { + return createPoll({ + ...data, + "channel_id": channelID + }).then(() => { + presentToast("Poll created successfully!", 'success') + onDismiss() + }).catch((err) => { + presentToast("Error while creating the poll.", 'danger') + }) + } + + const handleCancel = () => { + reset() + onDismiss() + } + + return ( + + + + +
+
+ +
+ Create Poll +
+ +
+
+
+
+ + +
+ + +
+ +
+ ( + + Question * + + + + + {formState.errors && formState.errors?.question?.message} + + )} + /> + ( + + Options * + + {fields.map((option, index) => ( +
+ + + + handleRemoveOption(index)}> + + +
+ ))} + + + Maximum of 10 options allowed + + {formState.errors && formState.errors?.options?.message} +
+ )} + /> + + + Settings + + + + Allow users to select multiple options + + + + + Make this poll anonymous + + + + + +
+
+
+
+ +
+ +
+ + ) +} \ No newline at end of file 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/mobile/src/components/layout/AuthContainer.tsx b/mobile/src/components/layout/AuthContainer.tsx index 8e3f3a9e1..e39783d7d 100644 --- a/mobile/src/components/layout/AuthContainer.tsx +++ b/mobile/src/components/layout/AuthContainer.tsx @@ -3,11 +3,13 @@ import { IonPage } from '@ionic/react'; import { LoginWithEmail } from '@/pages/auth/LoginWithEmail'; import { Login } from '@/pages/auth/Login'; import { SignUp } from '@/pages/auth/SignUp'; +import { ForgotPassword } from '@/pages/auth/ForgotPassword'; type ActiveScreenType = { login: boolean, loginWithEmail: boolean, - signup: boolean; + signup: boolean, + forgotPassword: boolean } export interface ActiveScreenProps { @@ -20,7 +22,8 @@ const AuthContainer = ({ children, ...props }: PropsWithChildren) => { const [activeScreen, setActiveScreen] = useState({ login: true, loginWithEmail: false, - signup: false + signup: false, + forgotPassword: false }) @@ -37,6 +40,7 @@ const AuthContainer = ({ children, ...props }: PropsWithChildren) => { {activeScreen.login && } {activeScreen.loginWithEmail && } {activeScreen.signup && } + {activeScreen.forgotPassword && } } diff --git a/mobile/src/pages/auth/ForgotPassword.tsx b/mobile/src/pages/auth/ForgotPassword.tsx new file mode 100644 index 000000000..5e82750be --- /dev/null +++ b/mobile/src/pages/auth/ForgotPassword.tsx @@ -0,0 +1,99 @@ +import { useState } from "react"; +import { FrappeError, useFrappePostCall } from "frappe-react-sdk"; +import { SuccessCallout, CalloutObject, ErrorCallout } from '@/components/common/Callouts' +import { useForm } from 'react-hook-form' +import { ForgotPasswordInput } from "@/types/Auth/Login"; +import { ActiveScreenProps } from "@/components/layout/AuthContainer"; +import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { Button } from "@radix-ui/themes"; + + +export const ForgotPassword = (props: ActiveScreenProps) => { + + const form = useForm({ + defaultValues: { + user: "", + } + }); + const [callout, setCallout] = useState(null) + + // POST Call to send reset password instructions on email + const { call, error } = useFrappePostCall('frappe.core.doctype.user.user.reset_password') + + async function resetPassword(values: ForgotPasswordInput) { + return call({ + user: values.user, + }) + .then((res) => { + setCallout({ + state: true, + message: "Password reset instructions have been sent to your email.", + }); + }).catch((err)=>{ + setCallout(null) + }) + } + + // TO-DO: To be removed once ErrorBanner/ ErrorCallout is fixed. + const generateErrorMessage = (error: FrappeError) =>{ + if (error.exc_type === "ValidationError") return 'Too many requests. Please try after some time.' + return 'User does not exist. Please Sign Up.' + } + + return ( + <> + +
+ {error && } + {callout && } +
+
+ +
+
+ ( + + Email * + + {/* Type=email as email is allowed */} + + + {formState.errors.user && {formState.errors.user.message}} + + )} + /> +
+
+ + +
+
+
+ +
+
+ + ); +}; \ No newline at end of file diff --git a/mobile/src/pages/auth/Login.tsx b/mobile/src/pages/auth/Login.tsx index 7abfe54c6..89951cc6c 100644 --- a/mobile/src/pages/auth/Login.tsx +++ b/mobile/src/pages/auth/Login.tsx @@ -9,9 +9,10 @@ import { Input } from '@/components/ui/input' import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form' import { OAuthProviderInterface, OAuthProvider, EmailLoginProvider, SocialSeparator } from '@/components/auth/SocialProviders' import { Button, Link, Text } from '@radix-ui/themes' +import { ForgotPasswordButton } from '@/components/auth/ForgotPasswordButton' -export const Login = (props: ActiveScreenProps) => { +export const Login = ({setActiveScreen}: ActiveScreenProps) => { const form = useForm({ defaultValues: { @@ -108,6 +109,9 @@ export const Login = (props: ActiveScreenProps) => { > Login + + {/* Forgot password */} + @@ -130,7 +134,7 @@ export const Login = (props: ActiveScreenProps) => {
{ loginContext?.message?.login_with_email_link ? - + : null }
@@ -147,7 +151,7 @@ export const Login = (props: ActiveScreenProps) => { underline='always' size='2' href='#' - onClick={() => props.setActiveScreen({ login: false, loginWithEmail: false, signup: true })} + onClick={() => setActiveScreen({ login: false, loginWithEmail: false, signup: true, forgotPassword: false })} > Sign up diff --git a/mobile/src/pages/auth/LoginWithEmail.tsx b/mobile/src/pages/auth/LoginWithEmail.tsx index d651930b9..1bcc5d1e3 100644 --- a/mobile/src/pages/auth/LoginWithEmail.tsx +++ b/mobile/src/pages/auth/LoginWithEmail.tsx @@ -32,6 +32,8 @@ export const LoginWithEmail = (props: ActiveScreenProps) => { state: true, message: "Login Link sent on Email", }); + }).catch((err)=>{ + setCallout(null) }) } @@ -81,7 +83,7 @@ export const LoginWithEmail = (props: ActiveScreenProps) => { size='3' variant='soft' className="cursor-pointer" - onClick={() => props.setActiveScreen({ login: true, loginWithEmail: false, signup: false })} + onClick={() => props.setActiveScreen({ login: true, loginWithEmail: false, signup: false, forgotPassword: false })} > Cancel diff --git a/mobile/src/pages/auth/SignUp.tsx b/mobile/src/pages/auth/SignUp.tsx index d06afd535..6049c0e76 100644 --- a/mobile/src/pages/auth/SignUp.tsx +++ b/mobile/src/pages/auth/SignUp.tsx @@ -152,7 +152,7 @@ export const SignUp = (props: ActiveScreenProps) => { underline='always' size='2' href='#' - onClick={() => props.setActiveScreen({ login: true, loginWithEmail: false, signup: false })} + onClick={() => props.setActiveScreen({ login: true, loginWithEmail: false, signup: false, forgotPassword: false })} > Login diff --git a/mobile/src/types/Auth/Login.ts b/mobile/src/types/Auth/Login.ts index e92e6ed7f..53cb20866 100644 --- a/mobile/src/types/Auth/Login.ts +++ b/mobile/src/types/Auth/Login.ts @@ -23,4 +23,8 @@ export type VerificationType = { prompt?: string | undefined, token_delivery?: boolean, setup?: boolean +} + +export type ForgotPasswordInput = { + user: LoginInputs["email"] } \ No newline at end of file diff --git a/mobile/src/types/RavenMessaging/RavenPoll.ts b/mobile/src/types/RavenMessaging/RavenPoll.ts new file mode 100644 index 000000000..2b34b5514 --- /dev/null +++ b/mobile/src/types/RavenMessaging/RavenPoll.ts @@ -0,0 +1,26 @@ +import { RavenPollOption } from './RavenPollOption' + +export interface RavenPoll { + creation: string + name: string + modified: string + owner: string + modified_by: string + docstatus: 0 | 1 | 2 + parent?: string + parentfield?: string + parenttype?: string + idx?: number + /** Question : Small Text */ + question: string + /** Options : Table - Raven Poll Option */ + options: RavenPollOption[] + /** Is Anonymous : Check */ + is_anonymous?: 0 | 1 + /** Is Multi Choice : Check */ + is_multi_choice?: 0 | 1 + /** Is Disabled : Check */ + is_disabled?: 0 | 1 + /** Total Votes : Int */ + total_votes: number +} \ No newline at end of file diff --git a/mobile/src/types/RavenMessaging/RavenPollOption.ts b/mobile/src/types/RavenMessaging/RavenPollOption.ts new file mode 100644 index 000000000..c9b8a2b09 --- /dev/null +++ b/mobile/src/types/RavenMessaging/RavenPollOption.ts @@ -0,0 +1,17 @@ + +export interface RavenPollOption { + creation: string + name: string + modified: string + owner: string + modified_by: string + docstatus: 0 | 1 | 2 + parent?: string + parentfield?: string + parenttype?: string + idx?: number + /** Option : Small Text */ + option: string + /** Votes : Int */ + votes?: number +} \ No newline at end of file diff --git a/package.json b/package.json index 3192c5088..a9fee4059 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "raven", - "version": "1.4.4", + "version": "1.5.0", "description": "Messaging Application", "main": "index.js", "scripts": { diff --git a/raven-app/package.json b/raven-app/package.json index 6116a80fc..605166334 100644 --- a/raven-app/package.json +++ b/raven-app/package.json @@ -1,7 +1,7 @@ { "name": "raven-app", "private": true, - "version": "1.4.4", + "version": "1.5.0", "type": "module", "scripts": { "dev": "vite", @@ -61,4 +61,4 @@ "@types/turndown": "^5.0.4", "typescript": "^5.3.3" } -} +} \ No newline at end of file diff --git a/raven-app/src/App.tsx b/raven-app/src/App.tsx index 168891ad8..cdab4d829 100644 --- a/raven-app/src/App.tsx +++ b/raven-app/src/App.tsx @@ -17,6 +17,7 @@ const router = createBrowserRouter( import('@/pages/auth/Login')} /> import('@/pages/auth/LoginWithEmail')} /> import('@/pages/auth/SignUp')} /> + import('@/pages/auth/ForgotPassword')} /> }> }> } > diff --git a/raven-app/src/components/feature/channel-details/ChannelDetails.tsx b/raven-app/src/components/feature/channel-details/ChannelDetails.tsx index 546e59a84..763deddff 100644 --- a/raven-app/src/components/feature/channel-details/ChannelDetails.tsx +++ b/raven-app/src/components/feature/channel-details/ChannelDetails.tsx @@ -55,7 +55,7 @@ export const ChannelDetails = ({ channelData, channelMembers, onClose }: Channel Created by - {channelData?.owner && {users[channelData.owner]?.full_name}} + {channelData?.owner && {users[channelData.owner]?.full_name ?? channelData?.owner}} {channelData.creation && on } diff --git a/raven-app/src/components/feature/channel-settings/change-channel-type/ChangeChannelTypeButton.tsx b/raven-app/src/components/feature/channel-settings/change-channel-type/ChangeChannelTypeButton.tsx index 1dde9a65e..2c83f29b8 100644 --- a/raven-app/src/components/feature/channel-settings/change-channel-type/ChangeChannelTypeButton.tsx +++ b/raven-app/src/components/feature/channel-settings/change-channel-type/ChangeChannelTypeButton.tsx @@ -1,14 +1,30 @@ import { ChannelListItem } from '@/utils/channel/ChannelListProvider' -import { BiHash, BiLockAlt } from 'react-icons/bi' +import { BiGlobe, BiHash, BiLockAlt } from 'react-icons/bi' import { ChangeChannelTypeModal } from './ChangeChannelTypeModal' import { useState } from 'react' import { Button, Dialog } from '@radix-ui/themes' import { DIALOG_CONTENT_CLASS } from '@/utils/layout/dialog' + interface ChangeChannelTypeButtonProps { channelData: ChannelListItem } export const ChangeChannelTypeButton = ({ channelData }: ChangeChannelTypeButtonProps) => { + return ( + <> + {channelData.type === 'Public' && } + {channelData.type === 'Public' && } + + {channelData.type === 'Private' && } + {channelData.type === 'Private' && } + + {channelData.type === 'Open' && } + {channelData.type === 'Open' && } + + ) +} + +const ChangeChannelTypePublicPrivate = ({ channelData }: ChangeChannelTypeButtonProps) => { const [open, setOpen] = useState(false) const onClose = () => { @@ -25,9 +41,60 @@ export const ChangeChannelTypeButton = ({ channelData }: ChangeChannelTypeButton + + + ) +} + +const ChangeChannelTypeOpenPublic = ({ channelData }: ChangeChannelTypeButtonProps) => { + + const [open, setOpen] = useState(false) + const onClose = () => { + setOpen(false) + } + + return ( + + + + + + ) -} \ No newline at end of file +} + +const ChangeChannelTypeOpenPrivate = ({ channelData }: ChangeChannelTypeButtonProps) => { + + const [open, setOpen] = useState(false) + const onClose = () => { + setOpen(false) + } + + return ( + + + + + + + + + ) +} diff --git a/raven-app/src/components/feature/channel-settings/change-channel-type/ChangeChannelTypeModal.tsx b/raven-app/src/components/feature/channel-settings/change-channel-type/ChangeChannelTypeModal.tsx index 5c749e1a9..06d0e9895 100644 --- a/raven-app/src/components/feature/channel-settings/change-channel-type/ChangeChannelTypeModal.tsx +++ b/raven-app/src/components/feature/channel-settings/change-channel-type/ChangeChannelTypeModal.tsx @@ -1,4 +1,4 @@ -import { useFrappeUpdateDoc } from 'frappe-react-sdk' +import { useFrappeUpdateDoc, useSWRConfig } from 'frappe-react-sdk' import { ErrorBanner } from '../../../layout/AlertBanner' import { ChannelListItem } from '@/utils/channel/ChannelListProvider' import { Button, Dialog, Flex, Text } from '@radix-ui/themes' @@ -7,19 +7,21 @@ import { useToast } from '@/hooks/useToast' interface ChangeChannelTypeModalProps { onClose: () => void - channelData: ChannelListItem + channelData: ChannelListItem, + newChannelType: 'Public' | 'Private' | 'Open' } -export const ChangeChannelTypeModal = ({ onClose, channelData }: ChangeChannelTypeModalProps) => { +export const ChangeChannelTypeModal = ({ onClose, channelData, newChannelType }: ChangeChannelTypeModalProps) => { const { toast } = useToast() + const { mutate } = useSWRConfig() const { updateDoc, loading: updatingDoc, error } = useFrappeUpdateDoc() - const new_channel_type = channelData?.type === 'Public' ? 'Private' : 'Public' - const changeChannelType = (new_channel_type: 'Public' | 'Private') => { + const changeChannelType = (newChannelType: 'Public' | 'Private' | 'Open') => { updateDoc("Raven Channel", channelData?.name ?? null, { - type: new_channel_type + type: newChannelType }).then(() => { + mutate(`raven.api.chat.get_channel_members:${channelData.name}`) toast({ title: "Channel type updated", variant: "success", @@ -31,33 +33,40 @@ export const ChangeChannelTypeModal = ({ onClose, channelData }: ChangeChannelTy return ( <> - Change to a {new_channel_type.toLocaleLowerCase()} channel? + Change to a {newChannelType.toLocaleLowerCase()} channel? - {channelData?.type === 'Private' && + {newChannelType === 'Public' && Please understand that when you make {channelData?.channel_name} a public channel:
  • Anyone from your organisation can join this channel and view its message history.
  • -
  • If you make this channel private again, it willbe visible to anyone who has joined the channel up until that point.
  • +
  • If you make this channel private, it will be visible to anyone who has joined the channel up until that point.
} - {channelData?.type === 'Public' && + {newChannelType === 'Private' && Please understand that when you make {channelData?.channel_name} a private channel:
  • No changes will be made to the channel's history or members
  • All files shared in this channel will become private and will be accessible only to the channel members
} + {newChannelType === 'Open' && + Please understand that when you make {channelData?.channel_name} a open channel: +
    +
  • Everyone from your organisation will become a channel member and will be able to view its message history.
  • +
  • If you later intend to make this private you will have to manually remove members that should not have access to this channel.
  • +
+
}
- diff --git a/raven-app/src/components/feature/chat/ChatInput/FileInput/useFileUpload.ts b/raven-app/src/components/feature/chat/ChatInput/FileInput/useFileUpload.ts index e12d91486..3c1e9199f 100644 --- a/raven-app/src/components/feature/chat/ChatInput/FileInput/useFileUpload.ts +++ b/raven-app/src/components/feature/chat/ChatInput/FileInput/useFileUpload.ts @@ -16,6 +16,10 @@ export default function useFileUpload(channelID: string, selectedMessage?: Messa const [files, setFiles] = useState([]) + const filesStateRef = useRef([]) + + filesStateRef.current = files + const [fileUploadProgress, setFileUploadProgress] = useState>({}) const addFile = (file: File) => { @@ -38,7 +42,7 @@ export default function useFileUpload(channelID: string, selectedMessage?: Messa } const uploadFiles = async () => { - const newFiles = [...files] + const newFiles = [...filesStateRef.current] if (newFiles.length > 0) { const promises = newFiles.map(async (f: CustomFile) => { return file.uploadFile(f, diff --git a/raven-app/src/components/feature/chat/ChatMessage/MessageActions/MessageActions.tsx b/raven-app/src/components/feature/chat/ChatMessage/MessageActions/MessageActions.tsx index ede5eea68..4646495f9 100644 --- a/raven-app/src/components/feature/chat/ChatMessage/MessageActions/MessageActions.tsx +++ b/raven-app/src/components/feature/chat/ChatMessage/MessageActions/MessageActions.tsx @@ -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, @@ -25,6 +26,9 @@ export const MessageContextMenu = ({ message, onDelete, onEdit, onReply }: Messa return ( {message ? <> + + {message && message.message_type === 'Poll' && } + diff --git a/raven-app/src/components/feature/chat/ChatMessage/MessageActions/RetractVote.tsx b/raven-app/src/components/feature/chat/ChatMessage/MessageActions/RetractVote.tsx new file mode 100644 index 000000000..b6f09629c --- /dev/null +++ b/raven-app/src/components/feature/chat/ChatMessage/MessageActions/RetractVote.tsx @@ -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 ( + <> + + + + Retract vote + + + + + ) + + return null +} \ No newline at end of file diff --git a/raven-app/src/components/feature/chat/ChatMessage/Renderers/PollMessage.tsx b/raven-app/src/components/feature/chat/ChatMessage/Renderers/PollMessage.tsx index 638b83b6b..1da40427a 100644 --- a/raven-app/src/components/feature/chat/ChatMessage/Renderers/PollMessage.tsx +++ b/raven-app/src/components/feature/chat/ChatMessage/Renderers/PollMessage.tsx @@ -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" @@ -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', { @@ -40,7 +40,7 @@ export const PollMessageBlock = memo(({ message, user, ...props }: PollMessageBl {data && } ) -}) +} const PollMessageBox = ({ data, messageID }: { data: Poll, messageID: string }) => { return ( @@ -132,7 +132,6 @@ 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) => { @@ -140,7 +139,6 @@ const SingleChoicePoll = ({ data, messageID }: { data: Poll, messageID: string } 'message_id': messageID, 'option_id': option.name }).then(() => { - mutate(`poll_data_${data.poll.name}`) toast({ title: "Your vote has been submitted!", variant: 'success', @@ -168,7 +166,6 @@ const SingleChoicePoll = ({ data, messageID }: { data: Poll, messageID: string } const MultiChoicePoll = ({ data, messageID }: { data: Poll, messageID: string }) => { const [selectedOptions, setSelectedOptions] = useState([]) - const { mutate } = useSWRConfig() const { toast } = useToast() const handleCheckboxChange = (name: string, value: boolean | string) => { @@ -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', diff --git a/raven-app/src/components/feature/chat/ChatMessage/Renderers/TiptapRenderer/Italic.tsx b/raven-app/src/components/feature/chat/ChatMessage/Renderers/TiptapRenderer/Italic.tsx deleted file mode 100644 index cf00cde08..000000000 --- a/raven-app/src/components/feature/chat/ChatMessage/Renderers/TiptapRenderer/Italic.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import { mergeAttributes } from "@tiptap/react"; -import Italic from '@tiptap/extension-italic'; -export const CustomItalic = Italic.extend({ - renderHTML({ HTMLAttributes }) { - return [ - "em", - mergeAttributes(HTMLAttributes, { - class: 'rt-Em' - }), // mergeAttributes is a exported function from @tiptap/core - 0, - ]; - }, -}) \ No newline at end of file diff --git a/raven-app/src/components/feature/chat/ChatMessage/Renderers/TiptapRenderer/TiptapRenderer.tsx b/raven-app/src/components/feature/chat/ChatMessage/Renderers/TiptapRenderer/TiptapRenderer.tsx index 9ab0e1f2e..ded1b1498 100644 --- a/raven-app/src/components/feature/chat/ChatMessage/Renderers/TiptapRenderer/TiptapRenderer.tsx +++ b/raven-app/src/components/feature/chat/ChatMessage/Renderers/TiptapRenderer/TiptapRenderer.tsx @@ -2,7 +2,7 @@ import { EditorContent, EditorContext, useEditor } from '@tiptap/react' import { TextMessage } from '../../../../../../../../types/Messaging/Message' import { UserFields } from '@/utils/users/UserListProvider' import { BoxProps } from '@radix-ui/themes/dist/cjs/components/box' -import { Box, Text } from '@radix-ui/themes' +import { Box } from '@radix-ui/themes' import Highlight from '@tiptap/extension-highlight' import StarterKit from '@tiptap/starter-kit' import css from 'highlight.js/lib/languages/css' @@ -17,10 +17,10 @@ import { CustomBlockquote } from './Blockquote' import { CustomBold } from './Bold' import { CustomUserMention } from './Mention' import { CustomLink, LinkPreview } from './Link' -import { CustomItalic } from './Italic' import { CustomUnderline } from './Underline' import { Image } from '@tiptap/extension-image' import { clsx } from 'clsx' +import Italic from '@tiptap/extension-italic'; const lowlight = createLowlight(common) lowlight.register('html', html) @@ -80,7 +80,7 @@ export const TiptapRenderer = ({ message, user, isScrolling = false, isTruncated CustomBold, CustomUserMention, CustomLink, - CustomItalic, + Italic, Image.configure({ HTMLAttributes: { class: 'w-full h-auto' diff --git a/raven-app/src/pages/auth/ForgotPassword.tsx b/raven-app/src/pages/auth/ForgotPassword.tsx new file mode 100644 index 000000000..e27a30b35 --- /dev/null +++ b/raven-app/src/pages/auth/ForgotPassword.tsx @@ -0,0 +1,107 @@ +import { useState } from "react"; +import { useForm } from "react-hook-form"; +import { Link } from "react-router-dom"; +import { FrappeError, useFrappePostCall } from "frappe-react-sdk"; +import { + Box, + Button, + Flex, + TextField, + Link as LinkButton, +} from "@radix-ui/themes"; +import { ErrorText, Label } from "@/components/common/Form"; +import { Loader } from "@/components/common/Loader"; +import { CalloutObject } from "@/components/common/Callouts/CustomCallout"; +import { ErrorCallout } from "@/components/common/Callouts/ErrorCallouts"; +import { SuccessCallout } from "@/components/common/Callouts/SuccessCallout"; +import { isEmailValid } from "@/utils/validations"; +import { ForgotPasswordInput } from "@/types/Auth/Login"; +import AuthContainer from "@/components/layout/AuthContainer"; + + +export const Component = () => { + const { + register, + handleSubmit, + formState: { errors, isSubmitting }, + } = useForm(); + const [callout, setCallout] = useState(null) + + // POST Call to send reset password instructions on email + const { call, error } = useFrappePostCall('frappe.core.doctype.user.user.reset_password') + + async function resetPassword(values: ForgotPasswordInput) { + return call({ + user: values.user, + }) + .then((res) => { + setCallout({ + state: true, + message: "Password reset instructions have been sent to your email.", + }); + }).catch((err)=>{ + setCallout(null) + }) + } + + // TO-DO: To be removed once ErrorBanner/ ErrorCallout is fixed. + const generateErrorMessage = (error: FrappeError) =>{ + if (error.exc_type === "ValidationError") return 'Too many requests. Please try after some time.' + return 'User does not exist. Please Sign Up.' + } + + return ( + + + {error && } + {callout && } + + +
+ + + + + + isEmailValid(user) || + "Please enter a valid email address.", + required: "Email is required.", + })} + name="user" + type="email" + placeholder="jane@example.com" + tabIndex={0} + autoFocus + /> + + {errors?.user && ( + {errors?.user?.message} + )} + + + + + + + + Back to Login + + + + +
+
+
+ ); +}; + +Component.displayName = "ForgotPassword"; \ No newline at end of file diff --git a/raven-app/src/pages/auth/Login.tsx b/raven-app/src/pages/auth/Login.tsx index 9d7cae3f0..8dd5721a1 100644 --- a/raven-app/src/pages/auth/Login.tsx +++ b/raven-app/src/pages/auth/Login.tsx @@ -131,6 +131,16 @@ export const Component = () => { {isSubmitting ? : 'Login'}
+ + + + Forgot Password? + + +
diff --git a/raven-app/src/pages/auth/LoginWithEmail.tsx b/raven-app/src/pages/auth/LoginWithEmail.tsx index 3820627cc..1a7de1a4b 100644 --- a/raven-app/src/pages/auth/LoginWithEmail.tsx +++ b/raven-app/src/pages/auth/LoginWithEmail.tsx @@ -39,6 +39,8 @@ export const Component = () => { state: true, message: "Login Link sent on Email", }); + }).catch((err)=>{ + setCallout(null) }) } @@ -65,7 +67,6 @@ export const Component = () => { })} name="email" type="email" - required placeholder="jane@example.com" tabIndex={0} /> diff --git a/raven-app/src/types/Auth/Login.ts b/raven-app/src/types/Auth/Login.ts index a4c30eaff..e9412916b 100644 --- a/raven-app/src/types/Auth/Login.ts +++ b/raven-app/src/types/Auth/Login.ts @@ -22,4 +22,8 @@ export type VerificationType = { prompt?: string | undefined, token_delivery?: boolean, setup?: boolean +} + +export type ForgotPasswordInput = { + user: LoginInputs["email"] } \ No newline at end of file diff --git a/raven-app/yarn.lock b/raven-app/yarn.lock index 9f63812e8..1fb9e09eb 100644 --- a/raven-app/yarn.lock +++ b/raven-app/yarn.lock @@ -4800,6 +4800,7 @@ sourcemap-codec@^1.4.8: integrity sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA== "string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0: + name string-width-cjs version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -4873,6 +4874,7 @@ stringify-object@^3.3.0: is-regexp "^1.0.0" "strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: + name strip-ansi-cjs version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== diff --git a/raven/__init__.py b/raven/__init__.py index c0f285b05..5b6018861 100644 --- a/raven/__init__.py +++ b/raven/__init__.py @@ -1 +1 @@ -__version__ = "1.4.4" +__version__ = "1.5.0" diff --git a/raven/api/chat.py b/raven/api/chat.py index 31a3a8b57..68ea867fc 100644 --- a/raven/api/chat.py +++ b/raven/api/chat.py @@ -17,7 +17,21 @@ def get_channel_members(channel_id): channel_member = frappe.qb.DocType("Raven Channel Member") user = frappe.qb.DocType("Raven User") if frappe.get_cached_value("Raven Channel", channel_id, "type") == "Open": - member_array = get_list() + # 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) + ) else: member_query = ( frappe.qb.from_(channel_member) @@ -35,7 +49,7 @@ def get_channel_members(channel_id): .orderby(channel_member.creation, order=Order.desc) ) - member_array = member_query.run(as_dict=True) + member_array = member_query.run(as_dict=True) member_object = {} for member in member_array: diff --git a/raven/api/raven_poll.py b/raven/api/raven_poll.py index d59fb6871..7863b7890 100644 --- a/raven/api/raven_poll.py +++ b/raven/api/raven_poll.py @@ -113,3 +113,62 @@ 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) + + +@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 diff --git a/raven/package.json b/raven/package.json index 53022cb4e..e1e2b57f0 100644 --- a/raven/package.json +++ b/raven/package.json @@ -1,6 +1,6 @@ { "name": "raven", - "version": "1.4.4", + "version": "1.5.0", "description": "", "main": "index.js", "scripts": { diff --git a/raven/permissions.py b/raven/permissions.py index 04227d5ea..36af8aec8 100644 --- a/raven/permissions.py +++ b/raven/permissions.py @@ -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": diff --git a/raven/raven_integrations/doctype/raven_scheduler_event/raven_scheduler_event.py b/raven/raven_integrations/doctype/raven_scheduler_event/raven_scheduler_event.py index 127fc204f..a204e9ae7 100644 --- a/raven/raven_integrations/doctype/raven_scheduler_event/raven_scheduler_event.py +++ b/raven/raven_integrations/doctype/raven_scheduler_event/raven_scheduler_event.py @@ -6,83 +6,82 @@ class RavenSchedulerEvent(Document): - # begin: auto-generated types - # This code is auto-generated. Do not modify anything in this block. + # begin: auto-generated types + # This code is auto-generated. Do not modify anything in this block. - from typing import TYPE_CHECKING + from typing import TYPE_CHECKING - if TYPE_CHECKING: - from frappe.types import DF + if TYPE_CHECKING: + from frappe.types import DF - bot: DF.Link - channel: DF.Link - content: DF.SmallText - cron_expression: DF.Data | None - disabled: DF.Check - dm: DF.Link | None - event_frequency: DF.Literal["Every Day", - "Every Day of the week", "Date of the month", "Cron"] - event_name: DF.Data - scheduler_event_id: DF.Link | None - send_to: DF.Literal["Channel", "DM"] - # end: auto-generated types + bot: DF.Link + channel: DF.Link + content: DF.SmallText + cron_expression: DF.Data | None + disabled: DF.Check + dm: DF.Link | None + event_frequency: DF.Literal["Every Day", "Every Day of the week", "Date of the month", "Cron"] + event_name: DF.Data + scheduler_event_id: DF.Link | None + send_to: DF.Literal["Channel", "DM"] + # end: auto-generated types - def before_save(self): - ''' - 1. If the 'scheduler_event_id' is not set, create a Server Script of type 'Scheduler Event' and set the 'scheduler_event_id' to the name of the Server Script. - ''' - if not self.scheduler_event_id: - self.scheduler_event_id = self.create_scheduler_event() - else: - server_script = frappe.get_doc( - 'Server Script', self.scheduler_event_id) - server_script.cron_format = self.cron_expression - server_script.script = self.get_scheduler_event_script() - server_script.save() + def before_save(self): + """ + 1. If the 'scheduler_event_id' is not set, create a Server Script of type 'Scheduler Event' and set the 'scheduler_event_id' to the name of the Server Script. + """ + if not self.scheduler_event_id: + self.scheduler_event_id = self.create_scheduler_event() + else: + server_script = frappe.get_doc("Server Script", self.scheduler_event_id) + server_script.cron_format = self.cron_expression + server_script.script = self.get_scheduler_event_script() + server_script.save() - def on_update(self): - ''' - 1. If the 'scheduler_event_id' is set, and the 'disabled' field is updated, update the 'disabled' field of the Server Script of type 'Scheduler Event' with the name 'scheduler_event_id'. - ''' - if self.scheduler_event_id: - server_script = frappe.get_doc( - 'Server Script', self.scheduler_event_id) - server_script.disabled = self.disabled - server_script.save() + def on_update(self): + """ + 1. If the 'scheduler_event_id' is set, and the 'disabled' field is updated, update the 'disabled' field of the Server Script of type 'Scheduler Event' with the name 'scheduler_event_id'. + """ + if self.scheduler_event_id: + server_script = frappe.get_doc("Server Script", self.scheduler_event_id) + server_script.disabled = self.disabled + server_script.save() - def on_trash(self): - ''' - 1. If the 'scheduler_event_id' is set, delete the Server Script of type 'Scheduler Event' with the name 'scheduler_event_id'. - ''' - if self.scheduler_event_id: - frappe.delete_doc('Server Script', self.scheduler_event_id) + def on_trash(self): + """ + 1. If the 'scheduler_event_id' is set, delete the Server Script of type 'Scheduler Event' with the name 'scheduler_event_id'. + """ + if self.scheduler_event_id: + frappe.delete_doc("Server Script", self.scheduler_event_id) - def create_scheduler_event(self): - ''' - Create a Server Script of type 'Scheduler Event' and set the 'scheduler_event_id' to the name of the Server Script. - ''' - server_script = frappe.get_doc({ - 'doctype': 'Server Script', - 'script_type': 'Scheduler Event', - 'name': self.event_name, - 'disabled': 0, - 'event_frequency': 'Cron', - 'cron_format': self.cron_expression, - 'script': self.get_scheduler_event_script() - }) - server_script.insert() - return server_script.name + def create_scheduler_event(self): + """ + Create a Server Script of type 'Scheduler Event' and set the 'scheduler_event_id' to the name of the Server Script. + """ + server_script = frappe.get_doc( + { + "doctype": "Server Script", + "script_type": "Scheduler Event", + "name": self.event_name, + "disabled": 0, + "event_frequency": "Cron", + "cron_format": self.cron_expression, + "script": self.get_scheduler_event_script(), + } + ) + server_script.insert() + return server_script.name - def get_scheduler_event_script(self): - ''' - Get the script for the Scheduler Event - ''' - # bot = frappe.get_doc('Raven Bot', self.bot) - # bot.send_message(self.channel, {'text': self.content}) - # return code snippet with bot & content as values - message = {'text': self.content} - script = f''' + def get_scheduler_event_script(self): + """ + Get the script for the Scheduler Event + """ + # bot = frappe.get_doc('Raven Bot', self.bot) + # bot.send_message(self.channel, {'text': self.content}) + # return code snippet with bot & content as values + message = {"text": self.content} + script = f""" bot = frappe.get_doc('Raven Bot', '{self.bot}')\n bot.send_message('{self.channel}', {message}) -''' - return script +""" + return script diff --git a/raven/raven_integrations/doctype/raven_webhook/raven_webhook.py b/raven/raven_integrations/doctype/raven_webhook/raven_webhook.py index 862a5c49a..28d453f4e 100644 --- a/raven/raven_integrations/doctype/raven_webhook/raven_webhook.py +++ b/raven/raven_integrations/doctype/raven_webhook/raven_webhook.py @@ -17,8 +17,7 @@ class RavenWebhook(Document): from frappe.types import DF channel_id: DF.Link | None - channel_type: DF.Literal["", "Public", - "Private", "Open", "DM", "Self Message"] + channel_type: DF.Literal["", "Public", "Private", "Open", "DM", "Self Message"] condition: DF.SmallText | None conditions_on: DF.Literal["", "Channel", "User", "Channel Type", "Custom"] enable_security: DF.Check @@ -32,29 +31,36 @@ class RavenWebhook(Document): webhook_data: DF.Table[WebhookData] webhook_headers: DF.Table[WebhookHeader] webhook_secret: DF.Password | None - webhook_trigger: DF.Literal["Message Sent", "Message Edited", "Message Deleted", "Message Reacted On", - "Channel Created", "Channel Deleted", "Channel Member Added", "Channel Member Deleted", "User Added", "User Deleted"] + webhook_trigger: DF.Literal[ + "Message Sent", + "Message Edited", + "Message Deleted", + "Message Reacted On", + "Channel Created", + "Channel Deleted", + "Channel Member Added", + "Channel Member Deleted", + "User Added", + "User Deleted", + ] # end: auto-generated types def validate(self): # 1. Check if webhook name is unique # 2. Check if webhook_data and webhook_headers are unique - - # 1. Check if webhook name is unique - webhook = frappe.get_all('Raven Webhook', filters={ - 'webhook_name': self.name}) + webhook = frappe.get_all("Raven Webhook", filters={"webhook_name": self.name}) if webhook: - frappe.throw('Webhook name already exists') + frappe.throw("Webhook name already exists") # 2. Check if webhook_data and webhook_headers are unique webhook_data_keys = [data.key for data in self.webhook_data] webhook_header_keys = [data.key for data in self.webhook_headers] if len(webhook_data_keys) != len(set(webhook_data_keys)): - frappe.throw('Webhook Data keys should be unique') + frappe.throw("Webhook Data keys should be unique") if len(webhook_header_keys) != len(set(webhook_header_keys)): - frappe.throw('Webhook Headers keys should be unique') + frappe.throw("Webhook Headers keys should be unique") def before_save(self): # 1. Check if webhook ID is exists @@ -70,26 +76,25 @@ def before_save(self): # 3. Create the webhook self.create_webhook() - def on_trash(self): # Delete the webhook if self.webhook: - frappe.db.delete('Webhook', self.webhook) + frappe.db.delete("Webhook", self.webhook) def create_webhook(self): # Create a new webhook doctype, event = self.get_doctype_and_event() conditions = self.get_conditions() - webhook_doc = frappe.new_doc('Webhook') + webhook_doc = frappe.new_doc("Webhook") webhook_doc.name = self.name webhook_doc.request_url = self.request_url webhook_doc.is_dynamic_url = self.is_dynamic_url webhook_doc.timeout = self.timeout webhook_doc.enable_security = self.enable_security webhook_doc.webhook_secret = self.webhook_secret - webhook_doc.request_method = 'POST' - webhook_doc.request_structure = 'Form URL-Encoded' + webhook_doc.request_method = "POST" + webhook_doc.request_structure = "Form URL-Encoded" self.set_webhook_data_and_headers(webhook_doc) webhook_doc.webhook_doctype = doctype webhook_doc.webhook_docevent = event @@ -101,7 +106,7 @@ def update_webhook(self): # Update the webhook conditions = self.get_conditions() - webhook_doc = frappe.get_doc('Webhook', self.webhook) + webhook_doc = frappe.get_doc("Webhook", self.webhook) webhook_doc.request_url = self.request_url webhook_doc.is_dynamic_url = self.is_dynamic_url webhook_doc.timeout = self.timeout @@ -115,8 +120,8 @@ def set_webhook_data_and_headers(self, webhook_doc): # Set the webhook data and headers # get the existing webhook data and headers - webhook_header = webhook_doc.get('webhook_headers', []) - webhook_data = webhook_doc.get('webhook_data', []) + webhook_header = webhook_doc.get("webhook_headers", []) + webhook_data = webhook_doc.get("webhook_data", []) # get the existing webhook data and headers keys webhook_data_keys = [data.key for data in self.webhook_data] @@ -127,81 +132,54 @@ def set_webhook_data_and_headers(self, webhook_doc): # and append the new webhook data and headers for data in self.webhook_data: if data.key not in webhook_data_keys: - webhook_doc.append('webhook_data', { - 'key': data.key, - 'fieldname': data.fieldname, - }) + webhook_doc.append( + "webhook_data", + { + "key": data.key, + "fieldname": data.fieldname, + }, + ) for data in self.webhook_headers: if data.key not in webhook_header_keys: - webhook_doc.append('webhook_headers', { - 'key': data.key, - 'value': data.value, - }) + webhook_doc.append( + "webhook_headers", + { + "key": data.key, + "value": data.value, + }, + ) def get_doctype_and_event(self): doctypes_and_events = [ { - 'label': 'Message Sent', - 'doctype': 'Raven Message', - 'event': 'after_insert', - }, - { - - 'label': 'Message Edited', - 'doctype': 'Raven Message', - 'event': 'on_update' - }, - { - - 'label': 'Message Deleted', - 'doctype': 'Raven Message', - 'event': 'on_trash' - }, - { - - 'label': 'Message Reacted On', - 'doctype': 'Raven Message Reaction', - 'event': 'after_insert' - }, - { - - 'label': 'Channel Created', - 'doctype': 'Raven Channel', - 'event': 'after_insert' + "label": "Message Sent", + "doctype": "Raven Message", + "event": "after_insert", }, + {"label": "Message Edited", "doctype": "Raven Message", "event": "on_update"}, + {"label": "Message Deleted", "doctype": "Raven Message", "event": "on_trash"}, + {"label": "Message Reacted On", "doctype": "Raven Message Reaction", "event": "after_insert"}, + {"label": "Channel Created", "doctype": "Raven Channel", "event": "after_insert"}, + {"label": "Channel Deleted", "doctype": "Raven Channel", "event": "on_trash"}, { - - 'label': 'Channel Deleted', - 'doctype': 'Raven Channel', - 'event': 'on_trash' + "label": "Member Added to the Channel", + "doctype": "Raven Channel Member", + "event": "after_insert", }, { - 'label': 'Member Added to the Channel', - 'doctype': 'Raven Channel Member', - 'event': 'after_insert' + "label": "Member Deleted from the Channel", + "doctype": "Raven Channel Member", + "event": "on_trash", }, - { - 'label': 'Member Deleted from the Channel', - 'doctype': 'Raven Channel Member', - 'event': 'on_trash' - }, - { - 'label': 'User Added', - 'doctype': 'Raven User', - 'event': 'after_insert' - }, - { - 'label': 'User Deleted', - 'doctype': 'Raven User', - 'event': 'on_trash' - } + {"label": "User Added", "doctype": "Raven User", "event": "after_insert"}, + {"label": "User Deleted", "doctype": "Raven User", "event": "on_trash"}, ] doctype, event = None, None for doctype_and_event in doctypes_and_events: - if self.webhook_trigger == doctype_and_event['label']: - doctype = doctype_and_event['doctype'] - event = doctype_and_event['event'] + if self.webhook_trigger == doctype_and_event["label"]: + doctype = doctype_and_event["doctype"] + event = doctype_and_event["event"] break return doctype, event @@ -209,42 +187,42 @@ def get_conditions(self): # Get the conditions for the webhook doctype, event = self.get_doctype_and_event() if self.trigger_webhook_on_condition: - if self.conditions_on == 'Channel': - if doctype == 'Raven Channel': + if self.conditions_on == "Channel": + if doctype == "Raven Channel": # return 'doc.name == self.channel_id' return f'doc.name == "{self.channel_id}"' - elif doctype == 'Raven Channel Member': + elif doctype == "Raven Channel Member": return f'doc.channel_id == "{self.channel_id}"' - elif doctype == 'Raven Message': + elif doctype == "Raven Message": return f'doc.channel_id == "{self.channel_id}"' - elif doctype == 'Raven Message Reaction': - frappe.throw('Message Reaction cannot be triggered on Channel') - elif doctype == 'Raven User': - frappe.throw('Raven User cannot be triggered on Channel') - - elif self.conditions_on == 'User': - if doctype == 'Raven Channel': - frappe.throw('Channel cannot be triggered on User') - elif doctype == 'Raven Channel Member': + elif doctype == "Raven Message Reaction": + frappe.throw("Message Reaction cannot be triggered on Channel") + elif doctype == "Raven User": + frappe.throw("Raven User cannot be triggered on Channel") + + elif self.conditions_on == "User": + if doctype == "Raven Channel": + frappe.throw("Channel cannot be triggered on User") + elif doctype == "Raven Channel Member": return f'doc.user_id == "{self.user}"' - elif doctype == 'Raven Message': + elif doctype == "Raven Message": return f'doc.owner == "{self.user}"' - elif doctype == 'Raven Message Reaction': + elif doctype == "Raven Message Reaction": return f'doc.owner == "{self.user}"' - elif self.conditions_on == 'Channel Type': - if doctype == 'Raven Channel': - if self.channel_type in ['Public', 'Private', 'Open']: + elif self.conditions_on == "Channel Type": + if doctype == "Raven Channel": + if self.channel_type in ["Public", "Private", "Open"]: return f'doc.type == "{self.channel_type}"' - elif self.channel_type == 'DM': - return f'doc.is_direct_message == 1' - elif self.channel_type == 'Self Message': - return f'doc.is_self_message == 1' + elif self.channel_type == "DM": + return f"doc.is_direct_message == 1" + elif self.channel_type == "Self Message": + return f"doc.is_self_message == 1" else: - frappe.throw('Invalid Channel Type') + frappe.throw("Invalid Channel Type") else: - frappe.throw('Channel Type cannot be triggered on other doctypes') - elif self.conditions_on == 'Custom': + frappe.throw("Channel Type cannot be triggered on other doctypes") + elif self.conditions_on == "Custom": return self.condition return None diff --git a/raven/raven_messaging/doctype/raven_poll_vote/raven_poll_vote.json b/raven/raven_messaging/doctype/raven_poll_vote/raven_poll_vote.json index dea96348a..751c9f845 100644 --- a/raven/raven_messaging/doctype/raven_poll_vote/raven_poll_vote.json +++ b/raven/raven_messaging/doctype/raven_poll_vote/raven_poll_vote.json @@ -53,7 +53,7 @@ ], "index_web_pages_for_search": 1, "links": [], - "modified": "2024-03-22 20:08:19.982449", + "modified": "2024-04-08 13:32:30.594728", "modified_by": "Administrator", "module": "Raven Messaging", "name": "Raven Poll Vote", @@ -73,6 +73,7 @@ }, { "create": 1, + "delete": 1, "email": 1, "export": 1, "if_owner": 1,