Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/api/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import getMessages from './messages/getMessages';
import getMessageHistory from './messages/getMessageHistory';
import messagesFlags from './messages/messagesFlags';
import sendMessage from './messages/sendMessage';
import sendSubmessage from './submessages/sendSubmessage';
import updateMessage from './messages/updateMessage';
import savePushToken from './notifications/savePushToken';
import forgetPushToken from './notifications/forgetPushToken';
Expand Down Expand Up @@ -77,6 +78,7 @@ export {
getMessageHistory,
messagesFlags,
sendMessage,
sendSubmessage,
updateMessage,
savePushToken,
forgetPushToken,
Expand Down Expand Up @@ -132,6 +134,7 @@ export default {
getMessageHistory,
messagesFlags,
sendMessage,
sendSubmessage,
updateMessage,
savePushToken,
forgetPushToken,
Expand Down
13 changes: 13 additions & 0 deletions src/api/submessages/sendSubmessage.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/* @flow strict-local */

import type { ApiResponse, Auth } from '../transportTypes';
import { apiPost } from '../apiFetch';

/** See https://zulip.readthedocs.io/en/latest/subsystems/widgets.html#poll-todo-lists-and-games */
// `msg_type` only exists as widget at the moment, see #3205.
export default async (auth: Auth, messageId: number, content: string): Promise<ApiResponse> =>
apiPost(auth, 'submessage', {
message_id: messageId,
msg_type: 'widget',
content,
});
12 changes: 11 additions & 1 deletion src/types.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,16 @@
import type { IntlShape } from 'react-intl';
import type { DangerouslyImpreciseStyleProp } from 'react-native/Libraries/StyleSheet/StyleSheet';

import type { Auth, Topic, Message, Reaction, Narrow, CrossRealmBot, User } from './api/apiTypes';
import type {
Auth,
Topic,
Message,
Submessage,
Reaction,
Narrow,
CrossRealmBot,
User,
} from './api/apiTypes';
import type { AppStyles } from './styles/theme';

export type * from './reduxTypes';
Expand Down Expand Up @@ -157,6 +166,7 @@ export type Outbox = {|
sender_email: string,
sender_full_name: string,
subject: string,
submessages?: $ReadOnlyArray<Submessage>,
timestamp: number,
type: 'stream' | 'private',

Expand Down
3 changes: 3 additions & 0 deletions src/webview/MessageList.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import {
getAllImageEmojiById,
getCurrentTypingUsers,
getDebug,
getOwnUser,
getRenderedMessages,
getFlags,
getAnchorForNarrow,
Expand Down Expand Up @@ -69,6 +70,7 @@ export type BackgroundData = $ReadOnly<{
flags: FlagsState,
mute: MuteState,
ownEmail: string,
ownUser: User,
allImageEmojiById: $ReadOnly<{ [id: string]: ImageEmojiType }>,
twentyFourHourTime: boolean,
subscriptions: Subscription[],
Expand Down Expand Up @@ -226,6 +228,7 @@ export default connect((state, props: OuterProps): SelectorProps => {
flags: getFlags(state),
mute: getMute(state),
ownEmail: getOwnEmail(state),
ownUser: getOwnUser(state),
allImageEmojiById: getAllImageEmojiById(state),
subscriptions: getSubscriptions(state),
twentyFourHourTime: getRealm(state).twentyFourHourTime,
Expand Down
56 changes: 42 additions & 14 deletions src/webview/css/css.js
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,8 @@ hr {
}
.avatar,
.header-wrapper,
.message-brief {
.message-brief,
.poll-button {
cursor: pointer;
}
.stream-header {
Expand Down Expand Up @@ -362,11 +363,7 @@ blockquote {
margin: 8px 0;
}
.reaction {
color: hsl(0, 0%, 50%);
display: inline-block;
padding: 5px 6.5px;
border-radius: 3px;
border: 1px solid hsla(0, 0%, 50%, 0.75);
line-height: 1rem;
height: 1rem;
margin: 4px 8px 4px 0;
Expand All @@ -378,11 +375,6 @@ blockquote {
max-width: 1rem;
vertical-align: top;
}
.self-voted {
color: ${BRAND_COLOR};
border: 1px solid ${BRAND_COLOR};
background: hsla(177.1, 69.7%, 46.7%, 0.1);
}
.hidden {
display: none;
}
Expand All @@ -398,14 +390,50 @@ blockquote {
color: white;
}
.widget {
padding: 1rem;
border: 1px solid hsl(0, 0%, 60%);
border-radius: 0.5rem;
}
.dummy-widget {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 1rem;
background: hsla(0, 0%, 50%, 0.1);
border: 1px dashed hsla(0, 0%, 50%, 0.5);
border-radius: 0.5rem;
}
.poll-question {
text-overflow: ellipsis;
font-size: 20px;
margin-bottom: 4px;
border-bottom: 1px solid hsla(0, 0%, 60%, 0.2);
}
.poll-button {
height: 24px;
width: 32px;
margin: 4px 4px 4px 0;
text-align: center;
}
.poll-button, .reaction {
border-radius: 3px;
border: 1px solid hsla(0, 0%, 50%, 0.75);
color: hsl(0, 0%, 50%);
display: inline-block;
}
.reaction.self-voted, .poll-button.self-voted {
color: ${BRAND_COLOR};
border-color: ${BRAND_COLOR};
background: hsla(177, 70%, 47%, 0.1);
}
.poll-option {
display: inline;
}
.poll:after {
content: "Poll";
display: block;
font-size: 12px;
font-style: italic;
color: hsl(0, 0%, 60%);
text-align: right;
margin: 0 -8px -12px 0;
}
#typing {
display: flex;
Expand Down
13 changes: 4 additions & 9 deletions src/webview/html/messageAsHtml.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { shortTime } from '../../utils/date';
import aggregateReactions from '../../reactions/aggregateReactions';
import { codeToEmojiMap } from '../../emoji/data';
import processAlertWords from './processAlertWords';
import widgetAsHtml from './widgetAsHtml';

const messageTagsAsHtml = (isStarred: boolean, timeEdited: number | void): string => {
const pieces = [];
Expand Down Expand Up @@ -69,14 +70,6 @@ $!${messageReactionListAsHtml(reactions, ownEmail, allImageEmojiById)}
`;
};

const widgetBody = (message: Message | Outbox) => template`
$!${message.content}
<div class="widget"
><p>Interactive message</p
><p>To use, open on web or desktop</p
></div>
`;

export const flagsStateToStringList = (flags: FlagsState, id: number): string[] =>
Object.keys(flags).filter(key => flags[key][id]);

Expand All @@ -99,9 +92,11 @@ export default (backgroundData: BackgroundData, message: Message | Outbox, isBri
</div>
</div>
`;

const { ownUser } = backgroundData;
const bodyHtml =
message.submessages && message.submessages.length > 0
? widgetBody(message)
? widgetAsHtml(ownUser.user_id, message.submessages, message.content)
: messageBody(backgroundData, message);

if (isBrief) {
Expand Down
105 changes: 105 additions & 0 deletions src/webview/html/widgetAsHtml.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
/* @flow strict-local */
import type { Submessage } from '../../types';
import template from './template';

/**
* Option submessages are of type -
* {
* type: 'new_option',
* idx: number,
* option: string (Question String),
* }
*/
const extractPollOptions = submessages => {
const pollOptions = submessages
.filter(submessage => JSON.parse(submessage.content).type === 'new_option')
.reduce((options, option) => {
const rawOption = JSON.parse(option.content);
const optionId = rawOption.idx;
options.set(optionId.toString(), {
id: optionId,
option: rawOption.option,
voters: [],
});
return options;
}, new Map());

/**
* Vote submessages are of type -
* {
* type: 'vote',
* key: string ((Voter's user ID) + , + (IDX of option)),
* vote: number,
* }
*/
const pollVotes = submessages
.filter(submessage => JSON.parse(submessage.content).type === 'vote')
.map(vote => JSON.parse(vote.content));

pollVotes.forEach(vote => {
const voterUserId = vote.key.split(',')[0];
const votedOption = vote.key.split(',')[1];
const optionFromVote = pollOptions.get(votedOption);
if (!optionFromVote) {
return;
}

if (vote.vote === -1) {
optionFromVote.voters = optionFromVote.voters.filter(voter => voter !== voterUserId);
return;
}
optionFromVote.voters.push(voterUserId);
});
return pollOptions;
};

const pollWidgetAsHtml = (ownUserId, submessages): string => {
const htmlPieces: string[] = [];
let pollOptions = new Map();

const widgetMetadata = JSON.parse(submessages[0].content);

htmlPieces.push('<div class="widget poll">');
htmlPieces.push(template`<div class="poll-question">${widgetMetadata.extra_data.question}</div>`);

pollOptions = extractPollOptions(submessages);

if (pollOptions.size === 0) {
htmlPieces.push('<i>No options have yet been added to this widget.</i>');
}

pollOptions.forEach(option => {
htmlPieces.push(template`
<div class="poll-option-container" data-option-id="${option.id}">
<div class="poll-button${
option.voters.find(voter => voter === ownUserId.toString()) ? ' self-voted' : ''
}">
${option.voters.length}</div>
<div class="poll-option">${option.option}</div>
</div>
`);
});

htmlPieces.push('</div>');
return htmlPieces.join('');
};

export default (
ownUserId: number,
submessages: $ReadOnlyArray<Submessage>,
contentHtml: string,
) => {
const widgetMetadata = JSON.parse(submessages[0].content);

if (widgetMetadata.widget_type === 'poll') {
return pollWidgetAsHtml(ownUserId, submessages);
}

return template`
$!${contentHtml}
<div class="widget dummy-widget">
<p>Interactive message</p>
<p>To use, open on web or desktop</p>
</div>
`;
};
16 changes: 16 additions & 0 deletions src/webview/js/generatedEs3.js
Original file line number Diff line number Diff line change
Expand Up @@ -541,6 +541,22 @@ documentBody.addEventListener('click', function (e) {
return;
}

if (target.matches('.poll-button')) {
var optionElement = target.closest('.poll-option-container');

if (!optionElement) {
throw new Error('Corresponding option element not found.');
}

sendMessage({
type: 'vote',
messageId: getMessageIdFromNode(target),
optionId: parseInt(requireAttribute(optionElement, 'data-option-id'), 10),
vote: target.classList.contains('self-voted') ? -1 : 1
});
return;
}

var messageElement = target.closest('.message-brief');

if (messageElement) {
Expand Down
14 changes: 14 additions & 0 deletions src/webview/js/js.js
Original file line number Diff line number Diff line change
Expand Up @@ -699,6 +699,20 @@ documentBody.addEventListener('click', (e: MouseEvent) => {
return;
}

if (target.matches('.poll-button')) {
const optionElement = target.closest('.poll-option-container');
if (!optionElement) {
throw new Error('Corresponding option element not found.');
}
sendMessage({
type: 'vote',
messageId: getMessageIdFromNode(target),
optionId: parseInt(requireAttribute(optionElement, 'data-option-id'), 10),
vote: target.classList.contains('self-voted') ? -1 : 1,
});
return;
}

const messageElement = target.closest('.message-brief');
if (messageElement) {
messageElement.getElementsByClassName('timestamp')[0].classList.toggle('show');
Expand Down
Loading