Skip to content

Commit 4dd9ad8

Browse files
committed
custom achievements and a bunch of improvements
1 parent 0d77b7e commit 4dd9ad8

File tree

17 files changed

+344
-54
lines changed

17 files changed

+344
-54
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,4 @@ npm-*
2727
# for act
2828
.secrets
2929
.env
30+
.env.*

src/addons/addons/collaboration/helpers/DebugRecorder.js

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
// helpers/DebugRecorder.js
22
import * as constants from './constants.js';
3+
import storage from '../../../../lib/storage';
34

45
export class DebugRecorder {
56
constructor() {
@@ -10,6 +11,7 @@ export class DebugRecorder {
1011
this.projectId = null;
1112
this.ready = false;
1213
this.enabled = true;
14+
this._accessToken = null;
1315

1416
// Batching
1517
this.writeBuffer = [];
@@ -224,14 +226,12 @@ export class DebugRecorder {
224226
}
225227
}
226228

227-
const projectIdForUpload = match[1];
228-
229229
console.log(`[BlackBox] Found old session: ${name}. Initiating upload sequence...`);
230230

231231
try {
232232
const dumpBlob = await this._exportDatabaseToBlob(name);
233233
if (dumpBlob) {
234-
const uploaded = await this._uploadDump(dumpBlob, `${name}.json`, projectIdForUpload);
234+
const uploaded = await this._uploadDump(dumpBlob, `${name}.json`);
235235
if (uploaded) {
236236
console.log(`[BlackBox] Upload successful. Deleting local DB: ${name}`);
237237
window.indexedDB.deleteDatabase(name);
@@ -327,21 +327,23 @@ export class DebugRecorder {
327327
* Uploads the exported database blob to the server.
328328
* @param {Blob} blob The blob containing the database dump.
329329
* @param {string} filename The filename for the upload.
330-
* @param {string} roomUuidForUpload The project/room UUID for the API endpoint.
331330
*/
332-
async _uploadDump(blob, filename, roomUuidForUpload) {
331+
async _uploadDump(blob, filename) {
332+
if (this._accessToken == null) {
333+
this._accessToken = await storage.loadAccessToken();
334+
}
333335
const formData = new FormData();
334336
formData.append('file', blob, filename);
335337

336-
const token = window.collaborationOTT || '';
337-
const roomUuid = roomUuidForUpload || 'unknown_project';
338-
339-
const url = `${constants.apiHostURL}/v1/feedback/black-box?ott=${encodeURIComponent(token)}&room_uuid=${encodeURIComponent(roomUuid)}`;
338+
const url = `${constants.apiHostURL}/v1/feedback/black-box`;
340339

341340
try {
342341
const response = await fetch(url, {
343342
method: 'POST',
344-
body: formData
343+
body: formData,
344+
headers: {
345+
Authorization: `Bearer ${this._accessToken}`
346+
}
345347
});
346348

347349
return response.ok;

src/addons/addons/collaboration/helpers/yProjectEvents.js

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import * as assetSync from './assetSync.js';
44
import * as helper from './helper.js';
55
import CollaborationConsole from './CollaborationConsole.js';
66
import { recorder } from './DebugRecorder.js';
7+
import storage from '../../../../lib/storage';
8+
import { API_HOST, TRUSTED_IFRAME_HOST } from '../../../../lib/brand.js';
79

810
/**
911
* Flag indicating whether project events can be processed immediately or if they should be queued.
@@ -407,8 +409,21 @@ export async function processSpecificEvent(item) {
407409
}
408410
if (constants.debugging) CollaborationConsole.log(`Collab RX (constants.mutableRefs.yProjectEvents) [${constants.CUSTOM_REMOTE_EXTENSION_LOADED_CALL_TYPE}]: Attempting to load extension from URL "${extensionURL}".`);
409411
try {
410-
// Await the promise returned by `loadExtensionURL`. `false` prevents local re-emission.
411-
await constants.mutableRefs.vm.extensionManager.loadExtensionURL(extensionURL, false);
412+
const { accessToken, customAchievements } = await storage.loadCustomAchievementData();
413+
let projectId = '0';
414+
if (constants.mutableRefs.addon && constants.mutableRefs.addon.tab && constants.mutableRefs.addon.tab.redux) {
415+
projectId = constants.mutableRefs.addon.tab.redux.state.scratchGui.projectState.projectId;
416+
}
417+
const canSave = true; // you can't collab if you can't save // constants.mutableRefs.addon?.tab?.redux?.state?.scratchGui?.mode?.isPlayerOnly === false;
418+
await constants.mutableRefs.vm.extensionManager.loadExtensionURL(extensionURL, false, {
419+
projectId: projectId,
420+
authToken: accessToken,
421+
API_HOST: API_HOST,
422+
TRUSTED_IFRAME_HOST: TRUSTED_IFRAME_HOST,
423+
customAchievements: customAchievements || [],
424+
canRecieveAchievement: false,
425+
canSave: canSave,
426+
});
412427
if (constants.debugging) CollaborationConsole.log(`Collab RX (constants.mutableRefs.yProjectEvents) [${constants.CUSTOM_REMOTE_EXTENSION_LOADED_CALL_TYPE}]: Extension loaded successfully.`);
413428
} catch (e) {
414429
CollaborationConsole.error(`Collab RX (constants.mutableRefs.yProjectEvents) [${constants.CUSTOM_REMOTE_EXTENSION_LOADED_CALL_TYPE}]: Error loading extension:`, e, item);
@@ -877,4 +892,4 @@ export async function processSpecificEvent(item) {
877892
else {
878893
CollaborationConsole.warn(`Collab RX (constants.mutableRefs.yProjectEvents): Received unhandled project event type: ${item.type}`, item);
879894
}
880-
}
895+
}

src/components/gui/gui.jsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -401,6 +401,7 @@ const GUIComponent = props => {
401401
onOpenCustomExtensionModal={onOpenCustomExtensionModal}
402402
theme={theme}
403403
vm={vm}
404+
canSave={canSave}
404405
/>
405406
</Box>
406407
<Box className={styles.extensionButtonContainer}>

src/containers/blocks.jsx

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ import {setConnectionModalExtensionId} from '../reducers/connection-modal';
3636
import {updateMetrics} from '../reducers/workspace-metrics';
3737
import {isTimeTravel2020} from '../reducers/time-travel';
3838

39+
// eslint-disable-next-line import/no-commonjs
40+
const {TRUSTED_IFRAME_HOST} = require('../lib/brand.js');
41+
3942
import {
4043
activateTab,
4144
SOUNDS_TAB_INDEX
@@ -191,6 +194,13 @@ class Blocks extends React.Component {
191194
window.open(docsURI, '_blank');
192195
}
193196
});
197+
toolboxWorkspace.registerButtonCallback('OPEN_ACHIEVEMENT_POPUP', () => {
198+
const trustedOrigin = TRUSTED_IFRAME_HOST;
199+
window.parent.postMessage(
200+
{type: 'block-compiler-action', action: 'show_achievement_setup_popup', canSave: this.props.canSave}
201+
, trustedOrigin);
202+
});
203+
194204
toolboxWorkspace.registerButtonCallback('OPEN_RETURN_DOCS', () => {
195205
window.open('https://docs.turbowarp.org/return', '_blank');
196206
});
@@ -589,7 +599,7 @@ class Blocks extends React.Component {
589599
setBlocks (blocks) {
590600
this.blocks = blocks;
591601
}
592-
handlePromptStart (message, defaultValue = "", callback, optTitle, optVarType,noInput=false) {
602+
handlePromptStart (message, defaultValue = '', callback, optTitle, optVarType, noInput = false) {
593603
const p = {prompt: {callback, message, defaultValue: defaultValue}}; // Set defaultValue to empty string or null
594604
p.prompt.title = optTitle ? optTitle :
595605
this.ScratchBlocks.Msg.VARIABLE_MODAL_TITLE;
@@ -672,6 +682,7 @@ class Blocks extends React.Component {
672682
const {
673683
anyModalVisible,
674684
canUseCloud,
685+
canSave,
675686
customStageSize,
676687
customProceduresVisible,
677688
extensionLibraryVisible,
@@ -726,6 +737,7 @@ class Blocks extends React.Component {
726737
onEnableProcedureReturns={this.handleEnableProcedureReturns}
727738
onRequestClose={onRequestCloseExtensionLibrary}
728739
onOpenCustomExtensionModal={onOpenCustomExtensionModal || reduxOnOpenCustomExtensionModal}
740+
canSave={canSave}
729741
/>
730742
) : null}
731743
{customProceduresVisible ? (
@@ -745,6 +757,7 @@ Blocks.propTypes = {
745757
intl: intlShape,
746758
anyModalVisible: PropTypes.bool,
747759
canUseCloud: PropTypes.bool,
760+
canSave: PropTypes.bool,
748761
customStageSize: PropTypes.shape({
749762
width: PropTypes.number,
750763
height: PropTypes.number

src/containers/controls.jsx

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@ import PropTypes from 'prop-types';
33
import React from 'react';
44
import VM from 'scratch-vm';
55
import {connect} from 'react-redux';
6+
import {compose} from 'redux'; // Added compose
67

7-
import {showStandardAlert, showAlertWithTimeout} from '../reducers/alerts';
8-
8+
import {showStandardAlert} from '../reducers/alerts';
9+
import ProjectAnalyticsHOC from '../lib/project-analytics-hoc.jsx'; // Import HOC
910

1011
import ControlsComponent from '../components/controls/controls.jsx';
1112

@@ -20,12 +21,18 @@ class Controls extends React.Component {
2021
showedPopup: false
2122
};
2223
}
24+
2325
handleGreenFlagClick (e) {
24-
if(!this.props.disableCompiler && !this.state.showedPopup) {
26+
// Controls-specific logic (Save Warning)
27+
if (!this.props.disableCompiler && !this.state.showedPopup) {
2528
this.setState({showedPopup: true});
2629
this.props.onShowSaveErrorAlert();
2730
}
2831
e.preventDefault();
32+
33+
// Trigger Shared Analytics Logic
34+
this.props.onGreenFlagClickAnalytics();
35+
2936
// tw: implement alt+click and right click to toggle FPS
3037
if (e.shiftKey || e.altKey || e.type === 'contextmenu') {
3138
if (e.shiftKey) {
@@ -52,18 +59,21 @@ class Controls extends React.Component {
5259
render () {
5360
const {
5461
vm, // eslint-disable-line no-unused-vars
55-
isStarted, // eslint-disable-line no-unused-vars
62+
isStarted,
5663
projectRunning,
5764
turbo,
5865
disableCompiler,
5966
onShowSaveErrorAlert,
6067
...props
6168
} = this.props;
69+
6270
return (
6371
<ControlsComponent
6472
{...props}
6573
active={projectRunning && isStarted}
6674
turbo={turbo}
75+
disableCompiler={disableCompiler}
76+
onShowSaveErrorAlert={onShowSaveErrorAlert}
6777
onGreenFlagClick={this.handleGreenFlagClick}
6878
onStopAllClick={this.handleStopAllClick}
6979
/>
@@ -78,7 +88,10 @@ Controls.propTypes = {
7888
framerate: PropTypes.number.isRequired,
7989
interpolation: PropTypes.bool.isRequired,
8090
isSmall: PropTypes.bool,
81-
vm: PropTypes.instanceOf(VM)
91+
vm: PropTypes.instanceOf(VM).isRequired,
92+
disableCompiler: PropTypes.bool.isRequired,
93+
onShowSaveErrorAlert: PropTypes.func.isRequired,
94+
onGreenFlagClickAnalytics: PropTypes.func.isRequired
8295
};
8396

8497
const mapStateToProps = state => ({
@@ -89,9 +102,12 @@ const mapStateToProps = state => ({
89102
turbo: state.scratchGui.vmStatus.turbo,
90103
disableCompiler: !state.scratchGui.tw.compilerOptions.enabled
91104
});
92-
// no-op function to prevent dispatch prop being passed to component
105+
93106
const mapDispatchToProps = dispatch => ({
94-
onShowSaveErrorAlert: () => dispatch(showStandardAlert('LiveReloadDisabledNotice')),
107+
onShowSaveErrorAlert: () => dispatch(showStandardAlert('LiveReloadDisabledNotice'))
95108
});
96109

97-
export default connect(mapStateToProps, mapDispatchToProps)(Controls);
110+
export default compose(
111+
connect(mapStateToProps, mapDispatchToProps),
112+
ProjectAnalyticsHOC
113+
)(Controls);

src/containers/extension-library.jsx

Lines changed: 50 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@ import React from 'react';
44
import VM from 'scratch-vm';
55
import {defineMessages, injectIntl, intlShape} from 'react-intl';
66
import log from '../lib/log';
7+
import {connect} from 'react-redux'; // Import connect
78
// eslint-disable-next-line import/no-commonjs
8-
const {EXTENSION_HOST} = require('../lib/brand');
9+
const {EXTENSION_HOST, TRUSTED_IFRAME_HOST, API_HOST} = require('../lib/brand');
10+
import storage from '../lib/storage';
911

1012
import extensionLibraryContent, {
1113
galleryError,
@@ -123,7 +125,7 @@ class ExtensionLibrary extends React.PureComponent {
123125
});
124126
}
125127
}
126-
handleItemSelect (item) {
128+
handleItemSelect = async item => {
127129
if (item.href) {
128130
return;
129131
}
@@ -140,13 +142,46 @@ class ExtensionLibrary extends React.PureComponent {
140142
this.props.onCategorySelected('myBlocks');
141143
return;
142144
}
145+
146+
const additionalData = {};
147+
148+
// SPECIAL CHECK FOR CUSTOM ACHIEVEMENTS EXTENSION
149+
if (extensionId === 'customAchievements') {
150+
// Check if project ID exists and is not the default '0' (unsaved)
151+
if (!this.props.projectId || this.props.projectId === '0') {
152+
window.parent.postMessage({
153+
type: 'block-compiler-action',
154+
action: 'extension-error',
155+
error: 'login_required'
156+
}, '*');
157+
return;
158+
}
159+
160+
// Check if user owns the project (canSave)
161+
if (!this.props.canSave) {
162+
window.parent.postMessage({
163+
type: 'block-compiler-action',
164+
action: 'extension-error',
165+
error: 'not_owner'
166+
}, '*');
167+
return;
168+
}
169+
const {accessToken, customAchievements} = await storage.loadCustomAchievementData();
170+
additionalData.projectId = this.props.projectId;
171+
additionalData.customAchievements = customAchievements;
172+
additionalData.authToken = accessToken;
173+
additionalData.API_HOST = API_HOST;
174+
additionalData.TRUSTED_IFRAME_HOST = TRUSTED_IFRAME_HOST;
175+
additionalData.canRecieveAchievement = false; // you are in editor without a doubt...
176+
additionalData.canSave = this.props.canSave;
177+
}
143178

144179
const url = item.extensionURL ? item.extensionURL : extensionId;
145180
if (!item.disabled) {
146181
if (this.props.vm.extensionManager.isExtensionLoaded(extensionId)) {
147182
this.props.onCategorySelected(extensionId);
148183
} else {
149-
this.props.vm.extensionManager.loadExtensionURL(url)
184+
this.props.vm.extensionManager.loadExtensionURL(url, true, additionalData)
150185
.then(() => {
151186
this.props.onCategorySelected(extensionId);
152187
})
@@ -157,7 +192,8 @@ class ExtensionLibrary extends React.PureComponent {
157192
});
158193
}
159194
}
160-
}
195+
};
196+
161197
render () {
162198
let library = null;
163199
if (this.state.gallery || this.state.galleryError || this.state.galleryTimedOut) {
@@ -201,7 +237,15 @@ ExtensionLibrary.propTypes = {
201237
onOpenCustomExtensionModal: PropTypes.func,
202238
onRequestClose: PropTypes.func,
203239
visible: PropTypes.bool,
204-
vm: PropTypes.instanceOf(VM).isRequired // eslint-disable-line react/no-unused-prop-types
240+
vm: PropTypes.instanceOf(VM).isRequired, // eslint-disable-line react/no-unused-prop-types
241+
projectId: PropTypes.string,
242+
canSave: PropTypes.bool
205243
};
206244

207-
export default injectIntl(ExtensionLibrary);
245+
const mapStateToProps = state => ({
246+
projectId: state.scratchGui.projectState.projectId
247+
});
248+
249+
export default injectIntl(connect(
250+
mapStateToProps
251+
)(ExtensionLibrary));

0 commit comments

Comments
 (0)