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}} -
    +
  • {{else}} {{#if hasServiceAccounts}} {{#each users}} -
  • +
  • {{> avatar username=username}} -
    - {{username}} + +
    + {{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 @@
    -
    - {{#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 @@