diff --git a/.eslintrc.js b/.eslintrc.js index 2d4a2a4dc58..8ae96a7754c 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -21,6 +21,7 @@ module.exports = { 'src/ext/', 'src/script/localization/**/webapp*.js', 'src/worker/', + 'src/script/generated/', '*.js', ], parserOptions: { diff --git a/.gitignore b/.gitignore index 8e3c33965f7..22835e77626 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ /keys /package-lock.json /resource +/src/script/generated /temp CHANGELOG.md node_modules diff --git a/.prettierignore b/.prettierignore index 439678a4661..b07ba1a1e9e 100644 --- a/.prettierignore +++ b/.prettierignore @@ -7,6 +7,7 @@ src/i18n/ /resource /assets /src/ext +/src/script/generated CHANGELOG.md node_modules npm-debug.log diff --git a/bin/generate_icon_names.ts b/bin/generate_icon_names.ts new file mode 100644 index 00000000000..1186d8ffb2f --- /dev/null +++ b/bin/generate_icon_names.ts @@ -0,0 +1,37 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + * + */ +import fs from 'fs'; + +const fileLocation = 'resource/image/icon'; +const fileList = fs.readdirSync(fileLocation).filter(file => file.endsWith('.svg')); + +const fileContent = ` +/* + * This file is generated by bin/generate_icons.ts + * To refetch all the icons and regenerate their names, run yarn configure. +*/ +export const iconFileNames = [${fileList.map(name => `'${name}'`).join(', ')}] as const; +`; + +const dir = 'src/script/generated'; + +if (!fs.existsSync(dir)) { + fs.mkdirSync(dir); +} +fs.writeFileSync(`${dir}/iconFileNames.ts`, fileContent); diff --git a/package.json b/package.json index a40abe330e0..ac0b27a44b1 100644 --- a/package.json +++ b/package.json @@ -176,7 +176,8 @@ "changelog:production": "ts-node ./bin/changelog.ts production", "changelog:staging": "ts-node ./bin/changelog.ts staging", "changelog:rc": "ts-node ./bin/changelog.ts production master", - "configure": "copy-config", + "configure": "copy-config && yarn generate-icon-names", + "generate-icon-names": "ts-node ./bin/generate_icon_names.ts", "deploy": "yarn build:prod && eb deploy", "dev": "yarn start", "docker": "node ./bin/push_docker.js", diff --git a/src/script/auth/component/WirelessContainer.tsx b/src/script/auth/component/WirelessContainer.tsx index 87b7b7a2d76..9b2c98fa8c6 100644 --- a/src/script/auth/component/WirelessContainer.tsx +++ b/src/script/auth/component/WirelessContainer.tsx @@ -24,10 +24,11 @@ import {FormattedMessage, useIntl} from 'react-intl'; import {CloseIcon, Content, Footer, Header, Link, Small} from '@wireapp/react-ui-kit'; +import {getSVG} from 'Util/SVGProvider'; + import {Config} from '../../Config'; import {cookiePolicyStrings, footerStrings} from '../../strings'; import {EXTERNAL_ROUTE} from '../externalRoute'; -import {getSVG} from '../util/SVGProvider'; export interface Props extends React.HTMLAttributes { children: React.ReactNode; diff --git a/src/script/auth/page/CustomEnvironmentRedirect.test.tsx b/src/script/auth/page/CustomEnvironmentRedirect.test.tsx index c17c45f4d52..25332d5962a 100644 --- a/src/script/auth/page/CustomEnvironmentRedirect.test.tsx +++ b/src/script/auth/page/CustomEnvironmentRedirect.test.tsx @@ -26,7 +26,7 @@ import {initialRootState} from '../module/reducer'; import {mockStoreFactory} from '../util/test/mockStoreFactory'; import {mountComponent} from '../util/test/TestUtil'; -jest.mock('../util/SVGProvider'); +jest.mock('Util/SVGProvider'); function createMockedURLSearchParams(value: string) { return class MockedURLSearchParams extends window.URLSearchParams { constructor() { diff --git a/src/script/auth/page/CustomEnvironmentRedirect.tsx b/src/script/auth/page/CustomEnvironmentRedirect.tsx index e62e3c162e0..6d5e063744d 100644 --- a/src/script/auth/page/CustomEnvironmentRedirect.tsx +++ b/src/script/auth/page/CustomEnvironmentRedirect.tsx @@ -27,6 +27,7 @@ import {AnyAction, Dispatch} from 'redux'; import {Runtime, UrlUtil} from '@wireapp/commons'; import {COLOR, ContainerXS, FlexBox, Text} from '@wireapp/react-ui-kit'; +import {getSVG} from 'Util/SVGProvider'; import {afterRender} from 'Util/util'; import {Page} from './Page'; @@ -35,7 +36,6 @@ import {customEnvRedirectStrings} from '../../strings'; import {actionRoot} from '../module/action'; import {bindActionCreators} from '../module/reducer'; import {QUERY_KEY} from '../route'; -import {getSVG} from '../util/SVGProvider'; const REDIRECT_DELAY = 5000; const CustomEnvironmentRedirectComponent = ({doNavigate, doSendNavigationEvent}: DispatchProps) => { diff --git a/src/script/auth/page/Index.test.tsx b/src/script/auth/page/Index.test.tsx index 91073c9322e..08f98684d90 100644 --- a/src/script/auth/page/Index.test.tsx +++ b/src/script/auth/page/Index.test.tsx @@ -29,7 +29,7 @@ import {ROUTE} from '../route'; import {mockStoreFactory} from '../util/test/mockStoreFactory'; import {mountComponent} from '../util/test/TestUtil'; -jest.mock('../util/SVGProvider'); +jest.mock('Util/SVGProvider'); jest.mock('react-router-dom', () => ({ ...jest.requireActual('react-router-dom'), diff --git a/src/script/auth/page/Index.tsx b/src/script/auth/page/Index.tsx index c06b663c43d..34cac91f44d 100644 --- a/src/script/auth/page/Index.tsx +++ b/src/script/auth/page/Index.tsx @@ -28,6 +28,8 @@ import {AnyAction, Dispatch} from 'redux'; import {UrlUtil} from '@wireapp/commons'; import {Button, ButtonVariant, ContainerXS, ErrorMessage, Text} from '@wireapp/react-ui-kit'; +import {getSVG} from 'Util/SVGProvider'; + import {Page} from './Page'; import {Config} from '../../Config'; @@ -36,7 +38,6 @@ import {indexStrings, logoutReasonStrings} from '../../strings'; import {bindActionCreators, RootState} from '../module/reducer'; import * as AuthSelector from '../module/selector/AuthSelector'; import {QUERY_KEY, ROUTE} from '../route'; -import {getSVG} from '../util/SVGProvider'; type Props = React.HTMLProps; diff --git a/src/script/auth/page/Login.test.tsx b/src/script/auth/page/Login.test.tsx index 8a2fdf70e8e..24aab88153a 100644 --- a/src/script/auth/page/Login.test.tsx +++ b/src/script/auth/page/Login.test.tsx @@ -33,7 +33,7 @@ import {ROUTE} from '../route'; import {mockStoreFactory} from '../util/test/mockStoreFactory'; import {mountComponent} from '../util/test/TestUtil'; -jest.mock('../util/SVGProvider'); +jest.mock('Util/SVGProvider'); describe('Login', () => { it('successfully logs in with email', async () => { const historyPushSpy = spyOn(history, 'pushState'); diff --git a/src/script/auth/page/PhoneLogin.test.tsx b/src/script/auth/page/PhoneLogin.test.tsx index 4355c502404..c62e779e773 100644 --- a/src/script/auth/page/PhoneLogin.test.tsx +++ b/src/script/auth/page/PhoneLogin.test.tsx @@ -27,7 +27,7 @@ import {initialRootState} from '../module/reducer'; import {ROUTE} from '../route'; import {mockStoreFactory} from '../util/test/mockStoreFactory'; import {mountComponent} from '../util/test/TestUtil'; -jest.mock('../util/SVGProvider'); +jest.mock('Util/SVGProvider'); const backButtonId = 'go-login'; const phoneInputId = 'enter-phone'; const countryCodeInputId = 'enter-country-code'; diff --git a/src/script/auth/page/SetAccountType.test.tsx b/src/script/auth/page/SetAccountType.test.tsx index 4f80bf84ca2..6e3c54109aa 100644 --- a/src/script/auth/page/SetAccountType.test.tsx +++ b/src/script/auth/page/SetAccountType.test.tsx @@ -25,7 +25,7 @@ import {Config, Configuration} from '../../Config'; import {initialRootState} from '../module/reducer'; import {mockStoreFactory} from '../util/test/mockStoreFactory'; import {mountComponent} from '../util/test/TestUtil'; -jest.mock('../util/SVGProvider'); +jest.mock('Util/SVGProvider'); jest.mock('react-router-dom', () => ({ ...jest.requireActual('react-router-dom'), Navigate: function Navigate({to}: any) { diff --git a/src/script/auth/page/SetEmail.test.tsx b/src/script/auth/page/SetEmail.test.tsx index a821b9998e9..4d16a7ac1e2 100644 --- a/src/script/auth/page/SetEmail.test.tsx +++ b/src/script/auth/page/SetEmail.test.tsx @@ -28,7 +28,7 @@ import {initialRootState} from '../module/reducer'; import {mockStoreFactory} from '../util/test/mockStoreFactory'; import {mountComponent} from '../util/test/TestUtil'; -jest.mock('../util/SVGProvider'); +jest.mock('Util/SVGProvider'); const emailInputId = 'enter-email'; const verifyButtonId = 'do-verify-email'; diff --git a/src/script/auth/page/SetHandle.test.tsx b/src/script/auth/page/SetHandle.test.tsx index 60c2605ea97..89a599e3e0a 100644 --- a/src/script/auth/page/SetHandle.test.tsx +++ b/src/script/auth/page/SetHandle.test.tsx @@ -25,7 +25,7 @@ import {actionRoot} from '../module/action'; import {initialRootState} from '../module/reducer'; import {mockStoreFactory} from '../util/test/mockStoreFactory'; import {mountComponent} from '../util/test/TestUtil'; -jest.mock('../util/SVGProvider'); +jest.mock('Util/SVGProvider'); const handleInputId = 'enter-handle'; const setHandleButtonId = 'do-send-handle'; diff --git a/src/script/auth/page/SetPassword.test.tsx b/src/script/auth/page/SetPassword.test.tsx index 856ab2bd21d..c138f3c088e 100644 --- a/src/script/auth/page/SetPassword.test.tsx +++ b/src/script/auth/page/SetPassword.test.tsx @@ -25,7 +25,7 @@ import {ValidationError} from '../module/action/ValidationError'; import {initialRootState} from '../module/reducer'; import {mockStoreFactory} from '../util/test/mockStoreFactory'; import {mountComponent} from '../util/test/TestUtil'; -jest.mock('../util/SVGProvider'); +jest.mock('Util/SVGProvider'); const passwordInputId = 'enter-password'; const setPasswordButtonId = 'do-set-password'; diff --git a/src/script/auth/page/TeamName.test.tsx b/src/script/auth/page/TeamName.test.tsx index 95dcb1a5c0a..e8ebc469030 100644 --- a/src/script/auth/page/TeamName.test.tsx +++ b/src/script/auth/page/TeamName.test.tsx @@ -26,7 +26,7 @@ import {initialRootState} from '../module/reducer'; import {initialAuthState} from '../module/reducer/authReducer'; import {mockStoreFactory} from '../util/test/mockStoreFactory'; import {mountComponent} from '../util/test/TestUtil'; -jest.mock('../util/SVGProvider'); +jest.mock('Util/SVGProvider'); describe('when entering a team name', () => { describe('the submit button', () => { diff --git a/src/script/auth/util/SVGProvider.ts b/src/script/auth/util/SVGProvider.ts deleted file mode 100644 index da246b2bfaa..00000000000 --- a/src/script/auth/util/SVGProvider.ts +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Wire - * Copyright (C) 2020 Wire Swiss GmbH - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see http://www.gnu.org/licenses/. - * - */ - -const fileList = require.context('Resource/image/icon', true, /.+\.svg$/); -export type SVGProvider = {[index: string]: Document}; - -const svgs: SVGProvider = {}; - -const parser = new DOMParser(); -fileList.keys().forEach(iconFileName => { - const iconPath = iconFileName.replace(/^\.\//, ''); - const iconName = iconFileName.substring(iconFileName.lastIndexOf('/') + 1).replace(/\.svg$/i, ''); - const svgString = require(`Resource/image/icon/${iconPath}`); - svgs[iconName] = parser.parseFromString(svgString, 'image/svg+xml'); -}); - -const getAllSVGs = () => svgs; -const getSVG = (iconName: string) => svgs[iconName]; - -export {getAllSVGs, getSVG}; diff --git a/src/script/components/Avatar/AvatarImage.test.tsx b/src/script/components/Avatar/AvatarImage.test.tsx index 29fd8dc0b9c..db1636bcfce 100644 --- a/src/script/components/Avatar/AvatarImage.test.tsx +++ b/src/script/components/Avatar/AvatarImage.test.tsx @@ -26,7 +26,7 @@ import {AvatarImage} from './AvatarImage'; import {AssetRepository} from '../../assets/AssetRepository'; import {User} from '../../entity/User'; -jest.mock('../../auth/util/SVGProvider'); +jest.mock('Util/SVGProvider'); describe('AvatarImage', () => { it('fetches full avatar image for large avatars', async () => { diff --git a/src/script/components/Avatar/ServiceAvatar.test.tsx b/src/script/components/Avatar/ServiceAvatar.test.tsx index 72e18416360..df903d1c929 100644 --- a/src/script/components/Avatar/ServiceAvatar.test.tsx +++ b/src/script/components/Avatar/ServiceAvatar.test.tsx @@ -24,7 +24,7 @@ import {AVATAR_SIZE} from 'Components/Avatar'; import {ServiceAvatar} from './ServiceAvatar'; import {ServiceEntity} from '../../integration/ServiceEntity'; -jest.mock('../../auth/util/SVGProvider'); +jest.mock('Util/SVGProvider'); describe('ServiceAvatar', () => { it('shows a service icon', async () => { diff --git a/src/script/components/Avatar/TemporaryGuest.test.tsx b/src/script/components/Avatar/TemporaryGuest.test.tsx index 331f99f8e13..9f1f92d3fce 100644 --- a/src/script/components/Avatar/TemporaryGuest.test.tsx +++ b/src/script/components/Avatar/TemporaryGuest.test.tsx @@ -24,7 +24,7 @@ import {TemporaryGuestAvatar} from './TemporaryGuestAvatar'; import {User} from '../../entity/User'; import {AVATAR_SIZE, STATE} from '.'; -jest.mock('../../auth/util/SVGProvider'); +jest.mock('Util/SVGProvider'); describe('TemporaryGuestAvatar', () => { it('shows expiration circle', async () => { diff --git a/src/script/components/Avatar/UserAvatar.test.tsx b/src/script/components/Avatar/UserAvatar.test.tsx index 2193ac9c1ef..3636dbf3bd6 100644 --- a/src/script/components/Avatar/UserAvatar.test.tsx +++ b/src/script/components/Avatar/UserAvatar.test.tsx @@ -24,7 +24,7 @@ import {UserAvatar} from './UserAvatar'; import {User} from '../../entity/User'; import {AVATAR_SIZE, STATE} from '.'; -jest.mock('../../auth/util/SVGProvider'); +jest.mock('Util/SVGProvider'); describe('UserAvatar', () => { it('shows participant initials if no avatar is defined', async () => { diff --git a/src/script/components/Icon.tsx b/src/script/components/Icon.tsx index cc93f6e0296..498737ad426 100644 --- a/src/script/components/Icon.tsx +++ b/src/script/components/Icon.tsx @@ -19,30 +19,41 @@ import React from 'react'; -import {getAllSVGs} from '../auth/util/SVGProvider'; +import {typedEntries} from 'Util/ArrayUtil'; +import {SVGIconName, getAllSVGs} from 'Util/SVGProvider'; +import {PascalCase, RemoveSuffix} from 'Util/TypeUtil'; + +type PascalCaseIconName = PascalCase>; type IconProps = React.SVGProps; -type IconList = Record>; +type IconList = Record>; interface NamedIconProps extends IconProps { - name: string; + name: SVGIconName; } -const normalizeIconName = (name: string) => +const normalizeIconName = (name: SVGIconName): PascalCaseIconName => name .replace(/-icon$/, '') - .replace(/\b\w/g, found => found.toUpperCase()) - .replace(/-/g, ''); + .replace(/\b\w/g, (found: string) => found.toUpperCase()) + .replace(/-/g, '') as PascalCaseIconName; const createSvgComponent = (svg: HTMLElement, displayName: string): React.FC => { const SVGComponent: React.FC = oProps => { const viewBox = svg.getAttribute('viewBox'); if (!viewBox) { - console.error('Svg icon must have a viewBox attribute'); + throw Error('Svg icon must have a viewBox attribute'); } const regex = /0 0 (?\d+) (?\d+)/; - const {width, height} = regex.exec(viewBox).groups; + + const match = regex.exec(viewBox); + + if (!match) { + throw Error('Svg icon viewBox attribute must be in the format "0 0 width height"'); + } + + const {width, height} = match.groups as {width: string; height: string}; const props = { height: oProps.height ?? height, @@ -57,10 +68,13 @@ const createSvgComponent = (svg: HTMLElement, displayName: string): React.FC((list, [key, svg]) => { - const name = normalizeIconName(key); - return Object.assign(list, {[name]: createSvgComponent(svg.documentElement, `Icon.${name}`)}); -}, {}); +const icons = typedEntries(getAllSVGs()).reduce( + (list, [key, svg]) => { + const name = normalizeIconName(key); + return Object.assign(list, {[name]: createSvgComponent(svg.documentElement, `Icon.${name}`)}); + }, + {} as Record>, +); const IconComponent: React.FC = ({name, ...props}) => { const componentName = normalizeIconName(name); diff --git a/src/script/components/calling/CallingCell.tsx b/src/script/components/calling/CallingCell.tsx index 5637c32e97f..f714820ea2d 100644 --- a/src/script/components/calling/CallingCell.tsx +++ b/src/script/components/calling/CallingCell.tsx @@ -198,7 +198,7 @@ const CallingCell: React.FC = ({ const getParticipantContext = (event: React.MouseEvent, participant: Participant) => { event.preventDefault(); - const muteParticipant = { + const muteParticipant: ContextMenuEntry = { click: () => callingRepository.sendModeratorMute(conversation.qualifiedId, [participant]), icon: 'mic-off-icon', identifier: `moderator-mute-participant`, diff --git a/src/script/components/calling/ParticipantMicOnIcon.tsx b/src/script/components/calling/ParticipantMicOnIcon.tsx index 52cfa208be2..71b528ae1f1 100644 --- a/src/script/components/calling/ParticipantMicOnIcon.tsx +++ b/src/script/components/calling/ParticipantMicOnIcon.tsx @@ -21,7 +21,7 @@ import React from 'react'; import {keyframes} from '@emotion/react'; -import {getSVG} from '../../auth/util/SVGProvider'; +import {getSVG} from 'Util/SVGProvider'; const fadeAnimation = keyframes` 0% { opacity: 0.2; } diff --git a/src/script/components/panel/PanelActions/PanelActions.tsx b/src/script/components/panel/PanelActions/PanelActions.tsx index 002d6e2f43e..2ca00dafe20 100644 --- a/src/script/components/panel/PanelActions/PanelActions.tsx +++ b/src/script/components/panel/PanelActions/PanelActions.tsx @@ -19,13 +19,15 @@ import React from 'react'; +import {SVGIconName} from 'Util/SVGProvider'; + import {listCSS} from './PanelActions.styles'; import {Icon} from '../../Icon'; export interface MenuItem { click: () => void; - icon: string; + icon: SVGIconName; identifier: string; label: string; } diff --git a/src/script/page/RightSidebar/ConversationDetails/utils/getConversationActions.ts b/src/script/page/RightSidebar/ConversationDetails/utils/getConversationActions.ts index 1ba4607dc13..1fe935977c9 100644 --- a/src/script/page/RightSidebar/ConversationDetails/utils/getConversationActions.ts +++ b/src/script/page/RightSidebar/ConversationDetails/utils/getConversationActions.ts @@ -50,7 +50,7 @@ const getConversationActions = ( const getNextConversation = () => conversationRepository.getNextConversation(conversationEntity); const userPermissions = UserPermission.generatePermissionHelpers(teamRole); - const allMenuElements = [ + const allMenuElements: {item: MenuItem; condition: boolean}[] = [ { condition: userPermissions.canCreateGroupConversation() && is1to1Action && !isServiceMode, item: { diff --git a/src/script/ui/ContextMenu.tsx b/src/script/ui/ContextMenu.tsx index de449db3d5f..471211a8c07 100644 --- a/src/script/ui/ContextMenu.tsx +++ b/src/script/ui/ContextMenu.tsx @@ -28,11 +28,12 @@ import {Icon} from 'Components/Icon'; import {IgnoreOutsideClickWrapper} from 'Components/InputBar/util/clickHandlers'; import {useMessageActionsState} from 'Components/MessagesList/Message/ContentMessage/MessageActions/MessageActions.state'; import {isEnterKey, isEscapeKey, isKey, isOneOfKeys, isSpaceKey, KEY} from 'Util/KeyboardUtil'; +import {SVGIconName} from 'Util/SVGProvider'; export interface ContextMenuEntry { availability?: Availability.Type; click?: (event?: MouseEvent) => void; - icon?: string; + icon?: SVGIconName; identifier?: string; isChecked?: boolean; isDisabled?: boolean; diff --git a/src/script/util/ArrayUtil.ts b/src/script/util/ArrayUtil.ts index f8972a0d5af..72f9e6bc337 100644 --- a/src/script/util/ArrayUtil.ts +++ b/src/script/util/ArrayUtil.ts @@ -137,3 +137,11 @@ export const partition = (array: T[], condition: (element: T) => boolean): [T }); return [matching, notMatching]; }; + +type Entries = { + [K in keyof T]: [K, T[K]]; +}[keyof T][]; + +export const typedEntries = (obj: T): Entries => { + return Object.entries(obj) as Entries; +}; diff --git a/src/script/util/SVGProvider/SVGProvider.ts b/src/script/util/SVGProvider/SVGProvider.ts new file mode 100644 index 00000000000..26a2f4fc7b9 --- /dev/null +++ b/src/script/util/SVGProvider/SVGProvider.ts @@ -0,0 +1,43 @@ +/* + * Wire + * Copyright (C) 2020 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + * + */ + +import {iconFileNames} from '../../generated/iconFileNames'; + +type ElementType> = T extends ReadonlyArray ? ElementType : never; +type SVGFileNameWithExtension = ElementType; + +export type SVGIconName = SVGFileNameWithExtension extends `${infer Name}.svg` ? Name : never; +export type SVGProvider = Record; + +const parser = new DOMParser(); + +const createSVGs = (fileNames: typeof iconFileNames): SVGProvider => { + return fileNames.reduce((acc, iconFileName) => { + const iconName = iconFileName.substring(iconFileName.lastIndexOf('/') + 1).replace(/\.svg$/i, ''); + const svgString = require(`Resource/image/icon/${iconFileName}`); + return {...acc, [iconName]: parser.parseFromString(svgString, 'image/svg+xml')}; + }, {} as SVGProvider); +}; + +const svgs = createSVGs(iconFileNames); + +const getAllSVGs = () => svgs; +const getSVG = (iconName: SVGIconName) => svgs[iconName]; + +export {getAllSVGs, getSVG}; diff --git a/src/script/auth/util/__mocks__/SVGProvider.ts b/src/script/util/SVGProvider/index.ts similarity index 76% rename from src/script/auth/util/__mocks__/SVGProvider.ts rename to src/script/util/SVGProvider/index.ts index 663ad1c5e62..b92866d18c2 100644 --- a/src/script/auth/util/__mocks__/SVGProvider.ts +++ b/src/script/util/SVGProvider/index.ts @@ -1,6 +1,6 @@ /* * Wire - * Copyright (C) 2020 Wire Swiss GmbH + * Copyright (C) 2024 Wire Swiss GmbH * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -17,9 +17,4 @@ * */ -const svgs: {[index: string]: Document} = {}; - -const getAllSVGs = () => svgs; -const getSVG = (iconName: string) => svgs[iconName]; - -export {getAllSVGs, getSVG}; +export * from './SVGProvider'; diff --git a/src/script/util/TypeUtil.ts b/src/script/util/TypeUtil.ts new file mode 100644 index 00000000000..a9132285eda --- /dev/null +++ b/src/script/util/TypeUtil.ts @@ -0,0 +1,24 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + * + */ + +export type RemoveSuffix = S extends `${infer P}${Suffix}` ? P : S; + +export type PascalCase = S extends `${infer F}-${infer R}` + ? `${Capitalize}${PascalCase>}` + : Capitalize; diff --git a/src/script/util/test/mock/SVGProviderMock.ts b/src/script/util/test/mock/SVGProviderMock.ts index b58ee2c3c81..ff900fedc64 100644 --- a/src/script/util/test/mock/SVGProviderMock.ts +++ b/src/script/util/test/mock/SVGProviderMock.ts @@ -20,7 +20,7 @@ import fs from 'fs'; import path from 'path'; -import {SVGProvider} from 'src/script/auth/util/SVGProvider'; +import {SVGIconName, SVGProvider} from 'Util/SVGProvider'; const parser = new DOMParser(); const mockSVG = parser.parseFromString( @@ -28,15 +28,15 @@ const mockSVG = parser.parseFromString( 'image/svg+xml', ); -const mockFileList: SVGProvider = fs +const mockFileList = fs .readdirSync(path.resolve(__dirname, '../../../../../resource/image/icon')) .filter(file => file.endsWith('.svg')) .reduce((list, file: string) => { const iconName = file.substring(file.lastIndexOf('/') + 1).replace(/\.svg$/i, ''); return Object.assign(list, {[iconName]: mockSVG}); - }, {}); + }, {} as SVGProvider); -jest.mock('../../../auth/util/SVGProvider', () => ({ +jest.mock('Util/SVGProvider', () => ({ getAllSVGs: jest.fn().mockImplementation(() => mockFileList), - getSVG: jest.fn().mockImplementation((iconName: string) => mockFileList[iconName]), + getSVG: jest.fn().mockImplementation((iconName: SVGIconName) => mockFileList[iconName]), }));