Stickers
Co-authored-by: scott@signal.org Co-authored-by: ken@signal.org
|
@ -1730,5 +1730,109 @@
|
||||||
"example": "Alice, Bob"
|
"example": "Alice, Bob"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"message--getNotificationText--stickers": {
|
||||||
|
"message": "Sticker message",
|
||||||
|
"description":
|
||||||
|
"Shown in notifications and in the left pane instead of sticker image."
|
||||||
|
},
|
||||||
|
"stickers--toast--InstallFailed": {
|
||||||
|
"message": "Sticker pack could not be installed",
|
||||||
|
"description":
|
||||||
|
"Shown in a toast if the user attempts to install a sticker pack and it fails"
|
||||||
|
},
|
||||||
|
"stickers--StickerManager--InstalledPacks": {
|
||||||
|
"message": "Installed Stickers",
|
||||||
|
"description":
|
||||||
|
"Shown in the sticker pack manager above your installed sticker packs."
|
||||||
|
},
|
||||||
|
"stickers--StickerManager--InstalledPacks--Empty": {
|
||||||
|
"message": "No stickers installed",
|
||||||
|
"description":
|
||||||
|
"Shown in the sticker pack manager when you don't have any installed sticker packs."
|
||||||
|
},
|
||||||
|
"stickers--StickerManager--BlessedPacks": {
|
||||||
|
"message": "Signal Artist Series",
|
||||||
|
"description":
|
||||||
|
"Shown in the sticker pack manager above the default sticker packs."
|
||||||
|
},
|
||||||
|
"stickers--StickerManager--BlessedPacks--Empty": {
|
||||||
|
"message": "No Signal Artist stickers available",
|
||||||
|
"description":
|
||||||
|
"Shown in the sticker pack manager when there are no blessed sticker packs available."
|
||||||
|
},
|
||||||
|
"stickers--StickerManager--ReceivedPacks": {
|
||||||
|
"message": "Stickers You Received",
|
||||||
|
"description":
|
||||||
|
"Shown in the sticker pack manager above sticker packs which you have received in messages."
|
||||||
|
},
|
||||||
|
"stickers--StickerManager--ReceivedPacks--Empty": {
|
||||||
|
"message": "Stickers from incoming messages will appear here",
|
||||||
|
"description":
|
||||||
|
"Shown in the sticker pack manager when you have not received any sticker packs in messages."
|
||||||
|
},
|
||||||
|
"stickers--StickerManager--Install": {
|
||||||
|
"message": "Install",
|
||||||
|
"description":
|
||||||
|
"Shown in the sticker pack manager next to sticker packs which can be installed."
|
||||||
|
},
|
||||||
|
"stickers--StickerManager--Uninstall": {
|
||||||
|
"message": "Uninstall",
|
||||||
|
"description":
|
||||||
|
"Shown in the sticker pack manager next to sticker packs which are already installed."
|
||||||
|
},
|
||||||
|
"stickers--StickerManager--UninstallWarning": {
|
||||||
|
"message":
|
||||||
|
"You may not be able to re-install this sticker pack if you no longer have the source message.",
|
||||||
|
"description":
|
||||||
|
"Shown in the sticker pack manager next to sticker packs which are already installed."
|
||||||
|
},
|
||||||
|
"stickers--StickerManager--Introduction--Title": {
|
||||||
|
"message": "Introducing Stickers",
|
||||||
|
"description":
|
||||||
|
"Shown as the title on a tooltip when the user upgrades to a version of Signal supporting stickers."
|
||||||
|
},
|
||||||
|
"stickers--StickerManager--Introduction--Body": {
|
||||||
|
"message": "Why use words when you can use stickers?",
|
||||||
|
"description":
|
||||||
|
"Shown as the body on a tooltip when the user upgrades to a version of Signal supporting stickers."
|
||||||
|
},
|
||||||
|
"stickers--StickerPicker--DownloadError": {
|
||||||
|
"message": "Some stickers could not be downloaded.",
|
||||||
|
"description":
|
||||||
|
"Shown in the sticker picker when one or more stickers could not be downloaded."
|
||||||
|
},
|
||||||
|
"stickers--StickerPicker--DownloadPending": {
|
||||||
|
"message": "Installing sticker pack...",
|
||||||
|
"description":
|
||||||
|
"Shown in the sticker picker when one or more stickers are still downloading."
|
||||||
|
},
|
||||||
|
"stickers--StickerPicker--Empty": {
|
||||||
|
"message": "No stickers found",
|
||||||
|
"description":
|
||||||
|
"Shown in the sticker picker when there are no stickers to show."
|
||||||
|
},
|
||||||
|
"stickers--StickerPicker--Hint": {
|
||||||
|
"message": "New sticker packs from your messages are available to install",
|
||||||
|
"description":
|
||||||
|
"Shown in the sticker picker the first time you have received new packs you can install."
|
||||||
|
},
|
||||||
|
"stickers--StickerPicker--NoPacks": {
|
||||||
|
"message": "No sticker packs found",
|
||||||
|
"description":
|
||||||
|
"Shown in the sticker picker when there are no installed sticker packs."
|
||||||
|
},
|
||||||
|
"stickers--StickerPicker--NoRecents": {
|
||||||
|
"message": "Recently used stickers will appear here.",
|
||||||
|
"description":
|
||||||
|
"Shown in the sticker picker when there are no recent stickers to show."
|
||||||
|
},
|
||||||
|
"stickers--StickerPreview--Title": {
|
||||||
|
"message": "Sticker Pack",
|
||||||
|
"description": "The title that appears in the sticker pack preview modal."
|
||||||
|
},
|
||||||
|
"confirmation-dialog--Cancel": {
|
||||||
|
"message": "Cancel",
|
||||||
|
"description": "Appears on the cancel button in confirmation dialogs."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,7 @@ module.exports = {
|
||||||
let initialized = false;
|
let initialized = false;
|
||||||
|
|
||||||
const ERASE_ATTACHMENTS_KEY = 'erase-attachments';
|
const ERASE_ATTACHMENTS_KEY = 'erase-attachments';
|
||||||
|
const ERASE_STICKERS_KEY = 'erase-stickers';
|
||||||
const CLEANUP_ORPHANED_ATTACHMENTS_KEY = 'cleanup-orphaned-attachments';
|
const CLEANUP_ORPHANED_ATTACHMENTS_KEY = 'cleanup-orphaned-attachments';
|
||||||
|
|
||||||
async function initialize({ configDir, cleanupOrphanedAttachments }) {
|
async function initialize({ configDir, cleanupOrphanedAttachments }) {
|
||||||
|
@ -19,12 +20,10 @@ async function initialize({ configDir, cleanupOrphanedAttachments }) {
|
||||||
}
|
}
|
||||||
initialized = true;
|
initialized = true;
|
||||||
|
|
||||||
console.log('Ensure attachments directory exists');
|
|
||||||
await Attachments.ensureDirectory(configDir);
|
|
||||||
|
|
||||||
const attachmentsDir = Attachments.getPath(configDir);
|
const attachmentsDir = Attachments.getPath(configDir);
|
||||||
|
const stickersDir = Attachments.getStickersPath(configDir);
|
||||||
|
|
||||||
ipcMain.on(ERASE_ATTACHMENTS_KEY, async event => {
|
ipcMain.on(ERASE_ATTACHMENTS_KEY, event => {
|
||||||
try {
|
try {
|
||||||
rimraf.sync(attachmentsDir);
|
rimraf.sync(attachmentsDir);
|
||||||
event.sender.send(`${ERASE_ATTACHMENTS_KEY}-done`);
|
event.sender.send(`${ERASE_ATTACHMENTS_KEY}-done`);
|
||||||
|
@ -35,6 +34,17 @@ async function initialize({ configDir, cleanupOrphanedAttachments }) {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ipcMain.on(ERASE_STICKERS_KEY, event => {
|
||||||
|
try {
|
||||||
|
rimraf.sync(stickersDir);
|
||||||
|
event.sender.send(`${ERASE_STICKERS_KEY}-done`);
|
||||||
|
} catch (error) {
|
||||||
|
const errorForDisplay = error && error.stack ? error.stack : error;
|
||||||
|
console.log(`erase stickers error: ${errorForDisplay}`);
|
||||||
|
event.sender.send(`${ERASE_STICKERS_KEY}-done`, error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
ipcMain.on(CLEANUP_ORPHANED_ATTACHMENTS_KEY, async event => {
|
ipcMain.on(CLEANUP_ORPHANED_ATTACHMENTS_KEY, async event => {
|
||||||
try {
|
try {
|
||||||
await cleanupOrphanedAttachments();
|
await cleanupOrphanedAttachments();
|
||||||
|
|
|
@ -8,6 +8,7 @@ const toArrayBuffer = require('to-arraybuffer');
|
||||||
const { map, isArrayBuffer, isString } = require('lodash');
|
const { map, isArrayBuffer, isString } = require('lodash');
|
||||||
|
|
||||||
const PATH = 'attachments.noindex';
|
const PATH = 'attachments.noindex';
|
||||||
|
const STICKER_PATH = 'stickers.noindex';
|
||||||
|
|
||||||
exports.getAllAttachments = async userDataPath => {
|
exports.getAllAttachments = async userDataPath => {
|
||||||
const dir = exports.getPath(userDataPath);
|
const dir = exports.getPath(userDataPath);
|
||||||
|
@ -17,6 +18,14 @@ exports.getAllAttachments = async userDataPath => {
|
||||||
return map(files, file => path.relative(dir, file));
|
return map(files, file => path.relative(dir, file));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
exports.getAllStickers = async userDataPath => {
|
||||||
|
const dir = exports.getStickersPath(userDataPath);
|
||||||
|
const pattern = path.join(dir, '**', '*');
|
||||||
|
|
||||||
|
const files = await pify(glob)(pattern, { nodir: true });
|
||||||
|
return map(files, file => path.relative(dir, file));
|
||||||
|
};
|
||||||
|
|
||||||
// getPath :: AbsolutePath -> AbsolutePath
|
// getPath :: AbsolutePath -> AbsolutePath
|
||||||
exports.getPath = userDataPath => {
|
exports.getPath = userDataPath => {
|
||||||
if (!isString(userDataPath)) {
|
if (!isString(userDataPath)) {
|
||||||
|
@ -25,12 +34,12 @@ exports.getPath = userDataPath => {
|
||||||
return path.join(userDataPath, PATH);
|
return path.join(userDataPath, PATH);
|
||||||
};
|
};
|
||||||
|
|
||||||
// ensureDirectory :: AbsolutePath -> IO Unit
|
// getStickersPath :: AbsolutePath -> AbsolutePath
|
||||||
exports.ensureDirectory = async userDataPath => {
|
exports.getStickersPath = userDataPath => {
|
||||||
if (!isString(userDataPath)) {
|
if (!isString(userDataPath)) {
|
||||||
throw new TypeError("'userDataPath' must be a string");
|
throw new TypeError("'userDataPath' must be a string");
|
||||||
}
|
}
|
||||||
await fse.ensureDir(exports.getPath(userDataPath));
|
return path.join(userDataPath, STICKER_PATH);
|
||||||
};
|
};
|
||||||
|
|
||||||
// createReader :: AttachmentsPath ->
|
// createReader :: AttachmentsPath ->
|
||||||
|
@ -56,6 +65,30 @@ exports.createReader = root => {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
exports.copyIntoAttachmentsDirectory = root => {
|
||||||
|
if (!isString(root)) {
|
||||||
|
throw new TypeError("'root' must be a path");
|
||||||
|
}
|
||||||
|
|
||||||
|
return async sourcePath => {
|
||||||
|
if (!isString(sourcePath)) {
|
||||||
|
throw new TypeError('sourcePath must be a string');
|
||||||
|
}
|
||||||
|
|
||||||
|
const name = exports.createName();
|
||||||
|
const relativePath = exports.getRelativePath(name);
|
||||||
|
const absolutePath = path.join(root, relativePath);
|
||||||
|
const normalized = path.normalize(absolutePath);
|
||||||
|
if (!normalized.startsWith(root)) {
|
||||||
|
throw new Error('Invalid relative path');
|
||||||
|
}
|
||||||
|
|
||||||
|
await fse.ensureFile(normalized);
|
||||||
|
await fse.copy(sourcePath, normalized);
|
||||||
|
return relativePath;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
// createWriterForNew :: AttachmentsPath ->
|
// createWriterForNew :: AttachmentsPath ->
|
||||||
// ArrayBuffer ->
|
// ArrayBuffer ->
|
||||||
// IO (Promise RelativePath)
|
// IO (Promise RelativePath)
|
||||||
|
@ -142,6 +175,20 @@ exports.deleteAll = async ({ userDataPath, attachments }) => {
|
||||||
console.log(`deleteAll: deleted ${attachments.length} files`);
|
console.log(`deleteAll: deleted ${attachments.length} files`);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
exports.deleteAllStickers = async ({ userDataPath, stickers }) => {
|
||||||
|
const deleteFromDisk = exports.createDeleter(
|
||||||
|
exports.getStickersPath(userDataPath)
|
||||||
|
);
|
||||||
|
|
||||||
|
for (let index = 0, max = stickers.length; index < max; index += 1) {
|
||||||
|
const file = stickers[index];
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
await deleteFromDisk(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`deleteAllStickers: deleted ${stickers.length} files`);
|
||||||
|
};
|
||||||
|
|
||||||
// createName :: Unit -> IO String
|
// createName :: Unit -> IO String
|
||||||
exports.createName = () => {
|
exports.createName = () => {
|
||||||
const buffer = crypto.randomBytes(32);
|
const buffer = crypto.randomBytes(32);
|
||||||
|
|
527
app/sql.js
|
@ -1,4 +1,4 @@
|
||||||
const path = require('path');
|
const { join } = require('path');
|
||||||
const mkdirp = require('mkdirp');
|
const mkdirp = require('mkdirp');
|
||||||
const rimraf = require('rimraf');
|
const rimraf = require('rimraf');
|
||||||
const sql = require('@journeyapps/sqlcipher');
|
const sql = require('@journeyapps/sqlcipher');
|
||||||
|
@ -8,7 +8,15 @@ const { remove: removeUserConfig } = require('./user_config');
|
||||||
|
|
||||||
const pify = require('pify');
|
const pify = require('pify');
|
||||||
const uuidv4 = require('uuid/v4');
|
const uuidv4 = require('uuid/v4');
|
||||||
const { map, isObject, isString, fromPairs, forEach, last } = require('lodash');
|
const {
|
||||||
|
forEach,
|
||||||
|
fromPairs,
|
||||||
|
isNumber,
|
||||||
|
isObject,
|
||||||
|
isString,
|
||||||
|
last,
|
||||||
|
map,
|
||||||
|
} = require('lodash');
|
||||||
|
|
||||||
// To get long stack traces
|
// To get long stack traces
|
||||||
// https://github.com/mapbox/node-sqlite3/wiki/API#sqlite3verbose
|
// https://github.com/mapbox/node-sqlite3/wiki/API#sqlite3verbose
|
||||||
|
@ -104,6 +112,17 @@ module.exports = {
|
||||||
removeAttachmentDownloadJob,
|
removeAttachmentDownloadJob,
|
||||||
removeAllAttachmentDownloadJobs,
|
removeAllAttachmentDownloadJobs,
|
||||||
|
|
||||||
|
createOrUpdateStickerPack,
|
||||||
|
updateStickerPackStatus,
|
||||||
|
createOrUpdateSticker,
|
||||||
|
updateStickerLastUsed,
|
||||||
|
addStickerPackReference,
|
||||||
|
deleteStickerPackReference,
|
||||||
|
deleteStickerPack,
|
||||||
|
getAllStickerPacks,
|
||||||
|
getAllStickers,
|
||||||
|
getRecentStickers,
|
||||||
|
|
||||||
removeAll,
|
removeAll,
|
||||||
removeAllConfiguration,
|
removeAllConfiguration,
|
||||||
|
|
||||||
|
@ -112,6 +131,7 @@ module.exports = {
|
||||||
getMessagesWithFileAttachments,
|
getMessagesWithFileAttachments,
|
||||||
|
|
||||||
removeKnownAttachments,
|
removeKnownAttachments,
|
||||||
|
removeKnownStickers,
|
||||||
};
|
};
|
||||||
|
|
||||||
function generateUUID() {
|
function generateUUID() {
|
||||||
|
@ -179,6 +199,9 @@ async function setupSQLCipher(instance, { key }) {
|
||||||
|
|
||||||
// https://www.zetetic.net/sqlcipher/sqlcipher-api/#key
|
// https://www.zetetic.net/sqlcipher/sqlcipher-api/#key
|
||||||
await instance.run(`PRAGMA key = "x'${key}'";`);
|
await instance.run(`PRAGMA key = "x'${key}'";`);
|
||||||
|
|
||||||
|
// Because foreign key support is not enabled by default!
|
||||||
|
await instance.run('PRAGMA foreign_keys = ON;');
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateToSchemaVersion1(currentVersion, instance) {
|
async function updateToSchemaVersion1(currentVersion, instance) {
|
||||||
|
@ -635,6 +658,83 @@ async function updateToSchemaVersion11(currentVersion, instance) {
|
||||||
console.log('updateToSchemaVersion11: success!');
|
console.log('updateToSchemaVersion11: success!');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function updateToSchemaVersion12(currentVersion, instance) {
|
||||||
|
if (currentVersion >= 12) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('updateToSchemaVersion12: starting...');
|
||||||
|
await instance.run('BEGIN TRANSACTION;');
|
||||||
|
|
||||||
|
await instance.run(`CREATE TABLE sticker_packs(
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
key TEXT NOT NULL,
|
||||||
|
|
||||||
|
author STRING,
|
||||||
|
coverStickerId INTEGER,
|
||||||
|
createdAt INTEGER,
|
||||||
|
downloadAttempts INTEGER,
|
||||||
|
installedAt INTEGER,
|
||||||
|
lastUsed INTEGER,
|
||||||
|
status STRING,
|
||||||
|
stickerCount INTEGER,
|
||||||
|
title STRING
|
||||||
|
);`);
|
||||||
|
|
||||||
|
await instance.run(`CREATE TABLE stickers(
|
||||||
|
id INTEGER NOT NULL,
|
||||||
|
packId TEXT NOT NULL,
|
||||||
|
|
||||||
|
emoji STRING,
|
||||||
|
height INTEGER,
|
||||||
|
isCoverOnly INTEGER,
|
||||||
|
lastUsed INTEGER,
|
||||||
|
path STRING,
|
||||||
|
width INTEGER,
|
||||||
|
|
||||||
|
PRIMARY KEY (id, packId),
|
||||||
|
CONSTRAINT stickers_fk
|
||||||
|
FOREIGN KEY (packId)
|
||||||
|
REFERENCES sticker_packs(id)
|
||||||
|
ON DELETE CASCADE
|
||||||
|
);`);
|
||||||
|
|
||||||
|
await instance.run(`CREATE INDEX stickers_recents
|
||||||
|
ON stickers (
|
||||||
|
lastUsed
|
||||||
|
) WHERE lastUsed IS NOT NULL;`);
|
||||||
|
|
||||||
|
await instance.run(`CREATE TABLE sticker_references(
|
||||||
|
messageId STRING,
|
||||||
|
packId TEXT,
|
||||||
|
CONSTRAINT sticker_references_fk
|
||||||
|
FOREIGN KEY(packId)
|
||||||
|
REFERENCES sticker_packs(id)
|
||||||
|
ON DELETE CASCADE
|
||||||
|
);`);
|
||||||
|
|
||||||
|
await instance.run('PRAGMA schema_version = 12;');
|
||||||
|
await instance.run('COMMIT TRANSACTION;');
|
||||||
|
console.log('updateToSchemaVersion12: success!');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateToSchemaVersion13(currentVersion, instance) {
|
||||||
|
if (currentVersion >= 13) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('updateToSchemaVersion13: starting...');
|
||||||
|
await instance.run('BEGIN TRANSACTION;');
|
||||||
|
|
||||||
|
await instance.run(
|
||||||
|
'ALTER TABLE sticker_packs ADD COLUMN attemptedStatus STRING;'
|
||||||
|
);
|
||||||
|
|
||||||
|
await instance.run('PRAGMA schema_version = 13;');
|
||||||
|
await instance.run('COMMIT TRANSACTION;');
|
||||||
|
console.log('updateToSchemaVersion13: success!');
|
||||||
|
}
|
||||||
|
|
||||||
const SCHEMA_VERSIONS = [
|
const SCHEMA_VERSIONS = [
|
||||||
updateToSchemaVersion1,
|
updateToSchemaVersion1,
|
||||||
updateToSchemaVersion2,
|
updateToSchemaVersion2,
|
||||||
|
@ -647,6 +747,8 @@ const SCHEMA_VERSIONS = [
|
||||||
updateToSchemaVersion9,
|
updateToSchemaVersion9,
|
||||||
updateToSchemaVersion10,
|
updateToSchemaVersion10,
|
||||||
updateToSchemaVersion11,
|
updateToSchemaVersion11,
|
||||||
|
updateToSchemaVersion12,
|
||||||
|
updateToSchemaVersion13,
|
||||||
];
|
];
|
||||||
|
|
||||||
async function updateSchema(instance) {
|
async function updateSchema(instance) {
|
||||||
|
@ -689,12 +791,12 @@ async function initialize({ configDir, key, messages }) {
|
||||||
throw new Error('initialize: message is required!');
|
throw new Error('initialize: message is required!');
|
||||||
}
|
}
|
||||||
|
|
||||||
indexedDBPath = path.join(configDir, 'IndexedDB');
|
indexedDBPath = join(configDir, 'IndexedDB');
|
||||||
|
|
||||||
const dbDir = path.join(configDir, 'sql');
|
const dbDir = join(configDir, 'sql');
|
||||||
mkdirp.sync(dbDir);
|
mkdirp.sync(dbDir);
|
||||||
|
|
||||||
filePath = path.join(dbDir, 'db.sqlite');
|
filePath = join(dbDir, 'db.sqlite');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const sqlInstance = await openDatabase(filePath);
|
const sqlInstance = await openDatabase(filePath);
|
||||||
|
@ -773,7 +875,7 @@ async function removeIndexedDBFiles() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const pattern = path.join(indexedDBPath, '*.leveldb');
|
const pattern = join(indexedDBPath, '*.leveldb');
|
||||||
rimraf.sync(pattern);
|
rimraf.sync(pattern);
|
||||||
indexedDBPath = null;
|
indexedDBPath = null;
|
||||||
}
|
}
|
||||||
|
@ -1507,6 +1609,7 @@ async function getOutgoingWithoutExpiresAt() {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getNextExpiringMessage() {
|
async function getNextExpiringMessage() {
|
||||||
|
// Note: we avoid 'IS NOT NULL' here because it does seem to bypass our index
|
||||||
const rows = await db.all(`
|
const rows = await db.all(`
|
||||||
SELECT json FROM messages
|
SELECT json FROM messages
|
||||||
WHERE expires_at > 0
|
WHERE expires_at > 0
|
||||||
|
@ -1658,6 +1761,8 @@ async function removeAllUnprocessed() {
|
||||||
await db.run('DELETE FROM unprocessed;');
|
await db.run('DELETE FROM unprocessed;');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Attachment Downloads
|
||||||
|
|
||||||
const ATTACHMENT_DOWNLOADS_TABLE = 'attachment_downloads';
|
const ATTACHMENT_DOWNLOADS_TABLE = 'attachment_downloads';
|
||||||
async function getNextAttachmentDownloadJobs(limit, options = {}) {
|
async function getNextAttachmentDownloadJobs(limit, options = {}) {
|
||||||
const timestamp = options.timestamp || Date.now();
|
const timestamp = options.timestamp || Date.now();
|
||||||
|
@ -1724,6 +1829,359 @@ async function removeAllAttachmentDownloadJobs() {
|
||||||
return removeAllFromTable(ATTACHMENT_DOWNLOADS_TABLE);
|
return removeAllFromTable(ATTACHMENT_DOWNLOADS_TABLE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Stickers
|
||||||
|
|
||||||
|
async function createOrUpdateStickerPack(pack) {
|
||||||
|
const {
|
||||||
|
attemptedStatus,
|
||||||
|
author,
|
||||||
|
coverStickerId,
|
||||||
|
createdAt,
|
||||||
|
downloadAttempts,
|
||||||
|
id,
|
||||||
|
installedAt,
|
||||||
|
key,
|
||||||
|
lastUsed,
|
||||||
|
status,
|
||||||
|
stickerCount,
|
||||||
|
title,
|
||||||
|
} = pack;
|
||||||
|
if (!id) {
|
||||||
|
throw new Error(
|
||||||
|
'createOrUpdateStickerPack: Provided data did not have a truthy id'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await db.run(
|
||||||
|
`INSERT OR REPLACE INTO sticker_packs (
|
||||||
|
attemptedStatus,
|
||||||
|
author,
|
||||||
|
coverStickerId,
|
||||||
|
createdAt,
|
||||||
|
downloadAttempts,
|
||||||
|
id,
|
||||||
|
installedAt,
|
||||||
|
key,
|
||||||
|
lastUsed,
|
||||||
|
status,
|
||||||
|
stickerCount,
|
||||||
|
title
|
||||||
|
) values (
|
||||||
|
$attemptedStatus,
|
||||||
|
$author,
|
||||||
|
$coverStickerId,
|
||||||
|
$createdAt,
|
||||||
|
$downloadAttempts,
|
||||||
|
$id,
|
||||||
|
$installedAt,
|
||||||
|
$key,
|
||||||
|
$lastUsed,
|
||||||
|
$status,
|
||||||
|
$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,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
async function updateStickerPackStatus(id, status, options) {
|
||||||
|
// Strange, but an undefined parameter gets coerced into null via ipc
|
||||||
|
const timestamp = (options || {}).timestamp || Date.now();
|
||||||
|
const installedAt = status === 'installed' ? timestamp : null;
|
||||||
|
|
||||||
|
await db.run(
|
||||||
|
`UPDATE sticker_packs
|
||||||
|
SET status = $status, installedAt = $installedAt
|
||||||
|
WHERE id = $id;
|
||||||
|
)`,
|
||||||
|
{
|
||||||
|
$id: id,
|
||||||
|
$status: status,
|
||||||
|
$installedAt: installedAt,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
async function createOrUpdateSticker(sticker) {
|
||||||
|
const {
|
||||||
|
emoji,
|
||||||
|
height,
|
||||||
|
id,
|
||||||
|
isCoverOnly,
|
||||||
|
lastUsed,
|
||||||
|
packId,
|
||||||
|
path,
|
||||||
|
width,
|
||||||
|
} = sticker;
|
||||||
|
if (!isNumber(id)) {
|
||||||
|
throw new Error(
|
||||||
|
'createOrUpdateSticker: Provided data did not have a numeric id'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!packId) {
|
||||||
|
throw new Error(
|
||||||
|
'createOrUpdateSticker: Provided data did not have a truthy id'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await db.run(
|
||||||
|
`INSERT OR REPLACE INTO stickers (
|
||||||
|
emoji,
|
||||||
|
height,
|
||||||
|
id,
|
||||||
|
isCoverOnly,
|
||||||
|
lastUsed,
|
||||||
|
packId,
|
||||||
|
path,
|
||||||
|
width
|
||||||
|
) values (
|
||||||
|
$emoji,
|
||||||
|
$height,
|
||||||
|
$id,
|
||||||
|
$isCoverOnly,
|
||||||
|
$lastUsed,
|
||||||
|
$packId,
|
||||||
|
$path,
|
||||||
|
$width
|
||||||
|
)`,
|
||||||
|
{
|
||||||
|
$emoji: emoji,
|
||||||
|
$height: height,
|
||||||
|
$id: id,
|
||||||
|
$isCoverOnly: isCoverOnly,
|
||||||
|
$lastUsed: lastUsed,
|
||||||
|
$packId: packId,
|
||||||
|
$path: path,
|
||||||
|
$width: width,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
async function updateStickerLastUsed(packId, stickerId, lastUsed) {
|
||||||
|
await db.run(
|
||||||
|
`UPDATE stickers
|
||||||
|
SET lastUsed = $lastUsed
|
||||||
|
WHERE id = $id AND packId = $packId;`,
|
||||||
|
{
|
||||||
|
$id: stickerId,
|
||||||
|
$packId: packId,
|
||||||
|
$lastUsed: lastUsed,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
await db.run(
|
||||||
|
`UPDATE sticker_packs
|
||||||
|
SET lastUsed = $lastUsed
|
||||||
|
WHERE id = $id;`,
|
||||||
|
{
|
||||||
|
$id: packId,
|
||||||
|
$lastUsed: lastUsed,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
async function addStickerPackReference(messageId, packId) {
|
||||||
|
if (!messageId) {
|
||||||
|
throw new Error(
|
||||||
|
'addStickerPackReference: Provided data did not have a truthy messageId'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!packId) {
|
||||||
|
throw new Error(
|
||||||
|
'addStickerPackReference: Provided data did not have a truthy packId'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await db.run(
|
||||||
|
`INSERT OR REPLACE INTO sticker_references (
|
||||||
|
messageId,
|
||||||
|
packId
|
||||||
|
) values (
|
||||||
|
$messageId,
|
||||||
|
$packId
|
||||||
|
)`,
|
||||||
|
{
|
||||||
|
$messageId: messageId,
|
||||||
|
$packId: packId,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
async function deleteStickerPackReference(messageId, packId) {
|
||||||
|
if (!messageId) {
|
||||||
|
throw new Error(
|
||||||
|
'addStickerPackReference: Provided data did not have a truthy messageId'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!packId) {
|
||||||
|
throw new Error(
|
||||||
|
'addStickerPackReference: Provided data did not have a truthy packId'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// We use an immediate transaction here to immediately acquire an exclusive lock,
|
||||||
|
// which would normally only happen when we did our first write.
|
||||||
|
|
||||||
|
// We need this to ensure that our five queries are all atomic, with no other changes
|
||||||
|
// happening while we do it:
|
||||||
|
// 1. Delete our target messageId/packId references
|
||||||
|
// 2. Check the number of references still pointing at packId
|
||||||
|
// 3. If that number is zero, get pack from sticker_packs database
|
||||||
|
// 4. If it's not installed, then grab all of its sticker paths
|
||||||
|
// 5. If it's not installed, then sticker pack (which cascades to all stickers and
|
||||||
|
// references)
|
||||||
|
await db.run('BEGIN IMMEDIATE TRANSACTION;');
|
||||||
|
|
||||||
|
await db.run(
|
||||||
|
`DELETE FROM sticker_references
|
||||||
|
WHERE messageId = $messageId AND packId = $packId;`,
|
||||||
|
{
|
||||||
|
$messageId: messageId,
|
||||||
|
$packId: packId,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const countRow = await db.get(
|
||||||
|
`SELECT count(*) FROM sticker_references
|
||||||
|
WHERE packId = $packId;`,
|
||||||
|
{ $packId: packId }
|
||||||
|
);
|
||||||
|
if (!countRow) {
|
||||||
|
throw new Error(
|
||||||
|
'deleteStickerPackReference: Unable to get count of references'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const count = countRow['count(*)'];
|
||||||
|
if (count > 0) {
|
||||||
|
await db.run('COMMIT TRANSACTION');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const packRow = await db.get(
|
||||||
|
`SELECT status FROM sticker_packs
|
||||||
|
WHERE id = $packId;`,
|
||||||
|
{ $packId: packId }
|
||||||
|
);
|
||||||
|
if (!packRow) {
|
||||||
|
console.log('deleteStickerPackReference: did not find referenced pack');
|
||||||
|
await db.run('COMMIT TRANSACTION');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const { status } = packRow;
|
||||||
|
|
||||||
|
if (status === 'installed') {
|
||||||
|
await db.run('COMMIT TRANSACTION');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const stickerPathRows = await db.all(
|
||||||
|
`SELECT path FROM stickers
|
||||||
|
WHERE packId = $packId;`,
|
||||||
|
{
|
||||||
|
$packId: packId,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
await db.run(
|
||||||
|
`DELETE FROM sticker_packs
|
||||||
|
WHERE id = $packId;`,
|
||||||
|
{ $packId: packId }
|
||||||
|
);
|
||||||
|
|
||||||
|
await db.run('COMMIT TRANSACTION;');
|
||||||
|
|
||||||
|
return (stickerPathRows || []).map(row => row.path);
|
||||||
|
} catch (error) {
|
||||||
|
await db.run('ROLLBACK;');
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async function deleteStickerPack(packId) {
|
||||||
|
if (!packId) {
|
||||||
|
throw new Error(
|
||||||
|
'deleteStickerPack: Provided data did not have a truthy packId'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// We use an immediate transaction here to immediately acquire an exclusive lock,
|
||||||
|
// which would normally only happen when we did our first write.
|
||||||
|
|
||||||
|
// We need this to ensure that our two queries are atomic, with no other changes
|
||||||
|
// happening while we do it:
|
||||||
|
// 1. Grab all of target pack's sticker paths
|
||||||
|
// 2. Delete sticker pack (which cascades to all stickers and references)
|
||||||
|
await db.run('BEGIN IMMEDIATE TRANSACTION;');
|
||||||
|
|
||||||
|
const stickerPathRows = await db.all(
|
||||||
|
`SELECT path FROM stickers
|
||||||
|
WHERE packId = $packId;`,
|
||||||
|
{
|
||||||
|
$packId: packId,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
await db.run(
|
||||||
|
`DELETE FROM sticker_packs
|
||||||
|
WHERE id = $packId;`,
|
||||||
|
{ $packId: packId }
|
||||||
|
);
|
||||||
|
|
||||||
|
await db.run('COMMIT TRANSACTION;');
|
||||||
|
|
||||||
|
return (stickerPathRows || []).map(row => row.path);
|
||||||
|
} catch (error) {
|
||||||
|
await db.run('ROLLBACK;');
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async function getStickerCount() {
|
||||||
|
const row = await db.get('SELECT count(*) from stickers;');
|
||||||
|
|
||||||
|
if (!row) {
|
||||||
|
throw new Error('getStickerCount: Unable to get count of stickers');
|
||||||
|
}
|
||||||
|
|
||||||
|
return row['count(*)'];
|
||||||
|
}
|
||||||
|
async function getAllStickerPacks() {
|
||||||
|
const rows = await db.all(
|
||||||
|
`SELECT * FROM sticker_packs
|
||||||
|
ORDER BY installedAt DESC, createdAt DESC`
|
||||||
|
);
|
||||||
|
|
||||||
|
return rows || [];
|
||||||
|
}
|
||||||
|
async function getAllStickers() {
|
||||||
|
const rows = await db.all(
|
||||||
|
`SELECT * FROM stickers
|
||||||
|
ORDER BY packId ASC, id ASC`
|
||||||
|
);
|
||||||
|
|
||||||
|
return rows || [];
|
||||||
|
}
|
||||||
|
async function getRecentStickers({ limit } = {}) {
|
||||||
|
// Note: we avoid 'IS NOT NULL' here because it does seem to bypass our index
|
||||||
|
const rows = await db.all(
|
||||||
|
`SELECT stickers.* FROM stickers
|
||||||
|
JOIN sticker_packs on stickers.packId = sticker_packs.id
|
||||||
|
WHERE stickers.lastUsed > 0 AND sticker_packs.status = 'installed'
|
||||||
|
ORDER BY stickers.lastUsed DESC
|
||||||
|
LIMIT $limit`,
|
||||||
|
{
|
||||||
|
$limit: limit || 24,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return rows || [];
|
||||||
|
}
|
||||||
|
|
||||||
// All data in database
|
// All data in database
|
||||||
async function removeAll() {
|
async function removeAll() {
|
||||||
let promise;
|
let promise;
|
||||||
|
@ -1741,6 +2199,9 @@ async function removeAll() {
|
||||||
db.run('DELETE FROM unprocessed;'),
|
db.run('DELETE FROM unprocessed;'),
|
||||||
db.run('DELETE FROM attachment_downloads;'),
|
db.run('DELETE FROM attachment_downloads;'),
|
||||||
db.run('DELETE FROM messages_fts;'),
|
db.run('DELETE FROM messages_fts;'),
|
||||||
|
db.run('DELETE FROM stickers;'),
|
||||||
|
db.run('DELETE FROM sticker_packs;'),
|
||||||
|
db.run('DELETE FROM sticker_references;'),
|
||||||
db.run('COMMIT TRANSACTION;'),
|
db.run('COMMIT TRANSACTION;'),
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
@ -1818,7 +2279,7 @@ async function getMessagesWithFileAttachments(conversationId, { limit }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function getExternalFilesForMessage(message) {
|
function getExternalFilesForMessage(message) {
|
||||||
const { attachments, contact, quote, preview } = message;
|
const { attachments, contact, quote, preview, sticker } = message;
|
||||||
const files = [];
|
const files = [];
|
||||||
|
|
||||||
forEach(attachments, attachment => {
|
forEach(attachments, attachment => {
|
||||||
|
@ -1866,6 +2327,14 @@ function getExternalFilesForMessage(message) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (sticker && sticker.data && sticker.data.path) {
|
||||||
|
files.push(sticker.data.path);
|
||||||
|
|
||||||
|
if (sticker.data.thumbnail && sticker.data.thumbnail.path) {
|
||||||
|
files.push(sticker.data.thumbnail.path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return files;
|
return files;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1972,3 +2441,47 @@ async function removeKnownAttachments(allAttachments) {
|
||||||
|
|
||||||
return Object.keys(lookup);
|
return Object.keys(lookup);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function removeKnownStickers(allStickers) {
|
||||||
|
const lookup = fromPairs(map(allStickers, file => [file, true]));
|
||||||
|
const chunkSize = 50;
|
||||||
|
|
||||||
|
const total = await getStickerCount();
|
||||||
|
console.log(
|
||||||
|
`removeKnownStickers: About to iterate through ${total} stickers`
|
||||||
|
);
|
||||||
|
|
||||||
|
let count = 0;
|
||||||
|
let complete = false;
|
||||||
|
let rowid = 0;
|
||||||
|
|
||||||
|
while (!complete) {
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
const rows = await db.all(
|
||||||
|
`SELECT rowid, path FROM stickers
|
||||||
|
WHERE rowid > $rowid
|
||||||
|
ORDER BY rowid ASC
|
||||||
|
LIMIT $chunkSize;`,
|
||||||
|
{
|
||||||
|
$rowid: rowid,
|
||||||
|
$chunkSize: chunkSize,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const files = map(rows, row => row.path);
|
||||||
|
forEach(files, file => {
|
||||||
|
delete lookup[file];
|
||||||
|
});
|
||||||
|
|
||||||
|
const lastSticker = last(rows);
|
||||||
|
if (lastSticker) {
|
||||||
|
({ rowid } = lastSticker);
|
||||||
|
}
|
||||||
|
complete = rows.length < chunkSize;
|
||||||
|
count += rows.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`removeKnownStickers: Done processing ${count} stickers`);
|
||||||
|
|
||||||
|
return Object.keys(lookup);
|
||||||
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
const electron = require('electron');
|
const electron = require('electron');
|
||||||
|
const Queue = require('p-queue');
|
||||||
const sql = require('./sql');
|
const sql = require('./sql');
|
||||||
const { remove: removeUserConfig } = require('./user_config');
|
const { remove: removeUserConfig } = require('./user_config');
|
||||||
const { remove: removeEphemeralConfig } = require('./ephemeral_config');
|
const { remove: removeEphemeralConfig } = require('./ephemeral_config');
|
||||||
|
@ -14,6 +15,8 @@ let initialized = false;
|
||||||
const SQL_CHANNEL_KEY = 'sql-channel';
|
const SQL_CHANNEL_KEY = 'sql-channel';
|
||||||
const ERASE_SQL_KEY = 'erase-sql-key';
|
const ERASE_SQL_KEY = 'erase-sql-key';
|
||||||
|
|
||||||
|
const queue = new Queue({ concurrency: 1 });
|
||||||
|
|
||||||
function initialize() {
|
function initialize() {
|
||||||
if (initialized) {
|
if (initialized) {
|
||||||
throw new Error('sqlChannels: already initialized!');
|
throw new Error('sqlChannels: already initialized!');
|
||||||
|
@ -29,7 +32,10 @@ function initialize() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await fn(...args);
|
// Note: we queue here to keep multi-query operations atomic. Without it, any
|
||||||
|
// multistage data operation (even within a BEGIN/COMMIT) can become interleaved,
|
||||||
|
// since all requests share one database connection.
|
||||||
|
const result = await queue.add(() => fn(...args));
|
||||||
event.sender.send(`${SQL_CHANNEL_KEY}-done`, jobId, null, result);
|
event.sender.send(`${SQL_CHANNEL_KEY}-done`, jobId, null, result);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorForDisplay = error && error.stack ? error.stack : error;
|
const errorForDisplay = error && error.stack ? error.stack : error;
|
||||||
|
|
|
@ -119,6 +119,7 @@
|
||||||
<div class='flex'>
|
<div class='flex'>
|
||||||
<button class='emoji'></button>
|
<button class='emoji'></button>
|
||||||
<textarea class='send-message' placeholder='{{ send-message }}' rows='1' dir='auto'></textarea>
|
<textarea class='send-message' placeholder='{{ send-message }}' rows='1' dir='auto'></textarea>
|
||||||
|
<div class='sticker-button-placeholder'></div>
|
||||||
<div class='capture-audio'>
|
<div class='capture-audio'>
|
||||||
<button class='microphone'></button>
|
<button class='microphone'></button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -524,7 +525,7 @@
|
||||||
<script type='text/javascript' src='js/rotate_signed_prekey_listener.js'></script>
|
<script type='text/javascript' src='js/rotate_signed_prekey_listener.js'></script>
|
||||||
<script type='text/javascript' src='js/keychange_listener.js'></script>
|
<script type='text/javascript' src='js/keychange_listener.js'></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body class="overflow-hidden">
|
||||||
<div class='app-loading-screen'>
|
<div class='app-loading-screen'>
|
||||||
<div class='content'>
|
<div class='content'>
|
||||||
<img src='images/icon_250.png' height='150'>
|
<img src='images/icon_250.png' height='150'>
|
||||||
|
|
BIN
fixtures/512x515-thumbs-up-lincoln.webp
Normal file
After Width: | Height: | Size: 37 KiB |
BIN
fixtures/kitten-1-64-64.jpg
Normal file
After Width: | Height: | Size: 1.4 KiB |
BIN
fixtures/kitten-2-64-64.jpg
Normal file
After Width: | Height: | Size: 8.6 KiB |
BIN
fixtures/kitten-3-64-64.jpg
Normal file
After Width: | Height: | Size: 9 KiB |
7
images/badge-filled-16.svg
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Generator: Adobe Illustrator 23.0.3, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||||
|
<svg version="1.1" id="Export" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||||
|
viewBox="0 0 16 16" style="enable-background:new 0 0 16 16;" xml:space="preserve">
|
||||||
|
<polygon points="8,1 9.7,3.8 12.9,3 12.2,6.3 15,8 12.2,9.7 12.9,12.9 9.7,12.2 8,15 6.3,12.2 3,12.9 3.8,9.7 1,8 3.8,6.3 3,3
|
||||||
|
6.3,3.8 "/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 488 B |
11
images/check-circle-filled-16.svg
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Generator: Adobe Illustrator 23.0.3, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||||
|
<svg version="1.1" id="Export" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||||
|
viewBox="0 0 16 16" style="enable-background:new 0 0 16 16;" xml:space="preserve">
|
||||||
|
<style type="text/css">
|
||||||
|
.st0{fill:#2090EA;}
|
||||||
|
.st1{fill:#FFFFFF;}
|
||||||
|
</style>
|
||||||
|
<circle class="st0" cx="8" cy="8" r="7"/>
|
||||||
|
<path class="st1" d="M7,11.5c-0.2,0-0.4-0.1-0.5-0.2L3.3,8.1l1.1-1.1L7,9.7l4.6-4.6l1.1,1.1l-5.2,5.2C7.4,11.4,7.2,11.5,7,11.5z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 596 B |
6
images/chevron-left-12.svg
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Generator: Adobe Illustrator 23.0.3, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||||
|
<svg version="1.1" id="Export" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||||
|
viewBox="0 0 12 12" style="enable-background:new 0 0 12 12;" xml:space="preserve">
|
||||||
|
<path d="M7.9,11.5L2.8,6.3c-0.2-0.2-0.2-0.5,0-0.7l5.1-5.1l0.7,0.7L3.8,6l4.8,4.8L7.9,11.5z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 444 B |
6
images/chevron-right-12.svg
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Generator: Adobe Illustrator 23.0.3, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||||
|
<svg version="1.1" id="Export" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||||
|
viewBox="0 0 12 12" style="enable-background:new 0 0 12 12;" xml:space="preserve">
|
||||||
|
<path d="M4.1,11.5l-0.7-0.7L8.2,6L3.4,1.2l0.7-0.7l5.1,5.1c0.2,0.2,0.2,0.5,0,0.7L4.1,11.5z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 444 B |
8
images/more-h.svg
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Generator: Adobe Illustrator 23.0.2, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||||
|
<svg version="1.1" id="Export" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||||
|
viewBox="0 0 24 24" style="enable-background:new 0 0 24 24;" xml:space="preserve">
|
||||||
|
<circle cx="18.5" cy="12" r="1.5"/>
|
||||||
|
<circle cx="12" cy="12" r="1.5"/>
|
||||||
|
<circle cx="5.5" cy="12" r="1.5"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 456 B |
6
images/plus-20.svg
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Generator: Adobe Illustrator 23.0.3, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||||
|
<svg version="1.1" id="Export" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||||
|
viewBox="0 0 20 20" style="enable-background:new 0 0 20 20;" xml:space="preserve">
|
||||||
|
<polygon points="18,9.2 10.8,9.2 10.8,2 9.2,2 9.2,9.2 2,9.2 2,10.8 9.2,10.8 9.2,18 10.8,18 10.8,10.8 18,10.8 "/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 464 B |
7
images/recent-outline.svg
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Generator: Adobe Illustrator 23.0.3, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||||
|
<svg version="1.1" id="Export" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||||
|
viewBox="0 0 24 24" style="enable-background:new 0 0 24 24;" xml:space="preserve">
|
||||||
|
<path d="M12,2.5c5.2,0,9.5,4.3,9.5,9.5s-4.3,9.5-9.5,9.5S2.5,17.2,2.5,12C2.5,6.8,6.8,2.5,12,2.5 M12,1C5.9,1,1,5.9,1,12
|
||||||
|
s4.9,11,11,11s11-4.9,11-11S18.1,1,12,1z M13,4h-1l-0.4,7.6L5,12v1l7.5,0.5c0.6,0,1-0.4,1-1L13,4z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 568 B |
10
images/sticker-filled.svg
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Generator: Adobe Illustrator 23.0.3, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||||
|
<svg version="1.1" id="Export" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||||
|
viewBox="0 0 24 24" style="enable-background:new 0 0 24 24;" xml:space="preserve">
|
||||||
|
<path d="M9,22H7.1c-1.1,0.1-2.1-0.1-3.1-0.5c-0.6-0.3-1.2-0.8-1.5-1.5c-0.5-1-0.6-2.1-0.5-3.1V7.1C1.9,6.1,2.1,5,2.5,4.1
|
||||||
|
c0.3-0.6,0.9-1.2,1.5-1.5c1-0.5,2-0.6,3.1-0.5h9.7C17.9,1.9,19,2.1,20,2.5c0.6,0.3,1.2,0.9,1.5,1.5c0.4,1,0.6,2,0.5,3.1V9
|
||||||
|
c0,0.5,0,1-1.5,1h-4.6c-1.2-0.1-2.4,0.1-3.4,0.6c-0.8,0.4-1.4,1.1-1.8,1.8c-0.5,1.1-0.7,2.3-0.6,3.4v4.6C10,21.9,9.6,22,9,22z
|
||||||
|
M15.9,11.5c-0.9-0.1-1.9,0-2.7,0.4c-0.5,0.3-0.9,0.7-1.2,1.2c-0.4,0.8-0.6,1.8-0.4,2.7v4.6c0,0.2,0,0.4,0,0.5l9.5-9.6
|
||||||
|
c-0.2,0-0.4,0-0.5,0L15.9,11.5z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 862 B |
122
js/background.js
|
@ -296,6 +296,24 @@
|
||||||
// Shut down the data interface cleanly
|
// Shut down the data interface cleanly
|
||||||
await window.Signal.Data.shutdown();
|
await window.Signal.Data.shutdown();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
installStickerPack: async (id, key) => {
|
||||||
|
const status = window.Signal.Stickers.getStickerPackStatus(id);
|
||||||
|
|
||||||
|
if (status === 'installed') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status === 'advertised') {
|
||||||
|
await window.reduxActions.stickers.installStickerPack(id, key, {
|
||||||
|
fromSync: true,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await window.Signal.Stickers.downloadStickerPack(id, key, {
|
||||||
|
finalStatus: 'installed',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const currentVersion = window.getVersion();
|
const currentVersion = window.getVersion();
|
||||||
|
@ -303,18 +321,23 @@
|
||||||
newVersion = !lastVersion || currentVersion !== lastVersion;
|
newVersion = !lastVersion || currentVersion !== lastVersion;
|
||||||
await storage.put('version', currentVersion);
|
await storage.put('version', currentVersion);
|
||||||
|
|
||||||
if (newVersion) {
|
if (newVersion && lastVersion) {
|
||||||
if (
|
|
||||||
lastVersion &&
|
|
||||||
window.isBeforeVersion(lastVersion, 'v1.15.0-beta.5')
|
|
||||||
) {
|
|
||||||
await window.Signal.Logs.deleteAll();
|
|
||||||
window.restart();
|
|
||||||
}
|
|
||||||
|
|
||||||
window.log.info(
|
window.log.info(
|
||||||
`New version detected: ${currentVersion}; previous: ${lastVersion}`
|
`New version detected: ${currentVersion}; previous: ${lastVersion}`
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (window.isBeforeVersion(lastVersion, 'v1.25.0')) {
|
||||||
|
// Stickers flags
|
||||||
|
await Promise.all([
|
||||||
|
storage.put('showStickersIntroduction', true),
|
||||||
|
storage.put('showStickerPickerHint', true),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (window.isBeforeVersion(lastVersion, 'v1.15.0-beta.5')) {
|
||||||
|
await window.Signal.Logs.deleteAll();
|
||||||
|
window.restart();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isIndexedDBPresent) {
|
if (isIndexedDBPresent) {
|
||||||
|
@ -395,6 +418,7 @@
|
||||||
try {
|
try {
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
ConversationController.load(),
|
ConversationController.load(),
|
||||||
|
Signal.Stickers.load(),
|
||||||
textsecure.storage.protocol.hydrateCaches(),
|
textsecure.storage.protocol.hydrateCaches(),
|
||||||
]);
|
]);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@ -418,7 +442,11 @@
|
||||||
conversations: {
|
conversations: {
|
||||||
conversationLookup: Signal.Util.makeLookup(conversations, 'id'),
|
conversationLookup: Signal.Util.makeLookup(conversations, 'id'),
|
||||||
},
|
},
|
||||||
|
items: storage.getItemsState(),
|
||||||
|
stickers: Signal.Stickers.getInitialState(),
|
||||||
user: {
|
user: {
|
||||||
|
attachmentsPath: window.baseAttachmentsPath,
|
||||||
|
stickersPath: window.baseStickersPath,
|
||||||
regionCode: window.storage.get('regionCode'),
|
regionCode: window.storage.get('regionCode'),
|
||||||
ourNumber: textsecure.storage.user.getNumber(),
|
ourNumber: textsecure.storage.user.getNumber(),
|
||||||
i18n: window.i18n,
|
i18n: window.i18n,
|
||||||
|
@ -437,10 +465,18 @@
|
||||||
Signal.State.Ducks.conversations.actions,
|
Signal.State.Ducks.conversations.actions,
|
||||||
store.dispatch
|
store.dispatch
|
||||||
);
|
);
|
||||||
|
actions.items = Signal.State.bindActionCreators(
|
||||||
|
Signal.State.Ducks.items.actions,
|
||||||
|
store.dispatch
|
||||||
|
);
|
||||||
actions.user = Signal.State.bindActionCreators(
|
actions.user = Signal.State.bindActionCreators(
|
||||||
Signal.State.Ducks.user.actions,
|
Signal.State.Ducks.user.actions,
|
||||||
store.dispatch
|
store.dispatch
|
||||||
);
|
);
|
||||||
|
actions.stickers = Signal.State.bindActionCreators(
|
||||||
|
Signal.State.Ducks.stickers.actions,
|
||||||
|
store.dispatch
|
||||||
|
);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
conversationAdded,
|
conversationAdded,
|
||||||
|
@ -759,6 +795,7 @@
|
||||||
messageReceiver.addEventListener('progress', onProgress);
|
messageReceiver.addEventListener('progress', onProgress);
|
||||||
messageReceiver.addEventListener('configuration', onConfiguration);
|
messageReceiver.addEventListener('configuration', onConfiguration);
|
||||||
messageReceiver.addEventListener('typing', onTyping);
|
messageReceiver.addEventListener('typing', onTyping);
|
||||||
|
messageReceiver.addEventListener('sticker-pack', onStickerPack);
|
||||||
|
|
||||||
window.Signal.AttachmentDownloads.start({
|
window.Signal.AttachmentDownloads.start({
|
||||||
getMessageReceiver: () => messageReceiver,
|
getMessageReceiver: () => messageReceiver,
|
||||||
|
@ -770,6 +807,10 @@
|
||||||
PASSWORD
|
PASSWORD
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (connectCount === 1) {
|
||||||
|
window.Signal.Stickers.downloadQueuedPacks();
|
||||||
|
}
|
||||||
|
|
||||||
// On startup after upgrading to a new version, request a contact sync
|
// On startup after upgrading to a new version, request a contact sync
|
||||||
// (but only if we're not the primary device)
|
// (but only if we're not the primary device)
|
||||||
if (
|
if (
|
||||||
|
@ -831,11 +872,34 @@
|
||||||
Whisper.events.trigger('contactsync');
|
Whisper.events.trigger('contactsync');
|
||||||
});
|
});
|
||||||
|
|
||||||
if (Whisper.Import.isComplete()) {
|
const ourNumber = textsecure.storage.user.getNumber();
|
||||||
const { wrap, sendOptions } = ConversationController.prepareForSend(
|
const { wrap, sendOptions } = ConversationController.prepareForSend(
|
||||||
textsecure.storage.user.getNumber(),
|
ourNumber,
|
||||||
{ syncMessage: true }
|
{ syncMessage: true }
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const installedStickerPacks = window.Signal.Stickers.getInstalledStickerPacks();
|
||||||
|
if (installedStickerPacks.length) {
|
||||||
|
const operations = installedStickerPacks.map(pack => ({
|
||||||
|
packId: pack.id,
|
||||||
|
packKey: pack.key,
|
||||||
|
installed: true,
|
||||||
|
}));
|
||||||
|
|
||||||
|
wrap(
|
||||||
|
window.textsecure.messaging.sendStickerPackSync(
|
||||||
|
operations,
|
||||||
|
sendOptions
|
||||||
|
)
|
||||||
|
).catch(error => {
|
||||||
|
window.log.error(
|
||||||
|
'Failed to send installed sticker packs via sync message',
|
||||||
|
error && error.stack ? error.stack : error
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Whisper.Import.isComplete()) {
|
||||||
wrap(
|
wrap(
|
||||||
textsecure.messaging.sendRequestConfigurationSyncMessage(sendOptions)
|
textsecure.messaging.sendRequestConfigurationSyncMessage(sendOptions)
|
||||||
).catch(error => {
|
).catch(error => {
|
||||||
|
@ -942,6 +1006,42 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function onStickerPack(ev) {
|
||||||
|
const packs = ev.stickerPacks || [];
|
||||||
|
|
||||||
|
packs.forEach(pack => {
|
||||||
|
const { id, key, isInstall, isRemove } = pack || {};
|
||||||
|
|
||||||
|
if (!id || !key || (!isInstall && !isRemove)) {
|
||||||
|
window.log.warn(
|
||||||
|
'Received malformed sticker pack operation sync message'
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const status = window.Signal.Stickers.getStickerPackStatus(id);
|
||||||
|
|
||||||
|
if (status === 'installed' && isRemove) {
|
||||||
|
window.reduxActions.stickers.uninstallStickerPack(id, key, {
|
||||||
|
fromSync: true,
|
||||||
|
});
|
||||||
|
} else if (isInstall) {
|
||||||
|
if (status === 'advertised') {
|
||||||
|
window.reduxActions.stickers.installStickerPack(id, key, {
|
||||||
|
fromSync: true,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
window.Signal.Stickers.downloadStickerPack(id, key, {
|
||||||
|
finalStatus: 'installed',
|
||||||
|
fromSync: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ev.confirm();
|
||||||
|
}
|
||||||
|
|
||||||
async function onContactReceived(ev) {
|
async function onContactReceived(ev) {
|
||||||
const details = ev.contactDetails;
|
const details = ev.contactDetails;
|
||||||
|
|
||||||
|
|
|
@ -12,6 +12,10 @@
|
||||||
const HOUR = MINUTE * 60;
|
const HOUR = MINUTE * 60;
|
||||||
|
|
||||||
function register(id, message) {
|
function register(id, message) {
|
||||||
|
if (!id || !message) {
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
|
||||||
const existing = messageLookup[id];
|
const existing = messageLookup[id];
|
||||||
if (existing) {
|
if (existing) {
|
||||||
messageLookup[id] = {
|
messageLookup[id] = {
|
||||||
|
|
|
@ -35,12 +35,14 @@
|
||||||
PhoneNumber,
|
PhoneNumber,
|
||||||
} = window.Signal.Types;
|
} = window.Signal.Types;
|
||||||
const {
|
const {
|
||||||
upgradeMessageSchema,
|
|
||||||
loadAttachmentData,
|
|
||||||
getAbsoluteAttachmentPath,
|
|
||||||
writeNewAttachmentData,
|
|
||||||
deleteAttachmentData,
|
deleteAttachmentData,
|
||||||
|
getAbsoluteAttachmentPath,
|
||||||
|
loadAttachmentData,
|
||||||
|
readStickerData,
|
||||||
|
upgradeMessageSchema,
|
||||||
|
writeNewAttachmentData,
|
||||||
} = window.Signal.Migrations;
|
} = window.Signal.Migrations;
|
||||||
|
const { addStickerPackReference } = window.Signal.Data;
|
||||||
|
|
||||||
const COLORS = [
|
const COLORS = [
|
||||||
'red',
|
'red',
|
||||||
|
@ -761,7 +763,7 @@
|
||||||
return _.without(this.get('members'), me);
|
return _.without(this.get('members'), me);
|
||||||
},
|
},
|
||||||
|
|
||||||
async getQuoteAttachment(attachments, preview) {
|
async getQuoteAttachment(attachments, preview, sticker) {
|
||||||
if (attachments && attachments.length) {
|
if (attachments && attachments.length) {
|
||||||
return Promise.all(
|
return Promise.all(
|
||||||
attachments
|
attachments
|
||||||
|
@ -817,6 +819,23 @@
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (sticker && sticker.data && sticker.data.path) {
|
||||||
|
const { path, contentType } = sticker.data;
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
contentType,
|
||||||
|
// Our protos library complains about this field being undefined, so we
|
||||||
|
// force it to null
|
||||||
|
fileName: null,
|
||||||
|
thumbnail: {
|
||||||
|
...(await loadAttachmentData(sticker.data)),
|
||||||
|
objectUrl: getAbsoluteAttachmentPath(path),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
return [];
|
return [];
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -825,6 +844,7 @@
|
||||||
const contact = quotedMessage.getContact();
|
const contact = quotedMessage.getContact();
|
||||||
const attachments = quotedMessage.get('attachments');
|
const attachments = quotedMessage.get('attachments');
|
||||||
const preview = quotedMessage.get('preview');
|
const preview = quotedMessage.get('preview');
|
||||||
|
const sticker = quotedMessage.get('sticker');
|
||||||
|
|
||||||
const body = quotedMessage.get('body');
|
const body = quotedMessage.get('body');
|
||||||
const embeddedContact = quotedMessage.get('contact');
|
const embeddedContact = quotedMessage.get('contact');
|
||||||
|
@ -837,11 +857,46 @@
|
||||||
author: contact.id,
|
author: contact.id,
|
||||||
id: quotedMessage.get('sent_at'),
|
id: quotedMessage.get('sent_at'),
|
||||||
text: body || embeddedContactName,
|
text: body || embeddedContactName,
|
||||||
attachments: await this.getQuoteAttachment(attachments, preview),
|
attachments: await this.getQuoteAttachment(
|
||||||
|
attachments,
|
||||||
|
preview,
|
||||||
|
sticker
|
||||||
|
),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
sendMessage(body, attachments, quote, preview) {
|
async sendStickerMessage(packId, stickerId) {
|
||||||
|
const packData = window.Signal.Stickers.getStickerPack(packId);
|
||||||
|
const stickerData = window.Signal.Stickers.getSticker(packId, stickerId);
|
||||||
|
if (!stickerData || !packData) {
|
||||||
|
window.log.warn(
|
||||||
|
`Attempted to send nonexistent (${packId}, ${stickerId}) sticker!`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { key } = packData;
|
||||||
|
const { path, width, height } = stickerData;
|
||||||
|
const arrayBuffer = await readStickerData(path);
|
||||||
|
|
||||||
|
const sticker = {
|
||||||
|
packId,
|
||||||
|
stickerId,
|
||||||
|
packKey: key,
|
||||||
|
data: {
|
||||||
|
size: arrayBuffer.byteLength,
|
||||||
|
data: arrayBuffer,
|
||||||
|
contentType: 'image/webp',
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
this.sendMessage(null, [], null, [], sticker);
|
||||||
|
window.reduxActions.stickers.useSticker(packId, stickerId);
|
||||||
|
},
|
||||||
|
|
||||||
|
sendMessage(body, attachments, quote, preview, sticker) {
|
||||||
this.clearTypingTimers();
|
this.clearTypingTimers();
|
||||||
|
|
||||||
const destination = this.id;
|
const destination = this.id;
|
||||||
|
@ -863,6 +918,7 @@
|
||||||
now
|
now
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Here we move attachments to disk
|
||||||
const messageWithSchema = await upgradeMessageSchema({
|
const messageWithSchema = await upgradeMessageSchema({
|
||||||
type: 'outgoing',
|
type: 'outgoing',
|
||||||
body,
|
body,
|
||||||
|
@ -874,6 +930,7 @@
|
||||||
received_at: now,
|
received_at: now,
|
||||||
expireTimer,
|
expireTimer,
|
||||||
recipients,
|
recipients,
|
||||||
|
sticker,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (this.isPrivate()) {
|
if (this.isPrivate()) {
|
||||||
|
@ -885,6 +942,9 @@
|
||||||
};
|
};
|
||||||
|
|
||||||
const model = this.addSingleMessage(attributes);
|
const model = this.addSingleMessage(attributes);
|
||||||
|
if (sticker) {
|
||||||
|
await addStickerPackReference(model.id, sticker.packId);
|
||||||
|
}
|
||||||
const message = MessageController.register(model.id, model);
|
const message = MessageController.register(model.id, model);
|
||||||
await window.Signal.Data.saveMessage(message.attributes, {
|
await window.Signal.Data.saveMessage(message.attributes, {
|
||||||
forceSave: true,
|
forceSave: true,
|
||||||
|
@ -935,6 +995,7 @@
|
||||||
finalAttachments,
|
finalAttachments,
|
||||||
quote,
|
quote,
|
||||||
preview,
|
preview,
|
||||||
|
sticker,
|
||||||
now,
|
now,
|
||||||
expireTimer,
|
expireTimer,
|
||||||
profileKey
|
profileKey
|
||||||
|
@ -955,6 +1016,7 @@
|
||||||
finalAttachments,
|
finalAttachments,
|
||||||
quote,
|
quote,
|
||||||
preview,
|
preview,
|
||||||
|
sticker,
|
||||||
now,
|
now,
|
||||||
expireTimer,
|
expireTimer,
|
||||||
profileKey,
|
profileKey,
|
||||||
|
@ -968,6 +1030,7 @@
|
||||||
finalAttachments,
|
finalAttachments,
|
||||||
quote,
|
quote,
|
||||||
preview,
|
preview,
|
||||||
|
sticker,
|
||||||
now,
|
now,
|
||||||
expireTimer,
|
expireTimer,
|
||||||
profileKey,
|
profileKey,
|
||||||
|
@ -1271,6 +1334,7 @@
|
||||||
[],
|
[],
|
||||||
null,
|
null,
|
||||||
[],
|
[],
|
||||||
|
null,
|
||||||
message.get('sent_at'),
|
message.get('sent_at'),
|
||||||
expireTimer,
|
expireTimer,
|
||||||
profileKey,
|
profileKey,
|
||||||
|
|
|
@ -27,8 +27,16 @@
|
||||||
loadAttachmentData,
|
loadAttachmentData,
|
||||||
loadQuoteData,
|
loadQuoteData,
|
||||||
loadPreviewData,
|
loadPreviewData,
|
||||||
|
loadStickerData,
|
||||||
upgradeMessageSchema,
|
upgradeMessageSchema,
|
||||||
} = window.Signal.Migrations;
|
} = window.Signal.Migrations;
|
||||||
|
const {
|
||||||
|
copyStickerToAttachments,
|
||||||
|
deletePackReference,
|
||||||
|
downloadStickerPack,
|
||||||
|
getStickerPackStatus,
|
||||||
|
} = window.Signal.Stickers;
|
||||||
|
const { addStickerPackReference } = window.Signal.Data;
|
||||||
const { bytesFromString } = window.Signal.Crypto;
|
const { bytesFromString } = window.Signal.Crypto;
|
||||||
|
|
||||||
window.AccountCache = Object.create(null);
|
window.AccountCache = Object.create(null);
|
||||||
|
@ -389,6 +397,29 @@
|
||||||
// It doesn't need anything right now!
|
// It doesn't need anything right now!
|
||||||
return {};
|
return {};
|
||||||
},
|
},
|
||||||
|
getAttachmentsForMessage() {
|
||||||
|
const sticker = this.get('sticker');
|
||||||
|
if (sticker && sticker.data) {
|
||||||
|
const { data } = sticker;
|
||||||
|
|
||||||
|
// We don't show anything if we're still loading a sticker
|
||||||
|
if (data.pending || !data.path) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
...data,
|
||||||
|
url: getAbsoluteAttachmentPath(data.path),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
const attachments = this.get('attachments') || [];
|
||||||
|
return attachments
|
||||||
|
.filter(attachment => !attachment.error)
|
||||||
|
.map(attachment => this.getPropsForAttachment(attachment));
|
||||||
|
},
|
||||||
getPropsForMessage() {
|
getPropsForMessage() {
|
||||||
const phoneNumber = this.getSource();
|
const phoneNumber = this.getSource();
|
||||||
const contact = this.findAndFormatContact(phoneNumber);
|
const contact = this.findAndFormatContact(phoneNumber);
|
||||||
|
@ -408,12 +439,13 @@
|
||||||
|
|
||||||
const conversation = this.getConversation();
|
const conversation = this.getConversation();
|
||||||
const isGroup = conversation && !conversation.isPrivate();
|
const isGroup = conversation && !conversation.isPrivate();
|
||||||
const attachments = this.get('attachments') || [];
|
const sticker = this.get('sticker');
|
||||||
|
|
||||||
return {
|
return {
|
||||||
text: this.createNonBreakingLastSeparator(this.get('body')),
|
text: this.createNonBreakingLastSeparator(this.get('body')),
|
||||||
textPending: this.get('bodyPending'),
|
textPending: this.get('bodyPending'),
|
||||||
id: this.id,
|
id: this.id,
|
||||||
|
isSticker: Boolean(sticker),
|
||||||
direction: this.isIncoming() ? 'incoming' : 'outgoing',
|
direction: this.isIncoming() ? 'incoming' : 'outgoing',
|
||||||
timestamp: this.get('sent_at'),
|
timestamp: this.get('sent_at'),
|
||||||
status: this.getMessagePropStatus(),
|
status: this.getMessagePropStatus(),
|
||||||
|
@ -423,9 +455,7 @@
|
||||||
authorProfileName: contact.profileName,
|
authorProfileName: contact.profileName,
|
||||||
authorPhoneNumber: contact.phoneNumber,
|
authorPhoneNumber: contact.phoneNumber,
|
||||||
conversationType: isGroup ? 'group' : 'direct',
|
conversationType: isGroup ? 'group' : 'direct',
|
||||||
attachments: attachments
|
attachments: this.getAttachmentsForMessage(),
|
||||||
.filter(attachment => !attachment.error)
|
|
||||||
.map(attachment => this.getPropsForAttachment(attachment)),
|
|
||||||
previews: this.getPropsForPreview(),
|
previews: this.getPropsForPreview(),
|
||||||
quote: this.getPropsForQuote(),
|
quote: this.getPropsForQuote(),
|
||||||
authorAvatarPath,
|
authorAvatarPath,
|
||||||
|
@ -584,6 +614,7 @@
|
||||||
|
|
||||||
return previews.map(preview => ({
|
return previews.map(preview => ({
|
||||||
...preview,
|
...preview,
|
||||||
|
isStickerPack: window.Signal.LinkPreviews.isStickerPack(preview.url),
|
||||||
domain: window.Signal.LinkPreviews.getDomain(preview.url),
|
domain: window.Signal.LinkPreviews.getDomain(preview.url),
|
||||||
image: preview.image ? this.getPropsForAttachment(preview.image) : null,
|
image: preview.image ? this.getPropsForAttachment(preview.image) : null,
|
||||||
}));
|
}));
|
||||||
|
@ -708,6 +739,9 @@
|
||||||
if (this.get('attachments').length > 0) {
|
if (this.get('attachments').length > 0) {
|
||||||
return i18n('mediaMessage');
|
return i18n('mediaMessage');
|
||||||
}
|
}
|
||||||
|
if (this.get('sticker')) {
|
||||||
|
return i18n('message--getNotificationText--stickers');
|
||||||
|
}
|
||||||
if (this.isExpirationTimerUpdate()) {
|
if (this.isExpirationTimerUpdate()) {
|
||||||
const { expireTimer } = this.get('expirationTimerUpdate');
|
const { expireTimer } = this.get('expirationTimerUpdate');
|
||||||
if (!expireTimer) {
|
if (!expireTimer) {
|
||||||
|
@ -775,6 +809,16 @@
|
||||||
MessageController.unregister(this.id);
|
MessageController.unregister(this.id);
|
||||||
this.unload();
|
this.unload();
|
||||||
await deleteExternalMessageFiles(this.attributes);
|
await deleteExternalMessageFiles(this.attributes);
|
||||||
|
|
||||||
|
const sticker = this.get('sticker');
|
||||||
|
if (!sticker) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { packId } = sticker;
|
||||||
|
if (packId) {
|
||||||
|
await deletePackReference(this.id, packId);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
unload() {
|
unload() {
|
||||||
if (this.quotedMessage) {
|
if (this.quotedMessage) {
|
||||||
|
@ -968,6 +1012,7 @@
|
||||||
|
|
||||||
const quoteWithData = await loadQuoteData(this.get('quote'));
|
const quoteWithData = await loadQuoteData(this.get('quote'));
|
||||||
const previewWithData = await loadPreviewData(this.get('preview'));
|
const previewWithData = await loadPreviewData(this.get('preview'));
|
||||||
|
const stickerWithData = await loadStickerData(this.get('sticker'));
|
||||||
|
|
||||||
// Special-case the self-send case - we send only a sync message
|
// Special-case the self-send case - we send only a sync message
|
||||||
if (recipients.length === 1 && recipients[0] === this.OUR_NUMBER) {
|
if (recipients.length === 1 && recipients[0] === this.OUR_NUMBER) {
|
||||||
|
@ -978,6 +1023,7 @@
|
||||||
attachments,
|
attachments,
|
||||||
quoteWithData,
|
quoteWithData,
|
||||||
previewWithData,
|
previewWithData,
|
||||||
|
stickerWithData,
|
||||||
this.get('sent_at'),
|
this.get('sent_at'),
|
||||||
this.get('expireTimer'),
|
this.get('expireTimer'),
|
||||||
profileKey
|
profileKey
|
||||||
|
@ -996,6 +1042,7 @@
|
||||||
attachments,
|
attachments,
|
||||||
quoteWithData,
|
quoteWithData,
|
||||||
previewWithData,
|
previewWithData,
|
||||||
|
stickerWithData,
|
||||||
this.get('sent_at'),
|
this.get('sent_at'),
|
||||||
this.get('expireTimer'),
|
this.get('expireTimer'),
|
||||||
profileKey,
|
profileKey,
|
||||||
|
@ -1013,6 +1060,7 @@
|
||||||
attachments,
|
attachments,
|
||||||
quote: quoteWithData,
|
quote: quoteWithData,
|
||||||
preview: previewWithData,
|
preview: previewWithData,
|
||||||
|
sticker: stickerWithData,
|
||||||
needsSync: !this.get('synced'),
|
needsSync: !this.get('synced'),
|
||||||
expireTimer: this.get('expireTimer'),
|
expireTimer: this.get('expireTimer'),
|
||||||
profileKey,
|
profileKey,
|
||||||
|
@ -1058,6 +1106,7 @@
|
||||||
|
|
||||||
const quoteWithData = await loadQuoteData(this.get('quote'));
|
const quoteWithData = await loadQuoteData(this.get('quote'));
|
||||||
const previewWithData = await loadPreviewData(this.get('preview'));
|
const previewWithData = await loadPreviewData(this.get('preview'));
|
||||||
|
const stickerWithData = await loadStickerData(this.get('sticker'));
|
||||||
|
|
||||||
// Special-case the self-send case - we send only a sync message
|
// Special-case the self-send case - we send only a sync message
|
||||||
if (number === this.OUR_NUMBER) {
|
if (number === this.OUR_NUMBER) {
|
||||||
|
@ -1067,6 +1116,7 @@
|
||||||
attachments,
|
attachments,
|
||||||
quoteWithData,
|
quoteWithData,
|
||||||
previewWithData,
|
previewWithData,
|
||||||
|
stickerWithData,
|
||||||
this.get('sent_at'),
|
this.get('sent_at'),
|
||||||
this.get('expireTimer'),
|
this.get('expireTimer'),
|
||||||
profileKey
|
profileKey
|
||||||
|
@ -1083,6 +1133,7 @@
|
||||||
attachments,
|
attachments,
|
||||||
quoteWithData,
|
quoteWithData,
|
||||||
previewWithData,
|
previewWithData,
|
||||||
|
stickerWithData,
|
||||||
this.get('sent_at'),
|
this.get('sent_at'),
|
||||||
this.get('expireTimer'),
|
this.get('expireTimer'),
|
||||||
profileKey,
|
profileKey,
|
||||||
|
@ -1405,8 +1456,62 @@
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let sticker = this.get('sticker');
|
||||||
|
if (sticker) {
|
||||||
|
count += 1;
|
||||||
|
const { packId, stickerId, packKey } = sticker;
|
||||||
|
|
||||||
|
const status = getStickerPackStatus(packId);
|
||||||
|
let data;
|
||||||
|
|
||||||
|
if (status && status !== 'pending' && status !== 'error') {
|
||||||
|
try {
|
||||||
|
const copiedSticker = await copyStickerToAttachments(
|
||||||
|
packId,
|
||||||
|
stickerId
|
||||||
|
);
|
||||||
|
data = {
|
||||||
|
...copiedSticker,
|
||||||
|
contentType: 'image/webp',
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
window.log.error(
|
||||||
|
`Problem copying sticker (${packId}, ${stickerId}) to attachments:`,
|
||||||
|
error && error.stack ? error.stack : error
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!data) {
|
||||||
|
data = await window.Signal.AttachmentDownloads.addJob(sticker.data, {
|
||||||
|
messageId,
|
||||||
|
type: 'sticker',
|
||||||
|
index: 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (!status) {
|
||||||
|
// kick off the download without waiting
|
||||||
|
downloadStickerPack(packId, packKey, { messageId });
|
||||||
|
} else {
|
||||||
|
await addStickerPackReference(messageId, packId);
|
||||||
|
}
|
||||||
|
|
||||||
|
sticker = {
|
||||||
|
...sticker,
|
||||||
|
packId,
|
||||||
|
data,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (count > 0) {
|
if (count > 0) {
|
||||||
this.set({ bodyPending, attachments, preview, contact, quote, group });
|
this.set({
|
||||||
|
bodyPending,
|
||||||
|
attachments,
|
||||||
|
preview,
|
||||||
|
contact,
|
||||||
|
quote,
|
||||||
|
group,
|
||||||
|
sticker,
|
||||||
|
});
|
||||||
|
|
||||||
await window.Signal.Data.saveMessage(this.attributes, {
|
await window.Signal.Data.saveMessage(this.attributes, {
|
||||||
Message: Whisper.Message,
|
Message: Whisper.Message,
|
||||||
|
@ -1481,7 +1586,6 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
const queryAttachments = queryMessage.get('attachments') || [];
|
const queryAttachments = queryMessage.get('attachments') || [];
|
||||||
|
|
||||||
if (queryAttachments.length > 0) {
|
if (queryAttachments.length > 0) {
|
||||||
const queryFirst = queryAttachments[0];
|
const queryFirst = queryAttachments[0];
|
||||||
const { thumbnail } = queryFirst;
|
const { thumbnail } = queryFirst;
|
||||||
|
@ -1507,6 +1611,14 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const sticker = queryMessage.get('sticker');
|
||||||
|
if (sticker && sticker.data && sticker.data.path) {
|
||||||
|
firstAttachment.thumbnail = {
|
||||||
|
...sticker.data,
|
||||||
|
copied: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return message;
|
return message;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -1617,9 +1729,10 @@
|
||||||
hasAttachments: dataMessage.hasAttachments,
|
hasAttachments: dataMessage.hasAttachments,
|
||||||
hasFileAttachments: dataMessage.hasFileAttachments,
|
hasFileAttachments: dataMessage.hasFileAttachments,
|
||||||
hasVisualMediaAttachments: dataMessage.hasVisualMediaAttachments,
|
hasVisualMediaAttachments: dataMessage.hasVisualMediaAttachments,
|
||||||
quote: dataMessage.quote,
|
|
||||||
preview,
|
preview,
|
||||||
|
quote: dataMessage.quote,
|
||||||
schemaVersion: dataMessage.schemaVersion,
|
schemaVersion: dataMessage.schemaVersion,
|
||||||
|
sticker: dataMessage.sticker,
|
||||||
});
|
});
|
||||||
if (type === 'outgoing') {
|
if (type === 'outgoing') {
|
||||||
const receipts = Whisper.DeliveryReceipts.forMessage(
|
const receipts = Whisper.DeliveryReceipts.forMessage(
|
||||||
|
@ -1841,7 +1954,7 @@
|
||||||
Whisper.Message.LONG_MESSAGE_CONTENT_TYPE = 'text/x-signal-plain';
|
Whisper.Message.LONG_MESSAGE_CONTENT_TYPE = 'text/x-signal-plain';
|
||||||
|
|
||||||
Whisper.Message.getLongMessageAttachment = ({ body, attachments, now }) => {
|
Whisper.Message.getLongMessageAttachment = ({ body, attachments, now }) => {
|
||||||
if (body.length <= 2048) {
|
if (!body || body.length <= 2048) {
|
||||||
return {
|
return {
|
||||||
body,
|
body,
|
||||||
attachments,
|
attachments,
|
||||||
|
|
|
@ -1,6 +1,14 @@
|
||||||
/* global Whisper, Signal, setTimeout, clearTimeout, MessageController */
|
/* global
|
||||||
|
ConversationController,
|
||||||
|
Whisper,
|
||||||
|
Signal,
|
||||||
|
setTimeout,
|
||||||
|
clearTimeout,
|
||||||
|
MessageController
|
||||||
|
*/
|
||||||
|
|
||||||
const { isFunction, isNumber, omit } = require('lodash');
|
const { isFunction, isNumber, omit } = require('lodash');
|
||||||
|
const { computeHash } = require('./types/conversation');
|
||||||
const getGuid = require('uuid/v4');
|
const getGuid = require('uuid/v4');
|
||||||
const {
|
const {
|
||||||
getMessageById,
|
getMessageById,
|
||||||
|
@ -356,17 +364,41 @@ async function _addAttachmentToMessage(message, attachment, { type, index }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type === 'group-avatar') {
|
if (type === 'group-avatar') {
|
||||||
const group = message.get('group');
|
const conversationId = message.get('conversationid');
|
||||||
if (!group) {
|
const conversation = ConversationController.get(conversationId);
|
||||||
throw new Error("_addAttachmentToMessage: group didn't exist");
|
if (!conversation) {
|
||||||
|
logger.warn("_addAttachmentToMessage: conversation didn't exist");
|
||||||
}
|
}
|
||||||
|
|
||||||
const existingAvatar = group.avatar;
|
const existingAvatar = conversation.get('avatar');
|
||||||
if (existingAvatar && existingAvatar.path) {
|
if (existingAvatar && existingAvatar.path) {
|
||||||
await Signal.Migrations.deleteAttachmentData(existingAvatar.path);
|
await Signal.Migrations.deleteAttachmentData(existingAvatar.path);
|
||||||
}
|
}
|
||||||
|
|
||||||
_replaceAttachment(group, 'avatar', attachment, logPrefix);
|
const data = await Signal.Migrations.loadAttachmentData(attachment.path);
|
||||||
|
conversation.set({
|
||||||
|
avatar: {
|
||||||
|
...attachment,
|
||||||
|
hash: await computeHash(data),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await Signal.Data.updateConversation(
|
||||||
|
conversationId,
|
||||||
|
conversation.attributes,
|
||||||
|
{
|
||||||
|
Conversation: Whisper.Conversation,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === 'sticker') {
|
||||||
|
const sticker = message.get('sticker');
|
||||||
|
if (!sticker) {
|
||||||
|
throw new Error("_addAttachmentToMessage: sticker didn't exist");
|
||||||
|
}
|
||||||
|
|
||||||
|
_replaceAttachment(sticker, 'data', attachment, logPrefix);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,7 @@ module.exports = {
|
||||||
arrayBufferToBase64,
|
arrayBufferToBase64,
|
||||||
typedArrayToArrayBuffer,
|
typedArrayToArrayBuffer,
|
||||||
base64ToArrayBuffer,
|
base64ToArrayBuffer,
|
||||||
|
bytesFromHexString,
|
||||||
bytesFromString,
|
bytesFromString,
|
||||||
concatenateBytes,
|
concatenateBytes,
|
||||||
constantTimeEqual,
|
constantTimeEqual,
|
||||||
|
@ -16,6 +17,7 @@ module.exports = {
|
||||||
decryptFile,
|
decryptFile,
|
||||||
decryptSymmetric,
|
decryptSymmetric,
|
||||||
deriveAccessKey,
|
deriveAccessKey,
|
||||||
|
deriveStickerPackKey,
|
||||||
encryptAesCtr,
|
encryptAesCtr,
|
||||||
encryptDeviceName,
|
encryptDeviceName,
|
||||||
encryptAttachment,
|
encryptAttachment,
|
||||||
|
@ -25,8 +27,10 @@ module.exports = {
|
||||||
getAccessKeyVerifier,
|
getAccessKeyVerifier,
|
||||||
getFirstBytes,
|
getFirstBytes,
|
||||||
getRandomBytes,
|
getRandomBytes,
|
||||||
|
getRandomValue,
|
||||||
getViewOfArrayBuffer,
|
getViewOfArrayBuffer,
|
||||||
getZeroes,
|
getZeroes,
|
||||||
|
hexFromBytes,
|
||||||
highBitsToInt,
|
highBitsToInt,
|
||||||
hmacSha256,
|
hmacSha256,
|
||||||
intsToByteHighAndLow,
|
intsToByteHighAndLow,
|
||||||
|
@ -58,6 +62,25 @@ function bytesFromString(string) {
|
||||||
function stringFromBytes(buffer) {
|
function stringFromBytes(buffer) {
|
||||||
return dcodeIO.ByteBuffer.wrap(buffer).toString('utf8');
|
return dcodeIO.ByteBuffer.wrap(buffer).toString('utf8');
|
||||||
}
|
}
|
||||||
|
function hexFromBytes(buffer) {
|
||||||
|
return dcodeIO.ByteBuffer.wrap(buffer).toString('hex');
|
||||||
|
}
|
||||||
|
function bytesFromHexString(string) {
|
||||||
|
return dcodeIO.ByteBuffer.wrap(string, 'hex').toArrayBuffer();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deriveStickerPackKey(packKey) {
|
||||||
|
const salt = getZeroes(32);
|
||||||
|
const info = bytesFromString('Sticker Pack');
|
||||||
|
|
||||||
|
const [part1, part2] = await libsignal.HKDF.deriveSecrets(
|
||||||
|
packKey,
|
||||||
|
salt,
|
||||||
|
info
|
||||||
|
);
|
||||||
|
|
||||||
|
return concatenateBytes(part1, part2);
|
||||||
|
}
|
||||||
|
|
||||||
// High-level Operations
|
// High-level Operations
|
||||||
|
|
||||||
|
@ -366,6 +389,16 @@ function getRandomBytes(n) {
|
||||||
return bytes;
|
return bytes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getRandomValue(low, high) {
|
||||||
|
const diff = high - low;
|
||||||
|
const bytes = new Uint32Array(1);
|
||||||
|
window.crypto.getRandomValues(bytes);
|
||||||
|
|
||||||
|
// Because high and low are inclusive
|
||||||
|
const mod = diff + 1;
|
||||||
|
return bytes[0] % mod + low;
|
||||||
|
}
|
||||||
|
|
||||||
function getZeroes(n) {
|
function getZeroes(n) {
|
||||||
const result = new Uint8Array(n);
|
const result = new Uint8Array(n);
|
||||||
|
|
||||||
|
|
18
js/modules/data.d.ts
vendored
|
@ -1,2 +1,20 @@
|
||||||
export function searchMessages(query: string): Promise<Array<any>>;
|
export function searchMessages(query: string): Promise<Array<any>>;
|
||||||
export function searchConversations(query: string): Promise<Array<any>>;
|
export function searchConversations(query: string): Promise<Array<any>>;
|
||||||
|
|
||||||
|
export function updateStickerLastUsed(
|
||||||
|
packId: string,
|
||||||
|
stickerId: number,
|
||||||
|
time: number
|
||||||
|
): Promise<void>;
|
||||||
|
export function updateStickerPackStatus(
|
||||||
|
packId: string,
|
||||||
|
status: 'advertised' | 'installed' | 'error' | 'pending',
|
||||||
|
options?: { timestamp: number }
|
||||||
|
): Promise<void>;
|
||||||
|
|
||||||
|
export function getRecentStickers(): Promise<
|
||||||
|
Array<{
|
||||||
|
id: number;
|
||||||
|
packId: string;
|
||||||
|
}>
|
||||||
|
>;
|
||||||
|
|
|
@ -27,6 +27,7 @@ const DATABASE_UPDATE_TIMEOUT = 2 * 60 * 1000; // two minutes
|
||||||
const SQL_CHANNEL_KEY = 'sql-channel';
|
const SQL_CHANNEL_KEY = 'sql-channel';
|
||||||
const ERASE_SQL_KEY = 'erase-sql-key';
|
const ERASE_SQL_KEY = 'erase-sql-key';
|
||||||
const ERASE_ATTACHMENTS_KEY = 'erase-attachments';
|
const ERASE_ATTACHMENTS_KEY = 'erase-attachments';
|
||||||
|
const ERASE_STICKERS_KEY = 'erase-stickers';
|
||||||
const CLEANUP_ORPHANED_ATTACHMENTS_KEY = 'cleanup-orphaned-attachments';
|
const CLEANUP_ORPHANED_ATTACHMENTS_KEY = 'cleanup-orphaned-attachments';
|
||||||
|
|
||||||
const _jobs = Object.create(null);
|
const _jobs = Object.create(null);
|
||||||
|
@ -138,6 +139,17 @@ module.exports = {
|
||||||
removeAttachmentDownloadJob,
|
removeAttachmentDownloadJob,
|
||||||
removeAllAttachmentDownloadJobs,
|
removeAllAttachmentDownloadJobs,
|
||||||
|
|
||||||
|
createOrUpdateStickerPack,
|
||||||
|
updateStickerPackStatus,
|
||||||
|
createOrUpdateSticker,
|
||||||
|
updateStickerLastUsed,
|
||||||
|
addStickerPackReference,
|
||||||
|
deleteStickerPackReference,
|
||||||
|
deleteStickerPack,
|
||||||
|
getAllStickerPacks,
|
||||||
|
getAllStickers,
|
||||||
|
getRecentStickers,
|
||||||
|
|
||||||
removeAll,
|
removeAll,
|
||||||
removeAllConfiguration,
|
removeAllConfiguration,
|
||||||
|
|
||||||
|
@ -884,6 +896,44 @@ async function removeAllAttachmentDownloadJobs() {
|
||||||
await channels.removeAllAttachmentDownloadJobs();
|
await channels.removeAllAttachmentDownloadJobs();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Stickers
|
||||||
|
|
||||||
|
async function createOrUpdateStickerPack(pack) {
|
||||||
|
await channels.createOrUpdateStickerPack(pack);
|
||||||
|
}
|
||||||
|
async function updateStickerPackStatus(packId, status, options) {
|
||||||
|
await channels.updateStickerPackStatus(packId, status, options);
|
||||||
|
}
|
||||||
|
async function createOrUpdateSticker(sticker) {
|
||||||
|
await channels.createOrUpdateSticker(sticker);
|
||||||
|
}
|
||||||
|
async function updateStickerLastUsed(packId, stickerId, timestamp) {
|
||||||
|
await channels.updateStickerLastUsed(packId, stickerId, timestamp);
|
||||||
|
}
|
||||||
|
async function addStickerPackReference(messageId, packId) {
|
||||||
|
await channels.addStickerPackReference(messageId, packId);
|
||||||
|
}
|
||||||
|
async function deleteStickerPackReference(messageId, packId) {
|
||||||
|
const paths = await channels.deleteStickerPackReference(messageId, packId);
|
||||||
|
return paths;
|
||||||
|
}
|
||||||
|
async function deleteStickerPack(packId) {
|
||||||
|
const paths = await channels.deleteStickerPack(packId);
|
||||||
|
return paths;
|
||||||
|
}
|
||||||
|
async function getAllStickerPacks() {
|
||||||
|
const packs = await channels.getAllStickerPacks();
|
||||||
|
return packs;
|
||||||
|
}
|
||||||
|
async function getAllStickers() {
|
||||||
|
const stickers = await channels.getAllStickers();
|
||||||
|
return stickers;
|
||||||
|
}
|
||||||
|
async function getRecentStickers() {
|
||||||
|
const recentStickers = await channels.getRecentStickers();
|
||||||
|
return recentStickers;
|
||||||
|
}
|
||||||
|
|
||||||
// Other
|
// Other
|
||||||
|
|
||||||
async function removeAll() {
|
async function removeAll() {
|
||||||
|
@ -903,6 +953,7 @@ async function removeOtherData() {
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
callChannel(ERASE_SQL_KEY),
|
callChannel(ERASE_SQL_KEY),
|
||||||
callChannel(ERASE_ATTACHMENTS_KEY),
|
callChannel(ERASE_ATTACHMENTS_KEY),
|
||||||
|
callChannel(ERASE_STICKERS_KEY),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -18,6 +18,7 @@ module.exports = {
|
||||||
isLinkInWhitelist,
|
isLinkInWhitelist,
|
||||||
isMediaLinkInWhitelist,
|
isMediaLinkInWhitelist,
|
||||||
isLinkSneaky,
|
isLinkSneaky,
|
||||||
|
isStickerPack,
|
||||||
};
|
};
|
||||||
|
|
||||||
const SUPPORTED_DOMAINS = [
|
const SUPPORTED_DOMAINS = [
|
||||||
|
@ -37,7 +38,9 @@ const SUPPORTED_DOMAINS = [
|
||||||
'pinterest.com',
|
'pinterest.com',
|
||||||
'www.pinterest.com',
|
'www.pinterest.com',
|
||||||
'pin.it',
|
'pin.it',
|
||||||
|
'signal.org',
|
||||||
];
|
];
|
||||||
|
|
||||||
function isLinkInWhitelist(link) {
|
function isLinkInWhitelist(link) {
|
||||||
try {
|
try {
|
||||||
const url = new URL(link);
|
const url = new URL(link);
|
||||||
|
@ -61,6 +64,10 @@ function isLinkInWhitelist(link) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isStickerPack(link) {
|
||||||
|
return (link || '').startsWith('https://signal.org/addstickers/');
|
||||||
|
}
|
||||||
|
|
||||||
const SUPPORTED_MEDIA_DOMAINS = /^([^.]+\.)*(ytimg.com|cdninstagram.com|redd.it|imgur.com|fbcdn.net|pinimg.com)$/i;
|
const SUPPORTED_MEDIA_DOMAINS = /^([^.]+\.)*(ytimg.com|cdninstagram.com|redd.it|imgur.com|fbcdn.net|pinimg.com)$/i;
|
||||||
function isMediaLinkInWhitelist(link) {
|
function isMediaLinkInWhitelist(link) {
|
||||||
try {
|
try {
|
||||||
|
@ -138,28 +145,33 @@ function getDomain(url) {
|
||||||
const MB = 1024 * 1024;
|
const MB = 1024 * 1024;
|
||||||
const KB = 1024;
|
const KB = 1024;
|
||||||
|
|
||||||
function getChunkPattern(size) {
|
function getChunkPattern(size, initialOffset) {
|
||||||
if (size > MB) {
|
if (size > MB) {
|
||||||
return _getRequestPattern(size, MB);
|
return _getRequestPattern(size, MB, initialOffset);
|
||||||
} else if (size > 500 * KB) {
|
} else if (size > 500 * KB) {
|
||||||
return _getRequestPattern(size, 500 * KB);
|
return _getRequestPattern(size, 500 * KB, initialOffset);
|
||||||
} else if (size > 100 * KB) {
|
} else if (size > 100 * KB) {
|
||||||
return _getRequestPattern(size, 100 * KB);
|
return _getRequestPattern(size, 100 * KB, initialOffset);
|
||||||
} else if (size > 50 * KB) {
|
} else if (size > 50 * KB) {
|
||||||
return _getRequestPattern(size, 50 * KB);
|
return _getRequestPattern(size, 50 * KB, initialOffset);
|
||||||
} else if (size > 10 * KB) {
|
} else if (size > 10 * KB) {
|
||||||
return _getRequestPattern(size, 10 * KB);
|
return _getRequestPattern(size, 10 * KB, initialOffset);
|
||||||
} else if (size > KB) {
|
} else if (size > KB) {
|
||||||
return _getRequestPattern(size, KB);
|
return _getRequestPattern(size, KB, initialOffset);
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Error(`getChunkPattern: Unsupported size: ${size}`);
|
return {
|
||||||
|
start: {
|
||||||
|
start: initialOffset,
|
||||||
|
end: size - 1,
|
||||||
|
},
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function _getRequestPattern(size, increment) {
|
function _getRequestPattern(size, increment, initialOffset) {
|
||||||
const results = [];
|
const results = [];
|
||||||
|
|
||||||
let offset = 0;
|
let offset = initialOffset || 0;
|
||||||
while (size - offset > increment) {
|
while (size - offset > increment) {
|
||||||
results.push({
|
results.push({
|
||||||
start: offset,
|
start: offset,
|
||||||
|
|
|
@ -9,6 +9,7 @@ const Emoji = require('../../ts/util/emoji');
|
||||||
const IndexedDB = require('./indexeddb');
|
const IndexedDB = require('./indexeddb');
|
||||||
const Notifications = require('../../ts/notifications');
|
const Notifications = require('../../ts/notifications');
|
||||||
const OS = require('../../ts/OS');
|
const OS = require('../../ts/OS');
|
||||||
|
const Stickers = require('./stickers');
|
||||||
const Settings = require('./settings');
|
const Settings = require('./settings');
|
||||||
const Util = require('../../ts/util');
|
const Util = require('../../ts/util');
|
||||||
const { migrateToSQL } = require('./migrate_to_sql');
|
const { migrateToSQL } = require('./migrate_to_sql');
|
||||||
|
@ -69,8 +70,20 @@ const {
|
||||||
|
|
||||||
// State
|
// State
|
||||||
const { createLeftPane } = require('../../ts/state/roots/createLeftPane');
|
const { createLeftPane } = require('../../ts/state/roots/createLeftPane');
|
||||||
|
const {
|
||||||
|
createStickerButton,
|
||||||
|
} = require('../../ts/state/roots/createStickerButton');
|
||||||
|
const {
|
||||||
|
createStickerManager,
|
||||||
|
} = require('../../ts/state/roots/createStickerManager');
|
||||||
|
const {
|
||||||
|
createStickerPreviewModal,
|
||||||
|
} = require('../../ts/state/roots/createStickerPreviewModal');
|
||||||
|
|
||||||
const { createStore } = require('../../ts/state/createStore');
|
const { createStore } = require('../../ts/state/createStore');
|
||||||
const conversationsDuck = require('../../ts/state/ducks/conversations');
|
const conversationsDuck = require('../../ts/state/ducks/conversations');
|
||||||
|
const itemsDuck = require('../../ts/state/ducks/items');
|
||||||
|
const stickersDuck = require('../../ts/state/ducks/stickers');
|
||||||
const userDuck = require('../../ts/state/ducks/user');
|
const userDuck = require('../../ts/state/ducks/user');
|
||||||
|
|
||||||
// Migrations
|
// Migrations
|
||||||
|
@ -112,6 +125,7 @@ function initializeMigrations({
|
||||||
}
|
}
|
||||||
const {
|
const {
|
||||||
getPath,
|
getPath,
|
||||||
|
getStickersPath,
|
||||||
createReader,
|
createReader,
|
||||||
createAbsolutePathGetter,
|
createAbsolutePathGetter,
|
||||||
createWriterForNew,
|
createWriterForNew,
|
||||||
|
@ -130,25 +144,40 @@ function initializeMigrations({
|
||||||
const loadAttachmentData = Type.loadData(readAttachmentData);
|
const loadAttachmentData = Type.loadData(readAttachmentData);
|
||||||
const loadPreviewData = MessageType.loadPreviewData(loadAttachmentData);
|
const loadPreviewData = MessageType.loadPreviewData(loadAttachmentData);
|
||||||
const loadQuoteData = MessageType.loadQuoteData(loadAttachmentData);
|
const loadQuoteData = MessageType.loadQuoteData(loadAttachmentData);
|
||||||
|
const loadStickerData = MessageType.loadStickerData(loadAttachmentData);
|
||||||
const getAbsoluteAttachmentPath = createAbsolutePathGetter(attachmentsPath);
|
const getAbsoluteAttachmentPath = createAbsolutePathGetter(attachmentsPath);
|
||||||
const deleteOnDisk = Attachments.createDeleter(attachmentsPath);
|
const deleteOnDisk = Attachments.createDeleter(attachmentsPath);
|
||||||
const writeNewAttachmentData = createWriterForNew(attachmentsPath);
|
const writeNewAttachmentData = createWriterForNew(attachmentsPath);
|
||||||
|
const copyIntoAttachmentsDirectory = Attachments.copyIntoAttachmentsDirectory(
|
||||||
|
attachmentsPath
|
||||||
|
);
|
||||||
|
|
||||||
|
const stickersPath = getStickersPath(userDataPath);
|
||||||
|
const writeNewStickerData = createWriterForNew(stickersPath);
|
||||||
|
const getAbsoluteStickerPath = createAbsolutePathGetter(stickersPath);
|
||||||
|
const deleteSticker = Attachments.createDeleter(stickersPath);
|
||||||
|
const readStickerData = createReader(stickersPath);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
attachmentsPath,
|
attachmentsPath,
|
||||||
|
copyIntoAttachmentsDirectory,
|
||||||
deleteAttachmentData: deleteOnDisk,
|
deleteAttachmentData: deleteOnDisk,
|
||||||
deleteExternalMessageFiles: MessageType.deleteAllExternalFiles({
|
deleteExternalMessageFiles: MessageType.deleteAllExternalFiles({
|
||||||
deleteAttachmentData: Type.deleteData(deleteOnDisk),
|
deleteAttachmentData: Type.deleteData(deleteOnDisk),
|
||||||
deleteOnDisk,
|
deleteOnDisk,
|
||||||
}),
|
}),
|
||||||
|
deleteSticker,
|
||||||
getAbsoluteAttachmentPath,
|
getAbsoluteAttachmentPath,
|
||||||
|
getAbsoluteStickerPath,
|
||||||
getPlaceholderMigrations,
|
getPlaceholderMigrations,
|
||||||
getCurrentVersion,
|
getCurrentVersion,
|
||||||
loadAttachmentData,
|
loadAttachmentData,
|
||||||
loadMessage: MessageType.createAttachmentLoader(loadAttachmentData),
|
loadMessage: MessageType.createAttachmentLoader(loadAttachmentData),
|
||||||
loadPreviewData,
|
loadPreviewData,
|
||||||
loadQuoteData,
|
loadQuoteData,
|
||||||
|
loadStickerData,
|
||||||
readAttachmentData,
|
readAttachmentData,
|
||||||
|
readStickerData,
|
||||||
run,
|
run,
|
||||||
processNewAttachment: attachment =>
|
processNewAttachment: attachment =>
|
||||||
MessageType.processNewAttachment(attachment, {
|
MessageType.processNewAttachment(attachment, {
|
||||||
|
@ -161,6 +190,13 @@ function initializeMigrations({
|
||||||
makeVideoScreenshot,
|
makeVideoScreenshot,
|
||||||
logger,
|
logger,
|
||||||
}),
|
}),
|
||||||
|
processNewSticker: stickerData =>
|
||||||
|
MessageType.processNewSticker(stickerData, {
|
||||||
|
writeNewStickerData,
|
||||||
|
getAbsoluteStickerPath,
|
||||||
|
getImageDimensions,
|
||||||
|
logger,
|
||||||
|
}),
|
||||||
upgradeMessageSchema: (message, options = {}) => {
|
upgradeMessageSchema: (message, options = {}) => {
|
||||||
const { maxVersion } = options;
|
const { maxVersion } = options;
|
||||||
|
|
||||||
|
@ -227,10 +263,15 @@ exports.setup = (options = {}) => {
|
||||||
|
|
||||||
const Roots = {
|
const Roots = {
|
||||||
createLeftPane,
|
createLeftPane,
|
||||||
|
createStickerButton,
|
||||||
|
createStickerManager,
|
||||||
|
createStickerPreviewModal,
|
||||||
};
|
};
|
||||||
const Ducks = {
|
const Ducks = {
|
||||||
conversations: conversationsDuck,
|
conversations: conversationsDuck,
|
||||||
|
items: itemsDuck,
|
||||||
user: userDuck,
|
user: userDuck,
|
||||||
|
stickers: stickersDuck,
|
||||||
};
|
};
|
||||||
const State = {
|
const State = {
|
||||||
bindActionCreators,
|
bindActionCreators,
|
||||||
|
@ -278,6 +319,7 @@ exports.setup = (options = {}) => {
|
||||||
RefreshSenderCertificate,
|
RefreshSenderCertificate,
|
||||||
Settings,
|
Settings,
|
||||||
State,
|
State,
|
||||||
|
Stickers,
|
||||||
Types,
|
Types,
|
||||||
Util,
|
Util,
|
||||||
Views,
|
Views,
|
||||||
|
|
1
js/modules/stickers.d.ts
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export function maybeDeletePack(packId: string): Promise<void>;
|
495
js/modules/stickers.js
Normal file
|
@ -0,0 +1,495 @@
|
||||||
|
/* global
|
||||||
|
textsecure,
|
||||||
|
Signal,
|
||||||
|
log,
|
||||||
|
reduxStore,
|
||||||
|
reduxActions,
|
||||||
|
URL
|
||||||
|
*/
|
||||||
|
|
||||||
|
const BLESSED_PACKS = {};
|
||||||
|
|
||||||
|
const { isNumber, pick, reject, groupBy } = require('lodash');
|
||||||
|
const pMap = require('p-map');
|
||||||
|
const Queue = require('p-queue');
|
||||||
|
const qs = require('qs');
|
||||||
|
|
||||||
|
const { makeLookup } = require('../../ts/util/makeLookup');
|
||||||
|
const { base64ToArrayBuffer, deriveStickerPackKey } = require('./crypto');
|
||||||
|
const {
|
||||||
|
addStickerPackReference,
|
||||||
|
createOrUpdateSticker,
|
||||||
|
createOrUpdateStickerPack,
|
||||||
|
deleteStickerPack,
|
||||||
|
deleteStickerPackReference,
|
||||||
|
getAllStickerPacks,
|
||||||
|
getAllStickers,
|
||||||
|
getRecentStickers,
|
||||||
|
updateStickerPackStatus,
|
||||||
|
} = require('./data');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
BLESSED_PACKS,
|
||||||
|
copyStickerToAttachments,
|
||||||
|
deletePack,
|
||||||
|
deletePackReference,
|
||||||
|
downloadStickerPack,
|
||||||
|
getDataFromLink,
|
||||||
|
getInitialState,
|
||||||
|
getInstalledStickerPacks,
|
||||||
|
getSticker,
|
||||||
|
getStickerPack,
|
||||||
|
getStickerPackStatus,
|
||||||
|
load,
|
||||||
|
maybeDeletePack,
|
||||||
|
downloadQueuedPacks,
|
||||||
|
redactPackId,
|
||||||
|
};
|
||||||
|
|
||||||
|
let initialState = null;
|
||||||
|
let packsToDownload = null;
|
||||||
|
const downloadQueue = new Queue({ concurrency: 1 });
|
||||||
|
|
||||||
|
async function load() {
|
||||||
|
const [packs, recentStickers] = await Promise.all([
|
||||||
|
getPacksForRedux(),
|
||||||
|
getRecentStickersForRedux(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
initialState = {
|
||||||
|
packs,
|
||||||
|
recentStickers,
|
||||||
|
blessedPacks: BLESSED_PACKS,
|
||||||
|
};
|
||||||
|
|
||||||
|
packsToDownload = capturePacksToDownload(packs);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDataFromLink(link) {
|
||||||
|
const { hash } = new URL(link);
|
||||||
|
if (!hash) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = hash.slice(1);
|
||||||
|
const params = qs.parse(data);
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: params.pack_id,
|
||||||
|
key: params.pack_key,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getInstalledStickerPacks() {
|
||||||
|
const state = reduxStore.getState();
|
||||||
|
const { stickers } = state;
|
||||||
|
const { packs } = stickers;
|
||||||
|
if (!packs) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const values = Object.values(packs);
|
||||||
|
return values.filter(pack => pack.status === 'installed');
|
||||||
|
}
|
||||||
|
|
||||||
|
function downloadQueuedPacks() {
|
||||||
|
const ids = Object.keys(packsToDownload);
|
||||||
|
ids.forEach(id => {
|
||||||
|
const { key, status } = packsToDownload[id];
|
||||||
|
|
||||||
|
// The queuing is done inside this function, no need to await here
|
||||||
|
downloadStickerPack(id, key, { finalStatus: status });
|
||||||
|
});
|
||||||
|
|
||||||
|
packsToDownload = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
function capturePacksToDownload(existingPackLookup) {
|
||||||
|
const toDownload = Object.create(null);
|
||||||
|
|
||||||
|
// First, ensure that blessed packs are in good shape
|
||||||
|
const blessedIds = Object.keys(BLESSED_PACKS);
|
||||||
|
blessedIds.forEach(id => {
|
||||||
|
const existing = existingPackLookup[id];
|
||||||
|
if (
|
||||||
|
!existing ||
|
||||||
|
(existing.status !== 'advertised' && existing.status !== 'installed')
|
||||||
|
) {
|
||||||
|
toDownload[id] = {
|
||||||
|
id,
|
||||||
|
...BLESSED_PACKS[id],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Then, find error cases in packs we already know about
|
||||||
|
const existingIds = Object.keys(existingPackLookup);
|
||||||
|
existingIds.forEach(id => {
|
||||||
|
if (toDownload[id]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const existing = existingPackLookup[id];
|
||||||
|
if (doesPackNeedDownload(existing)) {
|
||||||
|
toDownload[id] = {
|
||||||
|
id,
|
||||||
|
key: existing.key,
|
||||||
|
status: existing.attemptedStatus,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return toDownload;
|
||||||
|
}
|
||||||
|
|
||||||
|
function doesPackNeedDownload(pack) {
|
||||||
|
if (!pack) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const stickerCount = Object.keys(pack.stickers || {}).length;
|
||||||
|
return (
|
||||||
|
!pack.status ||
|
||||||
|
pack.status === 'error' ||
|
||||||
|
pack.status === 'pending' ||
|
||||||
|
!pack.stickerCount ||
|
||||||
|
stickerCount < pack.stickerCount
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getPacksForRedux() {
|
||||||
|
const [packs, stickers] = await Promise.all([
|
||||||
|
getAllStickerPacks(),
|
||||||
|
getAllStickers(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const stickersByPack = groupBy(stickers, sticker => sticker.packId);
|
||||||
|
const fullSet = packs.map(pack => ({
|
||||||
|
...pack,
|
||||||
|
stickers: makeLookup(stickersByPack[pack.id] || [], 'id'),
|
||||||
|
}));
|
||||||
|
|
||||||
|
return makeLookup(fullSet, 'id');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getRecentStickersForRedux() {
|
||||||
|
const recent = await getRecentStickers();
|
||||||
|
return recent.map(sticker => ({
|
||||||
|
packId: sticker.packId,
|
||||||
|
stickerId: sticker.id,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
function getInitialState() {
|
||||||
|
return initialState;
|
||||||
|
}
|
||||||
|
|
||||||
|
function redactPackId(packId) {
|
||||||
|
return `[REDACTED]${packId.slice(-3)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getReduxStickerActions() {
|
||||||
|
const actions = reduxActions;
|
||||||
|
|
||||||
|
if (actions && actions.stickers) {
|
||||||
|
return actions.stickers;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function decryptSticker(packKey, ciphertext) {
|
||||||
|
const binaryKey = base64ToArrayBuffer(packKey);
|
||||||
|
const derivedKey = await deriveStickerPackKey(binaryKey);
|
||||||
|
const plaintext = await textsecure.crypto.decryptAttachment(
|
||||||
|
ciphertext,
|
||||||
|
derivedKey
|
||||||
|
);
|
||||||
|
|
||||||
|
return plaintext;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function downloadSticker(packId, packKey, proto) {
|
||||||
|
const ciphertext = await textsecure.messaging.getSticker(packId, proto.id);
|
||||||
|
const plaintext = await decryptSticker(packKey, ciphertext);
|
||||||
|
const sticker = await Signal.Migrations.processNewSticker(plaintext);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...pick(proto, ['id', 'emoji']),
|
||||||
|
...sticker,
|
||||||
|
packId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
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 () => {
|
||||||
|
try {
|
||||||
|
await doDownloadStickerPack(packId, packKey, options);
|
||||||
|
} catch (error) {
|
||||||
|
log.error(
|
||||||
|
'doDownloadStickerPack threw an error:',
|
||||||
|
error && error.stack ? error.stack : error
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function doDownloadStickerPack(packId, packKey, options = {}) {
|
||||||
|
const { messageId, fromSync } = options;
|
||||||
|
const {
|
||||||
|
stickerAdded,
|
||||||
|
stickerPackAdded,
|
||||||
|
stickerPackUpdated,
|
||||||
|
installStickerPack,
|
||||||
|
} = getReduxStickerActions();
|
||||||
|
|
||||||
|
const finalStatus = options.finalStatus || 'advertised';
|
||||||
|
|
||||||
|
const existing = getStickerPack(packId);
|
||||||
|
if (!doesPackNeedDownload(existing)) {
|
||||||
|
log.warn(
|
||||||
|
`Download for pack ${redactPackId(
|
||||||
|
packId
|
||||||
|
)} requested, but it does not need re-download. Skipping.`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const downloadAttempts = (existing ? existing.downloadAttempts || 0 : 0) + 1;
|
||||||
|
if (downloadAttempts > 3) {
|
||||||
|
log.warn(
|
||||||
|
`Refusing to attempt another download for pack ${redactPackId(
|
||||||
|
packId
|
||||||
|
)}, attempt number ${downloadAttempts}`
|
||||||
|
);
|
||||||
|
|
||||||
|
if (existing.status !== 'error') {
|
||||||
|
await updateStickerPackStatus(packId, 'error');
|
||||||
|
stickerPackUpdated(packId, {
|
||||||
|
status: 'error',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let coverProto;
|
||||||
|
let coverStickerId;
|
||||||
|
let coverIncludedInList;
|
||||||
|
let nonCoverStickers;
|
||||||
|
|
||||||
|
try {
|
||||||
|
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;
|
||||||
|
|
||||||
|
coverProto = proto.cover || firstStickerProto;
|
||||||
|
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`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
nonCoverStickers = reject(
|
||||||
|
proto.stickers,
|
||||||
|
sticker => !isNumber(sticker.id) || sticker.id === coverStickerId
|
||||||
|
);
|
||||||
|
|
||||||
|
coverIncludedInList = nonCoverStickers.length < stickerCount;
|
||||||
|
|
||||||
|
// status can be:
|
||||||
|
// - 'pending'
|
||||||
|
// - 'advertised'
|
||||||
|
// - 'error'
|
||||||
|
// - 'installed'
|
||||||
|
const pack = {
|
||||||
|
id: packId,
|
||||||
|
key: packKey,
|
||||||
|
attemptedStatus: finalStatus,
|
||||||
|
coverStickerId,
|
||||||
|
downloadAttempts,
|
||||||
|
stickerCount,
|
||||||
|
status: 'pending',
|
||||||
|
...pick(proto, ['title', 'author']),
|
||||||
|
};
|
||||||
|
await createOrUpdateStickerPack(pack);
|
||||||
|
stickerPackAdded(pack);
|
||||||
|
|
||||||
|
if (messageId) {
|
||||||
|
await addStickerPackReference(messageId, packId);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
log.error(
|
||||||
|
`Error downloading manifest for sticker pack ${redactPackId(packId)}:`,
|
||||||
|
error && error.stack ? error.stack : error
|
||||||
|
);
|
||||||
|
|
||||||
|
const pack = {
|
||||||
|
id: packId,
|
||||||
|
key: packKey,
|
||||||
|
attemptedStatus: finalStatus,
|
||||||
|
downloadAttempts,
|
||||||
|
status: 'error',
|
||||||
|
};
|
||||||
|
await createOrUpdateStickerPack(pack);
|
||||||
|
stickerPackAdded(pack);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We have a separate try/catch here because we're starting to download stickers here
|
||||||
|
// and we want to preserve more of the pack on an error.
|
||||||
|
try {
|
||||||
|
const downloadStickerJob = async stickerProto => {
|
||||||
|
const stickerInfo = await downloadSticker(packId, packKey, stickerProto);
|
||||||
|
const sticker = {
|
||||||
|
...stickerInfo,
|
||||||
|
isCoverOnly: !coverIncludedInList && stickerInfo.id === coverStickerId,
|
||||||
|
};
|
||||||
|
await createOrUpdateSticker(sticker);
|
||||||
|
stickerAdded(sticker);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Download the cover first
|
||||||
|
await downloadStickerJob(coverProto);
|
||||||
|
|
||||||
|
// Then the rest
|
||||||
|
await pMap(nonCoverStickers, downloadStickerJob, { concurrency: 3 });
|
||||||
|
|
||||||
|
if (finalStatus === 'installed') {
|
||||||
|
await installStickerPack(packId, packKey, { fromSync });
|
||||||
|
} else {
|
||||||
|
// Mark the pack as complete
|
||||||
|
await updateStickerPackStatus(packId, finalStatus);
|
||||||
|
stickerPackUpdated(packId, {
|
||||||
|
status: finalStatus,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
log.error(
|
||||||
|
`Error downloading stickers for sticker pack ${redactPackId(packId)}:`,
|
||||||
|
error && error.stack ? error.stack : error
|
||||||
|
);
|
||||||
|
|
||||||
|
const errorState = 'error';
|
||||||
|
await updateStickerPackStatus(packId, errorState);
|
||||||
|
if (stickerPackUpdated) {
|
||||||
|
stickerPackUpdated(packId, {
|
||||||
|
state: errorState,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStickerPack(packId) {
|
||||||
|
const state = reduxStore.getState();
|
||||||
|
const { stickers } = state;
|
||||||
|
const { packs } = stickers;
|
||||||
|
if (!packs) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return packs[packId];
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStickerPackStatus(packId) {
|
||||||
|
const pack = getStickerPack(packId);
|
||||||
|
if (!pack) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return pack.status;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSticker(packId, stickerId) {
|
||||||
|
const state = reduxStore.getState();
|
||||||
|
const { stickers } = state;
|
||||||
|
const { packs } = stickers;
|
||||||
|
const pack = packs[packId];
|
||||||
|
|
||||||
|
if (!pack || !pack.stickers) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return pack.stickers[stickerId];
|
||||||
|
}
|
||||||
|
|
||||||
|
async function copyStickerToAttachments(packId, stickerId) {
|
||||||
|
const sticker = getSticker(packId, stickerId);
|
||||||
|
if (!sticker) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { path } = sticker;
|
||||||
|
const absolutePath = Signal.Migrations.getAbsoluteStickerPath(path);
|
||||||
|
const newPath = await Signal.Migrations.copyIntoAttachmentsDirectory(
|
||||||
|
absolutePath
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...sticker,
|
||||||
|
path: newPath,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// In the case where a sticker pack is uninstalled, we want to delete it if there are no
|
||||||
|
// more references left. We'll delete a nonexistent reference, then check if there are
|
||||||
|
// any references left, just like usual.
|
||||||
|
async function maybeDeletePack(packId) {
|
||||||
|
// This hardcoded string is fine because message ids are GUIDs
|
||||||
|
await deletePackReference('NOT-USED', packId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// We don't generally delete packs outright; we just remove references to them, and if
|
||||||
|
// the last reference is deleted, we finally then remove the pack itself from database
|
||||||
|
// and from disk.
|
||||||
|
async function deletePackReference(messageId, packId) {
|
||||||
|
const isBlessed = Boolean(BLESSED_PACKS[packId]);
|
||||||
|
if (isBlessed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// This call uses locking to prevent race conditions with other reference removals,
|
||||||
|
// or an incoming message creating a new message->pack reference
|
||||||
|
const paths = await deleteStickerPackReference(messageId, packId);
|
||||||
|
|
||||||
|
// If we don't get a list of paths back, then the sticker pack was not deleted
|
||||||
|
if (!paths) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { removeStickerPack } = getReduxStickerActions();
|
||||||
|
removeStickerPack(packId);
|
||||||
|
|
||||||
|
await pMap(paths, Signal.Migrations.deleteSticker, {
|
||||||
|
concurrency: 3,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// The override; doesn't honor our ref-counting scheme - just deletes it all.
|
||||||
|
async function deletePack(packId) {
|
||||||
|
const isBlessed = Boolean(BLESSED_PACKS[packId]);
|
||||||
|
if (isBlessed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// This call uses locking to prevent race conditions with other reference removals,
|
||||||
|
// or an incoming message creating a new message->pack reference
|
||||||
|
const paths = await deleteStickerPack(packId);
|
||||||
|
|
||||||
|
const { removeStickerPack } = getReduxStickerActions();
|
||||||
|
removeStickerPack(packId);
|
||||||
|
|
||||||
|
await pMap(paths, Signal.Migrations.deleteSticker, {
|
||||||
|
concurrency: 3,
|
||||||
|
});
|
||||||
|
}
|
|
@ -179,7 +179,7 @@ exports.loadData = readAttachmentData => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await readAttachmentData(attachment.path);
|
const data = await readAttachmentData(attachment.path);
|
||||||
return Object.assign({}, attachment, { data });
|
return Object.assign({}, attachment, { data, size: data.byteLength });
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -140,11 +140,12 @@ async function deleteExternalFiles(conversation, options = {}) {
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
deleteExternalFiles,
|
|
||||||
migrateConversation,
|
|
||||||
maybeUpdateAvatar,
|
|
||||||
maybeUpdateProfileAvatar,
|
|
||||||
createLastMessageUpdate,
|
|
||||||
arrayBufferToBase64,
|
arrayBufferToBase64,
|
||||||
base64ToArrayBuffer,
|
base64ToArrayBuffer,
|
||||||
|
computeHash,
|
||||||
|
createLastMessageUpdate,
|
||||||
|
deleteExternalFiles,
|
||||||
|
maybeUpdateAvatar,
|
||||||
|
maybeUpdateProfileAvatar,
|
||||||
|
migrateConversation,
|
||||||
};
|
};
|
||||||
|
|
|
@ -308,7 +308,33 @@ const toVersion9 = exports._withSchemaVersion({
|
||||||
});
|
});
|
||||||
const toVersion10 = exports._withSchemaVersion({
|
const toVersion10 = exports._withSchemaVersion({
|
||||||
schemaVersion: 10,
|
schemaVersion: 10,
|
||||||
upgrade: exports._mapPreviewAttachments(Attachment.migrateDataToFileSystem),
|
upgrade: async (message, context) => {
|
||||||
|
const processPreviews = exports._mapPreviewAttachments(
|
||||||
|
Attachment.migrateDataToFileSystem
|
||||||
|
);
|
||||||
|
const processSticker = async (stickerMessage, stickerContext) => {
|
||||||
|
const { sticker } = stickerMessage;
|
||||||
|
if (!sticker || !sticker.data || !sticker.data.data) {
|
||||||
|
return stickerMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...stickerMessage,
|
||||||
|
sticker: {
|
||||||
|
...sticker,
|
||||||
|
data: await Attachment.migrateDataToFileSystem(
|
||||||
|
sticker.data,
|
||||||
|
stickerContext
|
||||||
|
),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const previewProcessed = await processPreviews(message, context);
|
||||||
|
const stickerProcessed = await processSticker(previewProcessed, context);
|
||||||
|
|
||||||
|
return stickerProcessed;
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const VERSIONS = [
|
const VERSIONS = [
|
||||||
|
@ -462,6 +488,44 @@ exports.processNewAttachment = async (
|
||||||
return finalAttachment;
|
return finalAttachment;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
exports.processNewSticker = async (
|
||||||
|
stickerData,
|
||||||
|
{
|
||||||
|
writeNewStickerData,
|
||||||
|
getAbsoluteStickerPath,
|
||||||
|
getImageDimensions,
|
||||||
|
logger,
|
||||||
|
} = {}
|
||||||
|
) => {
|
||||||
|
if (!isFunction(writeNewStickerData)) {
|
||||||
|
throw new TypeError('context.writeNewStickerData is required');
|
||||||
|
}
|
||||||
|
if (!isFunction(getAbsoluteStickerPath)) {
|
||||||
|
throw new TypeError('context.getAbsoluteStickerPath is required');
|
||||||
|
}
|
||||||
|
if (!isFunction(getImageDimensions)) {
|
||||||
|
throw new TypeError('context.getImageDimensions is required');
|
||||||
|
}
|
||||||
|
if (!isObject(logger)) {
|
||||||
|
throw new TypeError('context.logger is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
const path = await writeNewStickerData(stickerData);
|
||||||
|
const absolutePath = await getAbsoluteStickerPath(path);
|
||||||
|
|
||||||
|
const { width, height } = await getImageDimensions({
|
||||||
|
objectUrl: absolutePath,
|
||||||
|
logger,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
contentType: 'image/webp',
|
||||||
|
path,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
exports.createAttachmentLoader = loadAttachmentData => {
|
exports.createAttachmentLoader = loadAttachmentData => {
|
||||||
if (!isFunction(loadAttachmentData)) {
|
if (!isFunction(loadAttachmentData)) {
|
||||||
throw new TypeError(
|
throw new TypeError(
|
||||||
|
@ -532,6 +596,23 @@ exports.loadPreviewData = loadAttachmentData => {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
exports.loadStickerData = loadAttachmentData => {
|
||||||
|
if (!isFunction(loadAttachmentData)) {
|
||||||
|
throw new TypeError('loadStickerData: loadAttachmentData is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
return async sticker => {
|
||||||
|
if (!sticker || !sticker.data) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...sticker,
|
||||||
|
data: await loadAttachmentData(sticker.data),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
exports.deleteAllExternalFiles = ({ deleteAttachmentData, deleteOnDisk }) => {
|
exports.deleteAllExternalFiles = ({ deleteAttachmentData, deleteOnDisk }) => {
|
||||||
if (!isFunction(deleteAttachmentData)) {
|
if (!isFunction(deleteAttachmentData)) {
|
||||||
throw new TypeError(
|
throw new TypeError(
|
||||||
|
@ -546,7 +627,7 @@ exports.deleteAllExternalFiles = ({ deleteAttachmentData, deleteOnDisk }) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
return async message => {
|
return async message => {
|
||||||
const { attachments, quote, contact, preview } = message;
|
const { attachments, quote, contact, preview, sticker } = message;
|
||||||
|
|
||||||
if (attachments && attachments.length) {
|
if (attachments && attachments.length) {
|
||||||
await Promise.all(attachments.map(deleteAttachmentData));
|
await Promise.all(attachments.map(deleteAttachmentData));
|
||||||
|
@ -590,6 +671,14 @@ exports.deleteAllExternalFiles = ({ deleteAttachmentData, deleteOnDisk }) => {
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (sticker && sticker.data && sticker.data.path) {
|
||||||
|
await deleteOnDisk(sticker.data.path);
|
||||||
|
|
||||||
|
if (sticker.data.thumbnail && sticker.data.thumbnail.path) {
|
||||||
|
await deleteOnDisk(sticker.data.thumbnail.path);
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -4,8 +4,9 @@ const ProxyAgent = require('proxy-agent');
|
||||||
const { Agent } = require('https');
|
const { Agent } = require('https');
|
||||||
|
|
||||||
const is = require('@sindresorhus/is');
|
const is = require('@sindresorhus/is');
|
||||||
|
const { redactPackId } = require('./stickers');
|
||||||
|
|
||||||
/* global Buffer, setTimeout, log, _, getGuid */
|
/* global Signal, Buffer, setTimeout, log, _, getGuid */
|
||||||
|
|
||||||
/* eslint-disable more/no-then, no-bitwise, no-nested-ternary */
|
/* eslint-disable more/no-then, no-bitwise, no-nested-ternary */
|
||||||
|
|
||||||
|
@ -175,16 +176,12 @@ function getContentType(response) {
|
||||||
function _promiseAjax(providedUrl, options) {
|
function _promiseAjax(providedUrl, options) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const url = providedUrl || `${options.host}/${options.path}`;
|
const url = providedUrl || `${options.host}/${options.path}`;
|
||||||
if (options.disableLogs) {
|
|
||||||
log.info(
|
const unauthLabel = options.unauthenticated ? ' (unauth)' : '';
|
||||||
`${options.type} [REDACTED_URL]${
|
if (options.redactUrl) {
|
||||||
options.unauthenticated ? ' (unauth)' : ''
|
log.info(`${options.type} ${options.redactUrl(url)}${unauthLabel}`);
|
||||||
}`
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
log.info(
|
log.info(`${options.type} ${url}${unauthLabel}`);
|
||||||
`${options.type} ${url}${options.unauthenticated ? ' (unauth)' : ''}`
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const timeout =
|
const timeout =
|
||||||
|
@ -282,10 +279,10 @@ function _promiseAjax(providedUrl, options) {
|
||||||
if (options.responseType === 'json') {
|
if (options.responseType === 'json') {
|
||||||
if (options.validateResponse) {
|
if (options.validateResponse) {
|
||||||
if (!_validateResponse(result, options.validateResponse)) {
|
if (!_validateResponse(result, options.validateResponse)) {
|
||||||
if (options.disableLogs) {
|
if (options.redactUrl) {
|
||||||
log.info(
|
log.info(
|
||||||
options.type,
|
options.type,
|
||||||
'[REDACTED_URL]',
|
options.redactUrl(url),
|
||||||
response.status,
|
response.status,
|
||||||
'Error'
|
'Error'
|
||||||
);
|
);
|
||||||
|
@ -304,10 +301,10 @@ function _promiseAjax(providedUrl, options) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (response.status >= 0 && response.status < 400) {
|
if (response.status >= 0 && response.status < 400) {
|
||||||
if (options.disableLogs) {
|
if (options.redactUrl) {
|
||||||
log.info(
|
log.info(
|
||||||
options.type,
|
options.type,
|
||||||
'[REDACTED_URL]',
|
options.redactUrl(url),
|
||||||
response.status,
|
response.status,
|
||||||
'Success'
|
'Success'
|
||||||
);
|
);
|
||||||
|
@ -324,8 +321,13 @@ function _promiseAjax(providedUrl, options) {
|
||||||
return resolve(result, response.status);
|
return resolve(result, response.status);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.disableLogs) {
|
if (options.redactUrl) {
|
||||||
log.info(options.type, '[REDACTED_URL]', response.status, 'Error');
|
log.info(
|
||||||
|
options.type,
|
||||||
|
options.redactUrl(url),
|
||||||
|
response.status,
|
||||||
|
'Error'
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
log.error(options.type, url, response.status, 'Error');
|
log.error(options.type, url, response.status, 'Error');
|
||||||
}
|
}
|
||||||
|
@ -340,8 +342,8 @@ function _promiseAjax(providedUrl, options) {
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch(e => {
|
.catch(e => {
|
||||||
if (options.disableLogs) {
|
if (options.redactUrl) {
|
||||||
log.error(options.type, '[REDACTED_URL]', 0, 'Error');
|
log.error(options.type, options.redactUrl(url), 0, 'Error');
|
||||||
} else {
|
} else {
|
||||||
log.error(options.type, url, 0, 'Error');
|
log.error(options.type, url, 0, 'Error');
|
||||||
}
|
}
|
||||||
|
@ -435,6 +437,7 @@ function initialize({
|
||||||
function connect({ username: initialUsername, password: initialPassword }) {
|
function connect({ username: initialUsername, password: initialPassword }) {
|
||||||
let username = initialUsername;
|
let username = initialUsername;
|
||||||
let password = initialPassword;
|
let password = initialPassword;
|
||||||
|
const PARSE_RANGE_HEADER = /\/(\d+)$/;
|
||||||
|
|
||||||
// Thanks, function hoisting!
|
// Thanks, function hoisting!
|
||||||
return {
|
return {
|
||||||
|
@ -449,8 +452,9 @@ function initialize({
|
||||||
getProfile,
|
getProfile,
|
||||||
getProfileUnauth,
|
getProfileUnauth,
|
||||||
getProvisioningSocket,
|
getProvisioningSocket,
|
||||||
getProxiedSize,
|
|
||||||
getSenderCertificate,
|
getSenderCertificate,
|
||||||
|
getSticker,
|
||||||
|
getStickerPackManifest,
|
||||||
makeProxiedRequest,
|
makeProxiedRequest,
|
||||||
putAttachment,
|
putAttachment,
|
||||||
registerKeys,
|
registerKeys,
|
||||||
|
@ -834,6 +838,33 @@ function initialize({
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function redactStickerUrl(stickerUrl) {
|
||||||
|
return stickerUrl.replace(
|
||||||
|
/(\/stickers\/)([^/]+)(\/)/,
|
||||||
|
(match, begin, packId, end) => `${begin}${redactPackId(packId)}${end}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSticker(packId, stickerId) {
|
||||||
|
return _outerAjax(`${cdnUrl}/stickers/${packId}/full/${stickerId}`, {
|
||||||
|
certificateAuthority,
|
||||||
|
proxyUrl,
|
||||||
|
responseType: 'arraybuffer',
|
||||||
|
type: 'GET',
|
||||||
|
redactUrl: redactStickerUrl,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStickerPackManifest(packId) {
|
||||||
|
return _outerAjax(`${cdnUrl}/stickers/${packId}/manifest.proto`, {
|
||||||
|
certificateAuthority,
|
||||||
|
proxyUrl,
|
||||||
|
responseType: 'arraybuffer',
|
||||||
|
type: 'GET',
|
||||||
|
redactUrl: redactStickerUrl,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async function getAttachment(id) {
|
async function getAttachment(id) {
|
||||||
// This is going to the CDN, not the service, so we use _outerAjax
|
// This is going to the CDN, not the service, so we use _outerAjax
|
||||||
return _outerAjax(`${cdnUrl}/attachments/${id}`, {
|
return _outerAjax(`${cdnUrl}/attachments/${id}`, {
|
||||||
|
@ -918,45 +949,64 @@ function initialize({
|
||||||
return attachmentIdString;
|
return attachmentIdString;
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line no-shadow
|
function getHeaderPadding() {
|
||||||
async function getProxiedSize(url) {
|
const length = Signal.Crypto.getRandomValue(1, 64);
|
||||||
const result = await _outerAjax(url, {
|
let characters = '';
|
||||||
processData: false,
|
|
||||||
responseType: 'arraybufferwithdetails',
|
|
||||||
proxyUrl: contentProxyUrl,
|
|
||||||
type: 'HEAD',
|
|
||||||
disableLogs: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
const { response } = result;
|
for (let i = 0, max = length; i < max; i += 1) {
|
||||||
if (!response.headers || !response.headers.get) {
|
characters += String.fromCharCode(
|
||||||
throw new Error('getProxiedSize: Problem retrieving header value');
|
Signal.Crypto.getRandomValue(65, 122)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const size = response.headers.get('content-length');
|
return characters;
|
||||||
return parseInt(size, 10);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line no-shadow
|
// eslint-disable-next-line no-shadow
|
||||||
function makeProxiedRequest(url, options = {}) {
|
async function makeProxiedRequest(url, options = {}) {
|
||||||
const { returnArrayBuffer, start, end } = options;
|
const { returnArrayBuffer, start, end } = options;
|
||||||
let headers;
|
const headers = {
|
||||||
|
'X-SignalPadding': getHeaderPadding(),
|
||||||
|
};
|
||||||
|
|
||||||
if (_.isNumber(start) && _.isNumber(end)) {
|
if (_.isNumber(start) && _.isNumber(end)) {
|
||||||
headers = {
|
headers.Range = `bytes=${start}-${end}`;
|
||||||
Range: `bytes=${start}-${end}`,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return _outerAjax(url, {
|
const result = await _outerAjax(url, {
|
||||||
processData: false,
|
processData: false,
|
||||||
responseType: returnArrayBuffer ? 'arraybufferwithdetails' : null,
|
responseType: returnArrayBuffer ? 'arraybufferwithdetails' : null,
|
||||||
proxyUrl: contentProxyUrl,
|
proxyUrl: contentProxyUrl,
|
||||||
type: 'GET',
|
type: 'GET',
|
||||||
redirect: 'follow',
|
redirect: 'follow',
|
||||||
disableLogs: true,
|
redactUrl: () => '[REDACTED_URL]',
|
||||||
headers,
|
headers,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!returnArrayBuffer) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { response } = result;
|
||||||
|
if (!response.headers || !response.headers.get) {
|
||||||
|
throw new Error('makeProxiedRequest: Problem retrieving header value');
|
||||||
|
}
|
||||||
|
|
||||||
|
const range = response.headers.get('content-range');
|
||||||
|
const match = PARSE_RANGE_HEADER.exec(range);
|
||||||
|
|
||||||
|
if (!match || !match[1]) {
|
||||||
|
throw new Error(
|
||||||
|
`makeProxiedRequest: Unable to parse total size from ${range}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalSize = parseInt(match[1], 10);
|
||||||
|
|
||||||
|
return {
|
||||||
|
totalSize,
|
||||||
|
result,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function getMessageSocket() {
|
function getMessageSocket() {
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
/* global _ */
|
||||||
/* eslint-disable more/no-then */
|
/* eslint-disable more/no-then */
|
||||||
|
|
||||||
// eslint-disable-next-line func-names
|
// eslint-disable-next-line func-names
|
||||||
|
@ -24,6 +25,10 @@
|
||||||
|
|
||||||
items[key] = data;
|
items[key] = data;
|
||||||
await window.Signal.Data.createOrUpdateItem(data);
|
await window.Signal.Data.createOrUpdateItem(data);
|
||||||
|
|
||||||
|
if (_.has(window, ['reduxActions', 'items', 'putItemExternal'])) {
|
||||||
|
window.reduxActions.items.putItemExternal(key, value);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function get(key, defaultValue) {
|
function get(key, defaultValue) {
|
||||||
|
@ -46,6 +51,10 @@
|
||||||
|
|
||||||
delete items[key];
|
delete items[key];
|
||||||
await window.Signal.Data.removeItemById(key);
|
await window.Signal.Data.removeItemById(key);
|
||||||
|
|
||||||
|
if (_.has(window, ['reduxActions', 'items', 'removeItemExternal'])) {
|
||||||
|
window.reduxActions.items.removeItemExternal(key);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function onready(callback) {
|
function onready(callback) {
|
||||||
|
@ -77,6 +86,10 @@
|
||||||
callListeners();
|
callListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getItemsState() {
|
||||||
|
return _.clone(items);
|
||||||
|
}
|
||||||
|
|
||||||
function reset() {
|
function reset() {
|
||||||
ready = false;
|
ready = false;
|
||||||
items = Object.create(null);
|
items = Object.create(null);
|
||||||
|
@ -86,6 +99,7 @@
|
||||||
fetch,
|
fetch,
|
||||||
put,
|
put,
|
||||||
get,
|
get,
|
||||||
|
getItemsState,
|
||||||
remove,
|
remove,
|
||||||
onready,
|
onready,
|
||||||
reset,
|
reset,
|
||||||
|
|
|
@ -104,6 +104,9 @@
|
||||||
);
|
);
|
||||||
this.listenTo(this.model.messageCollection, 'force-send', this.forceSend);
|
this.listenTo(this.model.messageCollection, 'force-send', this.forceSend);
|
||||||
this.listenTo(this.model.messageCollection, 'delete', this.deleteMessage);
|
this.listenTo(this.model.messageCollection, 'delete', this.deleteMessage);
|
||||||
|
this.listenTo(this.model.messageCollection, 'height-changed', () =>
|
||||||
|
this.view.scrollToBottomIfNeeded()
|
||||||
|
);
|
||||||
this.listenTo(
|
this.listenTo(
|
||||||
this.model.messageCollection,
|
this.model.messageCollection,
|
||||||
'scroll-to-message',
|
'scroll-to-message',
|
||||||
|
@ -276,15 +279,18 @@
|
||||||
this.$('.send-message').blur(this.unfocusBottomBar.bind(this));
|
this.$('.send-message').blur(this.unfocusBottomBar.bind(this));
|
||||||
|
|
||||||
this.$emojiPanelContainer = this.$('.emoji-panel-container');
|
this.$emojiPanelContainer = this.$('.emoji-panel-container');
|
||||||
|
|
||||||
|
this.setupStickerPickerButton();
|
||||||
},
|
},
|
||||||
|
|
||||||
events: {
|
events: {
|
||||||
keydown: 'onKeyDown',
|
keydown: 'onKeyDown',
|
||||||
'submit .send': 'checkUnverifiedSendMessage',
|
'submit .send': 'clickSend',
|
||||||
'input .send-message': 'updateMessageFieldSize',
|
'input .send-message': 'updateMessageFieldSize',
|
||||||
'keydown .send-message': 'updateMessageFieldSize',
|
'keydown .send-message': 'updateMessageFieldSize',
|
||||||
'keyup .send-message': 'onKeyUp',
|
'keyup .send-message': 'onKeyUp',
|
||||||
click: 'onClick',
|
click: 'onClick',
|
||||||
|
'click .sticker-button-placeholder': 'onClickStickerButtonPlaceholder',
|
||||||
'click .bottom-bar': 'focusMessageField',
|
'click .bottom-bar': 'focusMessageField',
|
||||||
'click .capture-audio .microphone': 'captureAudio',
|
'click .capture-audio .microphone': 'captureAudio',
|
||||||
'click .module-scroll-down': 'scrollToBottom',
|
'click .module-scroll-down': 'scrollToBottom',
|
||||||
|
@ -308,6 +314,28 @@
|
||||||
paste: 'onPaste',
|
paste: 'onPaste',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
setupStickerPickerButton() {
|
||||||
|
const props = {
|
||||||
|
onClickAddPack: () => this.showStickerManager(),
|
||||||
|
onPickSticker: (packId, stickerId) =>
|
||||||
|
this.sendStickerMessage({ packId, stickerId }),
|
||||||
|
};
|
||||||
|
|
||||||
|
this.stickerButtonView = new Whisper.ReactWrapperView({
|
||||||
|
className: 'sticker-button-wrapper',
|
||||||
|
JSX: Signal.State.Roots.createStickerButton(window.reduxStore, props),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Finally, add it to the DOM
|
||||||
|
this.$('.sticker-button-placeholder').append(this.stickerButtonView.el);
|
||||||
|
},
|
||||||
|
|
||||||
|
// We need this, or clicking the sticker button will submit the form and send any
|
||||||
|
// mid-composition message content.
|
||||||
|
onClickStickerButtonPlaceholder(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
},
|
||||||
|
|
||||||
onChooseAttachment(e) {
|
onChooseAttachment(e) {
|
||||||
if (e) {
|
if (e) {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
@ -366,6 +394,13 @@
|
||||||
|
|
||||||
this.fileInput.remove();
|
this.fileInput.remove();
|
||||||
this.titleView.remove();
|
this.titleView.remove();
|
||||||
|
if (this.stickerButtonView) {
|
||||||
|
this.stickerButtonView.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.stickerPreviewModalView) {
|
||||||
|
this.stickerPreviewModalView.remove();
|
||||||
|
}
|
||||||
|
|
||||||
if (this.captureAudioView) {
|
if (this.captureAudioView) {
|
||||||
this.captureAudioView.remove();
|
this.captureAudioView.remove();
|
||||||
|
@ -1282,6 +1317,26 @@
|
||||||
dialog.focusCancel();
|
dialog.focusCancel();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
showStickerPackPreview(packId) {
|
||||||
|
const props = {
|
||||||
|
packId,
|
||||||
|
onClose: () => {
|
||||||
|
this.stickerPreviewModalView.remove();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
this.stickerPreviewModalView = new Whisper.ReactWrapperView({
|
||||||
|
className: 'sticker-preview-modal-wrapper',
|
||||||
|
JSX: Signal.State.Roots.createStickerPreviewModal(
|
||||||
|
window.reduxStore,
|
||||||
|
props
|
||||||
|
),
|
||||||
|
onClose: () => {
|
||||||
|
this.stickerPreviewModalView = null;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
showLightbox({ attachment, messageId }) {
|
showLightbox({ attachment, messageId }) {
|
||||||
const message = this.model.messageCollection.get(messageId);
|
const message = this.model.messageCollection.get(messageId);
|
||||||
if (!message) {
|
if (!message) {
|
||||||
|
@ -1289,6 +1344,13 @@
|
||||||
`showLightbox: did not find message for id ${messageId}`
|
`showLightbox: did not find message for id ${messageId}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
const sticker = message.get('sticker');
|
||||||
|
if (sticker) {
|
||||||
|
const { packId } = sticker;
|
||||||
|
this.showStickerPackPreview(packId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const { contentType, path } = attachment;
|
const { contentType, path } = attachment;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
@ -1400,6 +1462,21 @@
|
||||||
view.render();
|
view.render();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
showStickerManager() {
|
||||||
|
const view = new Whisper.ReactWrapperView({
|
||||||
|
className: ['sticker-manager-wrapper', 'panel'].join(' '),
|
||||||
|
JSX: Signal.State.Roots.createStickerManager(window.reduxStore),
|
||||||
|
onClose: () => {
|
||||||
|
this.resetPanel();
|
||||||
|
this.updateHeader();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
this.listenBack(view);
|
||||||
|
this.updateHeader();
|
||||||
|
view.render();
|
||||||
|
},
|
||||||
|
|
||||||
showContactDetail({ contact, signalAccount }) {
|
showContactDetail({ contact, signalAccount }) {
|
||||||
const view = new Whisper.ReactWrapperView({
|
const view = new Whisper.ReactWrapperView({
|
||||||
Component: Signal.Components.ContactDetail,
|
Component: Signal.Components.ContactDetail,
|
||||||
|
@ -1449,6 +1526,8 @@
|
||||||
|
|
||||||
if (this.panels.length === 0) {
|
if (this.panels.length === 0) {
|
||||||
this.$el.trigger('force-resize');
|
this.$el.trigger('force-resize');
|
||||||
|
// Make sure poppers are positioned properly
|
||||||
|
window.dispatchEvent(new Event('resize'));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -1482,7 +1561,8 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
showSendConfirmationDialog(e, contacts) {
|
showSendAnywayDialog(contacts) {
|
||||||
|
return new Promise(resolve => {
|
||||||
let message;
|
let message;
|
||||||
const isUnverified = this.model.isUnverified();
|
const isUnverified = this.model.isUnverified();
|
||||||
|
|
||||||
|
@ -1504,77 +1584,98 @@
|
||||||
const dialog = new Whisper.ConfirmationDialogView({
|
const dialog = new Whisper.ConfirmationDialogView({
|
||||||
message,
|
message,
|
||||||
okText: i18n('sendAnyway'),
|
okText: i18n('sendAnyway'),
|
||||||
resolve: () => {
|
resolve: () => resolve(true),
|
||||||
this.checkUnverifiedSendMessage(e, { force: true });
|
reject: () => resolve(false),
|
||||||
},
|
|
||||||
reject: () => {
|
|
||||||
this.focusMessageFieldAndClearDisabled();
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
this.$el.prepend(dialog.el);
|
this.$el.prepend(dialog.el);
|
||||||
dialog.focusCancel();
|
dialog.focusCancel();
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
async checkUnverifiedSendMessage(e, options = {}) {
|
async clickSend(e, options) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
this.sendStart = Date.now();
|
this.sendStart = Date.now();
|
||||||
this.$messageField.attr('disabled', true);
|
this.$messageField.attr('disabled', true);
|
||||||
|
|
||||||
_.defaults(options, { force: false });
|
|
||||||
|
|
||||||
// This will go to the trust store for the latest identity key information,
|
|
||||||
// and may result in the display of a new banner for this conversation.
|
|
||||||
try {
|
try {
|
||||||
await this.model.updateVerified();
|
const contacts = await this.getUntrustedContacts(options);
|
||||||
const contacts = this.model.getUnverified();
|
|
||||||
if (!contacts.length) {
|
if (contacts && contacts.length) {
|
||||||
this.checkUntrustedSendMessage(e, options);
|
const sendAnyway = await this.showSendAnywayDialog(contacts);
|
||||||
|
if (sendAnyway) {
|
||||||
|
this.clickSend(e, { force: true });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.force) {
|
this.focusMessageFieldAndClearDisabled();
|
||||||
await this.markAllAsVerifiedDefault(contacts);
|
|
||||||
this.checkUnverifiedSendMessage(e, options);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.showSendConfirmationDialog(e, contacts);
|
this.sendMessage(e);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.focusMessageFieldAndClearDisabled();
|
this.focusMessageFieldAndClearDisabled();
|
||||||
window.log.error(
|
window.log.error(
|
||||||
'checkUnverifiedSendMessage error:',
|
'clickSend error:',
|
||||||
error && error.stack ? error.stack : error
|
error && error.stack ? error.stack : error
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async checkUntrustedSendMessage(e, options = {}) {
|
async sendStickerMessage(options = {}) {
|
||||||
_.defaults(options, { force: false });
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const contacts = await this.model.getUntrusted();
|
const contacts = await this.getUntrustedContacts(options);
|
||||||
if (!contacts.length) {
|
|
||||||
this.sendMessage(e);
|
if (contacts && contacts.length) {
|
||||||
|
const sendAnyway = await this.showSendAnywayDialog(contacts);
|
||||||
|
if (sendAnyway) {
|
||||||
|
this.sendStickerMessage({ ...options, force: true });
|
||||||
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.force) {
|
const { packId, stickerId } = options;
|
||||||
await this.markAllAsApproved(contacts);
|
this.model.sendStickerMessage(packId, stickerId);
|
||||||
this.sendMessage(e);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.showSendConfirmationDialog(e, contacts);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.focusMessageFieldAndClearDisabled();
|
|
||||||
window.log.error(
|
window.log.error(
|
||||||
'checkUntrustedSendMessage error:',
|
'clickSend error:',
|
||||||
error && error.stack ? error.stack : error
|
error && error.stack ? error.stack : error
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async getUntrustedContacts(options = {}) {
|
||||||
|
// This will go to the trust store for the latest identity key information,
|
||||||
|
// and may result in the display of a new banner for this conversation.
|
||||||
|
await this.model.updateVerified();
|
||||||
|
const unverifiedContacts = this.model.getUnverified();
|
||||||
|
|
||||||
|
if (options.force) {
|
||||||
|
if (unverifiedContacts.length) {
|
||||||
|
await this.markAllAsVerifiedDefault(unverifiedContacts);
|
||||||
|
// We only want force to break us through one layer of checks
|
||||||
|
// eslint-disable-next-line no-param-reassign
|
||||||
|
options.force = false;
|
||||||
|
}
|
||||||
|
} else if (unverifiedContacts.length) {
|
||||||
|
return unverifiedContacts;
|
||||||
|
}
|
||||||
|
|
||||||
|
const untrustedContacts = await this.model.getUntrusted();
|
||||||
|
|
||||||
|
if (options.force) {
|
||||||
|
if (untrustedContacts.length) {
|
||||||
|
await this.markAllAsApproved(untrustedContacts);
|
||||||
|
}
|
||||||
|
} else if (untrustedContacts.length) {
|
||||||
|
return untrustedContacts;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
|
||||||
toggleEmojiPanel(e) {
|
toggleEmojiPanel(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (!this.emojiPanel) {
|
if (!this.emojiPanel) {
|
||||||
|
@ -1839,14 +1940,29 @@
|
||||||
|
|
||||||
async makeChunkedRequest(url) {
|
async makeChunkedRequest(url) {
|
||||||
const PARALLELISM = 3;
|
const PARALLELISM = 3;
|
||||||
const size = await textsecure.messaging.getProxiedSize(url);
|
const first = await textsecure.messaging.makeProxiedRequest(url, {
|
||||||
const chunks = await Signal.LinkPreviews.getChunkPattern(size);
|
start: 0,
|
||||||
|
end: Signal.Crypto.getRandomValue(1023, 2047),
|
||||||
|
returnArrayBuffer: true,
|
||||||
|
});
|
||||||
|
const { totalSize, result } = first;
|
||||||
|
const initialOffset = result.data.byteLength;
|
||||||
|
const firstChunk = {
|
||||||
|
start: 0,
|
||||||
|
end: initialOffset,
|
||||||
|
...result,
|
||||||
|
};
|
||||||
|
|
||||||
|
const chunks = await Signal.LinkPreviews.getChunkPattern(
|
||||||
|
totalSize,
|
||||||
|
initialOffset
|
||||||
|
);
|
||||||
|
|
||||||
let results = [];
|
let results = [];
|
||||||
const jobs = chunks.map(chunk => async () => {
|
const jobs = chunks.map(chunk => async () => {
|
||||||
const { start, end } = chunk;
|
const { start, end } = chunk;
|
||||||
|
|
||||||
const result = await textsecure.messaging.makeProxiedRequest(url, {
|
const jobResult = await textsecure.messaging.makeProxiedRequest(url, {
|
||||||
start,
|
start,
|
||||||
end,
|
end,
|
||||||
returnArrayBuffer: true,
|
returnArrayBuffer: true,
|
||||||
|
@ -1854,7 +1970,7 @@
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...chunk,
|
...chunk,
|
||||||
...result,
|
...jobResult.result,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1878,7 +1994,9 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
const { contentType } = results[0];
|
const { contentType } = results[0];
|
||||||
const data = Signal.LinkPreviews.assembleChunks(results);
|
const data = Signal.LinkPreviews.assembleChunks(
|
||||||
|
[firstChunk].concat(results)
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
contentType,
|
contentType,
|
||||||
|
@ -1886,7 +2004,58 @@
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async getStickerPackPreview(url) {
|
||||||
|
const isPackValid = pack =>
|
||||||
|
pack && (pack.status === 'advertised' || pack.status === 'installed');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { id, key } = window.Signal.Stickers.getDataFromLink(url);
|
||||||
|
const keyBytes = window.Signal.Crypto.bytesFromHexString(key);
|
||||||
|
const keyBase64 = window.Signal.Crypto.arrayBufferToBase64(keyBytes);
|
||||||
|
|
||||||
|
const existing = window.Signal.Stickers.getStickerPack(id);
|
||||||
|
if (!isPackValid(existing)) {
|
||||||
|
await window.Signal.Stickers.downloadStickerPack(id, keyBase64);
|
||||||
|
}
|
||||||
|
|
||||||
|
const pack = window.Signal.Stickers.getStickerPack(id);
|
||||||
|
if (!isPackValid(pack)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (pack.key !== keyBase64) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { title, coverStickerId } = pack;
|
||||||
|
const sticker = pack.stickers[coverStickerId];
|
||||||
|
const data = await window.Signal.Migrations.readStickerData(
|
||||||
|
sticker.path
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
title,
|
||||||
|
url,
|
||||||
|
image: {
|
||||||
|
...sticker,
|
||||||
|
data,
|
||||||
|
size: data.byteLength,
|
||||||
|
contentType: 'image/webp',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
window.log.error(
|
||||||
|
'getStickerPackPreview error:',
|
||||||
|
error && error.stack ? error.stack : error
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
async getPreview(url) {
|
async getPreview(url) {
|
||||||
|
if (window.Signal.LinkPreviews.isStickerPack(url)) {
|
||||||
|
return this.getStickerPackPreview(url);
|
||||||
|
}
|
||||||
|
|
||||||
let html;
|
let html;
|
||||||
try {
|
try {
|
||||||
html = await textsecure.messaging.makeProxiedRequest(url);
|
html = await textsecure.messaging.makeProxiedRequest(url);
|
||||||
|
|
|
@ -13,6 +13,12 @@
|
||||||
|
|
||||||
window.Whisper = window.Whisper || {};
|
window.Whisper = window.Whisper || {};
|
||||||
|
|
||||||
|
Whisper.StickerPackInstallFailedToast = Whisper.ToastView.extend({
|
||||||
|
render_attributes() {
|
||||||
|
return { toastMessage: i18n('stickers--toast--InstallFailed') };
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
Whisper.ConversationStack = Whisper.View.extend({
|
Whisper.ConversationStack = Whisper.View.extend({
|
||||||
className: 'conversation-stack',
|
className: 'conversation-stack',
|
||||||
open(conversation) {
|
open(conversation) {
|
||||||
|
@ -36,6 +42,8 @@
|
||||||
$el.prependTo(this.el);
|
$el.prependTo(this.el);
|
||||||
}
|
}
|
||||||
conversation.trigger('opened');
|
conversation.trigger('opened');
|
||||||
|
// Make sure poppers are positioned properly
|
||||||
|
window.dispatchEvent(new Event('resize'));
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -92,6 +100,12 @@
|
||||||
this.$el.addClass('expired');
|
this.$el.addClass('expired');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Whisper.events.on('pack-install-failed', () => {
|
||||||
|
const toast = new Whisper.StickerPackInstallFailedToast();
|
||||||
|
toast.$el.appendTo(this.$el);
|
||||||
|
toast.render();
|
||||||
|
});
|
||||||
|
|
||||||
this.setupLeftPane();
|
this.setupLeftPane();
|
||||||
},
|
},
|
||||||
render_attributes: {
|
render_attributes: {
|
||||||
|
|
|
@ -16,6 +16,12 @@
|
||||||
this.listenTo(this.model, 'destroy', this.onDestroy);
|
this.listenTo(this.model, 'destroy', this.onDestroy);
|
||||||
this.listenTo(this.model, 'unload', this.onUnload);
|
this.listenTo(this.model, 'unload', this.onUnload);
|
||||||
this.listenTo(this.model, 'expired', this.onExpired);
|
this.listenTo(this.model, 'expired', this.onExpired);
|
||||||
|
|
||||||
|
this.updateHiddenSticker();
|
||||||
|
},
|
||||||
|
updateHiddenSticker() {
|
||||||
|
const sticker = this.model.get('sticker');
|
||||||
|
this.isHiddenSticker = sticker && (!sticker.data || !sticker.data.path);
|
||||||
},
|
},
|
||||||
onChange() {
|
onChange() {
|
||||||
this.addId();
|
this.addId();
|
||||||
|
@ -94,7 +100,17 @@
|
||||||
|
|
||||||
const update = () => {
|
const update = () => {
|
||||||
const info = this.getRenderInfo();
|
const info = this.getRenderInfo();
|
||||||
this.childView.update(info.props);
|
this.childView.update(info.props, () => {
|
||||||
|
if (!this.isHiddenSticker) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.updateHiddenSticker();
|
||||||
|
|
||||||
|
if (!this.isHiddenSticker) {
|
||||||
|
this.model.trigger('height-changed');
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
this.listenTo(this.model, 'change', update);
|
this.listenTo(this.model, 'change', update);
|
||||||
|
|
|
@ -38,12 +38,23 @@
|
||||||
|
|
||||||
this.hasRendered = false;
|
this.hasRendered = false;
|
||||||
},
|
},
|
||||||
update(props) {
|
update(props, cb) {
|
||||||
const updatedProps = this.augmentProps(props);
|
const updatedProps = this.augmentProps(props);
|
||||||
const reactElement = this.JSX
|
const reactElement = this.JSX
|
||||||
? this.JSX
|
? this.JSX
|
||||||
: React.createElement(this.Component, updatedProps);
|
: React.createElement(this.Component, updatedProps);
|
||||||
ReactDOM.render(reactElement, this.el, () => {
|
ReactDOM.render(reactElement, this.el, () => {
|
||||||
|
if (cb) {
|
||||||
|
try {
|
||||||
|
cb();
|
||||||
|
} catch (error) {
|
||||||
|
window.log.error(
|
||||||
|
'ReactWrapperView.update error:',
|
||||||
|
error && error.stack ? error.stack : error
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (this.hasRendered) {
|
if (this.hasRendered) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
@ -90,10 +90,11 @@
|
||||||
|
|
||||||
return verifyMAC(ivAndCiphertext, macKey, mac, 32)
|
return verifyMAC(ivAndCiphertext, macKey, mac, 32)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
if (!theirDigest) {
|
if (theirDigest) {
|
||||||
throw new Error('Failure: Ask sender to update Signal and resend.');
|
|
||||||
}
|
|
||||||
return verifyDigest(encryptedBin, theirDigest);
|
return verifyDigest(encryptedBin, theirDigest);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
})
|
})
|
||||||
.then(() => decrypt(aesKey, ciphertext, iv));
|
.then(() => decrypt(aesKey, ciphertext, iv));
|
||||||
},
|
},
|
||||||
|
|
|
@ -1110,6 +1110,11 @@ MessageReceiver.prototype.extend({
|
||||||
return this.handleVerified(envelope, syncMessage.verified);
|
return this.handleVerified(envelope, syncMessage.verified);
|
||||||
} else if (syncMessage.configuration) {
|
} else if (syncMessage.configuration) {
|
||||||
return this.handleConfiguration(envelope, syncMessage.configuration);
|
return this.handleConfiguration(envelope, syncMessage.configuration);
|
||||||
|
} else if (syncMessage.stickerPackOperation) {
|
||||||
|
return this.handleStickerPackOperation(
|
||||||
|
envelope,
|
||||||
|
syncMessage.stickerPackOperation
|
||||||
|
);
|
||||||
}
|
}
|
||||||
throw new Error('Got empty SyncMessage');
|
throw new Error('Got empty SyncMessage');
|
||||||
},
|
},
|
||||||
|
@ -1120,6 +1125,19 @@ MessageReceiver.prototype.extend({
|
||||||
ev.configuration = configuration;
|
ev.configuration = configuration;
|
||||||
return this.dispatchAndWait(ev);
|
return this.dispatchAndWait(ev);
|
||||||
},
|
},
|
||||||
|
handleStickerPackOperation(envelope, operations) {
|
||||||
|
const ENUM = textsecure.protobuf.SyncMessage.StickerPackOperation.Type;
|
||||||
|
window.log.info('got sticker pack operation sync message');
|
||||||
|
const ev = new Event('sticker-pack');
|
||||||
|
ev.confirm = this.removeFromCache.bind(this, envelope);
|
||||||
|
ev.stickerPacks = operations.map(operation => ({
|
||||||
|
id: operation.packId ? operation.packId.toString('hex') : null,
|
||||||
|
key: operation.packKey ? operation.packKey.toString('base64') : null,
|
||||||
|
isInstall: operation.type === ENUM.INSTALL,
|
||||||
|
isRemove: operation.type === ENUM.REMOVE,
|
||||||
|
}));
|
||||||
|
return this.dispatchAndWait(ev);
|
||||||
|
},
|
||||||
handleVerified(envelope, verified) {
|
handleVerified(envelope, verified) {
|
||||||
const ev = new Event('verified');
|
const ev = new Event('verified');
|
||||||
ev.confirm = this.removeFromCache.bind(this, envelope);
|
ev.confirm = this.removeFromCache.bind(this, envelope);
|
||||||
|
@ -1231,6 +1249,10 @@ MessageReceiver.prototype.extend({
|
||||||
const encrypted = await this.server.getAttachment(attachment.id);
|
const encrypted = await this.server.getAttachment(attachment.id);
|
||||||
const { key, digest, size } = attachment;
|
const { key, digest, size } = attachment;
|
||||||
|
|
||||||
|
if (!digest) {
|
||||||
|
throw new Error('Failure: Ask sender to update Signal and resend.');
|
||||||
|
}
|
||||||
|
|
||||||
const data = await textsecure.crypto.decryptAttachment(
|
const data = await textsecure.crypto.decryptAttachment(
|
||||||
encrypted,
|
encrypted,
|
||||||
window.Signal.Crypto.base64ToArrayBuffer(key),
|
window.Signal.Crypto.base64ToArrayBuffer(key),
|
||||||
|
@ -1400,6 +1422,19 @@ MessageReceiver.prototype.extend({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { sticker } = decrypted;
|
||||||
|
if (sticker) {
|
||||||
|
if (sticker.packId) {
|
||||||
|
sticker.packId = sticker.packId.toString('hex');
|
||||||
|
}
|
||||||
|
if (sticker.packKey) {
|
||||||
|
sticker.packKey = sticker.packKey.toString('base64');
|
||||||
|
}
|
||||||
|
if (sticker.data) {
|
||||||
|
sticker.data = this.cleanAttachment(sticker.data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return Promise.all(promises).then(() => decrypted);
|
return Promise.all(promises).then(() => decrypted);
|
||||||
/* eslint-enable no-bitwise, no-param-reassign */
|
/* eslint-enable no-bitwise, no-param-reassign */
|
||||||
},
|
},
|
||||||
|
|
|
@ -35,6 +35,7 @@
|
||||||
loadProtoBufs('SignalService.proto');
|
loadProtoBufs('SignalService.proto');
|
||||||
loadProtoBufs('SubProtocol.proto');
|
loadProtoBufs('SubProtocol.proto');
|
||||||
loadProtoBufs('DeviceMessages.proto');
|
loadProtoBufs('DeviceMessages.proto');
|
||||||
|
loadProtoBufs('Stickers.proto');
|
||||||
|
|
||||||
// Just for encrypting device names
|
// Just for encrypting device names
|
||||||
loadProtoBufs('DeviceName.proto');
|
loadProtoBufs('DeviceName.proto');
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
/* global _, textsecure, WebAPI, libsignal, OutgoingMessage, window */
|
/* global _, textsecure, WebAPI, libsignal, OutgoingMessage, window, dcodeIO */
|
||||||
|
|
||||||
/* eslint-disable more/no-then, no-bitwise */
|
/* eslint-disable more/no-then, no-bitwise */
|
||||||
|
|
||||||
|
@ -13,18 +13,26 @@ function stringToArrayBuffer(str) {
|
||||||
}
|
}
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
function hexStringToArrayBuffer(string) {
|
||||||
|
return dcodeIO.ByteBuffer.wrap(string, 'hex').toArrayBuffer();
|
||||||
|
}
|
||||||
|
function base64ToArrayBuffer(string) {
|
||||||
|
return dcodeIO.ByteBuffer.wrap(string, 'base64').toArrayBuffer();
|
||||||
|
}
|
||||||
|
|
||||||
function Message(options) {
|
function Message(options) {
|
||||||
this.body = options.body;
|
|
||||||
this.attachments = options.attachments || [];
|
this.attachments = options.attachments || [];
|
||||||
this.quote = options.quote;
|
this.body = options.body;
|
||||||
this.preview = options.preview;
|
|
||||||
this.group = options.group;
|
|
||||||
this.flags = options.flags;
|
|
||||||
this.recipients = options.recipients;
|
|
||||||
this.timestamp = options.timestamp;
|
|
||||||
this.expireTimer = options.expireTimer;
|
this.expireTimer = options.expireTimer;
|
||||||
|
this.flags = options.flags;
|
||||||
|
this.group = options.group;
|
||||||
|
this.needsSync = options.needsSync;
|
||||||
|
this.preview = options.preview;
|
||||||
this.profileKey = options.profileKey;
|
this.profileKey = options.profileKey;
|
||||||
|
this.quote = options.quote;
|
||||||
|
this.recipients = options.recipients;
|
||||||
|
this.sticker = options.sticker;
|
||||||
|
this.timestamp = options.timestamp;
|
||||||
|
|
||||||
if (!(this.recipients instanceof Array)) {
|
if (!(this.recipients instanceof Array)) {
|
||||||
throw new Error('Invalid recipient list');
|
throw new Error('Invalid recipient list');
|
||||||
|
@ -102,6 +110,16 @@ Message.prototype = {
|
||||||
proto.group.id = stringToArrayBuffer(this.group.id);
|
proto.group.id = stringToArrayBuffer(this.group.id);
|
||||||
proto.group.type = this.group.type;
|
proto.group.type = this.group.type;
|
||||||
}
|
}
|
||||||
|
if (this.sticker) {
|
||||||
|
proto.sticker = new textsecure.protobuf.DataMessage.Sticker();
|
||||||
|
proto.sticker.packId = hexStringToArrayBuffer(this.sticker.packId);
|
||||||
|
proto.sticker.packKey = base64ToArrayBuffer(this.sticker.packKey);
|
||||||
|
proto.sticker.stickerId = this.sticker.stickerId;
|
||||||
|
|
||||||
|
if (this.sticker.attachmentPointer) {
|
||||||
|
proto.sticker.data = this.sticker.attachmentPointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
if (Array.isArray(this.preview)) {
|
if (Array.isArray(this.preview)) {
|
||||||
proto.preview = this.preview.map(preview => {
|
proto.preview = this.preview.map(preview => {
|
||||||
const item = new textsecure.protobuf.DataMessage.Preview();
|
const item = new textsecure.protobuf.DataMessage.Preview();
|
||||||
|
@ -154,8 +172,6 @@ function MessageSender(username, password) {
|
||||||
this.pendingMessages = {};
|
this.pendingMessages = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
const DISABLE_PADDING = true;
|
|
||||||
|
|
||||||
MessageSender.prototype = {
|
MessageSender.prototype = {
|
||||||
constructor: MessageSender,
|
constructor: MessageSender,
|
||||||
|
|
||||||
|
@ -166,8 +182,8 @@ MessageSender.prototype = {
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
getPaddedAttachment(data) {
|
getPaddedAttachment(data, shouldPad) {
|
||||||
if (DISABLE_PADDING) {
|
if (!shouldPad) {
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -178,7 +194,7 @@ MessageSender.prototype = {
|
||||||
return window.Signal.Crypto.concatenateBytes(data, padding);
|
return window.Signal.Crypto.concatenateBytes(data, padding);
|
||||||
},
|
},
|
||||||
|
|
||||||
async makeAttachmentPointer(attachment) {
|
async makeAttachmentPointer(attachment, shouldPad = false) {
|
||||||
if (typeof attachment !== 'object' || attachment == null) {
|
if (typeof attachment !== 'object' || attachment == null) {
|
||||||
return Promise.resolve(undefined);
|
return Promise.resolve(undefined);
|
||||||
}
|
}
|
||||||
|
@ -197,7 +213,7 @@ MessageSender.prototype = {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const padded = this.getPaddedAttachment(data);
|
const padded = this.getPaddedAttachment(data, shouldPad);
|
||||||
const key = libsignal.crypto.getRandomBytes(64);
|
const key = libsignal.crypto.getRandomBytes(64);
|
||||||
const iv = libsignal.crypto.getRandomBytes(16);
|
const iv = libsignal.crypto.getRandomBytes(16);
|
||||||
|
|
||||||
|
@ -286,6 +302,32 @@ MessageSender.prototype = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async uploadSticker(message) {
|
||||||
|
try {
|
||||||
|
const { sticker } = message;
|
||||||
|
|
||||||
|
if (!sticker || !sticker.data) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const shouldPad = true;
|
||||||
|
// eslint-disable-next-line no-param-reassign
|
||||||
|
message.sticker = {
|
||||||
|
...sticker,
|
||||||
|
attachmentPointer: await this.makeAttachmentPointer(
|
||||||
|
sticker.data,
|
||||||
|
shouldPad
|
||||||
|
),
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof Error && error.name === 'HTTPError') {
|
||||||
|
throw new textsecure.MessageError(message, error);
|
||||||
|
} else {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
uploadThumbnails(message) {
|
uploadThumbnails(message) {
|
||||||
const makePointer = this.makeAttachmentPointer.bind(this);
|
const makePointer = this.makeAttachmentPointer.bind(this);
|
||||||
const { quote } = message;
|
const { quote } = message;
|
||||||
|
@ -323,6 +365,7 @@ MessageSender.prototype = {
|
||||||
this.uploadAttachments(message),
|
this.uploadAttachments(message),
|
||||||
this.uploadThumbnails(message),
|
this.uploadThumbnails(message),
|
||||||
this.uploadLinkPreviews(message),
|
this.uploadLinkPreviews(message),
|
||||||
|
this.uploadSticker(message),
|
||||||
]).then(
|
]).then(
|
||||||
() =>
|
() =>
|
||||||
new Promise((resolve, reject) => {
|
new Promise((resolve, reject) => {
|
||||||
|
@ -510,6 +553,13 @@ MessageSender.prototype = {
|
||||||
return this.server.getAvatar(path);
|
return this.server.getAvatar(path);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
getSticker(packId, stickerId) {
|
||||||
|
return this.server.getSticker(packId, stickerId);
|
||||||
|
},
|
||||||
|
getStickerPackManifest(packId) {
|
||||||
|
return this.server.getStickerPackManifest(packId);
|
||||||
|
},
|
||||||
|
|
||||||
sendRequestConfigurationSyncMessage(options) {
|
sendRequestConfigurationSyncMessage(options) {
|
||||||
const myNumber = textsecure.storage.user.getNumber();
|
const myNumber = textsecure.storage.user.getNumber();
|
||||||
const myDevice = textsecure.storage.user.getDeviceId();
|
const myDevice = textsecure.storage.user.getDeviceId();
|
||||||
|
@ -698,6 +748,41 @@ MessageSender.prototype = {
|
||||||
|
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
},
|
},
|
||||||
|
async sendStickerPackSync(operations, options) {
|
||||||
|
const myDevice = textsecure.storage.user.getDeviceId();
|
||||||
|
if (myDevice === 1 || myDevice === '1') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const myNumber = textsecure.storage.user.getNumber();
|
||||||
|
const ENUM = textsecure.protobuf.SyncMessage.StickerPackOperation.Type;
|
||||||
|
|
||||||
|
const packOperations = operations.map(item => {
|
||||||
|
const { packId, packKey, installed } = item;
|
||||||
|
|
||||||
|
const operation = new textsecure.protobuf.SyncMessage.StickerPackOperation();
|
||||||
|
operation.packId = hexStringToArrayBuffer(packId);
|
||||||
|
operation.packKey = base64ToArrayBuffer(packKey);
|
||||||
|
operation.type = installed ? ENUM.INSTALL : ENUM.REMOVE;
|
||||||
|
|
||||||
|
return operation;
|
||||||
|
});
|
||||||
|
|
||||||
|
const syncMessage = this.createSyncMessage();
|
||||||
|
syncMessage.stickerPackOperation = packOperations;
|
||||||
|
|
||||||
|
const contentMessage = new textsecure.protobuf.Content();
|
||||||
|
contentMessage.syncMessage = syncMessage;
|
||||||
|
|
||||||
|
const silent = true;
|
||||||
|
return this.sendIndividualProto(
|
||||||
|
myNumber,
|
||||||
|
contentMessage,
|
||||||
|
Date.now(),
|
||||||
|
silent,
|
||||||
|
options
|
||||||
|
);
|
||||||
|
},
|
||||||
syncVerification(destination, state, identityKey, options) {
|
syncVerification(destination, state, identityKey, options) {
|
||||||
const myNumber = textsecure.storage.user.getNumber();
|
const myNumber = textsecure.storage.user.getNumber();
|
||||||
const myDevice = textsecure.storage.user.getDeviceId();
|
const myDevice = textsecure.storage.user.getDeviceId();
|
||||||
|
@ -795,6 +880,7 @@ MessageSender.prototype = {
|
||||||
attachments,
|
attachments,
|
||||||
quote,
|
quote,
|
||||||
preview,
|
preview,
|
||||||
|
sticker,
|
||||||
timestamp,
|
timestamp,
|
||||||
expireTimer,
|
expireTimer,
|
||||||
profileKey,
|
profileKey,
|
||||||
|
@ -807,6 +893,7 @@ MessageSender.prototype = {
|
||||||
attachments,
|
attachments,
|
||||||
quote,
|
quote,
|
||||||
preview,
|
preview,
|
||||||
|
sticker,
|
||||||
expireTimer,
|
expireTimer,
|
||||||
profileKey,
|
profileKey,
|
||||||
flags,
|
flags,
|
||||||
|
@ -821,6 +908,7 @@ MessageSender.prototype = {
|
||||||
this.uploadAttachments(message),
|
this.uploadAttachments(message),
|
||||||
this.uploadThumbnails(message),
|
this.uploadThumbnails(message),
|
||||||
this.uploadLinkPreviews(message),
|
this.uploadLinkPreviews(message),
|
||||||
|
this.uploadSticker(message),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return message.toArrayBuffer();
|
return message.toArrayBuffer();
|
||||||
|
@ -832,6 +920,7 @@ MessageSender.prototype = {
|
||||||
attachments,
|
attachments,
|
||||||
quote,
|
quote,
|
||||||
preview,
|
preview,
|
||||||
|
sticker,
|
||||||
timestamp,
|
timestamp,
|
||||||
expireTimer,
|
expireTimer,
|
||||||
profileKey,
|
profileKey,
|
||||||
|
@ -845,6 +934,7 @@ MessageSender.prototype = {
|
||||||
attachments,
|
attachments,
|
||||||
quote,
|
quote,
|
||||||
preview,
|
preview,
|
||||||
|
sticker,
|
||||||
expireTimer,
|
expireTimer,
|
||||||
profileKey,
|
profileKey,
|
||||||
},
|
},
|
||||||
|
@ -928,6 +1018,7 @@ MessageSender.prototype = {
|
||||||
attachments,
|
attachments,
|
||||||
quote,
|
quote,
|
||||||
preview,
|
preview,
|
||||||
|
sticker,
|
||||||
timestamp,
|
timestamp,
|
||||||
expireTimer,
|
expireTimer,
|
||||||
profileKey,
|
profileKey,
|
||||||
|
@ -942,6 +1033,7 @@ MessageSender.prototype = {
|
||||||
attachments,
|
attachments,
|
||||||
quote,
|
quote,
|
||||||
preview,
|
preview,
|
||||||
|
sticker,
|
||||||
expireTimer,
|
expireTimer,
|
||||||
profileKey,
|
profileKey,
|
||||||
group: {
|
group: {
|
||||||
|
@ -1098,9 +1190,6 @@ MessageSender.prototype = {
|
||||||
makeProxiedRequest(url, options) {
|
makeProxiedRequest(url, options) {
|
||||||
return this.server.makeProxiedRequest(url, options);
|
return this.server.makeProxiedRequest(url, options);
|
||||||
},
|
},
|
||||||
getProxiedSize(url) {
|
|
||||||
return this.server.getProxiedSize(url);
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
window.textsecure = window.textsecure || {};
|
window.textsecure = window.textsecure || {};
|
||||||
|
@ -1142,10 +1231,11 @@ textsecure.MessageSender = function MessageSenderWrapper(username, password) {
|
||||||
this.sendDeliveryReceipt = sender.sendDeliveryReceipt.bind(sender);
|
this.sendDeliveryReceipt = sender.sendDeliveryReceipt.bind(sender);
|
||||||
this.sendReadReceipts = sender.sendReadReceipts.bind(sender);
|
this.sendReadReceipts = sender.sendReadReceipts.bind(sender);
|
||||||
this.makeProxiedRequest = sender.makeProxiedRequest.bind(sender);
|
this.makeProxiedRequest = sender.makeProxiedRequest.bind(sender);
|
||||||
this.getProxiedSize = sender.getProxiedSize.bind(sender);
|
|
||||||
this.getMessageProto = sender.getMessageProto.bind(sender);
|
this.getMessageProto = sender.getMessageProto.bind(sender);
|
||||||
|
|
||||||
this._getAttachmentSizeBucket = sender._getAttachmentSizeBucket.bind(sender);
|
this._getAttachmentSizeBucket = sender._getAttachmentSizeBucket.bind(sender);
|
||||||
|
this.getSticker = sender.getSticker.bind(sender);
|
||||||
|
this.getStickerPackManifest = sender.getStickerPackManifest.bind(sender);
|
||||||
|
this.sendStickerPackSync = sender.sendStickerPackSync.bind(sender);
|
||||||
};
|
};
|
||||||
|
|
||||||
textsecure.MessageSender.prototype = {
|
textsecure.MessageSender.prototype = {
|
||||||
|
|
39
main.js
|
@ -5,6 +5,7 @@ const url = require('url');
|
||||||
const os = require('os');
|
const os = require('os');
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const crypto = require('crypto');
|
const crypto = require('crypto');
|
||||||
|
const qs = require('qs');
|
||||||
|
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
const pify = require('pify');
|
const pify = require('pify');
|
||||||
|
@ -398,13 +399,16 @@ ipc.on('show-window', () => {
|
||||||
showWindow();
|
showWindow();
|
||||||
});
|
});
|
||||||
|
|
||||||
let updatesStarted = false;
|
ipc.once('ready-for-updates', async () => {
|
||||||
ipc.on('ready-for-updates', async () => {
|
// First, install requested sticker pack
|
||||||
if (updatesStarted) {
|
if (process.argv.length > 1) {
|
||||||
return;
|
const [incomingUrl] = process.argv;
|
||||||
|
if (incomingUrl.startsWith('sgnl://')) {
|
||||||
|
handleSgnlLink(incomingUrl);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
updatesStarted = true;
|
|
||||||
|
|
||||||
|
// Second, start checking for app updates
|
||||||
try {
|
try {
|
||||||
await updater.start(getMainWindow, locale.messages, logger);
|
await updater.start(getMainWindow, locale.messages, logger);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@ -714,6 +718,13 @@ app.on('ready', async () => {
|
||||||
userDataPath,
|
userDataPath,
|
||||||
attachments: orphanedAttachments,
|
attachments: orphanedAttachments,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const allStickers = await attachments.getAllStickers(userDataPath);
|
||||||
|
const orphanedStickers = await sql.removeKnownStickers(allStickers);
|
||||||
|
await attachments.deleteAllStickers({
|
||||||
|
userDataPath,
|
||||||
|
stickers: orphanedStickers,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
await attachmentChannel.initialize({
|
await attachmentChannel.initialize({
|
||||||
|
@ -840,6 +851,12 @@ app.on('web-contents-created', (createEvent, contents) => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
app.setAsDefaultProtocolClient('sgnl');
|
||||||
|
app.on('open-url', (event, incomingUrl) => {
|
||||||
|
event.preventDefault();
|
||||||
|
handleSgnlLink(incomingUrl);
|
||||||
|
});
|
||||||
|
|
||||||
ipc.on('set-badge-count', (event, count) => {
|
ipc.on('set-badge-count', (event, count) => {
|
||||||
app.setBadgeCount(count);
|
app.setBadgeCount(count);
|
||||||
});
|
});
|
||||||
|
@ -1011,3 +1028,15 @@ function installSettingsSetter(name) {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleSgnlLink(incomingUrl) {
|
||||||
|
const { host: command, query } = url.parse(incomingUrl);
|
||||||
|
const args = qs.parse(query);
|
||||||
|
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 });
|
||||||
|
} else {
|
||||||
|
console.error('Unhandled sgnl link');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -52,7 +52,7 @@
|
||||||
"bunyan": "1.8.12",
|
"bunyan": "1.8.12",
|
||||||
"classnames": "2.2.5",
|
"classnames": "2.2.5",
|
||||||
"config": "1.28.1",
|
"config": "1.28.1",
|
||||||
"electron-context-menu": "^0.11.0",
|
"electron-context-menu": "0.11.0",
|
||||||
"electron-editor-context-menu": "1.1.1",
|
"electron-editor-context-menu": "1.1.1",
|
||||||
"electron-is-dev": "0.3.0",
|
"electron-is-dev": "0.3.0",
|
||||||
"emoji-datasource": "4.0.0",
|
"emoji-datasource": "4.0.0",
|
||||||
|
@ -80,12 +80,16 @@
|
||||||
"node-gyp": "3.8.0",
|
"node-gyp": "3.8.0",
|
||||||
"node-sass": "4.9.3",
|
"node-sass": "4.9.3",
|
||||||
"os-locale": "2.1.0",
|
"os-locale": "2.1.0",
|
||||||
|
"p-map": "2.1.0",
|
||||||
|
"p-queue": "5.0.0",
|
||||||
"pify": "3.0.0",
|
"pify": "3.0.0",
|
||||||
"protobufjs": "6.8.6",
|
"protobufjs": "6.8.6",
|
||||||
"proxy-agent": "3.0.3",
|
"proxy-agent": "3.0.3",
|
||||||
|
"qs": "6.5.1",
|
||||||
"react": "16.8.3",
|
"react": "16.8.3",
|
||||||
"react-contextmenu": "2.11.0",
|
"react-contextmenu": "2.11.0",
|
||||||
"react-dom": "16.8.3",
|
"react-dom": "16.8.3",
|
||||||
|
"react-popper": "^1.3.3",
|
||||||
"react-redux": "6.0.1",
|
"react-redux": "6.0.1",
|
||||||
"react-virtualized": "9.21.0",
|
"react-virtualized": "9.21.0",
|
||||||
"read-last-lines": "1.3.0",
|
"read-last-lines": "1.3.0",
|
||||||
|
@ -158,7 +162,6 @@
|
||||||
"node-sass-import-once": "1.2.0",
|
"node-sass-import-once": "1.2.0",
|
||||||
"nyc": "11.4.1",
|
"nyc": "11.4.1",
|
||||||
"prettier": "1.12.0",
|
"prettier": "1.12.0",
|
||||||
"qs": "6.5.1",
|
|
||||||
"react-docgen-typescript": "1.2.6",
|
"react-docgen-typescript": "1.2.6",
|
||||||
"react-styleguidist": "7.0.1",
|
"react-styleguidist": "7.0.1",
|
||||||
"sinon": "4.4.2",
|
"sinon": "4.4.2",
|
||||||
|
|
13
preload.js
|
@ -150,6 +150,14 @@ ipc.on('delete-all-data', () => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ipc.on('add-sticker-pack', (_event, info) => {
|
||||||
|
const { packId, packKey } = info;
|
||||||
|
const { installStickerPack } = window.Events;
|
||||||
|
if (installStickerPack) {
|
||||||
|
installStickerPack(packId, packKey);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
ipc.on('get-ready-for-shutdown', async () => {
|
ipc.on('get-ready-for-shutdown', async () => {
|
||||||
const { shutdown } = window.Events || {};
|
const { shutdown } = window.Events || {};
|
||||||
if (!shutdown) {
|
if (!shutdown) {
|
||||||
|
@ -271,9 +279,12 @@ window.moment.updateLocale(locale, {
|
||||||
});
|
});
|
||||||
window.moment.locale(locale);
|
window.moment.locale(locale);
|
||||||
|
|
||||||
|
const userDataPath = app.getPath('userData');
|
||||||
|
window.baseAttachmentsPath = Attachments.getPath(userDataPath);
|
||||||
|
window.baseStickersPath = Attachments.getStickersPath(userDataPath);
|
||||||
window.Signal = Signal.setup({
|
window.Signal = Signal.setup({
|
||||||
Attachments,
|
Attachments,
|
||||||
userDataPath: app.getPath('userData'),
|
userDataPath,
|
||||||
getRegionCode: () => window.storage.get('regionCode'),
|
getRegionCode: () => window.storage.get('regionCode'),
|
||||||
logger: window.log,
|
logger: window.log,
|
||||||
});
|
});
|
||||||
|
|
|
@ -162,6 +162,13 @@ message DataMessage {
|
||||||
optional AttachmentPointer image = 3;
|
optional AttachmentPointer image = 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message Sticker {
|
||||||
|
optional bytes packId = 1;
|
||||||
|
optional bytes packKey = 2;
|
||||||
|
optional uint32 stickerId = 3;
|
||||||
|
optional AttachmentPointer data = 4;
|
||||||
|
}
|
||||||
|
|
||||||
optional string body = 1;
|
optional string body = 1;
|
||||||
repeated AttachmentPointer attachments = 2;
|
repeated AttachmentPointer attachments = 2;
|
||||||
optional GroupContext group = 3;
|
optional GroupContext group = 3;
|
||||||
|
@ -172,6 +179,7 @@ message DataMessage {
|
||||||
optional Quote quote = 8;
|
optional Quote quote = 8;
|
||||||
repeated Contact contact = 9;
|
repeated Contact contact = 9;
|
||||||
repeated Preview preview = 10;
|
repeated Preview preview = 10;
|
||||||
|
optional Sticker sticker = 11;
|
||||||
}
|
}
|
||||||
|
|
||||||
message NullMessage {
|
message NullMessage {
|
||||||
|
@ -265,6 +273,16 @@ message SyncMessage {
|
||||||
optional bool linkPreviews = 4;
|
optional bool linkPreviews = 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message StickerPackOperation {
|
||||||
|
enum Type {
|
||||||
|
INSTALL = 0;
|
||||||
|
REMOVE = 1;
|
||||||
|
}
|
||||||
|
optional bytes packId = 1;
|
||||||
|
optional bytes packKey = 2;
|
||||||
|
optional Type type = 3;
|
||||||
|
}
|
||||||
|
|
||||||
optional Sent sent = 1;
|
optional Sent sent = 1;
|
||||||
optional Contacts contacts = 2;
|
optional Contacts contacts = 2;
|
||||||
optional Groups groups = 3;
|
optional Groups groups = 3;
|
||||||
|
@ -274,6 +292,7 @@ message SyncMessage {
|
||||||
optional Verified verified = 7;
|
optional Verified verified = 7;
|
||||||
optional Configuration configuration = 9;
|
optional Configuration configuration = 9;
|
||||||
optional bytes padding = 8;
|
optional bytes padding = 8;
|
||||||
|
repeated StickerPackOperation stickerPackOperation = 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
message AttachmentPointer {
|
message AttachmentPointer {
|
||||||
|
|
13
protos/Stickers.proto
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
package signalservice;
|
||||||
|
|
||||||
|
message StickerPack {
|
||||||
|
message Sticker {
|
||||||
|
optional uint32 id = 1;
|
||||||
|
optional string emoji = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
optional string title = 1;
|
||||||
|
optional string author = 2;
|
||||||
|
optional Sticker cover = 3;
|
||||||
|
repeated Sticker stickers = 4;
|
||||||
|
}
|
|
@ -21,6 +21,11 @@ module.exports = {
|
||||||
description: 'Display media and documents in a conversation',
|
description: 'Display media and documents in a conversation',
|
||||||
components: 'ts/components/conversation/media-gallery/[^_]*.tsx',
|
components: 'ts/components/conversation/media-gallery/[^_]*.tsx',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'Stickers',
|
||||||
|
description: 'All components related to stickers',
|
||||||
|
components: 'ts/components/stickers/[^_]*.tsx',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'Utility',
|
name: 'Utility',
|
||||||
description: 'Utility components used across the application',
|
description: 'Utility components used across the application',
|
||||||
|
@ -73,7 +78,7 @@ module.exports = {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// To test handling of attachments, we need arraybuffers in memory
|
// To test handling of attachments, we need arraybuffers in memory
|
||||||
test: /\.(gif|mp3|mp4|txt|jpg|jpeg|png)$/,
|
test: /\.(gif|mp3|mp4|txt|jpg|jpeg|png|webp)$/,
|
||||||
loader: 'arraybuffer-loader',
|
loader: 'arraybuffer-loader',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
|
@ -51,6 +51,11 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Make sure the main panel is hidden when other panels are in the dom
|
||||||
|
.panel + .main.panel {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
.message-detail-wrapper {
|
.message-detail-wrapper {
|
||||||
height: calc(100% - 48px);
|
height: calc(100% - 48px);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
|
@ -630,3 +630,7 @@ $loading-height: 16px;
|
||||||
.inbox {
|
.inbox {
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.overflow-hidden {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
|
@ -43,6 +43,9 @@
|
||||||
.module-message__metadata__date--with-image-no-caption {
|
.module-message__metadata__date--with-image-no-caption {
|
||||||
color: $color-white;
|
color: $color-white;
|
||||||
}
|
}
|
||||||
|
.module-message__metadata__date--with-sticker {
|
||||||
|
color: $color-gray-60;
|
||||||
|
}
|
||||||
|
|
||||||
.module-message__metadata__status-icon--sending {
|
.module-message__metadata__status-icon--sending {
|
||||||
@include color-svg('../images/sending.svg', $color-white);
|
@include color-svg('../images/sending.svg', $color-white);
|
||||||
|
@ -61,6 +64,9 @@
|
||||||
.module-message__metadata__status-icon--with-image-no-caption {
|
.module-message__metadata__status-icon--with-image-no-caption {
|
||||||
background-color: $color-white;
|
background-color: $color-white;
|
||||||
}
|
}
|
||||||
|
.module-message__metadata__status-icon--with-sticker {
|
||||||
|
background-color: $color-gray-60;
|
||||||
|
}
|
||||||
|
|
||||||
.module-message__generic-attachment__file-name {
|
.module-message__generic-attachment__file-name {
|
||||||
color: $color-white;
|
color: $color-white;
|
||||||
|
@ -81,10 +87,12 @@
|
||||||
.module-expire-timer {
|
.module-expire-timer {
|
||||||
background-color: $color-white-08;
|
background-color: $color-white-08;
|
||||||
}
|
}
|
||||||
|
|
||||||
.module-expire-timer--incoming {
|
.module-expire-timer--incoming {
|
||||||
background-color: $color-gray-60;
|
background-color: $color-gray-60;
|
||||||
}
|
}
|
||||||
|
.module-expire-timer--with-sticker {
|
||||||
|
background-color: $color-gray-60;
|
||||||
|
}
|
||||||
|
|
||||||
.module-quote--outgoing {
|
.module-quote--outgoing {
|
||||||
border-left-color: $color-white;
|
border-left-color: $color-white;
|
||||||
|
@ -188,7 +196,6 @@
|
||||||
.module-message__metadata__date {
|
.module-message__metadata__date {
|
||||||
color: $color-white-08;
|
color: $color-white-08;
|
||||||
}
|
}
|
||||||
|
|
||||||
.module-message__metadata__date--incoming {
|
.module-message__metadata__date--incoming {
|
||||||
color: $color-gray-25;
|
color: $color-gray-25;
|
||||||
}
|
}
|
||||||
|
@ -203,7 +210,6 @@
|
||||||
.module-expire-timer {
|
.module-expire-timer {
|
||||||
background-color: $color-white-08;
|
background-color: $color-white-08;
|
||||||
}
|
}
|
||||||
|
|
||||||
.module-expire-timer--incoming {
|
.module-expire-timer--incoming {
|
||||||
background-color: $color-gray-25;
|
background-color: $color-gray-25;
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,7 +34,11 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
@mixin dark-theme() {
|
@mixin dark-theme() {
|
||||||
body.dark-theme & {
|
.dark-theme & {
|
||||||
@content;
|
@content;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@mixin popper-shadow() {
|
||||||
|
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.08), 0 8px 20px 0 rgba(0, 0, 0, 0.33);
|
||||||
|
}
|
||||||
|
|
|
@ -695,6 +695,10 @@ body.dark-theme {
|
||||||
color: $color-white;
|
color: $color-white;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.module-message__author_with_sticker {
|
||||||
|
color: $color-gray-05;
|
||||||
|
}
|
||||||
|
|
||||||
.module-message__text {
|
.module-message__text {
|
||||||
color: $color-dark-05;
|
color: $color-dark-05;
|
||||||
a {
|
a {
|
||||||
|
@ -1351,7 +1355,7 @@ body.dark-theme {
|
||||||
|
|
||||||
// Module: Image
|
// Module: Image
|
||||||
|
|
||||||
.module-image {
|
.module-image--with-background {
|
||||||
background-color: $color-black;
|
background-color: $color-black;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -42,6 +42,7 @@ $color-signal-blue-050: rgba($color-signal-blue, 0.5);
|
||||||
$color-white: #ffffff;
|
$color-white: #ffffff;
|
||||||
$color-gray-02: #f8f9f9;
|
$color-gray-02: #f8f9f9;
|
||||||
$color-gray-05: #eeefef;
|
$color-gray-05: #eeefef;
|
||||||
|
$color-gray-10: #e1e2e3;
|
||||||
$color-gray-15: #d5d6d6;
|
$color-gray-15: #d5d6d6;
|
||||||
$color-gray-25: #bbbdbe;
|
$color-gray-25: #bbbdbe;
|
||||||
$color-gray-45: #898a8c;
|
$color-gray-45: #898a8c;
|
||||||
|
|
16
ts/components/ConfirmationDialog.md
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
#### All Options
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
<util.ConversationContext theme={util.theme}>
|
||||||
|
<ConfirmationDialog
|
||||||
|
i18n={util.i18n}
|
||||||
|
onClose={() => console.log('onClose')}
|
||||||
|
onAffirmative={() => console.log('onAffirmative')}
|
||||||
|
affirmativeText="Affirm"
|
||||||
|
onNegative={() => console.log('onNegative')}
|
||||||
|
negativeText="Negate"
|
||||||
|
>
|
||||||
|
asdf child
|
||||||
|
</ConfirmationDialog>
|
||||||
|
</util.ConversationContext>
|
||||||
|
```
|
117
ts/components/ConfirmationDialog.tsx
Normal file
|
@ -0,0 +1,117 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import { LocalizerType } from '../types/Util';
|
||||||
|
|
||||||
|
export type OwnProps = {
|
||||||
|
readonly i18n: LocalizerType;
|
||||||
|
readonly children: React.ReactNode;
|
||||||
|
readonly affirmativeText?: string;
|
||||||
|
readonly onAffirmative?: () => unknown;
|
||||||
|
readonly onClose: () => unknown;
|
||||||
|
readonly negativeText?: string;
|
||||||
|
readonly onNegative?: () => unknown;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Props = OwnProps;
|
||||||
|
|
||||||
|
function focusRef(el: HTMLElement | null) {
|
||||||
|
if (el) {
|
||||||
|
el.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ConfirmationDialog = React.memo(
|
||||||
|
({
|
||||||
|
i18n,
|
||||||
|
onClose,
|
||||||
|
children,
|
||||||
|
onAffirmative,
|
||||||
|
onNegative,
|
||||||
|
affirmativeText,
|
||||||
|
negativeText,
|
||||||
|
}: Props) => {
|
||||||
|
React.useEffect(
|
||||||
|
() => {
|
||||||
|
const handler = ({ key }: KeyboardEvent) => {
|
||||||
|
if (key === 'Escape') {
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
document.addEventListener('keyup', handler);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('keyup', handler);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
[onClose]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleCancel = React.useCallback(
|
||||||
|
(e: React.MouseEvent) => {
|
||||||
|
if (e.target === e.currentTarget) {
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[onClose]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleNegative = React.useCallback(
|
||||||
|
() => {
|
||||||
|
onClose();
|
||||||
|
if (onNegative) {
|
||||||
|
onNegative();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[onClose, onNegative]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleAffirmative = React.useCallback(
|
||||||
|
() => {
|
||||||
|
onClose();
|
||||||
|
if (onAffirmative) {
|
||||||
|
onAffirmative();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[onClose, onAffirmative]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="module-confirmation-dialog__container">
|
||||||
|
<div className="module-confirmation-dialog__container__content">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
<div className="module-confirmation-dialog__container__buttons">
|
||||||
|
<button
|
||||||
|
onClick={handleCancel}
|
||||||
|
ref={focusRef}
|
||||||
|
className="module-confirmation-dialog__container__buttons__button"
|
||||||
|
>
|
||||||
|
{i18n('confirmation-dialog--Cancel')}
|
||||||
|
</button>
|
||||||
|
{onNegative && negativeText ? (
|
||||||
|
<button
|
||||||
|
onClick={handleNegative}
|
||||||
|
className={classNames(
|
||||||
|
'module-confirmation-dialog__container__buttons__button',
|
||||||
|
'module-confirmation-dialog__container__buttons__button--negative'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{negativeText}
|
||||||
|
</button>
|
||||||
|
) : null}
|
||||||
|
{onAffirmative && affirmativeText ? (
|
||||||
|
<button
|
||||||
|
onClick={handleAffirmative}
|
||||||
|
className={classNames(
|
||||||
|
'module-confirmation-dialog__container__buttons__button',
|
||||||
|
'module-confirmation-dialog__container__buttons__button--affirmative'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{affirmativeText}
|
||||||
|
</button>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
89
ts/components/ConfirmationModal.tsx
Normal file
|
@ -0,0 +1,89 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
import { createPortal } from 'react-dom';
|
||||||
|
import { ConfirmationDialog } from './ConfirmationDialog';
|
||||||
|
import { LocalizerType } from '../types/Util';
|
||||||
|
|
||||||
|
export type OwnProps = {
|
||||||
|
readonly i18n: LocalizerType;
|
||||||
|
readonly children: React.ReactNode;
|
||||||
|
readonly affirmativeText?: string;
|
||||||
|
readonly onAffirmative?: () => unknown;
|
||||||
|
readonly onClose: () => unknown;
|
||||||
|
readonly negativeText?: string;
|
||||||
|
readonly onNegative?: () => unknown;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Props = OwnProps;
|
||||||
|
|
||||||
|
export const ConfirmationModal = React.memo(
|
||||||
|
// tslint:disable-next-line max-func-body-length
|
||||||
|
({
|
||||||
|
i18n,
|
||||||
|
onClose,
|
||||||
|
children,
|
||||||
|
onAffirmative,
|
||||||
|
onNegative,
|
||||||
|
affirmativeText,
|
||||||
|
negativeText,
|
||||||
|
}: Props) => {
|
||||||
|
const [root, setRoot] = React.useState<HTMLElement | null>(null);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
document.body.appendChild(div);
|
||||||
|
setRoot(div);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.body.removeChild(div);
|
||||||
|
setRoot(null);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
React.useEffect(
|
||||||
|
() => {
|
||||||
|
const handler = ({ key }: KeyboardEvent) => {
|
||||||
|
if (key === 'Escape') {
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
document.addEventListener('keyup', handler);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('keyup', handler);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
[onClose]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleCancel = React.useCallback(
|
||||||
|
(e: React.MouseEvent) => {
|
||||||
|
if (e.target === e.currentTarget) {
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[onClose]
|
||||||
|
);
|
||||||
|
|
||||||
|
return root
|
||||||
|
? createPortal(
|
||||||
|
<div
|
||||||
|
role="button"
|
||||||
|
className="module-confirmation-dialog__overlay"
|
||||||
|
onClick={handleCancel}
|
||||||
|
>
|
||||||
|
<ConfirmationDialog
|
||||||
|
i18n={i18n}
|
||||||
|
onClose={onClose}
|
||||||
|
onAffirmative={onAffirmative}
|
||||||
|
onNegative={onNegative}
|
||||||
|
affirmativeText={affirmativeText}
|
||||||
|
negativeText={negativeText}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</ConfirmationDialog>
|
||||||
|
</div>,
|
||||||
|
root
|
||||||
|
)
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
);
|
|
@ -5,9 +5,10 @@ import { getIncrement, getTimerBucket } from '../../util/timer';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
withImageNoCaption: boolean;
|
withImageNoCaption: boolean;
|
||||||
|
withSticker: boolean;
|
||||||
expirationLength: number;
|
expirationLength: number;
|
||||||
expirationTimestamp: number;
|
expirationTimestamp: number;
|
||||||
direction: 'incoming' | 'outgoing';
|
direction?: 'incoming' | 'outgoing';
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ExpireTimer extends React.Component<Props> {
|
export class ExpireTimer extends React.Component<Props> {
|
||||||
|
@ -44,6 +45,7 @@ export class ExpireTimer extends React.Component<Props> {
|
||||||
expirationLength,
|
expirationLength,
|
||||||
expirationTimestamp,
|
expirationTimestamp,
|
||||||
withImageNoCaption,
|
withImageNoCaption,
|
||||||
|
withSticker,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const bucket = getTimerBucket(expirationTimestamp, expirationLength);
|
const bucket = getTimerBucket(expirationTimestamp, expirationLength);
|
||||||
|
@ -56,7 +58,8 @@ export class ExpireTimer extends React.Component<Props> {
|
||||||
`module-expire-timer--${direction}`,
|
`module-expire-timer--${direction}`,
|
||||||
withImageNoCaption
|
withImageNoCaption
|
||||||
? 'module-expire-timer--with-image-no-caption'
|
? 'module-expire-timer--with-image-no-caption'
|
||||||
: null
|
: null,
|
||||||
|
withSticker ? 'module-expire-timer--with-sticker' : null
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
|
@ -418,3 +418,51 @@
|
||||||
</div>
|
</div>
|
||||||
</util.ConversationContext>
|
</util.ConversationContext>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### No border, no background
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
<util.ConversationContext theme={util.theme}>
|
||||||
|
<div style={{ padding: '10px', backgroundColor: 'lightgrey' }}>
|
||||||
|
<div>
|
||||||
|
<Image
|
||||||
|
height="512"
|
||||||
|
width="512"
|
||||||
|
noBorder={true}
|
||||||
|
noBackground={true}
|
||||||
|
attachment={{}}
|
||||||
|
onClick={() => console.log('onClick')}
|
||||||
|
onClickClose={attachment => console.log('onClickClose', attachment)}
|
||||||
|
url={util.squareStickerObjectUrl}
|
||||||
|
i18n={util.i18n}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Image
|
||||||
|
height="256"
|
||||||
|
width="256"
|
||||||
|
noBorder={true}
|
||||||
|
noBackground={true}
|
||||||
|
attachment={{}}
|
||||||
|
onClick={() => console.log('onClick')}
|
||||||
|
onClickClose={attachment => console.log('onClickClose', attachment)}
|
||||||
|
url={util.squareStickerObjectUrl}
|
||||||
|
i18n={util.i18n}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Image
|
||||||
|
height="128"
|
||||||
|
width="128"
|
||||||
|
noBorder={true}
|
||||||
|
noBackground={true}
|
||||||
|
attachment={{}}
|
||||||
|
onClick={() => console.log('onClick')}
|
||||||
|
onClickClose={attachment => console.log('onClickClose', attachment)}
|
||||||
|
url={util.squareStickerObjectUrl}
|
||||||
|
i18n={util.i18n}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</util.ConversationContext>
|
||||||
|
```
|
||||||
|
|
|
@ -15,6 +15,8 @@ interface Props {
|
||||||
|
|
||||||
overlayText?: string;
|
overlayText?: string;
|
||||||
|
|
||||||
|
noBorder?: boolean;
|
||||||
|
noBackground?: boolean;
|
||||||
bottomOverlay?: boolean;
|
bottomOverlay?: boolean;
|
||||||
closeButton?: boolean;
|
closeButton?: boolean;
|
||||||
curveBottomLeft?: boolean;
|
curveBottomLeft?: boolean;
|
||||||
|
@ -49,6 +51,8 @@ export class Image extends React.Component<Props> {
|
||||||
darkOverlay,
|
darkOverlay,
|
||||||
height,
|
height,
|
||||||
i18n,
|
i18n,
|
||||||
|
noBackground,
|
||||||
|
noBorder,
|
||||||
onClick,
|
onClick,
|
||||||
onClickClose,
|
onClickClose,
|
||||||
onError,
|
onError,
|
||||||
|
@ -74,6 +78,7 @@ export class Image extends React.Component<Props> {
|
||||||
}}
|
}}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'module-image',
|
'module-image',
|
||||||
|
!noBackground ? 'module-image--with-background' : null,
|
||||||
canClick ? 'module-image__with-click-handler' : null,
|
canClick ? 'module-image__with-click-handler' : null,
|
||||||
curveBottomLeft ? 'module-image--curved-bottom-left' : null,
|
curveBottomLeft ? 'module-image--curved-bottom-left' : null,
|
||||||
curveBottomRight ? 'module-image--curved-bottom-right' : null,
|
curveBottomRight ? 'module-image--curved-bottom-right' : null,
|
||||||
|
@ -113,6 +118,7 @@ export class Image extends React.Component<Props> {
|
||||||
alt={i18n('imageCaptionIconAlt')}
|
alt={i18n('imageCaptionIconAlt')}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
{!noBorder ? (
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'module-image__border-overlay',
|
'module-image__border-overlay',
|
||||||
|
@ -125,6 +131,7 @@ export class Image extends React.Component<Props> {
|
||||||
darkOverlay ? 'module-image__border-overlay--dark' : null
|
darkOverlay ? 'module-image__border-overlay--dark' : null
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
) : null}
|
||||||
{closeButton ? (
|
{closeButton ? (
|
||||||
<div
|
<div
|
||||||
role="button"
|
role="button"
|
||||||
|
|
|
@ -384,3 +384,26 @@ const attachments = [
|
||||||
</div>
|
</div>
|
||||||
</div>;
|
</div>;
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Sticker
|
||||||
|
|
||||||
|
```
|
||||||
|
const attachments = [
|
||||||
|
{
|
||||||
|
url: util.squareStickerObjectUrl,
|
||||||
|
contentType: 'image/webp',
|
||||||
|
width: 512,
|
||||||
|
height: 512,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
<ImageGrid isSticker={true} stickerSize={128} attachments={attachments} i18n={util.i18n} />
|
||||||
|
</div>
|
||||||
|
<hr />
|
||||||
|
<div>
|
||||||
|
<ImageGrid isSticker={true} stickerSize={128} withContentAbove withContentBelow attachments={attachments} i18n={util.i18n} />
|
||||||
|
</div>
|
||||||
|
</div>;
|
||||||
|
```
|
||||||
|
|
|
@ -20,6 +20,8 @@ interface Props {
|
||||||
withContentAbove?: boolean;
|
withContentAbove?: boolean;
|
||||||
withContentBelow?: boolean;
|
withContentBelow?: boolean;
|
||||||
bottomOverlay?: boolean;
|
bottomOverlay?: boolean;
|
||||||
|
isSticker?: boolean;
|
||||||
|
stickerSize?: number;
|
||||||
|
|
||||||
i18n: LocalizerType;
|
i18n: LocalizerType;
|
||||||
|
|
||||||
|
@ -34,6 +36,8 @@ export class ImageGrid extends React.Component<Props> {
|
||||||
attachments,
|
attachments,
|
||||||
bottomOverlay,
|
bottomOverlay,
|
||||||
i18n,
|
i18n,
|
||||||
|
isSticker,
|
||||||
|
stickerSize,
|
||||||
onError,
|
onError,
|
||||||
onClick,
|
onClick,
|
||||||
withContentAbove,
|
withContentAbove,
|
||||||
|
@ -56,25 +60,31 @@ export class ImageGrid extends React.Component<Props> {
|
||||||
if (attachments.length === 1 || !areAllAttachmentsVisual(attachments)) {
|
if (attachments.length === 1 || !areAllAttachmentsVisual(attachments)) {
|
||||||
const { height, width } = getImageDimensions(attachments[0]);
|
const { height, width } = getImageDimensions(attachments[0]);
|
||||||
|
|
||||||
|
const finalHeight = isSticker ? stickerSize : height;
|
||||||
|
const finalWidth = isSticker ? stickerSize : width;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'module-image-grid',
|
'module-image-grid',
|
||||||
'module-image-grid--one-image'
|
'module-image-grid--one-image',
|
||||||
|
isSticker ? 'module-image-grid--with-sticker' : null
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Image
|
<Image
|
||||||
alt={getAlt(attachments[0], i18n)}
|
alt={getAlt(attachments[0], i18n)}
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
bottomOverlay={withBottomOverlay}
|
bottomOverlay={withBottomOverlay}
|
||||||
|
noBorder={isSticker}
|
||||||
|
noBackground={isSticker}
|
||||||
curveTopLeft={curveTopLeft}
|
curveTopLeft={curveTopLeft}
|
||||||
curveTopRight={curveTopRight}
|
curveTopRight={curveTopRight}
|
||||||
curveBottomLeft={curveBottomLeft}
|
curveBottomLeft={curveBottomLeft}
|
||||||
curveBottomRight={curveBottomRight}
|
curveBottomRight={curveBottomRight}
|
||||||
attachment={attachments[0]}
|
attachment={attachments[0]}
|
||||||
playIconOverlay={isVideoAttachment(attachments[0])}
|
playIconOverlay={isVideoAttachment(attachments[0])}
|
||||||
height={height}
|
height={finalHeight}
|
||||||
width={width}
|
width={finalWidth}
|
||||||
url={getUrl(attachments[0])}
|
url={getUrl(attachments[0])}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
onError={onError}
|
onError={onError}
|
||||||
|
@ -91,6 +101,7 @@ export class ImageGrid extends React.Component<Props> {
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
attachment={attachments[0]}
|
attachment={attachments[0]}
|
||||||
bottomOverlay={withBottomOverlay}
|
bottomOverlay={withBottomOverlay}
|
||||||
|
noBorder={isSticker}
|
||||||
curveTopLeft={curveTopLeft}
|
curveTopLeft={curveTopLeft}
|
||||||
curveBottomLeft={curveBottomLeft}
|
curveBottomLeft={curveBottomLeft}
|
||||||
playIconOverlay={isVideoAttachment(attachments[0])}
|
playIconOverlay={isVideoAttachment(attachments[0])}
|
||||||
|
@ -104,6 +115,7 @@ export class ImageGrid extends React.Component<Props> {
|
||||||
alt={getAlt(attachments[1], i18n)}
|
alt={getAlt(attachments[1], i18n)}
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
bottomOverlay={withBottomOverlay}
|
bottomOverlay={withBottomOverlay}
|
||||||
|
noBorder={isSticker}
|
||||||
curveTopRight={curveTopRight}
|
curveTopRight={curveTopRight}
|
||||||
curveBottomRight={curveBottomRight}
|
curveBottomRight={curveBottomRight}
|
||||||
playIconOverlay={isVideoAttachment(attachments[1])}
|
playIconOverlay={isVideoAttachment(attachments[1])}
|
||||||
|
@ -125,6 +137,7 @@ export class ImageGrid extends React.Component<Props> {
|
||||||
alt={getAlt(attachments[0], i18n)}
|
alt={getAlt(attachments[0], i18n)}
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
bottomOverlay={withBottomOverlay}
|
bottomOverlay={withBottomOverlay}
|
||||||
|
noBorder={isSticker}
|
||||||
curveTopLeft={curveTopLeft}
|
curveTopLeft={curveTopLeft}
|
||||||
curveBottomLeft={curveBottomLeft}
|
curveBottomLeft={curveBottomLeft}
|
||||||
attachment={attachments[0]}
|
attachment={attachments[0]}
|
||||||
|
@ -152,6 +165,7 @@ export class ImageGrid extends React.Component<Props> {
|
||||||
alt={getAlt(attachments[2], i18n)}
|
alt={getAlt(attachments[2], i18n)}
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
bottomOverlay={withBottomOverlay}
|
bottomOverlay={withBottomOverlay}
|
||||||
|
noBorder={isSticker}
|
||||||
curveBottomRight={curveBottomRight}
|
curveBottomRight={curveBottomRight}
|
||||||
height={99}
|
height={99}
|
||||||
width={99}
|
width={99}
|
||||||
|
@ -201,6 +215,7 @@ export class ImageGrid extends React.Component<Props> {
|
||||||
alt={getAlt(attachments[2], i18n)}
|
alt={getAlt(attachments[2], i18n)}
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
bottomOverlay={withBottomOverlay}
|
bottomOverlay={withBottomOverlay}
|
||||||
|
noBorder={isSticker}
|
||||||
curveBottomLeft={curveBottomLeft}
|
curveBottomLeft={curveBottomLeft}
|
||||||
playIconOverlay={isVideoAttachment(attachments[2])}
|
playIconOverlay={isVideoAttachment(attachments[2])}
|
||||||
height={149}
|
height={149}
|
||||||
|
@ -214,6 +229,7 @@ export class ImageGrid extends React.Component<Props> {
|
||||||
alt={getAlt(attachments[3], i18n)}
|
alt={getAlt(attachments[3], i18n)}
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
bottomOverlay={withBottomOverlay}
|
bottomOverlay={withBottomOverlay}
|
||||||
|
noBorder={isSticker}
|
||||||
curveBottomRight={curveBottomRight}
|
curveBottomRight={curveBottomRight}
|
||||||
playIconOverlay={isVideoAttachment(attachments[3])}
|
playIconOverlay={isVideoAttachment(attachments[3])}
|
||||||
height={149}
|
height={149}
|
||||||
|
@ -268,6 +284,7 @@ export class ImageGrid extends React.Component<Props> {
|
||||||
alt={getAlt(attachments[2], i18n)}
|
alt={getAlt(attachments[2], i18n)}
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
bottomOverlay={withBottomOverlay}
|
bottomOverlay={withBottomOverlay}
|
||||||
|
noBorder={isSticker}
|
||||||
curveBottomLeft={curveBottomLeft}
|
curveBottomLeft={curveBottomLeft}
|
||||||
playIconOverlay={isVideoAttachment(attachments[2])}
|
playIconOverlay={isVideoAttachment(attachments[2])}
|
||||||
height={99}
|
height={99}
|
||||||
|
@ -281,6 +298,7 @@ export class ImageGrid extends React.Component<Props> {
|
||||||
alt={getAlt(attachments[3], i18n)}
|
alt={getAlt(attachments[3], i18n)}
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
bottomOverlay={withBottomOverlay}
|
bottomOverlay={withBottomOverlay}
|
||||||
|
noBorder={isSticker}
|
||||||
playIconOverlay={isVideoAttachment(attachments[3])}
|
playIconOverlay={isVideoAttachment(attachments[3])}
|
||||||
height={99}
|
height={99}
|
||||||
width={98}
|
width={98}
|
||||||
|
@ -293,6 +311,7 @@ export class ImageGrid extends React.Component<Props> {
|
||||||
alt={getAlt(attachments[4], i18n)}
|
alt={getAlt(attachments[4], i18n)}
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
bottomOverlay={withBottomOverlay}
|
bottomOverlay={withBottomOverlay}
|
||||||
|
noBorder={isSticker}
|
||||||
curveBottomRight={curveBottomRight}
|
curveBottomRight={curveBottomRight}
|
||||||
playIconOverlay={isVideoAttachment(attachments[4])}
|
playIconOverlay={isVideoAttachment(attachments[4])}
|
||||||
height={99}
|
height={99}
|
||||||
|
|
|
@ -39,11 +39,13 @@ interface Trigger {
|
||||||
|
|
||||||
// Same as MIN_WIDTH in ImageGrid.tsx
|
// Same as MIN_WIDTH in ImageGrid.tsx
|
||||||
const MINIMUM_LINK_PREVIEW_IMAGE_WIDTH = 200;
|
const MINIMUM_LINK_PREVIEW_IMAGE_WIDTH = 200;
|
||||||
|
const STICKER_SIZE = 128;
|
||||||
|
|
||||||
interface LinkPreviewType {
|
interface LinkPreviewType {
|
||||||
title: string;
|
title: string;
|
||||||
domain: string;
|
domain: string;
|
||||||
url: string;
|
url: string;
|
||||||
|
isStickerPack: boolean;
|
||||||
image?: AttachmentType;
|
image?: AttachmentType;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -51,6 +53,7 @@ export type PropsData = {
|
||||||
id: string;
|
id: string;
|
||||||
text?: string;
|
text?: string;
|
||||||
textPending?: boolean;
|
textPending?: boolean;
|
||||||
|
isSticker: boolean;
|
||||||
direction: 'incoming' | 'outgoing';
|
direction: 'incoming' | 'outgoing';
|
||||||
timestamp: number;
|
timestamp: number;
|
||||||
status?: 'sending' | 'sent' | 'delivered' | 'read' | 'error';
|
status?: 'sending' | 'sent' | 'delivered' | 'read' | 'error';
|
||||||
|
@ -223,6 +226,7 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
expirationLength,
|
expirationLength,
|
||||||
expirationTimestamp,
|
expirationTimestamp,
|
||||||
i18n,
|
i18n,
|
||||||
|
isSticker,
|
||||||
status,
|
status,
|
||||||
text,
|
text,
|
||||||
textPending,
|
textPending,
|
||||||
|
@ -234,8 +238,9 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
}
|
}
|
||||||
|
|
||||||
const isShowingImage = this.isShowingImage();
|
const isShowingImage = this.isShowingImage();
|
||||||
const withImageNoCaption = Boolean(!text && isShowingImage);
|
const withImageNoCaption = Boolean(!isSticker && !text && isShowingImage);
|
||||||
const showError = status === 'error' && direction === 'outgoing';
|
const showError = status === 'error' && direction === 'outgoing';
|
||||||
|
const metadataDirection = isSticker ? undefined : direction;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
@ -250,7 +255,10 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
<span
|
<span
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'module-message__metadata__date',
|
'module-message__metadata__date',
|
||||||
`module-message__metadata__date--${direction}`,
|
isSticker ? 'module-message__metadata__date--with-sticker' : null,
|
||||||
|
!isSticker
|
||||||
|
? `module-message__metadata__date--${direction}`
|
||||||
|
: null,
|
||||||
withImageNoCaption
|
withImageNoCaption
|
||||||
? 'module-message__metadata__date--with-image-no-caption'
|
? 'module-message__metadata__date--with-image-no-caption'
|
||||||
: null
|
: null
|
||||||
|
@ -263,17 +271,19 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
timestamp={timestamp}
|
timestamp={timestamp}
|
||||||
extended={true}
|
extended={true}
|
||||||
direction={direction}
|
direction={metadataDirection}
|
||||||
withImageNoCaption={withImageNoCaption}
|
withImageNoCaption={withImageNoCaption}
|
||||||
|
withSticker={isSticker}
|
||||||
module="module-message__metadata__date"
|
module="module-message__metadata__date"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{expirationLength && expirationTimestamp ? (
|
{expirationLength && expirationTimestamp ? (
|
||||||
<ExpireTimer
|
<ExpireTimer
|
||||||
direction={direction}
|
direction={metadataDirection}
|
||||||
expirationLength={expirationLength}
|
expirationLength={expirationLength}
|
||||||
expirationTimestamp={expirationTimestamp}
|
expirationTimestamp={expirationTimestamp}
|
||||||
withImageNoCaption={withImageNoCaption}
|
withImageNoCaption={withImageNoCaption}
|
||||||
|
withSticker={isSticker}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
<span className="module-message__metadata__spacer" />
|
<span className="module-message__metadata__spacer" />
|
||||||
|
@ -287,6 +297,9 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'module-message__metadata__status-icon',
|
'module-message__metadata__status-icon',
|
||||||
`module-message__metadata__status-icon--${status}`,
|
`module-message__metadata__status-icon--${status}`,
|
||||||
|
isSticker
|
||||||
|
? 'module-message__metadata__status-icon--with-sticker'
|
||||||
|
: null,
|
||||||
withImageNoCaption
|
withImageNoCaption
|
||||||
? 'module-message__metadata__status-icon--with-image-no-caption'
|
? 'module-message__metadata__status-icon--with-image-no-caption'
|
||||||
: null
|
: null
|
||||||
|
@ -302,24 +315,33 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
authorName,
|
authorName,
|
||||||
authorPhoneNumber,
|
authorPhoneNumber,
|
||||||
authorProfileName,
|
authorProfileName,
|
||||||
|
collapseMetadata,
|
||||||
conversationType,
|
conversationType,
|
||||||
direction,
|
direction,
|
||||||
i18n,
|
i18n,
|
||||||
|
isSticker,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
|
if (collapseMetadata) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const title = authorName ? authorName : authorPhoneNumber;
|
const title = authorName ? authorName : authorPhoneNumber;
|
||||||
|
|
||||||
if (direction !== 'incoming' || conversationType !== 'group' || !title) {
|
if (direction !== 'incoming' || conversationType !== 'group' || !title) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const suffix = isSticker ? '_with_sticker' : '';
|
||||||
|
const moduleName = `module-message__author${suffix}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="module-message__author">
|
<div className={moduleName}>
|
||||||
<ContactName
|
<ContactName
|
||||||
phoneNumber={authorPhoneNumber}
|
phoneNumber={authorPhoneNumber}
|
||||||
name={authorName}
|
name={authorName}
|
||||||
profileName={authorProfileName}
|
profileName={authorProfileName}
|
||||||
module="module-message__author"
|
module={moduleName}
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -329,15 +351,16 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
// tslint:disable-next-line max-func-body-length cyclomatic-complexity
|
// tslint:disable-next-line max-func-body-length cyclomatic-complexity
|
||||||
public renderAttachment() {
|
public renderAttachment() {
|
||||||
const {
|
const {
|
||||||
id,
|
|
||||||
attachments,
|
attachments,
|
||||||
text,
|
|
||||||
collapseMetadata,
|
collapseMetadata,
|
||||||
conversationType,
|
conversationType,
|
||||||
direction,
|
direction,
|
||||||
i18n,
|
i18n,
|
||||||
|
id,
|
||||||
quote,
|
quote,
|
||||||
showVisualAttachment,
|
showVisualAttachment,
|
||||||
|
isSticker,
|
||||||
|
text,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
const { imageBroken } = this.state;
|
const { imageBroken } = this.state;
|
||||||
|
|
||||||
|
@ -359,23 +382,31 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
((isImage(attachments) && hasImage(attachments)) ||
|
((isImage(attachments) && hasImage(attachments)) ||
|
||||||
(isVideo(attachments) && hasVideoScreenshot(attachments)))
|
(isVideo(attachments) && hasVideoScreenshot(attachments)))
|
||||||
) {
|
) {
|
||||||
|
const prefix = isSticker ? 'sticker' : 'attachment';
|
||||||
|
const bottomOverlay = !isSticker && !collapseMetadata;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'module-message__attachment-container',
|
`module-message__${prefix}-container`,
|
||||||
withContentAbove
|
withContentAbove
|
||||||
? 'module-message__attachment-container--with-content-above'
|
? `module-message__${prefix}-container--with-content-above`
|
||||||
: null,
|
: null,
|
||||||
withContentBelow
|
withContentBelow
|
||||||
? 'module-message__attachment-container--with-content-below'
|
? 'module-message__attachment-container--with-content-below'
|
||||||
|
: null,
|
||||||
|
isSticker && !collapseMetadata
|
||||||
|
? 'module-message__sticker-container--with-content-below'
|
||||||
: null
|
: null
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<ImageGrid
|
<ImageGrid
|
||||||
attachments={attachments}
|
attachments={attachments}
|
||||||
withContentAbove={withContentAbove}
|
withContentAbove={isSticker || withContentAbove}
|
||||||
withContentBelow={withContentBelow}
|
withContentBelow={isSticker || withContentBelow}
|
||||||
bottomOverlay={!collapseMetadata}
|
isSticker={isSticker}
|
||||||
|
stickerSize={STICKER_SIZE}
|
||||||
|
bottomOverlay={bottomOverlay}
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
onError={this.handleImageErrorBound}
|
onError={this.handleImageErrorBound}
|
||||||
onClick={attachment => {
|
onClick={attachment => {
|
||||||
|
@ -494,7 +525,10 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
|
|
||||||
const previewHasImage = first.image && isImageAttachment(first.image);
|
const previewHasImage = first.image && isImageAttachment(first.image);
|
||||||
const width = first.image && first.image.width;
|
const width = first.image && first.image.width;
|
||||||
const isFullSizeImage = width && width >= MINIMUM_LINK_PREVIEW_IMAGE_WIDTH;
|
const isFullSizeImage =
|
||||||
|
!first.isStickerPack &&
|
||||||
|
width &&
|
||||||
|
width >= MINIMUM_LINK_PREVIEW_IMAGE_WIDTH;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
@ -768,6 +802,7 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
disableMenu,
|
disableMenu,
|
||||||
downloadAttachment,
|
downloadAttachment,
|
||||||
id,
|
id,
|
||||||
|
isSticker,
|
||||||
replyToMessage,
|
replyToMessage,
|
||||||
timestamp,
|
timestamp,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
@ -783,7 +818,10 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
const firstAttachment = attachments && attachments[0];
|
const firstAttachment = attachments && attachments[0];
|
||||||
|
|
||||||
const downloadButton =
|
const downloadButton =
|
||||||
!multipleAttachments && firstAttachment && !firstAttachment.pending ? (
|
!isSticker &&
|
||||||
|
!multipleAttachments &&
|
||||||
|
firstAttachment &&
|
||||||
|
!firstAttachment.pending ? (
|
||||||
<div
|
<div
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
downloadAttachment({
|
downloadAttachment({
|
||||||
|
@ -850,6 +888,7 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
downloadAttachment,
|
downloadAttachment,
|
||||||
i18n,
|
i18n,
|
||||||
id,
|
id,
|
||||||
|
isSticker,
|
||||||
deleteMessage,
|
deleteMessage,
|
||||||
showMessageDetail,
|
showMessageDetail,
|
||||||
replyToMessage,
|
replyToMessage,
|
||||||
|
@ -866,7 +905,7 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
|
|
||||||
const menu = (
|
const menu = (
|
||||||
<ContextMenu id={triggerId}>
|
<ContextMenu id={triggerId}>
|
||||||
{!multipleAttachments && attachments && attachments[0] ? (
|
{!isSticker && !multipleAttachments && attachments && attachments[0] ? (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
attributes={{
|
attributes={{
|
||||||
className: 'module-message__context__download',
|
className: 'module-message__context__download',
|
||||||
|
@ -931,9 +970,14 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
}
|
}
|
||||||
|
|
||||||
public getWidth(): number | undefined {
|
public getWidth(): number | undefined {
|
||||||
const { attachments, previews } = this.props;
|
const { attachments, isSticker, previews } = this.props;
|
||||||
|
|
||||||
if (attachments && attachments.length) {
|
if (attachments && attachments.length) {
|
||||||
|
if (isSticker) {
|
||||||
|
// Padding is 8px, on both sides
|
||||||
|
return STICKER_SIZE + 8 * 2;
|
||||||
|
}
|
||||||
|
|
||||||
const dimensions = getGridDimensions(attachments);
|
const dimensions = getGridDimensions(attachments);
|
||||||
if (dimensions) {
|
if (dimensions) {
|
||||||
return dimensions.width;
|
return dimensions.width;
|
||||||
|
@ -949,6 +993,7 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
const { width } = first.image;
|
const { width } = first.image;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
!first.isStickerPack &&
|
||||||
isImageAttachment(first.image) &&
|
isImageAttachment(first.image) &&
|
||||||
width &&
|
width &&
|
||||||
width >= MINIMUM_LINK_PREVIEW_IMAGE_WIDTH
|
width >= MINIMUM_LINK_PREVIEW_IMAGE_WIDTH
|
||||||
|
@ -999,11 +1044,13 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
const {
|
const {
|
||||||
authorPhoneNumber,
|
authorPhoneNumber,
|
||||||
authorColor,
|
authorColor,
|
||||||
|
attachments,
|
||||||
direction,
|
direction,
|
||||||
id,
|
id,
|
||||||
|
isSticker,
|
||||||
timestamp,
|
timestamp,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
const { expired, expiring } = this.state;
|
const { expired, expiring, imageBroken } = this.state;
|
||||||
|
|
||||||
// This id is what connects our triple-dot click with our associated pop-up menu.
|
// This id is what connects our triple-dot click with our associated pop-up menu.
|
||||||
// It needs to be unique.
|
// It needs to be unique.
|
||||||
|
@ -1013,6 +1060,10 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isSticker && (imageBroken || !attachments || !attachments.length)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const width = this.getWidth();
|
const width = this.getWidth();
|
||||||
const isShowingImage = this.isShowingImage();
|
const isShowingImage = this.isShowingImage();
|
||||||
|
|
||||||
|
@ -1029,8 +1080,9 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'module-message__container',
|
'module-message__container',
|
||||||
`module-message__container--${direction}`,
|
isSticker ? 'module-message__container--with-sticker' : null,
|
||||||
direction === 'incoming'
|
!isSticker ? `module-message__container--${direction}` : null,
|
||||||
|
!isSticker && direction === 'incoming'
|
||||||
? `module-message__container--incoming-${authorColor}`
|
? `module-message__container--incoming-${authorColor}`
|
||||||
: null
|
: null
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -11,6 +11,7 @@ interface Props {
|
||||||
extended?: boolean;
|
extended?: boolean;
|
||||||
module?: string;
|
module?: string;
|
||||||
withImageNoCaption?: boolean;
|
withImageNoCaption?: boolean;
|
||||||
|
withSticker?: boolean;
|
||||||
direction?: 'incoming' | 'outgoing';
|
direction?: 'incoming' | 'outgoing';
|
||||||
i18n: LocalizerType;
|
i18n: LocalizerType;
|
||||||
}
|
}
|
||||||
|
@ -48,6 +49,7 @@ export class Timestamp extends React.Component<Props> {
|
||||||
module,
|
module,
|
||||||
timestamp,
|
timestamp,
|
||||||
withImageNoCaption,
|
withImageNoCaption,
|
||||||
|
withSticker,
|
||||||
extended,
|
extended,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
const moduleName = module || 'module-timestamp';
|
const moduleName = module || 'module-timestamp';
|
||||||
|
@ -61,7 +63,8 @@ export class Timestamp extends React.Component<Props> {
|
||||||
className={classNames(
|
className={classNames(
|
||||||
moduleName,
|
moduleName,
|
||||||
direction ? `${moduleName}--${direction}` : null,
|
direction ? `${moduleName}--${direction}` : null,
|
||||||
withImageNoCaption ? `${moduleName}--with-image-no-caption` : null
|
withImageNoCaption ? `${moduleName}--with-image-no-caption` : null,
|
||||||
|
withSticker ? `${moduleName}--with-sticker` : null
|
||||||
)}
|
)}
|
||||||
title={moment(timestamp).format('llll')}
|
title={moment(timestamp).format('llll')}
|
||||||
>
|
>
|
||||||
|
|
271
ts/components/stickers/StickerButton.md
Normal file
|
@ -0,0 +1,271 @@
|
||||||
|
#### Default
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
const abeSticker = { id: 4, url: util.squareStickerObjectUrl, packId: 'abe' };
|
||||||
|
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 packs = [
|
||||||
|
{
|
||||||
|
id: 'foo',
|
||||||
|
cover: sticker1,
|
||||||
|
stickers: Array(101)
|
||||||
|
.fill(0)
|
||||||
|
.map((n, id) => ({ ...sticker1, id })),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'bar',
|
||||||
|
cover: sticker2,
|
||||||
|
stickers: Array(101)
|
||||||
|
.fill(0)
|
||||||
|
.map((n, id) => ({ ...sticker2, id })),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'baz',
|
||||||
|
cover: sticker3,
|
||||||
|
stickers: Array(101)
|
||||||
|
.fill(0)
|
||||||
|
.map((n, id) => ({ ...sticker3, id })),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
<util.ConversationContext theme={util.theme}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
height: '500px',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
justifyContent: 'flex-end',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<StickerButton
|
||||||
|
i18n={util.i18n}
|
||||||
|
receivedPacks={[]}
|
||||||
|
installedPacks={packs}
|
||||||
|
onPickSticker={(packId, stickerId) =>
|
||||||
|
console.log('onPickSticker', { packId, stickerId })
|
||||||
|
}
|
||||||
|
clearInstalledStickerPack={() => console.log('clearInstalledStickerPack')}
|
||||||
|
onClickAddPack={() => console.log('onClickAddPack')}
|
||||||
|
recentStickers={[abeSticker, sticker1, sticker2, sticker3]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</util.ConversationContext>;
|
||||||
|
```
|
||||||
|
|
||||||
|
#### No Installed Packs
|
||||||
|
|
||||||
|
When there are no installed packs the button should call the `onClickAddPack`
|
||||||
|
callback.
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
const abeSticker = { id: 4, url: util.squareStickerObjectUrl, packId: 'abe' };
|
||||||
|
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 packs = [
|
||||||
|
{
|
||||||
|
id: 'foo',
|
||||||
|
cover: sticker1,
|
||||||
|
stickers: Array(101)
|
||||||
|
.fill(0)
|
||||||
|
.map((n, id) => ({ ...sticker1, id })),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'bar',
|
||||||
|
cover: sticker2,
|
||||||
|
stickers: Array(101)
|
||||||
|
.fill(0)
|
||||||
|
.map((n, id) => ({ ...sticker2, id })),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'baz',
|
||||||
|
cover: sticker3,
|
||||||
|
stickers: Array(101)
|
||||||
|
.fill(0)
|
||||||
|
.map((n, id) => ({ ...sticker3, id })),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
<util.ConversationContext theme={util.theme}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
height: '500px',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
justifyContent: 'flex-end',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<StickerButton
|
||||||
|
i18n={util.i18n}
|
||||||
|
receivedPacks={[]}
|
||||||
|
installedPacks={packs}
|
||||||
|
onPickSticker={(packId, stickerId) =>
|
||||||
|
console.log('onPickSticker', { packId, stickerId })
|
||||||
|
}
|
||||||
|
clearInstalledStickerPack={() => console.log('clearInstalledStickerPack')}
|
||||||
|
onClickAddPack={() => console.log('onClickAddPack')}
|
||||||
|
recentStickers={[abeSticker, sticker1, sticker2, sticker3]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</util.ConversationContext>;
|
||||||
|
```
|
||||||
|
|
||||||
|
#### No Advertised Packs and No Installed Packs
|
||||||
|
|
||||||
|
When there are no advertised packs and no installed packs the button should not render anything.
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
<util.ConversationContext theme={util.theme}>
|
||||||
|
<StickerButton
|
||||||
|
i18n={util.i18n}
|
||||||
|
receivedPacks={[]}
|
||||||
|
installedPacks={[]}
|
||||||
|
onPickSticker={(packId, stickerId) =>
|
||||||
|
console.log('onPickSticker', { packId, stickerId })
|
||||||
|
}
|
||||||
|
clearInstalledStickerPack={() => console.log('clearInstalledStickerPack')}
|
||||||
|
onClickAddPack={() => console.log('onClickAddPack')}
|
||||||
|
recentStickers={[]}
|
||||||
|
/>
|
||||||
|
</util.ConversationContext>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Installed Pack Tooltip
|
||||||
|
|
||||||
|
When a pack is installed there should be a tooltip saying as such.
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
const abeSticker = { id: 4, url: util.squareStickerObjectUrl, packId: 'abe' };
|
||||||
|
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 packs = [
|
||||||
|
{
|
||||||
|
id: 'foo',
|
||||||
|
title: 'Abe',
|
||||||
|
cover: abeSticker,
|
||||||
|
stickers: Array(101)
|
||||||
|
.fill(0)
|
||||||
|
.map((n, id) => ({ ...abeSticker, id })),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'bar',
|
||||||
|
cover: sticker1,
|
||||||
|
stickers: Array(101)
|
||||||
|
.fill(0)
|
||||||
|
.map((n, id) => ({ ...sticker1, id })),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'baz',
|
||||||
|
cover: sticker2,
|
||||||
|
stickers: Array(101)
|
||||||
|
.fill(0)
|
||||||
|
.map((n, id) => ({ ...sticker2, id })),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'qux',
|
||||||
|
cover: sticker3,
|
||||||
|
stickers: Array(101)
|
||||||
|
.fill(0)
|
||||||
|
.map((n, id) => ({ ...sticker3, id })),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
<util.ConversationContext theme={util.theme}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
height: '500px',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
justifyContent: 'flex-end',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<StickerButton
|
||||||
|
i18n={util.i18n}
|
||||||
|
receivedPacks={[]}
|
||||||
|
installedPacks={packs}
|
||||||
|
installedPack={packs[0]}
|
||||||
|
onPickSticker={(packId, stickerId) =>
|
||||||
|
console.log('onPickSticker', { packId, stickerId })
|
||||||
|
}
|
||||||
|
clearInstalledStickerPack={() => console.log('clearInstalledStickerPack')}
|
||||||
|
onClickAddPack={() => console.log('onClickAddPack')}
|
||||||
|
recentStickers={[]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</util.ConversationContext>;
|
||||||
|
```
|
||||||
|
|
||||||
|
#### New Installation Splash Tooltip
|
||||||
|
|
||||||
|
When the application is updated or freshly installed there should be a tooltip
|
||||||
|
showing the user the sticker button.
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
const abeSticker = { id: 4, url: util.squareStickerObjectUrl, packId: 'abe' };
|
||||||
|
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 packs = [
|
||||||
|
{
|
||||||
|
id: 'foo',
|
||||||
|
title: 'Abe',
|
||||||
|
cover: abeSticker,
|
||||||
|
stickers: Array(101)
|
||||||
|
.fill(0)
|
||||||
|
.map((n, id) => ({ ...abeSticker, id })),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'bar',
|
||||||
|
cover: sticker1,
|
||||||
|
stickers: Array(101)
|
||||||
|
.fill(0)
|
||||||
|
.map((n, id) => ({ ...sticker1, id })),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'baz',
|
||||||
|
cover: sticker2,
|
||||||
|
stickers: Array(101)
|
||||||
|
.fill(0)
|
||||||
|
.map((n, id) => ({ ...sticker2, id })),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'qux',
|
||||||
|
cover: sticker3,
|
||||||
|
stickers: Array(101)
|
||||||
|
.fill(0)
|
||||||
|
.map((n, id) => ({ ...sticker3, id })),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
<util.ConversationContext theme={util.theme}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
height: '500px',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
justifyContent: 'flex-end',
|
||||||
|
alignItems: 'flex-end',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<StickerButton
|
||||||
|
i18n={util.i18n}
|
||||||
|
receivedPacks={[]}
|
||||||
|
installedPacks={packs}
|
||||||
|
onPickSticker={(packId, stickerId) =>
|
||||||
|
console.log('onPickSticker', { packId, stickerId })
|
||||||
|
}
|
||||||
|
clearInstalledStickerPack={() => console.log('clearInstalledStickerPack')}
|
||||||
|
onClickAddPack={() => console.log('onClickAddPack')}
|
||||||
|
recentStickers={[]}
|
||||||
|
showIntroduction
|
||||||
|
clearShowIntroduction={() => console.log('clearShowIntroduction')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</util.ConversationContext>;
|
||||||
|
```
|
255
ts/components/stickers/StickerButton.tsx
Normal file
|
@ -0,0 +1,255 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import { noop } from 'lodash';
|
||||||
|
import { Manager, Popper, Reference } from 'react-popper';
|
||||||
|
import { createPortal } from 'react-dom';
|
||||||
|
import { StickerPicker } from './StickerPicker';
|
||||||
|
import { StickerPackType, StickerType } from '../../state/ducks/stickers';
|
||||||
|
import { LocalizerType } from '../../types/Util';
|
||||||
|
|
||||||
|
export type OwnProps = {
|
||||||
|
readonly i18n: LocalizerType;
|
||||||
|
readonly receivedPacks: ReadonlyArray<StickerPackType>;
|
||||||
|
readonly installedPacks: ReadonlyArray<StickerPackType>;
|
||||||
|
readonly installedPack?: StickerPackType | null;
|
||||||
|
readonly recentStickers: ReadonlyArray<StickerType>;
|
||||||
|
readonly clearInstalledStickerPack: () => unknown;
|
||||||
|
readonly onClickAddPack: () => unknown;
|
||||||
|
readonly onPickSticker: (packId: string, stickerId: number) => unknown;
|
||||||
|
readonly showIntroduction?: boolean;
|
||||||
|
readonly clearShowIntroduction: () => unknown;
|
||||||
|
readonly showPickerHint: boolean;
|
||||||
|
readonly clearShowPickerHint: () => unknown;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Props = OwnProps;
|
||||||
|
|
||||||
|
export const StickerButton = React.memo(
|
||||||
|
// tslint:disable-next-line max-func-body-length
|
||||||
|
({
|
||||||
|
i18n,
|
||||||
|
clearInstalledStickerPack,
|
||||||
|
onClickAddPack,
|
||||||
|
onPickSticker,
|
||||||
|
recentStickers,
|
||||||
|
receivedPacks,
|
||||||
|
installedPack,
|
||||||
|
installedPacks,
|
||||||
|
showIntroduction,
|
||||||
|
clearShowIntroduction,
|
||||||
|
showPickerHint,
|
||||||
|
clearShowPickerHint,
|
||||||
|
}: Props) => {
|
||||||
|
const [open, setOpen] = React.useState(false);
|
||||||
|
const [popperRoot, setPopperRoot] = React.useState<HTMLElement | null>(
|
||||||
|
null
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleClickButton = React.useCallback(
|
||||||
|
() => {
|
||||||
|
// Clear tooltip state
|
||||||
|
clearInstalledStickerPack();
|
||||||
|
|
||||||
|
// Handle button click
|
||||||
|
if (installedPacks.length === 0) {
|
||||||
|
onClickAddPack();
|
||||||
|
} else if (popperRoot) {
|
||||||
|
setOpen(false);
|
||||||
|
} else {
|
||||||
|
setOpen(true);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[
|
||||||
|
clearInstalledStickerPack,
|
||||||
|
onClickAddPack,
|
||||||
|
installedPacks,
|
||||||
|
popperRoot,
|
||||||
|
setOpen,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handlePickSticker = React.useCallback(
|
||||||
|
(packId: string, stickerId: number) => {
|
||||||
|
setOpen(false);
|
||||||
|
onPickSticker(packId, stickerId);
|
||||||
|
},
|
||||||
|
[setOpen, onPickSticker]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleClickAddPack = React.useCallback(
|
||||||
|
() => {
|
||||||
|
setOpen(false);
|
||||||
|
if (showPickerHint) {
|
||||||
|
clearShowPickerHint();
|
||||||
|
}
|
||||||
|
onClickAddPack();
|
||||||
|
},
|
||||||
|
[onClickAddPack, showPickerHint, clearShowPickerHint]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleClearIntroduction = React.useCallback(
|
||||||
|
() => {
|
||||||
|
clearInstalledStickerPack();
|
||||||
|
clearShowIntroduction();
|
||||||
|
},
|
||||||
|
[clearInstalledStickerPack, clearShowIntroduction]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Create popper root and handle outside clicks
|
||||||
|
React.useEffect(
|
||||||
|
() => {
|
||||||
|
if (open) {
|
||||||
|
const root = document.createElement('div');
|
||||||
|
setPopperRoot(root);
|
||||||
|
document.body.appendChild(root);
|
||||||
|
const handleOutsideClick = ({ target }: MouseEvent) => {
|
||||||
|
if (!root.contains(target as Node)) {
|
||||||
|
setOpen(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
document.addEventListener('click', handleOutsideClick);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.body.removeChild(root);
|
||||||
|
document.removeEventListener('click', handleOutsideClick);
|
||||||
|
setPopperRoot(null);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return noop;
|
||||||
|
},
|
||||||
|
[open, setOpen, setPopperRoot]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Clear the installed pack after one minute
|
||||||
|
React.useEffect(
|
||||||
|
() => {
|
||||||
|
if (installedPack) {
|
||||||
|
// tslint:disable-next-line:no-string-based-set-timeout
|
||||||
|
const timerId = setTimeout(clearInstalledStickerPack, 60 * 1000);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clearTimeout(timerId);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return noop;
|
||||||
|
},
|
||||||
|
[installedPack, clearInstalledStickerPack]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (installedPacks.length + receivedPacks.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Manager>
|
||||||
|
<Reference>
|
||||||
|
{({ ref }) => (
|
||||||
|
<button
|
||||||
|
ref={ref}
|
||||||
|
onClick={handleClickButton}
|
||||||
|
className={classNames({
|
||||||
|
'module-sticker-button__button': true,
|
||||||
|
'module-sticker-button__button--active': open,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Reference>
|
||||||
|
{!open && !showIntroduction && installedPack ? (
|
||||||
|
<Popper placement="top-end" key={installedPack.id}>
|
||||||
|
{({ ref, style, placement, arrowProps }) => (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
style={style}
|
||||||
|
className="module-sticker-button__tooltip"
|
||||||
|
role="button"
|
||||||
|
onClick={clearInstalledStickerPack}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
className="module-sticker-button__tooltip__image"
|
||||||
|
src={installedPack.cover.url}
|
||||||
|
alt={installedPack.title}
|
||||||
|
/>
|
||||||
|
<span className="module-sticker-button__tooltip__text">
|
||||||
|
<span className="module-sticker-button__tooltip__text__title">
|
||||||
|
{installedPack.title}
|
||||||
|
</span>{' '}
|
||||||
|
installed
|
||||||
|
</span>
|
||||||
|
<div
|
||||||
|
ref={arrowProps.ref}
|
||||||
|
style={arrowProps.style}
|
||||||
|
className={classNames(
|
||||||
|
'module-sticker-button__tooltip__triangle',
|
||||||
|
`module-sticker-button__tooltip__triangle--${placement}`
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Popper>
|
||||||
|
) : null}
|
||||||
|
{!open && showIntroduction ? (
|
||||||
|
<Popper placement="top-end">
|
||||||
|
{({ ref, style, placement, arrowProps }) => (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
style={style}
|
||||||
|
className={classNames(
|
||||||
|
'module-sticker-button__tooltip',
|
||||||
|
'module-sticker-button__tooltip--introduction'
|
||||||
|
)}
|
||||||
|
role="button"
|
||||||
|
onClick={handleClearIntroduction}
|
||||||
|
>
|
||||||
|
<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')}
|
||||||
|
</div>
|
||||||
|
<div className="module-sticker-button__tooltip--introduction__meta__subtitle">
|
||||||
|
{i18n('stickers--StickerManager--Introduction--Body')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="module-sticker-button__tooltip--introduction__close">
|
||||||
|
<button
|
||||||
|
className="module-sticker-button__tooltip--introduction__close__button"
|
||||||
|
onClick={handleClearIntroduction}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
ref={arrowProps.ref}
|
||||||
|
style={arrowProps.style}
|
||||||
|
className={classNames(
|
||||||
|
'module-sticker-button__tooltip__triangle',
|
||||||
|
'module-sticker-button__tooltip__triangle--introduction',
|
||||||
|
`module-sticker-button__tooltip__triangle--${placement}`
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Popper>
|
||||||
|
) : null}
|
||||||
|
{open && popperRoot
|
||||||
|
? createPortal(
|
||||||
|
<Popper placement="top-end">
|
||||||
|
{({ ref, style }) => (
|
||||||
|
<StickerPicker
|
||||||
|
ref={ref}
|
||||||
|
i18n={i18n}
|
||||||
|
style={style}
|
||||||
|
packs={installedPacks}
|
||||||
|
onClickAddPack={handleClickAddPack}
|
||||||
|
onPickSticker={handlePickSticker}
|
||||||
|
recentStickers={recentStickers}
|
||||||
|
showPickerHint={showPickerHint}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Popper>,
|
||||||
|
popperRoot
|
||||||
|
)
|
||||||
|
: null}
|
||||||
|
</Manager>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
178
ts/components/stickers/StickerManager.md
Normal file
|
@ -0,0 +1,178 @@
|
||||||
|
#### Default
|
||||||
|
|
||||||
|
```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 packs = [
|
||||||
|
{
|
||||||
|
id: 'foo',
|
||||||
|
cover: sticker1,
|
||||||
|
title: 'Foo',
|
||||||
|
author: 'Foo McBarrington',
|
||||||
|
stickers: Array(101)
|
||||||
|
.fill(0)
|
||||||
|
.map((n, id) => ({ ...sticker1, id })),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'bar',
|
||||||
|
cover: sticker2,
|
||||||
|
title: 'Baz',
|
||||||
|
author: 'Foo McBarrington (Official)',
|
||||||
|
stickers: Array(101)
|
||||||
|
.fill(0)
|
||||||
|
.map((n, id) => ({ ...sticker2, id })),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'baz',
|
||||||
|
cover: sticker3,
|
||||||
|
title: 'Third',
|
||||||
|
author: 'Foo McBarrington',
|
||||||
|
stickers: Array(101)
|
||||||
|
.fill(0)
|
||||||
|
.map((n, id) => ({ ...sticker3, id })),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const receivedPacks = packs.map(p => ({ ...p, status: 'advertised' }));
|
||||||
|
const installedPacks = packs.map(p => ({ ...p, status: 'installed' }));
|
||||||
|
const blessedPacks = packs.map(p => ({
|
||||||
|
...p,
|
||||||
|
status: 'advertised',
|
||||||
|
isBlessed: true,
|
||||||
|
}));
|
||||||
|
|
||||||
|
<util.ConversationContext theme={util.theme}>
|
||||||
|
<StickerManager
|
||||||
|
i18n={util.i18n}
|
||||||
|
installedPacks={installedPacks}
|
||||||
|
receivedPacks={receivedPacks}
|
||||||
|
blessedPacks={blessedPacks}
|
||||||
|
installStickerPack={id => console.log('installStickerPack', id)}
|
||||||
|
uninstallStickerPack={id => console.log('uninstallStickerPack', id)}
|
||||||
|
/>
|
||||||
|
</util.ConversationContext>;
|
||||||
|
```
|
||||||
|
|
||||||
|
#### No Advertised Packs
|
||||||
|
|
||||||
|
```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 packs = [
|
||||||
|
{
|
||||||
|
id: 'foo',
|
||||||
|
cover: sticker1,
|
||||||
|
title: 'Foo',
|
||||||
|
author: 'Foo McBarrington',
|
||||||
|
stickers: Array(101)
|
||||||
|
.fill(0)
|
||||||
|
.map((n, id) => ({ ...sticker1, id })),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'bar',
|
||||||
|
cover: sticker2,
|
||||||
|
title: 'Baz',
|
||||||
|
author: 'Foo McBarrington',
|
||||||
|
stickers: Array(101)
|
||||||
|
.fill(0)
|
||||||
|
.map((n, id) => ({ ...sticker2, id })),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'baz',
|
||||||
|
cover: sticker3,
|
||||||
|
title: 'Baz',
|
||||||
|
author: 'Foo McBarrington',
|
||||||
|
stickers: Array(101)
|
||||||
|
.fill(0)
|
||||||
|
.map((n, id) => ({ ...sticker3, id })),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const installedPacks = packs.map(p => ({ ...p, status: 'installed' }));
|
||||||
|
const noPacks = [];
|
||||||
|
|
||||||
|
<util.ConversationContext theme={util.theme}>
|
||||||
|
<StickerManager
|
||||||
|
i18n={util.i18n}
|
||||||
|
installedPacks={installedPacks}
|
||||||
|
receivedPacks={noPacks}
|
||||||
|
blessedPacks={noPacks}
|
||||||
|
installStickerPack={id => console.log('installStickerPack', id)}
|
||||||
|
uninstallStickerPack={id => console.log('uninstallStickerPack', id)}
|
||||||
|
/>
|
||||||
|
</util.ConversationContext>;
|
||||||
|
```
|
||||||
|
|
||||||
|
#### No Installed Packs
|
||||||
|
|
||||||
|
```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 packs = [
|
||||||
|
{
|
||||||
|
id: 'foo',
|
||||||
|
cover: sticker1,
|
||||||
|
title: 'Foo',
|
||||||
|
author: 'Foo McBarrington',
|
||||||
|
stickers: Array(101)
|
||||||
|
.fill(0)
|
||||||
|
.map((n, id) => ({ ...sticker1, id })),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'bar',
|
||||||
|
cover: sticker2,
|
||||||
|
title: 'Baz',
|
||||||
|
author: 'Foo McBarrington',
|
||||||
|
stickers: Array(101)
|
||||||
|
.fill(0)
|
||||||
|
.map((n, id) => ({ ...sticker2, id })),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'baz',
|
||||||
|
cover: sticker3,
|
||||||
|
title: 'Baz',
|
||||||
|
author: 'Foo McBarrington',
|
||||||
|
stickers: Array(101)
|
||||||
|
.fill(0)
|
||||||
|
.map((n, id) => ({ ...sticker3, id })),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const receivedPacks = packs.map(p => ({ ...p, status: 'installed' }));
|
||||||
|
const noPacks = [];
|
||||||
|
|
||||||
|
<util.ConversationContext theme={util.theme}>
|
||||||
|
<StickerManager
|
||||||
|
i18n={util.i18n}
|
||||||
|
installedPacks={noPacks}
|
||||||
|
receivedPacks={receivedPacks}
|
||||||
|
blessedPacks={noPacks}
|
||||||
|
installStickerPack={id => console.log('installStickerPack', id)}
|
||||||
|
/>
|
||||||
|
</util.ConversationContext>;
|
||||||
|
```
|
||||||
|
|
||||||
|
#### No Packs at All
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
const noPacks = [];
|
||||||
|
|
||||||
|
<util.ConversationContext theme={util.theme}>
|
||||||
|
<div style={{ height: '500px' }}>
|
||||||
|
<StickerManager
|
||||||
|
i18n={util.i18n}
|
||||||
|
installedPacks={noPacks}
|
||||||
|
receivedPacks={noPacks}
|
||||||
|
blessedPacks={noPacks}
|
||||||
|
installStickerPack={id => console.log('installStickerPack', id)}
|
||||||
|
uninstallStickerPack={id => console.log('uninstallStickerPack', id)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</util.ConversationContext>;
|
||||||
|
```
|
107
ts/components/stickers/StickerManager.tsx
Normal file
|
@ -0,0 +1,107 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import { StickerManagerPackRow } from './StickerManagerPackRow';
|
||||||
|
import { StickerPreviewModal } from './StickerPreviewModal';
|
||||||
|
import { LocalizerType } from '../../types/Util';
|
||||||
|
import { StickerPackType } from '../../state/ducks/stickers';
|
||||||
|
|
||||||
|
export type OwnProps = {
|
||||||
|
readonly installedPacks: ReadonlyArray<StickerPackType>;
|
||||||
|
readonly receivedPacks: ReadonlyArray<StickerPackType>;
|
||||||
|
readonly blessedPacks: ReadonlyArray<StickerPackType>;
|
||||||
|
readonly installStickerPack: (packId: string, packKey: string) => unknown;
|
||||||
|
readonly uninstallStickerPack: (packId: string, packKey: string) => unknown;
|
||||||
|
readonly i18n: LocalizerType;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Props = OwnProps;
|
||||||
|
|
||||||
|
export const StickerManager = React.memo(
|
||||||
|
({
|
||||||
|
installedPacks,
|
||||||
|
receivedPacks,
|
||||||
|
blessedPacks,
|
||||||
|
installStickerPack,
|
||||||
|
uninstallStickerPack,
|
||||||
|
i18n,
|
||||||
|
}: Props) => {
|
||||||
|
const [
|
||||||
|
packToPreview,
|
||||||
|
setPackToPreview,
|
||||||
|
] = React.useState<StickerPackType | null>(null);
|
||||||
|
|
||||||
|
const clearPackToPreview = React.useCallback(
|
||||||
|
() => {
|
||||||
|
setPackToPreview(null);
|
||||||
|
},
|
||||||
|
[setPackToPreview]
|
||||||
|
);
|
||||||
|
|
||||||
|
const previewPack = React.useCallback(
|
||||||
|
(pack: StickerPackType) => {
|
||||||
|
setPackToPreview(pack);
|
||||||
|
},
|
||||||
|
[clearPackToPreview]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{packToPreview ? (
|
||||||
|
<StickerPreviewModal
|
||||||
|
i18n={i18n}
|
||||||
|
pack={packToPreview}
|
||||||
|
onClose={clearPackToPreview}
|
||||||
|
installStickerPack={installStickerPack}
|
||||||
|
uninstallStickerPack={uninstallStickerPack}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
<div className="module-sticker-manager">
|
||||||
|
{[
|
||||||
|
{
|
||||||
|
i18nKey: 'stickers--StickerManager--InstalledPacks',
|
||||||
|
i18nEmptyKey: 'stickers--StickerManager--InstalledPacks--Empty',
|
||||||
|
packs: installedPacks,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
i18nKey: 'stickers--StickerManager--BlessedPacks',
|
||||||
|
i18nEmptyKey: 'stickers--StickerManager--BlessedPacks--Empty',
|
||||||
|
packs: blessedPacks,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
i18nKey: 'stickers--StickerManager--ReceivedPacks',
|
||||||
|
i18nEmptyKey: 'stickers--StickerManager--ReceivedPacks--Empty',
|
||||||
|
packs: receivedPacks,
|
||||||
|
},
|
||||||
|
].map(section => (
|
||||||
|
<React.Fragment key={section.i18nKey}>
|
||||||
|
<h2
|
||||||
|
className={classNames(
|
||||||
|
'module-sticker-manager__text',
|
||||||
|
'module-sticker-manager__text--heading'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{i18n(section.i18nKey)}
|
||||||
|
</h2>
|
||||||
|
{section.packs.length > 0 ? (
|
||||||
|
section.packs.map(pack => (
|
||||||
|
<StickerManagerPackRow
|
||||||
|
key={pack.id}
|
||||||
|
pack={pack}
|
||||||
|
i18n={i18n}
|
||||||
|
onClickPreview={previewPack}
|
||||||
|
installStickerPack={installStickerPack}
|
||||||
|
uninstallStickerPack={uninstallStickerPack}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<div className="module-sticker-manager__empty">
|
||||||
|
{i18n(section.i18nEmptyKey)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</React.Fragment>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
129
ts/components/stickers/StickerManagerPackRow.tsx
Normal file
|
@ -0,0 +1,129 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
import { StickerPackInstallButton } from './StickerPackInstallButton';
|
||||||
|
import { ConfirmationModal } from '../ConfirmationModal';
|
||||||
|
import { LocalizerType } from '../../types/Util';
|
||||||
|
import { StickerPackType } from '../../state/ducks/stickers';
|
||||||
|
|
||||||
|
export type OwnProps = {
|
||||||
|
readonly i18n: LocalizerType;
|
||||||
|
readonly pack: StickerPackType;
|
||||||
|
readonly onClickPreview?: (sticker: StickerPackType) => unknown;
|
||||||
|
readonly installStickerPack?: (packId: string, packKey: string) => unknown;
|
||||||
|
readonly uninstallStickerPack?: (packId: string, packKey: string) => unknown;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Props = OwnProps;
|
||||||
|
|
||||||
|
export const StickerManagerPackRow = React.memo(
|
||||||
|
// tslint:disable-next-line max-func-body-length
|
||||||
|
({
|
||||||
|
installStickerPack,
|
||||||
|
uninstallStickerPack,
|
||||||
|
onClickPreview,
|
||||||
|
pack,
|
||||||
|
i18n,
|
||||||
|
}: Props) => {
|
||||||
|
const { id, key, isBlessed } = pack;
|
||||||
|
const [uninstalling, setUninstalling] = React.useState(false);
|
||||||
|
|
||||||
|
const clearUninstalling = React.useCallback(
|
||||||
|
() => {
|
||||||
|
setUninstalling(false);
|
||||||
|
},
|
||||||
|
[setUninstalling]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleInstall = React.useCallback(
|
||||||
|
(e: React.MouseEvent) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
if (installStickerPack) {
|
||||||
|
installStickerPack(id, key);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[installStickerPack, pack]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleUninstall = React.useCallback(
|
||||||
|
(e: React.MouseEvent) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
if (isBlessed && uninstallStickerPack) {
|
||||||
|
uninstallStickerPack(id, key);
|
||||||
|
} else {
|
||||||
|
setUninstalling(true);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[setUninstalling, id, key, isBlessed]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleConfirmUninstall = React.useCallback(
|
||||||
|
() => {
|
||||||
|
clearUninstalling();
|
||||||
|
if (uninstallStickerPack) {
|
||||||
|
uninstallStickerPack(id, key);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[id, key, clearUninstalling]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleClickPreview = React.useCallback(
|
||||||
|
() => {
|
||||||
|
if (onClickPreview) {
|
||||||
|
onClickPreview(pack);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[onClickPreview, pack]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{uninstalling ? (
|
||||||
|
<ConfirmationModal
|
||||||
|
i18n={i18n}
|
||||||
|
onClose={clearUninstalling}
|
||||||
|
negativeText={i18n('stickers--StickerManager--Uninstall')}
|
||||||
|
onNegative={handleConfirmUninstall}
|
||||||
|
>
|
||||||
|
{i18n('stickers--StickerManager--UninstallWarning')}
|
||||||
|
</ConfirmationModal>
|
||||||
|
) : null}
|
||||||
|
<div
|
||||||
|
role="button"
|
||||||
|
onClick={handleClickPreview}
|
||||||
|
className="module-sticker-manager__pack-row"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={pack.cover.url}
|
||||||
|
alt={pack.title}
|
||||||
|
className="module-sticker-manager__pack-row__cover"
|
||||||
|
/>
|
||||||
|
<div className="module-sticker-manager__pack-row__meta">
|
||||||
|
<div className="module-sticker-manager__pack-row__meta__title">
|
||||||
|
{pack.title}
|
||||||
|
{pack.isBlessed ? (
|
||||||
|
<span className="module-sticker-manager__pack-row__meta__blessed-icon" />
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
<div className="module-sticker-manager__pack-row__meta__author">
|
||||||
|
{pack.author}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="module-sticker-manager__pack-row__controls">
|
||||||
|
{pack.status === 'advertised' ? (
|
||||||
|
<StickerPackInstallButton
|
||||||
|
installed={false}
|
||||||
|
i18n={i18n}
|
||||||
|
onClick={handleInstall}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<StickerPackInstallButton
|
||||||
|
installed={true}
|
||||||
|
i18n={i18n}
|
||||||
|
onClick={handleUninstall}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
29
ts/components/stickers/StickerPackInstallButton.tsx
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import { LocalizerType } from '../../types/Util';
|
||||||
|
|
||||||
|
export type OwnProps = {
|
||||||
|
readonly installed: boolean;
|
||||||
|
readonly i18n: LocalizerType;
|
||||||
|
readonly blue?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Props = OwnProps & React.HTMLProps<HTMLButtonElement>;
|
||||||
|
|
||||||
|
export const StickerPackInstallButton = React.forwardRef<
|
||||||
|
HTMLButtonElement,
|
||||||
|
Props
|
||||||
|
>(({ i18n, installed, blue, ...props }: Props, ref) => (
|
||||||
|
<button
|
||||||
|
ref={ref}
|
||||||
|
className={classNames({
|
||||||
|
'module-sticker-manager__install-button': true,
|
||||||
|
'module-sticker-manager__install-button--blue': blue,
|
||||||
|
})}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{installed
|
||||||
|
? i18n('stickers--StickerManager--Uninstall')
|
||||||
|
: i18n('stickers--StickerManager--Install')}
|
||||||
|
</button>
|
||||||
|
));
|
321
ts/components/stickers/StickerPicker.md
Normal file
|
@ -0,0 +1,321 @@
|
||||||
|
#### Default
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
const abeSticker = { id: 4, url: util.squareStickerObjectUrl, packId: 'abe' };
|
||||||
|
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 packs = [
|
||||||
|
{
|
||||||
|
id: 'foo',
|
||||||
|
cover: sticker1,
|
||||||
|
stickerCount: 101,
|
||||||
|
stickers: Array(101)
|
||||||
|
.fill(0)
|
||||||
|
.map((n, id) => ({ ...sticker1, id })),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'bar',
|
||||||
|
cover: sticker2,
|
||||||
|
stickerCount: 101,
|
||||||
|
stickers: Array(101)
|
||||||
|
.fill(0)
|
||||||
|
.map((n, id) => ({ ...sticker2, id })),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'baz',
|
||||||
|
cover: sticker3,
|
||||||
|
stickerCount: 101,
|
||||||
|
stickers: Array(101)
|
||||||
|
.fill(0)
|
||||||
|
.map((n, id) => ({ ...sticker3, id })),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'qux',
|
||||||
|
cover: sticker2,
|
||||||
|
stickerCount: 101,
|
||||||
|
stickers: Array(101)
|
||||||
|
.fill(0)
|
||||||
|
.map((n, id) => ({ ...sticker2, id })),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'quux',
|
||||||
|
cover: sticker3,
|
||||||
|
stickerCount: 101,
|
||||||
|
stickers: Array(101)
|
||||||
|
.fill(0)
|
||||||
|
.map((n, id) => ({ ...sticker1, id })),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'corge',
|
||||||
|
cover: sticker2,
|
||||||
|
stickerCount: 101,
|
||||||
|
stickers: Array(101)
|
||||||
|
.fill(0)
|
||||||
|
.map((n, id) => ({ ...sticker2, id })),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'grault',
|
||||||
|
cover: sticker1,
|
||||||
|
stickerCount: 101,
|
||||||
|
stickers: Array(101)
|
||||||
|
.fill(0)
|
||||||
|
.map((n, id) => ({ ...sticker1, id })),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'garply',
|
||||||
|
cover: sticker2,
|
||||||
|
stickerCount: 101,
|
||||||
|
stickers: Array(101)
|
||||||
|
.fill(0)
|
||||||
|
.map((n, id) => ({ ...sticker2, id })),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'waldo',
|
||||||
|
cover: sticker3,
|
||||||
|
stickerCount: 101,
|
||||||
|
stickers: Array(101)
|
||||||
|
.fill(0)
|
||||||
|
.map((n, id) => ({ ...sticker3, id })),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'fred',
|
||||||
|
cover: sticker2,
|
||||||
|
stickerCount: 101,
|
||||||
|
stickers: Array(101)
|
||||||
|
.fill(0)
|
||||||
|
.map((n, id) => ({ ...sticker2, id })),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'plugh',
|
||||||
|
cover: sticker1,
|
||||||
|
stickerCount: 101,
|
||||||
|
stickers: Array(101)
|
||||||
|
.fill(0)
|
||||||
|
.map((n, id) => ({ ...sticker1, id })),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'xyzzy',
|
||||||
|
cover: sticker2,
|
||||||
|
stickerCount: 101,
|
||||||
|
stickers: Array(101)
|
||||||
|
.fill(0)
|
||||||
|
.map((n, id) => ({ ...sticker2, id })),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'thud',
|
||||||
|
cover: abeSticker,
|
||||||
|
stickerCount: 101,
|
||||||
|
stickers: Array(101)
|
||||||
|
.fill(0)
|
||||||
|
.map((n, id) => ({ ...abeSticker, id })),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'banana',
|
||||||
|
cover: sticker2,
|
||||||
|
stickerCount: 101,
|
||||||
|
stickers: Array(101)
|
||||||
|
.fill(0)
|
||||||
|
.map((n, id) => ({ ...sticker2, id })),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'apple',
|
||||||
|
cover: sticker1,
|
||||||
|
stickerCount: 101,
|
||||||
|
stickers: Array(101)
|
||||||
|
.fill(0)
|
||||||
|
.map((n, id) => ({ ...sticker1, id })),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'strawberry',
|
||||||
|
cover: sticker2,
|
||||||
|
stickerCount: 101,
|
||||||
|
stickers: Array(101)
|
||||||
|
.fill(0)
|
||||||
|
.map((n, id) => ({ ...sticker2, id })),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'tombrady',
|
||||||
|
cover: abeSticker,
|
||||||
|
stickerCount: 101,
|
||||||
|
stickers: Array(101)
|
||||||
|
.fill(0)
|
||||||
|
.map((n, id) => ({ ...abeSticker, id })),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
<util.ConversationContext theme={util.theme}>
|
||||||
|
<StickerPicker
|
||||||
|
i18n={util.i18n}
|
||||||
|
packs={packs}
|
||||||
|
recentStickers={[
|
||||||
|
abeSticker,
|
||||||
|
sticker1,
|
||||||
|
sticker2,
|
||||||
|
sticker3,
|
||||||
|
{ ...sticker2, id: 9999 },
|
||||||
|
]}
|
||||||
|
onClickAddPack={() => console.log('onClickAddPack')}
|
||||||
|
onPickSticker={(packId, stickerId) =>
|
||||||
|
console.log('onPickSticker', { packId, stickerId })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</util.ConversationContext>;
|
||||||
|
```
|
||||||
|
|
||||||
|
#### No Recently Used Stickers
|
||||||
|
|
||||||
|
The sticker picker defaults to the first pack when there are no recent stickers.
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
const abeSticker = { id: 4, url: util.squareStickerObjectUrl, packId: 'abe' };
|
||||||
|
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 packs = [
|
||||||
|
{
|
||||||
|
id: 'foo',
|
||||||
|
cover: sticker1,
|
||||||
|
stickerCount: 101,
|
||||||
|
stickers: Array(101)
|
||||||
|
.fill(0)
|
||||||
|
.map((n, id) => ({ ...sticker1, id })),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'bar',
|
||||||
|
cover: sticker2,
|
||||||
|
stickerCount: 101,
|
||||||
|
stickers: Array(101)
|
||||||
|
.fill(0)
|
||||||
|
.map((n, id) => ({ ...sticker2, id })),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'baz',
|
||||||
|
cover: sticker3,
|
||||||
|
stickerCount: 101,
|
||||||
|
stickers: Array(101)
|
||||||
|
.fill(0)
|
||||||
|
.map((n, id) => ({ ...sticker3, id })),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
<util.ConversationContext theme={util.theme}>
|
||||||
|
<StickerPicker
|
||||||
|
i18n={util.i18n}
|
||||||
|
packs={packs}
|
||||||
|
recentStickers={[]}
|
||||||
|
onClickAddPack={() => console.log('onClickAddPack')}
|
||||||
|
onPickSticker={(packId, stickerId) =>
|
||||||
|
console.log('onPickSticker', { packId, stickerId })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</util.ConversationContext>;
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Empty
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
<util.ConversationContext theme={util.theme}>
|
||||||
|
<StickerPicker
|
||||||
|
i18n={util.i18n}
|
||||||
|
packs={[]}
|
||||||
|
recentStickers={[]}
|
||||||
|
onClickAddPack={() => console.log('onClickAddPack')}
|
||||||
|
onPickSticker={(packId, stickerId) =>
|
||||||
|
console.log('onPickSticker', { packId, stickerId })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</util.ConversationContext>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Pending Download
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
const abeSticker = { id: 4, url: util.squareStickerObjectUrl, packId: 'abe' };
|
||||||
|
const packs = [
|
||||||
|
{
|
||||||
|
id: 'tombrady',
|
||||||
|
status: 'pending',
|
||||||
|
cover: abeSticker,
|
||||||
|
stickerCount: 30,
|
||||||
|
stickers: [abeSticker],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
<util.ConversationContext theme={util.theme}>
|
||||||
|
<StickerPicker
|
||||||
|
i18n={util.i18n}
|
||||||
|
packs={packs}
|
||||||
|
recentStickers={[]}
|
||||||
|
onClickAddPack={() => console.log('onClickAddPack')}
|
||||||
|
onPickSticker={(packId, stickerId) =>
|
||||||
|
console.log('onPickSticker', { packId, stickerId })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</util.ConversationContext>;
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Picker Hint
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
const abeSticker = { id: 4, url: util.squareStickerObjectUrl, packId: 'abe' };
|
||||||
|
const packs = [
|
||||||
|
{
|
||||||
|
id: 'tombrady',
|
||||||
|
cover: abeSticker,
|
||||||
|
stickerCount: 100,
|
||||||
|
stickers: Array(100)
|
||||||
|
.fill(0)
|
||||||
|
.map((_el, i) => ({ ...abeSticker, id: i })),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
<util.ConversationContext theme={util.theme}>
|
||||||
|
<StickerPicker
|
||||||
|
i18n={util.i18n}
|
||||||
|
packs={packs}
|
||||||
|
recentStickers={[]}
|
||||||
|
onClickAddPack={() => console.log('onClickAddPack')}
|
||||||
|
onPickSticker={(packId, stickerId) =>
|
||||||
|
console.log('onPickSticker', { packId, stickerId })
|
||||||
|
}
|
||||||
|
showPickerHint={true}
|
||||||
|
/>
|
||||||
|
</util.ConversationContext>;
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Pack With Error
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
const abeSticker = { id: 4, url: util.squareStickerObjectUrl, packId: 'abe' };
|
||||||
|
const packs = [
|
||||||
|
{
|
||||||
|
id: 'tombrady',
|
||||||
|
status: 'error',
|
||||||
|
cover: abeSticker,
|
||||||
|
stickerCount: 3,
|
||||||
|
stickers: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'foo',
|
||||||
|
status: 'error',
|
||||||
|
cover: abeSticker,
|
||||||
|
stickerCount: 3,
|
||||||
|
stickers: [abeSticker],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
<util.ConversationContext theme={util.theme}>
|
||||||
|
<StickerPicker
|
||||||
|
i18n={util.i18n}
|
||||||
|
packs={packs}
|
||||||
|
recentStickers={[]}
|
||||||
|
onClickAddPack={() => console.log('onClickAddPack')}
|
||||||
|
onPickSticker={(packId, stickerId) =>
|
||||||
|
console.log('onPickSticker', { packId, stickerId })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</util.ConversationContext>;
|
||||||
|
```
|
282
ts/components/stickers/StickerPicker.tsx
Normal file
|
@ -0,0 +1,282 @@
|
||||||
|
/* tslint:disable:max-func-body-length */
|
||||||
|
/* tslint:disable:cyclomatic-complexity */
|
||||||
|
import * as React from 'react';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import { StickerPackType, StickerType } from '../../state/ducks/stickers';
|
||||||
|
import { LocalizerType } from '../../types/Util';
|
||||||
|
|
||||||
|
export type OwnProps = {
|
||||||
|
readonly i18n: LocalizerType;
|
||||||
|
readonly onClickAddPack: () => unknown;
|
||||||
|
readonly onPickSticker: (packId: string, stickerId: number) => unknown;
|
||||||
|
readonly packs: ReadonlyArray<StickerPackType>;
|
||||||
|
readonly recentStickers: ReadonlyArray<StickerType>;
|
||||||
|
readonly showPickerHint?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Props = OwnProps & Pick<React.HTMLProps<HTMLDivElement>, 'style'>;
|
||||||
|
|
||||||
|
function useTabs<T>(tabs: ReadonlyArray<T>, initialTab = tabs[0]) {
|
||||||
|
const [tab, setTab] = React.useState(initialTab);
|
||||||
|
const handlers = React.useMemo(
|
||||||
|
() =>
|
||||||
|
tabs.map(t => () => {
|
||||||
|
setTab(t);
|
||||||
|
}),
|
||||||
|
tabs
|
||||||
|
);
|
||||||
|
|
||||||
|
return [tab, handlers] as [T, ReadonlyArray<() => void>];
|
||||||
|
}
|
||||||
|
|
||||||
|
const PACKS_PAGE_SIZE = 7;
|
||||||
|
const PACK_ICON_WIDTH = 32;
|
||||||
|
const PACK_PAGE_WIDTH = PACKS_PAGE_SIZE * PACK_ICON_WIDTH;
|
||||||
|
|
||||||
|
function getPacksPageOffset(page: number, packs: number): number {
|
||||||
|
if (page === 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLastPacksPage(page, packs)) {
|
||||||
|
return (
|
||||||
|
PACK_PAGE_WIDTH * (Math.floor(packs / PACKS_PAGE_SIZE) - 1) +
|
||||||
|
(packs % PACKS_PAGE_SIZE - 1) * PACK_ICON_WIDTH
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return page * PACK_ICON_WIDTH * PACKS_PAGE_SIZE;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isLastPacksPage(page: number, packs: number): boolean {
|
||||||
|
return page === Math.floor(packs / PACKS_PAGE_SIZE);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const StickerPicker = React.memo(
|
||||||
|
React.forwardRef<HTMLDivElement, Props>(
|
||||||
|
(
|
||||||
|
{
|
||||||
|
i18n,
|
||||||
|
packs,
|
||||||
|
recentStickers,
|
||||||
|
onClickAddPack,
|
||||||
|
onPickSticker,
|
||||||
|
showPickerHint,
|
||||||
|
style,
|
||||||
|
}: Props,
|
||||||
|
ref
|
||||||
|
) => {
|
||||||
|
const tabIds = React.useMemo(
|
||||||
|
() => ['recents', ...packs.map(({ id }) => id)],
|
||||||
|
packs
|
||||||
|
);
|
||||||
|
const [currentTab, [recentsHandler, ...packsHandlers]] = useTabs(
|
||||||
|
tabIds,
|
||||||
|
// If there are no recent stickers, default to the first sticker pack, unless there are no sticker packs.
|
||||||
|
tabIds[recentStickers.length > 0 ? 0 : Math.min(1, tabIds.length)]
|
||||||
|
);
|
||||||
|
const selectedPack = packs.find(({ id }) => id === currentTab);
|
||||||
|
const {
|
||||||
|
stickers = recentStickers,
|
||||||
|
title: packTitle = 'Recent Stickers',
|
||||||
|
} =
|
||||||
|
selectedPack || {};
|
||||||
|
|
||||||
|
const [packsPage, setPacksPage] = React.useState(0);
|
||||||
|
const onClickPrevPackPage = React.useCallback(
|
||||||
|
() => {
|
||||||
|
setPacksPage(i => i - 1);
|
||||||
|
},
|
||||||
|
[setPacksPage]
|
||||||
|
);
|
||||||
|
const onClickNextPackPage = React.useCallback(
|
||||||
|
() => {
|
||||||
|
setPacksPage(i => i + 1);
|
||||||
|
},
|
||||||
|
[setPacksPage]
|
||||||
|
);
|
||||||
|
|
||||||
|
const isEmpty = stickers.length === 0;
|
||||||
|
const downloadError =
|
||||||
|
selectedPack &&
|
||||||
|
selectedPack.status === 'error' &&
|
||||||
|
selectedPack.stickerCount !== selectedPack.stickers.length;
|
||||||
|
const pendingCount =
|
||||||
|
selectedPack && selectedPack.status === 'pending'
|
||||||
|
? selectedPack.stickerCount - stickers.length
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
const hasPacks = packs.length > 0;
|
||||||
|
const isRecents = hasPacks && currentTab === 'recents';
|
||||||
|
const showPendingText = pendingCount > 0;
|
||||||
|
const showDownlaodErrorText = downloadError;
|
||||||
|
const showEmptyText = !downloadError && isEmpty;
|
||||||
|
const showText =
|
||||||
|
showPendingText || showDownlaodErrorText || showEmptyText;
|
||||||
|
const showLongText = showPickerHint;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="module-sticker-picker" ref={ref} style={style}>
|
||||||
|
<div className="module-sticker-picker__header">
|
||||||
|
<div className="module-sticker-picker__header__packs">
|
||||||
|
<div
|
||||||
|
className="module-sticker-picker__header__packs__slider"
|
||||||
|
style={{
|
||||||
|
transform: `translateX(-${getPacksPageOffset(
|
||||||
|
packsPage,
|
||||||
|
packs.length
|
||||||
|
)}px)`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{hasPacks ? (
|
||||||
|
<button
|
||||||
|
onClick={recentsHandler}
|
||||||
|
className={classNames({
|
||||||
|
'module-sticker-picker__header__button': true,
|
||||||
|
'module-sticker-picker__header__button--recents': true,
|
||||||
|
'module-sticker-picker__header__button--selected':
|
||||||
|
currentTab === 'recents',
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
{packs.map((pack, i) => (
|
||||||
|
<button
|
||||||
|
key={pack.id}
|
||||||
|
onClick={packsHandlers[i]}
|
||||||
|
className={classNames(
|
||||||
|
'module-sticker-picker__header__button',
|
||||||
|
{
|
||||||
|
'module-sticker-picker__header__button--selected':
|
||||||
|
currentTab === pack.id,
|
||||||
|
'module-sticker-picker__header__button--error':
|
||||||
|
pack.status === 'error',
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
className="module-sticker-picker__header__button__image"
|
||||||
|
src={pack.cover.url}
|
||||||
|
alt={pack.title}
|
||||||
|
title={pack.title}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
{packsPage > 0 ? (
|
||||||
|
<button
|
||||||
|
className={classNames(
|
||||||
|
'module-sticker-picker__header__button',
|
||||||
|
'module-sticker-picker__header__button--prev-page'
|
||||||
|
)}
|
||||||
|
onClick={onClickPrevPackPage}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
{!isLastPacksPage(packsPage, packs.length) ? (
|
||||||
|
<button
|
||||||
|
className={classNames(
|
||||||
|
'module-sticker-picker__header__button',
|
||||||
|
'module-sticker-picker__header__button--next-page'
|
||||||
|
)}
|
||||||
|
onClick={onClickNextPackPage}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
className={classNames(
|
||||||
|
'module-sticker-picker__header__button',
|
||||||
|
'module-sticker-picker__header__button--add-pack',
|
||||||
|
{
|
||||||
|
'module-sticker-picker__header__button--hint': showPickerHint,
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
onClick={onClickAddPack}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={classNames('module-sticker-picker__body', {
|
||||||
|
'module-sticker-picker__body--empty': isEmpty,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{showPickerHint ? (
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
'module-sticker-picker__body__text',
|
||||||
|
'module-sticker-picker__body__text--hint',
|
||||||
|
{
|
||||||
|
'module-sticker-picker__body__text--pin': showEmptyText,
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{i18n('stickers--StickerPicker--Hint')}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
{!hasPacks ? (
|
||||||
|
<div className="module-sticker-picker__body__text">
|
||||||
|
{i18n('stickers--StickerPicker--NoPacks')}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
{pendingCount > 0 ? (
|
||||||
|
<div className="module-sticker-picker__body__text">
|
||||||
|
{i18n('stickers--StickerPicker--DownloadPending')}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
{downloadError ? (
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
'module-sticker-picker__body__text',
|
||||||
|
'module-sticker-picker__body__text--error'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{stickers.length > 0
|
||||||
|
? i18n('stickers--StickerPicker--DownloadError')
|
||||||
|
: i18n('stickers--StickerPicker--Empty')}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
{hasPacks && showEmptyText ? (
|
||||||
|
<div
|
||||||
|
className={classNames('module-sticker-picker__body__text', {
|
||||||
|
'module-sticker-picker__body__text--error': !isRecents,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{isRecents
|
||||||
|
? i18n('stickers--StickerPicker--NoRecents')
|
||||||
|
: i18n('stickers--StickerPicker--Empty')}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
{!isEmpty ? (
|
||||||
|
<div
|
||||||
|
className={classNames('module-sticker-picker__body__content', {
|
||||||
|
'module-sticker-picker__body__content--under-text': showText,
|
||||||
|
'module-sticker-picker__body__content--under-long-text': showLongText,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{stickers.map(({ packId, id, url }) => (
|
||||||
|
<button
|
||||||
|
key={`${packId}-${id}`}
|
||||||
|
className="module-sticker-picker__body__cell"
|
||||||
|
onClick={() => onPickSticker(packId, id)}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
className="module-sticker-picker__body__cell__image"
|
||||||
|
src={url}
|
||||||
|
alt={packTitle}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
{Array(pendingCount)
|
||||||
|
.fill(0)
|
||||||
|
.map((_, i) => (
|
||||||
|
<div
|
||||||
|
key={i}
|
||||||
|
className="module-sticker-picker__body__cell__placeholder"
|
||||||
|
role="presentation"
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
29
ts/components/stickers/StickerPreviewModal.md
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
#### Not yet installed
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
const abeSticker = { url: util.squareStickerObjectUrl, packId: 'abe' };
|
||||||
|
|
||||||
|
const pack = {
|
||||||
|
id: 'foo',
|
||||||
|
cover: abeSticker,
|
||||||
|
title: 'Foo',
|
||||||
|
isBlessed: true,
|
||||||
|
author: 'Foo McBarrington',
|
||||||
|
status: 'advertised',
|
||||||
|
stickers: Array(101)
|
||||||
|
.fill(0)
|
||||||
|
.map((n, id) => ({ ...abeSticker, id })),
|
||||||
|
};
|
||||||
|
|
||||||
|
<util.ConversationContext theme={util.theme}>
|
||||||
|
<StickerPreviewModal
|
||||||
|
onClose={() => console.log('onClose')}
|
||||||
|
installStickerPack={(...args) => console.log('installStickerPack', ...args)}
|
||||||
|
uninstallStickerPack={(...args) =>
|
||||||
|
console.log('uninstallStickerPack', ...args)
|
||||||
|
}
|
||||||
|
i18n={util.i18n}
|
||||||
|
pack={pack}
|
||||||
|
/>
|
||||||
|
</util.ConversationContext>;
|
||||||
|
```
|
165
ts/components/stickers/StickerPreviewModal.tsx
Normal file
|
@ -0,0 +1,165 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
import { createPortal } from 'react-dom';
|
||||||
|
import { StickerPackInstallButton } from './StickerPackInstallButton';
|
||||||
|
import { ConfirmationDialog } from '../ConfirmationDialog';
|
||||||
|
import { LocalizerType } from '../../types/Util';
|
||||||
|
import { StickerPackType } from '../../state/ducks/stickers';
|
||||||
|
|
||||||
|
export type OwnProps = {
|
||||||
|
readonly onClose: () => unknown;
|
||||||
|
readonly installStickerPack: (packId: string, packKey: string) => unknown;
|
||||||
|
readonly uninstallStickerPack: (packId: string, packKey: string) => unknown;
|
||||||
|
readonly pack: StickerPackType;
|
||||||
|
readonly i18n: LocalizerType;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Props = OwnProps;
|
||||||
|
|
||||||
|
function focusRef(el: HTMLElement | null) {
|
||||||
|
if (el) {
|
||||||
|
el.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const StickerPreviewModal = React.memo(
|
||||||
|
// tslint:disable-next-line max-func-body-length
|
||||||
|
({
|
||||||
|
onClose,
|
||||||
|
pack,
|
||||||
|
i18n,
|
||||||
|
installStickerPack,
|
||||||
|
uninstallStickerPack,
|
||||||
|
}: Props) => {
|
||||||
|
const [root, setRoot] = React.useState<HTMLElement | null>(null);
|
||||||
|
const [confirmingUninstall, setConfirmingUninstall] = React.useState(false);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
document.body.appendChild(div);
|
||||||
|
setRoot(div);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.body.removeChild(div);
|
||||||
|
setRoot(null);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const isInstalled = pack.status === 'installed';
|
||||||
|
const handleToggleInstall = React.useCallback(
|
||||||
|
() => {
|
||||||
|
if (isInstalled) {
|
||||||
|
setConfirmingUninstall(true);
|
||||||
|
} else {
|
||||||
|
installStickerPack(pack.id, pack.key);
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[isInstalled, pack, setConfirmingUninstall, installStickerPack, onClose]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleUninstall = React.useCallback(
|
||||||
|
() => {
|
||||||
|
uninstallStickerPack(pack.id, pack.key);
|
||||||
|
setConfirmingUninstall(false);
|
||||||
|
// onClose is called by the confirmation modal
|
||||||
|
},
|
||||||
|
[uninstallStickerPack, setConfirmingUninstall, pack]
|
||||||
|
);
|
||||||
|
|
||||||
|
React.useEffect(
|
||||||
|
() => {
|
||||||
|
const handler = ({ key }: KeyboardEvent) => {
|
||||||
|
if (key === 'Escape') {
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('keyup', handler);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('keyup', handler);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
[onClose]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleClickToClose = React.useCallback(
|
||||||
|
(e: React.MouseEvent) => {
|
||||||
|
if (e.target === e.currentTarget) {
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[onClose]
|
||||||
|
);
|
||||||
|
|
||||||
|
return root
|
||||||
|
? createPortal(
|
||||||
|
<div
|
||||||
|
role="button"
|
||||||
|
className="module-sticker-manager__preview-modal__overlay"
|
||||||
|
onClick={handleClickToClose}
|
||||||
|
>
|
||||||
|
{confirmingUninstall ? (
|
||||||
|
<ConfirmationDialog
|
||||||
|
i18n={i18n}
|
||||||
|
onClose={onClose}
|
||||||
|
negativeText={i18n('stickers--StickerManager--Uninstall')}
|
||||||
|
onNegative={handleUninstall}
|
||||||
|
>
|
||||||
|
{i18n('stickers--StickerManager--UninstallWarning')}
|
||||||
|
</ConfirmationDialog>
|
||||||
|
) : (
|
||||||
|
<div className="module-sticker-manager__preview-modal__container">
|
||||||
|
<header className="module-sticker-manager__preview-modal__container__header">
|
||||||
|
<h2 className="module-sticker-manager__preview-modal__container__header__text">
|
||||||
|
{i18n('stickers--StickerPreview--Title')}
|
||||||
|
</h2>
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
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}
|
||||||
|
/>
|
||||||
|
</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>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>,
|
||||||
|
root
|
||||||
|
)
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
);
|
9
ts/shims/storage.ts
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
export async function put(key: string, value: any) {
|
||||||
|
// @ts-ignore
|
||||||
|
return window.storage.put(key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function remove(key: string) {
|
||||||
|
// @ts-ignore
|
||||||
|
return window.storage.remove(key);
|
||||||
|
}
|
77
ts/shims/textsecure.ts
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
type LoggerType = (...args: Array<any>) => void;
|
||||||
|
|
||||||
|
type TextSecureType = {
|
||||||
|
storage: {
|
||||||
|
user: {
|
||||||
|
getNumber: () => string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
messaging: {
|
||||||
|
sendStickerPackSync: (
|
||||||
|
operations: Array<{
|
||||||
|
packId: string;
|
||||||
|
packKey: string;
|
||||||
|
installed: boolean;
|
||||||
|
}>,
|
||||||
|
options: Object
|
||||||
|
) => Promise<void>;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
type ConversationControllerType = {
|
||||||
|
prepareForSend: (
|
||||||
|
id: string,
|
||||||
|
options: Object
|
||||||
|
) => {
|
||||||
|
wrap: (promise: Promise<any>) => Promise<void>;
|
||||||
|
sendOptions: Object;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
interface ShimmedWindow extends Window {
|
||||||
|
log: {
|
||||||
|
error: LoggerType;
|
||||||
|
info: LoggerType;
|
||||||
|
};
|
||||||
|
textsecure: TextSecureType;
|
||||||
|
ConversationController: ConversationControllerType;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function sendStickerPackSync(
|
||||||
|
packId: string,
|
||||||
|
packKey: string,
|
||||||
|
installed: boolean
|
||||||
|
) {
|
||||||
|
const { ConversationController, textsecure, log } = window as ShimmedWindow;
|
||||||
|
const ourNumber = textsecure.storage.user.getNumber();
|
||||||
|
const { wrap, sendOptions } = ConversationController.prepareForSend(
|
||||||
|
ourNumber,
|
||||||
|
{ syncMessage: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!textsecure.messaging) {
|
||||||
|
log.error(
|
||||||
|
'shim: Cannot call sendStickerPackSync, textsecure.messaging is falsey'
|
||||||
|
);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
wrap(
|
||||||
|
textsecure.messaging.sendStickerPackSync(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
packId,
|
||||||
|
packKey,
|
||||||
|
installed,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
sendOptions
|
||||||
|
)
|
||||||
|
).catch(error => {
|
||||||
|
log.error(
|
||||||
|
'shim: Error calling sendStickerPackSync:',
|
||||||
|
error && error.stack ? error.stack : error
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
|
@ -1,15 +1,13 @@
|
||||||
import { bindActionCreators, Dispatch } from 'redux';
|
|
||||||
|
|
||||||
import { actions as search } from './ducks/search';
|
|
||||||
import { actions as conversations } from './ducks/conversations';
|
import { actions as conversations } from './ducks/conversations';
|
||||||
|
import { actions as items } from './ducks/items';
|
||||||
|
import { actions as search } from './ducks/search';
|
||||||
|
import { actions as stickers } from './ducks/stickers';
|
||||||
import { actions as user } from './ducks/user';
|
import { actions as user } from './ducks/user';
|
||||||
|
|
||||||
const actions = {
|
export const mapDispatchToProps = {
|
||||||
...search,
|
|
||||||
...conversations,
|
...conversations,
|
||||||
|
...items,
|
||||||
|
...search,
|
||||||
|
...stickers,
|
||||||
...user,
|
...user,
|
||||||
};
|
};
|
||||||
|
|
||||||
export function mapDispatchToProps(dispatch: Dispatch): Object {
|
|
||||||
return bindActionCreators(actions, dispatch);
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { AnyAction } from 'redux';
|
||||||
import { omit } from 'lodash';
|
import { omit } from 'lodash';
|
||||||
|
|
||||||
import { trigger } from '../../shims/events';
|
import { trigger } from '../../shims/events';
|
||||||
|
@ -127,6 +128,7 @@ type ShowArchivedConversationsActionType = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ConversationActionType =
|
export type ConversationActionType =
|
||||||
|
| AnyAction
|
||||||
| ConversationAddedActionType
|
| ConversationAddedActionType
|
||||||
| ConversationChangedActionType
|
| ConversationChangedActionType
|
||||||
| ConversationRemovedActionType
|
| ConversationRemovedActionType
|
||||||
|
@ -256,13 +258,9 @@ function getEmptyState(): ConversationsStateType {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function reducer(
|
export function reducer(
|
||||||
state: ConversationsStateType,
|
state: ConversationsStateType = getEmptyState(),
|
||||||
action: ConversationActionType
|
action: ConversationActionType
|
||||||
): ConversationsStateType {
|
): ConversationsStateType {
|
||||||
if (!state) {
|
|
||||||
return getEmptyState();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (action.type === 'CONVERSATION_ADDED') {
|
if (action.type === 'CONVERSATION_ADDED') {
|
||||||
const { payload } = action;
|
const { payload } = action;
|
||||||
const { id, data } = payload;
|
const { id, data } = payload;
|
||||||
|
|
121
ts/state/ducks/items.ts
Normal file
|
@ -0,0 +1,121 @@
|
||||||
|
import { omit } from 'lodash';
|
||||||
|
import * as storageShim from '../../shims/storage';
|
||||||
|
|
||||||
|
// State
|
||||||
|
|
||||||
|
export type ItemsStateType = {
|
||||||
|
readonly [key: string]: any;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Actions
|
||||||
|
|
||||||
|
type ItemPutAction = {
|
||||||
|
type: 'items/PUT';
|
||||||
|
payload: Promise<void>;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ItemPutExternalAction = {
|
||||||
|
type: 'items/PUT_EXTERNAL';
|
||||||
|
payload: {
|
||||||
|
key: string;
|
||||||
|
value: any;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
type ItemRemoveAction = {
|
||||||
|
type: 'items/REMOVE';
|
||||||
|
payload: Promise<void>;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ItemRemoveExternalAction = {
|
||||||
|
type: 'items/REMOVE_EXTERNAL';
|
||||||
|
payload: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ItemsResetAction = {
|
||||||
|
type: 'items/RESET';
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ItemsActionType =
|
||||||
|
| ItemPutAction
|
||||||
|
| ItemPutExternalAction
|
||||||
|
| ItemRemoveAction
|
||||||
|
| ItemRemoveExternalAction
|
||||||
|
| ItemsResetAction;
|
||||||
|
|
||||||
|
// Action Creators
|
||||||
|
|
||||||
|
export const actions = {
|
||||||
|
putItem,
|
||||||
|
putItemExternal,
|
||||||
|
removeItem,
|
||||||
|
removeItemExternal,
|
||||||
|
resetItems,
|
||||||
|
};
|
||||||
|
|
||||||
|
function putItem(key: string, value: any): ItemPutAction {
|
||||||
|
return {
|
||||||
|
type: 'items/PUT',
|
||||||
|
payload: storageShim.put(key, value),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function putItemExternal(key: string, value: any): ItemPutExternalAction {
|
||||||
|
return {
|
||||||
|
type: 'items/PUT_EXTERNAL',
|
||||||
|
payload: {
|
||||||
|
key,
|
||||||
|
value,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeItem(key: string): ItemRemoveAction {
|
||||||
|
return {
|
||||||
|
type: 'items/REMOVE',
|
||||||
|
payload: storageShim.remove(key),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeItemExternal(key: string): ItemRemoveExternalAction {
|
||||||
|
return {
|
||||||
|
type: 'items/REMOVE_EXTERNAL',
|
||||||
|
payload: key,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetItems(): ItemsResetAction {
|
||||||
|
return { type: 'items/RESET' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reducer
|
||||||
|
|
||||||
|
function getEmptyState(): ItemsStateType {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function reducer(
|
||||||
|
state: ItemsStateType = getEmptyState(),
|
||||||
|
action: ItemsActionType
|
||||||
|
): ItemsStateType {
|
||||||
|
if (action.type === 'items/PUT_EXTERNAL') {
|
||||||
|
const { payload } = action;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
[payload.key]: payload.value,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action.type === 'items/REMOVE_EXTERNAL') {
|
||||||
|
const { payload } = action;
|
||||||
|
|
||||||
|
return omit(state, payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action.type === 'items/RESET') {
|
||||||
|
return getEmptyState();
|
||||||
|
}
|
||||||
|
|
||||||
|
return state;
|
||||||
|
}
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { AnyAction } from 'redux';
|
||||||
import { omit, reject } from 'lodash';
|
import { omit, reject } from 'lodash';
|
||||||
|
|
||||||
import { normalize } from '../../types/PhoneNumber';
|
import { normalize } from '../../types/PhoneNumber';
|
||||||
|
@ -63,6 +64,7 @@ type ClearSearchActionType = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SEARCH_TYPES =
|
export type SEARCH_TYPES =
|
||||||
|
| AnyAction
|
||||||
| SearchResultsFulfilledActionType
|
| SearchResultsFulfilledActionType
|
||||||
| UpdateSearchTermActionType
|
| UpdateSearchTermActionType
|
||||||
| ClearSearchActionType
|
| ClearSearchActionType
|
||||||
|
@ -218,13 +220,9 @@ function getEmptyState(): SearchStateType {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function reducer(
|
export function reducer(
|
||||||
state: SearchStateType | undefined,
|
state: SearchStateType = getEmptyState(),
|
||||||
action: SEARCH_TYPES
|
action: SEARCH_TYPES
|
||||||
): SearchStateType {
|
): SearchStateType {
|
||||||
if (!state) {
|
|
||||||
return getEmptyState();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (action.type === 'SEARCH_CLEAR') {
|
if (action.type === 'SEARCH_CLEAR') {
|
||||||
return getEmptyState();
|
return getEmptyState();
|
||||||
}
|
}
|
||||||
|
|
463
ts/state/ducks/stickers.ts
Normal file
|
@ -0,0 +1,463 @@
|
||||||
|
import { Dictionary, omit, reject } from 'lodash';
|
||||||
|
import {
|
||||||
|
getRecentStickers,
|
||||||
|
updateStickerLastUsed,
|
||||||
|
updateStickerPackStatus,
|
||||||
|
} from '../../../js/modules/data';
|
||||||
|
import { maybeDeletePack } from '../../../js/modules/stickers';
|
||||||
|
import { sendStickerPackSync } from '../../shims/textsecure';
|
||||||
|
import { trigger } from '../../shims/events';
|
||||||
|
|
||||||
|
// State
|
||||||
|
|
||||||
|
export type StickerDBType = {
|
||||||
|
readonly id: number;
|
||||||
|
readonly packId: string;
|
||||||
|
|
||||||
|
readonly emoji: string;
|
||||||
|
readonly isCoverOnly: string;
|
||||||
|
readonly lastUsed: number;
|
||||||
|
readonly path: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type StickerPackDBType = {
|
||||||
|
readonly id: string;
|
||||||
|
readonly key: string;
|
||||||
|
|
||||||
|
readonly attemptedStatus: string;
|
||||||
|
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 stickerCount: number;
|
||||||
|
readonly stickers: Dictionary<StickerDBType>;
|
||||||
|
readonly title: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type RecentStickerType = {
|
||||||
|
readonly stickerId: number;
|
||||||
|
readonly packId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type StickersStateType = {
|
||||||
|
readonly installedPack: string | null;
|
||||||
|
readonly packs: Dictionary<StickerPackDBType>;
|
||||||
|
readonly recentStickers: Array<RecentStickerType>;
|
||||||
|
readonly blessedPacks: Dictionary<boolean>;
|
||||||
|
};
|
||||||
|
|
||||||
|
// These are for the React components
|
||||||
|
|
||||||
|
export type StickerType = {
|
||||||
|
readonly id: number;
|
||||||
|
readonly packId: string;
|
||||||
|
readonly emoji: string;
|
||||||
|
readonly url: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type StickerPackType = {
|
||||||
|
readonly id: string;
|
||||||
|
readonly key: string;
|
||||||
|
readonly title: string;
|
||||||
|
readonly author: string;
|
||||||
|
readonly isBlessed: boolean;
|
||||||
|
readonly cover: StickerType;
|
||||||
|
readonly lastUsed: number;
|
||||||
|
readonly status: 'advertised' | 'installed' | 'pending' | 'error';
|
||||||
|
readonly stickers: Array<StickerType>;
|
||||||
|
readonly stickerCount: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Actions
|
||||||
|
|
||||||
|
type StickerPackAddedAction = {
|
||||||
|
type: 'stickers/STICKER_PACK_ADDED';
|
||||||
|
payload: StickerPackDBType;
|
||||||
|
};
|
||||||
|
|
||||||
|
type StickerAddedAction = {
|
||||||
|
type: 'stickers/STICKER_ADDED';
|
||||||
|
payload: StickerDBType;
|
||||||
|
};
|
||||||
|
|
||||||
|
type InstallStickerPackPayloadType = {
|
||||||
|
packId: string;
|
||||||
|
status: 'installed';
|
||||||
|
installedAt: number;
|
||||||
|
recentStickers: Array<RecentStickerType>;
|
||||||
|
};
|
||||||
|
type InstallStickerPackAction = {
|
||||||
|
type: 'stickers/INSTALL_STICKER_PACK';
|
||||||
|
payload: Promise<InstallStickerPackPayloadType>;
|
||||||
|
};
|
||||||
|
type InstallStickerPackFulfilledAction = {
|
||||||
|
type: 'stickers/INSTALL_STICKER_PACK_FULFILLED';
|
||||||
|
payload: InstallStickerPackPayloadType;
|
||||||
|
};
|
||||||
|
type ClearInstalledStickerPackAction = {
|
||||||
|
type: 'stickers/CLEAR_INSTALLED_STICKER_PACK';
|
||||||
|
};
|
||||||
|
|
||||||
|
type UninstallStickerPackPayloadType = {
|
||||||
|
packId: string;
|
||||||
|
status: 'advertised';
|
||||||
|
installedAt: null;
|
||||||
|
recentStickers: Array<RecentStickerType>;
|
||||||
|
};
|
||||||
|
type UninstallStickerPackAction = {
|
||||||
|
type: 'stickers/UNINSTALL_STICKER_PACK';
|
||||||
|
payload: Promise<UninstallStickerPackPayloadType>;
|
||||||
|
};
|
||||||
|
type UninstallStickerPackFulfilledAction = {
|
||||||
|
type: 'stickers/UNINSTALL_STICKER_PACK_FULFILLED';
|
||||||
|
payload: UninstallStickerPackPayloadType;
|
||||||
|
};
|
||||||
|
|
||||||
|
type StickerPackUpdatedAction = {
|
||||||
|
type: 'stickers/STICKER_PACK_UPDATED';
|
||||||
|
payload: { packId: string; patch: Partial<StickerPackDBType> };
|
||||||
|
};
|
||||||
|
|
||||||
|
type StickerPackRemovedAction = {
|
||||||
|
type: 'stickers/REMOVE_STICKER_PACK';
|
||||||
|
payload: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type UseStickerPayloadType = {
|
||||||
|
packId: string;
|
||||||
|
stickerId: number;
|
||||||
|
time: number;
|
||||||
|
};
|
||||||
|
type UseStickerAction = {
|
||||||
|
type: 'stickers/USE_STICKER';
|
||||||
|
payload: Promise<UseStickerPayloadType>;
|
||||||
|
};
|
||||||
|
type UseStickerFulfilledAction = {
|
||||||
|
type: 'stickers/USE_STICKER_FULFILLED';
|
||||||
|
payload: UseStickerPayloadType;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type StickersActionType =
|
||||||
|
| ClearInstalledStickerPackAction
|
||||||
|
| StickerAddedAction
|
||||||
|
| StickerPackAddedAction
|
||||||
|
| InstallStickerPackFulfilledAction
|
||||||
|
| UninstallStickerPackFulfilledAction
|
||||||
|
| StickerPackUpdatedAction
|
||||||
|
| StickerPackRemovedAction
|
||||||
|
| UseStickerFulfilledAction;
|
||||||
|
|
||||||
|
// Action Creators
|
||||||
|
|
||||||
|
export const actions = {
|
||||||
|
clearInstalledStickerPack,
|
||||||
|
removeStickerPack,
|
||||||
|
stickerAdded,
|
||||||
|
stickerPackAdded,
|
||||||
|
installStickerPack,
|
||||||
|
uninstallStickerPack,
|
||||||
|
stickerPackUpdated,
|
||||||
|
useSticker,
|
||||||
|
};
|
||||||
|
|
||||||
|
function removeStickerPack(id: string): StickerPackRemovedAction {
|
||||||
|
return {
|
||||||
|
type: 'stickers/REMOVE_STICKER_PACK',
|
||||||
|
payload: id,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function stickerAdded(payload: StickerDBType): StickerAddedAction {
|
||||||
|
return {
|
||||||
|
type: 'stickers/STICKER_ADDED',
|
||||||
|
payload,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function stickerPackAdded(payload: StickerPackDBType): StickerPackAddedAction {
|
||||||
|
const { status, attemptedStatus } = payload;
|
||||||
|
|
||||||
|
// 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_ADDED',
|
||||||
|
payload,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function installStickerPack(
|
||||||
|
packId: string,
|
||||||
|
packKey: string,
|
||||||
|
options: { fromSync: boolean } | null = null
|
||||||
|
): InstallStickerPackAction {
|
||||||
|
return {
|
||||||
|
type: 'stickers/INSTALL_STICKER_PACK',
|
||||||
|
payload: doInstallStickerPack(packId, packKey, options),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
async function doInstallStickerPack(
|
||||||
|
packId: string,
|
||||||
|
packKey: string,
|
||||||
|
options: { fromSync: boolean } | null
|
||||||
|
): Promise<InstallStickerPackPayloadType> {
|
||||||
|
const { fromSync } = options || { fromSync: false };
|
||||||
|
|
||||||
|
const status = 'installed';
|
||||||
|
const timestamp = Date.now();
|
||||||
|
await updateStickerPackStatus(packId, status, { timestamp });
|
||||||
|
|
||||||
|
if (!fromSync) {
|
||||||
|
// Kick this off, but don't wait for it
|
||||||
|
sendStickerPackSync(packId, packKey, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
const recentStickers = await getRecentStickers();
|
||||||
|
|
||||||
|
return {
|
||||||
|
packId,
|
||||||
|
installedAt: timestamp,
|
||||||
|
status,
|
||||||
|
recentStickers: recentStickers.map(item => ({
|
||||||
|
packId: item.packId,
|
||||||
|
stickerId: item.id,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
function uninstallStickerPack(
|
||||||
|
packId: string,
|
||||||
|
packKey: string,
|
||||||
|
options: { fromSync: boolean } | null = null
|
||||||
|
): UninstallStickerPackAction {
|
||||||
|
return {
|
||||||
|
type: 'stickers/UNINSTALL_STICKER_PACK',
|
||||||
|
payload: doUninstallStickerPack(packId, packKey, options),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
async function doUninstallStickerPack(
|
||||||
|
packId: string,
|
||||||
|
packKey: string,
|
||||||
|
options: { fromSync: boolean } | null
|
||||||
|
): Promise<UninstallStickerPackPayloadType> {
|
||||||
|
const { fromSync } = options || { fromSync: false };
|
||||||
|
|
||||||
|
const status = 'advertised';
|
||||||
|
await updateStickerPackStatus(packId, status);
|
||||||
|
|
||||||
|
// If there are no more references, it should be removed
|
||||||
|
await maybeDeletePack(packId);
|
||||||
|
|
||||||
|
if (!fromSync) {
|
||||||
|
// Kick this off, but don't wait for it
|
||||||
|
sendStickerPackSync(packId, packKey, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
const recentStickers = await getRecentStickers();
|
||||||
|
|
||||||
|
return {
|
||||||
|
packId,
|
||||||
|
status,
|
||||||
|
installedAt: null,
|
||||||
|
recentStickers: recentStickers.map(item => ({
|
||||||
|
packId: item.packId,
|
||||||
|
stickerId: item.id,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
function clearInstalledStickerPack(): ClearInstalledStickerPackAction {
|
||||||
|
return { type: 'stickers/CLEAR_INSTALLED_STICKER_PACK' };
|
||||||
|
}
|
||||||
|
|
||||||
|
function stickerPackUpdated(
|
||||||
|
packId: string,
|
||||||
|
patch: Partial<StickerPackDBType>
|
||||||
|
): StickerPackUpdatedAction {
|
||||||
|
return {
|
||||||
|
type: 'stickers/STICKER_PACK_UPDATED',
|
||||||
|
payload: {
|
||||||
|
packId,
|
||||||
|
patch,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function useSticker(
|
||||||
|
packId: string,
|
||||||
|
stickerId: number,
|
||||||
|
time = Date.now()
|
||||||
|
): UseStickerAction {
|
||||||
|
return {
|
||||||
|
type: 'stickers/USE_STICKER',
|
||||||
|
payload: doUseSticker(packId, stickerId, time),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
async function doUseSticker(
|
||||||
|
packId: string,
|
||||||
|
stickerId: number,
|
||||||
|
time = Date.now()
|
||||||
|
): Promise<UseStickerPayloadType> {
|
||||||
|
await updateStickerLastUsed(packId, stickerId, time);
|
||||||
|
|
||||||
|
return {
|
||||||
|
packId,
|
||||||
|
stickerId,
|
||||||
|
time,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reducer
|
||||||
|
|
||||||
|
function getEmptyState(): StickersStateType {
|
||||||
|
return {
|
||||||
|
installedPack: null,
|
||||||
|
packs: {},
|
||||||
|
recentStickers: [],
|
||||||
|
blessedPacks: {},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// tslint:disable-next-line max-func-body-length
|
||||||
|
export function reducer(
|
||||||
|
state: StickersStateType = getEmptyState(),
|
||||||
|
action: StickersActionType
|
||||||
|
): StickersStateType {
|
||||||
|
if (action.type === 'stickers/STICKER_PACK_ADDED') {
|
||||||
|
const { payload } = action;
|
||||||
|
const newPack = {
|
||||||
|
stickers: {},
|
||||||
|
...payload,
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
packs: {
|
||||||
|
...state.packs,
|
||||||
|
[payload.id]: newPack,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action.type === 'stickers/STICKER_ADDED') {
|
||||||
|
const { payload } = action;
|
||||||
|
const packToUpdate = state.packs[payload.packId];
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
packs: {
|
||||||
|
...state.packs,
|
||||||
|
[packToUpdate.id]: {
|
||||||
|
...packToUpdate,
|
||||||
|
stickers: {
|
||||||
|
...packToUpdate.stickers,
|
||||||
|
[payload.id]: payload,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action.type === 'stickers/STICKER_PACK_UPDATED') {
|
||||||
|
const { payload } = action;
|
||||||
|
const packToUpdate = state.packs[payload.packId];
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
packs: {
|
||||||
|
...state.packs,
|
||||||
|
[packToUpdate.id]: {
|
||||||
|
...packToUpdate,
|
||||||
|
...payload.patch,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
action.type === 'stickers/INSTALL_STICKER_PACK_FULFILLED' ||
|
||||||
|
action.type === 'stickers/UNINSTALL_STICKER_PACK_FULFILLED'
|
||||||
|
) {
|
||||||
|
const { payload } = action;
|
||||||
|
const { installedAt, packId, status, recentStickers } = payload;
|
||||||
|
const { packs } = state;
|
||||||
|
const existingPack = packs[packId];
|
||||||
|
|
||||||
|
// A pack might be deleted as part of the uninstall process
|
||||||
|
if (!existingPack) {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
installedPack:
|
||||||
|
state.installedPack === packId ? null : state.installedPack,
|
||||||
|
recentStickers,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
installedPack: packId,
|
||||||
|
packs: {
|
||||||
|
...packs,
|
||||||
|
[packId]: {
|
||||||
|
...packs[packId],
|
||||||
|
status,
|
||||||
|
installedAt,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
recentStickers,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action.type === 'stickers/CLEAR_INSTALLED_STICKER_PACK') {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
installedPack: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action.type === 'stickers/REMOVE_STICKER_PACK') {
|
||||||
|
const { payload } = action;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
packs: omit(state.packs, payload),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action.type === 'stickers/USE_STICKER_FULFILLED') {
|
||||||
|
const { payload } = action;
|
||||||
|
const { packId, stickerId, time } = payload;
|
||||||
|
const { recentStickers, packs } = state;
|
||||||
|
|
||||||
|
const filteredRecents = reject(
|
||||||
|
recentStickers,
|
||||||
|
item => item.packId === packId && item.stickerId === stickerId
|
||||||
|
);
|
||||||
|
const pack = packs[packId];
|
||||||
|
const sticker = pack.stickers[stickerId];
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
recentStickers: [payload, ...filteredRecents],
|
||||||
|
packs: {
|
||||||
|
...state.packs,
|
||||||
|
[packId]: {
|
||||||
|
...pack,
|
||||||
|
lastUsed: time,
|
||||||
|
stickers: {
|
||||||
|
...pack.stickers,
|
||||||
|
[stickerId]: {
|
||||||
|
...sticker,
|
||||||
|
lastUsed: time,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return state;
|
||||||
|
}
|
|
@ -1,8 +1,11 @@
|
||||||
|
import { AnyAction } from 'redux';
|
||||||
import { LocalizerType } from '../../types/Util';
|
import { LocalizerType } from '../../types/Util';
|
||||||
|
|
||||||
// State
|
// State
|
||||||
|
|
||||||
export type UserStateType = {
|
export type UserStateType = {
|
||||||
|
attachmentsPath: string;
|
||||||
|
stickersPath: string;
|
||||||
ourNumber: string;
|
ourNumber: string;
|
||||||
regionCode: string;
|
regionCode: string;
|
||||||
i18n: LocalizerType;
|
i18n: LocalizerType;
|
||||||
|
@ -18,7 +21,7 @@ type UserChangedActionType = {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export type UserActionType = UserChangedActionType;
|
export type UserActionType = AnyAction | UserChangedActionType;
|
||||||
|
|
||||||
// Action Creators
|
// Action Creators
|
||||||
|
|
||||||
|
@ -40,6 +43,8 @@ function userChanged(attributes: {
|
||||||
|
|
||||||
function getEmptyState(): UserStateType {
|
function getEmptyState(): UserStateType {
|
||||||
return {
|
return {
|
||||||
|
attachmentsPath: 'missing',
|
||||||
|
stickersPath: 'missing',
|
||||||
ourNumber: 'missing',
|
ourNumber: 'missing',
|
||||||
regionCode: 'missing',
|
regionCode: 'missing',
|
||||||
i18n: () => 'missing',
|
i18n: () => 'missing',
|
||||||
|
@ -47,7 +52,7 @@ function getEmptyState(): UserStateType {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function reducer(
|
export function reducer(
|
||||||
state: UserStateType,
|
state: UserStateType = getEmptyState(),
|
||||||
action: UserActionType
|
action: UserActionType
|
||||||
): UserStateType {
|
): UserStateType {
|
||||||
if (!state) {
|
if (!state) {
|
||||||
|
|
|
@ -1,25 +1,48 @@
|
||||||
import { combineReducers } from 'redux';
|
import { combineReducers } from 'redux';
|
||||||
|
|
||||||
import { reducer as search, SearchStateType } from './ducks/search';
|
|
||||||
import {
|
import {
|
||||||
|
ConversationActionType,
|
||||||
ConversationsStateType,
|
ConversationsStateType,
|
||||||
reducer as conversations,
|
reducer as conversations,
|
||||||
} from './ducks/conversations';
|
} from './ducks/conversations';
|
||||||
|
import {
|
||||||
|
ItemsActionType,
|
||||||
|
ItemsStateType,
|
||||||
|
reducer as items,
|
||||||
|
} from './ducks/items';
|
||||||
|
import {
|
||||||
|
reducer as search,
|
||||||
|
SEARCH_TYPES as SearchActionType,
|
||||||
|
SearchStateType,
|
||||||
|
} from './ducks/search';
|
||||||
|
import {
|
||||||
|
reducer as stickers,
|
||||||
|
StickersActionType,
|
||||||
|
StickersStateType,
|
||||||
|
} from './ducks/stickers';
|
||||||
import { reducer as user, UserStateType } from './ducks/user';
|
import { reducer as user, UserStateType } from './ducks/user';
|
||||||
|
|
||||||
export type StateType = {
|
export type StateType = {
|
||||||
search: SearchStateType;
|
|
||||||
conversations: ConversationsStateType;
|
conversations: ConversationsStateType;
|
||||||
|
items: ItemsStateType;
|
||||||
|
search: SearchStateType;
|
||||||
|
stickers: StickersStateType;
|
||||||
user: UserStateType;
|
user: UserStateType;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ActionsType =
|
||||||
|
| ItemsActionType
|
||||||
|
| ConversationActionType
|
||||||
|
| StickersActionType
|
||||||
|
| SearchActionType;
|
||||||
|
|
||||||
export const reducers = {
|
export const reducers = {
|
||||||
search,
|
|
||||||
conversations,
|
conversations,
|
||||||
|
items,
|
||||||
|
search,
|
||||||
|
stickers,
|
||||||
user,
|
user,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Making this work would require that our reducer signature supported AnyAction, not
|
// @ts-ignore: AnyAction breaks strong type checking inside reducers
|
||||||
// our restricted actions
|
export const reducer = combineReducers<StateType, ActionsType>(reducers);
|
||||||
// @ts-ignore
|
|
||||||
export const reducer = combineReducers(reducers);
|
|
||||||
|
|
16
ts/state/roots/createStickerButton.tsx
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { Provider } from 'react-redux';
|
||||||
|
|
||||||
|
import { Store } from 'redux';
|
||||||
|
|
||||||
|
import { SmartStickerButton } from '../smart/StickerButton';
|
||||||
|
|
||||||
|
// Workaround: A react component's required properties are filtering up through connect()
|
||||||
|
// https://github.com/DefinitelyTyped/DefinitelyTyped/issues/31363
|
||||||
|
const FilteredStickerButton = SmartStickerButton as any;
|
||||||
|
|
||||||
|
export const createStickerButton = (store: Store, props: Object) => (
|
||||||
|
<Provider store={store}>
|
||||||
|
<FilteredStickerButton {...props} />
|
||||||
|
</Provider>
|
||||||
|
);
|
16
ts/state/roots/createStickerManager.tsx
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { Provider } from 'react-redux';
|
||||||
|
|
||||||
|
import { Store } from 'redux';
|
||||||
|
|
||||||
|
import { SmartStickerManager } from '../smart/StickerManager';
|
||||||
|
|
||||||
|
// Workaround: A react component's required properties are filtering up through connect()
|
||||||
|
// https://github.com/DefinitelyTyped/DefinitelyTyped/issues/31363
|
||||||
|
const FilteredStickerManager = SmartStickerManager as any;
|
||||||
|
|
||||||
|
export const createStickerManager = (store: Store) => (
|
||||||
|
<Provider store={store}>
|
||||||
|
<FilteredStickerManager />
|
||||||
|
</Provider>
|
||||||
|
);
|
16
ts/state/roots/createStickerPreviewModal.tsx
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { Provider } from 'react-redux';
|
||||||
|
|
||||||
|
import { Store } from 'redux';
|
||||||
|
|
||||||
|
import { SmartStickerPreviewModal } from '../smart/StickerPreviewModal';
|
||||||
|
|
||||||
|
// Workaround: A react component's required properties are filtering up through connect()
|
||||||
|
// https://github.com/DefinitelyTyped/DefinitelyTyped/issues/31363
|
||||||
|
const FilteredStickerPreviewModal = SmartStickerPreviewModal as any;
|
||||||
|
|
||||||
|
export const createStickerPreviewModal = (store: Store, props: Object) => (
|
||||||
|
<Provider store={store}>
|
||||||
|
<FilteredStickerPreviewModal {...props} />
|
||||||
|
</Provider>
|
||||||
|
);
|
206
ts/state/selectors/stickers.ts
Normal file
|
@ -0,0 +1,206 @@
|
||||||
|
import { join } from 'path';
|
||||||
|
import {
|
||||||
|
compact,
|
||||||
|
Dictionary,
|
||||||
|
filter,
|
||||||
|
map,
|
||||||
|
orderBy,
|
||||||
|
reject,
|
||||||
|
sortBy,
|
||||||
|
values,
|
||||||
|
} from 'lodash';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
|
||||||
|
import { StateType } from '../reducer';
|
||||||
|
import {
|
||||||
|
RecentStickerType,
|
||||||
|
StickerDBType,
|
||||||
|
StickerPackDBType,
|
||||||
|
StickerPackType,
|
||||||
|
StickersStateType,
|
||||||
|
StickerType,
|
||||||
|
} from '../ducks/stickers';
|
||||||
|
import { getStickersPath } from './user';
|
||||||
|
|
||||||
|
const getSticker = (
|
||||||
|
packs: Dictionary<StickerPackDBType>,
|
||||||
|
packId: string,
|
||||||
|
stickerId: number,
|
||||||
|
stickerPath: string
|
||||||
|
): StickerType | undefined => {
|
||||||
|
const pack = packs[packId];
|
||||||
|
if (!pack) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sticker = pack.stickers[stickerId];
|
||||||
|
if (!sticker) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return translateStickerFromDB(sticker, stickerPath);
|
||||||
|
};
|
||||||
|
|
||||||
|
const translateStickerFromDB = (
|
||||||
|
sticker: StickerDBType,
|
||||||
|
stickerPath: string
|
||||||
|
): StickerType => {
|
||||||
|
const { id, packId, emoji, path } = sticker;
|
||||||
|
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
packId,
|
||||||
|
emoji,
|
||||||
|
url: join(stickerPath, path),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const translatePackFromDB = (
|
||||||
|
pack: StickerPackDBType,
|
||||||
|
packs: Dictionary<StickerPackDBType>,
|
||||||
|
blessedPacks: Dictionary<boolean>,
|
||||||
|
stickersPath: string
|
||||||
|
) => {
|
||||||
|
const { id, stickers, coverStickerId } = pack;
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
const filteredStickers = reject(
|
||||||
|
values(stickers),
|
||||||
|
sticker => sticker.isCoverOnly
|
||||||
|
);
|
||||||
|
const translatedStickers = map(filteredStickers, sticker =>
|
||||||
|
translateStickerFromDB(sticker, stickersPath)
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...pack,
|
||||||
|
isBlessed: Boolean(blessedPacks[id]),
|
||||||
|
cover: getSticker(packs, id, coverStickerId, stickersPath),
|
||||||
|
stickers: sortBy(translatedStickers, sticker => sticker.id),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const filterAndTransformPacks = (
|
||||||
|
packs: Dictionary<StickerPackDBType>,
|
||||||
|
packFilter: (sticker: StickerPackDBType) => boolean,
|
||||||
|
packSort: (sticker: StickerPackDBType) => any,
|
||||||
|
blessedPacks: Dictionary<boolean>,
|
||||||
|
stickersPath: 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)
|
||||||
|
);
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
|
||||||
|
export const getPacks = createSelector(
|
||||||
|
getStickers,
|
||||||
|
(stickers: StickersStateType) => stickers.packs
|
||||||
|
);
|
||||||
|
|
||||||
|
const getRecents = createSelector(
|
||||||
|
getStickers,
|
||||||
|
(stickers: StickersStateType) => stickers.recentStickers
|
||||||
|
);
|
||||||
|
|
||||||
|
export const getBlessedPacks = createSelector(
|
||||||
|
getStickers,
|
||||||
|
(stickers: StickersStateType) => stickers.blessedPacks
|
||||||
|
);
|
||||||
|
|
||||||
|
export const getRecentStickers = createSelector(
|
||||||
|
getRecents,
|
||||||
|
getPacks,
|
||||||
|
getStickersPath,
|
||||||
|
(
|
||||||
|
recents: Array<RecentStickerType>,
|
||||||
|
packs: Dictionary<StickerPackDBType>,
|
||||||
|
stickersPath: string
|
||||||
|
) => {
|
||||||
|
return compact(
|
||||||
|
recents.map(({ packId, stickerId }) => {
|
||||||
|
return getSticker(packs, packId, stickerId, stickersPath);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export const getInstalledStickerPacks = createSelector(
|
||||||
|
getPacks,
|
||||||
|
getBlessedPacks,
|
||||||
|
getStickersPath,
|
||||||
|
(
|
||||||
|
packs: Dictionary<StickerPackDBType>,
|
||||||
|
blessedPacks: Dictionary<boolean>,
|
||||||
|
stickersPath: string
|
||||||
|
): Array<StickerPackType> => {
|
||||||
|
return filterAndTransformPacks(
|
||||||
|
packs,
|
||||||
|
pack => pack.status === 'installed',
|
||||||
|
pack => pack.installedAt,
|
||||||
|
blessedPacks,
|
||||||
|
stickersPath
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export const getRecentlyInstalledStickerPack = createSelector(
|
||||||
|
getInstalledStickerPacks,
|
||||||
|
getStickers,
|
||||||
|
(packs, { installedPack: packId }) => {
|
||||||
|
if (!packId) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return packs.find(({ id }) => id === packId) || null;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export const getReceivedStickerPacks = createSelector(
|
||||||
|
getPacks,
|
||||||
|
getBlessedPacks,
|
||||||
|
getStickersPath,
|
||||||
|
(
|
||||||
|
packs: Dictionary<StickerPackDBType>,
|
||||||
|
blessedPacks: Dictionary<boolean>,
|
||||||
|
stickersPath: string
|
||||||
|
): Array<StickerPackType> => {
|
||||||
|
return filterAndTransformPacks(
|
||||||
|
packs,
|
||||||
|
pack =>
|
||||||
|
(pack.status === 'advertised' || pack.status === 'pending') &&
|
||||||
|
!blessedPacks[pack.id],
|
||||||
|
pack => pack.createdAt,
|
||||||
|
blessedPacks,
|
||||||
|
stickersPath
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export const getBlessedStickerPacks = createSelector(
|
||||||
|
getPacks,
|
||||||
|
getBlessedPacks,
|
||||||
|
getStickersPath,
|
||||||
|
(
|
||||||
|
packs: Dictionary<StickerPackDBType>,
|
||||||
|
blessedPacks: Dictionary<boolean>,
|
||||||
|
stickersPath: string
|
||||||
|
): Array<StickerPackType> => {
|
||||||
|
return filterAndTransformPacks(
|
||||||
|
packs,
|
||||||
|
pack => blessedPacks[pack.id] && pack.status !== 'installed',
|
||||||
|
pack => pack.createdAt,
|
||||||
|
blessedPacks,
|
||||||
|
stickersPath
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
|
@ -21,3 +21,13 @@ export const getIntl = createSelector(
|
||||||
getUser,
|
getUser,
|
||||||
(state: UserStateType): LocalizerType => state.i18n
|
(state: UserStateType): LocalizerType => state.i18n
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const getAttachmentsPath = createSelector(
|
||||||
|
getUser,
|
||||||
|
(state: UserStateType): string => state.attachmentsPath
|
||||||
|
);
|
||||||
|
|
||||||
|
export const getStickersPath = createSelector(
|
||||||
|
getUser,
|
||||||
|
(state: UserStateType): string => state.stickersPath
|
||||||
|
);
|
||||||
|
|
48
ts/state/smart/StickerButton.tsx
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { get } from 'lodash';
|
||||||
|
import { mapDispatchToProps } from '../actions';
|
||||||
|
import { StickerButton } from '../../components/stickers/StickerButton';
|
||||||
|
import { StateType } from '../reducer';
|
||||||
|
|
||||||
|
import { getIntl } from '../selectors/user';
|
||||||
|
import {
|
||||||
|
getInstalledStickerPacks,
|
||||||
|
getReceivedStickerPacks,
|
||||||
|
getRecentlyInstalledStickerPack,
|
||||||
|
getRecentStickers,
|
||||||
|
} from '../selectors/stickers';
|
||||||
|
|
||||||
|
const mapStateToProps = (state: StateType) => {
|
||||||
|
const receivedPacks = getReceivedStickerPacks(state);
|
||||||
|
const installedPacks = getInstalledStickerPacks(state);
|
||||||
|
const recentStickers = getRecentStickers(state);
|
||||||
|
const installedPack = getRecentlyInstalledStickerPack(state);
|
||||||
|
const showIntroduction = get(
|
||||||
|
state.items,
|
||||||
|
['showStickersIntroduction', 'value'],
|
||||||
|
false
|
||||||
|
);
|
||||||
|
const showPickerHint =
|
||||||
|
get(state.items, ['showStickerPickerHint', 'value'], false) &&
|
||||||
|
receivedPacks.length > 0;
|
||||||
|
|
||||||
|
return {
|
||||||
|
receivedPacks,
|
||||||
|
installedPack,
|
||||||
|
installedPacks,
|
||||||
|
recentStickers,
|
||||||
|
showIntroduction,
|
||||||
|
showPickerHint,
|
||||||
|
i18n: getIntl(state),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const smart = connect(mapStateToProps, {
|
||||||
|
...mapDispatchToProps,
|
||||||
|
clearShowIntroduction: () =>
|
||||||
|
mapDispatchToProps.removeItem('showStickersIntroduction'),
|
||||||
|
clearShowPickerHint: () =>
|
||||||
|
mapDispatchToProps.removeItem('showStickerPickerHint'),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const SmartStickerButton = smart(StickerButton);
|
28
ts/state/smart/StickerManager.tsx
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { mapDispatchToProps } from '../actions';
|
||||||
|
import { StickerManager } from '../../components/stickers/StickerManager';
|
||||||
|
import { StateType } from '../reducer';
|
||||||
|
|
||||||
|
import { getIntl } from '../selectors/user';
|
||||||
|
import {
|
||||||
|
getBlessedStickerPacks,
|
||||||
|
getInstalledStickerPacks,
|
||||||
|
getReceivedStickerPacks,
|
||||||
|
} from '../selectors/stickers';
|
||||||
|
|
||||||
|
const mapStateToProps = (state: StateType) => {
|
||||||
|
const blessedPacks = getBlessedStickerPacks(state);
|
||||||
|
const receivedPacks = getReceivedStickerPacks(state);
|
||||||
|
const installedPacks = getInstalledStickerPacks(state);
|
||||||
|
|
||||||
|
return {
|
||||||
|
blessedPacks,
|
||||||
|
receivedPacks,
|
||||||
|
installedPacks,
|
||||||
|
i18n: getIntl(state),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const smart = connect(mapStateToProps, mapDispatchToProps);
|
||||||
|
|
||||||
|
export const SmartStickerManager = smart(StickerManager);
|
54
ts/state/smart/StickerPreviewModal.tsx
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { mapDispatchToProps } from '../actions';
|
||||||
|
import { StickerPreviewModal } from '../../components/stickers/StickerPreviewModal';
|
||||||
|
import { StateType } from '../reducer';
|
||||||
|
|
||||||
|
import { getIntl, getStickersPath } from '../selectors/user';
|
||||||
|
import {
|
||||||
|
getBlessedPacks,
|
||||||
|
getPacks,
|
||||||
|
translatePackFromDB,
|
||||||
|
} from '../selectors/stickers';
|
||||||
|
|
||||||
|
type ExternalProps = {
|
||||||
|
packId: string;
|
||||||
|
readonly onClose: () => unknown;
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapStateToProps = (state: StateType, props: ExternalProps) => {
|
||||||
|
const { packId } = props;
|
||||||
|
const stickersPath = getStickersPath(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',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
i18n: getIntl(state),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const smart = connect(mapStateToProps, mapDispatchToProps);
|
||||||
|
|
||||||
|
export const SmartStickerPreviewModal = smart(StickerPreviewModal);
|
|
@ -41,6 +41,10 @@ import landscape from '../../fixtures/koushik-chowdavarapu-105425-unsplash.jpg';
|
||||||
// 800×1200
|
// 800×1200
|
||||||
const landscapeObjectUrl = makeObjectUrl(landscape, 'image/png');
|
const landscapeObjectUrl = makeObjectUrl(landscape, 'image/png');
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
import squareSticker from '../../fixtures/512x515-thumbs-up-lincoln.webp';
|
||||||
|
const squareStickerObjectUrl = makeObjectUrl(squareSticker, 'image/webp');
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import landscapeGreen from '../../fixtures/1000x50-green.jpeg';
|
import landscapeGreen from '../../fixtures/1000x50-green.jpeg';
|
||||||
const landscapeGreenObjectUrl = makeObjectUrl(landscapeGreen, 'image/jpeg');
|
const landscapeGreenObjectUrl = makeObjectUrl(landscapeGreen, 'image/jpeg');
|
||||||
|
@ -57,6 +61,16 @@ const landscapeRedObjectUrl = makeObjectUrl(landscapeRed, 'image/png');
|
||||||
import portraitTeal from '../../fixtures/50x1000-teal.jpeg';
|
import portraitTeal from '../../fixtures/50x1000-teal.jpeg';
|
||||||
const portraitTealObjectUrl = makeObjectUrl(portraitTeal, 'image/png');
|
const portraitTealObjectUrl = makeObjectUrl(portraitTeal, 'image/png');
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
import kitten164 from '../../fixtures/kitten-1-64-64.jpg';
|
||||||
|
const kitten164ObjectUrl = makeObjectUrl(kitten164, 'image/jpeg');
|
||||||
|
// @ts-ignore
|
||||||
|
import kitten264 from '../../fixtures/kitten-2-64-64.jpg';
|
||||||
|
const kitten264ObjectUrl = makeObjectUrl(kitten264, 'image/jpeg');
|
||||||
|
// @ts-ignore
|
||||||
|
import kitten364 from '../../fixtures/kitten-3-64-64.jpg';
|
||||||
|
const kitten364ObjectUrl = makeObjectUrl(kitten364, 'image/jpeg');
|
||||||
|
|
||||||
function makeObjectUrl(data: ArrayBuffer, contentType: string): string {
|
function makeObjectUrl(data: ArrayBuffer, contentType: string): string {
|
||||||
const blob = new Blob([data], {
|
const blob = new Blob([data], {
|
||||||
type: contentType,
|
type: contentType,
|
||||||
|
@ -66,6 +80,12 @@ function makeObjectUrl(data: ArrayBuffer, contentType: string): string {
|
||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
kitten164,
|
||||||
|
kitten164ObjectUrl,
|
||||||
|
kitten264,
|
||||||
|
kitten264ObjectUrl,
|
||||||
|
kitten364,
|
||||||
|
kitten364ObjectUrl,
|
||||||
mp3,
|
mp3,
|
||||||
mp3ObjectUrl,
|
mp3ObjectUrl,
|
||||||
gif,
|
gif,
|
||||||
|
@ -76,6 +96,8 @@ export {
|
||||||
mp4ObjectUrlV2,
|
mp4ObjectUrlV2,
|
||||||
png,
|
png,
|
||||||
pngObjectUrl,
|
pngObjectUrl,
|
||||||
|
squareSticker,
|
||||||
|
squareStickerObjectUrl,
|
||||||
txt,
|
txt,
|
||||||
txtObjectUrl,
|
txtObjectUrl,
|
||||||
landscape,
|
landscape,
|
||||||
|
|
|
@ -6,6 +6,7 @@ export const AUDIO_AAC = 'audio/aac' as MIMEType;
|
||||||
export const AUDIO_MP3 = 'audio/mp3' as MIMEType;
|
export const AUDIO_MP3 = 'audio/mp3' as MIMEType;
|
||||||
export const IMAGE_GIF = 'image/gif' as MIMEType;
|
export const IMAGE_GIF = 'image/gif' as MIMEType;
|
||||||
export const IMAGE_JPEG = 'image/jpeg' as MIMEType;
|
export const IMAGE_JPEG = 'image/jpeg' as MIMEType;
|
||||||
|
export const IMAGE_WEBP = 'image/webp' as MIMEType;
|
||||||
export const VIDEO_MP4 = 'video/mp4' as MIMEType;
|
export const VIDEO_MP4 = 'video/mp4' as MIMEType;
|
||||||
export const VIDEO_QUICKTIME = 'video/quicktime' as MIMEType;
|
export const VIDEO_QUICKTIME = 'video/quicktime' as MIMEType;
|
||||||
|
|
||||||
|
|
|
@ -203,46 +203,6 @@
|
||||||
"updated": "2018-09-19T18:13:29.628Z",
|
"updated": "2018-09-19T18:13:29.628Z",
|
||||||
"reasonDetail": "Interacting with already-existing DOM nodes"
|
"reasonDetail": "Interacting with already-existing DOM nodes"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"rule": "jQuery-wrap(",
|
|
||||||
"path": "js/modules/crypto.js",
|
|
||||||
"line": " return dcodeIO.ByteBuffer.wrap(arrayBuffer).toString('base64');",
|
|
||||||
"lineNumber": 45,
|
|
||||||
"reasonCategory": "falseMatch",
|
|
||||||
"updated": "2018-10-05T23:12:28.961Z"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"rule": "jQuery-wrap(",
|
|
||||||
"path": "js/modules/crypto.js",
|
|
||||||
"line": " return dcodeIO.ByteBuffer.wrap(base64string, 'base64').toArrayBuffer();",
|
|
||||||
"lineNumber": 48,
|
|
||||||
"reasonCategory": "falseMatch",
|
|
||||||
"updated": "2018-10-05T23:12:28.961Z"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"rule": "jQuery-wrap(",
|
|
||||||
"path": "js/modules/crypto.js",
|
|
||||||
"line": " return dcodeIO.ByteBuffer.wrap(key, 'binary').toArrayBuffer();",
|
|
||||||
"lineNumber": 52,
|
|
||||||
"reasonCategory": "falseMatch",
|
|
||||||
"updated": "2018-10-05T23:12:28.961Z"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"rule": "jQuery-wrap(",
|
|
||||||
"path": "js/modules/crypto.js",
|
|
||||||
"line": " return dcodeIO.ByteBuffer.wrap(string, 'utf8').toArrayBuffer();",
|
|
||||||
"lineNumber": 56,
|
|
||||||
"reasonCategory": "falseMatch",
|
|
||||||
"updated": "2018-10-05T23:12:28.961Z"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"rule": "jQuery-wrap(",
|
|
||||||
"path": "js/modules/crypto.js",
|
|
||||||
"line": " return dcodeIO.ByteBuffer.wrap(buffer).toString('utf8');",
|
|
||||||
"lineNumber": 59,
|
|
||||||
"reasonCategory": "falseMatch",
|
|
||||||
"updated": "2018-10-05T23:12:28.961Z"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"rule": "jQuery-append(",
|
"rule": "jQuery-append(",
|
||||||
"path": "js/modules/debuglogs.js",
|
"path": "js/modules/debuglogs.js",
|
||||||
|
@ -267,6 +227,14 @@
|
||||||
"reasonCategory": "falseMatch",
|
"reasonCategory": "falseMatch",
|
||||||
"updated": "2018-09-19T18:13:29.628Z"
|
"updated": "2018-09-19T18:13:29.628Z"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"rule": "jQuery-load(",
|
||||||
|
"path": "js/modules/stickers.js",
|
||||||
|
"line": "async function load() {",
|
||||||
|
"lineNumber": 53,
|
||||||
|
"reasonCategory": "falseMatch",
|
||||||
|
"updated": "2019-04-26T17:48:30.675Z"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"rule": "jQuery-$(",
|
"rule": "jQuery-$(",
|
||||||
"path": "js/permissions_popup_start.js",
|
"path": "js/permissions_popup_start.js",
|
||||||
|
@ -501,7 +469,7 @@
|
||||||
"rule": "jQuery-$(",
|
"rule": "jQuery-$(",
|
||||||
"path": "js/views/inbox_view.js",
|
"path": "js/views/inbox_view.js",
|
||||||
"line": " let $el = this.$(`#${id}`);",
|
"line": " let $el = this.$(`#${id}`);",
|
||||||
"lineNumber": 27,
|
"lineNumber": 33,
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2018-09-19T21:59:32.770Z",
|
"updated": "2018-09-19T21:59:32.770Z",
|
||||||
"reasonDetail": "Protected from arbitrary input"
|
"reasonDetail": "Protected from arbitrary input"
|
||||||
|
@ -510,7 +478,7 @@
|
||||||
"rule": "jQuery-prependTo(",
|
"rule": "jQuery-prependTo(",
|
||||||
"path": "js/views/inbox_view.js",
|
"path": "js/views/inbox_view.js",
|
||||||
"line": " $el.prependTo(this.el);",
|
"line": " $el.prependTo(this.el);",
|
||||||
"lineNumber": 36,
|
"lineNumber": 42,
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2018-09-19T18:13:29.628Z",
|
"updated": "2018-09-19T18:13:29.628Z",
|
||||||
"reasonDetail": "Interacting with already-existing DOM nodes"
|
"reasonDetail": "Interacting with already-existing DOM nodes"
|
||||||
|
@ -519,7 +487,7 @@
|
||||||
"rule": "jQuery-$(",
|
"rule": "jQuery-$(",
|
||||||
"path": "js/views/inbox_view.js",
|
"path": "js/views/inbox_view.js",
|
||||||
"line": " this.$('.message').text(message);",
|
"line": " this.$('.message').text(message);",
|
||||||
"lineNumber": 48,
|
"lineNumber": 56,
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2018-09-19T21:59:32.770Z",
|
"updated": "2018-09-19T21:59:32.770Z",
|
||||||
"reasonDetail": "Protected from arbitrary input"
|
"reasonDetail": "Protected from arbitrary input"
|
||||||
|
@ -528,7 +496,7 @@
|
||||||
"rule": "jQuery-$(",
|
"rule": "jQuery-$(",
|
||||||
"path": "js/views/inbox_view.js",
|
"path": "js/views/inbox_view.js",
|
||||||
"line": " el: this.$('.conversation-stack'),",
|
"line": " el: this.$('.conversation-stack'),",
|
||||||
"lineNumber": 65,
|
"lineNumber": 73,
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2018-09-19T21:59:32.770Z",
|
"updated": "2018-09-19T21:59:32.770Z",
|
||||||
"reasonDetail": "Protected from arbitrary input"
|
"reasonDetail": "Protected from arbitrary input"
|
||||||
|
@ -537,7 +505,7 @@
|
||||||
"rule": "jQuery-prependTo(",
|
"rule": "jQuery-prependTo(",
|
||||||
"path": "js/views/inbox_view.js",
|
"path": "js/views/inbox_view.js",
|
||||||
"line": " this.appLoadingScreen.$el.prependTo(this.el);",
|
"line": " this.appLoadingScreen.$el.prependTo(this.el);",
|
||||||
"lineNumber": 72,
|
"lineNumber": 80,
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2018-09-19T18:13:29.628Z",
|
"updated": "2018-09-19T18:13:29.628Z",
|
||||||
"reasonDetail": "Interacting with already-existing DOM nodes"
|
"reasonDetail": "Interacting with already-existing DOM nodes"
|
||||||
|
@ -546,7 +514,7 @@
|
||||||
"rule": "jQuery-append(",
|
"rule": "jQuery-append(",
|
||||||
"path": "js/views/inbox_view.js",
|
"path": "js/views/inbox_view.js",
|
||||||
"line": " .append(this.networkStatusView.render().el);",
|
"line": " .append(this.networkStatusView.render().el);",
|
||||||
"lineNumber": 87,
|
"lineNumber": 95,
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2018-09-19T18:13:29.628Z",
|
"updated": "2018-09-19T18:13:29.628Z",
|
||||||
"reasonDetail": "Interacting with already-existing DOM nodes"
|
"reasonDetail": "Interacting with already-existing DOM nodes"
|
||||||
|
@ -555,16 +523,25 @@
|
||||||
"rule": "jQuery-prependTo(",
|
"rule": "jQuery-prependTo(",
|
||||||
"path": "js/views/inbox_view.js",
|
"path": "js/views/inbox_view.js",
|
||||||
"line": " banner.$el.prependTo(this.$el);",
|
"line": " banner.$el.prependTo(this.$el);",
|
||||||
"lineNumber": 91,
|
"lineNumber": 99,
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2018-09-19T18:13:29.628Z",
|
"updated": "2018-09-19T18:13:29.628Z",
|
||||||
"reasonDetail": "Interacting with already-existing DOM nodes"
|
"reasonDetail": "Interacting with already-existing DOM nodes"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"rule": "jQuery-appendTo(",
|
||||||
|
"path": "js/views/inbox_view.js",
|
||||||
|
"line": " toast.$el.appendTo(this.$el);",
|
||||||
|
"lineNumber": 105,
|
||||||
|
"reasonCategory": "usageTrusted",
|
||||||
|
"updated": "2019-05-10T00:25:51.515Z",
|
||||||
|
"reasonDetail": "Interacting with already-existing DOM nodes"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"rule": "jQuery-$(",
|
"rule": "jQuery-$(",
|
||||||
"path": "js/views/inbox_view.js",
|
"path": "js/views/inbox_view.js",
|
||||||
"line": " this.$('.left-pane-placeholder').append(this.leftPaneView.el);",
|
"line": " this.$('.left-pane-placeholder').append(this.leftPaneView.el);",
|
||||||
"lineNumber": 111,
|
"lineNumber": 125,
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2019-03-08T23:49:08.796Z",
|
"updated": "2019-03-08T23:49:08.796Z",
|
||||||
"reasonDetail": "Protected from arbitrary input"
|
"reasonDetail": "Protected from arbitrary input"
|
||||||
|
@ -573,7 +550,7 @@
|
||||||
"rule": "jQuery-append(",
|
"rule": "jQuery-append(",
|
||||||
"path": "js/views/inbox_view.js",
|
"path": "js/views/inbox_view.js",
|
||||||
"line": " this.$('.left-pane-placeholder').append(this.leftPaneView.el);",
|
"line": " this.$('.left-pane-placeholder').append(this.leftPaneView.el);",
|
||||||
"lineNumber": 111,
|
"lineNumber": 125,
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2019-03-08T23:49:08.796Z",
|
"updated": "2019-03-08T23:49:08.796Z",
|
||||||
"reasonDetail": "Protected from arbitrary input"
|
"reasonDetail": "Protected from arbitrary input"
|
||||||
|
@ -582,7 +559,7 @@
|
||||||
"rule": "jQuery-$(",
|
"rule": "jQuery-$(",
|
||||||
"path": "js/views/inbox_view.js",
|
"path": "js/views/inbox_view.js",
|
||||||
"line": " if (e && this.$(e.target).closest('.placeholder').length) {",
|
"line": " if (e && this.$(e.target).closest('.placeholder').length) {",
|
||||||
"lineNumber": 152,
|
"lineNumber": 166,
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2019-03-08T23:49:08.796Z",
|
"updated": "2019-03-08T23:49:08.796Z",
|
||||||
"reasonDetail": "Protected from arbitrary input"
|
"reasonDetail": "Protected from arbitrary input"
|
||||||
|
@ -591,7 +568,7 @@
|
||||||
"rule": "jQuery-$(",
|
"rule": "jQuery-$(",
|
||||||
"path": "js/views/inbox_view.js",
|
"path": "js/views/inbox_view.js",
|
||||||
"line": " this.$('#header, .gutter').addClass('inactive');",
|
"line": " this.$('#header, .gutter').addClass('inactive');",
|
||||||
"lineNumber": 156,
|
"lineNumber": 170,
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2019-03-08T23:49:08.796Z",
|
"updated": "2019-03-08T23:49:08.796Z",
|
||||||
"reasonDetail": "Protected from arbitrary input"
|
"reasonDetail": "Protected from arbitrary input"
|
||||||
|
@ -600,7 +577,7 @@
|
||||||
"rule": "jQuery-$(",
|
"rule": "jQuery-$(",
|
||||||
"path": "js/views/inbox_view.js",
|
"path": "js/views/inbox_view.js",
|
||||||
"line": " this.$('.conversation-stack').addClass('inactive');",
|
"line": " this.$('.conversation-stack').addClass('inactive');",
|
||||||
"lineNumber": 160,
|
"lineNumber": 174,
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2019-03-08T23:49:08.796Z",
|
"updated": "2019-03-08T23:49:08.796Z",
|
||||||
"reasonDetail": "Protected from arbitrary input"
|
"reasonDetail": "Protected from arbitrary input"
|
||||||
|
@ -609,7 +586,7 @@
|
||||||
"rule": "jQuery-$(",
|
"rule": "jQuery-$(",
|
||||||
"path": "js/views/inbox_view.js",
|
"path": "js/views/inbox_view.js",
|
||||||
"line": " this.$('.conversation:first .menu').trigger('close');",
|
"line": " this.$('.conversation:first .menu').trigger('close');",
|
||||||
"lineNumber": 162,
|
"lineNumber": 176,
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2019-03-08T23:49:08.796Z",
|
"updated": "2019-03-08T23:49:08.796Z",
|
||||||
"reasonDetail": "Protected from arbitrary input"
|
"reasonDetail": "Protected from arbitrary input"
|
||||||
|
@ -618,7 +595,7 @@
|
||||||
"rule": "jQuery-$(",
|
"rule": "jQuery-$(",
|
||||||
"path": "js/views/inbox_view.js",
|
"path": "js/views/inbox_view.js",
|
||||||
"line": " if (e && this.$(e.target).closest('.capture-audio').length > 0) {",
|
"line": " if (e && this.$(e.target).closest('.capture-audio').length > 0) {",
|
||||||
"lineNumber": 182,
|
"lineNumber": 196,
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2019-03-08T23:49:08.796Z",
|
"updated": "2019-03-08T23:49:08.796Z",
|
||||||
"reasonDetail": "Protected from arbitrary input"
|
"reasonDetail": "Protected from arbitrary input"
|
||||||
|
@ -627,7 +604,7 @@
|
||||||
"rule": "jQuery-$(",
|
"rule": "jQuery-$(",
|
||||||
"path": "js/views/inbox_view.js",
|
"path": "js/views/inbox_view.js",
|
||||||
"line": " this.$('.conversation:first .recorder').trigger('close');",
|
"line": " this.$('.conversation:first .recorder').trigger('close');",
|
||||||
"lineNumber": 185,
|
"lineNumber": 199,
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2019-03-08T23:49:08.796Z",
|
"updated": "2019-03-08T23:49:08.796Z",
|
||||||
"reasonDetail": "Protected from arbitrary input"
|
"reasonDetail": "Protected from arbitrary input"
|
||||||
|
@ -887,7 +864,7 @@
|
||||||
"rule": "jQuery-append(",
|
"rule": "jQuery-append(",
|
||||||
"path": "js/views/message_view.js",
|
"path": "js/views/message_view.js",
|
||||||
"line": " this.$el.append(this.childView.el);",
|
"line": " this.$el.append(this.childView.el);",
|
||||||
"lineNumber": 123,
|
"lineNumber": 139,
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2018-09-19T18:13:29.628Z",
|
"updated": "2018-09-19T18:13:29.628Z",
|
||||||
"reasonDetail": "Interacting with already-existing DOM nodes"
|
"reasonDetail": "Interacting with already-existing DOM nodes"
|
||||||
|
@ -1335,7 +1312,7 @@
|
||||||
"rule": "jQuery-wrap(",
|
"rule": "jQuery-wrap(",
|
||||||
"path": "libtextsecure/crypto.js",
|
"path": "libtextsecure/crypto.js",
|
||||||
"line": " const data = dcodeIO.ByteBuffer.wrap(",
|
"line": " const data = dcodeIO.ByteBuffer.wrap(",
|
||||||
"lineNumber": 205,
|
"lineNumber": 206,
|
||||||
"reasonCategory": "falseMatch",
|
"reasonCategory": "falseMatch",
|
||||||
"updated": "2018-09-19T18:13:29.628Z"
|
"updated": "2018-09-19T18:13:29.628Z"
|
||||||
},
|
},
|
||||||
|
@ -1343,7 +1320,7 @@
|
||||||
"rule": "jQuery-wrap(",
|
"rule": "jQuery-wrap(",
|
||||||
"path": "libtextsecure/crypto.js",
|
"path": "libtextsecure/crypto.js",
|
||||||
"line": " return dcodeIO.ByteBuffer.wrap(padded)",
|
"line": " return dcodeIO.ByteBuffer.wrap(padded)",
|
||||||
"lineNumber": 219,
|
"lineNumber": 220,
|
||||||
"reasonCategory": "falseMatch",
|
"reasonCategory": "falseMatch",
|
||||||
"updated": "2018-09-19T18:13:29.628Z"
|
"updated": "2018-09-19T18:13:29.628Z"
|
||||||
},
|
},
|
||||||
|
@ -1379,6 +1356,22 @@
|
||||||
"reasonCategory": "falseMatch",
|
"reasonCategory": "falseMatch",
|
||||||
"updated": "2018-09-19T18:13:29.628Z"
|
"updated": "2018-09-19T18:13:29.628Z"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"rule": "jQuery-wrap(",
|
||||||
|
"path": "libtextsecure/sendmessage.js",
|
||||||
|
"line": " return dcodeIO.ByteBuffer.wrap(string, 'hex').toArrayBuffer();",
|
||||||
|
"lineNumber": 17,
|
||||||
|
"reasonCategory": "falseMatch",
|
||||||
|
"updated": "2019-04-26T17:48:30.675Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule": "jQuery-wrap(",
|
||||||
|
"path": "libtextsecure/sendmessage.js",
|
||||||
|
"line": " return dcodeIO.ByteBuffer.wrap(string, 'base64').toArrayBuffer();",
|
||||||
|
"lineNumber": 20,
|
||||||
|
"reasonCategory": "falseMatch",
|
||||||
|
"updated": "2019-04-26T17:48:30.675Z"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"rule": "jQuery-wrap(",
|
"rule": "jQuery-wrap(",
|
||||||
"path": "libtextsecure/sync_request.js",
|
"path": "libtextsecure/sync_request.js",
|
||||||
|
@ -1825,7 +1818,7 @@
|
||||||
"lineNumber": 31,
|
"lineNumber": 31,
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2019-03-22T19:15:12.445Z",
|
"updated": "2019-03-22T19:15:12.445Z",
|
||||||
"reasonDetail": "<optional>"
|
"reasonDetail": "It's usage of bluebird that's the problem, not bluebird itself with this rule"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"rule": "thenify-multiArgs",
|
"rule": "thenify-multiArgs",
|
||||||
|
@ -1897,7 +1890,7 @@
|
||||||
"lineNumber": 3519,
|
"lineNumber": 3519,
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2019-03-22T19:15:12.445Z",
|
"updated": "2019-03-22T19:15:12.445Z",
|
||||||
"reasonDetail": "<optional>"
|
"reasonDetail": "Usage of bluebird is the problem with this rule, not bluebird itself"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"rule": "thenify-multiArgs",
|
"rule": "thenify-multiArgs",
|
||||||
|
@ -2510,142 +2503,104 @@
|
||||||
"rule": "jQuery-$(",
|
"rule": "jQuery-$(",
|
||||||
"path": "node_modules/core-js/build/index.js",
|
"path": "node_modules/core-js/build/index.js",
|
||||||
"line": " if (name.indexOf(ns + \".\") === 0 && !in$(name, experimental)) {",
|
"line": " if (name.indexOf(ns + \".\") === 0 && !in$(name, experimental)) {",
|
||||||
"lineNumber": 36,
|
"lineNumber": 43,
|
||||||
"reasonCategory": "falseMatch",
|
"reasonCategory": "falseMatch",
|
||||||
"updated": "2018-09-19T21:59:32.770Z"
|
"updated": "2019-04-26T19:18:14.550Z"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"rule": "jQuery-$(",
|
"rule": "jQuery-$(",
|
||||||
"path": "node_modules/core-js/build/index.js",
|
"path": "node_modules/core-js/build/index.js",
|
||||||
"line": " function in$(x, xs){",
|
"line": " function in$(x, xs){",
|
||||||
"lineNumber": 99,
|
"lineNumber": 93,
|
||||||
"reasonCategory": "falseMatch",
|
"reasonCategory": "falseMatch",
|
||||||
"updated": "2018-09-19T21:59:32.770Z"
|
"updated": "2019-04-26T19:18:14.550Z"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"rule": "jQuery-wrap(",
|
"rule": "jQuery-wrap(",
|
||||||
"path": "node_modules/core-js/client/core.js",
|
"path": "node_modules/core-js/client/core.js",
|
||||||
"line": "\t return wrap(tag);",
|
"line": "\t return wrap(uid(arguments.length > 0 ? arguments[0] : undefined));",
|
||||||
"lineNumber": 391,
|
"lineNumber": 1082,
|
||||||
"reasonCategory": "falseMatch",
|
"reasonCategory": "falseMatch"
|
||||||
"updated": "2018-09-19T18:13:29.628Z"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"rule": "jQuery-wrap(",
|
"rule": "jQuery-wrap(",
|
||||||
"path": "node_modules/core-js/client/core.js",
|
"path": "node_modules/core-js/client/core.js",
|
||||||
"line": "\t return wrap(wks(name));",
|
"line": "\t symbolStatics[it] = useNative ? sym : wrap(sym);",
|
||||||
"lineNumber": 408,
|
"lineNumber": 1135,
|
||||||
"reasonCategory": "falseMatch",
|
"reasonCategory": "falseMatch",
|
||||||
"updated": "2018-09-19T18:13:29.628Z"
|
"updated": "2019-04-26T19:18:14.550Z"
|
||||||
},
|
|
||||||
{
|
|
||||||
"rule": "jQuery-$(",
|
|
||||||
"path": "node_modules/core-js/client/core.js",
|
|
||||||
"line": "\t if(!NPCG)separator2 = new RegExp('^' + separatorCopy.source + '$(?!\\\\s)', flags);",
|
|
||||||
"lineNumber": 3893,
|
|
||||||
"reasonCategory": "falseMatch",
|
|
||||||
"updated": "2018-09-19T21:59:32.770Z"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"rule": "jQuery-wrap(",
|
"rule": "jQuery-wrap(",
|
||||||
"path": "node_modules/core-js/client/core.js",
|
"path": "node_modules/core-js/client/core.js",
|
||||||
"line": "\t setTimeout: wrap(global.setTimeout),",
|
"line": "\t setTimeout: wrap(global.setTimeout),",
|
||||||
"lineNumber": 7226,
|
"lineNumber": 4496,
|
||||||
"reasonCategory": "falseMatch",
|
"reasonCategory": "falseMatch",
|
||||||
"updated": "2018-09-19T18:13:29.628Z"
|
"updated": "2019-04-26T19:18:14.550Z"
|
||||||
},
|
|
||||||
{
|
|
||||||
"rule": "jQuery-$(",
|
|
||||||
"path": "node_modules/core-js/client/core.min.js",
|
|
||||||
"lineNumber": 8,
|
|
||||||
"reasonCategory": "falseMatch",
|
|
||||||
"updated": "2018-09-19T21:59:32.770Z"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"rule": "jQuery-wrap(",
|
"rule": "jQuery-wrap(",
|
||||||
"path": "node_modules/core-js/client/library.js",
|
"path": "node_modules/core-js/client/library.js",
|
||||||
"line": "\t return wrap(tag);",
|
"line": "\t return wrap(uid(arguments.length > 0 ? arguments[0] : undefined));",
|
||||||
"lineNumber": 379,
|
"lineNumber": 1033,
|
||||||
"reasonCategory": "falseMatch",
|
"reasonCategory": "falseMatch",
|
||||||
"updated": "2018-09-19T18:13:29.628Z"
|
"updated": "2019-04-26T19:18:14.550Z"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"rule": "jQuery-wrap(",
|
"rule": "jQuery-wrap(",
|
||||||
"path": "node_modules/core-js/client/library.js",
|
"path": "node_modules/core-js/client/library.js",
|
||||||
"line": "\t return wrap(wks(name));",
|
"line": "\t symbolStatics[it] = useNative ? sym : wrap(sym);",
|
||||||
"lineNumber": 396,
|
"lineNumber": 1086,
|
||||||
"reasonCategory": "falseMatch",
|
"reasonCategory": "falseMatch",
|
||||||
"updated": "2018-09-19T18:13:29.628Z"
|
"updated": "2019-04-26T19:18:14.550Z"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"rule": "jQuery-wrap(",
|
"rule": "jQuery-wrap(",
|
||||||
"path": "node_modules/core-js/client/library.js",
|
"path": "node_modules/core-js/client/library.js",
|
||||||
"line": "\t setTimeout: wrap(global.setTimeout),",
|
"line": "\t setTimeout: wrap(global.setTimeout),",
|
||||||
"lineNumber": 6749,
|
"lineNumber": 4136,
|
||||||
"reasonCategory": "falseMatch",
|
"reasonCategory": "falseMatch",
|
||||||
"updated": "2018-09-19T18:13:29.628Z"
|
"updated": "2019-04-26T19:18:14.550Z"
|
||||||
},
|
|
||||||
{
|
|
||||||
"rule": "jQuery-$(",
|
|
||||||
"path": "node_modules/core-js/client/library.min.js",
|
|
||||||
"lineNumber": 8,
|
|
||||||
"reasonCategory": "falseMatch",
|
|
||||||
"updated": "2018-09-19T21:59:32.770Z"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"rule": "jQuery-wrap(",
|
"rule": "jQuery-wrap(",
|
||||||
"path": "node_modules/core-js/client/shim.js",
|
"path": "node_modules/core-js/client/shim.js",
|
||||||
"line": "\t return wrap(tag);",
|
"line": "\t return wrap(uid(arguments.length > 0 ? arguments[0] : undefined));",
|
||||||
"lineNumber": 377,
|
"lineNumber": 1068,
|
||||||
"reasonCategory": "falseMatch",
|
"reasonCategory": "falseMatch",
|
||||||
"updated": "2018-09-19T18:13:29.628Z"
|
"updated": "2019-04-26T19:18:14.550Z"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"rule": "jQuery-wrap(",
|
"rule": "jQuery-wrap(",
|
||||||
"path": "node_modules/core-js/client/shim.js",
|
"path": "node_modules/core-js/client/shim.js",
|
||||||
"line": "\t return wrap(wks(name));",
|
"line": "\t symbolStatics[it] = useNative ? sym : wrap(sym);",
|
||||||
"lineNumber": 394,
|
"lineNumber": 1121,
|
||||||
"reasonCategory": "falseMatch",
|
"reasonCategory": "falseMatch",
|
||||||
"updated": "2018-09-19T18:13:29.628Z"
|
"updated": "2019-04-26T19:18:14.550Z"
|
||||||
},
|
|
||||||
{
|
|
||||||
"rule": "jQuery-$(",
|
|
||||||
"path": "node_modules/core-js/client/shim.js",
|
|
||||||
"line": "\t if(!NPCG)separator2 = new RegExp('^' + separatorCopy.source + '$(?!\\\\s)', flags);",
|
|
||||||
"lineNumber": 3879,
|
|
||||||
"reasonCategory": "falseMatch",
|
|
||||||
"updated": "2018-09-19T21:59:32.770Z"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"rule": "jQuery-wrap(",
|
"rule": "jQuery-wrap(",
|
||||||
"path": "node_modules/core-js/client/shim.js",
|
"path": "node_modules/core-js/client/shim.js",
|
||||||
"line": "\t setTimeout: wrap(global.setTimeout),",
|
"line": "\t setTimeout: wrap(global.setTimeout),",
|
||||||
"lineNumber": 7212,
|
"lineNumber": 4482,
|
||||||
"reasonCategory": "falseMatch",
|
"reasonCategory": "falseMatch",
|
||||||
"updated": "2018-09-19T18:13:29.628Z"
|
"updated": "2019-04-26T19:18:14.550Z"
|
||||||
},
|
|
||||||
{
|
|
||||||
"rule": "jQuery-$(",
|
|
||||||
"path": "node_modules/core-js/client/shim.min.js",
|
|
||||||
"lineNumber": 8,
|
|
||||||
"reasonCategory": "falseMatch",
|
|
||||||
"updated": "2018-09-19T21:59:32.770Z"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"rule": "jQuery-wrap(",
|
"rule": "jQuery-wrap(",
|
||||||
"path": "node_modules/core-js/library/modules/es6.symbol.js",
|
"path": "node_modules/core-js/library/modules/es6.symbol.js",
|
||||||
"line": " return wrap(tag);",
|
"line": " return wrap(uid(arguments.length > 0 ? arguments[0] : undefined));",
|
||||||
"lineNumber": 142,
|
"lineNumber": 142,
|
||||||
"reasonCategory": "falseMatch",
|
"reasonCategory": "falseMatch",
|
||||||
"updated": "2018-09-19T18:13:29.628Z"
|
"updated": "2019-04-26T19:18:14.550Z"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"rule": "jQuery-wrap(",
|
"rule": "jQuery-wrap(",
|
||||||
"path": "node_modules/core-js/library/modules/es6.symbol.js",
|
"path": "node_modules/core-js/library/modules/es6.symbol.js",
|
||||||
"line": " return wrap(wks(name));",
|
"line": " symbolStatics[it] = useNative ? sym : wrap(sym);",
|
||||||
"lineNumber": 159,
|
"lineNumber": 195,
|
||||||
"reasonCategory": "falseMatch",
|
"reasonCategory": "falseMatch",
|
||||||
"updated": "2018-09-19T18:13:29.628Z"
|
"updated": "2019-04-26T19:18:14.550Z"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"rule": "jQuery-wrap(",
|
"rule": "jQuery-wrap(",
|
||||||
|
@ -2653,31 +2608,23 @@
|
||||||
"line": " setTimeout: wrap(global.setTimeout),",
|
"line": " setTimeout: wrap(global.setTimeout),",
|
||||||
"lineNumber": 18,
|
"lineNumber": 18,
|
||||||
"reasonCategory": "falseMatch",
|
"reasonCategory": "falseMatch",
|
||||||
"updated": "2018-09-19T18:13:29.628Z"
|
"updated": "2019-04-26T19:26:59.689Z"
|
||||||
},
|
|
||||||
{
|
|
||||||
"rule": "jQuery-$(",
|
|
||||||
"path": "node_modules/core-js/modules/es6.regexp.split.js",
|
|
||||||
"line": " if(!NPCG)separator2 = new RegExp('^' + separatorCopy.source + '$(?!\\\\s)', flags);",
|
|
||||||
"lineNumber": 36,
|
|
||||||
"reasonCategory": "falseMatch",
|
|
||||||
"updated": "2018-09-19T21:59:32.770Z"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"rule": "jQuery-wrap(",
|
"rule": "jQuery-wrap(",
|
||||||
"path": "node_modules/core-js/modules/es6.symbol.js",
|
"path": "node_modules/core-js/modules/es6.symbol.js",
|
||||||
"line": " return wrap(tag);",
|
"line": " return wrap(uid(arguments.length > 0 ? arguments[0] : undefined));",
|
||||||
"lineNumber": 142,
|
"lineNumber": 142,
|
||||||
"reasonCategory": "falseMatch",
|
"reasonCategory": "falseMatch",
|
||||||
"updated": "2018-09-19T18:13:29.628Z"
|
"updated": "2019-04-26T19:18:14.550Z"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"rule": "jQuery-wrap(",
|
"rule": "jQuery-wrap(",
|
||||||
"path": "node_modules/core-js/modules/es6.symbol.js",
|
"path": "node_modules/core-js/modules/es6.symbol.js",
|
||||||
"line": " return wrap(wks(name));",
|
"line": " symbolStatics[it] = useNative ? sym : wrap(sym);",
|
||||||
"lineNumber": 159,
|
"lineNumber": 195,
|
||||||
"reasonCategory": "falseMatch",
|
"reasonCategory": "falseMatch",
|
||||||
"updated": "2018-09-19T18:13:29.628Z"
|
"updated": "2019-04-26T19:18:14.550Z"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"rule": "jQuery-wrap(",
|
"rule": "jQuery-wrap(",
|
||||||
|
@ -2685,7 +2632,78 @@
|
||||||
"line": " setTimeout: wrap(global.setTimeout),",
|
"line": " setTimeout: wrap(global.setTimeout),",
|
||||||
"lineNumber": 18,
|
"lineNumber": 18,
|
||||||
"reasonCategory": "falseMatch",
|
"reasonCategory": "falseMatch",
|
||||||
"updated": "2018-09-19T18:13:29.628Z"
|
"updated": "2019-04-26T19:26:59.689Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule": "fbjs-createNodesFromMarkup",
|
||||||
|
"path": "node_modules/create-react-context/node_modules/fbjs/lib/createNodesFromMarkup.js",
|
||||||
|
"line": "function createNodesFromMarkup(markup, handleScript) {",
|
||||||
|
"lineNumber": 51,
|
||||||
|
"reasonCategory": "falseMatch",
|
||||||
|
"updated": "2019-04-26T19:18:14.550Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule": "fbjs-createNodesFromMarkup",
|
||||||
|
"path": "node_modules/create-react-context/node_modules/fbjs/lib/createNodesFromMarkup.js",
|
||||||
|
"line": " !!!dummyNode ? process.env.NODE_ENV !== 'production' ? invariant(false, 'createNodesFromMarkup dummy not initialized') : invariant(false) : void 0;",
|
||||||
|
"lineNumber": 53,
|
||||||
|
"reasonCategory": "falseMatch",
|
||||||
|
"updated": "2019-04-26T19:18:14.550Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule": "DOM-innerHTML",
|
||||||
|
"path": "node_modules/create-react-context/node_modules/fbjs/lib/createNodesFromMarkup.js",
|
||||||
|
"line": " node.innerHTML = wrap[1] + markup + wrap[2];",
|
||||||
|
"lineNumber": 58,
|
||||||
|
"reasonCategory": "falseMatch",
|
||||||
|
"updated": "2019-04-26T19:18:14.550Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule": "DOM-innerHTML",
|
||||||
|
"path": "node_modules/create-react-context/node_modules/fbjs/lib/createNodesFromMarkup.js",
|
||||||
|
"line": " node.innerHTML = markup;",
|
||||||
|
"lineNumber": 65,
|
||||||
|
"reasonCategory": "falseMatch",
|
||||||
|
"updated": "2019-04-26T19:18:14.550Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule": "fbjs-createNodesFromMarkup",
|
||||||
|
"path": "node_modules/create-react-context/node_modules/fbjs/lib/createNodesFromMarkup.js",
|
||||||
|
"line": " !handleScript ? process.env.NODE_ENV !== 'production' ? invariant(false, 'createNodesFromMarkup(...): Unexpected <script> element rendered.') : invariant(false) : void 0;",
|
||||||
|
"lineNumber": 70,
|
||||||
|
"reasonCategory": "falseMatch",
|
||||||
|
"updated": "2019-04-26T19:18:14.550Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule": "fbjs-createNodesFromMarkup",
|
||||||
|
"path": "node_modules/create-react-context/node_modules/fbjs/lib/createNodesFromMarkup.js",
|
||||||
|
"line": "module.exports = createNodesFromMarkup;",
|
||||||
|
"lineNumber": 81,
|
||||||
|
"reasonCategory": "falseMatch",
|
||||||
|
"updated": "2019-04-26T19:18:14.550Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule": "DOM-innerHTML",
|
||||||
|
"path": "node_modules/create-react-context/node_modules/fbjs/lib/getMarkupWrap.js",
|
||||||
|
"line": " * Some browsers cannot use `innerHTML` to render certain elements standalone,",
|
||||||
|
"lineNumber": 23,
|
||||||
|
"reasonCategory": "falseMatch",
|
||||||
|
"updated": "2019-04-26T19:18:14.550Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule": "DOM-innerHTML",
|
||||||
|
"path": "node_modules/create-react-context/node_modules/fbjs/lib/getMarkupWrap.js",
|
||||||
|
"line": " dummyNode.innerHTML = '<link />';",
|
||||||
|
"lineNumber": 83,
|
||||||
|
"reasonCategory": "falseMatch"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule": "DOM-innerHTML",
|
||||||
|
"path": "node_modules/create-react-context/node_modules/fbjs/lib/getMarkupWrap.js",
|
||||||
|
"line": " dummyNode.innerHTML = '<' + nodeName + '></' + nodeName + '>';",
|
||||||
|
"lineNumber": 85,
|
||||||
|
"reasonCategory": "falseMatch",
|
||||||
|
"updated": "2019-04-26T19:18:14.550Z"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"rule": "jQuery-prepend(",
|
"rule": "jQuery-prepend(",
|
||||||
|
@ -3169,143 +3187,6 @@
|
||||||
"updated": "2018-09-18T19:19:27.699Z",
|
"updated": "2018-09-18T19:19:27.699Z",
|
||||||
"reasonDetail": "nodeName is limited to set of safe tag names."
|
"reasonDetail": "nodeName is limited to set of safe tag names."
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"rule": "jQuery-$(",
|
|
||||||
"path": "node_modules/fbjs/node_modules/core-js/build/index.js",
|
|
||||||
"line": " if (name.indexOf(ns + \".\") === 0 && !in$(name, experimental)) {",
|
|
||||||
"lineNumber": 43,
|
|
||||||
"reasonCategory": "falseMatch",
|
|
||||||
"updated": "2018-09-19T21:59:32.770Z"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"rule": "jQuery-$(",
|
|
||||||
"path": "node_modules/fbjs/node_modules/core-js/build/index.js",
|
|
||||||
"line": " function in$(x, xs){",
|
|
||||||
"lineNumber": 93,
|
|
||||||
"reasonCategory": "falseMatch",
|
|
||||||
"updated": "2018-09-19T21:59:32.770Z"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"rule": "jQuery-wrap(",
|
|
||||||
"path": "node_modules/fbjs/node_modules/core-js/client/core.js",
|
|
||||||
"line": "\t return wrap(uid(arguments.length > 0 ? arguments[0] : undefined));",
|
|
||||||
"lineNumber": 1082,
|
|
||||||
"reasonCategory": "falseMatch",
|
|
||||||
"updated": "2018-09-19T18:13:29.628Z"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"rule": "jQuery-wrap(",
|
|
||||||
"path": "node_modules/fbjs/node_modules/core-js/client/core.js",
|
|
||||||
"line": "\t symbolStatics[it] = useNative ? sym : wrap(sym);",
|
|
||||||
"lineNumber": 1135,
|
|
||||||
"reasonCategory": "falseMatch",
|
|
||||||
"updated": "2018-09-19T18:13:29.628Z"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"rule": "jQuery-wrap(",
|
|
||||||
"path": "node_modules/fbjs/node_modules/core-js/client/core.js",
|
|
||||||
"line": "\t setTimeout: wrap(global.setTimeout),",
|
|
||||||
"lineNumber": 4496,
|
|
||||||
"reasonCategory": "falseMatch",
|
|
||||||
"updated": "2018-09-19T18:13:29.628Z"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"rule": "jQuery-wrap(",
|
|
||||||
"path": "node_modules/fbjs/node_modules/core-js/client/library.js",
|
|
||||||
"line": "\t return wrap(uid(arguments.length > 0 ? arguments[0] : undefined));",
|
|
||||||
"lineNumber": 1033,
|
|
||||||
"reasonCategory": "falseMatch",
|
|
||||||
"updated": "2018-09-19T18:13:29.628Z"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"rule": "jQuery-wrap(",
|
|
||||||
"path": "node_modules/fbjs/node_modules/core-js/client/library.js",
|
|
||||||
"line": "\t symbolStatics[it] = useNative ? sym : wrap(sym);",
|
|
||||||
"lineNumber": 1086,
|
|
||||||
"reasonCategory": "falseMatch",
|
|
||||||
"updated": "2018-09-19T18:13:29.628Z"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"rule": "jQuery-wrap(",
|
|
||||||
"path": "node_modules/fbjs/node_modules/core-js/client/library.js",
|
|
||||||
"line": "\t setTimeout: wrap(global.setTimeout),",
|
|
||||||
"lineNumber": 4136,
|
|
||||||
"reasonCategory": "falseMatch",
|
|
||||||
"updated": "2018-09-19T18:13:29.628Z",
|
|
||||||
"reasonDetail": "Protected from arbitrary input"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"rule": "jQuery-wrap(",
|
|
||||||
"path": "node_modules/fbjs/node_modules/core-js/client/shim.js",
|
|
||||||
"line": "\t return wrap(uid(arguments.length > 0 ? arguments[0] : undefined));",
|
|
||||||
"lineNumber": 1068,
|
|
||||||
"reasonCategory": "falseMatch",
|
|
||||||
"updated": "2018-09-19T18:13:29.628Z"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"rule": "jQuery-wrap(",
|
|
||||||
"path": "node_modules/fbjs/node_modules/core-js/client/shim.js",
|
|
||||||
"line": "\t symbolStatics[it] = useNative ? sym : wrap(sym);",
|
|
||||||
"lineNumber": 1121,
|
|
||||||
"reasonCategory": "falseMatch",
|
|
||||||
"updated": "2018-09-19T18:13:29.628Z"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"rule": "jQuery-wrap(",
|
|
||||||
"path": "node_modules/fbjs/node_modules/core-js/client/shim.js",
|
|
||||||
"line": "\t setTimeout: wrap(global.setTimeout),",
|
|
||||||
"lineNumber": 4482,
|
|
||||||
"reasonCategory": "falseMatch",
|
|
||||||
"updated": "2018-09-19T18:13:29.628Z"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"rule": "jQuery-wrap(",
|
|
||||||
"path": "node_modules/fbjs/node_modules/core-js/library/modules/es6.symbol.js",
|
|
||||||
"line": " return wrap(uid(arguments.length > 0 ? arguments[0] : undefined));",
|
|
||||||
"lineNumber": 142,
|
|
||||||
"reasonCategory": "falseMatch",
|
|
||||||
"updated": "2018-09-19T18:13:29.628Z"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"rule": "jQuery-wrap(",
|
|
||||||
"path": "node_modules/fbjs/node_modules/core-js/library/modules/es6.symbol.js",
|
|
||||||
"line": " symbolStatics[it] = useNative ? sym : wrap(sym);",
|
|
||||||
"lineNumber": 195,
|
|
||||||
"reasonCategory": "falseMatch",
|
|
||||||
"updated": "2018-09-19T18:13:29.628Z"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"rule": "jQuery-wrap(",
|
|
||||||
"path": "node_modules/fbjs/node_modules/core-js/library/modules/web.timers.js",
|
|
||||||
"line": " setTimeout: wrap(global.setTimeout),",
|
|
||||||
"lineNumber": 18,
|
|
||||||
"reasonCategory": "falseMatch",
|
|
||||||
"updated": "2018-09-19T18:13:29.628Z"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"rule": "jQuery-wrap(",
|
|
||||||
"path": "node_modules/fbjs/node_modules/core-js/modules/es6.symbol.js",
|
|
||||||
"line": " return wrap(uid(arguments.length > 0 ? arguments[0] : undefined));",
|
|
||||||
"lineNumber": 142,
|
|
||||||
"reasonCategory": "falseMatch",
|
|
||||||
"updated": "2018-09-19T18:13:29.628Z"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"rule": "jQuery-wrap(",
|
|
||||||
"path": "node_modules/fbjs/node_modules/core-js/modules/es6.symbol.js",
|
|
||||||
"line": " symbolStatics[it] = useNative ? sym : wrap(sym);",
|
|
||||||
"lineNumber": 195,
|
|
||||||
"reasonCategory": "falseMatch",
|
|
||||||
"updated": "2018-09-19T18:13:29.628Z"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"rule": "jQuery-wrap(",
|
|
||||||
"path": "node_modules/fbjs/node_modules/core-js/modules/web.timers.js",
|
|
||||||
"line": " setTimeout: wrap(global.setTimeout),",
|
|
||||||
"lineNumber": 18,
|
|
||||||
"reasonCategory": "falseMatch",
|
|
||||||
"updated": "2018-09-19T18:13:29.628Z"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"rule": "jQuery-load(",
|
"rule": "jQuery-load(",
|
||||||
"path": "node_modules/file-entry-cache/cache.js",
|
"path": "node_modules/file-entry-cache/cache.js",
|
||||||
|
@ -5123,6 +5004,27 @@
|
||||||
"reasonCategory": "falseMatch",
|
"reasonCategory": "falseMatch",
|
||||||
"updated": "2018-09-19T21:59:32.770Z"
|
"updated": "2018-09-19T21:59:32.770Z"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"rule": "jQuery-$(",
|
||||||
|
"path": "node_modules/popper.js/dist/esm/popper.min.js",
|
||||||
|
"lineNumber": 4,
|
||||||
|
"reasonCategory": "falseMatch",
|
||||||
|
"updated": "2019-04-26T19:18:14.550Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule": "jQuery-$(",
|
||||||
|
"path": "node_modules/popper.js/dist/popper.min.js",
|
||||||
|
"lineNumber": 4,
|
||||||
|
"reasonCategory": "falseMatch",
|
||||||
|
"updated": "2019-04-26T19:18:14.550Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule": "jQuery-$(",
|
||||||
|
"path": "node_modules/popper.js/dist/umd/popper.min.js",
|
||||||
|
"lineNumber": 4,
|
||||||
|
"reasonCategory": "falseMatch",
|
||||||
|
"updated": "2019-04-26T19:18:14.550Z"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"rule": "jQuery-append(",
|
"rule": "jQuery-append(",
|
||||||
"path": "node_modules/progress-stream/node_modules/through2/test.js",
|
"path": "node_modules/progress-stream/node_modules/through2/test.js",
|
||||||
|
@ -6250,5 +6152,21 @@
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2019-04-17T18:44:33.207Z",
|
"updated": "2019-04-17T18:44:33.207Z",
|
||||||
"reasonDetail": "Necessary to interact with child react-virtualized/List"
|
"reasonDetail": "Necessary to interact with child react-virtualized/List"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule": "jQuery-wrap(",
|
||||||
|
"path": "ts/shims/textsecure.js",
|
||||||
|
"line": " wrap(textsecure.messaging.sendStickerPackSync([",
|
||||||
|
"lineNumber": 11,
|
||||||
|
"reasonCategory": "falseMatch",
|
||||||
|
"updated": "2019-05-02T20:44:56.470Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule": "jQuery-wrap(",
|
||||||
|
"path": "ts/shims/textsecure.ts",
|
||||||
|
"line": " wrap(",
|
||||||
|
"lineNumber": 60,
|
||||||
|
"reasonCategory": "falseMatch",
|
||||||
|
"updated": "2019-05-02T20:44:56.470Z"
|
||||||
}
|
}
|
||||||
]
|
]
|
|
@ -51,6 +51,7 @@ const results: Array<ExceptionType> = [];
|
||||||
const excludedFiles = [
|
const excludedFiles = [
|
||||||
// High-traffic files in our project
|
// High-traffic files in our project
|
||||||
'^js/models/messages.js',
|
'^js/models/messages.js',
|
||||||
|
'^js/modules/crypto.js',
|
||||||
'^js/views/conversation_view.js',
|
'^js/views/conversation_view.js',
|
||||||
'^js/views/file_input_view.js',
|
'^js/views/file_input_view.js',
|
||||||
'^js/background.js',
|
'^js/background.js',
|
||||||
|
|
|
@ -77,6 +77,9 @@
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|
||||||
|
// Crashing!
|
||||||
|
"use-default-type-parameter": false,
|
||||||
|
|
||||||
// Disabling a large set of Microsoft-recommended rules
|
// Disabling a large set of Microsoft-recommended rules
|
||||||
|
|
||||||
// Modifying:
|
// Modifying:
|
||||||
|
|
79
yarn.lock
|
@ -2015,6 +2015,14 @@ create-hmac@^1.1.0, create-hmac@^1.1.2, create-hmac@^1.1.4:
|
||||||
safe-buffer "^5.0.1"
|
safe-buffer "^5.0.1"
|
||||||
sha.js "^2.4.8"
|
sha.js "^2.4.8"
|
||||||
|
|
||||||
|
create-react-context@<=0.2.2:
|
||||||
|
version "0.2.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/create-react-context/-/create-react-context-0.2.2.tgz#9836542f9aaa22868cd7d4a6f82667df38019dca"
|
||||||
|
integrity sha512-KkpaLARMhsTsgp0d2NA/R94F/eDLbhXERdIq3LvX2biCAXcDvHYoOqHfWCHf1+OLj+HKBotLG3KqaOOf+C1C+A==
|
||||||
|
dependencies:
|
||||||
|
fbjs "^0.8.0"
|
||||||
|
gud "^1.0.0"
|
||||||
|
|
||||||
cross-spawn@5.1.0, cross-spawn@^5.0.1, cross-spawn@^5.1.0:
|
cross-spawn@5.1.0, cross-spawn@^5.0.1, cross-spawn@^5.1.0:
|
||||||
version "5.1.0"
|
version "5.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-5.1.0.tgz#e8bd0efee58fcff6f8f94510a0a554bbfa235449"
|
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-5.1.0.tgz#e8bd0efee58fcff6f8f94510a0a554bbfa235449"
|
||||||
|
@ -2638,7 +2646,7 @@ electron-chromedriver@~3.0.0:
|
||||||
electron-download "^4.1.0"
|
electron-download "^4.1.0"
|
||||||
extract-zip "^1.6.5"
|
extract-zip "^1.6.5"
|
||||||
|
|
||||||
electron-context-menu@^0.11.0:
|
electron-context-menu@0.11.0:
|
||||||
version "0.11.0"
|
version "0.11.0"
|
||||||
resolved "https://registry.yarnpkg.com/electron-context-menu/-/electron-context-menu-0.11.0.tgz#3ecefb0231151add474c9b0df2fb66fde3ad5731"
|
resolved "https://registry.yarnpkg.com/electron-context-menu/-/electron-context-menu-0.11.0.tgz#3ecefb0231151add474c9b0df2fb66fde3ad5731"
|
||||||
integrity sha512-sgDIGqjgazUQ5fbfz0ObRkmODAsw00eylQprp5q4jyuL6dskd27yslhoJTrjLbFGErfVVYzRXPW2rQPJxARKmg==
|
integrity sha512-sgDIGqjgazUQ5fbfz0ObRkmODAsw00eylQprp5q4jyuL6dskd27yslhoJTrjLbFGErfVVYzRXPW2rQPJxARKmg==
|
||||||
|
@ -3109,6 +3117,11 @@ eventemitter3@1.x.x:
|
||||||
version "1.2.0"
|
version "1.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-1.2.0.tgz#1c86991d816ad1e504750e73874224ecf3bec508"
|
resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-1.2.0.tgz#1c86991d816ad1e504750e73874224ecf3bec508"
|
||||||
|
|
||||||
|
eventemitter3@^3.1.0:
|
||||||
|
version "3.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-3.1.0.tgz#090b4d6cdbd645ed10bf750d4b5407942d7ba163"
|
||||||
|
integrity sha512-ivIvhpq/Y0uSjcHDcOIccjmYjGLcP09MFGE7ysAwkAvkXfpZlC985pH2/ui64DKazbTW/4kN3yqozUxlXzI6cA==
|
||||||
|
|
||||||
events@^1.0.0:
|
events@^1.0.0:
|
||||||
version "1.1.1"
|
version "1.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/events/-/events-1.1.1.tgz#9ebdb7635ad099c70dcc4c2a1f5004288e8bd924"
|
resolved "https://registry.yarnpkg.com/events/-/events-1.1.1.tgz#9ebdb7635ad099c70dcc4c2a1f5004288e8bd924"
|
||||||
|
@ -3345,6 +3358,19 @@ faye-websocket@~0.11.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
websocket-driver ">=0.5.1"
|
websocket-driver ">=0.5.1"
|
||||||
|
|
||||||
|
fbjs@^0.8.0:
|
||||||
|
version "0.8.17"
|
||||||
|
resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-0.8.17.tgz#c4d598ead6949112653d6588b01a5cdcd9f90fdd"
|
||||||
|
integrity sha1-xNWY6taUkRJlPWWIsBpc3Nn5D90=
|
||||||
|
dependencies:
|
||||||
|
core-js "^1.0.0"
|
||||||
|
isomorphic-fetch "^2.1.1"
|
||||||
|
loose-envify "^1.0.0"
|
||||||
|
object-assign "^4.1.0"
|
||||||
|
promise "^7.1.1"
|
||||||
|
setimmediate "^1.0.5"
|
||||||
|
ua-parser-js "^0.7.18"
|
||||||
|
|
||||||
fbjs@^0.8.16:
|
fbjs@^0.8.16:
|
||||||
version "0.8.16"
|
version "0.8.16"
|
||||||
resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-0.8.16.tgz#5e67432f550dc41b572bf55847b8aca64e5337db"
|
resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-0.8.16.tgz#5e67432f550dc41b572bf55847b8aca64e5337db"
|
||||||
|
@ -4120,6 +4146,11 @@ grunt@1.0.1:
|
||||||
path-is-absolute "~1.0.0"
|
path-is-absolute "~1.0.0"
|
||||||
rimraf "~2.2.8"
|
rimraf "~2.2.8"
|
||||||
|
|
||||||
|
gud@^1.0.0:
|
||||||
|
version "1.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/gud/-/gud-1.0.0.tgz#a489581b17e6a70beca9abe3ae57de7a499852c0"
|
||||||
|
integrity sha512-zGEOVKFM5sVPPrYs7J5/hYEw2Pof8KCyOwyhG8sAF26mCAeUFAcYPu1mwB7hhpIP29zOIBaDqwuHdLp0jvZXjw==
|
||||||
|
|
||||||
gzip-size@3.0.0:
|
gzip-size@3.0.0:
|
||||||
version "3.0.0"
|
version "3.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/gzip-size/-/gzip-size-3.0.0.tgz#546188e9bdc337f673772f81660464b389dce520"
|
resolved "https://registry.yarnpkg.com/gzip-size/-/gzip-size-3.0.0.tgz#546188e9bdc337f673772f81660464b389dce520"
|
||||||
|
@ -6666,10 +6697,22 @@ p-locate@^3.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
p-limit "^2.0.0"
|
p-limit "^2.0.0"
|
||||||
|
|
||||||
|
p-map@2.1.0:
|
||||||
|
version "2.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/p-map/-/p-map-2.1.0.tgz#310928feef9c9ecc65b68b17693018a665cea175"
|
||||||
|
integrity sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==
|
||||||
|
|
||||||
p-map@^1.1.1:
|
p-map@^1.1.1:
|
||||||
version "1.2.0"
|
version "1.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/p-map/-/p-map-1.2.0.tgz#e4e94f311eabbc8633a1e79908165fca26241b6b"
|
resolved "https://registry.yarnpkg.com/p-map/-/p-map-1.2.0.tgz#e4e94f311eabbc8633a1e79908165fca26241b6b"
|
||||||
|
|
||||||
|
p-queue@5.0.0:
|
||||||
|
version "5.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/p-queue/-/p-queue-5.0.0.tgz#80f1741d5e78a6fa72fce889406481baa5617a3c"
|
||||||
|
integrity sha512-6QfeouDf236N+MAxHch0CVIy8o/KBnmhttKjxZoOkUlzqU+u9rZgEyXH3OdckhTgawbqf5rpzmyR+07+Lv0+zg==
|
||||||
|
dependencies:
|
||||||
|
eventemitter3 "^3.1.0"
|
||||||
|
|
||||||
p-timeout@^2.0.1:
|
p-timeout@^2.0.1:
|
||||||
version "2.0.1"
|
version "2.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/p-timeout/-/p-timeout-2.0.1.tgz#d8dd1979595d2dc0139e1fe46b8b646cb3cdf038"
|
resolved "https://registry.yarnpkg.com/p-timeout/-/p-timeout-2.0.1.tgz#d8dd1979595d2dc0139e1fe46b8b646cb3cdf038"
|
||||||
|
@ -6985,6 +7028,11 @@ pngjs@^3.0.0:
|
||||||
version "3.0.1"
|
version "3.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-3.0.1.tgz#b15086ac1ac47298c8fd3f9cdf364fa9879c4db6"
|
resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-3.0.1.tgz#b15086ac1ac47298c8fd3f9cdf364fa9879c4db6"
|
||||||
|
|
||||||
|
popper.js@^1.14.4:
|
||||||
|
version "1.15.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/popper.js/-/popper.js-1.15.0.tgz#5560b99bbad7647e9faa475c6b8056621f5a4ff2"
|
||||||
|
integrity sha512-w010cY1oCUmI+9KwwlWki+r5jxKfTFDVoadl7MSrIujHU5MJ5OR6HTDj6Xo8aoR/QsA56x8jKjA59qGH4ELtrA==
|
||||||
|
|
||||||
portfinder@^1.0.9:
|
portfinder@^1.0.9:
|
||||||
version "1.0.13"
|
version "1.0.13"
|
||||||
resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.13.tgz#bb32ecd87c27104ae6ee44b5a3ccbf0ebb1aede9"
|
resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.13.tgz#bb32ecd87c27104ae6ee44b5a3ccbf0ebb1aede9"
|
||||||
|
@ -7696,6 +7744,18 @@ react-lifecycles-compat@^3.0.4:
|
||||||
resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362"
|
resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362"
|
||||||
integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==
|
integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==
|
||||||
|
|
||||||
|
react-popper@^1.3.3:
|
||||||
|
version "1.3.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/react-popper/-/react-popper-1.3.3.tgz#2c6cef7515a991256b4f0536cd4bdcb58a7b6af6"
|
||||||
|
integrity sha512-ynMZBPkXONPc5K4P5yFWgZx5JGAUIP3pGGLNs58cfAPgK67olx7fmLp+AdpZ0+GoQ+ieFDa/z4cdV6u7sioH6w==
|
||||||
|
dependencies:
|
||||||
|
"@babel/runtime" "^7.1.2"
|
||||||
|
create-react-context "<=0.2.2"
|
||||||
|
popper.js "^1.14.4"
|
||||||
|
prop-types "^15.6.1"
|
||||||
|
typed-styles "^0.0.7"
|
||||||
|
warning "^4.0.2"
|
||||||
|
|
||||||
react-redux@6.0.1:
|
react-redux@6.0.1:
|
||||||
version "6.0.1"
|
version "6.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-6.0.1.tgz#0d423e2c1cb10ada87293d47e7de7c329623ba4d"
|
resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-6.0.1.tgz#0d423e2c1cb10ada87293d47e7de7c329623ba4d"
|
||||||
|
@ -9587,6 +9647,11 @@ type-is@~1.6.15, type-is@~1.6.16:
|
||||||
media-typer "0.3.0"
|
media-typer "0.3.0"
|
||||||
mime-types "~2.1.18"
|
mime-types "~2.1.18"
|
||||||
|
|
||||||
|
typed-styles@^0.0.7:
|
||||||
|
version "0.0.7"
|
||||||
|
resolved "https://registry.yarnpkg.com/typed-styles/-/typed-styles-0.0.7.tgz#93392a008794c4595119ff62dde6809dbc40a3d9"
|
||||||
|
integrity sha512-pzP0PWoZUhsECYjABgCGQlRGL1n7tOHsgwYv3oIiEpJwGhFTuty/YNeduxQYzXXa3Ge5BdT6sHYIQYpl4uJ+5Q==
|
||||||
|
|
||||||
typedarray-to-buffer@^3.1.5:
|
typedarray-to-buffer@^3.1.5:
|
||||||
version "3.1.5"
|
version "3.1.5"
|
||||||
resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080"
|
resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080"
|
||||||
|
@ -9603,6 +9668,11 @@ typescript@3.3.3333:
|
||||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.3.3333.tgz#171b2c5af66c59e9431199117a3bcadc66fdcfd6"
|
resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.3.3333.tgz#171b2c5af66c59e9431199117a3bcadc66fdcfd6"
|
||||||
integrity sha512-JjSKsAfuHBE/fB2oZ8NxtRTk5iGcg6hkYXMnZ3Wc+b2RSqejEqTaem11mHASMnFilHrax3sLK0GDzcJrekZYLw==
|
integrity sha512-JjSKsAfuHBE/fB2oZ8NxtRTk5iGcg6hkYXMnZ3Wc+b2RSqejEqTaem11mHASMnFilHrax3sLK0GDzcJrekZYLw==
|
||||||
|
|
||||||
|
ua-parser-js@^0.7.18:
|
||||||
|
version "0.7.19"
|
||||||
|
resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.19.tgz#94151be4c0a7fb1d001af7022fdaca4642659e4b"
|
||||||
|
integrity sha512-T3PVJ6uz8i0HzPxOF9SWzWAlfN/DavlpQqepn22xgve/5QecC+XMCAtmUNnY7C9StehaV6exjUCI801lOI7QlQ==
|
||||||
|
|
||||||
ua-parser-js@^0.7.9:
|
ua-parser-js@^0.7.9:
|
||||||
version "0.7.17"
|
version "0.7.17"
|
||||||
resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.17.tgz#e9ec5f9498b9ec910e7ae3ac626a805c4d09ecac"
|
resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.17.tgz#e9ec5f9498b9ec910e7ae3ac626a805c4d09ecac"
|
||||||
|
@ -9950,6 +10020,13 @@ warning@^3.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
loose-envify "^1.0.0"
|
loose-envify "^1.0.0"
|
||||||
|
|
||||||
|
warning@^4.0.2:
|
||||||
|
version "4.0.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/warning/-/warning-4.0.3.tgz#16e9e077eb8a86d6af7d64aa1e05fd85b4678ca3"
|
||||||
|
integrity sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==
|
||||||
|
dependencies:
|
||||||
|
loose-envify "^1.0.0"
|
||||||
|
|
||||||
watchpack@^1.5.0:
|
watchpack@^1.5.0:
|
||||||
version "1.5.0"
|
version "1.5.0"
|
||||||
resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.5.0.tgz#231e783af830a22f8966f65c4c4bacc814072eed"
|
resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.5.0.tgz#231e783af830a22f8966f65c4c4bacc814072eed"
|
||||||
|
|