Skip to content

Commit edc7410

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

File tree

3 files changed

+134
-2
lines changed

3 files changed

+134
-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: 84 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,6 +14,7 @@ import type {
1014
MessageLike,
1115
Outbox,
1216
Reaction,
17+
SubmessageData,
1318
ImageEmojiType,
1419
UserId,
1520
} from '../../types';
@@ -18,6 +23,7 @@ import { shortTime } from '../../utils/date';
1823
import aggregateReactions from '../../reactions/aggregateReactions';
1924
import { codeToEmojiMap } from '../../emoji/data';
2025
import processAlertWords from './processAlertWords';
26+
import * as logging from '../../utils/logging';
2127

2228
const messageTagsAsHtml = (isStarred: boolean, timeEdited: number | void): string => {
2329
const pieces = [];
@@ -72,13 +78,89 @@ $!${messageReactionListAsHtml(reactions, ownUser.user_id, allImageEmojiById)}
7278
`;
7379
};
7480

75-
const widgetBody = (message: Message | Outbox) => template`
81+
/**
82+
* Render the body of a message that has submessages.
83+
*
84+
* Must not be called on a message without any submessages.
85+
*/
86+
const widgetBody = (message: Message, ownUserId: UserId) => {
87+
invariant(
88+
message.submessages !== undefined && message.submessages.length > 0,
89+
'should have submessages',
90+
);
91+
92+
const sortedSubmessages = message.submessages.concat().sort((m1, m2) => m1.id - m2.id);
93+
94+
const widgetSubmessages: Array<{
95+
sender_id: number,
96+
content: SubmessageData,
97+
}> = sortedSubmessages
98+
.filter(submessage => submessage.msg_type === 'widget')
99+
.map(submessage => ({
100+
sender_id: submessage.sender_id,
101+
content: JSON.parse(submessage.content),
102+
}));
103+
104+
const pollWidget = widgetSubmessages.shift();
105+
if (!pollWidget || !pollWidget.content || pollWidget.content.widget_type !== 'poll') {
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+
115+
if (pollWidget.content.extra_data == null) {
116+
// We don't expect this to happen in general, but there are some malformed
117+
// messages lying around that will trigger this.
118+
return template`$!${message.content}`;
119+
}
120+
121+
const pollData = new PollData({
122+
message_sender_id: message.sender_id,
123+
current_user_id: ownUserId,
124+
is_my_poll: message.sender_id === ownUserId,
125+
question:
126+
pollWidget.content.extra_data.question == null ? '' : pollWidget.content.extra_data.question,
127+
options:
128+
pollWidget.content.extra_data.options == null ? [] : pollWidget.content.extra_data.options,
129+
// TODO: Implement this.
130+
comma_separated_names: () => '',
131+
report_error_function: (msg: string) => {
132+
logging.error(msg);
133+
},
134+
});
135+
136+
for (const pollEvent of widgetSubmessages) {
137+
pollData.handle_event(pollEvent.sender_id, pollEvent.content);
138+
}
139+
140+
const parsedPollData = pollData.get_widget_data();
141+
142+
return template`
143+
<div class="poll-widget">
144+
<p class="poll-question">${parsedPollData.question}</p>
145+
<ul>
146+
$!${parsedPollData.options
147+
.map(
148+
option =>
149+
template`
150+
<li>
151+
<button
152+
class="poll-vote"
153+
data-voted="${option.current_user_vote}"
154+
data-key="${option.key}"
155+
>${option.count}</button>
156+
<span class="poll-option">${option.option}</span>
157+
</li>`,
158+
)
159+
.join('')}
160+
</ul>
161+
</div>
162+
`;
163+
};
82164

83165
export const flagsStateToStringList = (flags: FlagsState, id: number): string[] =>
84166
Object.keys(flags).filter(key => flags[key][id]);
@@ -112,7 +194,7 @@ export default (
112194
`;
113195
const bodyHtml =
114196
message.submessages && message.submessages.length > 0
115-
? widgetBody(message)
197+
? widgetBody(message, backgroundData.ownUser.user_id)
116198
: messageBody(backgroundData, message);
117199

118200
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)