░▒▓████████████████████████████████▓▒░ ░▒▓█ ▓▒░ ░▒▓█ ~ S W A M P ~ ▓▒░ ░▒▓█ ▓▒░ ░▒▓████████████████████████████████▓▒░
pplmvsvm
LOCATION:
/var/www/canvas/ui/features/conversations
☗ ROOT
↻ REFRESH
✎ CARVE FLESH
EDITING: index.js
/* * Copyright (C) 2013 - 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 {useScope as useI18nScope} from '@canvas/i18n' import $ from 'jquery' import _ from 'underscore' import Backbone from '@canvas/backbone' import MessageCollection from './backbone/collections/MessageCollection' import MessageListView from './backbone/views/MessageListView' import MessageDetailView from './backbone/views/MessageDetailView' import MessageFormDialog from './backbone/views/MessageFormDialog' import SubmissionCommentFormDialog from './backbone/views/SubmissionCommentFormDialog' import InboxHeaderView from './backbone/views/InboxHeaderView' import deparam from 'deparam' import CourseCollection from './backbone/collections/CourseCollection' import FavoriteCourseCollection from './backbone/collections/FavoriteCourseCollection' import GroupCollection from '@canvas/groups/backbone/collections/GroupCollection' import '@canvas/jquery/jquery.disableWhileLoading' import React from 'react' import ReactDOM from 'react-dom' import {decodeQueryString} from '@canvas/query-string-encoding' import ConversationStatusFilter from './react/ConversationStatusFilter' import ready from '@instructure/ready' const I18n = useI18nScope('conversations') const ConversationsRouter = Backbone.Router.extend({ routes: { '': 'index', 'filter=:state': 'filter', }, sendingCount: 0, initialize() { ;[ 'onSelected', 'selectConversation', 'onSubmissionReply', 'onReply', 'onReplyAll', 'onArchive', 'onDelete', 'onCompose', 'onMarkUnread', 'onMarkRead', 'onForward', 'onStarToggle', 'onFilter', 'onCourse', '_replyFromRemote', '_initViews', 'onSubmit', 'onAddMessage', 'onSubmissionAddMessage', 'onSearch', 'onKeyDown', ].forEach(method => (this[method] = this[method].bind(this))) const dfd = this._initCollections() this._initViews() this._attachEvents() if (this._isRemoteLaunch()) return dfd.then(this._replyFromRemote) }, // Public: Pull a value from the query string. // // name - The name of the query string param. // // Returns a string value or null. param(name) { const regex = new RegExp(`${name}=([^&]+)`) const value = window.location.search.match(regex) if (value) return decodeURIComponent(value[1]) return null }, // Internal: Perform a batch update of all selected messages. // // event - The event to batch (e.g. 'star' or 'destroy'). // fn - A function called with each selected message. Used for side-effecting. // // Returns an array of impacted message IDs. batchUpdate(event, fn = $.noop) { const messages = _.map(this.list.selectedMessages, message => { fn.call(this, message) return message.get('id') }) $.ajaxJSON('/api/v1/conversations', 'PUT', { 'conversation_ids[]': messages, event, }) if (event === 'destroy') this.list.selectedMessages = [] if (event === 'archive' && this.filters.type !== 'sent') this.list.selectedMessages = [] if (event === 'mark_as_read' && this.filters.type === 'archived') this.list.selectedMessages = [] if (event === 'unstar' && this.filters.type === 'starred') this.list.selectedMessages = [] return messages }, lastFetch: null, onSelected(model) { if (this.lastFetch) this.lastFetch.abort() this.header.onModelChange(null, this.model) this.detail.onModelChange(null, this.model) this.model = model const messages = this.list.selectedMessages if (messages.length === 0) { delete this.detail.model return this.detail.render() } else if (messages.length > 1) { delete this.detail.model messages[0].set('canArchive', this.filters.type !== 'sent') this.detail.onModelChange(messages[0], null) this.detail.render({batch: true}) this.header.onModelChange(messages[0], null) this.header.toggleReplyBtn(true) this.header.toggleReplyAllBtn(true) this.header.hideForwardBtn(true) } else { model = this.list.selectedMessage() if (model.get('messages')) { this.selectConversation(model) } else { this.lastFetch = model.fetch({ data: { include_participant_contexts: false, include_private_conversation_enrollments: false, }, success: this.selectConversation, }) this.detail.$el.disableWhileLoading(this.lastFetch) } } }, selectConversation(model) { if (model) model.set('canArchive', this.filters.type !== 'sent') this.header.onModelChange(model, null) this.detail.onModelChange(model, null) this.detail.render() }, onSubmissionReply() { this.submissionReply.show(this.detail.model, {trigger: $('#submission-reply-btn')}) }, onReply(message, trigger) { if (this.detail.model.get('for_submission')) { this.onSubmissionReply() } else { this._delegateReply(message, 'reply', trigger) } }, onReplyAll(message, trigger) { this._delegateReply(message, 'replyAll', trigger) }, _delegateReply(message, type, trigger) { this.compose.show(this.detail.model, {to: type, trigger, message}) }, onArchive(focusNext, trigger) { const action = this.list.selectedMessage().get('workflow_state') === 'archived' ? 'mark_as_read' : 'archive' const confirmMessage = action === 'archive' ? I18n.t( { one: 'Are you sure you want to archive your copy of this conversation?', other: 'Are you sure you want to archive your copies of these conversations?', }, {count: this.list.selectedMessages.length} ) : I18n.t( { one: 'Are you sure you want to unarchive this conversation?', other: 'Are you sure you want to unarchive these conversations?', }, {count: this.list.selectedMessages.length} ) if (!window.confirm(confirmMessage)) { $(trigger).focus() return } const messages = this.batchUpdate(action, function (m) { const newState = action === 'mark_as_read' ? 'read' : 'archived' m.set('workflow_state', newState) this.header.onArchivedStateChange(m) }) if (['inbox', 'archived'].includes(this.filters.type)) { this.list.collection.remove(messages) this.header.onModelChange(null, null) this.detail.onModelChange(null, null) this.detail.renderEmpty() } let $focusNext = $(focusNext) if ($focusNext.length === 0) { $focusNext = $('#compose-message-recipients') } $focusNext.focus() }, onDelete(focusNext, trigger) { const confirmMsg = I18n.t( { one: 'Are you sure you want to delete your copy of this conversation? This action cannot be undone.', other: 'Are you sure you want to delete your copy of these conversations? This action cannot be undone.', }, {count: this.list.selectedMessages.length} ) if (!window.confirm(confirmMsg)) { $(trigger).focus() return } const delmsg = I18n.t( { one: 'Message Deleted!', other: 'Messages Deleted!', }, {count: this.list.selectedMessages.length} ) const messages = this.batchUpdate('destroy') delete this.detail.model this.list.collection.remove(messages) this.header.updateUi(null) $.flashMessage(delmsg) this.detail.renderEmpty() let $focusNext = $(focusNext) if ($focusNext.length === 0) { $focusNext = $('#compose-message-recipients') } $focusNext.focus() }, onCompose(_e) { this.compose.show(null, {trigger: '#compose-btn'}) }, index() { return this.filter('') }, filter(state) { const filters = (this.filters = deparam(state)) this.header.displayState(filters) this.selectConversation(null) this.list.selectedMessages = [] this.list.collection.reset() if (filters.type === 'submission_comments') { ;['scope', 'filter', 'filter_mode', 'include_private_conversation_enrollments'].forEach( this.list.collection.deleteParam.bind(this.list.collection) ) this.list.collection.url = '/api/v1/users/self/activity_stream' this.list.collection.setParam('asset_type', 'Submission') if (filters.course) { this.list.collection.setParam('context_code', filters.course) } else { this.list.collection.deleteParam('context_code') } } else { ;['context_code', 'asset_type', 'submission_user_id'].forEach( this.list.collection.deleteParam.bind(this.list.collection) ) this.list.collection.url = '/api/v1/conversations' this.list.collection.setParam('scope', filters.type) this.list.collection.setParam('filter', this._currentFilter()) this.list.collection.setParam('filter_mode', 'and') this.list.collection.setParam('include_private_conversation_enrollments', false) } this.list.collection.fetch() this.compose.setDefaultCourse(filters.course) }, onMarkUnread() { return this.batchUpdate('mark_as_unread', m => m.toggleReadState(false)) }, onMarkRead() { return this.batchUpdate('mark_as_read', m => m.toggleReadState(true)) }, onForward(message, trigger) { let model if (message) { model = this.detail.model.clone() model.handleMessages() model.set( 'messages', _.filter( model.get('messages'), m => m.id === message.id || (_.includes(m.participating_user_ids, message.author_id) && m.created_at < message.created_at) ) ) } else { model = this.detail.model } this.compose.show(model, {to: 'forward', trigger}) }, onStarToggle() { const event = this.list.selectedMessage().get('starred') ? 'unstar' : 'star' const messages = this.batchUpdate(event, m => m.toggleStarred(event === 'star')) if (this.filters.type === 'starred') { if (event === 'unstar') this.selectConversation(null) return this.list.collection.remove(messages) } }, onFilter(filters) { // Update the hash. Replace if there isn't already a hash - we're in the // process of loading the page if so, and we wouldn't want to create a // spurious history entry by not doing so. const existingHash = window.location.hash && window.location.hash.substring(1) return this.navigate(`filter=${$.param(filters)}`, {trigger: true, replace: !existingHash}) }, onCourse(course) { return this.list.updateCourse(course) }, // Internal: Determine if a reply was launched from another URL. // // Returns a boolean. _isRemoteLaunch() { return !!window.location.search.match(/user_id/) }, // Internal: Open and populate the new message dialog from a remote launch. // // Returns nothing. _replyFromRemote() { this.compose.show(null, { user: { id: this.param('user_id'), name: this.param('user_name'), }, context: this.param('context_id'), remoteLaunch: true, }) }, _initCollections() { const gc = new GroupCollection() gc.setParam('include[]', 'can_message') this.courses = { favorites: new FavoriteCourseCollection(), all: new CourseCollection(), groups: gc, } this.courses.all.setParam('per_page', '50') return this.courses.favorites.fetch() }, _initViews() { this._initListView() this._initDetailView() this._initHeaderView() this._initComposeDialog() this._initSubmissionCommentReplyDialog() }, _attachEvents() { this.list.collection.on('change:selected', this.onSelected) this.header.on('compose', this.onCompose) this.header.on('reply', this.onReply) this.header.on('reply-all', this.onReplyAll) this.header.on('archive', this.onArchive) this.header.on('delete', this.onDelete) this.header.on('filter', this.onFilter) this.header.on('course', this.onCourse) this.header.on('mark-unread', this.onMarkUnread) this.header.on('mark-read', this.onMarkRead) this.header.on('forward', this.onForward) this.header.on('star-toggle', this.onStarToggle) this.header.on('search', this.onSearch) this.header.on('submission-reply', this.onReply) this.compose.on('close', this.onCloseCompose) this.compose.on('addMessage', this.onAddMessage) this.compose.on('addMessage', this.list.updateMessage, this.list) this.compose.on('newConversations', this.onNewConversations) this.compose.on('submitting', this.onSubmit) this.submissionReply.on('addMessage', this.onSubmissionAddMessage) this.submissionReply.on('submitting', this.onSubmit) this.detail.on('reply', this.onReply) this.detail.on('reply-all', this.onReplyAll) this.detail.on('forward', this.onForward) this.detail.on('star-toggle', this.onStarToggle) this.detail.on('delete', this.onDelete) this.detail.on('archive', this.onArchive) $(document).ready(this.onPageLoad) $(window).keydown(this.onKeyDown) }, onPageLoad(_e) { $('#main').css({display: 'block'}) }, onSubmit(dfd) { this._incrementSending(1) return dfd.always(() => this._incrementSending(-1)) }, onAddMessage(message, conversation) { const model = this.list.collection.get(conversation.id) if (model && model.get('messages')) { message.context_name = model.messageCollection.last().get('context_name') model.get('messages').unshift(message) model.trigger('change:messages') if (model === this.detail.model) { return this.detail.render() } } }, onSubmissionAddMessage(message, submission) { const model = this.list.collection.findWhere({submission_id: submission.id}) if (model && model.get('messages')) { model.get('messages').unshift(message) model.trigger('change:messages') if (model === this.detail.model) { return this.detail.render() } } }, onNewConversations(_conversations) {}, _incrementSending(increment) { this.sendingCount += increment return this.header.toggleSending(this.sendingCount > 0) }, _currentFilter() { let filter = this.searchTokens || [] if (this.filters.course) filter = filter.concat(this.filters.course) return filter }, onSearch(tokens) { this.list.collection.reset() this.searchTokens = tokens.length ? tokens : null if (this.filters.type === 'submission_comments') { let match if (this.searchTokens && (match = this.searchTokens[0].match(/^user_(\d+)$/))) { this.list.collection.setParam('submission_user_id', match[1]) } else { this.list.collection.deleteParam('submission_user_id') } } else { this.list.collection.setParam('filter', this._currentFilter()) } delete this.detail.model this.list.selectedMessages = [] this.detail.render() return this.list.collection.fetch() }, _initListView() { this.list = new MessageListView({ collection: new MessageCollection(), el: $('.message-list'), scrollContainer: $('.message-list-scroller'), buffer: 50, }) this.list.render() }, _initDetailView() { this.detail = new MessageDetailView({el: $('.message-detail')}) this.detail.render() }, _initHeaderView() { const defaultFilter = 'inbox' const filters = { inbox: I18n.t('Inbox'), unread: I18n.t('Unread'), starred: I18n.t('Starred'), sent: I18n.t('Sent'), archived: I18n.t('Archived'), submission_comments: I18n.t('Submission Comments'), } // The onArchive function requires the filter to always be set in the url. // If you are accessing the page iniitially, the filter will be set to // inbox, but we have to update the url here manually to match. Further // updates to the url are handled by the filter trigger and backbone history const hash = window.location.hash const hashParams = hash.substring('#filter='.length) const filterType = decodeQueryString(hashParams).filter(i => i.type !== undefined) const validFilter = filterType.length === 1 && Object.keys(filters).includes(filterType[0].type) let initialFilter if (hash.startsWith('#filter=') && validFilter) { initialFilter = filterType[0].type } else { window.location.hash = `#filter=type=${defaultFilter}` initialFilter = defaultFilter } this.header = new InboxHeaderView({el: $('header.panel'), courses: this.courses}) this.header.render() ReactDOM.render( <ConversationStatusFilter router={this} filters={filters} defaultFilter={defaultFilter} initialFilter={initialFilter} />, document.getElementById('conversation_filter') ) }, _initComposeDialog() { this.compose = new MessageFormDialog({ courses: this.courses, folderId: ENV.CONVERSATIONS.ATTACHMENTS_FOLDER_ID, account_context_code: ENV.CONVERSATIONS.ACCOUNT_CONTEXT_CODE, }) }, _initSubmissionCommentReplyDialog() { this.submissionReply = new SubmissionCommentFormDialog() }, onKeyDown(e) { const nodeName = e.target.nodeName.toLowerCase() if (nodeName === 'input' || nodeName === 'textarea') return const ctrl = e.ctrlKey || e.metaKey if (e.which === 65 && ctrl) { // ctrl-a e.preventDefault() this.list.selectAll() } }, }) ready(() => { window.conversationsRouter = new ConversationsRouter() Backbone.history.start() })
CANCEL
Name
Type
Size
Modified
Actions
↩ ..
DIR
—
—
📁 backbone/
DIR
—
2023-09-24 03:36
📁 jst/
DIR
—
2023-09-24 03:36
📁 react/
DIR
—
2023-09-24 03:39
📄 index.js
JS
17.9 KB
2023-09-24 03:39
EDIT
📄 package.json
JSON
87 B
2023-09-24 03:36
EDIT