[NEW] Room files search form (#11486)

* Keep only one PhotoSwipe gallery open

* Refactor PhotoSwipe code

* Handle preloading in PhotoSwipe

* Use common fixCordova helper in uploadFilesList

* Fix log helper

* Fix the server-side handling of room_files collection

* Improvements in uploadedFilesList

* Adjust margins

* Add file name filter for uploaded files

* Add search form for uploaded files

* Add missing translation key

* Fix indentation in template

* Add layout changes

* Fix progressive loading in uploaded files list

* Add rel="noopener noreferrer" to links in uploaded files list

* Rename parameter

* Handle image loading for PhotoShipe gallery

* Show images in uploaded files list in PhotoSwipe gallery

* Add escapeCssUrl helper

* Escape thumbnail URL in uploaded files list

* Force download on uploaded files list

* Add a publication for uploaded files list with search text

* Update roomFiles.js

* Update roomFilesWithSearchText.js
pull/11164/head^2
Tasso Evangelista 8 years ago committed by Guilherme Gazzo
parent f47cf2a449
commit 0306438ecf
  1. 3
      client/helpers/escapeCssUrl.js
  2. 4
      client/helpers/log.js
  3. 2
      packages/rocketchat-i18n/i18n/en.i18n.json
  4. 6
      packages/rocketchat-lib/server/models/Uploads.js
  5. 28
      packages/rocketchat-theme/client/imports/components/contextual-bar.css
  6. 3
      packages/rocketchat-theme/client/imports/components/file-list.css
  7. 44
      packages/rocketchat-theme/client/imports/general/base_old.css
  8. 16
      packages/rocketchat-theme/client/imports/general/rtl.css
  9. 1
      packages/rocketchat-theme/client/main.css
  10. 54
      packages/rocketchat-ui-flextab/client/tabs/uploadedFilesList.html
  11. 207
      packages/rocketchat-ui-flextab/client/tabs/uploadedFilesList.js
  12. 42
      packages/rocketchat-ui/client/components/contextualBar.html
  13. 113
      packages/rocketchat-ui/client/views/app/photoswipe.js
  14. 27
      server/lib/roomFiles.js
  15. 28
      server/publications/roomFiles.js
  16. 5
      server/publications/roomFilesWithSearchText.js

@ -0,0 +1,3 @@
Template.registerHelper('escapeCssUrl', url => {
return url.replace(/(['"])/g, '\\$1');
});

@ -1,3 +1,3 @@
Template.registerHelper('log', () => {
console.log.apply(console, arguments);
Template.registerHelper('log', (...args) => {
console.log.apply(console, args);
});

@ -1113,6 +1113,7 @@
"Field_removed": "Field removed",
"Field_required": "Field required",
"File_exceeds_allowed_size_of_bytes": "File exceeds allowed size of __size__.",
"File_name_Placeholder": "Search files...",
"File_not_allowed_direct_messages": "File sharing not allowed in direct messages.",
"File_type_is_not_accepted": "File type is not accepted.",
"File_uploaded": "File uploaded",
@ -2160,6 +2161,7 @@
"Screen_Share": "Screen Share",
"Script_Enabled": "Script Enabled",
"Search": "Search",
"Search_by_file_name": "Search by file name",
"Search_by_username": "Search by username",
"Search_Channels": "Search Channels",
"Search_current_provider_not_active": "Current Search Provider is not active",

@ -14,7 +14,7 @@ RocketChat.models.Uploads = new class extends RocketChat.models._Base {
this.tryEnsureIndex({ 'uploadedAt': 1 });
}
findNotHiddenFilesOfRoom(roomId, limit) {
findNotHiddenFilesOfRoom(roomId, searchText, limit) {
const fileQuery = {
rid: roomId,
complete: true,
@ -24,6 +24,10 @@ RocketChat.models.Uploads = new class extends RocketChat.models._Base {
}
};
if (searchText) {
fileQuery.name = { $regex: new RegExp(RegExp.escape(searchText), 'i') };
}
const fileOptions = {
limit,
sort: {

@ -119,20 +119,21 @@
.attachments {
&__item {
display: flex;
overflow: hidden;
flex: 1;
transition: background-color 0.3s;
align-items: center;
transition: background-color 300ms linear;
margin-bottom: 10px;
&:hover {
cursor: pointer;
background-color: #f7f8fa;
}
&-link {
padding: 8px 0;
display: flex;
flex-direction: row;
align-items: center;
}
}
&__file,
@ -145,7 +146,7 @@
width: 50px;
height: 50px;
margin: 8px;
margin: 0 8px;
border-radius: 2px;
@ -187,16 +188,17 @@
}
&__name {
overflow: hidden;
margin: 0 8px 8px;
margin: 0 8px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
min-width: 0;
color: #2f343d;
font-size: 14px;
line-height: 1.5;
}
&__details {
@ -220,8 +222,6 @@
flex: 1 1 100%;
height: 50px;
color: #9ea2a8;
}
}

@ -1,3 +0,0 @@
.attachments__content .attachments__name {
overflow: unset;
}

@ -3779,50 +3779,6 @@ body:not(.is-cordova) {
margin: 1em auto 0;
}
&.uploaded-files-list {
& ul {
& li {
margin-bottom: 10px;
}
}
& .file-name {
display: block;
padding: 10px 5px;
color: #008ce3;
border-width: 0 0 1px;
border-bottom: 1px solid #eaeaea;
&:hover {
text-decoration: underline;
color: #006db0;
}
& p {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
}
& i {
float: left;
margin-right: 10px;
&.file-delete {
float: right;
}
&.file-download {
float: right;
}
}
}
}
.rc-old .user-view {

@ -670,22 +670,6 @@
& > .title .see-all {
float: left;
}
&.uploaded-files-list {
& i {
float: right;
margin-right: auto;
margin-left: 10px;
&.file-delete {
float: left;
}
&.file-download {
float: left;
}
}
}
}
& .page-list .list {

@ -48,7 +48,6 @@
@import 'imports/components/contextual-bar.css';
@import 'imports/components/emojiPicker.css';
@import 'imports/components/table.css';
@import 'imports/components/file-list.css';
@import 'imports/components/tabs.css';
/* Modal */

@ -1,27 +1,43 @@
<template name="uploadedFilesList">
<div class="uploaded-files-list list-view flex-tab__result js-list">
<form class="search-form" role="form">
<div class="rc-input">
<label class="rc-input__label">
<div class="rc-input__title">{{_ "Search_by_file_name"}}</div>
<div class="rc-input__wrapper">
<div class="rc-input__icon">
{{> icon block="rc-input__icon-svg" icon="magnifier"}}
</div>
<input type="text" class="rc-input__element uploaded-files-list__search-input" name="file-search" placeholder={{_ "File_name_Placeholder"}} autocomplete="off" />
</div>
</label>
</div>
</form>
<div class="flex-tab__result">
<ul class="attachments">
{{#each files}}
<li class="attachments__item">
{{#with thumb}}
<div class="attachments__thumb" style="background-image:url('{{.}}');"></div>
{{else}}
{{#with iconType}}
<div class="attachments__thumb attachments__file--{{type}}">
{{>icon icon=icon}}
<div class="attachments__type">{{extension}}</div>
</div>
{{/with}}
{{/with}}
<a title="{{escapedName}}" href="{{fixCordova url}}" target="_blank" class="attachments__item {{customClassForFileType}}">
<a href="{{fixCordova url}}" title="{{name}}" data-description="{{description}}"
class="attachments__item-link {{fileTypeClass}}"
download rel="noopener noreferrer" target="_blank">
{{#with thumb}}
<span class="attachments__thumb" style="background-image: url('{{escapeCssUrl .}}');"></span>
{{else}}
{{#with fileTypeIcon}}
<span class="attachments__file attachments__file--{{type}}">
{{>icon icon=id}}
<div class="attachments__type">{{extension}}</div>
</span>
{{/with}}
{{/with}}
<div class="attachments__content">
<div class="attachments__name">{{name}}</div>
<div class="attachments__details attachments__bold">@{{user.username}}</div>
<div class="attachments__details">{{format uploadedAt}}</div>
</div>
</a>
</li>
<span class="attachments__content">
<span class="attachments__name">{{name}}</span>
<span class="attachments__details attachments__bold">@{{user.username}}</span>
<span class="attachments__details">{{formatTimestamp uploadedAt}}</span>
</span>
</a>
</li>
{{/each}}
</ul>
{{#if hasMore}}

@ -1,172 +1,113 @@
/* globals chatMessages*/
import { fixCordova } from 'meteor/rocketchat:lazy-load';
import moment from 'moment';
import _ from 'underscore';
import s from 'underscore.string';
const fixCordova = (url) => {
if ((url != null ? url.indexOf('data:image') : undefined) === 0) {
return url;
}
if (Meteor.isCordova && ((url != null ? url[0] : undefined) === '/')) {
url = Meteor.absoluteUrl().replace(/\/$/, '') + url;
const query = `rc_uid=${ Meteor.userId() }&rc_token=${ Meteor._localStorage.getItem('Meteor.loginToken') }`;
if (url.indexOf('?') === -1) {
url = `${ url }?${ query }`;
} else {
url = `${ url }&${ query }`;
}
}
const roomFiles = new Mongo.Collection('room_files');
if ((Meteor.settings && Meteor.settings.public && Meteor.settings.sandstorm) || url.match(/^(https?:)?\/\//i)) {
return url;
} else if (navigator.userAgent.indexOf('Electron') > -1) {
return __meteor_runtime_config__.ROOT_URL_PATH_PREFIX + url;
} else {
return Meteor.absoluteUrl().replace(/\/$/, '') + url;
}
};
Template.uploadedFilesList.onCreated(function() {
const { rid } = Template.currentData();
this.searchText = new ReactiveVar(null);
this.hasMore = new ReactiveVar(true);
this.limit = new ReactiveVar(50);
const roomFiles = new Mongo.Collection('room_files');
this.autorun(() => {
this.subscribe('roomFilesWithSearchText', rid, this.searchText.get(), this.limit.get(), () => {
if (roomFiles.find({ rid }).fetch().length < this.limit.get()) {
this.hasMore.set(false);
}
});
});
});
Template.uploadedFilesList.helpers({
iconType() {
let icon = 'file-generic';
let type = '';
if (this.type.match(/application\/pdf/)) {
icon = 'file-pdf';
type = 'pdf';
}
if (['application/vnd.oasis.opendocument.text', 'application/vnd.oasis.opendocument.presentation'].includes(this.type)) {
icon = 'file-document';
type = 'document';
}
if (['application/vnd.ms-excel', 'application/vnd.oasis.opendocument.spreadsheet',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'].includes(this.type)) {
icon = 'file-sheets';
type = 'sheets';
}
files() {
return roomFiles.find({ rid: this.rid }, { sort: { uploadedAt: -1 } });
},
if (['application/vnd.ms-powerpoint', 'application/vnd.oasis.opendocument.presentation'].includes(this.type)) {
icon = 'file-sheets';
type = 'ppt';
}
fixCordova,
const [, extension] = this.name.match(/.*?\.(.*)$/);
url() {
return `/file-upload/${ this._id }/${ this.name }`;
},
return {icon, extension, type};
fileTypeClass() {
const [ , type ] = this.type && /^(.+?)\//.exec(this.type) || [];
if (type) {
return `room-files-${ type }`;
}
},
thumb() {
if (/image/.test(this.type)) {
return fixCordova(this.url);
}
},
format(timestamp) {
return moment(timestamp).format(RocketChat.settings.get('Message_TimeAndDateFormat') || 'LLL');
},
files() {
return roomFiles.find({ rid: this.rid }, { sort: { uploadedAt: -1 } });
},
hasFiles() {
return roomFiles.find({ rid: this.rid }).count() > 0;
},
fileTypeIcon() {
const [ , extension ] = this.name.match(/.*?\.(.*)$/);
hasMore() {
return Template.instance().hasMore.get();
},
if (this.type.match(/application\/pdf/)) {
return {
id: 'file-pdf',
type: 'pdf',
extension
};
}
getFileIcon(type) {
if (type.match(/^image\/.+$/)) {
return 'icon-picture';
if (['application/vnd.oasis.opendocument.text', 'application/vnd.oasis.opendocument.presentation'].includes(this.type)) {
return {
id: 'file-document',
type: 'document',
extension
};
}
return 'icon-docs';
},
if (['application/vnd.ms-excel', 'application/vnd.oasis.opendocument.spreadsheet',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'].includes(this.type)) {
return {
id: 'file-sheets',
type: 'sheets',
extension
};
}
customClassForFileType() {
if (this.type.match(/^image\/.+$/)) {
return 'room-files-image';
if (['application/vnd.ms-powerpoint', 'application/vnd.oasis.opendocument.presentation'].includes(this.type)) {
return {
id: 'file-sheets',
type: 'ppt',
extension
};
}
},
escapedName() {
return s.escapeHTML(this.name);
return {
id: 'file-generic',
type: 'generic',
extension
};
},
canDelete() {
return RocketChat.authz.hasAtLeastOnePermission('delete-message', this.rid) || (RocketChat.settings && RocketChat.settings.get('Message_AllowDeleting') && (this.userId === Meteor.userId()));
formatTimestamp(timestamp) {
return moment(timestamp).format(RocketChat.settings.get('Message_TimeAndDateFormat') || 'LLL');
},
url() {
return `/file-upload/${ this._id }/${ this.name }`;
hasMore() {
return Template.instance().hasMore.get();
},
fixCordova
hasFiles() {
return roomFiles.find({ rid: this.rid }).count() > 0;
}
});
Template.uploadedFilesList.events({
'click .room-file-item'(e) {
if ($(e.currentTarget).siblings('.icon-picture').length) {
return e.preventDefault();
}
'input .uploaded-files-list__search-input'(e, t) {
t.searchText.set(e.target.value.trim());
t.hasMore.set(true);
},
'click .icon-trash'() {
const self = this;
modal.open({
title: TAPi18n.__('Are_you_sure'),
text: TAPi18n.__('You_will_not_be_able_to_recover_file'),
type: 'warning',
showCancelButton: true,
confirmButtonColor: '#DD6B55',
confirmButtonText: TAPi18n.__('Yes_delete_it'),
cancelButtonText: TAPi18n.__('Cancel'),
closeOnConfirm: false,
html: false
}, function() {
modal.open({
title: TAPi18n.__('Deleted'),
text: TAPi18n.__('Your_file_has_been_deleted'),
type: 'success',
timer: 1000,
showConfirmButton: false
});
// Check if the upload message for this file is currently loaded
const msg = ChatMessage.findOne({ file: { _id: self._id } });
return RocketChat.models.Uploads.remove(self._id, function() {
if (msg) {
return chatMessages[Session.get('openedRoom')].deleteMsg(msg);
} else {
return Meteor.call('deleteFileMessage', self._id, function(error) {
if (error) {
return handleError(error);
}
});
}
});
});
},
'scroll .js-list': _.throttle(function(e, t) {
'scroll .flex-tab__result': _.throttle(function(e, t) {
if (e.target.scrollTop >= (e.target.scrollHeight - e.target.clientHeight)) {
return t.limit.set(t.limit.get() + 50);
}
}, 200)
});
Template.uploadedFilesList.onCreated(function() {
const { rid } = Template.currentData();
this.hasMore = new ReactiveVar(true);
this.limit = new ReactiveVar(50);
return this.autorun(() => {
return this.subscribe('roomFiles', rid, this.limit.get(), () => {
if (roomFiles.find({ rid }).fetch().length < this.limit.get()) {
return this.hasMore.set(false);
}
}
);
}
);
});

@ -1,22 +1,22 @@
<template name="contextualBar">
{{#if template}}
<div class="contextual-bar">
<section class="contextual-bar-wrap">
<header class="contextual-bar__header">
{{#with headerData}}
<div class="contextual-bar__header-data">
{{> icon block="contextual-bar__header-icon" icon=icon}}
<h1 class="contextual-bar__header-title">{{_ label}}</h1>
</div>
{{/with}}
<button class="contextual-bar__header-close js-close">
{{> icon block="contextual-bar__header-close-icon" icon="plus"}}
</button>
</header>
<main class="contextual-bar__content flex-tab {{id}}">
{{> Template.dynamic template=template data=flexData}}
</main>
</section>
</div>
{{/if}}
</template>
{{#if template}}
<div class="contextual-bar">
<section class="contextual-bar-wrap">
<header class="contextual-bar__header">
{{#with headerData}}
<div class="contextual-bar__header-data">
{{> icon block="contextual-bar__header-icon" icon=icon}}
<h1 class="contextual-bar__header-title">{{_ label}}</h1>
</div>
{{/with}}
<button class="contextual-bar__header-close js-close">
{{> icon block="contextual-bar__header-close-icon" icon="plus"}}
</button>
</header>
<main class="contextual-bar__content flex-tab {{id}}">
{{> Template.dynamic template=template data=flexData}}
</main>
</section>
</div>
{{/if}}
</template>

@ -1,70 +1,89 @@
import PhotoSwipe from 'photoswipe';
import PhotoSwipeUI_Default from 'photoswipe/dist/photoswipe-ui-default';
import 'photoswipe/dist/photoswipe.css';
import s from 'underscore.string';
const escapeHTML = (html) => (html || '').replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
Meteor.startup(() => {
const initGallery = (selector, items, options) => {
const gallery = new PhotoSwipe(selector, PhotoSwipeUI_Default, items, options);
gallery.init();
};
const getItems = (selector, imageSrc) => {
const results = {
index: 0,
items: []
};
for (let i = 0, len = selector.length; i < len; i++) {
results.items.push({
src: selector[i].src,
w: selector[i].naturalWidth,
h: selector[i].naturalHeight,
title: selector[i].dataset.title,
description: selector[i].dataset.description
let currentGallery = null;
const initGallery = (items, options) => {
if (!currentGallery) {
currentGallery = new PhotoSwipe(document.getElementById('pswp'), PhotoSwipeUI_Default, items, options);
currentGallery.listen('destroy', () => {
currentGallery = null;
});
if (imageSrc === selector[i].src) {
results.index = i;
}
currentGallery.init();
}
return results;
};
const galleryOptions = {
index: 0,
const defaultGalleryOptions = {
bgOpacity: 0.8,
showHideOpacity: true,
counterEl: false,
shareEl: false
};
$(document).on('click', '.gallery-item', function() {
const images = getItems(document.querySelectorAll('.gallery-item'), $(this)[0].src);
const createEventListenerFor = className => event => {
event.preventDefault();
event.stopPropagation();
if (currentGallery) {
return;
}
galleryOptions.index = images.index;
galleryOptions.addCaptionHTMLFn = function(item, captionEl) {
captionEl.children[0].innerHTML = `${ escapeHTML(item.title) }<br/><small>${ escapeHTML(item.description) }</small> `;
return true;
const galleryOptions = {
...defaultGalleryOptions,
index: 0,
addCaptionHTMLFn(item, captionEl) {
captionEl.children[0].innerHTML =
`${ s.escapeHTML(item.title) }<br/><small>${ s.escapeHTML(item.description) }</small>`;
return true;
}
};
initGallery(document.getElementById('pswp'), images.items, galleryOptions);
});
const items = Array.from(document.querySelectorAll(className))
.map((element, i) => {
if (element === event.currentTarget) {
galleryOptions.index = i;
}
if (element.dataset.src || element.href) {
const img = new Image();
img.addEventListener('load', () => {
if (!currentGallery) {
return;
}
delete currentGallery.items[i].html;
currentGallery.items[i].src = img.src;
currentGallery.items[i].w = img.naturalWidth;
currentGallery.items[i].h = img.naturalHeight;
currentGallery.invalidateCurrItems();
currentGallery.updateSize(true);
});
$(document).on('click', '.room-files-image', (e) => {
e.preventDefault();
e.stopPropagation();
img.src = element.dataset.src || element.href;
const img = new Image();
img.src = e.currentTarget.href;
img.addEventListener('load', function() {
const item = [{
src: this.src,
w: this.naturalWidth,
h: this.naturalHeight
}];
return {
html: '',
title: element.dataset.title || element.title,
description: element.dataset.description
};
}
return {
src: element.src,
w: element.naturalWidth,
h: element.naturalHeight,
title: element.dataset.title || element.title,
description: element.dataset.description
};
});
initGallery(items, galleryOptions);
};
initGallery(document.getElementById('pswp'), item, galleryOptions);
});
});
$(document).on('click', '.gallery-item', createEventListenerFor('.gallery-item'));
$(document).on('click', '.room-files-image', createEventListenerFor('.room-files-image'));
});

@ -0,0 +1,27 @@
export const roomFiles = (pub, { rid, searchText, limit = 50 }) => {
if (!pub.userId) {
return pub.ready();
}
const cursorFileListHandle = RocketChat.models.Uploads.findNotHiddenFilesOfRoom(rid, searchText, limit).observeChanges({
added(_id, record) {
const { username, name } = record.userId ? RocketChat.models.Users.findOneById(record.userId) : {};
return pub.added('room_files', _id, { ...record, user: { username, name } });
},
changed(_id, recordChanges) {
if (!recordChanges.hasOwnProperty('user') && recordChanges.userId) {
recordChanges.user = RocketChat.models.Users.findOneById(recordChanges.userId);
}
return pub.changed('room_files', _id, recordChanges);
},
removed(_id) {
return pub.removed('room_files', _id);
}
});
pub.ready();
return pub.onStop(function() {
return cursorFileListHandle.stop();
});
};

@ -1,27 +1,5 @@
Meteor.publish('roomFiles', function(rid, limit = 50) {
if (!this.userId) {
return this.ready();
}
const pub = this;
const cursorFileListHandle = RocketChat.models.Uploads.findNotHiddenFilesOfRoom(rid, limit).observeChanges({
added(_id, record) {
const {username, name} = record.userId ? RocketChat.models.Users.findOneById(record.userId) : {};
return pub.added('room_files', _id, {...record, user:{username, name}});
},
changed(_id, record) {
const {username, name} = record.userId ? RocketChat.models.Users.findOneById(record.userId) : {};
return pub.changed('room_files', _id, {...record, user:{username, name}});
},
removed(_id, record) {
return pub.removed('room_files', _id, record);
}
});
import { roomFiles } from '../lib/roomFiles';
this.ready();
return this.onStop(function() {
return cursorFileListHandle.stop();
});
Meteor.publish('roomFiles', function(rid, limit = 50) {
return roomFiles(this, { rid, limit });
});

@ -0,0 +1,5 @@
import { roomFiles } from '../lib/roomFiles';
Meteor.publish('roomFilesWithSearchText', function(rid, searchText, limit = 50) {
return roomFiles(this, { rid, searchText, limit });
});
Loading…
Cancel
Save