- }
- // @ts-ignore -- I couldn't find a way to fix how this is typed
- // this is part of React's ugly insides
- as="label"
- htmlFor="file-upload"
- className={
- currentTab === "upload" ? subtleButtonActive : subtleButton
- }
- >
- Upload audio
-
-
+ <>{content.length > 0 ? content : "No audio available for this word."}>
)
}
-function StatusMessage({
- children,
- error = false,
-}: {
- children: ReactNode
- error?: boolean
+export function ContributorEditWordAudio(p: {
+ word: Dailp.FormFieldsFragment
}) {
- return (
-
- {children}
-
- )
-}
-
-function UploadAudioSection({
- selectedFile,
- uploadAudio,
-}: {
- selectedFile?: File
- uploadAudio: (data: Blob) => Promise
-}): ReactElement {
- const selectedFileDataUrl = useMemo(
- () => selectedFile && window.URL.createObjectURL(selectedFile),
- [selectedFile]
- )
-
- if (!selectedFileDataUrl || !selectedFile)
- return (
-
- No file selected! Hit upload file above to select a file from your
- computer.
-
- )
-
- return (
- <>
-
- Below is the audio you selected. If you would like to select a different
- file click "Upload audio" again.
-
-
- {
- uploadAudio(selectedFile)
- }}
- >
- Save audio
-
- >
- )
-}
-
-function RecordAudioSection({
- uploadAudio,
-}: {
- uploadAudio: (data: Blob) => Promise
-}): ReactElement {
- const {
- permissionStatus,
- recordingStatus,
- startRecording,
- stopRecording,
- clearRecording,
- requestMediaPermissions,
- } = useMediaRecorder()
-
- useEffect(() => {
- if (permissionStatus === MediaPermissionStatus.UNREQUESTED)
- requestMediaPermissions()
- }, [permissionStatus])
-
- if (permissionStatus !== MediaPermissionStatus.APPROVED)
- return (
-
- You need to allow DAILP to use your microphone in order to record audio.
-
- )
-
return (
-
- Below are the controls to record audio on your device now. If you are
- happy with your recording, you can hit "Save audio" below.
-
- {recordingStatus.lastRecording === undefined ? (
-
- recordingStatus.isRecording ? stopRecording() : startRecording()
- }
- >
- {recordingStatus.isRecording ? "Stop" : "Start"} recording
-
- ) : (
- <>
-
-
+ )
+}
diff --git a/website/src/components/edit-word-audio/upload.tsx b/website/src/components/edit-word-audio/upload.tsx
new file mode 100644
index 00000000..50166d4f
--- /dev/null
+++ b/website/src/components/edit-word-audio/upload.tsx
@@ -0,0 +1,124 @@
+import { ChangeEvent, ReactElement, useMemo, useState } from "react"
+import { MdUploadFile } from "react-icons/md/index"
+import { VisuallyHidden } from "reakit"
+import * as Dailp from "src/graphql/dailp"
+import { AudioPlayer } from "../audio-player"
+import { CleanButton, IconTextButton } from "../button"
+import { subtleButton } from "../subtle-button.css"
+import { ContributeAudioPanel } from "./contributor-audio-panel"
+import { contributeAudioOptions } from "./contributor.css"
+
+export function UploadAudioPanel(p: { word: Dailp.FormFieldsFragment }) {
+ return (
+
+ )
+}
+
+const ALLOWED_EXTENSIONS = ["mp3"]
+
+export function UploadAudioContent({
+ uploadAudio,
+}: {
+ uploadAudio: (data: Blob) => Promise
+}): ReactElement {
+ const [selectedFile, setSelectedFile] = useState()
+
+ function onFileChanged(event: ChangeEvent) {
+ if (!event.currentTarget.files) return
+
+ const [file] = event.currentTarget.files
+
+ if (!file) return
+
+ const fileExtension = file.name.split(".").pop()
+ if (!fileExtension) return
+
+ if (!ALLOWED_EXTENSIONS.includes(fileExtension)) {
+ window.alert(
+ `The file you've selected does not have a supported extension (${fileExtension}). The following formats are supported:\n\t${ALLOWED_EXTENSIONS.join(
+ "\n\t"
+ )}`
+ )
+ return
+ }
+
+ setSelectedFile(file)
+ }
+
+ async function uploadAudioAndReset(data: Blob) {
+ const success = await uploadAudio(data)
+ if (success) {
+ setSelectedFile(undefined)
+ }
+ }
+
+ const selectedFileDataUrl = useMemo(
+ () => selectedFile && window.URL.createObjectURL(selectedFile),
+ [selectedFile]
+ )
+
+ if (!selectedFileDataUrl || !selectedFile)
+ return (
+ <>
+
+ "." + s).join(",")}
+ />
+
+ }
+ className={subtleButton}
+ // @ts-ignore -- I couldn't find a way to fix how this is typed
+ // this is part of React's ugly insides
+ as="label"
+ htmlFor="file-upload"
+ >
+ Upload file
+
+
+ No file selected! Hit upload file above to select a file from your
+ computer.
+
+ >
+ )
+
+ return (
+ <>
+
+
+
+
+ Below is the audio you selected. If you would like to select a different
+ file click "Upload audio" again.
+
+
+
+ {
+ uploadAudioAndReset(selectedFile)
+ }}
+ >
+ Save audio
+
+
+ Choose a different file
+
+
+ >
+ )
+}
diff --git a/website/src/components/edit-word-audio/utils.ts b/website/src/components/edit-word-audio/utils.ts
new file mode 100644
index 00000000..a6410a27
--- /dev/null
+++ b/website/src/components/edit-word-audio/utils.ts
@@ -0,0 +1,100 @@
+import { CognitoIdentityClient } from "@aws-sdk/client-cognito-identity"
+import { PutObjectCommand, S3Client } from "@aws-sdk/client-s3"
+import { fromCognitoIdentityPool } from "@aws-sdk/credential-provider-cognito-identity"
+import { CognitoUser } from "amazon-cognito-identity-js"
+import { useMemo, useState } from "react"
+import { v4 } from "uuid"
+import { useUser } from "src/auth"
+import * as Dailp from "../../graphql/dailp"
+
+type UploadAudioState = "ready" | "uploading" | "error"
+
+export function useAudioUpload(wordId: string) {
+ const [uploadAudioState, setUploadAudioState] =
+ useState("ready")
+ const { user } = useUser()
+ const [_contributeAudioResult, contributeAudio] =
+ Dailp.useAttachAudioToWordMutation()
+
+ /**
+ * Try to upload the audio.
+ *
+ * Returns success boolean and updates state to ready or error
+ */
+ const uploadAudio = useMemo(
+ () =>
+ async function (data: Blob) {
+ setUploadAudioState("uploading")
+ try {
+ const { resourceUrl } = await uploadContributorAudioToS3(user!, data)
+ // const resourceUrl = "https://" + prompt("url?")
+ const result = await contributeAudio({
+ input: {
+ wordId,
+ contributorAudioUrl: resourceUrl,
+ },
+ })
+ if (result.error) {
+ console.log(result.error)
+ setUploadAudioState("error")
+ return false
+ }
+ } catch (error) {
+ console.log(error)
+ setUploadAudioState("error")
+ return false
+ }
+
+ setUploadAudioState("ready")
+ return true
+ },
+ [user, contributeAudio, wordId]
+ )
+
+ function clearError() {
+ // only map "error" -> "ready" otherwise do nothing
+ setUploadAudioState((s) => (s === "error" ? "ready" : s))
+ }
+
+ // `as const` makes sure this gets typed as a tuple, not a list with a union type
+ return [uploadAudio, uploadAudioState, clearError] as const
+}
+
+export async function uploadContributorAudioToS3(
+ user: CognitoUser,
+ data: Blob
+) {
+ // Get the Amazon Cognito ID token for the user. 'getToken()' below.
+ const REGION = process.env["DAILP_AWS_REGION"]
+ // TF Stage matches infra environment names: "dev" "prod" or "uat". If TF_STAGE not found, fall back to dev
+ const BUCKET = `dailp-${process.env["TF_STAGE"] || "dev"}-media-storage`
+ // let accessToken = user.getSignInUserSession()?.getAccessToken() // 'COGNITO_ID' has the format 'COGNITO_USER_POOL_ID'
+ // let loginData = {
+ // [COGNITO_ID]: token,
+ // }
+
+ const s3Client = new S3Client({
+ region: REGION,
+ credentials: fromCognitoIdentityPool({
+ identityPoolId: process.env["DAILP_IDENTITY_POOL"]!,
+ client: new CognitoIdentityClient({
+ region: REGION,
+ }),
+ logins: {
+ [`cognito-idp.${REGION}.amazonaws.com/${process.env["DAILP_USER_POOL"]}`]:
+ user.getSignInUserSession()?.getIdToken().getJwtToken() ?? "",
+ },
+ }),
+ })
+
+ const key = `user-uploaded-audio/${v4()}`
+ await s3Client.send(
+ new PutObjectCommand({
+ Body: data,
+ Bucket: BUCKET,
+ Key: key,
+ })
+ )
+
+ return { resourceUrl: `https://${process.env["CF_URL"]}/${key}` }
+}
diff --git a/website/src/edit-doc-data-panel.tsx b/website/src/edit-doc-data-panel.tsx
index 71e2ae81..d5ad32f7 100644
--- a/website/src/edit-doc-data-panel.tsx
+++ b/website/src/edit-doc-data-panel.tsx
@@ -115,7 +115,7 @@ export const EditDocPanel = (props: { document?: Dailp.AnnotatedDoc }) => {
{...form}
className={css.formInput}
name={["document", "title"]}
- disabled={!(userRole == UserRole.EDITOR)}
+ disabled={!(userRole == UserRole.Editor)}
/>
@@ -130,7 +130,7 @@ export const EditDocPanel = (props: { document?: Dailp.AnnotatedDoc }) => {
onChange={(date: any) => handleDateChange(date)}
value={selectedDate}
format="dd-MM-y"
- disabled={!(userRole == UserRole.EDITOR)}
+ disabled={!(userRole == UserRole.Editor)}
/>
>
diff --git a/website/src/edit-word-feature.tsx b/website/src/edit-word-feature.tsx
index 86659037..45177b11 100644
--- a/website/src/edit-word-feature.tsx
+++ b/website/src/edit-word-feature.tsx
@@ -94,7 +94,7 @@ export const EditWordFeature = (props: {
as={props.input ? props.input : "input"}
className={css.formInput}
name={["word", props.feature]}
- disabled={userRole == UserRole.READER}
+ disabled={userRole == UserRole.Reader}
/>
>
)
diff --git a/website/src/graphql/wordpress/index.ts b/website/src/graphql/wordpress/index.ts
index fa125fd3..c08e9959 100644
--- a/website/src/graphql/wordpress/index.ts
+++ b/website/src/graphql/wordpress/index.ts
@@ -63,6 +63,8 @@ export type ActionMonitorAction = ContentNode &
readonly enqueuedStylesheets: Maybe
/** The global unique identifier for this post. This currently matches the value stored in WP_Post->guid and the guid column in the "post_objects" database table. */
readonly guid: Maybe
+ /** Whether the action_monitor object is password protected. */
+ readonly hasPassword: Maybe
/** The globally unique identifier of the action_monitor object. */
readonly id: Scalars["ID"]
/** Whether the node is a Comment */
@@ -87,6 +89,8 @@ export type ActionMonitorAction = ContentNode &
readonly modified: Maybe
/** The GMT modified time for a post. If a post was recently updated the modified field will change to match the corresponding time in GMT. */
readonly modifiedGmt: Maybe
+ /** The password for the action_monitor object. */
+ readonly password: Maybe
/** Connection between the ActionMonitorAction type and the ActionMonitorAction type */
readonly preview: Maybe
/** The preview data of the post that triggered this action. */
@@ -2903,6 +2907,8 @@ export type MediaItem = ContentNode &
readonly fileSize: Maybe
/** The global unique identifier for this post. This currently matches the value stored in WP_Post->guid and the guid column in the "post_objects" database table. */
readonly guid: Maybe
+ /** Whether the attachment object is password protected. */
+ readonly hasPassword: Maybe
/** The globally unique identifier of the attachment object. */
readonly id: Scalars["ID"]
/** Whether the node is a Comment */
@@ -2946,6 +2952,8 @@ export type MediaItem = ContentNode &
readonly parentDatabaseId: Maybe
/** The globally unique identifier of the parent node. */
readonly parentId: Maybe
+ /** The password for the attachment object. */
+ readonly password: Maybe
/** The database id of the preview node */
readonly previewRevisionDatabaseId: Maybe
/** Whether the object is a node in the preview state */
@@ -4030,6 +4038,8 @@ export type Page = ContentNode &
readonly featuredImageId: Maybe
/** The global unique identifier for this post. This currently matches the value stored in WP_Post->guid and the guid column in the "post_objects" database table. */
readonly guid: Maybe
+ /** Whether the page object is password protected. */
+ readonly hasPassword: Maybe
/** The globally unique identifier of the page object. */
readonly id: Scalars["ID"]
/** Whether the node is a Comment */
@@ -4071,6 +4081,8 @@ export type Page = ContentNode &
readonly parentDatabaseId: Maybe
/** The globally unique identifier of the parent node. */
readonly parentId: Maybe
+ /** The password for the page object. */
+ readonly password: Maybe
/** Connection between the Page type and the page type */
readonly preview: Maybe
/** The database id of the preview node */
@@ -4542,6 +4554,8 @@ export type Post = ContentNode &
readonly featuredImageId: Maybe
/** The global unique identifier for this post. This currently matches the value stored in WP_Post->guid and the guid column in the "post_objects" database table. */
readonly guid: Maybe
+ /** Whether the post object is password protected. */
+ readonly hasPassword: Maybe
/** The globally unique identifier of the post object. */
readonly id: Scalars["ID"]
/** Whether the node is a Comment */
@@ -4570,6 +4584,8 @@ export type Post = ContentNode &
readonly modified: Maybe
/** The GMT modified time for a post. If a post was recently updated the modified field will change to match the corresponding time in GMT. */
readonly modifiedGmt: Maybe
+ /** The password for the post object. */
+ readonly password: Maybe
/** Whether the pings are open or closed for this particular post. */
readonly pingStatus: Maybe
/** URLs that have been pinged. */
diff --git a/website/src/panel-layout.tsx b/website/src/panel-layout.tsx
index 86799440..db490195 100644
--- a/website/src/panel-layout.tsx
+++ b/website/src/panel-layout.tsx
@@ -20,6 +20,8 @@ import { AudioPlayer, Button, IconButton } from "./components"
import { CommentSection } from "./components/comment-section"
import { CustomCreatable } from "./components/creatable"
import { EditWordAudio } from "./components/edit-word-audio"
+import { RecordAudioPanel } from "./components/edit-word-audio/record"
+import { UploadAudioPanel } from "./components/edit-word-audio/upload"
import { SubtleButton } from "./components/subtle-button"
import { EditButton, EditWordFeature } from "./edit-word-feature"
import { formInput } from "./edit-word-feature.css"
@@ -203,6 +205,8 @@ export const PanelContent = (p: {
word: Dailp.FormFieldsFragment
options: GroupedOption[]
}) => {
+ const userGroups = useCognitoUserGroups()
+
// what should be used to render word features? eg, syllabary, commentary, etc.
const PanelFeatureComponent =
p.panel === PanelType.EditWordPanel ? EditWordFeature : WordFeature
@@ -286,13 +290,23 @@ export const PanelContent = (p: {
<>
{(p.word.editedAudio.length || p.panel === PanelType.EditWordPanel) && (
}
icon={
}
/>
)}
+ {p.panel === PanelType.EditWordPanel &&
+ userGroups.includes(Dailp.UserGroup.Contributors) && (
+
+ )}
+ {p.panel === PanelType.EditWordPanel &&
+ userGroups.includes(Dailp.UserGroup.Editors) && (
+
+ )}