diff --git a/app/attachments.js b/app/attachments.js index e9f044d36..393c2c794 100644 --- a/app/attachments.js +++ b/app/attachments.js @@ -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"); diff --git a/app/logging.js b/app/logging.js index ceced27b2..bf13209a3 100644 --- a/app/logging.js +++ b/app/logging.js @@ -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'); }); } diff --git a/js/background.js b/js/background.js index 1cf0c7c6e..c576466ea 100644 --- a/js/background.js +++ b/js/background.js @@ -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); diff --git a/js/models/conversations.js b/js/models/conversations.js index e19fb58b7..a2cd6b043 100644 --- a/js/models/conversations.js +++ b/js/models/conversations.js @@ -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); diff --git a/js/modules/signal.js b/js/modules/signal.js index c6fedcae0..672d59d69 100644 --- a/js/modules/signal.js +++ b/js/modules/signal.js @@ -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, diff --git a/js/modules/types/conversation.js b/js/modules/types/conversation.js index 8607d99dc..23a78abe0 100644 --- a/js/modules/types/conversation.js +++ b/js/modules/types/conversation.js @@ -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; } diff --git a/libtextsecure/sendmessage.js b/libtextsecure/sendmessage.js index 7b2818683..d5d3cb066 100644 --- a/libtextsecure/sendmessage.js +++ b/libtextsecure/sendmessage.js @@ -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); diff --git a/libtextsecure/sync_request.js b/libtextsecure/sync_request.js index cf6e51f48..ecdf1f96d 100644 --- a/libtextsecure/sync_request.js +++ b/libtextsecure/sync_request.js @@ -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(() => { diff --git a/libtextsecure/websocket-resources.js b/libtextsecure/websocket-resources.js index 5d807c416..933b6bee8 100644 --- a/libtextsecure/websocket-resources.js +++ b/libtextsecure/websocket-resources.js @@ -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(); } diff --git a/main.js b/main.js index 8aae8a21f..3ac4bb779 100644 --- a/main.js +++ b/main.js @@ -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}`); + } } } diff --git a/package.json b/package.json index 5c5879f7a..859e7f73e 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/stylesheets/_modules.scss b/stylesheets/_modules.scss index 4330c504a..0a0b38bbd 100644 --- a/stylesheets/_modules.scss +++ b/stylesheets/_modules.scss @@ -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; diff --git a/ts/components/Avatar.tsx b/ts/components/Avatar.tsx index b231bf52d..260e2b24f 100644 --- a/ts/components/Avatar.tsx +++ b/ts/components/Avatar.tsx @@ -24,6 +24,7 @@ export interface Props { interface State { imageBroken: boolean; + lastAvatarPath?: string; } export class Avatar extends React.Component { @@ -35,10 +36,23 @@ export class Avatar extends React.Component { 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'); diff --git a/ts/components/CompositionInput.tsx b/ts/components/CompositionInput.tsx index 0e8ef9e4e..9eb402c41 100644 --- a/ts/components/CompositionInput.tsx +++ b/ts/components/CompositionInput.tsx @@ -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] diff --git a/ts/components/MainHeader.tsx b/ts/components/MainHeader.tsx index 452729928..66d4ba59f 100644 --- a/ts/components/MainHeader.tsx +++ b/ts/components/MainHeader.tsx @@ -70,15 +70,6 @@ export class MainHeader extends React.Component { }; } - 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 { }; 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); } } diff --git a/ts/components/conversation/Image.tsx b/ts/components/conversation/Image.tsx index ad52ec8b2..86cd36ace 100644 --- a/ts/components/conversation/Image.tsx +++ b/ts/components/conversation/Image.tsx @@ -162,20 +162,6 @@ export class Image extends React.Component { alt={i18n('imageCaptionIconAlt')} /> ) : null} - {closeButton ? ( -