Release 1.0.3 (#14446)

* [FIX] New day separator overlapping above system message (#14362)

* Improve German translations (#14351)

* Use the plural for discussions-section in side panel

* Formal and informal translations for 1.0

* fix german typos

* [FIX] Main thread title on replies (#14372)

* fix

* fix test

* fix setting

* Update tests/pageobjects/main-content.page.js

Co-Authored-By: ggazzo <guilhermegazzo@gmail.com>

* Update app/ui-utils/client/lib/RoomHistoryManager.js

Co-Authored-By: ggazzo <guilhermegazzo@gmail.com>

* [FIX] Bell was too small on threads (#14394)

* [FIX] Messages on threads disappearing (#14393)

* fix subscription-changed updating all messages(#14391)

* Fix: Message body was not being updated when user disabled nrr message (#14390)

* [NEW] Allow change Discussion's properties (#14389)

* [FIX] Unnecessary meteor.defer on openRoom (#14396)

* [FIX] more message actions to threads context(follow, unfollow, copy, delete) (#14387)

* added more message actions to threads context

* more actions

* change token name (#14379)

* [FIX] Pressing Enter in User Search field at channel causes reload (#14388)

* Prevent default on enter in User search

* Prevent form submission in membersList

* If using subpath make sure streams use that also for multi-instance.  Fixes #13200 (#14376)

* Revert "[IMPROVE] Use SessionId for credential token in SAML request (#13791)" (#14345)

This reverts commit 3967a74f5b.

* Add fallback to mongo version that doesn't require clusterMonitor role (#14403)

* [FIX] Users actions in administration were returning error (#14400)

* Fix actions collapse into popup in userInfo

* Refactor userActions

* [FIX] Error 400 on send a reply to an old thread (#14402)

* fix error 400 on send a reply to an old thread

* ignoring properly hidden messages

* [FIX] Messages on thread panel were receiving wrong context/subscription  (#14404)

* [FIX] preview pdf its not working (#14419)

* [FIX] renderMessageBody was caching messages in wrong scenarios #14420

* LingoHub Update 🚀 (#14426)

Manual push by LingoHub User: Diego Sampaio.
Project: Rocket.Chat

Made with ❤️ by https://lingohub.com

* [FIX] Mentions message missing 'jump to message' action (#14430)

* fixed context

* threads context

* [FIX] Escape unrecognized slash command message (#14432)

* Add missing german translations (#14386)

* [FIX] IE11 support (#14422)

* Add symlinks to ES6 node_modules imports

* Add URL polyfill for IE11

* Fix thread replies for IE11

* [IMPROVE] allow users to skip activeUsers to be ready (#14431)

* allow users to skip activeUsers to be ready

* Update main.js

* Update app/ui-master/client/main.js

Co-Authored-By: ggazzo <guilhermegazzo@gmail.com>

* [IMPROVE] Don't use regex to find users (#14397)

* Don't use regex to find users

* Invert logic on model methods

* Escape username regex

* Find users in batch

* Use only normalizeMessagesForUser

* Don't ignore username case to get owners on graphql

* Fixes on DAU and MAU aggregations (#14418)

* Fixes on SAU and MAU aggregations

* Report new data from DAU/MAU

* Run tests agains a mongodb container in CI

* Try to run CI correctly

* Fix drop database

* Parse desktop app User Agent correctly

* Fix aggregation of past sessions

* Return past month today

* Fix bug

* Add migration

* Fixed migration

* Migration improvements

* Fix crowd sync by using correct logging method (#14405)

* Fix room names in user info dialogs (#14415)

* Fix discussion name being invalid (#14442)

Closes #14378

* Fix i18n files keys sort (#14433)

* Add script to normalize i18n files

* Fix i18n files

* Set as official script

* Update package-lock.json

* fix (#14443)

* Update threads.css

* Bump version to 1.0.3

* regen changelog
pull/14653/head 1.0.3
Rodrigo Nascimento 7 years ago committed by GitHub
parent abf67ce985
commit f19473c626
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 3
      .circleci/config.yml
  2. 2
      .docker/Dockerfile.rhel
  3. 1
      .eslintignore
  4. 308
      .github/history.json
  5. 28
      .scripts/fix-i18n.js
  6. 2
      .travis/snap.sh
  7. 113
      HISTORY.md
  8. 5
      app/api/server/helpers/composeRoomWithLastMessage.js
  9. 4
      app/api/server/helpers/getUserFromParams.js
  10. 7
      app/api/server/v1/channels.js
  11. 31
      app/api/server/v1/chat.js
  12. 4
      app/api/server/v1/groups.js
  13. 6
      app/api/server/v1/im.js
  14. 2
      app/api/server/v1/users.js
  15. 2
      app/authorization/server/methods/addUserToRole.js
  16. 2
      app/autotranslate/client/lib/actionButton.js
  17. 2
      app/channel-settings-mail-messages/server/methods/mailMessages.js
  18. 4
      app/channel-settings/client/views/channelSettings.js
  19. 2
      app/channel-settings/server/functions/saveRoomName.js
  20. 2
      app/crowd/server/crowd.js
  21. 2
      app/custom-oauth/server/custom_oauth_server.js
  22. 2
      app/discussion/client/views/DiscussionList.html
  23. 2
      app/discussion/server/hooks/propagateDiscussionMetadata.js
  24. 3
      app/discussion/server/methods/createDiscussion.js
  25. 2
      app/gitlab/lib/common.js
  26. 4
      app/importer-csv/server/importer.js
  27. 2
      app/importer-hipchat-enterprise/server/importer.js
  28. 2
      app/importer-slack-users/server/importer.js
  29. 2
      app/importer-slack/server/importer.js
  30. 4
      app/integrations/server/lib/triggerHandler.js
  31. 2
      app/lib/server/functions/createRoom.js
  32. 2
      app/lib/server/functions/getUsernameSuggestion.js
  33. 4
      app/lib/server/functions/loadMessageHistory.js
  34. 2
      app/lib/server/methods/addUsersToRoom.js
  35. 4
      app/lib/server/methods/getChannelHistory.js
  36. 2
      app/livechat/server/methods/searchAgent.js
  37. 2
      app/mentions-flextab/client/views/mentionsFlexTab.html
  38. 3
      app/mentions-flextab/client/views/mentionsFlexTab.js
  39. 4
      app/message-attachments/client/messageAttachment.js
  40. 8
      app/message-star/client/actionButton.js
  41. 5
      app/meteor-accounts-saml/client/saml_client.js
  42. 4
      app/meteor-accounts-saml/server/saml_server.js
  43. 521
      app/models/server/models/Sessions.js
  44. 7
      app/models/server/models/Sessions.mocks.js
  45. 821
      app/models/server/models/Sessions.tests.js
  46. 10
      app/models/server/models/Users.js
  47. 1
      app/reactions/client/init.js
  48. 4
      app/slackbridge/server/RocketAdapter.js
  49. 2
      app/slashcommands-kick/server/server.js
  50. 2
      app/slashcommands-msg/server/server.js
  51. 2
      app/slashcommands-mute/server/mute.js
  52. 2
      app/slashcommands-mute/server/unmute.js
  53. 33
      app/statistics/server/functions/get.js
  54. 47
      app/statistics/server/lib/SAUMonitor.js
  55. 41
      app/statistics/server/lib/UAParserCustom.js
  56. 77
      app/statistics/server/lib/UAParserCustom.tests.js
  57. 1
      app/theme/client/imports/components/contextual-bar.css
  58. 7
      app/theme/client/imports/general/base_old.css
  59. 4
      app/threads/client/flextab/thread.html
  60. 7
      app/threads/client/flextab/thread.js
  61. 6
      app/threads/client/flextab/threads.html
  62. 11
      app/threads/client/flextab/threads.js
  63. 2
      app/threads/client/messageAction/follow.js
  64. 2
      app/threads/client/messageAction/unfollow.js
  65. 9
      app/threads/client/threads.css
  66. 2
      app/ui-flextab/client/tabs/membersList.html
  67. 7
      app/ui-flextab/client/tabs/membersList.js
  68. 149
      app/ui-flextab/client/tabs/userActions.js
  69. 23
      app/ui-flextab/client/tabs/userInfo.js
  70. 25
      app/ui-master/client/main.js
  71. 79
      app/ui-message/client/message.js
  72. 1
      app/ui-message/client/messageBox/messageBox.js
  73. 3
      app/ui-message/client/messageBox/messageBoxNotSubscribed.js
  74. 14
      app/ui-utils/client/lib/MessageAction.js
  75. 12
      app/ui-utils/client/lib/RoomHistoryManager.js
  76. 6
      app/ui-utils/client/lib/RoomManager.js
  77. 4
      app/ui-utils/client/lib/messageContext.js
  78. 146
      app/ui-utils/client/lib/openRoom.js
  79. 28
      app/ui-utils/client/lib/renderMessageBody.js
  80. 5
      app/ui/client/lib/chatMessages.js
  81. 4
      app/ui/client/views/app/room.js
  82. 2
      app/utils/rocketchat.info
  83. 54
      app/utils/server/functions/getMongoInfo.js
  84. 2
      app/utils/server/index.js
  85. 34
      app/utils/server/lib/composeMessageObjectWithUser.js
  86. 56
      app/utils/server/lib/normalizeMessagesForUser.js
  87. 2
      client/main.js
  88. 1
      imports/client/limax
  89. 1
      imports/client/map-age-cleaner
  90. 1
      imports/client/mem
  91. 1
      imports/client/mimic-fn
  92. 1
      imports/client/p-defer
  93. 1
      imports/client/p-is-promise
  94. 1
      imports/client/pinyin
  95. 619
      package-lock.json
  96. 6
      package.json
  97. 2
      packages/rocketchat-i18n/i18n/af.i18n.json
  98. 2
      packages/rocketchat-i18n/i18n/ar.i18n.json
  99. 2
      packages/rocketchat-i18n/i18n/az.i18n.json
  100. 2
      packages/rocketchat-i18n/i18n/be-BY.i18n.json
  101. Some files were not shown because too many files have changed in this diff Show More

@ -69,6 +69,7 @@ jobs:
<<: *defaults
docker:
- image: circleci/node:8.11-stretch
- image: mongo:3.2
steps:
- checkout
@ -129,7 +130,7 @@ jobs:
- run:
name: Unit Test
command: |
meteor npm run testunit
MONGO_URL=mongodb://localhost:27017 meteor npm run testunit
- restore_cache:
keys:

@ -1,6 +1,6 @@
FROM registry.access.redhat.com/rhscl/nodejs-8-rhel7
ENV RC_VERSION 1.0.2
ENV RC_VERSION 1.0.3
MAINTAINER buildmaster@rocket.chat

@ -22,3 +22,4 @@ public/livechat/
!.scripts
!packages/rocketchat-livechat/.app
public/pdf.worker.min.js
imports/client/

@ -15362,9 +15362,6 @@
}
]
},
"HEAD": {
"pull_requests": []
},
"0.66.0-rc.0": {
"node_version": "8.11.1",
"npm_version": "5.6.0",
@ -30124,6 +30121,309 @@
]
}
]
},
"1.0.3": {
"node_version": "8.11.4",
"npm_version": "6.4.1",
"mongo_versions": [
"3.2",
"3.4",
"3.6",
"4.0"
],
"pull_requests": [
{
"pr": "14443",
"title": "[FIX] Channel Leader Bar is in the way of Thread Header ",
"userLogin": "ggazzo",
"milestone": "1.0.3",
"contributors": [
"ggazzo"
]
},
{
"pr": "14433",
"title": "Fix i18n files keys sort",
"userLogin": "sampaiodiego",
"milestone": "1.0.3",
"contributors": [
"sampaiodiego",
"web-flow"
]
},
{
"pr": "14442",
"title": "[FIX] Discussion name being invalid",
"userLogin": "sampaiodiego",
"milestone": "1.0.3",
"contributors": [
"sampaiodiego"
]
},
{
"pr": "14415",
"title": "[FIX] Room name was undefined in some info dialogs",
"userLogin": "MarcosSpessatto",
"milestone": "1.0.3",
"contributors": [
"MarcosSpessatto"
]
},
{
"pr": "14405",
"title": "[FIX] Exception on crowd sync due to a wrong logging method",
"userLogin": "rodrigok",
"milestone": "1.0.3",
"contributors": [
"rodrigok"
]
},
{
"pr": "14418",
"title": "Fixes on DAU and MAU aggregations",
"userLogin": "rodrigok",
"milestone": "1.0.3",
"contributors": [
"rodrigok",
"web-flow"
]
},
{
"pr": "14397",
"title": "[IMPROVE] Don't use regex to find users",
"userLogin": "sampaiodiego",
"milestone": "1.0.3",
"contributors": [
"sampaiodiego"
]
},
{
"pr": "14431",
"title": "[IMPROVE] Added flag `skipActiveUsersToBeReady` to not wait the load of `active users` to present the Web interface",
"userLogin": "ggazzo",
"milestone": "1.0.3",
"contributors": [
"ggazzo",
"web-flow",
"tassoevan"
]
},
{
"pr": "14422",
"title": "[FIX] IE11 support",
"userLogin": "tassoevan",
"milestone": "1.0.3",
"contributors": [
"tassoevan",
"web-flow"
]
},
{
"pr": "14386",
"title": "Add missing german translations",
"userLogin": "mrsimpson",
"milestone": "1.0.3",
"contributors": [
"mrsimpson",
"sampaiodiego",
"web-flow"
]
},
{
"pr": "14432",
"title": "[FIX] Escape unrecognized slash command message",
"userLogin": "tassoevan",
"milestone": "1.0.3",
"contributors": [
"tassoevan"
]
},
{
"pr": "14430",
"title": "[FIX] Mentions message missing 'jump to message' action",
"userLogin": "ggazzo",
"milestone": "1.0.3",
"contributors": [
"ggazzo"
]
},
{
"pr": "14426",
"title": "LingoHub based on develop",
"userLogin": "engelgabriel",
"milestone": "1.0.3",
"contributors": [
"sampaiodiego"
]
},
{
"pr": "14419",
"title": "[FIX] preview pdf its not working",
"userLogin": "ggazzo",
"milestone": "1.0.3",
"contributors": [
"ggazzo"
]
},
{
"pr": "14404",
"title": "[FIX] Messages on thread panel were receiving wrong context/subscription",
"userLogin": "ggazzo",
"milestone": "1.0.3",
"contributors": [
"ggazzo",
"web-flow"
]
},
{
"pr": "14402",
"title": "[FIX] Error 400 on send a reply to an old thread",
"userLogin": "ggazzo",
"milestone": "1.0.3",
"contributors": [
"ggazzo"
]
},
{
"pr": "14400",
"title": "[FIX] Users actions in administration were returning error",
"userLogin": "tassoevan",
"milestone": "1.0.3",
"contributors": [
"tassoevan"
]
},
{
"pr": "14403",
"title": "[FIX] Fallback to mongo version that doesn't require clusterMonitor role",
"userLogin": "geekgonecrazy",
"milestone": "1.0.3",
"contributors": [
"geekgonecrazy"
]
},
{
"pr": "14345",
"title": "[FIX] SAML credentialToken removal was preventing mobile from being able to authenticate",
"userLogin": "geekgonecrazy",
"milestone": "1.0.3",
"contributors": [
"geekgonecrazy",
"web-flow"
]
},
{
"pr": "14376",
"title": "[FIX] Stream not connecting connect when using subdir and multi-instance",
"userLogin": "geekgonecrazy",
"milestone": "1.0.3",
"contributors": [
"geekgonecrazy"
]
},
{
"pr": "14388",
"title": "[FIX] Pressing Enter in User Search field at channel causes reload",
"userLogin": "MarcosSpessatto",
"milestone": "1.0.3",
"contributors": [
"MarcosSpessatto",
"tassoevan"
]
},
{
"pr": "14379",
"title": "[FIX] Wrong token name was generating error on Gitlab OAuth login",
"userLogin": "MarcosSpessatto",
"milestone": "1.0.3",
"contributors": [
"MarcosSpessatto"
]
},
{
"pr": "14387",
"title": "[FIX] more message actions to threads context(follow, unfollow, copy, delete)",
"userLogin": "ggazzo",
"milestone": "1.0.3",
"contributors": [
"ggazzo"
]
},
{
"pr": "14396",
"title": "[FIX] Unnecessary meteor.defer on openRoom",
"userLogin": "ggazzo",
"milestone": "1.0.3",
"contributors": [
"ggazzo"
]
},
{
"pr": "14389",
"title": "[IMPROVE] Allow change Discussion's properties",
"userLogin": "ggazzo",
"milestone": "1.0.3",
"contributors": [
"ggazzo"
]
},
{
"pr": "14390",
"title": "Fix: Message body was not being updated when user disabled nrr message",
"userLogin": "ggazzo",
"milestone": "1.0.3",
"contributors": [
"ggazzo"
]
},
{
"pr": "14393",
"title": "[FIX] Messages on threads disappearing",
"userLogin": "ggazzo",
"milestone": "1.0.3",
"contributors": [
"ggazzo"
]
},
{
"pr": "14394",
"title": "[FIX] Bell was too small on threads",
"userLogin": "ggazzo",
"milestone": "1.0.3",
"contributors": [
"ggazzo"
]
},
{
"pr": "14372",
"title": "[FIX] Main thread title on replies",
"userLogin": "ggazzo",
"milestone": "1.0.3",
"contributors": [
"ggazzo",
"sampaiodiego",
"web-flow"
]
},
{
"pr": "14351",
"title": "Improve German translations",
"userLogin": "mrsimpson",
"milestone": "1.0.3",
"contributors": [
"mrsimpson"
]
},
{
"pr": "14362",
"title": "[FIX] New day separator overlapping above system message",
"userLogin": "rodrigok",
"milestone": "1.0.3",
"contributors": [
"rodrigok"
]
}
]
}
}
}
}

@ -0,0 +1,28 @@
/**
* This script will:
*
* - remove any duplicated i18n key on the same file;
* - re-order all keys based on source i18n file (en.i18n.json)
* - remove all keys not present in source i18n file
*/
const fg = require('fast-glob');
const fs = require('fs');
const fixFiles = (path, source, newlineAtEnd = false) => {
const sourceFile = JSON.parse(fs.readFileSync(`${ path }${ source }`, 'utf8'));
const sourceKeys = Object.keys(sourceFile);
fg([`${ path }/**/*.i18n.json`]).then((entries) => {
entries.forEach((file) => {
console.log(file);
const json = JSON.parse(fs.readFileSync(file, 'utf8'));
fs.writeFileSync(file, `${ JSON.stringify(json, sourceKeys, 2) }${ newlineAtEnd ? '\n' : '' }`);
});
});
};
fixFiles('./packages/rocketchat-i18n', '/i18n/en.i18n.json');
fixFiles('./packages/rocketchat-livechat/.app/i18n', '/en.i18n.json');

@ -17,7 +17,7 @@ elif [[ $TRAVIS_TAG ]]; then
RC_VERSION=$TRAVIS_TAG
else
CHANNEL=edge
RC_VERSION=1.0.2
RC_VERSION=1.0.3
fi
echo "Preparing to trigger a snap release for $CHANNEL channel"

@ -1,4 +1,65 @@
# 1.0.3
`2019-05-09 · 3 🚀 · 22 🐛 · 6 🔍 · 7 👩💻👨💻`
### Engine versions
- Node: `8.11.4`
- NPM: `6.4.1`
- MongoDB: `3.2, 3.4, 3.6, 4.0`
### 🚀 Improvements
- Don't use regex to find users ([#14397](https://github.com/RocketChat/Rocket.Chat/pull/14397))
- Added flag `skipActiveUsersToBeReady` to not wait the load of `active users` to present the Web interface ([#14431](https://github.com/RocketChat/Rocket.Chat/pull/14431))
- Allow change Discussion's properties ([#14389](https://github.com/RocketChat/Rocket.Chat/pull/14389))
### 🐛 Bug fixes
- Channel Leader Bar is in the way of Thread Header ([#14443](https://github.com/RocketChat/Rocket.Chat/pull/14443))
- Discussion name being invalid ([#14442](https://github.com/RocketChat/Rocket.Chat/pull/14442))
- Room name was undefined in some info dialogs ([#14415](https://github.com/RocketChat/Rocket.Chat/pull/14415))
- Exception on crowd sync due to a wrong logging method ([#14405](https://github.com/RocketChat/Rocket.Chat/pull/14405))
- IE11 support ([#14422](https://github.com/RocketChat/Rocket.Chat/pull/14422))
- Escape unrecognized slash command message ([#14432](https://github.com/RocketChat/Rocket.Chat/pull/14432))
- Mentions message missing 'jump to message' action ([#14430](https://github.com/RocketChat/Rocket.Chat/pull/14430))
- preview pdf its not working ([#14419](https://github.com/RocketChat/Rocket.Chat/pull/14419))
- Messages on thread panel were receiving wrong context/subscription ([#14404](https://github.com/RocketChat/Rocket.Chat/pull/14404))
- Error 400 on send a reply to an old thread ([#14402](https://github.com/RocketChat/Rocket.Chat/pull/14402))
- Users actions in administration were returning error ([#14400](https://github.com/RocketChat/Rocket.Chat/pull/14400))
- Fallback to mongo version that doesn't require clusterMonitor role ([#14403](https://github.com/RocketChat/Rocket.Chat/pull/14403))
- SAML credentialToken removal was preventing mobile from being able to authenticate ([#14345](https://github.com/RocketChat/Rocket.Chat/pull/14345))
- Stream not connecting connect when using subdir and multi-instance ([#14376](https://github.com/RocketChat/Rocket.Chat/pull/14376))
- Pressing Enter in User Search field at channel causes reload ([#14388](https://github.com/RocketChat/Rocket.Chat/pull/14388))
- Wrong token name was generating error on Gitlab OAuth login ([#14379](https://github.com/RocketChat/Rocket.Chat/pull/14379))
- more message actions to threads context(follow, unfollow, copy, delete) ([#14387](https://github.com/RocketChat/Rocket.Chat/pull/14387))
- Unnecessary meteor.defer on openRoom ([#14396](https://github.com/RocketChat/Rocket.Chat/pull/14396))
- Messages on threads disappearing ([#14393](https://github.com/RocketChat/Rocket.Chat/pull/14393))
- Bell was too small on threads ([#14394](https://github.com/RocketChat/Rocket.Chat/pull/14394))
- Main thread title on replies ([#14372](https://github.com/RocketChat/Rocket.Chat/pull/14372))
- New day separator overlapping above system message ([#14362](https://github.com/RocketChat/Rocket.Chat/pull/14362))
<details>
<summary>🔍 Minor changes</summary>
- Fix i18n files keys sort ([#14433](https://github.com/RocketChat/Rocket.Chat/pull/14433))
- Fixes on DAU and MAU aggregations ([#14418](https://github.com/RocketChat/Rocket.Chat/pull/14418))
- Add missing german translations ([#14386](https://github.com/RocketChat/Rocket.Chat/pull/14386))
- LingoHub based on develop ([#14426](https://github.com/RocketChat/Rocket.Chat/pull/14426))
- Fix: Message body was not being updated when user disabled nrr message ([#14390](https://github.com/RocketChat/Rocket.Chat/pull/14390))
- Improve German translations ([#14351](https://github.com/RocketChat/Rocket.Chat/pull/14351))
</details>
### 👩💻👨💻 Core Team 🤓
- [@MarcosSpessatto](https://github.com/MarcosSpessatto)
- [@geekgonecrazy](https://github.com/geekgonecrazy)
- [@ggazzo](https://github.com/ggazzo)
- [@mrsimpson](https://github.com/mrsimpson)
- [@rodrigok](https://github.com/rodrigok)
- [@sampaiodiego](https://github.com/sampaiodiego)
- [@tassoevan](https://github.com/tassoevan)
# 1.0.2
`2019-04-30 · 2 🚀 · 8 🐛 · 5 🔍 · 10 👩💻👨💻`
@ -824,7 +885,7 @@
- Syncloud deploy option ([#12867](https://github.com/RocketChat/Rocket.Chat/pull/12867) by [@cyberb](https://github.com/cyberb))
- Config hooks for snap ([#12351](https://github.com/RocketChat/Rocket.Chat/pull/12351))
- Livechat registration form message ([#12597](https://github.com/RocketChat/Rocket.Chat/pull/12597))
- Include message type & id in push notification payload ([#12771](https://github.com/RocketChat/Rocket.Chat/pull/12771) by [@cardoso](https://github.com/cardoso))
- Include message type & id in push notification payload ([#12771](https://github.com/RocketChat/Rocket.Chat/pull/12771))
### 🚀 Improvements
@ -968,7 +1029,6 @@
### 👩💻👨💻 Contributors 😍
- [@alexbartsch](https://github.com/alexbartsch)
- [@cardoso](https://github.com/cardoso)
- [@cyberb](https://github.com/cyberb)
- [@hypery2k](https://github.com/hypery2k)
- [@karakayasemi](https://github.com/karakayasemi)
@ -987,6 +1047,7 @@
- [@Hudell](https://github.com/Hudell)
- [@LuluGO](https://github.com/LuluGO)
- [@MarcosSpessatto](https://github.com/MarcosSpessatto)
- [@cardoso](https://github.com/cardoso)
- [@d-gubert](https://github.com/d-gubert)
- [@engelgabriel](https://github.com/engelgabriel)
- [@geekgonecrazy](https://github.com/geekgonecrazy)
@ -1082,7 +1143,7 @@
- Add permission to enable personal access token to specific roles ([#12309](https://github.com/RocketChat/Rocket.Chat/pull/12309))
- Option to reset e2e key ([#12483](https://github.com/RocketChat/Rocket.Chat/pull/12483))
- /api/v1/spotlight: return joinCodeRequired field for rooms ([#12651](https://github.com/RocketChat/Rocket.Chat/pull/12651) by [@cardoso](https://github.com/cardoso))
- /api/v1/spotlight: return joinCodeRequired field for rooms ([#12651](https://github.com/RocketChat/Rocket.Chat/pull/12651))
- New API Endpoints for the new version of JS SDK ([#12623](https://github.com/RocketChat/Rocket.Chat/pull/12623))
- Setting to configure robots.txt content ([#12547](https://github.com/RocketChat/Rocket.Chat/pull/12547))
- Make Livechat's widget draggable ([#12378](https://github.com/RocketChat/Rocket.Chat/pull/12378))
@ -1220,7 +1281,6 @@
- [@AndreamApp](https://github.com/AndreamApp)
- [@Ismaw34](https://github.com/Ismaw34)
- [@cardoso](https://github.com/cardoso)
- [@imronras](https://github.com/imronras)
- [@karlprieb](https://github.com/karlprieb)
- [@mbrodala](https://github.com/mbrodala)
@ -1237,6 +1297,7 @@
- [@Hudell](https://github.com/Hudell)
- [@MarcosSpessatto](https://github.com/MarcosSpessatto)
- [@cardoso](https://github.com/cardoso)
- [@engelgabriel](https://github.com/engelgabriel)
- [@ggazzo](https://github.com/ggazzo)
- [@marceloschmidt](https://github.com/marceloschmidt)
@ -1452,7 +1513,7 @@
<summary>🔍 Minor changes</summary>
- Release 0.70.1 ([#12270](https://github.com/RocketChat/Rocket.Chat/pull/12270) by [@edzluhan](https://github.com/edzluhan))
- Merge master into develop & Set version to 0.71.0-develop ([#12264](https://github.com/RocketChat/Rocket.Chat/pull/12264) by [@cardoso](https://github.com/cardoso) & [@kaiiiiiiiii](https://github.com/kaiiiiiiiii))
- Merge master into develop & Set version to 0.71.0-develop ([#12264](https://github.com/RocketChat/Rocket.Chat/pull/12264) by [@kaiiiiiiiii](https://github.com/kaiiiiiiiii))
- Regression: fix modal submit ([#12233](https://github.com/RocketChat/Rocket.Chat/pull/12233))
- Add reetp to the issues' bot whitelist ([#12227](https://github.com/RocketChat/Rocket.Chat/pull/12227))
- Fix: Remove semver satisfies from Apps details that is already done my marketplace ([#12268](https://github.com/RocketChat/Rocket.Chat/pull/12268))
@ -1461,13 +1522,13 @@
### 👩💻👨💻 Contributors 😍
- [@cardoso](https://github.com/cardoso)
- [@edzluhan](https://github.com/edzluhan)
- [@kaiiiiiiiii](https://github.com/kaiiiiiiiii)
### 👩💻👨💻 Core Team 🤓
- [@Hudell](https://github.com/Hudell)
- [@cardoso](https://github.com/cardoso)
- [@ggazzo](https://github.com/ggazzo)
- [@renatobecker](https://github.com/renatobecker)
- [@rodrigok](https://github.com/rodrigok)
@ -1493,7 +1554,7 @@
- Allow multiple subcommands in MIGRATION_VERSION env variable ([#11184](https://github.com/RocketChat/Rocket.Chat/pull/11184) by [@arch119](https://github.com/arch119))
- Support for end to end encryption ([#10094](https://github.com/RocketChat/Rocket.Chat/pull/10094) by [@mrinaldhar](https://github.com/mrinaldhar))
- Livechat Analytics and Reports ([#11238](https://github.com/RocketChat/Rocket.Chat/pull/11238) by [@pkgodara](https://github.com/pkgodara))
- Apps: Add handlers for message updates ([#11993](https://github.com/RocketChat/Rocket.Chat/pull/11993) by [@cardoso](https://github.com/cardoso))
- Apps: Add handlers for message updates ([#11993](https://github.com/RocketChat/Rocket.Chat/pull/11993))
- Livechat notifications on new incoming inquiries for guest-pool ([#10588](https://github.com/RocketChat/Rocket.Chat/pull/10588))
- Customizable default directory view ([#11965](https://github.com/RocketChat/Rocket.Chat/pull/11965) by [@ohmonster](https://github.com/ohmonster))
- Blockstack as decentralized auth provider ([#12047](https://github.com/RocketChat/Rocket.Chat/pull/12047))
@ -1552,13 +1613,13 @@
<details>
<summary>🔍 Minor changes</summary>
- Release 0.69.2 ([#12026](https://github.com/RocketChat/Rocket.Chat/pull/12026) by [@cardoso](https://github.com/cardoso) & [@kaiiiiiiiii](https://github.com/kaiiiiiiiii))
- Release 0.69.2 ([#12026](https://github.com/RocketChat/Rocket.Chat/pull/12026) by [@kaiiiiiiiii](https://github.com/kaiiiiiiiii))
- LingoHub based on develop ([#11936](https://github.com/RocketChat/Rocket.Chat/pull/11936))
- Better organize package.json ([#12115](https://github.com/RocketChat/Rocket.Chat/pull/12115))
- Fix using wrong variable ([#12114](https://github.com/RocketChat/Rocket.Chat/pull/12114))
- Fix the style lint ([#11991](https://github.com/RocketChat/Rocket.Chat/pull/11991))
- Merge master into develop & Set version to 0.70.0-develop ([#11921](https://github.com/RocketChat/Rocket.Chat/pull/11921) by [@c0dzilla](https://github.com/c0dzilla) & [@rndmh3ro](https://github.com/rndmh3ro) & [@ubarsaiyan](https://github.com/ubarsaiyan) & [@vynmera](https://github.com/vynmera))
- Release 0.69.2 ([#12026](https://github.com/RocketChat/Rocket.Chat/pull/12026) by [@cardoso](https://github.com/cardoso) & [@kaiiiiiiiii](https://github.com/kaiiiiiiiii))
- Release 0.69.2 ([#12026](https://github.com/RocketChat/Rocket.Chat/pull/12026) by [@kaiiiiiiiii](https://github.com/kaiiiiiiiii))
- Regression: fix message box autogrow ([#12138](https://github.com/RocketChat/Rocket.Chat/pull/12138))
- Regression: Modal height ([#12122](https://github.com/RocketChat/Rocket.Chat/pull/12122))
- Fix: Change wording on e2e to make a little more clear ([#12124](https://github.com/RocketChat/Rocket.Chat/pull/12124))
@ -1583,7 +1644,6 @@
- [@aferreira44](https://github.com/aferreira44)
- [@arch119](https://github.com/arch119)
- [@c0dzilla](https://github.com/c0dzilla)
- [@cardoso](https://github.com/cardoso)
- [@crazy-max](https://github.com/crazy-max)
- [@edzluhan](https://github.com/edzluhan)
- [@flaviogrossi](https://github.com/flaviogrossi)
@ -1604,6 +1664,7 @@
- [@Hudell](https://github.com/Hudell)
- [@MarcosSpessatto](https://github.com/MarcosSpessatto)
- [@MartinSchoeler](https://github.com/MartinSchoeler)
- [@cardoso](https://github.com/cardoso)
- [@engelgabriel](https://github.com/engelgabriel)
- [@geekgonecrazy](https://github.com/geekgonecrazy)
- [@ggazzo](https://github.com/ggazzo)
@ -1629,17 +1690,17 @@
### 🐛 Bug fixes
- Reset password link error if already logged in ([#12022](https://github.com/RocketChat/Rocket.Chat/pull/12022))
- Apps: setting with 'code' type only saving last line ([#11992](https://github.com/RocketChat/Rocket.Chat/pull/11992) by [@cardoso](https://github.com/cardoso))
- Apps: setting with 'code' type only saving last line ([#11992](https://github.com/RocketChat/Rocket.Chat/pull/11992))
- Update user information not possible by admin if disabled to users ([#11955](https://github.com/RocketChat/Rocket.Chat/pull/11955) by [@kaiiiiiiiii](https://github.com/kaiiiiiiiii))
- Hidden admin sidenav on embedded layout ([#12025](https://github.com/RocketChat/Rocket.Chat/pull/12025))
### 👩💻👨💻 Contributors 😍
- [@cardoso](https://github.com/cardoso)
- [@kaiiiiiiiii](https://github.com/kaiiiiiiiii)
### 👩💻👨💻 Core Team 🤓
- [@cardoso](https://github.com/cardoso)
- [@ggazzo](https://github.com/ggazzo)
- [@rodrigok](https://github.com/rodrigok)
- [@sampaiodiego](https://github.com/sampaiodiego)
@ -1937,7 +1998,7 @@
### 🚀 Improvements
- Set default max upload size to 100mb ([#11327](https://github.com/RocketChat/Rocket.Chat/pull/11327) by [@cardoso](https://github.com/cardoso))
- Set default max upload size to 100mb ([#11327](https://github.com/RocketChat/Rocket.Chat/pull/11327))
- Typing indicators now use Real Names ([#11164](https://github.com/RocketChat/Rocket.Chat/pull/11164) by [@vynmera](https://github.com/vynmera))
- Allow markdown in room topic, announcement, and description including single quotes ([#11408](https://github.com/RocketChat/Rocket.Chat/pull/11408))
@ -1990,7 +2051,6 @@
- [@PhpXp](https://github.com/PhpXp)
- [@arminfelder](https://github.com/arminfelder)
- [@arungalva](https://github.com/arungalva)
- [@cardoso](https://github.com/cardoso)
- [@karlprieb](https://github.com/karlprieb)
- [@soundstorm](https://github.com/soundstorm)
- [@tpDBL](https://github.com/tpDBL)
@ -2002,6 +2062,7 @@
- [@MarcosSpessatto](https://github.com/MarcosSpessatto)
- [@MartinSchoeler](https://github.com/MartinSchoeler)
- [@brunosquadros](https://github.com/brunosquadros)
- [@cardoso](https://github.com/cardoso)
- [@engelgabriel](https://github.com/engelgabriel)
- [@geekgonecrazy](https://github.com/geekgonecrazy)
- [@ggazzo](https://github.com/ggazzo)
@ -2231,7 +2292,7 @@
- Allow inviting livechat managers to the same LiveChat room ([#10956](https://github.com/RocketChat/Rocket.Chat/pull/10956))
- Cannot read property 'debug' of undefined when trying to use REST API ([#10805](https://github.com/RocketChat/Rocket.Chat/pull/10805) by [@haffla](https://github.com/haffla))
- Icons svg xml structure ([#10771](https://github.com/RocketChat/Rocket.Chat/pull/10771))
- Remove outdated 2FA warning for mobile clients ([#10916](https://github.com/RocketChat/Rocket.Chat/pull/10916) by [@cardoso](https://github.com/cardoso))
- Remove outdated 2FA warning for mobile clients ([#10916](https://github.com/RocketChat/Rocket.Chat/pull/10916))
- Update Sandstorm build config ([#10867](https://github.com/RocketChat/Rocket.Chat/pull/10867) by [@ocdtrekkie](https://github.com/ocdtrekkie))
- "blank messages" on iOS < 11 ([#11221](https://github.com/RocketChat/Rocket.Chat/pull/11221))
- "blank" screen on iOS < 11 ([#11199](https://github.com/RocketChat/Rocket.Chat/pull/11199))
@ -2300,7 +2361,6 @@
- [@JoseRenan](https://github.com/JoseRenan)
- [@brylie](https://github.com/brylie)
- [@c0dzilla](https://github.com/c0dzilla)
- [@cardoso](https://github.com/cardoso)
- [@cliffparnitzky](https://github.com/cliffparnitzky)
- [@cpitman](https://github.com/cpitman)
- [@filipealva](https://github.com/filipealva)
@ -2335,6 +2395,7 @@
- [@Hudell](https://github.com/Hudell)
- [@MarcosSpessatto](https://github.com/MarcosSpessatto)
- [@alansikora](https://github.com/alansikora)
- [@cardoso](https://github.com/cardoso)
- [@engelgabriel](https://github.com/engelgabriel)
- [@geekgonecrazy](https://github.com/geekgonecrazy)
- [@ggazzo](https://github.com/ggazzo)
@ -2416,7 +2477,7 @@
- Now is possible to access files using header authorization (`x-user-id` and `x-auth-token`) ([#10741](https://github.com/RocketChat/Rocket.Chat/pull/10741))
- Add REST API endpoints `channels.counters`, `groups.counters and `im.counters` ([#9679](https://github.com/RocketChat/Rocket.Chat/pull/9679) by [@xbolshe](https://github.com/xbolshe))
- Add REST API endpoints `channels.setCustomFields` and `groups.setCustomFields` ([#9733](https://github.com/RocketChat/Rocket.Chat/pull/9733) by [@xbolshe](https://github.com/xbolshe))
- Add permission `view-broadcast-member-list` ([#10753](https://github.com/RocketChat/Rocket.Chat/pull/10753) by [@cardoso](https://github.com/cardoso))
- Add permission `view-broadcast-member-list` ([#10753](https://github.com/RocketChat/Rocket.Chat/pull/10753))
### 🐛 Bug fixes
@ -2440,7 +2501,7 @@
<details>
<summary>🔍 Minor changes</summary>
- Release 0.65.0 ([#10893](https://github.com/RocketChat/Rocket.Chat/pull/10893) by [@Sameesunkaria](https://github.com/Sameesunkaria) & [@cardoso](https://github.com/cardoso) & [@erhan-](https://github.com/erhan-) & [@gdelavald](https://github.com/gdelavald) & [@karlprieb](https://github.com/karlprieb) & [@peccu](https://github.com/peccu) & [@winterstefan](https://github.com/winterstefan))
- Release 0.65.0 ([#10893](https://github.com/RocketChat/Rocket.Chat/pull/10893) by [@Sameesunkaria](https://github.com/Sameesunkaria) & [@erhan-](https://github.com/erhan-) & [@gdelavald](https://github.com/gdelavald) & [@karlprieb](https://github.com/karlprieb) & [@peccu](https://github.com/peccu) & [@winterstefan](https://github.com/winterstefan))
- Apps: Command Previews, Message and Room Removal Events ([#10822](https://github.com/RocketChat/Rocket.Chat/pull/10822))
- Develop sync ([#10815](https://github.com/RocketChat/Rocket.Chat/pull/10815) by [@nsuchy](https://github.com/nsuchy))
- Major dependencies update ([#10661](https://github.com/RocketChat/Rocket.Chat/pull/10661))
@ -2464,7 +2525,6 @@
- [@Sameesunkaria](https://github.com/Sameesunkaria)
- [@ThomasRoehl](https://github.com/ThomasRoehl)
- [@c0dzilla](https://github.com/c0dzilla)
- [@cardoso](https://github.com/cardoso)
- [@cfunkles](https://github.com/cfunkles)
- [@chuckAtCataworx](https://github.com/chuckAtCataworx)
- [@erhan-](https://github.com/erhan-)
@ -2480,6 +2540,7 @@
- [@Hudell](https://github.com/Hudell)
- [@MarcosSpessatto](https://github.com/MarcosSpessatto)
- [@cardoso](https://github.com/cardoso)
- [@engelgabriel](https://github.com/engelgabriel)
- [@geekgonecrazy](https://github.com/geekgonecrazy)
- [@ggazzo](https://github.com/ggazzo)
@ -2498,11 +2559,11 @@
### 🎉 New features
- Add REST endpoints `channels.roles` & `groups.roles` ([#10607](https://github.com/RocketChat/Rocket.Chat/pull/10607) by [@cardoso](https://github.com/cardoso))
- Add REST endpoints `channels.roles` & `groups.roles` ([#10607](https://github.com/RocketChat/Rocket.Chat/pull/10607))
- Add more options for Wordpress OAuth configuration ([#10724](https://github.com/RocketChat/Rocket.Chat/pull/10724))
- Setup Wizard ([#10523](https://github.com/RocketChat/Rocket.Chat/pull/10523) by [@karlprieb](https://github.com/karlprieb))
- Improvements to notifications logic ([#10686](https://github.com/RocketChat/Rocket.Chat/pull/10686))
- Add REST endpoints `channels.roles` & `groups.roles` ([#10607](https://github.com/RocketChat/Rocket.Chat/pull/10607) by [@cardoso](https://github.com/cardoso))
- Add REST endpoints `channels.roles` & `groups.roles` ([#10607](https://github.com/RocketChat/Rocket.Chat/pull/10607))
- Add more options for Wordpress OAuth configuration ([#10724](https://github.com/RocketChat/Rocket.Chat/pull/10724))
- Setup Wizard ([#10523](https://github.com/RocketChat/Rocket.Chat/pull/10523) by [@karlprieb](https://github.com/karlprieb))
- Improvements to notifications logic ([#10686](https://github.com/RocketChat/Rocket.Chat/pull/10686))
@ -2529,7 +2590,7 @@
<details>
<summary>🔍 Minor changes</summary>
- Release 0.64.2 ([#10812](https://github.com/RocketChat/Rocket.Chat/pull/10812) by [@Sameesunkaria](https://github.com/Sameesunkaria) & [@cardoso](https://github.com/cardoso) & [@erhan-](https://github.com/erhan-) & [@gdelavald](https://github.com/gdelavald) & [@karlprieb](https://github.com/karlprieb) & [@peccu](https://github.com/peccu) & [@winterstefan](https://github.com/winterstefan))
- Release 0.64.2 ([#10812](https://github.com/RocketChat/Rocket.Chat/pull/10812) by [@Sameesunkaria](https://github.com/Sameesunkaria) & [@erhan-](https://github.com/erhan-) & [@gdelavald](https://github.com/gdelavald) & [@karlprieb](https://github.com/karlprieb) & [@peccu](https://github.com/peccu) & [@winterstefan](https://github.com/winterstefan))
- Prometheus: Add metric to track hooks time ([#10798](https://github.com/RocketChat/Rocket.Chat/pull/10798))
- Regression: Autorun of wizard was not destroyed after completion ([#10802](https://github.com/RocketChat/Rocket.Chat/pull/10802))
- Prometheus: Fix notification metric ([#10803](https://github.com/RocketChat/Rocket.Chat/pull/10803))
@ -2566,7 +2627,6 @@
### 👩💻👨💻 Contributors 😍
- [@Sameesunkaria](https://github.com/Sameesunkaria)
- [@cardoso](https://github.com/cardoso)
- [@erhan-](https://github.com/erhan-)
- [@gdelavald](https://github.com/gdelavald)
- [@karlprieb](https://github.com/karlprieb)
@ -2577,6 +2637,7 @@
- [@Hudell](https://github.com/Hudell)
- [@MarcosSpessatto](https://github.com/MarcosSpessatto)
- [@cardoso](https://github.com/cardoso)
- [@engelgabriel](https://github.com/engelgabriel)
- [@rafaelks](https://github.com/rafaelks)
- [@rodrigok](https://github.com/rodrigok)
@ -2704,7 +2765,7 @@
- Release 0.64.0 ([#10613](https://github.com/RocketChat/Rocket.Chat/pull/10613) by [@christianh814](https://github.com/christianh814) & [@gdelavald](https://github.com/gdelavald) & [@tttt-conan](https://github.com/tttt-conan))
- Regression: Various search provider fixes ([#10591](https://github.com/RocketChat/Rocket.Chat/pull/10591) by [@tkurz](https://github.com/tkurz))
- Regression: /api/v1/settings.oauth not sending needed info for SAML & CAS ([#10596](https://github.com/RocketChat/Rocket.Chat/pull/10596) by [@cardoso](https://github.com/cardoso))
- Regression: /api/v1/settings.oauth not sending needed info for SAML & CAS ([#10596](https://github.com/RocketChat/Rocket.Chat/pull/10596))
- Regression: Apps and Livechats not getting along well with each other ([#10598](https://github.com/RocketChat/Rocket.Chat/pull/10598))
- Development: Add Visual Studio Code debugging configuration ([#10586](https://github.com/RocketChat/Rocket.Chat/pull/10586))
- Included missing lib for migrations ([#10532](https://github.com/RocketChat/Rocket.Chat/pull/10532))
@ -2725,7 +2786,7 @@
- Regression: Revert announcement structure ([#10544](https://github.com/RocketChat/Rocket.Chat/pull/10544) by [@gdelavald](https://github.com/gdelavald))
- Regression: Upload was not working ([#10543](https://github.com/RocketChat/Rocket.Chat/pull/10543))
- Deps update ([#10549](https://github.com/RocketChat/Rocket.Chat/pull/10549))
- Regression: /api/v1/settings.oauth not returning clientId for Twitter ([#10560](https://github.com/RocketChat/Rocket.Chat/pull/10560) by [@cardoso](https://github.com/cardoso))
- Regression: /api/v1/settings.oauth not returning clientId for Twitter ([#10560](https://github.com/RocketChat/Rocket.Chat/pull/10560))
- Regression: Webhooks breaking due to restricted test ([#10555](https://github.com/RocketChat/Rocket.Chat/pull/10555))
- Regression: Rooms and Apps weren't playing nice with each other ([#10559](https://github.com/RocketChat/Rocket.Chat/pull/10559))
- Regression: Fix announcement bar being displayed without content ([#10554](https://github.com/RocketChat/Rocket.Chat/pull/10554) by [@gdelavald](https://github.com/gdelavald))
@ -2743,7 +2804,6 @@
- [@abernix](https://github.com/abernix)
- [@brendangadd](https://github.com/brendangadd)
- [@c0dzilla](https://github.com/c0dzilla)
- [@cardoso](https://github.com/cardoso)
- [@christianh814](https://github.com/christianh814)
- [@dschuan](https://github.com/dschuan)
- [@gdelavald](https://github.com/gdelavald)
@ -2763,6 +2823,7 @@
- [@Hudell](https://github.com/Hudell)
- [@MarcosSpessatto](https://github.com/MarcosSpessatto)
- [@TwizzyDizzy](https://github.com/TwizzyDizzy)
- [@cardoso](https://github.com/cardoso)
- [@engelgabriel](https://github.com/engelgabriel)
- [@geekgonecrazy](https://github.com/geekgonecrazy)
- [@ggazzo](https://github.com/ggazzo)

@ -1,9 +1,10 @@
import { composeMessageObjectWithUser } from '../../../utils';
import { normalizeMessagesForUser } from '../../../utils/server/lib/normalizeMessagesForUser';
import { API } from '../api';
API.helperMethods.set('composeRoomWithLastMessage', function _composeRoomWithLastMessage(room, userId) {
if (room.lastMessage) {
room.lastMessage = composeMessageObjectWithUser(room.lastMessage, userId);
const [lastMessage] = normalizeMessagesForUser([room.lastMessage], userId);
room.lastMessage = lastMessage;
}
return room;
});

@ -11,9 +11,9 @@ API.helperMethods.set('getUserFromParams', function _getUserFromParams() {
if (params.userId && params.userId.trim()) {
user = Users.findOneById(params.userId) || doesntExist;
} else if (params.username && params.username.trim()) {
user = Users.findOneByUsername(params.username) || doesntExist;
user = Users.findOneByUsernameIgnoringCase(params.username) || doesntExist;
} else if (params.user && params.user.trim()) {
user = Users.findOneByUsername(params.user) || doesntExist;
user = Users.findOneByUsernameIgnoringCase(params.user) || doesntExist;
} else {
throw new Meteor.Error('error-user-param-not-provided', 'The required "userId" or "username" param was not provided');
}

@ -1,7 +1,7 @@
import { Meteor } from 'meteor/meteor';
import { Rooms, Subscriptions, Messages, Uploads, Integrations, Users } from '../../../models';
import { hasPermission } from '../../../authorization';
import { composeMessageObjectWithUser } from '../../../utils';
import { normalizeMessagesForUser } from '../../../utils/server/lib/normalizeMessagesForUser';
import { API } from '../api';
import _ from 'underscore';
@ -28,7 +28,8 @@ function findChannelByIdOrName({ params, checkedArchived = true, userId }) {
throw new Meteor.Error('error-room-archived', `The channel, ${ room.name }, is archived`);
}
if (userId && room.lastMessage) {
room.lastMessage = composeMessageObjectWithUser(room.lastMessage, userId);
const [lastMessage] = normalizeMessagesForUser([room.lastMessage], userId);
room.lastMessage = lastMessage;
}
return room;
@ -578,7 +579,7 @@ API.v1.addRoute('channels.messages', { authRequired: true }, {
const messages = cursor.fetch();
return API.v1.success({
messages: messages.map((record) => composeMessageObjectWithUser(record, this.userId)),
messages: normalizeMessagesForUser(messages, this.userId),
count: messages.length,
offset,
total,

@ -2,7 +2,7 @@ import { Meteor } from 'meteor/meteor';
import { Match, check } from 'meteor/check';
import { Messages } from '../../../models';
import { canAccessRoom, hasPermission } from '../../../authorization';
import { composeMessageObjectWithUser } from '../../../utils';
import { normalizeMessagesForUser } from '../../../utils/server/lib/normalizeMessagesForUser';
import { processWebhookMessage } from '../../../lib';
import { API } from '../api';
import Rooms from '../../../models/server/models/Rooms';
@ -68,8 +68,8 @@ API.v1.addRoute('chat.syncMessages', { authRequired: true }, {
return API.v1.success({
result: {
updated: result.updated.map((message) => composeMessageObjectWithUser(message, this.userId)),
deleted: result.deleted.map((message) => composeMessageObjectWithUser(message, this.userId)),
updated: normalizeMessagesForUser(result.updated, this.userId),
deleted: normalizeMessagesForUser(result.deleted, this.userId),
},
});
},
@ -90,8 +90,10 @@ API.v1.addRoute('chat.getMessage', { authRequired: true }, {
return API.v1.failure();
}
const [message] = normalizeMessagesForUser([msg], this.userId);
return API.v1.success({
message: composeMessageObjectWithUser(msg, this.userId),
message,
});
},
});
@ -111,8 +113,10 @@ API.v1.addRoute('chat.pinMessage', { authRequired: true }, {
let pinnedMessage;
Meteor.runAsUser(this.userId, () => pinnedMessage = Meteor.call('pinMessage', msg));
const [message] = normalizeMessagesForUser([pinnedMessage], this.userId);
return API.v1.success({
message: composeMessageObjectWithUser(pinnedMessage, this.userId),
message,
});
},
});
@ -125,10 +129,12 @@ API.v1.addRoute('chat.postMessage', { authRequired: true }, {
return API.v1.failure('unknown-error');
}
const [message] = normalizeMessagesForUser([messageReturn.message], this.userId);
return API.v1.success({
ts: Date.now(),
channel: messageReturn.channel,
message: composeMessageObjectWithUser(messageReturn.message, this.userId),
message,
});
},
});
@ -150,7 +156,7 @@ API.v1.addRoute('chat.search', { authRequired: true }, {
Meteor.runAsUser(this.userId, () => result = Meteor.call('messageSearch', searchText, roomId, count).message.docs);
return API.v1.success({
messages: result.map((message) => composeMessageObjectWithUser(message, this.userId)),
messages: normalizeMessagesForUser(result, this.userId),
});
},
});
@ -164,11 +170,12 @@ API.v1.addRoute('chat.sendMessage', { authRequired: true }, {
throw new Meteor.Error('error-invalid-params', 'The "message" parameter must be provided.');
}
let message;
Meteor.runAsUser(this.userId, () => message = Meteor.call('sendMessage', this.bodyParams.message));
const sent = Meteor.runAsUser(this.userId, () => Meteor.call('sendMessage', this.bodyParams.message));
const [message] = normalizeMessagesForUser([sent], this.userId);
return API.v1.success({
message: composeMessageObjectWithUser(message, this.userId),
message,
});
},
});
@ -259,8 +266,10 @@ API.v1.addRoute('chat.update', { authRequired: true }, {
Meteor.call('updateMessage', { _id: msg._id, msg: this.bodyParams.text, rid: msg.rid });
});
const [message] = normalizeMessagesForUser([Messages.findOneById(msg._id)], this.userId);
return API.v1.success({
message: composeMessageObjectWithUser(Messages.findOneById(msg._id), this.userId),
message,
});
},
});

@ -4,7 +4,7 @@ import { Meteor } from 'meteor/meteor';
import { Subscriptions, Rooms, Messages, Uploads, Integrations, Users } from '../../../models/server';
import { hasPermission, canAccessRoom } from '../../../authorization/server';
import { composeMessageObjectWithUser } from '../../../utils/server';
import { normalizeMessagesForUser } from '../../../utils/server/lib/normalizeMessagesForUser';
import { API } from '../api';
@ -528,7 +528,7 @@ API.v1.addRoute('groups.messages', { authRequired: true }, {
}).fetch();
return API.v1.success({
messages: messages.map((message) => composeMessageObjectWithUser(message, this.userId)),
messages: normalizeMessagesForUser(messages, this.userId),
count: messages.length,
offset,
total: Messages.find(ourQuery).count(),

@ -2,7 +2,7 @@ import { Meteor } from 'meteor/meteor';
import { getRoomByNameOrIdWithOptionToJoin } from '../../../lib';
import { Subscriptions, Uploads, Users, Messages, Rooms } from '../../../models';
import { hasPermission } from '../../../authorization';
import { composeMessageObjectWithUser } from '../../../utils';
import { normalizeMessagesForUser } from '../../../utils/server/lib/normalizeMessagesForUser';
import { settings } from '../../../settings';
import { API } from '../api';
@ -234,7 +234,7 @@ API.v1.addRoute(['dm.messages', 'im.messages'], { authRequired: true }, {
}).fetch();
return API.v1.success({
messages: messages.map((message) => composeMessageObjectWithUser(message, this.userId)),
messages: normalizeMessagesForUser(messages, this.userId),
count: messages.length,
offset,
total: Messages.find(ourQuery).count(),
@ -274,7 +274,7 @@ API.v1.addRoute(['dm.messages.others', 'im.messages.others'], { authRequired: tr
}).fetch();
return API.v1.success({
messages: msgs.map((message) => composeMessageObjectWithUser(message, this.userId)),
messages: normalizeMessagesForUser(msgs, this.userId),
offset,
count: msgs.length,
total: Messages.find(ourQuery).count(),

@ -282,7 +282,7 @@ API.v1.addRoute('users.setAvatar', { authRequired: true }, {
return Users.findOneById(fields.userId, { _id: 1 });
}
if (fields.username) {
return Users.findOneByUsername(fields.username, { _id: 1 });
return Users.findOneByUsernameIgnoringCase(fields.username, { _id: 1 });
}
};

@ -27,7 +27,7 @@ Meteor.methods({
});
}
const user = Users.findOneByUsername(username, {
const user = Users.findOneByUsernameIgnoringCase(username, {
fields: {
_id: 1,
},

@ -17,6 +17,7 @@ Meteor.startup(function() {
context: [
'message',
'message-mobile',
'threads',
],
action() {
const { msg: message } = messageArgs(this);
@ -41,6 +42,7 @@ Meteor.startup(function() {
context: [
'message',
'message-mobile',
'threads',
],
action() {
const { msg: message } = messageArgs(this);

@ -41,7 +41,7 @@ Meteor.methods({
const missing = [];
if (data.to_users.length > 0) {
_.each(data.to_users, (username) => {
const user = Users.findOneByUsername(username);
const user = Users.findOneByUsernameIgnoringCase(username);
if (user && user.emails && user.emails[0] && user.emails[0].address) {
emails.push(user.emails[0].address);
} else {

@ -29,8 +29,8 @@ const common = {
return roomType && roomTypes.roomTypes[roomType].canBeDeleted(hasPermission, room);
},
canEditRoom() {
const { _id, prid } = Template.instance().room;
return !prid && hasAllPermission('edit-room', _id);
const { _id } = Template.instance().room;
return hasAllPermission('edit-room', _id);
},
isDirectMessage() {
const { room: { t } } = Template.instance();

@ -27,6 +27,6 @@ export const saveRoomName = function(rid, displayName, user, sendMessage = true)
if (sendMessage) {
Messages.createRoomRenamedWithRoomIdRoomNameAndUser(rid, displayName, user);
}
callbacks.run('afterRoomNameChange', { rid, name: displayName });
callbacks.run('afterRoomNameChange', { rid, name: displayName, oldName: room.name });
return displayName;
};

@ -201,7 +201,7 @@ export class CROWD {
const response = self.crowdClient.searchSync('user', `email=" ${ email } "`);
if (!response || response.users.length === 0) {
logger.warning('Could not find user in CROWD with username or email:', crowd_username, email);
logger.warn('Could not find user in CROWD with username or email:', crowd_username, email);
return;
}
crowd_username = response.users[0].name;

@ -345,7 +345,7 @@ export class CustomOAuth {
}
if (serviceData.username) {
const user = Users.findOneByUsername(serviceData.username);
const user = Users.findOneByUsernameIgnoringCase(serviceData.username);
if (!user) {
return;
}

@ -1,7 +1,7 @@
<template name="DiscussionList">
{{#if rooms}}
<h3 class="rooms-list__type">
{{_ "Discussion"}}
{{_ "Discussions"}}
</h3>
<ul class="rooms-list__list">
{{#each room in rooms}} {{> chatRoomItem room }} {{/each}}

@ -26,7 +26,7 @@ callbacks.add('afterDeleteMessage', function(message, { _id, prid } = {}) {
callbacks.add('afterDeleteRoom', (rid) => Rooms.find({ prid: rid }, { fields: { _id: 1 } }).forEach(({ _id }) => deleteRoom(_id)), 'DeleteDiscussionChain');
// TODO discussions define new fields
callbacks.add('afterRoomNameChange', ({ rid, name }) => Rooms.update({ prid: rid }, { $set: { topic: name } }, { multi: true }));
callbacks.add('afterRoomNameChange', ({ rid, name, oldName }) => Rooms.update({ prid: rid, ...(oldName && { topic: oldName }) }, { $set: { topic: name } }, { multi: true }), 'updateTopicDiscussion');
callbacks.add('afterDeleteRoom', (drid) => Messages.update({ drid }, {
$unset: {

@ -92,6 +92,9 @@ const create = ({ prid, pmid, t_name, reply, users }) => {
description: message.msg, // TODO discussions remove
topic: p_room.name, // TODO discussions remove
prid,
}, {
// overrides name validation to allow anything, because discussion's name is randomly generated
nameValidationRegex: /.*/,
});
if (pmid) {

@ -11,7 +11,7 @@ const config = {
forLoggedInUser: ['services.gitlab'],
forOtherUsers: ['services.gitlab.username'],
},
accessTokenParam: 'private_token',
accessTokenParam: 'access_token',
};
const Gitlab = new CustomOAuth('gitlab', config);

@ -185,7 +185,7 @@ export class CsvImporter extends Base {
// If we couldn't find one by their email address, try to find an existing user by their username
if (!existantUser) {
existantUser = Users.findOneByUsername(u.username);
existantUser = Users.findOneByUsernameIgnoringCase(u.username);
}
if (existantUser) {
@ -271,7 +271,7 @@ export class CsvImporter extends Base {
for (const msgs of messagesMap.values()) {
for (const msg of msgs.messages) {
if (!this.getUserFromUsername(msg.username)) {
const user = Users.findOneByUsername(msg.username);
const user = Users.findOneByUsernameIgnoringCase(msg.username);
if (user) {
this.users.users.push({
rocketId: user._id,

@ -710,7 +710,7 @@ export class HipChatEnterpriseImporter extends Base {
_importUser(userToImport, startedByUserId) {
Meteor.runAsUser(startedByUserId, () => {
let existingUser = Users.findOneByUsername(userToImport.username);
let existingUser = Users.findOneByUsernameIgnoringCase(userToImport.username);
if (!existingUser) {
// If there's no user with that username, but there's an imported user with the same original ID and no username, use that
existingUser = Users.findOne({

@ -94,7 +94,7 @@ export class SlackUsersImporter extends Base {
}
Meteor.runAsUser(startedByUserId, () => {
const existantUser = Users.findOneByEmailAddress(u.email) || Users.findOneByUsername(u.username);
const existantUser = Users.findOneByEmailAddress(u.email) || Users.findOneByUsernameIgnoringCase(u.username);
let userId;
if (existantUser) {

@ -163,7 +163,7 @@ export class SlackImporter extends Base {
}
Meteor.runAsUser(startedByUserId, () => {
const existantUser = Users.findOneByEmailAddress(user.profile.email) || Users.findOneByUsername(user.name);
const existantUser = Users.findOneByEmailAddress(user.profile.email) || Users.findOneByUsernameIgnoringCase(user.name);
if (existantUser) {
user.rocketId = existantUser._id;
Users.update({ _id: user.rocketId }, { $addToSet: { importIds: user.id } });

@ -166,13 +166,13 @@ integrations.triggerHandler = new class RocketChatIntegrationHandler {
let user;
// Try to find the user who we are impersonating
if (trigger.impersonateUser) {
user = Models.Users.findOneByUsername(data.user_name);
user = Models.Users.findOneByUsernameIgnoringCase(data.user_name);
}
// If they don't exist (aka the trigger didn't contain a user) then we set the user based upon the
// configured username for the integration since this is required at all times.
if (!user) {
user = Models.Users.findOneByUsername(trigger.username);
user = Models.Users.findOneByUsernameIgnoringCase(trigger.username);
}
let tmpRoom;

@ -66,7 +66,7 @@ export const createRoom = function(type, name, owner, members, readOnly, extraDa
throw new Meteor.Error('error-invalid-name', 'Invalid name', { function: 'RocketChat.createRoom' });
}
owner = Users.findOneByUsername(owner, { fields: { username: 1 } });
owner = Users.findOneByUsernameIgnoringCase(owner, { fields: { username: 1 } });
if (!owner) {
throw new Meteor.Error('error-invalid-user', 'Invalid user', { function: 'RocketChat.createRoom' });
}

@ -15,7 +15,7 @@ function usernameIsAvaliable(username) {
return false;
}
return !Users.findOneByUsername(username);
return !Users.findOneByUsernameIgnoringCase(username);
}

@ -1,6 +1,6 @@
import { settings } from '../../../settings';
import { Messages } from '../../../models';
import { composeMessageObjectWithUser } from '../../../utils';
import { normalizeMessagesForUser } from '../../../utils/server/lib/normalizeMessagesForUser';
const hideMessagesOfType = [];
@ -41,7 +41,7 @@ export const loadMessageHistory = function loadMessageHistory({ userId, rid, end
} else {
records = Messages.findVisibleByRoomIdNotContainingTypes(rid, hideMessagesOfType, options).fetch();
}
const messages = records.map((record) => composeMessageObjectWithUser(record, userId));
const messages = normalizeMessagesForUser(records, userId);
let unreadNotLoaded = 0;
let firstUnread;

@ -59,7 +59,7 @@ Meteor.methods({
// Validate each user, then add to room
const user = Meteor.user();
data.users.forEach((username) => {
const newUser = Users.findOneByUsername(username);
const newUser = Users.findOneByUsernameIgnoringCase(username);
if (!newUser) {
throw new Meteor.Error('error-invalid-username', 'Invalid username', {
method: 'addUsersToRoom',

@ -3,7 +3,7 @@ import { check } from 'meteor/check';
import { hasPermission } from '../../../authorization';
import { Subscriptions, Messages } from '../../../models';
import { settings } from '../../../settings';
import { composeMessageObjectWithUser } from '../../../utils';
import { normalizeMessagesForUser } from '../../../utils/server/lib/normalizeMessagesForUser';
import _ from 'underscore';
Meteor.methods({
@ -58,7 +58,7 @@ Meteor.methods({
records = Messages.findVisibleByRoomIdBetweenTimestamps(rid, oldest, latest, options).fetch();
}
const messages = records.map((record) => composeMessageObjectWithUser(record, fromUserId));
const messages = normalizeMessagesForUser(records, fromUserId);
if (unreads) {
let unreadNotLoaded = 0;

@ -13,7 +13,7 @@ Meteor.methods({
throw new Meteor.Error('error-invalid-arguments', 'Invalid arguments', { method: 'livechat:searchAgent' });
}
const user = Users.findOneByUsername(username, { fields: { _id: 1, username: 1 } });
const user = Users.findOneByUsernameIgnoringCase(username, { fields: { _id: 1, username: 1 } });
if (!user) {
throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'livechat:searchAgent' });

@ -9,7 +9,7 @@
<div class="flex-tab__result js-list">
<ul class="mentioned-messages-list list clearfix">
{{# with messageContext}}
{{#each msg in messages}}{{#nrr nrrargs 'message' msg=msg room=room subscription=subscription settings=settings u=u}}{{/nrr}}{{/each}}
{{#each msg in messages}}{{#nrr nrrargs 'message' msg=msg room=room subscription=subscription settings=settings u=u customClass="mentions" context="mentions"}}{{/nrr}}{{/each}}
{{/with}}
</ul>
{{#if hasMore}}

@ -10,9 +10,6 @@ Template.mentionsFlexTab.helpers({
messages() {
return Template.instance().cursor;
},
message() {
return _.extend(this, { customClass: 'mentions', actionContext: 'mentions' });
},
hasMore() {
return Template.instance().hasMore.get();
},

@ -72,8 +72,8 @@ Template.messageAttachment.helpers({
return this.type === 'file';
},
isPDF() {
if (this.type === 'file' && this.title_link.endsWith('.pdf') && Template.parentData().file) {
this.fileId = Template.parentData().file._id;
if (this.type === 'file' && this.title_link.endsWith('.pdf') && Template.parentData().msg.file) {
this.fileId = Template.parentData().msg.file._id;
return true;
}
return false;

@ -12,7 +12,7 @@ Meteor.startup(function() {
id: 'star-message',
icon: 'star',
label: 'Star',
context: ['starred', 'message', 'message-mobile'],
context: ['starred', 'message', 'message-mobile', 'threads'],
action() {
const { msg: message } = messageArgs(this);
message.starred = Meteor.userId();
@ -37,7 +37,7 @@ Meteor.startup(function() {
id: 'unstar-message',
icon: 'star',
label: 'Unstar_Message',
context: ['starred', 'message', 'message-mobile'],
context: ['starred', 'message', 'message-mobile', 'threads'],
action() {
const { msg: message } = messageArgs(this);
message.starred = false;
@ -62,7 +62,7 @@ Meteor.startup(function() {
id: 'jump-to-star-message',
icon: 'jump',
label: 'Jump_to_message',
context: ['starred'],
context: ['starred', 'threads'],
action() {
const { msg: message } = messageArgs(this);
if (window.matchMedia('(max-width: 500px)').matches) {
@ -85,7 +85,7 @@ Meteor.startup(function() {
icon: 'permalink',
label: 'Get_link',
classes: 'clipboard',
context: ['starred'],
context: ['starred', 'threads'],
async action(event) {
const { msg: message } = messageArgs(this);
$(event.currentTarget).attr('data-clipboard-text', await MessageAction.getPermaLink(message._id));

@ -1,5 +1,6 @@
import { Meteor } from 'meteor/meteor';
import { Accounts } from 'meteor/accounts-base';
import { Random } from 'meteor/random';
import { ServiceConfiguration } from 'meteor/service-configuration';
if (!Accounts.saml) {
@ -94,12 +95,14 @@ Accounts.saml.initiateLogin = function(options, callback, dimensions) {
Meteor.loginWithSaml = function(options, callback) {
options = options || {};
options.credentialToken = Meteor.default_connection._lastSessionId;
const credentialToken = `id-${ Random.id() }`;
options.credentialToken = credentialToken;
Accounts.saml.initiateLogin(options, function(/* error, result*/) {
Accounts.callLoginMethod({
methodArguments: [{
saml: true,
credentialToken,
}],
userCallback: callback,
});

@ -92,11 +92,11 @@ Meteor.methods({
});
Accounts.registerLoginHandler(function(loginRequest) {
if (!loginRequest.saml) {
if (!loginRequest.saml || !loginRequest.credentialToken) {
return undefined;
}
const loginResult = Accounts.saml.retrieveCredential(this.connection.id);
const loginResult = Accounts.saml.retrieveCredential(loginRequest.credentialToken);
if (Accounts.saml.settings.debug) {
console.log(`RESULT :${ JSON.stringify(loginResult) }`);
}

@ -1,5 +1,374 @@
import { Base } from './_Base';
export const aggregates = {
dailySessionsOfYesterday(collection, { year, month, day }) {
return collection.aggregate([{
$match: {
userId: { $exists: true },
lastActivityAt: { $exists: true },
device: { $exists: true },
type: 'session',
$or: [{
year: { $lt: year },
}, {
year,
month: { $lt: month },
}, {
year,
month,
day: { $lte: day },
}],
},
}, {
$sort: {
_id: 1,
},
}, {
$project: {
userId: 1,
device: 1,
day: 1,
month: 1,
year: 1,
time: { $trunc: { $divide: [{ $subtract: ['$lastActivityAt', '$loginAt'] }, 1000] } },
},
}, {
$match: {
time: { $gt: 0 },
},
}, {
$group: {
_id: {
userId: '$userId',
device: '$device',
day: '$day',
month: '$month',
year: '$year',
},
time: { $sum: '$time' },
sessions: { $sum: 1 },
},
}, {
$group: {
_id: {
userId: '$_id.userId',
day: '$_id.day',
month: '$_id.month',
year: '$_id.year',
},
time: { $sum: '$time' },
sessions: { $sum: '$sessions' },
devices: {
$push: {
sessions: '$sessions',
time: '$time',
device: '$_id.device',
},
},
},
}, {
$project: {
_id: 0,
type: { $literal: 'user_daily' },
_computedAt: { $literal: new Date() },
day: '$_id.day',
month: '$_id.month',
year: '$_id.year',
userId: '$_id.userId',
time: 1,
sessions: 1,
devices: 1,
},
}], { allowDiskUse: true });
},
getUniqueUsersOfYesterday(collection, { year, month, day }) {
return collection.aggregate([{
$match: {
year,
month,
day,
type: 'user_daily',
},
}, {
$group: {
_id: {
day: '$day',
month: '$month',
year: '$year',
},
count: {
$sum: 1,
},
sessions: {
$sum: '$sessions',
},
time: {
$sum: '$time',
},
},
}, {
$project: {
_id: 0,
count: 1,
sessions: 1,
time: 1,
},
}]).toArray();
},
getUniqueUsersOfLastMonth(collection, { year, month, day }) {
return collection.aggregate([{
$match: {
type: 'user_daily',
...aggregates.getMatchOfLastMonthToday({ year, month, day }),
},
}, {
$group: {
_id: {
userId: '$userId',
},
sessions: {
$sum: '$sessions',
},
time: {
$sum: '$time',
},
},
}, {
$group: {
_id: 1,
count: {
$sum: 1,
},
sessions: {
$sum: '$sessions',
},
time: {
$sum: '$time',
},
},
}, {
$project: {
_id: 0,
count: 1,
sessions: 1,
time: 1,
},
}], { allowDiskUse: true }).toArray();
},
getMatchOfLastMonthToday({ year, month, day }) {
const pastMonthLastDay = (new Date(year, month - 1, 0)).getDate();
const currMonthLastDay = (new Date(year, month, 0)).getDate();
const lastMonthToday = new Date(year, month - 1, day);
lastMonthToday.setMonth(lastMonthToday.getMonth() - 1, (currMonthLastDay === day ? pastMonthLastDay : Math.min(pastMonthLastDay, day)) + 1);
const lastMonthTodayObject = {
year: lastMonthToday.getFullYear(),
month: lastMonthToday.getMonth() + 1,
day: lastMonthToday.getDate(),
};
if (year === lastMonthTodayObject.year && month === lastMonthTodayObject.month) {
return {
year,
month,
day: { $gte: lastMonthTodayObject.day, $lte: day },
};
}
if (year === lastMonthTodayObject.year) {
return {
year,
$and: [{
$or: [{
month: { $gt: lastMonthTodayObject.month },
}, {
month: lastMonthTodayObject.month,
day: { $gte: lastMonthTodayObject.day },
}],
}, {
$or: [{
month: { $lt: month },
}, {
month,
day: { $lte: day },
}],
}],
};
}
return {
$and: [{
$or: [{
year: { $gt: lastMonthTodayObject.year },
}, {
year: lastMonthTodayObject.year,
month: { $gt: lastMonthTodayObject.month },
}, {
year: lastMonthTodayObject.year,
month: lastMonthTodayObject.month,
day: { $gte: lastMonthTodayObject.day },
}],
}, {
$or: [{
year: { $lt: year },
}, {
year,
month: { $lt: month },
}, {
year,
month,
day: { $lte: day },
}],
}],
};
},
getUniqueDevicesOfLastMonth(collection, { year, month, day }) {
return collection.aggregate([{
$match: {
type: 'user_daily',
...aggregates.getMatchOfLastMonthToday({ year, month, day }),
},
}, {
$unwind: '$devices',
}, {
$group: {
_id: {
type : '$devices.device.type',
name : '$devices.device.name',
version : '$devices.device.version',
},
count: {
$sum: '$devices.sessions',
},
time: {
$sum: '$devices.time',
},
},
}, {
$project: {
_id: 0,
type: '$_id.type',
name: '$_id.name',
version: '$_id.version',
count: 1,
time: 1,
},
}], { allowDiskUse: true }).toArray();
},
getUniqueDevicesOfYesterday(collection, { year, month, day }) {
return collection.aggregate([{
$match: {
year,
month,
day,
type: 'user_daily',
},
}, {
$unwind: '$devices',
}, {
$group: {
_id: {
type : '$devices.device.type',
name : '$devices.device.name',
version : '$devices.device.version',
},
count: {
$sum: '$devices.sessions',
},
time: {
$sum: '$devices.time',
},
},
}, {
$project: {
_id: 0,
type: '$_id.type',
name: '$_id.name',
version: '$_id.version',
count: 1,
time: 1,
},
}]).toArray();
},
getUniqueOSOfLastMonth(collection, { year, month, day }) {
return collection.aggregate([{
$match: {
type: 'user_daily',
'devices.device.os.name': {
$exists: true,
},
...aggregates.getMatchOfLastMonthToday({ year, month, day }),
},
}, {
$unwind: '$devices',
}, {
$group: {
_id: {
name : '$devices.device.os.name',
version : '$devices.device.os.version',
},
count: {
$sum: '$devices.sessions',
},
time: {
$sum: '$devices.time',
},
},
}, {
$project: {
_id: 0,
name: '$_id.name',
version: '$_id.version',
count: 1,
time: 1,
},
}], { allowDiskUse: true }).toArray();
},
getUniqueOSOfYesterday(collection, { year, month, day }) {
return collection.aggregate([{
$match: {
year,
month,
day,
type: 'user_daily',
'devices.device.os.name': {
$exists: true,
},
},
}, {
$unwind: '$devices',
}, {
$group: {
_id: {
name : '$devices.device.os.name',
version : '$devices.device.os.version',
},
count: {
$sum: '$devices.sessions',
},
time: {
$sum: '$devices.time',
},
},
}, {
$project: {
_id: 0,
name: '$_id.name',
version: '$_id.version',
count: 1,
time: 1,
},
}]).toArray();
},
};
export class Sessions extends Base {
constructor(...args) {
super(...args);
@ -8,6 +377,7 @@ export class Sessions extends Base {
this.tryEnsureIndex({ instanceId: 1, sessionId: 1, userId: 1 });
this.tryEnsureIndex({ instanceId: 1, sessionId: 1 });
this.tryEnsureIndex({ year: 1, month: 1, day: 1, type: 1 });
this.tryEnsureIndex({ type: 1 });
this.tryEnsureIndex({ _computedAt: 1 }, { expireAfterSeconds: 60 * 60 * 24 * 45 });
}
@ -23,34 +393,7 @@ export class Sessions extends Base {
year,
month,
day,
data: Promise.await(this.model.rawCollection().aggregate([{
$match: {
year,
month,
day,
type: 'user_daily',
},
}, {
$group: {
_id: {
day: '$day',
month: '$month',
year: '$year',
},
count: {
$sum: '$count',
},
time: {
$sum: '$time',
},
},
}, {
$project: {
_id: 0,
count: 1,
time: 1,
},
}]).toArray()),
data: Promise.await(aggregates.getUniqueUsersOfYesterday(this.model.rawCollection(), { year, month, day })),
};
}
@ -60,36 +403,13 @@ export class Sessions extends Base {
const year = date.getFullYear();
const month = date.getMonth() + 1;
const day = date.getDate();
return {
year,
month,
data: Promise.await(this.model.rawCollection().aggregate([{
$match: {
year,
month,
type: 'user_daily',
},
}, {
$group: {
_id: {
month: '$month',
year: '$year',
},
count: {
$sum: '$count',
},
time: {
$sum: '$time',
},
},
}, {
$project: {
_id: 0,
count: 1,
time: 1,
},
}]).toArray()),
day,
data: Promise.await(aggregates.getUniqueUsersOfLastMonth(this.model.rawCollection(), { year, month, day })),
};
}
@ -105,35 +425,23 @@ export class Sessions extends Base {
year,
month,
day,
data: Promise.await(this.model.rawCollection().aggregate([{
$match: {
year,
month,
day,
type: 'user_daily',
},
}, {
$unwind: '$devices',
}, {
$group: {
_id: {
type : '$devices.type',
name : '$devices.name',
version : '$devices.version',
},
count: {
$sum: '$count',
},
},
}, {
$project: {
_id: 0,
type: '$_id.type',
name: '$_id.name',
version: '$_id.version',
count: 1,
},
}]).toArray()),
data: Promise.await(aggregates.getUniqueDevicesOfYesterday(this.model.rawCollection(), { year, month, day })),
};
}
getUniqueDevicesOfLastMonth() {
const date = new Date();
date.setDate(date.getDate() - 1);
const year = date.getFullYear();
const month = date.getMonth() + 1;
const day = date.getDate();
return {
year,
month,
day,
data: Promise.await(aggregates.getUniqueDevicesOfLastMonth(this.model.rawCollection(), { year, month, day })),
};
}
@ -149,36 +457,23 @@ export class Sessions extends Base {
year,
month,
day,
data: Promise.await(this.model.rawCollection().aggregate([{
$match: {
year,
month,
day,
type: 'user_daily',
'devices.os.name': {
$exists: true,
},
},
}, {
$unwind: '$devices',
}, {
$group: {
_id: {
name : '$devices.os.name',
version : '$devices.os.version',
},
count: {
$sum: '$count',
},
},
}, {
$project: {
_id: 0,
name: '$_id.name',
version: '$_id.version',
count: 1,
},
}]).toArray()),
data: Promise.await(aggregates.getUniqueOSOfYesterday(this.model.rawCollection(), { year, month, day })),
};
}
getUniqueOSOfLastMonth() {
const date = new Date();
date.setDate(date.getDate() - 1);
const year = date.getFullYear();
const month = date.getMonth() + 1;
const day = date.getDate();
return {
year,
month,
day,
data: Promise.await(aggregates.getUniqueOSOfLastMonth(this.model.rawCollection(), { year, month, day })),
};
}

@ -0,0 +1,7 @@
import mock from 'mock-require';
mock('./_Base', {
Base: class Base {
tryEnsureIndex() {}
},
});

@ -0,0 +1,821 @@
/* eslint-env mocha */
import assert from 'assert';
import './Sessions.mocks.js';
const mongoUnit = require('mongo-unit');
const { MongoClient } = require('mongodb');
const { aggregates } = require('./Sessions');
const sessions_dates = [];
const baseDate = new Date(2018, 6, 1);
for (let index = 0; index < 365; index++) {
sessions_dates.push({
_id: `${ baseDate.getFullYear() }-${ baseDate.getMonth() + 1 }-${ baseDate.getDate() }`,
year: baseDate.getFullYear(),
month: baseDate.getMonth() + 1,
day: baseDate.getDate(),
});
baseDate.setDate(baseDate.getDate() + 1);
}
const DATA = {
sessions: [{
_id : 'fNFyFcjszvoN6Grip2',
day : 30,
instanceId : 'HvbqxukP8E65LAGMY',
month : 4,
sessionId : 'kiA4xX33AyzPgpBNs2',
year : 2019,
_updatedAt : new Date('2019-04-30T16:33:24.311Z'),
createdAt : new Date('2019-04-30T00:11:34.047Z'),
device : {
type : 'browser',
name : 'Firefox',
longVersion : '66.0.3',
os : {
name : 'Linux',
version : '12',
},
version : '66.0.3',
},
host : 'localhost:3000',
ip : '127.0.0.1',
loginAt : new Date('2019-04-30T00:11:34.047Z'),
type : 'session',
userId : 'xPZXw9xqM3kKshsse',
lastActivityAt : new Date('2019-04-30T00:16:20.349Z'),
closedAt : new Date('2019-04-30T00:16:20.349Z'),
}, {
_id : 'fNFyFcjszvoN6Grip',
day : 2,
instanceId : 'HvbqxukP8E65LAGMY',
month : 5,
sessionId : 'kiA4xX33AyzPgpBNs',
year : 2019,
_updatedAt : new Date('2019-05-06T16:33:24.311Z'),
createdAt : new Date('2019-05-03T00:11:34.047Z'),
device : {
type : 'browser',
name : 'Firefox',
longVersion : '66.0.3',
os : {
name : 'Linux',
version : '12',
},
version : '66.0.3',
},
host : 'localhost:3000',
ip : '127.0.0.1',
loginAt : new Date('2019-05-03T00:11:34.047Z'),
type : 'session',
userId : 'xPZXw9xqM3kKshsse',
lastActivityAt : new Date('2019-05-03T00:16:20.349Z'),
closedAt : new Date('2019-05-03T00:16:20.349Z'),
}, {
_id : 'oZMkfR3gFB6kuKDK2',
day : 2,
instanceId : 'HvbqxukP8E65LAGMY',
month : 5,
sessionId : 'i8uJFekr9np4x88kS',
year : 2019,
_updatedAt : new Date('2019-05-06T16:33:24.311Z'),
createdAt : new Date('2019-05-03T00:16:21.847Z'),
device : {
type : 'browser',
name : 'Chrome',
longVersion : '73.0.3683.103',
os : {
name : 'Mac OS',
version : '10.14.1',
},
version : '73.0.3683',
},
host : 'localhost:3000',
ip : '127.0.0.1',
loginAt : new Date('2019-05-03T00:16:21.846Z'),
type : 'session',
userId : 'xPZXw9xqM3kKshsse',
lastActivityAt : new Date('2019-05-03T00:17:21.081Z'),
closedAt : new Date('2019-05-03T00:17:21.081Z'),
}, {
_id : 'ABXKoXKTZpPpzLjKd',
day : 2,
instanceId : 'HvbqxukP8E65LAGMY',
month : 5,
sessionId : 'T8MB28cpx2ZjfEDXr',
year : 2019,
_updatedAt : new Date('2019-05-06T16:33:24.311Z'),
createdAt : new Date('2019-05-03T00:17:22.375Z'),
device : {
type : 'browser',
name : 'Chrome',
longVersion : '73.0.3683.103',
os : {
name : 'Mac OS',
version : '10.14.1',
},
version : '73.0.3683',
},
host : 'localhost:3000',
ip : '127.0.0.1',
loginAt : new Date('2019-05-03T00:17:22.375Z'),
type : 'session',
userId : 'xPZXw9xqM3kKshsse',
lastActivityAt : new Date('2019-05-03T01:48:31.695Z'),
closedAt : new Date('2019-05-03T01:48:31.695Z'),
}, {
_id : 's4ucvvcfBjnTEtYEb',
day : 2,
instanceId : 'HvbqxukP8E65LAGMY',
month : 5,
sessionId : '8mHbJJypgeRG27TYF',
year : 2019,
_updatedAt : new Date('2019-05-06T16:33:24.311Z'),
createdAt : new Date('2019-05-03T01:48:43.521Z'),
device : {
type : 'browser',
name : 'Chrome',
longVersion : '73.0.3683.103',
os : {
name : 'Mac OS',
version : '10.14.1',
},
version : '73.0.3683',
},
host : 'localhost:3000',
ip : '127.0.0.1',
loginAt : new Date('2019-05-03T01:48:43.521Z'),
type : 'session',
userId : 'xPZXw9xqM3kKshsse',
closedAt : new Date('2019-05-03T01:48:43.761Z'),
lastActivityAt : new Date('2019-05-03T01:48:43.761Z'),
}, {
_id : 'MDs9SzQKmwaDmXL8s',
day : 2,
instanceId : 'HvbqxukP8E65LAGMY',
month : 5,
sessionId : 'GmoBDPKy9RW2eXdCG',
year : 2019,
_updatedAt : new Date('2019-05-06T16:33:24.311Z'),
createdAt : new Date('2019-05-03T01:48:45.064Z'),
device : {
type : 'browser',
name : 'Chrome',
longVersion : '73.0.3683.103',
os : {
name : 'Mac OS',
version : '10.14.1',
},
version : '73.0.3683',
},
host : 'localhost:3000',
ip : '127.0.0.1',
loginAt : new Date('2019-05-03T01:48:45.064Z'),
type : 'session',
userId : 'xPZXw9xqM3kKshsse',
}, {
_id : 'CJwfxASo62FHDgqog',
day : 2,
instanceId : 'Nmwo2ttFeWZSrowNh',
month : 5,
sessionId : 'LMrrL4sbpNMLWYomA',
year : 2019,
_updatedAt : new Date('2019-05-06T16:33:24.311Z'),
createdAt : new Date('2019-05-03T01:50:31.098Z'),
device : {
type : 'browser',
name : 'Chrome',
longVersion : '73.0.3683.103',
os : {
name : 'Mac OS',
version : '10.14.1',
},
version : '73.0.3683',
},
host : 'localhost:3000',
ip : '127.0.0.1',
loginAt : new Date('2019-05-03T01:50:31.092Z'),
type : 'session',
userId : 'xPZXw9xqM3kKshsse',
closedAt : new Date('2019-05-03T01:50:31.355Z'),
lastActivityAt : new Date('2019-05-03T01:50:31.355Z'),
}, {
_id : 'iGAcPobWfTQtN6s4K',
day : 1,
instanceId : 'Nmwo2ttFeWZSrowNh',
month : 5,
sessionId : 'AsbjZRLNQMqfbyYFS',
year : 2019,
_updatedAt : new Date('2019-05-06T16:33:24.311Z'),
createdAt : new Date('2019-05-03T01:50:32.765Z'),
device : {
type : 'browser',
name : 'Chrome',
longVersion : '73.0.3683.103',
os : {
name : 'Mac OS',
version : '10.14.1',
},
version : '73.0.3683',
},
host : 'localhost:3000',
ip : '127.0.0.1',
loginAt : new Date('2019-05-03T01:50:32.765Z'),
type : 'session',
userId : 'xPZXw9xqM3kKshsse2',
lastActivityAt : new Date('2019-05-03T02:59:59.999Z'),
}],
sessions_dates,
}; // require('./fixtures/testData.json')
describe.only('Sessions Aggregates', () => {
let db;
if (!process.env.MONGO_URL) {
before(function() {
this.timeout(120000);
return mongoUnit.start({ version: '3.2.22' })
.then((testMongoUrl) => process.env.MONGO_URL = testMongoUrl);
});
after(() => { mongoUnit.stop(); });
}
before(function() {
return MongoClient.connect(process.env.MONGO_URL)
.then((client) => db = client.db('test'));
});
before(() => db.dropDatabase().then(() => {
const sessions = db.collection('sessions');
const sessions_dates = db.collection('sessions_dates');
return Promise.all([
sessions.insertMany(DATA.sessions),
sessions_dates.insertMany(DATA.sessions_dates),
]);
}));
after(() => { db.close(); });
it('should have sessions_dates data saved', () => {
const collection = db.collection('sessions_dates');
return collection.find().toArray()
.then((docs) => assert.equal(docs.length, DATA.sessions_dates.length));
});
it('should match sessions between 2018-12-11 and 2019-1-10', () => {
const collection = db.collection('sessions_dates');
const $match = aggregates.getMatchOfLastMonthToday({ year: 2019, month: 1, day: 10 });
assert.deepEqual($match, {
$and: [{
$or: [
{ year: { $gt: 2018 } },
{ year: 2018, month: { $gt: 12 } },
{ year: 2018, month: 12, day: { $gte: 11 } },
],
}, {
$or: [
{ year: { $lt: 2019 } },
{ year: 2019, month: { $lt: 1 } },
{ year: 2019, month: 1, day: { $lte: 10 } },
],
}],
});
return collection.aggregate([{
$match,
}]).toArray()
.then((docs) => {
assert.equal(docs.length, 31);
assert.deepEqual(docs, [
{ _id: '2018-12-11', year: 2018, month: 12, day: 11 },
{ _id: '2018-12-12', year: 2018, month: 12, day: 12 },
{ _id: '2018-12-13', year: 2018, month: 12, day: 13 },
{ _id: '2018-12-14', year: 2018, month: 12, day: 14 },
{ _id: '2018-12-15', year: 2018, month: 12, day: 15 },
{ _id: '2018-12-16', year: 2018, month: 12, day: 16 },
{ _id: '2018-12-17', year: 2018, month: 12, day: 17 },
{ _id: '2018-12-18', year: 2018, month: 12, day: 18 },
{ _id: '2018-12-19', year: 2018, month: 12, day: 19 },
{ _id: '2018-12-20', year: 2018, month: 12, day: 20 },
{ _id: '2018-12-21', year: 2018, month: 12, day: 21 },
{ _id: '2018-12-22', year: 2018, month: 12, day: 22 },
{ _id: '2018-12-23', year: 2018, month: 12, day: 23 },
{ _id: '2018-12-24', year: 2018, month: 12, day: 24 },
{ _id: '2018-12-25', year: 2018, month: 12, day: 25 },
{ _id: '2018-12-26', year: 2018, month: 12, day: 26 },
{ _id: '2018-12-27', year: 2018, month: 12, day: 27 },
{ _id: '2018-12-28', year: 2018, month: 12, day: 28 },
{ _id: '2018-12-29', year: 2018, month: 12, day: 29 },
{ _id: '2018-12-30', year: 2018, month: 12, day: 30 },
{ _id: '2018-12-31', year: 2018, month: 12, day: 31 },
{ _id: '2019-1-1', year: 2019, month: 1, day: 1 },
{ _id: '2019-1-2', year: 2019, month: 1, day: 2 },
{ _id: '2019-1-3', year: 2019, month: 1, day: 3 },
{ _id: '2019-1-4', year: 2019, month: 1, day: 4 },
{ _id: '2019-1-5', year: 2019, month: 1, day: 5 },
{ _id: '2019-1-6', year: 2019, month: 1, day: 6 },
{ _id: '2019-1-7', year: 2019, month: 1, day: 7 },
{ _id: '2019-1-8', year: 2019, month: 1, day: 8 },
{ _id: '2019-1-9', year: 2019, month: 1, day: 9 },
{ _id: '2019-1-10', year: 2019, month: 1, day: 10 },
]);
});
});
it('should match sessions between 2019-1-11 and 2019-2-10', () => {
const collection = db.collection('sessions_dates');
const $match = aggregates.getMatchOfLastMonthToday({ year: 2019, month: 2, day: 10 });
assert.deepEqual($match, {
year: 2019,
$and: [{
$or: [
{ month: { $gt: 1 } },
{ month: 1, day: { $gte: 11 } },
],
}, {
$or: [
{ month: { $lt: 2 } },
{ month: 2, day: { $lte: 10 } },
],
}],
});
return collection.aggregate([{
$match,
}]).toArray()
.then((docs) => {
assert.equal(docs.length, 31);
assert.deepEqual(docs, [
{ _id: '2019-1-11', year: 2019, month: 1, day: 11 },
{ _id: '2019-1-12', year: 2019, month: 1, day: 12 },
{ _id: '2019-1-13', year: 2019, month: 1, day: 13 },
{ _id: '2019-1-14', year: 2019, month: 1, day: 14 },
{ _id: '2019-1-15', year: 2019, month: 1, day: 15 },
{ _id: '2019-1-16', year: 2019, month: 1, day: 16 },
{ _id: '2019-1-17', year: 2019, month: 1, day: 17 },
{ _id: '2019-1-18', year: 2019, month: 1, day: 18 },
{ _id: '2019-1-19', year: 2019, month: 1, day: 19 },
{ _id: '2019-1-20', year: 2019, month: 1, day: 20 },
{ _id: '2019-1-21', year: 2019, month: 1, day: 21 },
{ _id: '2019-1-22', year: 2019, month: 1, day: 22 },
{ _id: '2019-1-23', year: 2019, month: 1, day: 23 },
{ _id: '2019-1-24', year: 2019, month: 1, day: 24 },
{ _id: '2019-1-25', year: 2019, month: 1, day: 25 },
{ _id: '2019-1-26', year: 2019, month: 1, day: 26 },
{ _id: '2019-1-27', year: 2019, month: 1, day: 27 },
{ _id: '2019-1-28', year: 2019, month: 1, day: 28 },
{ _id: '2019-1-29', year: 2019, month: 1, day: 29 },
{ _id: '2019-1-30', year: 2019, month: 1, day: 30 },
{ _id: '2019-1-31', year: 2019, month: 1, day: 31 },
{ _id: '2019-2-1', year: 2019, month: 2, day: 1 },
{ _id: '2019-2-2', year: 2019, month: 2, day: 2 },
{ _id: '2019-2-3', year: 2019, month: 2, day: 3 },
{ _id: '2019-2-4', year: 2019, month: 2, day: 4 },
{ _id: '2019-2-5', year: 2019, month: 2, day: 5 },
{ _id: '2019-2-6', year: 2019, month: 2, day: 6 },
{ _id: '2019-2-7', year: 2019, month: 2, day: 7 },
{ _id: '2019-2-8', year: 2019, month: 2, day: 8 },
{ _id: '2019-2-9', year: 2019, month: 2, day: 9 },
{ _id: '2019-2-10', year: 2019, month: 2, day: 10 },
]);
});
});
it('should match sessions between 2019-5-1 and 2019-5-31', () => {
const collection = db.collection('sessions_dates');
const $match = aggregates.getMatchOfLastMonthToday({ year: 2019, month: 5, day: 31 });
assert.deepEqual($match, {
year: 2019,
month: 5,
day: { $gte: 1, $lte: 31 },
});
return collection.aggregate([{
$match,
}]).toArray()
.then((docs) => {
assert.equal(docs.length, 31);
assert.deepEqual(docs, [
{ _id: '2019-5-1', year: 2019, month: 5, day: 1 },
{ _id: '2019-5-2', year: 2019, month: 5, day: 2 },
{ _id: '2019-5-3', year: 2019, month: 5, day: 3 },
{ _id: '2019-5-4', year: 2019, month: 5, day: 4 },
{ _id: '2019-5-5', year: 2019, month: 5, day: 5 },
{ _id: '2019-5-6', year: 2019, month: 5, day: 6 },
{ _id: '2019-5-7', year: 2019, month: 5, day: 7 },
{ _id: '2019-5-8', year: 2019, month: 5, day: 8 },
{ _id: '2019-5-9', year: 2019, month: 5, day: 9 },
{ _id: '2019-5-10', year: 2019, month: 5, day: 10 },
{ _id: '2019-5-11', year: 2019, month: 5, day: 11 },
{ _id: '2019-5-12', year: 2019, month: 5, day: 12 },
{ _id: '2019-5-13', year: 2019, month: 5, day: 13 },
{ _id: '2019-5-14', year: 2019, month: 5, day: 14 },
{ _id: '2019-5-15', year: 2019, month: 5, day: 15 },
{ _id: '2019-5-16', year: 2019, month: 5, day: 16 },
{ _id: '2019-5-17', year: 2019, month: 5, day: 17 },
{ _id: '2019-5-18', year: 2019, month: 5, day: 18 },
{ _id: '2019-5-19', year: 2019, month: 5, day: 19 },
{ _id: '2019-5-20', year: 2019, month: 5, day: 20 },
{ _id: '2019-5-21', year: 2019, month: 5, day: 21 },
{ _id: '2019-5-22', year: 2019, month: 5, day: 22 },
{ _id: '2019-5-23', year: 2019, month: 5, day: 23 },
{ _id: '2019-5-24', year: 2019, month: 5, day: 24 },
{ _id: '2019-5-25', year: 2019, month: 5, day: 25 },
{ _id: '2019-5-26', year: 2019, month: 5, day: 26 },
{ _id: '2019-5-27', year: 2019, month: 5, day: 27 },
{ _id: '2019-5-28', year: 2019, month: 5, day: 28 },
{ _id: '2019-5-29', year: 2019, month: 5, day: 29 },
{ _id: '2019-5-30', year: 2019, month: 5, day: 30 },
{ _id: '2019-5-31', year: 2019, month: 5, day: 31 },
]);
});
});
it('should match sessions between 2019-4-1 and 2019-4-30', () => {
const collection = db.collection('sessions_dates');
const $match = aggregates.getMatchOfLastMonthToday({ year: 2019, month: 4, day: 30 });
assert.deepEqual($match, {
year: 2019,
month: 4,
day: { $gte: 1, $lte: 30 },
});
return collection.aggregate([{
$match,
}]).toArray()
.then((docs) => {
assert.equal(docs.length, 30);
assert.deepEqual(docs, [
{ _id: '2019-4-1', year: 2019, month: 4, day: 1 },
{ _id: '2019-4-2', year: 2019, month: 4, day: 2 },
{ _id: '2019-4-3', year: 2019, month: 4, day: 3 },
{ _id: '2019-4-4', year: 2019, month: 4, day: 4 },
{ _id: '2019-4-5', year: 2019, month: 4, day: 5 },
{ _id: '2019-4-6', year: 2019, month: 4, day: 6 },
{ _id: '2019-4-7', year: 2019, month: 4, day: 7 },
{ _id: '2019-4-8', year: 2019, month: 4, day: 8 },
{ _id: '2019-4-9', year: 2019, month: 4, day: 9 },
{ _id: '2019-4-10', year: 2019, month: 4, day: 10 },
{ _id: '2019-4-11', year: 2019, month: 4, day: 11 },
{ _id: '2019-4-12', year: 2019, month: 4, day: 12 },
{ _id: '2019-4-13', year: 2019, month: 4, day: 13 },
{ _id: '2019-4-14', year: 2019, month: 4, day: 14 },
{ _id: '2019-4-15', year: 2019, month: 4, day: 15 },
{ _id: '2019-4-16', year: 2019, month: 4, day: 16 },
{ _id: '2019-4-17', year: 2019, month: 4, day: 17 },
{ _id: '2019-4-18', year: 2019, month: 4, day: 18 },
{ _id: '2019-4-19', year: 2019, month: 4, day: 19 },
{ _id: '2019-4-20', year: 2019, month: 4, day: 20 },
{ _id: '2019-4-21', year: 2019, month: 4, day: 21 },
{ _id: '2019-4-22', year: 2019, month: 4, day: 22 },
{ _id: '2019-4-23', year: 2019, month: 4, day: 23 },
{ _id: '2019-4-24', year: 2019, month: 4, day: 24 },
{ _id: '2019-4-25', year: 2019, month: 4, day: 25 },
{ _id: '2019-4-26', year: 2019, month: 4, day: 26 },
{ _id: '2019-4-27', year: 2019, month: 4, day: 27 },
{ _id: '2019-4-28', year: 2019, month: 4, day: 28 },
{ _id: '2019-4-29', year: 2019, month: 4, day: 29 },
{ _id: '2019-4-30', year: 2019, month: 4, day: 30 },
]);
});
});
it('should match sessions between 2019-2-1 and 2019-2-28', () => {
const collection = db.collection('sessions_dates');
const $match = aggregates.getMatchOfLastMonthToday({ year: 2019, month: 2, day: 28 });
assert.deepEqual($match, {
year: 2019,
month: 2,
day: { $gte: 1, $lte: 28 },
});
return collection.aggregate([{
$match,
}]).toArray()
.then((docs) => {
assert.equal(docs.length, 28);
assert.deepEqual(docs, [
{ _id: '2019-2-1', year: 2019, month: 2, day: 1 },
{ _id: '2019-2-2', year: 2019, month: 2, day: 2 },
{ _id: '2019-2-3', year: 2019, month: 2, day: 3 },
{ _id: '2019-2-4', year: 2019, month: 2, day: 4 },
{ _id: '2019-2-5', year: 2019, month: 2, day: 5 },
{ _id: '2019-2-6', year: 2019, month: 2, day: 6 },
{ _id: '2019-2-7', year: 2019, month: 2, day: 7 },
{ _id: '2019-2-8', year: 2019, month: 2, day: 8 },
{ _id: '2019-2-9', year: 2019, month: 2, day: 9 },
{ _id: '2019-2-10', year: 2019, month: 2, day: 10 },
{ _id: '2019-2-11', year: 2019, month: 2, day: 11 },
{ _id: '2019-2-12', year: 2019, month: 2, day: 12 },
{ _id: '2019-2-13', year: 2019, month: 2, day: 13 },
{ _id: '2019-2-14', year: 2019, month: 2, day: 14 },
{ _id: '2019-2-15', year: 2019, month: 2, day: 15 },
{ _id: '2019-2-16', year: 2019, month: 2, day: 16 },
{ _id: '2019-2-17', year: 2019, month: 2, day: 17 },
{ _id: '2019-2-18', year: 2019, month: 2, day: 18 },
{ _id: '2019-2-19', year: 2019, month: 2, day: 19 },
{ _id: '2019-2-20', year: 2019, month: 2, day: 20 },
{ _id: '2019-2-21', year: 2019, month: 2, day: 21 },
{ _id: '2019-2-22', year: 2019, month: 2, day: 22 },
{ _id: '2019-2-23', year: 2019, month: 2, day: 23 },
{ _id: '2019-2-24', year: 2019, month: 2, day: 24 },
{ _id: '2019-2-25', year: 2019, month: 2, day: 25 },
{ _id: '2019-2-26', year: 2019, month: 2, day: 26 },
{ _id: '2019-2-27', year: 2019, month: 2, day: 27 },
{ _id: '2019-2-28', year: 2019, month: 2, day: 28 },
]);
});
});
it('should match sessions between 2019-1-28 and 2019-2-27', () => {
const collection = db.collection('sessions_dates');
const $match = aggregates.getMatchOfLastMonthToday({ year: 2019, month: 2, day: 27 });
assert.deepEqual($match, {
year: 2019,
$and: [{
$or: [
{ month: { $gt: 1 } },
{ month: 1, day: { $gte: 28 } },
],
}, {
$or: [
{ month: { $lt: 2 } },
{ month: 2, day: { $lte: 27 } },
],
}],
});
return collection.aggregate([{
$match,
}]).toArray()
.then((docs) => {
assert.equal(docs.length, 31);
assert.deepEqual(docs, [
{ _id: '2019-1-28', year: 2019, month: 1, day: 28 },
{ _id: '2019-1-29', year: 2019, month: 1, day: 29 },
{ _id: '2019-1-30', year: 2019, month: 1, day: 30 },
{ _id: '2019-1-31', year: 2019, month: 1, day: 31 },
{ _id: '2019-2-1', year: 2019, month: 2, day: 1 },
{ _id: '2019-2-2', year: 2019, month: 2, day: 2 },
{ _id: '2019-2-3', year: 2019, month: 2, day: 3 },
{ _id: '2019-2-4', year: 2019, month: 2, day: 4 },
{ _id: '2019-2-5', year: 2019, month: 2, day: 5 },
{ _id: '2019-2-6', year: 2019, month: 2, day: 6 },
{ _id: '2019-2-7', year: 2019, month: 2, day: 7 },
{ _id: '2019-2-8', year: 2019, month: 2, day: 8 },
{ _id: '2019-2-9', year: 2019, month: 2, day: 9 },
{ _id: '2019-2-10', year: 2019, month: 2, day: 10 },
{ _id: '2019-2-11', year: 2019, month: 2, day: 11 },
{ _id: '2019-2-12', year: 2019, month: 2, day: 12 },
{ _id: '2019-2-13', year: 2019, month: 2, day: 13 },
{ _id: '2019-2-14', year: 2019, month: 2, day: 14 },
{ _id: '2019-2-15', year: 2019, month: 2, day: 15 },
{ _id: '2019-2-16', year: 2019, month: 2, day: 16 },
{ _id: '2019-2-17', year: 2019, month: 2, day: 17 },
{ _id: '2019-2-18', year: 2019, month: 2, day: 18 },
{ _id: '2019-2-19', year: 2019, month: 2, day: 19 },
{ _id: '2019-2-20', year: 2019, month: 2, day: 20 },
{ _id: '2019-2-21', year: 2019, month: 2, day: 21 },
{ _id: '2019-2-22', year: 2019, month: 2, day: 22 },
{ _id: '2019-2-23', year: 2019, month: 2, day: 23 },
{ _id: '2019-2-24', year: 2019, month: 2, day: 24 },
{ _id: '2019-2-25', year: 2019, month: 2, day: 25 },
{ _id: '2019-2-26', year: 2019, month: 2, day: 26 },
{ _id: '2019-2-27', year: 2019, month: 2, day: 27 },
]);
});
});
it('should have sessions data saved', () => {
const collection = db.collection('sessions');
return collection.find().toArray()
.then((docs) => assert.equal(docs.length, DATA.sessions.length));
});
it('should generate daily sessions', () => {
const collection = db.collection('sessions');
return aggregates.dailySessionsOfYesterday(collection, { year: 2019, month: 5, day: 2 }).toArray()
.then((docs) => {
docs.forEach((doc) => {
doc._id = `${ doc.userId }-${ doc.year }-${ doc.month }-${ doc.day }`;
});
assert.equal(docs.length, 3);
assert.deepEqual(docs, [{
_id: 'xPZXw9xqM3kKshsse-2019-5-2',
time: 5814,
sessions: 3,
devices: [{
sessions: 1,
time: 286,
device: {
type: 'browser',
name: 'Firefox',
longVersion: '66.0.3',
os: {
name: 'Linux',
version: '12',
},
version: '66.0.3',
},
}, {
sessions: 2,
time: 5528,
device: {
type: 'browser',
name: 'Chrome',
longVersion: '73.0.3683.103',
os: {
name: 'Mac OS',
version: '10.14.1',
},
version: '73.0.3683',
},
}],
type: 'user_daily',
_computedAt: docs[0]._computedAt,
day: 2,
month: 5,
year: 2019,
userId: 'xPZXw9xqM3kKshsse',
}, {
_id: 'xPZXw9xqM3kKshsse-2019-4-30',
day: 30,
devices: [{
device: {
longVersion: '66.0.3',
name: 'Firefox',
os: {
name: 'Linux',
version: '12',
},
type: 'browser',
version: '66.0.3',
},
sessions: 1,
time: 286,
}],
month: 4,
sessions: 1,
time: 286,
type: 'user_daily',
_computedAt: docs[1]._computedAt,
userId: 'xPZXw9xqM3kKshsse',
year: 2019,
}, {
_id: 'xPZXw9xqM3kKshsse2-2019-5-1',
time: 4167,
sessions: 1,
devices: [{
sessions: 1,
time: 4167,
device: {
type: 'browser',
name: 'Chrome',
longVersion: '73.0.3683.103',
os: {
name: 'Mac OS',
version: '10.14.1',
},
version: '73.0.3683',
},
}],
type: 'user_daily',
_computedAt: docs[2]._computedAt,
day: 1,
month: 5,
year: 2019,
userId: 'xPZXw9xqM3kKshsse2',
}]);
return collection.insertMany(docs);
});
});
it('should have 2 unique users for month 5 of 2019', () => {
const collection = db.collection('sessions');
return aggregates.getUniqueUsersOfLastMonth(collection, { year: 2019, month: 5, day: 31 })
.then((docs) => {
assert.equal(docs.length, 1);
assert.deepEqual(docs, [{
count: 2,
sessions: 4,
time: 9981,
}]);
});
});
it('should have 1 unique user for 1st of month 5 of 2019', () => {
const collection = db.collection('sessions');
return aggregates.getUniqueUsersOfYesterday(collection, { year: 2019, month: 5, day: 1 })
.then((docs) => {
assert.equal(docs.length, 1);
assert.deepEqual(docs, [{
count: 1,
sessions: 1,
time: 4167,
}]);
});
});
it('should have 1 unique user for 2nd of month 5 of 2019', () => {
const collection = db.collection('sessions');
return aggregates.getUniqueUsersOfYesterday(collection, { year: 2019, month: 5, day: 2 })
.then((docs) => {
assert.equal(docs.length, 1);
assert.deepEqual(docs, [{
count: 1,
sessions: 3,
time: 5814,
}]);
});
});
it('should have 2 unique devices for month 5 of 2019', () => {
const collection = db.collection('sessions');
return aggregates.getUniqueDevicesOfLastMonth(collection, { year: 2019, month: 5, day: 31 })
.then((docs) => {
assert.equal(docs.length, 2);
assert.deepEqual(docs, [{
count: 3,
time: 9695,
type: 'browser',
name: 'Chrome',
version: '73.0.3683',
}, {
count: 1,
time: 286,
type: 'browser',
name: 'Firefox',
version: '66.0.3',
}]);
});
});
it('should have 2 unique devices for 2nd of month 5 of 2019', () => {
const collection = db.collection('sessions');
return aggregates.getUniqueDevicesOfYesterday(collection, { year: 2019, month: 5, day: 2 })
.then((docs) => {
assert.equal(docs.length, 2);
assert.deepEqual(docs, [{
count: 2,
time: 5528,
type: 'browser',
name: 'Chrome',
version: '73.0.3683',
}, {
count: 1,
time: 286,
type: 'browser',
name: 'Firefox',
version: '66.0.3',
}]);
});
});
it('should have 2 unique OS for month 5 of 2019', () => {
const collection = db.collection('sessions');
return aggregates.getUniqueOSOfLastMonth(collection, { year: 2019, month: 5, day: 31 })
.then((docs) => {
assert.equal(docs.length, 2);
assert.deepEqual(docs, [{
count: 3,
time: 9695,
name: 'Mac OS',
version: '10.14.1',
}, {
count: 1,
time: 286,
name: 'Linux',
version: '12',
}]);
});
});
it('should have 2 unique OS for 2nd of month 5 of 2019', () => {
const collection = db.collection('sessions');
return aggregates.getUniqueOSOfYesterday(collection, { year: 2019, month: 5, day: 2 })
.then((docs) => {
assert.equal(docs.length, 2);
assert.deepEqual(docs, [{
count: 2,
time: 5528,
name: 'Mac OS',
version: '10.14.1',
}, {
count: 1,
time: 286,
name: 'Linux',
version: '12',
}]);
});
});
});

@ -364,9 +364,9 @@ export class Users extends Base {
return this.findOne({ importIds: _id }, options);
}
findOneByUsername(username, options) {
findOneByUsernameIgnoringCase(username, options) {
if (typeof username === 'string') {
username = new RegExp(`^${ username }$`, 'i');
username = new RegExp(`^${ s.escapeRegExp(username) }$`, 'i');
}
const query = { username };
@ -374,6 +374,12 @@ export class Users extends Base {
return this.findOne(query, options);
}
findOneByUsername(username, options) {
const query = { username };
return this.findOne(query, options);
}
findOneByEmailAddress(emailAddress, options) {
const query = { 'emails.address': new RegExp(`^${ s.escapeRegExp(emailAddress) }$`, 'i') };

@ -55,6 +55,7 @@ Meteor.startup(function() {
context: [
'message',
'message-mobile',
'threads',
],
action(event) {
event.stopPropagation();

@ -355,9 +355,9 @@ export default class RocketAdapter {
const email = (rocketUserData.profile && rocketUserData.profile.email) || '';
let existingRocketUser;
if (!isBot) {
existingRocketUser = Users.findOneByEmailAddress(email) || Users.findOneByUsername(rocketUserData.name);
existingRocketUser = Users.findOneByEmailAddress(email) || Users.findOneByUsernameIgnoringCase(rocketUserData.name);
} else {
existingRocketUser = Users.findOneByUsername(rocketUserData.name);
existingRocketUser = Users.findOneByUsernameIgnoringCase(rocketUserData.name);
}
if (existingRocketUser) {

@ -18,7 +18,7 @@ const Kick = function(command, params, { rid }) {
}
const userId = Meteor.userId();
const user = Meteor.users.findOne(userId);
const kickedUser = Users.findOneByUsername(username);
const kickedUser = Users.findOneByUsernameIgnoringCase(username);
if (kickedUser == null) {
return Notifications.notifyUser(userId, 'message', {

@ -28,7 +28,7 @@ function Msg(command, params, item) {
const message = trimmedParams.slice(separator + 1);
const targetUsernameOrig = trimmedParams.slice(0, separator);
const targetUsername = targetUsernameOrig.replace('@', '');
const targetUser = Users.findOneByUsername(targetUsername);
const targetUser = Users.findOneByUsernameIgnoringCase(targetUsername);
if (targetUser == null) {
Notifications.notifyUser(Meteor.userId(), 'message', {
_id: Random.id(),

@ -20,7 +20,7 @@ slashCommands.add('mute', function Mute(command, params, item) {
}
const userId = Meteor.userId();
const user = Meteor.users.findOne(userId);
const mutedUser = Users.findOneByUsername(username);
const mutedUser = Users.findOneByUsernameIgnoringCase(username);
if (mutedUser == null) {
Notifications.notifyUser(userId, 'message', {
_id: Random.id(),

@ -20,7 +20,7 @@ slashCommands.add('unmute', function Unmute(command, params, item) {
return;
}
const user = Meteor.users.findOne(Meteor.userId());
const unmutedUser = Users.findOneByUsername(username);
const unmutedUser = Users.findOneByUsernameIgnoringCase(username);
if (unmutedUser == null) {
return Notifications.notifyUser(Meteor.userId(), 'message', {
_id: Random.id(),

@ -2,7 +2,6 @@ import _ from 'underscore';
import os from 'os';
import { Meteor } from 'meteor/meteor';
import { MongoInternals } from 'meteor/mongo';
import { InstanceStatus } from 'meteor/konecty:multiple-instances-status';
import {
@ -16,7 +15,7 @@ import {
LivechatVisitors,
} from '../../../models/server';
import { settings } from '../../../settings/server';
import { Info } from '../../../utils/server';
import { Info, getMongoInfo } from '../../../utils/server';
import { Migrations } from '../../../migrations/server';
import { statistics } from '../statisticsNamespace';
@ -136,35 +135,17 @@ statistics.get = function _getStatistics() {
statistics.migration = Migrations._getControl();
statistics.instanceCount = InstanceStatus.getCollection().find({ _updatedAt: { $gt: new Date(Date.now() - process.uptime() * 1000 - 2000) } }).count();
const { mongo } = MongoInternals.defaultRemoteCollectionDriver();
if (mongo._oplogHandle && mongo._oplogHandle.onOplogEntry) {
statistics.oplogEnabled = true;
}
try {
const { version, storageEngine } = Promise.await(mongo.db.command({ serverStatus: 1 }));
statistics.mongoVersion = version;
statistics.mongoStorageEngine = storageEngine.name;
} catch (e) {
console.error('=== Error getting MongoDB info ===');
console.error(e && e.toString());
console.error('----------------------------------');
console.error('Without mongodb version we can\'t ensure you are running a compatible version.');
console.error('If you are running your mongodb with auth enabled and an user different from admin');
console.error('you may need to grant permissions for this user to check cluster data.');
console.error('You can do it via mongo shell running the following command replacing');
console.error('the string YOUR_USER by the correct user\'s name:');
console.error('');
console.error(' db.runCommand({ grantRolesToUser: "YOUR_USER" , roles: [{role: "clusterMonitor", db: "admin"}]})');
console.error('');
console.error('==================================');
}
const { oplogEnabled, mongoVersion, mongoStorageEngine } = getMongoInfo();
statistics.oplogEnabled = oplogEnabled;
statistics.mongoVersion = mongoVersion;
statistics.mongoStorageEngine = mongoStorageEngine;
statistics.uniqueUsersOfYesterday = Sessions.getUniqueUsersOfYesterday();
statistics.uniqueUsersOfLastMonth = Sessions.getUniqueUsersOfLastMonth();
statistics.uniqueDevicesOfYesterday = Sessions.getUniqueDevicesOfYesterday();
statistics.uniqueDevicesOfLastMonth = Sessions.getUniqueDevicesOfLastMonth();
statistics.uniqueOSOfYesterday = Sessions.getUniqueOSOfYesterday();
statistics.uniqueOSOfLastMonth = Sessions.getUniqueOSOfLastMonth();
return statistics;
};

@ -1,10 +1,11 @@
import { Meteor } from 'meteor/meteor';
import { Accounts } from 'meteor/accounts-base';
import UAParser from 'ua-parser-js';
import { UAParserMobile } from './UAParserMobile';
import { Sessions } from '../../../models';
import { UAParserMobile, UAParserDesktop } from './UAParserCustom';
import { Sessions } from '../../../models/server';
import { Logger } from '../../../logger';
import { SyncedCron } from 'meteor/littledata:synced-cron';
import { aggregates } from '../../../models/server/models/Sessions';
const getDateObj = (dateTime = new Date()) => ({
day: dateTime.getDate(),
@ -199,6 +200,8 @@ export class SAUMonitorClass {
if (UAParserMobile.isMobileApp(uaString)) {
result = UAParserMobile.uaObject(uaString);
} else if (UAParserDesktop.isDesktopApp(uaString)) {
result = UAParserDesktop.uaObject(uaString);
} else {
const ua = new UAParser(uaString);
result = ua.getResult();
@ -224,7 +227,7 @@ export class SAUMonitorClass {
}
if (result.device && (result.device.type || result.device.model)) {
info.type = 'mobile-app';
info.type = result.device.type;
if (result.app && result.app.name) {
info.name = result.app.name;
@ -331,43 +334,7 @@ export class SAUMonitorClass {
day: { $lte: yesterday.day },
};
Sessions.model.rawCollection().aggregate([{
$match: {
userId: { $exists: true },
lastActivityAt: { $exists: true },
device: { $exists: true },
...match,
},
}, {
$group: {
_id: {
userId: '$userId',
day: '$day',
month: '$month',
year: '$year',
},
times: { $push: { $trunc: { $divide: [{ $subtract: ['$lastActivityAt', '$loginAt'] }, 1000] } } },
devices: { $addToSet: '$device' },
},
}, {
$project: {
_id: '$_id',
times: { $filter: { input: '$times', as: 'item', cond: { $gt: ['$$item', 0] } } },
devices: '$devices',
},
}, {
$project: {
type: 'user_daily',
_computedAt: new Date(),
day: '$_id.day',
month: '$_id.month',
year: '$_id.year',
userId: '$_id.userId',
time: { $sum: '$times' },
count: { $size: '$times' },
devices: '$devices',
},
}]).forEach(Meteor.bindEnvironment((record) => {
aggregates.dailySessionsOfYesterday(Sessions.model.rawCollection(), yesterday).forEach(Meteor.bindEnvironment((record) => {
record._id = `${ record.userId }-${ record.year }-${ record.month }-${ record.day }`;
Sessions.upsert({ _id: record._id }, record);
}));

@ -1,3 +1,5 @@
import UAParser from 'ua-parser-js';
const mergeDeep = ((target, source) => {
if (!(typeof target === 'object' && typeof source === 'object')) {
return target;
@ -20,9 +22,9 @@ const mergeDeep = ((target, source) => {
return target;
});
const UAParserMobile = {
export const UAParserMobile = {
appName: 'RC Mobile',
device: 'mobile',
device: 'mobile-app',
uaSeparator: ';',
props: {
os: {
@ -103,4 +105,37 @@ const UAParserMobile = {
},
};
export { UAParserMobile };
export const UAParserDesktop = {
device: 'desktop-app',
isDesktopApp(uaString) {
if (!uaString || typeof uaString !== 'string') {
return false;
}
return uaString.includes(' Electron/');
},
uaObject(uaString) {
if (!this.isDesktopApp(uaString)) {
return {};
}
const ua = new UAParser(uaString);
const uaParsed = ua.getResult();
const [, name, version] = uaString.match(/(Rocket\.Chat)\/(\d+(\.\d+)+)/) || [];
return {
device: {
type: this.device,
},
os: uaParsed.os,
app: {
name,
version,
},
};
},
};

@ -0,0 +1,77 @@
/* eslint-env mocha */
import { expect } from 'chai';
import { UAParserMobile, UAParserDesktop } from './UAParserCustom';
const UAMobile = 'RC Mobile; iOS 12.2; v3.4.0 (250)';
const UADesktop = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_1) AppleWebKit/537.36 (KHTML, like Gecko) Rocket.Chat/2.15.2 Chrome/69.0.3497.128 Electron/4.1.4 Safari/537.36';
const UAChrome = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.131 Safari/537.36';
describe('UAParserCustom', () => {
describe('UAParserMobile', () => {
it('should identify mobile UA', () => {
expect(UAParserMobile.isMobileApp(UAMobile)).to.be.true;
});
it('should not identify desktop UA', () => {
expect(UAParserMobile.isMobileApp(UADesktop)).to.be.false;
});
it('should not identify chrome UA', () => {
expect(UAParserMobile.isMobileApp(UAChrome)).to.be.false;
});
it('should parse mobile UA', () => {
expect(UAParserMobile.uaObject(UAMobile)).to.be.deep.equal({
device: {
type: 'mobile-app',
},
app: {
name: 'RC Mobile',
version: '3.4.0',
bundle: '250',
},
os: {
name: 'iOS',
version: '12.2',
},
});
});
});
describe('UAParserDesktop', () => {
it('should not identify mobile UA', () => {
expect(UAParserDesktop.isDesktopApp(UAMobile)).to.be.false;
});
it('should identify desktop UA', () => {
expect(UAParserDesktop.isDesktopApp(UADesktop)).to.be.true;
});
it('should not identify chrome UA', () => {
expect(UAParserDesktop.isDesktopApp(UAChrome)).to.be.false;
});
it('should parse desktop UA', () => {
expect(UAParserDesktop.uaObject(UADesktop)).to.be.deep.equal({
device: {
type: 'desktop-app',
},
app: {
name: 'Rocket.Chat',
version: '2.15.2',
},
os: {
name: 'Mac OS',
version: '10.14.1',
},
});
});
});
});

@ -140,6 +140,7 @@
@media (width <= 1100px) {
.contextual-bar {
position: absolute;
z-index: 10;
right: 0;
}

@ -5366,7 +5366,6 @@ rc-old select,
.room-leader .chat-now {
position: absolute;
top: 15px;
right: 25px;
width: 80px;
@ -5395,14 +5394,18 @@ rc-old select,
right: 0;
left: 0;
display: flex;
visibility: visible;
flex-direction: column;
height: 57px;
padding-bottom: 8px;
transition: transform 0.15s cubic-bezier(0.5, 0, 0.1, 1), visibility 0.15s cubic-bezier(0.5, 0, 0.1, 1);
border-bottom: 1px solid;
border-bottom: 1px solid var(--color-gray-lightest);
justify-content: center;
&.message:hover {
background-color: #ffffff;

@ -21,9 +21,9 @@
{{> loading}}
</li>
{{else}}
{{> nrr nrrargs 'message' groupable=false hideRoles=true msg=mainMessage room=room subscription=subscription settings=settings customClass="thread-message" templatePrefix='thread-' customClass="thread-main" u=u}}
{{> message groupable=false hideRoles=true msg=mainMessage room=room subscription=subscription settings=settings customClass="thread-message" templatePrefix='thread-' customClass="thread-main" u=u}}
{{#each msg in messages}}
{{> nrr nrrargs 'message' hideRoles=true msg=msg room=room subscription=subscription settings=settings templatePrefix='thread-' u=u}}
{{> message hideRoles=true msg=msg room=room subscription=subscription settings=settings templatePrefix='thread-' u=u context="threads"}}
{{/each}}
{{/if}}
{{/with}}

@ -51,7 +51,7 @@ Template.thread.helpers({
return Threads.find({ tmid }, { sort });
},
messageContext() {
const result = messageContext.apply(this);
const result = messageContext.call(this, { rid: this.mainMessage.rid });
return {
...result,
settings: {
@ -63,9 +63,10 @@ Template.thread.helpers({
},
messageBoxData() {
const instance = Template.instance();
const { mainMessage: { rid, _id: tmid } } = this;
const { mainMessage: { rid, _id: tmid }, subscription } = this;
return {
subscription,
rid,
tmid,
onSend: (...args) => instance.chatMessages && instance.chatMessages.send.apply(instance.chatMessages, args),
@ -104,7 +105,7 @@ Template.thread.onRendered(function() {
const tmid = this.state.get('tmid');
this.threadsObserve && this.threadsObserve.stop();
this.threadsObserve = Messages.find({ tmid, _updatedAt: { $gt: new Date() }, _hidden: { $ne: true } }, {
this.threadsObserve = Messages.find({ tmid, _hidden: { $ne: true } }, {
fields: {
collapsed: 0,
threadMsg: 0,

@ -7,7 +7,7 @@
<div class="thread-list js-scroll-threads">
<ul class="thread">
{{#each thread in threads}}
{{> nrr nrrargs 'message'
{{> message
groupable=false
msg=thread
room=room
@ -30,9 +30,9 @@
</div>
{{/if}}
{{/unless}}
{{#if message}}
{{#if msg}}
<div class="rc-user-info-container flex-nav">
{{> thread mainMessage=message room=room subscription=subscription settings=settings close=close}}
{{> thread mainMessage=msg room=room subscription=subscription settings=settings close=close}}
</div>
{{/if}}
{{/with}}

@ -37,6 +37,9 @@ Template.threads.events({
});
Template.threads.helpers({
subscription() {
return Template.currentData().subscription;
},
doDotLoadThreads() {
return Template.instance().state.get('close');
},
@ -45,7 +48,7 @@ Template.threads.helpers({
const { tabBar } = data;
return () => (state.get('close') ? tabBar.close() : state.set('mid', null));
},
message() {
msg() {
return Template.instance().state.get('thread');
},
isLoading() {
@ -71,6 +74,7 @@ Template.threads.onCreated(async function() {
thread: msg,
});
this.rid = rid;
this.incLimit = () => {
@ -114,6 +118,9 @@ Template.threads.onCreated(async function() {
});
this.autorun(() => {
if (mid) {
return;
}
const rid = this.state.get('rid');
this.rid = rid;
this.state.set({
@ -125,7 +132,7 @@ Template.threads.onCreated(async function() {
this.autorun(() => {
const rid = this.state.get('rid');
this.threadsObserve && this.threadsObserve.stop();
this.threadsObserve = Messages.find({ rid, _updatedAt: { $gt: new Date() }, tcount: { $exists: true }, _hidden: { $ne: true } }).observe({
this.threadsObserve = Messages.find({ rid, tcount: { $exists: true }, _hidden: { $ne: true } }).observe({
added: ({ _id, ...message }) => {
this.Threads.upsert({ _id }, message);
}, // Update message to re-render DOM

@ -15,7 +15,7 @@ Meteor.startup(function() {
id: 'follow-message',
icon: 'bell',
label: 'Follow_message',
context: ['threads'],
context: ['message', 'message-mobile', 'threads'],
async action() {
const { msg } = messageArgs(this);
call('followMessage', { mid: msg._id });

@ -15,7 +15,7 @@ Meteor.startup(function() {
id: 'unfollow-message',
icon: 'bell-off',
label: 'Unfollow_message',
context: ['threads'],
context: ['message', 'message-mobile', 'threads'],
async action() {
const { msg } = messageArgs(this);
call('unfollowMessage', { mid: msg._id });

@ -73,6 +73,8 @@
cursor: pointer;
color: #a0a0a0;
font-size: 1rem;
}
}
@ -115,11 +117,11 @@
}
&.collapsed .thread-reply-preview {
display: initial;
display: inline;
}
}
.message.collapsed + .message.system:not(.collapsed) {
.message.collapsed + .message.system:not(.collapsed):not(.new-day) {
margin-top: calc(var(--default-padding) / 2);
margin-bottom: calc(var(--default-padding) / 2);
}
@ -129,10 +131,13 @@
}
.message.collapsed > .thread-replied > .thumb {
bottom: 0;
left: 40px;
width: 20px;
height: 20px;
margin-top: auto;
margin-bottom: auto;
margin-left: 0;
& .avatar {

@ -3,7 +3,7 @@
{{#if isGroupChat}}
{{#with roomUsers}}
<div class="flex-tab__header">
<form class="search-form rc-member-list__search" role="form">
<form class="search-form rc-member-list__search js-search-form" role="form">
<div class="rc-input">
<div class="rc-input__icon">

@ -167,6 +167,13 @@ Template.membersList.events({
Template.parentData(0).tabBar.open();
},
'submit .js-search-form'(event) {
event.preventDefault();
event.stopPropagation();
},
'keydown .js-filter'(event, instance) {
instance.filter.set(event.target.value.trim());
},
'input .js-filter'(e, instance) {
instance.filter.set(e.target.value.trim());
},

@ -7,89 +7,89 @@ import { ChatRoom, ChatSubscription, RoomRoles, Subscriptions } from '../../../m
import { modal } from '../../../ui-utils';
import { t, handleError, roomTypes } from '../../../utils';
import { settings } from '../../../settings';
import { hasAllPermission, hasRole } from '../../../authorization';
import _ from 'underscore';
import { hasPermission, hasAllPermission, hasRole } from '../../../authorization';
import toastr from 'toastr';
export const getActions = function({ user, directActions, hideAdminControls }) {
const canSetLeader = () => hasAllPermission('set-leader', Session.get('openedRoom'));
const hasPermission = hasAllPermission;
const canSetOwner = () => hasAllPermission('set-owner', Session.get('openedRoom'));
const canSetModerator = () => hasAllPermission('set-moderator', Session.get('openedRoom'));
const canMuteUser = () => hasAllPermission('mute-user', Session.get('openedRoom'));
const canRemoveUser = () => hasAllPermission('remove-user', Session.get('openedRoom'));
const canBlockUser = () => (
ChatSubscription.findOne({ rid: Session.get('openedRoom'), 'u._id': Meteor.userId() }, { fields: { blocker: 1 } })
).blocker;
const canDirectMessageTo = (username) => {
const subscription = Subscriptions.findOne({ rid: Session.get('openedRoom') });
const canOpenDm = hasAllPermission('create-d') || Subscriptions.findOne({ name: username });
const dmIsNotAlreadyOpen = subscription && subscription.name !== username;
return canOpenDm && dmIsNotAlreadyOpen;
};
export const getActions = ({ user, directActions, hideAdminControls }) => {
const isIgnored = () => {
const sub = Subscriptions.findOne({ rid : Session.get('openedRoom') });
const sub = Subscriptions.findOne({ rid: Session.get('openedRoom') });
return sub && sub.ignored && sub.ignored.indexOf(user._id) > -1;
};
const canSetLeader = () => hasAllPermission('set-leader', Session.get('openedRoom'));
const active = () => user && user.active;
const hasAdminRole = () => {
if (user && user._id) {
return hasRole(user._id, 'admin');
}
};
const canRemoveUser = () => hasAllPermission('remove-user', Session.get('openedRoom'));
const canSetModerator = () => hasAllPermission('set-moderator', Session.get('openedRoom'));
const isDirect = () => {
const isActive = () => user && user.active;
const isLeader = () => (
user && user._id && !!RoomRoles.findOne({ rid: Session.get('openedRoom'), 'u._id': user._id, roles: 'leader' })
);
const isOwner = () => (
user && user._id && !!RoomRoles.findOne({ rid: Session.get('openedRoom'), 'u._id': user._id, roles: 'owner' })
);
const isModerator = () => (
user && user._id && !!RoomRoles.findOne({ rid: Session.get('openedRoom'), 'u._id': user._id, roles: 'moderator' })
);
const isInDirectMessageRoom = () => {
const room = ChatRoom.findOne(Session.get('openedRoom'));
return (room != null ? room.t : undefined) === 'd';
};
const isBlocker = () => {
const subscription = ChatSubscription.findOne({ rid:Session.get('openedRoom'), 'u._id': Meteor.userId() }, { fields: { blocker: 1 } });
return subscription.blocker;
};
const isLeader = () => {
if (user && user._id) {
return !!RoomRoles.findOne({ rid: Session.get('openedRoom'), 'u._id': user._id, roles: 'leader' });
}
return (room && room.t) === 'd';
};
const isOwner = () => {
if (user && user._id) {
return !!RoomRoles.findOne({ rid: Session.get('openedRoom'), 'u._id': user._id, roles: 'owner' });
}
};
const isModerator = () => {
if (user && user._id) {
return !!RoomRoles.findOne({ rid: Session.get('openedRoom'), 'u._id': user._id, roles: 'moderator' });
}
};
const canSetOwner = () => hasAllPermission('set-owner', Session.get('openedRoom'));
const canDirectMessage = (username) => {
const rid = Session.get('openedRoom');
const subscription = Subscriptions.findOne({ rid });
const canOpenDm = hasAllPermission('create-d') || Subscriptions.findOne({ name: username });
const dmIsNotAlreadyOpen = subscription && subscription.name !== username;
return canOpenDm && dmIsNotAlreadyOpen;
};
const canMuteUser = () => hasAllPermission('mute-user', Session.get('openedRoom'));
const userMuted = () => {
const isMuted = () => {
const room = ChatRoom.findOne(Session.get('openedRoom'));
return _.isArray(room && room.muted) && (room.muted.indexOf(user && user.username) !== -1);
return room && Array.isArray(room.muted) && room.muted.indexOf(user && user.username) > -1;
};
const isSelf = (username) => {
const user = Meteor.user();
return user && user.username === username;
};
const hasAdminRole = () => user && user._id && hasRole(user._id, 'admin');
const getUser = function getUser(fn, ...args) {
if (!user) {
return;
}
return fn.apply(this, [user, ...args]);
};
const prevent = function prevent(fn, ...args) {
return function(e, { instance }) {
e.stopPropagation();
e.preventDefault();
return fn.apply(instance, args);
};
const prevent = (fn, ...args) => function(e, { instance }) {
e.stopPropagation();
e.preventDefault();
return fn.apply(instance, args);
};
const success = function success(fn) {
return function(error, result) {
if (error) {
return handleError(error);
}
if (result) {
fn.call(this, result);
}
};
const success = (fn) => function(error, result) {
if (error) {
return handleError(error);
}
if (result) {
fn.call(this, result);
}
};
const actions = [
{
icon: 'message',
@ -98,7 +98,7 @@ export const getActions = function({ user, directActions, hideAdminControls }) {
Meteor.call('createDirectMessage', username, success((result) => result.rid && FlowRouter.go('direct', { username }, FlowRouter.current().queryParams)))
),
condition() {
return canDirectMessage(this.username);
return canDirectMessageTo(this.username);
},
},
@ -179,10 +179,10 @@ export const getActions = function({ user, directActions, hideAdminControls }) {
},
};
}, function() {
if (!isDirect() || isSelf(this.username)) {
if (!isInDirectMessageRoom() || isSelf(this.username)) {
return;
}
if (isBlocker()) {
if (canBlockUser()) {
return {
icon : 'ban',
name:t('Unblock_User'),
@ -211,7 +211,7 @@ export const getActions = function({ user, directActions, hideAdminControls }) {
}
Meteor.call('removeRoomOwner', Session.get('openedRoom'), _id, success(() => {
const room = ChatRoom.findOne(Session.get('openedRoom'));
toastr.success(TAPi18n.__('User__username__removed_from__room_name__owners', { username, room_name: room.name }));
toastr.success(TAPi18n.__('User__username__removed_from__room_name__owners', { username, room_name: roomTypes.getRoomName(room.t, room) }));
}));
}) };
}
@ -226,7 +226,7 @@ export const getActions = function({ user, directActions, hideAdminControls }) {
}
Meteor.call('addRoomOwner', Session.get('openedRoom'), _id, success(() => {
const room = ChatRoom.findOne(Session.get('openedRoom'));
toastr.success(TAPi18n.__('User__username__is_now_a_owner_of__room_name_', { username, room_name: room.name }));
toastr.success(TAPi18n.__('User__username__is_now_a_owner_of__room_name_', { username, room_name: roomTypes.getRoomName(room.t, room) }));
}));
}),
@ -247,7 +247,7 @@ export const getActions = function({ user, directActions, hideAdminControls }) {
}
Meteor.call('removeRoomLeader', Session.get('openedRoom'), _id, success(() => {
const room = ChatRoom.findOne(Session.get('openedRoom'));
toastr.success(TAPi18n.__('User__username__removed_from__room_name__leaders', { username, room_name: room.name }));
toastr.success(TAPi18n.__('User__username__removed_from__room_name__leaders', { username, room_name: roomTypes.getRoomName(room.t, room) }));
}));
}),
};
@ -263,7 +263,7 @@ export const getActions = function({ user, directActions, hideAdminControls }) {
}
Meteor.call('addRoomLeader', Session.get('openedRoom'), _id, success(() => {
const room = ChatRoom.findOne(Session.get('openedRoom'));
toastr.success(TAPi18n.__('User__username__is_now_a_leader_of__room_name_', { username, room_name: room.name }));
toastr.success(TAPi18n.__('User__username__is_now_a_leader_of__room_name_', { username, room_name: roomTypes.getRoomName(room.t, room) }));
}));
}),
};
@ -284,7 +284,7 @@ export const getActions = function({ user, directActions, hideAdminControls }) {
}
Meteor.call('removeRoomModerator', Session.get('openedRoom'), _id, success(() => {
const room = ChatRoom.findOne(Session.get('openedRoom'));
toastr.success(TAPi18n.__('User__username__removed_from__room_name__moderators', { username, room_name: room.name }));
toastr.success(TAPi18n.__('User__username__removed_from__room_name__moderators', { username, room_name: roomTypes.getRoomName(room.t, room) }));
}));
}),
};
@ -300,7 +300,7 @@ export const getActions = function({ user, directActions, hideAdminControls }) {
}
Meteor.call('addRoomModerator', Session.get('openedRoom'), _id, success(() => {
const room = ChatRoom.findOne(Session.get('openedRoom'));
toastr.success(TAPi18n.__('User__username__is_now_a_moderator_of__room_name_', { username, room_name: room.name }));
toastr.success(TAPi18n.__('User__username__is_now_a_moderator_of__room_name_', { username, room_name: roomTypes.getRoomName(room.t, room) }));
}));
}),
};
@ -326,7 +326,7 @@ export const getActions = function({ user, directActions, hideAdminControls }) {
if (!directActions || !canMuteUser()) {
return;
}
if (userMuted()) {
if (isMuted()) {
return {
group: 'channel',
icon : 'mic',
@ -352,7 +352,7 @@ export const getActions = function({ user, directActions, hideAdminControls }) {
}
modal.open({
title: t('Are_you_sure'),
text: t('The_user_wont_be_able_to_type_in_s', room.name),
text: t('The_user_wont_be_able_to_type_in_s', roomTypes.getRoomName(room.t, room)),
type: 'warning',
showCancelButton: true,
confirmButtonColor: '#DD6B55',
@ -364,7 +364,7 @@ export const getActions = function({ user, directActions, hideAdminControls }) {
Meteor.call('muteUserInRoom', { rid, username }, success(() => {
modal.open({
title: t('Muted'),
text: t('User_has_been_muted_in_s', room.name),
text: t('User_has_been_muted_in_s', roomTypes.getRoomName(room.t, room)),
type: 'success',
timer: 2000,
showConfirmButton: false,
@ -468,7 +468,7 @@ export const getActions = function({ user, directActions, hideAdminControls }) {
if (hideAdminControls || !hasPermission('edit-other-user-active-status')) {
return;
}
if (active()) {
if (isActive()) {
return {
group: 'admin',
icon : 'user',
@ -501,5 +501,6 @@ export const getActions = function({ user, directActions, hideAdminControls }) {
action: prevent(getUser, ({ _id }) => Meteor.call('e2e.resetUserE2EKey', _id, success(() => toastr.success(t('User_e2e_key_was_reset'))))),
};
}];
return actions;
};

@ -14,26 +14,31 @@ import { settings } from '../../../settings';
import { getActions } from './userActions';
import './userInfo.html';
const more = function() {
return Template.instance().actions.get()
.map((action) => (typeof action === 'function' ? action.call(this) : action))
.filter((action) => action && (!action.condition || action.condition.call(this)))
.slice(3);
};
const shownActionsCount = 2;
const moreActions = function() {
return (
Template.instance().actions.get()
.map((action) => (typeof action === 'function' ? action.call(this) : action))
.filter((action) => action && (!action.condition || action.condition.call(this)))
.slice(shownActionsCount)
);
};
Template.userInfo.helpers({
hideHeader() {
return ['Template.adminUserInfo', 'adminUserInfo'].includes(Template.parentData(2).viewName);
},
moreActions: more,
moreActions,
actions() {
return Template.instance().actions.get()
.map((action) => (typeof action === 'function' ? action.call(this) : action))
.filter((action) => action && (!action.condition || action.condition.call(this)))
.slice(0, 2);
.slice(0, shownActionsCount);
},
customField() {
const sCustomFieldsToShow = settings.get('Accounts_CustomFieldsToShowInUserInfo').trim();
const customFields = [];
@ -184,7 +189,7 @@ Template.userInfo.helpers({
Template.userInfo.events({
'click .js-more'(e, instance) {
const actions = more.call(this);
const actions = moreActions.call(this);
const groups = [];
const columns = [];
const admin = actions.filter((actions) => actions.group === 'admin');

@ -5,13 +5,14 @@ import { Match } from 'meteor/check';
import { Tracker } from 'meteor/tracker';
import { FlowRouter } from 'meteor/kadira:flow-router';
import { t, getUserPreference } from '../../utils';
import { getConfig } from '../../ui-utils/client/config';
import { Session } from 'meteor/session';
import { Template } from 'meteor/templating';
import { chatMessages } from '../../ui';
import { mainReady, Layout, iframeLogin, modal, popover, menu, fireGlobalEvent, RoomManager } from '../../ui-utils';
import { toolbarSearch } from '../../ui-sidenav';
import { settings } from '../../settings';
import { CachedChatSubscription, Roles, ChatSubscription } from '../../models';
import { CachedChatSubscription, Roles, ChatSubscription, Users } from '../../models';
import { CachedCollectionManager } from '../../ui-cached-collection';
import { hasRole } from '../../authorization';
import { tooltip } from '../../tooltip';
@ -131,6 +132,8 @@ Template.main.onCreated(function() {
tooltip.init();
});
const skipActiveUsersToBeReady = [getConfig('experimental'), getConfig('skipActiveUsersToBeReady')].includes('true');
Template.main.helpers({
removeSidenav() {
return Layout.isEmbedded() && !/^\/admin/.test(FlowRouter.current().route.path);
@ -156,18 +159,26 @@ Template.main.helpers({
return iframeEnabled && iframeLogin.reactiveIframeUrl.get();
},
subsReady() {
const routerReady = FlowRouter.subsReady('userData', 'activeUsers');
const subscriptions = ['userData'];
if (!skipActiveUsersToBeReady) {
subscriptions.push('activeUsers');
}
const routerReady = FlowRouter.subsReady.apply(FlowRouter, subscriptions);
const subscriptionsReady = CachedChatSubscription.ready.get();
const settingsReady = settings.cachedCollection.ready.get();
const ready = (Meteor.userId() == null) || (routerReady && subscriptionsReady && settingsReady);
const ready = (routerReady && subscriptionsReady && settingsReady) || !Meteor.userId();
CachedCollectionManager.syncEnabled = ready;
Meteor.defer(() => {
mainReady.set(ready);
});
mainReady.set(ready);
return ready;
},
hasUsername() {
return (Meteor.userId() != null && Meteor.user().username != null) || (Meteor.userId() == null && settings.get('Accounts_AllowAnonymousRead') === true);
const uid = Meteor.userId();
const user = uid && Users.findOne({ _id: uid }, { fields: { username: 1 } });
return (user && user.username) || settings.get('Accounts_AllowAnonymousRead');
},
requirePasswordChange() {
const user = Meteor.user();

@ -70,7 +70,35 @@ async function renderPdfToCanvas(canvasId, pdfLink) {
canvas.style.display = 'block';
}
const renderBody = (msg, settings) => {
const isSystemMessage = MessageTypes.isSystemMessage(msg);
const messageType = MessageTypes.getType(msg) || {};
if (messageType.render) {
msg = messageType.render(msg);
} else if (messageType.template) {
// render template
} else if (messageType.message) {
msg = TAPi18n.__(messageType.message, { ... typeof messageType.data === 'function' && messageType.data(msg) });
} else if (msg.u && msg.u.username === settings.Chatops_Username) {
msg.html = msg.msg;
msg = callbacks.run('renderMentions', msg);
msg = msg.html;
} else {
msg = renderMessageBody(msg);
}
if (isSystemMessage) {
msg.html = Markdown.parse(msg.html);
}
return msg;
};
Template.message.helpers({
body() {
const { msg, settings } = this;
return Tracker.nonreactive(() => renderBody(msg, settings));
},
and(a, b) {
return a && b;
},
@ -200,9 +228,6 @@ Template.message.helpers({
return 'temp';
}
},
body() {
return Template.instance().body;
},
threadMessage() {
const { msg } = this;
return normalizeThreadMessage(msg);
@ -349,9 +374,13 @@ Template.message.helpers({
}
return roomTypes.getIcon(room);
},
customClass() {
const { customClass, msg } = this;
return customClass || msg.customClass;
},
fromSearch() {
const { customClass } = this;
return customClass === 'search';
const { customClass, msg } = this;
return [msg.customClass, customClass].includes('search');
},
actionContext() {
const { msg } = this;
@ -420,13 +449,7 @@ const findParentMessage = (() => {
},
}, { multi: true });
if (!Messages.findOne({ _id })) {
/**
* Delete rid from message to not render it and to not be considred in last message
* find from load history method what was preveting the load of some messages in
* between the reals last loaded message and this one if this one is older than
* the real last loaded message.
*/
delete msg.rid;
msg._hidden = true;
Messages.upsert({ _id }, msg);
}
});
@ -454,39 +477,13 @@ const findParentMessage = (() => {
};
})();
const renderBody = (msg, settings) => {
const isSystemMessage = MessageTypes.isSystemMessage(msg);
const messageType = MessageTypes.getType(msg) || {};
if (messageType.render) {
msg = messageType.render(msg);
} else if (messageType.template) {
// render template
} else if (messageType.message) {
msg = TAPi18n.__(messageType.message, { ... typeof messageType.data === 'function' && messageType.data(msg) });
} else if (msg.u && msg.u.username === settings.Chatops_Username) {
msg.html = msg.msg;
msg = callbacks.run('renderMentions', msg);
msg = msg.html;
} else {
msg = renderMessageBody(msg);
}
if (isSystemMessage) {
msg.html = Markdown.parse(msg.html);
}
return msg;
};
Template.message.onCreated(function() {
const { msg, settings } = Template.currentData();
const { msg, shouldCollapseReplies } = Template.currentData();
this.wasEdited = msg.editedAt && !MessageTypes.isSystemMessage(msg);
if (msg.tmid && !msg.threadMsg) {
if (shouldCollapseReplies && msg.tmid && !msg.threadMsg) {
findParentMessage(msg.tmid);
}
return this.body = Tracker.nonreactive(() => renderBody(msg, settings));
});
const hasTempClass = (node) => node.classList.contains('temp');
@ -601,7 +598,7 @@ const processSequentials = ({ currentNode, settings, forceDate, showDateSeparato
} else {
nextNode.classList.remove('new-day');
}
} else {
} else if (shouldCollapseReplies) {
const [el] = $(`#chat-window-${ msg.rid }`);
const view = el && Blaze.getView(el);
const templateInstance = view && view.templateInstance();

@ -181,7 +181,6 @@ Template.messageBox.helpers({
if (!rid) {
return false;
}
const isAnonymous = !Meteor.userId();
return isAnonymous || instance.state.get('mustJoinWithCode');
},

@ -2,7 +2,8 @@ import { Meteor } from 'meteor/meteor';
import { Session } from 'meteor/session';
import { Template } from 'meteor/templating';
import { settings } from '../../../settings';
import { call, roomTypes, RoomManager, RoomHistoryManager } from '../../../ui-utils';
import { call, RoomManager, RoomHistoryManager } from '../../../ui-utils';
import { roomTypes } from '../../../utils';
import { hasAllPermission } from '../../../authorization';
import './messageBoxNotSubscribed.html';

@ -149,7 +149,7 @@ Meteor.startup(async function() {
id: 'reply-directly',
icon: 'reply-directly',
label: 'Reply_in_direct_message',
context: ['message', 'message-mobile'],
context: ['message', 'message-mobile', 'threads'],
action() {
const { msg } = messageArgs(this);
roomTypes.openRouteLink('d', { name: msg.u.username }, {
@ -174,7 +174,7 @@ Meteor.startup(async function() {
id: 'quote-message',
icon: 'quote',
label: 'Quote',
context: ['message', 'message-mobile'],
context: ['message', 'message-mobile', 'threads'],
action() {
const { msg: message } = messageArgs(this);
const { input } = chatMessages[message.rid];
@ -206,7 +206,7 @@ Meteor.startup(async function() {
icon: 'permalink',
label: 'Get_link',
classes: 'clipboard',
context: ['message', 'message-mobile'],
context: ['message', 'message-mobile', 'threads'],
async action(event) {
const { msg: message } = messageArgs(this);
const permalink = await MessageAction.getPermaLink(message._id);
@ -229,7 +229,7 @@ Meteor.startup(async function() {
icon: 'copy',
label: 'Copy',
classes: 'clipboard',
context: ['message', 'message-mobile'],
context: ['message', 'message-mobile', 'threads'],
action(event) {
const { msg: message } = messageArgs(this);
$(event.currentTarget).attr('data-clipboard-text', message);
@ -250,7 +250,7 @@ Meteor.startup(async function() {
id: 'edit-message',
icon: 'edit',
label: 'Edit',
context: ['message', 'message-mobile'],
context: ['message', 'message-mobile', 'threads'],
action() {
const { msg } = messageArgs(this);
chatMessages[Session.get('openedRoom')].edit(document.getElementById(msg._id));
@ -290,7 +290,7 @@ Meteor.startup(async function() {
id: 'delete-message',
icon: 'trash',
label: 'Delete',
context: ['message', 'message-mobile'],
context: ['message', 'message-mobile', 'threads'],
color: 'alert',
action() {
const { msg: message } = messageArgs(this);
@ -315,7 +315,7 @@ Meteor.startup(async function() {
id: 'report-message',
icon: 'report',
label: 'Report',
context: ['message', 'message-mobile'],
context: ['message', 'message-mobile', 'threads'],
color: 'alert',
action() {
const { msg: message } = messageArgs(this);

@ -1,4 +1,3 @@
import mem from 'mem';
import s from 'underscore.string';
import { Meteor } from 'meteor/meteor';
import { Tracker } from 'meteor/tracker';
@ -10,7 +9,8 @@ import { RoomManager } from './RoomManager';
import { readMessage } from './readMessages';
import { renderMessageBody } from './renderMessageBody';
export const normalizeThreadMessage = mem((message) => {
export const normalizeThreadMessage = (message) => {
if (message.msg) {
return renderMessageBody(message).replace(/<br\s?\\?>/g, ' ');
}
@ -26,7 +26,7 @@ export const normalizeThreadMessage = mem((message) => {
return s.escapeHTML(attachment.title);
}
}
}, { maxAge: 1000 });
};
export const upsertMessage = ({ msg: { _id, ...msg }, subscription }) => {
const userId = msg.u && msg.u._id;
@ -102,7 +102,7 @@ export const RoomHistoryManager = new class {
room.isLoading.set(true);
// ScrollListener.setLoader true
const lastMessage = ChatMessage.findOne({ rid }, { sort: { ts: 1 } });
const lastMessage = ChatMessage.findOne({ rid, _hidden: { $ne: true } }, { sort: { ts: 1 } });
// lastMessage ?= ChatMessage.findOne({rid: rid}, {sort: {ts: 1}})
if (lastMessage) {
@ -180,7 +180,7 @@ export const RoomHistoryManager = new class {
room.isLoading.set(true);
const lastMessage = ChatMessage.findOne({ rid }, { sort: { ts: -1 } });
const lastMessage = ChatMessage.findOne({ rid, _hidden: { $ne: true } }, { sort: { ts: -1 } });
let typeName = undefined;
@ -225,7 +225,7 @@ export const RoomHistoryManager = new class {
const instance = Blaze.getView($('.messages-box .wrapper')[0]).templateInstance();
if (ChatMessage.findOne(message._id)) {
if (ChatMessage.findOne({ _id: message._id, _hidden: { $ne: true } })) {
const wrapper = $('.messages-box .wrapper');
const msgElement = $(`#${ message._id }`, wrapper);
if (msgElement.length === 0) {

@ -258,7 +258,7 @@ export const RoomManager = new function() {
};
const loadMissedMessages = function(rid) {
const lastMessage = ChatMessage.findOne({ rid, temp: { $exists: false } }, { sort: { ts: -1 }, limit: 1 });
const lastMessage = ChatMessage.findOne({ rid, _hidden: { $ne: true }, temp: { $exists: false } }, { sort: { ts: -1 }, limit: 1 });
if (lastMessage == null) {
return;
}
@ -338,7 +338,9 @@ callbacks.add('afterLogoutCleanUp', () => RoomManager.closeAllRooms(), callbacks
CachedCollectionManager.onLogin(() => {
Notifications.onUser('subscriptions-changed', (action, sub) => {
ChatMessage.update({ rid: sub.rid }, { $unset : { ignored : '' } }, { multi : true });
const ignored = sub && sub.ignored ? { $nin: sub.ignored } : { $exists: true };
ChatMessage.update({ rid: sub.rid, ignored }, { $unset: { ignored: true } }, { multi: true });
if (sub && sub.ignored) {
ChatMessage.update({ rid: sub.rid, t: { $ne: 'command' }, 'u._id': { $in : sub.ignored } }, { $set: { ignored : true } }, { multi : true });
}

@ -6,8 +6,7 @@ import { hasPermission } from '../../../authorization/client';
import { settings } from '../../../settings/client';
import { getUserPreference } from '../../../utils/client';
export function messageContext() {
const { rid } = Template.instance();
export function messageContext({ rid } = Template.instance()) {
const uid = Meteor.userId();
return {
u: Users.findOne({ _id: uid }, { fields: { name: 1, username: 1 } }),
@ -22,6 +21,7 @@ export function messageContext() {
fields: {
name: 1,
autoTranslate: 1,
rid: 1,
},
}),
settings: {

@ -42,87 +42,83 @@ function replaceCenterDomBy(dom) {
}
export const openRoom = function(type, name) {
Session.set('openedRoom', null);
return Meteor.defer(() =>
window.currentTracker = Tracker.autorun(function(c) {
const user = Meteor.user();
if ((user && user.username == null) || (user == null && settings.get('Accounts_AllowAnonymousRead') === false)) {
BlazeLayout.render('main');
return;
}
window.currentTracker = Tracker.autorun(function(c) {
const user = Meteor.user();
if ((user && user.username == null) || (user == null && settings.get('Accounts_AllowAnonymousRead') === false)) {
BlazeLayout.render('main');
return;
}
if (RoomManager.open(type + name).ready() !== true) {
replaceCenterDomBy(getDomOfLoading());
return;
}
if (window.currentTracker) {
window.currentTracker = undefined;
}
c.stop();
const room = roomTypes.findRoom(type, name, user);
if (room == null) {
if (type === 'd') {
Meteor.call('createDirectMessage', name, function(error) {
if (!error) {
RoomManager.close(type + name);
return openRoom('d', name);
} else {
Session.set('roomNotFound', { type, name, error });
BlazeLayout.render('main', { center: 'roomNotFound' });
return;
}
});
} else {
Meteor.call('getRoomByTypeAndName', type, name, function(error, record) {
if (error) {
Session.set('roomNotFound', { type, name, error });
return BlazeLayout.render('main', { center: 'roomNotFound' });
} else {
Rooms.upsert({ _id: record._id }, _.omit(record, '_id'));
RoomManager.close(type + name);
return openRoom(type, name);
}
});
}
return;
if (RoomManager.open(type + name).ready() !== true) {
replaceCenterDomBy(getDomOfLoading());
return;
}
if (window.currentTracker) {
window.currentTracker = undefined;
}
c.stop();
const room = roomTypes.findRoom(type, name, user);
if (room == null) {
if (type === 'd') {
Meteor.call('createDirectMessage', name, function(error) {
if (!error) {
RoomManager.close(type + name);
return openRoom('d', name);
} else {
Session.set('roomNotFound', { type, name, error });
BlazeLayout.render('main', { center: 'roomNotFound' });
return;
}
});
} else {
Meteor.call('getRoomByTypeAndName', type, name, function(error, record) {
if (error) {
Session.set('roomNotFound', { type, name, error });
return BlazeLayout.render('main', { center: 'roomNotFound' });
} else {
Rooms.upsert({ _id: record._id }, _.omit(record, '_id'));
RoomManager.close(type + name);
return openRoom(type, name);
}
});
}
return;
}
const roomDom = RoomManager.getDomOfRoom(type + name, room._id);
const mainNode = replaceCenterDomBy(roomDom);
const roomDom = RoomManager.getDomOfRoom(type + name, room._id);
const mainNode = replaceCenterDomBy(roomDom);
if (mainNode) {
if (roomDom.classList.contains('room-container')) {
roomDom.querySelector('.messages-box > .wrapper').scrollTop = roomDom.oldScrollTop;
}
if (mainNode) {
if (roomDom.classList.contains('room-container')) {
roomDom.querySelector('.messages-box > .wrapper').scrollTop = roomDom.oldScrollTop;
}
}
Session.set('openedRoom', room._id);
RoomManager.openedRoom = room._id;
fireGlobalEvent('room-opened', _.omit(room, 'usernames'));
Session.set('editRoomTitle', false);
RoomManager.updateMentionsMarksOfRoom(type + name);
Meteor.setTimeout(() => readMessage.readNow(), 2000);
// KonchatNotification.removeRoomNotification(params._id)
// update user's room subscription
const sub = ChatSubscription.findOne({ rid: room._id });
if (sub && sub.open === false) {
Meteor.call('openRoom', room._id, function(err) {
if (err) {
return handleError(err);
}
});
}
Session.set('openedRoom', room._id);
RoomManager.openedRoom = room._id;
fireGlobalEvent('room-opened', _.omit(room, 'usernames'));
Session.set('editRoomTitle', false);
RoomManager.updateMentionsMarksOfRoom(type + name);
Meteor.setTimeout(() => readMessage.readNow(), 2000);
// KonchatNotification.removeRoomNotification(params._id)
// update user's room subscription
const sub = ChatSubscription.findOne({ rid: room._id });
if (sub && sub.open === false) {
Meteor.call('openRoom', room._id, function(err) {
if (err) {
return handleError(err);
}
});
}
if (FlowRouter.getQueryParam('msg')) {
const msg = { _id: FlowRouter.getQueryParam('msg'), rid: room._id };
RoomHistoryManager.getSurroundingMessages(msg);
}
if (FlowRouter.getQueryParam('msg')) {
const msg = { _id: FlowRouter.getQueryParam('msg'), rid: room._id };
RoomHistoryManager.getSurroundingMessages(msg);
}
return callbacks.run('enter-room', sub);
})
);
return callbacks.run('enter-room', sub);
});
};

@ -1,11 +1,35 @@
import { callbacks } from '../../../callbacks';
import s from 'underscore.string';
export const renderMessageBody = (message) => {
const generateKeyDefault = (...args) => args.map((item) => JSON.stringify(item)).join('-');
const mem = (fn, tm = 500, generateKey = generateKeyDefault) => {
const cache = {};
const timeout = {};
const invalidateCache = (key) => delete cache[key];
return (...args) => {
const key = generateKey(...args);
if (!key) {
return fn(...args);
}
if (!cache[key]) {
cache[key] = fn(...args);
}
if (timeout[key]) {
clearTimeout(timeout[key]);
}
timeout[key] = setTimeout(invalidateCache, tm, key);
return cache[key];
};
};
export const renderMessageBody = mem((message) => {
message.html = s.trim(message.msg) ? s.escapeHTML(message.msg) : '';
const { tokens, html } = callbacks.run('renderMessage', message);
return (Array.isArray(tokens) ? tokens.reverse() : [])
.reduce((html, { token, text }) => html.replace(token, () => text), html);
};
}, 5000, ({ _id, _updatedAt }) => (_id && _updatedAt && _id + _updatedAt));

@ -1,6 +1,7 @@
import _ from 'underscore';
import moment from 'moment';
import toastr from 'toastr';
import _ from 'underscore';
import s from 'underscore.string';
import { Meteor } from 'meteor/meteor';
import { Random } from 'meteor/random';
@ -429,7 +430,7 @@ export class ChatMessages {
_id: Random.id(),
rid: msgObject.rid,
ts: new Date,
msg: TAPi18n.__('No_such_command', { command: match[1] }),
msg: TAPi18n.__('No_such_command', { command: s.escapeHTML(match[1]) }),
u: {
username: settings.get('InternalHubot_Username'),
},

@ -276,6 +276,7 @@ Template.room.helpers({
const viewMode = getUserPreference(Meteor.userId(), 'messageViewMode');
const query = {
rid,
_hidden: { $ne: true },
...((ignoreReplies || modes[viewMode] === 'compact') && { tmid: { $exists: 0 } }),
};
@ -596,13 +597,14 @@ Template.room.events({
event.preventDefault();
event.stopPropagation();
const { tabBar } = Template.instance();
const { tabBar, subscription } = Template.instance();
const { msg, msg: { rid, _id, tmid } } = messageArgs(this);
const $flexTab = $('.flex-tab-container .flex-tab');
$flexTab.attr('template', 'thread');
tabBar.setData({
subscription: subscription.get(),
msg,
rid,
mid: tmid || _id,

@ -1,3 +1,3 @@
{
"version": "1.0.2"
"version": "1.0.3"
}

@ -0,0 +1,54 @@
import { MongoInternals } from 'meteor/mongo';
function fallbackMongoInfo() {
let mongoVersion;
let mongoStorageEngine;
const { mongo } = MongoInternals.defaultRemoteCollectionDriver();
const oplogEnabled = Boolean(mongo._oplogHandle && mongo._oplogHandle.onOplogEntry);
try {
const { version } = Promise.await(mongo.db.command({ buildinfo: 1 }));
mongoVersion = version;
mongoStorageEngine = 'unknown';
} catch (e) {
console.error('=== Error getting MongoDB info ===');
console.error(e && e.toString());
console.error('----------------------------------');
console.error('Without mongodb version we can\'t ensure you are running a compatible version.');
console.error('If you are running your mongodb with auth enabled and an user different from admin');
console.error('you may need to grant permissions for this user to check cluster data.');
console.error('You can do it via mongo shell running the following command replacing');
console.error('the string YOUR_USER by the correct user\'s name:');
console.error('');
console.error(' db.runCommand({ grantRolesToUser: "YOUR_USER" , roles: [{role: "clusterMonitor", db: "admin"}]})');
console.error('');
console.error('==================================');
}
return { oplogEnabled, mongoVersion, mongoStorageEngine };
}
export function getMongoInfo() {
let mongoVersion;
let mongoStorageEngine;
const { mongo } = MongoInternals.defaultRemoteCollectionDriver();
const oplogEnabled = Boolean(mongo._oplogHandle && mongo._oplogHandle.onOplogEntry);
try {
const { version, storageEngine } = Promise.await(mongo.db.command({ serverStatus: 1 }));
mongoVersion = version;
mongoStorageEngine = storageEngine.name;
} catch (e) {
return fallbackMongoInfo();
}
return { oplogEnabled, mongoVersion, mongoStorageEngine };
}

@ -7,6 +7,7 @@ export { roomTypes } from './lib/roomTypes';
export { RoomTypeRouteConfig, RoomTypeConfig, RoomSettingsEnum, UiTextContext } from '../lib/RoomTypeConfig';
export { RoomTypesCommon } from '../lib/RoomTypesCommon';
export { isDocker } from './functions/isDocker';
export { getMongoInfo } from './functions/getMongoInfo';
export { getUserAvatarURL } from '../lib/getUserAvatarURL';
export { slashCommands } from '../lib/slashCommand';
export { getUserNotificationPreference } from '../lib/getUserNotificationPreference';
@ -14,6 +15,5 @@ export { getAvatarColor } from '../lib/getAvatarColor';
export { getURL } from '../lib/getURL';
export { getValidRoomName } from '../lib/getValidRoomName';
export { placeholders } from '../lib/placeholders';
export { composeMessageObjectWithUser } from './lib/composeMessageObjectWithUser';
export { templateVarHandler } from '../lib/templateVarHandler';
export { mime } from '../lib/mimeTypes';

@ -1,34 +0,0 @@
import { Users } from '../../../models';
import { settings } from '../../../settings';
import memoize from 'mem';
const maxAgeInMS = 1000;
const getUserByUsername = (username) => Users.findOneByUsername(username, { fields: { name: 1 } });
const getNameOfUser = memoize((username) => {
const user = getUserByUsername(username);
return user ? user.name : undefined;
}, { maxAge: maxAgeInMS });
export const composeMessageObjectWithUser = function(message, userId) {
if (message) {
if (message.starred && Array.isArray(message.starred)) {
message.starred = message.starred.filter((star) => star._id === userId);
}
if (settings.get('UI_Use_Real_Name')) {
if (message.u && message.u._id) {
message.u.name = getNameOfUser(message.u.username);
}
if (message.mentions && message.mentions.length) {
message.mentions.forEach((mention) => mention.name = getNameOfUser(mention.username));
}
if (message.reactions && Object.keys(message.reactions).length) {
Object.keys(message.reactions).forEach((reaction) => {
const names = message.reactions[reaction].usernames.map(getNameOfUser);
message.reactions[reaction].names = names;
});
}
}
}
return message;
};

@ -0,0 +1,56 @@
import { Users } from '../../../models/server';
import { settings } from '../../../settings/server';
const filterStarred = (message, uid) => {
// only return starred field if user has it starred
if (message.starred && Array.isArray(message.starred)) {
message.starred = message.starred.filter((star) => star._id === uid);
}
return message;
};
// TODO: we should let clients get user names on demand instead of doing this
export const normalizeMessagesForUser = (messages, uid) => {
// if not using real names, there is nothing else to do
if (!settings.get('UI_Use_Real_Name')) {
return messages.map((message) => filterStarred(message, uid));
}
const usernames = new Set();
messages.forEach((message) => {
message = filterStarred(message, uid);
usernames.add(message.u.username);
(message.mentions || []).forEach(({ username }) => { usernames.add(username); });
Object.values(message.reactions || {})
.forEach((reaction) => reaction.usernames.forEach((username) => usernames.add(username)));
});
const users = {};
Users.findUsersByUsernames([...usernames.values()], {
fields: {
username: 1,
name: 1,
},
}).forEach((user) => {
users[user.username] = user.name;
});
messages.forEach((message) => {
message.u.name = users[message.u.username];
(message.mentions || []).forEach((mention) => { mention.name = users[mention.username]; });
Object.keys(message.reactions || {}).forEach((reaction) => {
const names = message.reactions[reaction].usernames.map((username) => users[username]);
message.reactions[reaction].names = names;
});
});
return messages;
};

@ -1,3 +1,5 @@
import 'url-polyfill';
import './importsCss';
import './importPackages';
import '../imports/startup/client';

@ -0,0 +1 @@
../../node_modules/limax

@ -0,0 +1 @@
../../node_modules/map-age-cleaner

@ -0,0 +1 @@
../../node_modules/mem

@ -0,0 +1 @@
../../node_modules/mimic-fn

@ -0,0 +1 @@
../../node_modules/p-defer

@ -0,0 +1 @@
../../node_modules/p-is-promise

@ -0,0 +1 @@
../../node_modules/pinyin

619
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -1,7 +1,7 @@
{
"name": "Rocket.Chat",
"description": "The Ultimate Open Source WebChat Platform",
"version": "1.0.2",
"version": "1.0.3",
"author": {
"name": "Rocket.Chat",
"url": "https://rocket.chat/"
@ -76,6 +76,7 @@
"coverage": "nyc -r html mocha --opts ./mocha.opts \"`node -e \"console.log(require('./package.json').mocha.tests.join(' '))\"`\"",
"testunit": "mocha --opts ./mocha.opts \"`node -e \"console.log(require('./package.json').mocha.tests.join(' '))\"`\"",
"translation-diff": "node .scripts/translationDiff.js",
"translation-fix-order": "node .scripts/fix-i18n.js",
"version": "node .scripts/version.js",
"set-version": "node .scripts/set-version.js",
"release": "meteor npm run set-version --silent"
@ -101,9 +102,11 @@
"chai-datetime": "^1.5.0",
"chimp": "^0.51.1",
"eslint": "^5.9.0",
"fast-glob": "^2.2.6",
"husky": "^1.2.0",
"mocha": "^5.2.0",
"mock-require": "^3.0.2",
"mongo-unit": "^1.4.4",
"postcss": "^7.0.6",
"postcss-custom-properties": "^8.0.9",
"postcss-easy-import": "^3.0.0",
@ -221,6 +224,7 @@
"ua-parser-js": "^0.7.19",
"underscore": "^1.9.1",
"underscore.string": "^3.3.5",
"url-polyfill": "^1.1.5",
"uuid": "^3.3.2",
"webdav": "^2.0.0",
"wolfy87-eventemitter": "^5.2.5",

@ -2837,4 +2837,4 @@
"Your_push_was_sent_to_s_devices": "Jou druk is gestuur na%s toestelle",
"Your_server_link": "Jou bediener skakel",
"Your_workspace_is_ready": "Jou werkruimte is gereed om 🎉 te gebruik"
}
}

@ -2837,4 +2837,4 @@
"Your_push_was_sent_to_s_devices": "وقد أرسلت دفعك إلى أجهزة٪ الصورة",
"Your_server_link": "رابط الخادم الخاص بك",
"Your_workspace_is_ready": "مساحة العمل الخاصة بك جاهزة لاستخدام 🎉"
}
}

@ -2837,4 +2837,4 @@
"Your_push_was_sent_to_s_devices": "Sizin itəniz%s cihazlarına göndərildi",
"Your_server_link": "Sizin server bağlantınız",
"Your_workspace_is_ready": "İş yeriniz 🎉 istifadə etməyə hazırdır"
}
}

@ -2854,4 +2854,4 @@
"Your_push_was_sent_to_s_devices": "Ваш штуршок быў адпраўлены ў%s прылад",
"Your_server_link": "Ваша спасылка сервера",
"Your_workspace_is_ready": "Ваша працоўная вобласць гатова да выкарыстання 🎉"
}
}

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save