// // marketplaces.js // // Created by Eric Levin on 8 Jan 2016 // Copyright 2016 High Fidelity, Inc. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // /* global Tablet, Script, HMD, UserActivityLogger, Entities, Account, Wallet, ContextOverlay, Settings, Camera, Vec3, Quat, MyAvatar, Clipboard, Menu, Grid, Uuid, GlobalServices, openLoginWindow, getConnectionData, Overlays, SoundCache, DesktopPreviewProvider */ /* eslint indent: ["error", 4, { "outerIIFEBody": 0 }] */ var selectionDisplay = null; // for gridTool.js to ignore (function () { // BEGIN LOCAL_SCOPE var AppUi = Script.require('appUi'); Script.include("/~/system/libraries/gridTool.js"); Script.include("/~/system/libraries/connectionUtils.js"); var MARKETPLACE_CHECKOUT_QML_PATH = "hifi/commerce/checkout/Checkout.qml"; var MARKETPLACE_INSPECTIONCERTIFICATE_QML_PATH = "hifi/commerce/inspectionCertificate/InspectionCertificate.qml"; var MARKETPLACE_ITEM_TESTER_QML_PATH = "hifi/commerce/marketplaceItemTester/MarketplaceItemTester.qml"; var MARKETPLACE_PURCHASES_QML_PATH = "hifi/commerce/purchases/Purchases.qml"; var MARKETPLACE_WALLET_QML_PATH = "hifi/commerce/wallet/Wallet.qml"; var MARKETPLACES_INJECT_SCRIPT_URL = Script.resolvePath("../html/js/marketplacesInject.js"); var MARKETPLACES_URL = Script.resolvePath("../html/marketplaces.html"); var METAVERSE_SERVER_URL = Account.metaverseServerURL; var REZZING_SOUND = SoundCache.getSound(Script.resolvePath("../assets/sounds/rezzing.wav")); // Event bridge messages. var CLARA_IO_DOWNLOAD = "CLARA.IO DOWNLOAD"; var CLARA_IO_STATUS = "CLARA.IO STATUS"; var CLARA_IO_CANCEL_DOWNLOAD = "CLARA.IO CANCEL DOWNLOAD"; var CLARA_IO_CANCELLED_DOWNLOAD = "CLARA.IO CANCELLED DOWNLOAD"; var GOTO_DIRECTORY = "GOTO_DIRECTORY"; var QUERY_CAN_WRITE_ASSETS = "QUERY_CAN_WRITE_ASSETS"; var CAN_WRITE_ASSETS = "CAN_WRITE_ASSETS"; var WARN_USER_NO_PERMISSIONS = "WARN_USER_NO_PERMISSIONS"; var CLARA_DOWNLOAD_TITLE = "Preparing Download"; var messageBox = null; var isDownloadBeingCancelled = false; var CANCEL_BUTTON = 4194304; // QMessageBox::Cancel var NO_BUTTON = 0; // QMessageBox::NoButton var NO_PERMISSIONS_ERROR_MESSAGE = "Cannot download model because you can't write to \nthe domain's Asset Server."; var resourceRequestEvents = []; function signalResourceRequestEvent(data) { // Once we can tie resource request events to specific resources, // we will have to update the "0" in here. resourceObjectsInTest[0].resourceAccessEventText += "[" + data.date.toISOString() + "] " + data.url.toString().replace("__NONE__,", "") + "\n"; ui.tablet.sendToQml({ method: "resourceRequestEvent", data: data, resourceAccessEventText: resourceObjectsInTest[0].resourceAccessEventText }); } function onResourceRequestEvent(data) { // Once we can tie resource request events to specific resources, // we will have to update the "0" in here. if (resourceObjectsInTest[0] && resourceObjectsInTest[0].currentlyRecordingResources) { var resourceRequestEvent = { "date": new Date(), "url": data.url, "callerId": data.callerId, "extra": data.extra }; resourceRequestEvents.push(resourceRequestEvent); signalResourceRequestEvent(resourceRequestEvent); } } function onMessageBoxClosed(id, button) { if (id === messageBox && button === CANCEL_BUTTON) { isDownloadBeingCancelled = true; messageBox = null; ui.sendToHtml({ type: CLARA_IO_CANCEL_DOWNLOAD }); } } function onCanWriteAssetsChanged() { ui.sendToHtml({ type: CAN_WRITE_ASSETS, canWriteAssets: Entities.canWriteAssets() }); } var tabletShouldBeVisibleInSecondaryCamera = false; function setTabletVisibleInSecondaryCamera(visibleInSecondaryCam) { if (visibleInSecondaryCam) { // if we're potentially showing the tablet, only do so if it was visible before if (!tabletShouldBeVisibleInSecondaryCamera) { return; } } else { // if we're hiding the tablet, check to see if it was visible in the first place tabletShouldBeVisibleInSecondaryCamera = Overlays.getProperty(HMD.tabletID, "isVisibleInSecondaryCamera"); } Overlays.editOverlay(HMD.tabletID, { isVisibleInSecondaryCamera : visibleInSecondaryCam }); Overlays.editOverlay(HMD.homeButtonID, { isVisibleInSecondaryCamera : visibleInSecondaryCam }); Overlays.editOverlay(HMD.homeButtonHighlightID, { isVisibleInSecondaryCamera : visibleInSecondaryCam }); Overlays.editOverlay(HMD.tabletScreenID, { isVisibleInSecondaryCamera : visibleInSecondaryCam }); } function openWallet() { ui.open(MARKETPLACE_WALLET_QML_PATH); } function setupWallet(referrer) { // Needs to be done within the QML page in order to get access to QmlCommerce openWallet(); var ALLOWANCE_FOR_EVENT_BRIDGE_SETUP = 0; Script.setTimeout(function () { ui.tablet.sendToQml({ method: 'updateWalletReferrer', referrer: referrer }); }, ALLOWANCE_FOR_EVENT_BRIDGE_SETUP); } function onMarketplaceOpen(referrer) { var cta = referrer, match; if (Account.loggedIn && walletNeedsSetup()) { if (referrer === MARKETPLACE_URL_INITIAL) { setupWallet('marketplace cta'); } else { match = referrer.match(/\/item\/(\w+)$/); if (match && match[1]) { setupWallet(match[1]); } else if (referrer.indexOf(METAVERSE_SERVER_URL) === -1) { // not a url setupWallet(referrer); } else { print("WARNING: opening marketplace to", referrer, "without wallet setup."); } } } } function openMarketplace(optionalItemOrUrl) { // This is a bit of a kluge, but so is the whole file. // If given a whole path, use it with no cta. // If given an id, build the appropriate url and use the id as the cta. // Otherwise, use home and 'marketplace cta'. // AND... if call onMarketplaceOpen to setupWallet if we need to. var url = optionalItemOrUrl || MARKETPLACE_URL_INITIAL; // If optionalItemOrUrl contains the metaverse base, then it's a url, not an item id. if (optionalItemOrUrl && optionalItemOrUrl.indexOf(METAVERSE_SERVER_URL) === -1) { url = MARKETPLACE_URL + '/items/' + optionalItemOrUrl; } ui.open(url, MARKETPLACES_INJECT_SCRIPT_URL); } // Function Name: wireQmlEventBridge() // // Description: // -Used to connect/disconnect the script's response to the tablet's "fromQml" signal. Set the "on" argument to enable or // disable to event bridge. // // Relevant Variables: // -hasEventBridge: true/false depending on whether we've already connected the event bridge. var hasEventBridge = false; function wireQmlEventBridge(on) { if (!ui.tablet) { print("Warning in wireQmlEventBridge(): 'tablet' undefined!"); return; } if (on) { if (!hasEventBridge) { ui.tablet.fromQml.connect(onQmlMessageReceived); hasEventBridge = true; } } else { if (hasEventBridge) { ui.tablet.fromQml.disconnect(onQmlMessageReceived); hasEventBridge = false; } } } var contextOverlayEntity = ""; function openInspectionCertificateQML(currentEntityWithContextOverlay) { ui.open(MARKETPLACE_INSPECTIONCERTIFICATE_QML_PATH); contextOverlayEntity = currentEntityWithContextOverlay; } function setCertificateInfo(currentEntityWithContextOverlay, itemCertificateId) { var certificateId = itemCertificateId || (Entities.getEntityProperties(currentEntityWithContextOverlay, ['certificateID']).certificateID); ui.tablet.sendToQml({ method: 'inspectionCertificate_setCertificateId', entityId: currentEntityWithContextOverlay, certificateId: certificateId }); } function onUsernameChanged() { if (onMarketplaceScreen) { openMarketplace(); } } function walletNeedsSetup() { return Wallet.walletStatus === 1; } function sendCommerceSettings() { ui.sendToHtml({ type: "marketplaces", action: "commerceSetting", data: { commerceMode: Settings.getValue("commerce", true), userIsLoggedIn: Account.loggedIn, walletNeedsSetup: walletNeedsSetup(), metaverseServerURL: Account.metaverseServerURL, messagesWaiting: shouldShowDot } }); } // BEGIN AVATAR SELECTOR LOGIC var UNSELECTED_COLOR = { red: 0x1F, green: 0xC6, blue: 0xA6 }; var SELECTED_COLOR = { red: 0xF3, green: 0x91, blue: 0x29 }; var HOVER_COLOR = { red: 0xD0, green: 0xD0, blue: 0xD0 }; var overlays = {}; // Keeps track of all our extended overlay data objects, keyed by target identifier. function ExtendedOverlay(key, type, properties) { // A wrapper around overlays to store the key it is associated with. overlays[key] = this; this.key = key; this.selected = false; this.hovering = false; this.activeOverlay = Overlays.addOverlay(type, properties); // We could use different overlays for (un)selected... } // Instance methods: ExtendedOverlay.prototype.deleteOverlay = function () { // remove display and data of this overlay Overlays.deleteOverlay(this.activeOverlay); delete overlays[this.key]; }; ExtendedOverlay.prototype.editOverlay = function (properties) { // change display of this overlay Overlays.editOverlay(this.activeOverlay, properties); }; function color(selected, hovering) { var base = hovering ? HOVER_COLOR : selected ? SELECTED_COLOR : UNSELECTED_COLOR; function scale(component) { return component; } return { red: scale(base.red), green: scale(base.green), blue: scale(base.blue) }; } // so we don't have to traverse the overlays to get the last one var lastHoveringId = 0; ExtendedOverlay.prototype.hover = function (hovering) { this.hovering = hovering; if (this.key === lastHoveringId) { if (hovering) { return; } lastHoveringId = 0; } this.editOverlay({ color: color(this.selected, hovering) }); if (hovering) { // un-hover the last hovering overlay if (lastHoveringId && lastHoveringId !== this.key) { ExtendedOverlay.get(lastHoveringId).hover(false); } lastHoveringId = this.key; } }; ExtendedOverlay.prototype.select = function (selected) { if (this.selected === selected) { return; } this.editOverlay({ color: color(selected, this.hovering) }); this.selected = selected; }; // Class methods: var selectedId = false; ExtendedOverlay.isSelected = function (id) { return selectedId === id; }; ExtendedOverlay.get = function (key) { // answer the extended overlay data object associated with the given avatar identifier return overlays[key]; }; ExtendedOverlay.some = function (iterator) { // Bails early as soon as iterator returns truthy. var key; for (key in overlays) { if (iterator(ExtendedOverlay.get(key))) { return; } } }; ExtendedOverlay.unHover = function () { // calls hover(false) on lastHoveringId (if any) if (lastHoveringId) { ExtendedOverlay.get(lastHoveringId).hover(false); } }; // hit(overlay) on the one overlay intersected by pickRay, if any. // noHit() if no ExtendedOverlay was intersected (helps with hover) ExtendedOverlay.applyPickRay = function (pickRay, hit, noHit) { var pickedOverlay = Overlays.findRayIntersection(pickRay); // Depends on nearer coverOverlays to extend closer to us than farther ones. if (!pickedOverlay.intersects) { if (noHit) { return noHit(); } return; } ExtendedOverlay.some(function (overlay) { // See if pickedOverlay is one of ours. if ((overlay.activeOverlay) === pickedOverlay.overlayID) { hit(overlay); return true; } }); }; function addAvatarNode(id) { return new ExtendedOverlay(id, "sphere", { drawInFront: true, solid: true, alpha: 0.8, color: color(false, false), ignoreRayIntersection: false }); } var pingPong = true; function updateOverlays() { var eye = Camera.position; AvatarList.getAvatarIdentifiers().forEach(function (id) { if (!id) { return; // don't update ourself, or avatars we're not interested in } var avatar = AvatarList.getAvatar(id); if (!avatar) { return; // will be deleted below if there had been an overlay. } var overlay = ExtendedOverlay.get(id); if (!overlay) { // For now, we're treating this as a temporary loss, as from the personal space bubble. Add it back. overlay = addAvatarNode(id); } var target = avatar.position; var distance = Vec3.distance(target, eye); var offset = 0.2; var diff = Vec3.subtract(target, eye); // get diff between target and eye (a vector pointing to the eye from avatar position) var headIndex = avatar.getJointIndex("Head"); // base offset on 1/2 distance from hips to head if we can if (headIndex > 0) { offset = avatar.getAbsoluteJointTranslationInObjectFrame(headIndex).y / 2; } // move a bit in front, towards the camera target = Vec3.subtract(target, Vec3.multiply(Vec3.normalize(diff), offset)); // now bump it up a bit target.y = target.y + offset; overlay.ping = pingPong; overlay.editOverlay({ color: color(ExtendedOverlay.isSelected(id), overlay.hovering), position: target, dimensions: 0.032 * distance }); }); pingPong = !pingPong; ExtendedOverlay.some(function (overlay) { // Remove any that weren't updated. (User is gone.) if (overlay.ping === pingPong) { overlay.deleteOverlay(); } }); } function removeOverlays() { selectedId = false; lastHoveringId = 0; ExtendedOverlay.some(function (overlay) { overlay.deleteOverlay(); }); } // // Clicks. // function usernameFromIDReply(id, username, machineFingerprint, isAdmin) { if (selectedId === id) { var message = { method: 'updateSelectedRecipientUsername', userName: username === "" ? "unknown username" : username }; ui.tablet.sendToQml(message); } } function handleClick(pickRay) { ExtendedOverlay.applyPickRay(pickRay, function (overlay) { var nextSelectedStatus = !overlay.selected; var avatarId = overlay.key; selectedId = nextSelectedStatus ? avatarId : false; if (nextSelectedStatus) { Users.requestUsernameFromID(avatarId); } var message = { method: 'selectRecipient', id: avatarId, isSelected: nextSelectedStatus, displayName: '"' + AvatarList.getAvatar(avatarId).sessionDisplayName + '"', userName: '' }; ui.tablet.sendToQml(message); ExtendedOverlay.some(function (overlay) { var id = overlay.key; var selected = ExtendedOverlay.isSelected(id); overlay.select(selected); }); return true; }); } function handleMouseEvent(mousePressEvent) { // handleClick if we get one. if (!mousePressEvent.isLeftButton) { return; } handleClick(Camera.computePickRay(mousePressEvent.x, mousePressEvent.y)); } function handleMouseMove(pickRay) { // given the pickRay, just do the hover logic ExtendedOverlay.applyPickRay(pickRay, function (overlay) { overlay.hover(true); }, function () { ExtendedOverlay.unHover(); }); } // handy global to keep track of which hand is the mouse (if any) var currentHandPressed = 0; var TRIGGER_CLICK_THRESHOLD = 0.85; var TRIGGER_PRESS_THRESHOLD = 0.05; function handleMouseMoveEvent(event) { // find out which overlay (if any) is over the mouse position var pickRay; if (HMD.active) { if (currentHandPressed !== 0) { pickRay = controllerComputePickRay(currentHandPressed); } else { // nothing should hover, so ExtendedOverlay.unHover(); return; } } else { pickRay = Camera.computePickRay(event.x, event.y); } handleMouseMove(pickRay); } function handleTriggerPressed(hand, value) { // The idea is if you press one trigger, it is the one // we will consider the mouse. Even if the other is pressed, // we ignore it until this one is no longer pressed. var isPressed = value > TRIGGER_PRESS_THRESHOLD; if (currentHandPressed === 0) { currentHandPressed = isPressed ? hand : 0; return; } if (currentHandPressed === hand) { currentHandPressed = isPressed ? hand : 0; return; } // otherwise, the other hand is still triggered // so do nothing. } // We get mouseMoveEvents from the handControllers, via handControllerPointer. // But we don't get mousePressEvents. var triggerMapping = Controller.newMapping(Script.resolvePath('') + '-click'); var triggerPressMapping = Controller.newMapping(Script.resolvePath('') + '-press'); function controllerComputePickRay(hand) { var controllerPose = getControllerWorldLocation(hand, true); if (controllerPose.valid) { return { origin: controllerPose.position, direction: Quat.getUp(controllerPose.orientation) }; } } function makeClickHandler(hand) { return function (clicked) { if (clicked > TRIGGER_CLICK_THRESHOLD) { var pickRay = controllerComputePickRay(hand); handleClick(pickRay); } }; } function makePressHandler(hand) { return function (value) { handleTriggerPressed(hand, value); }; } triggerMapping.from(Controller.Standard.RTClick).peek().to(makeClickHandler(Controller.Standard.RightHand)); triggerMapping.from(Controller.Standard.LTClick).peek().to(makeClickHandler(Controller.Standard.LeftHand)); triggerPressMapping.from(Controller.Standard.RT).peek().to(makePressHandler(Controller.Standard.RightHand)); triggerPressMapping.from(Controller.Standard.LT).peek().to(makePressHandler(Controller.Standard.LeftHand)); // END AVATAR SELECTOR LOGIC var grid = new Grid(); function adjustPositionPerBoundingBox(position, direction, registration, dimensions, orientation) { // Adjust the position such that the bounding box (registration, dimenions, and orientation) lies behind the original // position in the given direction. var CORNERS = [ { x: 0, y: 0, z: 0 }, { x: 0, y: 0, z: 1 }, { x: 0, y: 1, z: 0 }, { x: 0, y: 1, z: 1 }, { x: 1, y: 0, z: 0 }, { x: 1, y: 0, z: 1 }, { x: 1, y: 1, z: 0 }, { x: 1, y: 1, z: 1 } ]; // Go through all corners and find least (most negative) distance in front of position. var distance = 0; for (var i = 0, length = CORNERS.length; i < length; i++) { var cornerVector = Vec3.multiplyQbyV(orientation, Vec3.multiplyVbyV(Vec3.subtract(CORNERS[i], registration), dimensions)); var cornerDistance = Vec3.dot(cornerVector, direction); distance = Math.min(cornerDistance, distance); } position = Vec3.sum(Vec3.multiply(distance, direction), position); return position; } var HALF_TREE_SCALE = 16384; function getPositionToCreateEntity(extra) { var CREATE_DISTANCE = 2; var position; var delta = extra !== undefined ? extra : 0; if (Camera.mode === "entity" || Camera.mode === "independent") { position = Vec3.sum(Camera.position, Vec3.multiply(Quat.getForward(Camera.orientation), CREATE_DISTANCE + delta)); } else { position = Vec3.sum(MyAvatar.position, Vec3.multiply(Quat.getForward(MyAvatar.orientation), CREATE_DISTANCE + delta)); position.y += 0.5; } if (position.x > HALF_TREE_SCALE || position.y > HALF_TREE_SCALE || position.z > HALF_TREE_SCALE) { return null; } return position; } function defaultFor(arg, val) { return typeof arg !== 'undefined' ? arg : val; } function rezEntity(itemHref, itemType, marketplaceItemTesterId) { var isWearable = itemType === "wearable"; print("!!!!! Clipboard.importEntities " + marketplaceItemTesterId); var success = Clipboard.importEntities(itemHref, true, marketplaceItemTesterId); var wearableLocalPosition = null; var wearableLocalRotation = null; var wearableLocalDimensions = null; var wearableDimensions = null; marketplaceItemTesterId = defaultFor(marketplaceItemTesterId, -1); if (itemType === "contentSet") { console.log("Item is a content set; codepath shouldn't go here."); return; } if (isWearable) { var wearableTransforms = Settings.getValue("io.highfidelity.avatarStore.checkOut.transforms"); if (!wearableTransforms) { // TODO delete this clause wearableTransforms = Settings.getValue("io.highfidelity.avatarStore.checkOut.tranforms"); } var certPos = itemHref.search("certificate_id="); // TODO how do I parse a URL from here? if (certPos >= 0) { certPos += 15; // length of "certificate_id=" var certURLEncoded = itemHref.substring(certPos); var certB64Encoded = decodeURIComponent(certURLEncoded); for (var key in wearableTransforms) { if (wearableTransforms.hasOwnProperty(key)) { var certificateTransforms = wearableTransforms[key].certificateTransforms; if (certificateTransforms) { for (var certID in certificateTransforms) { if (certificateTransforms.hasOwnProperty(certID) && certID == certB64Encoded) { var certificateTransform = certificateTransforms[certID]; wearableLocalPosition = certificateTransform.localPosition; wearableLocalRotation = certificateTransform.localRotation; wearableLocalDimensions = certificateTransform.localDimensions; wearableDimensions = certificateTransform.dimensions; } } } } } } } if (success) { var VERY_LARGE = 10000; var isLargeImport = Clipboard.getClipboardContentsLargestDimension() >= VERY_LARGE; var position = Vec3.ZERO; if (!isLargeImport) { position = getPositionToCreateEntity(Clipboard.getClipboardContentsLargestDimension() / 2); } if (position !== null && position !== undefined) { var pastedEntityIDs = Clipboard.pasteEntities(position); if (!isLargeImport) { // The first entity in Clipboard gets the specified position with the rest being relative to it. Therefore, move // entities after they're imported so that they're all the correct distance in front of and with geometric mean // centered on the avatar/camera direction. var deltaPosition = Vec3.ZERO; var entityPositions = []; var entityParentIDs = []; var propType = Entities.getEntityProperties(pastedEntityIDs[0], ["type"]).type; var NO_ADJUST_ENTITY_TYPES = ["Zone", "Light", "ParticleEffect"]; if (NO_ADJUST_ENTITY_TYPES.indexOf(propType) === -1) { var targetDirection; if (Camera.mode === "entity" || Camera.mode === "independent") { targetDirection = Camera.orientation; } else { targetDirection = MyAvatar.orientation; } targetDirection = Vec3.multiplyQbyV(targetDirection, Vec3.UNIT_Z); var targetPosition = getPositionToCreateEntity(); var deltaParallel = HALF_TREE_SCALE; // Distance to move entities parallel to targetDirection. var deltaPerpendicular = Vec3.ZERO; // Distance to move entities perpendicular to targetDirection. for (var i = 0, length = pastedEntityIDs.length; i < length; i++) { var curLoopEntityProps = Entities.getEntityProperties(pastedEntityIDs[i], ["position", "dimensions", "registrationPoint", "rotation", "parentID"]); var adjustedPosition = adjustPositionPerBoundingBox(targetPosition, targetDirection, curLoopEntityProps.registrationPoint, curLoopEntityProps.dimensions, curLoopEntityProps.rotation); var delta = Vec3.subtract(adjustedPosition, curLoopEntityProps.position); var distance = Vec3.dot(delta, targetDirection); deltaParallel = Math.min(distance, deltaParallel); deltaPerpendicular = Vec3.sum(Vec3.subtract(delta, Vec3.multiply(distance, targetDirection)), deltaPerpendicular); entityPositions[i] = curLoopEntityProps.position; entityParentIDs[i] = curLoopEntityProps.parentID; } deltaPerpendicular = Vec3.multiply(1 / pastedEntityIDs.length, deltaPerpendicular); deltaPosition = Vec3.sum(Vec3.multiply(deltaParallel, targetDirection), deltaPerpendicular); } if (grid.getSnapToGrid()) { var firstEntityProps = Entities.getEntityProperties(pastedEntityIDs[0], ["position", "dimensions", "registrationPoint"]); var positionPreSnap = Vec3.sum(deltaPosition, firstEntityProps.position); position = grid.snapToSurface(grid.snapToGrid(positionPreSnap, false, firstEntityProps.dimensions, firstEntityProps.registrationPoint), firstEntityProps.dimensions, firstEntityProps.registrationPoint); deltaPosition = Vec3.subtract(position, firstEntityProps.position); } if (!Vec3.equal(deltaPosition, Vec3.ZERO)) { for (var editEntityIndex = 0, numEntities = pastedEntityIDs.length; editEntityIndex < numEntities; editEntityIndex++) { if (Uuid.isNull(entityParentIDs[editEntityIndex])) { Entities.editEntity(pastedEntityIDs[editEntityIndex], { position: Vec3.sum(deltaPosition, entityPositions[editEntityIndex]) }); } } } } if (isWearable) { // apply the relative offsets saved during checkout var offsets = {}; if (wearableLocalPosition) { offsets.localPosition = wearableLocalPosition; } if (wearableLocalRotation) { offsets.localRotation = wearableLocalRotation; } if (wearableLocalDimensions) { offsets.localDimensions = wearableLocalDimensions; } else if (wearableDimensions) { offsets.dimensions = wearableDimensions; } // we currently assume a wearable is a single entity Entities.editEntity(pastedEntityIDs[0], offsets); } var rezPosition = Entities.getEntityProperties(pastedEntityIDs[0], "position").position; Audio.playSound(REZZING_SOUND, { volume: 1.0, position: rezPosition, localOnly: true }); } else { Window.notifyEditError("Can't import entities: entities would be out of bounds."); } } else { Window.notifyEditError("There was an error importing the entity file."); } } var referrerURL; // Used for updating Purchases QML var filterText; // Used for updating Purchases QML function onWebEventReceived(message) { message = JSON.parse(message); if (message.type === GOTO_DIRECTORY) { // This is the chooser between marketplaces. Only OUR markteplace // requires/makes-use-of wallet, so doesn't go through openMarketplace bottleneck. ui.open(MARKETPLACES_URL, MARKETPLACES_INJECT_SCRIPT_URL); } else if (message.type === QUERY_CAN_WRITE_ASSETS) { ui.sendToHtml(CAN_WRITE_ASSETS + " " + Entities.canWriteAssets()); } else if (message.type === WARN_USER_NO_PERMISSIONS) { Window.alert(NO_PERMISSIONS_ERROR_MESSAGE); } else if (message.type === CLARA_IO_STATUS) { if (isDownloadBeingCancelled) { return; } var text = message.status; if (messageBox === null) { messageBox = Window.openMessageBox(CLARA_DOWNLOAD_TITLE, text, CANCEL_BUTTON, NO_BUTTON); } else { Window.updateMessageBox(messageBox, CLARA_DOWNLOAD_TITLE, text, CANCEL_BUTTON, NO_BUTTON); } return; } else if (message.type === CLARA_IO_DOWNLOAD) { if (messageBox !== null) { Window.closeMessageBox(messageBox); messageBox = null; } return; } else if (message.type === CLARA_IO_CANCELLED_DOWNLOAD) { isDownloadBeingCancelled = false; } else if (message.type === "CHECKOUT") { wireQmlEventBridge(true); ui.open(MARKETPLACE_CHECKOUT_QML_PATH); ui.tablet.sendToQml({ method: 'updateCheckoutQML', params: message }); } else if (message.type === "REQUEST_SETTING") { sendCommerceSettings(); } else if (message.type === "PURCHASES") { referrerURL = message.referrerURL; filterText = ""; ui.open(MARKETPLACE_PURCHASES_QML_PATH); } else if (message.type === "LOGIN") { openLoginWindow(); } else if (message.type === "WALLET_SETUP") { setupWallet('marketplace cta'); } else if (message.type === "MY_ITEMS") { referrerURL = MARKETPLACE_URL_INITIAL; filterText = ""; ui.open(MARKETPLACE_PURCHASES_QML_PATH); wireQmlEventBridge(true); ui.tablet.sendToQml({ method: 'purchases_showMyItems' }); } } var sendAssetRecipient; var sendAssetParticleEffectUpdateTimer; var particleEffectTimestamp; var sendAssetParticleEffect; var SEND_ASSET_PARTICLE_TIMER_UPDATE = 250; var SEND_ASSET_PARTICLE_EMITTING_DURATION = 3000; var SEND_ASSET_PARTICLE_LIFETIME_SECONDS = 8; var SEND_ASSET_PARTICLE_PROPERTIES = { accelerationSpread: { x: 0, y: 0, z: 0 }, alpha: 1, alphaFinish: 1, alphaSpread: 0, alphaStart: 1, azimuthFinish: 0, azimuthStart: -6, color: { red: 255, green: 222, blue: 255 }, colorFinish: { red: 255, green: 229, blue: 225 }, colorSpread: { red: 0, green: 0, blue: 0 }, colorStart: { red: 243, green: 255, blue: 255 }, emitAcceleration: { x: 0, y: 0, z: 0 }, // Immediately gets updated to be accurate emitDimensions: { x: 0, y: 0, z: 0 }, emitOrientation: { x: 0, y: 0, z: 0 }, emitRate: 4, emitSpeed: 2.1, emitterShouldTrail: true, isEmitting: 1, lifespan: SEND_ASSET_PARTICLE_LIFETIME_SECONDS + 1, // Immediately gets updated to be accurate lifetime: SEND_ASSET_PARTICLE_LIFETIME_SECONDS + 1, maxParticles: 20, name: 'asset-particles', particleRadius: 0.2, polarFinish: 0, polarStart: 0, radiusFinish: 0.05, radiusSpread: 0, radiusStart: 0.2, speedSpread: 0, textures: "http://hifi-content.s3.amazonaws.com/alan/dev/Particles/Bokeh-Particle-HFC.png", type: 'ParticleEffect' }; function updateSendAssetParticleEffect() { var timestampNow = Date.now(); if ((timestampNow - particleEffectTimestamp) > (SEND_ASSET_PARTICLE_LIFETIME_SECONDS * 1000)) { deleteSendAssetParticleEffect(); return; } else if ((timestampNow - particleEffectTimestamp) > SEND_ASSET_PARTICLE_EMITTING_DURATION) { Entities.editEntity(sendAssetParticleEffect, { isEmitting: 0 }); } else if (sendAssetParticleEffect) { var recipientPosition = AvatarList.getAvatar(sendAssetRecipient).position; var distance = Vec3.distance(recipientPosition, MyAvatar.position); var accel = Vec3.subtract(recipientPosition, MyAvatar.position); accel.y -= 3.0; var life = Math.sqrt(2 * distance / Vec3.length(accel)); Entities.editEntity(sendAssetParticleEffect, { emitAcceleration: accel, lifespan: life }); } } function deleteSendAssetParticleEffect() { if (sendAssetParticleEffectUpdateTimer) { Script.clearInterval(sendAssetParticleEffectUpdateTimer); sendAssetParticleEffectUpdateTimer = null; } if (sendAssetParticleEffect) { sendAssetParticleEffect = Entities.deleteEntity(sendAssetParticleEffect); } sendAssetRecipient = null; } var savedDisablePreviewOption = Menu.isOptionChecked("Disable Preview"); var UI_FADE_TIMEOUT_MS = 150; function maybeEnableHMDPreview() { // Set a small timeout to prevent sensitive data from being shown during UI fade Script.setTimeout(function () { setTabletVisibleInSecondaryCamera(true); DesktopPreviewProvider.setPreviewDisabledReason("USER"); Menu.setIsOptionChecked("Disable Preview", savedDisablePreviewOption); }, UI_FADE_TIMEOUT_MS); } var resourceObjectsInTest = []; function signalNewResourceObjectInTest(resourceObject) { ui.tablet.sendToQml({ method: "newResourceObjectInTest", resourceObject: resourceObject }); } var onQmlMessageReceived = function onQmlMessageReceived(message) { if (message.messageSrc === "HTML") { return; } switch (message.method) { case 'gotoBank': ui.close(); if (Account.metaverseServerURL.indexOf("staging") >= 0) { Window.location = "hifi://hifiqa-master-metaverse-staging"; // So that we can test in staging. } else { Window.location = "hifi://BankOfHighFidelity"; } break; case 'purchases_openWallet': case 'checkout_openWallet': case 'checkout_setUpClicked': openWallet(); break; case 'purchases_walletNotSetUp': wireQmlEventBridge(true); ui.tablet.sendToQml({ method: 'updateWalletReferrer', referrer: "purchases" }); openWallet(); break; case 'checkout_walletNotSetUp': wireQmlEventBridge(true); ui.tablet.sendToQml({ method: 'updateWalletReferrer', referrer: message.referrer === "itemPage" ? message.itemId : message.referrer }); openWallet(); break; case 'checkout_cancelClicked': openMarketplace(message.params); break; case 'header_goToPurchases': case 'checkout_goToPurchases': referrerURL = MARKETPLACE_URL_INITIAL; filterText = message.filterText; ui.open(MARKETPLACE_PURCHASES_QML_PATH); break; case 'checkout_itemLinkClicked': openMarketplace(message.itemId); break; case 'checkout_continueShopping': openMarketplace(); break; case 'purchases_itemInfoClicked': var itemId = message.itemId; if (itemId && itemId !== "") { openMarketplace(itemId); } break; case 'checkout_rezClicked': case 'purchases_rezClicked': case 'tester_rezClicked': print("!!!!! marketplaces tester_rezClicked"); rezEntity(message.itemHref, message.itemType, message.itemId); break; case 'tester_newResourceObject': var resourceObject = message.resourceObject; resourceObjectsInTest = []; // REMOVE THIS once we support specific referrers resourceObject.currentlyRecordingResources = false; resourceObject.resourceAccessEventText = ""; resourceObjectsInTest[resourceObject.resourceObjectId] = resourceObject; signalNewResourceObjectInTest(resourceObject); break; case 'tester_updateResourceObjectAssetType': resourceObjectsInTest[message.objectId].assetType = message.assetType; break; case 'tester_deleteResourceObject': delete resourceObjectsInTest[message.objectId]; break; case 'tester_updateResourceRecordingStatus': resourceObjectsInTest[message.objectId].currentlyRecordingResources = message.status; if (message.status) { resourceObjectsInTest[message.objectId].resourceAccessEventText = ""; } break; case 'header_marketplaceImageClicked': case 'purchases_backClicked': openMarketplace(message.referrerURL); break; case 'purchases_goToMarketplaceClicked': openMarketplace(); break; case 'updateItemClicked': openMarketplace(message.upgradeUrl + "?edition=" + message.itemEdition); break; case 'giftAsset': break; case 'passphrasePopup_cancelClicked': case 'needsLogIn_cancelClicked': // Should/must NOT check for wallet setup. ui.open(MARKETPLACE_URL_INITIAL, MARKETPLACES_INJECT_SCRIPT_URL); break; case 'needsLogIn_loginClicked': openLoginWindow(); break; case 'disableHmdPreview': if (!savedDisablePreviewOption) { DesktopPreviewProvider.setPreviewDisabledReason("SECURE_SCREEN"); Menu.setIsOptionChecked("Disable Preview", true); setTabletVisibleInSecondaryCamera(false); } break; case 'maybeEnableHmdPreview': maybeEnableHMDPreview(); break; case 'purchases_openGoTo': ui.open("hifi/tablet/TabletAddressDialog.qml"); break; case 'purchases_itemCertificateClicked': contextOverlayEntity = ""; setCertificateInfo(contextOverlayEntity, message.itemCertificateId); break; case 'inspectionCertificate_closeClicked': ui.close(); break; case 'inspectionCertificate_requestOwnershipVerification': ContextOverlay.requestOwnershipVerification(message.entity); break; case 'inspectionCertificate_showInMarketplaceClicked': openMarketplace(message.marketplaceUrl); break; case 'header_myItemsClicked': referrerURL = MARKETPLACE_URL_INITIAL; filterText = ""; ui.open(MARKETPLACE_PURCHASES_QML_PATH); wireQmlEventBridge(true); ui.tablet.sendToQml({ method: 'purchases_showMyItems' }); break; case 'refreshConnections': // Guard to prevent this code from being executed while sending money -- // we only want to execute this while sending non-HFC gifts if (!onWalletScreen) { print('Refreshing Connections...'); getConnectionData(false); } break; case 'enable_ChooseRecipientNearbyMode': // Guard to prevent this code from being executed while sending money -- // we only want to execute this while sending non-HFC gifts if (!onWalletScreen) { if (!isUpdateOverlaysWired) { Script.update.connect(updateOverlays); isUpdateOverlaysWired = true; } } break; case 'disable_ChooseRecipientNearbyMode': // Guard to prevent this code from being executed while sending money -- // we only want to execute this while sending non-HFC gifts if (!onWalletScreen) { if (isUpdateOverlaysWired) { Script.update.disconnect(updateOverlays); isUpdateOverlaysWired = false; } removeOverlays(); } break; case 'purchases_availableUpdatesReceived': shouldShowDot = message.numUpdates > 0; ui.messagesWaiting(shouldShowDot && !ui.isOpen); break; case 'purchases_updateWearables': var currentlyWornWearables = []; var ATTACHMENT_SEARCH_RADIUS = 100; // meters (just in case) var nearbyEntities = Entities.findEntitiesByType('Model', MyAvatar.position, ATTACHMENT_SEARCH_RADIUS); for (var i = 0; i < nearbyEntities.length; i++) { var currentProperties = Entities.getEntityProperties( nearbyEntities[i], ['certificateID', 'editionNumber', 'parentID'] ); if (currentProperties.parentID === MyAvatar.sessionUUID) { currentlyWornWearables.push({ entityID: nearbyEntities[i], entityCertID: currentProperties.certificateID, entityEdition: currentProperties.editionNumber }); } } ui.tablet.sendToQml({ method: 'updateWearables', wornWearables: currentlyWornWearables }); break; case 'sendAsset_sendPublicly': if (message.assetName !== "") { deleteSendAssetParticleEffect(); sendAssetRecipient = message.recipient; var props = SEND_ASSET_PARTICLE_PROPERTIES; props.parentID = MyAvatar.sessionUUID; props.position = MyAvatar.position; props.position.y += 0.2; if (message.effectImage) { props.textures = message.effectImage; } sendAssetParticleEffect = Entities.addEntity(props, true); particleEffectTimestamp = Date.now(); updateSendAssetParticleEffect(); sendAssetParticleEffectUpdateTimer = Script.setInterval(updateSendAssetParticleEffect, SEND_ASSET_PARTICLE_TIMER_UPDATE); } break; case 'http.request': // Handled elsewhere, don't log. break; case 'goToPurchases_fromWalletHome': // HRS FIXME What's this about? break; default: print('Unrecognized message from Checkout.qml or Purchases.qml: ' + JSON.stringify(message)); } }; function pushResourceObjectsInTest() { var maxResourceObjectId = -1; var length = resourceObjectsInTest.length; for (var i = 0; i < length; i++) { if (i in resourceObjectsInTest) { signalNewResourceObjectInTest(resourceObjectsInTest[i]); var resourceObjectId = resourceObjectsInTest[i].resourceObjectId; maxResourceObjectId = (maxResourceObjectId < resourceObjectId) ? parseInt(resourceObjectId) : maxResourceObjectId; } } // N.B. Thinking about removing the following sendToQml? Be sure // that the marketplace item tester QML has heard from us, at least // so that it can indicate to the user that all of the resoruce // objects in test have been transmitted to it. //ui.tablet.sendToQml({ method: "nextObjectIdInTest", id: maxResourceObjectId + 1 }); // Since, for now, we only support 1 object in test, always send id: 0 ui.tablet.sendToQml({ method: "nextObjectIdInTest", id: 0 }); } // Function Name: onTabletScreenChanged() // // Description: // -Called when the TabletScriptingInterface::screenChanged() signal is emitted. The "type" argument can be either the string // value of "Home", "Web", "Menu", "QML", or "Closed". The "url" argument is only valid for Web and QML. var onCommerceScreen = false; var onInspectionCertificateScreen = false; var onMarketplaceItemTesterScreen = false; var onMarketplaceScreen = false; var onWalletScreen = false; var onTabletScreenChanged = function onTabletScreenChanged(type, url) { ui.setCurrentVisibleScreenMetadata(type, url); onMarketplaceScreen = type === "Web" && url.indexOf(MARKETPLACE_URL) !== -1; onInspectionCertificateScreen = type === "QML" && url.indexOf(MARKETPLACE_INSPECTIONCERTIFICATE_QML_PATH) !== -1; var onWalletScreenNow = url.indexOf(MARKETPLACE_WALLET_QML_PATH) !== -1; var onCommerceScreenNow = type === "QML" && ( url.indexOf(MARKETPLACE_CHECKOUT_QML_PATH) !== -1 || url === MARKETPLACE_PURCHASES_QML_PATH || url.indexOf(MARKETPLACE_INSPECTIONCERTIFICATE_QML_PATH) !== -1); var onMarketplaceItemTesterScreenNow = ( url.indexOf(MARKETPLACE_ITEM_TESTER_QML_PATH) !== -1 || url === MARKETPLACE_ITEM_TESTER_QML_PATH); if ((!onWalletScreenNow && onWalletScreen) || (!onCommerceScreenNow && onCommerceScreen) || (!onMarketplaceItemTesterScreenNow && onMarketplaceScreen) ) { // exiting wallet, commerce, or marketplace item tester screen maybeEnableHMDPreview(); } onCommerceScreen = onCommerceScreenNow; onWalletScreen = onWalletScreenNow; onMarketplaceItemTesterScreen = onMarketplaceItemTesterScreenNow; wireQmlEventBridge(onCommerceScreen || onWalletScreen || onMarketplaceItemTesterScreen); if (url === MARKETPLACE_PURCHASES_QML_PATH) { // FIXME? Is there a race condition here in which the event // bridge may not be up yet? If so, Script.setTimeout(..., 750) // may help avoid the condition. ui.tablet.sendToQml({ method: 'updatePurchases', referrerURL: referrerURL, filterText: filterText }); } ui.isOpen = (onMarketplaceScreen || onCommerceScreen) && !onWalletScreen; ui.buttonActive(ui.isOpen); if (type === "Web" && url.indexOf(MARKETPLACE_URL) !== -1) { ContextOverlay.isInMarketplaceInspectionMode = true; } else { ContextOverlay.isInMarketplaceInspectionMode = false; } if (onInspectionCertificateScreen) { setCertificateInfo(contextOverlayEntity); } if (onCommerceScreen) { if (!isWired) { Users.usernameFromIDReply.connect(usernameFromIDReply); Controller.mousePressEvent.connect(handleMouseEvent); Controller.mouseMoveEvent.connect(handleMouseMoveEvent); triggerMapping.enable(); triggerPressMapping.enable(); } isWired = true; Wallet.refreshWalletStatus(); } else { if (onMarketplaceScreen) { onMarketplaceOpen('marketplace cta'); } ui.tablet.sendToQml({ method: 'inspectionCertificate_resetCert' }); off(); } if (onMarketplaceItemTesterScreen) { // Why setTimeout? The QML event bridge, wired above, requires a // variable amount of time to come up, in practice less than // 750ms. Script.setTimeout(pushResourceObjectsInTest, 750); } console.debug(ui.buttonName + " app reports: Tablet screen changed.\nNew screen type: " + type + "\nNew screen URL: " + url + "\nCurrent app open status: " + ui.isOpen + "\n"); }; function notificationDataProcessPage(data) { return data.data.updates; } var shouldShowDot = false; function notificationPollCallback(updatesArray) { shouldShowDot = shouldShowDot || updatesArray.length > 0; ui.messagesWaiting(shouldShowDot && !ui.isOpen); if (updatesArray.length > 0) { var message; if (!ui.notificationInitialCallbackMade) { message = updatesArray.length + " of your purchased items " + (updatesArray.length === 1 ? "has an update " : "have updates ") + "available. Open MARKET to update."; ui.notificationDisplayBanner(message); ui.notificationPollCaresAboutSince = true; } else { for (var i = 0; i < updatesArray.length; i++) { message = "Update available for \"" + updatesArray[i].base_item_title + "\"." + "Open MARKET to update."; ui.notificationDisplayBanner(message); } } } } function isReturnedDataEmpty(data) { var historyArray = data.data.updates; return historyArray.length === 0; } var BUTTON_NAME = "MARKET"; var MARKETPLACE_URL = METAVERSE_SERVER_URL + "/marketplace"; var MARKETPLACE_URL_INITIAL = MARKETPLACE_URL + "?"; // Append "?" to signal injected script that it's the initial page. var ui; function startup() { ui = new AppUi({ buttonName: BUTTON_NAME, sortOrder: 9, inject: MARKETPLACES_INJECT_SCRIPT_URL, home: MARKETPLACE_URL_INITIAL, onScreenChanged: onTabletScreenChanged, onMessage: onQmlMessageReceived, notificationPollEndpoint: "/api/v1/commerce/available_updates?per_page=10", notificationPollTimeoutMs: 300000, notificationDataProcessPage: notificationDataProcessPage, notificationPollCallback: notificationPollCallback, notificationPollStopPaginatingConditionMet: isReturnedDataEmpty, notificationPollCaresAboutSince: false // Changes to true after first poll }); ContextOverlay.contextOverlayClicked.connect(openInspectionCertificateQML); Entities.canWriteAssetsChanged.connect(onCanWriteAssetsChanged); GlobalServices.myUsernameChanged.connect(onUsernameChanged); ui.tablet.webEventReceived.connect(onWebEventReceived); Wallet.walletStatusChanged.connect(sendCommerceSettings); Window.messageBoxClosed.connect(onMessageBoxClosed); ResourceRequestObserver.resourceRequestEvent.connect(onResourceRequestEvent); Wallet.refreshWalletStatus(); } var isWired = false; var isUpdateOverlaysWired = false; function off() { if (isWired) { Users.usernameFromIDReply.disconnect(usernameFromIDReply); Controller.mousePressEvent.disconnect(handleMouseEvent); Controller.mouseMoveEvent.disconnect(handleMouseMoveEvent); triggerMapping.disable(); triggerPressMapping.disable(); isWired = false; } if (isUpdateOverlaysWired) { Script.update.disconnect(updateOverlays); isUpdateOverlaysWired = false; } removeOverlays(); } function shutdown() { maybeEnableHMDPreview(); deleteSendAssetParticleEffect(); Window.messageBoxClosed.disconnect(onMessageBoxClosed); Wallet.walletStatusChanged.disconnect(sendCommerceSettings); ui.tablet.webEventReceived.disconnect(onWebEventReceived); GlobalServices.myUsernameChanged.disconnect(onUsernameChanged); Entities.canWriteAssetsChanged.disconnect(onCanWriteAssetsChanged); ContextOverlay.contextOverlayClicked.disconnect(openInspectionCertificateQML); ResourceRequestObserver.resourceRequestEvent.disconnect(onResourceRequestEvent); off(); } // // Run the functions. // startup(); Script.scriptEnding.connect(shutdown); }()); // END LOCAL_SCOPE