Skip to content

Commit bb4804e

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

File tree

3 files changed

+131
-2
lines changed

3 files changed

+131
-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: 81 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,
@@ -18,6 +22,7 @@ import { shortTime } from '../../utils/date';
1822
import aggregateReactions from '../../reactions/aggregateReactions';
1923
import { codeToEmojiMap } from '../../emoji/data';
2024
import processAlertWords from './processAlertWords';
25+
import * as logging from '../../utils/logging';
2126

2227
const messageTagsAsHtml = (isStarred: boolean, timeEdited: number | void): string => {
2328
const pieces = [];
@@ -72,13 +77,87 @@ $!${messageReactionListAsHtml(reactions, ownUser.user_id, allImageEmojiById)}
7277
`;
7378
};
7479

75-
const widgetBody = (message: Message | Outbox) => template`
80+
/**
81+
* Render the body of a message that has submessages.
82+
*
83+
* Must not be called on a message without any submessages.
84+
*/
85+
const widgetBody = (message: Message, ownUserId: UserId) => {
86+
invariant(
87+
message.submessages !== undefined && message.submessages.length > 0,
88+
'should have submessages',
89+
);
90+
91+
const widgetSubmessages = message.submessages
92+
.filter(submessage => submessage.msg_type === 'widget')
93+
.map(submessage => ({
94+
...submessage,
95+
content: JSON.parse(submessage.content),
96+
}));
97+
98+
const poll_widgets = widgetSubmessages.filter(widget => widget.content.widget_type === 'poll');
99+
// "events" are submessages other than the main widget submessage - for
100+
// instance, `vote`, `new_option`, and similar submessages. These are not
101+
// documented, look at the PollData code if you're interested in what the
102+
// possibilities are.
103+
const events = widgetSubmessages.filter(widget => 'type' in widget.content);
104+
105+
if (poll_widgets.length == 0) {
106+
return template`
76107
$!${message.content}
77108
<div class="special-message"
78109
><p>Interactive message</p
79110
><p>To use, open on web or desktop</p
80111
></div>
81112
`;
113+
}
114+
const poll_widget = poll_widgets[0];
115+
116+
if (!poll_widget.content || !poll_widget.content.extra_data) {
117+
// We don't expect this to happen in general, but there are some malformed
118+
// messages lying around that will trigger this.
119+
return template`$!${message.content}`;
120+
}
121+
122+
const poll_data = new PollData({
123+
current_user_id: ownUserId,
124+
is_my_poll: message.sender_id === ownUserId,
125+
question: poll_widget.content.extra_data.question,
126+
options: poll_widget.content.extra_data.options,
127+
comma_separated_names: () => {},
128+
report_error_function: (msg: string) => {
129+
logging.error(msg);
130+
},
131+
});
132+
133+
for (const poll_event of events) {
134+
poll_data.handle_event(poll_event.sender_id, poll_event.content);
135+
}
136+
137+
const parsed_poll_data = poll_data.get_widget_data();
138+
139+
return template`
140+
<div class="poll-widget">
141+
<p class="poll-question">${parsed_poll_data.question}</p>
142+
<ul>
143+
$!${parsed_poll_data.options
144+
.map(
145+
option =>
146+
template`
147+
<li>
148+
<button
149+
class="poll-vote"
150+
data-voted="${option.current_user_vote}"
151+
data-key="${option.key}"
152+
>${option.count}</button>
153+
<span class="poll-option">${option.option}</span>
154+
</li>`,
155+
)
156+
.join('')}
157+
</ul>
158+
</div>
159+
`;
160+
};
82161

83162
export const flagsStateToStringList = (flags: FlagsState, id: number): string[] =>
84163
Object.keys(flags).filter(key => flags[key][id]);
@@ -112,7 +191,7 @@ export default (
112191
`;
113192
const bodyHtml =
114193
message.submessages && message.submessages.length > 0
115-
? widgetBody(message)
194+
? widgetBody(message, backgroundData.ownUser.user_id)
116195
: messageBody(backgroundData, message);
117196

118197
if (isBrief) {

src/webview/static/base.css

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

0 commit comments

Comments
 (0)