Remove Sticker Creator in favor of Web App

This commit is contained in:
Fedor Indutny 2023-03-15 17:59:30 -07:00 committed by GitHub
parent 78f0626e68
commit f84b6a31dc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
117 changed files with 67 additions and 5269 deletions

View File

@ -13,7 +13,6 @@ js/util_worker.js
libtextsecure/components.js
libtextsecure/test/test.js
test/test.js
sticker-creator/dist/**
ts/protobuf/compiled.d.ts
# Third-party files
@ -23,7 +22,6 @@ js/WebAudioRecorderMp3.js
# TypeScript generated files
app/**/*.js
ts/**/*.js
sticker-creator/**/*.js
.eslintrc.js
webpack.config.ts

View File

@ -241,8 +241,6 @@ module.exports = {
'ts/**/*.ts',
'ts/**/*.tsx',
'app/**/*.ts',
'sticker-creator/**/*.ts',
'sticker-creator/**/*.tsx',
'build/intl-linter/**/*.ts',
],
parser: '@typescript-eslint/parser',

View File

@ -51,7 +51,7 @@ jobs:
- name: Build typescript
run: yarn generate
- name: Bundle
run: yarn build:webpack
run: yarn build:esbuild:prod
- name: Run startup benchmarks
run: |

View File

@ -208,7 +208,7 @@ jobs:
- name: Build typescript
run: yarn generate
- name: Bundle
run: yarn build:webpack
run: yarn build:esbuild:prod
- name: Run mock server tests
run: |

5
.gitignore vendored
View File

@ -34,15 +34,10 @@ ts/sql/mainWorker.bundle.js.LICENSE.txt
app/*.js
ts/**/*.js
ts/protobuf/*.d.ts
sticker-creator/**/*.js
# CSS Modules
**/*.scss.d.ts
# Sticker Creator
sticker-creator/dist/*
sticker-creator/**/*.js
# Editors
/.idea
/.vscode

View File

@ -3,7 +3,6 @@
# Generated files
app/**/*.js
sticker-creator/**/*.js
config/local-*.json
config/local.json
dist/**
@ -19,7 +18,6 @@ ts/protobuf/*.d.ts
ts/protobuf/*.js
stylesheets/manifest.css
ts/util/lint/exceptions.json
sticker-creator/dist/**
# Third-party files
node_modules/**

View File

@ -5,10 +5,7 @@ module.exports = {
typescript: {
reactDocgen: false,
},
stories: [
'../ts/components/**/*.stories.tsx',
'../sticker-creator/**/*.stories.tsx',
],
stories: ['../ts/components/**/*.stories.tsx'],
addons: [
'@storybook/addon-a11y',
'@storybook/addon-actions',

View File

@ -8,7 +8,6 @@ import { withKnobs, boolean, optionsKnob } from '@storybook/addon-knobs';
import * as styles from './styles.scss';
import messages from '../_locales/en/messages.json';
import { ClassyProvider } from '../ts/components/PopperRootContext';
import { I18n } from '../sticker-creator/util/i18n';
import { StorybookThemeContext } from './StorybookThemeContext';
import { ThemeType } from '../ts/types/Util';
import { setupI18n } from '../ts/util/setupI18n';
@ -72,13 +71,7 @@ const withModeAndThemeProvider = (Story, context) => {
);
};
const withI18n = (Story, context) => (
<I18n messages={messages} locale="en">
<Story {...context} />
</I18n>
);
export const decorators = [withModeAndThemeProvider, withI18n];
export const decorators = [withModeAndThemeProvider];
export const parameters = {
axe: {

View File

@ -131,18 +131,6 @@ Signal Desktop makes use of the following open source projects.
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE
## array-move
MIT License
Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (sindresorhus.com)
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
## backbone
Copyright (c) 2010-2019 Jeremy Ashkenas, DocumentCloud
@ -2017,30 +2005,6 @@ Signal Desktop makes use of the following open source projects.
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
## react-dropzone
The MIT License (MIT)
Copyright (c) 2018 Param Aggarwal
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
## react-hot-loader
MIT License
@ -2169,30 +2133,6 @@ Signal Desktop makes use of the following open source projects.
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
## react-sortable-hoc
The MIT License (MIT)
Copyright (c) 2016, Claudéric Demers
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
## react-textarea-autosize
The MIT License (MIT)

View File

@ -59,7 +59,6 @@ git clone https://github.com/signalapp/Signal-Desktop.git
cd Signal-Desktop
yarn install --frozen-lockfile # Install and build dependencies (this will take a while)
yarn generate # Generate final JS and CSS assets
yarn build:webpack # Build parts of the app that use webpack (Sticker Creator)
yarn test # A good idea to make sure tests run first
yarn start # Start Signal!
```

View File

@ -788,7 +788,7 @@
},
"signalDesktopStickerCreator": {
"message": "Sticker pack creator",
"description": "Title of the window that pops up with Signal Desktop preferences in it"
"description": "(deleted 03/15/2023) Title of the window that pops up with Signal Desktop preferences in it"
},
"aboutSignalDesktop": {
"message": "About Signal Desktop",
@ -2989,203 +2989,203 @@
},
"StickerCreator--title": {
"message": "Sticker pack creator",
"description": "The title of the Sticker Pack Creator window"
"description": "(deleted 03/15/2023) The title of the Sticker Pack Creator window"
},
"StickerCreator--DropZone--staticText": {
"message": "Click to add or drop images here",
"description": "Text which appears on the Sticker Creator drop zone when there is no active drag"
"description": "(deleted 03/15/2023) Text which appears on the Sticker Creator drop zone when there is no active drag"
},
"StickerCreator--DropZone--activeText": {
"message": "Drop images here",
"description": "Text which appears on the Sticker Creator drop zone when there is an active drag"
"description": "(deleted 03/15/2023) Text which appears on the Sticker Creator drop zone when there is an active drag"
},
"StickerCreator--Preview--title": {
"message": "Sticker pack",
"description": "The 'title' of the sticker pack preview 'modal'"
"description": "(deleted 03/15/2023) The 'title' of the sticker pack preview 'modal'"
},
"StickerCreator--ConfirmDialog--cancel": {
"message": "Cancel",
"description": "The default text for the confirm dialog cancel button"
"description": "(deleted 03/15/2023) The default text for the confirm dialog cancel button"
},
"StickerCreator--CopyText--button": {
"message": "Copy",
"description": "The text which appears on the copy button for the sticker creator share screen"
"description": "(deleted 03/15/2023) The text which appears on the copy button for the sticker creator share screen"
},
"StickerCreator--ShareButtons--facebook": {
"message": "Facebook",
"description": "Title for Facebook button"
"description": "(deleted 03/15/2023) Title for Facebook button"
},
"StickerCreator--ShareButtons--twitter": {
"message": "Twitter",
"description": "Title for Twitter button"
"description": "(deleted 03/15/2023) Title for Twitter button"
},
"StickerCreator--ShareButtons--pinterest": {
"message": "Pinterest",
"description": "Title for Pinterest button"
"description": "(deleted 03/15/2023) Title for Pinterest button"
},
"StickerCreator--ShareButtons--whatsapp": {
"message": "WhatsApp",
"description": "Title for WhatsApp button"
"description": "(deleted 03/15/2023) Title for WhatsApp button"
},
"StickerCreator--AppStage--next": {
"message": "Next",
"description": "Default text for the next button on all stages of the sticker creator"
"description": "(deleted 03/15/2023) Default text for the next button on all stages of the sticker creator"
},
"StickerCreator--AppStage--prev": {
"message": "Back",
"description": "Default text for the previous button on all stages of the sticker creator"
"description": "(deleted 03/15/2023) Default text for the previous button on all stages of the sticker creator"
},
"icu:StickerCreator--DropStage--title": {
"messageformat": "Add your stickers",
"description": "Title for the drop stage of the sticker creator"
"description": "(deleted 03/15/2023) Title for the drop stage of the sticker creator"
},
"StickerCreator--DropStage--removeSticker": {
"message": "Remove sticker",
"description": "Label for the X button used to remove a staged sticker"
"description": "(deleted 03/15/2023) Label for the X button used to remove a staged sticker"
},
"StickerCreator--DropStage--dragDrop": {
"message": "Click or drag/drop a file to add a sticker",
"description": "Shown on the + section of the file addition stage of sticker pack creation"
"description": "(deleted 03/15/2023) Shown on the + section of the file addition stage of sticker pack creation"
},
"StickerCreator--DropStage--help": {
"message": "Stickers must be in PNG, APNG, or WebP format with a transparent background and 512x512 pixels. Recommended margin is 16px.",
"description": "Help text for the drop stage of the sticker creator"
"description": "(deleted 03/15/2023) Help text for the drop stage of the sticker creator"
},
"StickerCreator--DropStage--showMargins": {
"message": "Show margins",
"description": "Text for the show margins toggle on the drop stage of the sticker creator"
"description": "(deleted 03/15/2023) Text for the show margins toggle on the drop stage of the sticker creator"
},
"icu:StickerCreator--DropStage--addMore": {
"messageformat": "Add {count, number} or more",
"description": "Text to show user how many more stickers they must add"
"description": "(deleted 03/15/2023) Text to show user how many more stickers they must add"
},
"StickerCreator--EmojiStage--title": {
"message": "Add an emoji to each sticker",
"description": "Title for the drop stage of the sticker creator"
"description": "(deleted 03/15/2023) Title for the drop stage of the sticker creator"
},
"StickerCreator--EmojiStage--help": {
"message": "This allows us to suggest stickers to you as you're messaging.",
"description": "Help text for the drop stage of the sticker creator"
"description": "(deleted 03/15/2023) Help text for the drop stage of the sticker creator"
},
"StickerCreator--MetaStage--title": {
"message": "Just a few more details...",
"description": "Title for the meta stage of the sticker creator"
"description": "(deleted 03/15/2023) Title for the meta stage of the sticker creator"
},
"StickerCreator--MetaStage--Field--title": {
"message": "Title",
"description": "Label for the title input of the meta stage of the sticker creator"
"description": "(deleted 03/15/2023) Label for the title input of the meta stage of the sticker creator"
},
"StickerCreator--MetaStage--Field--author": {
"message": "Author",
"description": "Label for the author input of the meta stage of the sticker creator"
"description": "(deleted 03/15/2023) Label for the author input of the meta stage of the sticker creator"
},
"StickerCreator--MetaStage--Field--cover": {
"message": "Cover image",
"description": "Label for the cover image picker of the meta stage of the sticker creator"
"description": "(deleted 03/15/2023) Label for the cover image picker of the meta stage of the sticker creator"
},
"StickerCreator--MetaStage--Field--cover--help": {
"message": "This is the image that will show up when you share your sticker pack",
"description": "Help text for the cover image picker of the meta stage of the sticker creator"
"description": "(deleted 03/15/2023) Help text for the cover image picker of the meta stage of the sticker creator"
},
"StickerCreator--MetaStage--ConfirmDialog--title": {
"message": "Are you sure you want to upload your sticker pack?",
"description": "Title for the confirm dialog on the meta stage of the sticker creator"
"description": "(deleted 03/15/2023) Title for the confirm dialog on the meta stage of the sticker creator"
},
"StickerCreator--MetaStage--ConfirmDialog--confirm": {
"message": "Upload",
"description": "Text for the upload button in the confirmation dialog on the meta stage of the sticker creator"
"description": "(deleted 03/15/2023) Text for the upload button in the confirmation dialog on the meta stage of the sticker creator"
},
"StickerCreator--MetaStage--ConfirmDialog--text": {
"message": "You will no longer be able to make edits or delete after creating a sticker pack.",
"description": "The text inside the confirmation dialog on the meta stage of the sticker creator"
"description": "(deleted 03/15/2023) The text inside the confirmation dialog on the meta stage of the sticker creator"
},
"StickerCreator--UploadStage--title": {
"message": "Creating your sticker pack",
"description": "Title for the upload stage of the sticker creator"
"description": "(deleted 03/15/2023) Title for the upload stage of the sticker creator"
},
"StickerCreator--UploadStage-uploaded": {
"message": "$count$ of $total$ uploaded",
"description": "Title for the upload stage of the sticker creator"
"description": "(deleted 03/15/2023) Title for the upload stage of the sticker creator"
},
"StickerCreator--ShareStage--title": {
"message": "Congratulations! You created a sticker pack.",
"description": "Title for the share stage of the sticker creator"
"description": "(deleted 03/15/2023) Title for the share stage of the sticker creator"
},
"StickerCreator--ShareStage--help": {
"message": "Access your new stickers through the sticker icon, or share with your friends using the link below.",
"description": "Help text for the share stage of the sticker creator"
"description": "(deleted 03/15/2023) Help text for the share stage of the sticker creator"
},
"StickerCreator--ShareStage--callToAction": {
"message": "Use the hashtag $hashtag$ to help other people find the URLs for any custom sticker packs that you would like to make publicly accessible.",
"description": "Call to action text for the share stage of the sticker creator"
"description": "(deleted 03/15/2023) Call to action text for the share stage of the sticker creator"
},
"StickerCreator--ShareStage--copyTitle": {
"message": "Sticker Pack URL",
"description": "Title for the copy button on the share stage of the sticker creator"
"description": "(deleted 03/15/2023) Title for the copy button on the share stage of the sticker creator"
},
"StickerCreator--ShareStage--close": {
"message": "Close",
"description": "Text for the close button on the share stage of the sticker creator"
"description": "(deleted 03/15/2023) Text for the close button on the share stage of the sticker creator"
},
"StickerCreator--ShareStage--createAnother": {
"message": "Create another sticker pack",
"description": "Text for the create another sticker pack button on the share stage of the sticker creator"
"description": "(deleted 03/15/2023) Text for the create another sticker pack button on the share stage of the sticker creator"
},
"StickerCreator--ShareStage--socialMessage": {
"message": "Check out this new sticker pack I created for Signal. #makeprivacystick",
"description": "Text which is shared to social media platforms for sticker packs"
"description": "(deleted 03/15/2023) Text which is shared to social media platforms for sticker packs"
},
"icu:StickerCreator--Toasts--imagesAdded": {
"messageformat": "{count, plural, one {1 image} other {# images}} added",
"description": "Text for the toast when images are added to the sticker creator"
"description": "(deleted 03/15/2023) Text for the toast when images are added to the sticker creator"
},
"StickerCreator--Toasts--animated": {
"message": "Animated stickers are not currently supported",
"description": "Text for the toast when an image that is animated was dropped on the sticker creator"
"description": "(deleted 03/15/2023) Text for the toast when an image that is animated was dropped on the sticker creator"
},
"StickerCreator--Toasts--tooLarge": {
"message": "Dropped image is too large",
"description": "Text for the toast when an image that is too large was dropped on the sticker creator"
"description": "(deleted 03/15/2023) Text for the toast when an image that is too large was dropped on the sticker creator"
},
"StickerCreator--Toasts--errorProcessing": {
"message": "Error processing image",
"description": "Text for the toast when an image cannot be processed was dropped on the sticker creator with a generic error"
"description": "(deleted 03/15/2023) Text for the toast when an image cannot be processed was dropped on the sticker creator with a generic error"
},
"StickerCreator--Toasts--APNG--notSquare": {
"message": "Animated PNG stickers must be square",
"description": "Text for the toast when someone tries to upload a non-square APNG"
"description": "(deleted 03/15/2023) Text for the toast when someone tries to upload a non-square APNG"
},
"StickerCreator--Toasts--mustLoopForever": {
"message": "Animated stickers must loop forever",
"description": "Text for the toast when an image in the sticker creator does not animate forever"
"description": "(deleted 03/15/2023) Text for the toast when an image in the sticker creator does not animate forever"
},
"StickerCreator--Toasts--APNG--dimensionsTooLarge": {
"message": "Animated PNG sticker dimensions are too large",
"description": "Text for the toast when an APNG image in the sticker creator is too large"
"description": "(deleted 03/15/2023) Text for the toast when an APNG image in the sticker creator is too large"
},
"StickerCreator--Toasts--APNG--dimensionsTooSmall": {
"message": "Animated PNG sticker dimensions are too small",
"description": "Text for the toast when an APNG image in the sticker creator is too small"
"description": "(deleted 03/15/2023) Text for the toast when an APNG image in the sticker creator is too small"
},
"StickerCreator--Toasts--errorUploading": {
"message": "Error uploading stickers: $message$",
"description": "Text for the toast when a sticker pack cannot be uploaded"
"description": "(deleted 03/15/2023) Text for the toast when a sticker pack cannot be uploaded"
},
"StickerCreator--Toasts--linkedCopied": {
"message": "Link copied",
"description": "Text for the toast when a link for sharing is copied from the Sticker Creator"
"description": "(deleted 03/15/2023) Text for the toast when a link for sharing is copied from the Sticker Creator"
},
"StickerCreator--StickerPreview--light": {
"message": "My sticker in light theme",
"description": "Text for the sticker preview for the light theme"
"description": "(deleted 03/15/2023) Text for the sticker preview for the light theme"
},
"StickerCreator--StickerPreview--dark": {
"message": "My sticker in dark theme",
"description": "Text for the sticker preview for the dark theme"
"description": "(deleted 03/15/2023) Text for the sticker preview for the dark theme"
},
"StickerCreator--Authentication--error": {
"message": "Please set up Signal on your phone and desktop to use the Sticker Pack Creator",
"description": "The error message which appears when the user has not linked their account and attempts to use the Sticker Creator"
"description": "(deleted 03/15/2023) The error message which appears when the user has not linked their account and attempts to use the Sticker Creator"
},
"icu:ArtCreator--Authentication--error": {
"messageformat": "Please set up Signal on your phone and desktop to use the Sticker Pack Creator",

View File

@ -95,7 +95,7 @@ import type { MenuActionType } from '../ts/types/menu';
import { createTemplate } from './menu';
import { installFileHandler, installWebHandler } from './protocol_filter';
import * as OS from '../ts/OS';
import { isProduction, isStaging } from '../ts/util/version';
import { isProduction } from '../ts/util/version';
import {
isSgnlHref,
isCaptchaHref,
@ -1342,85 +1342,6 @@ async function getIsLinked() {
}
}
let stickerCreatorWindow: BrowserWindow | undefined;
async function showStickerCreator() {
if (!(await getIsLinked())) {
const message = getResolvedMessagesLocale().i18n(
'StickerCreator--Authentication--error'
);
await dialog.showMessageBox({
type: 'warning',
message,
});
return;
}
if (stickerCreatorWindow) {
stickerCreatorWindow.show();
return;
}
const { x = 0, y = 0 } = windowConfig || {};
const titleBarOverlay = await getTitleBarOverlay();
const options = {
x: x + 100,
y: y + 100,
width: 800,
minWidth: 800,
height: 650,
title: getResolvedMessagesLocale().i18n('signalDesktopStickerCreator'),
titleBarStyle: nonMainTitleBarStyle,
titleBarOverlay,
autoHideMenuBar: true,
backgroundColor: await getBackgroundColor(),
show: false,
webPreferences: {
...defaultWebPrefs,
nodeIntegration: false,
nodeIntegrationInWorker: false,
sandbox: false,
contextIsolation: false,
preload: join(__dirname, '../sticker-creator/preload.js'),
nativeWindowOpen: true,
spellcheck: await getSpellCheckSetting(),
},
};
stickerCreatorWindow = new BrowserWindow(options);
setupSpellChecker(stickerCreatorWindow, getResolvedMessagesLocale());
handleCommonWindowEvents(stickerCreatorWindow, titleBarOverlay);
const appUrl = process.env.SIGNAL_ENABLE_HTTP
? prepareUrl(
new URL('http://localhost:6380/sticker-creator/dist/index.html')
)
: prepareFileUrl([__dirname, '../sticker-creator/dist/index.html']);
stickerCreatorWindow.on('closed', () => {
stickerCreatorWindow = undefined;
});
stickerCreatorWindow.once('ready-to-show', () => {
if (!stickerCreatorWindow) {
return;
}
stickerCreatorWindow.show();
if (config.get<boolean>('openDevTools')) {
// Open the DevTools.
stickerCreatorWindow.webContents.openDevTools();
}
});
await safeLoadURL(stickerCreatorWindow, await appUrl);
}
async function openArtCreator() {
if (!(await getIsLinked())) {
const message = getResolvedMessagesLocale().i18n(
@ -2013,7 +1934,6 @@ function setupMenu(options?: Partial<CreateTemplateOptionsType>) {
devTools: defaultWebPrefs.devTools,
includeSetup: false,
isProduction: isProduction(app.getVersion()),
isStaging: isStaging(app.getVersion()),
platform,
// actions
@ -2030,7 +1950,6 @@ function setupMenu(options?: Partial<CreateTemplateOptionsType>) {
showDebugLog: showDebugLogWindow,
showKeyboardShortcuts,
showSettings: showSettingsWindow,
showStickerCreator,
showWindow,
// overrides
@ -2642,8 +2561,6 @@ ipc.handle('executeMenuAction', async (_event, action: MenuActionType) => {
showKeyboardShortcuts();
} else if (action === 'showSettings') {
drop(showSettingsWindow());
} else if (action === 'showStickerCreator') {
drop(showStickerCreator());
} else if (action === 'showWindow') {
showWindow();
} else {

View File

@ -22,7 +22,6 @@ export const createTemplate = (
const {
isProduction,
isStaging,
devTools,
includeSetup,
openContactUs,
@ -38,7 +37,6 @@ export const createTemplate = (
showDebugLog,
showKeyboardShortcuts,
showSettings,
showStickerCreator,
openArtCreator,
} = options;
@ -48,7 +46,7 @@ export const createTemplate = (
submenu: [
{
label: i18n('mainMenuCreateStickers'),
click: isStaging ? openArtCreator : showStickerCreator,
click: openArtCreator,
},
{
label: i18n('mainMenuSettings'),

View File

@ -55,28 +55,22 @@
"svgo": "svgo --multipass images/**/*.svg",
"transpile": "run-p check:types build:esbuild",
"check:types": "tsc --noEmit",
"clean-transpile-once": "rimraf app/**/*.js app/*.js sticker-creator/**/*.js sticker-creator/*.js ts/**/*.js ts/*.js tsconfig.tsbuildinfo",
"clean-transpile-once": "rimraf app/**/*.js app/*.js ts/**/*.js ts/*.js tsconfig.tsbuildinfo",
"clean-transpile": "yarn run clean-transpile-once && yarn run clean-transpile-once",
"open-coverage": "open coverage/lcov-report/index.html",
"ready": "npm-run-all --print-label clean-transpile generate --parallel lint lint-deps lint-intl test-node test-electron",
"dev": "run-p --print-label dev:*",
"dev:transpile": "run-p \"check:types --watch\" dev:esbuild",
"dev:webpack": "run-p dev:esbuild dev:webpack:sticker-creator",
"dev:webpack:sticker-creator": "cross-env NODE_ENV=development webpack serve --mode development",
"dev:esbuild": "node scripts/esbuild.js --watch",
"dev:typed-scss": "yarn build:typed-scss -w",
"dev:storybook": "cross-env SIGNAL_ENV=storybook start-storybook -p 6006 -s ./",
"dev:sass": "yarn sass --watch",
"build": "run-s --print-label generate build:typed-scss build:webpack build:release",
"build": "run-s --print-label generate build:esbuild:prod build:release",
"build:acknowledgments": "node scripts/generate-acknowledgments.js",
"build:dev": "run-s --print-label generate build:typed-scss build:webpack",
"build:typed-scss": "tsm sticker-creator",
"build:webpack": "run-p build:webpack:sticker-creator \"build:esbuild --prod\"",
"build:webpack:sticker-creator": "cross-env NODE_ENV=production webpack",
"build:dev": "run-s --print-label generate build:esbuild:prod",
"build:esbuild": "node scripts/esbuild.js",
"build:esbuild:prod": "node scripts/esbuild.js --prod",
"build:electron": "electron-builder --config.extraMetadata.environment=$SIGNAL_ENV",
"build:release": "cross-env SIGNAL_ENV=production yarn build:electron -- --config.directories.output=release",
"preverify:ts": "yarn build:typed-scss",
"verify": "run-p --print-label verify:*",
"verify:ts": "tsc --noEmit",
"electron:install-app-deps": "electron-builder install-app-deps"
@ -94,7 +88,6 @@
"@signalapp/libsignal-client": "0.22.0",
"@signalapp/ringrtc": "2.25.2",
"@types/fabric": "4.5.3",
"array-move": "2.1.0",
"backbone": "1.4.0",
"blob-util": "2.0.2",
"blueimp-load-image": "5.14.0",
@ -151,7 +144,6 @@
"react-blurhash": "0.1.2",
"react-contextmenu": "2.11.0",
"react-dom": "17.0.2",
"react-dropzone": "10.2.2",
"react-hot-loader": "4.13.0",
"react-intl": "6.1.1",
"react-measure": "2.3.0",
@ -159,7 +151,6 @@
"react-quill": "2.0.0-beta.4",
"react-redux": "7.2.8",
"react-router-dom": "5.0.1",
"react-sortable-hoc": "2.0.0",
"react-textarea-autosize": "8.3.4",
"react-virtualized": "9.22.3",
"read-last-lines": "1.8.0",
@ -240,7 +231,6 @@
"@types/react-measure": "2.0.5",
"@types/react-redux": "7.1.24",
"@types/react-router-dom": "4.3.4",
"@types/react-sortable-hoc": "0.6.5",
"@types/react-virtualized": "9.18.12",
"@types/redux-logger": "3.0.7",
"@types/rimraf": "2.0.2",
@ -254,7 +244,6 @@
"@types/yargs": "17.0.7",
"@typescript-eslint/eslint-plugin": "5.47.0",
"@typescript-eslint/parser": "5.47.0",
"arraybuffer-loader": "1.0.3",
"asar": "3.1.0",
"axe-core": "4.1.4",
"babel-core": "7.0.0-bridge.0",
@ -283,7 +272,6 @@
"eslint-plugin-mocha": "10.1.0",
"eslint-plugin-more": "1.0.5",
"eslint-plugin-react": "7.31.10",
"file-loader": "4.2.0",
"html-webpack-plugin": "5.3.1",
"json-to-ast": "2.1.0",
"mocha": "9.1.3",
@ -470,9 +458,6 @@
"sounds/*",
"build/icons",
"node_modules/**",
"sticker-creator/preload.js",
"sticker-creator/window/*.js",
"sticker-creator/dist/**",
"!node_modules/underscore/**",
"!node_modules/emoji-datasource/emoji_pretty.json",
"!node_modules/emoji-datasource/**/*.png",

View File

@ -68,7 +68,7 @@ async function main() {
format: 'cjs',
mainFields: ['browser', 'main'],
entryPoints: glob
.sync('{app,ts,sticker-creator}/**/*.{ts,tsx}', {
.sync('{app,ts}/**/*.{ts,tsx}', {
nodir: true,
root: ROOT_DIR,
})

View File

@ -1,12 +0,0 @@
// Copyright 2019 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
@mixin light-theme() {
@content;
}
@mixin dark-theme() {
:global(.dark-theme) & {
@content;
}
}

View File

@ -1,19 +0,0 @@
// Copyright 2019 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
@import '../mixins';
@import '../../stylesheets/variables';
.container {
display: grid;
height: var(--window-height);
grid-template-rows: 47px calc(var(--window-height) - 47px - 68px) 68px;
@include light-theme() {
background-color: $color-white;
}
@include dark-theme() {
background-color: $color-gray-90;
}
}

View File

@ -1,60 +0,0 @@
// Copyright 2019 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import * as React from 'react';
import { Redirect, Route, Switch } from 'react-router-dom';
import { DropStage } from './stages/DropStage';
import { EmojiStage } from './stages/EmojiStage';
import { UploadStage } from './stages/UploadStage';
import { MetaStage } from './stages/MetaStage';
import { ShareStage } from './stages/ShareStage';
import * as styles from './index.scss';
import { PageHeader } from '../elements/PageHeader';
import { useI18n } from '../util/i18n';
import { TitleBarContainer } from '../../ts/components/TitleBarContainer';
import type { ExecuteMenuRoleType } from '../../ts/components/TitleBarContainer';
import { useTheme } from '../../ts/hooks/useTheme';
export type AppPropsType = Readonly<{
executeMenuRole: ExecuteMenuRoleType;
hasCustomTitleBar: boolean;
}>;
export function App({
executeMenuRole,
hasCustomTitleBar,
}: AppPropsType): JSX.Element {
const i18n = useI18n();
const theme = useTheme();
return (
<TitleBarContainer
iconSrc="../../images/icon_32.png"
hasCustomTitleBar={hasCustomTitleBar}
theme={theme}
executeMenuRole={executeMenuRole}
>
<div className={styles.container}>
<PageHeader>{i18n('StickerCreator--title')}</PageHeader>
<Switch>
<Route path="/drop">
<DropStage />
</Route>
<Route path="/add-emojis">
<EmojiStage />
</Route>
<Route path="/add-meta">
<MetaStage />
</Route>
<Route path="/upload">
<UploadStage />
</Route>
<Route path="/share">
<ShareStage />
</Route>
<Redirect to="/drop" />
</Switch>
</div>
</TitleBarContainer>
);
}

View File

@ -1,50 +0,0 @@
// Copyright 2019 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
.padded {
padding: 0 16px;
}
.main {
composes: padded;
padding-top: 16px;
display: grid;
height: 100%;
grid-template-rows: 26px 36px calc(100% - 26px - 36px);
overflow: auto;
}
.no-message {
composes: main;
grid-template-rows: 26px calc(100% - 26px);
}
.empty {
composes: main;
grid-template-rows: 100%;
}
.footer {
composes: padded;
display: flex;
flex-direction: row;
justify-content: flex-end;
align-items: center;
}
.footer-right {
display: flex;
flex-direction: row;
align-items: center;
}
.button {
margin-left: 12px;
}
.toaster {
position: fixed;
bottom: 16px;
left: 50%;
transform: translate(-50%, 0px);
}

View File

@ -1,105 +0,0 @@
// Copyright 2019 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React from 'react';
import * as styles from './AppStage.scss';
import { history } from '../../util/history';
import { Button } from '../../elements/Button';
import { useI18n } from '../../util/i18n';
import { Text } from '../../elements/Typography';
import { Toaster } from '../../components/Toaster';
import { stickersDuck } from '../../store';
export type Props = {
readonly children: React.ReactNode;
readonly empty?: boolean;
readonly prev?: string;
readonly prevText?: string;
readonly next?: string;
readonly nextActive?: boolean;
readonly noMessage?: boolean;
readonly onNext?: () => unknown;
readonly onPrev?: () => unknown;
readonly nextText?: string;
};
const getClassName = ({ noMessage, empty }: Props) => {
if (noMessage) {
return styles.noMessage;
}
if (empty) {
return styles.empty;
}
return styles.main;
};
export function AppStage(props: Props): JSX.Element {
const {
children,
next,
nextActive,
nextText,
onNext,
onPrev,
prev,
prevText,
} = props;
const i18n = useI18n();
const handleNext = React.useCallback(() => {
if (next) {
history.push(next);
}
}, [next]);
const handlePrev = React.useCallback(() => {
if (prev) {
history.push(prev);
}
}, [prev]);
const addMoreCount = stickersDuck.useAddMoreCount();
const toasts = stickersDuck.useToasts();
const { dismissToast } = stickersDuck.useStickerActions();
return (
<>
<main className={getClassName(props)}>{children}</main>
<footer className={styles.footer}>
{prev || onPrev ? (
<Button className={styles.button} onClick={onPrev || handlePrev}>
{prevText || i18n('StickerCreator--AppStage--prev')}
</Button>
) : null}
{addMoreCount > 0 ? (
<Text secondary>
{i18n('icu:StickerCreator--DropStage--addMore', {
count: addMoreCount,
})}
</Text>
) : null}
{next || onNext ? (
<Button
className={styles.button}
onClick={onNext || handleNext}
primary
disabled={!nextActive}
>
{nextText || i18n('StickerCreator--AppStage--next')}
</Button>
) : null}
</footer>
<Toaster
className={styles.toaster}
loaf={toasts.map((slice, id) => ({
id,
// eslint-disable-next-line local-rules/valid-i18n-keys
text: i18n(slice.key, slice.subs),
}))}
onDismiss={dismissToast}
/>
</>
);
}

View File

@ -1,27 +0,0 @@
// Copyright 2019 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
.message {
max-width: 450px;
}
.main {
flex-grow: 1;
margin-top: 16px;
display: flex;
flex-direction: column;
}
.info {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: flex-start;
}
.sticker-grid {
display: grid;
grid-template-columns: repeat(auto-fill, 186px);
grid-gap: 8px;
justify-content: center;
}

View File

@ -1,43 +0,0 @@
// Copyright 2019 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import * as React from 'react';
import { AppStage } from './AppStage';
import * as styles from './DropStage.scss';
import { H2, Text } from '../../elements/Typography';
import { LabeledCheckbox } from '../../elements/LabeledCheckbox';
import { StickerGrid } from '../../components/StickerGrid';
import { stickersDuck } from '../../store';
import { useI18n } from '../../util/i18n';
export function DropStage(): JSX.Element {
const i18n = useI18n();
const stickerPaths = stickersDuck.useStickerOrder();
const stickersReady = stickersDuck.useStickersReady();
const haveStickers = stickerPaths.length > 0;
const [showGuide, setShowGuide] = React.useState<boolean>(true);
const { resetStatus } = stickersDuck.useStickerActions();
React.useEffect(() => {
resetStatus();
}, [resetStatus]);
return (
<AppStage next="/add-emojis" nextActive={stickersReady}>
<H2>{i18n('icu:StickerCreator--DropStage--title')}</H2>
<div className={styles.info}>
<Text className={styles.message}>
{i18n('StickerCreator--DropStage--help')}
</Text>
{haveStickers ? (
<LabeledCheckbox onChange={setShowGuide} value={showGuide}>
{i18n('StickerCreator--DropStage--showMargins')}
</LabeledCheckbox>
) : null}
</div>
<div className={styles.main}>
<StickerGrid mode="add" showGuide={showGuide} />
</div>
</AppStage>
);
}

View File

@ -1,29 +0,0 @@
// Copyright 2019 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import * as React from 'react';
import { AppStage } from './AppStage';
import * as styles from './DropStage.scss';
import { H2, Text } from '../../elements/Typography';
import { StickerGrid } from '../../components/StickerGrid';
import { stickersDuck } from '../../store';
import { useI18n } from '../../util/i18n';
export function EmojiStage(): JSX.Element {
const i18n = useI18n();
const emojisReady = stickersDuck.useEmojisReady();
return (
<AppStage next="/add-meta" prev="/drop" nextActive={emojisReady}>
<H2>{i18n('StickerCreator--EmojiStage--title')}</H2>
<div className={styles.info}>
<Text className={styles.message}>
{i18n('StickerCreator--EmojiStage--help')}
</Text>
</div>
<div className={styles.main}>
<StickerGrid mode="pick-emoji" />
</div>
</AppStage>
);
}

View File

@ -1,75 +0,0 @@
// Copyright 2019 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
@import '../../../stylesheets/variables';
@import '../../mixins';
.main {
display: flex;
flex-direction: column;
align-items: center;
}
.row {
margin-bottom: 18px;
width: 448px;
}
.cover-container {
display: flex;
flex-direction: row;
justify-content: center;
padding: 18px;
}
.label {
user-select: none;
font-size: 13px;
font-family: $inter;
font-weight: 500;
margin: 0;
@include light-theme() {
color: $color-gray-90;
}
@include dark-theme() {
color: $color-white;
}
}
.cover-image {
width: 178px;
height: 178px;
}
.cover-frame {
composes: cover-image;
overflow: hidden;
border: {
radius: 4px;
style: solid;
width: 1px;
}
@include light-theme() {
border-color: $color-gray-60;
}
@include dark-theme() {
border-color: $color-gray-25;
}
}
.cover-frame-active {
composes: cover-frame;
@include light-theme() {
border-color: $color-ultramarine;
}
@include dark-theme() {
border-color: $color-ultramarine-light;
}
}

View File

@ -1,102 +0,0 @@
// Copyright 2019 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import * as React from 'react';
import type { FileWithPath } from 'react-dropzone';
import { AppStage } from './AppStage';
import * as styles from './MetaStage.scss';
import { processStickerImage } from '../../util/preload';
import { useStickerDropzone } from '../../util/useStickerDropzone';
import { history } from '../../util/history';
import { H2, Text } from '../../elements/Typography';
import { LabeledInput } from '../../elements/LabeledInput';
import { ConfirmModal } from '../../components/ConfirmModal';
import { stickersDuck } from '../../store';
import { useI18n } from '../../util/i18n';
export function MetaStage(): JSX.Element {
const i18n = useI18n();
const actions = stickersDuck.useStickerActions();
const valid = stickersDuck.useAllDataValid();
const cover = stickersDuck.useCover();
const title = stickersDuck.useTitle();
const author = stickersDuck.useAuthor();
const [confirming, setConfirming] = React.useState(false);
const onDrop = React.useCallback(
async ([{ path }]: Array<FileWithPath>) => {
try {
const stickerImage = await processStickerImage(path);
actions.setCover(stickerImage);
} catch (e) {
actions.removeSticker(path);
}
},
[actions]
);
const { getRootProps, getInputProps, isDragActive } =
useStickerDropzone(onDrop);
const onNext = React.useCallback(() => {
setConfirming(true);
}, [setConfirming]);
const onCancel = React.useCallback(() => {
setConfirming(false);
}, [setConfirming]);
const onConfirm = React.useCallback(() => {
history.push('/upload');
}, []);
const coverFrameClass = isDragActive
? styles.coverFrameActive
: styles.coverFrame;
return (
<AppStage onNext={onNext} nextActive={valid} noMessage prev="/add-emojis">
{confirming ? (
<ConfirmModal
title={i18n('StickerCreator--MetaStage--ConfirmDialog--title')}
confirm={i18n('StickerCreator--MetaStage--ConfirmDialog--confirm')}
onCancel={onCancel}
onConfirm={onConfirm}
>
{i18n('StickerCreator--MetaStage--ConfirmDialog--text')}
</ConfirmModal>
) : null}
<H2>{i18n('StickerCreator--MetaStage--title')}</H2>
<div className={styles.main}>
<div className={styles.row}>
<LabeledInput value={title} onChange={actions.setTitle}>
{i18n('StickerCreator--MetaStage--Field--title')}
</LabeledInput>
</div>
<div className={styles.row}>
<LabeledInput value={author} onChange={actions.setAuthor}>
{i18n('StickerCreator--MetaStage--Field--author')}
</LabeledInput>
</div>
<div className={styles.row}>
<h3 className={styles.label}>
{i18n('StickerCreator--MetaStage--Field--cover')}
</h3>
<Text>{i18n('StickerCreator--MetaStage--Field--cover--help')}</Text>
<div className={styles.coverContainer}>
<div {...getRootProps()} className={coverFrameClass}>
{cover?.src ? (
<img
className={styles.coverImage}
src={cover.src}
alt="Cover"
/>
) : null}
<input {...getInputProps()} />
</div>
</div>
</div>
</div>
</AppStage>
);
}

View File

@ -1,27 +0,0 @@
// Copyright 2019 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
@import '../../../stylesheets/variables';
@import '../../mixins';
.main {
display: flex;
flex-direction: column;
align-items: center;
}
.message {
max-width: 450px;
}
.call-to-action {
max-width: 500px;
}
.row {
margin-bottom: 18px;
display: flex;
flex-direction: row;
justify-content: center;
flex-shrink: 0;
}

View File

@ -1,97 +0,0 @@
// Copyright 2019 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import * as React from 'react';
import { AppStage } from './AppStage';
import * as styles from './ShareStage.scss';
import * as appStyles from './AppStage.scss';
import { history } from '../../util/history';
import { H2, Text } from '../../elements/Typography';
import { CopyText } from '../../elements/CopyText';
import { Toast } from '../../elements/Toast';
import { ShareButtons } from '../../components/ShareButtons';
import { StickerPackPreview } from '../../components/StickerPackPreview';
import { stickersDuck } from '../../store';
import { useI18n } from '../../util/i18n';
import { Intl } from '../../../ts/components/Intl';
export function ShareStage(): JSX.Element {
const i18n = useI18n();
const actions = stickersDuck.useStickerActions();
const title = stickersDuck.useTitle();
const author = stickersDuck.useAuthor();
const images = stickersDuck.useOrderedImagePaths();
const shareUrl = stickersDuck.usePackUrl();
const [linkCopied, setLinkCopied] = React.useState(false);
const onCopy = React.useCallback(() => {
setLinkCopied(true);
}, [setLinkCopied]);
const resetLinkCopied = React.useCallback(() => {
setLinkCopied(false);
}, [setLinkCopied]);
const handleNext = React.useCallback(() => {
window.close();
}, []);
const handlePrev = React.useCallback(() => {
actions.reset();
history.push('/');
}, [actions]);
return (
<AppStage
nextText={i18n('StickerCreator--ShareStage--close')}
onNext={handleNext}
nextActive
prevText={i18n('StickerCreator--ShareStage--createAnother')}
onPrev={handlePrev}
>
{shareUrl ? (
<>
<H2>{i18n('StickerCreator--ShareStage--title')}</H2>
<Text className={styles.message}>
{i18n('StickerCreator--ShareStage--help')}
</Text>
<div className={styles.main}>
<div className={styles.row}>
<StickerPackPreview
title={title}
author={author}
images={images}
/>
</div>
<div className={styles.row}>
<CopyText
value={shareUrl}
label={i18n('StickerCreator--ShareStage--copyTitle')}
onCopy={onCopy}
/>
</div>
<div className={styles.row}>
<Text className={styles.callToAction} center>
<Intl
i18n={i18n}
id="StickerCreator--ShareStage--callToAction"
components={[
<strong key="hashtag">#makeprivacystick</strong>,
]}
/>
</Text>
</div>
<div className={styles.row}>
<ShareButtons value={shareUrl} />
</div>
</div>
{linkCopied ? (
<div className={appStyles.toaster}>
<Toast onClick={resetLinkCopied}>
{i18n('StickerCreator--Toasts--linkedCopied')}
</Toast>
</div>
) : null}
</>
) : null}
</AppStage>
);
}

View File

@ -1,13 +0,0 @@
// Copyright 2019 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
.base {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.progress {
margin: 24px 0;
}

View File

@ -1,84 +0,0 @@
// Copyright 2019 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import * as React from 'react';
import { noop } from 'lodash';
import { AppStage } from './AppStage';
import * as styles from './UploadStage.scss';
import { history } from '../../util/history';
import { ProgressBar } from '../../elements/ProgressBar';
import { H2, Text } from '../../elements/Typography';
import { Button } from '../../elements/Button';
import { stickersDuck } from '../../store';
import { encryptAndUpload } from '../../util/preload';
import { useI18n } from '../../util/i18n';
import * as Errors from '../../../ts/types/errors';
const handleCancel = () => {
history.push('/add-meta');
};
export function UploadStage(): JSX.Element {
const i18n = useI18n();
const actions = stickersDuck.useStickerActions();
const cover = stickersDuck.useCover();
const title = stickersDuck.useTitle();
const author = stickersDuck.useAuthor();
const orderedData = stickersDuck.useSelectOrderedData();
const total = orderedData.length;
const [complete, setComplete] = React.useState(0);
React.useEffect(() => {
void (async () => {
const onProgress = () => {
setComplete(i => i + 1);
};
try {
if (!cover) {
throw new Error('UploadStage: Cover was missing on upload!');
}
const packMeta = await encryptAndUpload(
{ title, author },
orderedData,
cover,
onProgress
);
actions.setPackMeta(packMeta);
history.push('/share');
} catch (e) {
window.SignalContext.log.error(
'Error uploading pack:',
Errors.toLogFormat(e)
);
actions.addToast({
key: 'StickerCreator--Toasts--errorUploading',
subs: { message: e.message },
});
history.push('/add-meta');
}
})();
return noop;
}, [actions, title, author, cover, orderedData]);
return (
<AppStage empty>
<div className={styles.base}>
<H2>{i18n('StickerCreator--UploadStage--title')}</H2>
<Text>
{i18n('StickerCreator--UploadStage-uploaded', {
// We convert these to string so that 0 isn't falsy, which i18n checks for.
count: String(complete),
total: String(total),
})}
</Text>
<ProgressBar
count={complete}
total={total}
className={styles.progress}
/>
<Button onClick={handleCancel}>{i18n('cancel')}</Button>
</div>
</AppStage>
);
}

View File

@ -1,14 +0,0 @@
// Copyright 2019 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
.facade {
background: rgba(0, 0, 0, 0.33);
width: var(--window-width);
height: var(--window-height);
display: flex;
justify-content: center;
align-items: center;
position: fixed;
left: var(--window-border);
top: var(--titlebar-height);
}

View File

@ -1,44 +0,0 @@
// Copyright 2019 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import * as React from 'react';
import { createPortal } from 'react-dom';
import * as styles from './ConfirmModal.scss';
import type { Props } from '../elements/ConfirmDialog';
import { ConfirmDialog } from '../elements/ConfirmDialog';
export type Mode = 'removable' | 'pick-emoji' | 'add';
export const ConfirmModal = React.memo(function ConfirmModalInner(
props: Props
) {
const { onCancel } = props;
const [popperRoot, setPopperRoot] = React.useState<HTMLDivElement>();
// Create popper root and handle outside clicks
React.useEffect(() => {
const root = document.createElement('div');
setPopperRoot(root);
document.body.appendChild(root);
const handleOutsideClick = ({ target }: MouseEvent) => {
if (!root.contains(target as Node)) {
onCancel();
}
};
document.addEventListener('click', handleOutsideClick);
return () => {
document.body.removeChild(root);
document.removeEventListener('click', handleOutsideClick);
};
}, [onCancel]);
return popperRoot
? createPortal(
<div className={styles.facade}>
<ConfirmDialog {...props} />
</div>,
popperRoot
)
: null;
});

View File

@ -1,30 +0,0 @@
// Copyright 2019 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
@import '../../stylesheets/variables';
@import '../mixins';
.container {
display: flex;
justify-content: center;
}
.text {
@include light-theme() {
border: 1px solid $color-gray-15;
color: $color-gray-90;
}
@include dark-theme() {
border: 1px solid $color-gray-60;
color: $color-white;
}
}
.button {
width: 32px;
height: 32px;
background: transparent;
border: none;
margin-left: 12px;
}

View File

@ -1,26 +0,0 @@
// Copyright 2019 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import * as React from 'react';
import { text } from '@storybook/addon-knobs';
import { StoryRow } from '../elements/StoryRow';
import { ShareButtons } from './ShareButtons';
export default {
title: 'Sticker Creator/components',
};
export const _ShareButtons = (): JSX.Element => {
const value = text('value', 'https://signal.org');
return (
<StoryRow>
<ShareButtons value={value} />
</StoryRow>
);
};
_ShareButtons.story = {
name: 'ShareButtons',
};

View File

@ -1,77 +0,0 @@
// Copyright 2019 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
/* eslint-disable max-len */
import * as React from 'react';
import * as styles from './ShareButtons.scss';
import { useI18n } from '../util/i18n';
export type Props = {
value: string;
};
export const ShareButtons: React.ComponentType<Props> = React.memo(
function ShareButtonsInner({ value }) {
const i18n = useI18n();
const buttonPaths = React.useMemo<
Array<[string, string, string, string]>
>(() => {
const packUrl = encodeURIComponent(value);
const text = encodeURIComponent(
`${i18n('StickerCreator--ShareStage--socialMessage')} ${value}`
);
return [
// Facebook
[
i18n('StickerCreator--ShareButtons--facebook'),
'#4267B2',
'M20.155 10.656l-1.506.001c-1.181 0-1.41.561-1.41 1.384v1.816h2.817l-.367 2.845h-2.45V24h-2.937v-7.298h-2.456v-2.845h2.456V11.76c0-2.435 1.487-3.76 3.658-3.76 1.04 0 1.934.077 2.195.112v2.544z',
`https://www.facebook.com/sharer/sharer.php?u=${packUrl}`,
],
// Twitter
[
i18n('StickerCreator--ShareButtons--twitter'),
'#1CA1F2',
'M22.362 12.737c.006.141.01.282.01.425 0 4.337-3.302 9.339-9.34 9.339A9.294 9.294 0 018 21.027c.257.03.518.045.783.045a6.584 6.584 0 004.077-1.405 3.285 3.285 0 01-3.067-2.279 3.312 3.312 0 001.483-.057 3.283 3.283 0 01-2.633-3.218v-.042c.442.246.949.394 1.487.411a3.282 3.282 0 01-1.016-4.383 9.32 9.32 0 006.766 3.43 3.283 3.283 0 015.593-2.994 6.568 6.568 0 002.085-.796 3.299 3.299 0 01-1.443 1.816A6.587 6.587 0 0024 11.038a6.682 6.682 0 01-1.638 1.699',
`https://twitter.com/intent/tweet?text=${text}`,
],
// Pinterest
// [
// i18n('StickerCreator--ShareButtons--pinterest'),
// '#BD081C',
// 'M17.234 19.563c-.992 0-1.926-.536-2.245-1.146 0 0-.534 2.118-.646 2.527-.398 1.444-1.569 2.889-1.66 3.007-.063.083-.203.057-.218-.052-.025-.184-.324-2.007.028-3.493l1.182-5.008s-.293-.587-.293-1.454c0-1.362.789-2.379 1.772-2.379.836 0 1.239.628 1.239 1.38 0 .84-.535 2.097-.811 3.261-.231.975.489 1.77 1.451 1.77 1.74 0 2.913-2.236 2.913-4.886 0-2.014-1.356-3.522-3.824-3.522-2.787 0-4.525 2.079-4.525 4.402 0 .8.237 1.365.607 1.802.17.201.194.282.132.512-.045.17-.145.576-.188.738-.061.233-.249.316-.46.23-1.283-.524-1.882-1.931-1.882-3.511C9.806 11.13 12.008 8 16.374 8c3.51 0 5.819 2.538 5.819 5.265 0 3.605-2.005 6.298-4.959 6.298',
// `https://pinterest.com/pin/create/button/?url=${packUrl}`,
// ],
// Whatsapp
[
i18n('StickerCreator--ShareButtons--whatsapp'),
'#25D366',
'M16.033 23.862h-.003a7.914 7.914 0 01-3.79-.965L8.035 24l1.126-4.109a7.907 7.907 0 01-1.059-3.964C8.104 11.556 11.661 8 16.033 8c2.121 0 4.113.826 5.61 2.325a7.878 7.878 0 012.321 5.609c-.002 4.371-3.56 7.928-7.931 7.928zm3.88-5.101c-.165.463-.957.885-1.338.942a2.727 2.727 0 01-1.248-.078 11.546 11.546 0 01-1.13-.418c-1.987-.858-3.286-2.859-3.385-2.991-.1-.132-.81-1.074-.81-2.049 0-.975.513-1.455.695-1.653a.728.728 0 01.528-.248c.132 0 .264.001.38.007.122.006.285-.046.446.34.165.397.56 1.372.61 1.471.05.099.083.215.017.347-.066.132-.1.215-.198.331-.1.115-.208.258-.297.347-.1.098-.203.206-.087.404.116.198.513.847 1.102 1.372.757.675 1.396.884 1.594.984.198.099.314.082.429-.05.116-.132.496-.578.628-.777.132-.198.264-.165.446-.099.18.066 1.156.545 1.354.645.198.099.33.148.38.231.049.083.049.479-.116.942zm-3.877-9.422c-3.636 0-6.594 2.956-6.595 6.589 0 1.245.348 2.458 1.008 3.507l.157.249-.666 2.432 2.495-.654.24.142a6.573 6.573 0 003.355.919h.003a6.6 6.6 0 006.592-6.59 6.55 6.55 0 00-1.93-4.662 6.549 6.549 0 00-4.66-1.932z',
`https://wa.me?text=${text}`,
],
];
}, [i18n, value]);
return (
<div className={styles.container}>
{buttonPaths.map(([title, fill, path, url]) => (
<button
type="button"
key={path}
className={styles.button}
onClick={() => window.open(url)}
title={title}
>
<svg width={32} height={32}>
<circle cx="16" cy="16" r="16" fill={fill} />
<path d={path} fill="#FFF" fillRule="evenodd" />
</svg>
</button>
))}
</div>
);
}
);

View File

@ -1,143 +0,0 @@
// Copyright 2019 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
@import '../mixins';
@import '../../stylesheets/variables';
$width: 186px;
$height: 186px;
$guide-offset: 6px;
$border-width: 1px;
.container {
position: relative;
width: $width;
height: $height;
border: {
radius: 4px;
width: $border-width;
style: solid;
}
overflow: hidden;
user-select: none;
@include light-theme() {
border-color: $color-gray-25;
background: $color-white;
}
@include dark-theme() {
border-color: $color-gray-60;
background: $color-gray-90;
}
}
.dragActive {
composes: container;
@include light-theme() {
border-color: $color-ultramarine;
}
@include dark-theme() {
border-color: $color-ultramarine-light;
}
}
.image {
width: $width;
height: $height;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.spinner {
composes: image;
animation: spin 1s linear infinite;
display: flex;
justify-content: center;
align-items: center;
@include light-theme() {
color: $color-gray-25;
}
@include dark-theme() {
color: $color-gray-60;
}
}
.guide {
width: $width - (2 * $guide-offset);
height: $height - (2 * $guide-offset);
position: absolute;
left: $guide-offset - $border-width;
top: $guide-offset - $border-width;
border: {
radius: 0px;
width: $border-width;
style: dashed;
}
pointer-events: none;
@include light-theme() {
border-color: $color-gray-25;
}
@include dark-theme() {
border-color: $color-gray-60;
}
}
.close-button {
width: 16px;
height: 16px;
position: absolute;
display: flex;
justify-content: center;
align-items: center;
top: 8px;
right: 8px;
font-family: $inter;
border: none;
background: none;
padding: 0;
&-icon {
@include light-theme() {
color: $color-black;
}
@include dark-theme() {
color: $color-white;
}
}
}
.emoji-button {
width: 41px;
height: 28px;
position: absolute;
top: 6px;
right: 6px;
border: none;
border-radius: 13.5px;
padding: 0;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
@include light-theme() {
background-color: $color-gray-05;
color: $color-gray-90;
}
@include dark-theme() {
background-color: $color-gray-75;
color: rgba(255, 255, 255, 0.75);
}
}

View File

@ -1,78 +0,0 @@
// Copyright 2019 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import * as React from 'react';
import { boolean, select, text } from '@storybook/addon-knobs';
import { action } from '@storybook/addon-actions';
import { StoryRow } from '../elements/StoryRow';
import { StickerFrame } from './StickerFrame';
import type { EmojiPickDataType } from '../../ts/components/emoji/EmojiPicker';
export default {
title: 'Sticker Creator/components',
};
export const _StickerFrame = (): JSX.Element => {
const image = text('image url', '/fixtures/512x515-thumbs-up-lincoln.webp');
const showGuide = boolean('show guide', true);
const mode = select('mode', ['removable', 'pick-emoji', 'add'], 'add');
const onRemove = action('onRemove');
const onDrop = action('onDrop');
const [skinTone, setSkinTone] = React.useState(0);
const [emoji, setEmoji] = React.useState<EmojiPickDataType | undefined>(
undefined
);
return (
<StoryRow top>
<StickerFrame
id="1337"
emojiData={emoji}
image={image}
mode={mode}
showGuide={showGuide}
onRemove={onRemove}
skinTone={skinTone}
onSetSkinTone={setSkinTone}
onPickEmoji={e => setEmoji(e.emoji)}
onDrop={onDrop}
/>
</StoryRow>
);
};
_StickerFrame.story = {
name: 'StickerFrame, add sticker',
};
export function 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',
};

View File

@ -1,304 +0,0 @@
// Copyright 2019 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import * as React from 'react';
import { createPortal } from 'react-dom';
import { SortableHandle } from 'react-sortable-hoc';
import { noop } from 'lodash';
import {
Manager as PopperManager,
Popper,
Reference as PopperReference,
} from 'react-popper';
import { AddEmoji } from '../elements/icons';
import type { Props as DropZoneProps } from '../elements/DropZone';
import { DropZone } from '../elements/DropZone';
import { StickerPreview } from '../elements/StickerPreview';
import * as styles from './StickerFrame.scss';
import type {
EmojiPickDataType,
Props as EmojiPickerProps,
} from '../../ts/components/emoji/EmojiPicker';
import { EmojiPicker } from '../../ts/components/emoji/EmojiPicker';
import { Emoji } from '../../ts/components/emoji/Emoji';
import { PopperRootContext } from '../../ts/components/PopperRootContext';
import { useI18n } from '../util/i18n';
export type Mode = 'removable' | 'pick-emoji' | 'add';
export type Props = Partial<
Pick<EmojiPickerProps, 'skinTone' | 'onSetSkinTone'>
> &
Partial<Pick<DropZoneProps, 'onDrop'>> & {
readonly id?: string;
readonly emojiData?: EmojiPickDataType;
readonly image?: string;
readonly mode?: Mode;
readonly showGuide?: boolean;
onPickEmoji?({
id,
emoji,
}: {
id: string;
emoji: EmojiPickDataType;
}): unknown;
onRemove?(id: string): unknown;
};
const spinnerSvg = (
<svg width={56} height={56}>
<path d="M52.36 14.185A27.872 27.872 0 0156 28c0 15.464-12.536 28-28 28v-2c14.36 0 26-11.64 26-26 0-4.66-1.226-9.033-3.372-12.815l1.732-1z" />
</svg>
);
const closeSvg = (
<svg
viewBox="0 0 16 16"
width="16px"
height="16px"
className={styles.closeButtonIcon}
>
<path d="M13.4 3.3l-.8-.6L8 7.3 3.3 2.7l-.6.6L7.3 8l-4.6 4.6.6.8L8 8.7l4.6 4.7.8-.8L8.7 8z" />
</svg>
);
const ImageHandle = SortableHandle((props: { src: string }) => (
<img className={styles.image} {...props} alt="Sticker" />
));
export const StickerFrame = React.memo(function StickerFrameInner({
id,
emojiData,
image,
showGuide,
mode,
onRemove,
onPickEmoji,
skinTone,
onSetSkinTone,
onDrop,
}: Props) {
const i18n = useI18n();
const [emojiPickerOpen, setEmojiPickerOpen] = React.useState(false);
const [emojiPopperRoot, setEmojiPopperRoot] =
React.useState<HTMLElement | null>(null);
const [previewActive, setPreviewActive] = React.useState(false);
const [previewPopperRoot, setPreviewPopperRoot] =
React.useState<HTMLElement | null>(null);
const timerRef = React.useRef<number>();
const handleToggleEmojiPicker = React.useCallback(() => {
setEmojiPickerOpen(open => !open);
}, [setEmojiPickerOpen]);
const handlePickEmoji = React.useCallback(
(emoji: EmojiPickDataType) => {
if (!id) {
return;
}
if (!onPickEmoji) {
throw new Error(
'StickerFrame/handlePickEmoji: onPickEmoji was not provided!'
);
}
onPickEmoji({ id, emoji });
setEmojiPickerOpen(false);
},
[id, onPickEmoji, setEmojiPickerOpen]
);
const handleRemove = React.useCallback(() => {
if (!id) {
return;
}
if (!onRemove) {
throw new Error('StickerFrame/handleRemove: onRemove was not provided!');
}
onRemove(id);
}, [onRemove, id]);
const handleMouseEnter = React.useCallback(() => {
window.clearTimeout(timerRef.current);
timerRef.current = window.setTimeout(() => {
setPreviewActive(true);
}, 500);
}, [timerRef, setPreviewActive]);
const handleMouseLeave = React.useCallback(() => {
clearTimeout(timerRef.current);
setPreviewActive(false);
}, [timerRef, setPreviewActive]);
React.useEffect(
() => () => {
clearTimeout(timerRef.current);
},
[timerRef]
);
const { createRoot, removeRoot } = React.useContext(PopperRootContext);
// Create popper root and handle outside clicks
React.useEffect(() => {
if (emojiPickerOpen) {
const root = createRoot();
setEmojiPopperRoot(root);
const handleOutsideClick = ({ target }: MouseEvent) => {
if (!root.contains(target as Node)) {
setEmojiPickerOpen(false);
}
};
document.addEventListener('click', handleOutsideClick);
return () => {
removeRoot(root);
setEmojiPopperRoot(null);
document.removeEventListener('click', handleOutsideClick);
};
}
return noop;
}, [
createRoot,
emojiPickerOpen,
removeRoot,
setEmojiPickerOpen,
setEmojiPopperRoot,
]);
React.useEffect(() => {
if (mode !== 'pick-emoji' && image && previewActive) {
const root = createRoot();
setPreviewPopperRoot(root);
return () => {
removeRoot(root);
};
}
return noop;
}, [
createRoot,
image,
mode,
previewActive,
removeRoot,
setPreviewPopperRoot,
]);
const [dragActive, setDragActive] = React.useState<boolean>(false);
const containerClass = dragActive ? styles.dragActive : styles.container;
return (
<PopperManager>
<PopperReference>
{({ ref: rootRef }) => (
<div
className={containerClass}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
ref={rootRef}
>
{
// eslint-disable-next-line no-nested-ternary
mode !== 'add' ? (
image ? (
<ImageHandle src={image} />
) : (
<div className={styles.spinner}>{spinnerSvg}</div>
)
) : null
}
{showGuide && mode !== 'add' ? (
<div className={styles.guide} />
) : null}
{mode === 'add' && onDrop ? (
<DropZone
label={i18n('StickerCreator--DropStage--dragDrop')}
onDrop={onDrop}
inner
onDragActive={setDragActive}
/>
) : null}
{mode === 'removable' ? (
<button
type="button"
aria-label={i18n('StickerCreator--DropStage--removeSticker')}
className={styles.closeButton}
onClick={handleRemove}
// Reverse the mouseenter/leave logic for the remove button so
// we don't accidentally cover the remove button
onMouseEnter={handleMouseLeave}
onMouseLeave={handleMouseEnter}
>
{closeSvg}
</button>
) : null}
{mode === 'pick-emoji' ? (
<PopperManager>
<PopperReference>
{({ ref }) => (
<button
type="button"
ref={ref}
className={styles.emojiButton}
onClick={handleToggleEmojiPicker}
>
{emojiData ? (
<Emoji {...emojiData} size={24} />
) : (
<AddEmoji />
)}
</button>
)}
</PopperReference>
{emojiPickerOpen && emojiPopperRoot
? createPortal(
<Popper placement="bottom-start">
{({ ref, style }) => (
<EmojiPicker
ref={ref}
style={{ ...style, marginTop: '8px' }}
i18n={i18n}
onPickEmoji={handlePickEmoji}
skinTone={skinTone}
onSetSkinTone={onSetSkinTone}
onClose={handleToggleEmojiPicker}
/>
)}
</Popper>,
emojiPopperRoot
)
: null}
</PopperManager>
) : null}
{mode !== 'pick-emoji' &&
image &&
previewActive &&
previewPopperRoot
? createPortal(
<Popper
placement="bottom"
modifiers={[
{ name: 'offset', options: { offset: [undefined, 8] } },
]}
>
{({ ref, style, arrowProps, placement }) => (
<StickerPreview
ref={ref}
style={style}
image={image}
arrowProps={arrowProps}
placement={placement}
/>
)}
</Popper>,
previewPopperRoot
)
: null}
</div>
)}
</PopperReference>
</PopperManager>
);
});

View File

@ -1,16 +0,0 @@
// Copyright 2019 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
.grid {
display: grid;
grid-template-columns: repeat(auto-fill, 186px);
grid-template-rows: repeat(auto-fill, 186px);
grid-gap: 8px;
justify-content: center;
}
.drop {
display: flex;
flex-direction: column;
flex-grow: 1;
}

View File

@ -1,140 +0,0 @@
// Copyright 2019 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import * as React from 'react';
import PQueue from 'p-queue';
import type { SortEndHandler } from 'react-sortable-hoc';
import { SortableContainer, SortableElement } from 'react-sortable-hoc';
import * as styles from './StickerGrid.scss';
import type { Props as StickerFrameProps } from './StickerFrame';
import { StickerFrame } from './StickerFrame';
import { stickersDuck } from '../store';
import type { Props as DropZoneProps } from '../elements/DropZone';
import { DropZone } from '../elements/DropZone';
import { processStickerImage } from '../util/preload';
import { useI18n } from '../util/i18n';
import { MINUTE } from '../../ts/util/durations';
import { drop } from '../../ts/util/drop';
import * as Errors from '../../ts/types/errors';
const queue = new PQueue({ concurrency: 3, timeout: MINUTE * 30 });
type SmartStickerFrameProps = Omit<StickerFrameProps, 'id'> & { id: string };
const SmartStickerFrame = SortableElement(
({ id, showGuide, mode }: SmartStickerFrameProps) => {
const data = stickersDuck.useStickerData(id);
const actions = stickersDuck.useStickerActions();
const image = data.imageData ? data.imageData.src : undefined;
return (
<StickerFrame
id={id}
showGuide={showGuide}
mode={mode}
image={image}
onRemove={actions.removeSticker}
onPickEmoji={actions.setEmoji}
emojiData={data.emoji}
/>
);
}
);
export type Props = Pick<StickerFrameProps, 'showGuide' | 'mode'>;
export type InnerGridProps = Props & {
ids: Array<string>;
};
const InnerGrid = SortableContainer(
({ ids, mode, showGuide }: InnerGridProps) => {
const i18n = useI18n();
const containerClassName = ids.length > 0 ? styles.grid : styles.drop;
const frameMode = mode === 'add' ? 'removable' : 'pick-emoji';
const actions = stickersDuck.useStickerActions();
const handleDrop = React.useCallback<DropZoneProps['onDrop']>(
async paths => {
actions.initializeStickers(paths);
paths.forEach(path => {
drop(
queue.add(async () => {
try {
const stickerImage = await processStickerImage(path);
actions.addImageData(stickerImage);
} catch (e) {
window.SignalContext.log.error(
'Error processing image:',
Errors.toLogFormat(e)
);
actions.removeSticker(path);
const key =
e instanceof window.ProcessStickerImageError
? e.errorMessageI18nKey
: 'StickerCreator--Toasts--errorProcessing';
actions.addToast({
key,
});
}
})
);
});
},
[actions]
);
return (
<div className={containerClassName}>
{ids.length > 0 ? (
<>
{ids.map((p, i) => (
<SmartStickerFrame
key={p}
index={i}
id={p}
showGuide={showGuide}
mode={frameMode}
/>
))}
{mode === 'add' && ids.length < stickersDuck.maxStickers ? (
<StickerFrame
showGuide={showGuide}
mode="add"
onDrop={handleDrop}
/>
) : null}
</>
) : (
<DropZone
label={i18n('StickerCreator--DropStage--dragDrop')}
onDrop={handleDrop}
/>
)}
</div>
);
}
);
export const StickerGrid = SortableContainer((props: Props) => {
const ids = stickersDuck.useStickerOrder();
const actions = stickersDuck.useStickerActions();
const handleSortEnd = React.useCallback<SortEndHandler>(
sortEnd => {
actions.moveSticker(sortEnd);
},
[actions]
);
return (
<InnerGrid
{...props}
ids={ids}
axis="xy"
onSortEnd={handleSortEnd}
useDragHandle
/>
);
});

View File

@ -1,129 +0,0 @@
// Copyright 2019 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
@import '../mixins';
@import '../../stylesheets/variables';
@mixin background() {
@include light-theme() {
background: $color-white;
}
@include dark-theme() {
background: $color-gray-75;
}
}
.container {
position: relative;
width: 330px;
height: 270px;
border-radius: 3px;
overflow: hidden;
box-shadow: 0 3px 9px 0px rgba(0, 0, 0, 0.2);
@include background();
}
.title-bar {
height: 27px;
padding: 0 12px;
display: flex;
flex-direction: row;
align-items: center;
font: {
family: $inter;
size: 10.5px;
weight: 500;
}
@include background();
@include light-theme {
color: $color-gray-90;
}
@include dark-theme {
color: $color-gray-05;
}
}
.scroller {
height: calc(100% - 27px);
padding-bottom: 57px;
overflow: auto;
}
.grid {
display: grid;
grid-gap: 6px;
padding: 0 16px 0 12px;
grid-template-columns: repeat(4, 1fr);
overflow: auto;
justify-items: center;
}
.sticker {
width: 72px;
height: 72px;
}
.meta {
width: 306px;
height: 39px;
border-radius: 3px;
padding: 0 9px;
display: flex;
flex-direction: column;
justify-content: center;
position: absolute;
left: 12px;
bottom: 12px;
@include light-theme {
background: $color-gray-05;
}
@include dark-theme {
background: $color-gray-60;
}
}
.text {
font-family: $inter;
}
.meta-title {
composes: text;
height: 15px;
line-height: 15px;
font: {
size: 12px;
weight: 500;
}
@include light-theme {
color: $color-gray-90;
}
@include dark-theme {
color: $color-gray-05;
}
}
.meta-author {
composes: text;
height: 14px;
line-height: 14px;
font: {
size: 10px;
weight: normal;
}
@include light-theme {
color: $color-gray-60;
}
@include dark-theme {
color: $color-gray-25;
}
}

View File

@ -1,29 +0,0 @@
// Copyright 2019 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import * as React from 'react';
import { text } from '@storybook/addon-knobs';
import { StoryRow } from '../elements/StoryRow';
import { StickerPackPreview } from './StickerPackPreview';
export default {
title: 'Sticker Creator/components',
};
export const _StickerPackPreview = (): JSX.Element => {
const image = text('image url', '/fixtures/512x515-thumbs-up-lincoln.webp');
const title = text('title', 'Sticker pack title');
const author = text('author', 'Sticker pack author');
const images = React.useMemo(() => Array(39).fill(image), [image]);
return (
<StoryRow top>
<StickerPackPreview images={images} title={title} author={author} />
</StoryRow>
);
};
_StickerPackPreview.story = {
name: 'StickerPackPreview',
};

View File

@ -1,39 +0,0 @@
// Copyright 2019 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import * as React from 'react';
import * as styles from './StickerPackPreview.scss';
import { useI18n } from '../util/i18n';
export type Props = {
images: Array<string>;
title: string;
author: string;
};
export const StickerPackPreview = React.memo(function StickerPackPreviewInner({
images,
title,
author,
}: Props) {
const i18n = useI18n();
return (
<div className={styles.container}>
<div className={styles.titleBar}>
{i18n('StickerCreator--Preview--title')}
</div>
<div className={styles.scroller}>
<div className={styles.grid}>
{images.map(src => (
<img key={src} className={styles.sticker} src={src} alt={src} />
))}
</div>
</div>
<div className={styles.meta}>
<div className={styles.metaTitle}>{title}</div>
<div className={styles.metaAuthor}>{author}</div>
</div>
</div>
);
});

View File

@ -1,39 +0,0 @@
// Copyright 2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import * as React from 'react';
import { debounce, dropRight } from 'lodash';
import { text as textKnob } from '@storybook/addon-knobs';
import { StoryRow } from '../elements/StoryRow';
import { Toaster } from './Toaster';
export default {
title: 'Sticker Creator/components',
};
export const _Toaster = (): JSX.Element => {
const inputText = textKnob('Slices', ['error 1', 'error 2'].join('|'));
const initialState = React.useMemo(() => inputText.split('|'), [inputText]);
const [state, setState] = React.useState(initialState);
// TODO not sure how to fix this
// eslint-disable-next-line react-hooks/exhaustive-deps
const handleDismiss = React.useCallback(
// Debounce is required here since auto-dismiss is asynchronously called
// from multiple rendered instances (multiple themes)
debounce(() => {
setState(dropRight);
}, 10),
[setState]
);
return (
<StoryRow>
<Toaster
loaf={state.map((text, id) => ({ id, text }))}
onDismiss={handleDismiss}
/>
</StoryRow>
);
};

View File

@ -1,47 +0,0 @@
// Copyright 2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import * as React from 'react';
import { last, noop } from 'lodash';
import { Toast } from '../elements/Toast';
export type Props = React.HTMLAttributes<HTMLDivElement> & {
loaf: Array<{ id: number; text: string }>;
onDismiss: () => unknown;
};
const DEFAULT_DISMISS = 1e4;
export const Toaster = React.memo(function ToasterInner({
loaf,
onDismiss,
className,
}: Props) {
const slice = last(loaf);
React.useEffect(() => {
if (!slice) {
return noop;
}
const timer = setTimeout(() => {
onDismiss();
}, DEFAULT_DISMISS);
return () => {
clearTimeout(timer);
};
}, [slice, onDismiss]);
if (!slice) {
return null;
}
return (
<div className={className}>
<Toast key={slice.id} onClick={onDismiss} tabIndex={0}>
{slice.text}
</Toast>
</div>
);
});

View File

@ -1,4 +0,0 @@
// Copyright 2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
declare module '*.scss';

View File

@ -1,83 +0,0 @@
// Copyright 2019 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
@import '../mixins';
@import '../../stylesheets/variables';
.base {
border: none;
min-width: 80px;
height: 36px;
padding: 0 25px;
border-radius: 4px;
display: flex;
justify-content: center;
align-items: center;
font-family: $inter;
font-weight: normal;
font-size: 14px;
white-space: nowrap;
@include light-theme() {
background-color: $color-gray-05;
color: $color-gray-90;
}
@include dark-theme() {
background-color: $color-gray-75;
color: $color-white;
}
&:disabled {
opacity: 0.4;
}
}
.primary {
composes: base;
@include light-theme() {
background-color: $color-ultramarine;
color: $color-white;
}
@include dark-theme() {
background-color: $color-ultramarine-light;
color: $color-white;
}
}
.pill {
composes: base;
height: 28px;
border-radius: 15px;
padding: 0 17px;
@include light-theme() {
color: $color-gray-90;
border: 1px solid $color-gray-90;
background: transparent;
}
@include dark-theme() {
color: $color-white;
border: 1px solid $color-white;
background: transparent;
}
}
.pill-primary {
composes: pill;
@include light-theme() {
border: none;
background-color: $color-ultramarine;
color: $color-white;
}
@include dark-theme() {
border: none;
background-color: $color-ultramarine-light;
color: $color-white;
}
}

View File

@ -1,61 +0,0 @@
// Copyright 2019 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import * as React from 'react';
import { action } from '@storybook/addon-actions';
import { text } from '@storybook/addon-knobs';
import { StoryRow } from './StoryRow';
import { Button } from './Button';
export default {
title: 'Sticker Creator/elements',
};
export const _Button = (): JSX.Element => {
const onClick = action('onClick');
const child = text('text', 'foo bar');
return (
<>
<StoryRow>
<Button onClick={onClick} primary>
{child}
</Button>
</StoryRow>
<StoryRow>
<Button onClick={onClick} primary disabled>
{child}
</Button>
</StoryRow>
<StoryRow>
<Button onClick={onClick}>{child}</Button>
</StoryRow>
<StoryRow>
<Button onClick={onClick} disabled>
{child}
</Button>
</StoryRow>
<StoryRow>
<Button onClick={onClick} primary pill>
{child}
</Button>
</StoryRow>
<StoryRow>
<Button onClick={onClick} primary pill disabled>
{child}
</Button>
</StoryRow>
<StoryRow>
<Button onClick={onClick} pill>
{child}
</Button>
</StoryRow>
<StoryRow>
<Button onClick={onClick} pill disabled>
{child}
</Button>
</StoryRow>
</>
);
};

View File

@ -1,44 +0,0 @@
// Copyright 2019 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React from 'react';
import classnames from 'classnames';
import * as styles from './Button.scss';
export type Props = React.ButtonHTMLAttributes<HTMLButtonElement> & {
pill?: boolean;
primary?: boolean;
};
const getClassName = ({ primary, pill }: Props) => {
if (pill && primary) {
return styles.pillPrimary;
}
if (pill) {
return styles.pill;
}
if (primary) {
return styles.primary;
}
return styles.base;
};
export function Button({
className,
children,
...otherProps
}: React.PropsWithChildren<Props>): JSX.Element {
return (
<button
type="button"
className={classnames(getClassName(otherProps), className)}
{...otherProps}
>
{children}
</button>
);
}

View File

@ -1,101 +0,0 @@
// Copyright 2019 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
@import '../mixins';
@import '../../stylesheets/variables';
.base {
width: 468px;
height: 138px;
padding: 16px 16px 8px 16px;
display: grid;
flex-direction: column;
grid-template-rows: 33px 1fr 28px;
border-radius: 8px;
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.08), 0 8px 20px 0 rgba(0, 0, 0, 0.33);
@include light-theme() {
background: $color-white;
}
@include dark-theme() {
background: $color-gray-75;
}
}
.text {
font: {
family: $inter;
size: 14px;
}
margin: 0;
@include light-theme() {
color: $color-gray-90;
}
@include dark-theme() {
color: $color-gray-05;
}
}
.title {
composes: text;
font-weight: 500;
@include light-theme() {
color: $color-gray-90;
}
@include dark-theme() {
color: $color-white;
}
}
.bottom {
display: flex;
flex-direction: row;
justify-content: flex-end;
align-content: flex-end;
}
.button {
min-width: 64px;
height: 28px;
border-radius: 14px;
background: transparent;
margin-left: 4px;
text-align: center;
font: {
family: $inter;
weight: 500;
size: 13px;
}
@include light-theme() {
color: $color-gray-60;
border-color: $color-gray-60;
}
@include dark-theme() {
color: $color-gray-25;
border-color: $color-gray-25;
}
}
.button-primary {
composes: button;
@include light-theme() {
color: $color-white;
border-color: $color-ultramarine;
background: $color-ultramarine;
}
@include dark-theme() {
color: $color-white;
border-color: $color-ultramarine-light;
background: $color-ultramarine-light;
}
}

View File

@ -1,39 +0,0 @@
// Copyright 2019 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import * as React from 'react';
import { text } from '@storybook/addon-knobs';
import { action } from '@storybook/addon-actions';
import { StoryRow } from './StoryRow';
import { ConfirmDialog } from './ConfirmDialog';
export default {
title: 'Sticker Creator/elements',
};
export const _ConfirmDialog = (): JSX.Element => {
const title = text('title', 'Foo bar banana baz?');
const child = text(
'text',
'Yadda yadda yadda yadda yadda yadda foo bar banana baz.'
);
const confirm = text('confirm', 'Upload');
const cancel = text('cancel', 'Cancel');
return (
<StoryRow>
<ConfirmDialog
{...{ title, confirm, cancel }}
onConfirm={action('onConfirm')}
onCancel={action('onCancel')}
>
{child}
</ConfirmDialog>
</StoryRow>
);
};
_ConfirmDialog.story = {
name: 'ConfirmDialog',
};

View File

@ -1,46 +0,0 @@
// Copyright 2019 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import * as React from 'react';
import * as styles from './ConfirmDialog.scss';
import { useI18n } from '../util/i18n';
export type Props = {
readonly title: string;
readonly children: React.ReactNode;
readonly confirm: string;
readonly onConfirm: () => unknown;
readonly cancel?: string;
readonly onCancel: () => unknown;
};
export function ConfirmDialog({
title,
children,
confirm,
cancel,
onConfirm,
onCancel,
}: Props): JSX.Element {
const i18n = useI18n();
const cancelText = cancel || i18n('StickerCreator--ConfirmDialog--cancel');
return (
<div className={styles.base}>
<h1 className={styles.title}>{title}</h1>
<p className={styles.text}>{children}</p>
<div className={styles.bottom}>
<button type="button" className={styles.button} onClick={onCancel}>
{cancelText}
</button>
<button
type="button"
className={styles.buttonPrimary}
onClick={onConfirm}
>
{confirm}
</button>
</div>
</div>
);
}

View File

@ -1,39 +0,0 @@
// Copyright 2019 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
@import '../../stylesheets/variables';
@import '../mixins';
.container {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
width: 330px;
height: 36px;
}
.input {
width: 242px;
height: 36px;
line-height: 34px;
padding: 0 12px;
border-radius: 4px;
background-color: transparent;
font-size: 14px;
font-family: $inter;
&::placeholder {
color: $color-gray-45;
}
@include light-theme() {
border: 1px solid $color-gray-15;
color: $color-gray-90;
}
@include dark-theme() {
border: 1px solid $color-gray-60;
color: $color-white;
}
}

View File

@ -1,27 +0,0 @@
// Copyright 2019 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import * as React from 'react';
import { text } from '@storybook/addon-knobs';
import { StoryRow } from './StoryRow';
import { CopyText } from './CopyText';
export default {
title: 'Sticker Creator/elements',
};
export const _CopyText = (): JSX.Element => {
const label = text('label', 'foo bar');
const value = text('value', 'foo bar');
return (
<StoryRow>
<CopyText label={label} value={value} />
</StoryRow>
);
};
_CopyText.story = {
name: 'CopyText',
};

View File

@ -1,41 +0,0 @@
// Copyright 2019 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import * as React from 'react';
import copy from 'copy-text-to-clipboard';
import * as styles from './CopyText.scss';
import { Button } from './Button';
import { useI18n } from '../util/i18n';
export type Props = {
value: string;
label: string;
onCopy?: () => unknown;
};
export const CopyText: React.ComponentType<Props> = React.memo(
function CopyTextInner({ label, onCopy, value }) {
const i18n = useI18n();
const handleClick = React.useCallback(() => {
copy(value);
if (onCopy) {
onCopy();
}
}, [onCopy, value]);
return (
<div className={styles.container}>
<input
type="text"
className={styles.input}
value={value}
aria-label={label}
readOnly
/>
<Button onClick={handleClick}>
{i18n('StickerCreator--CopyText--button')}
</Button>
</div>
);
}
);

View File

@ -1,64 +0,0 @@
// Copyright 2019 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
@import '../../stylesheets/variables';
@import '../mixins';
.base {
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
flex-grow: 1;
@include light-theme() {
color: $color-gray-60;
}
@include dark-theme() {
color: $color-gray-45;
}
}
.text {
margin: 16px 0 0 0;
font-family: $inter;
font-size: 14px;
font-weight: normal;
@include light-theme() {
color: $color-gray-60;
}
@include dark-theme() {
color: $color-gray-45;
}
}
.standalone {
composes: base;
border-radius: 4px;
border: 2px solid;
@include light-theme() {
border-color: $color-gray-25;
}
@include dark-theme() {
border-color: $color-gray-60;
}
}
.active {
composes: standalone;
@include light-theme() {
border-color: $color-ultramarine;
}
@include dark-theme() {
border-color: $color-ultramarine-light;
}
}

View File

@ -1,19 +0,0 @@
// Copyright 2019 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import * as React from 'react';
import { action } from '@storybook/addon-actions';
import { DropZone } from './DropZone';
export default {
title: 'Sticker Creator/elements',
};
export const _DropZone = (): JSX.Element => {
return <DropZone label="This is the label" onDrop={action('onDrop')} />;
};
_DropZone.story = {
name: 'DropZone',
};

View File

@ -1,70 +0,0 @@
// Copyright 2019 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import * as React from 'react';
import type { FileWithPath } from 'react-dropzone';
import * as styles from './DropZone.scss';
import { useI18n } from '../util/i18n';
import { useStickerDropzone } from '../util/useStickerDropzone';
import { isNotNil } from '../../ts/util/isNotNil';
export type Props = {
readonly inner?: boolean;
readonly label: string;
onDrop(files: Array<string>): unknown;
onDragActive?(active: boolean): unknown;
};
const getClassName = ({ inner }: Props, isDragActive: boolean) => {
if (inner) {
return styles.base;
}
if (isDragActive) {
return styles.active;
}
return styles.standalone;
};
export function DropZone(props: Props): JSX.Element {
const { inner, label, onDrop, onDragActive } = props;
const i18n = useI18n();
const handleDrop = React.useCallback(
(files: ReadonlyArray<FileWithPath>) => {
onDrop(files.map(({ path }) => path).filter(isNotNil));
},
[onDrop]
);
const { getRootProps, getInputProps, isDragActive } =
useStickerDropzone(handleDrop);
React.useEffect(() => {
if (onDragActive) {
onDragActive(isDragActive);
}
}, [isDragActive, onDragActive]);
return (
<div
{...getRootProps({ className: getClassName(props, isDragActive) })}
role="button"
aria-label={label}
>
<input {...getInputProps()} />
<svg viewBox="0 0 36 36" width="36px" height="36px">
<path d="M32 17.25H18.75V4h-1.5v13.25H4v1.5h13.25V32h1.5V18.75H32v-1.5z" />
</svg>
{!inner ? (
<p className={styles.text}>
{isDragActive
? i18n('StickerCreator--DropZone--activeText')
: i18n('StickerCreator--DropZone--staticText')}
</p>
) : null}
</div>
);
}

View File

@ -1,60 +0,0 @@
// Copyright 2019 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
@import '../../stylesheets/variables';
@import '../mixins';
.base {
display: flex;
flex-direction: row;
align-items: center;
padding: 2px;
// We'd really like to use focus-within-visible or :has(:focus-visible), to ensure that
// this doesn't show when using the mouse, but neither are ready yet!
&:focus-within {
outline: 2px solid -webkit-focus-ring-color;
}
}
.input {
position: absolute;
width: 0;
height: 0;
opacity: 0;
}
.checkbox {
width: 18px;
height: 18px;
border-radius: 2px;
display: flex;
justify-content: center;
align-items: center;
border: {
width: 2px;
style: solid;
}
@include light-theme() {
border-color: $color-gray-60;
}
@include dark-theme() {
border-color: $color-gray-25;
}
}
.checkbox-checked {
composes: checkbox;
border: none;
background-color: $color-ultramarine;
color: $color-white;
}
.label {
margin-left: 6px;
position: relative;
user-select: none;
}

View File

@ -1,25 +0,0 @@
// Copyright 2019 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import * as React from 'react';
import { text } from '@storybook/addon-knobs';
import { StoryRow } from './StoryRow';
import { LabeledCheckbox } from './LabeledCheckbox';
export default {
title: 'Sticker Creator/elements',
};
export const _LabeledCheckbox = (): JSX.Element => {
const child = text('label', 'foo bar');
const [checked, setChecked] = React.useState(false);
return (
<StoryRow>
<LabeledCheckbox value={checked} onChange={setChecked}>
{child}
</LabeledCheckbox>
</StoryRow>
);
};

View File

@ -1,46 +0,0 @@
// Copyright 2019 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import * as React from 'react';
import * as styles from './LabeledCheckbox.scss';
import { Inline } from './Typography';
export type Props = {
children: React.ReactNode;
value?: boolean;
onChange?: (value: boolean) => unknown;
};
const checkSvg = (
<svg viewBox="0 0 16 16" width="16px" height="16px">
<path d="M7 11.5c-.2 0-.4-.1-.5-.2L3.3 8.1 4.4 7 7 9.7l4.6-4.6 1.1 1.1-5.2 5.2c-.1 0-.3.1-.5.1z" />
</svg>
);
export const LabeledCheckbox = React.memo(function LabeledCheckboxInner({
children,
value,
onChange,
}: Props) {
const handleChange = React.useCallback(() => {
if (onChange !== undefined) {
onChange(!value);
}
}, [onChange, value]);
const className = value ? styles.checkboxChecked : styles.checkbox;
return (
<label className={styles.base}>
<input
type="checkbox"
className={styles.input}
checked={value}
aria-checked={value}
onChange={handleChange}
/>
<span className={className}>{value ? checkSvg : null}</span>
<Inline className={styles.label}>{children}</Inline>
</label>
);
});

View File

@ -1,58 +0,0 @@
// Copyright 2019 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
@import '../../stylesheets/variables';
@import '../mixins';
.container {
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: space-between;
height: 56px;
}
.label {
user-select: none;
font-size: 13px;
font-family: $inter;
font-weight: 500;
}
.input {
width: 448px;
height: 34px;
line-height: 34px;
padding: 0 12px;
border-radius: 4px;
background-color: transparent;
font-size: 14px;
font-family: $inter;
&::placeholder {
color: $color-gray-45;
}
@include light-theme() {
border: 1px solid $color-gray-15;
color: $color-gray-90;
}
@include dark-theme() {
border: 1px solid $color-gray-60;
color: $color-white;
}
&:focus {
outline: none;
padding: 0 11px;
@include light-theme() {
border: 2px solid $color-ultramarine;
}
@include dark-theme() {
border: 2px solid $color-ultramarine-light;
}
}
}

View File

@ -1,30 +0,0 @@
// Copyright 2019 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import * as React from 'react';
import { text } from '@storybook/addon-knobs';
import { StoryRow } from './StoryRow';
import { LabeledInput } from './LabeledInput';
export default {
title: 'Sticker Creator/elements',
};
export const _LabeledInput = (): JSX.Element => {
const child = text('label', 'foo bar');
const placeholder = text('placeholder', 'foo bar');
const [value, setValue] = React.useState('');
return (
<StoryRow>
<LabeledInput value={value} onChange={setValue} placeholder={placeholder}>
{child}
</LabeledInput>
</StoryRow>
);
};
_LabeledInput.story = {
name: 'LabeledInput',
};

View File

@ -1,42 +0,0 @@
// Copyright 2019 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import * as React from 'react';
import * as styles from './LabeledInput.scss';
import { Inline } from './Typography';
export type Props = {
children: React.ReactNode;
placeholder?: string;
value?: string;
onChange?: (value: string) => unknown;
};
export const LabeledInput = React.memo(function LabeledInputInner({
children,
value,
placeholder,
onChange,
}: Props) {
const handleChange = React.useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
if (onChange !== undefined) {
onChange(e.currentTarget.value);
}
},
[onChange]
);
return (
<label className={styles.container}>
<Inline className={styles.label}>{children}</Inline>
<input
type="text"
className={styles.input}
placeholder={placeholder}
value={value}
onChange={handleChange}
/>
</label>
);
});

View File

@ -1,16 +0,0 @@
// Copyright 2019 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
@import '../../stylesheets/variables';
.base {
background-color: $color-ultramarine;
padding: 6px 12px;
border-radius: 18px;
color: $color-white-alpha-90;
font: {
size: 12px;
family: $inter;
weight: normal;
}
}

View File

@ -1,27 +0,0 @@
// Copyright 2019 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import * as React from 'react';
import { number, text } from '@storybook/addon-knobs';
import { StoryRow } from './StoryRow';
import { MessageBubble } from './MessageBubble';
export default {
title: 'Sticker Creator/elements',
};
export const _MessageBubble = (): JSX.Element => {
const child = text('text', 'Foo bar banana baz');
const minutesAgo = number('minutesAgo', 3);
return (
<StoryRow>
<MessageBubble minutesAgo={minutesAgo}>{child}</MessageBubble>
</StoryRow>
);
};
_MessageBubble.story = {
name: 'MessageBubble',
};

View File

@ -1,20 +0,0 @@
// Copyright 2019 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import * as React from 'react';
import * as styles from './MessageBubble.scss';
import type { Props as MessageMetaProps } from './MessageMeta';
import { MessageMeta } from './MessageMeta';
export type Props = Pick<MessageMetaProps, 'minutesAgo'> & {
children: React.ReactNode;
};
export function MessageBubble({ children, minutesAgo }: Props): JSX.Element {
return (
<div className={styles.base}>
{children}
<MessageMeta kind="bubble" minutesAgo={minutesAgo} />
</div>
);
}

View File

@ -1,36 +0,0 @@
// Copyright 2019 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
@import '../mixins';
@import '../../stylesheets/variables';
.base {
display: flex;
flex-direction: row;
justify-content: flex-end;
margin-top: 3px;
}
.item {
margin-left: 6px;
font: {
size: 11px;
family: $inter;
weight: normal;
}
}
.bubble {
composes: item;
color: rgba(255, 255, 255, 0.8);
}
.light {
composes: item;
color: $color-gray-60;
}
.dark {
composes: item;
color: rgba(255, 255, 255, 0.8);
}

View File

@ -1,59 +0,0 @@
// Copyright 2019 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import * as React from 'react';
import * as styles from './MessageMeta.scss';
import { useI18n } from '../util/i18n';
export type Props = {
kind?: 'bubble' | 'dark' | 'light';
minutesAgo: number;
};
const getItemClass = ({ kind }: Props) => {
if (kind === 'dark') {
return styles.dark;
}
if (kind === 'light') {
return styles.light;
}
return styles.bubble;
};
export const MessageMeta = React.memo(function MessageMetaInner(props: Props) {
const i18n = useI18n();
const itemClass = getItemClass(props);
return (
<div className={styles.base}>
<svg width={12} height={12} className={itemClass}>
<g fillRule="evenodd">
<path d="M8.5 1.67L9 .804a6 6 0 11-7.919 1.76l.868.504-.008.011L6.25 5.567l-.5.866-4.309-2.488A5 5 0 108.5 1.67z" />
<path d="M6.003 1H6a5.06 5.06 0 00-.5.025V.02A6.08 6.08 0 016 0h.003A6 6 0 0112 6h-1a5 5 0 00-4.997-5zM3.443.572l.502.87a5.06 5.06 0 00-.866.5l-.502-.87a6.08 6.08 0 01.866-.5z" />
</g>
</svg>
<div className={itemClass}>
{i18n('minutesAgo', [props.minutesAgo.toString()])}
</div>
<svg width={18} height={12} className={itemClass}>
<defs>
<path
d="M7.917.313a6.99 6.99 0 00-2.844 6.7L5 7.084l-1.795-1.79L2.5 6 5 8.5l.34-.34a7.015 7.015 0 002.577 3.527A6.002 6.002 0 010 6 6.002 6.002 0 017.917.313zM12 0c3.312 0 6 2.688 6 6s-2.688 6-6 6-6-2.688-6-6 2.688-6 6-6zm-1 8.5L15.5 4l-.705-.71L11 7.085l-1.795-1.79L8.5 6 11 8.5z"
id="prefix__a"
/>
<path id="prefix__c" d="M0 0h18v12H0z" />
</defs>
<g fillRule="evenodd">
<mask id="prefix__b">
<use xlinkHref="#prefix__a" />
</mask>
<g mask="url(#prefix__b)">
<use xlinkHref="#prefix__c" />
</g>
</g>
</svg>
</div>
);
});

View File

@ -1,13 +0,0 @@
// Copyright 2019 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
@import '../../stylesheets/variables';
.base {
padding: 6px 12px;
}
.image {
width: 116px;
height: 116px;
}

View File

@ -1,27 +0,0 @@
// Copyright 2019 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import * as React from 'react';
import { number, text } from '@storybook/addon-knobs';
import { StoryRow } from './StoryRow';
import { MessageSticker } from './MessageSticker';
export default {
title: 'Sticker Creator/elements',
};
export const _MessageSticker = (): JSX.Element => {
const image = text('image url', '/fixtures/512x515-thumbs-up-lincoln.webp');
const minutesAgo = number('minutesAgo', 3);
return (
<StoryRow>
<MessageSticker image={image} minutesAgo={minutesAgo} />
</StoryRow>
);
};
_MessageSticker.story = {
name: 'MessageSticker',
};

View File

@ -1,24 +0,0 @@
// Copyright 2019 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import * as React from 'react';
import * as styles from './MessageSticker.scss';
import type { Props as MessageMetaProps } from './MessageMeta';
import { MessageMeta } from './MessageMeta';
export type Props = MessageMetaProps & {
image: string;
};
export function MessageSticker({
image,
kind,
minutesAgo,
}: Props): JSX.Element {
return (
<div className={styles.base}>
<img src={image} alt="Sticker" className={styles.image} />
<MessageMeta kind={kind} minutesAgo={minutesAgo} />
</div>
);
}

View File

@ -1,25 +0,0 @@
// Copyright 2019 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
@import '../../stylesheets/variables';
@import '../mixins';
.base {
height: 47px;
width: 100%;
border-bottom-width: 1px;
border-bottom-style: solid;
padding: 0 16px;
display: flex;
flex-direction: row;
align-items: center;
flex-shrink: 0;
@include light-theme() {
border-bottom-color: $color-gray-15;
}
@include dark-theme() {
border-bottom-color: $color-gray-75;
}
}

View File

@ -1,26 +0,0 @@
// Copyright 2019 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import * as React from 'react';
import { text } from '@storybook/addon-knobs';
import { StoryRow } from './StoryRow';
import { PageHeader } from './PageHeader';
export default {
title: 'Sticker Creator/elements',
};
export const _PageHeader = (): JSX.Element => {
const child = text('text', 'foo bar');
return (
<StoryRow>
<PageHeader>{child}</PageHeader>
</StoryRow>
);
};
_PageHeader.story = {
name: 'PageHeader',
};

View File

@ -1,16 +0,0 @@
// Copyright 2019 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import * as React from 'react';
import * as styles from './PageHeader.scss';
import { H1 } from './Typography';
export type Props = {
children: React.ReactNode;
};
export const PageHeader = React.memo(function PageHeaderInner({
children,
}: Props) {
return <H1 className={styles.base}>{children}</H1>;
});

View File

@ -1,27 +0,0 @@
// Copyright 2019 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
@import '../../stylesheets/variables';
@import '../mixins';
.base {
height: 4px;
width: 100%;
max-width: 448px;
@include light-theme() {
background: $color-gray-15;
}
@include dark-theme() {
background: $color-gray-75;
}
}
.bar {
height: 4px;
width: 0px;
background: $color-ultramarine;
transition: width 100ms ease-out;
}

View File

@ -1,27 +0,0 @@
// Copyright 2019 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import * as React from 'react';
import { number } from '@storybook/addon-knobs';
import { StoryRow } from './StoryRow';
import { ProgressBar } from './ProgressBar';
export default {
title: 'Sticker Creator/elements',
};
export const _ProgressBar = (): JSX.Element => {
const count = number('count', 5);
const total = number('total', 10);
return (
<StoryRow>
<ProgressBar count={count} total={total} />
</StoryRow>
);
};
_ProgressBar.story = {
name: 'ProgressBar',
};

View File

@ -1,27 +0,0 @@
// Copyright 2019 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import * as React from 'react';
import classnames from 'classnames';
import * as styles from './ProgressBar.scss';
export type Props = Pick<React.HTMLAttributes<HTMLDivElement>, 'className'> & {
readonly count: number;
readonly total: number;
};
export const ProgressBar = React.memo(function ProgressBarInner({
className,
count,
total,
}: Props) {
return (
<div className={classnames(styles.base, className)}>
<div
className={styles.bar}
style={{ width: `${Math.floor((count / total) * 100)}%` }}
/>
</div>
);
});

View File

@ -1,103 +0,0 @@
// Copyright 2019 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
@import '../mixins';
@import '../../stylesheets/variables';
.base {
width: 380px;
padding: 4px;
display: flex;
flex-direction: row;
border-radius: 8px;
box-shadow: 0 2px 13px 3px rgba(0, 0, 0, 0.3);
position: relative;
@include light-theme() {
background: $color-white;
}
@include dark-theme() {
background: $color-gray-75;
}
}
.frame {
width: 50%;
padding: 12px 12px 3px 12px;
}
.frame-light {
composes: frame;
background: $color-white;
border-radius: 6px 0 0 6px;
}
.frame-dark {
composes: frame;
background: $color-black;
border-radius: 0 6px 6px 0;
}
.arrow {
position: absolute;
width: 0;
height: 0;
border-style: solid;
}
.arrow-top {
composes: arrow;
border-width: 0 8px 8px 8px;
top: -8px;
@include light-theme() {
border-color: transparent transparent $color-white transparent;
}
@include dark-theme() {
border-color: transparent transparent $color-gray-75 transparent;
}
}
.arrow-bottom {
composes: arrow;
border-width: 8px 8px 0 8px;
bottom: -8px;
@include light-theme() {
border-color: $color-white transparent transparent transparent;
}
@include dark-theme() {
border-color: $color-gray-75 transparent transparent transparent;
}
}
.arrow-left {
composes: arrow;
border-width: 8px 8px 8px 0;
left: -8px;
@include light-theme() {
border-color: transparent $color-white transparent transparent;
}
@include dark-theme() {
border-color: transparent $color-gray-75 transparent transparent;
}
}
.arrow-right {
composes: arrow;
border-width: 8px 0 8px 8px;
right: -8px;
@include light-theme() {
border-color: transparent transparent transparent $color-white;
}
@include dark-theme() {
border-color: transparent transparent transparent $color-gray-75;
}
}

View File

@ -1,26 +0,0 @@
// Copyright 2019 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import * as React from 'react';
import { text } from '@storybook/addon-knobs';
import { StoryRow } from './StoryRow';
import { StickerPreview } from './StickerPreview';
export default {
title: 'Sticker Creator/elements',
};
export const _StickerPreview = (): JSX.Element => {
const image = text('image url', '/fixtures/512x515-thumbs-up-lincoln.webp');
return (
<StoryRow>
<StickerPreview image={image} />
</StoryRow>
);
};
_StickerPreview.story = {
name: 'StickerPreview',
};

View File

@ -1,78 +0,0 @@
// Copyright 2019 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import * as React from 'react';
import type { PopperArrowProps } from 'react-popper';
import type { Placement } from '@popperjs/core';
import * as styles from './StickerPreview.scss';
import { MessageBubble } from './MessageBubble';
import type { Props as MessageStickerProps } from './MessageSticker';
import { MessageSticker } from './MessageSticker';
import { useI18n } from '../util/i18n';
export type Props = Pick<React.HTMLProps<HTMLDivElement>, 'style'> & {
image: string;
arrowProps?: PopperArrowProps;
placement?: Placement;
};
const renderMessages = (
text: string,
image: string,
kind: MessageStickerProps['kind']
) => (
<>
<MessageBubble minutesAgo={3}>{text}</MessageBubble>
<MessageSticker image={image} kind={kind} minutesAgo={2} />
</>
);
const getArrowClass = (placement?: Placement) => {
if (placement === 'top') {
return styles.arrowBottom;
}
if (placement === 'right') {
return styles.arrowLeft;
}
if (placement === 'left') {
return styles.arrowRight;
}
return styles.arrowTop;
};
export const StickerPreview = React.memo(
React.forwardRef<HTMLDivElement, Props>(
({ image, style, arrowProps, placement }: Props, ref) => {
const i18n = useI18n();
return (
<div className={styles.base} ref={ref} style={style}>
{arrowProps ? (
<div
ref={arrowProps.ref}
style={arrowProps.style}
className={getArrowClass(placement)}
/>
) : null}
<div className={styles.frameLight}>
{renderMessages(
i18n('StickerCreator--StickerPreview--light'),
image,
'light'
)}
</div>
<div className={styles.frameDark}>
{renderMessages(
i18n('StickerCreator--StickerPreview--dark'),
image,
'dark'
)}
</div>
</div>
);
}
)
);

View File

@ -1,31 +0,0 @@
// Copyright 2019 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
.base {
flex: 1;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
width: 100%;
}
.left {
composes: base;
justify-content: flex-start;
}
.right {
composes: base;
justify-content: flex-end;
}
.top {
composes: base;
align-items: flex-start;
}
.bottom {
composes: base;
align-items: flex-end;
}

View File

@ -1,39 +0,0 @@
// Copyright 2019 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import * as React from 'react';
import * as styles from './StoryRow.scss';
type Props = {
left?: boolean;
right?: boolean;
top?: boolean;
bottom?: boolean;
};
const getClassName = ({ left, right, top, bottom }: Props) => {
if (left) {
return styles.left;
}
if (right) {
return styles.right;
}
if (top) {
return styles.top;
}
if (bottom) {
return styles.bottom;
}
return styles.base;
};
export function StoryRow({
children,
...props
}: React.PropsWithChildren<Props>): JSX.Element {
return <div className={getClassName(props)}>{children}</div>;
}

View File

@ -1,18 +0,0 @@
// Copyright 2019 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
@import '../../stylesheets/variables';
.base {
padding: 8px 12px;
border-radius: 4px;
border: none;
background-color: $color-gray-75;
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.08), 0 8px 20px 0px rgba(0, 0, 0, 0.33);
font-family: $inter;
font-weight: normal;
font-size: 14px;
color: $color-gray-05;
line-height: 18px;
cursor: pointer;
}

View File

@ -1,23 +0,0 @@
// Copyright 2019 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import * as React from 'react';
import { text } from '@storybook/addon-knobs';
import { action } from '@storybook/addon-actions';
import { StoryRow } from './StoryRow';
import { Toast } from './Toast';
export default {
title: 'Sticker Creator/elements',
};
export const _Toast = (): JSX.Element => {
const child = text('text', 'foo bar');
return (
<StoryRow>
<Toast onClick={action('click')}>{child}</Toast>
</StoryRow>
);
};

View File

@ -1,26 +0,0 @@
// Copyright 2019 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import * as React from 'react';
import classNames from 'classnames';
import * as styles from './Toast.scss';
export type Props = React.HTMLAttributes<HTMLButtonElement> & {
children: React.ReactNode;
};
export const Toast = React.memo(function ToastInner({
children,
className,
...rest
}: Props) {
return (
<button
type="button"
className={classNames(styles.base, className)}
{...rest}
>
{children}
</button>
);
});

View File

@ -1,78 +0,0 @@
// Copyright 2019 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
@import '../../stylesheets/variables';
@import '../mixins';
.base {
font-family: $inter;
margin: 0;
}
.heading {
composes: base;
font-weight: 500;
}
.h1 {
composes: heading;
font-size: 16px;
line-height: 20px;
@include light-theme() {
color: $color-gray-90;
}
@include dark-theme() {
color: $color-gray-05;
}
}
.h2 {
composes: heading;
font-size: 14px;
line-height: 18px;
margin-bottom: 8px;
@include light-theme() {
color: $color-gray-90;
}
@include dark-theme() {
color: $color-white;
}
}
.text {
composes: base;
font-size: 13px;
line-height: 18px;
@include light-theme() {
color: $color-gray-90;
}
@include dark-theme() {
color: $color-white;
}
a {
color: $color-ultramarine;
text-decoration: none;
}
}
.text-center {
composes: text;
text-align: center;
}
.secondary {
@include light-theme() {
color: $color-gray-60;
}
@include dark-theme() {
color: $color-gray-25;
}
}

View File

@ -1,40 +0,0 @@
// Copyright 2019 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
/* eslint-disable jsx-a11y/anchor-is-valid */
import * as React from 'react';
import { text } from '@storybook/addon-knobs';
import { StoryRow } from './StoryRow';
import { H1, H2, Text } from './Typography';
export default {
title: 'Sticker Creator/elements',
};
export function Typography(): JSX.Element {
const child = text('text', 'foo bar');
return (
<>
<StoryRow left>
<H1>{child}</H1>
</StoryRow>
<StoryRow left>
<H2>{child}</H2>
</StoryRow>
<StoryRow left>
<Text>
{child} {child} {child} {child}
</Text>
</StoryRow>
<StoryRow left>
<Text>
{child} {child} {child} {child}{' '}
<a href="#">Something something something dark side.</a>
</Text>
</StoryRow>
</>
);
}

View File

@ -1,76 +0,0 @@
// Copyright 2019 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import * as React from 'react';
import classnames from 'classnames';
import * as styles from './Typography.scss';
export type Props = {
children: React.ReactNode;
};
export type HeadingProps = React.HTMLAttributes<HTMLHeadingElement>;
export type ParagraphProps = React.HTMLAttributes<HTMLParagraphElement> & {
center?: boolean;
wide?: boolean;
secondary?: boolean;
};
export type SpanProps = React.HTMLAttributes<HTMLSpanElement>;
export const H1 = React.memo(function H1Inner({
children,
className,
...rest
}: Props & HeadingProps) {
return (
<h1 className={classnames(styles.h1, className)} {...rest}>
{children}
</h1>
);
});
export const H2 = React.memo(function H2Inner({
children,
className,
...rest
}: Props & HeadingProps) {
return (
<h2 className={classnames(styles.h2, className)} {...rest}>
{children}
</h2>
);
});
export const Text = React.memo(function TextInner({
children,
className,
center,
secondary,
...rest
}: Props & ParagraphProps) {
return (
<p
className={classnames(
center ? styles.textCenter : styles.text,
secondary ? styles.secondary : null,
className
)}
{...rest}
>
{children}
</p>
);
});
export const Inline = React.memo(function InlineInner({
children,
className,
...rest
}: Props & SpanProps) {
return (
<span className={classnames(styles.text, className)} {...rest}>
{children}
</span>
);
});

View File

@ -1,12 +0,0 @@
// Copyright 2019 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import * as React from 'react';
export const AddEmoji = React.memo(function AddEmojiInner() {
return (
<svg width="32px" height="20px" viewBox="0 0 32 20">
<path d="M21.993 15.5a5.679 5.679 0 0 1-4.6-2.29.75.75 0 1 1 1.212-.883 4.266 4.266 0 0 0 6.786-.015.749.749 0 0 1 1.216.876 5.671 5.671 0 0 1-4.614 2.312zM22 2.5a7.254 7.254 0 0 0-7.5 7.5 7.254 7.254 0 0 0 7.5 7.5 7.254 7.254 0 0 0 7.5-7.5A7.254 7.254 0 0 0 22 2.5M22 1a8.751 8.751 0 0 1 9 9 8.751 8.751 0 0 1-9 9 8.751 8.751 0 0 1-9-9 8.751 8.751 0 0 1 9-9zm-2.75 5.75A1.476 1.476 0 0 0 18 8.375 1.476 1.476 0 0 0 19.25 10a1.476 1.476 0 0 0 1.25-1.625 1.476 1.476 0 0 0-1.25-1.625zm5.5 0a1.476 1.476 0 0 0-1.25 1.625A1.476 1.476 0 0 0 24.75 10 1.476 1.476 0 0 0 26 8.375a1.476 1.476 0 0 0-1.25-1.625zM10 9.25H6.75V6h-1.5v3.25H2v1.5h3.25V14h1.5v-3.25H10z" />
</svg>
);
});

View File

@ -1,4 +0,0 @@
// Copyright 2019 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
export { AddEmoji } from './AddEmoji';

View File

@ -1,18 +0,0 @@
<!-- Copyright 2019 Signal Messenger, LLC -->
<!-- SPDX-License-Identifier: AGPL-3.0-only -->
<!DOCTYPE html>
<html>
<head>
<link
href="../../node_modules/@indutny/frameless-titlebar/dist/styles.css"
rel="stylesheet"
type="text/css"
/>
<link rel="stylesheet" href="../../stylesheets/manifest_bridge.css" />
</head>
<body>
<div id="root"></div>
<script type="text/javascript" src="../../js/components.js"></script>
</body>
</html>

View File

@ -1,12 +0,0 @@
// Copyright 2019 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import * as React from 'react';
import { render } from 'react-dom';
import { Root } from './root';
const root = document.getElementById('root');
// eslint-disable-next-line no-console
console.log('Sticker Creator: Starting root');
render(<Root />, root);

View File

@ -1,11 +0,0 @@
// Copyright 2019 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import './window/phase1-dependencies';
import './window/phase2-signal';
import './window/phase3-sticker-functions';
import './window/phase4-theme';
import { SignalContext } from '../ts/windows/context';
SignalContext.log.info('sticker-creator preload complete...');

View File

@ -1,33 +0,0 @@
// Copyright 2019 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { hot } from 'react-hot-loader/root';
import * as React from 'react';
import { Provider as ReduxProvider } from 'react-redux';
import { Router } from 'react-router-dom';
import { App } from './app';
import { history } from './util/history';
import { store } from './store';
import { I18n } from './util/i18n';
const { localeMessages, SignalContext } = window;
function ColdRoot() {
return (
<ReduxProvider store={store}>
<Router history={history}>
<I18n
messages={localeMessages}
locale={SignalContext.config.resolvedTranslationsLocale}
>
<App
executeMenuRole={SignalContext.executeMenuRole}
hasCustomTitleBar={SignalContext.OS.hasCustomTitleBar()}
/>
</I18n>
</Router>
</ReduxProvider>
);
}
export const Root = hot(ColdRoot);

View File

@ -1,332 +0,0 @@
// Copyright 2019 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
/* eslint-disable no-param-reassign */
import { useMemo } from 'react';
import type { Draft } from 'redux-ts-utils';
import { createAction, handleAction, reduceReducers } from 'redux-ts-utils';
import { useDispatch, useSelector } from 'react-redux';
import { createSelector } from 'reselect';
import { clamp, find, isString, pull, remove, take, uniq } from 'lodash';
import type { SortEnd } from 'react-sortable-hoc';
import { bindActionCreators } from 'redux';
import arrayMove from 'array-move';
import type { AppState } from '../reducer';
import type {
PackMetaData,
StickerImageData,
StickerData,
} from '../../util/preload';
import type { EmojiPickDataType } from '../../../ts/components/emoji/EmojiPicker';
import { convertShortName } from '../../../ts/components/emoji/lib';
import { isNotNil } from '../../../ts/util/isNotNil';
type StickerEmojiData = { id: string; emoji: EmojiPickDataType };
export const initializeStickers = createAction<Array<string>>(
'stickers/initializeStickers'
);
export const addImageData = createAction<StickerImageData>(
'stickers/addSticker'
);
export const removeSticker = createAction<string>('stickers/removeSticker');
export const moveSticker = createAction<SortEnd>('stickers/moveSticker');
export const setCover = createAction<StickerImageData>('stickers/setCover');
export const resetCover = createAction<StickerImageData>('stickers/resetCover');
export const setEmoji = createAction<StickerEmojiData>('stickers/setEmoji');
export const setTitle = createAction<string>('stickers/setTitle');
export const setAuthor = createAction<string>('stickers/setAuthor');
export const setPackMeta = createAction<PackMetaData>('stickers/setPackMeta');
export const addToast = createAction<{
key: string;
subs?: Record<string, string>;
}>('stickers/addToast');
export const dismissToast = createAction<void>('stickers/dismissToast');
export const resetStatus = createAction<void>('stickers/resetStatus');
export const reset = createAction<void>('stickers/reset');
export const minStickers = 1;
export const maxStickers = 200;
export const maxByteSize = 300 * 1024;
type StateStickerData = {
readonly imageData?: StickerImageData;
readonly emoji?: EmojiPickDataType;
};
type StateToastData = {
key: string;
subs?: Record<string, string>;
};
export type State = {
readonly order: Array<string>;
readonly cover?: StickerImageData;
readonly title: string;
readonly author: string;
readonly packId: string;
readonly packKey: string;
readonly toasts: Array<StateToastData>;
readonly data: {
readonly [src: string]: StateStickerData;
};
};
export type Actions = {
addImageData: typeof addImageData;
initializeStickers: typeof initializeStickers;
removeSticker: typeof removeSticker;
moveSticker: typeof moveSticker;
setCover: typeof setCover;
setEmoji: typeof setEmoji;
setTitle: typeof setTitle;
setAuthor: typeof setAuthor;
setPackMeta: typeof setPackMeta;
addToast: typeof addToast;
dismissToast: typeof dismissToast;
reset: typeof reset;
resetStatus: typeof resetStatus;
};
const defaultState: State = {
order: [],
data: {},
title: '',
author: '',
packId: '',
packKey: '',
toasts: [],
};
const adjustCover = (state: Draft<State>) => {
const first = state.order[0];
if (first) {
state.cover = state.data[first].imageData;
} else {
delete state.cover;
}
};
export const reducer = reduceReducers<State>(
[
handleAction(initializeStickers, (state, { payload }) => {
const truncated = take(
uniq([...state.order, ...payload]),
maxStickers - state.order.length
);
truncated.forEach(path => {
if (!state.data[path]) {
state.data[path] = {};
state.order.push(path);
}
});
}),
handleAction(addImageData, (state, { payload }) => {
if (payload.buffer.byteLength > maxByteSize) {
state.toasts.push({ key: 'StickerCreator--Toasts--tooLarge' });
pull(state.order, payload.path);
delete state.data[payload.path];
} else {
const data = state.data[payload.path];
// If we are adding image data, proceed to update the state and add/update a toast
if (data && !data.imageData) {
data.imageData = payload;
const key = 'icu:StickerCreator--Toasts--imagesAdded';
const toast = (() => {
const oldToast = find(state.toasts, { key });
if (oldToast) {
return oldToast;
}
const newToast = { key, subs: { count: '0' } };
state.toasts.push(newToast);
return newToast;
})();
const previousSub = toast?.subs?.count;
if (toast && isString(previousSub)) {
const previousCount = parseInt(previousSub, 10);
const newCount = Number.isFinite(previousCount)
? previousCount + 1
: 1;
toast.subs = toast.subs || {};
toast.subs.count = newCount.toString();
}
}
}
adjustCover(state);
}),
handleAction(removeSticker, (state, { payload }) => {
pull(state.order, payload);
delete state.data[payload];
adjustCover(state);
}),
handleAction(moveSticker, (state, { payload }) => {
arrayMove.mutate(state.order, payload.oldIndex, payload.newIndex);
}),
handleAction(setCover, (state, { payload }) => {
state.cover = payload;
}),
handleAction(resetCover, state => {
adjustCover(state);
}),
handleAction(setEmoji, (state, { payload }) => {
const data = state.data[payload.id];
if (data) {
data.emoji = payload.emoji;
}
}),
handleAction(setTitle, (state, { payload }) => {
state.title = payload;
}),
handleAction(setAuthor, (state, { payload }) => {
state.author = payload;
}),
handleAction(setPackMeta, (state, { payload: { packId, key } }) => {
state.packId = packId;
state.packKey = key;
}),
handleAction(addToast, (state, { payload: toast }) => {
remove(state.toasts, { key: toast.key });
state.toasts.push(toast);
}),
handleAction(dismissToast, state => {
state.toasts.pop();
}),
handleAction(resetStatus, state => {
state.toasts = [];
}),
handleAction(reset, () => defaultState),
],
defaultState
);
export const useTitle = (): string =>
useSelector(({ stickers }: AppState) => stickers.title);
export const useAuthor = (): string =>
useSelector(({ stickers }: AppState) => stickers.author);
export const useCover = (): StickerImageData | undefined =>
useSelector(({ stickers }: AppState) => stickers.cover);
export const useStickerOrder = (): Array<string> =>
useSelector(({ stickers }: AppState) => stickers.order);
export const useStickerData = (src: string): StateStickerData =>
useSelector(({ stickers }: AppState) => stickers.data[src]);
export const useStickersReady = (): boolean =>
useSelector(
({ stickers }: AppState) =>
stickers.order.length >= minStickers &&
stickers.order.length <= maxStickers &&
Object.values(stickers.data).every(({ imageData }) => Boolean(imageData))
);
export const useEmojisReady = (): boolean =>
useSelector(({ stickers }: AppState) =>
Object.values(stickers.data).every(({ emoji }) => !!emoji)
);
export const useAllDataValid = (): boolean => {
const stickersReady = useStickersReady();
const emojisReady = useEmojisReady();
const cover = useCover();
const title = useTitle();
const author = useAuthor();
return !!(stickersReady && emojisReady && cover && title && author);
};
const selectUrl = createSelector(
({ stickers }: AppState) => stickers.packId,
({ stickers }: AppState) => stickers.packKey,
(id, key) => `https://signal.art/addstickers/#pack_id=${id}&pack_key=${key}`
);
export const usePackUrl = (): string => useSelector(selectUrl);
export const useToasts = (): Array<StateToastData> =>
useSelector(({ stickers }: AppState) => stickers.toasts);
export const useAddMoreCount = (): number =>
useSelector(({ stickers }: AppState) =>
clamp(minStickers - stickers.order.length, 0, minStickers)
);
const selectOrderedData = createSelector(
({ stickers }: AppState) => stickers.order,
({ stickers }: AppState) => stickers.data,
(order, data) =>
order.map(id => ({
...data[id],
emoji: convertShortName(
(data[id].emoji as EmojiPickDataType).shortName,
(data[id].emoji as EmojiPickDataType).skinTone
),
}))
);
export const useSelectOrderedData = (): Array<StickerData> =>
useSelector(selectOrderedData);
const selectOrderedImagePaths = createSelector(
selectOrderedData,
(data: Array<StickerData>) =>
data.map(({ imageData }) => imageData?.src).filter(isNotNil)
);
export const useOrderedImagePaths = (): Array<string> =>
useSelector(selectOrderedImagePaths);
export const useStickerActions = (): Actions => {
const dispatch = useDispatch();
return useMemo(
() =>
bindActionCreators(
{
addImageData,
initializeStickers,
removeSticker,
moveSticker,
setCover,
setEmoji,
setTitle,
setAuthor,
setPackMeta,
addToast,
dismissToast,
reset,
resetStatus,
},
dispatch
),
[dispatch]
);
};

View File

@ -1,11 +0,0 @@
// Copyright 2019 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { createStore } from 'redux';
import { reducer } from './reducer';
import * as stickersDuck from './ducks/stickers';
export { stickersDuck };
export const store = createStore(reducer);

View File

@ -1,12 +0,0 @@
// Copyright 2019 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { Reducer } from 'redux';
import { combineReducers } from 'redux';
import { reducer as stickers } from './ducks/stickers';
export const reducer = combineReducers({
stickers,
});
export type AppState = typeof reducer extends Reducer<infer U> ? U : never;

View File

@ -1,6 +0,0 @@
// Copyright 2019 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { createMemoryHistory } from 'history';
export const history = createMemoryHistory();

View File

@ -1,116 +0,0 @@
// Copyright 2019 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import * as React from 'react';
import type { LocaleMessagesType } from '../../ts/types/I18N';
import type { LocalizerType, ReplacementValuesType } from '../../ts/types/Util';
import {
classifyMessages,
createCachedIntl,
formatIcuMessage,
} from '../../ts/util/setupI18n';
const placeholder = () => 'NO LOCALE LOADED';
placeholder.getLocale = () => 'none';
placeholder.isLegacyFormat = () => {
throw new Error("Can't call isLegacyFormat on placeholder");
};
placeholder.getIntl = () => {
throw new Error("Can't call getIntl on placeholder");
};
const I18nContext = React.createContext<LocalizerType>(placeholder);
export type I18nProps = {
children: React.ReactNode;
locale: string;
messages: LocaleMessagesType;
};
export function I18n({ messages, locale, children }: I18nProps): JSX.Element {
const { icuMessages, legacyMessages } = React.useMemo(() => {
return classifyMessages(messages);
}, [messages]);
const intl = React.useMemo(() => {
return createCachedIntl(locale, icuMessages);
}, [locale, icuMessages]);
const callback = (key: string, substitutions?: ReplacementValuesType) => {
if (Array.isArray(substitutions) && substitutions.length > 1) {
throw new Error(
'Array syntax is not supported with more than one placeholder'
);
}
const messageformat = icuMessages[key];
if (messageformat != null) {
return formatIcuMessage(intl, key, substitutions);
}
const message = legacyMessages[key];
if (message == null) {
window.SignalContext.log.warn(
`getMessage: No string found for key ${key}`
);
return '';
}
if (!substitutions) {
return message;
}
if (Array.isArray(substitutions)) {
return substitutions.reduce(
(result, substitution) =>
result.toString().replace(/\$.+?\$/, substitution.toString()),
message
) as string;
}
const FIND_REPLACEMENTS = /\$([^$]+)\$/g;
let match = FIND_REPLACEMENTS.exec(message);
let builder = '';
let lastTextIndex = 0;
while (match) {
if (lastTextIndex < match.index) {
builder += message.slice(lastTextIndex, match.index);
}
const placeholderName = match[1];
const value = substitutions[placeholderName];
if (!value) {
// eslint-disable-next-line no-console
console.error(
`i18n: Value not provided for placeholder ${placeholderName} in key '${key}'`
);
}
builder += value || '';
lastTextIndex = FIND_REPLACEMENTS.lastIndex;
match = FIND_REPLACEMENTS.exec(message);
}
if (lastTextIndex < message.length) {
builder += message.slice(lastTextIndex);
}
return builder;
};
callback.getLocale = () => locale;
callback.isLegacyFormat = (key: string) => {
return legacyMessages[key] != null;
};
callback.getIntl = () => intl;
const getMessage = React.useCallback<LocalizerType>(callback, [
icuMessages,
legacyMessages,
intl,
]);
return (
<I18nContext.Provider value={getMessage}>{children}</I18nContext.Provider>
);
}
export const useI18n = (): LocalizerType => React.useContext(I18nContext);

View File

@ -1,35 +0,0 @@
// Copyright 2019 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type {
ProcessStickerImageErrorType,
StickerImageData,
} from '../window/phase3-sticker-functions';
declare global {
// We want to extend `window`'s properties, so we need an interface.
// eslint-disable-next-line no-restricted-syntax
interface Window {
processStickerImage: ProcessStickerImageFn;
encryptAndUpload: EncryptAndUploadFn;
ProcessStickerImageError: ProcessStickerImageErrorType;
}
}
export { StickerImageData };
type ProcessStickerImageFn = (
path: string | undefined
) => Promise<StickerImageData>;
export type StickerData = { imageData?: StickerImageData; emoji?: string };
export type PackMetaData = { packId: string; key: string };
export type EncryptAndUploadFn = (
manifest: { title: string; author: string },
stickers: Array<StickerData>,
cover: StickerImageData,
onProgress?: () => unknown
) => Promise<PackMetaData>;
export const { encryptAndUpload, processStickerImage } = window;

Some files were not shown because too many files have changed in this diff Show More