Merge pull request #5110 from nextcloud/enh/5106/share-deck-card-with-conversation

Add sharing deck card with a conversation
This commit is contained in:
Vincent Petry 2021-02-15 10:03:09 +01:00 коммит произвёл GitHub
Родитель fff52a8909 ab45b798e6
Коммит 6f88aa9a91
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
8 изменённых файлов: 241 добавлений и 20 удалений

Просмотреть файл

@ -38,6 +38,7 @@ use OCA\Talk\Config;
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 @@ class Application extends App implements IBootstrap {
$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);

Просмотреть файл

@ -0,0 +1,54 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2021 Vincent Petry <vincent@nextcloud.com>
*
* @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');
}
}
}

Просмотреть файл

@ -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)

111
src/deck.js Normal file
Просмотреть файл

@ -0,0 +1,111 @@
/*
* @copyright Copyright (c) 2020 Vincent Petry <vincent@nextcloud.com>
*
* @author Vincent Petry <vincent@nextcloud.com>
*
* @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)

Просмотреть файл

@ -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

Просмотреть файл

@ -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,
}

Просмотреть файл

@ -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 }"
@ -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>
@ -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 {
@ -61,31 +69,34 @@ 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
&& 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'
})
@ -93,6 +104,11 @@ export default {
},
beforeMount() {
this.fetchRooms()
const $store = OCA.Talk?.instance?.$store
if ($store) {
this.currentRoom = $store.getters.getToken()
}
},
methods: {
fetchRooms() {
@ -138,6 +154,7 @@ export default {
#room-list {
overflow-y: auto;
flex: 0 1 auto;
height: 100%;
}
li {
@ -159,6 +176,9 @@ li {
& > span {
padding: 5px 5px 5px 10px;
vertical-align: middle;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
}

Просмотреть файл

@ -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'),