From 196b6c3bd8914237a006a83822e65b07f5f567bc Mon Sep 17 00:00:00 2001 From: Charlie McVicker Date: Thu, 25 Apr 2024 17:04:10 -0400 Subject: [PATCH 1/7] split contributor audio section into different accordians --- website/src/auth.tsx | 23 +- .../contributor-audio-panels.tsx | 91 +++++ .../edit-word-audio/contributor.tsx | 362 +----------------- .../src/components/edit-word-audio/index.tsx | 19 +- .../src/components/edit-word-audio/record.tsx | 90 +++++ .../src/components/edit-word-audio/upload.tsx | 91 +++++ .../src/components/edit-word-audio/utils.ts | 100 +++++ website/src/edit-doc-data-panel.tsx | 4 +- website/src/edit-word-feature.tsx | 2 +- website/src/panel-layout.tsx | 7 + 10 files changed, 424 insertions(+), 365 deletions(-) create mode 100644 website/src/components/edit-word-audio/contributor-audio-panels.tsx create mode 100644 website/src/components/edit-word-audio/record.tsx create mode 100644 website/src/components/edit-word-audio/upload.tsx create mode 100644 website/src/components/edit-word-audio/utils.ts diff --git a/website/src/auth.tsx b/website/src/auth.tsx index 07496bf1..3bb1b6bd 100644 --- a/website/src/auth.tsx +++ b/website/src/auth.tsx @@ -8,6 +8,7 @@ import { } from "amazon-cognito-identity-js" import React, { createContext, useContext, useEffect, useState } from "react" import { navigate } from "vite-plugin-ssr/client/router" +import { UserGroup } from "./graphql/dailp" type UserContextType = { user: CognitoUser | null @@ -320,24 +321,32 @@ export const useCredentials = () => { return creds ?? null } -export const useCognitoUserGroups = () => { +export const useCognitoUserGroups = (): UserGroup[] => { const { user } = useContext(UserContext) const groups: string[] = user?.getSignInUserSession()?.getIdToken().payload["cognito:groups"] ?? [] return groups + .map((g) => g.toUpperCase()) + .filter((g): g is UserGroup => + Object.values(UserGroup).includes(g as UserGroup) + ) } +/** + * A user has one and only one `role`. A role is typically a users most + * permissive `group`, or `READER` if they have no `groups`. + */ export enum UserRole { - READER = "READER", - CONTRIBUTOR = "CONTRIBUTOR", - EDITOR = "EDITOR", + Reader = "READER", + Contributor = "CONTRIBUTOR", + Editor = "EDITOR", } export function useUserRole(): UserRole { const groups = useCognitoUserGroups() - if (groups.includes("Editors")) return UserRole.EDITOR - else if (groups.includes("Contributors")) return UserRole.CONTRIBUTOR - else return UserRole.READER + if (groups.includes(UserGroup.Editors)) return UserRole.Editor + else if (groups.includes(UserGroup.Contributors)) return UserRole.Contributor + else return UserRole.Reader } export const useUserId = () => { diff --git a/website/src/components/edit-word-audio/contributor-audio-panels.tsx b/website/src/components/edit-word-audio/contributor-audio-panels.tsx new file mode 100644 index 00000000..a6b1217b --- /dev/null +++ b/website/src/components/edit-word-audio/contributor-audio-panels.tsx @@ -0,0 +1,91 @@ +import cx from "classnames" +import { ReactElement, ReactNode } from "react" +import { AiFillSound } from "react-icons/ai" +import * as Dailp from "src/graphql/dailp" +import { CollapsiblePanel } from "src/panel-layout" +import * as css from "../../panel-layout.css" +import { + contributeAudioContainer, + statusMessage, + statusMessageError, +} from "./contributor.css" +import { RecordAudioSection } from "./record" +import { UploadAudioSection } from "./upload" +import { useAudioUpload } from "./utils" + +export function ContributorAudioPanels(p: { word: Dailp.FormFieldsFragment }) { + return ( + <> + + } + icon={ + + } + /> + + } + icon={ + + } + /> + + ) +} + +type ContributeAudioComponent = (p: { + uploadAudio: (blob: Blob) => Promise +}) => ReactElement + +function ContributeAudioSection(p: { + word: Dailp.FormFieldsFragment + Component: ContributeAudioComponent +}): ReactElement { + const [uploadAudio, uploadState, clearUploadError] = useAudioUpload(p.word.id) + + return ( +
+ + + {uploadState === "uploading" && ( + +

Uploading...

+
+ )} + + {uploadState === "error" && ( + +

Something went wrong with the upload

+ +
+ )} +
+ ) +} + +function StatusMessage({ + children, + error = false, +}: { + children: ReactNode + error?: boolean +}) { + return ( +
+ {children} +
+ ) +} diff --git a/website/src/components/edit-word-audio/contributor.tsx b/website/src/components/edit-word-audio/contributor.tsx index f74ae9e5..8965a28a 100644 --- a/website/src/components/edit-word-audio/contributor.tsx +++ b/website/src/components/edit-word-audio/contributor.tsx @@ -1,102 +1,8 @@ -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 cx from "classnames" -import { - ChangeEvent, - Fragment, - ReactElement, - ReactNode, - useEffect, - useMemo, - useState, -} from "react" -import { MdRecordVoiceOver, MdUploadFile } from "react-icons/md/index" -import { VisuallyHidden } from "reakit" -import { v4 } from "uuid" -import { AudioPlayer, CleanButton } from ".." -import { useUser, useUserId } from "../../auth" +import { Fragment } from "react" +import { useUserId } from "src/auth" +import { WordAudio } from "src/panel-layout" +import { AudioPlayer } from "../" import * as Dailp from "../../graphql/dailp" -import { WordAudio } from "../../panel-layout" -import { - MediaPermissionStatus, - useMediaRecorder, -} from "../../use-media-recorder" -import { IconTextButton } from "../button" -import { SubtleButton } from "../subtle-button" -import { subtleButton, subtleButtonActive } from "../subtle-button.css" -import { - contributeAudioContainer, - contributeAudioOptions, - contributeAudioOptionsItem, - statusMessage, - statusMessageError, -} from "./contributor.css" - -type UploadAudioState = "ready" | "uploading" | "error" - -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 function ContributorEditWordAudio(p: { - word: Dailp.FormFieldsFragment -}) { - return ( - <> - - - - ) -} function AvailableAudioSection(p: { word: Dailp.FormFieldsFragment }) { const userId = useUserId() @@ -161,262 +67,26 @@ function AvailableAudioSection(p: { word: Dailp.FormFieldsFragment }) { ) - return ( - <> - {[curatedAudioContent, userAudioContent, otherUserAudioContent] - .filter((content) => content !== false) - .map((content, idx) => ( - - {idx > 0 &&
} - {content} -
- ))} - - ) -} - -function ContributeAudioSection(p: { - word: Dailp.FormFieldsFragment -}): ReactElement { - const [currentTab, setCurrentTab] = useState<"upload" | "record">() - const [selectedFile, setSelectedFile] = useState() - const [uploadAudio, uploadState, clearUploadError] = useAudioUpload(p.word.id) - - function onFileChanged(event: ChangeEvent) { - if (!event.currentTarget.files) return - - const [file] = event.currentTarget.files - if (!file) return - console.log("New file selected", file) - setSelectedFile(file) - setCurrentTab("upload") - } - - async function uploadAudioAndReset(data: Blob) { - const success = await uploadAudio(data) - if (success) { - setCurrentTab(undefined) - setSelectedFile(undefined) - } - } + const content = [curatedAudioContent, userAudioContent, otherUserAudioContent] + .filter((content) => content !== false) + .map((content, idx) => ( + + {idx > 0 &&
} + {content} +
+ )) return ( -
- - - -
-
- } - // @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 - -
-
- } - className={ - currentTab === "record" ? subtleButtonActive : subtleButton - } - onClick={() => { - setCurrentTab("record") - }} - > - Record audio - -
-
- - {currentTab === "upload" && ( - - )} - - {currentTab === "record" && ( - - )} - - {uploadState === "uploading" && ( - -

Uploading...

-
- )} - - {uploadState === "error" && ( - -

Something went wrong with the upload

- -
- )} -
+ <>{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 - - ) : ( - <> - -
- { - uploadAudio(recordingStatus.lastRecording!.data) - }} - > - Save audio - - { - clearRecording() - }} - > - Delete recording - -
- - )} +
) } - -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/components/edit-word-audio/index.tsx b/website/src/components/edit-word-audio/index.tsx index 46234f2c..072ed445 100644 --- a/website/src/components/edit-word-audio/index.tsx +++ b/website/src/components/edit-word-audio/index.tsx @@ -1,4 +1,5 @@ import { ReactElement } from "react" +import { WordAudio } from "src/panel-layout" import { UserRole, useCognitoUserGroups, useUserRole } from "../../auth" import * as Dailp from "../../graphql/dailp" import { ContributorEditWordAudio } from "./contributor" @@ -7,17 +8,17 @@ import { EditorEditWordAudio } from "./editor" export const EditWordAudio = (p: { word: Dailp.FormFieldsFragment }): ReactElement => { - const groups = useCognitoUserGroups() - const userRole = useUserRole() + const role = useUserRole() - console.log("Branching based on", { groups, userRole }) + console.log("Branching based on", { role }) - switch (userRole) { - case UserRole.CONTRIBUTOR: - return - case UserRole.EDITOR: + switch (role) { + case UserRole.Editor: return - case UserRole.READER: - return
You do not have permission to edit word audio.
+ + case UserRole.Contributor: + return + case UserRole.Reader: + return } } diff --git a/website/src/components/edit-word-audio/record.tsx b/website/src/components/edit-word-audio/record.tsx new file mode 100644 index 00000000..d964b42c --- /dev/null +++ b/website/src/components/edit-word-audio/record.tsx @@ -0,0 +1,90 @@ +import { ReactElement, useEffect } from "react" +import { FaRegStopCircle } from "react-icons/fa" +import { RiRecordCircleFill } from "react-icons/ri" +import { AudioPlayer, CleanButton } from ".." +import { + MediaPermissionStatus, + useMediaRecorder, +} from "../../use-media-recorder" +import { IconTextButton } from "../button" +import { SubtleButton } from "../subtle-button" +import { subtleButton } from "../subtle-button.css" +import { contributeAudioOptions } from "./contributor.css" + +export function RecordAudioSection({ + uploadAudio, +}: { + uploadAudio: (data: Blob) => Promise +}): ReactElement { + const { + permissionStatus, + recordingStatus, + startRecording, + stopRecording, + clearRecording, + requestMediaPermissions, + } = useMediaRecorder() + + useEffect(() => { + if (permissionStatus === MediaPermissionStatus.UNREQUESTED) + requestMediaPermissions() + }, [permissionStatus]) + + async function uploadAudioAndReset(data: Blob) { + if (await uploadAudio(data)) { + clearRecording?.() + } + } + + if (permissionStatus !== MediaPermissionStatus.APPROVED) + return ( + + You need to allow DAILP to use your microphone in order to record audio. + + ) + + return ( +
+ {recordingStatus.lastRecording === undefined ? ( + + ) : ( + + ) + } + onClick={() => + recordingStatus.isRecording ? stopRecording() : startRecording() + } + className={subtleButton} + > + {recordingStatus.isRecording ? "Stop" : "Start"} recording + + ) : ( + <> +
+ { + uploadAudioAndReset(recordingStatus.lastRecording!.data) + }} + > + Save audio + + { + clearRecording() + }} + > + Delete 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..aef37883 --- /dev/null +++ b/website/src/components/edit-word-audio/upload.tsx @@ -0,0 +1,91 @@ +import { ChangeEvent, ReactElement, useMemo, useState } from "react" +import { MdUploadFile } from "react-icons/md" +import { VisuallyHidden } from "reakit" +import { AudioPlayer } from "../audio-player" +import { CleanButton, IconTextButton } from "../button" +import { subtleButton } from "../subtle-button.css" +import { contributeAudioOptions } from "./contributor.css" + +export function UploadAudioSection({ + 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 + + 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 ( + <> + + + + } + 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/panel-layout.tsx b/website/src/panel-layout.tsx index 86799440..926a5a42 100644 --- a/website/src/panel-layout.tsx +++ b/website/src/panel-layout.tsx @@ -20,6 +20,7 @@ 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 { ContributorAudioPanels } from "./components/edit-word-audio/contributor-audio-panels" import { SubtleButton } from "./components/subtle-button" import { EditButton, EditWordFeature } from "./edit-word-feature" import { formInput } from "./edit-word-feature.css" @@ -203,6 +204,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 @@ -293,6 +296,10 @@ export const PanelContent = (p: { } /> )} + {p.panel === PanelType.EditWordPanel && + userGroups.includes(Dailp.UserGroup.Contributors) && ( + + )} Date: Thu, 25 Apr 2024 17:04:26 -0400 Subject: [PATCH 2/7] wp graphql spam --- website/src/graphql/wordpress/index.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) 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. */ From 928c441614ea91fffbb356d900ec19512e741ef0 Mon Sep 17 00:00:00 2001 From: Charlie McVicker Date: Sat, 27 Apr 2024 12:03:13 -0400 Subject: [PATCH 3/7] uploading files is for editors --- ...panels.tsx => contributor-audio-panel.tsx} | 52 +++++++------------ .../src/components/edit-word-audio/record.tsx | 18 ++++++- .../src/components/edit-word-audio/upload.tsx | 15 +++++- website/src/panel-layout.tsx | 9 +++- 4 files changed, 57 insertions(+), 37 deletions(-) rename website/src/components/edit-word-audio/{contributor-audio-panels.tsx => contributor-audio-panel.tsx} (56%) diff --git a/website/src/components/edit-word-audio/contributor-audio-panels.tsx b/website/src/components/edit-word-audio/contributor-audio-panel.tsx similarity index 56% rename from website/src/components/edit-word-audio/contributor-audio-panels.tsx rename to website/src/components/edit-word-audio/contributor-audio-panel.tsx index a6b1217b..129e3068 100644 --- a/website/src/components/edit-word-audio/contributor-audio-panels.tsx +++ b/website/src/components/edit-word-audio/contributor-audio-panel.tsx @@ -1,6 +1,6 @@ import cx from "classnames" import { ReactElement, ReactNode } from "react" -import { AiFillSound } from "react-icons/ai" +import { IconType } from "react-icons" import * as Dailp from "src/graphql/dailp" import { CollapsiblePanel } from "src/panel-layout" import * as css from "../../panel-layout.css" @@ -9,38 +9,20 @@ import { statusMessage, statusMessageError, } from "./contributor.css" -import { RecordAudioSection } from "./record" -import { UploadAudioSection } from "./upload" import { useAudioUpload } from "./utils" -export function ContributorAudioPanels(p: { word: Dailp.FormFieldsFragment }) { +export function ContributeAudioPanel(p: { + panelTitle: string + Icon: IconType + Component: ContributeAudioComponent + word: Dailp.FormFieldsFragment +}) { return ( - <> - - } - icon={ - - } - /> - - } - icon={ - - } - /> - + } + icon={} + /> ) } @@ -48,6 +30,10 @@ type ContributeAudioComponent = (p: { uploadAudio: (blob: Blob) => Promise }) => ReactElement +/** + * This wrapper component provides upload functionality and feedback for an + * audio contribution component. + */ function ContributeAudioSection(p: { word: Dailp.FormFieldsFragment Component: ContributeAudioComponent @@ -66,8 +52,10 @@ function ContributeAudioSection(p: { {uploadState === "error" && ( -

Something went wrong with the upload

- +

+ Something went wrong with the upload{" "} + +

)} diff --git a/website/src/components/edit-word-audio/record.tsx b/website/src/components/edit-word-audio/record.tsx index d964b42c..d456a91e 100644 --- a/website/src/components/edit-word-audio/record.tsx +++ b/website/src/components/edit-word-audio/record.tsx @@ -1,7 +1,9 @@ import { ReactElement, useEffect } from "react" import { FaRegStopCircle } from "react-icons/fa" +import { FaMicrophone } from "react-icons/fa" import { RiRecordCircleFill } from "react-icons/ri" -import { AudioPlayer, CleanButton } from ".." +import { AudioPlayer } from ".." +import * as Dailp from "../../graphql/dailp" import { MediaPermissionStatus, useMediaRecorder, @@ -9,9 +11,21 @@ import { import { IconTextButton } from "../button" import { SubtleButton } from "../subtle-button" import { subtleButton } from "../subtle-button.css" +import { ContributeAudioPanel } from "./contributor-audio-panel" import { contributeAudioOptions } from "./contributor.css" -export function RecordAudioSection({ +export function RecordAudioPanel(p: { word: Dailp.FormFieldsFragment }) { + return ( + + ) +} + +function RecordAudioContent({ uploadAudio, }: { uploadAudio: (data: Blob) => Promise diff --git a/website/src/components/edit-word-audio/upload.tsx b/website/src/components/edit-word-audio/upload.tsx index aef37883..32ade719 100644 --- a/website/src/components/edit-word-audio/upload.tsx +++ b/website/src/components/edit-word-audio/upload.tsx @@ -1,12 +1,25 @@ import { ChangeEvent, ReactElement, useMemo, useState } from "react" import { MdUploadFile } from "react-icons/md" 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 UploadAudioSection({ +export function UploadAudioPanel(p: { word: Dailp.FormFieldsFragment }) { + return ( + + ) +} + +export function UploadAudioContent({ uploadAudio, }: { uploadAudio: (data: Blob) => Promise diff --git a/website/src/panel-layout.tsx b/website/src/panel-layout.tsx index 926a5a42..51d1351d 100644 --- a/website/src/panel-layout.tsx +++ b/website/src/panel-layout.tsx @@ -20,7 +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 { ContributorAudioPanels } from "./components/edit-word-audio/contributor-audio-panels" +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" @@ -298,7 +299,11 @@ export const PanelContent = (p: { )} {p.panel === PanelType.EditWordPanel && userGroups.includes(Dailp.UserGroup.Contributors) && ( - + + )} + {p.panel === PanelType.EditWordPanel && + userGroups.includes(Dailp.UserGroup.Editors) && ( + )} Date: Sat, 27 Apr 2024 12:04:06 -0400 Subject: [PATCH 4/7] clean up styling now that sections are smaller --- website/src/components/edit-word-audio/contributor.css.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/src/components/edit-word-audio/contributor.css.ts b/website/src/components/edit-word-audio/contributor.css.ts index 85314a52..88efa4f0 100644 --- a/website/src/components/edit-word-audio/contributor.css.ts +++ b/website/src/components/edit-word-audio/contributor.css.ts @@ -23,7 +23,7 @@ export const statusMessage = style({ bottom: 0, backgroundColor: "rgba(255,255,255,0.65)", color: "black", - padding: 40, + padding: 10, backdropFilter: "blur(2px)", border: "4px solid grey", }) From 730708722f5f5f40f7a87272a184e1d6f103014b Mon Sep 17 00:00:00 2001 From: Charlie McVicker Date: Sat, 27 Apr 2024 12:19:03 -0400 Subject: [PATCH 5/7] restrict which file extensions can be uploaded --- .../src/components/edit-word-audio/upload.tsx | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/website/src/components/edit-word-audio/upload.tsx b/website/src/components/edit-word-audio/upload.tsx index 32ade719..ab272f4b 100644 --- a/website/src/components/edit-word-audio/upload.tsx +++ b/website/src/components/edit-word-audio/upload.tsx @@ -19,6 +19,8 @@ export function UploadAudioPanel(p: { word: Dailp.FormFieldsFragment }) { ) } +const ALLOWED_EXTENSIONS = ["mp3"] + export function UploadAudioContent({ uploadAudio, }: { @@ -30,8 +32,21 @@ export function UploadAudioContent({ 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) } @@ -51,7 +66,12 @@ export function UploadAudioContent({ return ( <> - + "." + s).join(",")} + /> } From 3c3bd8562fcc96eaf8b78153b8b2d0df73156060 Mon Sep 17 00:00:00 2001 From: Charlie McVicker Date: Mon, 29 Apr 2024 14:39:16 -0400 Subject: [PATCH 6/7] title panel "contributed audio" in edit view --- website/src/panel-layout.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/website/src/panel-layout.tsx b/website/src/panel-layout.tsx index 51d1351d..db490195 100644 --- a/website/src/panel-layout.tsx +++ b/website/src/panel-layout.tsx @@ -290,7 +290,9 @@ export const PanelContent = (p: { <> {(p.word.editedAudio.length || p.panel === PanelType.EditWordPanel) && ( } icon={ From e6f2dc2a73d9ef090c23001ff5785277dc3c39d3 Mon Sep 17 00:00:00 2001 From: Charlie McVicker Date: Tue, 30 Apr 2024 11:31:55 -0400 Subject: [PATCH 7/7] add /index to react-icons imports --- website/src/components/edit-word-audio/record.tsx | 5 ++--- website/src/components/edit-word-audio/upload.tsx | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/website/src/components/edit-word-audio/record.tsx b/website/src/components/edit-word-audio/record.tsx index d456a91e..733c62be 100644 --- a/website/src/components/edit-word-audio/record.tsx +++ b/website/src/components/edit-word-audio/record.tsx @@ -1,7 +1,6 @@ import { ReactElement, useEffect } from "react" -import { FaRegStopCircle } from "react-icons/fa" -import { FaMicrophone } from "react-icons/fa" -import { RiRecordCircleFill } from "react-icons/ri" +import { FaMicrophone, FaRegStopCircle } from "react-icons/fa/index" +import { RiRecordCircleFill } from "react-icons/ri/index" import { AudioPlayer } from ".." import * as Dailp from "../../graphql/dailp" import { diff --git a/website/src/components/edit-word-audio/upload.tsx b/website/src/components/edit-word-audio/upload.tsx index ab272f4b..50166d4f 100644 --- a/website/src/components/edit-word-audio/upload.tsx +++ b/website/src/components/edit-word-audio/upload.tsx @@ -1,5 +1,5 @@ import { ChangeEvent, ReactElement, useMemo, useState } from "react" -import { MdUploadFile } from "react-icons/md" +import { MdUploadFile } from "react-icons/md/index" import { VisuallyHidden } from "reakit" import * as Dailp from "src/graphql/dailp" import { AudioPlayer } from "../audio-player"