Fix sticker creator in our new typescriptified world
This commit is contained in:
parent
f2af71f8b5
commit
68f27c1c7c
15 changed files with 381 additions and 311 deletions
|
@ -24,7 +24,6 @@ js/WebAudioRecorderMp3.js
|
||||||
app/**/*.js
|
app/**/*.js
|
||||||
ts/**/*.js
|
ts/**/*.js
|
||||||
sticker-creator/**/*.js
|
sticker-creator/**/*.js
|
||||||
!sticker-creator/preload.js
|
|
||||||
|
|
||||||
**/*.d.ts
|
**/*.d.ts
|
||||||
.eslintrc.js
|
.eslintrc.js
|
||||||
|
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -37,6 +37,7 @@ sticker-creator/**/*.js
|
||||||
|
|
||||||
# Sticker Creator
|
# Sticker Creator
|
||||||
sticker-creator/dist/*
|
sticker-creator/dist/*
|
||||||
|
sticker-creator/**/*.js
|
||||||
|
|
||||||
# Editors
|
# Editors
|
||||||
/.idea
|
/.idea
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
|
|
||||||
# Generated files
|
# Generated files
|
||||||
app/**/*.js
|
app/**/*.js
|
||||||
|
sticker-creator/**/*.js
|
||||||
config/local-*.json
|
config/local-*.json
|
||||||
config/local.json
|
config/local.json
|
||||||
dist/**
|
dist/**
|
||||||
|
|
|
@ -68,7 +68,7 @@ esbuild.build({
|
||||||
format: 'cjs',
|
format: 'cjs',
|
||||||
mainFields: ['browser', 'main'],
|
mainFields: ['browser', 'main'],
|
||||||
entryPoints: glob
|
entryPoints: glob
|
||||||
.sync('{app,ts}/**/*.{ts,tsx}', {
|
.sync('{app,ts,sticker-creator}/**/*.{ts,tsx}', {
|
||||||
nodir: true,
|
nodir: true,
|
||||||
root: ROOT_DIR,
|
root: ROOT_DIR,
|
||||||
})
|
})
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
.facade {
|
.facade {
|
||||||
background: rgba(0, 0, 0, 0.33);
|
background: rgba(0, 0, 0, 0.33);
|
||||||
width: 100vw;
|
width: 100vw;
|
||||||
height: var(--window-height);
|
height: 100vh;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
|
@ -44,5 +44,35 @@ export const _StickerFrame = (): JSX.Element => {
|
||||||
};
|
};
|
||||||
|
|
||||||
_StickerFrame.story = {
|
_StickerFrame.story = {
|
||||||
name: 'StickerFrame',
|
name: 'StickerFrame, add sticker',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const EmojiSelectMode = (): JSX.Element => {
|
||||||
|
const image = text('image url', '/fixtures/512x515-thumbs-up-lincoln.webp');
|
||||||
|
const setSkinTone = action('setSkinTone');
|
||||||
|
const onRemove = action('onRemove');
|
||||||
|
const onDrop = action('onDrop');
|
||||||
|
const [emoji, setEmoji] = React.useState<EmojiPickDataType | undefined>(
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StoryRow top>
|
||||||
|
<StickerFrame
|
||||||
|
id="1337"
|
||||||
|
emojiData={emoji}
|
||||||
|
image={image}
|
||||||
|
mode="pick-emoji"
|
||||||
|
onRemove={onRemove}
|
||||||
|
skinTone={0}
|
||||||
|
onSetSkinTone={setSkinTone}
|
||||||
|
onPickEmoji={e => setEmoji(e.emoji)}
|
||||||
|
onDrop={onDrop}
|
||||||
|
/>
|
||||||
|
</StoryRow>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
EmojiSelectMode.story = {
|
||||||
|
name: 'StickerFrame, emoji select mode',
|
||||||
};
|
};
|
||||||
|
|
|
@ -155,6 +155,7 @@ export const StickerFrame = React.memo(
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
removeRoot(root);
|
removeRoot(root);
|
||||||
|
setEmojiPopperRoot(null);
|
||||||
document.removeEventListener('click', handleOutsideClick);
|
document.removeEventListener('click', handleOutsideClick);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -254,7 +255,7 @@ export const StickerFrame = React.memo(
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</PopperReference>
|
</PopperReference>
|
||||||
{emojiPickerOpen && onSetSkinTone && emojiPopperRoot
|
{emojiPickerOpen && emojiPopperRoot
|
||||||
? createPortal(
|
? createPortal(
|
||||||
<Popper placement="bottom-start">
|
<Popper placement="bottom-start">
|
||||||
{({ ref, style }) => (
|
{({ ref, style }) => (
|
||||||
|
|
|
@ -1,290 +1,11 @@
|
||||||
// Copyright 2019-2022 Signal Messenger, LLC
|
// Copyright 2019-2022 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import PQueue from 'p-queue';
|
import './window/phase1-dependencies';
|
||||||
import Backbone from 'backbone';
|
import './window/phase2-signal';
|
||||||
|
import './window/phase3-sticker-functions';
|
||||||
|
import './window/phase4-theme';
|
||||||
|
|
||||||
import { ipcRenderer as ipc } from 'electron';
|
|
||||||
import sharp from 'sharp';
|
|
||||||
import pify from 'pify';
|
|
||||||
import { readFile } from 'fs';
|
|
||||||
import { noop, uniqBy } from 'lodash';
|
|
||||||
|
|
||||||
// It is important to call this as early as possible
|
|
||||||
import { SignalContext } from '../ts/windows/context';
|
import { SignalContext } from '../ts/windows/context';
|
||||||
|
|
||||||
import {
|
|
||||||
deriveStickerPackKey,
|
|
||||||
encryptAttachment,
|
|
||||||
getRandomBytes,
|
|
||||||
} from '../ts/Crypto';
|
|
||||||
import * as Bytes from '../ts/Bytes';
|
|
||||||
import { SignalService as Proto } from '../ts/protobuf';
|
|
||||||
import { getEnvironment } from '../ts/environment';
|
|
||||||
import { createSetting } from '../ts/util/preload';
|
|
||||||
import * as Attachments from '../ts/windows/attachments';
|
|
||||||
|
|
||||||
import * as Signal from '../ts/signal';
|
|
||||||
import { textsecure } from '../ts/textsecure';
|
|
||||||
|
|
||||||
import { initialize as initializeWebAPI } from '../ts/textsecure/WebAPI';
|
|
||||||
import { getAnimatedPngDataIfExists } from '../ts/util/getAnimatedPngDataIfExists';
|
|
||||||
|
|
||||||
const { config } = SignalContext;
|
|
||||||
window.i18n = SignalContext.i18n;
|
|
||||||
|
|
||||||
const STICKER_SIZE = 512;
|
|
||||||
const MIN_STICKER_DIMENSION = 10;
|
|
||||||
const MAX_STICKER_DIMENSION = STICKER_SIZE;
|
|
||||||
const MAX_STICKER_BYTE_LENGTH = 300 * 1024;
|
|
||||||
|
|
||||||
window.ROOT_PATH = window.location.href.startsWith('file') ? '../../' : '/';
|
|
||||||
window.getEnvironment = getEnvironment;
|
|
||||||
window.getVersion = () => window.SignalContext.config.version;
|
|
||||||
|
|
||||||
window.PQueue = PQueue;
|
|
||||||
window.Backbone = Backbone;
|
|
||||||
|
|
||||||
window.localeMessages = ipc.sendSync('locale-data');
|
|
||||||
|
|
||||||
require('../ts/SignalProtocolStore');
|
|
||||||
|
|
||||||
SignalContext.log.info('sticker-creator starting up...');
|
|
||||||
|
|
||||||
window.Signal = Signal.setup({
|
|
||||||
Attachments,
|
|
||||||
getRegionCode: () => {
|
|
||||||
throw new Error('Sticker Creator preload: Not implemented!');
|
|
||||||
},
|
|
||||||
logger: SignalContext.log,
|
|
||||||
userDataPath: SignalContext.config.userDataPath,
|
|
||||||
});
|
|
||||||
window.textsecure = textsecure;
|
|
||||||
|
|
||||||
const WebAPI = initializeWebAPI({
|
|
||||||
url: config.serverUrl,
|
|
||||||
storageUrl: config.storageUrl,
|
|
||||||
updatesUrl: config.updatesUrl,
|
|
||||||
directoryVersion: config.directoryVersion,
|
|
||||||
directoryUrl: config.directoryUrl,
|
|
||||||
directoryEnclaveId: config.directoryEnclaveId,
|
|
||||||
directoryTrustAnchor: config.directoryTrustAnchor,
|
|
||||||
directoryV2Url: config.directoryV2Url,
|
|
||||||
directoryV2PublicKey: config.directoryV2PublicKey,
|
|
||||||
directoryV2CodeHashes: config.directoryV2CodeHashes,
|
|
||||||
cdnUrlObject: {
|
|
||||||
0: config.cdnUrl0,
|
|
||||||
2: config.cdnUrl2,
|
|
||||||
},
|
|
||||||
certificateAuthority: config.certificateAuthority,
|
|
||||||
contentProxyUrl: config.contentProxyUrl,
|
|
||||||
proxyUrl: config.proxyUrl,
|
|
||||||
version: config.version,
|
|
||||||
});
|
|
||||||
|
|
||||||
function processStickerError(message: string, i18nKey: string): Error {
|
|
||||||
const result = new Error(message);
|
|
||||||
result.errorMessageI18nKey = i18nKey;
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
window.processStickerImage = async (path: string | undefined) => {
|
|
||||||
if (!path) {
|
|
||||||
throw new Error(`Path ${path} is not valid!`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const imgBuffer = await pify(readFile)(path);
|
|
||||||
const sharpImg = sharp(imgBuffer);
|
|
||||||
const meta = await sharpImg.metadata();
|
|
||||||
|
|
||||||
const { width, height } = meta;
|
|
||||||
if (!width || !height) {
|
|
||||||
throw processStickerError(
|
|
||||||
'Sticker height or width were falsy',
|
|
||||||
'StickerCreator--Toasts--errorProcessing'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let contentType;
|
|
||||||
let processedBuffer;
|
|
||||||
|
|
||||||
// [Sharp doesn't support APNG][0], so we do something simpler: validate the file size
|
|
||||||
// and dimensions without resizing, cropping, or converting. In a perfect world, we'd
|
|
||||||
// resize and convert any animated image (GIF, animated WebP) to APNG.
|
|
||||||
// [0]: https://github.com/lovell/sharp/issues/2375
|
|
||||||
const animatedPngDataIfExists = getAnimatedPngDataIfExists(imgBuffer);
|
|
||||||
if (animatedPngDataIfExists) {
|
|
||||||
if (imgBuffer.byteLength > MAX_STICKER_BYTE_LENGTH) {
|
|
||||||
throw processStickerError(
|
|
||||||
'Sticker file was too large',
|
|
||||||
'StickerCreator--Toasts--tooLarge'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (width !== height) {
|
|
||||||
throw processStickerError(
|
|
||||||
'Sticker must be square',
|
|
||||||
'StickerCreator--Toasts--APNG--notSquare'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (width > MAX_STICKER_DIMENSION) {
|
|
||||||
throw processStickerError(
|
|
||||||
'Sticker dimensions are too large',
|
|
||||||
'StickerCreator--Toasts--APNG--dimensionsTooLarge'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (width < MIN_STICKER_DIMENSION) {
|
|
||||||
throw processStickerError(
|
|
||||||
'Sticker dimensions are too small',
|
|
||||||
'StickerCreator--Toasts--APNG--dimensionsTooSmall'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (animatedPngDataIfExists.numPlays !== Infinity) {
|
|
||||||
throw processStickerError(
|
|
||||||
'Animated stickers must loop forever',
|
|
||||||
'StickerCreator--Toasts--mustLoopForever'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
contentType = 'image/png';
|
|
||||||
processedBuffer = imgBuffer;
|
|
||||||
} else {
|
|
||||||
contentType = 'image/webp';
|
|
||||||
processedBuffer = await sharpImg
|
|
||||||
.resize({
|
|
||||||
width: STICKER_SIZE,
|
|
||||||
height: STICKER_SIZE,
|
|
||||||
fit: 'contain',
|
|
||||||
background: { r: 0, g: 0, b: 0, alpha: 0 },
|
|
||||||
})
|
|
||||||
.webp()
|
|
||||||
.toBuffer();
|
|
||||||
if (processedBuffer.byteLength > MAX_STICKER_BYTE_LENGTH) {
|
|
||||||
throw processStickerError(
|
|
||||||
'Sticker file was too large',
|
|
||||||
'StickerCreator--Toasts--tooLarge'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
path,
|
|
||||||
buffer: processedBuffer,
|
|
||||||
src: `data:${contentType};base64,${processedBuffer.toString('base64')}`,
|
|
||||||
meta,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
window.encryptAndUpload = async (
|
|
||||||
manifest,
|
|
||||||
stickers,
|
|
||||||
cover,
|
|
||||||
onProgress = noop
|
|
||||||
) => {
|
|
||||||
const usernameItem = await window.Signal.Data.getItemById('uuid_id');
|
|
||||||
const oldUsernameItem = await window.Signal.Data.getItemById('number_id');
|
|
||||||
const passwordItem = await window.Signal.Data.getItemById('password');
|
|
||||||
|
|
||||||
const username = usernameItem?.value || oldUsernameItem?.value;
|
|
||||||
if (!username || !passwordItem?.value) {
|
|
||||||
const { message } =
|
|
||||||
window.localeMessages['StickerCreator--Authentication--error'];
|
|
||||||
|
|
||||||
ipc.send('show-message-box', {
|
|
||||||
type: 'warning',
|
|
||||||
message,
|
|
||||||
});
|
|
||||||
|
|
||||||
throw new Error(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { value: password } = passwordItem;
|
|
||||||
|
|
||||||
const packKey = getRandomBytes(32);
|
|
||||||
const encryptionKey = deriveStickerPackKey(packKey);
|
|
||||||
const iv = getRandomBytes(16);
|
|
||||||
|
|
||||||
const server = WebAPI.connect({
|
|
||||||
username,
|
|
||||||
password,
|
|
||||||
useWebSocket: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
const uniqueStickers = uniqBy(
|
|
||||||
[...stickers, { imageData: cover }],
|
|
||||||
'imageData'
|
|
||||||
);
|
|
||||||
|
|
||||||
const manifestProto = new Proto.StickerPack();
|
|
||||||
manifestProto.title = manifest.title;
|
|
||||||
manifestProto.author = manifest.author;
|
|
||||||
manifestProto.stickers = stickers.map(({ emoji }, id) => {
|
|
||||||
const s = new Proto.StickerPack.Sticker();
|
|
||||||
s.id = id;
|
|
||||||
if (emoji) {
|
|
||||||
s.emoji = emoji;
|
|
||||||
}
|
|
||||||
|
|
||||||
return s;
|
|
||||||
});
|
|
||||||
const coverSticker = new Proto.StickerPack.Sticker();
|
|
||||||
coverSticker.id =
|
|
||||||
uniqueStickers.length === stickers.length ? 0 : uniqueStickers.length - 1;
|
|
||||||
coverSticker.emoji = '';
|
|
||||||
manifestProto.cover = coverSticker;
|
|
||||||
|
|
||||||
const encryptedManifest = await encrypt(
|
|
||||||
Proto.StickerPack.encode(manifestProto).finish(),
|
|
||||||
encryptionKey,
|
|
||||||
iv
|
|
||||||
);
|
|
||||||
const encryptedStickers = uniqueStickers.map(({ imageData }) => {
|
|
||||||
if (!imageData?.buffer) {
|
|
||||||
throw new Error('encryptStickers: Missing image data on sticker');
|
|
||||||
}
|
|
||||||
|
|
||||||
return encrypt(imageData.buffer, encryptionKey, iv);
|
|
||||||
});
|
|
||||||
|
|
||||||
const packId = await server.putStickers(
|
|
||||||
encryptedManifest,
|
|
||||||
encryptedStickers,
|
|
||||||
onProgress
|
|
||||||
);
|
|
||||||
|
|
||||||
const hexKey = Bytes.toHex(packKey);
|
|
||||||
|
|
||||||
ipc.send('install-sticker-pack', packId, hexKey);
|
|
||||||
|
|
||||||
return { packId, key: hexKey };
|
|
||||||
};
|
|
||||||
|
|
||||||
function encrypt(
|
|
||||||
data: Uint8Array,
|
|
||||||
key: Uint8Array,
|
|
||||||
iv: Uint8Array
|
|
||||||
): Uint8Array {
|
|
||||||
const { ciphertext } = encryptAttachment(data, key, iv);
|
|
||||||
|
|
||||||
return ciphertext;
|
|
||||||
}
|
|
||||||
|
|
||||||
const getThemeSetting = createSetting('themeSetting');
|
|
||||||
|
|
||||||
async function resolveTheme() {
|
|
||||||
const theme = (await getThemeSetting.getValue()) || 'system';
|
|
||||||
if (theme === 'system') {
|
|
||||||
return SignalContext.nativeThemeListener.getSystemTheme();
|
|
||||||
}
|
|
||||||
return theme;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function applyTheme() {
|
|
||||||
window.document.body.classList.remove('dark-theme');
|
|
||||||
window.document.body.classList.remove('light-theme');
|
|
||||||
window.document.body.classList.add(`${await resolveTheme()}-theme`);
|
|
||||||
}
|
|
||||||
|
|
||||||
window.addEventListener('DOMContentLoaded', applyTheme);
|
|
||||||
|
|
||||||
SignalContext.nativeThemeListener.subscribe(() => applyTheme());
|
|
||||||
|
|
||||||
SignalContext.log.info('sticker-creator preload complete...');
|
SignalContext.log.info('sticker-creator preload complete...');
|
||||||
|
|
23
sticker-creator/window/phase1-dependencies.ts
Normal file
23
sticker-creator/window/phase1-dependencies.ts
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
// Copyright 2022 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import PQueue from 'p-queue';
|
||||||
|
import Backbone from 'backbone';
|
||||||
|
|
||||||
|
import { ipcRenderer as ipc } from 'electron';
|
||||||
|
|
||||||
|
// It is important to call this as early as possible
|
||||||
|
import { SignalContext } from '../../ts/windows/context';
|
||||||
|
|
||||||
|
import { getEnvironment } from '../../ts/environment';
|
||||||
|
|
||||||
|
SignalContext.log.info('sticker-creator starting up...');
|
||||||
|
|
||||||
|
window.ROOT_PATH = window.location.href.startsWith('file') ? '../../' : '/';
|
||||||
|
window.getEnvironment = getEnvironment;
|
||||||
|
window.getVersion = () => window.SignalContext.config.version;
|
||||||
|
|
||||||
|
window.PQueue = PQueue;
|
||||||
|
window.Backbone = Backbone;
|
||||||
|
|
||||||
|
window.localeMessages = ipc.sendSync('locale-data');
|
20
sticker-creator/window/phase2-signal.ts
Normal file
20
sticker-creator/window/phase2-signal.ts
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
// Copyright 2022 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import * as Signal from '../../ts/signal';
|
||||||
|
import { textsecure } from '../../ts/textsecure';
|
||||||
|
|
||||||
|
import * as Attachments from '../../ts/windows/attachments';
|
||||||
|
import '../../ts/SignalProtocolStore';
|
||||||
|
|
||||||
|
import { SignalContext } from '../../ts/windows/context';
|
||||||
|
|
||||||
|
window.Signal = Signal.setup({
|
||||||
|
Attachments,
|
||||||
|
getRegionCode: () => {
|
||||||
|
throw new Error('Sticker Creator preload: Not implemented!');
|
||||||
|
},
|
||||||
|
logger: SignalContext.log,
|
||||||
|
userDataPath: SignalContext.config.userDataPath,
|
||||||
|
});
|
||||||
|
window.textsecure = textsecure;
|
233
sticker-creator/window/phase3-sticker-functions.ts
Normal file
233
sticker-creator/window/phase3-sticker-functions.ts
Normal file
|
@ -0,0 +1,233 @@
|
||||||
|
// Copyright 2022 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import sharp from 'sharp';
|
||||||
|
import pify from 'pify';
|
||||||
|
import { readFile } from 'fs';
|
||||||
|
import { noop, uniqBy } from 'lodash';
|
||||||
|
import { ipcRenderer as ipc } from 'electron';
|
||||||
|
|
||||||
|
import {
|
||||||
|
deriveStickerPackKey,
|
||||||
|
encryptAttachment,
|
||||||
|
getRandomBytes,
|
||||||
|
} from '../../ts/Crypto';
|
||||||
|
import * as Bytes from '../../ts/Bytes';
|
||||||
|
import { SignalService as Proto } from '../../ts/protobuf';
|
||||||
|
import { initialize as initializeWebAPI } from '../../ts/textsecure/WebAPI';
|
||||||
|
|
||||||
|
import { SignalContext } from '../../ts/windows/context';
|
||||||
|
import { getAnimatedPngDataIfExists } from '../../ts/util/getAnimatedPngDataIfExists';
|
||||||
|
|
||||||
|
const STICKER_SIZE = 512;
|
||||||
|
const MIN_STICKER_DIMENSION = 10;
|
||||||
|
const MAX_STICKER_DIMENSION = STICKER_SIZE;
|
||||||
|
const MAX_STICKER_BYTE_LENGTH = 300 * 1024;
|
||||||
|
|
||||||
|
const { config } = SignalContext;
|
||||||
|
|
||||||
|
const WebAPI = initializeWebAPI({
|
||||||
|
url: config.serverUrl,
|
||||||
|
storageUrl: config.storageUrl,
|
||||||
|
updatesUrl: config.updatesUrl,
|
||||||
|
directoryVersion: config.directoryVersion,
|
||||||
|
directoryUrl: config.directoryUrl,
|
||||||
|
directoryEnclaveId: config.directoryEnclaveId,
|
||||||
|
directoryTrustAnchor: config.directoryTrustAnchor,
|
||||||
|
directoryV2Url: config.directoryV2Url,
|
||||||
|
directoryV2PublicKey: config.directoryV2PublicKey,
|
||||||
|
directoryV2CodeHashes: config.directoryV2CodeHashes,
|
||||||
|
cdnUrlObject: {
|
||||||
|
0: config.cdnUrl0,
|
||||||
|
2: config.cdnUrl2,
|
||||||
|
},
|
||||||
|
certificateAuthority: config.certificateAuthority,
|
||||||
|
contentProxyUrl: config.contentProxyUrl,
|
||||||
|
proxyUrl: config.proxyUrl,
|
||||||
|
version: config.version,
|
||||||
|
});
|
||||||
|
|
||||||
|
function processStickerError(message: string, i18nKey: string): Error {
|
||||||
|
const result = new Error(message);
|
||||||
|
result.errorMessageI18nKey = i18nKey;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.processStickerImage = async (path: string | undefined) => {
|
||||||
|
if (!path) {
|
||||||
|
throw new Error(`Path ${path} is not valid!`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const imgBuffer = await pify(readFile)(path);
|
||||||
|
const sharpImg = sharp(imgBuffer);
|
||||||
|
const meta = await sharpImg.metadata();
|
||||||
|
|
||||||
|
const { width, height } = meta;
|
||||||
|
if (!width || !height) {
|
||||||
|
throw processStickerError(
|
||||||
|
'Sticker height or width were falsy',
|
||||||
|
'StickerCreator--Toasts--errorProcessing'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let contentType;
|
||||||
|
let processedBuffer;
|
||||||
|
|
||||||
|
// [Sharp doesn't support APNG][0], so we do something simpler: validate the file size
|
||||||
|
// and dimensions without resizing, cropping, or converting. In a perfect world, we'd
|
||||||
|
// resize and convert any animated image (GIF, animated WebP) to APNG.
|
||||||
|
// [0]: https://github.com/lovell/sharp/issues/2375
|
||||||
|
const animatedPngDataIfExists = getAnimatedPngDataIfExists(imgBuffer);
|
||||||
|
if (animatedPngDataIfExists) {
|
||||||
|
if (imgBuffer.byteLength > MAX_STICKER_BYTE_LENGTH) {
|
||||||
|
throw processStickerError(
|
||||||
|
'Sticker file was too large',
|
||||||
|
'StickerCreator--Toasts--tooLarge'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (width !== height) {
|
||||||
|
throw processStickerError(
|
||||||
|
'Sticker must be square',
|
||||||
|
'StickerCreator--Toasts--APNG--notSquare'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (width > MAX_STICKER_DIMENSION) {
|
||||||
|
throw processStickerError(
|
||||||
|
'Sticker dimensions are too large',
|
||||||
|
'StickerCreator--Toasts--APNG--dimensionsTooLarge'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (width < MIN_STICKER_DIMENSION) {
|
||||||
|
throw processStickerError(
|
||||||
|
'Sticker dimensions are too small',
|
||||||
|
'StickerCreator--Toasts--APNG--dimensionsTooSmall'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (animatedPngDataIfExists.numPlays !== Infinity) {
|
||||||
|
throw processStickerError(
|
||||||
|
'Animated stickers must loop forever',
|
||||||
|
'StickerCreator--Toasts--mustLoopForever'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
contentType = 'image/png';
|
||||||
|
processedBuffer = imgBuffer;
|
||||||
|
} else {
|
||||||
|
contentType = 'image/webp';
|
||||||
|
processedBuffer = await sharpImg
|
||||||
|
.resize({
|
||||||
|
width: STICKER_SIZE,
|
||||||
|
height: STICKER_SIZE,
|
||||||
|
fit: 'contain',
|
||||||
|
background: { r: 0, g: 0, b: 0, alpha: 0 },
|
||||||
|
})
|
||||||
|
.webp()
|
||||||
|
.toBuffer();
|
||||||
|
if (processedBuffer.byteLength > MAX_STICKER_BYTE_LENGTH) {
|
||||||
|
throw processStickerError(
|
||||||
|
'Sticker file was too large',
|
||||||
|
'StickerCreator--Toasts--tooLarge'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
path,
|
||||||
|
buffer: processedBuffer,
|
||||||
|
src: `data:${contentType};base64,${processedBuffer.toString('base64')}`,
|
||||||
|
meta,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
window.encryptAndUpload = async (
|
||||||
|
manifest,
|
||||||
|
stickers,
|
||||||
|
cover,
|
||||||
|
onProgress = noop
|
||||||
|
) => {
|
||||||
|
const usernameItem = await window.Signal.Data.getItemById('uuid_id');
|
||||||
|
const oldUsernameItem = await window.Signal.Data.getItemById('number_id');
|
||||||
|
const passwordItem = await window.Signal.Data.getItemById('password');
|
||||||
|
|
||||||
|
const username = usernameItem?.value || oldUsernameItem?.value;
|
||||||
|
if (!username || !passwordItem?.value) {
|
||||||
|
const { message } =
|
||||||
|
window.localeMessages['StickerCreator--Authentication--error'];
|
||||||
|
|
||||||
|
ipc.send('show-message-box', {
|
||||||
|
type: 'warning',
|
||||||
|
message,
|
||||||
|
});
|
||||||
|
|
||||||
|
throw new Error(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { value: password } = passwordItem;
|
||||||
|
|
||||||
|
const packKey = getRandomBytes(32);
|
||||||
|
const encryptionKey = deriveStickerPackKey(packKey);
|
||||||
|
const iv = getRandomBytes(16);
|
||||||
|
|
||||||
|
const server = WebAPI.connect({
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
useWebSocket: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const uniqueStickers = uniqBy(
|
||||||
|
[...stickers, { imageData: cover }],
|
||||||
|
'imageData'
|
||||||
|
);
|
||||||
|
|
||||||
|
const manifestProto = new Proto.StickerPack();
|
||||||
|
manifestProto.title = manifest.title;
|
||||||
|
manifestProto.author = manifest.author;
|
||||||
|
manifestProto.stickers = stickers.map(({ emoji }, id) => {
|
||||||
|
const s = new Proto.StickerPack.Sticker();
|
||||||
|
s.id = id;
|
||||||
|
if (emoji) {
|
||||||
|
s.emoji = emoji;
|
||||||
|
}
|
||||||
|
|
||||||
|
return s;
|
||||||
|
});
|
||||||
|
const coverSticker = new Proto.StickerPack.Sticker();
|
||||||
|
coverSticker.id =
|
||||||
|
uniqueStickers.length === stickers.length ? 0 : uniqueStickers.length - 1;
|
||||||
|
coverSticker.emoji = '';
|
||||||
|
manifestProto.cover = coverSticker;
|
||||||
|
|
||||||
|
const encryptedManifest = await encrypt(
|
||||||
|
Proto.StickerPack.encode(manifestProto).finish(),
|
||||||
|
encryptionKey,
|
||||||
|
iv
|
||||||
|
);
|
||||||
|
const encryptedStickers = uniqueStickers.map(({ imageData }) => {
|
||||||
|
if (!imageData?.buffer) {
|
||||||
|
throw new Error('encryptStickers: Missing image data on sticker');
|
||||||
|
}
|
||||||
|
|
||||||
|
return encrypt(imageData.buffer, encryptionKey, iv);
|
||||||
|
});
|
||||||
|
|
||||||
|
const packId = await server.putStickers(
|
||||||
|
encryptedManifest,
|
||||||
|
encryptedStickers,
|
||||||
|
onProgress
|
||||||
|
);
|
||||||
|
|
||||||
|
const hexKey = Bytes.toHex(packKey);
|
||||||
|
|
||||||
|
ipc.send('install-sticker-pack', packId, hexKey);
|
||||||
|
|
||||||
|
return { packId, key: hexKey };
|
||||||
|
};
|
||||||
|
|
||||||
|
function encrypt(
|
||||||
|
data: Uint8Array,
|
||||||
|
key: Uint8Array,
|
||||||
|
iv: Uint8Array
|
||||||
|
): Uint8Array {
|
||||||
|
const { ciphertext } = encryptAttachment(data, key, iv);
|
||||||
|
|
||||||
|
return ciphertext;
|
||||||
|
}
|
25
sticker-creator/window/phase4-theme.ts
Normal file
25
sticker-creator/window/phase4-theme.ts
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
// Copyright 2022 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import { createSetting } from '../../ts/util/preload';
|
||||||
|
import { SignalContext } from '../../ts/windows/context';
|
||||||
|
|
||||||
|
const getThemeSetting = createSetting('themeSetting');
|
||||||
|
|
||||||
|
async function resolveTheme() {
|
||||||
|
const theme = (await getThemeSetting.getValue()) || 'system';
|
||||||
|
if (theme === 'system') {
|
||||||
|
return SignalContext.nativeThemeListener.getSystemTheme();
|
||||||
|
}
|
||||||
|
return theme;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function applyTheme() {
|
||||||
|
window.document.body.classList.remove('dark-theme');
|
||||||
|
window.document.body.classList.remove('light-theme');
|
||||||
|
window.document.body.classList.add(`${await resolveTheme()}-theme`);
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('DOMContentLoaded', applyTheme);
|
||||||
|
|
||||||
|
SignalContext.nativeThemeListener.subscribe(() => applyTheme());
|
|
@ -34,7 +34,7 @@ export type OwnProps = {
|
||||||
readonly onPickEmoji: (o: EmojiPickDataType) => unknown;
|
readonly onPickEmoji: (o: EmojiPickDataType) => unknown;
|
||||||
readonly doSend?: () => unknown;
|
readonly doSend?: () => unknown;
|
||||||
readonly skinTone?: number;
|
readonly skinTone?: number;
|
||||||
readonly onSetSkinTone: (tone: number) => unknown;
|
readonly onSetSkinTone?: (tone: number) => unknown;
|
||||||
readonly recentEmojis?: Array<string>;
|
readonly recentEmojis?: Array<string>;
|
||||||
readonly onClickSettings?: () => unknown;
|
readonly onClickSettings?: () => unknown;
|
||||||
readonly onClose?: () => unknown;
|
readonly onClose?: () => unknown;
|
||||||
|
@ -406,26 +406,28 @@ export const EmojiPicker = React.memo(
|
||||||
type="button"
|
type="button"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<div className="module-emoji-picker__footer__skin-tones">
|
{onSetSkinTone ? (
|
||||||
{[0, 1, 2, 3, 4, 5].map(tone => (
|
<div className="module-emoji-picker__footer__skin-tones">
|
||||||
<button
|
{[0, 1, 2, 3, 4, 5].map(tone => (
|
||||||
type="button"
|
<button
|
||||||
key={tone}
|
type="button"
|
||||||
data-tone={tone}
|
key={tone}
|
||||||
onClick={handlePickTone}
|
data-tone={tone}
|
||||||
title={i18n('EmojiPicker--skin-tone', [`${tone}`])}
|
onClick={handlePickTone}
|
||||||
className={classNames(
|
title={i18n('EmojiPicker--skin-tone', [`${tone}`])}
|
||||||
'module-emoji-picker__button',
|
className={classNames(
|
||||||
'module-emoji-picker__button--footer',
|
'module-emoji-picker__button',
|
||||||
selectedTone === tone
|
'module-emoji-picker__button--footer',
|
||||||
? 'module-emoji-picker__button--selected'
|
selectedTone === tone
|
||||||
: null
|
? 'module-emoji-picker__button--selected'
|
||||||
)}
|
: null
|
||||||
>
|
)}
|
||||||
<Emoji shortName="hand" skinTone={tone} size={20} />
|
>
|
||||||
</button>
|
<Emoji shortName="hand" skinTone={tone} size={20} />
|
||||||
))}
|
</button>
|
||||||
</div>
|
))}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
{Boolean(onClickSettings) && (
|
{Boolean(onClickSettings) && (
|
||||||
<div className="module-emoji-picker__footer__settings-spacer" />
|
<div className="module-emoji-picker__footer__settings-spacer" />
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -913,8 +913,7 @@
|
||||||
"path": "node_modules/agent-base/node_modules/debug/src/common.js",
|
"path": "node_modules/agent-base/node_modules/debug/src/common.js",
|
||||||
"line": "\tcreateDebug.enable(createDebug.load());",
|
"line": "\tcreateDebug.enable(createDebug.load());",
|
||||||
"reasonCategory": "falseMatch|testCode|exampleCode|otherUtilityCode|regexMatchedSafeCode|notExercisedByOurApp|ruleNeeded|usageTrusted",
|
"reasonCategory": "falseMatch|testCode|exampleCode|otherUtilityCode|regexMatchedSafeCode|notExercisedByOurApp|ruleNeeded|usageTrusted",
|
||||||
"updated": "2022-02-11T21:58:24.827Z",
|
"updated": "2022-02-11T21:58:24.827Z"
|
||||||
"reasonDetail": "<optional>"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"rule": "jQuery-load(",
|
"rule": "jQuery-load(",
|
||||||
|
@ -8273,6 +8272,13 @@
|
||||||
"reasonCategory": "falseMatch",
|
"reasonCategory": "falseMatch",
|
||||||
"updated": "2018-09-19T21:59:32.770Z"
|
"updated": "2018-09-19T21:59:32.770Z"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"rule": "React-useRef",
|
||||||
|
"path": "sticker-creator/components/StickerFrame.js",
|
||||||
|
"line": " const timerRef = React.useRef();",
|
||||||
|
"reasonCategory": "falseMatch",
|
||||||
|
"updated": "2022-06-14T01:19:45.446Z"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"rule": "React-useRef",
|
"rule": "React-useRef",
|
||||||
"path": "sticker-creator/components/StickerFrame.tsx",
|
"path": "sticker-creator/components/StickerFrame.tsx",
|
||||||
|
@ -8280,6 +8286,13 @@
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2021-07-30T16:57:33.618Z"
|
"updated": "2021-07-30T16:57:33.618Z"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"rule": "jQuery-$(",
|
||||||
|
"path": "sticker-creator/util/i18n.js",
|
||||||
|
"line": " const FIND_REPLACEMENTS = /\\$([^$]+)\\$/g;",
|
||||||
|
"reasonCategory": "falseMatch",
|
||||||
|
"updated": "2022-06-14T01:19:45.446Z"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"rule": "jQuery-$(",
|
"rule": "jQuery-$(",
|
||||||
"path": "sticker-creator/util/i18n.tsx",
|
"path": "sticker-creator/util/i18n.tsx",
|
||||||
|
|
|
@ -129,3 +129,4 @@ export const SignalContext: SignalContextType = {
|
||||||
};
|
};
|
||||||
|
|
||||||
window.SignalContext = SignalContext;
|
window.SignalContext = SignalContext;
|
||||||
|
window.i18n = SignalContext.i18n;
|
||||||
|
|
Loading…
Reference in a new issue