diff --git a/src/components/pin-networked-object-button.js b/src/components/pin-networked-object-button.js index 1dbe6ece49..fb41ff893c 100644 --- a/src/components/pin-networked-object-button.js +++ b/src/components/pin-networked-object-button.js @@ -42,7 +42,7 @@ AFRAME.registerComponent("pin-networked-object-button", { if (!NAF.utils.isMine(this.targetEl) && !NAF.utils.takeOwnership(this.targetEl)) return; const wasPinned = this.targetEl.components.pinnable && this.targetEl.components.pinnable.data.pinned; - this.targetEl.setAttribute("pinnable", "pinned", !wasPinned); + window.APP.pinningHelper.setPinned(this.targetEl, !wasPinned); if (!wasPinned) { this.el.sceneEl.systems["hubs-systems"].soundEffectsSystem.playSoundOneShot(SOUND_PIN); } diff --git a/src/components/pinnable.js b/src/components/pinnable.js index 9e8f2e943c..ded6194643 100644 --- a/src/components/pinnable.js +++ b/src/components/pinnable.js @@ -4,97 +4,84 @@ AFRAME.registerComponent("pinnable", { }, init() { - this._fireEventsAndAnimate = this._fireEventsAndAnimate.bind(this); + this._persist = this._persist.bind(this); + this._persistAndAnimate = this._persistAndAnimate.bind(this); - // Fire pinned events when media is refreshed since the version will be bumped. - this.el.addEventListener("media_refreshed", this._fireEventsAndAnimate); + // Persist when media is refreshed since the version will be bumped. + this.el.addEventListener("media_refreshed", this._persistAndAnimate); - // Fire pinned events when page changes so we can persist the page. - this.el.addEventListener("owned-pager-page-changed", () => { - this._fireEventsAndAnimate({ animate: false }); - }); + this.el.addEventListener("owned-pager-page-changed", this._persist); - // Fire pinned events when video state changes so we can persist the page. - this.el.addEventListener("owned-video-state-changed", this._fireEventsAndAnimate); + this.el.addEventListener("owned-video-state-changed", this._persistAndAnimate); }, - update(oldData) { - this._fireEventsAndAnimate({ oldData }); + update() { + this._animate(); }, - _fireEventsAndAnimate({ oldData = {}, force = false, animate = true }) { - // We need to guard against _fireEventsAndAnimate being called during entity initialization, - // when the networked component isn't initialized yet. - if (!this.el.components.networked || !this.el.components.networked.data) return; - - const isMine = NAF.utils.isMine(this.el); + _persistAndAnimate() { + this._persist(); + this._animate(); + }, - // Avoid firing events during initialization by checking if the pin state has changed before doing so. - const pinStateChanged = !!oldData.pinned !== this.data.pinned; + _persist() { + // Re-pin or unpin entity to reflect state changes. + window.APP.pinningHelper.setPinned(this.el, this.data.pinned); + }, - if (this.data.pinned) { - if (pinStateChanged || force) { - this.el.emit("pinned", { el: this.el }); - } + _isMine() { + return this.el.components.networked?.data && NAF.utils.isMine(this.el); + }, - const isAnimationRunning = - this.el.components["animation__pin-start"]?.animationIsPlaying || - this.el.components["animation__pin-end"]?.animationIsPlaying; - - if (isMine && animate && !isAnimationRunning) { - this.el.removeAttribute("animation__pin-start"); - this.el.removeAttribute("animation__pin-end"); - const currentScale = this.el.object3D.scale; - - this.el.setAttribute("animation__pin-start", { - property: "scale", - dur: 200, - from: { x: currentScale.x, y: currentScale.y, z: currentScale.z }, - to: { x: currentScale.x * 1.1, y: currentScale.y * 1.1, z: currentScale.z * 1.1 }, - easing: "easeOutElastic" - }); - - this.el.setAttribute("animation__pin-end", { - property: "scale", - delay: 200, - dur: 200, - from: { x: currentScale.x * 1.1, y: currentScale.y * 1.1, z: currentScale.z * 1.1 }, - to: { x: currentScale.x, y: currentScale.y, z: currentScale.z }, - easing: "easeOutElastic" - }); - - if (this.el.components["body-helper"] && !this.el.sceneEl.systems.interaction.isHeld(this.el)) { - this.el.setAttribute("body-helper", { type: "kinematic" }); - } - } - } else { - if (pinStateChanged || force) { - this.el.emit("unpinned", { el: this.el }); + _animate() { + const isAnimationRunning = + this.el.components["animation__pin-start"]?.animationIsPlaying || + this.el.components["animation__pin-end"]?.animationIsPlaying; + + if (this._isMine() && this.data.pinned && !isAnimationRunning) { + this.el.removeAttribute("animation__pin-start"); + this.el.removeAttribute("animation__pin-end"); + const currentScale = this.el.object3D.scale; + + this.el.setAttribute("animation__pin-start", { + property: "scale", + dur: 200, + from: { x: currentScale.x, y: currentScale.y, z: currentScale.z }, + to: { x: currentScale.x * 1.1, y: currentScale.y * 1.1, z: currentScale.z * 1.1 }, + easing: "easeOutElastic" + }); + + this.el.setAttribute("animation__pin-end", { + property: "scale", + delay: 200, + dur: 200, + from: { x: currentScale.x * 1.1, y: currentScale.y * 1.1, z: currentScale.z * 1.1 }, + to: { x: currentScale.x, y: currentScale.y, z: currentScale.z }, + easing: "easeOutElastic" + }); + + if (this.el.components["body-helper"] && !this.el.sceneEl.systems.interaction.isHeld(this.el)) { + this.el.setAttribute("body-helper", { type: "kinematic" }); } } }, - isHeld(el) { - const { leftHand, rightHand, rightRemote } = el.sceneEl.systems.interaction.state; - return leftHand.held === el || rightHand.held === el || rightRemote.held === el; - }, - tick() { - const held = this.isHeld(this.el); - const isMine = this.el.components.networked && this.el.components.networked.data && NAF.utils.isMine(this.el); + const isHeld = this.el.sceneEl.systems.interaction.isHeld(this.el); + const isMine = this._isMine(); let didFireThisFrame = false; - if (!held && this.wasHeld && isMine) { + if (!isHeld && this.wasHeld && isMine) { didFireThisFrame = true; - this._fireEventsAndAnimate({ oldData: this.data, force: true }); + this._persistAndAnimate(); } - this.wasHeld = held; + this.wasHeld = isHeld; this.transformObjectSystem = this.transformObjectSystem || AFRAME.scenes[0].systems["transform-selected-object"]; const transforming = this.transformObjectSystem.transforming && this.transformObjectSystem.target.el === this.el; if (!didFireThisFrame && !transforming && this.wasTransforming && isMine) { - this._fireEventsAndAnimate({ oldData: this.data, force: true }); + this._persistAndAnimate(); } this.wasTransforming = transforming; } diff --git a/src/components/tools/networked-drawing.js b/src/components/tools/networked-drawing.js index 32d3035376..4439e81368 100644 --- a/src/components/tools/networked-drawing.js +++ b/src/components/tools/networked-drawing.js @@ -764,7 +764,7 @@ AFRAME.registerComponent("deserialize-drawing-button", { addMeshScaleAnimation(drawingManager.drawing.el.object3DMap.mesh, { x: 0.001, y: 0.001, z: 0.001 }); if (this.targetEl.components.pinnable && this.targetEl.components.pinnable.data.pinned) { - this.targetEl.setAttribute("pinnable", "pinned", false); + window.APP.pinningHelper.setPinned(this.targetEl, false); } this.targetEl.parentEl.removeChild(this.targetEl); this.el.sceneEl.systems["hubs-systems"].soundEffectsSystem.playSoundOneShot(SOUND_PEN_START_DRAW); diff --git a/src/hub.js b/src/hub.js index f899d3dda3..996a825825 100644 --- a/src/hub.js +++ b/src/hub.js @@ -197,6 +197,7 @@ import "./gltf-component-mappings"; import { App } from "./App"; import MediaDevicesManager from "./utils/media-devices-manager"; +import PinningHelper from "./utils/pinning-helper"; import { sleep } from "./utils/async-utils"; import { platformUnsupported } from "./support"; @@ -804,6 +805,8 @@ document.addEventListener("DOMContentLoaded", async () => { }); }; + window.APP.pinningHelper = new PinningHelper(hubChannel, authChannel, store, performConditionalSignIn); + window.addEventListener("action_create_avatar", () => { performConditionalSignIn( () => hubChannel.signedIn, diff --git a/src/react-components/room/object-hooks.js b/src/react-components/room/object-hooks.js index fb63078772..7f95dc304a 100644 --- a/src/react-components/room/object-hooks.js +++ b/src/react-components/room/object-hooks.js @@ -39,8 +39,7 @@ export function usePinObject(hubChannel, scene, object) { () => { const el = object.el; if (!NAF.utils.isMine(el) && !NAF.utils.takeOwnership(el)) return; - el.setAttribute("pinnable", "pinned", true); - el.emit("pinned", { el }); + window.APP.pinningHelper.setPinned(el, true); }, [object] ); @@ -49,8 +48,7 @@ export function usePinObject(hubChannel, scene, object) { () => { const el = object.el; if (!NAF.utils.isMine(el) && !NAF.utils.takeOwnership(el)) return; - el.setAttribute("pinnable", "pinned", false); - el.emit("unpinned", { el }); + window.APP.pinningHelper.setPinned(el, false); }, [object] ); diff --git a/src/scene-entry-manager.js b/src/scene-entry-manager.js index e776541e86..101473905a 100644 --- a/src/scene-entry-manager.js +++ b/src/scene-entry-manager.js @@ -1,6 +1,5 @@ import qsTruthy from "./utils/qs_truthy"; import nextTick from "./utils/next-tick"; -import pinnedEntityToGltf from "./utils/pinned-entity-to-gltf"; import { hackyMobileSafariTest } from "./utils/detect-touchscreen"; import { isIOS as detectIOS } from "./utils/is-mobile"; import { SignInMessages } from "./react-components/auth/SignInModal"; @@ -12,7 +11,7 @@ const isMobileVR = AFRAME.utils.device.isMobileVR(); const isDebug = qsTruthy("debug"); const qs = new URLSearchParams(location.search); -import { addMedia, getPromotionTokenForFile } from "./utils/media-utils"; +import { addMedia } from "./utils/media-utils"; import { isIn2DInterstitial, handleExitTo2DInterstitial, @@ -189,7 +188,7 @@ export default class SceneEntryManager { if (entity.components.networked.data.persistent) { NAF.utils.takeOwnership(entity); - this._unpinElement(entity); + window.APP.pinningHelper.unpinElement(entity); entity.parentNode.removeChild(entity); } else { NAF.entities.removeEntity(id); @@ -208,74 +207,6 @@ export default class SceneEntryManager { }); }; - _pinElement = async el => { - const { networkId } = el.components.networked.data; - - const { fileId, src } = el.components["media-loader"].data; - - let fileAccessToken, promotionToken; - if (fileId) { - fileAccessToken = new URL(src).searchParams.get("token"); - const storedPromotionToken = getPromotionTokenForFile(fileId); - if (storedPromotionToken) { - promotionToken = storedPromotionToken.promotionToken; - } - } - - const gltfNode = pinnedEntityToGltf(el); - if (!gltfNode) return; - el.setAttribute("networked", { persistent: true }); - el.setAttribute("media-loader", { fileIsOwned: true }); - - try { - await this.hubChannel.pin(networkId, gltfNode, fileId, fileAccessToken, promotionToken); - this.store.update({ activity: { hasPinned: true } }); - } catch (e) { - if (e.reason === "invalid_token") { - await this.authChannel.signOut(this.hubChannel); - this._signInAndPinOrUnpinElement(el); - } else { - console.warn("Pin failed for unknown reason", e); - } - } - }; - - _signInAndPinOrUnpinElement = (el, pin) => { - const action = pin - ? () => this._pinElement(el) - : async () => { - await this._unpinElement(el); - }; - - this.performConditionalSignIn( - () => this.hubChannel.signedIn, - action, - pin ? SignInMessages.pin : SignInMessages.unpin, - () => { - // UI pins/un-pins the entity optimistically, so we undo that here. - // Note we have to disable the sign in flow here otherwise this will recurse. - this._disableSignInOnPinAction = true; - el.setAttribute("pinnable", "pinned", !pin); - this._disableSignInOnPinAction = false; - } - ); - }; - - _unpinElement = el => { - const components = el.components; - const networked = components.networked; - - if (!networked || !networked.data || !NAF.utils.isMine(el)) return; - - const networkId = components.networked.data.networkId; - el.setAttribute("networked", { persistent: false }); - - const mediaLoader = components["media-loader"]; - const fileId = mediaLoader.data && mediaLoader.data.fileId; - - this.hubChannel.unpin(networkId, fileId); - }; - _setupMedia = () => { const offset = { x: 0, y: 0, z: -1.5 }; const spawnMediaInfrontOfPlayer = (src, contentOrigin) => { @@ -305,18 +236,6 @@ export default class SceneEntryManager { spawnMediaInfrontOfPlayer(e.detail, contentOrigin); }); - const handlePinEvent = (e, pinned) => { - if (this._disableSignInOnPinAction) return; - const el = e.detail.el; - - if (NAF.utils.isMine(el)) { - this._signInAndPinOrUnpinElement(e.detail.el, pinned); - } - }; - - this.scene.addEventListener("pinned", e => handlePinEvent(e, true)); - this.scene.addEventListener("unpinned", e => handlePinEvent(e, false)); - this.scene.addEventListener("object_spawned", e => { this.hubChannel.sendObjectSpawnedEvent(e.detail.objectType); }); diff --git a/src/utils/media-url-utils.js b/src/utils/media-url-utils.js index 15faec5b14..416ee15104 100644 --- a/src/utils/media-url-utils.js +++ b/src/utils/media-url-utils.js @@ -3,7 +3,7 @@ import configs from "./configs"; const nonCorsProxyDomains = (configs.NON_CORS_PROXY_DOMAINS || "").split(","); if (configs.CORS_PROXY_SERVER) { - nonCorsProxyDomains.push(configs.CORS_PROXY_SERVER); + nonCorsProxyDomains.push(configs.CORS_PROXY_SERVER.split(":")[0]); } nonCorsProxyDomains.push(document.location.hostname); diff --git a/src/utils/pinning-helper.js b/src/utils/pinning-helper.js new file mode 100644 index 0000000000..a3e8d9dc7e --- /dev/null +++ b/src/utils/pinning-helper.js @@ -0,0 +1,96 @@ +import pinnedEntityToGltf from "./pinned-entity-to-gltf"; +import { getPromotionTokenForFile } from "./media-utils"; +import { SignInMessages } from "../react-components/auth/SignInModal"; + +export default class PinningHelper { + constructor(hubChannel, authChannel, store, performConditionalSignIn) { + this.hubChannel = hubChannel; + this.authChannel = authChannel; + this.store = store; + this.performConditionalSignIn = performConditionalSignIn; + } + + async setPinned(el, pin) { + if (NAF.utils.isMine(el)) { + this._signInAndPinOrUnpinElement(el, pin); + } else { + console.warn("PinningHelper: Attempted to set pin state on object that was not mine."); + } + } + + _signInAndPinOrUnpinElement = (el, pin) => { + const action = pin ? () => this._pinElement(el) : () => this.unpinElement(el); + + this.performConditionalSignIn( + () => this.hubChannel.signedIn, + action, + pin ? SignInMessages.pin : SignInMessages.unpin, + e => { + console.warn(`PinningHelper: Conditional sign-in failed. ${e}`); + } + ); + }; + + async _pinElement(el) { + const { networkId } = el.components.networked.data; + + const { fileId, src } = el.components["media-loader"].data; + let fileAccessToken, promotionToken; + if (fileId) { + fileAccessToken = new URL(src).searchParams.get("token"); + const storedPromotionToken = getPromotionTokenForFile(fileId); + if (storedPromotionToken) { + promotionToken = storedPromotionToken.promotionToken; + } + } + + const gltfNode = pinnedEntityToGltf(el); + if (!gltfNode) { + console.warn("PinningHelper: Entity did not produce a GLTF node."); + return; + } + el.setAttribute("networked", { persistent: true }); + el.setAttribute("media-loader", { fileIsOwned: true }); + + try { + await this.hubChannel.pin(networkId, gltfNode, fileId, fileAccessToken, promotionToken); + + // If we lost ownership of the entity while waiting for the pin to go through, + // try to regain ownership before setting the "pinned" state. + if (!NAF.utils.isMine(el) && !NAF.utils.takeOwnership(el)) { + console.warn("PinningHelper: Pinning succeeded, but ownership was lost in the mean time"); + } + + el.setAttribute("pinnable", "pinned", true); + el.emit("pinned", { el }); + this.store.update({ activity: { hasPinned: true } }); + } catch (e) { + if (e.reason === "invalid_token") { + await this.authChannel.signOut(this.hubChannel); + this._signInAndPinOrUnpinElement(el); + } else { + console.warn("PinningHelper: Pin failed for unknown reason", e); + } + } + } + + unpinElement(el) { + const components = el.components; + const networked = components.networked; + + if (!networked || !networked.data || !NAF.utils.isMine(el)) { + console.warn("PinningHelper: Tried to unpin element that is not networked or not mine."); + return; + } + + const networkId = components.networked.data.networkId; + el.setAttribute("networked", { persistent: false }); + + const mediaLoader = components["media-loader"]; + const fileId = mediaLoader.data && mediaLoader.data.fileId; + + this.hubChannel.unpin(networkId, fileId); + el.setAttribute("pinnable", "pinned", false); + el.emit("unpinned", { el }); + } +}