diff --git a/client/packages/design/components/Icon/index.tsx b/client/packages/design/components/Icon/index.tsx index 3c60e904..a056ff04 100644 --- a/client/packages/design/components/Icon/index.tsx +++ b/client/packages/design/components/Icon/index.tsx @@ -1,9 +1,14 @@ import React, { useState } from 'react'; -import { Icon as Iconify, IconProps } from '@iconify/react'; +import { + Icon as Iconify, + IconProps, + addIcon, + addCollection, +} from '@iconify/react'; const placeHolderStyle = { width: '1em', height: '1em' }; -export const Icon: React.FC> = React.memo((props) => { +const InternalIcon: React.FC> = React.memo((props) => { const [loaded, setLoaded] = useState(false); return ( @@ -13,4 +18,14 @@ export const Icon: React.FC> = React.memo((props) => { ); }); -Icon.displayName = 'Icon'; +InternalIcon.displayName = 'Icon'; + +type CompoundedComponent = React.FC> & { + addIcon: typeof addIcon; + addCollection: typeof addCollection; +}; + +export const Icon = InternalIcon as CompoundedComponent; + +Icon.addIcon = addIcon; +Icon.addCollection = addCollection; diff --git a/client/web/plugins/com.msgbyte.offline-icons/manifest.json b/client/web/plugins/com.msgbyte.offline-icons/manifest.json new file mode 100644 index 00000000..20f25bb3 --- /dev/null +++ b/client/web/plugins/com.msgbyte.offline-icons/manifest.json @@ -0,0 +1,11 @@ +{ + "label": "Offline Icons", + "label.zh-CN": "离线图标", + "name": "com.msgbyte.offline-icons", + "url": "/plugins/com.msgbyte.offline-icons/index.js", + "version": "0.0.0", + "author": "moonrailgun", + "description": "Add prefetched icons which need run in intranet environment", + "description.zh-CN": "增加预获取的图标,适用于内网环境", + "requireRestart": true +} diff --git a/client/web/plugins/com.msgbyte.offline-icons/package.json b/client/web/plugins/com.msgbyte.offline-icons/package.json new file mode 100644 index 00000000..c24e23b3 --- /dev/null +++ b/client/web/plugins/com.msgbyte.offline-icons/package.json @@ -0,0 +1,23 @@ +{ + "name": "@plugins/com.msgbyte.offline-icons", + "main": "src/index.tsx", + "version": "0.0.0", + "description": "Add prefetched icons which need run in offline environment", + "private": true, + "scripts": { + "extract": "ts-node scripts/extract-icons.ts", + "sync:declaration": "tailchat declaration github" + }, + "devDependencies": { + "@babel/parser": "^7.20.5", + "@babel/traverse": "^7.20.5", + "@babel/types": "^7.20.5", + "@types/babel__traverse": "^7.18.3", + "@types/styled-components": "^5.1.26", + "globby": "11.1.0", + "react": "18.2.0", + "styled-components": "^5.3.6", + "ts-node": "10.9.1", + "typescript": "4.9.4" + } +} diff --git a/client/web/plugins/com.msgbyte.offline-icons/scripts/extract-icons.ts b/client/web/plugins/com.msgbyte.offline-icons/scripts/extract-icons.ts new file mode 100644 index 00000000..da4310ca --- /dev/null +++ b/client/web/plugins/com.msgbyte.offline-icons/scripts/extract-icons.ts @@ -0,0 +1,160 @@ +import { parse } from '@babel/parser'; +import traverse from '@babel/traverse'; +import { + isArrayExpression, + isExpression, + isIdentifier, + isJSXAttribute, + isJSXIdentifier, + isObjectExpression, + isObjectProperty, + isStringLiteral, +} from '@babel/types'; +import globby from 'globby'; +import fs from 'fs-extra'; +import path from 'path'; +import _ from 'lodash'; +import axios from 'axios'; + +const PROJECT_ROOT = path.resolve(__dirname, '../../../../../'); + +(async () => { + const start = Date.now(); + const paths = await globby(['**.tsx'], { + cwd: PROJECT_ROOT, + }); + + console.log(`extract icons from ${paths.length} files...`); + + const res = await Promise.all( + paths.map((p) => extractIcons(path.resolve(PROJECT_ROOT, p))) + ); + + const icons = _.uniq(_.flatten(res)); + + console.log(`extract ${icons.length} icons, usage: ${Date.now() - start}ms`); + + const group = _.mapValues( + _.groupBy(icons, (icon) => icon.split(':')[0]), + (value) => { + return value.map((item) => item.split(':')[1]); + } + ); + + console.log(`fetching remote svg....`); + + const svgs = await Promise.all( + _.map(group, (icons, prefix) => { + return fetchSvgs(prefix, icons); + }) + ); + + const target = path.resolve(__dirname, '../src/icons.json'); + await fs.writeJSON(target, svgs, { spaces: 2 }); + + console.log('DONE! Assets has been write into:', target); +})(); + +async function extractIcons(filepath: string): Promise { + const code = await fs.readFile(filepath, 'utf-8'); + + const ast = parse(code, { + sourceType: 'module', + plugins: ['jsx', 'typescript'], + }); + + const icons = []; + + traverse(ast, { + JSXOpeningElement(path) { + const name = path.node.name; + if (isJSXIdentifier(name) && name.name === 'Icon') { + path.node.attributes.forEach((attribute) => { + if (isJSXAttribute(attribute) && attribute.name.name === 'icon') { + if (isStringLiteral(attribute.value)) { + icons.push(attribute.value.value); + } + } + }); + } + }, + CallExpression(path) { + if (!isIdentifier(path.node.callee)) { + return; + } + + if ( + ['regCustomPanel', 'regPluginPanelAction'].includes( + path.node.callee.name + ) + ) { + path.node.arguments.forEach((argument) => { + if (isObjectExpression(argument)) { + argument.properties.forEach((property) => { + if ( + isObjectProperty(property) && + isIdentifier(property.key) && + property.key.name === 'icon' && + isStringLiteral(property.value) && + property.value.value // icon maybe empty string in some type + ) { + icons.push(property.value.value); + } + }); + } + }); + } + + if (path.node.callee.name === 'regGroupPanel') { + path.node.arguments.forEach((argument) => { + if (isObjectExpression(argument)) { + argument.properties.forEach((property) => { + if ( + isObjectProperty(property) && + isIdentifier(property.key) && + property.key.name === 'menus' && + isArrayExpression(property.value) + ) { + property.value.elements.forEach((element) => { + if (isObjectExpression(element)) { + element.properties.forEach((property) => { + if ( + isObjectProperty(property) && + isIdentifier(property.key) && + property.key.name === 'icon' && + isStringLiteral(property.value) && + property.value.value // icon maybe empty string in some type + ) { + icons.push(property.value.value); + } + }); + } + }); + } + }); + } + }); + } + }, + }); + + return icons; +} + +async function fetchSvgs( + prefix: string, + icons: string[] +): Promise<{ + aliases: any; + height: number; + width: number; + icons: Record; + lastModified: number; + prefix: string; +}> { + const { data } = await axios.get(`/${prefix}.json?icons=${icons.join(',')}`, { + baseURL: 'https://api.simplesvg.com/', // https://iconify.design/docs/api/#public-api + }); + + return data; +} diff --git a/client/web/plugins/com.msgbyte.offline-icons/src/icons.json b/client/web/plugins/com.msgbyte.offline-icons/src/icons.json new file mode 100644 index 00000000..790832a5 --- /dev/null +++ b/client/web/plugins/com.msgbyte.offline-icons/src/icons.json @@ -0,0 +1,173 @@ +[ + { + "prefix": "mdi", + "lastModified": 1684129624, + "aliases": {}, + "width": 24, + "height": 24, + "icons": { + "account": { + "body": "" + }, + "camera-outline": { + "body": "" + }, + "chevron-right": { + "body": "" + }, + "code-braces": { + "body": "" + }, + "close": { + "body": "" + }, + "chevron-down": { + "body": "" + }, + "loading": { + "body": "" + }, + "alert-circle-outline": { + "body": "" + }, + "content-cut": { + "body": "" + }, + "disc-player": { + "body": "" + }, + "radio-tower": { + "body": "" + }, + "web": { + "body": "" + }, + "paperclip": { + "body": "" + }, + "close-circle-outline": { + "body": "" + }, + "arrow-left": { + "body": "" + }, + "github": { + "body": "" + }, + "arrow-right": { + "body": "" + }, + "pound": { + "body": "" + }, + "chevron-double-down": { + "body": "" + }, + "content-copy": { + "body": "" + }, + "reply": { + "body": "" + }, + "restore": { + "body": "" + }, + "delete-outline": { + "body": "" + }, + "file-question-outline": { + "body": "" + }, + "plus-circle-outline": { + "body": "" + }, + "cloud-upload": { + "body": "" + }, + "send-circle-outline": { + "body": "" + }, + "email-edit-outline": { + "body": "" + }, + "magnify": { + "body": "" + }, + "message-badge-outline": { + "body": "" + }, + "plus": { + "body": "" + }, + "inbox-arrow-down": { + "body": "" + }, + "download": { + "body": "" + }, + "dots-horizontal": { + "body": "" + }, + "dock-window": { + "body": "" + }, + "pin-off": { + "body": "" + }, + "pin": { + "body": "" + }, + "bell-off-outline": { + "body": "" + }, + "account-multiple": { + "body": "" + }, + "puzzle": { + "body": "" + }, + "video-box": { + "body": "" + }, + "compass": { + "body": "" + }, + "checkbox-marked-outline": { + "body": "" + }, + "projector-screen-outline": { + "body": "" + }, + "open-in-new": { + "body": "" + } + } + }, + { + "prefix": "openmoji", + "lastModified": 1686116966, + "aliases": {}, + "width": 72, + "height": 72, + "icons": { + "frog": { + "body": "" + } + } + }, + { + "prefix": "emojione", + "lastModified": 1672651879, + "aliases": {}, + "width": 64, + "height": 64, + "icons": { + "white-heavy-check-mark": { + "body": "" + }, + "cross-mark-button": { + "body": "" + } + } + } +] diff --git a/client/web/plugins/com.msgbyte.offline-icons/src/index.tsx b/client/web/plugins/com.msgbyte.offline-icons/src/index.tsx new file mode 100644 index 00000000..5ae045e1 --- /dev/null +++ b/client/web/plugins/com.msgbyte.offline-icons/src/index.tsx @@ -0,0 +1,20 @@ +import { Icon } from '@capital/component'; +import icons from './icons.json'; + +const PLUGIN_ID = 'com.msgbyte.offline-icons'; +const PLUGIN_NAME = 'Offline Icons'; + +console.log(`Plugin ${PLUGIN_NAME}(${PLUGIN_ID}) is loaded`); + +// Icon.addIcon + +icons.forEach((collection) => { + if (!Icon.addCollection) { + console.warn( + 'Cannot call addCollection because of Icon.addCollection has not exposed!' + ); + return; + } + + Icon.addCollection(collection); +}); diff --git a/client/web/plugins/com.msgbyte.offline-icons/src/translate.ts b/client/web/plugins/com.msgbyte.offline-icons/src/translate.ts new file mode 100644 index 00000000..e6830708 --- /dev/null +++ b/client/web/plugins/com.msgbyte.offline-icons/src/translate.ts @@ -0,0 +1,8 @@ +import { localTrans } from '@capital/common'; + +export const Translate = { + name: localTrans({ + 'zh-CN': 'Offline Icons', + 'en-US': 'Offline Icons', + }), +}; diff --git a/client/web/plugins/com.msgbyte.offline-icons/tsconfig.json b/client/web/plugins/com.msgbyte.offline-icons/tsconfig.json new file mode 100644 index 00000000..89ae244b --- /dev/null +++ b/client/web/plugins/com.msgbyte.offline-icons/tsconfig.json @@ -0,0 +1,8 @@ +{ + "compilerOptions": { + "esModuleInterop": true, + "jsx": "react", + "resolveJsonModule": true, + "importsNotUsedAsValues": "error" + } +} diff --git a/client/web/plugins/com.msgbyte.offline-icons/types/tailchat.d.ts b/client/web/plugins/com.msgbyte.offline-icons/types/tailchat.d.ts new file mode 100644 index 00000000..49f524ae --- /dev/null +++ b/client/web/plugins/com.msgbyte.offline-icons/types/tailchat.d.ts @@ -0,0 +1,2 @@ +declare module '@capital/common'; +declare module '@capital/component'; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f522f299..971295b9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -575,7 +575,7 @@ importers: version: 0.32.11 zustand: specifier: ^4.3.6 - version: 4.3.6(immer@9.0.15)(react@18.2.0) + version: 4.3.6(immer@9.0.21)(react@18.2.0) devDependencies: '@types/crc': specifier: ^3.4.0 @@ -1169,6 +1169,42 @@ importers: specifier: ^0.6.3 version: 0.6.3 + client/web/plugins/com.msgbyte.offline-icons: + devDependencies: + '@babel/parser': + specifier: ^7.20.5 + version: 7.20.5 + '@babel/traverse': + specifier: ^7.20.5 + version: 7.20.5 + '@babel/types': + specifier: ^7.20.5 + version: 7.20.5 + '@iconify/utils': + specifier: ^2.1.7 + version: 2.1.7 + '@types/babel__traverse': + specifier: ^7.18.3 + version: 7.18.3 + '@types/styled-components': + specifier: ^5.1.26 + version: 5.1.26 + globby: + specifier: 11.1.0 + version: 11.1.0 + react: + specifier: 18.2.0 + version: 18.2.0 + styled-components: + specifier: ^5.3.6 + version: 5.3.6(react-dom@18.2.0)(react-is@18.2.0)(react@18.2.0) + ts-node: + specifier: 10.9.1 + version: 10.9.1(@types/node@18.11.9)(typescript@4.9.4) + typescript: + specifier: 4.9.4 + version: 4.9.4 + client/web/plugins/com.msgbyte.openapi: {} client/web/plugins/com.msgbyte.posthog: @@ -1886,7 +1922,7 @@ importers: version: 5.3.6(react-dom@18.2.0)(react-is@18.2.0)(react@18.2.0) zustand: specifier: ^4.3.6 - version: 4.3.6(immer@9.0.15)(react@18.2.0) + version: 4.3.6(immer@9.0.21)(react@18.2.0) server/plugins/com.msgbyte.discover: dependencies: @@ -2117,7 +2153,7 @@ importers: version: 5.3.6(react-dom@18.2.0)(react-is@18.2.0)(react@18.2.0) zustand: specifier: ^4.3.6 - version: 4.3.6(immer@9.0.15)(react@18.2.0) + version: 4.3.6(immer@9.0.21)(react@18.2.0) server/plugins/com.msgbyte.welcome: dependencies: @@ -2465,6 +2501,17 @@ packages: resize-observer-polyfill: 1.5.1 dev: false + /@antfu/install-pkg@0.1.1: + resolution: {integrity: sha512-LyB/8+bSfa0DFGC06zpCEfs89/XoWZwws5ygEa5D+Xsm3OfI+aXQ86VgVG7Acyef+rSZ5HE7J8rrxzrQeM3PjQ==} + dependencies: + execa: 5.1.1 + find-up: 5.0.0 + dev: true + + /@antfu/utils@0.7.5: + resolution: {integrity: sha512-dlR6LdS+0SzOAPx/TPRhnoi7hE251OVeT2Snw0RguNbBSbjUHdWr0l3vcUUDg26rEysT89kCbtw1lVorBXLLCg==} + dev: true + /@apideck/better-ajv-errors@0.3.6(ajv@8.12.0): resolution: {integrity: sha512-P+ZygBLZtkp0qqOAJJVX4oX/sFo5JR3eBWwwuqHHhK0GIgQOKWrAfiAaWX0aArHkRWHMuggFEgAZNxVPwPZYaA==} engines: {node: '>=10'} @@ -2830,14 +2877,6 @@ packages: dependencies: '@babel/types': 7.21.2 - /@babel/helper-function-name@7.19.0: - resolution: {integrity: sha512-WAwHBINyrpqywkUH0nTnNgI5ina5TFn85HKS0pbPDfxFfhyR/aNQEn4hGi1P1JyT//I0t4OgXUlofzWILRvS5w==} - engines: {node: '>=6.9.0'} - dependencies: - '@babel/template': 7.20.7 - '@babel/types': 7.21.2 - dev: false - /@babel/helper-function-name@7.21.0: resolution: {integrity: sha512-HfK1aMRanKHpxemaY2gqBmL04iAPOPRj7DxtNbiDOrJK+gdwkiNRVpCpUJYbUT+aZyemKN8brqTOxzCaG6ExRg==} engines: {node: '>=6.9.0'} @@ -3026,8 +3065,7 @@ packages: engines: {node: '>=6.0.0'} hasBin: true dependencies: - '@babel/types': 7.20.5 - dev: false + '@babel/types': 7.21.2 /@babel/parser@7.20.7: resolution: {integrity: sha512-T3Z9oHybU+0vZlY9CiDSJQTD5ZapcW18ZctFMi0MOAl/4BjFF4ul7NVSARLdbGO5vDqy9eQiGTV0LtKfvCYvcg==} @@ -5046,18 +5084,17 @@ packages: engines: {node: '>=6.9.0'} dependencies: '@babel/code-frame': 7.18.6 - '@babel/generator': 7.20.5 + '@babel/generator': 7.21.1 '@babel/helper-environment-visitor': 7.18.9 - '@babel/helper-function-name': 7.19.0 + '@babel/helper-function-name': 7.21.0 '@babel/helper-hoist-variables': 7.18.6 '@babel/helper-split-export-declaration': 7.18.6 - '@babel/parser': 7.20.5 - '@babel/types': 7.20.5 + '@babel/parser': 7.21.2 + '@babel/types': 7.21.2 debug: 4.3.4(supports-color@9.2.2) globals: 11.12.0 transitivePeerDependencies: - supports-color - dev: false /@babel/traverse@7.21.2(supports-color@5.5.0): resolution: {integrity: sha512-ts5FFU/dSUPS13tv8XiEObDu9K+iagEKME9kAbaP7r0Y9KtZJZ+NGndDvWoRAYNpeWafbpFeki3q9QoMD6gxyw==} @@ -7146,6 +7183,23 @@ packages: react: 18.2.0 dev: false + /@iconify/types@2.0.0: + resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==} + dev: true + + /@iconify/utils@2.1.7: + resolution: {integrity: sha512-P8S3z/L1LcV4Qem9AoCfVAaTFGySEMzFEY4CHZLkfRj0Fv9LiR+AwjDgrDrzyI93U2L2mg9JHsbTJ52mF8suNw==} + dependencies: + '@antfu/install-pkg': 0.1.1 + '@antfu/utils': 0.7.5 + '@iconify/types': 2.0.0 + debug: 4.3.4(supports-color@9.2.2) + kolorist: 1.8.0 + local-pkg: 0.4.3 + transitivePeerDependencies: + - supports-color + dev: true + /@icons/material@0.2.4(react@18.2.0): resolution: {integrity: sha512-QPcGmICAPbGLGb6F/yNf/KzKqvFx8z5qx3D1yFqVAjoFmXK35EgyW+cJ57Te3CNsmzblwtzakLGFqHPqrfb4Tw==} peerDependencies: @@ -21600,10 +21654,10 @@ packages: /immer@9.0.15: resolution: {integrity: sha512-2eB/sswms9AEUSkOm4SbV5Y7Vmt/bKRwByd52jfLkW4OLYeaTP3EEiJ9agqU0O/tq6Dk62Zfj+TJSqfm1rLVGQ==} + dev: false /immer@9.0.21: resolution: {integrity: sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==} - dev: false /import-fresh@2.0.0: resolution: {integrity: sha512-eZ5H8rcgYazHbKC3PG4ClHNykCSxtAhxSSEM+2mb+7evD2CKF5V7c0dNum7AdpDh0ZdICwZY9sRSn8f+KH96sg==} @@ -23715,6 +23769,10 @@ packages: - supports-color dev: false + /kolorist@1.8.0: + resolution: {integrity: sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==} + dev: true + /latest-version@5.1.0: resolution: {integrity: sha512-weT+r0kTkRQdCdYCNtkMwWXQTMEswKrFBkm4ckQOMVhhqhIMI1UT2hMj+1iigIhgSZm5gTmrRXBNoGUgaTY1xA==} engines: {node: '>=8'} @@ -23982,6 +24040,11 @@ packages: engines: {node: '>= 12.13.0'} dev: false + /local-pkg@0.4.3: + resolution: {integrity: sha512-SFppqq5p42fe2qcZQqqEOiVRXl+WCP1MdT6k7BDEW1j++sp5fIY+/fdRQitvKgB5BrBcmrs5m/L0v2FrU5MY1g==} + engines: {node: '>=14'} + dev: true + /localforage@1.10.0: resolution: {integrity: sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg==} dependencies: @@ -36110,6 +36173,7 @@ packages: immer: 9.0.15 react: 18.2.0 use-sync-external-store: 1.2.0(react@18.2.0) + dev: false /zustand@4.3.6(immer@9.0.21)(react@18.2.0): resolution: {integrity: sha512-6J5zDxjxLE+yukC2XZWf/IyWVKnXT9b9HUv09VJ/bwGCpKNcaTqp7Ws28Xr8jnbvnZcdRaidztAPsXFBIqufiw==} @@ -36126,7 +36190,6 @@ packages: immer: 9.0.21 react: 18.2.0 use-sync-external-store: 1.2.0(react@18.2.0) - dev: false /zwitch@1.0.5: resolution: {integrity: sha512-V50KMwwzqJV0NpZIZFwfOD5/lyny3WlSzRiXgA0G7VUnRlqttta1L6UQIHzd6EuBY/cHGfwTIck7w1yH6Q5zUw==}