Unverified Commit 37382ca3 authored by Tasso Evangelista's avatar Tasso Evangelista Committed by GitHub

[IMPROVE] New tray icons (#1093)

* Add new tray icons for MacOS

* Add new tray icons for Linux

* Add new tray icons for Windows

* Remove option to show user status in tray icon

* Remove old icon module

* Don't use gulp to update icons

* Optimize tray icons

* Refactor icon module

* Build app icons

* Refactor and test icon module

* Fix Windows icons
parent bc4d1603
build/appx/Square150x150Logo.png

3.73 KB | W: | H:

build/appx/Square150x150Logo.png

6.76 KB | W: | H:

build/appx/Square150x150Logo.png
build/appx/Square150x150Logo.png
build/appx/Square150x150Logo.png
build/appx/Square150x150Logo.png
  • 2-up
  • Swipe
  • Onion skin
build/appx/Square44x44Logo.png

1.14 KB | W: | H:

build/appx/Square44x44Logo.png

1.87 KB | W: | H:

build/appx/Square44x44Logo.png
build/appx/Square44x44Logo.png
build/appx/Square44x44Logo.png
build/appx/Square44x44Logo.png
  • 2-up
  • Swipe
  • Onion skin
build/appx/StoreLogo.png

1.29 KB | W: | H:

build/appx/StoreLogo.png

2.15 KB | W: | H:

build/appx/StoreLogo.png
build/appx/StoreLogo.png
build/appx/StoreLogo.png
build/appx/StoreLogo.png
  • 2-up
  • Swipe
  • Onion skin
build/appx/Wide310x150Logo.png

4.4 KB | W: | H:

build/appx/Wide310x150Logo.png

7.09 KB | W: | H:

build/appx/Wide310x150Logo.png
build/appx/Wide310x150Logo.png
build/appx/Wide310x150Logo.png
build/appx/Wide310x150Logo.png
  • 2-up
  • Swipe
  • Onion skin
No preview for this file type
build/icon.ico

32.2 KB | W: | H:

build/icon.ico

353 KB | W: | H:

build/icon.ico
build/icon.ico
build/icon.ico
build/icon.ico
  • 2-up
  • Swipe
  • Onion skin
build/icons/512x512.png

50.1 KB | W: | H:

build/icons/512x512.png

29.2 KB | W: | H:

build/icons/512x512.png
build/icons/512x512.png
build/icons/512x512.png
build/icons/512x512.png
  • 2-up
  • Swipe
  • Onion skin
build/installerIcon.ico

32.2 KB | W: | H:

build/installerIcon.ico

353 KB | W: | H:

build/installerIcon.ico
build/installerIcon.ico
build/installerIcon.ico
build/installerIcon.ico
  • 2-up
  • Swipe
  • Onion skin
build/uninstallerIcon.ico

32.2 KB | W: | H:

build/uninstallerIcon.ico

353 KB | W: | H:

build/uninstallerIcon.ico
build/uninstallerIcon.ico
build/uninstallerIcon.ico
build/uninstallerIcon.ico
  • 2-up
  • Swipe
  • Onion skin
'use strict';
require('./tasks/build-app');
require('./tasks/build-tests');
require('./tasks/release');
......
......@@ -17,6 +17,7 @@
"postremove": "electron-builder install-app-deps",
"start": "gulp start",
"build": "gulp build-app",
"build:icons": "node tasks/build-icons.js",
"changelog": "conventional-changelog --config .github/changelog.js -i HISTORY.md -s",
"release": "gulp release --env=production",
"release-dev": "gulp release --env=development",
......@@ -43,10 +44,12 @@
"tmp": "^0.0.33"
},
"devDependencies": {
"@fiahfy/icns-convert": "^0.0.5",
"@rocket.chat/eslint-config": "^0.1.2",
"builtin-modules": "^3.0.0",
"chai": "^4.2.0",
"conventional-changelog-cli": "^2.0.11",
"convert-svg-to-png": "^0.5.0",
"electron": "^4.0.1",
"electron-builder": "^20.38.4",
"electron-mocha": "^6.0.4",
......@@ -71,6 +74,7 @@
"run-sequence": "^2.2.1",
"sinon": "^7.2.2",
"spectron": "^5.0.0",
"to-ico": "^1.1.5",
"xvfb-maybe": "^0.2.1"
},
"devEngines": {
......
import { app } from 'electron';
import { EventEmitter } from 'events';
import { getMainWindow } from './mainWindow';
import icon from './icon';
import { getTrayIconImage, getAppIconImage } from './icon';
const getBadgeText = ({ badge: { title, count } }) => {
if (title === '') {
return '';
} else if (count > 0) {
return count > 9 ? '9+' : String(count);
} else if (title) {
return '!';
return String(count);
}
};
......@@ -19,7 +17,7 @@ let state = {
title: '',
count: 0,
},
status: 'online',
hasTrayIcon: false,
};
const instance = new (class Dock extends EventEmitter {});
......@@ -32,18 +30,6 @@ const update = async(previousState) => {
const mainWindow = await getMainWindow();
const badgeText = getBadgeText(state);
if (process.platform === 'win32') {
const image = badgeText ? await icon.render({
overlay: true,
size: 16,
badgeText,
}) : null;
mainWindow.setOverlayIcon(image, badgeText || '');
mainWindow.removeListener('show', update);
mainWindow.on('show', update);
}
if (process.platform === 'darwin') {
app.dock.setBadge(badgeText || '');
if (state.badge.count > 0 && previousState.badge.count === 0) {
......@@ -51,14 +37,9 @@ const update = async(previousState) => {
}
}
if (process.platform === 'linux') {
mainWindow.setIcon(await icon.render({
badgeText,
size: {
win32: [256, 128, 64, 48, 32, 24, 16],
linux: 128,
}[process.platform],
}));
if (process.platform === 'linux' || process.platform === 'win32') {
const image = state.hasTrayIcon ? getAppIconImage() : getTrayIconImage(state.badge);
mainWindow.setIcon(image);
}
if (!mainWindow.isFocused()) {
......
import { BrowserWindow, nativeImage } from 'electron';
import jetpack from 'fs-jetpack';
import { whenReadyToShow } from './utils';
import { nativeImage, systemPreferences } from 'electron';
let rendererWindow = null;
const getRendererWindow = async() => {
if (!rendererWindow) {
rendererWindow = new BrowserWindow({ show: false });
const dataURL = `data:text/html,<!doctype html>
${ jetpack.read(`${ __dirname }/public/images/icon.svg`) }`;
rendererWindow.loadURL(dataURL);
await whenReadyToShow(rendererWindow);
function getTrayIconSet({ platform, dark }) {
if (platform === 'darwin') {
return `darwin${ dark ? '-dark' : '' }`;
}
return rendererWindow;
};
/* istanbul ignore next */
const renderInWindow = async(style) => {
const statusColors = {
offline: null,
away: 'yellow',
busy: 'red',
online: 'lime',
};
const create = ({ overlay, template, status, badgeText } = {}) => {
const svg = document.querySelector('#icon').cloneNode(true);
svg.querySelector('.logo .baloon').style.fill = template ? '#FFFFFF' : '#DB2323';
svg.querySelector('.logo .circles').style.fill = template ? '#FFFFFF' : '#DB2323';
svg.querySelector('.status .away').style.fill = template ? '#FFFFFF' : '#DB2323';
svg.querySelector('.status .busy').style.fill = template ? '#FFFFFF' : '#DB2323';
svg.querySelector('.logo .bubble').style.display = template ? 'none' : null;
svg.querySelector('.badge').style.display = (!template && badgeText) ? null : 'none';
svg.querySelector('.badge text').innerHTML = badgeText;
svg.querySelector('.logo .circles').style.display = (template && status && status !== 'online') ? 'none' : '';
svg.querySelector('.status circle').style.display = (template || !status) ? 'none' : null;
svg.querySelector('.status .away').style.display = (template && status === 'away') ? null : 'none';
svg.querySelector('.status .busy').style.display = (template && status === 'busy') ? null : 'none';
svg.querySelector('.status circle').style.fill = statusColors[status];
return platform;
}
if (overlay) {
const overlaySVG = svg.cloneNode(true);
svg.remove();
overlaySVG.querySelector('.logo').remove();
overlaySVG.querySelector('.status').remove();
overlaySVG.setAttribute('viewBox', '96 -32 160 160');
return overlaySVG;
}
return svg;
};
const rasterize = async(svg, size) => {
const image = new Image();
image.src = `data:image/svg+xml,${ encodeURIComponent(svg.outerHTML) }`;
image.width = image.height = size;
await new Promise((resolve, reject) => {
image.onload = resolve;
image.onerror = reject;
});
function getTrayIconName({ title, count, platform }) {
if (platform === 'darwin') {
return (title || count) ? 'notification' : 'default';
}
const canvas = document.createElement('canvas');
canvas.width = canvas.height = size;
if (title === '') {
return 'notification-dot';
} else if (count > 0) {
return count > 9 ? 'notification-plus-9' : `notification-${ String(count) }`;
}
const ctx = canvas.getContext('2d');
ctx.drawImage(image, 0, 0);
return 'default';
}
return canvas.toDataURL('image/png');
};
function getTrayIconExtension({ platform }) {
if (platform === 'win32') {
return 'ico';
}
const svg = create(style);
const pixelRatio = window.devicePixelRatio;
const sizes = Array.isArray(style.size) ? style.size : [style.size || 256];
const images = await Promise.all(sizes.map(async(size) => ({
dataURL: await rasterize(svg, size * pixelRatio),
size,
pixelRatio,
})));
svg.remove();
return images;
};
return 'png';
}
const render = async(style = {}) => {
const encodedArgs = JSON.stringify(style);
render.cache = render.cache || [];
export function getAppIconPath() {
return 'public/images/icon.png';
}
if (render.cache[encodedArgs]) {
return render.cache[encodedArgs];
export function getTrayIconPath({ title, count, platform, dark } = {}) {
if (typeof platform === 'undefined') {
platform = process.platform;
}
const rendererWindow = await getRendererWindow();
const jsCode = `(${ renderInWindow.toString() })(${ encodedArgs })`;
const images = await rendererWindow.webContents.executeJavaScript(jsCode);
const image = nativeImage.createEmpty();
for (const { dataURL, size, pixelRatio } of images) {
image.addRepresentation({
scaleFactor: pixelRatio,
width: size,
height: size,
dataURL,
});
if (platform === 'darwin' && typeof dark === 'undefined') {
dark = systemPreferences.isDarkMode();
}
image.setTemplateImage(style.template || false);
render.cache[encodedArgs] = image;
return image;
};
export default {
render,
};
const params = { title, count, platform, dark };
const iconset = getTrayIconSet(params);
const name = getTrayIconName(params);
const extension = getTrayIconExtension(params);
return `public/images/tray/${ iconset }/${ name }.${ extension }`;
}
export function getAppIconImage() {
return nativeImage.createFromPath(`${ __dirname }/${ getAppIconPath() }`);
}
export function getTrayIconImage({ title, count, platform, dark } = {}) {
return nativeImage.createFromPath(`${ __dirname }/${ getTrayIconPath({ title, count, platform, dark }) }`);
}
export function getIconImage({ badge: { title, count } }) {
const iconsetsPath = `${ __dirname }/public/images/tray`;
const { platform } = process;
const dark = systemPreferences.isDarkMode();
const params = { title, count, platform, dark };
const iconset = getTrayIconSet(params);
const name = getTrayIconName(params);
const extension = getTrayIconExtension(params);
return nativeImage.createFromPath(`${ iconsetsPath }/${ iconset }/${ name }.${ extension }`);
}
import { expect } from 'chai';
import { describe, it } from 'mocha';
import { getAppIconPath, getTrayIconPath, getAppIconImage, getTrayIconImage } from './icon';
describe('icon', () => {
describe('paths', () => {
it('app', () => {
expect(getAppIconPath()).to.be.equals('public/images/icon.png');
});
describe('tray', () => {
it('darwin', () => {
expect(getTrayIconPath({ platform: 'darwin', dark: false })).to.be.equals('public/images/tray/darwin/default.png');
expect(getTrayIconPath({ title: '', platform: 'darwin', dark: false })).to.be.equals('public/images/tray/darwin/notification.png');
expect(getTrayIconPath({ count: 1, platform: 'darwin', dark: false })).to.be.equals('public/images/tray/darwin/notification.png');
expect(getTrayIconPath({ count: 2, platform: 'darwin', dark: false })).to.be.equals('public/images/tray/darwin/notification.png');
expect(getTrayIconPath({ count: 3, platform: 'darwin', dark: false })).to.be.equals('public/images/tray/darwin/notification.png');
expect(getTrayIconPath({ count: 4, platform: 'darwin', dark: false })).to.be.equals('public/images/tray/darwin/notification.png');
expect(getTrayIconPath({ count: 5, platform: 'darwin', dark: false })).to.be.equals('public/images/tray/darwin/notification.png');
expect(getTrayIconPath({ count: 6, platform: 'darwin', dark: false })).to.be.equals('public/images/tray/darwin/notification.png');
expect(getTrayIconPath({ count: 7, platform: 'darwin', dark: false })).to.be.equals('public/images/tray/darwin/notification.png');
expect(getTrayIconPath({ count: 8, platform: 'darwin', dark: false })).to.be.equals('public/images/tray/darwin/notification.png');
expect(getTrayIconPath({ count: 9, platform: 'darwin', dark: false })).to.be.equals('public/images/tray/darwin/notification.png');
expect(getTrayIconPath({ count: 10, platform: 'darwin', dark: false })).to.be.equals('public/images/tray/darwin/notification.png');
});
it('darwin-dark', () => {
expect(getTrayIconPath({ platform: 'darwin', dark: true })).to.be.equals('public/images/tray/darwin-dark/default.png');
expect(getTrayIconPath({ title: '', platform: 'darwin', dark: true })).to.be.equals('public/images/tray/darwin-dark/notification.png');
expect(getTrayIconPath({ count: 1, platform: 'darwin', dark: true })).to.be.equals('public/images/tray/darwin-dark/notification.png');
expect(getTrayIconPath({ count: 2, platform: 'darwin', dark: true })).to.be.equals('public/images/tray/darwin-dark/notification.png');
expect(getTrayIconPath({ count: 3, platform: 'darwin', dark: true })).to.be.equals('public/images/tray/darwin-dark/notification.png');
expect(getTrayIconPath({ count: 4, platform: 'darwin', dark: true })).to.be.equals('public/images/tray/darwin-dark/notification.png');
expect(getTrayIconPath({ count: 5, platform: 'darwin', dark: true })).to.be.equals('public/images/tray/darwin-dark/notification.png');
expect(getTrayIconPath({ count: 6, platform: 'darwin', dark: true })).to.be.equals('public/images/tray/darwin-dark/notification.png');
expect(getTrayIconPath({ count: 7, platform: 'darwin', dark: true })).to.be.equals('public/images/tray/darwin-dark/notification.png');
expect(getTrayIconPath({ count: 8, platform: 'darwin', dark: true })).to.be.equals('public/images/tray/darwin-dark/notification.png');
expect(getTrayIconPath({ count: 9, platform: 'darwin', dark: true })).to.be.equals('public/images/tray/darwin-dark/notification.png');
expect(getTrayIconPath({ count: 10, platform: 'darwin', dark: true })).to.be.equals('public/images/tray/darwin-dark/notification.png');
});
it('linux', () => {
expect(getTrayIconPath({ platform: 'linux' })).to.be.equals('public/images/tray/linux/default.png');
expect(getTrayIconPath({ title: '', platform: 'linux' })).to.be.equals('public/images/tray/linux/notification-dot.png');
expect(getTrayIconPath({ count: 1, platform: 'linux' })).to.be.equals('public/images/tray/linux/notification-1.png');
expect(getTrayIconPath({ count: 2, platform: 'linux' })).to.be.equals('public/images/tray/linux/notification-2.png');
expect(getTrayIconPath({ count: 3, platform: 'linux' })).to.be.equals('public/images/tray/linux/notification-3.png');
expect(getTrayIconPath({ count: 4, platform: 'linux' })).to.be.equals('public/images/tray/linux/notification-4.png');
expect(getTrayIconPath({ count: 5, platform: 'linux' })).to.be.equals('public/images/tray/linux/notification-5.png');
expect(getTrayIconPath({ count: 6, platform: 'linux' })).to.be.equals('public/images/tray/linux/notification-6.png');
expect(getTrayIconPath({ count: 7, platform: 'linux' })).to.be.equals('public/images/tray/linux/notification-7.png');
expect(getTrayIconPath({ count: 8, platform: 'linux' })).to.be.equals('public/images/tray/linux/notification-8.png');
expect(getTrayIconPath({ count: 9, platform: 'linux' })).to.be.equals('public/images/tray/linux/notification-9.png');
expect(getTrayIconPath({ count: 10, platform: 'linux' })).to.be.equals('public/images/tray/linux/notification-plus-9.png');
});
it('win32', () => {
expect(getTrayIconPath({ platform: 'win32' })).to.be.equals('public/images/tray/win32/default.ico');
expect(getTrayIconPath({ title: '', platform: 'win32' })).to.be.equals('public/images/tray/win32/notification-dot.ico');
expect(getTrayIconPath({ count: 1, platform: 'win32' })).to.be.equals('public/images/tray/win32/notification-1.ico');
expect(getTrayIconPath({ count: 2, platform: 'win32' })).to.be.equals('public/images/tray/win32/notification-2.ico');
expect(getTrayIconPath({ count: 3, platform: 'win32' })).to.be.equals('public/images/tray/win32/notification-3.ico');
expect(getTrayIconPath({ count: 4, platform: 'win32' })).to.be.equals('public/images/tray/win32/notification-4.ico');
expect(getTrayIconPath({ count: 5, platform: 'win32' })).to.be.equals('public/images/tray/win32/notification-5.ico');
expect(getTrayIconPath({ count: 6, platform: 'win32' })).to.be.equals('public/images/tray/win32/notification-6.ico');
expect(getTrayIconPath({ count: 7, platform: 'win32' })).to.be.equals('public/images/tray/win32/notification-7.ico');
expect(getTrayIconPath({ count: 8, platform: 'win32' })).to.be.equals('public/images/tray/win32/notification-8.ico');
expect(getTrayIconPath({ count: 9, platform: 'win32' })).to.be.equals('public/images/tray/win32/notification-9.ico');
expect(getTrayIconPath({ count: 10, platform: 'win32' })).to.be.equals('public/images/tray/win32/notification-plus-9.ico');
});
});
});
describe('image', () => {
expect(getAppIconImage()).to.not.be.null;
expect(getTrayIconImage()).to.not.be.null;
});
});
......@@ -2,7 +2,6 @@ import { app, BrowserWindow, ipcMain } from 'electron';
import createWindowStateKeeper from './windowState';
import { whenReady, whenReadyToShow } from './utils';
import env from '../env';
import icon from './icon';
let mainWindow = null;
......@@ -84,15 +83,6 @@ export const getMainWindow = async() => {
mainWindow.loadURL(`file://${ __dirname }/public/app.html`);
attachWindowStateHandling(mainWindow);
if (process.platform !== 'darwin') {
mainWindow.setIcon(await icon.render({
size: {
win32: [256, 128, 64, 48, 32, 24, 16],
linux: 128,
}[process.platform],
}));
}
if (env.name === 'development') {
mainWindow.openDevTools();
}
......
......@@ -8,7 +8,6 @@ const createTemplate = ({
servers = [],
currentServerUrl = null,
showTrayIcon = true,
showUserStatusInTray = true,
showFullScreen = false,
showMenuBar = true,
showServerList = true,
......@@ -152,13 +151,6 @@ const createTemplate = ({
checked: showTrayIcon,
click: () => events.emit('toggle', 'showTrayIcon'),
},
{
label: i18n.__('User status in tray'),
type: 'checkbox',
enabled: showTrayIcon,
checked: showTrayIcon && showUserStatusInTray,
click: () => events.emit('toggle', 'showUserStatusInTray'),
},
...(process.platform === 'darwin' ? [
{
label: i18n.__('Full screen'),
......
import { Menu, Tray as TrayIcon } from 'electron';
import { Menu, systemPreferences, Tray as TrayIcon } from 'electron';
import { EventEmitter } from 'events';
import icon from './icon';
import i18n from '../i18n/index.js';
const getIconStyle = ({ badge: { title, count }, status, showUserStatus }) => {
const style = {
template: process.platform === 'darwin',
size: {
darwin: 24,
win32: [32, 24, 16],
linux: 22,
}[process.platform],
};
if (showUserStatus) {
style.status = status;
}
if (process.platform !== 'darwin') {
if (title === '') {
style.badgeText = '';
} else if (count > 0) {
style.badgeText = count > 9 ? '9+' : String(count);
} else if (title) {
style.badgeText = '!';
}
}
import i18n from '../i18n';
import { getTrayIconImage } from './icon';
return style;
};
const getIconTitle = ({ badge: { title, count } }) => ((count > 0) ? title : '');
......@@ -53,21 +26,30 @@ let state = {
title: '',
count: 0,
},
status: 'online',
isMainWindowVisible: true,
showIcon: true,
showUserStatus: true,
};
const instance = new (class Tray extends EventEmitter {});
const createIcon = (image) => {
let darwinThemeSubscriberId = null;
const createIcon = () => {
const image = getTrayIconImage(state.badge);
if (trayIcon) {
trayIcon.setImage(image);
return;
}
trayIcon = new TrayIcon(image);
if (process.platform === 'darwin') {
darwinThemeSubscriberId = systemPreferences.subscribeNotification('AppleInterfaceThemeChangedNotification', () => {
trayIcon.setImage(getTrayIconImage(state.badge));
});
}
trayIcon.on('click', () => instance.emit('set-main-window-visibility', !state.isMainWindowVisible));
trayIcon.on('right-click', (event, bounds) => trayIcon.popUpContextMenu(undefined, bounds));
......@@ -79,6 +61,12 @@ const destroyIcon = () => {
return;
}
if (process.platform === 'darwin' && darwinThemeSubscriberId) {
systemPreferences.unsubscribeNotification(darwinThemeSubscriberId);
darwinThemeSubscriberId = null;
}
trayIcon.destroy();
instance.emit('destroyed');
trayIcon = null;
......@@ -89,20 +77,14 @@ const destroy = () => {
instance.removeAllListeners();
};
const update = async() => {
const update = () => {
if (!state.showIcon) {
destroyIcon();
instance.emit('update');
return;
}
const image = await icon.render(getIconStyle(state));
if (!trayIcon) {
createIcon(image);
} else {
trayIcon.setImage(image);
}
createIcon();
trayIcon.setToolTip(getIconTooltip(state));
......
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 64 64">
<defs>
<path id="b" d="M52.5968293 20.8792219l.0060158.0094865a54.88627861 54.88627861 0 0 0-.0030086-.0047427 62.25256027 62.25256027 0 0 0-.0030072-.0047438zM18.0701317 2.27327151c1.8488166 1.04693238 3.5964185 2.37153218 5.0888873 3.84380055 2.4057974-.44426787 4.8864578-.66829499 7.4030524-.66829499 7.5336135 0 14.6761427 2.01498197 20.1105976 5.67261913 2.8142499 1.8950801 5.0511564 4.1435552 6.6478345 6.683581C59.0986495 20.6352916 60 23.6789052 60 26.9402352c0 3.1736124-.9013505 6.2184881-2.6794965 9.0481715-1.5966781 2.5412879-3.8335846 4.7891319-6.6478345 6.684212-5.4344549 3.6576372-12.5763852 5.671357-20.1105976 5.671357-2.5165946 0-4.9966561-.2240271-7.4030524-.6676639-1.4930677 1.4716373-3.2400707 2.7968681-5.0888873 3.8438005C8.19180899 56.5641934 0 51.638752 0 51.638752s7.61626224-6.592708 6.37772876-12.3719766c-3.4077638-3.5610847-5.25418485-7.8560948-5.25418485-12.4129957 0-4.4710765 1.84701996-8.7660866 5.25418485-12.3278023C7.61605716 8.74829665.00252205 2.15681509 6.3e-7 2.15463234c.00243608-.00146559 8.19327752-4.92532268 18.07013107.11863917zM12.1617249 36.8828377c.9535649 3.1010866.3949246 6.5962718-1.675921 10.4855555-.0990736.1860716-.1913618.3742694-.2915859.5600486 1.7766537-.1515808 3.5724085-.6483592 5.3909295-1.5588996 1.371962-.7874304 2.6881752-1.7866971 3.8111477-2.8909174l2.0667191-2.0322075c2.8268721.776453 5.9283366 1.2051709 9.1816138 1.2051709 13.200062 0 23.9008264-7.0579968 23.9008264-15.764477 0-8.7064803-10.7007644-15.764477-23.9008264-15.764477-13.200062 0-23.90082645 7.0579967-23.90082645 15.764477 0 3.7942172 2.03223505 7.2753627 5.41792325 9.9957265z"/>
<filter id="a" width="111.1%" height="111.2%" x="-6.1%" y="-5.6%" filterUnits="objectBoundingBox">
<feMorphology in="SourceAlpha" operator="dilate" radius=".5" result="shadowSpreadOuter1"/>
<feOffset in="shadowSpreadOuter1" result="shadowOffsetOuter1"/>
<feGaussianBlur in="shadowOffsetOuter1" result="shadowBlurOuter1" stdDeviation=".5"/>
<feColorMatrix in="shadowBlurOuter1" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.05 0"/>
</filter>
<path id="c" d="M19.0811772 30.2143846c-1.9302871 0-3.495094-1.6072207-3.495094-3.5898279s1.5648069-3.5898279 3.495094-3.5898279c1.930287 0 3.4950939 1.6072207 3.4950939 3.5898279s-1.5648069 3.5898279-3.4950939 3.5898279zm11.2524975 0c-1.9302871 0-3.495094-1.6072207-3.495094-3.5898279s1.5648069-3.5898279 3.495094-3.5898279c1.930287 0 3.4950939 1.6072207 3.4950939 3.5898279s-1.5648069 3.5898279-3.4950939 3.5898279zm11.2524975 0c-1.9302871 0-3.495094-1.6072207-3.495094-3.5898279s1.5648069-3.5898279 3.495094-3.5898279c1.930287 0 3.4950939 1.6072207 3.4950939 3.5898279s-1.5648069 3.5898279-3.4950939 3.5898279z"/>
<filter id="d" width="103.4%" height="113.9%" x="-1.7%" y="-7%" filterUnits="objectBoundingBox">
<feGaussianBlur in="SourceAlpha" result="shadowBlurInner1" stdDeviation=".5"/>
<feOffset in="shadowBlurInner1" result="shadowOffsetInner1"/>
<feComposite in="shadowOffsetInner1" in2="SourceAlpha" k2="-1" k3="1" operator="arithmetic" result="shadowInnerInner1"/>
<feColorMatrix in="shadowInnerInner1" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.1 0"/>
</filter>
</defs>
<g fill="#000" fill-rule="nonzero">
<g transform="translate(2 5)">
<use filter="url(#a)" xlink:href="#b"/>
<use xlink:href="#b"/>
</g>
<g transform="translate(2 5)">
<use xlink:href="#c"/>
<use filter="url(#d)" xlink:href="#c"/>
</g>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 64 64">
<defs>
<path id="b" d="M58.6682942 33.4215668c-.9643773-2.08574-2.4097131-3.9035789-4.193725-5.3112344.046968-.4036748.0708853-.8116057.0708853-1.2232212 0-8.7064803-10.7007644-15.764477-23.9008264-15.764477-13.200062 0-23.90082645 7.0579967-23.90082645 15.764477 0 3.7942172 2.03223505 7.2753627 5.41792325 9.9957265.9535649 3.1010866.3949246 6.5962718-1.675921 10.4855555-.0990736.1860716-.1913618.3742694-.2915859.5600486 1.7766537-.1515808 3.5724085-.6483592 5.3909295-1.5588996 1.371962-.7874304 2.6881752-1.7866971 3.8111477-2.8909174l2.0667191-2.0322075c2.8268721.776453 5.9283366 1.2051709 9.1816138 1.2051709.2332548 0 .4657291-.0022039.6973778-.0065818.4538902 2.0524604 1.3427017 3.9414928 2.5648137 5.5654767-1.1032478.0885845-2.2196238.1334926-3.3447482.1334926-2.5165946 0-4.9966561-.2240271-7.4030524-.6676639-1.4930677 1.4716373-3.2400707 2.7968681-5.0888873 3.8438005C8.19180899 56.5641934 0 51.638752 0 51.638752s7.61626224-6.592708 6.37772876-12.3719766c-3.4077638-3.5610847-5.25418485-7.8560948-5.25418485-12.4129957 0-4.4710765 1.84701996-8.7660866 5.25418485-12.3278023C7.61605716 8.74829665.00252205 2.15681509 6.3e-7 2.15463234c.00243608-.00146559 8.19327752-4.92532268 18.07013107.11863917 1.8488166 1.04693238 3.5964185 2.37153218 5.0888873 3.84380055 2.4057974-.44426787 4.8864578-.66829499 7.4030524-.66829499 7.5336135 0 14.6761427 2.01498197 20.1105976 5.67261913 2.8142499 1.8950801 5.0511564 4.1435552 6.6478345 6.683581C59.0986495 20.6352916 60 23.6789052 60 26.9402352c0 2.2343091-.4467581 4.4048093-1.3317058 6.4813316zm-6.0714649-12.5423449l.0060158.0094865a54.88627861 54.88627861 0 0 0-.0030086-.0047427 62.25256027 62.25256027 0 0 0-.0030072-.0047438z"/>
<filter id="a" width="111.1%" height="111.2%" x="-6.1%" y="-5.6%" filterUnits="objectBoundingBox">
<feMorphology in="SourceAlpha" operator="dilate" radius=".5" result="shadowSpreadOuter1"/>
<feOffset in="shadowSpreadOuter1" result="shadowOffsetOuter1"/>
<feGaussianBlur in="shadowOffsetOuter1" result="shadowBlurOuter1" stdDeviation=".5"/>
<feColorMatrix in="shadowBlurOuter1" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.05 0"/>
</filter>
<path id="c" d="M19.0811772 30.2143846c-1.9302871 0-3.495094-1.6072207-3.495094-3.5898279s1.5648069-3.5898279 3.495094-3.5898279c1.930287 0 3.4950939 1.6072207 3.4950939 3.5898279s-1.5648069 3.5898279-3.4950939 3.5898279zm11.2524975 0c-1.9302871 0-3.495094-1.6072207-3.495094-3.5898279s1.5648069-3.5898279 3.495094-3.5898279c1.930287 0 3.4950939 1.6072207 3.4950939 3.5898279s-1.5648069 3.5898279-3.4950939 3.5898279zm7.7784344-3.1937589c-.0138975-.130059-.0210309-.2622084-.0210309-.396069 0-1.9826072 1.5648069-3.5898279 3.495094-3.5898279 1.3691471 0 2.5544203.8085968 3.1278408 1.9862112-2.402504.1283338-4.649736.8414931-6.6019039 1.9996857z"/>
<filter id="d" width="103.4%" height="113.9%" x="-1.7%" y="-7%" filterUnits="objectBoundingBox">
<feGaussianBlur in="SourceAlpha" result="shadowBlurInner1" stdDeviation=".5"/>
<feOffset in="shadowBlurInner1" result="shadowOffsetInner1"/>
<feComposite in="shadowOffsetInner1" in2="SourceAlpha" k2="-1" k3="1" operator="arithmetic" result="shadowInnerInner1"/>
<feColorMatrix in="shadowInnerInner1" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.1 0"/>
</filter>
</defs>
<g fill="none" fill-rule="evenodd">
<g fill-rule="nonzero">
<g fill="#000" transform="translate(2 5)">
<use filter="url(#a)" xlink:href="#b"/>
<use xlink:href="#b"/>
</g>
<path fill="#FFF" d="M56.4745692 33.1103324C54.0058894 31.1624471 50.8886967 30 47.5 30 39.4918711 30 33 36.4918711 33 44.5c0 1.0799802.1180699 2.1323836.3420059 3.1450064-.2316487.0043779-.464123.0065818-.6973778.0065818-3.2532772 0-6.3547417-.4287179-9.1816138-1.2051709l-2.0667191 2.0322075c-1.1229725 1.1042203-2.4391857 2.103487-3.8111477 2.8909174-1.818521.9105404-3.6142758 1.4073188-5.3909295 1.5588996.1002241-.1857792.1925123-.373977.2915859-.5600486 2.0708456-3.8892837 2.6294859-7.3844689 1.675921-10.4855555-3.3856882-2.7203638-5.41792325-6.2015093-5.41792325-9.9957265 0-8.7064803 10.70076445-15.764477 23.90082645-15.764477s23.9008264 7.0579967 23.9008264 15.764477c0 .4116155-.0239173.8195464-.0708853 1.2232212z"/>
<g transform="translate(2 5)">
<use fill="#1F2329" xlink:href="#c"/>
<use fill="#000" filter="url(#d)" xlink:href="#c"/>
</g>
</g>
<path fill="#F5455C" fill-rule="nonzero" d="M47.5 56C41.14872538 56 36 50.8512746 36 44.5 36 38.14872538 41.14872538 33 47.5 33 53.8512746 33 59 38.14872538 59 44.5 59 50.8512746 53.8512746 56 47.5 56z"/>
<path fill="#FFF" d="M44.4 39h4.4v8.8H51V50h-6.6v-2.2h2.2v-6.6h-2.2z"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 64 64">
<defs>
<path id="b" d="M58.6682942 33.4215668c-.9643773-2.08574-2.4097131-3.9035789-4.193725-5.3112344.046968-.4036748.0708853-.8116057.0708853-1.2232212 0-8.7064803-10.7007644-15.764477-23.9008264-15.764477-13.200062 0-23.90082645 7.0579967-23.90082645 15.764477 0 3.7942172 2.03223505 7.2753627 5.41792325 9.9957265.9535649 3.1010866.3949246 6.5962718-1.675921 10.4855555-.0990736.1860716-.1913618.3742694-.2915859.5600486 1.7766537-.1515808 3.5724085-.6483592 5.3909295-1.5588996 1.371962-.7874304 2.6881752-1.7866971 3.8111477-2.8909174l2.0667191-2.0322075c2.8268721.776453 5.9283366 1.2051709 9.1816138 1.2051709.2332548 0 .4657291-.0022039.6973778-.0065818.4538902 2.0524604 1.3427017 3.9414928 2.5648137 5.5654767-1.1032478.0885845-2.2196238.1334926-3.3447482.1334926-2.5165946 0-4.9966561-.2240271-7.4030524-.6676639-1.4930677 1.4716373-3.2400707 2.7968681-5.0888873 3.8438005C8.19180899 56.5641934 0 51.638752 0 51.638752s7.61626224-6.592708 6.37772876-12.3719766c-3.4077638-3.5610847-5.25418485-7.8560948-5.25418485-12.4129957 0-4.4710765 1.84701996-8.7660866 5.25418485-12.3278023C7.61605716 8.74829665.00252205 2.15681509 6.3e-7 2.15463234c.00243608-.00146559 8.19327752-4.92532268 18.07013107.11863917 1.8488166 1.04693238 3.5964185 2.37153218 5.0888873 3.84380055 2.4057974-.44426787 4.8864578-.66829499 7.4030524-.66829499 7.5336135 0 14.6761427 2.01498197 20.1105976 5.67261913 2.8142499 1.8950801 5.0511564 4.1435552 6.6478345 6.683581C59.0986495 20.6352916 60 23.6789052 60 26.9402352c0 2.2343091-.4467581 4.4048093-1.3317058 6.4813316zm-6.0714649-12.5423449l.0060158.0094865a54.88627861 54.88627861 0 0 0-.0030086-.0047427 62.25256027 62.25256027 0 0 0-.0030072-.0047438z"/>
<filter id="a" width="111.1%" height="111.2%" x="-6.1%" y="-5.6%" filterUnits="objectBoundingBox">
<feMorphology in="SourceAlpha" operator="dilate" radius=".5" result="shadowSpreadOuter1"/>
<feOffset in="shadowSpreadOuter1" result="shadowOffsetOuter1"/>
<feGaussianBlur in="shadowOffsetOuter1" result="shadowBlurOuter1" stdDeviation=".5"/>
<feColorMatrix in="shadowBlurOuter1" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.05 0"/>
</filter>
<path id="c" d="M19.0811772 30.2143846c-1.9302871 0-3.495094-1.6072207-3.495094-3.5898279s1.5648069-3.5898279 3.495094-3.5898279c1.930287 0 3.4950939 1.6072207 3.4950939 3.5898279s-1.5648069 3.5898279-3.4950939 3.5898279zm11.2524975 0c-1.9302871 0-3.495094-1.6072207-3.495094-3.5898279s1.5648069-3.5898279 3.495094-3.5898279c1.930287 0 3.4950939 1.6072207 3.4950939 3.5898279s-1.5648069 3.5898279-3.4950939 3.5898279zm7.7784344-3.1937589c-.0138975-.130059-.0210309-.2622084-.0210309-.396069 0-1.9826072 1.5648069-3.5898279 3.495094-3.5898279 1.3691471 0 2.5544203.8085968 3.1278408 1.9862112-2.402504.1283338-4.649736.8414931-6.6019039 1.9996857z"/>
<filter id="d" width="103.4%" height="113.9%" x="-1.7%" y="-7%" filterUnits="objectBoundingBox">
<feGaussianBlur in="SourceAlpha" result="shadowBlurInner1" stdDeviation=".5"/>
<feOffset in="shadowBlurInner1" result="shadowOffsetInner1"/>
<feComposite in="shadowOffsetInner1" in2="SourceAlpha" k2="-1" k3="1" operator="arithmetic" result="shadowInnerInner1"/>
<feColorMatrix in="shadowInnerInner1" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.1 0"/>
</filter>
</defs>
<g fill="none" fill-rule="evenodd">
<g fill-rule="nonzero">
<g fill="#000" transform="translate(2 5)">
<use filter="url(#a)" xlink:href="#b"/>
<use xlink:href="#b"/>
</g>
<path fill="#FFF" d="M56.4745692 33.1103324C54.0058894 31.1624471 50.8886967 30 47.5 30 39.4918711 30 33 36.4918711 33 44.5c0 1.0799802.1180699 2.1323836.3420059 3.1450064-.2316487.0043779-.464123.0065818-.6973778.0065818-3.2532772 0-6.3547417-.4287179-9.1816138-1.2051709l-2.0667191 2.0322075c-1.1229725 1.1042203-2.4391857 2.103487-3.8111477 2.8909174-1.818521.9105404-3.6142758 1.4073188-5.3909295 1.5588996.1002241-.1857792.1925123-.373977.2915859-.5600486 2.0708456-3.8892837 2.6294859-7.3844689 1.675921-10.4855555-3.3856882-2.7203638-5.41792325-6.2015093-5.41792325-9.9957265 0-8.7064803 10.70076445-15.764477 23.90082645-15.764477s23.9008264 7.0579967 23.9008264 15.764477c0 .4116155-.0239173.8195464-.0708853 1.2232212z"/>
<g transform="translate(2 5)">