Bugfixes for v1.29

* If focus was set to document.body during archive, focus left pane
* Shortcut Guide: Add space between text and shortcut highlight
* Ensure that draft attachment can be closed with click on X button
* Move to keyDown event for user idle checking
* Additional resiliency around avatars; check for them on on-disk
* Increase timeouts to preserve websocket connection
* On startup, be resilient to malformed JSON in log files
* Don't crash if shell.openExternal returns an error
* Whenever we request a contact/group sync, also request block list
* Avatar popup: Ensure styling is mouse- and keyboard-appropriate
* MainHeader: Create popperRoot on demand, not on mount
* CompositionInput: Disable default Ctrl-/ shortcut
* Update libphonenumber
This commit is contained in:
Scott Nonnenberg 2019-12-03 12:02:50 -08:00 committed by Ken Powers
parent ee9e86ab7a
commit e9f08c3da9
19 changed files with 300 additions and 128 deletions

View File

@ -105,6 +105,30 @@ exports.createReader = root => {
};
};
exports.createDoesExist = root => {
if (!isString(root)) {
throw new TypeError("'root' must be a path");
}
return async relativePath => {
if (!isString(relativePath)) {
throw new TypeError("'relativePath' must be a string");
}
const absolutePath = path.join(root, relativePath);
const normalized = path.normalize(absolutePath);
if (!normalized.startsWith(root)) {
throw new Error('Invalid relative path');
}
try {
await fse.access(normalized, fse.constants.F_OK);
return true;
} catch (error) {
return false;
}
};
};
exports.copyIntoAttachmentsDirectory = root => {
if (!isString(root)) {
throw new TypeError("'root' must be a path");

View File

@ -31,7 +31,7 @@ module.exports = {
fetch,
};
function initialize() {
async function initialize() {
if (logger) {
throw new Error('Already called initialize!');
}
@ -40,66 +40,81 @@ function initialize() {
const logPath = path.join(basePath, 'logs');
mkdirp.sync(logPath);
return cleanupLogs(logPath).then(() => {
const logFile = path.join(logPath, 'log.log');
const loggerOptions = {
name: 'log',
streams: [
try {
await cleanupLogs(logPath);
} catch (error) {
const errorString = `Failed to clean logs; deleting all. Error: ${
error.stack
}`;
console.error(errorString);
await deleteAllLogs(logPath);
mkdirp.sync(logPath);
// If we want this log entry to persist on disk, we need to wait until we've
// set up our logging infrastructure.
setTimeout(() => {
console.error(errorString);
}, 500);
}
const logFile = path.join(logPath, 'log.log');
const loggerOptions = {
name: 'log',
streams: [
{
type: 'rotating-file',
path: logFile,
period: '1d',
count: 3,
},
],
};
if (isRunningFromConsole) {
loggerOptions.streams.push({
level: 'debug',
stream: process.stdout,
});
}
logger = bunyan.createLogger(loggerOptions);
LEVELS.forEach(level => {
ipc.on(`log-${level}`, (first, ...rest) => {
logger[level](...rest);
});
});
ipc.on('batch-log', (first, batch) => {
batch.forEach(item => {
logger[item.level](
{
type: 'rotating-file',
path: logFile,
period: '1d',
count: 3,
time: new Date(item.timestamp),
},
],
};
if (isRunningFromConsole) {
loggerOptions.streams.push({
level: 'debug',
stream: process.stdout,
});
}
logger = bunyan.createLogger(loggerOptions);
LEVELS.forEach(level => {
ipc.on(`log-${level}`, (first, ...rest) => {
logger[level](...rest);
});
});
ipc.on('batch-log', (first, batch) => {
batch.forEach(item => {
logger[item.level](
{
time: new Date(item.timestamp),
},
item.logText
);
});
});
ipc.on('fetch-log', event => {
fetch(logPath).then(
data => {
event.sender.send('fetched-log', data);
},
error => {
logger.error(`Problem loading log from disk: ${error.stack}`);
}
item.logText
);
});
});
ipc.on('delete-all-logs', async event => {
try {
await deleteAllLogs(logPath);
} catch (error) {
logger.error(`Problem deleting all logs: ${error.stack}`);
ipc.on('fetch-log', event => {
fetch(logPath).then(
data => {
event.sender.send('fetched-log', data);
},
error => {
logger.error(`Problem loading log from disk: ${error.stack}`);
}
);
});
event.sender.send('delete-all-logs-complete');
});
ipc.on('delete-all-logs', async event => {
try {
await deleteAllLogs(logPath);
} catch (error) {
logger.error(`Problem deleting all logs: ${error.stack}`);
}
event.sender.send('delete-all-logs-complete');
});
}

View File

@ -76,7 +76,7 @@
const ACTIVE_TIMEOUT = 15 * 1000;
const ACTIVE_EVENTS = [
'click',
'keypress',
'keydown',
'mousedown',
'mousemove',
// 'scroll', // this is triggered by Timeline re-renders, can't use
@ -193,6 +193,7 @@
upgradeMessageSchema,
writeNewAttachmentData,
deleteAttachmentData,
doesAttachmentExist,
} = window.Signal.Migrations;
const { Views } = window.Signal;
@ -1049,6 +1050,23 @@
document.body
);
// It's very likely that the act of archiving a conversation will set focus to
// 'none,' or the top-level body element. This resets it to the left pane,
// whether in the normal conversation list or search results.
if (document.activeElement === document.body) {
const leftPaneEl = document.querySelector('.module-left-pane__list');
if (leftPaneEl) {
leftPaneEl.focus();
}
const searchResultsEl = document.querySelector(
'.module-search-results'
);
if (searchResultsEl) {
searchResultsEl.focus();
}
}
event.preventDefault();
event.stopPropagation();
return;
@ -1852,12 +1870,14 @@
{
writeNewAttachmentData,
deleteAttachmentData,
doesAttachmentExist,
}
);
conversation.set(newAttributes);
}
window.Signal.Data.updateConversation(id, conversation.attributes);
const { expireTimer } = details;
const isValidExpireTimer = typeof expireTimer === 'number';
if (isValidExpireTimer) {
@ -1934,6 +1954,7 @@
{
writeNewAttachmentData,
deleteAttachmentData,
doesAttachmentExist,
}
);
conversation.set(newAttributes);

View File

@ -30,6 +30,7 @@
const { Conversation, Contact, Message, PhoneNumber } = window.Signal.Types;
const {
deleteAttachmentData,
doesAttachmentExist,
getAbsoluteAttachmentPath,
loadAttachmentData,
readStickerData,
@ -1746,7 +1747,6 @@
error && error.stack ? error.stack : error
);
await c.dropProfileKey();
return;
}
try {
@ -1814,6 +1814,7 @@
{
writeNewAttachmentData,
deleteAttachmentData,
doesAttachmentExist,
}
);
this.set(newAttributes);

View File

@ -114,6 +114,7 @@ function initializeMigrations({
createReader,
createWriterForExisting,
createWriterForNew,
createDoesExist,
getDraftPath,
getPath,
getStickersPath,
@ -139,6 +140,7 @@ function initializeMigrations({
const copyIntoAttachmentsDirectory = Attachments.copyIntoAttachmentsDirectory(
attachmentsPath
);
const doesAttachmentExist = createDoesExist(attachmentsPath);
const stickersPath = getStickersPath(userDataPath);
const writeNewStickerData = createWriterForNew(stickersPath);
@ -173,6 +175,7 @@ function initializeMigrations({
}),
deleteSticker,
deleteTempFile,
doesAttachmentExist,
getAbsoluteAttachmentPath,
getAbsoluteDraftPath,
getAbsoluteStickerPath,

View File

@ -1,4 +1,4 @@
/* global crypto */
/* global crypto, window */
const { isFunction, isNumber } = require('lodash');
const { createLastMessageUpdate } = require('../../../ts/types/Conversation');
@ -16,17 +16,26 @@ function buildAvatarUpdater({ field }) {
}
const avatar = conversation[field];
const { writeNewAttachmentData, deleteAttachmentData } = options;
if (!isFunction(writeNewAttachmentData)) {
throw new Error(
'Conversation.buildAvatarUpdater: writeNewAttachmentData must be a function'
);
}
const {
deleteAttachmentData,
doesAttachmentExist,
writeNewAttachmentData,
} = options;
if (!isFunction(deleteAttachmentData)) {
throw new Error(
'Conversation.buildAvatarUpdater: deleteAttachmentData must be a function'
);
}
if (!isFunction(doesAttachmentExist)) {
throw new Error(
'Conversation.buildAvatarUpdater: deleteAttachmentData must be a function'
);
}
if (!isFunction(writeNewAttachmentData)) {
throw new Error(
'Conversation.buildAvatarUpdater: writeNewAttachmentData must be a function'
);
}
const newHash = await computeHash(data);
@ -41,8 +50,14 @@ function buildAvatarUpdater({ field }) {
}
const { hash, path } = avatar;
const exists = await doesAttachmentExist(path);
if (!exists) {
window.log.warn(
`Conversation.buildAvatarUpdater: attachment ${path} did not exist`
);
}
if (hash === newHash) {
if (exists && hash === newHash) {
return conversation;
}

View File

@ -556,6 +556,30 @@ MessageSender.prototype = {
return this.server.getStickerPackManifest(packId);
},
sendRequestBlockSyncMessage(options) {
const myNumber = textsecure.storage.user.getNumber();
const myDevice = textsecure.storage.user.getDeviceId();
if (myDevice !== 1 && myDevice !== '1') {
const request = new textsecure.protobuf.SyncMessage.Request();
request.type = textsecure.protobuf.SyncMessage.Request.Type.BLOCKED;
const syncMessage = this.createSyncMessage();
syncMessage.request = request;
const contentMessage = new textsecure.protobuf.Content();
contentMessage.syncMessage = syncMessage;
const silent = true;
return this.sendIndividualProto(
myNumber,
contentMessage,
Date.now(),
silent,
options
);
}
return Promise.resolve();
},
sendRequestConfigurationSyncMessage(options) {
const myNumber = textsecure.storage.user.getNumber();
const myDevice = textsecure.storage.user.getDeviceId();
@ -1236,6 +1260,10 @@ textsecure.MessageSender = function MessageSenderWrapper(username, password) {
this.sendRequestConfigurationSyncMessage = sender.sendRequestConfigurationSyncMessage.bind(
sender
);
this.sendRequestBlockSyncMessage = sender.sendRequestBlockSyncMessage.bind(
sender
);
this.sendMessageToNumber = sender.sendMessageToNumber.bind(sender);
this.sendMessage = sender.sendMessage.bind(sender);
this.resetSession = sender.resetSession.bind(sender);

View File

@ -32,6 +32,9 @@
window.log.info('SyncRequest created. Sending config sync request...');
wrap(sender.sendRequestConfigurationSyncMessage(sendOptions));
window.log.info('SyncRequest now sending block sync request...');
wrap(sender.sendRequestBlockSyncMessage(sendOptions));
window.log.info('SyncRequest now sending contact sync message...');
wrap(sender.sendRequestContactSyncMessage(sendOptions))
.then(() => {

View File

@ -191,7 +191,7 @@
ev.code = code;
ev.reason = reason;
this.dispatchEvent(ev);
}, 1000);
}, 5000);
};
};
window.WebSocketResource.prototype = new textsecure.EventTarget();
@ -227,7 +227,7 @@
this.disconnectTimer = setTimeout(() => {
clearTimeout(this.keepAliveTimer);
this.wsr.close(3001, 'No response to keepalive request');
}, 1000);
}, 10000);
} else {
this.reset();
}

View File

@ -166,11 +166,15 @@ function prepareURL(pathSegments, moreKeys) {
});
}
function handleUrl(event, target) {
async function handleUrl(event, target) {
event.preventDefault();
const { protocol } = url.parse(target);
if (protocol === 'http:' || protocol === 'https:') {
shell.openExternal(target);
try {
await shell.openExternal(target);
} catch (error) {
console.log(`Failed to open url: ${error.stack}`);
}
}
}

View File

@ -70,7 +70,7 @@
"fs-extra": "5.0.0",
"fuse.js": "3.4.4",
"glob": "7.1.2",
"google-libphonenumber": "3.2.2",
"google-libphonenumber": "3.2.6",
"got": "8.2.0",
"he": "1.2.0",
"intl-tel-input": "12.1.15",

View File

@ -6831,13 +6831,27 @@ button.module-image__border-overlay:focus {
padding: 6px;
@include light-theme {
&:hover,
&:hover {
background-color: $color-gray-05;
}
}
@include keyboard-mode {
&:hover {
background-color: inherit;
}
&:focus {
background-color: $color-gray-05;
}
}
@include dark-theme {
&:hover,
&:hover {
background-color: $color-gray-60;
}
}
@include dark-keyboard-mode {
&:hover {
background-color: inherit;
}
&:focus {
background-color: $color-gray-60;
}
@ -7009,6 +7023,7 @@ button.module-image__border-overlay:focus {
align-items: center;
break-inside: avoid;
padding-left: 4px;
min-height: 40px;
outline: none;

View File

@ -24,6 +24,7 @@ export interface Props {
interface State {
imageBroken: boolean;
lastAvatarPath?: string;
}
export class Avatar extends React.Component<Props, State> {
@ -35,10 +36,23 @@ export class Avatar extends React.Component<Props, State> {
this.handleImageErrorBound = this.handleImageError.bind(this);
this.state = {
lastAvatarPath: props.avatarPath,
imageBroken: false,
};
}
public static getDerivedStateFromProps(props: Props, state: State): State {
if (props.avatarPath !== state.lastAvatarPath) {
return {
...state,
lastAvatarPath: props.avatarPath,
imageBroken: false,
};
}
return state;
}
public handleImageError() {
// tslint:disable-next-line no-console
console.log('Avatar: Image failed to load; failing over to placeholder');

View File

@ -700,6 +700,13 @@ export const CompositionInput = ({
return null;
}
// Get rid of Ctrl-/, which on GNOME is bound to 'select all'
if (e.key === '/' && !e.shiftKey && e.ctrlKey) {
e.preventDefault();
return null;
}
return getDefaultKeyBinding(e);
},
[emojiResults, large]

View File

@ -70,15 +70,6 @@ export class MainHeader extends React.Component<PropsType, StateType> {
};
}
public componentDidMount() {
const popperRoot = document.createElement('div');
document.body.appendChild(popperRoot);
this.setState({
popperRoot,
});
}
public componentDidUpdate(prevProps: PropsType) {
const { searchConversationId, startSearchCounter } = this.props;
@ -114,28 +105,41 @@ export class MainHeader extends React.Component<PropsType, StateType> {
};
public showAvatarPopup = () => {
const popperRoot = document.createElement('div');
document.body.appendChild(popperRoot);
this.setState({
showingAvatarPopup: true,
popperRoot,
});
document.addEventListener('click', this.handleOutsideClick);
document.addEventListener('keydown', this.handleOutsideKeyDown);
};
public hideAvatarPopup = () => {
const { popperRoot } = this.state;
document.removeEventListener('click', this.handleOutsideClick);
document.removeEventListener('keydown', this.handleOutsideKeyDown);
this.setState({
showingAvatarPopup: false,
popperRoot: null,
});
if (popperRoot) {
document.body.removeChild(popperRoot);
}
};
public componentWillUnmount() {
const { popperRoot } = this.state;
document.removeEventListener('click', this.handleOutsideClick);
document.removeEventListener('keydown', this.handleOutsideKeyDown);
if (popperRoot) {
document.body.removeChild(popperRoot);
document.removeEventListener('click', this.handleOutsideClick);
document.removeEventListener('keydown', this.handleOutsideKeyDown);
}
}

View File

@ -162,20 +162,6 @@ export class Image extends React.Component<Props> {
alt={i18n('imageCaptionIconAlt')}
/>
) : null}
{closeButton ? (
<button
onClick={(e: React.MouseEvent<{}>) => {
e.preventDefault();
e.stopPropagation();
if (onClickClose) {
onClickClose(attachment);
}
}}
className="module-image__close-button"
title={i18n('remove-attachment')}
/>
) : null}
{bottomOverlay ? (
<div
className={classNames(
@ -199,6 +185,20 @@ export class Image extends React.Component<Props> {
</div>
) : null}
{overlay}
{closeButton ? (
<button
onClick={(e: React.MouseEvent<{}>) => {
e.preventDefault();
e.stopPropagation();
if (onClickClose) {
onClickClose(attachment);
}
}}
className="module-image__close-button"
title={i18n('remove-attachment')}
/>
) : null}
</div>
);
}

View File

@ -33,6 +33,7 @@ export const actions = {
};
function userChanged(attributes: {
interactionMode?: 'mouse' | 'keyboard';
ourNumber: string;
regionCode: string;
}): UserChangedActionType {

View File

@ -1292,18 +1292,26 @@
{
"rule": "jQuery-wrap(",
"path": "libtextsecure/sync_request.js",
"line": " wrap(sender.sendRequestContactSyncMessage(sendOptions))",
"line": " wrap(sender.sendRequestBlockSyncMessage(sendOptions));",
"lineNumber": 36,
"reasonCategory": "falseMatch",
"updated": "2018-10-05T23:12:28.961Z"
"updated": "2019-12-03T00:28:08.683Z"
},
{
"rule": "jQuery-wrap(",
"path": "libtextsecure/sync_request.js",
"line": " wrap(sender.sendRequestContactSyncMessage(sendOptions))",
"lineNumber": 39,
"reasonCategory": "falseMatch",
"updated": "2019-12-03T00:28:08.683Z"
},
{
"rule": "jQuery-wrap(",
"path": "libtextsecure/sync_request.js",
"line": " return wrap(sender.sendRequestGroupSyncMessage(sendOptions));",
"lineNumber": 39,
"lineNumber": 42,
"reasonCategory": "falseMatch",
"updated": "2018-10-05T23:12:28.961Z"
"updated": "2019-12-03T00:28:08.683Z"
},
{
"rule": "jQuery-wrap(",
@ -3673,7 +3681,7 @@
"rule": "eval",
"path": "node_modules/google-libphonenumber/dist/libphonenumber.js",
"line": " var a = !eval('\"use strict\";let x = 1; function f() { return typeof x; };f() == \"number\";');",
"lineNumber": 232,
"lineNumber": 233,
"reasonCategory": "usageTrusted",
"updated": "2018-11-27T01:31:13.384Z",
"reasonDetail": "Hard-coded string used for testing capabilities."
@ -3682,25 +3690,16 @@
"rule": "eval",
"path": "node_modules/google-libphonenumber/dist/libphonenumber.js",
"line": " eval(a);",
"lineNumber": 267,
"lineNumber": 268,
"reasonCategory": "notExercisedByOurApp",
"updated": "2018-11-27T01:31:13.384Z",
"reasonDetail": "Used to load dependencies; parent function loadModuleFromSource_ is used in one place only."
},
{
"rule": "eval",
"path": "node_modules/google-libphonenumber/dist/libphonenumber.js",
"line": " eval(g + \"\\n//# sourceURL=\" + f);",
"lineNumber": 298,
"reasonCategory": "notExercisedByOurApp",
"updated": "2019-04-12T00:50:12.124Z",
"reasonDetail": "Used for google closure compiler transpilation scenarios"
},
{
"rule": "eval",
"path": "node_modules/google-libphonenumber/dist/libphonenumber.js",
"line": " goog.global.eval(\"var _evalTest_ = 1;\");",
"lineNumber": 444,
"lineNumber": 445,
"reasonCategory": "usageTrusted",
"updated": "2019-04-12T00:50:12.124Z",
"reasonDetail": "Hard-coded string used for testing capabilities"
@ -3709,7 +3708,7 @@
"rule": "eval",
"path": "node_modules/google-libphonenumber/dist/libphonenumber.js",
"line": " goog.global.eval(a);",
"lineNumber": 458,
"lineNumber": 459,
"reasonCategory": "notExercisedByOurApp",
"updated": "2018-11-27T01:31:13.384Z",
"reasonDetail": "More transpilation logic"
@ -3717,16 +3716,16 @@
{
"rule": "jQuery-$(",
"path": "node_modules/google-libphonenumber/dist/libphonenumber.js",
"line": " b && (a = a.replace(/\\{\\$([^}]+)}/g, function(a, d) {",
"lineNumber": 498,
"line": " b && (a = a.replace(/\\{\\$([^}]+)}/g, function(a, c) {",
"lineNumber": 500,
"reasonCategory": "falseMatch",
"updated": "2018-11-27T01:31:13.384Z"
"updated": "2019-12-03T19:24:21.611Z"
},
{
"rule": "eval",
"path": "node_modules/google-libphonenumber/dist/libphonenumber.js",
"line": " return !!eval(a);",
"lineNumber": 640,
"lineNumber": 642,
"reasonCategory": "notExercisedByOurApp",
"updated": "2018-11-27T01:31:13.384Z",
"reasonDetail": "More transpilation logic"
@ -3735,7 +3734,7 @@
"rule": "jQuery-load(",
"path": "node_modules/google-libphonenumber/dist/libphonenumber.js",
"line": " d.load(f);",
"lineNumber": 796,
"lineNumber": 794,
"reasonCategory": "notExercisedByOurApp",
"updated": "2019-04-12T00:50:12.124Z",
"reasonDetail": "Part of their google closure 'debug loader'"
@ -3752,11 +3751,29 @@
{
"rule": "DOM-innerHTML",
"path": "node_modules/google-libphonenumber/dist/libphonenumber.js",
"line": " e || (d.innerHTML = a + \" \", e = d.firstChild.nodeValue.slice(0, -1));",
"lineNumber": 2045,
"reasonCategory": "notExercisedByOurApp",
"updated": "2018-11-27T01:31:13.384Z",
"reasonDetail": "An odd technique of unescaping content by putting it into the dom"
"line": " a.innerHTML = goog.html.SafeHtml.unwrapTrustedHTML(goog.html.SafeHtml.EMPTY);",
"lineNumber": 3448,
"reasonCategory": "usageTrusted",
"updated": "2019-12-03T19:24:21.611Z",
"reasonDetail": "HTML is escaped"
},
{
"rule": "DOM-innerHTML",
"path": "node_modules/google-libphonenumber/dist/libphonenumber.js",
"line": " a.innerHTML = goog.html.SafeHtml.unwrapTrustedHTML(b);",
"lineNumber": 3457,
"reasonCategory": "usageTrusted",
"updated": "2019-12-03T19:24:21.611Z",
"reasonDetail": "HTML is escaped"
},
{
"rule": "DOM-outerHTML",
"path": "node_modules/google-libphonenumber/dist/libphonenumber.js",
"line": " a.outerHTML = goog.html.SafeHtml.unwrapTrustedHTML(b);",
"lineNumber": 3469,
"reasonCategory": "usageTrusted",
"updated": "2019-12-03T19:24:21.611Z",
"reasonDetail": "HTML is escaped"
},
{
"rule": "DOM-innerHTML",
@ -7560,7 +7577,7 @@
"rule": "React-createRef",
"path": "ts/components/MainHeader.js",
"line": " this.inputRef = react_1.default.createRef();",
"lineNumber": 134,
"lineNumber": 142,
"reasonCategory": "usageTrusted",
"updated": "2019-08-09T21:17:57.798Z",
"reasonDetail": "Used only to set focus"

View File

@ -4242,10 +4242,10 @@ glogg@^1.0.1:
dependencies:
sparkles "^1.0.0"
google-libphonenumber@3.2.2:
version "3.2.2"
resolved "https://registry.yarnpkg.com/google-libphonenumber/-/google-libphonenumber-3.2.2.tgz#3d9d7ba727e99a50812f21b0ed313723b76c5c54"
integrity sha512-ubjGeosYPeusjYbUHy76lCniGTTI0k1rIFc+uKBX+jHQLDmWOSUtlFUxaeoLJ+Y+PAMM6dWp+C1HjHx5BI8kEw==
google-libphonenumber@3.2.6:
version "3.2.6"
resolved "https://registry.yarnpkg.com/google-libphonenumber/-/google-libphonenumber-3.2.6.tgz#3d725b48ff44706b80246e77f95f2c2fdc6fd729"
integrity sha512-6QCQAaKJlSd/1dUqvdQf7zzfb3uiZHsG8yhCfOdCVRfMuPZ/VDIEB47y5SYwjPQJPs7ebfW5jj6PeobB9JJ4JA==
got@8.2.0:
version "8.2.0"