pxt-core 8.2.9 → 8.2.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pxt-core",
3
- "version": "8.2.9",
3
+ "version": "8.2.11",
4
4
  "description": "Microsoft MakeCode provides Blocks / JavaScript / Python tools and editors",
5
5
  "keywords": [
6
6
  "TypeScript",
@@ -1,6 +1,7 @@
1
1
  /// <reference path="../types.d.ts" />
2
2
 
3
3
  import * as React from "react";
4
+ import { SimRecorder } from "./ThumbnailRecorder";
4
5
  import { ShareInfo } from "./ShareInfo";
5
6
 
6
7
  export interface ShareData {
@@ -18,34 +19,20 @@ export interface ShareData {
18
19
  export interface ShareProps {
19
20
  projectName: string;
20
21
  screenshotUri?: string;
21
- showShareDropdown?: boolean;
22
+ isLoggedIn?: boolean;
22
23
 
23
- screenshotAsync: () => Promise<string>;
24
- gifRecordAsync: () => Promise<void>;
25
- gifRenderAsync: () => Promise<string | void>;
26
- gifAddFrame: (dataUri: ImageData, delay?: number) => boolean;
24
+ simRecorder: SimRecorder;
27
25
  publishAsync: (name: string, screenshotUri?: string, forceAnonymous?: boolean) => Promise<ShareData>;
28
- registerSimulatorMsgHandler?: (handler: (msg: any) => void) => void;
29
- unregisterSimulatorMsgHandler?: () => void;
30
26
  }
31
27
 
32
28
  export const Share = (props: ShareProps) => {
33
- const { projectName, screenshotUri, showShareDropdown, screenshotAsync, gifRecordAsync, gifRenderAsync,
34
- gifAddFrame, publishAsync, registerSimulatorMsgHandler, unregisterSimulatorMsgHandler } = props;
29
+ const { projectName, screenshotUri, isLoggedIn, simRecorder, publishAsync} = props;
35
30
 
36
- return <div className="project-share">
37
- {(!!screenshotAsync || !!gifRecordAsync) && <div className="project-share-simulator">
38
- <div id="shareLoanedSimulator" />
39
- </div>}
31
+ return <div className="project-share">
40
32
  <ShareInfo projectName={projectName}
41
- showShareDropdown={showShareDropdown}
33
+ isLoggedIn={isLoggedIn}
42
34
  screenshotUri={screenshotUri}
43
- screenshotAsync={screenshotAsync}
44
- gifRecordAsync={gifRecordAsync}
45
- gifRenderAsync={gifRenderAsync}
46
- gifAddFrame={gifAddFrame}
47
- publishAsync={publishAsync}
48
- registerSimulatorMsgHandler={registerSimulatorMsgHandler}
49
- unregisterSimulatorMsgHandler={unregisterSimulatorMsgHandler} />
35
+ simRecorder={simRecorder}
36
+ publishAsync={publishAsync} />
50
37
  </div>
51
38
  }
@@ -2,32 +2,26 @@ import * as React from "react";
2
2
  import { Button } from "../controls/Button";
3
3
  import { EditorToggle } from "../controls/EditorToggle";
4
4
  import { Input } from "../controls/Input";
5
- import { MenuDropdown } from "../controls/MenuDropdown";
6
5
  import { Textarea } from "../controls/Textarea";
7
6
  import { Modal } from "../controls/Modal";
8
7
 
9
8
  import { ShareData } from "./Share";
10
- import { GifInfo } from "./GifInfo";
9
+ import { ThumbnailRecorder } from "./ThumbnailRecorder";
11
10
  import { SocialButton } from "./SocialButton";
11
+ import { Checkbox } from "../controls/Checkbox";
12
+ import { SimRecorder } from "./ThumbnailRecorder";
12
13
 
13
14
  export interface ShareInfoProps {
14
15
  projectName: string;
15
16
  description?: string;
16
17
  screenshotUri?: string;
17
- showShareDropdown?: boolean;
18
-
19
- screenshotAsync?: () => Promise<string>;
20
- gifRecordAsync?: () => Promise<void>;
21
- gifRenderAsync?: () => Promise<string | void>;
22
- gifAddFrame?: (dataUri: ImageData, delay?: number) => boolean;
18
+ isLoggedIn?: boolean;
19
+ simRecorder: SimRecorder;
23
20
  publishAsync: (name: string, screenshotUri?: string, forceAnonymous?: boolean) => Promise<ShareData>;
24
- registerSimulatorMsgHandler?: (handler: (msg: any) => void) => void;
25
- unregisterSimulatorMsgHandler?: () => void;
26
21
  }
27
22
 
28
23
  export const ShareInfo = (props: ShareInfoProps) => {
29
- const { projectName, description, screenshotUri, showShareDropdown, screenshotAsync, gifRecordAsync,
30
- gifRenderAsync, gifAddFrame, publishAsync, registerSimulatorMsgHandler, unregisterSimulatorMsgHandler } = props;
24
+ const { projectName, description, screenshotUri, isLoggedIn, simRecorder, publishAsync } = props;
31
25
  const [ name, setName ] = React.useState(projectName);
32
26
  const [ thumbnailUri, setThumbnailUri ] = React.useState(screenshotUri);
33
27
  const [ shareState, setShareState ] = React.useState<"share" | "gifrecord" | "publish" | "publishing">("share");
@@ -35,8 +29,9 @@ export const ShareInfo = (props: ShareInfoProps) => {
35
29
  const [ embedState, setEmbedState ] = React.useState<"none" | "code" | "editor" | "simulator">("none");
36
30
  const [ showQRCode, setShowQRCode ] = React.useState(false);
37
31
  const [ copySuccessful, setCopySuccessful ] = React.useState(false);
32
+ const [ isAnonymous, setIsAnonymous ] = React.useState(!isLoggedIn);
38
33
 
39
- const showSimulator = !!screenshotAsync || !!gifRecordAsync;
34
+ const showSimulator = !!simRecorder;
40
35
  const showDescription = shareState !== "publish";
41
36
  let qrCodeButtonRef: HTMLButtonElement;
42
37
  let inputRef: HTMLInputElement;
@@ -54,9 +49,9 @@ export const ShareInfo = (props: ShareInfoProps) => {
54
49
  exitGifRecord();
55
50
  }
56
51
 
57
- const handlePublishClick = async (forceAnonymous?: boolean) => {
52
+ const handlePublishClick = async () => {
58
53
  setShareState("publishing");
59
- let publishedShareData = await publishAsync(name, thumbnailUri, forceAnonymous);
54
+ let publishedShareData = await publishAsync(name, thumbnailUri, isAnonymous);
60
55
  setShareData(publishedShareData);
61
56
  if (!publishedShareData?.error) setShareState("publish");
62
57
  else setShareState("share")
@@ -132,12 +127,6 @@ export const ShareInfo = (props: ShareInfoProps) => {
132
127
  onClick: () => setEmbedState("simulator")
133
128
  }];
134
129
 
135
- const dropdownOptions = [{
136
- title: lf("Create snapshot"),
137
- label: lf("Create snapshot"),
138
- onClick: () => handlePublishClick(true)
139
- }];
140
-
141
130
  const handleQRCodeButtonRef = (ref: HTMLButtonElement) => {
142
131
  if (ref) qrCodeButtonRef = ref;
143
132
  }
@@ -153,124 +142,145 @@ export const ShareInfo = (props: ShareInfoProps) => {
153
142
 
154
143
  const prePublish = shareState === "share" || shareState === "publishing";
155
144
 
145
+ const inputTitle = showSimulator && prePublish ? lf("Project Title") : lf("Project Link")
146
+
156
147
  return <>
157
148
  <div className="project-share-info">
158
- {(prePublish|| shareState === "publish") && <>
159
- {showSimulator && <div className="project-share-title">
160
- <h2>{lf("About your project")}</h2>
161
- {showShareDropdown && prePublish && <MenuDropdown id="project-share-dropdown"
162
- icon="fas fa-ellipsis-h"
163
- title={lf("More share options")}
164
- items={dropdownOptions}
165
- />}
166
- </div>}
167
- {showDescription && <>
168
- <Input label={lf("Project Name")}
169
- initialValue={name}
170
- placeholder={lf("Name your project")}
171
- onChange={setName} />
172
- <Textarea label={lf("Description")}
173
- initialValue={description}
174
- placeholder={lf("Tell others about your project")}
175
- rows={5} />
176
- </>
177
- }
178
- {prePublish && <>
179
- {showSimulator && <div className="project-share-thumbnail">
180
- {thumbnailUri
181
- ? <img src={thumbnailUri} />
182
- : <div className="project-thumbnail-placeholder" />
183
- }
184
- <Button title={lf("Update project thumbnail")}
149
+ {showSimulator && shareState !== "gifrecord" &&
150
+ <div className="project-share-thumbnail">
151
+ {thumbnailUri
152
+ ? <img src={thumbnailUri} />
153
+ : <div className="project-thumbnail-placeholder">
154
+ <div className="common-spinner" />
155
+ </div>
156
+ }
157
+ {shareState !== "publish" &&
158
+ <Button
159
+ className="link-button"
160
+ title={lf("Update project thumbnail")}
185
161
  label={lf("Update project thumbnail")}
186
162
  onClick={() => setShareState("gifrecord")} />
187
- </div>}
188
- <div>{lf("You need to publish your project to share it or embed it in other web pages. You acknowledge having consent to publish this project.")}</div>
189
- {shareData?.error && <div className="project-share-error">
190
- {(shareData.error.statusCode === 413
191
- && pxt.appTarget?.cloud?.cloudProviders?.github)
192
- ? lf("Oops! Your project is too big. You can create a GitHub repository to share it.")
193
- : lf("Oops! There was an error. Please ensure you are connected to the Internet and try again.")}
194
- </div>}
195
- {shareState === "share" ?
196
- <Button className="primary share-publish-button"
197
- title={lf("Publish to share")}
198
- label={lf("Publish to share")}
199
- onClick={handlePublishClick} /> :
200
- <Button className="primary share-publish-button"
201
- title={lf("Publishing...")}
202
- label={ <div className="common-spinner" />}
203
- onClick={() => {}} />
204
163
  }
205
- </>}
206
-
207
- {shareState === "publish" &&
208
- <div className="project-share-data">
209
- <div className="project-share-text">
210
- {lf("Your project is ready! Use the address below to share your projects.")}
211
- </div>
212
- <div className="common-input-attached-button">
213
- <Input
214
- handleInputRef={handleInputRef}
215
- initialValue={shareData.url}
216
- readOnly={true}
217
- onChange={setName} />
218
- <Button className={copySuccessful ? "green" : "primary"}
219
- title={lf("Copy link")}
220
- label={copySuccessful ? lf("Copied!") : lf("Copy link")}
221
- leftIcon="fas fa-link"
222
- onClick={handleCopyClick}
223
- onBlur={handleCopyBlur} />
224
- </div>
225
- <div className="project-share-actions">
226
- <Button className="circle-button gray embed mobile-portrait-hidden"
227
- title={lf("Show embed code")}
228
- leftIcon="fas fa-code"
229
- onClick={handleEmbedClick} />
230
- <SocialButton className="circle-button facebook"
231
- url={shareData?.url}
232
- type='facebook'
233
- heading={lf("Share on Facebook")} />
234
- <SocialButton className="circle-button twitter"
235
- url={shareData?.url}
236
- type='twitter'
237
- heading={lf("Share on Twitter")} />
238
- {navigator.share && <Button className="circle-button device-share"
239
- title={lf("Show device share options")}
240
- ariaLabel={lf("Show device share options")}
241
- leftIcon={"icon share"}
242
- onClick={handleDeviceShareClick}
164
+ </div>
165
+ }
166
+ <div className="project-share-content">
167
+ {(prePublish || shareState === "publish") && <>
168
+ <div className="project-share-title project-share-label" id="share-input-title">
169
+ {inputTitle}
170
+ </div>
171
+ {showDescription && <>
172
+ <Input
173
+ ariaDescribedBy="share-input-title"
174
+ className="name-input"
175
+ initialValue={name}
176
+ placeholder={lf("Name your project")}
177
+ onChange={setName} />
178
+ {isLoggedIn && <Checkbox
179
+ id="persistent-share-checkbox"
180
+ label={lf("Allow people to see future changes to my project")}
181
+ isChecked={!isAnonymous}
182
+ onChange={val => setIsAnonymous(!val)}
243
183
  />}
244
- <Button
245
- className="menu-button project-qrcode"
246
- buttonRef={handleQRCodeButtonRef}
247
- title={lf("Show QR Code")}
248
- label={<img className="qrcode-image" src={shareData?.qr} />}
249
- onClick={handleQRCodeClick}
250
- />
184
+ </>
185
+ }
186
+ {prePublish && <>
187
+ {shareData?.error && <div className="project-share-error">
188
+ {(shareData.error.statusCode === 413
189
+ && pxt.appTarget?.cloud?.cloudProviders?.github)
190
+ ? lf("Oops! Your project is too big. You can create a GitHub repository to share it.")
191
+ : lf("Oops! There was an error. Please ensure you are connected to the Internet and try again.")}
192
+ </div>}
193
+ <div>
194
+ {shareState === "share" &&
195
+ <Button className="primary share-publish-button"
196
+ title={lf("Continue")}
197
+ label={lf("Continue")}
198
+ onClick={handlePublishClick} />
199
+ }
200
+ { shareState === "publishing" &&
201
+ <Button className="primary share-publish-button"
202
+ title={lf("Publishing...")}
203
+ label={ <div className="common-spinner" />}
204
+ onClick={() => {}} />
205
+ }
251
206
  </div>
252
- </div>
253
- }
254
- {embedState !== "none" && <div className="project-embed">
255
- <EditorToggle id="project-embed-toggle"
256
- className="slim tablet-compact"
257
- items={embedOptions}
258
- selected={embedOptions.findIndex(i => i.name === embedState)} />
259
- <Textarea readOnly={true}
260
- rows={5}
261
- initialValue={shareData?.embed[embedState]} />
262
- </div>}
263
- </>}
264
- {shareState === "gifrecord" && <GifInfo
265
- initialUri={thumbnailUri}
266
- onApply={applyGifChange}
267
- onCancel={exitGifRecord}
268
- screenshotAsync={screenshotAsync}
269
- gifRecordAsync={gifRecordAsync}
270
- gifRenderAsync={gifRenderAsync}
271
- gifAddFrame={gifAddFrame}
272
- registerSimulatorMsgHandler={registerSimulatorMsgHandler}
273
- unregisterSimulatorMsgHandler={unregisterSimulatorMsgHandler} />}
207
+ </>}
208
+
209
+ {shareState === "publish" &&
210
+ <div className="project-share-data">
211
+ <div className="common-input-attached-button">
212
+ <Input
213
+ ariaDescribedBy="share-input-title"
214
+ handleInputRef={handleInputRef}
215
+ initialValue={shareData.url}
216
+ readOnly={true}
217
+ onChange={setName} />
218
+ <Button className={copySuccessful ? "green" : "primary"}
219
+ title={lf("Copy link")}
220
+ label={copySuccessful ? lf("Copied!") : lf("Copy")}
221
+ leftIcon="fas fa-link"
222
+ onClick={handleCopyClick}
223
+ onBlur={handleCopyBlur} />
224
+ </div>
225
+ <div className="project-share-actions">
226
+ <div className="project-share-social">
227
+ <Button className="square-button gray embed mobile-portrait-hidden"
228
+ title={lf("Show embed code")}
229
+ leftIcon="fas fa-code"
230
+ onClick={handleEmbedClick} />
231
+ <SocialButton className="square-button facebook"
232
+ url={shareData?.url}
233
+ type='facebook'
234
+ heading={lf("Share on Facebook")} />
235
+ <SocialButton className="square-button twitter"
236
+ url={shareData?.url}
237
+ type='twitter'
238
+ heading={lf("Share on Twitter")} />
239
+ <SocialButton className="square-button google-classroom"
240
+ url={shareData?.url}
241
+ type='google-classroom'
242
+ heading={lf("Share on Google Classroom")} />
243
+ <SocialButton className="square-button microsoft-teams"
244
+ url={shareData?.url}
245
+ type='microsoft-teams'
246
+ heading={lf("Share on Microsoft Teams")} />
247
+ <SocialButton className="square-button whatsapp"
248
+ url={shareData?.url}
249
+ type='whatsapp'
250
+ heading={lf("Share on WhatsApp")} />
251
+ {navigator.share && <Button className="square-button device-share"
252
+ title={lf("Show device share options")}
253
+ ariaLabel={lf("Show device share options")}
254
+ leftIcon={"icon share"}
255
+ onClick={handleDeviceShareClick}
256
+ />}
257
+ </div>
258
+ <Button
259
+ className="menu-button project-qrcode"
260
+ buttonRef={handleQRCodeButtonRef}
261
+ title={lf("Show QR Code")}
262
+ label={<img className="qrcode-image" src={shareData?.qr} />}
263
+ onClick={handleQRCodeClick}
264
+ />
265
+ </div>
266
+ </div>
267
+ }
268
+ {embedState !== "none" && <div className="project-embed">
269
+ <EditorToggle id="project-embed-toggle"
270
+ className="slim tablet-compact"
271
+ items={embedOptions}
272
+ selected={embedOptions.findIndex(i => i.name === embedState)} />
273
+ <Textarea readOnly={true}
274
+ rows={5}
275
+ initialValue={shareData?.embed[embedState]} />
276
+ </div>}
277
+ </>}
278
+ {shareState === "gifrecord" && <ThumbnailRecorder
279
+ initialUri={thumbnailUri}
280
+ onApply={applyGifChange}
281
+ onCancel={exitGifRecord}
282
+ simRecorder={simRecorder}/>}
283
+ </div>
274
284
 
275
285
  {showQRCode &&
276
286
  <Modal title={lf("QR Code")} onClose={handleQRCodeModalClose}>
@@ -1,16 +1,19 @@
1
1
  import * as React from "react";
2
2
  import { Button } from "../controls/Button";
3
+ import { classList } from "../util";
3
4
 
4
5
  interface SocialButtonProps {
5
6
  className?: string;
6
7
  url?: string;
7
- type?: "facebook" | "twitter" | "discourse";
8
+ type?: "facebook" | "twitter" | "discourse" | "google-classroom" | "microsoft-teams" | "whatsapp";
8
9
  heading?: string;
9
10
  }
10
11
 
11
12
  export const SocialButton = (props: SocialButtonProps) => {
12
13
  const { className, url, type, heading } = props;
13
14
 
15
+ const classes = classList(className, "social-button", "type")
16
+
14
17
  const handleClick = () => {
15
18
  const socialOptions = pxt.appTarget.appTheme.socialOptions;
16
19
  let socialUrl = '';
@@ -41,13 +44,40 @@ export const SocialButton = (props: SocialButtonProps) => {
41
44
  socialUrl += `&category=${encodeURIComponent(socialOptions.discourseCategory)}`;
42
45
  break;
43
46
  }
47
+ case "google-classroom":
48
+ socialUrl = `https://classroom.google.com/share?url=${encodeURIComponent(url)}`;
49
+ break;
50
+ case "microsoft-teams":
51
+ socialUrl = `https://teams.microsoft.com/share?href=${encodeURIComponent(url)}`;
52
+ break;
53
+ case "whatsapp":
54
+ socialUrl = `https://api.whatsapp.com/send?text=${encodeURIComponent(url)}`;
55
+ break;
44
56
  }
45
57
  pxt.BrowserUtils.popupWindow(socialUrl, heading, 600, 600);
46
58
  }
47
59
 
48
- return <Button className={className}
49
- ariaLabel={type}
50
- title={heading}
51
- leftIcon={`icon ${type}`}
52
- onClick={handleClick} />
60
+ switch (type) {
61
+ // Icon buttons
62
+ case "facebook":
63
+ case "twitter":
64
+ case "discourse":
65
+ case "whatsapp":
66
+ return <Button className={classes}
67
+ ariaLabel={type}
68
+ title={heading}
69
+ leftIcon={`icon ${type}`}
70
+ onClick={handleClick} />
71
+
72
+ // Image buttons
73
+ case "google-classroom":
74
+ case "microsoft-teams":
75
+ return <Button className={classes}
76
+ ariaLabel={type}
77
+ title={heading}
78
+ label={<img src={`/static/logo/social-buttons/${type}.png`} alt={heading || pxt.U.rlf(type)} />}
79
+ onClick={handleClick} />
80
+ }
81
+
82
+
53
83
  }
@@ -0,0 +1,149 @@
1
+ import * as React from "react";
2
+ import { Button } from "../controls/Button";
3
+ import { classList } from "../util";
4
+
5
+ export interface ThumbnailRecorderProps {
6
+ initialUri?: string;
7
+
8
+ onApply: (uri: string) => void;
9
+ onCancel: () => void;
10
+
11
+ simRecorder: SimRecorder;
12
+ }
13
+
14
+ export interface SimRecorderProps {
15
+ onSimRecorderInit: (ref: SimRecorderRef) => void;
16
+ }
17
+ export type SimRecorder = (props: SimRecorderProps) => JSX.Element
18
+ export type SimRecorderState = "default" | "recording" | "rendering"
19
+ export interface SimRecorderRef {
20
+ state: SimRecorderState;
21
+ startRecordingAsync: () => Promise<void>;
22
+ stopRecordingAsync: () => Promise<string>;
23
+ screenshotAsync: () => Promise<string>;
24
+ addStateChangeListener: (handler: (newState: SimRecorderState) => void) => void;
25
+ addThumbnailListener: (handler: (uri: string, type: "gif" | "png") => void) => void;
26
+ addErrorListener: (handler: (message: string) => void) => void;
27
+ removeStateChangeListener: (handler: (newState: SimRecorderState) => void) => void;
28
+ removeThumbnailListener: (handler: (uri: string, type: "gif" | "png") => void) => void;
29
+ removeErrorListener: (handler: (message: string) => void) => void;
30
+ }
31
+
32
+
33
+ export const ThumbnailRecorder = (props: ThumbnailRecorderProps) => {
34
+ const { initialUri, onApply, onCancel, simRecorder } = props;
35
+ const [ uri, setUri ] = React.useState(undefined);
36
+ const [ error, setError] = React.useState<string>(undefined)
37
+ const [ recorderRef, setRecorderRef ] = React.useState<SimRecorderRef>(undefined);
38
+ const [ recorderState, setRecorderState ] = React.useState<SimRecorderState>("default");
39
+
40
+ React.useEffect(() => {
41
+ if (!recorderRef) return undefined;
42
+ recorderRef.addStateChangeListener(setRecorderState);
43
+ recorderRef.addThumbnailListener(setUri);
44
+ recorderRef.addErrorListener(setError);
45
+
46
+ return () => {
47
+ recorderRef.removeStateChangeListener(setRecorderState);
48
+ recorderRef.removeThumbnailListener(setUri);
49
+ recorderRef.removeErrorListener(setError);
50
+ }
51
+ }, [recorderRef])
52
+
53
+ const handleApplyClick = (evt?: any) => {
54
+ onApply(uri);
55
+ }
56
+
57
+ const handleScreenshotClick = async () => {
58
+ setError(undefined);
59
+ if (recorderRef) recorderRef.screenshotAsync();
60
+ }
61
+
62
+ const handleRecordClick = async () => {
63
+ setError(undefined);
64
+ if (!recorderRef) return;
65
+
66
+ if (recorderRef.state === "recording") {
67
+ recorderRef.stopRecordingAsync();
68
+ }
69
+ else if (recorderRef.state === "default") {
70
+ recorderRef.startRecordingAsync();
71
+ }
72
+ }
73
+
74
+ const targetTheme = pxt.appTarget.appTheme;
75
+
76
+ const handleSimRecorderRef = (ref: SimRecorderRef) => {
77
+ setRecorderRef(ref);
78
+ }
79
+
80
+ const screenshotLabel = lf("Screenshot ({0})", targetTheme.simScreenshotKey);
81
+ const startRecordingLabel = lf("Record ({0})", targetTheme.simGifKey);
82
+ const stopRecordingLabel = lf("Stop recording ({0})", targetTheme.simGifKey) ;
83
+
84
+ const thumbnailLabel = uri ? lf("New Thumbnail") : lf("Current Thumbnail");
85
+ const classes = classList(
86
+ "gif-recorder-content",
87
+ uri && "has-uri"
88
+ )
89
+
90
+ return <>
91
+ <div className={classes}>
92
+ <div className="gif-recorder-sim">
93
+ <div className="gif-recorder-sim-embed">
94
+ {React.createElement(simRecorder, { onSimRecorderInit: handleSimRecorderRef })}
95
+ </div>
96
+ <div className="gif-recorder">
97
+ <div className="gif-recorder-actions">
98
+ <Button className="teal inverted"
99
+ title={screenshotLabel}
100
+ label={screenshotLabel}
101
+ leftIcon="fas fa-camera"
102
+ onClick={handleScreenshotClick} />
103
+ <Button className="teal inverted"
104
+ title={recorderState === "recording" ? stopRecordingLabel : startRecordingLabel}
105
+ label={recorderState === "recording" ? stopRecordingLabel : startRecordingLabel}
106
+ leftIcon={`fas fa-${recorderState === "recording" ? "square" : "circle"}`}
107
+ onClick={handleRecordClick} />
108
+ <div className="spacer mobile-only" />
109
+ <Button className="mobile-only"
110
+ title={lf("Cancel")}
111
+ label={lf("Cancel")}
112
+ onClick={onCancel} />
113
+ </div>
114
+ </div>
115
+ </div>
116
+ <div className="thumbnail-controls">
117
+ <div className="thumbnail-preview">
118
+ <div>
119
+ <div className="thumbnail-header">
120
+ <span className="project-share-label">{thumbnailLabel}</span>
121
+ <Button
122
+ className="link-button mobile-only"
123
+ title={lf("Try again?")}
124
+ label={lf("Try again?")}
125
+ onClick={() => setUri(undefined)} />
126
+ </div>
127
+ <div className="thumbnail-image">
128
+ {(uri || initialUri)
129
+ ? <img src={uri || initialUri} />
130
+ : <div className="thumbnail-placeholder" />
131
+ }
132
+ </div>
133
+ </div>
134
+ </div>
135
+ <div className="thumbnail-actions">
136
+ {uri &&
137
+ <Button className="primary"
138
+ title={lf("Apply")}
139
+ label={lf("Apply")}
140
+ onClick={handleApplyClick} />
141
+ }
142
+ <Button title={lf("Cancel")}
143
+ label={lf("Cancel")}
144
+ onClick={onCancel} />
145
+ </div>
146
+ </div>
147
+ </div>
148
+ </>
149
+ }