diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php index 819cb4cc48b..56a25854ab9 100644 --- a/lib/AppInfo/Application.php +++ b/lib/AppInfo/Application.php @@ -38,6 +38,7 @@ use OCA\Talk\Dashboard\TalkWidget; use OCA\Talk\Events\ChatEvent; use OCA\Talk\Events\RoomEvent; +use OCA\Talk\Deck\DeckPluginLoader; use OCA\Talk\Files\Listener as FilesListener; use OCA\Talk\Files\TemplateLoader as FilesTemplateLoader; use OCA\Talk\Flow\Operation; @@ -99,6 +100,7 @@ public function register(IRegistrationContext $context): void { $context->registerEventListener(BeforeTemplateRenderedEvent::class, PublicShareAuthTemplateLoader::class); $context->registerEventListener(\OCP\AppFramework\Http\Events\BeforeTemplateRenderedEvent::class, UnifiedSearchCSSLoader::class); $context->registerEventListener(UserChangedEvent::class, UserDisplayNameListener::class); + $context->registerEventListener(\OCP\AppFramework\Http\Events\BeforeTemplateRenderedEvent::class, DeckPluginLoader::class); $context->registerSearchProvider(ConversationSearch::class); $context->registerSearchProvider(CurrentMessageSearch::class); diff --git a/lib/Deck/DeckPluginLoader.php b/lib/Deck/DeckPluginLoader.php new file mode 100644 index 00000000000..7fd27226aeb --- /dev/null +++ b/lib/Deck/DeckPluginLoader.php @@ -0,0 +1,54 @@ + + * + * @license GNU AGPL version 3 or any later version + * + * This program 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, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 . + * + */ + +namespace OCA\Talk\Deck; + +use OCP\AppFramework\Http\Events\BeforeTemplateRenderedEvent; +use OCP\EventDispatcher\Event; +use OCP\EventDispatcher\IEventListener; +use OCP\IRequest; +use OCP\Util; + +class DeckPluginLoader implements IEventListener { + /** @var IRequest */ + private $request; + + public function __construct(IRequest $request) { + $this->request = $request; + } + + public function handle(Event $event): void { + if (!($event instanceof BeforeTemplateRenderedEvent)) { + return; + } + + if (!$event->isLoggedIn()) { + return; + } + + if (strpos($this->request->getPathInfo(), '/apps/deck') === 0) { + Util::addScript('spreed', 'collections'); + Util::addScript('spreed', 'deck'); + } + } +} diff --git a/src/collections.js b/src/collections.js index 8d6b9cd7975..27d79afd6c7 100644 --- a/src/collections.js +++ b/src/collections.js @@ -49,7 +49,7 @@ import RoomSelector from './views/RoomSelector' ComponentVM.$root.$on('close', () => { ComponentVM.$el.remove() ComponentVM.$destroy() - reject(new Error('User canceled resource selection')) + reject(new Error('User cancelled resource selection')) }) ComponentVM.$root.$on('select', (id) => { resolve(id) diff --git a/src/deck.js b/src/deck.js new file mode 100644 index 00000000000..d0f63f047f6 --- /dev/null +++ b/src/deck.js @@ -0,0 +1,111 @@ +/* + * @copyright Copyright (c) 2020 Vincent Petry + * + * @author Vincent Petry + * + * @license GNU AGPL version 3 or any later version + * + * This program 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, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 Vue from 'vue' +import { generateFilePath, generateUrl } from '@nextcloud/router' +import { getRequestToken } from '@nextcloud/auth' +import { translate, translatePlural } from '@nextcloud/l10n' +import { showSuccess, showError } from '@nextcloud/dialogs' +import { postRichObjectToConversation } from './services/messagesService' +import RoomSelector from './views/RoomSelector' + +(function(OC, OCA, t, n) { + async function postCardToRoom(card, token) { + try { + const response = await postRichObjectToConversation(token, { + objectType: 'deck-card', + objectId: card.id, + metaData: JSON.stringify(card), + }) + const messageId = response.data.ocs.data.id + const targetUrl = generateUrl('/call/{token}#message_{messageId}', { token, messageId }) + showSuccess(t('spreed', 'Deck card has been posted to the selected conversation.', { + link: targetUrl, + }), { + isHTML: true, + }) + } catch (exception) { + console.error('Error posting deck card to conversation', exception, exception.response?.status) + if (exception.response?.status === 403) { + showError(t('spreed', 'No permission to post messages in this conversation')) + } else { + showError(t('spreed', 'An error occurred while posting deck card to conversation.')) + } + } + } + + function init() { + if (!OCA.Deck) { + return + } + + OCA.Deck.registerCardAction({ + label: t('spreed', 'Post to a conversation'), + icon: 'icon-talk', + callback: (card) => { + const container = document.createElement('div') + container.id = 'spreed-post-card-to-room-select' + const body = document.getElementById('body-user') + body.appendChild(container) + + const ComponentVM = Vue.extend(RoomSelector) + const vm = new ComponentVM({ + el: container, + propsData: { + dialogTitle: t('spreed', 'Post to conversation'), + showPostableOnly: true, + }, + }) + + vm.$root.$on('close', () => { + vm.$el.remove() + vm.$destroy() + }) + vm.$root.$on('select', (token) => { + vm.$el.remove() + vm.$destroy() + + postCardToRoom(card, token) + }) + }, + }) + } + + // CSP config for webpack dynamic chunk loading + // eslint-disable-next-line + __webpack_nonce__ = btoa(getRequestToken()) + + // Correct the root of the app for chunk loading + // OC.linkTo matches the apps folders + // OC.generateUrl ensure the index.php (or not) + // We do not want the index.php since we're loading files + // eslint-disable-next-line + __webpack_public_path__ = generateFilePath('spreed', '', 'js/') + + Vue.prototype.t = translate + Vue.prototype.n = translatePlural + Vue.prototype.OC = OC + Vue.prototype.OCA = OCA + + document.addEventListener('DOMContentLoaded', init) + +})(window.OC, window.OCA, t, n) diff --git a/src/main.js b/src/main.js index c21ed964367..9dec9551f40 100644 --- a/src/main.js +++ b/src/main.js @@ -71,7 +71,7 @@ Vue.use(VueObserveVisibility) Vue.use(VueShortKey, { prevent: ['input', 'textarea', 'div'] }) Vue.use(vOutsideEvents) -export default new Vue({ +const instance = new Vue({ el: '#content', store, router, @@ -155,3 +155,11 @@ Sidebar.prototype.close = function() { Object.assign(window.OCA.Files, { Sidebar: new Sidebar(), }) + +// make the instance available to global components that might run on the same page +if (!window.OCA.Talk) { + window.OCA.Talk = {} +} +OCA.Talk.instance = instance + +export default instance diff --git a/src/services/messagesService.js b/src/services/messagesService.js index 02d4af8fcd0..8a2b14951b8 100644 --- a/src/services/messagesService.js +++ b/src/services/messagesService.js @@ -23,6 +23,8 @@ import axios from '@nextcloud/axios' import { generateOcsUrl } from '@nextcloud/router' import store from '../store/index' +import SHA1 from 'crypto-js/sha1' +import Hex from 'crypto-js/enc-hex' /** * Fetches messages that belong to a particular conversation @@ -114,9 +116,32 @@ const deleteMessage = async function({ token, id }) { return axios.delete(generateOcsUrl('apps/spreed/api/v1/chat', 2) + token + '/' + id) } +/** + * Post a rich object to a conversation + * + * @param {string} token conversation token + * @param {string} objectType object type + * @param {string} objectId object id + * @param {string} metaData JSON metadata of the rich object encoded as string + * @param {string} referenceId generated reference id, leave empty to generate it based on the other args + */ +const postRichObjectToConversation = async function(token, { objectType, objectId, metaData, referenceId }) { + if (!referenceId) { + const tempId = 'richobject-' + objectType + '-' + objectId + '-' + token + '-' + (new Date().getTime()) + referenceId = Hex.stringify(SHA1(tempId)) + } + return axios.post(generateOcsUrl('apps/spreed/api/v1', 2) + `chat/${token}/share`, { + objectType, + objectId, + metaData, + referenceId, + }) +} + export { fetchMessages, lookForNewMessages, postNewMessage, deleteMessage, + postRichObjectToConversation, } diff --git a/src/views/RoomSelector.vue b/src/views/RoomSelector.vue index 7b0842aa685..ec9dca1654f 100644 --- a/src/views/RoomSelector.vue +++ b/src/views/RoomSelector.vue @@ -24,9 +24,9 @@