Skip to content

Commit

Permalink
add media comment to compose modal
Browse files Browse the repository at this point in the history
fixes VICE-1090
flag=react_inbox

Test Plan:
- Enable react inbox feature flag
- Follow the steps in the following confluence doc to get kaltura
  working locally
- https://instructure.atlassian.net/wiki/spaces/ENG/pages/45645877/Enable+the+Notorious+Plugin+Replacement+for+Kaltura
- Navigate to the inbox and open the compose modal
- Click the "add media comment" button on the bottom left of the
  compose modal
- record or upload a media file
- The media uplaod should display below the rest of the header inputs
- Click the x button to remove the media comment
- Create a conversation with a media comment
- Inspect the graphql response and note the presence of the media comment

Change-Id: I6a9af8f1d0ba6473ee8262a148ccd5561dda1336
Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/275995
Tested-by: Service Cloud Jenkins <[email protected]>
Reviewed-by: Rob Orton <[email protected]>
Reviewed-by: Drake Harper <[email protected]>
Product-Review: Drake Harper <[email protected]>
QA-Review: Drake Harper <[email protected]>
  • Loading branch information
mlemon-instructure committed Oct 18, 2021
1 parent e069e51 commit 8b5d979
Show file tree
Hide file tree
Showing 10 changed files with 229 additions and 18 deletions.
2 changes: 1 addition & 1 deletion app/graphql/mutations/add_conversation_message.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion app/graphql/mutations/create_conversation.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down
10 changes: 5 additions & 5 deletions app/helpers/conversations_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ const renderUploadButtons = props => {
onClick={props.onMediaUpload}
margin="xx-small"
data-testid="media-upload"
interaction={props.hasMediaComment ? 'disabled' : 'enabled'}
>
<IconAttachMediaLine />
</IconButton>
Expand Down Expand Up @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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(<ComposeActionButtons {...props} />)
expect(container.getByTestId('media-upload')).toBeDisabled()
})
})

describe('message cancel button', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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`
Expand Down Expand Up @@ -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 {
Expand All @@ -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
}
})
}
Expand All @@ -154,6 +176,7 @@ const ComposeModalContainer = props => {
props.setSendingMessage(false)
setSubject(null)
setSendIndividualMessages(false)
setMediaUploadFile(null)
}

return (
Expand Down Expand Up @@ -184,12 +207,14 @@ const ComposeModalContainer = props => {
onSubjectChange={onSubjectChange}
sendIndividualMessages={sendIndividualMessages}
subject={props.isReply ? props.pastConversation?.subject : subject}
mediaAttachmentTitle={mediaUploadFile?.uploadedFile.name}
onRemoveMediaComment={onRemoveMedia}
/>
</ModalBody>
<Modal.Footer>
<ComposeActionButtons
onAttachmentUpload={addAttachment}
onMediaUpload={() => {}}
onMediaUpload={() => setMediaUploadOpen(true)}
onCancel={props.onDismiss}
onSend={() => {
if (!validMessageFields()) {
Expand All @@ -202,19 +227,40 @@ const ComposeModalContainer = props => {
props.setSendingMessage(true)
}}
isSending={false}
hasMediaComment={!!mediaUploadFile}
/>
</Modal.Footer>
</Modal>
<UploadMedia
onStartUpload={() => 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'
}}
/>
<ModalSpinner
label={I18n.t('Sending Message')}
message={I18n.t('Sending Message')}
open={props.sendingMessage && !attachmentsToUpload.length}
open={props.sendingMessage && !attachmentsToUpload.length && !uploadingMediaFile}
/>
<ModalSpinner
label={I18n.t('Uploading Files')}
message={I18n.t('Please wait while we upload attachments')}
message={I18n.t('Please wait while we upload attachments and media')}
onExited={() => sendMessage()}
open={props.sendingMessage && !!attachmentsToUpload.length}
open={props.sendingMessage && !!attachmentsToUpload.length && uploadingMediaFile}
/>
</>
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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}
/>
Expand Down Expand Up @@ -91,6 +92,19 @@ const HeaderInputs = (props) => {
/>
</Flex.Item>
)}
{props.mediaAttachmentTitle && (
<Flex.Item data-testid="media-attachment">
<ComposeInputWrapper
shouldGrow
input={
<MediaAttachment
mediaTitle={props.mediaAttachmentTitle}
onRemoveMedia={props.onRemoveMediaComment}
/>
}
/>
</Flex.Item>
)}
</Flex>
)
}
Expand All @@ -104,6 +118,8 @@ HeaderInputs.propTypes = {
onSubjectChange: PropTypes.func,
sendIndividualMessages: PropTypes.bool,
subject: PropTypes.string,
mediaAttachmentTitle: PropTypes.string,
onRemoveMediaComment: PropTypes.func
}

export default HeaderInputs
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.
*/

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(<HeaderInputs {...defaultProps()} />)
expect(container.queryByTestId('media-attachment')).toBeNull()
})

it('does render a media comment if one is provided', () => {
const container = render(
<HeaderInputs {...defaultProps()} mediaAttachmentTitle="I am Lord Lemon" />
)
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(
<HeaderInputs {...props} mediaAttachmentTitle="No really I am Lord Lemon" />
)
const removeMediaButton = container.getByTestId('remove-media-attachment')
fireEvent.click(removeMediaButton)
expect(props.onRemoveMediaComment).toHaveBeenCalled()
})
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
Loading

0 comments on commit 8b5d979

Please sign in to comment.