diff --git a/.circleci/sign.key.gpg b/.circleci/sign.key.gpg
index 488e275998d5..6d005764c11b 100644
Binary files a/.circleci/sign.key.gpg and b/.circleci/sign.key.gpg differ
diff --git a/.meteor/packages b/.meteor/packages
index 6cfd59d2a843..98e3d061be48 100644
--- a/.meteor/packages
+++ b/.meteor/packages
@@ -71,7 +71,6 @@ raix:handlebar-helpers
rocketchat:push
raix:ui-dropped-event
todda00:friendly-slugs
-yasinuslu:blaze-meta
tap:i18n
underscore@1.0.10
diff --git a/.meteor/versions b/.meteor/versions
index e1e6cb375828..0fd124157955 100644
--- a/.meteor/versions
+++ b/.meteor/versions
@@ -70,7 +70,7 @@ konecty:change-case@2.3.0
konecty:delayed-task@1.0.0
konecty:mongo-counter@0.0.5_3
konecty:multiple-instances-status@1.1.0
-konecty:user-presence@2.4.0
+konecty:user-presence@2.5.0
launch-screen@1.1.1
less@2.8.0
littledata:synced-cron@1.5.1
@@ -159,4 +159,3 @@ underscore@1.0.10
url@1.2.0
webapp@1.7.2
webapp-hashing@1.0.9
-yasinuslu:blaze-meta@0.3.3
diff --git a/app/channel-settings/client/views/channelSettings.html b/app/channel-settings/client/views/channelSettings.html
index 7e736cbeee8c..32f93b538b39 100644
--- a/app/channel-settings/client/views/channelSettings.html
+++ b/app/channel-settings/client/views/channelSettings.html
@@ -122,9 +122,9 @@
- {{_ "React_when_read_only"}}
+ {{_ "Disallow_reacting"}}
- {{_ "React_when_read_only"}}
+ {{_ "Disallow_reacting_Description"}}
@@ -136,9 +136,9 @@
- {{_ "Disallow_reacting"}}
+ {{_ "React_when_read_only"}}
- {{_ "Disallow_reacting_Description"}}
+ {{_ "React_when_read_only"}}
diff --git a/app/custom-oauth/server/custom_oauth_server.js b/app/custom-oauth/server/custom_oauth_server.js
index 658262575003..5becea434c06 100644
--- a/app/custom-oauth/server/custom_oauth_server.js
+++ b/app/custom-oauth/server/custom_oauth_server.js
@@ -151,6 +151,7 @@ export class CustomOAuth {
const params = {};
const headers = {
'User-Agent': this.userAgent, // http://doc.gitlab.com/ce/api/users.html#Current-user
+ Accept: 'application/json',
};
if (this.identityTokenSentVia === 'header') {
diff --git a/app/emoji-custom/client/admin/adminEmoji.js b/app/emoji-custom/client/admin/adminEmoji.js
index 3d64e3b189ce..7a46f4c9cb0c 100644
--- a/app/emoji-custom/client/admin/adminEmoji.js
+++ b/app/emoji-custom/client/admin/adminEmoji.js
@@ -42,6 +42,18 @@ Template.adminEmoji.helpers({
data: Template.instance().tabBarData.get(),
};
},
+ onTableScroll() {
+ const instance = Template.instance();
+ return function(currentTarget) {
+ if ((currentTarget.offsetHeight + currentTarget.scrollTop) < (currentTarget.scrollHeight - 100)) {
+ return;
+ }
+ if (Template.instance().limit.get() > Template.instance().customemoji().length) {
+ return false;
+ }
+ instance.limit.set(instance.limit.get() + 50);
+ };
+ },
onTableItemClick() {
const instance = Template.instance();
return function({ _id }) {
diff --git a/app/federation/server/config.js b/app/federation/server/config.js
new file mode 100644
index 000000000000..b8f1f2b12287
--- /dev/null
+++ b/app/federation/server/config.js
@@ -0,0 +1,74 @@
+import mem from 'mem';
+
+import { getWorkspaceAccessToken } from '../../cloud/server';
+import { FederationKeys } from '../../models/server';
+import { settings } from '../../settings/server';
+import * as SettingsUpdater from './settingsUpdater';
+import { logger } from './logger';
+
+const defaultConfig = {
+ hub: {
+ active: null,
+ url: null,
+ },
+ peer: {
+ uniqueId: null,
+ domain: null,
+ url: null,
+ public_key: null,
+ },
+ cloud: {
+ token: null,
+ },
+};
+
+const getConfigLocal = () => {
+ const _enabled = settings.get('FEDERATION_Enabled');
+
+ if (!_enabled) { return defaultConfig; }
+
+ // If it is enabled, check if the settings are there
+ const _uniqueId = settings.get('FEDERATION_Unique_Id');
+ const _domain = settings.get('FEDERATION_Domain');
+ const _discoveryMethod = settings.get('FEDERATION_Discovery_Method');
+ const _hubUrl = settings.get('FEDERATION_Hub_URL');
+ const _peerUrl = settings.get('Site_Url');
+
+ if (!_domain || !_discoveryMethod || !_hubUrl || !_peerUrl) {
+ SettingsUpdater.updateStatus('Could not enable, settings are not fully set');
+
+ logger.setup.error('Could not enable Federation, settings are not fully set');
+
+ return defaultConfig;
+ }
+
+ logger.setup.info('Updating settings...');
+
+ // Normalize the config values
+ return {
+ hub: {
+ active: _discoveryMethod === 'hub',
+ url: _hubUrl.replace(/\/+$/, ''),
+ },
+ peer: {
+ uniqueId: _uniqueId,
+ domain: _domain.replace('@', '').trim(),
+ url: _peerUrl.replace(/\/+$/, ''),
+ public_key: FederationKeys.getPublicKeyString(),
+ },
+ cloud: {
+ token: getWorkspaceAccessToken(),
+ },
+ };
+};
+
+export const getConfig = mem(getConfigLocal);
+
+const updateValue = () => mem.clear(getConfig);
+
+settings.get('FEDERATION_Enabled', updateValue);
+settings.get('FEDERATION_Unique_Id', updateValue);
+settings.get('FEDERATION_Domain', updateValue);
+settings.get('FEDERATION_Status', updateValue);
+settings.get('FEDERATION_Discovery_Method', updateValue);
+settings.get('FEDERATION_Hub_URL', updateValue);
diff --git a/app/federation/server/index.js b/app/federation/server/index.js
index 834a67c10f6a..f0c6844e86a1 100644
--- a/app/federation/server/index.js
+++ b/app/federation/server/index.js
@@ -13,9 +13,9 @@ import './methods/dashboard';
import { addUser } from './methods/addUser';
import { searchUsers } from './methods/searchUsers';
import { ping } from './methods/ping';
-import { getWorkspaceAccessToken } from '../../cloud/server';
import { FederationKeys } from '../../models';
import { settings } from '../../settings';
+import { getConfig } from './config';
const peerClient = new PeerClient();
const peerDNS = new PeerDNS();
@@ -73,39 +73,7 @@ const updateSettings = _.debounce(Meteor.bindEnvironment(function() {
if (!_enabled) { return; }
- // If it is enabled, check if the settings are there
- const _uniqueId = settings.get('FEDERATION_Unique_Id');
- const _domain = settings.get('FEDERATION_Domain');
- const _discoveryMethod = settings.get('FEDERATION_Discovery_Method');
- const _hubUrl = settings.get('FEDERATION_Hub_URL');
- const _peerUrl = settings.get('Site_Url');
-
- if (!_domain || !_discoveryMethod || !_hubUrl || !_peerUrl) {
- SettingsUpdater.updateStatus('Could not enable, settings are not fully set');
-
- logger.setup.error('Could not enable Federation, settings are not fully set');
-
- return;
- }
-
- logger.setup.info('Updating settings...');
-
- // Normalize the config values
- const config = {
- hub: {
- active: _discoveryMethod === 'hub',
- url: _hubUrl.replace(/\/+$/, ''),
- },
- peer: {
- uniqueId: _uniqueId,
- domain: _domain.replace('@', '').trim(),
- url: _peerUrl.replace(/\/+$/, ''),
- public_key: FederationKeys.getPublicKeyString(),
- },
- cloud: {
- token: getWorkspaceAccessToken(),
- },
- };
+ const config = getConfig();
// If the settings are correctly set, let's update the configuration
diff --git a/app/ldap/server/sync.js b/app/ldap/server/sync.js
index 550c079a697f..ac9d6fe33f0b 100644
--- a/app/ldap/server/sync.js
+++ b/app/ldap/server/sync.js
@@ -144,7 +144,7 @@ export function getDataToSyncUserData(ldapUser, user) {
if (currKey === lastKey) {
obj[currKey] = tmpLdapField;
} else {
- obj[currKey] = obj[currKey];
+ obj[currKey] = obj[currKey] || {};
}
return obj[currKey];
}, userData);
diff --git a/app/lib/client/methods/sendMessage.js b/app/lib/client/methods/sendMessage.js
index 4ba64c0059a7..d765eefe27c7 100644
--- a/app/lib/client/methods/sendMessage.js
+++ b/app/lib/client/methods/sendMessage.js
@@ -1,17 +1,23 @@
import { Meteor } from 'meteor/meteor';
import { TimeSync } from 'meteor/mizzao:timesync';
import s from 'underscore.string';
+import toastr from 'toastr';
import { ChatMessage } from '../../../models';
import { settings } from '../../../settings';
import { callbacks } from '../../../callbacks';
import { promises } from '../../../promises/client';
+import { t } from '../../../utils/client';
Meteor.methods({
sendMessage(message) {
if (!Meteor.userId() || s.trim(message.msg) === '') {
return false;
}
+ const messageAlreadyExists = message._id && ChatMessage.findOne({ _id: message._id });
+ if (messageAlreadyExists) {
+ return toastr.error(t('Message_Already_Sent'));
+ }
const user = Meteor.user();
message.ts = isNaN(TimeSync.serverOffset()) ? new Date() : new Date(Date.now() + TimeSync.serverOffset());
message.u = {
diff --git a/app/lib/server/functions/checkUsernameAvailability.js b/app/lib/server/functions/checkUsernameAvailability.js
index e881786805aa..064753575d59 100644
--- a/app/lib/server/functions/checkUsernameAvailability.js
+++ b/app/lib/server/functions/checkUsernameAvailability.js
@@ -8,7 +8,7 @@ let usernameBlackList = [];
const toRegExp = (username) => new RegExp(`^${ s.escapeRegExp(username).trim() }$`, 'i');
settings.get('Accounts_BlockedUsernameList', (key, value) => {
- usernameBlackList = value.split(',').map(toRegExp);
+ usernameBlackList = ['all', 'here'].concat(value.split(',')).map(toRegExp);
});
const usernameIsBlocked = (username, usernameBlackList) => usernameBlackList.length
diff --git a/app/lib/server/functions/notifications/serviceAccount.js b/app/lib/server/functions/notifications/serviceAccount.js
new file mode 100644
index 000000000000..92d45270ffe8
--- /dev/null
+++ b/app/lib/server/functions/notifications/serviceAccount.js
@@ -0,0 +1,31 @@
+import { metrics } from '../../../../metrics';
+import { Notifications } from '../../../../notifications';
+
+export function shouldNotifyServiceAccountOwner({
+ statusConnection,
+ hasMentionToAll,
+ hasMentionToHere,
+ isHighlighted,
+ hasMentionToUser,
+ hasReplyToThread,
+ roomType,
+}) {
+ if (statusConnection === 'online') {
+ return false;
+ }
+ return roomType === 'd' || hasMentionToAll || hasMentionToHere || isHighlighted || hasMentionToUser || hasReplyToThread;
+}
+
+export function notifyServiceAccountOwner(receiver, ownerId, message, room) {
+ metrics.notificationsSent.inc({ notification_type: 'sa' });
+ Notifications.notifyUser(ownerId, 'sa-notification', {
+ payload: {
+ _id: message._id,
+ rid: message.rid,
+ sender: message.u,
+ receiver: receiver.username,
+ type: room.t,
+ name: room.name,
+ },
+ });
+}
diff --git a/app/lib/server/functions/sendMessage.js b/app/lib/server/functions/sendMessage.js
index 290218f14e3d..2cb04ff465cb 100644
--- a/app/lib/server/functions/sendMessage.js
+++ b/app/lib/server/functions/sendMessage.js
@@ -198,6 +198,10 @@ export const sendMessage = function(user, message, room, upsert = false) {
}, message);
message._id = _id;
} else {
+ const messageAlreadyExists = message._id && Messages.findOneById(message._id, { fields: { _id: 1 } });
+ if (messageAlreadyExists) {
+ return;
+ }
message._id = Messages.insert(message);
}
diff --git a/app/lib/server/functions/setStatusText.js b/app/lib/server/functions/setStatusText.js
index c852f21a6ab0..3b06c36ab28d 100644
--- a/app/lib/server/functions/setStatusText.js
+++ b/app/lib/server/functions/setStatusText.js
@@ -45,7 +45,7 @@ export const _setStatusText = function(userId, statusText) {
return true;
};
-export const setStatusText = RateLimiter.limitFunction(_setStatusText, 1, 60000, {
+export const setStatusText = RateLimiter.limitFunction(_setStatusText, 5, 60000, {
0() {
// Administrators have permission to change others status, so don't limit those
return !Meteor.userId() || !hasPermission(Meteor.userId(), 'edit-other-user-info');
diff --git a/app/lib/server/lib/sendNotificationsOnMessage.js b/app/lib/server/lib/sendNotificationsOnMessage.js
index db544f058509..ddda261a0537 100644
--- a/app/lib/server/lib/sendNotificationsOnMessage.js
+++ b/app/lib/server/lib/sendNotificationsOnMessage.js
@@ -11,6 +11,7 @@ import { sendEmail, shouldNotifyEmail } from '../functions/notifications/email';
import { sendSinglePush, shouldNotifyMobile } from '../functions/notifications/mobile';
import { notifyDesktopUser, shouldNotifyDesktop } from '../functions/notifications/desktop';
import { notifyAudioUser, shouldNotifyAudio } from '../functions/notifications/audio';
+import { notifyServiceAccountOwner, shouldNotifyServiceAccountOwner } from '../functions/notifications/serviceAccount';
export const sendNotification = async ({
subscription,
@@ -46,6 +47,7 @@ export const sendNotification = async ({
status: 1,
statusConnection: 1,
username: 1,
+ u: 1,
},
}),
];
@@ -149,6 +151,18 @@ export const sendNotification = async ({
return false;
});
}
+
+ if (receiver.u && shouldNotifyServiceAccountOwner({
+ statusConnection: receiver.statusConnection,
+ hasMentionToAll,
+ hasMentionToHere,
+ isHighlighted,
+ hasMentionToUser,
+ hasReplyToThread,
+ roomType,
+ })) {
+ notifyServiceAccountOwner(receiver, receiver.u._id, message, room);
+ }
};
const project = {
@@ -168,6 +182,7 @@ const project = {
'receiver.status': 1,
'receiver.statusConnection': 1,
'receiver.username': 1,
+ 'receiver.u': 1,
},
};
diff --git a/app/livechat/server/api/v1/pageVisited.js b/app/livechat/server/api/v1/pageVisited.js
index 4b35cd3adb6e..e5ef7c42ba64 100644
--- a/app/livechat/server/api/v1/pageVisited.js
+++ b/app/livechat/server/api/v1/pageVisited.js
@@ -1,9 +1,7 @@
-import { Meteor } from 'meteor/meteor';
import { Match, check } from 'meteor/check';
import _ from 'underscore';
import { API } from '../../../../api';
-import { findGuest, findRoom } from '../lib/livechat';
import { Livechat } from '../../lib/Livechat';
API.v1.addRoute('livechat/page.visited', {
@@ -11,7 +9,7 @@ API.v1.addRoute('livechat/page.visited', {
try {
check(this.bodyParams, {
token: String,
- rid: String,
+ rid: Match.Maybe(String),
pageInfo: Match.ObjectIncluding({
change: String,
title: String,
@@ -22,17 +20,6 @@ API.v1.addRoute('livechat/page.visited', {
});
const { token, rid, pageInfo } = this.bodyParams;
-
- const guest = findGuest(token);
- if (!guest) {
- throw new Meteor.Error('invalid-token');
- }
-
- const room = findRoom(token, rid);
- if (!room) {
- throw new Meteor.Error('invalid-room');
- }
-
const obj = Livechat.savePageHistory(token, rid, pageInfo);
if (obj) {
const page = _.pick(obj, 'msg', 'navigation');
diff --git a/app/message-pin/client/actionButton.js b/app/message-pin/client/actionButton.js
index 1fcf1f19cc21..7ea7f5913f4e 100644
--- a/app/message-pin/client/actionButton.js
+++ b/app/message-pin/client/actionButton.js
@@ -64,7 +64,7 @@ Meteor.startup(function() {
id: 'jump-to-pin-message',
icon: 'jump',
label: 'Jump_to_message',
- context: ['pinned'],
+ context: ['pinned', 'message', 'message-mobile'],
action() {
const { msg: message } = messageArgs(this);
if (window.matchMedia('(max-width: 500px)').matches) {
diff --git a/app/message-star/client/actionButton.js b/app/message-star/client/actionButton.js
index fee7c64c2dba..c6898ef9d50f 100644
--- a/app/message-star/client/actionButton.js
+++ b/app/message-star/client/actionButton.js
@@ -63,7 +63,7 @@ Meteor.startup(function() {
id: 'jump-to-star-message',
icon: 'jump',
label: 'Jump_to_message',
- context: ['starred', 'threads'],
+ context: ['starred', 'threads', 'message', 'message-mobile'],
action() {
const { msg: message } = messageArgs(this);
if (window.matchMedia('(max-width: 500px)').matches) {
diff --git a/app/search/client/provider/result.js b/app/search/client/provider/result.js
index aa64d32da6ba..03367888e29c 100644
--- a/app/search/client/provider/result.js
+++ b/app/search/client/provider/result.js
@@ -1,4 +1,5 @@
import { Meteor } from 'meteor/meteor';
+import { Tracker } from 'meteor/tracker';
import { ReactiveVar } from 'meteor/reactive-var';
import { FlowRouter } from 'meteor/kadira:flow-router';
import { Session } from 'meteor/session';
@@ -39,9 +40,22 @@ Meteor.startup(function() {
});
});
-Template.DefaultSearchResultTemplate.onCreated(function() {
- const self = this;
+Template.DefaultSearchResultTemplate.onRendered(function() {
+ const list = this.firstNode.parentNode.querySelector('.rocket-default-search-results');
+ this.autorun(() => {
+ const result = this.data.result.get();
+ if (result && this.hasMore.get()) {
+ Tracker.afterFlush(() => {
+ if (list.scrollHeight <= list.offsetHeight) {
+ this.data.payload.limit = (this.data.payload.limit || this.pageSize) + this.pageSize;
+ this.data.search();
+ }
+ });
+ }
+ });
+});
+Template.DefaultSearchResultTemplate.onCreated(function() {
// paging
this.pageSize = this.data.settings.PageSize;
@@ -53,7 +67,7 @@ Template.DefaultSearchResultTemplate.onCreated(function() {
this.autorun(() => {
const result = this.data.result.get();
- self.hasMore.set(!(result && result.message.docs.length < (self.data.payload.limit || self.pageSize)));
+ this.hasMore.set(!(result && result.message.docs.length < (this.data.payload.limit || this.pageSize)));
});
});
@@ -86,7 +100,7 @@ Template.DefaultSearchResultTemplate.helpers({
return Template.instance().hasMore.get();
},
message(msg) {
- return { customClass: 'search', actionContext: 'search', ...msg };
+ return { customClass: 'search', actionContext: 'search', ...msg, groupable: false };
},
messageContext,
});
diff --git a/app/search/client/style/style.css b/app/search/client/style/style.css
index 2b611bb6c241..93e7c4aa8d00 100644
--- a/app/search/client/style/style.css
+++ b/app/search/client/style/style.css
@@ -23,6 +23,7 @@
.rocket-search-result {
display: flex;
+ overflow: hidden;
flex-direction: column;
flex: 1;
}
diff --git a/app/service-accounts/client/stylesheets/serviceAccounts.css b/app/service-accounts/client/stylesheets/serviceAccounts.css
new file mode 100644
index 000000000000..4f7b1103f5df
--- /dev/null
+++ b/app/service-accounts/client/stylesheets/serviceAccounts.css
@@ -0,0 +1,43 @@
+.rc-service-account-list {
+ &__user {
+
+ display: flex;
+
+ padding: 8px 0;
+
+ cursor: pointer;
+ align-items: center;
+
+ &.active,
+ &:hover {
+ background-color: #eeeeee;
+
+ & .rc-member-list__menu {
+ opacity: 1;
+ }
+ }
+
+ & > .avatar {
+ width: var(--sidebar-account-thumb-size);
+ height: var(--sidebar-account-thumb-size);
+ }
+ }
+
+ &__username {
+ display: flex;
+ flex: 1 1 auto;
+
+ margin: 0 12px 0 8px;
+
+ font-size: 16px;
+ align-items: center;
+ }
+
+ &__username-alert {
+ font-weight: bold;
+ }
+}
+
+.service-account-notification {
+ color: var(--rc-color-alert-message-primary-background);
+}
diff --git a/app/service-accounts/client/views/serviceAccountDashboard.js b/app/service-accounts/client/views/serviceAccountDashboard.js
index aaaffc1483d7..0c2e49865650 100644
--- a/app/service-accounts/client/views/serviceAccountDashboard.js
+++ b/app/service-accounts/client/views/serviceAccountDashboard.js
@@ -50,9 +50,7 @@ Template.serviceAccountDashboard.helpers({
Template.serviceAccountDashboard.events({
'click .accept-service-account'(e) {
e.preventDefault();
- Meteor.call('authorization:addUserToRole', 'service-account-approved', this.u.username, null, success(() => {
- Meteor.call('setUserActiveStatus', this._id, true, success(() => toastr.success(t('User_has_been_activated'))));
- }));
+ Meteor.call('setUserActiveStatus', this._id, true, success(() => toastr.success(t('User_has_been_activated'))));
},
'click .reject-service-account'(e) {
e.preventDefault();
diff --git a/app/service-accounts/client/views/serviceAccountSidebarLogin.html b/app/service-accounts/client/views/serviceAccountSidebarLogin.html
index 914863701fd6..f5a8b22fe5cd 100644
--- a/app/service-accounts/client/views/serviceAccountSidebarLogin.html
+++ b/app/service-accounts/client/views/serviceAccountSidebarLogin.html
@@ -6,19 +6,28 @@
{{else}}
{{#if showOwnerAccountLink}}
Go back to main account
-
+
{{> avatar username=owner.username}}
-
+
{{owner.username}}
{{else}}
{{#if hasServiceAccounts}}
{{#each users}}
-
+
{{> avatar username=username}}
-
- {{username}}
+
+ {{#if receivedNewMessage username}}
+
+ {{username}}
+
+ {{else}}
+ {{username}}
+ {{/if}}
+
+
+ {{unread}}
{{/each}}
diff --git a/app/service-accounts/client/views/serviceAccountSidebarLogin.js b/app/service-accounts/client/views/serviceAccountSidebarLogin.js
index 98a86d213ba5..beb2d47c47c8 100644
--- a/app/service-accounts/client/views/serviceAccountSidebarLogin.js
+++ b/app/service-accounts/client/views/serviceAccountSidebarLogin.js
@@ -2,6 +2,7 @@ import { Meteor } from 'meteor/meteor';
import { ReactiveVar } from 'meteor/reactive-var';
import { Template } from 'meteor/templating';
import { FlowRouter } from 'meteor/kadira:flow-router';
+import { Session } from 'meteor/session';
import { handleError } from '../../../utils';
import './serviceAccountSidebarLogin.html';
@@ -22,6 +23,12 @@ Template.serviceAccountSidebarLogin.helpers({
showOwnerAccountLink() {
return localStorage.getItem('serviceAccountForceLogin') && !!Meteor.user().u;
},
+ receivedNewMessage(username) {
+ if (Template.instance().notifiedServiceAccount) {
+ return username === Template.instance().notifiedServiceAccount.get();
+ }
+ return false;
+ },
});
Template.serviceAccountSidebarLogin.events({
@@ -56,6 +63,10 @@ Template.serviceAccountSidebarLogin.onCreated(function() {
this.ready = new ReactiveVar(true);
this.users = new ReactiveVar([]);
this.loading = new ReactiveVar(true);
+ this.notifiedServiceAccount = new ReactiveVar('');
+ instance.notifiedServiceAccount.set(Session.get('saMessageReceiver'));
+ Session.delete('saMessageReceiver');
+ Session.delete('saNotification');
this.autorun(() => {
instance.loading.set(true);
Meteor.call('getLinkedServiceAccounts', function(err, serviceAccounts) {
diff --git a/app/service-accounts/server/config.js b/app/service-accounts/server/config.js
index 45f2ab361af5..ff4116d3edd2 100644
--- a/app/service-accounts/server/config.js
+++ b/app/service-accounts/server/config.js
@@ -18,6 +18,12 @@ Meteor.startup(() => {
type: 'string',
public: true,
});
+ this.add('Service_accounts_approval_required', true, {
+ group: 'Service Accounts',
+ i18nLabel: 'Service_accounts_approval_required',
+ type: 'boolean',
+ public: true,
+ });
});
settings.add('Accounts_Default_User_Preferences_sidebarShowServiceAccounts', true, {
group: 'Accounts',
diff --git a/app/service-accounts/server/functions/sendBroadcastMessage.js b/app/service-accounts/server/functions/sendBroadcastMessage.js
new file mode 100644
index 000000000000..5e37a61cfc9d
--- /dev/null
+++ b/app/service-accounts/server/functions/sendBroadcastMessage.js
@@ -0,0 +1,24 @@
+import { Meteor } from 'meteor/meteor';
+
+import { Rooms } from '../../../models/server';
+import { sendMessage } from '../../../lib/server';
+import { RateLimiter } from '../../../lib/server/lib';
+
+const _sendBroadcastMessage = function(message) {
+ if (!Meteor.userId()) {
+ throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'sendBroadcastMessage' });
+ }
+
+ if (!Meteor.user().u) {
+ throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'sendBroadcastMessage' });
+ }
+
+ const rooms = Rooms.findDirectRoomContainingUsername(Meteor.user().username);
+ for (const targetRoom of rooms) {
+ sendMessage(Meteor.user(), { msg: message.msg }, targetRoom);
+ }
+};
+
+export const sendBroadcastMessage = RateLimiter.limitFunction(_sendBroadcastMessage, 1, 8640000, {
+ 0() { return !Meteor.userId(); },
+});
diff --git a/app/service-accounts/server/hooks/serviceAccountBroadcast.js b/app/service-accounts/server/hooks/serviceAccountBroadcast.js
new file mode 100644
index 000000000000..26b3289ed540
--- /dev/null
+++ b/app/service-accounts/server/hooks/serviceAccountBroadcast.js
@@ -0,0 +1,11 @@
+import { callbacks } from '../../../callbacks/server';
+import { sendBroadcastMessage } from '../functions/sendBroadcastMessage';
+
+callbacks.add('beforeSaveMessage', (message, room) => {
+ // abort if room is not with a service account broadcast room
+ if (!room || !room.sa) {
+ return message;
+ }
+ sendBroadcastMessage(message);
+ return message;
+});
diff --git a/app/service-accounts/server/index.js b/app/service-accounts/server/index.js
index 82ecebac65c1..30b2f26eea13 100644
--- a/app/service-accounts/server/index.js
+++ b/app/service-accounts/server/index.js
@@ -9,6 +9,7 @@ import './methods/getLoginToken';
import './methods/getLinkedServiceAccounts';
import './hooks/serviceAccountCallback';
+import './hooks/serviceAccountBroadcast';
import './publications/fullServiceAccountData';
diff --git a/app/service-accounts/server/methods/addServiceAccount.js b/app/service-accounts/server/methods/addServiceAccount.js
index 596bf9e8d312..ee763c73068d 100644
--- a/app/service-accounts/server/methods/addServiceAccount.js
+++ b/app/service-accounts/server/methods/addServiceAccount.js
@@ -5,7 +5,7 @@ import _ from 'underscore';
import { saveUser, checkUsernameAvailability } from '../../../lib/server/functions';
import { Users } from '../../../models';
import { settings } from '../../../settings';
-import { hasPermission, hasRole } from '../../../authorization/server';
+import { hasPermission } from '../../../authorization/server';
Meteor.methods({
addServiceAccount(userData) {
@@ -49,7 +49,7 @@ Meteor.methods({
userData.joinDefaultChannels = false;
userData.roles = ['user'];
- userData.active = hasRole(user._id, 'service-account-approved');
+ userData.active = !settings.get('Service_accounts_approval_required');
return saveUser(Meteor.userId(), userData);
},
});
diff --git a/app/theme/client/imports/components/tabs.css b/app/theme/client/imports/components/tabs.css
index cf7a87448cd2..187394482509 100644
--- a/app/theme/client/imports/components/tabs.css
+++ b/app/theme/client/imports/components/tabs.css
@@ -13,8 +13,9 @@
}
.tab {
+ display: flex;
+
margin: 0 1rem;
- padding: 1rem 0;
cursor: pointer;
@@ -24,13 +25,25 @@
border-bottom: 2px solid transparent;
+ font-family: inherit;
font-size: 1rem;
-
font-weight: 500;
line-height: 1.25rem;
+ align-items: stretch;
+ flex-flow: row nowrap;
&.active {
color: var(--rc-color-button-primary);
border-bottom-color: var(--rc-color-button-primary);
}
+
+ &:focus {
+ text-decoration: underline;
+ }
+
+ & > span {
+ flex: 1;
+
+ padding: 1rem 0;
+ }
}
diff --git a/app/theme/client/imports/general/base.css b/app/theme/client/imports/general/base.css
index 4c5467746743..f7b8eb0bc8f4 100644
--- a/app/theme/client/imports/general/base.css
+++ b/app/theme/client/imports/general/base.css
@@ -219,56 +219,3 @@ button {
.hidden {
display: none;
}
-
-.loading-animation {
- position: absolute;
- top: 0;
- right: 0;
- bottom: 0;
- left: 0;
-
- display: flex;
-
- text-align: center;
- align-items: center;
- justify-content: center;
-}
-
-.loading-animation > .bounce {
- display: inline-block;
-
- width: 10px;
- height: 10px;
- margin: 2px;
-
- animation: loading-bouncedelay 1.4s infinite ease-in-out both;
-
- border-radius: 100%;
- background-color: rgba(255, 255, 255, 0.6);
-}
-
-.loading-animation .bounce1 {
- -webkit-animation-delay: -0.32s;
- animation-delay: -0.32s;
-}
-
-.loading-animation .bounce2 {
- -webkit-animation-delay: -0.16s;
- animation-delay: -0.16s;
-}
-
-.file-picker-loading .loading-animation > .bounce {
- background-color: #444444;
-}
-
-@keyframes loading-bouncedelay {
- 0%,
- 80%,
- 100% {
- transform: scale(0);
- }
-
- 40% {
- transform: scale(1);
- }
-}
diff --git a/app/theme/client/imports/general/base_old.css b/app/theme/client/imports/general/base_old.css
index bb298b0566c5..5c4c10743925 100644
--- a/app/theme/client/imports/general/base_old.css
+++ b/app/theme/client/imports/general/base_old.css
@@ -4951,6 +4951,12 @@ rc-old select,
}
}
+.rc-old .load-more {
+ position: relative;
+
+ height: 2rem;
+}
+
.rc-old .flex-tab {
&__content {
display: flex;
diff --git a/app/ui-flextab/client/tabs/uploadedFilesList.html b/app/ui-flextab/client/tabs/uploadedFilesList.html
index ad09ca8819dc..3da55f8603fb 100644
--- a/app/ui-flextab/client/tabs/uploadedFilesList.html
+++ b/app/ui-flextab/client/tabs/uploadedFilesList.html
@@ -12,7 +12,6 @@
-
{{#each files}}
@@ -39,19 +38,17 @@
{{> icon file=. block="attachments-menu js-action" icon="menu"}}
-
+ {{else}}
+ {{#if Template.subscriptionsReady}}
+ {{_ "Room_uploaded_file_list_empty"}}
+ {{/if}}
{{/each}}
- {{#if hasMore}}
+ {{#if isLoading}}
{{> loading}}
{{/if}}
- {{#if Template.subscriptionsReady}}
- {{#unless hasFiles}}
-
{{_ "Room_uploaded_file_list_empty"}}
- {{/unless}}
- {{/if}}
diff --git a/app/ui-flextab/client/tabs/uploadedFilesList.js b/app/ui-flextab/client/tabs/uploadedFilesList.js
index 9f65c18d2ad9..abbacb4556db 100644
--- a/app/ui-flextab/client/tabs/uploadedFilesList.js
+++ b/app/ui-flextab/client/tabs/uploadedFilesList.js
@@ -3,6 +3,7 @@ import { Meteor } from 'meteor/meteor';
import { Template } from 'meteor/templating';
import { Mongo } from 'meteor/mongo';
import { ReactiveVar } from 'meteor/reactive-var';
+import { ReactiveDict } from 'meteor/reactive-dict';
import { DateFormat } from '../../../lib/client';
import { canDeleteMessage, getURL, handleError, t } from '../../../utils/client';
@@ -10,18 +11,20 @@ import { popover, modal } from '../../../ui-utils/client';
const roomFiles = new Mongo.Collection('room_files');
+const LIST_SIZE = 50;
+
Template.uploadedFilesList.onCreated(function() {
const { rid } = Template.currentData();
this.searchText = new ReactiveVar(null);
- this.hasMore = new ReactiveVar(true);
- this.limit = new ReactiveVar(50);
+
+ this.state = new ReactiveDict({
+ limit: LIST_SIZE,
+ hasMore: true,
+ });
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);
- }
- });
+ const ready = this.subscribe('roomFilesWithSearchText', rid, this.searchText.get(), this.state.get('limit'), () => this.state.set('hasMore', this.state.get('limit') <= roomFiles.find({ rid }).count())).ready();
+ this.state.set('loading', !ready);
});
});
@@ -46,6 +49,9 @@ Template.uploadedFilesList.helpers({
return getURL(this.url);
}
},
+ limit() {
+ return Template.instance().state.get('limit');
+ },
format(timestamp) {
return DateFormat.formatDateAndTime(timestamp);
},
@@ -96,12 +102,8 @@ Template.uploadedFilesList.helpers({
return DateFormat.formatDateAndTime(timestamp);
},
- hasMore() {
- return Template.instance().hasMore.get();
- },
-
- hasFiles() {
- return roomFiles.find({ rid: this.rid }).count() > 0;
+ isLoading() {
+ return Template.instance().state.get('loading');
},
});
@@ -112,12 +114,15 @@ Template.uploadedFilesList.events({
'input .uploaded-files-list__search-input'(e, t) {
t.searchText.set(e.target.value.trim());
- t.hasMore.set(true);
+ t.state.set('hasMore', true);
},
'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);
+ if (!t.state.get('hasMore')) {
+ return;
+ }
+ return t.state.set('limit', t.state.get('limit') + LIST_SIZE);
}
}, 200),
diff --git a/app/ui-master/client/index.js b/app/ui-master/client/index.js
index b5870d194100..67abd78bcfe5 100644
--- a/app/ui-master/client/index.js
+++ b/app/ui-master/client/index.js
@@ -1,4 +1,4 @@
-import './loading.html';
+import './loading';
import './error.html';
import './logoLayout.html';
import './main.html';
diff --git a/app/ui-master/client/loading/index.js b/app/ui-master/client/loading/index.js
new file mode 100644
index 000000000000..66a769be942f
--- /dev/null
+++ b/app/ui-master/client/loading/index.js
@@ -0,0 +1,2 @@
+import './loading.css';
+import './loading.html';
diff --git a/app/ui-master/client/loading/loading.css b/app/ui-master/client/loading/loading.css
new file mode 100644
index 000000000000..29ab7c8125e7
--- /dev/null
+++ b/app/ui-master/client/loading/loading.css
@@ -0,0 +1,52 @@
+.loading-animation {
+ position: absolute;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 0;
+
+ display: flex;
+
+ text-align: center;
+ align-items: center;
+ justify-content: center;
+}
+
+.loading-animation > .bounce {
+ display: inline-block;
+
+ width: 10px;
+ height: 10px;
+ margin: 2px;
+
+ animation: loading-bouncedelay 1.4s infinite ease-in-out both;
+
+ border-radius: 100%;
+ background-color: rgba(255, 255, 255, 0.6);
+}
+
+.loading-animation .bounce1 {
+ -webkit-animation-delay: -0.32s;
+ animation-delay: -0.32s;
+}
+
+.loading-animation .bounce2 {
+ -webkit-animation-delay: -0.16s;
+ animation-delay: -0.16s;
+}
+
+.file-picker-loading .loading-animation > .bounce {
+ background-color: #444444;
+}
+
+@keyframes loading-bouncedelay {
+ 0%,
+ 80%,
+ 100% {
+ transform: scale(0);
+ }
+
+ 40% {
+ transform: scale(1);
+ }
+}
diff --git a/app/ui-master/client/loading.html b/app/ui-master/client/loading/loading.html
similarity index 100%
rename from app/ui-master/client/loading.html
rename to app/ui-master/client/loading/loading.html
diff --git a/app/ui-message/client/message.html b/app/ui-message/client/message.html
index a155f1697046..5e80a3329fce 100644
--- a/app/ui-message/client/message.html
+++ b/app/ui-message/client/message.html
@@ -27,6 +27,7 @@
+
{{#each role in roleTags}}
{{role.description}}
{{/each}}
diff --git a/app/ui-message/client/message.js b/app/ui-message/client/message.js
index b0fcd419455a..97d400f6b2fb 100644
--- a/app/ui-message/client/message.js
+++ b/app/ui-message/client/message.js
@@ -204,6 +204,10 @@ Template.message.helpers({
return 'own';
}
},
+ t() {
+ const { msg } = this;
+ return msg.t;
+ },
timestamp() {
const { msg } = this;
return +msg.ts;
diff --git a/app/ui-sidenav/client/sidebarHeader.html b/app/ui-sidenav/client/sidebarHeader.html
index 5a34a677bc91..6fd3a7106283 100644
--- a/app/ui-sidenav/client/sidebarHeader.html
+++ b/app/ui-sidenav/client/sidebarHeader.html
@@ -7,8 +7,14 @@
diff --git a/app/ui-sidenav/client/sidebarHeader.js b/app/ui-sidenav/client/sidebarHeader.js
index 448bd7c61348..4b966431ad02 100644
--- a/app/ui-sidenav/client/sidebarHeader.js
+++ b/app/ui-sidenav/client/sidebarHeader.js
@@ -3,6 +3,7 @@ import { ReactiveVar } from 'meteor/reactive-var';
import { FlowRouter } from 'meteor/kadira:flow-router';
import { Template } from 'meteor/templating';
import toastr from 'toastr';
+import { Session } from 'meteor/session';
import { popover, AccountBox, menu, SideNav, modal } from '../../ui-utils';
import { t, getUserPreference, handleError } from '../../utils';
@@ -75,6 +76,7 @@ const toolbarButtons = (user) => [{
{
name: t('Service_account_login'),
icon: 'reload',
+ active: Session.get('saNotification'),
condition: () => !Meteor.user().u || (Meteor.user().u && localStorage.getItem('serviceAccountForceLogin')),
action: (e) => {
const options = [];
diff --git a/app/ui-utils/client/lib/RoomHistoryManager.js b/app/ui-utils/client/lib/RoomHistoryManager.js
index b6d0db8beb9e..2010ecb519a8 100644
--- a/app/ui-utils/client/lib/RoomHistoryManager.js
+++ b/app/ui-utils/client/lib/RoomHistoryManager.js
@@ -160,11 +160,13 @@ export const RoomHistoryManager = new class {
}
if (wrapper) {
- if (wrapper.scrollHeight <= wrapper.offsetHeight) {
- return this.getMore(rid);
- }
- const heightDiff = wrapper.scrollHeight - previousHeight;
- wrapper.scrollTop += heightDiff;
+ Tracker.afterFlush(() => {
+ if (wrapper.scrollHeight <= wrapper.offsetHeight) {
+ return this.getMore(rid);
+ }
+ const heightDiff = wrapper.scrollHeight - previousHeight;
+ wrapper.scrollTop += heightDiff;
+ });
}
room.isLoading.set(false);
diff --git a/app/ui/client/components/icon.js b/app/ui/client/components/icon.js
index 779ea70f1632..6e759b01b17c 100644
--- a/app/ui/client/components/icon.js
+++ b/app/ui/client/components/icon.js
@@ -1,10 +1,23 @@
import { FlowRouter } from 'meteor/kadira:flow-router';
import { Template } from 'meteor/templating';
-import { isChrome, isFirefox } from '../../../utils';
+import './icon.html';
+
const baseUrlFix = () => `${ document.baseURI }${ FlowRouter.current().path.substring(1) }`;
+const isMozillaFirefoxBelowVersion = (upperVersion) => {
+ const [, version] = navigator.userAgent.match(/Firefox\/(\d+)\.\d/) || [];
+ return parseInt(version, 10) < upperVersion;
+};
+
+const isGoogleChromeBelowVersion = (upperVersion) => {
+ const [, version] = navigator.userAgent.match(/Chrome\/(\d+)\.\d/) || [];
+ return parseInt(version, 10) < upperVersion;
+};
+
+const isBaseUrlFixNeeded = () => isMozillaFirefoxBelowVersion(55) || isGoogleChromeBelowVersion(55);
+
Template.icon.helpers({
- baseUrl: (isFirefox && isFirefox[1] < 55) || (isChrome && isChrome[1] < 55) ? baseUrlFix : undefined,
+ baseUrl: isBaseUrlFixNeeded() ? baseUrlFix : undefined,
});
diff --git a/app/ui/client/components/tabs.html b/app/ui/client/components/tabs.html
index 37ec16ef2270..5ed7ec7395d5 100644
--- a/app/ui/client/components/tabs.html
+++ b/app/ui/client/components/tabs.html
@@ -1,8 +1,15 @@
-
- {{#each tabs}}
-
{{label}}
+
+ {{#each tab in tabs}}
+
{{/each}}
diff --git a/app/ui/client/components/tabs.js b/app/ui/client/components/tabs.js
index ab49fa82d191..eb429fb4f76d 100644
--- a/app/ui/client/components/tabs.js
+++ b/app/ui/client/components/tabs.js
@@ -1,18 +1,21 @@
import { ReactiveVar } from 'meteor/reactive-var';
import { Template } from 'meteor/templating';
+import './tabs.html';
+
+
Template.tabs.onCreated(function() {
this.activeTab = new ReactiveVar(this.data.tabs.tabs.find((tab) => tab.active).value);
});
Template.tabs.events({
- 'click .tab'(e) {
- const { value } = e.currentTarget.dataset;
- if (value === Template.instance().activeTab.get()) {
+ 'click .tab'(event, instance) {
+ const { value } = event.currentTarget.dataset;
+ if (value === instance.activeTab.get()) {
return;
}
- Template.instance().activeTab.set(value);
- Template.instance().data.tabs.onChange(value);
+ instance.activeTab.set(value);
+ instance.data.tabs.onChange(value);
},
});
@@ -23,4 +26,7 @@ Template.tabs.helpers({
isActive(value) {
return Template.instance().activeTab.get() === value;
},
+ ariaSelected(value) {
+ return Template.instance().activeTab.get() === value ? { 'aria-selected': 'true' } : {};
+ },
});
diff --git a/app/ui/client/index.js b/app/ui/client/index.js
index aafc4dc9cbfd..89e709dfe536 100644
--- a/app/ui/client/index.js
+++ b/app/ui/client/index.js
@@ -44,11 +44,9 @@ import './views/app/secretURL';
import './views/app/videoCall/videoButtons';
import './views/app/videoCall/videoCall';
import './views/app/photoswipe';
-import './components/icon.html';
import './components/icon';
import './components/table.html';
import './components/table';
-import './components/tabs.html';
import './components/tabs';
import './components/popupList.html';
import './components/popupList';
diff --git a/app/ui/client/views/app/room.html b/app/ui/client/views/app/room.html
index a676a96d5356..7dce04f57ed3 100644
--- a/app/ui/client/views/app/room.html
+++ b/app/ui/client/views/app/room.html
@@ -87,7 +87,7 @@
{{name}}
- {{_ statusDisplay}}
+ {{statusDisplay}}
{{_ "Chat_Now"}}
@@ -144,13 +144,6 @@
{{/if}}
- {{#if hasMoreNext}}
-
- {{#if isLoading}}
- {{> loading}}
- {{/if}}
-
- {{/if}}