diff --git a/app/graphql/mutations/add_conversation_message.rb b/app/graphql/mutations/add_conversation_message.rb
index b4d06db81a837..fa06964ae1876 100644
--- a/app/graphql/mutations/add_conversation_message.rb
+++ b/app/graphql/mutations/add_conversation_message.rb
@@ -28,7 +28,7 @@ class Mutations::AddConversationMessage < Mutations::BaseMutation
argument :recipients, [String], required: true
argument :included_messages, [ID], required: false, prepare: GraphQLHelpers.relay_or_legacy_ids_prepare_func('ConversationMessage')
argument :attachment_ids, [ID], required: false, prepare: GraphQLHelpers.relay_or_legacy_ids_prepare_func('Attachment')
- argument :media_comment_id, ID, required: false, prepare: GraphQLHelpers.relay_or_legacy_id_prepare_func('MediaObject')
+ argument :media_comment_id, ID, required: false
argument :media_comment_type, String, required: false
argument :user_note, Boolean, required: false
diff --git a/app/graphql/mutations/create_conversation.rb b/app/graphql/mutations/create_conversation.rb
index 0accbce276e78..397826731c474 100644
--- a/app/graphql/mutations/create_conversation.rb
+++ b/app/graphql/mutations/create_conversation.rb
@@ -30,7 +30,7 @@ class Mutations::CreateConversation < Mutations::BaseMutation
argument :force_new, Boolean, required: false
argument :group_conversation, Boolean, required: false
argument :attachment_ids, [ID], required: false, prepare: GraphQLHelpers.relay_or_legacy_ids_prepare_func('Attachment')
- argument :media_comment_id, ID, required: false, prepare: GraphQLHelpers.relay_or_legacy_id_prepare_func('MediaObject')
+ argument :media_comment_id, ID, required: false
argument :media_comment_type, String, required: false
argument :context_code, String, required: false
argument :conversation_id, ID, required: false, prepare: GraphQLHelpers.relay_or_legacy_id_prepare_func('Conversation')
diff --git a/app/helpers/conversations_helper.rb b/app/helpers/conversations_helper.rb
index 5ca7db23042ac..b802e0c7f8471 100644
--- a/app/helpers/conversations_helper.rb
+++ b/app/helpers/conversations_helper.rb
@@ -220,23 +220,23 @@ def build_message_args(
attachment_ids: attachment_ids,
forwarded_message_ids: forwarded_message_ids,
root_account_id: domain_root_account_id,
- media_comment: infer_media_comment(media_comment_id, media_comment_type),
+ media_comment: infer_media_comment(media_comment_id, media_comment_type, domain_root_account_id, current_user),
generate_user_note: user_note
}
]
end
- def infer_media_comment(media_id, media_type)
+ def infer_media_comment(media_id, media_type, root_account_id, user)
if media_id.present? && media_type.present?
media_comment = MediaObject.by_media_id(media_id).first
unless media_comment
media_comment ||= MediaObject.new
media_comment.media_type = media_type
media_comment.media_id = media_id
- media_comment.root_account_id = @domain_root_account.id
- media_comment.user = @current_user
+ media_comment.root_account_id = root_account_id
+ media_comment.user = user
end
- media_comment.context = @current_user
+ media_comment.context = user
media_comment.save
media_comment
end
diff --git a/ui/features/inbox/react/components/ComposeActionButtons/ComposeActionButtons.js b/ui/features/inbox/react/components/ComposeActionButtons/ComposeActionButtons.js
index e1f449e6713da..fbfa4a58d9bda 100644
--- a/ui/features/inbox/react/components/ComposeActionButtons/ComposeActionButtons.js
+++ b/ui/features/inbox/react/components/ComposeActionButtons/ComposeActionButtons.js
@@ -65,6 +65,7 @@ const renderUploadButtons = props => {
onClick={props.onMediaUpload}
margin="xx-small"
data-testid="media-upload"
+ interaction={props.hasMediaComment ? 'disabled' : 'enabled'}
>
@@ -118,7 +119,11 @@ ComposeActionButtons.propTypes = {
/**
* Indicates that a message is currently being sent
*/
- isSending: PropTypes.bool.isRequired
+ isSending: PropTypes.bool.isRequired,
+ /**
+ * Indicates whether or not there is a media comment already attached
+ */
+ hasMediaComment: PropTypes.bool
}
export default ComposeActionButtons
diff --git a/ui/features/inbox/react/components/ComposeActionButtons/__tests__/ComposeActionButtons.test.js b/ui/features/inbox/react/components/ComposeActionButtons/__tests__/ComposeActionButtons.test.js
index 387ed922c1a3e..a31eeda86b9ec 100644
--- a/ui/features/inbox/react/components/ComposeActionButtons/__tests__/ComposeActionButtons.test.js
+++ b/ui/features/inbox/react/components/ComposeActionButtons/__tests__/ComposeActionButtons.test.js
@@ -57,6 +57,12 @@ describe('ComposeActionButtons', () => {
expect(queryByTestId('media-upload')).toBe(null)
})
})
+
+ it('disables the media upload button if hasMediaComment is true', () => {
+ const props = createProps({hasMediaComment: true})
+ const container = render()
+ expect(container.getByTestId('media-upload')).toBeDisabled()
+ })
})
describe('message cancel button', () => {
diff --git a/ui/features/inbox/react/containers/ComposeModalContainer/ComposeModalContainer.js b/ui/features/inbox/react/containers/ComposeModalContainer/ComposeModalContainer.js
index 1be30f4df8c42..a639dec1b122d 100644
--- a/ui/features/inbox/react/containers/ComposeModalContainer/ComposeModalContainer.js
+++ b/ui/features/inbox/react/containers/ComposeModalContainer/ComposeModalContainer.js
@@ -17,18 +17,20 @@
*/
import {AlertManagerContext} from '@canvas/alerts/react/AlertManager'
+import closedCaptionLanguages from '@canvas/util/closedCaptionLanguages'
import {ComposeActionButtons} from '../../components/ComposeActionButtons/ComposeActionButtons'
import {Conversation} from '../../../graphql/Conversation'
import HeaderInputs from './HeaderInputs'
import I18n from 'i18n!conversations_2'
+import {Modal} from '@instructure/ui-modal'
import ModalBody from './ModalBody'
import ModalHeader from './ModalHeader'
import ModalSpinner from './ModalSpinner'
import PropTypes from 'prop-types'
import React, {useContext, useState} from 'react'
import {uploadFiles} from '@canvas/upload-file'
-
-import {Modal} from '@instructure/ui-modal'
+import UploadMedia from '@instructure/canvas-media'
+import {MediaCaptureStrings, SelectStrings, UploadMediaStrings} from '../../../util/constants'
const ComposeModalContainer = props => {
const {setOnFailure, setOnSuccess} = useContext(AlertManagerContext)
@@ -40,6 +42,22 @@ const ComposeModalContainer = props => {
const [bodyMessages, setBodyMessages] = useState([])
const [sendIndividualMessages, setSendIndividualMessages] = useState(false)
const [selectedContext, setSelectedContext] = useState()
+ const [mediaUploadOpen, setMediaUploadOpen] = useState(false)
+ const [uploadingMediaFile, setUploadingMediaFile] = useState(false)
+ const [mediaUploadFile, setMediaUploadFile] = useState(null)
+
+ const onMediaUploadComplete = (err, data) => {
+ if (err) {
+ setOnFailure(I18n.t('There was an error uploading the media.'))
+ } else {
+ setUploadingMediaFile(false)
+ setMediaUploadFile(data)
+ }
+ }
+
+ const onRemoveMedia = () => {
+ setMediaUploadFile(null)
+ }
const fileUploadUrl = attachmentFolderId => {
return `/api/v1/folders/${attachmentFolderId}/files`
@@ -126,7 +144,9 @@ const ComposeModalContainer = props => {
body,
includedMessages: props.pastConversation?.conversationMessagesConnection.nodes.map(
c => c._id
- )
+ ),
+ mediaCommentId: mediaUploadFile?.mediaObject?.media_object?.media_id,
+ mediaCommentType: mediaUploadFile?.mediaObject?.media_object?.media_type
}
})
} else {
@@ -137,7 +157,9 @@ const ComposeModalContainer = props => {
contextCode: selectedContext,
recipients: ['5'], // TODO: replace this with selected users
subject,
- groupConversation: !sendIndividualMessages
+ groupConversation: !sendIndividualMessages,
+ mediaCommentId: mediaUploadFile?.mediaObject?.media_object?.media_id,
+ mediaCommentType: mediaUploadFile?.mediaObject?.media_object?.media_type
}
})
}
@@ -154,6 +176,7 @@ const ComposeModalContainer = props => {
props.setSendingMessage(false)
setSubject(null)
setSendIndividualMessages(false)
+ setMediaUploadFile(null)
}
return (
@@ -184,12 +207,14 @@ const ComposeModalContainer = props => {
onSubjectChange={onSubjectChange}
sendIndividualMessages={sendIndividualMessages}
subject={props.isReply ? props.pastConversation?.subject : subject}
+ mediaAttachmentTitle={mediaUploadFile?.uploadedFile.name}
+ onRemoveMediaComment={onRemoveMedia}
/>
{}}
+ onMediaUpload={() => setMediaUploadOpen(true)}
onCancel={props.onDismiss}
onSend={() => {
if (!validMessageFields()) {
@@ -202,19 +227,40 @@ const ComposeModalContainer = props => {
props.setSendingMessage(true)
}}
isSending={false}
+ hasMediaComment={!!mediaUploadFile}
/>
+ setUploadingMediaFile(true)}
+ onUploadComplete={onMediaUploadComplete}
+ onDismiss={() => setMediaUploadOpen(false)}
+ open={mediaUploadOpen}
+ tabs={{embed: false, record: true, upload: true}}
+ uploadMediaTranslations={{
+ UploadMediaStrings: UploadMediaStrings(),
+ MediaCaptureStrings: MediaCaptureStrings(),
+ SelectStrings: SelectStrings()
+ }}
+ liveRegion={() => document.getElementById('flash_screenreader_holder')}
+ languages={Object.keys(closedCaptionLanguages).map(key => {
+ return {id: key, label: closedCaptionLanguages[key]}
+ })}
+ rcsConfig={{
+ contextId: ENV.current_user_id,
+ contextType: 'user'
+ }}
+ />
sendMessage()}
- open={props.sendingMessage && !!attachmentsToUpload.length}
+ open={props.sendingMessage && !!attachmentsToUpload.length && uploadingMediaFile}
/>
>
)
diff --git a/ui/features/inbox/react/containers/ComposeModalContainer/HeaderInputs.js b/ui/features/inbox/react/containers/ComposeModalContainer/HeaderInputs.js
index f694358e207da..52ba7f0418390 100644
--- a/ui/features/inbox/react/containers/ComposeModalContainer/HeaderInputs.js
+++ b/ui/features/inbox/react/containers/ComposeModalContainer/HeaderInputs.js
@@ -26,10 +26,11 @@ import {reduceDuplicateCourses} from '../../../util/courses_helper'
import {SubjectInput} from '../../components/SubjectInput/SubjectInput'
import {Flex} from '@instructure/ui-flex'
+import {MediaAttachment} from '../../components/MediaAttachment/MediaAttachment'
import {PresentationContent} from '@instructure/ui-a11y-content'
import {Text} from '@instructure/ui-text'
-const HeaderInputs = (props) => {
+const HeaderInputs = props => {
let moreCourses
if (!props.isReply) {
moreCourses = reduceDuplicateCourses(
@@ -57,7 +58,7 @@ const HeaderInputs = (props) => {
favoriteCourses: props.courses?.favoriteCoursesConnection.nodes,
moreCourses,
concludedCourses: [],
- groups: props.courses?.favoriteGroupsConnection.nodes,
+ groups: props.courses?.favoriteGroupsConnection.nodes
}}
onCourseFilterSelect={props.onContextSelect}
/>
@@ -91,6 +92,19 @@ const HeaderInputs = (props) => {
/>
)}
+ {props.mediaAttachmentTitle && (
+
+
+ }
+ />
+
+ )}
)
}
@@ -104,6 +118,8 @@ HeaderInputs.propTypes = {
onSubjectChange: PropTypes.func,
sendIndividualMessages: PropTypes.bool,
subject: PropTypes.string,
+ mediaAttachmentTitle: PropTypes.string,
+ onRemoveMediaComment: PropTypes.func
}
export default HeaderInputs
diff --git a/ui/features/inbox/react/containers/ComposeModalContainer/__tests__/HeaderInputs.test.js b/ui/features/inbox/react/containers/ComposeModalContainer/__tests__/HeaderInputs.test.js
new file mode 100644
index 0000000000000..4e65d95c8de45
--- /dev/null
+++ b/ui/features/inbox/react/containers/ComposeModalContainer/__tests__/HeaderInputs.test.js
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2020 - present Instructure, Inc.
+ *
+ * This file is part of Canvas.
+ *
+ * Canvas is free software: you can redistribute it and/or modify it under
+ * the terms of the GNU Affero General Public License as published by the Free
+ * Software Foundation, version 3 of the License.
+ *
+ * Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
+ * details.
+ *
+ * You should have received a copy of the GNU Affero General Public License along
+ * with this program. If not, see .
+ */
+
+import {Course} from '../../../../graphql/Course'
+import {Enrollment} from '../../../../graphql/Enrollment'
+import {fireEvent, render} from '@testing-library/react'
+import {Group} from '../../../../graphql/Group'
+import HeaderInputs from '../HeaderInputs'
+import React from 'react'
+
+describe('HeaderInputs', () => {
+ const defaultProps = () => ({
+ courses: {
+ favoriteGroupsConnection: {
+ nodes: [Group.mock()]
+ },
+ favoriteCoursesConnection: {
+ nodes: [Course.mock()]
+ },
+ enrollments: [Enrollment.mock()]
+ },
+ onContextSelect: jest.fn(),
+ onSendIndividualMessagesChange: jest.fn(),
+ onSubjectChange: jest.fn(),
+ onRemoveMediaComment: jest.fn()
+ })
+
+ describe('Media Comments', () => {
+ it('does not render a media comment if one is not provided', () => {
+ const container = render()
+ expect(container.queryByTestId('media-attachment')).toBeNull()
+ })
+
+ it('does render a media comment if one is provided', () => {
+ const container = render(
+
+ )
+ expect(container.getByTestId('media-attachment')).toBeInTheDocument()
+ expect(container.getByText('I am Lord Lemon')).toBeInTheDocument()
+ })
+
+ it('calls the onRemoveMediaComment callback when the remove media button is clicked', () => {
+ const props = defaultProps()
+ const container = render(
+
+ )
+ const removeMediaButton = container.getByTestId('remove-media-attachment')
+ fireEvent.click(removeMediaButton)
+ expect(props.onRemoveMediaComment).toHaveBeenCalled()
+ })
+ })
+})
diff --git a/ui/features/inbox/react/containers/__tests__/ComposeModalContainer.test.js b/ui/features/inbox/react/containers/__tests__/ComposeModalContainer.test.js
index fe8fdddde32ef..e0f7e137982dc 100644
--- a/ui/features/inbox/react/containers/__tests__/ComposeModalContainer.test.js
+++ b/ui/features/inbox/react/containers/__tests__/ComposeModalContainer.test.js
@@ -126,6 +126,15 @@ describe('ComposeModalContainer', () => {
})
})
+ describe('Media', () => {
+ it('opens the media upload modal', async () => {
+ const container = setup()
+ const mediaButton = await container.findByTestId('media-upload')
+ fireEvent.click(mediaButton)
+ expect(await container.findByText('Upload Media')).toBeInTheDocument()
+ })
+ })
+
describe('Subject', () => {
it('allows setting the subject', async () => {
const {findByTestId} = setup()
diff --git a/ui/features/inbox/util/constants.js b/ui/features/inbox/util/constants.js
index c4511932567ee..d5f679a2f8cd4 100644
--- a/ui/features/inbox/util/constants.js
+++ b/ui/features/inbox/util/constants.js
@@ -15,5 +15,67 @@
* You should have received a copy of the GNU Affero General Public License along
* with this program. If not, see .
*/
+import I18n from 'i18n!conversations_2'
export const PARTICIPANT_EXPANSION_THRESHOLD = 2
+
+export const MediaCaptureStrings = () => ({
+ ARIA_VIDEO_LABEL: I18n.t('Video Player'),
+ ARIA_VOLUME: I18n.t('Current Volume Level'),
+ ARIA_RECORDING: I18n.t('Recording'),
+ DEFAULT_ERROR: I18n.t('Something went wrong accessing your mic or webcam.'),
+ DEVICE_AUDIO: I18n.t('Mic'),
+ DEVICE_VIDEO: I18n.t('Webcam'),
+ FILE_PLACEHOLDER: I18n.t('Untitled'),
+ FINISH: I18n.t('Finish'),
+ NO_WEBCAM: I18n.t('No Video'),
+ NOT_ALLOWED_ERROR: I18n.t('Please allow Canvas to access your microphone and webcam.'),
+ NOT_READABLE_ERROR: I18n.t('Your webcam may already be in use.'),
+ PLAYBACK_PAUSE: I18n.t('Pause'),
+ PLAYBACK_PLAY: I18n.t('Play'),
+ PREVIEW: I18n.t('PREVIEW'),
+ SAVE: I18n.t('Save'),
+ SR_FILE_INPUT: I18n.t('File name'),
+ START: I18n.t('Start Recording'),
+ START_OVER: I18n.t('Start Over')
+})
+
+export const UploadMediaStrings = () => ({
+ LOADING_MEDIA: I18n.t('Loading Media'),
+ PROGRESS_LABEL: I18n.t('Uploading media Progress'),
+ ADD_CLOSED_CAPTIONS_OR_SUBTITLES: I18n.t('Add CC/Subtitle'),
+ COMPUTER_PANEL_TITLE: I18n.t('Computer'),
+ DRAG_FILE_TEXT: I18n.t('Drag a File Here'),
+ RECORD_PANEL_TITLE: I18n.t('Record'),
+ EMBED_PANEL_TITLE: I18n.t('Embed'),
+ SUBMIT_TEXT: I18n.t('Submit'),
+ CLOSE_TEXT: I18n.t('Close'),
+ UPLOAD_MEDIA_LABEL: I18n.t('Upload Media'),
+ CLEAR_FILE_TEXT: I18n.t('Clear selected file'),
+ INVALID_FILE_TEXT: I18n.t('Invalid file type'),
+ DRAG_DROP_CLICK_TO_BROWSE: I18n.t('Drag and drop, or click to browse your computer'),
+ EMBED_VIDEO_CODE_TEXT: I18n.t('Embed Video Code'),
+ UPLOADING_ERROR: I18n.t('Error uploading video/audio recording'),
+ CLOSED_CAPTIONS_PANEL_TITLE: I18n.t('CC/Subtitles'),
+ CLOSED_CAPTIONS_LANGUAGE_HEADER: I18n.t('Language'),
+ CLOSED_CAPTIONS_FILE_NAME_HEADER: I18n.t('File Name'),
+ CLOSED_CAPTIONS_ACTIONS_HEADER: I18n.t('Actions'),
+ CLOSED_CAPTIONS_ADD_SUBTITLE: I18n.t('Subtitle'),
+ CLOSED_CAPTIONS_ADD_SUBTITLE_SCREENREADER: I18n.t('Add Subtitle'),
+ CLOSED_CAPTIONS_CHOOSE_FILE: I18n.t('Choose File'),
+ CLOSED_CAPTIONS_SELECT_LANGUAGE: I18n.t('Select Language'),
+ MEDIA_RECORD_NOT_AVAILABLE: I18n.t('Media record not available'),
+ ADDED_CAPTION: I18n.t('Added caption'),
+ DELETED_CAPTION: I18n.t('Deleted caption'),
+ REMOVE_FILE: I18n.t('Remove file'),
+ NO_FILE_CHOSEN: I18n.t('No file selected'),
+ SUPPORTED_FILE_TYPES: I18n.t('Supported file types: .vtt, .srt'),
+ ADD_NEW_CAPTION_OR_SUBTITLE: I18n.t('Add new caption or subtitle')
+})
+
+export const SelectStrings = () => ({
+ USE_ARROWS: I18n.t('Use Arrows'),
+ LIST_COLLAPSED: I18n.t('List Collapsed'),
+ LIST_EXPANDED: I18n.t('List Expanded'),
+ OPTION_SELECTED: I18n.t('{option} Selected')
+})