From a3eb7914935152526ffc039b2f0a82fca491a4f9 Mon Sep 17 00:00:00 2001 From: Dmitry Yudakov <dmitry.yudakov@gmail.com> Date: Wed, 4 Oct 2023 17:52:48 +0300 Subject: [PATCH 01/19] feat(plugins): initial version of creator plugins --- .../pluginsRepository.api.endpoints.ts | 2 +- .../pages/CreatorWidget/CreatorWidget.tsx | 70 ++++++++-- .../pages/PluginHolder/PluginHolder.tsx | 88 +++++++++++++ .../CreatorWidget/pages/PluginHolder/index.ts | 1 + .../pages/CreatorWidget/pages/index.ts | 1 + .../stores/CreatorStore/CreatorStore.ts | 3 +- .../app/src/stores/PluginStore/PluginStore.ts | 124 ++++++++++++++++++ packages/app/src/stores/PluginStore/index.ts | 1 + packages/app/src/stores/RootStore.ts | 5 +- packages/plugin_video/package.json | 5 + packages/sdk/bin/momentum-plugin.js | 16 ++- packages/sdk/craco.config.js | 7 +- .../src/interfaces/usePluginHook.interface.ts | 9 +- 13 files changed, 314 insertions(+), 18 deletions(-) create mode 100644 packages/app/src/scenes/widgets/pages/CreatorWidget/pages/PluginHolder/PluginHolder.tsx create mode 100644 packages/app/src/scenes/widgets/pages/CreatorWidget/pages/PluginHolder/index.ts create mode 100644 packages/app/src/stores/PluginStore/PluginStore.ts create mode 100644 packages/app/src/stores/PluginStore/index.ts diff --git a/packages/app/src/api/repositories/pluginsRepository/pluginsRepository.api.endpoints.ts b/packages/app/src/api/repositories/pluginsRepository/pluginsRepository.api.endpoints.ts index d6c5fc287..078138157 100644 --- a/packages/app/src/api/repositories/pluginsRepository/pluginsRepository.api.endpoints.ts +++ b/packages/app/src/api/repositories/pluginsRepository/pluginsRepository.api.endpoints.ts @@ -1,5 +1,5 @@ export const pluginsRepositoryEndpoints = () => { - const BASE_URL = '/plugins'; + const BASE_URL = '/plugin'; return { list: `${BASE_URL}`, diff --git a/packages/app/src/scenes/widgets/pages/CreatorWidget/CreatorWidget.tsx b/packages/app/src/scenes/widgets/pages/CreatorWidget/CreatorWidget.tsx index a660b7347..bb1df39b1 100644 --- a/packages/app/src/scenes/widgets/pages/CreatorWidget/CreatorWidget.tsx +++ b/packages/app/src/scenes/widgets/pages/CreatorWidget/CreatorWidget.tsx @@ -1,6 +1,7 @@ import {observer} from 'mobx-react-lite'; -import {FC, useCallback, useEffect} from 'react'; +import {FC, useCallback, useEffect, useState} from 'react'; import {Dialog, IconNameType, Panel, SideMenu, SideMenuItemInterface} from '@momentum-xyz/ui-kit'; +import {UsePluginHookReturnInterface} from '@momentum-xyz/sdk'; import {i18n, useI18n} from '@momentum-xyz/core'; import {toast} from 'react-toastify'; @@ -10,6 +11,7 @@ import {FeatureFlagEnum} from 'api/enums'; import {CreatorTabsEnum} from 'core/enums'; import {isFeatureEnabled} from 'api/constants'; import {subMenuKeyWidgetEnumMap} from 'core/constants'; +import {PluginLoaderModelType} from 'core/models'; import * as styled from './CreatorWidget.styled'; import { @@ -20,10 +22,11 @@ import { SkyboxSelector, SpawnAsset, WorldEditor, - WorldMembers + WorldMembers, + PluginHolder } from './pages'; -type MenuItemType = keyof typeof CreatorTabsEnum; +type MenuItemType = keyof typeof CreatorTabsEnum; // | string; const SIZE_MENU_ITEMS: SideMenuItemInterface<MenuItemType>[] = [ { @@ -73,7 +76,7 @@ const ALL_MENU_ITEMS: SideMenuItemInterface<MenuItemType>[] = [ ]; const CreatorWidget: FC = () => { - const {universeStore, widgetStore, widgetManagerStore} = useStore(); + const {universeStore, widgetStore, widgetManagerStore, pluginStore} = useStore(); const {world2dStore, world3dStore, worldId} = universeStore; const {creatorStore} = widgetStore; @@ -90,6 +93,40 @@ const CreatorWidget: FC = () => { console.log('CreatorWidget render', {selectedTab}); + const [pluginsCreatorTabData, setPluginsCreatorTabData] = useState< + Record< + string, + { + pluginLoader: PluginLoaderModelType; + creatorTab: UsePluginHookReturnInterface['creatorTab']; + } + > + >({}); + + useEffect(() => { + pluginStore.preloadPluginsByScope('creatorTab'); + }, [pluginStore]); + + console.log('pluginsByScope', pluginStore.pluginsByScope('creatorTab')); + const pluginLoaders = pluginStore.pluginsByScope('creatorTab'); + const pluginHolders = pluginLoaders.map((pluginLoader, idx) => { + return ( + <PluginHolder + key={pluginLoader.id} + pluginLoader={pluginLoader} + onCreatorTabChanged={(data) => { + console.log('onCreatorTabChanged:', data); + if (data) { + setPluginsCreatorTabData((prev) => { + const next = {...prev, [pluginLoader.id]: {pluginLoader, creatorTab: data}}; + return next; + }); + } + }} + /> + ); + }); + useEffect(() => { world3dStore?.enableCreatorMode(); spawnAssetStore.init(worldId); // TEMP @@ -104,10 +141,18 @@ const CreatorWidget: FC = () => { const panel = ALL_MENU_ITEMS.find((panel) => panel.id === selectedTab); const menuItem = SIZE_MENU_ITEMS.find((item) => item.id === selectedTab); - const filteredSideMenuItems = !isFeatureEnabled(FeatureFlagEnum.CANVAS) + const enabledSideMenuItems = !isFeatureEnabled(FeatureFlagEnum.CANVAS) ? SIZE_MENU_ITEMS.filter((item) => item.id !== 'canvas') : SIZE_MENU_ITEMS; + const sideMenuItems = enabledSideMenuItems.concat( + Object.values(pluginsCreatorTabData).map(({pluginLoader, creatorTab}) => ({ + id: pluginLoader.id, + iconName: creatorTab?.icon || 'rabbit_fill', + label: creatorTab?.title || pluginLoader.name + })) as any + ); + const handleSubMenuActiveChange = useCallback( (tab: keyof typeof CreatorTabsEnum | null): void => { const currentTabIsOnSubMenu = selectedTab && subMenuKeyWidgetEnumMap[selectedTab]; @@ -160,6 +205,10 @@ const CreatorWidget: FC = () => { case 'editMembers': return <WorldMembers />; default: + if (selectedTab && pluginsCreatorTabData[selectedTab]) { + const {creatorTab} = pluginsCreatorTabData[selectedTab]; + return creatorTab?.content || null; + } } return null; })(); @@ -170,7 +219,7 @@ const CreatorWidget: FC = () => { <SideMenu orientation="left" activeId={menuItem?.id} - sideMenuItems={filteredSideMenuItems} + sideMenuItems={sideMenuItems as any} onSelect={handleTabChange} /> </div> @@ -185,8 +234,11 @@ const CreatorWidget: FC = () => { : 'large' } variant="primary" - title={panel?.label || ''} - icon={panel?.iconName as IconNameType} + title={panel?.label || pluginsCreatorTabData[selectedTab]?.creatorTab?.title || ''} + icon={ + (panel?.iconName || + pluginsCreatorTabData[selectedTab]?.creatorTab?.icon) as IconNameType + } onClose={() => handleTabChange()} > {content} @@ -225,6 +277,8 @@ const CreatorWidget: FC = () => { onClose={removeObjectDialog.close} /> )} + + {pluginHolders} </styled.Container> ); }; diff --git a/packages/app/src/scenes/widgets/pages/CreatorWidget/pages/PluginHolder/PluginHolder.tsx b/packages/app/src/scenes/widgets/pages/CreatorWidget/pages/PluginHolder/PluginHolder.tsx new file mode 100644 index 000000000..cfe740159 --- /dev/null +++ b/packages/app/src/scenes/widgets/pages/CreatorWidget/pages/PluginHolder/PluginHolder.tsx @@ -0,0 +1,88 @@ +import {FC, memo, useEffect, useMemo} from 'react'; +import {useTheme} from 'styled-components'; +import {ErrorBoundary, useMutableCallback} from '@momentum-xyz/ui-kit'; +import {useI18n} from '@momentum-xyz/core'; +import { + PluginInterface, + PluginPropsInterface, + ObjectGlobalPropsContextProvider, + UsePluginHookReturnInterface +} from '@momentum-xyz/sdk'; + +import {PluginLoaderModelType} from 'core/models'; + +interface PropsInterface { + pluginLoader: PluginLoaderModelType; + onCreatorTabChanged: (data: UsePluginHookReturnInterface['creatorTab']) => void; +} + +const PluginHolder: FC<PropsInterface> = ({pluginLoader, onCreatorTabChanged}) => { + useEffect(() => { + console.log('PluginHolder'); + return () => { + console.log('PluginHolder unmount'); + }; + }, []); + + const theme = useTheme(); + const {t} = useI18n(); + + const onCreatorTabChangedMut = useMutableCallback(onCreatorTabChanged); + + const pluginProps: PluginPropsInterface = useMemo( + () => ({ + // @ts-ignore: FIXME + theme, + isAdmin: true, + // objectId, + isExpanded: pluginLoader.isExpanded, + onToggleExpand: pluginLoader.toggleIsExpanded, + pluginApi: pluginLoader.attributesManager.pluginApi, + api: pluginLoader.attributesManager.api, + onClose: () => {} + }), + [pluginLoader, theme] + ); + + return ( + <ErrorBoundary errorMessage={t('errors.errorWhileLoadingPlugin')}> + <ObjectGlobalPropsContextProvider props={pluginProps}> + {pluginLoader.plugin ? ( + <PluginInnerWrapper + onCreatorTabChanged={onCreatorTabChangedMut} + pluginProps={pluginProps} + plugin={pluginLoader.plugin} + /> + ) : null} + + {pluginLoader.isError && <div>{t('errors.errorWhileLoadingPlugin')}</div>} + </ObjectGlobalPropsContextProvider> + </ErrorBoundary> + ); +}; + +const PluginInnerWrapper = memo( + ({ + onCreatorTabChanged, + pluginProps, + plugin + }: { + onCreatorTabChanged: (data: UsePluginHookReturnInterface['creatorTab']) => void; + pluginProps: PluginPropsInterface; + plugin: PluginInterface; + // hideWhenUnset: boolean; + }) => { + // const {t} = useI18n(); + + const {creatorTab} = plugin.usePlugin(pluginProps); + console.log('PluginInnerWrapper creatorTab', creatorTab); + + useEffect(() => { + onCreatorTabChanged(creatorTab); + }, [creatorTab, onCreatorTabChanged]); + + return null; + } +); + +export default PluginHolder; diff --git a/packages/app/src/scenes/widgets/pages/CreatorWidget/pages/PluginHolder/index.ts b/packages/app/src/scenes/widgets/pages/CreatorWidget/pages/PluginHolder/index.ts new file mode 100644 index 000000000..a9c0b7ce1 --- /dev/null +++ b/packages/app/src/scenes/widgets/pages/CreatorWidget/pages/PluginHolder/index.ts @@ -0,0 +1 @@ +export {default as PluginHolder} from './PluginHolder'; diff --git a/packages/app/src/scenes/widgets/pages/CreatorWidget/pages/index.ts b/packages/app/src/scenes/widgets/pages/CreatorWidget/pages/index.ts index 2f7236fd2..6cc8c5636 100644 --- a/packages/app/src/scenes/widgets/pages/CreatorWidget/pages/index.ts +++ b/packages/app/src/scenes/widgets/pages/CreatorWidget/pages/index.ts @@ -6,3 +6,4 @@ export * from './MusicManager'; export * from './SceneExplorer'; export * from './WorldEditor'; export * from './WorldMembers'; +export * from './PluginHolder'; diff --git a/packages/app/src/scenes/widgets/stores/CreatorStore/CreatorStore.ts b/packages/app/src/scenes/widgets/stores/CreatorStore/CreatorStore.ts index 36c839a06..86cff67dd 100644 --- a/packages/app/src/scenes/widgets/stores/CreatorStore/CreatorStore.ts +++ b/packages/app/src/scenes/widgets/stores/CreatorStore/CreatorStore.ts @@ -34,7 +34,8 @@ const CreatorStore = types objectFunctionalityStore: types.optional(ObjectFunctionalityStore, {}), objectColorStore: types.optional(ObjectColorStore, {}), - selectedTab: types.maybeNull(types.enumeration(Object.keys(CreatorTabsEnum))), + // selectedTab: types.maybeNull(types.enumeration(Object.keys(CreatorTabsEnum))), + selectedTab: types.maybeNull(types.string), selectedObjectId: types.maybeNull(types.string), objectName: types.maybeNull(types.string), objectInfo: types.maybeNull(types.frozen<GetObjectInfoResponse>()), diff --git a/packages/app/src/stores/PluginStore/PluginStore.ts b/packages/app/src/stores/PluginStore/PluginStore.ts new file mode 100644 index 000000000..b38b11cd0 --- /dev/null +++ b/packages/app/src/stores/PluginStore/PluginStore.ts @@ -0,0 +1,124 @@ +import {types, flow, cast} from 'mobx-state-tree'; +import {RequestModel} from '@momentum-xyz/core'; + +import {api} from 'api'; +import {DynamicScriptList, PluginAttributesManager, PluginLoader} from 'core/models'; + +interface PluginInfoInterface { + plugin_id: string; + meta: any; + options?: any; + created_at?: string; + updated_at?: string; +} + +const localPluginInfosByScopes = { + creatorTab: [ + { + plugin_id: '99c9a0ba-0c19-4ef5-a995-9bc3af39a0a5', + meta: { + name: 'plugin_odyssey_creator_openai', + scopeName: 'plugin_odyssey_creator_openai', + scriptUrl: 'http://localhost:3001/remoteEntry.js' + } + } + ] +}; + +export const PluginStore = types + .model('PluginStore', { + dynamicScriptList: types.optional(DynamicScriptList, {}), + pluginInfosByScopes: types.optional( + types.frozen<Record<string, PluginInfoInterface[]>>({}), + {} + ), + // pluginsLoadersByIds: types.optional(types.frozen<Record<string, typeof PluginLoader>>({})), {}), + pluginsLoadersByIds: types.map(PluginLoader), + pluginLoadersByScopes: types.map(types.array(types.reference(PluginLoader))), + + // pluginLoadersByScopes: types.optional( + // types.frozen<Record<string, typeof PluginLoader>>({}), + // {} + // ), + + pluginsRequest: types.optional(RequestModel, {}) + }) + .actions((self) => ({ + init: flow(function* () { + // init: function () { + const pluginsResponse = yield self.pluginsRequest.send( + api.pluginsRepository.getPluginsList, + {} + ); + console.log('pluginsResponse', pluginsResponse); + + // if (pluginsResponse) { + // self.pluginInfosByScopes = pluginsResponse.plugins; + self.pluginInfosByScopes = localPluginInfosByScopes; + // } + }), + storePluginLoadersByScope: (scope: string, pluginLoaders: any[]) => { + self.pluginLoadersByScopes.set(scope, cast(pluginLoaders)); + for (const plugin of pluginLoaders) { + self.pluginsLoadersByIds.set(plugin.pluginId, plugin); + } + } + })) + .actions((self) => ({ + preloadPluginsByScope: flow(function* (scope: string) { + console.log('getPluginsByScope', scope); + const pluginInfos = self.pluginInfosByScopes[scope] || []; + const plugins = yield Promise.all( + pluginInfos.map(async ({plugin_id, meta, options}) => { + console.log('get plugin ', {plugin_id, meta, options}); + const preloaded = self.pluginsLoadersByIds.get(plugin_id); + if (preloaded) { + return preloaded; + } + try { + console.log('create plugin ', {plugin_id, meta, options}); + + if (!self.dynamicScriptList.containsLoaderWithName(meta.scopeName)) { + await self.dynamicScriptList.addScript(meta.scopeName, meta.scriptUrl); + } + + const pluginLoader = PluginLoader.create({ + id: plugin_id, + pluginId: plugin_id, + ...options, + ...meta, + attributesManager: PluginAttributesManager.create({ + pluginId: plugin_id, + spaceId: '123' // TODO remove + }) + }); + + console.log('load plugin ', {plugin_id, meta, options}); + + await pluginLoader.loadPlugin(); + + console.log('loaded plugin ', {plugin_id, meta, options}); + + // self.pluginsLoadersByIds.set(plugin_id, pluginLoader); + + console.log('pluginLoader', pluginLoader); + + return pluginLoader; + } catch (e) { + console.error('Error loading plugin', plugin_id, e); + return null; + } + }) + ); + const loadedPlugins = plugins.filter((plugin: any) => !!plugin); + // self.pluginLoadersByScopes.set(scope, cast(loadedPlugins)); + self.storePluginLoadersByScope(scope, loadedPlugins); + + return loadedPlugins; + }) + })) + .views((self) => ({ + pluginsByScope(scope: string) { + return self.pluginLoadersByScopes.get(scope) || []; + } + })); diff --git a/packages/app/src/stores/PluginStore/index.ts b/packages/app/src/stores/PluginStore/index.ts new file mode 100644 index 000000000..f0094c7e8 --- /dev/null +++ b/packages/app/src/stores/PluginStore/index.ts @@ -0,0 +1 @@ +export * from './PluginStore'; diff --git a/packages/app/src/stores/RootStore.ts b/packages/app/src/stores/RootStore.ts index b64b65a51..93b8fe085 100644 --- a/packages/app/src/stores/RootStore.ts +++ b/packages/app/src/stores/RootStore.ts @@ -11,6 +11,7 @@ import {AgoraStore} from './AgoraStore'; import {SentryStore} from './SentryStore'; import {WidgetManagerStore} from './WidgetManagerStore'; import {MusicStore} from './MusicStore'; +import {PluginStore} from './PluginStore'; const RootStore = types .model('RootStore', { @@ -26,13 +27,15 @@ const RootStore = types musicStore: types.optional(MusicStore, {}), /* Connect independent stores */ - widgetStore: types.optional(WidgetsStore, {}) + widgetStore: types.optional(WidgetsStore, {}), + pluginStore: types.optional(PluginStore, {}) }) .actions((self) => ({ async initApplication() { await self.configStore.init(); self.agoraStore.userDevicesStore.init(); self.themeStore.init(); + self.pluginStore.init(); }, async refreshStakeRelatedData() { await self.nftStore.loadMyStakes(); diff --git a/packages/plugin_video/package.json b/packages/plugin_video/package.json index be058416b..771b7b0d8 100644 --- a/packages/plugin_video/package.json +++ b/packages/plugin_video/package.json @@ -6,6 +6,11 @@ "license": "GPL-3.0", "homepage": "https://odyssey.org", "author": "info@odyssey.org", + "provides": { + "ui": [ + "objectView" + ] + }, "attribute_types": [ { "name": "state", diff --git a/packages/sdk/bin/momentum-plugin.js b/packages/sdk/bin/momentum-plugin.js index 457e064fe..6dbbc8f18 100755 --- a/packages/sdk/bin/momentum-plugin.js +++ b/packages/sdk/bin/momentum-plugin.js @@ -79,8 +79,17 @@ function spawnProcess(command, args, env) { } function generateAndStoreMetadata() { - const {name, version, description, author, repository, homepage, license, attribute_types} = - packageJSON; + const { + name, + version, + description, + author, + repository, + homepage, + license, + attribute_types = [], + scopes = [] + } = packageJSON; const metadata = { name, version, @@ -89,7 +98,8 @@ function generateAndStoreMetadata() { repository, homepage, license, - attribute_types: attribute_types ?? [] + attribute_types, + scopes }; const filename = path.resolve(BUILD_DIR, 'metadata.json'); fs.writeFileSync(filename, JSON.stringify(metadata, null, 2)); diff --git a/packages/sdk/craco.config.js b/packages/sdk/craco.config.js index 1eb39f588..beeb642d2 100644 --- a/packages/sdk/craco.config.js +++ b/packages/sdk/craco.config.js @@ -33,7 +33,8 @@ module.exports = { paths.appPublic = publicPath; webpackConfig.plugins = webpackConfig.plugins.map((webpackPlugin) => { - if (webpackPlugin instanceof HtmlWebpackPlugin) { + // if (webpackPlugin instanceof HtmlWebpackPlugin) { + if (webpackPlugin.constructor.name === 'HtmlWebpackPlugin') { return new HtmlWebpackPlugin({ template: htmlPath, inject: true @@ -62,11 +63,11 @@ module.exports = { shared: { react: { singleton: true, - requiredVersion: '^16.14.0' + requiredVersion: '^18.2.0' }, 'react-dom': { singleton: true, - requiredVersion: '^16.14.0' + requiredVersion: '^18.2.0' }, mobx: { requiredVersion: '^6.4.2' diff --git a/packages/sdk/src/interfaces/usePluginHook.interface.ts b/packages/sdk/src/interfaces/usePluginHook.interface.ts index c3b95f97e..0447f0fe2 100644 --- a/packages/sdk/src/interfaces/usePluginHook.interface.ts +++ b/packages/sdk/src/interfaces/usePluginHook.interface.ts @@ -4,7 +4,7 @@ export interface EditObjectViewInterface { onSave: () => void; } -interface UsePluginHookReturnInterface { +export interface UsePluginHookReturnInterface { content?: JSX.Element | null; objectView?: { title?: string; @@ -20,6 +20,13 @@ interface UsePluginHookReturnInterface { discardChanges?: () => void; remove?: () => Promise<void>; }; + creatorTab?: { + title?: string; + icon?: string; + content: JSX.Element | null; + onOpen?: () => void; + onClose?: () => void; + }; } export type UsePluginHookType<C = unknown> = ( From 96c9c9e16b2f2c4d2b5a0898d8ecaf9268415296 Mon Sep 17 00:00:00 2001 From: Dmitry Yudakov <dmitry.yudakov@gmail.com> Date: Wed, 11 Oct 2023 14:07:55 +0300 Subject: [PATCH 02/19] refactor(plugins): remove obsolete api, leave only pluginApi --- .../PluginAttributesManager.ts | 114 +----------------- .../components/AssignVideo/AssignVideo.tsx | 1 - .../pages/PluginHolder/PluginHolder.tsx | 1 - .../components/PluginViewer/PluginViewer.tsx | 1 - .../src/contexts/ObjectGlobalPropsContext.tsx | 17 --- .../ObjectViewEmulator/ObjectViewEmulator.tsx | 112 +---------------- packages/sdk/src/hooks/index.ts | 1 - packages/sdk/src/hooks/useObjectAttributes.ts | 7 -- .../sdk/src/hooks/useSharedObjectState.ts | 35 ++---- packages/sdk/src/interfaces/api.interface.ts | 54 --------- packages/sdk/src/interfaces/index.ts | 1 - .../src/interfaces/pluginProps.interface.ts | 2 - 12 files changed, 11 insertions(+), 335 deletions(-) delete mode 100644 packages/sdk/src/hooks/useObjectAttributes.ts delete mode 100644 packages/sdk/src/interfaces/api.interface.ts diff --git a/packages/app/src/core/models/PluginAttributesManager/PluginAttributesManager.ts b/packages/app/src/core/models/PluginAttributesManager/PluginAttributesManager.ts index 7a7c9f3db..ec0d0646c 100644 --- a/packages/app/src/core/models/PluginAttributesManager/PluginAttributesManager.ts +++ b/packages/app/src/core/models/PluginAttributesManager/PluginAttributesManager.ts @@ -1,15 +1,9 @@ import {RequestModel} from '@momentum-xyz/core'; -import { - PluginApiInterface, - AttributeValueInterface, - ApiInterface, - AttributeNameEnum -} from '@momentum-xyz/sdk'; +import {PluginApiInterface, AttributeValueInterface, AttributeNameEnum} from '@momentum-xyz/sdk'; import {flow, Instance, types} from 'mobx-state-tree'; import {api, GetObjectAttributeResponse} from 'api'; import {appVariables} from 'api/constants'; -import {PosBusService} from 'shared/services'; import {usePosBusEvent} from 'shared/hooks'; const PluginAttributesManager = types @@ -259,112 +253,6 @@ const PluginAttributesManager = types ); } }; - }, - get api(): ApiInterface { - return { - getSpaceAttributeValue: async <T extends AttributeValueInterface>( - spaceId: string, - attributeName: string - ) => self.getSpaceAttributeValue<T>(spaceId, attributeName) as Promise<T>, - setSpaceAttributeValue: async <T extends AttributeValueInterface>( - spaceId: string, - attributeName: string, - value: T - ) => self.setSpaceAttributeValue<T>(spaceId, attributeName, value) as Promise<T>, - deleteSpaceAttribute: self.deleteSpaceAttribute, - - getSpaceAttributeItem: async <T>( - spaceId: string, - attributeName: string, - attributeItemName: string - ) => self.getSpaceAttributeItem<T>(spaceId, attributeName, attributeItemName) as Promise<T>, - setSpaceAttributeItem: async <T>( - spaceId: string, - attributeName: string, - attributeItemName: string, - value: T - ) => - self.setSpaceAttributeItem<T>( - spaceId, - attributeName, - attributeItemName, - value - ) as Promise<T>, - // TODO: Change bellow to this after PosBus supports attribute items - // deleteSpaceAttributeItem: async ( - // spaceId: string, - // attributeName: string, - // attributeItemName: string - // ) => self.deleteObjectAttributeItem(spaceId, attributeName, attributeItemName), - - // TODO: Change above to this after PosBus supports attribute items - deleteSpaceAttributeItem: async ( - spaceId: string, - attributeName: string, - attributeItemName: string - ) => self.deleteSpaceAttribute(spaceId, attributeName), - subscribeToTopic: (topic) => { - PosBusService.subscribe(topic); - }, - unsubscribeFromTopic: (topic) => { - PosBusService.unsubscribe(topic); - }, - - useAttributeChange: <T extends AttributeValueInterface>( - topic: string, - attributeName: string, - callback: (value: T) => void - ) => { - return usePosBusEvent( - 'space-attribute-changed', - (posBusTopic, posBusAttributeName, value) => { - if (posBusTopic === topic && posBusAttributeName === attributeName) { - callback(value as unknown as T); - } - } - ); - }, - useAttributeRemove(topic, attributeName, callback) { - return usePosBusEvent('space-attribute-removed', (posBusTopic, posBusAttributeName) => { - if (posBusTopic === topic && posBusAttributeName === attributeName) { - callback(); - } - }); - }, - useAttributeItemChange: <T>( - topic: string, - attributeName: string, - attributeItemName: string, - callback: (value: T) => void - ) => { - return usePosBusEvent( - 'space-attribute-item-changed', - (posBusTopic, posBusAttributeName, posBusAttributItemName, value) => { - if ( - posBusTopic === topic && - posBusAttributeName === attributeName && - posBusAttributItemName === attributeItemName - ) { - callback(value as T); - } - } - ); - }, - useAttributeItemRemove(topic, attributeName, attributeItemName, callback) { - return usePosBusEvent( - 'space-attribute-item-removed', - (posBusTopic, posBusAttributeName, posBusAttributItemName) => { - if ( - posBusTopic === topic && - posBusAttributeName === attributeName && - posBusAttributItemName === attributeItemName - ) { - callback(); - } - } - ); - } - }; } })); diff --git a/packages/app/src/scenes/widgets/pages/CreatorWidget/pages/ObjectInspector/components/AssignVideo/AssignVideo.tsx b/packages/app/src/scenes/widgets/pages/CreatorWidget/pages/ObjectInspector/components/AssignVideo/AssignVideo.tsx index 4df1bb85f..e89f84e68 100644 --- a/packages/app/src/scenes/widgets/pages/CreatorWidget/pages/ObjectInspector/components/AssignVideo/AssignVideo.tsx +++ b/packages/app/src/scenes/widgets/pages/CreatorWidget/pages/ObjectInspector/components/AssignVideo/AssignVideo.tsx @@ -46,7 +46,6 @@ export const useAssignVideo: UseAssignVideoHookType = ({objectId, plugin, plugin onToggleExpand: pluginLoader.toggleIsExpanded, objectId, pluginApi: pluginLoader.attributesManager.pluginApi, - api: pluginLoader.attributesManager.api, onClose: () => {} } : null; diff --git a/packages/app/src/scenes/widgets/pages/CreatorWidget/pages/PluginHolder/PluginHolder.tsx b/packages/app/src/scenes/widgets/pages/CreatorWidget/pages/PluginHolder/PluginHolder.tsx index cfe740159..970b96fce 100644 --- a/packages/app/src/scenes/widgets/pages/CreatorWidget/pages/PluginHolder/PluginHolder.tsx +++ b/packages/app/src/scenes/widgets/pages/CreatorWidget/pages/PluginHolder/PluginHolder.tsx @@ -38,7 +38,6 @@ const PluginHolder: FC<PropsInterface> = ({pluginLoader, onCreatorTabChanged}) = isExpanded: pluginLoader.isExpanded, onToggleExpand: pluginLoader.toggleIsExpanded, pluginApi: pluginLoader.attributesManager.pluginApi, - api: pluginLoader.attributesManager.api, onClose: () => {} }), [pluginLoader, theme] diff --git a/packages/app/src/scenes/widgets/pages/ObjectWidget/components/ObjectViewer/components/PluginViewer/PluginViewer.tsx b/packages/app/src/scenes/widgets/pages/ObjectWidget/components/ObjectViewer/components/PluginViewer/PluginViewer.tsx index 048358dc1..b896f68fc 100644 --- a/packages/app/src/scenes/widgets/pages/ObjectWidget/components/ObjectViewer/components/PluginViewer/PluginViewer.tsx +++ b/packages/app/src/scenes/widgets/pages/ObjectWidget/components/ObjectViewer/components/PluginViewer/PluginViewer.tsx @@ -64,7 +64,6 @@ const PluginViewer: FC<PropsInterface> = ({ isExpanded: pluginLoader.isExpanded, onToggleExpand: pluginLoader.toggleIsExpanded, pluginApi: pluginLoader.attributesManager.pluginApi, - api: pluginLoader.attributesManager.api, onClose: () => {} }; diff --git a/packages/sdk/src/contexts/ObjectGlobalPropsContext.tsx b/packages/sdk/src/contexts/ObjectGlobalPropsContext.tsx index e5cc6b1dd..1ae8007df 100644 --- a/packages/sdk/src/contexts/ObjectGlobalPropsContext.tsx +++ b/packages/sdk/src/contexts/ObjectGlobalPropsContext.tsx @@ -9,23 +9,6 @@ import {ThemeContextProvider} from './ThemeContext'; export const ObjectGlobalPropsContext = createContext<PluginPropsInterface>({ theme: DefaultThemeConfig, isAdmin: false, - api: { - getSpaceAttributeValue: () => Promise.reject(), - setSpaceAttributeValue: () => Promise.reject(), - deleteSpaceAttribute: () => Promise.reject(), - - getSpaceAttributeItem: () => Promise.reject(), - setSpaceAttributeItem: () => Promise.reject(), - deleteSpaceAttributeItem: () => Promise.reject(), - - subscribeToTopic: () => Promise.reject(), - unsubscribeFromTopic: () => Promise.reject(), - useAttributeChange: () => Promise.reject(), - useAttributeRemove: () => Promise.reject(), - - useAttributeItemChange: () => Promise.reject(), - useAttributeItemRemove: () => Promise.reject() - }, pluginApi: { getStateItem: () => Promise.reject(), setStateItem: () => Promise.reject(), diff --git a/packages/sdk/src/emulator/components/ObjectViewEmulator/ObjectViewEmulator.tsx b/packages/sdk/src/emulator/components/ObjectViewEmulator/ObjectViewEmulator.tsx index a1bd05934..6dd3520c1 100644 --- a/packages/sdk/src/emulator/components/ObjectViewEmulator/ObjectViewEmulator.tsx +++ b/packages/sdk/src/emulator/components/ObjectViewEmulator/ObjectViewEmulator.tsx @@ -4,7 +4,7 @@ import {Panel, ErrorBoundary, ThemeInterface} from '@momentum-xyz/ui-kit'; import {useAttributesEmulator} from '../../hooks'; import {useTheme} from '../../../contexts/ThemeContext'; -import {AttributeValueInterface, PluginPropsInterface, PluginInterface} from '../../../interfaces'; +import {PluginPropsInterface, PluginInterface} from '../../../interfaces'; import {ObjectGlobalPropsContextProvider} from '../../../contexts'; import * as styled from './ObjectViewEmulator.styled'; @@ -22,14 +22,8 @@ export const ObjectViewEmulator: FC<PropsInterface> = ({plugin, isAdmin, onClose const theme = useTheme(); const { spaceAttributes, - useAttributeChange, - useAttributeRemove, useAttributeItemChange, useAttributeItemRemove, - changedAttribute, - removedAttribute, - changedAttributeItem, - removedAttributeItem, subscribeToTopic, unsubscribeFromTopic } = useAttributesEmulator(); @@ -47,104 +41,6 @@ export const ObjectViewEmulator: FC<PropsInterface> = ({plugin, isAdmin, onClose theme: theme as ThemeInterface, isAdmin, objectId, - api: { - getSpaceAttributeValue: <T extends AttributeValueInterface>( - spaceId: string, - attributeName: string - ) => { - const attributeValue = spaceAttributes.current.find( - (attribute) => attribute.attributeName === attributeName - )?.attributeValue; - - if (!attributeValue) { - return Promise.reject(); - } - - return Promise.resolve(attributeValue as T); - }, - setSpaceAttributeValue: <T extends AttributeValueInterface>( - spaceId: string, - attributeName: string, - value: AttributeValueInterface - ) => { - const attribute = spaceAttributes.current.find( - (attribute) => attribute.attributeName === attributeName - ); - - if (!attribute) { - return Promise.reject(); - } - - attribute.attributeValue = value; - changedAttribute({attributeName, value}); - return Promise.resolve(attribute.attributeValue as T); - }, - deleteSpaceAttribute: (spaceId: string, attributeName) => { - const attributes = spaceAttributes.current.filter( - (attribute) => attribute.attributeName !== attributeName - ); - - spaceAttributes.current = attributes; - removedAttribute({attributeName}); - return Promise.resolve(null); - }, - - getSpaceAttributeItem: <T,>( - spaceId: string, - attributeName: string, - attributeItemName: string - ) => { - const attributeValue = spaceAttributes.current.find( - (attribute) => attribute.attributeName === attributeName - )?.attributeValue; - - if (!attributeValue) { - return Promise.reject(); - } - - return Promise.resolve(attributeValue[attributeItemName] as T); - }, - setSpaceAttributeItem: <T,>( - spaceId: string, - attributeName: string, - attributeItemName: string, - value: T - ) => { - const attributeValue = spaceAttributes.current.find( - (attribute) => attribute.attributeName === attributeName - )?.attributeValue; - - if (!attributeValue) { - return Promise.reject(); - } - - attributeValue[attributeItemName] = value; - changedAttributeItem({attributeName, attributeItemName, value}); - return Promise.resolve(attributeValue[attributeItemName] as T); - }, - deleteSpaceAttributeItem: (spaceId: string, attributeName, attributeItemName) => { - const attributeValue = spaceAttributes.current.find( - (attribute) => attribute.attributeName === attributeName - )?.attributeValue; - - if (!attributeValue) { - return Promise.reject(); - } - - delete attributeValue[attributeItemName]; - removedAttributeItem({attributeName, attributeItemName}); - return Promise.resolve(null); - }, - - subscribeToTopic, - unsubscribeFromTopic, - - useAttributeChange, - useAttributeRemove, - - useAttributeItemChange, - useAttributeItemRemove - }, pluginApi: { getStateItem: <T,>(key: string) => { const state = spaceAttributes.current.find( @@ -195,15 +91,9 @@ export const ObjectViewEmulator: FC<PropsInterface> = ({plugin, isAdmin, onClose objectId, subscribeToTopic, unsubscribeFromTopic, - useAttributeChange, - useAttributeRemove, useAttributeItemChange, useAttributeItemRemove, spaceAttributes, - changedAttribute, - removedAttribute, - changedAttributeItem, - removedAttributeItem, config, onClose, isAdmin diff --git a/packages/sdk/src/hooks/index.ts b/packages/sdk/src/hooks/index.ts index 1cfb8c91d..72fd360ab 100644 --- a/packages/sdk/src/hooks/index.ts +++ b/packages/sdk/src/hooks/index.ts @@ -1,4 +1,3 @@ export * from './useObject'; -export * from './useObjectAttributes'; export * from './useSharedObjectState'; export * from './useConfigEmulatorStorage'; diff --git a/packages/sdk/src/hooks/useObjectAttributes.ts b/packages/sdk/src/hooks/useObjectAttributes.ts deleted file mode 100644 index f89ea84c0..000000000 --- a/packages/sdk/src/hooks/useObjectAttributes.ts +++ /dev/null @@ -1,7 +0,0 @@ -import {useObjectGlobalProps} from '../contexts/ObjectGlobalPropsContext'; - -export const useSpaceAttributesApi = () => { - const {api} = useObjectGlobalProps(); - - return api; -}; diff --git a/packages/sdk/src/hooks/useSharedObjectState.ts b/packages/sdk/src/hooks/useSharedObjectState.ts index c008b366f..058c77931 100644 --- a/packages/sdk/src/hooks/useSharedObjectState.ts +++ b/packages/sdk/src/hooks/useSharedObjectState.ts @@ -4,8 +4,6 @@ import {useObjectGlobalProps} from '../contexts/ObjectGlobalPropsContext'; import {AttributeNameEnum} from '../enums'; import {AttributeValueInterface} from '../interfaces'; -import {useObject} from './useObject'; - type ReturnType<T> = [T | null | undefined, (value: T | null) => Promise<T | null>]; export const useSharedObjectState = <T extends AttributeValueInterface>( @@ -13,47 +11,32 @@ export const useSharedObjectState = <T extends AttributeValueInterface>( ): ReturnType<T> => { const [state, setState] = useState<T | null>(); - const {api} = useObjectGlobalProps(); - const {objectId} = useObject(); - const { - useAttributeChange, - useAttributeRemove, - getSpaceAttributeValue, - setSpaceAttributeValue, - deleteSpaceAttribute - } = api; + const {pluginApi} = useObjectGlobalProps(); + const {getStateItem, setStateItem, deleteStateItem, useStateItemChange, useStateItemRemove} = + pluginApi; useEffect(() => { - if (!objectId) { - return; - } - getSpaceAttributeValue<T>(objectId, name) + getStateItem<T>(name) .then((data) => setState((state) => (state === undefined ? data : state) ?? null)) .catch((err) => { console.log('Error getting initial value of', name, 'attribute:', err); setState(null); }); - }, [name, getSpaceAttributeValue, objectId]); + }, [name, getStateItem]); - useAttributeChange<T>('space-attribute-changed', name, setState); - useAttributeRemove('space-attribute-removed', name, () => setState(null)); + useStateItemChange<T>(name, setState); + useStateItemRemove(name, () => setState(null)); const setSharedState = useCallback( async (value: T | null) => { - if (!objectId) { - throw new Error('No objectId found in useSharedObjectState'); - } - const isDelete = value === null || value === undefined; console.log('setSharedState', {name, value, isDelete}); - const result = await (isDelete - ? deleteSpaceAttribute(objectId, name) - : setSpaceAttributeValue(objectId, name, value)); + const result = await (isDelete ? deleteStateItem(name) : setStateItem(name, value)); console.log('setSharedState result', result); setState(result); return result; }, - [deleteSpaceAttribute, name, objectId, setSpaceAttributeValue] + [deleteStateItem, name, setStateItem] ); return [state, setSharedState]; diff --git a/packages/sdk/src/interfaces/api.interface.ts b/packages/sdk/src/interfaces/api.interface.ts deleted file mode 100644 index 59265163a..000000000 --- a/packages/sdk/src/interfaces/api.interface.ts +++ /dev/null @@ -1,54 +0,0 @@ -import {AttributeValueInterface} from './attributeValue.interface'; - -export interface ApiInterface { - getSpaceAttributeValue: <T extends AttributeValueInterface>( - spaceId: string, - attributeName: string - ) => Promise<T>; - setSpaceAttributeValue: <T extends AttributeValueInterface>( - spaceId: string, - attributeName: string, - value: T - ) => Promise<T>; - deleteSpaceAttribute: (spaceId: string, attributeName: string) => Promise<null>; - - getSpaceAttributeItem: <T>( - spaceId: string, - attributeName: string, - attributeItemName: string - ) => Promise<T>; - setSpaceAttributeItem: <T>( - spaceId: string, - attributeName: string, - attributeItemName: string, - value: T - ) => Promise<T>; - deleteSpaceAttributeItem: ( - spaceId: string, - attributeName: string, - attributeItemName: string - ) => Promise<null>; - - subscribeToTopic: (topic: string) => void; - unsubscribeFromTopic: (topic: string) => void; - - useAttributeChange: <T extends AttributeValueInterface = AttributeValueInterface>( - topic: string, - attributeName: string, - callback: (value: T) => void - ) => void; - useAttributeRemove: (topic: string, attributeName: string, callback: () => void) => void; - - useAttributeItemChange: <T = unknown>( - topic: string, - attributeName: string, - attributeItemName: string, - callback: (value: T) => void - ) => void; - useAttributeItemRemove: ( - topic: string, - attributeName: string, - attributeItemName: string, - callback: () => void - ) => void; -} diff --git a/packages/sdk/src/interfaces/index.ts b/packages/sdk/src/interfaces/index.ts index b0d7e78dc..bf4d6c780 100644 --- a/packages/sdk/src/interfaces/index.ts +++ b/packages/sdk/src/interfaces/index.ts @@ -3,5 +3,4 @@ export * from './plugin.interface'; export * from './pluginApi.interface'; export * from './pluginConfig.interface'; export * from './attributeValue.interface'; -export * from './api.interface'; export * from './usePluginHook.interface'; diff --git a/packages/sdk/src/interfaces/pluginProps.interface.ts b/packages/sdk/src/interfaces/pluginProps.interface.ts index 536df44bf..59e3df69b 100644 --- a/packages/sdk/src/interfaces/pluginProps.interface.ts +++ b/packages/sdk/src/interfaces/pluginProps.interface.ts @@ -1,7 +1,6 @@ import {ThemeInterface} from '@momentum-xyz/ui-kit'; import {PluginApiInterface} from './pluginApi.interface'; -import {ApiInterface} from './api.interface'; export interface PluginPropsInterface<C = unknown> { theme: ThemeInterface; @@ -12,7 +11,6 @@ export interface PluginPropsInterface<C = unknown> { onToggleExpand?: () => void; pluginApi: PluginApiInterface<C>; - api: ApiInterface; onClose: () => void; } From b640b0ada74d4cf089adff38e9f1948270e0c129 Mon Sep 17 00:00:00 2001 From: Dmitry Yudakov <dmitry.yudakov@gmail.com> Date: Fri, 13 Oct 2023 21:56:12 +0300 Subject: [PATCH 03/19] feat(plugins): extended api, useWorldHook Object lock/unlock, spawn/transform/remove, get assets3d is supported as functions. Most posbus messages supported as events. In plugin code it's possible to either use pluginApi to subscribe with on() function passing object with event name and handler - this way eventName-handler types matching is preserved. PosBusService caches some of the received data - objects/transforms/users/... - they are used to untie event subscription from posbus messages being received. This way when a plugin decides to subscribe to joining world or objects/users/etc, it will receive this data as event right after the subscription. TODO better attributes operations and shortcuts for setting name, color. --- .../objectRepository/objectRepository.api.ts | 2 + .../objectRepository.api.types.ts | 4 +- .../PluginAttributesManager.ts | 220 ++++++++++++- .../app/src/core/types/posBusEvent.type.ts | 4 +- .../shared/services/posBus/PosBusService.tsx | 55 +++- .../app/src/stores/PluginStore/PluginStore.ts | 5 +- .../models/ObjectStore/ObjectStore.ts | 2 +- packages/sdk/package.json | 1 + .../src/contexts/ObjectGlobalPropsContext.tsx | 57 +++- .../ObjectViewEmulator/ObjectViewEmulator.tsx | 200 ++++++------ packages/sdk/src/hooks/index.ts | 1 + packages/sdk/src/hooks/useWorld.ts | 136 ++++++++ packages/sdk/src/interfaces/index.ts | 1 + .../sdk/src/interfaces/pluginApi.interface.ts | 64 ++++ .../src/interfaces/useWorldHook.interface.ts | 298 ++++++++++++++++++ 15 files changed, 936 insertions(+), 114 deletions(-) create mode 100644 packages/sdk/src/hooks/useWorld.ts create mode 100644 packages/sdk/src/interfaces/useWorldHook.interface.ts diff --git a/packages/app/src/api/repositories/objectRepository/objectRepository.api.ts b/packages/app/src/api/repositories/objectRepository/objectRepository.api.ts index 680c720d0..aa002d854 100644 --- a/packages/app/src/api/repositories/objectRepository/objectRepository.api.ts +++ b/packages/app/src/api/repositories/objectRepository/objectRepository.api.ts @@ -30,6 +30,7 @@ export const createObject: RequestInterface<CreateObjectRequest, CreateObjectRes object_type_id, asset_2d_id, asset_3d_id, + transform, minimap, ...restOptions } = options; @@ -42,6 +43,7 @@ export const createObject: RequestInterface<CreateObjectRequest, CreateObjectRes parent_id, asset_2d_id, asset_3d_id, + transform, minimap }, restOptions diff --git a/packages/app/src/api/repositories/objectRepository/objectRepository.api.types.ts b/packages/app/src/api/repositories/objectRepository/objectRepository.api.types.ts index 1021f94ff..eedc6da65 100644 --- a/packages/app/src/api/repositories/objectRepository/objectRepository.api.types.ts +++ b/packages/app/src/api/repositories/objectRepository/objectRepository.api.types.ts @@ -1,4 +1,4 @@ -import {AttributeValueInterface} from '@momentum-xyz/sdk'; +import {AttributeValueInterface, Transform} from '@momentum-xyz/sdk'; import {MetadataInterface, OptionsInterface} from 'api/interfaces'; @@ -41,6 +41,8 @@ export interface CreateObjectRequest { asset_2d_id?: string; asset_3d_id?: string; + transform?: Transform; + minimap?: boolean; } diff --git a/packages/app/src/core/models/PluginAttributesManager/PluginAttributesManager.ts b/packages/app/src/core/models/PluginAttributesManager/PluginAttributesManager.ts index ec0d0646c..f9fdd66c3 100644 --- a/packages/app/src/core/models/PluginAttributesManager/PluginAttributesManager.ts +++ b/packages/app/src/core/models/PluginAttributesManager/PluginAttributesManager.ts @@ -1,15 +1,25 @@ -import {RequestModel} from '@momentum-xyz/core'; -import {PluginApiInterface, AttributeValueInterface, AttributeNameEnum} from '@momentum-xyz/sdk'; +import {ObjectTypeIdEnum, RequestModel} from '@momentum-xyz/core'; +import { + PluginApiInterface, + AttributeValueInterface, + AttributeNameEnum, + SetWorld, + Transform, + PluginApiEventHandlersType +} from '@momentum-xyz/sdk'; import {flow, Instance, types} from 'mobx-state-tree'; import {api, GetObjectAttributeResponse} from 'api'; import {appVariables} from 'api/constants'; import {usePosBusEvent} from 'shared/hooks'; +import {PosBusService} from 'shared/services'; +import {PosBusEventEmitter} from 'core/constants'; +import {Asset3dCategoryEnum} from 'api/enums'; const PluginAttributesManager = types .model('PluginAttributesManager', { pluginId: types.string, - spaceId: types.string, + worldId: types.maybeNull(types.string), getAttributeRequest: types.optional(RequestModel, {}), setAttributeRequest: types.optional(RequestModel, {}), @@ -21,7 +31,9 @@ const PluginAttributesManager = types setStateRequest: types.optional(RequestModel, {}), deleteStateRequest: types.optional(RequestModel, {}), - getConfigRequest: types.optional(RequestModel, {}) + getConfigRequest: types.optional(RequestModel, {}), + + objectOperationRequest: types.optional(RequestModel, {}) }) .actions((self) => ({ getSpaceAttributeValue: flow(function* <T extends AttributeValueInterface>( @@ -160,16 +172,31 @@ const PluginAttributesManager = types }) })) .actions((self) => ({ - getItem: flow(function* <T extends AttributeValueInterface>(spaceId: string, key: string) { - return yield self.getSpaceAttributeItem<T>(spaceId, AttributeNameEnum.STATE, key); + setWorldId(worldInfo: SetWorld | null) { + self.worldId = worldInfo?.id || null; + } + })) + .actions((self) => ({ + afterCreate() { + console.log('PluginAttributesManager afterCreate'); + PosBusEventEmitter.on('set-world', self.setWorldId); + }, + beforeDestroy() { + console.log('PluginAttributesManager beforeDestroy'); + PosBusEventEmitter.off('set-world', self.setWorldId); + } + })) + .actions((self) => ({ + getItem: flow(function* <T extends AttributeValueInterface>(objectId: string, key: string) { + return yield self.getSpaceAttributeItem<T>(objectId, AttributeNameEnum.STATE, key); }), - set: flow(function* <T>(spaceId: string, key: string, value: T) { - return yield self.setSpaceAttributeItem(spaceId, AttributeNameEnum.STATE, key, value); + set: flow(function* <T>(objectId: string, key: string, value: T) { + return yield self.setSpaceAttributeItem(objectId, AttributeNameEnum.STATE, key, value); }), - deleteItem: flow(function* (spaceId: string, key: string) { + deleteItem: flow(function* (objectId: string, key: string) { // TODO: Replace after attribute item is implemented on PosBus // return yield self.deleteObjectAttributeItem(spaceId, AttributeNameEnum.STATE, key); - return yield self.deleteSpaceAttribute(spaceId, AttributeNameEnum.STATE); + return yield self.deleteSpaceAttribute(objectId, AttributeNameEnum.STATE); }), getConfig: flow(function* <C extends GetObjectAttributeResponse>() { return yield self.getSpaceAttributeValue<C>(appVariables.NODE_ID, AttributeNameEnum.CONFIG); @@ -179,13 +206,24 @@ const PluginAttributesManager = types get pluginApi(): PluginApiInterface { return { getStateItem: async <T>(key: string) => { - const result = await self.getItem(self.spaceId, key); + if (self.worldId === null) { + throw new Error('worldId is not set'); + } + const result = await self.getItem(self.worldId, key); return result as T; }, setStateItem: async <T>(key: string, value: T) => { - return await self.set(self.spaceId, key, value); + if (self.worldId === null) { + throw new Error('worldId is not set'); + } + return await self.set(self.worldId, key, value); + }, + deleteStateItem: (key: string) => { + if (self.worldId === null) { + throw new Error('worldId is not set'); + } + return self.deleteItem(self.worldId, key); }, - deleteStateItem: (key: string) => self.deleteItem(self.spaceId, key), getConfig: self.getConfig, // TODO: Temporary, change to below after PosBus supports attribute items @@ -251,7 +289,161 @@ const PluginAttributesManager = types } } ); - } + }, + + on(handlers: Partial<PluginApiEventHandlersType>) { + const unsubscribeCallbacks = Object.entries(handlers).map(([eventName, handler]) => { + if (handler) { + PosBusEventEmitter.on(eventName as keyof PluginApiEventHandlersType, handler); + return () => + PosBusEventEmitter.off(eventName as keyof PluginApiEventHandlersType, handler); + } + return () => {}; + }); + + setTimeout(() => { + if (!PosBusService.worldInfo) { + return; + } + handlers['set-world']?.(PosBusService.worldInfo); + + if (PosBusService.myTransform) { + handlers['my-transform']?.(PosBusService.myTransform); + } + + for (const [, objectDefinition] of PosBusService.objectDefinitions) { + handlers['add-object']?.(objectDefinition); + } + + for (const [, objectData] of PosBusService.objectDatas) { + handlers['object-data']?.(objectData.id, objectData); + } + + for (const [id, objectTransform] of PosBusService.objectTransforms) { + handlers['object-transform']?.(id, objectTransform); + } + + if (PosBusService.users.size > 0) { + handlers['users-added']?.(Array.from(PosBusService.users.values())); + } + + if (PosBusService.usersTransforms.size > 0) { + handlers['users-transform-list']?.( + Array.from(PosBusService.usersTransforms.values()) + ); + } + }, 0); + + return () => { + unsubscribeCallbacks.forEach((unsubscribeCallback) => unsubscribeCallback()); + }; + }, + + requestObjectLock: (objectId: string) => { + console.log('requestObjectLock', objectId); + return PosBusService.requestObjectLock(objectId); + }, + requestObjectUnlock: (objectId: string) => { + console.log('requestObjectUnlock', objectId); + return PosBusService.requestObjectUnlock(objectId); + }, + spawnObject: async ({ + name, + asset_2d_id = null, + asset_3d_id = null, + transform, + object_type_id = ObjectTypeIdEnum.NORMAL + }: { + name: string; + asset_2d_id?: string | null; + asset_3d_id: string | null; + object_type_id?: string; + transform?: Transform; + }) => { + if (!self.worldId) { + throw new Error('worldId is not set'); + } + const response = await self.objectOperationRequest.send( + api.objectRepository.createObject, + { + parent_id: self.worldId, + object_name: name, + asset_2d_id: asset_2d_id || undefined, + asset_3d_id: asset_3d_id || undefined, + object_type_id, + transform + } + ); + + if (self.objectOperationRequest.isError || !response) { + throw Error('Unknown error'); + } + + return response; + }, + transformObject: (objectId: string, objectTransform: Transform) => { + PosBusService.sendObjectTransform(objectId, objectTransform); + }, + removeObject: async (objectId: string) => { + await self.objectOperationRequest.send(api.objectRepository.deleteObject, { + objectId + }); + + if (self.objectOperationRequest.isError) { + throw Error('Unknown error'); + } + }, + getSupportedAssets3d: async (category: 'basic' | 'custom') => { + // const response = await self.objectOperationRequest.send( + // api.assets3dRepository.fetchAssets3d, + // { + // category: category as Asset3dCategoryEnum + // } + // ); + + // if (self.objectOperationRequest.isError || !response) { + // throw Error('Unknown error'); + // } + + // return response as any; // TODO + const response = await api.assets3dRepository.fetchAssets3d( + { + category: category as Asset3dCategoryEnum + }, + undefined as any + ); + console.log('getSupportedAssets3d', response); + if (response.status !== 200) { + throw Error(response.statusText); + } + return response.data; + }, + + setObjectAttribute: (props: { + name: string; + value: any; + objectId: string; + // pluginId?: string + }) => Promise.reject(), + + removeObjectAttribute: ({ + name, + objectId, + pluginId + }: { + name: string; + objectId: string; + pluginId?: string; + }) => Promise.reject(), + getObjectAttribute: ({ + name, + objectId, + pluginId + }: { + name: string; + objectId: string; + pluginId?: string; + }) => Promise.reject() }; } })); diff --git a/packages/app/src/core/types/posBusEvent.type.ts b/packages/app/src/core/types/posBusEvent.type.ts index f6aa95680..e512e052c 100644 --- a/packages/app/src/core/types/posBusEvent.type.ts +++ b/packages/app/src/core/types/posBusEvent.type.ts @@ -1,4 +1,4 @@ -import {AttributeValueInterface} from '@momentum-xyz/sdk'; +import {AttributeValueInterface, PluginApiEventHandlersType} from '@momentum-xyz/sdk'; // import { // PosBusEmojiMessageType, @@ -78,4 +78,4 @@ export type PosBusEventType = { attributeName: string, attributeItemName: string ) => void; -}; +} & PluginApiEventHandlersType; diff --git a/packages/app/src/shared/services/posBus/PosBusService.tsx b/packages/app/src/shared/services/posBus/PosBusService.tsx index 9e2ddda5f..9932f6460 100644 --- a/packages/app/src/shared/services/posBus/PosBusService.tsx +++ b/packages/app/src/shared/services/posBus/PosBusService.tsx @@ -1,4 +1,12 @@ -import {AttributeNameEnum} from '@momentum-xyz/sdk'; +import { + AttributeNameEnum, + ObjectData, + ObjectDefinition, + SetWorld, + Transform, + UserData, + UserTransform +} from '@momentum-xyz/sdk'; import { Client, loadClientWorker, @@ -28,6 +36,15 @@ class PosBusService { private static userId: string; private static worldId?: string; + public static worldInfo?: SetWorld; + public static myTransform?: TransformNoScaleInterface; + + public static objectDefinitions = new Map<string, ObjectDefinition>(); + public static objectTransforms = new Map<string, Transform>(); + public static objectDatas = new Map<string, ObjectData>(); + public static users = new Map<string, UserData>(); + public static usersTransforms = new Map<string, UserTransform>(); + public static attachNextReceivedObjectToCamera = false; public static init(token: string, userId: string /*, worldId?: string | null*/) { @@ -116,6 +133,7 @@ class PosBusService { PosBusEventEmitter.emit('posbus-duplicated-sessions'); console.log('PosBus duplicated sessions. Leave world!'); PosBusService.leaveWorld(); + PosBusService.clearCachedData(); } break; } @@ -125,7 +143,9 @@ class PosBusService { const {users} = data; for (const user of users) { Event3dEmitter.emit('UserAdded', user); + PosBusService.users.set(user.id, user); } + PosBusEventEmitter.emit('users-added', users); break; } @@ -134,7 +154,10 @@ class PosBusService { const {users} = data; for (const user of users) { Event3dEmitter.emit('UserRemoved', user); + PosBusService.users.delete(user); + PosBusService.usersTransforms.delete(user); } + PosBusEventEmitter.emit('users-removed', users); break; } @@ -143,6 +166,10 @@ class PosBusService { const {value: users} = data; Event3dEmitter.emit('UsersTransformChanged', users); + for (const user of users) { + PosBusService.usersTransforms.set(user.id, user); + } + PosBusEventEmitter.emit('users-transform-list', users); break; } @@ -150,6 +177,8 @@ class PosBusService { console.log('PosBus object_transform', data); const {id, object_transform} = data; Event3dEmitter.emit('ObjectTransform', id, object_transform); + PosBusService.objectTransforms.set(id, object_transform); + PosBusEventEmitter.emit('object-transform', id, object_transform); break; } @@ -187,12 +216,16 @@ class PosBusService { }); } + PosBusService.objectDatas.set(id, data); + PosBusEventEmitter.emit('object-data', id, data); + break; } case MsgType.SET_WORLD: { console.log('Handle posbus set_world', data); Event3dEmitter.emit('SetWorld', data, PosBusService.userId); - + PosBusService.worldInfo = data; + PosBusEventEmitter.emit('set-world', data); break; } @@ -200,6 +233,8 @@ class PosBusService { console.log('Handle posbus message my_transform', data); Event3dEmitter.emit('MyInitialTransform', data); + PosBusService.myTransform = data; + PosBusEventEmitter.emit('my-transform', data); break; } @@ -233,7 +268,11 @@ class PosBusService { } PosBusService.attachNextReceivedObjectToCamera = false; + + PosBusService.objectDefinitions.set(object.id, object); + PosBusEventEmitter.emit('add-object', object); } + break; } @@ -242,6 +281,8 @@ class PosBusService { const {objects} = data; for (const objectId of objects) { Event3dEmitter.emit('RemoveObject', objectId); + PosBusService.objectDefinitions.delete(objectId); + PosBusEventEmitter.emit('remove-object', objectId); } break; } @@ -435,6 +476,16 @@ class PosBusService { static unsubscribe(topic: string) { this.main._subscribedAttributeTypeTopics.delete(topic); } + + static clearCachedData() { + this.objectDefinitions.clear(); + this.objectTransforms.clear(); + this.objectDatas.clear(); + this.users.clear(); + this.usersTransforms.clear(); + this.myTransform = undefined; + this.worldInfo = undefined; + } } export default PosBusService; diff --git a/packages/app/src/stores/PluginStore/PluginStore.ts b/packages/app/src/stores/PluginStore/PluginStore.ts index b38b11cd0..d4e8cb9d8 100644 --- a/packages/app/src/stores/PluginStore/PluginStore.ts +++ b/packages/app/src/stores/PluginStore/PluginStore.ts @@ -3,6 +3,7 @@ import {RequestModel} from '@momentum-xyz/core'; import {api} from 'api'; import {DynamicScriptList, PluginAttributesManager, PluginLoader} from 'core/models'; +import {getRootStore} from 'core/utils'; interface PluginInfoInterface { plugin_id: string; @@ -82,6 +83,8 @@ export const PluginStore = types await self.dynamicScriptList.addScript(meta.scopeName, meta.scriptUrl); } + const worldId = getRootStore(self).universeStore.worldId; + const pluginLoader = PluginLoader.create({ id: plugin_id, pluginId: plugin_id, @@ -89,7 +92,7 @@ export const PluginStore = types ...meta, attributesManager: PluginAttributesManager.create({ pluginId: plugin_id, - spaceId: '123' // TODO remove + worldId }) }); diff --git a/packages/app/src/stores/UniverseStore/models/ObjectStore/ObjectStore.ts b/packages/app/src/stores/UniverseStore/models/ObjectStore/ObjectStore.ts index eefe1382b..8bd2a2607 100644 --- a/packages/app/src/stores/UniverseStore/models/ObjectStore/ObjectStore.ts +++ b/packages/app/src/stores/UniverseStore/models/ObjectStore/ObjectStore.ts @@ -89,7 +89,7 @@ const ObjectStore = types ...meta, attributesManager: PluginAttributesManager.create({ pluginId: meta.pluginId, - spaceId: objectId + worldId: objectId }) }); diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 36bc5dbda..24eafc4a4 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -17,6 +17,7 @@ }, "dependencies": { "@craco/craco": "^6.1.1", + "@momentum-xyz/core": "^0.1.2", "@momentum-xyz/ui-kit": "^0.1.4", "react-router-dom": "^6.6.2" }, diff --git a/packages/sdk/src/contexts/ObjectGlobalPropsContext.tsx b/packages/sdk/src/contexts/ObjectGlobalPropsContext.tsx index 1ae8007df..5e71c3b4b 100644 --- a/packages/sdk/src/contexts/ObjectGlobalPropsContext.tsx +++ b/packages/sdk/src/contexts/ObjectGlobalPropsContext.tsx @@ -2,7 +2,7 @@ import {FC, useContext, PropsWithChildren} from 'react'; import {createContext} from 'react'; import {DefaultThemeConfig} from '@momentum-xyz/ui-kit'; -import {PluginPropsInterface} from '../interfaces'; +import {PluginPropsInterface, Transform} from '../interfaces'; import {ThemeContextProvider} from './ThemeContext'; @@ -16,7 +16,60 @@ export const ObjectGlobalPropsContext = createContext<PluginPropsInterface>({ deleteStateItem: () => Promise.reject(), useStateItemChange: () => Promise.reject(), - useStateItemRemove: () => Promise.reject() + useStateItemRemove: () => Promise.reject(), + + on: (handlers) => { + throw new Error('Method not implemented.'); + }, + requestObjectLock: (objectId: string) => Promise.reject(), + requestObjectUnlock: (objectId: string) => { + throw new Error('Method not implemented.'); + }, + + spawnObject: ({ + name, + asset_2d_id, + asset_3d_id, + transform, + object_type_id + }: { + name: string; + asset_2d_id?: string | null; + asset_3d_id: string | null; + object_type_id?: string; + transform?: Transform; + }) => Promise.reject(), + transformObject(objectId, transform) { + throw new Error('Method not implemented.'); + }, + removeObject: (objectId: string) => Promise.reject(), + getSupportedAssets3d: (category: 'basic' | 'custom') => Promise.reject(), + + setObjectAttribute: (props: { + name: string; + value: any; + objectId: string; + // pluginId?: string + }) => Promise.reject(), + + removeObjectAttribute: ({ + name, + objectId, + pluginId + }: { + name: string; + objectId: string; + pluginId?: string; + }) => Promise.reject(), + getObjectAttribute: ({ + name, + objectId, + pluginId + }: { + name: string; + objectId: string; + pluginId?: string; + }) => Promise.reject() }, onClose: () => {} }); diff --git a/packages/sdk/src/emulator/components/ObjectViewEmulator/ObjectViewEmulator.tsx b/packages/sdk/src/emulator/components/ObjectViewEmulator/ObjectViewEmulator.tsx index 6dd3520c1..acd96eda8 100644 --- a/packages/sdk/src/emulator/components/ObjectViewEmulator/ObjectViewEmulator.tsx +++ b/packages/sdk/src/emulator/components/ObjectViewEmulator/ObjectViewEmulator.tsx @@ -1,10 +1,11 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ import {FC, useMemo} from 'react'; import {useParams} from 'react-router-dom'; import {Panel, ErrorBoundary, ThemeInterface} from '@momentum-xyz/ui-kit'; import {useAttributesEmulator} from '../../hooks'; import {useTheme} from '../../../contexts/ThemeContext'; -import {PluginPropsInterface, PluginInterface} from '../../../interfaces'; +import {PluginPropsInterface, PluginInterface, Transform} from '../../../interfaces'; import {ObjectGlobalPropsContextProvider} from '../../../contexts'; import * as styled from './ObjectViewEmulator.styled'; @@ -19,96 +20,113 @@ export const ObjectViewEmulator: FC<PropsInterface> = ({plugin, isAdmin, onClose const {objectId} = useParams<{objectId: string}>(); console.log('RENDER ObjectViewEmulator', {plugin, objectId}); - const theme = useTheme(); - const { - spaceAttributes, - useAttributeItemChange, - useAttributeItemRemove, - subscribeToTopic, - unsubscribeFromTopic - } = useAttributesEmulator(); - - const config = useMemo( - () => - ({ - APP_ID: '' - } as any), - [] - ); - - const coreProps: PluginPropsInterface = useMemo( - () => ({ - theme: theme as ThemeInterface, - isAdmin, - objectId, - pluginApi: { - getStateItem: <T,>(key: string) => { - const state = spaceAttributes.current.find( - ({attributeName}) => attributeName === 'state' - )?.attributeValue; - - if (!state) { - return Promise.reject(); - } - - return Promise.resolve(state[key] as T); - }, - setStateItem: <T,>(key: string, value: T) => { - const state = spaceAttributes.current.find( - ({attributeName}) => attributeName === 'state' - )?.attributeValue; - - if (!state) { - return Promise.reject(); - } - state[key] = value; - return Promise.resolve(state[key] as T); - }, - getConfig: () => Promise.resolve(config), - deleteStateItem: (key) => { - const state = spaceAttributes.current.find( - ({attributeName}) => attributeName === 'state' - )?.attributeValue; - - if (!state) { - return Promise.reject(); - } - - delete state[key]; - return Promise.resolve(null); - }, - - subscribeToStateUsingTopic: subscribeToTopic, - unsubscribeFromStateUsingTopic: unsubscribeFromTopic, - - useStateItemChange: (key, callback) => useAttributeItemChange('', 'state', key, callback), - useStateItemRemove: (key, callback) => useAttributeItemRemove('', 'state', key, callback) - }, - onClose - }), - [ - theme, - objectId, - subscribeToTopic, - unsubscribeFromTopic, - useAttributeItemChange, - useAttributeItemRemove, - spaceAttributes, - config, - onClose, - isAdmin - ] - ); - - return ( - <ErrorBoundary errorMessage="Error while rendering plugin"> - <ObjectGlobalPropsContextProvider props={coreProps}> - <styled.Container> - <PluginInnerWrapper pluginProps={coreProps} plugin={plugin} /> - </styled.Container> - </ObjectGlobalPropsContextProvider> - </ErrorBoundary> - ); + return <div>Under construction! Use Odyssey UI for testing</div>; + + // const theme = useTheme(); + // const { + // spaceAttributes, + // useAttributeItemChange, + // useAttributeItemRemove, + // subscribeToTopic, + // unsubscribeFromTopic + // } = useAttributesEmulator(); + + // const config = useMemo( + // () => + // ({ + // APP_ID: '' + // } as any), + // [] + // ); + + // const coreProps: PluginPropsInterface = useMemo( + // () => ({ + // theme: theme as ThemeInterface, + // isAdmin, + // objectId, + // pluginApi: { + // getStateItem: <T,>(key: string) => { + // const state = spaceAttributes.current.find( + // ({attributeName}) => attributeName === 'state' + // )?.attributeValue; + + // if (!state) { + // return Promise.reject(); + // } + + // return Promise.resolve(state[key] as T); + // }, + // setStateItem: <T,>(key: string, value: T) => { + // const state = spaceAttributes.current.find( + // ({attributeName}) => attributeName === 'state' + // )?.attributeValue; + + // if (!state) { + // return Promise.reject(); + // } + // state[key] = value; + // return Promise.resolve(state[key] as T); + // }, + // getConfig: () => Promise.resolve(config), + // deleteStateItem: (key) => { + // const state = spaceAttributes.current.find( + // ({attributeName}) => attributeName === 'state' + // )?.attributeValue; + + // if (!state) { + // return Promise.reject(); + // } + + // delete state[key]; + // return Promise.resolve(null); + // }, + + // subscribeToStateUsingTopic: subscribeToTopic, + // unsubscribeFromStateUsingTopic: unsubscribeFromTopic, + + // useStateItemChange: (key, callback) => useAttributeItemChange('', 'state', key, callback), + // useStateItemRemove: (key, callback) => useAttributeItemRemove('', 'state', key, callback), + + // on: (handlers) => { + // console.log('on', {handlers}); + // return () => {}; + // }, + + // requestObjectLock: (objectId: string) => { + // return Promise.resolve(); + // }, + // requestObjectUnlock: (objectId: string) => {}, + + // transformObject: (objectId: string, transform: Transform) => { + // console.log('transformObject', {objectId, transform}); + // return Promise.resolve(); + // } + // }, + // onClose + // }), + // [ + // theme, + // objectId, + // subscribeToTopic, + // unsubscribeFromTopic, + // useAttributeItemChange, + // useAttributeItemRemove, + // spaceAttributes, + // config, + // onClose, + // isAdmin + // ] + // ); + + // return ( + // <ErrorBoundary errorMessage="Error while rendering plugin"> + // <ObjectGlobalPropsContextProvider props={coreProps}> + // <styled.Container> + // <PluginInnerWrapper pluginProps={coreProps} plugin={plugin} /> + // </styled.Container> + // </ObjectGlobalPropsContextProvider> + // </ErrorBoundary> + // ); }; const PluginInnerWrapper = ({ diff --git a/packages/sdk/src/hooks/index.ts b/packages/sdk/src/hooks/index.ts index 72fd360ab..7dc61cace 100644 --- a/packages/sdk/src/hooks/index.ts +++ b/packages/sdk/src/hooks/index.ts @@ -1,3 +1,4 @@ export * from './useObject'; export * from './useSharedObjectState'; export * from './useConfigEmulatorStorage'; +export * from './useWorld'; diff --git a/packages/sdk/src/hooks/useWorld.ts b/packages/sdk/src/hooks/useWorld.ts new file mode 100644 index 000000000..c71119f02 --- /dev/null +++ b/packages/sdk/src/hooks/useWorld.ts @@ -0,0 +1,136 @@ +import {useEffect, useMemo, useRef} from 'react'; +// import {Event3dEmitter} from '@momentum-xyz/core'; + +import {useObjectGlobalProps} from '../contexts/ObjectGlobalPropsContext'; +// import {AttributeNameEnum} from '../enums'; +import {Transform, UseWorldPropsInterface, UseWorldReturnInterface} from '../interfaces'; + +import {useObject} from './useObject'; + +export const useWorld = (props: UseWorldPropsInterface): UseWorldReturnInterface => { + const refProps = useRef<UseWorldPropsInterface>(props); + refProps.current = props; + + const {pluginApi} = useObjectGlobalProps(); + const {objectId} = useObject(); + useEffect(() => { + console.log('[useWorld]: useEffect', {pluginApi}); + const { + onJoinedWorld, + onLeftWorld, + onObjectAdded, + onObjectMove, + onObjectData, + onObjectRemoved, + onMyPosition, + onUserAdded, + onUserMove, + onUserRemoved + } = refProps.current; + + return pluginApi?.on({ + 'set-world': (worldInfo) => { + console.log('[useWorld]: set-world', {worldInfo}); + if (worldInfo) { + onJoinedWorld?.(worldInfo); + } else { + onLeftWorld?.(); + } + }, + 'users-added': (users) => { + console.log('[useWorld]: users-added', {users}); + users.forEach((user) => onUserAdded?.(user)); + }, + 'users-removed': (userIds) => { + console.log('[useWorld]: users-removed', {userIds}); + userIds.forEach((userId) => onUserRemoved?.(userId)); + }, + 'users-transform-list': (transforms) => { + console.log('[useWorld]: users-transform-list', {transforms}); + transforms.forEach((transform) => onUserMove?.(transform)); + }, + 'object-transform': (objectId, transform) => { + console.log('[useWorld]: object-transform', {objectId, transform}); + onObjectMove?.(objectId, transform); + }, + 'object-data': (objectId, data) => { + console.log('[useWorld]: object-data', {objectId, data}); + onObjectData?.(objectId, data); + }, + 'my-transform': (transform) => { + console.log('[useWorld]: my-transform', {transform}); + onMyPosition?.(transform); + }, + 'add-object': (object) => { + console.log('[useWorld]: add-object', {object}); + onObjectAdded?.(object); + }, + 'remove-object': (objectId) => { + console.log('[useWorld]: remove-object', {objectId}); + onObjectRemoved?.(objectId); + } + }); + }, [refProps, pluginApi]); + + const returnObject: UseWorldReturnInterface = useMemo(() => { + return { + worldId: objectId || null, + requestObjectLock(objectId: string) { + console.log('[useWorld]: call requestObjectLock', {objectId}); + return pluginApi.requestObjectLock(objectId); + }, + requestObjectUnlock(objectId: string) { + console.log('[useWorld]: call requestObjectUnlock', {objectId}); + return pluginApi.requestObjectUnlock(objectId); + }, + transformObject(objectId: string, object_transform: Transform) { + console.log('[useWorld]: call transformObject', {objectId, object_transform}); + return pluginApi.transformObject(objectId, object_transform); + }, + setObjectAttribute(data: any) { + console.log('[useWorld]: call setObjectAttribute', {data}); + throw new Error('setObjectAttribute not implemented'); + }, + removeObjectAttribute(data: any) { + console.log('[useWorld]: call removeObjectAttribute', {data}); + throw new Error('removeObjectAttribute not implemented'); + }, + getObjectAttribute(data: any) { + console.log('[useWorld]: call getObjectAttribute', {data}); + throw new Error('getObjectAttribute not implemented'); + }, + setObjectColor(objectId: string, color: string | null) { + console.log('[useWorld]: call setObjectColor', {objectId, color}); + throw new Error('setObjectColor not implemented'); + }, + setObjectName(objectId: string, name: string) { + console.log('[useWorld]: call setObjectName', {objectId, name}); + throw new Error('setObjectName not implemented'); + }, + getObjectInfo(objectId: string) { + console.log('[useWorld]: call getObjectInfo', {objectId}); + throw new Error('getObjectInfo not implemented'); + }, + spawnObject(data: { + name: string; + asset_2d_id?: string | null; + asset_3d_id: string | null; + object_type_id?: string; + transform?: Transform; + }) { + console.log('[useWorld]: call spawnObject', {data}); + return pluginApi.spawnObject(data); + }, + removeObject(objectId: string) { + console.log('[useWorld]: call removeObject', {objectId}); + return pluginApi.removeObject(objectId); + }, + getSupportedAssets3d(category: 'basic' | 'custom') { + console.log('[useWorld]: call getSupportedAssets3d', {category}); + return pluginApi.getSupportedAssets3d(category); + } + }; + }, [objectId, pluginApi]); + + return returnObject; +}; diff --git a/packages/sdk/src/interfaces/index.ts b/packages/sdk/src/interfaces/index.ts index bf4d6c780..b18da9bb3 100644 --- a/packages/sdk/src/interfaces/index.ts +++ b/packages/sdk/src/interfaces/index.ts @@ -4,3 +4,4 @@ export * from './pluginApi.interface'; export * from './pluginConfig.interface'; export * from './attributeValue.interface'; export * from './usePluginHook.interface'; +export * from './useWorldHook.interface'; diff --git a/packages/sdk/src/interfaces/pluginApi.interface.ts b/packages/sdk/src/interfaces/pluginApi.interface.ts index c8019245d..55defb3fa 100644 --- a/packages/sdk/src/interfaces/pluginApi.interface.ts +++ b/packages/sdk/src/interfaces/pluginApi.interface.ts @@ -1,3 +1,28 @@ +import { + Asset3d, + ObjectData, + ObjectDefinition, + SetWorld, + Transform, + TransformNoScale, + UserData, + UserTransform +} from './useWorldHook.interface'; + +type UnsubscribeCallbackType = () => void; + +export type PluginApiEventHandlersType = { + 'set-world': (worldInfo: SetWorld | null) => void; + 'users-added': (users: UserData[]) => void; + 'users-removed': (userIds: string[]) => void; + 'users-transform-list': (transforms: UserTransform[]) => void; + 'object-transform': (objectId: string, transform: Transform) => void; + 'object-data': (objectId: string, data: ObjectData) => void; + 'my-transform': (transform: TransformNoScale) => void; + 'add-object': (object: ObjectDefinition) => void; + 'remove-object': (objectId: string) => void; +}; + export interface PluginApiInterface<C = unknown> { getStateItem: <T>(key: string) => Promise<T>; setStateItem: <T>(key: string, value: T) => Promise<T>; @@ -6,4 +31,43 @@ export interface PluginApiInterface<C = unknown> { useStateItemChange: <T = unknown>(key: string, callback: (value: T) => void) => void; useStateItemRemove: (key: string, callback: () => void) => void; + + on: (handlers: Partial<PluginApiEventHandlersType>) => UnsubscribeCallbackType; + + requestObjectLock: (objectId: string) => Promise<void>; + requestObjectUnlock: (objectId: string) => void; + + spawnObject({ + name, + asset_2d_id, + asset_3d_id, + transform, + object_type_id + }: { + name: string; + asset_2d_id?: string | null; + asset_3d_id: string | null; + object_type_id?: string; + transform?: Transform; + }): Promise<any>; + transformObject: (objectId: string, transform: Transform) => void; + removeObject(objectId: string): Promise<any>; + getSupportedAssets3d(category: 'basic' | 'custom'): Promise<Asset3d[]>; + + setObjectAttribute(data: { + name: string; + value: any; + objectId: string; + // pluginId?: string + }): Promise<any>; + removeObjectAttribute(data: { + name: string; + objectId: string; + // pluginId?: string + }): Promise<any>; + getObjectAttribute(data: { + name: string; + objectId: string; + // pluginId?: string + }): Promise<any>; } diff --git a/packages/sdk/src/interfaces/useWorldHook.interface.ts b/packages/sdk/src/interfaces/useWorldHook.interface.ts new file mode 100644 index 000000000..bc4f30645 --- /dev/null +++ b/packages/sdk/src/interfaces/useWorldHook.interface.ts @@ -0,0 +1,298 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable @typescript-eslint/naming-convention */ +export const CORE_PLUGIN_ID = 'f0f0f0f0-0f0f-4ff0-af0f-f0f0f0f0f0f0'; +export const CUSTOM_OBJECT_TYPE_ID = '4ed3a5bb-53f8-4511-941b-07902982c31c'; + +export interface TransformNoScale { + position: { + x: number; + y: number; + z: number; + }; + rotation: { + x: number; + y: number; + z: number; + }; +} + +export interface Transform extends TransformNoScale { + scale: { + x: number; + y: number; + z: number; + }; +} +export interface ObjectDefinition { + id: string; + parent_id: string; + asset_type: string; + asset_format: string; + name: string; + transform: Transform; + // is_editable: boolean; + // tethered_to_parent: boolean; + // show_on_minimap: boolean; +} + +export interface ObjectInfo { + owner_id: string; + parent_id: string; + object_type_id: string; + asset_2d_id: string | null; + asset_3d_id: string | null; + transform: Transform; +} + +export interface Asset3d { + id: string; + user_id: string; + meta: { + category: string; + name: string; + preview_hash?: string; + type: number; + }; + is_private: boolean; + createdAt: string; + updatedAt: string; +} + +export interface SetWorld { + id: string; + name: string; + avatar: string; + owner: string; + // avatar_3d_asset_id: string; +} + +export interface UserData { + id: string; + name: string; + avatar: string; + transform: TransformNoScale; + is_guest: boolean; +} + +export interface UserTransform { + id: string; + transform: TransformNoScale; +} + +export type SlotType = '' | 'texture' | 'string' | 'number'; +export interface ObjectData { + id: string; + entries: { + SlotType: { + [key: string]: any; + }; + }; +} + +export interface UseWorldPropsInterface { + onJoinedWorld?: (worldInfo: SetWorld) => void; + onLeftWorld?: () => void; + + onMyPosition?: (transform: TransformNoScale) => void; + + onUserAdded?: (user: UserData) => void; + onUserMove?: (user: UserTransform) => void; + onUserRemoved?: (userId: string) => void; + + onObjectAdded?(object: ObjectDefinition): void; + onObjectMove?: (objectId: string, transform: Transform) => void; + onObjectData?: (objectId: string, data: ObjectData) => void; + onObjectRemoved?: (objectId: string) => void; +} + +export interface UseWorldReturnInterface { + worldId: string | null; + + // /** + // * Moves the user to a new position and orientation defined by the provided transform. + // * + // * @param {TransformNoScale} transform - The transformation parameters to move the user. This includes position, rotation, but not scale. + // */ + // moveUser(transform: TransformNoScale): void; + + /** + * Requests a lock on an object required for tranform operation - otherwise it will be ignored. + * If the lock is not aquired, the function will throw an error. + * + * @param {string} objectId - The ID of the object that is being locked. + */ + requestObjectLock(objectId: string): Promise<void>; + + /** + * Releases a lock on an object. + * + * @param {string} objectId - The ID of the object that is being unlocked. + */ + requestObjectUnlock(objectId: string): void; + + /** + * Transforms an object by changing its position, rotation, and/or scale. + * + * @param {string} objectId - The ID of the object that is being transformed. + * @param {Transform} object_transform - An object containing the parameters for the transformation. This includes new position, rotation, and scale. + */ + transformObject(objectId: string, object_transform: Transform): void; + + // /** + // * Sends a high-five action to another user, along with an optional message. + // * + // * @param {string} userId - The ID of the user who will receive the high-five. + // * @param {string} [message] - Optional. A message to send along with the high-five. + // */ + // sendHighFive(userId: string, message?: string): void; + + /** + * Sets an attribute of a specified object with a given value. + * + * @param {Object} params - An object that contains parameters for setting the attribute. + * @param {string} params.name - The name of the attribute. + * @param {any} params.value - The value to set the attribute to. + * @param {string} params.objectId - The ID of the object to which the attribute will be set. + * @param {string} params.pluginId - Optional. The ID of the plugin to which the attribute is related. Defaults to the core plugin ID. + */ + setObjectAttribute({ + name, + value, + objectId, + pluginId + }: { + name: string; + value: any; + objectId: string; + pluginId?: string; + }): Promise<any>; + + /** + * Removes an attribute of a specified object. + * + * @param {Object} params - An object that contains parameters for removing the attribute. + * @param {string} params.name - The name of the attribute. + * @param {string} params.objectId - The ID of the object from which the attribute will be removed. + * @param {string} params.pluginId - Optional. The ID of the plugin from which the attribute will be removed. Defaults to the core plugin ID. + */ + removeObjectAttribute({ + name, + objectId, + pluginId = CORE_PLUGIN_ID + }: { + name: string; + objectId: string; + pluginId?: string; + }): Promise<null>; + + /** + * Fetches the value of a specified attribute of an object. + * + * @param {Object} params - An object that contains parameters for fetching the attribute. + * @param {string} params.name - The name of the attribute. + * @param {string} params.objectId - The ID of the object from which the attribute will be fetched. + * @param {string} params.pluginId - Optional. The ID of the plugin from which the attribute will be fetched. Defaults to the core plugin ID. + */ + getObjectAttribute({ + name, + objectId, + pluginId = CORE_PLUGIN_ID + }: { + name: string; + objectId: string; + pluginId?: string; + }): Promise<any>; + + // /** + // * Subscribes to changes in an attribute of a specified object, and provides callbacks to handle change or error events. + // * + // * Note that changes detection doesn't work for every attribute. The attribute needs to have posbus_auto Option in attribute_type. + // * + // * @param {Object} params - An object that contains parameters for the subscription. + // * @param {string} params.name - The name of the attribute to subscribe to. + // * @param {string} params.objectId - The ID of the object whose attribute is being subscribed to. + // * @param {string} params.pluginId - Optional. The ID of the plugin for the attribute. Defaults to the core plugin ID. + // * @param {(value: any) => void} params.onChange - Optional. A callback function that is called when the attribute changes. + // * @param {(err: Error) => void} params.onError - Optional. A callback function that is called when an error occurs. + // * @returns {Function} - Returns a function that unsubscribes from the attribute when called. + // */ + // subscribeToObjectAttribute({ + // name, + // objectId, + // pluginId = CORE_PLUGIN_ID, + // onChange, + // onError + // }: { + // name: string; + // objectId: string; + // pluginId?: string; + // onChange?: (value: any) => void; + // onError?: (err: Error) => void; + // }): () => void; + + /** + * Sets the color of a specified object. + * The color is specified as a hex string, e.g. '#ff0000' for red. + * If the color is null, the color will be cleared. + */ + setObjectColor(objectId: string, color: string | null): Promise<null>; + + /** + * Sets the name of a specified object. + * The name is specified as a string. + * + * @param {string} objectId - The ID of the object to set the name of. + * @param {string} name - The name to set. + */ + setObjectName(objectId: string, name: string): Promise<null>; + + /** + * Fetches info of a specified object. + * + * @param {string} objectId - The ID of the object to fetch info from. + */ + getObjectInfo(objectId: string): Promise<ObjectInfo>; + + /** + * Creates a new object in the virtual world. + * + * @param {Object} params - An object that contains parameters for the new object. + * @param {string} params.name - The name of the new object. + * @param {string} params.object_type_id - Optional. The type of the new object. Defaults to the default object type. + * @param {string} params.asset_3d_id - The 3D model that the new object will use. + * @param {Transform} params.transform - Optional. The initial position and rotation of the new object. + * Current user position and rotation will be used if not specified. + */ + spawnObject({ + name, + asset_2d_id, + asset_3d_id, + object_type_id, + transform + }: { + name: string; + asset_2d_id?: string | null; + asset_3d_id: string | null; + object_type_id?: string; + transform?: Transform; + }): Promise<ObjectDefinition>; + + /** + * Removes an object from the virtual world. + * + * @param {string} objectId - The ID of the object to remove. + * @returns {Promise<null>} - Returns a promise that resolves when the object is removed. + * The promise will reject if the object is not found or user has no admin rights. + * + */ + removeObject(objectId: string): Promise<null>; + + /** + * Fetches a list of supported 3D assets - Odyssey basic, Community ones and your account's private collection. + * + * @param {string} category - The category of assets to fetch. Can be 'basic' or 'custom'. + * + * @returns {Promise<any>} - Returns a promise that resolves to an array of supported assets. + */ + getSupportedAssets3d(category: 'basic' | 'custom'): Promise<Asset3d[]>; +} From 9bc5fe495613ce52c00243992c27288098443d2a Mon Sep 17 00:00:00 2001 From: Dmitry Yudakov <dmitry.yudakov@gmail.com> Date: Mon, 16 Oct 2023 17:28:43 +0300 Subject: [PATCH 04/19] feat(core): make RequestInterface send() instance argument optional Axios default instance is being taken in this case --- packages/core/src/interfaces/request.interface.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/interfaces/request.interface.ts b/packages/core/src/interfaces/request.interface.ts index f73dd0e95..86b07dd60 100644 --- a/packages/core/src/interfaces/request.interface.ts +++ b/packages/core/src/interfaces/request.interface.ts @@ -1,7 +1,7 @@ import {AxiosResponse, AxiosRequestConfig, AxiosInstance} from 'axios'; export interface RequestInterface<P, R> { - (params: (P | null) & Partial<AxiosRequestConfig>, request: AxiosInstance): Promise< + (params: (P | null) & Partial<AxiosRequestConfig>, request?: AxiosInstance): Promise< AxiosResponse<R> >; } From 65a053fdd3b2a667f50c87fa76d6e19678144356 Mon Sep 17 00:00:00 2001 From: Dmitry Yudakov <dmitry.yudakov@gmail.com> Date: Mon, 16 Oct 2023 17:30:04 +0300 Subject: [PATCH 05/19] feat(plugins): object attributes in pluginApi/useWorld --- .../PluginAttributesManager.ts | 129 ++++++++++++------ .../src/contexts/ObjectGlobalPropsContext.tsx | 9 +- packages/sdk/src/hooks/useWorld.ts | 12 +- .../sdk/src/interfaces/pluginApi.interface.ts | 4 + 4 files changed, 103 insertions(+), 51 deletions(-) diff --git a/packages/app/src/core/models/PluginAttributesManager/PluginAttributesManager.ts b/packages/app/src/core/models/PluginAttributesManager/PluginAttributesManager.ts index f9fdd66c3..37346dfb5 100644 --- a/packages/app/src/core/models/PluginAttributesManager/PluginAttributesManager.ts +++ b/packages/app/src/core/models/PluginAttributesManager/PluginAttributesManager.ts @@ -16,6 +16,8 @@ import {PosBusService} from 'shared/services'; import {PosBusEventEmitter} from 'core/constants'; import {Asset3dCategoryEnum} from 'api/enums'; +import {ObjectAttribute} from '../ObjectAttribute'; + const PluginAttributesManager = types .model('PluginAttributesManager', { pluginId: types.string, @@ -31,9 +33,7 @@ const PluginAttributesManager = types setStateRequest: types.optional(RequestModel, {}), deleteStateRequest: types.optional(RequestModel, {}), - getConfigRequest: types.optional(RequestModel, {}), - - objectOperationRequest: types.optional(RequestModel, {}) + getConfigRequest: types.optional(RequestModel, {}) }) .actions((self) => ({ getSpaceAttributeValue: flow(function* <T extends AttributeValueInterface>( @@ -343,10 +343,12 @@ const PluginAttributesManager = types console.log('requestObjectLock', objectId); return PosBusService.requestObjectLock(objectId); }, + requestObjectUnlock: (objectId: string) => { console.log('requestObjectUnlock', objectId); return PosBusService.requestObjectUnlock(objectId); }, + spawnObject: async ({ name, asset_2d_id = null, @@ -363,49 +365,49 @@ const PluginAttributesManager = types if (!self.worldId) { throw new Error('worldId is not set'); } - const response = await self.objectOperationRequest.send( - api.objectRepository.createObject, - { - parent_id: self.worldId, - object_name: name, - asset_2d_id: asset_2d_id || undefined, - asset_3d_id: asset_3d_id || undefined, - object_type_id, - transform - } - ); + const response = await api.objectRepository.createObject({ + parent_id: self.worldId, + object_name: name, + asset_2d_id: asset_2d_id || undefined, + asset_3d_id: asset_3d_id || undefined, + object_type_id, + transform + }); - if (self.objectOperationRequest.isError || !response) { - throw Error('Unknown error'); + console.log('spawnObject', response); + if (response.status >= 300) { + throw Error(response.statusText); } - - return response; + return response.data; }, + transformObject: (objectId: string, objectTransform: Transform) => { PosBusService.sendObjectTransform(objectId, objectTransform); }, + + getObjectInfo: async (objectId: string) => { + const response = await api.objectInfoRepository.getObjectInfo({objectId}); + console.log('getObjectInfo', response); + if (response.status >= 300) { + throw Error(response.statusText); + } + return response.data; + }, + removeObject: async (objectId: string) => { - await self.objectOperationRequest.send(api.objectRepository.deleteObject, { + const response = await api.objectRepository.deleteObject({ objectId }); - if (self.objectOperationRequest.isError) { - throw Error('Unknown error'); + console.log('removeObject', response); + if (response.status >= 300) { + throw Error(response.statusText); } + + return response.data; }, + getSupportedAssets3d: async (category: 'basic' | 'custom') => { - // const response = await self.objectOperationRequest.send( - // api.assets3dRepository.fetchAssets3d, - // { - // category: category as Asset3dCategoryEnum - // } - // ); - - // if (self.objectOperationRequest.isError || !response) { - // throw Error('Unknown error'); - // } - - // return response as any; // TODO const response = await api.assets3dRepository.fetchAssets3d( { category: category as Asset3dCategoryEnum @@ -413,37 +415,76 @@ const PluginAttributesManager = types undefined as any ); console.log('getSupportedAssets3d', response); - if (response.status !== 200) { + if (response.status >= 300) { throw Error(response.statusText); } return response.data; }, - setObjectAttribute: (props: { + setObjectAttribute: ({ + name, + value, + objectId + }: { name: string; value: any; objectId: string; // pluginId?: string - }) => Promise.reject(), + }) => { + const model = ObjectAttribute.create({ + objectId, + attributeName: name + }); + return model.set(value); + }, removeObjectAttribute: ({ name, - objectId, - pluginId - }: { + objectId + }: // pluginId + { name: string; objectId: string; pluginId?: string; - }) => Promise.reject(), + }) => { + const model = ObjectAttribute.create({ + objectId, + attributeName: name + }); + return model.delete(); + }, + getObjectAttribute: ({ name, - objectId, - pluginId - }: { + objectId + }: // pluginId + { name: string; objectId: string; pluginId?: string; - }) => Promise.reject() + }) => { + const model = ObjectAttribute.create({ + objectId, + attributeName: name + }); + return model.load(); + }, + + setObjectColor: (objectId: string, color: string | null) => { + const model = ObjectAttribute.create({ + objectId, + attributeName: 'object_color' + }); + return model.set({value: color}); + }, + + setObjectName: (objectId: string, name: string) => { + const model = ObjectAttribute.create({ + objectId, + attributeName: 'name' + }); + return model.set({value: name}); + } }; } })); diff --git a/packages/sdk/src/contexts/ObjectGlobalPropsContext.tsx b/packages/sdk/src/contexts/ObjectGlobalPropsContext.tsx index 5e71c3b4b..52989b163 100644 --- a/packages/sdk/src/contexts/ObjectGlobalPropsContext.tsx +++ b/packages/sdk/src/contexts/ObjectGlobalPropsContext.tsx @@ -42,6 +42,7 @@ export const ObjectGlobalPropsContext = createContext<PluginPropsInterface>({ transformObject(objectId, transform) { throw new Error('Method not implemented.'); }, + getObjectInfo: (objectId: string) => Promise.reject(), removeObject: (objectId: string) => Promise.reject(), getSupportedAssets3d: (category: 'basic' | 'custom') => Promise.reject(), @@ -69,7 +70,13 @@ export const ObjectGlobalPropsContext = createContext<PluginPropsInterface>({ name: string; objectId: string; pluginId?: string; - }) => Promise.reject() + }) => Promise.reject(), + setObjectColor(objectId, color) { + throw new Error('Method not implemented.'); + }, + setObjectName(objectId, name) { + throw new Error('Method not implemented.'); + } }, onClose: () => {} }); diff --git a/packages/sdk/src/hooks/useWorld.ts b/packages/sdk/src/hooks/useWorld.ts index c71119f02..4a9bbc51f 100644 --- a/packages/sdk/src/hooks/useWorld.ts +++ b/packages/sdk/src/hooks/useWorld.ts @@ -89,27 +89,27 @@ export const useWorld = (props: UseWorldPropsInterface): UseWorldReturnInterface }, setObjectAttribute(data: any) { console.log('[useWorld]: call setObjectAttribute', {data}); - throw new Error('setObjectAttribute not implemented'); + return pluginApi.setObjectAttribute(data); }, removeObjectAttribute(data: any) { console.log('[useWorld]: call removeObjectAttribute', {data}); - throw new Error('removeObjectAttribute not implemented'); + return pluginApi.removeObjectAttribute(data); }, getObjectAttribute(data: any) { console.log('[useWorld]: call getObjectAttribute', {data}); - throw new Error('getObjectAttribute not implemented'); + return pluginApi.getObjectAttribute(data); }, setObjectColor(objectId: string, color: string | null) { console.log('[useWorld]: call setObjectColor', {objectId, color}); - throw new Error('setObjectColor not implemented'); + return pluginApi.setObjectColor(objectId, color); }, setObjectName(objectId: string, name: string) { console.log('[useWorld]: call setObjectName', {objectId, name}); - throw new Error('setObjectName not implemented'); + return pluginApi.setObjectName(objectId, name); }, getObjectInfo(objectId: string) { console.log('[useWorld]: call getObjectInfo', {objectId}); - throw new Error('getObjectInfo not implemented'); + return pluginApi.getObjectInfo(objectId); }, spawnObject(data: { name: string; diff --git a/packages/sdk/src/interfaces/pluginApi.interface.ts b/packages/sdk/src/interfaces/pluginApi.interface.ts index 55defb3fa..51a90c03e 100644 --- a/packages/sdk/src/interfaces/pluginApi.interface.ts +++ b/packages/sdk/src/interfaces/pluginApi.interface.ts @@ -2,6 +2,7 @@ import { Asset3d, ObjectData, ObjectDefinition, + ObjectInfo, SetWorld, Transform, TransformNoScale, @@ -51,6 +52,7 @@ export interface PluginApiInterface<C = unknown> { transform?: Transform; }): Promise<any>; transformObject: (objectId: string, transform: Transform) => void; + getObjectInfo(objectId: string): Promise<ObjectInfo>; removeObject(objectId: string): Promise<any>; getSupportedAssets3d(category: 'basic' | 'custom'): Promise<Asset3d[]>; @@ -70,4 +72,6 @@ export interface PluginApiInterface<C = unknown> { objectId: string; // pluginId?: string }): Promise<any>; + setObjectColor(objectId: string, color: string | null): Promise<any>; + setObjectName(objectId: string, name: string): Promise<any>; } From 6671aa449a1eb556dfb0b3906ba5d0f961a60c35 Mon Sep 17 00:00:00 2001 From: Dmitry Yudakov <dmitry.yudakov@gmail.com> Date: Mon, 16 Oct 2023 19:22:55 +0300 Subject: [PATCH 06/19] feat(attribute-models): better error reporting in ObjectAttribute/ObjectUserAttribute --- .../models/ObjectAttribute/ObjectAttribute.ts | 51 ++++++++++++------ .../ObjectUserAttribute.ts | 54 +++++++++++++++---- 2 files changed, 80 insertions(+), 25 deletions(-) diff --git a/packages/app/src/core/models/ObjectAttribute/ObjectAttribute.ts b/packages/app/src/core/models/ObjectAttribute/ObjectAttribute.ts index 417a99ae4..3684fca19 100644 --- a/packages/app/src/core/models/ObjectAttribute/ObjectAttribute.ts +++ b/packages/app/src/core/models/ObjectAttribute/ObjectAttribute.ts @@ -40,6 +40,13 @@ export const ObjectAttribute = types response ); + if (self.request.isError) { + throw new Error( + 'Error loading attribute: ' + + ((response?.error as any)?.message || self.request.errorCode) + ); + } + if (response) { self._value = response; } @@ -49,7 +56,7 @@ export const ObjectAttribute = types set: flow(function* (value: AttributeValueInterface) { console.log('ObjectAttribute set:', value); - yield self.request.send(api.objectAttributeRepository.setObjectAttribute, { + const response = yield self.request.send(api.objectAttributeRepository.setObjectAttribute, { objectId: self.objectId, plugin_id: self.pluginId, attribute_name: self.attributeName, @@ -57,22 +64,29 @@ export const ObjectAttribute = types }); if (self.request.isError) { - throw new Error('Error setting attribute: ' + self.request.errorCode); + throw new Error( + 'Error setting attribute: ' + (response?.error?.message || self.request.errorCode) + ); } self._value = value; }), setItem: flow(function* (itemName: string, value: unknown) { - yield self.request.send(api.objectAttributeRepository.setObjectAttributeItem, { - objectId: self.objectId, - plugin_id: self.pluginId, - attribute_name: self.attributeName, - sub_attribute_key: itemName, - value - }); + const response = yield self.request.send( + api.objectAttributeRepository.setObjectAttributeItem, + { + objectId: self.objectId, + plugin_id: self.pluginId, + attribute_name: self.attributeName, + sub_attribute_key: itemName, + value + } + ); if (self.request.isError) { - throw new Error('Error setting attribute item: ' + self.request.errorCode); + throw new Error( + 'Error setting attribute item: ' + (response?.error?.message || self.request.errorCode) + ); } if (self._value) { @@ -82,14 +96,19 @@ export const ObjectAttribute = types } }), delete: flow(function* () { - yield self.request.send(api.objectAttributeRepository.deleteObjectAttribute, { - objectId: self.objectId, - plugin_id: self.pluginId, - attribute_name: self.attributeName - }); + const response = yield self.request.send( + api.objectAttributeRepository.deleteObjectAttribute, + { + objectId: self.objectId, + plugin_id: self.pluginId, + attribute_name: self.attributeName + } + ); if (self.request.isError) { - throw new Error('Error deleting attribute: ' + self.request.errorCode); + throw new Error( + 'Error deleting attribute: ' + (response?.error?.message || self.request.errorCode) + ); } }) })) diff --git a/packages/app/src/core/models/ObjectUserAttribute/ObjectUserAttribute.ts b/packages/app/src/core/models/ObjectUserAttribute/ObjectUserAttribute.ts index fe7f0e90d..a95923256 100644 --- a/packages/app/src/core/models/ObjectUserAttribute/ObjectUserAttribute.ts +++ b/packages/app/src/core/models/ObjectUserAttribute/ObjectUserAttribute.ts @@ -36,6 +36,12 @@ export const ObjectUserAttribute = types ); console.log('[ObjectUserAttribute] load', params, 'resp:', response); + if (self.request.isError) { + throw new Error( + 'Error loading attribute: ' + + ((response?.error as any)?.message || self.request.errorCode) + ); + } if (response) { self._value = response; @@ -54,10 +60,16 @@ export const ObjectUserAttribute = types value }; - yield self.request.send(api.objectUserAttributeRepository.setObjectUserAttribute, data); + const response = yield self.request.send( + api.objectUserAttributeRepository.setObjectUserAttribute, + data + ); if (self.request.isError) { - throw new Error('Error setting attribute: ' + self.request.errorCode); + throw new Error( + 'Error setting attribute: ' + + ((response?.error as any)?.message || self.request.errorCode) + ); } self._value = value; @@ -72,10 +84,15 @@ export const ObjectUserAttribute = types sub_attribute_key: itemName, sub_attribute_value: value }; - yield self.request.send(api.objectUserAttributeRepository.setObjectUserSubAttribute, data); + const response = yield self.request.send( + api.objectUserAttributeRepository.setObjectUserSubAttribute, + data + ); if (self.request.isError) { - throw new Error('Error setting attribute item: ' + self.request.errorCode); + throw new Error( + 'Error setting attribute item: ' + (response?.error?.message || self.request.errorCode) + ); } if (self._value) { @@ -92,10 +109,15 @@ export const ObjectUserAttribute = types attributeName: self.attributeName }; console.log('[ObjectUserAttribute] delete', params); - yield self.request.send(api.objectUserAttributeRepository.deleteObjectUserAttribute, params); + const response = yield self.request.send( + api.objectUserAttributeRepository.deleteObjectUserAttribute, + params + ); if (self.request.isError) { - throw new Error('Error deleting attribute: ' + self.request.errorCode); + throw new Error( + 'Error deleting attribute: ' + (response?.error?.message || self.request.errorCode) + ); } self._value = null; @@ -109,13 +131,15 @@ export const ObjectUserAttribute = types sub_attribute_key: itemName }; console.log('[ObjectUserAttribute] deleteItem', params); - yield self.request.send( + const response = yield self.request.send( api.objectUserAttributeRepository.deleteObjectUserSubAttribute, params ); if (self.request.isError) { - throw new Error('Error deleting attribute item: ' + self.request.errorCode); + throw new Error( + 'Error deleting attribute item: ' + (response?.error?.message || self.request.errorCode) + ); } }), countAllUsers: flow(function* () { @@ -125,13 +149,19 @@ export const ObjectUserAttribute = types attributeName: self.attributeName }; console.log('[ObjectUserAttribute] countAllUsers', params); - const response: {count: number} = yield self.request.send( + const response: {count: number; error?: any} = yield self.request.send( api.objectUserAttributeRepository.getObjectUserAttributeCount, params ); console.log('[ObjectUserAttribute] countAllUsers', params, 'resp:', response); + if (self.request.isError) { + throw new Error( + 'Error counting users: ' + (response?.error?.message || self.request.errorCode) + ); + } + self.count = response?.count || 0; return response; @@ -172,6 +202,12 @@ export const ObjectUserAttribute = types } ); + if (self.request.isError) { + throw new Error( + 'Error getting users: ' + ((response as any)?.error?.message || self.request.errorCode) + ); + } + if (response) { const {items, count} = response; self.count = count; From 2b94439dcf8e108dc285eff252e6f73e122cc370 Mon Sep 17 00:00:00 2001 From: Dmitry Yudakov <dmitry.yudakov@gmail.com> Date: Mon, 16 Oct 2023 19:47:57 +0300 Subject: [PATCH 07/19] fix(plugin): spawnObject bad signature --- .../models/PluginAttributesManager/PluginAttributesManager.ts | 3 ++- packages/sdk/src/interfaces/pluginApi.interface.ts | 2 +- packages/sdk/src/interfaces/useWorldHook.interface.ts | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/app/src/core/models/PluginAttributesManager/PluginAttributesManager.ts b/packages/app/src/core/models/PluginAttributesManager/PluginAttributesManager.ts index 37346dfb5..fc10eb7b6 100644 --- a/packages/app/src/core/models/PluginAttributesManager/PluginAttributesManager.ts +++ b/packages/app/src/core/models/PluginAttributesManager/PluginAttributesManager.ts @@ -378,7 +378,8 @@ const PluginAttributesManager = types if (response.status >= 300) { throw Error(response.statusText); } - return response.data; + const {object_id} = response.data; + return {id: object_id}; }, transformObject: (objectId: string, objectTransform: Transform) => { diff --git a/packages/sdk/src/interfaces/pluginApi.interface.ts b/packages/sdk/src/interfaces/pluginApi.interface.ts index 51a90c03e..91edfe7ec 100644 --- a/packages/sdk/src/interfaces/pluginApi.interface.ts +++ b/packages/sdk/src/interfaces/pluginApi.interface.ts @@ -50,7 +50,7 @@ export interface PluginApiInterface<C = unknown> { asset_3d_id: string | null; object_type_id?: string; transform?: Transform; - }): Promise<any>; + }): Promise<{id: string}>; transformObject: (objectId: string, transform: Transform) => void; getObjectInfo(objectId: string): Promise<ObjectInfo>; removeObject(objectId: string): Promise<any>; diff --git a/packages/sdk/src/interfaces/useWorldHook.interface.ts b/packages/sdk/src/interfaces/useWorldHook.interface.ts index bc4f30645..2e3e7ac7d 100644 --- a/packages/sdk/src/interfaces/useWorldHook.interface.ts +++ b/packages/sdk/src/interfaces/useWorldHook.interface.ts @@ -275,7 +275,7 @@ export interface UseWorldReturnInterface { asset_3d_id: string | null; object_type_id?: string; transform?: Transform; - }): Promise<ObjectDefinition>; + }): Promise<{id: string}>; /** * Removes an object from the virtual world. From ee63ed4db96765f57dabafa289c787261288a391 Mon Sep 17 00:00:00 2001 From: Dmitry Yudakov <dmitry.yudakov@gmail.com> Date: Mon, 16 Oct 2023 19:48:25 +0300 Subject: [PATCH 08/19] fix(plugin): emit my-transform when current user position is changed --- packages/app/src/shared/services/posBus/PosBusService.tsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/app/src/shared/services/posBus/PosBusService.tsx b/packages/app/src/shared/services/posBus/PosBusService.tsx index 9932f6460..e3a71b0b7 100644 --- a/packages/app/src/shared/services/posBus/PosBusService.tsx +++ b/packages/app/src/shared/services/posBus/PosBusService.tsx @@ -168,6 +168,11 @@ class PosBusService { Event3dEmitter.emit('UsersTransformChanged', users); for (const user of users) { PosBusService.usersTransforms.set(user.id, user); + + if (user.id === PosBusService.userId) { + PosBusService.myTransform = user.transform; + PosBusEventEmitter.emit('my-transform', user.transform); + } } PosBusEventEmitter.emit('users-transform-list', users); break; From d6b5deec2b3378fb0b2b0bb66d76f65b39a19dc4 Mon Sep 17 00:00:00 2001 From: Dmitry Yudakov <dmitry.yudakov@gmail.com> Date: Tue, 17 Oct 2023 14:39:12 +0300 Subject: [PATCH 09/19] fix(plugins): strange scene offset appearing when plugin is activated The canvas gets moved few pixels up creating a gap at the bottom. I cannot see what pushes it but setting display flex for the parent element seems to fix it in the right place. --- .../src/scenes/WorldBabylonScene/WorldBabylonScene.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/core3d/src/scenes/WorldBabylonScene/WorldBabylonScene.tsx b/packages/core3d/src/scenes/WorldBabylonScene/WorldBabylonScene.tsx index 5e69079dc..812e92f06 100644 --- a/packages/core3d/src/scenes/WorldBabylonScene/WorldBabylonScene.tsx +++ b/packages/core3d/src/scenes/WorldBabylonScene/WorldBabylonScene.tsx @@ -189,7 +189,13 @@ const WorldBabylonScene: FC<Odyssey3dPropsInterface> = ({events, renderURL, ...c }; return ( - <div data-testid="Babylon-scene"> + <div + data-testid="Babylon-scene" + style={{ + // fixes strange offset appearing when creator plugin is opened + display: 'flex' + }} + > <SceneComponent id="babylon-canvas" antialias From 5a911b409ef8d2ae19518a9945bf7676bce72eda Mon Sep 17 00:00:00 2001 From: Dmitry Yudakov <dmitry.yudakov@gmail.com> Date: Thu, 19 Oct 2023 18:33:53 +0300 Subject: [PATCH 10/19] feat(ui-kit): built-in withFrame option in FileUploader component in most cases we want this dot-bordered frame and it saves some styling code --- .../molecules/FileUploader/FileUploader.stories.tsx | 13 +++++++++++++ .../molecules/FileUploader/FileUploader.styled.ts | 11 +++++++++++ .../src/molecules/FileUploader/FileUploader.tsx | 7 ++++++- 3 files changed, 30 insertions(+), 1 deletion(-) diff --git a/packages/ui-kit/src/molecules/FileUploader/FileUploader.stories.tsx b/packages/ui-kit/src/molecules/FileUploader/FileUploader.stories.tsx index 8991b0977..a20646e1b 100644 --- a/packages/ui-kit/src/molecules/FileUploader/FileUploader.stories.tsx +++ b/packages/ui-kit/src/molecules/FileUploader/FileUploader.stories.tsx @@ -27,3 +27,16 @@ General.args = { console.log(file); } }; + +export const WithFrame = Template.bind({}); +WithFrame.args = { + fileType: 'image', + label: 'Choose image', + dragActiveLabel: 'Some content should be there.', + withFrame: true, + enableDragAndDrop: true, + maxSize: 50_100_000, + onFilesUpload: (file) => { + console.log(file); + } +}; diff --git a/packages/ui-kit/src/molecules/FileUploader/FileUploader.styled.ts b/packages/ui-kit/src/molecules/FileUploader/FileUploader.styled.ts index e6ed36bdd..d7858e3dc 100644 --- a/packages/ui-kit/src/molecules/FileUploader/FileUploader.styled.ts +++ b/packages/ui-kit/src/molecules/FileUploader/FileUploader.styled.ts @@ -25,6 +25,17 @@ export const Container = styled.div` box-shadow: none; } } + + &.withFrame { + position: relative; + padding-top: 0px; + margin-top: 0px; + + padding: 45px; + border: 1px dashed ${(props) => props.theme.text}; + border-radius: 8px; + margin-bottom: 20px; + } `; export const DropZone = styled.div` diff --git a/packages/ui-kit/src/molecules/FileUploader/FileUploader.tsx b/packages/ui-kit/src/molecules/FileUploader/FileUploader.tsx index 9ae2b0482..0a199beb5 100644 --- a/packages/ui-kit/src/molecules/FileUploader/FileUploader.tsx +++ b/packages/ui-kit/src/molecules/FileUploader/FileUploader.tsx @@ -12,6 +12,7 @@ export interface FileUploaderPropsInterface { buttonSize?: 'small' | 'normal' | 'medium'; iconButton?: boolean; dragActiveLabel: string; + withFrame?: boolean; onFilesUpload: (file: File | undefined) => void; onError?: (error: Error) => void; fileType?: 'image' | 'video' | 'audio' | 'asset'; @@ -39,6 +40,7 @@ const FileUploader: FC<FileUploaderPropsInterface> = ({ dragActiveLabel, buttonSize = 'normal', iconButton, + withFrame, onFilesUpload, onError, fileType, @@ -73,7 +75,10 @@ const FileUploader: FC<FileUploaderPropsInterface> = ({ const {onClick, ...restRootProps} = getRootProps(); return ( - <styled.Container data-testid="FileUploader-test" className={cn(iconButton && 'iconButton')}> + <styled.Container + data-testid="FileUploader-test" + className={cn(iconButton && 'iconButton', withFrame && 'withFrame')} + > {enableDragAndDrop && <styled.DropZone {...restRootProps} />} <input {...getInputProps()} disabled={disabled} data-testid="FileUploader-input-test" /> {isDragActive ? ( From 91d4d51735b66b006a836c4504453267373cf044 Mon Sep 17 00:00:00 2001 From: Dmitry Yudakov <dmitry.yudakov@gmail.com> Date: Thu, 19 Oct 2023 19:14:38 +0300 Subject: [PATCH 11/19] feat(node-admin): plugins load, upload --- .../mediaRepository.api.endpoints.ts | 3 +- .../mediaRepository/mediaRepository.api.ts | 17 ++++ .../models/MediaUploader/MediaUploader.ts | 12 +++ .../admin/pages/NodeConfig/NodeConfig.tsx | 7 +- .../PluginsManagement.styled.ts | 44 ++++++++++ .../PluginsManagement/PluginsManagement.tsx | 85 +++++++++++++++++++ .../components/PluginsManagement/index.ts | 1 + .../pages/NodeConfig/components/index.ts | 1 + .../app/src/stores/PluginStore/PluginStore.ts | 11 ++- .../molecules/FileUploader/FileUploader.tsx | 3 +- 10 files changed, 178 insertions(+), 6 deletions(-) create mode 100644 packages/app/src/scenes/admin/pages/NodeConfig/components/PluginsManagement/PluginsManagement.styled.ts create mode 100644 packages/app/src/scenes/admin/pages/NodeConfig/components/PluginsManagement/PluginsManagement.tsx create mode 100644 packages/app/src/scenes/admin/pages/NodeConfig/components/PluginsManagement/index.ts diff --git a/packages/app/src/api/repositories/mediaRepository/mediaRepository.api.endpoints.ts b/packages/app/src/api/repositories/mediaRepository/mediaRepository.api.endpoints.ts index b944c9874..2451a10ec 100644 --- a/packages/app/src/api/repositories/mediaRepository/mediaRepository.api.endpoints.ts +++ b/packages/app/src/api/repositories/mediaRepository/mediaRepository.api.endpoints.ts @@ -4,6 +4,7 @@ export const mediaRepositoryEndpoints = () => { return { uploadImage: `${BASE_URL}/image`, uploadVideo: `${BASE_URL}/video`, - uploadAudio: `${BASE_URL}/audio` + uploadAudio: `${BASE_URL}/audio`, + uploadPlugin: `${BASE_URL}/plugin` }; }; diff --git a/packages/app/src/api/repositories/mediaRepository/mediaRepository.api.ts b/packages/app/src/api/repositories/mediaRepository/mediaRepository.api.ts index 8da0ca42d..0dadb9a4d 100644 --- a/packages/app/src/api/repositories/mediaRepository/mediaRepository.api.ts +++ b/packages/app/src/api/repositories/mediaRepository/mediaRepository.api.ts @@ -54,3 +54,20 @@ export const uploadAudio: RequestInterface<UploadFileRequest, UploadFileResponse return request.post(mediaRepositoryEndpoints().uploadAudio, formData, requestOptions); }; + +export const uploadPlugin: RequestInterface<UploadFileRequest, UploadFileResponse> = (options) => { + const {file, headers, ...restOptions} = options; + + const formData: FormData = new FormData(); + formData.append('file', file); + + const requestOptions = { + headers: { + 'Content-Type': 'multipart/form-data', + ...headers + }, + ...restOptions + }; + + return request.post(mediaRepositoryEndpoints().uploadPlugin, formData, requestOptions); +}; diff --git a/packages/app/src/core/models/MediaUploader/MediaUploader.ts b/packages/app/src/core/models/MediaUploader/MediaUploader.ts index 4049b98ac..4cd680292 100644 --- a/packages/app/src/core/models/MediaUploader/MediaUploader.ts +++ b/packages/app/src/core/models/MediaUploader/MediaUploader.ts @@ -33,6 +33,18 @@ const MediaUploader = types.compose( {file: file} ); + return fileResponse?.hash || null; + }), + uploadPlugin: flow(function* (file?: File) { + if (!file) { + return null; + } + + const fileResponse: UploadFileResponse = yield self.fileRequest.send( + api.mediaRepository.uploadPlugin, + {file: file} + ); + return fileResponse?.hash || null; }) })) diff --git a/packages/app/src/scenes/admin/pages/NodeConfig/NodeConfig.tsx b/packages/app/src/scenes/admin/pages/NodeConfig/NodeConfig.tsx index 300543e1b..d8f7f439e 100644 --- a/packages/app/src/scenes/admin/pages/NodeConfig/NodeConfig.tsx +++ b/packages/app/src/scenes/admin/pages/NodeConfig/NodeConfig.tsx @@ -5,7 +5,7 @@ import BN from 'bn.js'; import {useBlockchain, useStore} from 'shared/hooks'; -import {ApiKeys, BlockchainRegistration} from './components'; +import {ApiKeys, BlockchainRegistration, PluginsManagement} from './components'; import * as styled from './NodeConfig.styled'; const NODE_ADDING_FEE = new BN('4200000000000000000'); @@ -327,11 +327,12 @@ const NodeReg: FC = () => { ); }; -type TabsType = 'blockchain' | 'api_keys' | 'mapping' | 'reg'; +type TabsType = 'blockchain' | 'api_keys' | 'plugins' | 'mapping' | 'reg'; const TABS_LIST: TabInterface<TabsType>[] = [ {id: 'blockchain', icon: 'connect', label: 'Blockchain registration'}, {id: 'api_keys', icon: 'key', label: 'API Keys'}, + {id: 'plugins', icon: 'info', label: 'Plugins'}, ...((process.env.NODE_ENV === 'development' ? [ {id: 'mapping', icon: 'info', label: 'mapping'}, @@ -366,6 +367,8 @@ const NodeConfig = () => { {activeTab === 'api_keys' && <ApiKeys />} + {activeTab === 'plugins' && <PluginsManagement />} + {activeTab === 'mapping' && <NodeOdysseyMapping />} {activeTab === 'reg' && <NodeReg />} diff --git a/packages/app/src/scenes/admin/pages/NodeConfig/components/PluginsManagement/PluginsManagement.styled.ts b/packages/app/src/scenes/admin/pages/NodeConfig/components/PluginsManagement/PluginsManagement.styled.ts new file mode 100644 index 000000000..fde924149 --- /dev/null +++ b/packages/app/src/scenes/admin/pages/NodeConfig/components/PluginsManagement/PluginsManagement.styled.ts @@ -0,0 +1,44 @@ +import styled from 'styled-components'; +import {rgba} from 'polished'; + +export const Container = styled.div` + display: flex; + flex-direction: column; + margin-bottom: 40px; + justify-content: space-between; + min-height: 100%; +`; + +export const List = styled.div` + display: flex; + flex-direction: column; + margin-bottom: 20px; +`; + +export const Item = styled.div` + margin: 10px 0 0 0; + padding: 8px 10px; + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; + background: ${(props) => props.theme.accentBg && rgba(props.theme.accentBg, 0.5)}; + box-shadow: -1px -1px 2px ${(props) => props.theme.accentText && rgba(props.theme.accentText, 0.1)}; + border-radius: 4px; + gap: 15px; +`; + +export const Date = styled.div` + font-size: var(--font-size-xs); +`; + +export const BottomPanel = styled.div` + display: flex; + gap: 10px; + flex-direction: column; +`; + +export const Error = styled.div` + color: ${(props) => props.theme.danger}; + font-size: var(--font-size-xs); +`; diff --git a/packages/app/src/scenes/admin/pages/NodeConfig/components/PluginsManagement/PluginsManagement.tsx b/packages/app/src/scenes/admin/pages/NodeConfig/components/PluginsManagement/PluginsManagement.tsx new file mode 100644 index 000000000..0a4cc206e --- /dev/null +++ b/packages/app/src/scenes/admin/pages/NodeConfig/components/PluginsManagement/PluginsManagement.tsx @@ -0,0 +1,85 @@ +import {FC, useState} from 'react'; +import {observer} from 'mobx-react-lite'; +import {Button, ErrorsEnum, FileUploader, Heading} from '@momentum-xyz/ui-kit'; +import {i18n} from '@momentum-xyz/core'; + +import {useStore} from 'shared/hooks'; + +import * as styled from './PluginsManagement.styled'; + +const MAX_FILE_SIZE = 20_000_000; + +const PluginsManagement: FC = () => { + const {pluginStore} = useStore(); + + const [fileToUpload, setFileToUpload] = useState<File>(); + const [error, setError] = useState<string>(); + + const handleUpload = async () => { + if (!fileToUpload) { + return; + } + + setError(undefined); + + await pluginStore.mediaUploader.uploadPlugin(fileToUpload); + + if (pluginStore.mediaUploader.isError) { + setError('Error uploading plugin'); + return; + } + + setFileToUpload(undefined); + + pluginStore.init(); + }; + + console.log('pluginStore', pluginStore.plugins); + + return ( + <styled.Container> + <styled.List> + <Heading variant="h3">Plugins</Heading> + {pluginStore.plugins?.map((plugin) => ( + <styled.Item key={plugin.plugin_id}> + <span>{plugin.meta.name}</span> + {/* <styled.Date>{plugin.updated_at?.slice(0, 19)}</styled.Date> */} + </styled.Item> + ))} + </styled.List> + <styled.BottomPanel> + <Heading variant="h3">Upload plugin</Heading> + Upload a tar.gz file containing a plugin. + <div> + <FileUploader + label="Select file" + dragActiveLabel="Drag and drop a file here" + withFrame + onFilesUpload={(file) => { + console.log('file', file); + setFileToUpload(file); + }} + maxSize={MAX_FILE_SIZE} + onError={(err) => { + console.log('File upload error:', err, err.message); + if (err.message === ErrorsEnum.FileSizeTooLarge) { + setError( + i18n.t('assetsUploader.errorTooLargeFile', { + size: MAX_FILE_SIZE + }) + ); + return; + } + setError(err.message); + }} + fileType="archive" + /> + </div> + {error && <styled.Error>{error}</styled.Error>} + {!!fileToUpload && <Button label="Upload" onClick={handleUpload} />} + </styled.BottomPanel> + </styled.Container> + ); +}; + +export default observer(PluginsManagement); diff --git a/packages/app/src/scenes/admin/pages/NodeConfig/components/PluginsManagement/index.ts b/packages/app/src/scenes/admin/pages/NodeConfig/components/PluginsManagement/index.ts new file mode 100644 index 000000000..5f2ce2406 --- /dev/null +++ b/packages/app/src/scenes/admin/pages/NodeConfig/components/PluginsManagement/index.ts @@ -0,0 +1 @@ +export {default as PluginsManagement} from './PluginsManagement'; diff --git a/packages/app/src/scenes/admin/pages/NodeConfig/components/index.ts b/packages/app/src/scenes/admin/pages/NodeConfig/components/index.ts index a8d27f4c1..c37704c9b 100644 --- a/packages/app/src/scenes/admin/pages/NodeConfig/components/index.ts +++ b/packages/app/src/scenes/admin/pages/NodeConfig/components/index.ts @@ -1,2 +1,3 @@ export * from './BlockchainRegistration'; export * from './ApiKeys'; +export * from './PluginsManagement'; diff --git a/packages/app/src/stores/PluginStore/PluginStore.ts b/packages/app/src/stores/PluginStore/PluginStore.ts index d4e8cb9d8..59d2f5b92 100644 --- a/packages/app/src/stores/PluginStore/PluginStore.ts +++ b/packages/app/src/stores/PluginStore/PluginStore.ts @@ -2,7 +2,7 @@ import {types, flow, cast} from 'mobx-state-tree'; import {RequestModel} from '@momentum-xyz/core'; import {api} from 'api'; -import {DynamicScriptList, PluginAttributesManager, PluginLoader} from 'core/models'; +import {DynamicScriptList, MediaUploader, PluginAttributesManager, PluginLoader} from 'core/models'; import {getRootStore} from 'core/utils'; interface PluginInfoInterface { @@ -42,7 +42,10 @@ export const PluginStore = types // {} // ), - pluginsRequest: types.optional(RequestModel, {}) + _plugins: types.optional(types.frozen<PluginInfoInterface[]>(), []), + + pluginsRequest: types.optional(RequestModel, {}), + mediaUploader: types.optional(MediaUploader, {}) }) .actions((self) => ({ init: flow(function* () { @@ -52,6 +55,7 @@ export const PluginStore = types {} ); console.log('pluginsResponse', pluginsResponse); + self._plugins = pluginsResponse || []; // if (pluginsResponse) { // self.pluginInfosByScopes = pluginsResponse.plugins; @@ -123,5 +127,8 @@ export const PluginStore = types .views((self) => ({ pluginsByScope(scope: string) { return self.pluginLoadersByScopes.get(scope) || []; + }, + get plugins() { + return self._plugins.filter((plugin) => !!plugin.meta.scopeName); } })); diff --git a/packages/ui-kit/src/molecules/FileUploader/FileUploader.tsx b/packages/ui-kit/src/molecules/FileUploader/FileUploader.tsx index 0a199beb5..c722c5820 100644 --- a/packages/ui-kit/src/molecules/FileUploader/FileUploader.tsx +++ b/packages/ui-kit/src/molecules/FileUploader/FileUploader.tsx @@ -15,7 +15,7 @@ export interface FileUploaderPropsInterface { withFrame?: boolean; onFilesUpload: (file: File | undefined) => void; onError?: (error: Error) => void; - fileType?: 'image' | 'video' | 'audio' | 'asset'; + fileType?: keyof typeof ALLOWED_EXTENSIONS; maxSize?: number; enableDragAndDrop?: boolean; disabled?: boolean; @@ -26,6 +26,7 @@ const ALLOWED_EXTENSIONS = { image: {'image/*': ['.jpeg', '.png', '.jpg', '.svg', '.gif']}, video: {'video/*': ['.mp4', '.mov', '.wmv', '.mpeg', '.webm', '.mkv']}, audio: {'audio/*': ['.mp3', '.ogg', '.aac', '.webm', '.flac']}, + archive: {'application/gzip': ['.tar.gz']}, asset: {'model/gltf-binary': ['.glb']} }; From f8a8e6f0353c630579a18d1231c5ce6c8ccc92a1 Mon Sep 17 00:00:00 2001 From: Dmitry Yudakov <dmitry.yudakov@gmail.com> Date: Fri, 20 Oct 2023 16:43:58 +0300 Subject: [PATCH 12/19] feat(sdk): build plugin to name-version.tar.gz, use manifest.json instead of metadata --- packages/sdk/bin/momentum-plugin.js | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/packages/sdk/bin/momentum-plugin.js b/packages/sdk/bin/momentum-plugin.js index 6dbbc8f18..21d98a30e 100755 --- a/packages/sdk/bin/momentum-plugin.js +++ b/packages/sdk/bin/momentum-plugin.js @@ -49,7 +49,7 @@ switch (script) { switch (script) { case 'build': - generateAndStoreMetadata(); + generateAndStoreManifest(); break; default: break; @@ -78,7 +78,7 @@ function spawnProcess(command, args, env) { // process.exit(child.status); } -function generateAndStoreMetadata() { +function generateAndStoreManifest() { const { name, version, @@ -90,7 +90,7 @@ function generateAndStoreMetadata() { attribute_types = [], scopes = [] } = packageJSON; - const metadata = { + const manifest = { name, version, description, @@ -101,7 +101,15 @@ function generateAndStoreMetadata() { attribute_types, scopes }; - const filename = path.resolve(BUILD_DIR, 'metadata.json'); - fs.writeFileSync(filename, JSON.stringify(metadata, null, 2)); - console.log('[momentum-plugin] Metadata generated and stored in', filename); + const filename = path.resolve(BUILD_DIR, 'manifest.json'); + fs.writeFileSync(filename, JSON.stringify(manifest, null, 2)); + console.log('[momentum-plugin] Manifest generated and stored in', filename); + + const versionedDir = `./${name}-${version}`; + + fs.renameSync(BUILD_DIR, versionedDir); + console.log('[momentum-plugin] Plugin build stored in', BUILD_DIR); + + spawnProcess('tar', ['-czf', `${versionedDir}.tar.gz`, versionedDir], {}); + console.log('[momentum-plugin] Plugin tarball generated: ', `${versionedDir}.tar.gz`); } From 6fd3ae74e2f1a7c9e97be5af0912e06623bb385e Mon Sep 17 00:00:00 2001 From: Dmitry Yudakov <dmitry.yudakov@gmail.com> Date: Fri, 20 Oct 2023 16:47:46 +0300 Subject: [PATCH 13/19] feat(node-admin): improve plugins load UX, error handling --- .../PluginsManagement/PluginsManagement.styled.ts | 9 ++++++++- .../PluginsManagement/PluginsManagement.tsx | 13 ++++++++++--- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/packages/app/src/scenes/admin/pages/NodeConfig/components/PluginsManagement/PluginsManagement.styled.ts b/packages/app/src/scenes/admin/pages/NodeConfig/components/PluginsManagement/PluginsManagement.styled.ts index fde924149..6d2b76b71 100644 --- a/packages/app/src/scenes/admin/pages/NodeConfig/components/PluginsManagement/PluginsManagement.styled.ts +++ b/packages/app/src/scenes/admin/pages/NodeConfig/components/PluginsManagement/PluginsManagement.styled.ts @@ -39,6 +39,13 @@ export const BottomPanel = styled.div` `; export const Error = styled.div` - color: ${(props) => props.theme.danger}; font-size: var(--font-size-xs); `; + +export const FilePreview = styled.div` + display: flex; + flex-direction: column; + align-items: center; + gap: 10px; + justify-content: center; +`; diff --git a/packages/app/src/scenes/admin/pages/NodeConfig/components/PluginsManagement/PluginsManagement.tsx b/packages/app/src/scenes/admin/pages/NodeConfig/components/PluginsManagement/PluginsManagement.tsx index 0a4cc206e..b09a81f1c 100644 --- a/packages/app/src/scenes/admin/pages/NodeConfig/components/PluginsManagement/PluginsManagement.tsx +++ b/packages/app/src/scenes/admin/pages/NodeConfig/components/PluginsManagement/PluginsManagement.tsx @@ -22,9 +22,9 @@ const PluginsManagement: FC = () => { setError(undefined); - await pluginStore.mediaUploader.uploadPlugin(fileToUpload); + const hash = await pluginStore.mediaUploader.uploadPlugin(fileToUpload); - if (pluginStore.mediaUploader.isError) { + if (!hash) { setError('Error uploading plugin'); return; } @@ -73,7 +73,14 @@ const PluginsManagement: FC = () => { setError(err.message); }} fileType="archive" - /> + > + {fileToUpload && ( + <styled.FilePreview> + <div>{fileToUpload.name}</div> + <div>size: {(fileToUpload.size / (1024 * 1024)).toFixed(1)}MB</div> + </styled.FilePreview> + )} + </FileUploader> </div> {error && <styled.Error>{error}</styled.Error>} {!!fileToUpload && <Button label="Upload" onClick={handleUpload} />} From 18799672adab7d263373a8cb6e0b46404873be4e Mon Sep 17 00:00:00 2001 From: Dmitry Yudakov <dmitry.yudakov@gmail.com> Date: Fri, 20 Oct 2023 20:37:37 +0300 Subject: [PATCH 14/19] feat(node-admin): activate uploaded plugin --- .../nodeRepository/nodeRepository.api.endpoints.ts | 3 ++- .../repositories/nodeRepository/nodeRepository.api.ts | 5 +++++ .../nodeRepository/nodeRepository.api.types.ts | 4 ++++ .../PluginsManagement/PluginsManagement.tsx | 5 ++++- packages/app/src/stores/AdminStore/AdminStore.ts | 11 +++++++++++ 5 files changed, 26 insertions(+), 2 deletions(-) diff --git a/packages/app/src/api/repositories/nodeRepository/nodeRepository.api.endpoints.ts b/packages/app/src/api/repositories/nodeRepository/nodeRepository.api.endpoints.ts index f4fea825f..a4fceaccc 100644 --- a/packages/app/src/api/repositories/nodeRepository/nodeRepository.api.endpoints.ts +++ b/packages/app/src/api/repositories/nodeRepository/nodeRepository.api.endpoints.ts @@ -4,6 +4,7 @@ export const configRepositoryEndpoints = () => { return { getChallenge: `${BASE_URL}/get-challenge`, hostingAllowList: `${BASE_URL}/hosting-allow-list`, - hostingAllowListRemove: `${BASE_URL}/hosting-allow-list/:userId` + hostingAllowListRemove: `${BASE_URL}/hosting-allow-list/:userId`, + activatePlugin: `${BASE_URL}/activate-plugin` }; }; diff --git a/packages/app/src/api/repositories/nodeRepository/nodeRepository.api.ts b/packages/app/src/api/repositories/nodeRepository/nodeRepository.api.ts index 0bcc76f5b..702935228 100644 --- a/packages/app/src/api/repositories/nodeRepository/nodeRepository.api.ts +++ b/packages/app/src/api/repositories/nodeRepository/nodeRepository.api.ts @@ -4,6 +4,7 @@ import {request} from 'api/request'; import {RequestInterface} from 'api/interfaces'; import { + ActivatePluginRequest, AddToHostingAllowListRequest, GetHostingAllowListRequest, GetHostingAllowListResponse, @@ -46,3 +47,7 @@ export const removeFromHostingAllowList: RequestInterface< restOptions ); }; + +export const activatePlugin: RequestInterface<ActivatePluginRequest, null> = (options) => { + return request.post(configRepositoryEndpoints().activatePlugin, options); +}; diff --git a/packages/app/src/api/repositories/nodeRepository/nodeRepository.api.types.ts b/packages/app/src/api/repositories/nodeRepository/nodeRepository.api.types.ts index 5acf1c81c..628606d1a 100644 --- a/packages/app/src/api/repositories/nodeRepository/nodeRepository.api.types.ts +++ b/packages/app/src/api/repositories/nodeRepository/nodeRepository.api.types.ts @@ -25,3 +25,7 @@ export interface HostingAllowListItemInterface { export interface GetHostingAllowListRequest {} export interface GetHostingAllowListResponse extends Array<HostingAllowListItemInterface> {} + +export interface ActivatePluginRequest { + plugin_hash: string; +} diff --git a/packages/app/src/scenes/admin/pages/NodeConfig/components/PluginsManagement/PluginsManagement.tsx b/packages/app/src/scenes/admin/pages/NodeConfig/components/PluginsManagement/PluginsManagement.tsx index b09a81f1c..fce6f9d13 100644 --- a/packages/app/src/scenes/admin/pages/NodeConfig/components/PluginsManagement/PluginsManagement.tsx +++ b/packages/app/src/scenes/admin/pages/NodeConfig/components/PluginsManagement/PluginsManagement.tsx @@ -10,7 +10,7 @@ import * as styled from './PluginsManagement.styled'; const MAX_FILE_SIZE = 20_000_000; const PluginsManagement: FC = () => { - const {pluginStore} = useStore(); + const {pluginStore, adminStore} = useStore(); const [fileToUpload, setFileToUpload] = useState<File>(); const [error, setError] = useState<string>(); @@ -29,6 +29,9 @@ const PluginsManagement: FC = () => { return; } + console.log('Activate plugin with hash', hash); + await adminStore.activatePlugin(hash); + setFileToUpload(undefined); pluginStore.init(); diff --git a/packages/app/src/stores/AdminStore/AdminStore.ts b/packages/app/src/stores/AdminStore/AdminStore.ts index ab678fa6a..868954b6f 100644 --- a/packages/app/src/stores/AdminStore/AdminStore.ts +++ b/packages/app/src/stores/AdminStore/AdminStore.ts @@ -56,6 +56,17 @@ export const AdminStore = types return api.nodeRepository.removeFromHostingAllowList({user_id}); } })) + .actions((self) => ({ + activatePlugin: flow(function* (plugin_hash: string) { + const resp = yield api.nodeRepository.activatePlugin({plugin_hash}); + + if (resp.error) { + throw new Error(resp.error); + } + + return resp; + }) + })) .actions((self) => ({ fetchApiKeys: flow(function* () { const resp = yield Promise.all([ From 369711f093247ef3af034715db886f7e4cae1146 Mon Sep 17 00:00:00 2001 From: Dmitry Yudakov <dmitry.yudakov@gmail.com> Date: Mon, 23 Oct 2023 13:45:30 +0300 Subject: [PATCH 15/19] feat(sdk): support displayName in plugin manifest --- packages/sdk/bin/momentum-plugin.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/sdk/bin/momentum-plugin.js b/packages/sdk/bin/momentum-plugin.js index 21d98a30e..3d7261870 100755 --- a/packages/sdk/bin/momentum-plugin.js +++ b/packages/sdk/bin/momentum-plugin.js @@ -81,6 +81,7 @@ function spawnProcess(command, args, env) { function generateAndStoreManifest() { const { name, + displayName, version, description, author, @@ -92,6 +93,7 @@ function generateAndStoreManifest() { } = packageJSON; const manifest = { name, + displayName, version, description, author, From 5df5c75f04226aa349c7610fc1efa5dcc4ecac9b Mon Sep 17 00:00:00 2001 From: Dmitry Yudakov <dmitry.yudakov@gmail.com> Date: Mon, 23 Oct 2023 14:51:53 +0300 Subject: [PATCH 16/19] feat(plugins): option to fail silently for creator plugins --- .../scenes/widgets/pages/CreatorWidget/CreatorWidget.tsx | 1 + .../CreatorWidget/pages/PluginHolder/PluginHolder.tsx | 7 ++++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/app/src/scenes/widgets/pages/CreatorWidget/CreatorWidget.tsx b/packages/app/src/scenes/widgets/pages/CreatorWidget/CreatorWidget.tsx index bb1df39b1..11b201671 100644 --- a/packages/app/src/scenes/widgets/pages/CreatorWidget/CreatorWidget.tsx +++ b/packages/app/src/scenes/widgets/pages/CreatorWidget/CreatorWidget.tsx @@ -113,6 +113,7 @@ const CreatorWidget: FC = () => { return ( <PluginHolder key={pluginLoader.id} + failSilently pluginLoader={pluginLoader} onCreatorTabChanged={(data) => { console.log('onCreatorTabChanged:', data); diff --git a/packages/app/src/scenes/widgets/pages/CreatorWidget/pages/PluginHolder/PluginHolder.tsx b/packages/app/src/scenes/widgets/pages/CreatorWidget/pages/PluginHolder/PluginHolder.tsx index 970b96fce..db715c084 100644 --- a/packages/app/src/scenes/widgets/pages/CreatorWidget/pages/PluginHolder/PluginHolder.tsx +++ b/packages/app/src/scenes/widgets/pages/CreatorWidget/pages/PluginHolder/PluginHolder.tsx @@ -14,9 +14,10 @@ import {PluginLoaderModelType} from 'core/models'; interface PropsInterface { pluginLoader: PluginLoaderModelType; onCreatorTabChanged: (data: UsePluginHookReturnInterface['creatorTab']) => void; + failSilently?: boolean; } -const PluginHolder: FC<PropsInterface> = ({pluginLoader, onCreatorTabChanged}) => { +const PluginHolder: FC<PropsInterface> = ({pluginLoader, onCreatorTabChanged, failSilently}) => { useEffect(() => { console.log('PluginHolder'); return () => { @@ -44,7 +45,7 @@ const PluginHolder: FC<PropsInterface> = ({pluginLoader, onCreatorTabChanged}) = ); return ( - <ErrorBoundary errorMessage={t('errors.errorWhileLoadingPlugin')}> + <ErrorBoundary errorMessage={failSilently ? '' : t('errors.errorWhileLoadingPlugin')}> <ObjectGlobalPropsContextProvider props={pluginProps}> {pluginLoader.plugin ? ( <PluginInnerWrapper @@ -54,7 +55,7 @@ const PluginHolder: FC<PropsInterface> = ({pluginLoader, onCreatorTabChanged}) = /> ) : null} - {pluginLoader.isError && <div>{t('errors.errorWhileLoadingPlugin')}</div>} + {pluginLoader.isError && !failSilently && <div>{t('errors.errorWhileLoadingPlugin')}</div>} </ObjectGlobalPropsContextProvider> </ErrorBoundary> ); From 203f88bb4d0eee49afb9403b3174baa03e1d9dad Mon Sep 17 00:00:00 2001 From: Dmitry Yudakov <dmitry.yudakov@gmail.com> Date: Tue, 24 Oct 2023 18:55:10 +0300 Subject: [PATCH 17/19] feat(plugin): load plugins from BE, show plugin meta details --- .../app/src/core/utils/renderService.utils.ts | 14 +++ .../PluginsManagement.styled.ts | 35 ++++-- .../PluginsManagement/PluginsManagement.tsx | 107 ++++++++++++++++-- .../app/src/stores/PluginStore/PluginStore.ts | 49 +++++--- .../models/ObjectStore/ObjectStore.ts | 6 +- 5 files changed, 177 insertions(+), 34 deletions(-) diff --git a/packages/app/src/core/utils/renderService.utils.ts b/packages/app/src/core/utils/renderService.utils.ts index eb116ba13..013ad5bf8 100644 --- a/packages/app/src/core/utils/renderService.utils.ts +++ b/packages/app/src/core/utils/renderService.utils.ts @@ -43,3 +43,17 @@ export const getTrackAbsoluteUrl = (trackUrlOrHash: string | undefined | null): return null; }; + +export const getPluginAbsoluteUrl = (pluginUrlOrHash: string | undefined | null): string | null => { + const pluginServerUrl = `${appVariables.RENDER_SERVICE_URL}/plugin`; + // const pluginServerUrl = `http://localhost:4000/api/v4/media/render/plugin`; + if (pluginUrlOrHash) { + if (pluginUrlOrHash.startsWith('http')) { + return pluginUrlOrHash; + } else { + return `${pluginServerUrl}/${pluginUrlOrHash}/remoteEntry.js`; + } + } + + return null; +}; diff --git a/packages/app/src/scenes/admin/pages/NodeConfig/components/PluginsManagement/PluginsManagement.styled.ts b/packages/app/src/scenes/admin/pages/NodeConfig/components/PluginsManagement/PluginsManagement.styled.ts index 6d2b76b71..44508e404 100644 --- a/packages/app/src/scenes/admin/pages/NodeConfig/components/PluginsManagement/PluginsManagement.styled.ts +++ b/packages/app/src/scenes/admin/pages/NodeConfig/components/PluginsManagement/PluginsManagement.styled.ts @@ -15,12 +15,14 @@ export const List = styled.div` margin-bottom: 20px; `; +export const ExpandHolder = styled.div``; + export const Item = styled.div` margin: 10px 0 0 0; padding: 8px 10px; display: flex; - align-items: center; - justify-content: space-between; + align-items: flex-start; + justify-content: flex-start; width: 100%; background: ${(props) => props.theme.accentBg && rgba(props.theme.accentBg, 0.5)}; box-shadow: -1px -1px 2px ${(props) => props.theme.accentText && rgba(props.theme.accentText, 0.1)}; @@ -28,8 +30,29 @@ export const Item = styled.div` gap: 15px; `; -export const Date = styled.div` - font-size: var(--font-size-xs); +export const Date = styled.div``; + +export const Title = styled.div` + font-size: var(--font-size-l); + font-weight: 500; +`; + +export const Description = styled.div` + font-style: italic; + font-weight: 300; +`; + +export const LayoutCollapsed = styled.div` + display: grid; + grid-template-columns: 200px auto 50px; + gap: 10px; + width: 100%; +`; + +export const LayoutExpanded = styled.div` + display: grid; + grid-template-columns: 1fr 1fr; + gap: 5px; `; export const BottomPanel = styled.div` @@ -38,9 +61,7 @@ export const BottomPanel = styled.div` flex-direction: column; `; -export const Error = styled.div` - font-size: var(--font-size-xs); -`; +export const Error = styled.div``; export const FilePreview = styled.div` display: flex; diff --git a/packages/app/src/scenes/admin/pages/NodeConfig/components/PluginsManagement/PluginsManagement.tsx b/packages/app/src/scenes/admin/pages/NodeConfig/components/PluginsManagement/PluginsManagement.tsx index fce6f9d13..a4837f309 100644 --- a/packages/app/src/scenes/admin/pages/NodeConfig/components/PluginsManagement/PluginsManagement.tsx +++ b/packages/app/src/scenes/admin/pages/NodeConfig/components/PluginsManagement/PluginsManagement.tsx @@ -1,6 +1,6 @@ import {FC, useState} from 'react'; import {observer} from 'mobx-react-lite'; -import {Button, ErrorsEnum, FileUploader, Heading} from '@momentum-xyz/ui-kit'; +import {Button, ErrorsEnum, FileUploader, Heading, IconButton} from '@momentum-xyz/ui-kit'; import {i18n} from '@momentum-xyz/core'; import {useStore} from 'shared/hooks'; @@ -15,6 +15,8 @@ const PluginsManagement: FC = () => { const [fileToUpload, setFileToUpload] = useState<File>(); const [error, setError] = useState<string>(); + const [expandedPluginId, setExpandedPluginId] = useState<string>(); + const handleUpload = async () => { if (!fileToUpload) { return; @@ -43,12 +45,103 @@ const PluginsManagement: FC = () => { <styled.Container> <styled.List> <Heading variant="h3">Plugins</Heading> - {pluginStore.plugins?.map((plugin) => ( - <styled.Item key={plugin.plugin_id}> - <span>{plugin.meta.name}</span> - {/* <styled.Date>{plugin.updated_at?.slice(0, 19)}</styled.Date> */} - </styled.Item> - ))} + {pluginStore.plugins?.map(({plugin_id, meta, updated_at}) => { + const { + name, + displayName, + description, + version, + scopes, + author, + homepage, + repository, + license + } = meta || {}; + const isExpanded = expandedPluginId === plugin_id; + return ( + <styled.Item key={plugin_id}> + <styled.ExpandHolder> + <IconButton + name={isExpanded ? 'chevron_down' : 'chevron_right'} + isWhite + size="s" + onClick={() => { + setExpandedPluginId(isExpanded ? undefined : plugin_id); + }} + /> + </styled.ExpandHolder> + {!isExpanded && ( + <styled.LayoutCollapsed> + <styled.Title>{displayName || name}</styled.Title> + <styled.Description>{description?.slice(0, 100)}</styled.Description> + <span>{version}</span> + </styled.LayoutCollapsed> + )} + {isExpanded && ( + <div> + <styled.LayoutExpanded> + <styled.Title>{displayName || name}</styled.Title> + <span /> + + <span>Version:</span> + <span>{version}</span> + + {!!author && ( + <> + <span>Author:</span> + <span>{author}</span> + </> + )} + + {!!homepage && ( + <> + <span>Homepage:</span> + <a href={homepage} target="_blank" rel="noreferrer"> + {homepage} + </a> + </> + )} + + {!!repository && ( + <> + <span>Repository:</span> + <a href={repository} target="_blank" rel="noreferrer"> + {repository} + </a> + </> + )} + + {!!license && ( + <> + <span>License:</span> + <span>{license}</span> + </> + )} + + <span>Last modified:</span> + <span>{updated_at?.slice(0, 16)?.split('T')?.join(' ')}</span> + + {!!scopes && ( + <> + <span>Scopes:</span> + <ul> + {Object.entries<string[]>(scopes).map(([scope, value]) => ( + <li key={scope}> + {scope}: {value.join(', ')} + </li> + ))} + </ul> + </> + )} + </styled.LayoutExpanded> + + <br /> + <styled.Description>{description?.slice(0, 100)}</styled.Description> + </div> + )} + </styled.Item> + ); + })} </styled.List> <styled.BottomPanel> <Heading variant="h3">Upload plugin</Heading> diff --git a/packages/app/src/stores/PluginStore/PluginStore.ts b/packages/app/src/stores/PluginStore/PluginStore.ts index 59d2f5b92..124b16fa5 100644 --- a/packages/app/src/stores/PluginStore/PluginStore.ts +++ b/packages/app/src/stores/PluginStore/PluginStore.ts @@ -3,7 +3,7 @@ import {RequestModel} from '@momentum-xyz/core'; import {api} from 'api'; import {DynamicScriptList, MediaUploader, PluginAttributesManager, PluginLoader} from 'core/models'; -import {getRootStore} from 'core/utils'; +import {getPluginAbsoluteUrl, getRootStore} from 'core/utils'; interface PluginInfoInterface { plugin_id: string; @@ -13,18 +13,18 @@ interface PluginInfoInterface { updated_at?: string; } -const localPluginInfosByScopes = { - creatorTab: [ - { - plugin_id: '99c9a0ba-0c19-4ef5-a995-9bc3af39a0a5', - meta: { - name: 'plugin_odyssey_creator_openai', - scopeName: 'plugin_odyssey_creator_openai', - scriptUrl: 'http://localhost:3001/remoteEntry.js' - } - } - ] -}; +// const localPluginInfosByScopes = { +// creatorTab: [ +// { +// plugin_id: '99c9a0ba-0c19-4ef5-a995-9bc3af39a0a5', +// meta: { +// name: 'plugin_odyssey_creator_openai', +// scopeName: 'plugin_odyssey_creator_openai', +// scriptUrl: 'http://localhost:3001/remoteEntry.js' +// } +// } +// ] +// }; export const PluginStore = types .model('PluginStore', { @@ -57,10 +57,21 @@ export const PluginStore = types console.log('pluginsResponse', pluginsResponse); self._plugins = pluginsResponse || []; - // if (pluginsResponse) { - // self.pluginInfosByScopes = pluginsResponse.plugins; - self.pluginInfosByScopes = localPluginInfosByScopes; - // } + // self.pluginInfosByScopes = localPluginInfosByScopes; + if (pluginsResponse) { + const pluginInfosByScopes: Record<string, PluginInfoInterface[]> = {}; + for (const p of pluginsResponse) { + if (Array.isArray(p.meta?.scopes?.ui)) { + for (const scope of p.meta.scopes.ui) { + if (!pluginInfosByScopes[scope]) { + pluginInfosByScopes[scope] = []; + } + pluginInfosByScopes[scope].push(p); + } + } + } + self.pluginInfosByScopes = pluginInfosByScopes; + } }), storePluginLoadersByScope: (scope: string, pluginLoaders: any[]) => { self.pluginLoadersByScopes.set(scope, cast(pluginLoaders)); @@ -84,7 +95,9 @@ export const PluginStore = types console.log('create plugin ', {plugin_id, meta, options}); if (!self.dynamicScriptList.containsLoaderWithName(meta.scopeName)) { - await self.dynamicScriptList.addScript(meta.scopeName, meta.scriptUrl); + const scriptUrl = getPluginAbsoluteUrl(meta.scriptUrl)!; + console.log('Load plugin', plugin_id, 'from scriptUrl', scriptUrl); + await self.dynamicScriptList.addScript(meta.scopeName, scriptUrl); } const worldId = getRootStore(self).universeStore.worldId; diff --git a/packages/app/src/stores/UniverseStore/models/ObjectStore/ObjectStore.ts b/packages/app/src/stores/UniverseStore/models/ObjectStore/ObjectStore.ts index 8bd2a2607..46c8f4638 100644 --- a/packages/app/src/stores/UniverseStore/models/ObjectStore/ObjectStore.ts +++ b/packages/app/src/stores/UniverseStore/models/ObjectStore/ObjectStore.ts @@ -12,7 +12,7 @@ import { ObjectUserAttribute, User } from 'core/models'; -import {getRootStore} from 'core/utils'; +import {getPluginAbsoluteUrl, getRootStore} from 'core/utils'; import {ObjectContentStore} from './models'; @@ -80,7 +80,9 @@ const ObjectStore = types const {options, meta} = assetData; if (!self.dynamicScriptList.containsLoaderWithName(meta.scopeName)) { - yield self.dynamicScriptList.addScript(meta.scopeName, meta.scriptUrl); + const scriptUrl = getPluginAbsoluteUrl(meta.scriptUrl)!; + console.log('Load plugin', assetData, 'from scriptUrl', scriptUrl); + yield self.dynamicScriptList.addScript(meta.scopeName, scriptUrl); } self.pluginLoader = PluginLoader.create({ From db05fc1702150971ed0d35b559c86f8c818f9086 Mon Sep 17 00:00:00 2001 From: Dmitry Yudakov <dmitry.yudakov@gmail.com> Date: Wed, 25 Oct 2023 13:22:04 +0300 Subject: [PATCH 18/19] refactor(plugin): cleanup plugin store --- packages/app/src/stores/PluginStore/PluginStore.ts | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/packages/app/src/stores/PluginStore/PluginStore.ts b/packages/app/src/stores/PluginStore/PluginStore.ts index 124b16fa5..d4d4a7349 100644 --- a/packages/app/src/stores/PluginStore/PluginStore.ts +++ b/packages/app/src/stores/PluginStore/PluginStore.ts @@ -33,15 +33,9 @@ export const PluginStore = types types.frozen<Record<string, PluginInfoInterface[]>>({}), {} ), - // pluginsLoadersByIds: types.optional(types.frozen<Record<string, typeof PluginLoader>>({})), {}), pluginsLoadersByIds: types.map(PluginLoader), pluginLoadersByScopes: types.map(types.array(types.reference(PluginLoader))), - // pluginLoadersByScopes: types.optional( - // types.frozen<Record<string, typeof PluginLoader>>({}), - // {} - // ), - _plugins: types.optional(types.frozen<PluginInfoInterface[]>(), []), pluginsRequest: types.optional(RequestModel, {}), @@ -49,7 +43,6 @@ export const PluginStore = types }) .actions((self) => ({ init: flow(function* () { - // init: function () { const pluginsResponse = yield self.pluginsRequest.send( api.pluginsRepository.getPluginsList, {} @@ -118,9 +111,6 @@ export const PluginStore = types await pluginLoader.loadPlugin(); console.log('loaded plugin ', {plugin_id, meta, options}); - - // self.pluginsLoadersByIds.set(plugin_id, pluginLoader); - console.log('pluginLoader', pluginLoader); return pluginLoader; @@ -131,7 +121,6 @@ export const PluginStore = types }) ); const loadedPlugins = plugins.filter((plugin: any) => !!plugin); - // self.pluginLoadersByScopes.set(scope, cast(loadedPlugins)); self.storePluginLoadersByScope(scope, loadedPlugins); return loadedPlugins; From e90dd3afe05f7586fc3baccdd2f57038e7afd64b Mon Sep 17 00:00:00 2001 From: Dmitry Yudakov <dmitry.yudakov@gmail.com> Date: Wed, 25 Oct 2023 13:27:20 +0300 Subject: [PATCH 19/19] refactor(plugin): cleanup useWorld --- packages/sdk/src/hooks/useWorld.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/sdk/src/hooks/useWorld.ts b/packages/sdk/src/hooks/useWorld.ts index 4a9bbc51f..396b0bc34 100644 --- a/packages/sdk/src/hooks/useWorld.ts +++ b/packages/sdk/src/hooks/useWorld.ts @@ -1,8 +1,6 @@ import {useEffect, useMemo, useRef} from 'react'; -// import {Event3dEmitter} from '@momentum-xyz/core'; import {useObjectGlobalProps} from '../contexts/ObjectGlobalPropsContext'; -// import {AttributeNameEnum} from '../enums'; import {Transform, UseWorldPropsInterface, UseWorldReturnInterface} from '../interfaces'; import {useObject} from './useObject';