Co-authored-by: scott@signal.org
Co-authored-by: ken@signal.org
This commit is contained in:
Ken Powers 2019-05-16 15:32:11 -07:00 committed by Scott Nonnenberg
parent 8c8856785b
commit 29de50c12a
100 changed files with 7572 additions and 693 deletions

View file

@ -104,6 +104,9 @@
);
this.listenTo(this.model.messageCollection, 'force-send', this.forceSend);
this.listenTo(this.model.messageCollection, 'delete', this.deleteMessage);
this.listenTo(this.model.messageCollection, 'height-changed', () =>
this.view.scrollToBottomIfNeeded()
);
this.listenTo(
this.model.messageCollection,
'scroll-to-message',
@ -276,15 +279,18 @@
this.$('.send-message').blur(this.unfocusBottomBar.bind(this));
this.$emojiPanelContainer = this.$('.emoji-panel-container');
this.setupStickerPickerButton();
},
events: {
keydown: 'onKeyDown',
'submit .send': 'checkUnverifiedSendMessage',
'submit .send': 'clickSend',
'input .send-message': 'updateMessageFieldSize',
'keydown .send-message': 'updateMessageFieldSize',
'keyup .send-message': 'onKeyUp',
click: 'onClick',
'click .sticker-button-placeholder': 'onClickStickerButtonPlaceholder',
'click .bottom-bar': 'focusMessageField',
'click .capture-audio .microphone': 'captureAudio',
'click .module-scroll-down': 'scrollToBottom',
@ -308,6 +314,28 @@
paste: 'onPaste',
},
setupStickerPickerButton() {
const props = {
onClickAddPack: () => this.showStickerManager(),
onPickSticker: (packId, stickerId) =>
this.sendStickerMessage({ packId, stickerId }),
};
this.stickerButtonView = new Whisper.ReactWrapperView({
className: 'sticker-button-wrapper',
JSX: Signal.State.Roots.createStickerButton(window.reduxStore, props),
});
// Finally, add it to the DOM
this.$('.sticker-button-placeholder').append(this.stickerButtonView.el);
},
// We need this, or clicking the sticker button will submit the form and send any
// mid-composition message content.
onClickStickerButtonPlaceholder(e) {
e.preventDefault();
},
onChooseAttachment(e) {
if (e) {
e.stopPropagation();
@ -366,6 +394,13 @@
this.fileInput.remove();
this.titleView.remove();
if (this.stickerButtonView) {
this.stickerButtonView.remove();
}
if (this.stickerPreviewModalView) {
this.stickerPreviewModalView.remove();
}
if (this.captureAudioView) {
this.captureAudioView.remove();
@ -1282,6 +1317,26 @@
dialog.focusCancel();
},
showStickerPackPreview(packId) {
const props = {
packId,
onClose: () => {
this.stickerPreviewModalView.remove();
},
};
this.stickerPreviewModalView = new Whisper.ReactWrapperView({
className: 'sticker-preview-modal-wrapper',
JSX: Signal.State.Roots.createStickerPreviewModal(
window.reduxStore,
props
),
onClose: () => {
this.stickerPreviewModalView = null;
},
});
},
showLightbox({ attachment, messageId }) {
const message = this.model.messageCollection.get(messageId);
if (!message) {
@ -1289,6 +1344,13 @@
`showLightbox: did not find message for id ${messageId}`
);
}
const sticker = message.get('sticker');
if (sticker) {
const { packId } = sticker;
this.showStickerPackPreview(packId);
return;
}
const { contentType, path } = attachment;
if (
@ -1400,6 +1462,21 @@
view.render();
},
showStickerManager() {
const view = new Whisper.ReactWrapperView({
className: ['sticker-manager-wrapper', 'panel'].join(' '),
JSX: Signal.State.Roots.createStickerManager(window.reduxStore),
onClose: () => {
this.resetPanel();
this.updateHeader();
},
});
this.listenBack(view);
this.updateHeader();
view.render();
},
showContactDetail({ contact, signalAccount }) {
const view = new Whisper.ReactWrapperView({
Component: Signal.Components.ContactDetail,
@ -1449,6 +1526,8 @@
if (this.panels.length === 0) {
this.$el.trigger('force-resize');
// Make sure poppers are positioned properly
window.dispatchEvent(new Event('resize'));
}
},
@ -1482,99 +1561,121 @@
}
},
showSendConfirmationDialog(e, contacts) {
let message;
const isUnverified = this.model.isUnverified();
showSendAnywayDialog(contacts) {
return new Promise(resolve => {
let message;
const isUnverified = this.model.isUnverified();
if (contacts.length > 1) {
if (isUnverified) {
message = i18n('changedSinceVerifiedMultiple');
if (contacts.length > 1) {
if (isUnverified) {
message = i18n('changedSinceVerifiedMultiple');
} else {
message = i18n('changedRecentlyMultiple');
}
} else {
message = i18n('changedRecentlyMultiple');
const contactName = contacts.at(0).getTitle();
if (isUnverified) {
message = i18n('changedSinceVerified', [contactName, contactName]);
} else {
message = i18n('changedRecently', [contactName, contactName]);
}
}
} else {
const contactName = contacts.at(0).getTitle();
if (isUnverified) {
message = i18n('changedSinceVerified', [contactName, contactName]);
} else {
message = i18n('changedRecently', [contactName, contactName]);
}
}
const dialog = new Whisper.ConfirmationDialogView({
message,
okText: i18n('sendAnyway'),
resolve: () => {
this.checkUnverifiedSendMessage(e, { force: true });
},
reject: () => {
this.focusMessageFieldAndClearDisabled();
},
const dialog = new Whisper.ConfirmationDialogView({
message,
okText: i18n('sendAnyway'),
resolve: () => resolve(true),
reject: () => resolve(false),
});
this.$el.prepend(dialog.el);
dialog.focusCancel();
});
this.$el.prepend(dialog.el);
dialog.focusCancel();
},
async checkUnverifiedSendMessage(e, options = {}) {
async clickSend(e, options) {
e.preventDefault();
this.sendStart = Date.now();
this.$messageField.attr('disabled', true);
_.defaults(options, { force: false });
// This will go to the trust store for the latest identity key information,
// and may result in the display of a new banner for this conversation.
try {
await this.model.updateVerified();
const contacts = this.model.getUnverified();
if (!contacts.length) {
this.checkUntrustedSendMessage(e, options);
const contacts = await this.getUntrustedContacts(options);
if (contacts && contacts.length) {
const sendAnyway = await this.showSendAnywayDialog(contacts);
if (sendAnyway) {
this.clickSend(e, { force: true });
return;
}
this.focusMessageFieldAndClearDisabled();
return;
}
if (options.force) {
await this.markAllAsVerifiedDefault(contacts);
this.checkUnverifiedSendMessage(e, options);
return;
}
this.showSendConfirmationDialog(e, contacts);
this.sendMessage(e);
} catch (error) {
this.focusMessageFieldAndClearDisabled();
window.log.error(
'checkUnverifiedSendMessage error:',
'clickSend error:',
error && error.stack ? error.stack : error
);
}
},
async checkUntrustedSendMessage(e, options = {}) {
_.defaults(options, { force: false });
async sendStickerMessage(options = {}) {
try {
const contacts = await this.model.getUntrusted();
if (!contacts.length) {
this.sendMessage(e);
const contacts = await this.getUntrustedContacts(options);
if (contacts && contacts.length) {
const sendAnyway = await this.showSendAnywayDialog(contacts);
if (sendAnyway) {
this.sendStickerMessage({ ...options, force: true });
}
return;
}
if (options.force) {
await this.markAllAsApproved(contacts);
this.sendMessage(e);
return;
}
this.showSendConfirmationDialog(e, contacts);
const { packId, stickerId } = options;
this.model.sendStickerMessage(packId, stickerId);
} catch (error) {
this.focusMessageFieldAndClearDisabled();
window.log.error(
'checkUntrustedSendMessage error:',
'clickSend error:',
error && error.stack ? error.stack : error
);
}
},
async getUntrustedContacts(options = {}) {
// This will go to the trust store for the latest identity key information,
// and may result in the display of a new banner for this conversation.
await this.model.updateVerified();
const unverifiedContacts = this.model.getUnverified();
if (options.force) {
if (unverifiedContacts.length) {
await this.markAllAsVerifiedDefault(unverifiedContacts);
// We only want force to break us through one layer of checks
// eslint-disable-next-line no-param-reassign
options.force = false;
}
} else if (unverifiedContacts.length) {
return unverifiedContacts;
}
const untrustedContacts = await this.model.getUntrusted();
if (options.force) {
if (untrustedContacts.length) {
await this.markAllAsApproved(untrustedContacts);
}
} else if (untrustedContacts.length) {
return untrustedContacts;
}
return null;
},
toggleEmojiPanel(e) {
e.preventDefault();
if (!this.emojiPanel) {
@ -1839,14 +1940,29 @@
async makeChunkedRequest(url) {
const PARALLELISM = 3;
const size = await textsecure.messaging.getProxiedSize(url);
const chunks = await Signal.LinkPreviews.getChunkPattern(size);
const first = await textsecure.messaging.makeProxiedRequest(url, {
start: 0,
end: Signal.Crypto.getRandomValue(1023, 2047),
returnArrayBuffer: true,
});
const { totalSize, result } = first;
const initialOffset = result.data.byteLength;
const firstChunk = {
start: 0,
end: initialOffset,
...result,
};
const chunks = await Signal.LinkPreviews.getChunkPattern(
totalSize,
initialOffset
);
let results = [];
const jobs = chunks.map(chunk => async () => {
const { start, end } = chunk;
const result = await textsecure.messaging.makeProxiedRequest(url, {
const jobResult = await textsecure.messaging.makeProxiedRequest(url, {
start,
end,
returnArrayBuffer: true,
@ -1854,7 +1970,7 @@
return {
...chunk,
...result,
...jobResult.result,
};
});
@ -1878,7 +1994,9 @@
}
const { contentType } = results[0];
const data = Signal.LinkPreviews.assembleChunks(results);
const data = Signal.LinkPreviews.assembleChunks(
[firstChunk].concat(results)
);
return {
contentType,
@ -1886,7 +2004,58 @@
};
},
async getStickerPackPreview(url) {
const isPackValid = pack =>
pack && (pack.status === 'advertised' || pack.status === 'installed');
try {
const { id, key } = window.Signal.Stickers.getDataFromLink(url);
const keyBytes = window.Signal.Crypto.bytesFromHexString(key);
const keyBase64 = window.Signal.Crypto.arrayBufferToBase64(keyBytes);
const existing = window.Signal.Stickers.getStickerPack(id);
if (!isPackValid(existing)) {
await window.Signal.Stickers.downloadStickerPack(id, keyBase64);
}
const pack = window.Signal.Stickers.getStickerPack(id);
if (!isPackValid(pack)) {
return null;
}
if (pack.key !== keyBase64) {
return null;
}
const { title, coverStickerId } = pack;
const sticker = pack.stickers[coverStickerId];
const data = await window.Signal.Migrations.readStickerData(
sticker.path
);
return {
title,
url,
image: {
...sticker,
data,
size: data.byteLength,
contentType: 'image/webp',
},
};
} catch (error) {
window.log.error(
'getStickerPackPreview error:',
error && error.stack ? error.stack : error
);
return null;
}
},
async getPreview(url) {
if (window.Signal.LinkPreviews.isStickerPack(url)) {
return this.getStickerPackPreview(url);
}
let html;
try {
html = await textsecure.messaging.makeProxiedRequest(url);

View file

@ -13,6 +13,12 @@
window.Whisper = window.Whisper || {};
Whisper.StickerPackInstallFailedToast = Whisper.ToastView.extend({
render_attributes() {
return { toastMessage: i18n('stickers--toast--InstallFailed') };
},
});
Whisper.ConversationStack = Whisper.View.extend({
className: 'conversation-stack',
open(conversation) {
@ -36,6 +42,8 @@
$el.prependTo(this.el);
}
conversation.trigger('opened');
// Make sure poppers are positioned properly
window.dispatchEvent(new Event('resize'));
},
});
@ -92,6 +100,12 @@
this.$el.addClass('expired');
}
Whisper.events.on('pack-install-failed', () => {
const toast = new Whisper.StickerPackInstallFailedToast();
toast.$el.appendTo(this.$el);
toast.render();
});
this.setupLeftPane();
},
render_attributes: {

View file

@ -16,6 +16,12 @@
this.listenTo(this.model, 'destroy', this.onDestroy);
this.listenTo(this.model, 'unload', this.onUnload);
this.listenTo(this.model, 'expired', this.onExpired);
this.updateHiddenSticker();
},
updateHiddenSticker() {
const sticker = this.model.get('sticker');
this.isHiddenSticker = sticker && (!sticker.data || !sticker.data.path);
},
onChange() {
this.addId();
@ -94,7 +100,17 @@
const update = () => {
const info = this.getRenderInfo();
this.childView.update(info.props);
this.childView.update(info.props, () => {
if (!this.isHiddenSticker) {
return;
}
this.updateHiddenSticker();
if (!this.isHiddenSticker) {
this.model.trigger('height-changed');
}
});
};
this.listenTo(this.model, 'change', update);

View file

@ -38,12 +38,23 @@
this.hasRendered = false;
},
update(props) {
update(props, cb) {
const updatedProps = this.augmentProps(props);
const reactElement = this.JSX
? this.JSX
: React.createElement(this.Component, updatedProps);
ReactDOM.render(reactElement, this.el, () => {
if (cb) {
try {
cb();
} catch (error) {
window.log.error(
'ReactWrapperView.update error:',
error && error.stack ? error.stack : error
);
}
}
if (this.hasRendered) {
return;
}