Support additional sticker states

Co-authored-by: scott@signal.org
Co-authored-by: ken@signal.org
This commit is contained in:
Ken Powers 2019-05-23 18:27:42 -07:00 committed by Scott Nonnenberg
parent 41880cfe66
commit be5d0837f8
35 changed files with 925 additions and 249 deletions

View File

@ -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."

View File

@ -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 {

1
app/attachments.d.ts vendored Normal file
View File

@ -0,0 +1 @@
export function getTempPath(userDataPath: string): string;

View File

@ -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)

View File

@ -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) {

View File

@ -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,
});

View File

@ -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);
}

View File

@ -8,7 +8,7 @@ export function updateStickerLastUsed(
): Promise<void>;
export function updateStickerPackStatus(
packId: string,
status: 'advertised' | 'installed' | 'error' | 'pending',
status: 'known' | 'downloaded' | 'installed' | 'error' | 'pending',
options?: { timestamp: number }
): Promise<void>;

View File

@ -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),
]);
}

View File

@ -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;

View File

@ -1 +1,11 @@
export function maybeDeletePack(packId: string): Promise<void>;
export function downloadStickerPack(
packId: string,
packKey: string,
options?: {
finalStatus?: 'installed' | 'downloaded';
messageId?: string;
fromSync?: boolean;
}
): Promise<void>;

View File

@ -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,
});
}
}

View File

@ -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);
}
}
},

10
main.js
View File

@ -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');
}

View File

@ -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,

View File

@ -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;
}

View File

@ -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;

View File

@ -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 = [
>
<StickerButton
i18n={util.i18n}
receivedPacks={[]}
installedPacks={packs}
receivedPacks={packs}
installedPacks={[]}
blessedPacks={[]}
knownPacks={[]}
onPickSticker={(packId, stickerId) =>
console.log('onPickSticker', { packId, stickerId })
}
@ -113,7 +117,75 @@ const packs = [
</util.ConversationContext>;
```
#### 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 })),
},
];
<util.ConversationContext theme={util.theme}>
<StickerButton
i18n={util.i18n}
receivedPacks={[]}
installedPacks={[]}
knownPacks={packs}
blessedPacks={[]}
onPickSticker={(packId, stickerId) =>
console.log('onPickSticker', { packId, stickerId })
}
clearInstalledStickerPack={() => console.log('clearInstalledStickerPack')}
onClickAddPack={() => console.log('onClickAddPack')}
recentStickers={[]}
/>
</util.ConversationContext>;
```
#### 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 })),
},
];
<util.ConversationContext theme={util.theme}>
<StickerButton
i18n={util.i18n}
receivedPacks={[]}
installedPacks={[]}
blessedPacks={packs}
knownPacks={[]}
onPickSticker={(packId, stickerId) =>
console.log('onPickSticker', { packId, stickerId })
}
clearInstalledStickerPack={() => console.log('clearInstalledStickerPack')}
onClickAddPack={() => console.log('onClickAddPack')}
recentStickers={[]}
/>
</util.ConversationContext>;
```
#### 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 })
}

View File

@ -11,6 +11,8 @@ export type OwnProps = {
readonly i18n: LocalizerType;
readonly receivedPacks: ReadonlyArray<StickerPackType>;
readonly installedPacks: ReadonlyArray<StickerPackType>;
readonly blessedPacks: ReadonlyArray<StickerPackType>;
readonly knownPacks: ReadonlyArray<StickerPackType>;
readonly installedPack?: StickerPackType | null;
readonly recentStickers: ReadonlyArray<StickerType>;
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}
>
<img
className="module-sticker-button__tooltip__image"
src={installedPack.cover.url}
alt={installedPack.title}
/>
{installedPack.cover ? (
<img
className="module-sticker-button__tooltip__image"
src={installedPack.cover.url}
alt={installedPack.title}
/>
) : (
<div className="module-sticker-button__tooltip__image-placeholder" />
)}
<span className="module-sticker-button__tooltip__text">
<span className="module-sticker-button__tooltip__text__title">
{installedPack.title}
@ -202,7 +215,7 @@ export const StickerButton = React.memo(
role="button"
onClick={handleClearIntroduction}
>
<div className="module-sticker-button__tooltip--introduction__image" />
{/* <div className="module-sticker-button__tooltip--introduction__image" /> */}
<div className="module-sticker-button__tooltip--introduction__meta">
<div className="module-sticker-button__tooltip--introduction__meta__title">
{i18n('stickers--StickerManager--Introduction--Title')}

View File

@ -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 = [];
</util.ConversationContext>;
```
#### 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 = [];
<util.ConversationContext theme={util.theme}>
<StickerManager
i18n={util.i18n}
installedPacks={installedPacks}
receivedPacks={noPacks}
blessedPacks={noPacks}
knownPacks={knownPacks}
installStickerPack={id => console.log('installStickerPack', id)}
downloadStickerPack={(packId, packKey, options) =>
console.log('downloadStickerPack', { packId, packKey, options })
}
/>
</util.ConversationContext>;
```
#### No Packs at All
```jsx

View File

@ -9,6 +9,8 @@ export type OwnProps = {
readonly installedPacks: ReadonlyArray<StickerPackType>;
readonly receivedPacks: ReadonlyArray<StickerPackType>;
readonly blessedPacks: ReadonlyArray<StickerPackType>;
readonly knownPacks?: ReadonlyArray<StickerPackType>;
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<StickerPackType | null>(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}
/>

View File

@ -91,11 +91,15 @@ export const StickerManagerPackRow = React.memo(
onClick={handleClickPreview}
className="module-sticker-manager__pack-row"
>
<img
src={pack.cover.url}
alt={pack.title}
className="module-sticker-manager__pack-row__cover"
/>
{pack.cover ? (
<img
src={pack.cover.url}
alt={pack.title}
className="module-sticker-manager__pack-row__cover"
/>
) : (
<div className="module-sticker-manager__pack-row__cover-placeholder" />
)}
<div className="module-sticker-manager__pack-row__meta">
<div className="module-sticker-manager__pack-row__meta__title">
{pack.title}
@ -108,18 +112,18 @@ export const StickerManagerPackRow = React.memo(
</div>
</div>
<div className="module-sticker-manager__pack-row__controls">
{pack.status === 'advertised' ? (
<StickerPackInstallButton
installed={false}
i18n={i18n}
onClick={handleInstall}
/>
) : (
{pack.status === 'installed' ? (
<StickerPackInstallButton
installed={true}
i18n={i18n}
onClick={handleUninstall}
/>
) : (
<StickerPackInstallButton
installed={false}
i18n={i18n}
onClick={handleInstall}
/>
)}
</div>
</div>

View File

@ -153,12 +153,16 @@ export const StickerPicker = React.memo(
}
)}
>
<img
className="module-sticker-picker__header__button__image"
src={pack.cover.url}
alt={pack.title}
title={pack.title}
/>
{pack.cover ? (
<img
className="module-sticker-picker__header__button__image"
src={pack.cover.url}
alt={pack.title}
title={pack.title}
/>
) : (
<div className="module-sticker-picker__header__button__image-placeholder" />
)}
</button>
))}
</div>

View File

@ -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 })),

View File

@ -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 (
<div className="module-sticker-manager__preview-modal__container__error">
{i18n('stickers--StickerPreview--Error')}
</div>
);
}
if (!pack || pack.stickerCount === 0 || !isNumber(pack.stickerCount)) {
return <Spinner size="normal" />;
}
return (
<div className="module-sticker-manager__preview-modal__container__sticker-grid">
{pack.stickers.map(({ id, url }) => (
<div
key={id}
className="module-sticker-manager__preview-modal__container__sticker-grid__cell"
>
<img
className="module-sticker-manager__preview-modal__container__sticker-grid__cell__image"
src={url}
alt={pack.title}
/>
</div>
))}
{range(pack.stickerCount - pack.stickers.length).map(i => (
<div
key={`placeholder-${i}`}
className={classNames(
'module-sticker-manager__preview-modal__container__sticker-grid__cell',
'module-sticker-manager__preview-modal__container__sticker-grid__cell--placeholder'
)}
/>
))}
</div>
);
}
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<HTMLElement | null>(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"
/>
</header>
<div className="module-sticker-manager__preview-modal__container__sticker-grid">
{pack.stickers.map(({ id, url }) => (
<div
key={id}
className="module-sticker-manager__preview-modal__container__sticker-grid__cell"
>
<img
className="module-sticker-manager__preview-modal__container__sticker-grid__cell__image"
src={url}
alt={pack.title}
/>
{renderBody(props)}
{pack && pack.status !== 'error' ? (
<div className="module-sticker-manager__preview-modal__container__meta-overlay">
<div className="module-sticker-manager__preview-modal__container__meta-overlay__info">
<h3 className="module-sticker-manager__preview-modal__container__meta-overlay__info__title">
{pack.title}
{pack.isBlessed ? (
<span className="module-sticker-manager__preview-modal__container__meta-overlay__info__blessed-icon" />
) : null}
</h3>
<h4 className="module-sticker-manager__preview-modal__container__meta-overlay__info__author">
{pack.author}
</h4>
</div>
<div className="module-sticker-manager__preview-modal__container__meta-overlay__install">
{pack.status === 'pending' ? (
<Spinner size="mini" />
) : (
<StickerPackInstallButton
ref={focusRef}
installed={isInstalled}
i18n={i18n}
onClick={handleToggleInstall}
blue={true}
/>
)}
</div>
))}
</div>
<div className="module-sticker-manager__preview-modal__container__meta-overlay">
<div className="module-sticker-manager__preview-modal__container__meta-overlay__info">
<h3 className="module-sticker-manager__preview-modal__container__meta-overlay__info__title">
{pack.title}
{pack.isBlessed ? (
<span className="module-sticker-manager__preview-modal__container__meta-overlay__info__blessed-icon" />
) : null}
</h3>
<h4 className="module-sticker-manager__preview-modal__container__meta-overlay__info__author">
{pack.author}
</h4>
</div>
<div className="module-sticker-manager__preview-modal__container__meta-overlay__install">
<StickerPackInstallButton
ref={focusRef}
installed={isInstalled}
i18n={i18n}
onClick={handleToggleInstall}
blue={true}
/>
</div>
</div>
) : null}
</div>
)}
</div>,

View File

@ -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<StickerDBType>;
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<StickerType>;
readonly stickerCount: number;
};
@ -103,7 +121,7 @@ type ClearInstalledStickerPackAction = {
type UninstallStickerPackPayloadType = {
packId: string;
status: 'advertised';
status: 'downloaded';
installedAt: null;
recentStickers: Array<RecentStickerType>;
};
@ -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<UninstallStickerPackPayloadType> {
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<StickerPackDBType>
): 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: {

View File

@ -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',

View File

@ -20,13 +20,14 @@ import {
StickersStateType,
StickerType,
} from '../ducks/stickers';
import { getStickersPath } from './user';
import { getStickersPath, getTempPath } from './user';
const getSticker = (
packs: Dictionary<StickerPackDBType>,
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<StickerPackDBType>,
blessedPacks: Dictionary<boolean>,
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<boolean>,
stickersPath: string
stickersPath: string,
tempPath: string
): Array<StickerPackType> => {
const list = filter(packs, packFilter);
const sorted = orderBy<StickerPackDBType>(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<StickerPackType>;
};
const getStickers = (state: StateType) => state.stickers;
@ -121,14 +126,16 @@ export const getRecentStickers = createSelector(
getRecents,
getPacks,
getStickersPath,
getTempPath,
(
recents: Array<RecentStickerType>,
packs: Dictionary<StickerPackDBType>,
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<StickerPackDBType>,
blessedPacks: Dictionary<boolean>,
stickersPath: string
stickersPath: string,
tempPath: string
): Array<StickerPackType> => {
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<StickerPackDBType>,
blessedPacks: Dictionary<boolean>,
stickersPath: string
stickersPath: string,
tempPath: string
): Array<StickerPackType> => {
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<StickerPackDBType>,
blessedPacks: Dictionary<boolean>,
stickersPath: string
stickersPath: string,
tempPath: string
): Array<StickerPackType> => {
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<StickerPackDBType>,
blessedPacks: Dictionary<boolean>,
stickersPath: string,
tempPath: string
): Array<StickerPackType> => {
return filterAndTransformPacks(
packs,
pack => !blessedPacks[pack.id] && pack.status === 'known',
pack => pack.createdAt,
blessedPacks,
stickersPath,
tempPath
);
}
);

View File

@ -31,3 +31,8 @@ export const getStickersPath = createSelector(
getUser,
(state: UserStateType): string => state.stickersPath
);
export const getTempPath = createSelector(
getUser,
(state: UserStateType): string => state.tempPath
);

View File

@ -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,

View File

@ -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),
};
};

View File

@ -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),
};
};

View File

@ -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<null> {
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<T>(options: any): T {
const parser = createParser({ options });
const cliOptions = parser.parse(process.argv);

View File

@ -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') {

View File

@ -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"
}
]
]