Skip to content

Commit 72c6e52

Browse files
committed
webview: Add read-only support for poll widgets.
Related: zulip#3205
1 parent f283738 commit 72c6e52

File tree

3 files changed

+151
-2
lines changed

3 files changed

+151
-2
lines changed

src/webview/css/cssNight.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ body {
55
color: hsl(210, 11%, 85%);
66
background: hsl(212, 28%, 18%);
77
}
8+
.poll-vote {
9+
color: hsl(210, 11%, 85%);
10+
}
811
.topic-header {
912
background: hsl(212, 13%, 38%);
1013
}

src/webview/html/messageAsHtml.js

Lines changed: 101 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
/* @flow strict-local */
22
import { PixelRatio } from 'react-native';
3+
import invariant from 'invariant';
34
import distanceInWordsToNow from 'date-fns/distance_in_words_to_now';
5+
// $FlowFixMe[untyped-import]
6+
import { PollData } from '@zulip/shared/js/poll_data';
7+
48
import template from './template';
59
import type {
610
AggregatedReaction,
@@ -10,14 +14,17 @@ import type {
1014
MessageLike,
1115
Outbox,
1216
Reaction,
17+
SubmessageData,
1318
ImageEmojiType,
1419
UserId,
20+
WidgetData,
1521
} from '../../types';
1622
import type { BackgroundData } from '../MessageList';
1723
import { shortTime } from '../../utils/date';
1824
import aggregateReactions from '../../reactions/aggregateReactions';
1925
import { codeToEmojiMap } from '../../emoji/data';
2026
import processAlertWords from './processAlertWords';
27+
import * as logging from '../../utils/logging';
2128

2229
const messageTagsAsHtml = (isStarred: boolean, timeEdited: number | void): string => {
2330
const pieces = [];
@@ -72,14 +79,106 @@ $!${messageReactionListAsHtml(reactions, ownUser.user_id, allImageEmojiById)}
7279
`;
7380
};
7481

75-
const widgetBody = (message: Message | Outbox) => template`
82+
/**
83+
* Render the body of a message that has submessages.
84+
*
85+
* Must not be called on a message without any submessages.
86+
*/
87+
const widgetBody = (message: Message, ownUserId: UserId) => {
88+
invariant(
89+
message.submessages !== undefined && message.submessages.length > 0,
90+
'should have submessages',
91+
);
92+
93+
const widgetSubmessages: Array<{
94+
sender_id: number,
95+
content: SubmessageData,
96+
...
97+
}> = message.submessages
98+
.filter(submessage => submessage.msg_type === 'widget')
99+
.sort((m1, m2) => m1.id - m2.id)
100+
.map(submessage => ({
101+
sender_id: submessage.sender_id,
102+
content: JSON.parse(submessage.content),
103+
}));
104+
105+
const errorMessage = template`
76106
$!${message.content}
77107
<div class="special-message"
78108
><p>Interactive message</p
79109
><p>To use, open on web or desktop</p
80110
></div>
81111
`;
82112

113+
const pollWidget = widgetSubmessages.shift();
114+
if (!pollWidget || !pollWidget.content) {
115+
return errorMessage;
116+
}
117+
118+
/* $FlowFixMe[incompatible-type]: The first widget submessage should be
119+
a `WidgetData`; see jsdoc on `SubmessageData`. */
120+
const pollWidgetContent: WidgetData = pollWidget.content;
121+
122+
if (pollWidgetContent.widget_type !== 'poll') {
123+
return errorMessage;
124+
}
125+
126+
if (pollWidgetContent.extra_data == null) {
127+
// We don't expect this to happen in general, but there are some malformed
128+
// messages lying around that will trigger this [1]. The code here is slightly
129+
// different the webapp code, but mostly because the current webapp
130+
// behaviour seems accidental: an error is printed to the console, and the
131+
// code that is written to handle the situation is never reached. Instead
132+
// of doing that, we've opted to catch this case here, and print out the
133+
// message (which matches the behaviour of the webapp, minus the console
134+
// error, although it gets to that behaviour in a different way). The bug
135+
// tracking fixing this on the webapp side is zulip/zulip#19145.
136+
// [1]: https://chat.zulip.org/#narrow/streams/public/near/582872
137+
return template`$!${message.content}`;
138+
}
139+
140+
const pollData = new PollData({
141+
message_sender_id: message.sender_id,
142+
current_user_id: ownUserId,
143+
is_my_poll: message.sender_id === ownUserId,
144+
question: pollWidgetContent.extra_data.question ?? '',
145+
options: pollWidgetContent.extra_data.options ?? [],
146+
// TODO: Implement this.
147+
comma_separated_names: () => '',
148+
report_error_function: (msg: string) => {
149+
logging.error(msg);
150+
},
151+
});
152+
153+
for (const pollEvent of widgetSubmessages) {
154+
pollData.handle_event(pollEvent.sender_id, pollEvent.content);
155+
}
156+
157+
const parsedPollData = pollData.get_widget_data();
158+
159+
return template`
160+
<div class="poll-widget">
161+
<p class="poll-question">${parsedPollData.question}</p>
162+
<ul>
163+
$!${parsedPollData.options
164+
.map(
165+
option =>
166+
template`
167+
<li>
168+
<button
169+
class="poll-vote"
170+
data-voted="${option.current_user_vote}"
171+
data-key="${option.key}"
172+
>${option.count}</button>
173+
<span class="poll-option">${option.option}</span>
174+
</li>`,
175+
)
176+
.join('')}
177+
</ul>
178+
</div>
179+
`;
180+
};
181+
83182
export const flagsStateToStringList = (flags: FlagsState, id: number): string[] =>
84183
Object.keys(flags).filter(key => flags[key][id]);
85184

@@ -112,7 +211,7 @@ export default (
112211
`;
113212
const bodyHtml =
114213
message.submessages && message.submessages.length > 0
115-
? widgetBody(message)
214+
? widgetBody(message, backgroundData.ownUser.user_id)
116215
: messageBody(backgroundData, message);
117216

118217
if (isBrief) {

src/webview/static/base.css

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -696,3 +696,50 @@ h1, h2, h3, h4, h5, h6 {
696696
.spoiler-block .spoiler-arrow.spoiler-button-open::after {
697697
transform: rotate(90deg) translate(10px, 0);
698698
}
699+
700+
/* Poll styling */
701+
702+
.poll-widget {
703+
border: hsl(0, 0%, 50%) 1px solid;
704+
padding: 2px 8px 2px 10px;
705+
border-radius: 10px;
706+
}
707+
708+
.poll-question {
709+
font-size: 1.2rem;
710+
margin-bottom: 8px;
711+
border-bottom: 1px solid hsla(0, 0%, 60%, 0.2);
712+
}
713+
714+
.poll-widget > ul {
715+
padding: 0px;
716+
}
717+
718+
.poll-widget > ul > li {
719+
list-style: none;
720+
margin-bottom: 4px;
721+
display: flex;
722+
align-items: center;
723+
}
724+
725+
.poll-vote {
726+
background-color: hsla(0, 0%, 0%, 0);
727+
border: 1.5px solid hsl(0, 0%, 50%);
728+
border-radius: 8px;
729+
height: 36px;
730+
min-width: 36px;
731+
padding: 1px 8px;
732+
font-weight: bold;
733+
font-size: 18px;
734+
flex-shrink: 0;
735+
}
736+
737+
.poll-vote[data-voted="true"] {
738+
border: 1.5px solid hsl(222, 99%, 69%);
739+
background-color: hsla(222, 99%, 69%, 25%);
740+
}
741+
742+
.poll-option {
743+
margin-left: 8px;
744+
width: 100%;
745+
}

0 commit comments

Comments
 (0)