From 778a557372355d19b7804b71ff6e7c7e5f57311f Mon Sep 17 00:00:00 2001 From: Loris Bettazza Date: Wed, 11 Sep 2024 01:34:12 +0200 Subject: [PATCH] :sparkles: Add graphical support for polls Closes #43 --- src/components/Message/Message.tsx | 28 +++++++++----- src/components/Poll/Poll.tsx | 27 +++++++++++++ src/components/Poll/style.ts | 62 ++++++++++++++++++++++++++++++ src/types.ts | 21 +++++++++- src/utils/poll-parser.ts | 31 +++++++++++++++ 5 files changed, 159 insertions(+), 10 deletions(-) create mode 100644 src/components/Poll/Poll.tsx create mode 100644 src/components/Poll/style.ts create mode 100644 src/utils/poll-parser.ts diff --git a/src/components/Message/Message.tsx b/src/components/Message/Message.tsx index f206922..8f3ebc2 100644 --- a/src/components/Message/Message.tsx +++ b/src/components/Message/Message.tsx @@ -2,8 +2,10 @@ import { Suspense } from 'react'; import Linkify from 'react-linkify'; import Attachment from '../Attachment/Attachment'; +import Poll from '../Poll/Poll'; import * as S from './style'; import { IndexedMessage } from '../../types'; +import { parsePollMessage } from '../../utils/poll-parser'; function Link( decoratedHref: string, @@ -32,6 +34,22 @@ function Message({ }: IMessage) { const isSystem = !message.author; const dateTime = message.date.toISOString().slice(0, 19).replace('T', ' '); + const pollData = parsePollMessage(message.message); + let messageComponent = ( + + {message.message} + + ); + + if (message.attachment) { + messageComponent = ( + + + + ); + } else if (pollData !== null) { + messageComponent = ; + } return ( {message.author} )} - {message.attachment ? ( - - - - ) : ( - - {message.message} - - )} + {messageComponent} {!isSystem && ( diff --git a/src/components/Poll/Poll.tsx b/src/components/Poll/Poll.tsx new file mode 100644 index 0000000..b7a6232 --- /dev/null +++ b/src/components/Poll/Poll.tsx @@ -0,0 +1,27 @@ +import { PollStructure } from '../../types'; +import * as S from './style'; + +interface IPoll { + pollData: PollStructure; +} + +function Poll({ pollData }: IPoll) { + return ( + + {pollData.title} + {pollData.options.map(option => { + return ( + +
{option.text}
+ + +
{option.votes}
+
+
+ ); + })} +
+ ); +} + +export default Poll; diff --git a/src/components/Poll/style.ts b/src/components/Poll/style.ts new file mode 100644 index 0000000..dd098aa --- /dev/null +++ b/src/components/Poll/style.ts @@ -0,0 +1,62 @@ +import styled from 'styled-components'; +import { + activeUserDarkBackgroundColor, + viewerDarkBackgroundColor, + whatsappThemeColor, +} from '../../utils/colors'; + +const Poll = styled.div` + display: flex; + flex-direction: column; + gap: 0.6rem; +`; + +const Title = styled.div` + font-weight: bolder; +`; + +const Option = styled.div` + display: flex; + flex-direction: column; + gap: 0.2rem; +`; + +const Flex = styled.div` + display: flex; + gap: 0.5rem; + align-items: center; + font-size: 0.8rem; +`; + +const Progress = styled.progress` + display: block; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.15); + width: 100%; + max-width: 500px; + height: 10px; + appearance: none; + border-radius: 99px; + padding: 1px; + + &::-webkit-progress-bar { + background-color: white; + border-radius: 99px; + } + + &::-webkit-progress-value { + background-color: ${whatsappThemeColor}; + border-radius: 99px; + } + + @media (prefers-color-scheme: dark) { + &::-webkit-progress-bar { + background-color: color-mix( + in srgb, + ${activeUserDarkBackgroundColor}, + ${viewerDarkBackgroundColor} + ); + } + } +`; + +export { Poll, Title, Option, Flex, Progress }; diff --git a/src/types.ts b/src/types.ts index 79343c5..6dc4ec8 100644 --- a/src/types.ts +++ b/src/types.ts @@ -19,4 +19,23 @@ interface DateBounds { end: Date; } -export type { FilterMode, ExtractedFile, IndexedMessage, ILimits, DateBounds }; +interface PollOption { + text: string; + votes: number; +} + +interface PollStructure { + title: string; + options: PollOption[]; + maxVotes: number; +} + +export type { + FilterMode, + ExtractedFile, + IndexedMessage, + ILimits, + DateBounds, + PollOption, + PollStructure, +}; diff --git a/src/utils/poll-parser.ts b/src/utils/poll-parser.ts new file mode 100644 index 0000000..bf5ca00 --- /dev/null +++ b/src/utils/poll-parser.ts @@ -0,0 +1,31 @@ +import { PollOption, PollStructure } from '../types'; + +function parsePollMessage(message: string): PollStructure | null { + const lines = message + .trim() + .split('\n') + .map(line => line.trim()); + + if (!lines[0].includes('POLL:') || !lines[2]?.includes('OPTION:')) + return null; + + const optionRegex = /OPTION: (?.+) \((?\d+).*\)/; + + const options = lines.slice(2).reduce((acc, line) => { + const match = optionRegex.exec(line); + + if (!match?.groups) return acc; + + const { text, votes } = match.groups; + + return acc.concat({ text, votes: Number(votes) }); + }, []); + + return { + title: lines[1], + options, + maxVotes: Math.max(...options.map(o => o.votes)), + }; +} + +export { parsePollMessage };