Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add sharing deck card with a conversation #5110

Merged
merged 3 commits into from
Feb 15, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions lib/AppInfo/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down
54 changes: 54 additions & 0 deletions lib/Deck/DeckPluginLoader.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<?php

declare(strict_types=1);
/**
* @copyright Copyright (c) 2021 Vincent Petry <[email protected]>
*
* @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 <http://www.gnu.org/licenses/>.
*
*/

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');
}
}
}
2 changes: 1 addition & 1 deletion src/collections.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
111 changes: 111 additions & 0 deletions src/deck.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
/*
* @copyright Copyright (c) 2020 Vincent Petry <[email protected]>
*
* @author Vincent Petry <[email protected]>
*
* @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 <http://www.gnu.org/licenses/>.
*
*/

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 <a href="{link}">conversation</a>.', {
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)
10 changes: 9 additions & 1 deletion src/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
25 changes: 25 additions & 0 deletions src/services/messagesService.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
}
56 changes: 38 additions & 18 deletions src/views/RoomSelector.vue
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@
<Modal @close="close">
<div id="modal-inner" class="talk-modal" :class="{ 'icon-loading': loading }">
<div id="modal-content">
<h2>{{ t('spreed', 'Link to a conversation') }}</h2>
<h2>{{ dialogTitle }}</h2>
<div id="room-list">
<ul v-if="!loading">
<ul v-if="!loading && availableRooms.length > 0">
<li v-for="room in availableRooms"
:key="room.token"
:class="{selected: selectedRoom === room.token }"
Expand All @@ -38,9 +38,16 @@
<span>{{ room.displayName }}</span>
</li>
</ul>
<div v-else-if="!loading">
{{ t('spreed', 'No conversations found') }}
</div>
</div>
<div id="modal-buttons">
<button v-if="!loading" class="primary" @click="select">
<button
v-if="!loading && availableRooms.length > 0"
class="primary"
:disabled="!selectedRoom"
@click="select">
{{ t('spreed', 'Select conversation') }}
</button>
</div>
Expand All @@ -53,6 +60,7 @@
import Modal from '@nextcloud/vue/dist/Components/Modal'
import axios from '@nextcloud/axios'
import { generateOcsUrl } from '@nextcloud/router'
import { CONVERSATION } from '../constants'
import ConversationIcon from '../components/ConversationIcon'

export default {
Expand All @@ -61,38 +69,46 @@ export default {
ConversationIcon,
Modal,
},
props: {
dialogTitle: {
type: String,
default: t('spreed', 'Link to a conversation'),
},
/**
* Whether to only show conversations to which
* the user can post messages.
*/
showPostableOnly: {
type: Boolean,
default: false,
},
},
data() {
return {
rooms: [],
selectedRoom: null,
currentRoom: null,
loading: true,
// TODO: should be included once this is properly available
types: {
ROOM_TYPE_ONE_TO_ONE: 1,
ROOM_TYPE_GROUP: 2,
ROOM_TYPE_PUBLIC: 3,
ROOM_TYPE_CHANGELOG: 4,
},
}
},
computed: {
currentRoom() {
if (OCA.SpreedMe && OCA.SpreedMe.app.activeRoom) {
return OCA.SpreedMe.app.activeRoom.get('token')
}
return null
},
availableRooms() {
return this.rooms.filter((room) => {
return room.token !== this.currentRoom
nickvergessen marked this conversation as resolved.
Show resolved Hide resolved
&& room.type !== this.types.ROOM_TYPE_CHANGELOG
return room.type !== CONVERSATION.TYPE.CHANGELOG
&& (!this.currentRoom || this.currentRoom !== room.token)
&& (!this.showPostableOnly || room.readOnly === CONVERSATION.STATE.READ_WRITE)
&& room.objectType !== 'file'
&& room.objectType !== 'share:password'
})
},
},
beforeMount() {
this.fetchRooms()

const $store = OCA.Talk?.instance?.$store
if ($store) {
this.currentRoom = $store.getters.getToken()
}
},
methods: {
fetchRooms() {
Expand Down Expand Up @@ -138,6 +154,7 @@ export default {
#room-list {
overflow-y: auto;
flex: 0 1 auto;
height: 100%;
}

li {
Expand All @@ -159,6 +176,9 @@ li {
& > span {
padding: 5px 5px 5px 10px;
vertical-align: middle;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
}

Expand Down
1 change: 1 addition & 0 deletions webpack.common.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ module.exports = {
'talk-public-share-sidebar': path.join(__dirname, 'src', 'mainPublicShareSidebar.js'),
'flow': path.join(__dirname, 'src', 'flow.js'),
'dashboard': path.join(__dirname, 'src', 'dashboard.js'),
'deck': path.join(__dirname, 'src', 'deck.js'),
},
output: {
path: path.resolve(__dirname, './js'),
Expand Down