Skip to content

Commit

Permalink
Merge pull request #53 from krowter/fix/handle-poll-message
Browse files Browse the repository at this point in the history
fix: handle messages of type poll
  • Loading branch information
krowter committed Mar 16, 2024
2 parents e9f23a6 + c67eb8a commit dbbfb12
Show file tree
Hide file tree
Showing 5 changed files with 230 additions and 4 deletions.
8 changes: 5 additions & 3 deletions src/components/chat-bubble/ChatBubble.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Match, Show, Switch } from "solid-js";
import type { TelegramMessage } from "~/service/Parser";
import { MediaPoll } from "./MediaPoll";
import "./ChatBubble.scss";

type ChatBubbleProps = TelegramMessage & {
Expand Down Expand Up @@ -47,18 +48,19 @@ export function ChatBubble(props: ChatBubbleProps) {
<Match when={props.hasMedia}>
<img
class={resolveMediaClassname()}
src={props.hasMedia ? `/chats/${props.slug}/${props.file}` : ""}
src={props.hasMedia && props.mediaType !== "poll" ? `/chats/${props.slug}/${props.file}` : ""}
/>
<Show when={props.text.length}>
<div
class="chat-content"
style={{
"padding": props.repliedTo ? "0" : "1rem 2rem 1.5rem 2rem",
"padding-top": "1rem"
padding: props.repliedTo ? "0" : "1rem 2rem 1.5rem 2rem",
"padding-top": "1rem",
}}
innerHTML={props.text}
/>
</Show>
{props.hasMedia && props.mediaType === "poll" && <MediaPoll {...props} />}
</Match>
<Match when={!props.hasMedia}>
<div class="chat-content" innerHTML={props.text} />
Expand Down
51 changes: 51 additions & 0 deletions src/components/chat-bubble/MediaPoll.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
@use "../../styles/variables.scss";

.poll {
display: flex;
align-items: center;
justify-content: center;
gap: 8rem;
padding: 2rem;

&-chart {
width: 50%;

rect {
stroke: variables.$black;
stroke-width: 0.01rem;
}
}

&-legend {
span {
display: block;
}
span::before {
content: "";
display: inline-block;
width: 2rem;
height: 2rem;
margin-right: 0.5rem;
background-color: var(--marker-color);
border: 0.2rem variables.$black solid;
vertical-align: middle;
}
}
}
@media (max-width: variables.$screen-md-min) {
.poll {
flex-direction: column;
gap: 2rem;

&-chart {
width: 80%;
}

&-legend {
span::before {
width: 1rem;
height: 1rem;
}
}
}
}
60 changes: 60 additions & 0 deletions src/components/chat-bubble/MediaPoll.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import type { TelegramMessagePoll } from "~/service/Parser";
import "./MediaPoll.scss";

// colorblind-friendly colors
// and Telegram has maximum of 10 options
const colors = [
"#1F77B4", // Strong Blue
"#FF7F0E", // Strong Orange
"#2CA02C", // Strong Green
"#D62728", // Strong Red
"#9467BD", // Strong Purple
"#8C564B", // Strong Brown
"#E377C2", // Strong Pink
"#7F7F7F", // Medium Gray
"#BCBD22", // Strong Yellow
"#17BECF", // Strong Cyan
];

export function MediaPoll(props: TelegramMessagePoll) {
const chartSize = 20;
const barWidth = 20 / props.pollResult.length;

const maxVotes = Math.max(...props.pollResult.map((opt) => opt.votes));
const normalizeVotes = (vote: number) => (vote / maxVotes) * chartSize * /* provide some top padding */ 0.8;

return (
<div class="poll">
<svg class="poll-chart" xmlns="http://www.w3.org/2000/svg" viewBox={`0 0 ${chartSize} ${chartSize}`}>
<title innerHTML={props.text} />

<g class="poll-bars">
{props.pollResult.map((option, i) => (
<>
<rect
x={i * barWidth}
y={chartSize - normalizeVotes(option.votes)}
height={normalizeVotes(option.votes)}
width={barWidth}
fill={colors[i] ?? "#7f7f7f"}
/>
<text
x={i * barWidth + barWidth / 2}
y={chartSize - normalizeVotes(option.votes) - 2}
font-size="0.14rem"
text-anchor="middle"
>
{option.votes}
</text>
</>
))}
</g>
</svg>
<div class="poll-legend">
{props.pollResult.map((option, i) => (
<span style={{ "--marker-color": colors[i] }}>{option.text}</span>
))}
</div>
</div>
);
}
47 changes: 46 additions & 1 deletion src/service/Parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,12 +54,23 @@ export type TelegramMessageWithVideoFile = {
height: number;
} & TelegramMessageBase;

export type TelegramMessagePoll = {
hasMedia: true;
mediaType: "poll";
pollResult: {
text: string;
votes: number;
}[];
explanation?: string;
} & TelegramMessageBase;

export type TelegramMessage =
| (TelegramMessageBase & { hasMedia: false })
| TelegramMessageWithAnimation
| TelegramMessageWithPhoto
| TelegramMessageWithSticker
| TelegramMessageWithVideoFile;
| TelegramMessageWithVideoFile
| TelegramMessagePoll;

export type TelegramChat = {
chatName: string;
Expand Down Expand Up @@ -131,6 +142,20 @@ const exportedChatHistorySchema = z.object({
})
)
.optional(),
poll: z
.object({
question: z.string(),
closed: z.boolean(),
total_voters: z.number(),
answers: z.array(
z.object({
text: z.string(),
voters: z.number(),
chosen: z.boolean(),
})
),
})
.optional(),
})
),
});
Expand Down Expand Up @@ -349,6 +374,26 @@ export class Parser {
mimeType: message.mime_type ?? "",
});
continue;
} else if (message.poll !== undefined) {
telegramChat.message.push({
date: new Date(message.date),
from: {
name: message.from,
id: Number.parseInt(message.from_id.replace("user", "")),
},
messageId: message.id,
replyToMessageId: message.reply_to_message_id,
text: message.poll.question,
hasMedia: true,
mediaType: "poll",
pollResult: message.poll.answers.map((answer) => {
return {
text: answer.text,
votes: answer.voters,
};
}),
});
continue;
} else {
telegramChat.message.push({
date: new Date(message.date),
Expand Down
68 changes: 68 additions & 0 deletions tests/service/Parser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1709,3 +1709,71 @@ test("parse without text_entity", () => {
},
]);
});

test("message poll", () => {
const stub = `{
"name": "Teknologi Umum v2.0",
"type": "public_supergroup",
"id": 1712691810,
"messages": [
{
"id": 632576,
"type": "message",
"date": "2024-02-25T11:53:31",
"date_unixtime": "1708836811",
"from": "Reinaldy",
"from_id": "user1462097294",
"poll": {
"question": "Apakah Anda tahu kalau SSL certificate bisa digunakan selain untuk keperluan website (https)?",
"closed": false,
"total_voters": 25,
"answers": [
{
"text": "Tahu",
"voters": 11,
"chosen": true
},
{
"text": "Tidak",
"voters": 14,
"chosen": false
}
]
},
"text": "",
"text_entities": []
}
]
}`;

const parser = new Parser();

const telegramChat = parser.fromExportedChatHistory(stub);

expect(telegramChat.chatId).toEqual(1712691810);
expect(telegramChat.chatName).toEqual("Teknologi Umum v2.0");
expect(telegramChat.message).toStrictEqual([
{
date: new Date("2024-02-25T11:53:31"),
from: {
id: 1462097294,
name: "Reinaldy",
},
hasMedia: true,
mediaType: "poll",
messageId: 632576,
replyToMessageId: undefined,
text: "Apakah Anda tahu kalau SSL certificate bisa digunakan selain untuk keperluan website (https)?",
pollResult: [
{
text: "Tahu",
votes: 11,
},
{
text: "Tidak",
votes: 14,
},
],
},
]);
});

0 comments on commit dbbfb12

Please sign in to comment.