diff --git a/_locales/en/messages.json b/_locales/en/messages.json index b01a94281..fe8625657 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -1835,6 +1835,12 @@ "message": "Sticker Pack", "description": "The title that appears in the sticker pack preview modal." }, + "stickers--StickerPreview--Error": { + "message": + "Error opening sticker pack. Check your internet connection and try again.", + "description": + "The message that appears in the sticker preview modal when there is an error." + }, "EmojiPicker--empty": { "message": "No emoji found", "description": "Shown in the emoji picker when a search yields 0 results." diff --git a/app/attachment_channel.js b/app/attachment_channel.js index 672b531a3..e32279e8c 100644 --- a/app/attachment_channel.js +++ b/app/attachment_channel.js @@ -12,6 +12,7 @@ let initialized = false; const ERASE_ATTACHMENTS_KEY = 'erase-attachments'; const ERASE_STICKERS_KEY = 'erase-stickers'; +const ERASE_TEMP_KEY = 'erase-temp'; const CLEANUP_ORPHANED_ATTACHMENTS_KEY = 'cleanup-orphaned-attachments'; async function initialize({ configDir, cleanupOrphanedAttachments }) { @@ -22,6 +23,18 @@ async function initialize({ configDir, cleanupOrphanedAttachments }) { const attachmentsDir = Attachments.getPath(configDir); const stickersDir = Attachments.getStickersPath(configDir); + const tempDir = Attachments.getTempPath(configDir); + + ipcMain.on(ERASE_TEMP_KEY, event => { + try { + rimraf.sync(tempDir); + event.sender.send(`${ERASE_TEMP_KEY}-done`); + } catch (error) { + const errorForDisplay = error && error.stack ? error.stack : error; + console.log(`erase temp error: ${errorForDisplay}`); + event.sender.send(`${ERASE_TEMP_KEY}-done`, error); + } + }); ipcMain.on(ERASE_ATTACHMENTS_KEY, event => { try { diff --git a/app/attachments.d.ts b/app/attachments.d.ts new file mode 100644 index 000000000..a7a5eb689 --- /dev/null +++ b/app/attachments.d.ts @@ -0,0 +1 @@ +export function getTempPath(userDataPath: string): string; diff --git a/app/attachments.js b/app/attachments.js index f6ea69f65..c83c028a4 100644 --- a/app/attachments.js +++ b/app/attachments.js @@ -9,6 +9,7 @@ const { map, isArrayBuffer, isString } = require('lodash'); const PATH = 'attachments.noindex'; const STICKER_PATH = 'stickers.noindex'; +const TEMP_PATH = 'temp'; exports.getAllAttachments = async userDataPath => { const dir = exports.getPath(userDataPath); @@ -42,6 +43,20 @@ exports.getStickersPath = userDataPath => { return path.join(userDataPath, STICKER_PATH); }; +// getTempPath :: AbsolutePath -> AbsolutePath +exports.getTempPath = userDataPath => { + if (!isString(userDataPath)) { + throw new TypeError("'userDataPath' must be a string"); + } + return path.join(userDataPath, TEMP_PATH); +}; + +// clearTempPath :: AbsolutePath -> AbsolutePath +exports.clearTempPath = userDataPath => { + const tempPath = exports.getTempPath(userDataPath); + return fse.emptyDir(tempPath); +}; + // createReader :: AttachmentsPath -> // RelativePath -> // IO (Promise ArrayBuffer) diff --git a/app/sql.js b/app/sql.js index 3df186023..e8e3a88f2 100644 --- a/app/sql.js +++ b/app/sql.js @@ -1193,14 +1193,14 @@ async function updateConversation(data) { await db.run( `UPDATE conversations SET - json = $json, + json = $json, - active_at = $active_at, - type = $type, - members = $members, - name = $name, - profileName = $profileName - WHERE id = $id;`, + active_at = $active_at, + type = $type, + members = $members, + name = $name, + profileName = $profileName + WHERE id = $id;`, { $id: id, $json: objectToJSON(data), @@ -1879,8 +1879,46 @@ async function createOrUpdateStickerPack(pack) { ); } + const rows = await db.all('SELECT id FROM sticker_packs WHERE id = $id;', { + $id: id, + }); + const payload = { + $attemptedStatus: attemptedStatus, + $author: author, + $coverStickerId: coverStickerId, + $createdAt: createdAt || Date.now(), + $downloadAttempts: downloadAttempts || 1, + $id: id, + $installedAt: installedAt, + $key: key, + $lastUsed: lastUsed || null, + $status: status, + $stickerCount: stickerCount, + $title: title, + }; + + if (rows && rows.length) { + await db.run( + `UPDATE sticker_packs SET + attemptedStatus = $attemptedStatus, + author = $author, + coverStickerId = $coverStickerId, + createdAt = $createdAt, + downloadAttempts = $downloadAttempts, + installedAt = $installedAt, + key = $key, + lastUsed = $lastUsed, + status = $status, + stickerCount = $stickerCount, + title = $title + WHERE id = $id;`, + payload + ); + return; + } + await db.run( - `INSERT OR REPLACE INTO sticker_packs ( + `INSERT INTO sticker_packs ( attemptedStatus, author, coverStickerId, @@ -1907,20 +1945,7 @@ async function createOrUpdateStickerPack(pack) { $stickerCount, $title )`, - { - $attemptedStatus: attemptedStatus, - $author: author, - $coverStickerId: coverStickerId, - $createdAt: createdAt || Date.now(), - $downloadAttempts: downloadAttempts || 1, - $id: id, - $installedAt: installedAt, - $key: key, - $lastUsed: lastUsed || null, - $status: status, - $stickerCount: stickerCount, - $title: title, - } + payload ); } async function updateStickerPackStatus(id, status, options) { diff --git a/js/background.js b/js/background.js index 1011eda1f..c58eaed96 100644 --- a/js/background.js +++ b/js/background.js @@ -302,22 +302,25 @@ await window.Signal.Data.shutdown(); }, - installStickerPack: async (id, key) => { - const status = window.Signal.Stickers.getStickerPackStatus(id); + showStickerPack: async (packId, key) => { + // Kick off the download + window.Signal.Stickers.downloadEphemeralPack(packId, key); - if (status === 'installed') { - return; - } + const props = { + packId, + onClose: async () => { + stickerPreviewModalView.remove(); + await window.Signal.Stickers.removeEphemeralPack(packId); + }, + }; - if (status === 'advertised') { - await window.reduxActions.stickers.installStickerPack(id, key, { - fromSync: true, - }); - } else { - await window.Signal.Stickers.downloadStickerPack(id, key, { - finalStatus: 'installed', - }); - } + const stickerPreviewModalView = new Whisper.ReactWrapperView({ + className: 'sticker-preview-modal-wrapper', + JSX: Signal.State.Roots.createStickerPreviewModal( + window.reduxStore, + props + ), + }); }, }; @@ -464,6 +467,7 @@ user: { attachmentsPath: window.baseAttachmentsPath, stickersPath: window.baseStickersPath, + tempPath: window.baseTempPath, regionCode: window.storage.get('regionCode'), ourNumber: textsecure.storage.user.getNumber(), i18n: window.i18n, @@ -1056,7 +1060,7 @@ fromSync: true, }); } else if (isInstall) { - if (status === 'advertised') { + if (status === 'downloaded') { window.reduxActions.stickers.installStickerPack(id, key, { fromSync: true, }); diff --git a/js/models/messages.js b/js/models/messages.js index cd1910804..77bd0c288 100644 --- a/js/models/messages.js +++ b/js/models/messages.js @@ -33,7 +33,7 @@ const { copyStickerToAttachments, deletePackReference, - downloadStickerPack, + savePackMetadata, getStickerPackStatus, } = window.Signal.Stickers; const { addStickerPackReference } = window.Signal.Data; @@ -1467,7 +1467,7 @@ const status = getStickerPackStatus(packId); let data; - if (status && status !== 'pending' && status !== 'error') { + if (status && (status === 'downloaded' || status === 'installed')) { try { const copiedSticker = await copyStickerToAttachments( packId, @@ -1492,8 +1492,8 @@ }); } if (!status) { - // kick off the download without waiting - downloadStickerPack(packId, packKey, { messageId }); + // Save the packId/packKey for future download/install + savePackMetadata(packId, packKey, { messageId }); } else { await addStickerPackReference(messageId, packId); } diff --git a/js/modules/data.d.ts b/js/modules/data.d.ts index 60dc7998d..d711ae4d9 100644 --- a/js/modules/data.d.ts +++ b/js/modules/data.d.ts @@ -8,7 +8,7 @@ export function updateStickerLastUsed( ): Promise; export function updateStickerPackStatus( packId: string, - status: 'advertised' | 'installed' | 'error' | 'pending', + status: 'known' | 'downloaded' | 'installed' | 'error' | 'pending', options?: { timestamp: number } ): Promise; diff --git a/js/modules/data.js b/js/modules/data.js index 452b18adf..627fdc8ea 100644 --- a/js/modules/data.js +++ b/js/modules/data.js @@ -28,6 +28,7 @@ const SQL_CHANNEL_KEY = 'sql-channel'; const ERASE_SQL_KEY = 'erase-sql-key'; const ERASE_ATTACHMENTS_KEY = 'erase-attachments'; const ERASE_STICKERS_KEY = 'erase-stickers'; +const ERASE_TEMP_KEY = 'erase-temp'; const CLEANUP_ORPHANED_ATTACHMENTS_KEY = 'cleanup-orphaned-attachments'; const _jobs = Object.create(null); @@ -965,6 +966,7 @@ async function removeOtherData() { callChannel(ERASE_SQL_KEY), callChannel(ERASE_ATTACHMENTS_KEY), callChannel(ERASE_STICKERS_KEY), + callChannel(ERASE_TEMP_KEY), ]); } diff --git a/js/modules/signal.js b/js/modules/signal.js index 09f88fff9..3e85ffc4f 100644 --- a/js/modules/signal.js +++ b/js/modules/signal.js @@ -129,6 +129,7 @@ function initializeMigrations({ const { getPath, getStickersPath, + getTempPath, createReader, createAbsolutePathGetter, createWriterForNew, @@ -161,6 +162,12 @@ function initializeMigrations({ const deleteSticker = Attachments.createDeleter(stickersPath); const readStickerData = createReader(stickersPath); + const tempPath = getTempPath(userDataPath); + const getAbsoluteTempPath = createAbsolutePathGetter(tempPath); + const writeNewTempData = createWriterForNew(tempPath); + const deleteTempFile = Attachments.createDeleter(tempPath); + const readTempData = createReader(tempPath); + return { attachmentsPath, copyIntoAttachmentsDirectory, @@ -170,6 +177,7 @@ function initializeMigrations({ deleteOnDisk, }), deleteSticker, + deleteTempFile, getAbsoluteAttachmentPath, getAbsoluteStickerPath, getPlaceholderMigrations, @@ -181,6 +189,7 @@ function initializeMigrations({ loadStickerData, readAttachmentData, readStickerData, + readTempData, run, processNewAttachment: attachment => MessageType.processNewAttachment(attachment, { @@ -200,6 +209,13 @@ function initializeMigrations({ getImageDimensions, logger, }), + processNewEphemeralSticker: stickerData => + MessageType.processNewSticker(stickerData, { + writeNewStickerData: writeNewTempData, + getAbsoluteStickerPath: getAbsoluteTempPath, + getImageDimensions, + logger, + }), upgradeMessageSchema: (message, options = {}) => { const { maxVersion } = options; diff --git a/js/modules/stickers.d.ts b/js/modules/stickers.d.ts index ced60aa43..d73e4372b 100644 --- a/js/modules/stickers.d.ts +++ b/js/modules/stickers.d.ts @@ -1 +1,11 @@ export function maybeDeletePack(packId: string): Promise; + +export function downloadStickerPack( + packId: string, + packKey: string, + options?: { + finalStatus?: 'installed' | 'downloaded'; + messageId?: string; + fromSync?: boolean; + } +): Promise; diff --git a/js/modules/stickers.js b/js/modules/stickers.js index 9c8928013..0a2f4ba6d 100644 --- a/js/modules/stickers.js +++ b/js/modules/stickers.js @@ -2,6 +2,7 @@ textsecure, Signal, log, + navigator, reduxStore, reduxActions, URL @@ -9,7 +10,7 @@ const BLESSED_PACKS = {}; -const { isNumber, pick, reject, groupBy } = require('lodash'); +const { isNumber, pick, reject, groupBy, values } = require('lodash'); const pMap = require('p-map'); const Queue = require('p-queue'); const qs = require('qs'); @@ -34,6 +35,7 @@ module.exports = { deletePack, deletePackReference, downloadStickerPack, + downloadEphemeralPack, getDataFromLink, getInitialState, getInstalledStickerPacks, @@ -44,6 +46,8 @@ module.exports = { maybeDeletePack, downloadQueuedPacks, redactPackId, + removeEphemeralPack, + savePackMetadata, }; let initialState = null; @@ -88,8 +92,8 @@ function getInstalledStickerPacks() { return []; } - const values = Object.values(packs); - return values.filter(pack => pack.status === 'installed'); + const items = Object.values(packs); + return items.filter(pack => pack.status === 'installed'); } function downloadQueuedPacks() { @@ -113,7 +117,7 @@ function capturePacksToDownload(existingPackLookup) { const existing = existingPackLookup[id]; if ( !existing || - (existing.status !== 'advertised' && existing.status !== 'installed') + (existing.status !== 'downloaded' && existing.status !== 'installed') ) { toDownload[id] = { id, @@ -130,6 +134,18 @@ function capturePacksToDownload(existingPackLookup) { } const existing = existingPackLookup[id]; + + // These packs should never end up in the database, but if they do we'll delete them + if (existing.status === 'ephemeral') { + deletePack(id); + return; + } + + // We don't automatically download these; not until a user action kicks it off + if (existing.status === 'known') { + return; + } + if (doesPackNeedDownload(existing)) { toDownload[id] = { id, @@ -147,14 +163,23 @@ function doesPackNeedDownload(pack) { return true; } - const stickerCount = Object.keys(pack.stickers || {}).length; - return ( - !pack.status || - pack.status === 'error' || - pack.status === 'pending' || - !pack.stickerCount || - stickerCount < pack.stickerCount - ); + const { status, stickerCount } = pack; + const stickersDownloaded = Object.keys(pack.stickers || {}).length; + + if ( + (status === 'installed' || status === 'downloaded') && + stickerCount > 0 && + stickersDownloaded >= stickerCount + ) { + return false; + } + + // If we don't understand a pack's status, we'll download it + // If a pack has any other status, we'll download it + // If a pack has zero stickers in it, we'll download it + // If a pack doesn't have enough downloaded stickers, we'll download it + + return true; } async function getPacksForRedux() { @@ -209,10 +234,15 @@ async function decryptSticker(packKey, ciphertext) { return plaintext; } -async function downloadSticker(packId, packKey, proto) { +async function downloadSticker(packId, packKey, proto, options) { + const { ephemeral } = options || {}; + const ciphertext = await textsecure.messaging.getSticker(packId, proto.id); const plaintext = await decryptSticker(packKey, ciphertext); - const sticker = await Signal.Migrations.processNewSticker(plaintext); + + const sticker = ephemeral + ? await Signal.Migrations.processNewEphemeralSticker(plaintext, options) + : await Signal.Migrations.processNewSticker(plaintext, options); return { ...pick(proto, ['id', 'emoji']), @@ -221,6 +251,156 @@ async function downloadSticker(packId, packKey, proto) { }; } +async function savePackMetadata(packId, packKey, options = {}) { + const { messageId } = options; + + const existing = getStickerPack(packId); + if (existing) { + return; + } + + const { stickerPackAdded } = getReduxStickerActions(); + const pack = { + id: packId, + key: packKey, + status: 'known', + }; + stickerPackAdded(pack); + + await createOrUpdateStickerPack(pack); + if (messageId) { + await addStickerPackReference(messageId, packId); + } +} + +async function removeEphemeralPack(packId) { + const existing = getStickerPack(packId); + if ( + existing.status !== 'ephemeral' && + !(existing.status === 'error' && existing.attemptedStatus === 'ephemeral') + ) { + return; + } + + const { removeStickerPack } = getReduxStickerActions(); + removeStickerPack(packId); + + const stickers = values(existing.stickers); + const paths = stickers.map(sticker => sticker.path); + await pMap(paths, Signal.Migrations.deleteTempFile, { + concurrency: 3, + }); + + // Remove it from database in case it made it there + await deleteStickerPack(packId); +} + +async function downloadEphemeralPack(packId, packKey) { + const { + stickerAdded, + stickerPackAdded, + stickerPackUpdated, + } = getReduxStickerActions(); + + const existingPack = getStickerPack(packId); + if (existingPack) { + log.warn( + `Ephemeral download for pack ${redactPackId( + packId + )} requested, we already know about it. Skipping.` + ); + return; + } + + try { + // Synchronous placeholder to help with race conditions + const placeholder = { + id: packId, + key: packKey, + status: 'ephemeral', + }; + stickerPackAdded(placeholder); + + const ciphertext = await textsecure.messaging.getStickerPackManifest( + packId + ); + const plaintext = await decryptSticker(packKey, ciphertext); + const proto = textsecure.protobuf.StickerPack.decode(plaintext); + const firstStickerProto = proto.stickers ? proto.stickers[0] : null; + const stickerCount = proto.stickers.length; + + const coverProto = proto.cover || firstStickerProto; + const coverStickerId = coverProto ? coverProto.id : null; + + if (!coverProto || !isNumber(coverStickerId)) { + throw new Error( + `Sticker pack ${redactPackId( + packId + )} is malformed - it has no cover, and no stickers` + ); + } + + const nonCoverStickers = reject( + proto.stickers, + sticker => !isNumber(sticker.id) || sticker.id === coverStickerId + ); + + const coverIncludedInList = nonCoverStickers.length < stickerCount; + + const pack = { + id: packId, + key: packKey, + coverStickerId, + stickerCount, + status: 'ephemeral', + ...pick(proto, ['title', 'author']), + }; + stickerPackAdded(pack); + + const downloadStickerJob = async stickerProto => { + const stickerInfo = await downloadSticker(packId, packKey, stickerProto, { + ephemeral: true, + }); + const sticker = { + ...stickerInfo, + isCoverOnly: !coverIncludedInList && stickerInfo.id === coverStickerId, + }; + + const statusCheck = getStickerPackStatus(packId); + if (statusCheck !== 'ephemeral') { + throw new Error( + `Ephemeral download for pack ${redactPackId( + packId + )} interrupted by status change. Status is now ${statusCheck}.` + ); + } + + stickerAdded(sticker); + }; + + // Download the cover first + await downloadStickerJob(coverProto); + + // Then the rest + await pMap(nonCoverStickers, downloadStickerJob, { concurrency: 3 }); + } catch (error) { + // Because the user could install this pack while we are still downloading this + // ephemeral pack, we don't want to go change its status unless we're still in + // ephemeral mode. + const statusCheck = getStickerPackStatus(packId); + if (statusCheck === 'ephemeral') { + stickerPackUpdated(packId, { + attemptedStatus: 'ephemeral', + status: 'error', + }); + } + log.error( + `Ephemeral download error for sticker pack ${redactPackId(packId)}:`, + error && error.stack ? error.stack : error + ); + } +} + async function downloadStickerPack(packId, packKey, options = {}) { // This will ensure that only one download process is in progress at any given time return downloadQueue.add(async () => { @@ -244,7 +424,12 @@ async function doDownloadStickerPack(packId, packKey, options = {}) { installStickerPack, } = getReduxStickerActions(); - const finalStatus = options.finalStatus || 'advertised'; + const finalStatus = options.finalStatus || 'downloaded'; + if (finalStatus !== 'downloaded' && finalStatus !== 'installed') { + throw new Error( + `doDownloadStickerPack: invalid finalStatus of ${finalStatus} requested.` + ); + } const existing = getStickerPack(packId); if (!doesPackNeedDownload(existing)) { @@ -256,7 +441,10 @@ async function doDownloadStickerPack(packId, packKey, options = {}) { return; } - const downloadAttempts = (existing ? existing.downloadAttempts || 0 : 0) + 1; + // We don't count this as an attempt if we're offline + const attemptIncrement = navigator.onLine ? 1 : 0; + const downloadAttempts = + (existing ? existing.downloadAttempts || 0 : 0) + attemptIncrement; if (downloadAttempts > 3) { log.warn( `Refusing to attempt another download for pack ${redactPackId( @@ -280,6 +468,16 @@ async function doDownloadStickerPack(packId, packKey, options = {}) { let nonCoverStickers; try { + // Synchronous placeholder to help with race conditions + const placeholder = { + id: packId, + key: packKey, + attemptedStatus: finalStatus, + downloadAttempts, + status: 'pending', + }; + stickerPackAdded(placeholder); + const ciphertext = await textsecure.messaging.getStickerPackManifest( packId ); @@ -307,8 +505,10 @@ async function doDownloadStickerPack(packId, packKey, options = {}) { coverIncludedInList = nonCoverStickers.length < stickerCount; // status can be: + // - 'known' + // - 'ephemeral' (should not hit database) // - 'pending' - // - 'advertised' + // - 'downloaded' // - 'error' // - 'installed' const pack = { @@ -365,6 +565,13 @@ async function doDownloadStickerPack(packId, packKey, options = {}) { // Then the rest await pMap(nonCoverStickers, downloadStickerJob, { concurrency: 3 }); + // Allow for the user marking this pack as installed in the middle of our download; + // don't overwrite that status. + const existingStatus = getStickerPackStatus(packId); + if (existingStatus === 'installed') { + return; + } + if (finalStatus === 'installed') { await installStickerPack(packId, packKey, { fromSync }); } else { @@ -380,11 +587,12 @@ async function doDownloadStickerPack(packId, packKey, options = {}) { error && error.stack ? error.stack : error ); - const errorState = 'error'; - await updateStickerPackStatus(packId, errorState); + const errorStatus = 'error'; + await updateStickerPackStatus(packId, errorStatus); if (stickerPackUpdated) { stickerPackUpdated(packId, { - state: errorState, + attemptedStatus: finalStatus, + status: errorStatus, }); } } diff --git a/js/views/conversation_view.js b/js/views/conversation_view.js index c02f36d60..1794c77fc 100644 --- a/js/views/conversation_view.js +++ b/js/views/conversation_view.js @@ -1331,15 +1331,19 @@ dialog.focusCancel(); }, - showStickerPackPreview(packId) { + showStickerPackPreview(packId, packKey) { if (!window.ENABLE_STICKER_SEND) { return; } + window.Signal.Stickers.downloadEphemeralPack(packId, packKey); + const props = { packId, - onClose: () => { + onClose: async () => { this.stickerPreviewModalView.remove(); + this.stickerPreviewModalView = null; + await window.Signal.Stickers.removeEphemeralPack(packId); }, }; @@ -1349,9 +1353,6 @@ window.reduxStore, props ), - onClose: () => { - this.stickerPreviewModalView = null; - }, }); }, @@ -1364,8 +1365,8 @@ } const sticker = message.get('sticker'); if (sticker) { - const { packId } = sticker; - this.showStickerPackPreview(packId); + const { packId, packKey } = sticker; + this.showStickerPackPreview(packId, packKey); return; } @@ -1992,17 +1993,25 @@ }, async getStickerPackPreview(url) { + const isPackDownloaded = pack => + pack && (pack.status === 'downloaded' || pack.status === 'installed'); const isPackValid = pack => - pack && (pack.status === 'advertised' || pack.status === 'installed'); + pack && + (pack.status === 'ephemeral' || + pack.status === 'downloaded' || + pack.status === 'installed'); + + let id; + let key; try { - const { id, key } = window.Signal.Stickers.getDataFromLink(url); + ({ 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); + if (!isPackDownloaded(existing)) { + await window.Signal.Stickers.downloadEphemeralPack(id, keyBase64); } const pack = window.Signal.Stickers.getStickerPack(id); @@ -2015,9 +2024,10 @@ const { title, coverStickerId } = pack; const sticker = pack.stickers[coverStickerId]; - const data = await window.Signal.Migrations.readStickerData( - sticker.path - ); + const data = + pack.status === 'ephemeral' + ? await window.Signal.Migrations.readTempData(sticker.path) + : await window.Signal.Migrations.readStickerData(sticker.path); return { title, @@ -2035,6 +2045,10 @@ error && error.stack ? error.stack : error ); return null; + } finally { + if (id) { + await window.Signal.Stickers.removeEphemeralPack(id); + } } }, diff --git a/main.js b/main.js index ce4cd3829..9fae51feb 100644 --- a/main.js +++ b/main.js @@ -726,6 +726,14 @@ app.on('ready', async () => { }); } + try { + await attachments.clearTempPath(userDataPath); + } catch (error) { + logger.error( + 'main/ready: Error deleting temp dir:', + error && error.stack ? error.stack : error + ); + } await attachmentChannel.initialize({ configDir: userDataPath, cleanupOrphanedAttachments, @@ -1034,7 +1042,7 @@ function handleSgnlLink(incomingUrl) { if (command === 'addstickers' && mainWindow && mainWindow.webContents) { const { pack_id: packId, pack_key: packKeyHex } = args; const packKey = Buffer.from(packKeyHex, 'hex').toString('base64'); - mainWindow.webContents.send('add-sticker-pack', { packId, packKey }); + mainWindow.webContents.send('show-sticker-pack', { packId, packKey }); } else { console.error('Unhandled sgnl link'); } diff --git a/preload.js b/preload.js index a9da1219b..c00e58c58 100644 --- a/preload.js +++ b/preload.js @@ -9,7 +9,7 @@ const { app } = electron.remote; const { systemPreferences } = electron.remote.require('electron'); // Waiting for clients to implement changes on receive side -window.ENABLE_STICKER_SEND = false; +window.ENABLE_STICKER_SEND = true; window.TIMESTAMP_VALIDATION = false; window.PAD_ALL_ATTACHMENTS = false; window.SEND_RECIPIENT_UPDATES = false; @@ -175,11 +175,11 @@ ipc.on('delete-all-data', () => { } }); -ipc.on('add-sticker-pack', (_event, info) => { +ipc.on('show-sticker-pack', (_event, info) => { const { packId, packKey } = info; - const { installStickerPack } = window.Events; - if (installStickerPack) { - installStickerPack(packId, packKey); + const { showStickerPack } = window.Events; + if (showStickerPack) { + showStickerPack(packId, packKey); } }); @@ -306,6 +306,7 @@ window.moment.locale(locale); const userDataPath = app.getPath('userData'); window.baseAttachmentsPath = Attachments.getPath(userDataPath); window.baseStickersPath = Attachments.getStickersPath(userDataPath); +window.baseTempPath = Attachments.getTempPath(userDataPath); window.Signal = Signal.setup({ Attachments, userDataPath, diff --git a/stylesheets/_index.scss b/stylesheets/_index.scss index 6c11abee1..76b572d6a 100644 --- a/stylesheets/_index.scss +++ b/stylesheets/_index.scss @@ -3,7 +3,11 @@ .inbox, .gutter { height: 100%; - overflow: hidden; +} + +.inbox { + display: flex; + flex-direction: row; } .expired { @@ -82,6 +86,7 @@ } .conversation-stack { + flex-grow: 1; .conversation { display: none; } diff --git a/stylesheets/_modules.scss b/stylesheets/_modules.scss index 55c377084..0114b1225 100644 --- a/stylesheets/_modules.scss +++ b/stylesheets/_modules.scss @@ -3376,6 +3376,14 @@ max-height: 20px; } +.module-sticker-picker__header__button__image--placeholder { + min-width: 20px; + min-height: 20px; + max-width: 20px; + max-height: 20px; + background-color: $color-gray-10; +} + .module-sticker-picker__body { position: relative; @@ -3553,6 +3561,11 @@ width: 48px; height: 48px; } + &__cover-placeholder { + width: 48px; + height: 48px; + background: $color-gray-10; + } &__meta { flex-grow: 1; @@ -3681,9 +3694,23 @@ background: $color-gray-75; } + &__error { + color: $color-core-red; + display: flex; + justify-content: center; + align-items: center; + text-align: center; + width: 100%; + height: 100%; + padding: 0 80px 30px 80px; + font-family: Roboto; + font-weight: 300; + } + &__header { display: flex; flex-direction: row; + flex-shrink: 0; height: 36px; padding: 0 8px 0 16px; justify-content: space-between; @@ -3727,6 +3754,18 @@ width: 100%; height: 100%; } + + &--placeholder { + border-radius: 4px; + + @include light-theme() { + background: $color-gray-05; + } + + @include dark-theme() { + background: $color-gray-60; + } + } } } @@ -3913,6 +3952,11 @@ width: 20px; height: 20px; } + &__image-placeholder { + width: 20px; + height: 20px; + background-color: $color-gray-10; + } &__text { margin-left: 4px; @@ -3938,11 +3982,11 @@ display: flex; flex-direction: row; - &__image { - width: 52px; - height: 52px; - background: #eaeaea; - } + // &__image { + // width: 52px; + // height: 52px; + // background: $color-gray-10; + // } &__meta { flex-grow: 1; diff --git a/ts/components/stickers/StickerButton.md b/ts/components/stickers/StickerButton.md index cf5b1badc..3647c5bdd 100644 --- a/ts/components/stickers/StickerButton.md +++ b/ts/components/stickers/StickerButton.md @@ -43,6 +43,8 @@ const packs = [ i18n={util.i18n} receivedPacks={[]} installedPacks={packs} + blessedPacks={[]} + knownPacks={[]} onPickSticker={(packId, stickerId) => console.log('onPickSticker', { packId, stickerId }) } @@ -100,8 +102,10 @@ const packs = [ > console.log('onPickSticker', { packId, stickerId }) } @@ -113,7 +117,75 @@ const packs = [ ; ``` -#### No Advertised Packs and No Installed Packs +#### Just known packs + +Even with just known packs, the button should render. + +```jsx +const sticker1 = { id: 1, url: util.kitten164ObjectUrl, packId: 'foo' }; + +const packs = [ + { + id: 'foo', + cover: sticker1, + stickers: Array(101) + .fill(0) + .map((n, id) => ({ ...sticker1, id })), + }, +]; + + + + console.log('onPickSticker', { packId, stickerId }) + } + clearInstalledStickerPack={() => console.log('clearInstalledStickerPack')} + onClickAddPack={() => console.log('onClickAddPack')} + recentStickers={[]} + /> +; +``` + +#### Just blessed packs + +Even with just blessed packs, the button should render. + +```jsx +const sticker1 = { id: 1, url: util.kitten164ObjectUrl, packId: 'foo' }; + +const packs = [ + { + id: 'foo', + cover: sticker1, + stickers: Array(101) + .fill(0) + .map((n, id) => ({ ...sticker1, id })), + }, +]; + + + + console.log('onPickSticker', { packId, stickerId }) + } + clearInstalledStickerPack={() => console.log('clearInstalledStickerPack')} + onClickAddPack={() => console.log('onClickAddPack')} + recentStickers={[]} + /> +; +``` + +#### No packs at all When there are no advertised packs and no installed packs the button should not render anything. @@ -123,6 +195,8 @@ When there are no advertised packs and no installed packs the button should not i18n={util.i18n} receivedPacks={[]} installedPacks={[]} + blessedPacks={[]} + knownPacks={[]} onPickSticker={(packId, stickerId) => console.log('onPickSticker', { packId, stickerId }) } @@ -188,6 +262,8 @@ const packs = [ i18n={util.i18n} receivedPacks={[]} installedPacks={packs} + blessedPacks={[]} + knownPacks={[]} installedPack={packs[0]} onPickSticker={(packId, stickerId) => console.log('onPickSticker', { packId, stickerId }) @@ -257,6 +333,8 @@ const packs = [ i18n={util.i18n} receivedPacks={[]} installedPacks={packs} + blessedPacks={[]} + knownPacks={[]} onPickSticker={(packId, stickerId) => console.log('onPickSticker', { packId, stickerId }) } diff --git a/ts/components/stickers/StickerButton.tsx b/ts/components/stickers/StickerButton.tsx index 89aaf9936..b56732713 100644 --- a/ts/components/stickers/StickerButton.tsx +++ b/ts/components/stickers/StickerButton.tsx @@ -11,6 +11,8 @@ export type OwnProps = { readonly i18n: LocalizerType; readonly receivedPacks: ReadonlyArray; readonly installedPacks: ReadonlyArray; + readonly blessedPacks: ReadonlyArray; + readonly knownPacks: ReadonlyArray; readonly installedPack?: StickerPackType | null; readonly recentStickers: ReadonlyArray; readonly clearInstalledStickerPack: () => unknown; @@ -35,6 +37,8 @@ export const StickerButton = React.memo( receivedPacks, installedPack, installedPacks, + blessedPacks, + knownPacks, showIntroduction, clearShowIntroduction, showPickerHint, @@ -138,7 +142,12 @@ export const StickerButton = React.memo( [installedPack, clearInstalledStickerPack] ); - if (installedPacks.length + receivedPacks.length === 0) { + const totalPacks = + knownPacks.length + + blessedPacks.length + + installedPacks.length + + receivedPacks.length; + if (totalPacks === 0) { return null; } @@ -166,11 +175,15 @@ export const StickerButton = React.memo( role="button" onClick={clearInstalledStickerPack} > - {installedPack.title} + {installedPack.cover ? ( + {installedPack.title} + ) : ( +
+ )} {installedPack.title} @@ -202,7 +215,7 @@ export const StickerButton = React.memo( role="button" onClick={handleClearIntroduction} > -
+ {/*
*/}
{i18n('stickers--StickerManager--Introduction--Title')} diff --git a/ts/components/stickers/StickerManager.md b/ts/components/stickers/StickerManager.md index 719f0dca4..590af0942 100644 --- a/ts/components/stickers/StickerManager.md +++ b/ts/components/stickers/StickerManager.md @@ -35,11 +35,11 @@ const packs = [ }, ]; -const receivedPacks = packs.map(p => ({ ...p, status: 'advertised' })); +const receivedPacks = packs.map(p => ({ ...p, status: 'downloaded' })); const installedPacks = packs.map(p => ({ ...p, status: 'installed' })); const blessedPacks = packs.map(p => ({ ...p, - status: 'advertised', + status: 'downloaded', isBlessed: true, })); @@ -158,6 +158,64 @@ const noPacks = []; ; ``` +#### No with 'known' + +```jsx +const sticker1 = { id: 1, url: util.kitten164ObjectUrl, packId: 'foo' }; +const sticker2 = { id: 2, url: util.kitten264ObjectUrl, packId: 'bar' }; +const sticker3 = { id: 3, url: util.kitten364ObjectUrl, packId: 'baz' }; + +const installedPacks = [ + { + id: 'foo', + cover: sticker1, + title: 'Foo', + status: 'installed', + author: 'Foo McBarrington', + stickers: Array(101) + .fill(0) + .map((n, id) => ({ ...sticker1, id })), + }, +]; + +const knownPacks = [ + { + id: 'foo', + key: 'key1', + stickers: [], + state: 'known', + }, + { + id: 'bar', + key: 'key2', + stickers: [], + state: 'known', + }, + { + id: 'baz', + key: 'key3', + stickers: [], + state: 'known', + }, +]; + +const noPacks = []; + + + console.log('installStickerPack', id)} + downloadStickerPack={(packId, packKey, options) => + console.log('downloadStickerPack', { packId, packKey, options }) + } + /> +; +``` + #### No Packs at All ```jsx diff --git a/ts/components/stickers/StickerManager.tsx b/ts/components/stickers/StickerManager.tsx index 8450a9533..a43d5e138 100644 --- a/ts/components/stickers/StickerManager.tsx +++ b/ts/components/stickers/StickerManager.tsx @@ -9,6 +9,8 @@ export type OwnProps = { readonly installedPacks: ReadonlyArray; readonly receivedPacks: ReadonlyArray; readonly blessedPacks: ReadonlyArray; + readonly knownPacks?: ReadonlyArray; + readonly downloadStickerPack: (packId: string, packKey: string) => unknown; readonly installStickerPack: (packId: string, packKey: string) => unknown; readonly uninstallStickerPack: (packId: string, packKey: string) => unknown; readonly i18n: LocalizerType; @@ -20,7 +22,9 @@ export const StickerManager = React.memo( ({ installedPacks, receivedPacks, + knownPacks, blessedPacks, + downloadStickerPack, installStickerPack, uninstallStickerPack, i18n, @@ -30,6 +34,15 @@ export const StickerManager = React.memo( setPackToPreview, ] = React.useState(null); + React.useEffect(() => { + if (!knownPacks) { + return; + } + knownPacks.forEach(pack => { + downloadStickerPack(pack.id, pack.key); + }); + }, []); + const clearPackToPreview = React.useCallback( () => { setPackToPreview(null); @@ -51,6 +64,7 @@ export const StickerManager = React.memo( i18n={i18n} pack={packToPreview} onClose={clearPackToPreview} + downloadStickerPack={downloadStickerPack} installStickerPack={installStickerPack} uninstallStickerPack={uninstallStickerPack} /> diff --git a/ts/components/stickers/StickerManagerPackRow.tsx b/ts/components/stickers/StickerManagerPackRow.tsx index 536d6cfda..3878062aa 100644 --- a/ts/components/stickers/StickerManagerPackRow.tsx +++ b/ts/components/stickers/StickerManagerPackRow.tsx @@ -91,11 +91,15 @@ export const StickerManagerPackRow = React.memo( onClick={handleClickPreview} className="module-sticker-manager__pack-row" > - {pack.title} + {pack.cover ? ( + {pack.title} + ) : ( +
+ )}
{pack.title} @@ -108,18 +112,18 @@ export const StickerManagerPackRow = React.memo(
- {pack.status === 'advertised' ? ( - - ) : ( + {pack.status === 'installed' ? ( + ) : ( + )}
diff --git a/ts/components/stickers/StickerPicker.tsx b/ts/components/stickers/StickerPicker.tsx index abc92860d..768f2d333 100644 --- a/ts/components/stickers/StickerPicker.tsx +++ b/ts/components/stickers/StickerPicker.tsx @@ -153,12 +153,16 @@ export const StickerPicker = React.memo( } )} > - {pack.title} + {pack.cover ? ( + {pack.title} + ) : ( +
+ )} ))}
diff --git a/ts/components/stickers/StickerPreviewModal.md b/ts/components/stickers/StickerPreviewModal.md index 90737b0d7..82ede3543 100644 --- a/ts/components/stickers/StickerPreviewModal.md +++ b/ts/components/stickers/StickerPreviewModal.md @@ -9,7 +9,7 @@ const pack = { title: 'Foo', isBlessed: true, author: 'Foo McBarrington', - status: 'advertised', + status: 'downloaded', stickers: Array(101) .fill(0) .map((n, id) => ({ ...abeSticker, id })), diff --git a/ts/components/stickers/StickerPreviewModal.tsx b/ts/components/stickers/StickerPreviewModal.tsx index bc51282a2..559350810 100644 --- a/ts/components/stickers/StickerPreviewModal.tsx +++ b/ts/components/stickers/StickerPreviewModal.tsx @@ -1,15 +1,23 @@ import * as React from 'react'; import { createPortal } from 'react-dom'; +import { isNumber, range } from 'lodash'; +import classNames from 'classnames'; import { StickerPackInstallButton } from './StickerPackInstallButton'; import { ConfirmationDialog } from '../ConfirmationDialog'; import { LocalizerType } from '../../types/Util'; import { StickerPackType } from '../../state/ducks/stickers'; +import { Spinner } from '../Spinner'; export type OwnProps = { readonly onClose: () => unknown; + readonly downloadStickerPack: ( + packId: string, + packKey: string, + options?: { finalStatus?: 'installed' | 'downloaded' } + ) => unknown; readonly installStickerPack: (packId: string, packKey: string) => unknown; readonly uninstallStickerPack: (packId: string, packKey: string) => unknown; - readonly pack: StickerPackType; + readonly pack?: StickerPackType; readonly i18n: LocalizerType; }; @@ -21,15 +29,57 @@ function focusRef(el: HTMLElement | null) { } } +function renderBody({ pack, i18n }: Props) { + if (pack && pack.status === 'error') { + return ( +
+ {i18n('stickers--StickerPreview--Error')} +
+ ); + } + + if (!pack || pack.stickerCount === 0 || !isNumber(pack.stickerCount)) { + return ; + } + + return ( +
+ {pack.stickers.map(({ id, url }) => ( +
+ {pack.title} +
+ ))} + {range(pack.stickerCount - pack.stickers.length).map(i => ( +
+ ))} +
+ ); +} + export const StickerPreviewModal = React.memo( // tslint:disable-next-line max-func-body-length - ({ - onClose, - pack, - i18n, - installStickerPack, - uninstallStickerPack, - }: Props) => { + (props: Props) => { + const { + onClose, + pack, + i18n, + downloadStickerPack, + installStickerPack, + uninstallStickerPack, + } = props; const [root, setRoot] = React.useState(null); const [confirmingUninstall, setConfirmingUninstall] = React.useState(false); @@ -40,15 +90,36 @@ export const StickerPreviewModal = React.memo( return () => { document.body.removeChild(div); - setRoot(null); }; }, []); - const isInstalled = pack.status === 'installed'; + React.useEffect(() => { + if (pack && pack.status === 'known') { + downloadStickerPack(pack.id, pack.key); + } + if ( + pack && + pack.status === 'error' && + (pack.attemptedStatus === 'downloaded' || + pack.attemptedStatus === 'installed') + ) { + downloadStickerPack(pack.id, pack.key, { + finalStatus: pack.attemptedStatus, + }); + } + }, []); + + const isInstalled = Boolean(pack && pack.status === 'installed'); const handleToggleInstall = React.useCallback( () => { + if (!pack) { + return; + } if (isInstalled) { setConfirmingUninstall(true); + } else if (pack.status === 'ephemeral') { + downloadStickerPack(pack.id, pack.key, { finalStatus: 'installed' }); + onClose(); } else { installStickerPack(pack.id, pack.key); onClose(); @@ -59,6 +130,9 @@ export const StickerPreviewModal = React.memo( const handleUninstall = React.useCallback( () => { + if (!pack) { + return; + } uninstallStickerPack(pack.id, pack.key); setConfirmingUninstall(false); // onClose is called by the confirmation modal @@ -119,42 +193,35 @@ export const StickerPreviewModal = React.memo( className="module-sticker-manager__preview-modal__container__header__close-button" /> -
- {pack.stickers.map(({ id, url }) => ( -
- {pack.title} + {renderBody(props)} + {pack && pack.status !== 'error' ? ( +
+
+

+ {pack.title} + {pack.isBlessed ? ( + + ) : null} +

+

+ {pack.author} +

+
+
+ {pack.status === 'pending' ? ( + + ) : ( + + )}
- ))} -
-
-
-

- {pack.title} - {pack.isBlessed ? ( - - ) : null} -

-

- {pack.author} -

-
- -
-
+ ) : null}
)}
, diff --git a/ts/state/ducks/stickers.ts b/ts/state/ducks/stickers.ts index a06602f67..ebc817648 100644 --- a/ts/state/ducks/stickers.ts +++ b/ts/state/ducks/stickers.ts @@ -4,10 +4,15 @@ import { updateStickerLastUsed, updateStickerPackStatus, } from '../../../js/modules/data'; -import { maybeDeletePack } from '../../../js/modules/stickers'; +import { + downloadStickerPack as externalDownloadStickerPack, + maybeDeletePack, +} from '../../../js/modules/stickers'; import { sendStickerPackSync } from '../../shims/textsecure'; import { trigger } from '../../shims/events'; +import { NoopActionType } from './noop'; + // State export type StickerDBType = { @@ -24,14 +29,20 @@ export type StickerPackDBType = { readonly id: string; readonly key: string; - readonly attemptedStatus: string; + readonly attemptedStatus: 'downloaded' | 'installed' | 'ephemeral'; readonly author: string; readonly coverStickerId: number; readonly createdAt: number; readonly downloadAttempts: number; readonly installedAt: number | null; readonly lastUsed: number; - readonly status: 'advertised' | 'installed' | 'pending' | 'error'; + readonly status: + | 'known' + | 'ephemeral' + | 'downloaded' + | 'installed' + | 'pending' + | 'error'; readonly stickerCount: number; readonly stickers: Dictionary; readonly title: string; @@ -64,9 +75,16 @@ export type StickerPackType = { readonly title: string; readonly author: string; readonly isBlessed: boolean; - readonly cover: StickerType; + readonly cover?: StickerType; readonly lastUsed: number; - readonly status: 'advertised' | 'installed' | 'pending' | 'error'; + readonly attemptedStatus?: 'downloaded' | 'installed' | 'ephemeral'; + readonly status: + | 'known' + | 'ephemeral' + | 'downloaded' + | 'installed' + | 'pending' + | 'error'; readonly stickers: Array; readonly stickerCount: number; }; @@ -103,7 +121,7 @@ type ClearInstalledStickerPackAction = { type UninstallStickerPackPayloadType = { packId: string; - status: 'advertised'; + status: 'downloaded'; installedAt: null; recentStickers: Array; }; @@ -148,11 +166,13 @@ export type StickersActionType = | UninstallStickerPackFulfilledAction | StickerPackUpdatedAction | StickerPackRemovedAction - | UseStickerFulfilledAction; + | UseStickerFulfilledAction + | NoopActionType; // Action Creators export const actions = { + downloadStickerPack, clearInstalledStickerPack, removeStickerPack, stickerAdded, @@ -191,6 +211,23 @@ function stickerPackAdded(payload: StickerPackDBType): StickerPackAddedAction { }; } +function downloadStickerPack( + packId: string, + packKey: string, + options?: { finalStatus?: 'installed' | 'downloaded' } +): NoopActionType { + const { finalStatus } = options || { finalStatus: undefined }; + + // We're just kicking this off, since it will generate more redux events + // tslint:disable-next-line:no-floating-promises + externalDownloadStickerPack(packId, packKey, { finalStatus }); + + return { + type: 'NOOP', + payload: null, + }; +} + function installStickerPack( packId: string, packKey: string, @@ -246,7 +283,7 @@ async function doUninstallStickerPack( ): Promise { const { fromSync } = options || { fromSync: false }; - const status = 'advertised'; + const status = 'downloaded'; await updateStickerPackStatus(packId, status); // If there are no more references, it should be removed @@ -277,6 +314,13 @@ function stickerPackUpdated( packId: string, patch: Partial ): StickerPackUpdatedAction { + const { status, attemptedStatus } = patch; + + // We do this to trigger a toast, which is still done via Backbone + if (status === 'error' && attemptedStatus === 'installed') { + trigger('pack-install-failed'); + } + return { type: 'stickers/STICKER_PACK_UPDATED', payload: { diff --git a/ts/state/ducks/user.ts b/ts/state/ducks/user.ts index fe8c685d3..cc08d772d 100644 --- a/ts/state/ducks/user.ts +++ b/ts/state/ducks/user.ts @@ -6,6 +6,7 @@ import { LocalizerType } from '../../types/Util'; export type UserStateType = { attachmentsPath: string; stickersPath: string; + tempPath: string; ourNumber: string; regionCode: string; i18n: LocalizerType; @@ -45,6 +46,7 @@ function getEmptyState(): UserStateType { return { attachmentsPath: 'missing', stickersPath: 'missing', + tempPath: 'missing', ourNumber: 'missing', regionCode: 'missing', i18n: () => 'missing', diff --git a/ts/state/selectors/stickers.ts b/ts/state/selectors/stickers.ts index 7734832b8..cd2966724 100644 --- a/ts/state/selectors/stickers.ts +++ b/ts/state/selectors/stickers.ts @@ -20,13 +20,14 @@ import { StickersStateType, StickerType, } from '../ducks/stickers'; -import { getStickersPath } from './user'; +import { getStickersPath, getTempPath } from './user'; const getSticker = ( packs: Dictionary, packId: string, stickerId: number, - stickerPath: string + stickerPath: string, + tempPath: string ): StickerType | undefined => { const pack = packs[packId]; if (!pack) { @@ -38,20 +39,25 @@ const getSticker = ( return; } - return translateStickerFromDB(sticker, stickerPath); + const isEphemeral = pack.status === 'ephemeral'; + + return translateStickerFromDB(sticker, stickerPath, tempPath, isEphemeral); }; const translateStickerFromDB = ( sticker: StickerDBType, - stickerPath: string + stickerPath: string, + tempPath: string, + isEphemeral: boolean ): StickerType => { const { id, packId, emoji, path } = sticker; + const prefix = isEphemeral ? tempPath : stickerPath; return { id, packId, emoji, - url: join(stickerPath, path), + url: join(prefix, path), }; }; @@ -59,9 +65,11 @@ export const translatePackFromDB = ( pack: StickerPackDBType, packs: Dictionary, blessedPacks: Dictionary, - stickersPath: string + stickersPath: string, + tempPath: string ) => { - const { id, stickers, coverStickerId } = pack; + const { id, stickers, status, coverStickerId } = pack; + const isEphemeral = status === 'ephemeral'; // Sometimes sticker packs have a cover which isn't included in their set of stickers. // We don't want to show cover-only images when previewing or picking from a pack. @@ -70,13 +78,13 @@ export const translatePackFromDB = ( sticker => sticker.isCoverOnly ); const translatedStickers = map(filteredStickers, sticker => - translateStickerFromDB(sticker, stickersPath) + translateStickerFromDB(sticker, stickersPath, tempPath, isEphemeral) ); return { ...pack, isBlessed: Boolean(blessedPacks[id]), - cover: getSticker(packs, id, coverStickerId, stickersPath), + cover: getSticker(packs, id, coverStickerId, stickersPath, tempPath), stickers: sortBy(translatedStickers, sticker => sticker.id), }; }; @@ -86,18 +94,15 @@ const filterAndTransformPacks = ( packFilter: (sticker: StickerPackDBType) => boolean, packSort: (sticker: StickerPackDBType) => any, blessedPacks: Dictionary, - stickersPath: string + stickersPath: string, + tempPath: string ): Array => { const list = filter(packs, packFilter); const sorted = orderBy(list, packSort, ['desc']); - const ready = sorted.map(pack => - translatePackFromDB(pack, packs, blessedPacks, stickersPath) + return sorted.map(pack => + translatePackFromDB(pack, packs, blessedPacks, stickersPath, tempPath) ); - - // We're explicitly forcing pack.cover to be truthy here, but TypeScript doesn't - // understand that. - return ready.filter(pack => Boolean(pack.cover)) as Array; }; const getStickers = (state: StateType) => state.stickers; @@ -121,14 +126,16 @@ export const getRecentStickers = createSelector( getRecents, getPacks, getStickersPath, + getTempPath, ( recents: Array, packs: Dictionary, - stickersPath: string + stickersPath: string, + tempPath: string ) => { return compact( recents.map(({ packId, stickerId }) => { - return getSticker(packs, packId, stickerId, stickersPath); + return getSticker(packs, packId, stickerId, stickersPath, tempPath); }) ); } @@ -138,17 +145,20 @@ export const getInstalledStickerPacks = createSelector( getPacks, getBlessedPacks, getStickersPath, + getTempPath, ( packs: Dictionary, blessedPacks: Dictionary, - stickersPath: string + stickersPath: string, + tempPath: string ): Array => { return filterAndTransformPacks( packs, pack => pack.status === 'installed', pack => pack.installedAt, blessedPacks, - stickersPath + stickersPath, + tempPath ); } ); @@ -169,19 +179,22 @@ export const getReceivedStickerPacks = createSelector( getPacks, getBlessedPacks, getStickersPath, + getTempPath, ( packs: Dictionary, blessedPacks: Dictionary, - stickersPath: string + stickersPath: string, + tempPath: string ): Array => { return filterAndTransformPacks( packs, pack => - (pack.status === 'advertised' || pack.status === 'pending') && + (pack.status === 'downloaded' || pack.status === 'pending') && !blessedPacks[pack.id], pack => pack.createdAt, blessedPacks, - stickersPath + stickersPath, + tempPath ); } ); @@ -190,17 +203,42 @@ export const getBlessedStickerPacks = createSelector( getPacks, getBlessedPacks, getStickersPath, + getTempPath, ( packs: Dictionary, blessedPacks: Dictionary, - stickersPath: string + stickersPath: string, + tempPath: string ): Array => { return filterAndTransformPacks( packs, pack => blessedPacks[pack.id] && pack.status !== 'installed', pack => pack.createdAt, blessedPacks, - stickersPath + stickersPath, + tempPath + ); + } +); + +export const getKnownStickerPacks = createSelector( + getPacks, + getBlessedPacks, + getStickersPath, + getTempPath, + ( + packs: Dictionary, + blessedPacks: Dictionary, + stickersPath: string, + tempPath: string + ): Array => { + return filterAndTransformPacks( + packs, + pack => !blessedPacks[pack.id] && pack.status === 'known', + pack => pack.createdAt, + blessedPacks, + stickersPath, + tempPath ); } ); diff --git a/ts/state/selectors/user.ts b/ts/state/selectors/user.ts index 636cd34df..236da0601 100644 --- a/ts/state/selectors/user.ts +++ b/ts/state/selectors/user.ts @@ -31,3 +31,8 @@ export const getStickersPath = createSelector( getUser, (state: UserStateType): string => state.stickersPath ); + +export const getTempPath = createSelector( + getUser, + (state: UserStateType): string => state.tempPath +); diff --git a/ts/state/smart/StickerButton.tsx b/ts/state/smart/StickerButton.tsx index d9ee73604..d41c9c34e 100644 --- a/ts/state/smart/StickerButton.tsx +++ b/ts/state/smart/StickerButton.tsx @@ -6,7 +6,9 @@ import { StateType } from '../reducer'; import { getIntl } from '../selectors/user'; import { + getBlessedStickerPacks, getInstalledStickerPacks, + getKnownStickerPacks, getReceivedStickerPacks, getRecentlyInstalledStickerPack, getRecentStickers, @@ -15,6 +17,9 @@ import { const mapStateToProps = (state: StateType) => { const receivedPacks = getReceivedStickerPacks(state); const installedPacks = getInstalledStickerPacks(state); + const blessedPacks = getBlessedStickerPacks(state); + const knownPacks = getKnownStickerPacks(state); + const recentStickers = getRecentStickers(state); const installedPack = getRecentlyInstalledStickerPack(state); const showIntroduction = get( @@ -29,6 +34,8 @@ const mapStateToProps = (state: StateType) => { return { receivedPacks, installedPack, + blessedPacks, + knownPacks, installedPacks, recentStickers, showIntroduction, diff --git a/ts/state/smart/StickerManager.tsx b/ts/state/smart/StickerManager.tsx index 6e042a228..58f327a06 100644 --- a/ts/state/smart/StickerManager.tsx +++ b/ts/state/smart/StickerManager.tsx @@ -7,6 +7,7 @@ import { getIntl } from '../selectors/user'; import { getBlessedStickerPacks, getInstalledStickerPacks, + getKnownStickerPacks, getReceivedStickerPacks, } from '../selectors/stickers'; @@ -14,11 +15,13 @@ const mapStateToProps = (state: StateType) => { const blessedPacks = getBlessedStickerPacks(state); const receivedPacks = getReceivedStickerPacks(state); const installedPacks = getInstalledStickerPacks(state); + const knownPacks = getKnownStickerPacks(state); return { blessedPacks, receivedPacks, installedPacks, + knownPacks, i18n: getIntl(state), }; }; diff --git a/ts/state/smart/StickerPreviewModal.tsx b/ts/state/smart/StickerPreviewModal.tsx index 2e94c3cbe..a7bca689b 100644 --- a/ts/state/smart/StickerPreviewModal.tsx +++ b/ts/state/smart/StickerPreviewModal.tsx @@ -3,7 +3,7 @@ import { mapDispatchToProps } from '../actions'; import { StickerPreviewModal } from '../../components/stickers/StickerPreviewModal'; import { StateType } from '../reducer'; -import { getIntl, getStickersPath } from '../selectors/user'; +import { getIntl, getStickersPath, getTempPath } from '../selectors/user'; import { getBlessedPacks, getPacks, @@ -18,33 +18,17 @@ type ExternalProps = { const mapStateToProps = (state: StateType, props: ExternalProps) => { const { packId } = props; const stickersPath = getStickersPath(state); + const tempPath = getTempPath(state); + const packs = getPacks(state); const blessedPacks = getBlessedPacks(state); const pack = packs[packId]; - if (!pack) { - throw new Error(`Cannot find pack ${packId}`); - } - const translated = translatePackFromDB( - pack, - packs, - blessedPacks, - stickersPath - ); - return { ...props, - pack: { - ...translated, - cover: translated.cover - ? translated.cover - : { - id: 0, - url: 'nonexistent', - packId, - emoji: 'WTF', - }, - }, + pack: pack + ? translatePackFromDB(pack, packs, blessedPacks, stickersPath, tempPath) + : undefined, i18n: getIntl(state), }; }; diff --git a/ts/updater/common.ts b/ts/updater/common.ts index 3bf774036..80cc89683 100644 --- a/ts/updater/common.ts +++ b/ts/updater/common.ts @@ -20,6 +20,8 @@ import mkdirp from 'mkdirp'; import rimraf from 'rimraf'; import { app, BrowserWindow, dialog } from 'electron'; +import { getTempPath } from '../../app/attachments'; + // @ts-ignore import * as packageJson from '../../package.json'; import { getSignatureFileName } from './signature'; @@ -269,7 +271,7 @@ function getGotOptions(): GotOptions { function getBaseTempDir() { // We only use tmpdir() when this code is run outside of an Electron app (as in: tests) - return app ? join(app.getPath('userData'), 'temp') : tmpdir(); + return app ? getTempPath(app.getPath('userData')) : tmpdir(); } export async function createTempDir() { @@ -303,11 +305,6 @@ export function getPrintableError(error: Error) { return error && error.stack ? error.stack : error; } -export async function deleteBaseTempDir() { - const baseTempDir = getBaseTempDir(); - await rimrafPromise(baseTempDir); -} - export function getCliOptions(options: any): T { const parser = createParser({ options }); const cliOptions = parser.parse(process.argv); diff --git a/ts/updater/index.ts b/ts/updater/index.ts index 04b6bb52c..0724319bb 100644 --- a/ts/updater/index.ts +++ b/ts/updater/index.ts @@ -3,12 +3,7 @@ import { BrowserWindow } from 'electron'; import { start as startMacOS } from './macos'; import { start as startWindows } from './windows'; -import { - deleteBaseTempDir, - getPrintableError, - LoggerType, - MessagesType, -} from './common'; +import { LoggerType, MessagesType } from './common'; let initialized = false; @@ -39,15 +34,6 @@ export async function start( return; } - try { - await deleteBaseTempDir(); - } catch (error) { - logger.error( - 'updater/start: Error deleting temp dir:', - getPrintableError(error) - ); - } - if (platform === 'win32') { await startWindows(getMainWindow, messages, logger); } else if (platform === 'darwin') { diff --git a/ts/util/lint/exceptions.json b/ts/util/lint/exceptions.json index accef5115..37a2791e8 100644 --- a/ts/util/lint/exceptions.json +++ b/ts/util/lint/exceptions.json @@ -227,11 +227,19 @@ "reasonCategory": "falseMatch", "updated": "2018-09-19T18:13:29.628Z" }, + { + "rule": "jQuery-load(", + "path": "js/modules/emojis.js", + "line": "async function load() {", + "lineNumber": 13, + "reasonCategory": "falseMatch", + "updated": "2019-05-23T22:27:53.554Z" + }, { "rule": "jQuery-load(", "path": "js/modules/stickers.js", "line": "async function load() {", - "lineNumber": 53, + "lineNumber": 57, "reasonCategory": "falseMatch", "updated": "2019-04-26T17:48:30.675Z" }, @@ -6066,13 +6074,5 @@ "lineNumber": 60, "reasonCategory": "falseMatch", "updated": "2019-05-02T20:44:56.470Z" - }, - { - "rule": "jQuery-load(", - "path": "js/modules/emojis.js", - "line": "async function load() {", - "lineNumber": 13, - "reasonCategory": "falseMatch", - "updated": "2019-05-23T22:27:53.554Z" } -] +] \ No newline at end of file