diff --git a/appinfo/routes.php b/appinfo/routes.php
index 884e86b9f4c..b5804d7117a 100644
--- a/appinfo/routes.php
+++ b/appinfo/routes.php
@@ -319,6 +319,19 @@
'requirements' => ['apiVersion' => 'v1'],
],
+ /**
+ * Files
+ */
+ [
+ 'name' => 'Files#getRoom',
+ 'url' => '/api/{apiVersion}/file/{fileId}',
+ 'verb' => 'GET',
+ 'requirements' => [
+ 'apiVersion' => 'v1',
+ 'fileId' => '.+'
+ ],
+ ],
+
/**
* Guest
*/
diff --git a/css/files.scss b/css/files.scss
new file mode 100644
index 00000000000..2deca5d89b4
--- /dev/null
+++ b/css/files.scss
@@ -0,0 +1,118 @@
+/**
+ * Cascade parent element height to the chat view in the sidebar to limit the
+ * vertical scroll bar only to the list of messages. Otherwise, the vertical
+ * scroll bar would be shown for the whole sidebar and everything would be
+ * moved when scrolling to see overflown messages.
+ *
+ * The list of messages should stretch to fill the available space at the bottom
+ * of the right sidebar, so the height is cascaded using flex boxes.
+ *
+ * It is horrible, I know (but better than using JavaScript ;-) ). Please
+ * improve it if you know how :-)
+ */
+#app-sidebar {
+ /* Override "display: block" set inline by jQuery. */
+ display: flex !important;
+ flex-direction: column;
+}
+
+#app-sidebar.disappear {
+ /* Override "display: flex !important" when the sidebar has to be hidden. */
+ display: none !important;
+}
+
+#app-sidebar .detailFileInfoContainer {
+ flex-shrink: 0;
+}
+
+#app-sidebar .tabsContainer {
+ display: flex;
+ flex-direction: column;
+
+ flex-grow: 1;
+}
+
+#app-sidebar .tab {
+ display: flex;
+ flex-direction: column;
+
+ flex-grow: 1;
+}
+
+#app-sidebar .tabsContainer.with-inner-scroll-bars,
+#app-sidebar .tabsContainer.with-inner-scroll-bars .tab {
+ overflow: hidden;
+}
+
+#app-sidebar .tab.hidden {
+ display: none;
+}
+
+#app-sidebar #chatView {
+ display: flex;
+ flex-direction: column;
+ overflow: hidden;
+
+ flex-grow: 1;
+}
+
+#app-sidebar #chatView .comments {
+ overflow-y: auto;
+
+ /* Needed for proper calculation of comment positions in the scrolling
+ container (as otherwise the comment position is calculated with respect
+ to the closest ancestor with a relative position) */
+ position: relative;
+}
+
+/**
+ * Place the scroll bar of the message list on the right edge of the sidebar,
+ * but keeping the padding of the tab view.
+ *
+ * The padding must be set on the left too to ensure that the contacts menu
+ * shown when clicking on an author name does not overflow the tab (as it would
+ * be hidden).
+ *
+ * The bottom padding is removed to extend the chat view to the bottom edge of
+ * the sidebar.
+ */
+#app-sidebar .tab-chat {
+ padding-left: 0;
+ padding-right: 0;
+ padding-bottom: 0;
+}
+
+/* Hack needed to overcome the padding of the tab container and move the scroll
+ * bar of the messages list to the right border of the sidebar. */
+#app-sidebar .tabsContainer.with-inner-scroll-bars .tab {
+ padding-right: 0px;
+}
+
+#app-sidebar .tabsContainer .tab .app-not-started-placeholder {
+ /* Make the placeholder take the full tab height until the app is
+ * started. */
+ flex-grow: 1;
+}
+
+#app-sidebar #chatView .comments {
+ padding-right: 15px;
+}
+
+#app-sidebar #chatView .comments .wrapper-background,
+#app-sidebar #chatView .comments .wrapper {
+ /* Padding is not respected in the comment wrapper due to its absolute
+ * positioning, so it must be set through its position. */
+ right: 15px;
+}
+
+#app-sidebar #chatView .newCommentRow {
+ /* The details view in the Files app has a bottom padding of 15px, so the
+ * general bottom margin used for comments should be reduced for the new
+ * comment form. */
+ margin-bottom: 5px;
+}
+
+#app-sidebar #chatView .newCommentForm {
+ /* Make room to show the "Add" button when chat is shown in the sidebar. */
+ margin-right: 44px;
+}
diff --git a/js/embedded.js b/js/embedded.js
new file mode 100644
index 00000000000..a2c79ff4ff8
--- /dev/null
+++ b/js/embedded.js
@@ -0,0 +1,128 @@
+/* global Marionette, Backbone, _, $ */
+
+/**
+ *
+ * @copyright Copyright (c) 2018, Daniel Calviño Sánchez (danxuliu@gmail.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 .
+ *
+ */
+
+(function(OC, OCA, Marionette, Backbone, _, $) {
+ 'use strict';
+
+ OCA.Talk = OCA.Talk || {};
+
+ var roomChannel = Backbone.Radio.channel('rooms');
+
+ OCA.Talk.Embedded = Marionette.Application.extend({
+ OWNER: 1,
+ MODERATOR: 2,
+ USER: 3,
+ GUEST: 4,
+ USERSELFJOINED: 5,
+
+ /* Must stay in sync with values in "lib/Room.php". */
+ FLAG_DISCONNECTED: 0,
+ FLAG_IN_CALL: 1,
+ FLAG_WITH_AUDIO: 2,
+ FLAG_WITH_VIDEO: 4,
+
+ /** @property {OCA.SpreedMe.Models.Room} activeRoom */
+ activeRoom: null,
+
+ /** @property {String} token */
+ token: null,
+
+ /** @property {OCA.Talk.Connection} connection */
+ connection: null,
+
+ /** @property {OCA.Talk.Signaling.base} signaling */
+ signaling: null,
+
+ /** @property {OCA.SpreedMe.Models.RoomCollection} _rooms */
+ _rooms: null,
+
+ _registerPageEvents: function() {
+ // Initialize button tooltips
+ $('[data-toggle="tooltip"]').tooltip({trigger: 'hover'}).click(function() {
+ $(this).tooltip('hide');
+ });
+ },
+
+ /**
+ * @param {string} token
+ */
+ _setRoomActive: function(token) {
+ if (OC.getCurrentUser().uid) {
+ this._rooms.forEach(function(room) {
+ room.set('active', room.get('token') === token);
+ });
+ }
+ },
+ syncAndSetActiveRoom: function(token) {
+ var self = this;
+ this.signaling.syncRooms()
+ .then(function() {
+ self.stopListening(self.activeRoom, 'change:participantFlags');
+
+ if (OC.getCurrentUser().uid) {
+ roomChannel.trigger('active', token);
+
+ self._rooms.forEach(function(room) {
+ if (room.get('token') === token) {
+ self.activeRoom = room;
+ }
+ });
+ }
+ });
+ },
+
+ setEmptyContentMessage: function() {
+ },
+ restoreEmptyContent: function() {
+ },
+
+ initialize: function() {
+ if (OC.getCurrentUser().uid) {
+ this._rooms = new OCA.SpreedMe.Models.RoomCollection();
+ this.listenTo(roomChannel, 'active', this._setRoomActive);
+ }
+
+ this._messageCollection = new OCA.SpreedMe.Models.ChatMessageCollection(null, {token: null});
+ this._chatView = new OCA.SpreedMe.Views.ChatView({
+ collection: this._messageCollection,
+ id: 'chatView'
+ });
+
+ this._messageCollection.listenTo(roomChannel, 'leaveCurrentRoom', function() {
+ this.stopReceivingMessages();
+ });
+ },
+ onStart: function() {
+ this.signaling = OCA.Talk.Signaling.createConnection();
+ this.connection = new OCA.Talk.Connection(this);
+
+ $(window).unload(function () {
+ this.connection.leaveCurrentRoom(false);
+ this.signaling.disconnect();
+ }.bind(this));
+
+ this._registerPageEvents();
+ },
+ });
+
+})(OC, OCA, Marionette, Backbone, _, $);
diff --git a/js/filesplugin.js b/js/filesplugin.js
new file mode 100644
index 00000000000..9eb35f2b01d
--- /dev/null
+++ b/js/filesplugin.js
@@ -0,0 +1,303 @@
+/**
+ *
+ * @copyright Copyright (c) 2018, Daniel Calviño Sánchez (danxuliu@gmail.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 .
+ *
+ */
+
+(function(OC, OCA) {
+
+ 'use strict';
+
+ OCA.Talk = OCA.Talk || {};
+
+ var roomsChannel = Backbone.Radio.channel('rooms');
+
+ /**
+ * Tab view for Talk chat in the details view of the Files app.
+ *
+ * This view shows the chat for the Talk room associated with the file. The
+ * tab is shown only for those files in which the Talk sidebar is supported,
+ * otherwise it is hidden.
+ */
+ OCA.Talk.TalkChatDetailTabView = OCA.Files.DetailTabView.extend({
+
+ id: 'talkChatTabView',
+
+ /**
+ * Higher priority than other tabs.
+ */
+ order: -10,
+
+ initialize: function() {
+ this.$el.append('
');
+ },
+
+ /**
+ * Returns a CSS class to force scroll bars in the chat view instead of
+ * in the whole sidebar.
+ */
+ getTabsContainerExtraClasses: function() {
+ return 'with-inner-scroll-bars';
+ },
+
+ getLabel: function() {
+ return t('spreed', 'Chat');
+ },
+
+ getIcon: function() {
+ return 'icon-talk';
+ },
+
+ /**
+ * Returns whether the Talk tab can be displayed for the file.
+ *
+ * @param OCA.Files.FileInfoModel fileInfo
+ * @return True if the tab can be displayed, false otherwise.
+ * @see OCA.Talk.FilesPlugin.isTalkSidebarSupportedForFile
+ */
+ canDisplay: function(fileInfo) {
+ if (OCA.Talk.FilesPlugin.isTalkSidebarSupportedForFile(fileInfo)) {
+ return true;
+ }
+
+ // If the Talk tab can not be displayed then the current room is
+ // left; this must be done here because "setFileInfo" will not get
+ // called with the new file if the tab can not be displayed.
+ if (this._appStarted) {
+ OCA.Talk.FilesPlugin.leaveCurrentRoom();
+ } else {
+ this.model = null;
+ }
+
+ return false;
+ },
+
+ /**
+ * Sets the FileInfoModel for the currently selected file.
+ *
+ * Rooms are associated to the id of the file, so the chat can not be
+ * loaded until the file info is set and the token for the room is got.
+ *
+ * @param OCA.Files.FileInfoModel fileInfo
+ */
+ setFileInfo: function(fileInfo) {
+ if (!this._appStarted) {
+ this.model = fileInfo;
+
+ return;
+ }
+
+ if (this.model === fileInfo) {
+ // If the tab was hidden and it is being shown again at this
+ // point the tab has not been made visible yet, so the
+ // operations need to be delayed. However, the scroll position
+ // is saved before the tab is made visible to avoid it being
+ // reset.
+ // Note that the system tags may finish to load once the chat
+ // view was already loaded; in that case the input for tags will
+ // be shown, "compressing" slightly the chat view and thus
+ // causing it to "lose" the last visible element (as the scroll
+ // position is kept so the elements at the bottom are hidden).
+ // Unfortunately there does not seem to be anything that can be
+ // done to prevent that.
+ var lastKnownScrollPosition = OCA.SpreedMe.app._chatView.getLastKnownScrollPosition();
+ setTimeout(function() {
+ OCA.SpreedMe.app._chatView.restoreScrollPosition(lastKnownScrollPosition);
+
+ // Load the pending elements that may have been added while
+ // the tab was hidden.
+ OCA.SpreedMe.app._chatView.reloadMessageList();
+
+ OCA.SpreedMe.app._chatView.focusChatInput();
+ }, 0);
+
+ return;
+ }
+
+ this.model = fileInfo;
+
+ if (!fileInfo || fileInfo.get('id') === undefined) {
+ // This should never happen, except during the initial setup of
+ // the Files app (and not even in that case due to having to
+ // wait for the signaling settings to be fetched before
+ // registering the tab).
+ // Nevertheless, disconnect from the previous room just in case.
+ OCA.Talk.FilesPlugin.leaveCurrentRoom();
+
+ return;
+ }
+
+ $.ajax({
+ url: OC.linkToOCS('apps/spreed/api/v1', 2) + 'file/' + fileInfo.get('id'),
+ type: 'GET',
+ beforeSend: function(request) {
+ request.setRequestHeader('Accept', 'application/json');
+ },
+ success: function(ocsResponse) {
+ OCA.Talk.FilesPlugin.joinRoom(ocsResponse.ocs.data.token);
+ },
+ error: function() {
+ OC.Notification.showTemporary(t('spreed', 'Error while getting the room ID'), {type: 'error'});
+
+ OCA.Talk.FilesPlugin.leaveCurrentRoom();
+ }
+ });
+
+ // If the details view is rendered again after the chat view has
+ // been appended to this tab the chat view would stop working due to
+ // the element being removed instead of detached, which would make
+ // the references to its elements invalid (apparently even if
+ // rendered again; "delegateEvents()" should probably need to be
+ // called too in that case). However, the details view would only be
+ // rendered again if new tabs were added, so in general this should
+ // be safe.
+ OCA.SpreedMe.app._chatView.$el.appendTo(this.$el);
+ OCA.SpreedMe.app._chatView.reloadMessageList();
+ OCA.SpreedMe.app._chatView.setTooltipContainer($('#app-sidebar'));
+ OCA.SpreedMe.app._chatView.focusChatInput();
+ },
+
+ setAppStarted: function() {
+ this._appStarted = true;
+
+ this.$el.find('.app-not-started-placeholder').remove();
+
+ // Set again the file info now that the app has started.
+ if (this.model !== null) {
+ var fileInfo = this.model;
+ this.model = null;
+ this.setFileInfo(fileInfo);
+ }
+ },
+
+ });
+
+ /**
+ * @namespace
+ */
+ OCA.Talk.FilesPlugin = {
+ ignoreLists: [
+ 'files_trashbin',
+ 'files.public'
+ ],
+
+ attach: function(fileList) {
+ var self = this;
+ if (this.ignoreLists.indexOf(fileList.id) >= 0) {
+ return;
+ }
+
+ var talkChatDetailTabView = new OCA.Talk.TalkChatDetailTabView();
+
+ OCA.SpreedMe.app.on('start', function() {
+ self.setupSignalingEventHandlers();
+
+ // While the app is being started the view just shows a
+ // placeholder UI that is replaced by the actual UI once
+ // started.
+ talkChatDetailTabView.setAppStarted();
+ }.bind(this));
+
+ fileList.registerTabView(talkChatDetailTabView);
+
+ // Unlike in the regular Talk app when Talk is embedded the
+ // signaling settings are not initially included in the HTML, so
+ // they need to be explicitly loaded before starting the app.
+ OCA.Talk.Signaling.loadSettings().then(function() {
+ OCA.SpreedMe.app.start();
+ });
+ },
+
+ /**
+ * Returns whether the Talk tab can be displayed for the file.
+ *
+ * @return True if the file is shared with the current user or by the
+ * current user to another user (as a user, group...), false
+ * otherwise.
+ */
+ isTalkSidebarSupportedForFile: function(fileInfo) {
+ if (!fileInfo) {
+ return false;
+ }
+
+ if (fileInfo.get('type') === 'dir') {
+ return false;
+ }
+
+ if (fileInfo.get('shareOwnerId')) {
+ // Shared with me
+ // TODO How to check that it is not a remote share? At least for
+ // local shares "shareTypes" is not defined when shared with me.
+ return true;
+ }
+
+ if (!fileInfo.get('shareTypes')) {
+ return false;
+ }
+
+ var shareTypes = fileInfo.get('shareTypes').filter(function(shareType) {
+ return shareType === OC.Share.SHARE_TYPE_USER ||
+ shareType === OC.Share.SHARE_TYPE_GROUP ||
+ shareType === OC.Share.SHARE_TYPE_CIRCLE ||
+ shareType === OC.Share.SHARE_TYPE_ROOM;
+ });
+
+ if (shareTypes.length === 0) {
+ return false;
+ }
+
+ return true;
+ },
+
+ setupSignalingEventHandlers: function() {
+ OCA.SpreedMe.app.signaling.on('joinRoom', function(joinedRoomToken) {
+ if (OCA.SpreedMe.app.token !== joinedRoomToken) {
+ return;
+ }
+
+ OCA.SpreedMe.app.signaling.syncRooms().then(function() {
+ OCA.SpreedMe.app._messageCollection.setRoomToken(OCA.SpreedMe.app.activeRoom.get('token'));
+ OCA.SpreedMe.app._messageCollection.receiveMessages();
+ });
+ });
+ },
+
+ joinRoom: function(token) {
+ OCA.SpreedMe.app.activeRoom = new OCA.SpreedMe.Models.Room({token: token});
+ OCA.SpreedMe.app.signaling.setRoom(OCA.SpreedMe.app.activeRoom);
+
+ OCA.SpreedMe.app.token = token;
+ OCA.SpreedMe.app.signaling.joinRoom(token);
+ },
+
+ leaveCurrentRoom: function() {
+ OCA.SpreedMe.app.signaling.leaveCurrentRoom();
+
+ roomsChannel.trigger('leaveCurrentRoom');
+
+ OCA.SpreedMe.app.token = null;
+ OCA.SpreedMe.app.activeRoom = null;
+ }
+
+ };
+
+ OCA.SpreedMe.app = new OCA.Talk.Embedded();
+
+ OC.Plugins.register('OCA.Files.FileList', OCA.Talk.FilesPlugin);
+
+})(OC, OCA);
diff --git a/js/views/chatview.js b/js/views/chatview.js
index bc0c14d60af..e4f17879075 100644
--- a/js/views/chatview.js
+++ b/js/views/chatview.js
@@ -283,19 +283,55 @@
},
/**
- * Restores the scroll position of the message list to the last saved
- * position.
+ * Restores the scroll position of the message list.
+ *
+ * The scroll position is restored to the given position or, if none is
+ * given, to the last saved position. If neither a scroll position is
+ * given nor a scroll position was saved the current scroll position is
+ * not modified.
*
* Note that the saved scroll position is valid only if the chat view
* was not resized since it was saved; restoring the scroll position
* after the chat view was resized may or may not work as expected.
+ *
+ * @param {number} scrollPosition the scroll position to restore to, or
+ * undefined to restore to the last saved position.
+ */
+ restoreScrollPosition: function(scrollPosition) {
+ if (_.isUndefined(this.$container) ||
+ (_.isUndefined(this._savedScrollPosition) && _.isUndefined(scrollPosition))) {
+ return;
+ }
+
+ if (_.isUndefined(scrollPosition)) {
+ this.$container.scrollTop(this._savedScrollPosition);
+
+ return;
+ }
+
+ this.$container.scrollTop(scrollPosition);
+ },
+
+ /**
+ * Returns the last known scroll position of the message list.
+ *
+ * Note that this value is updated asynchronously, so in some cases it
+ * will not match the current scroll position of the message list.
+ * Moreover, it could also be influenced in surprising ways, for
+ * example, by animations that change the width of the message list.
+ *
+ * If possible, save the scroll position explicitly at a known safe
+ * point to be able to restore to it instead of restoring to the value
+ * returned by this method.
+ *
+ * @return {number} the last known scroll position of the message list.
*/
- restoreScrollPosition: function() {
- if (_.isUndefined(this.$container) || _.isUndefined(this._savedScrollPosition)) {
+ getLastKnownScrollPosition: function() {
+ if (_.isUndefined(this._virtualList)) {
return;
}
- this.$container.scrollTop(this._savedScrollPosition);
+ return this._virtualList.getLastKnownScrollPosition();
},
/**
diff --git a/js/views/virtuallist.js b/js/views/virtuallist.js
index ec4f1fe2900..e2e1172a947 100644
--- a/js/views/virtuallist.js
+++ b/js/views/virtuallist.js
@@ -72,6 +72,15 @@
* the position and size of all the elements, so in that case "reload()"
* needs to be called.
*
+ * Some operations on the virtual list, like reloading it, updating the
+ * visible elements or scrolling to certain element, require that the
+ * container is visible; if called while the container is hidden those
+ * operations will just be ignored.
+ *
+ * Adding new elements is still possible while the virtual list is hidden,
+ * but note that "reload()" must be explicitly called once the container is
+ * visible again for the added elements to be loaded.
+ *
*
*
* Internal description:
@@ -174,6 +183,8 @@
var self = this;
this._$container.on('scroll', function() {
+ self._lastKnownScrollPosition = self._$container.scrollTop();
+
self.updateVisibleElements();
});
};
@@ -196,6 +207,10 @@
return this._$lastVisibleElement;
},
+ getLastKnownScrollPosition: function() {
+ return this._lastKnownScrollPosition;
+ },
+
prependElementStart: function() {
this._prependedElementsBuffer = document.createDocumentFragment();
@@ -253,6 +268,12 @@
},
prependElementEnd: function() {
+ if (this._isContainerHidden()) {
+ delete this._prependedElementsBuffer;
+
+ return;
+ }
+
// If the prepended elements are not immediately before the first
// loaded element there is nothing to load now; they will be loaded
// as needed with the other pending elements.
@@ -282,6 +303,12 @@
},
appendElementEnd: function() {
+ if (this._isContainerHidden()) {
+ delete this._prependedElementsBuffer;
+
+ return;
+ }
+
// If the appended elements are not immediately after the last
// loaded element there is nothing to load now; they will be loaded
// as needed with the other pending elements.
@@ -325,7 +352,8 @@
*
* On the other hand, when only the height has changed no reload is
* needed; in that case the visibility of the elements is updated based
- * on the new height.
+ * on the new height. If some elements were added to the list while its
+ * container was hidden they will be loaded too without a full reload.
*
* Reloading the list requires to recalculate the position and size of
* all the elements. The initial call reloads the last visible element
@@ -343,11 +371,20 @@
* elements is a float.
*/
reload: function() {
+ if (this._isContainerHidden()) {
+ return;
+ }
+
if (this._lastContainerWidth === this._$container.width()) {
// If the width is the same the cache is still valid, so no need
// for a full reload.
this.updateVisibleElements();
+ if (this._$firstLoadedElement !== this._$firstElement ||
+ this._$lastLoadedElement !== this._$lastElement) {
+ this._queueLoadOfPendingElements();
+ }
+
return;
}
@@ -466,6 +503,10 @@
this._pendingLoad = setTimeout(function() {
delete this._pendingLoad;
+ if (this._isContainerHidden()) {
+ return;
+ }
+
var numberOfElementsToLoad = 200;
numberOfElementsToLoad -= this._loadPreviousPendingElements(numberOfElementsToLoad/2);
this._loadNextPendingElements(numberOfElementsToLoad);
@@ -808,6 +849,10 @@
* of a pixel would be wrongly shown or hidden.
*/
updateVisibleElements: function() {
+ if (this._isContainerHidden()) {
+ return;
+ }
+
if (!this._$firstVisibleElement && !this._$firstLoadedElement) {
return;
}
@@ -909,6 +954,10 @@
this._$wrapper.appendTo(this._$container);
},
+ _isContainerHidden: function() {
+ return this._$container.is(":hidden");
+ },
+
/**
* Scroll the list to the given element.
*
@@ -918,6 +967,10 @@
* @param {jQuery} $element the element of the list to scroll to.
*/
scrollTo: function($element) {
+ if (this._isContainerHidden()) {
+ return;
+ }
+
if (!this._isLoaded($element)) {
return;
}
diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php
index 877c9472a7d..991b09c9e21 100644
--- a/lib/AppInfo/Application.php
+++ b/lib/AppInfo/Application.php
@@ -89,6 +89,14 @@ public function register() {
/** @var \OCA\Spreed\PublicShareAuth\TemplateLoader $shareAuthTemplateLoader */
$shareAuthTemplateLoader = $this->getContainer()->query(\OCA\Spreed\PublicShareAuth\TemplateLoader::class);
$shareAuthTemplateLoader->register();
+
+ /** @var \OCA\Spreed\Files\Listener $filesListener */
+ $filesListener = $this->getContainer()->query(\OCA\Spreed\Files\Listener::class);
+ $filesListener->register();
+
+ /** @var \OCA\Spreed\Files\TemplateLoader $filesTemplateLoader */
+ $filesTemplateLoader = $this->getContainer()->query(\OCA\Spreed\Files\TemplateLoader::class);
+ $filesTemplateLoader->register();
}
protected function registerNotifier(IServerContainer $server) {
diff --git a/lib/BackgroundJob/RemoveEmptyRooms.php b/lib/BackgroundJob/RemoveEmptyRooms.php
index 203697488fe..35df6200baa 100644
--- a/lib/BackgroundJob/RemoveEmptyRooms.php
+++ b/lib/BackgroundJob/RemoveEmptyRooms.php
@@ -64,7 +64,7 @@ public function callback(Room $room) {
if ($room->getType() === Room::ONE_TO_ONE_CALL && $room->getNumberOfParticipants(false) <= 1) {
$room->deleteRoom();
$this->numDeletedRooms++;
- } else if ($room->getNumberOfParticipants(false) === 0) {
+ } else if ($room->getNumberOfParticipants(false) === 0 && $room->getObjectType() !== 'file') {
$room->deleteRoom();
$this->numDeletedRooms++;
}
diff --git a/lib/Chat/AutoComplete/SearchPlugin.php b/lib/Chat/AutoComplete/SearchPlugin.php
index 3b7ec2d12c6..e48489c9b2a 100644
--- a/lib/Chat/AutoComplete/SearchPlugin.php
+++ b/lib/Chat/AutoComplete/SearchPlugin.php
@@ -22,6 +22,7 @@
namespace OCA\Spreed\Chat\AutoComplete;
+use OCA\Spreed\Files\Util;
use OCA\Spreed\Room;
use OCP\Collaboration\Collaborators\ISearchPlugin;
use OCP\Collaboration\Collaborators\ISearchResult;
@@ -33,6 +34,8 @@ class SearchPlugin implements ISearchPlugin {
/** @var IUserManager */
protected $userManager;
+ /** @var Util */
+ protected $util;
/** @var string */
protected $userId;
@@ -40,8 +43,9 @@ class SearchPlugin implements ISearchPlugin {
/** @var Room */
protected $room;
- public function __construct(IUserManager $userManager, $userId) {
+ public function __construct(IUserManager $userManager, Util $util, $userId) {
$this->userManager = $userManager;
+ $this->util = $util;
$this->userId = $userId;
}
@@ -62,6 +66,13 @@ public function search($search, $limit, $offset, ISearchResult $searchResult) {
// FIXME Handle guests
$this->searchUsers($search, $this->room->getParticipantUserIds(), $searchResult);
+ if ($this->room->getObjectType() === 'file') {
+ $usersWithFileAccess = $this->util->getUsersWithAccessFile($this->room->getObjectId());
+ if (!empty($usersWithFileAccess)) {
+ $this->searchUsers($search, $usersWithFileAccess, $searchResult);
+ }
+ }
+
return false;
}
diff --git a/lib/Chat/Notifier.php b/lib/Chat/Notifier.php
index 99d580f53f8..516291659c5 100644
--- a/lib/Chat/Notifier.php
+++ b/lib/Chat/Notifier.php
@@ -25,6 +25,7 @@
use OCA\Spreed\Exceptions\ParticipantNotFoundException;
use OCA\Spreed\Exceptions\RoomNotFoundException;
+use OCA\Spreed\Files\Util;
use OCA\Spreed\Manager;
use OCA\Spreed\Participant;
use OCA\Spreed\Room;
@@ -51,12 +52,17 @@ class Notifier {
/** @var Manager */
private $manager;
+ /** @var Util */
+ private $util;
+
public function __construct(INotificationManager $notificationManager,
IUserManager $userManager,
- Manager $manager) {
+ Manager $manager,
+ Util $util) {
$this->notificationManager = $notificationManager;
$this->userManager = $userManager;
$this->manager = $manager;
+ $this->util = $util;
}
/**
@@ -273,11 +279,17 @@ private function shouldUserBeNotified($userId, IComment $comment) {
try {
$room = $this->manager->getRoomById($comment->getObjectId());
- $participant = $room->getParticipant($userId);
- return $participant->getNotificationLevel() !== Participant::NOTIFY_NEVER;
} catch (RoomNotFoundException $e) {
return false;
+ }
+
+ try {
+ $participant = $room->getParticipant($userId);
+ return $participant->getNotificationLevel() !== Participant::NOTIFY_NEVER;
} catch (ParticipantNotFoundException $e) {
+ if ($room->getObjectType() === 'file') {
+ return $this->util->canUserAccessFile($room->getObjectId(), $userId);
+ }
return false;
}
}
diff --git a/lib/Controller/FilesController.php b/lib/Controller/FilesController.php
new file mode 100644
index 00000000000..a706b8404a0
--- /dev/null
+++ b/lib/Controller/FilesController.php
@@ -0,0 +1,109 @@
+.
+ *
+ */
+
+namespace OCA\Spreed\Controller;
+
+use OCA\Spreed\Exceptions\RoomNotFoundException;
+use OCA\Spreed\Files\Util;
+use OCA\Spreed\Manager;
+use OCP\AppFramework\Http\DataResponse;
+use OCP\AppFramework\OCS\OCSNotFoundException;
+use OCP\AppFramework\OCSController;
+use OCP\IL10N;
+use OCP\IRequest;
+
+class FilesController extends OCSController {
+
+ /** @var string */
+ private $currentUser;
+ /** @var Manager */
+ private $manager;
+ /** @var Util */
+ private $util;
+ /** @var IL10N */
+ private $l;
+
+ /**
+ * @param string $appName
+ * @param IRequest $request
+ * @param string $userId
+ * @param Manager $manager
+ * @param Util $util
+ * @param IL10N $l10n
+ */
+ public function __construct(
+ string $appName,
+ IRequest $request,
+ string $userId,
+ Manager $manager,
+ Util $util,
+ IL10N $l10n
+ ) {
+ parent::__construct($appName, $request);
+ $this->currentUser = $userId;
+ $this->manager = $manager;
+ $this->util = $util;
+ $this->l = $l10n;
+ }
+
+ /**
+ * @NoAdminRequired
+ *
+ * Returns the token of the room associated to the given file id.
+ *
+ * If there is no room associated to the given file id a new room is
+ * created; the new room is a public room associated with a "file" object
+ * with the given file id. Unlike normal rooms in which the owner is the
+ * user that created the room these are special rooms without owner or any
+ * other persistent participant.
+ *
+ * In any case, to create or even get the token of the room, the file must
+ * be shared and the user must have direct access to that file; an error
+ * is returned otherwise. A user has direct access to a file if she has
+ * access to it through a user, group, circle or room share (but not through
+ * a link share, for example), or if she is the owner of such a file.
+ *
+ * @param string $fileId
+ * @return DataResponse the status code is "200 OK" if a room is returned,
+ * or "404 Not found" if the given file id was invalid.
+ */
+ public function getRoom(string $fileId): DataResponse {
+ $share = $this->util->getAnyDirectShareOfFileAccessibleByUser($fileId, $this->currentUser);
+ if (!$share) {
+ throw new OCSNotFoundException($this->l->t('File is not shared, or shared but not with the user'));
+ }
+
+ try {
+ $room = $this->manager->getRoomByObject('file', $fileId);
+ } catch (RoomNotFoundException $e) {
+ $node = $share->getNode();
+ $room = $this->manager->createPublicRoom($node->getName(), 'file', $fileId);
+ }
+
+ return new DataResponse([
+ 'token' => $room->getToken()
+ ]);
+ }
+
+}
diff --git a/lib/Controller/PublicShareAuthController.php b/lib/Controller/PublicShareAuthController.php
index c0d61394d84..a180d6dbeae 100644
--- a/lib/Controller/PublicShareAuthController.php
+++ b/lib/Controller/PublicShareAuthController.php
@@ -33,14 +33,14 @@
use OCP\IUser;
use OCP\IUserManager;
use OCP\Share;
-use OCP\Share\IManager as ShareManager;
+use OCP\Share\IManager as IShareManager;
use OCP\Share\Exceptions\ShareNotFound;
class PublicShareAuthController extends OCSController {
/** @var IUserManager */
private $userManager;
- /** @var ShareManager */
+ /** @var IShareManager */
private $shareManager;
/** @var Manager */
private $manager;
@@ -49,14 +49,14 @@ class PublicShareAuthController extends OCSController {
* @param string $appName
* @param IRequest $request
* @param IUserManager $userManager
- * @param ShareManager $shareManager
+ * @param IShareManager $shareManager
* @param Manager $manager
*/
public function __construct(
string $appName,
IRequest $request,
IUserManager $userManager,
- ShareManager $shareManager,
+ IShareManager $shareManager,
Manager $manager
) {
parent::__construct($appName, $request);
diff --git a/lib/Files/Listener.php b/lib/Files/Listener.php
new file mode 100644
index 00000000000..965c662d152
--- /dev/null
+++ b/lib/Files/Listener.php
@@ -0,0 +1,114 @@
+.
+ *
+ */
+
+namespace OCA\Spreed\Files;
+
+use OCA\Spreed\Room;
+use Symfony\Component\EventDispatcher\EventDispatcherInterface;
+use Symfony\Component\EventDispatcher\GenericEvent;
+
+/**
+ * Custom behaviour for rooms for files.
+ *
+ * The rooms for files are intended to give the users a way to talk about a
+ * specific shared file, for example, when collaboratively editing it. The room
+ * is persistent and can be accessed simultaneously by any user with direct
+ * access (user, group, circle and room share, but not link share, for example)
+ * to that file. The room has no owner, and there are no persistent participants
+ * (it is a public room that users join and leave on each session).
+ *
+ * These rooms are associated to a "file" object, and their custom behaviour is
+ * provided by calling the methods of this class as a response to different room
+ * events.
+ */
+class Listener {
+
+ /** @var EventDispatcherInterface */
+ protected $dispatcher;
+ /** @var Util */
+ protected $util;
+
+ public function __construct(EventDispatcherInterface $dispatcher, Util $util) {
+ $this->dispatcher = $dispatcher;
+ $this->util = $util;
+ }
+
+ public function register() {
+ $listener = function(GenericEvent $event) {
+ /** @var Room $room */
+ $room = $event->getSubject();
+ $this->preventUsersWithoutDirectAccessToTheFileFromJoining($room, $event->getArgument('userId'));
+ };
+ $this->dispatcher->addListener(Room::class . '::preJoinRoom', $listener);
+
+ $listener = function(GenericEvent $event) {
+ /** @var Room $room */
+ $room = $event->getSubject();
+ $this->preventGuestsFromJoining($room);
+ };
+ $this->dispatcher->addListener(Room::class . '::preJoinRoomGuest', $listener);
+ }
+
+ /**
+ * Prevents users from joining if they do not have direct access to the
+ * file.
+ *
+ * A user has direct access to a file if she received the file through a
+ * user, group, circle or room share (but not through a link share, for
+ * example), or if she is the owner of such a file.
+ *
+ * This method should be called before a user joins a room.
+ *
+ * @param Room $room
+ * @param string $userId
+ * @throws \Exception
+ */
+ public function preventUsersWithoutDirectAccessToTheFileFromJoining(Room $room, string $userId) {
+ if ($room->getObjectType() !== 'file') {
+ return;
+ }
+
+ $share = $this->util->getAnyDirectShareOfFileAccessibleByUser($room->getObjectId(), $userId);
+ if (!$share) {
+ throw new \Exception('User does not have direct access to the file');
+ }
+ }
+
+ /**
+ * Prevents guests from joining the room.
+ *
+ * This method should be called before a guest joins a room.
+ *
+ * @param Room $room
+ * @throws \Exception
+ */
+ public function preventGuestsFromJoining(Room $room) {
+ if ($room->getObjectType() !== 'file') {
+ return;
+ }
+
+ throw new \Exception('Guests are not allowed in rooms for files');
+ }
+
+}
diff --git a/lib/Files/TemplateLoader.php b/lib/Files/TemplateLoader.php
new file mode 100644
index 00000000000..70418d0480d
--- /dev/null
+++ b/lib/Files/TemplateLoader.php
@@ -0,0 +1,82 @@
+.
+ *
+ */
+
+namespace OCA\Spreed\Files;
+
+use OCP\Util;
+use Symfony\Component\EventDispatcher\EventDispatcherInterface;
+use Symfony\Component\EventDispatcher\GenericEvent;
+
+/**
+ * Helper class to add the Talk UI to the sidebar of the Files app.
+ */
+class TemplateLoader {
+
+ /** @var EventDispatcherInterface */
+ protected $dispatcher;
+
+ public function __construct(EventDispatcherInterface $dispatcher) {
+ $this->dispatcher = $dispatcher;
+ }
+
+ public function register() {
+ $listener = function() {
+ $this->loadTalkSidebarForFilesApp();
+ };
+ $this->dispatcher->addListener('OCA\Files::loadAdditionalScripts', $listener);
+ }
+
+ /**
+ * Loads the Talk UI in the sidebar of the Files app.
+ *
+ * This method should be called when loading additional scripts for the
+ * Files app.
+ */
+ public function loadTalkSidebarForFilesApp() {
+ Util::addStyle('spreed', 'files');
+ Util::addStyle('spreed', 'chatview');
+ Util::addStyle('spreed', 'autocomplete');
+
+ Util::addScript('spreed', 'vendor/backbone/backbone-min');
+ Util::addScript('spreed', 'vendor/backbone.radio/build/backbone.radio.min');
+ Util::addScript('spreed', 'vendor/backbone.marionette/lib/backbone.marionette.min');
+ Util::addScript('spreed', 'vendor/jshashes/hashes.min');
+ Util::addScript('spreed', 'vendor/Caret.js/dist/jquery.caret.min');
+ Util::addScript('spreed', 'vendor/At.js/dist/js/jquery.atwho.min');
+ Util::addScript('spreed', 'models/chatmessage');
+ Util::addScript('spreed', 'models/chatmessagecollection');
+ Util::addScript('spreed', 'models/room');
+ Util::addScript('spreed', 'models/roomcollection');
+ Util::addScript('spreed', 'views/chatview');
+ Util::addScript('spreed', 'views/editabletextlabel');
+ Util::addScript('spreed', 'views/richobjectstringparser');
+ Util::addScript('spreed', 'views/templates');
+ Util::addScript('spreed', 'views/virtuallist');
+ Util::addScript('spreed', 'signaling');
+ Util::addScript('spreed', 'connection');
+ Util::addScript('spreed', 'embedded');
+ Util::addScript('spreed', 'filesplugin');
+ }
+
+}
diff --git a/lib/Files/Util.php b/lib/Files/Util.php
new file mode 100644
index 00000000000..a6fd328558c
--- /dev/null
+++ b/lib/Files/Util.php
@@ -0,0 +1,171 @@
+.
+ *
+ */
+
+namespace OCA\Spreed\Files;
+
+use OCP\Files\IRootFolder;
+use OCP\Files\Node;
+use OCP\Share\IManager as IShareManager;
+use OCP\Share\IShare;
+
+class Util {
+
+ /** @var IRootFolder */
+ private $rootFolder;
+ /** @var IShareManager */
+ private $shareManager;
+ /** @var array[] */
+ private $accessLists = [];
+
+ /**
+ * @param IRootFolder $rootFolder
+ * @param IShareManager $shareManager
+ */
+ public function __construct(
+ IRootFolder $rootFolder,
+ IShareManager $shareManager
+ ) {
+ $this->rootFolder = $rootFolder;
+ $this->shareManager = $shareManager;
+ }
+
+ public function getUsersWithAccessFile(string $fileId): array {
+ if (!isset($this->accessLists[$fileId])) {
+ $nodes = $this->rootFolder->getById($fileId);
+
+ if (empty($nodes)) {
+ return [];
+ }
+
+ $node = array_shift($nodes);
+ $accessList = $this->shareManager->getAccessList($node);
+
+ $this->accessLists[$fileId] = $accessList['users'];
+ }
+
+ return $this->accessLists[$fileId];
+ }
+
+ public function canUserAccessFile(string $fileId, string $userId): bool {
+ return \in_array($userId, $this->getUsersWithAccessFile($fileId), true);
+ }
+
+ /**
+ * Returns any share of the file that the user has direct access to.
+ *
+ * A user has direct access to a share and, thus, to a file, if she received
+ * the file through a user, group, circle or room share (but not through a
+ * public link, for example), or if she is the owner of such a share.
+ *
+ * Only files are taken into account; folders are ignored.
+ *
+ * @param string $fileId
+ * @param string $userId
+ * @return IShare|null
+ */
+ public function getAnyDirectShareOfFileAccessibleByUser(string $fileId, string $userId) {
+ $userFolder = $this->rootFolder->getUserFolder($userId);
+ $fileById = $userFolder->getById($fileId);
+ if (empty($fileById)) {
+ return null;
+ }
+
+ foreach ($fileById as $node) {
+ $share = $this->getAnyDirectShareOfNodeAccessibleByUser($node, $userId);
+ if ($share) {
+ return $share;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns any share of the node that the user has direct access to.
+ *
+ * Only files are taken into account; folders are ignored.
+ *
+ * @param Node $node
+ * @param string $userId
+ * @return IShare|null
+ */
+ private function getAnyDirectShareOfNodeAccessibleByUser(Node $node, string $userId) {
+ if ($node->getType() === \OCP\Files\FileInfo::TYPE_FOLDER) {
+ return null;
+ }
+
+ $reshares = false;
+ $limit = 1;
+
+ $shares = $this->shareManager->getSharesBy($userId, \OCP\Share::SHARE_TYPE_USER, $node, $reshares, $limit);
+ if (\count($shares) > 0) {
+ return $shares[0];
+ }
+
+ $shares = $this->shareManager->getSharesBy($userId, \OCP\Share::SHARE_TYPE_GROUP, $node, $reshares, $limit);
+ if (\count($shares) > 0) {
+ return $shares[0];
+ }
+
+ $shares = $this->shareManager->getSharesBy($userId, \OCP\Share::SHARE_TYPE_CIRCLE, $node, $reshares, $limit);
+ if (\count($shares) > 0) {
+ return $shares[0];
+ }
+
+ $shares = $this->shareManager->getSharesBy($userId, \OCP\Share::SHARE_TYPE_ROOM, $node, $reshares, $limit);
+ if (\count($shares) > 0) {
+ return $shares[0];
+ }
+
+ // If the node is not shared then there is no need for further checks.
+ // Note that "isShared()" returns false for owned shares, so the check
+ // can not be moved above.
+ if (!$node->isShared()) {
+ return null;
+ }
+
+ $shares = $this->shareManager->getSharedWith($userId, \OCP\Share::SHARE_TYPE_USER, $node, $limit);
+ if (\count($shares) > 0) {
+ return $shares[0];
+ }
+
+ $shares = $this->shareManager->getSharedWith($userId, \OCP\Share::SHARE_TYPE_GROUP, $node, $limit);
+ if (\count($shares) > 0) {
+ return $shares[0];
+ }
+
+ $shares = $this->shareManager->getSharedWith($userId, \OCP\Share::SHARE_TYPE_CIRCLE, $node, $limit);
+ if (\count($shares) > 0) {
+ return $shares[0];
+ }
+
+ $shares = $this->shareManager->getSharedWith($userId, \OCP\Share::SHARE_TYPE_ROOM, $node, $limit);
+ if (\count($shares) > 0) {
+ return $shares[0];
+ }
+
+ return null;
+ }
+
+}
diff --git a/lib/Manager.php b/lib/Manager.php
index 3316ee31d45..6d1865b6ac1 100644
--- a/lib/Manager.php
+++ b/lib/Manager.php
@@ -298,6 +298,30 @@ public function getRoomByToken($token) {
return $this->createRoomObject($row);
}
+ /**
+ * @param string $objectType
+ * @param string $objectId
+ * @return Room
+ * @throws RoomNotFoundException
+ */
+ public function getRoomByObject(string $objectType, string $objectId): Room {
+ $query = $this->db->getQueryBuilder();
+ $query->select('*')
+ ->from('talk_rooms')
+ ->where($query->expr()->eq('object_type', $query->createNamedParameter($objectType)))
+ ->andWhere($query->expr()->eq('object_id', $query->createNamedParameter($objectId)));
+
+ $result = $query->execute();
+ $row = $result->fetch();
+ $result->closeCursor();
+
+ if ($row === false) {
+ throw new RoomNotFoundException();
+ }
+
+ return $this->createRoomObject($row);
+ }
+
/**
* @param string|null $userId
* @param string $sessionId
diff --git a/lib/Notification/Hooks.php b/lib/Notification/Hooks.php
index 8db2a44551c..ebf2d1db532 100644
--- a/lib/Notification/Hooks.php
+++ b/lib/Notification/Hooks.php
@@ -99,6 +99,10 @@ public function generateCallNotifications(Room $room) {
return;
}
+ if ($room->getObjectType() === 'file') {
+ return;
+ }
+
$actor = $this->userSession->getUser();
$actorId = $actor instanceof IUser ? $actor->getUID() :'';
diff --git a/tests/php/Chat/AutoComplete/SearchPluginTest.php b/tests/php/Chat/AutoComplete/SearchPluginTest.php
index b4eff43da59..7ce7bfd5543 100644
--- a/tests/php/Chat/AutoComplete/SearchPluginTest.php
+++ b/tests/php/Chat/AutoComplete/SearchPluginTest.php
@@ -22,6 +22,7 @@
namespace OCA\Spreed\Tests\php\Chat;
use OCA\Spreed\Chat\AutoComplete\SearchPlugin;
+use OCA\Spreed\Files\Util;
use OCA\Spreed\Room;
use OCP\Collaboration\Collaborators\ISearchResult;
use OCP\IUser;
@@ -32,6 +33,9 @@ class SearchPluginTest extends \Test\TestCase {
/** @var IUserManager|\PHPUnit_Framework_MockObject_MockObject */
protected $userManager;
+ /** @var Util|\PHPUnit_Framework_MockObject_MockObject */
+ protected $util;
+
/** @var string */
protected $userId;
@@ -42,6 +46,7 @@ public function setUp() {
parent::setUp();
$this->userManager = $this->createMock(IUserManager::class);
+ $this->util = $this->createMock(Util::class);
$this->userId = 'current';
}
@@ -53,6 +58,7 @@ protected function getPlugin(array $methods = []) {
if (empty($methods)) {
return new SearchPlugin(
$this->userManager,
+ $this->util,
$this->userId
);
}
@@ -60,6 +66,7 @@ protected function getPlugin(array $methods = []) {
return $this->getMockBuilder(SearchPlugin::class)
->setConstructorArgs([
$this->userManager,
+ $this->util,
$this->userId,
])
->setMethods($methods)
diff --git a/tests/php/Chat/NotifierTest.php b/tests/php/Chat/NotifierTest.php
index bdf32fa6610..3c26e7fa94d 100644
--- a/tests/php/Chat/NotifierTest.php
+++ b/tests/php/Chat/NotifierTest.php
@@ -25,6 +25,7 @@
use OCA\Spreed\Chat\Notifier;
use OCA\Spreed\Exceptions\ParticipantNotFoundException;
+use OCA\Spreed\Files\Util;
use OCA\Spreed\Manager;
use OCA\Spreed\Participant;
use OCA\Spreed\Room;
@@ -44,6 +45,9 @@ class NotifierTest extends \Test\TestCase {
/** @var \OCA\Spreed\Manager|\PHPUnit_Framework_MockObject_MockObject */
protected $manager;
+ /** @var \OCA\Spreed\Files\Util|\PHPUnit_Framework_MockObject_MockObject */
+ protected $util;
+
/** @var \OCA\Spreed\Chat\Notifier */
protected $notifier;
@@ -65,9 +69,12 @@ public function setUp() {
$this->manager = $this->createMock(Manager::class);
+ $this->util = $this->createMock(Util::class);
+
$this->notifier = new Notifier($this->notificationManager,
$this->userManager,
- $this->manager);
+ $this->manager,
+ $this->util);
}
private function newComment($id, $actorType, $actorId, $creationDateTime, $message) {
diff --git a/tests/php/Notification/NotifierTest.php b/tests/php/Notification/NotifierTest.php
index b21602da728..a6aeb1bd5b0 100644
--- a/tests/php/Notification/NotifierTest.php
+++ b/tests/php/Notification/NotifierTest.php
@@ -35,7 +35,7 @@
use OCP\L10N\IFactory;
use OCP\Notification\INotification;
use OCP\RichObjectStrings\Definitions;
-use OCP\Share\IManager as ShareManager;
+use OCP\Share\IManager as IShareManager;
use PHPUnit\Framework\MockObject\MockObject;
class NotifierTest extends \Test\TestCase {
@@ -46,7 +46,7 @@ class NotifierTest extends \Test\TestCase {
protected $url;
/** @var IUserManager|\PHPUnit_Framework_MockObject_MockObject */
protected $userManager;
- /** @var ShareManager|\PHPUnit_Framework_MockObject_MockObject */
+ /** @var IShareManager|\PHPUnit_Framework_MockObject_MockObject */
protected $shareManager;
/** @var Manager|\PHPUnit_Framework_MockObject_MockObject */
protected $manager;
@@ -65,7 +65,7 @@ public function setUp() {
$this->lFactory = $this->createMock(IFactory::class);
$this->url = $this->createMock(IURLGenerator::class);
$this->userManager = $this->createMock(IUserManager::class);
- $this->shareManager = $this->createMock(ShareManager::class);
+ $this->shareManager = $this->createMock(IShareManager::class);
$this->manager = $this->createMock(Manager::class);
$this->commentsManager = $this->createMock(ICommentsManager::class);
$this->messageParser = $this->createMock(MessageParser::class);