diff --git a/.docker/Dockerfile.rhel b/.docker/Dockerfile.rhel
index 9369e091a32b..dc9695a75fb8 100644
--- a/.docker/Dockerfile.rhel
+++ b/.docker/Dockerfile.rhel
@@ -1,6 +1,6 @@
FROM registry.access.redhat.com/rhscl/nodejs-8-rhel7
-ENV RC_VERSION 3.1.2
+ENV RC_VERSION 3.2.0
MAINTAINER buildmaster@rocket.chat
diff --git a/.eslintignore b/.eslintignore
index dee29d8e38d0..0f70fd1c55c7 100644
--- a/.eslintignore
+++ b/.eslintignore
@@ -8,7 +8,6 @@ app/katex/client/katex/katex.min.js
packages/rocketchat-livechat/assets/rocketchat-livechat.min.js
packages/rocketchat-livechat/assets/rocket-livechat.js
app/theme/client/vendor/
-app/ui/client/lib/customEventPolyfill.js
app/ui/client/lib/Modernizr.js
public/mp3-realtime-worker.js
public/lame.min.js
diff --git a/.github/history.json b/.github/history.json
index f93ed35ca2ef..98c0a2c52507 100644
--- a/.github/history.json
+++ b/.github/history.json
@@ -42493,8 +42493,1039 @@
}
]
},
+ "3.2.0-rc.0": {
+ "node_version": "12.16.1",
+ "npm_version": "6.13.4",
+ "mongo_versions": [
+ "3.4",
+ "3.6",
+ "4.0"
+ ],
+ "pull_requests": [
+ {
+ "pr": "17333",
+ "title": "[NEW] [ENTERPRISE] Restrict the permissions configuration for guest users ",
+ "userLogin": "pierre-lehnen-rc",
+ "description": "The **Guest** role is blocked for edition on the EE version. This will allow the EE customers to receive licenses with extra seats for Guests for free. The CE version continues to have the Guest role configurable.",
+ "milestone": "3.2.0",
+ "contributors": [
+ "pierre-lehnen-rc",
+ "sampaiodiego",
+ "web-flow"
+ ]
+ },
+ {
+ "pr": "17091",
+ "title": "[NEW] Federation event for when users left rooms",
+ "userLogin": "alansikora",
+ "contributors": [
+ "alansikora",
+ "sampaiodiego",
+ "web-flow"
+ ]
+ },
+ {
+ "pr": "16790",
+ "title": "[FIX] CSV Importer fails when there are no users to import",
+ "userLogin": "pierre-lehnen-rc",
+ "contributors": [
+ "pierre-lehnen-rc",
+ "web-flow"
+ ]
+ },
+ {
+ "pr": "17206",
+ "title": "[FIX] Import slack's multiple direct messages as direct rooms instead of private groups",
+ "userLogin": "pierre-lehnen-rc",
+ "milestone": "3.2.0",
+ "contributors": [
+ "pierre-lehnen-rc",
+ "web-flow"
+ ]
+ },
+ {
+ "pr": "17324",
+ "title": "[FIX] SAML Idp Initiated Logout Error",
+ "userLogin": "pierre-lehnen-rc",
+ "milestone": "3.2.0",
+ "contributors": [
+ "pierre-lehnen-rc"
+ ]
+ },
+ {
+ "pr": "17357",
+ "title": "[NEW] Better Push and Email Notification logic",
+ "userLogin": "rodrigok",
+ "description": "We are still using the same logic to define which notifications every new message will generate, it takes some servers' settings, users's preferences and subscriptions' settings in consideration to determine who will receive each notification type (desktop, audio, email and mobile push), but now it doesn't check the user's status (online, away, offline) for email and mobile push notifications but send those notifications to a new queue with the following rules:\r\n\r\n- When the user is online the notification is scheduled to be sent in 120 seconds\r\n- When the user is away the notification is scheduled to be sent in 120 seconds minus the amount of time he is away\r\n- When the user is offline the notification is scheduled to be sent right away\r\n- When the user reads a channel all the notifications for that user are removed (clear queue)\r\n- When a notification is processed to be sent to a user and there are other scheduled notifications:\r\n - All the scheduled notifications for that user are rescheduled to now\r\n - The current notification goes back to the queue to be processed ordered by creation date",
+ "milestone": "3.2.0",
+ "contributors": [
+ "rodrigok"
+ ]
+ },
+ {
+ "pr": "17372",
+ "title": "[NEW] Error page when browser is not supported",
+ "userLogin": "ggazzo",
+ "milestone": "3.2.0",
+ "contributors": [
+ "ggazzo",
+ "rodrigok"
+ ]
+ },
+ {
+ "pr": "17141",
+ "title": "[NEW] [ENTERPRISE] Omnichannel queue priorities",
+ "userLogin": "MarcosSpessatto",
+ "milestone": "3.2.0",
+ "contributors": [
+ "renatobecker",
+ "MarcosSpessatto",
+ "web-flow"
+ ]
+ },
+ {
+ "pr": "15488",
+ "title": "[IMPROVE] Change the SAML metadata order to conform to XSD specification",
+ "userLogin": "fcrespo82",
+ "milestone": "3.2.0",
+ "contributors": [
+ "fcrespo82",
+ "web-flow",
+ "sampaiodiego"
+ ]
+ },
+ {
+ "pr": "9995",
+ "title": "[IMPROVE] Filter markdown in notifications",
+ "userLogin": "c0dzilla",
+ "milestone": "3.2.0",
+ "contributors": [
+ "c0dzilla",
+ "rodrigok",
+ "web-flow"
+ ]
+ },
+ {
+ "pr": "16056",
+ "title": "[IMPROVE] User gets UI feedback when message is pinned or unpinned",
+ "userLogin": "ashwaniYDV",
+ "milestone": "3.2.0",
+ "contributors": [
+ "ashwaniYDV",
+ "web-flow",
+ "ggazzo",
+ "rodrigok"
+ ]
+ },
+ {
+ "pr": "16343",
+ "title": "Remove set as alias setting",
+ "userLogin": "sampaiodiego",
+ "milestone": "3.2.0",
+ "contributors": [
+ "sampaiodiego"
+ ]
+ },
+ {
+ "pr": "16858",
+ "title": "[IMPROVE] Add `file-title` and `file-desc` as new filter tag options on message search",
+ "userLogin": "subham103",
+ "milestone": "3.2.0",
+ "contributors": [
+ "subham103",
+ "rodrigok",
+ "web-flow"
+ ]
+ },
+ {
+ "pr": "17254",
+ "title": "[NEW] Add ability to set tags in the Omnichannel room closing dialog",
+ "userLogin": "renatobecker",
+ "milestone": "3.2.0",
+ "contributors": [
+ "renatobecker",
+ "web-flow",
+ "rodrigok"
+ ]
+ },
+ {
+ "pr": "17047",
+ "title": "[FIX] Show active admin and user account menu item",
+ "userLogin": "hullen",
+ "milestone": "3.2.0",
+ "contributors": [
+ "hullen",
+ "web-flow"
+ ]
+ },
+ {
+ "pr": "17335",
+ "title": "[NEW] [ENTERPRISE] Allows to set a group of departments accepted for forwarding chats",
+ "userLogin": "MarcosSpessatto",
+ "milestone": "3.2.0",
+ "contributors": [
+ "MarcosSpessatto",
+ "web-flow",
+ "renatobecker"
+ ]
+ },
+ {
+ "pr": "17331",
+ "title": "[FIX] Prevent user from getting stuck on login, if there is some bad fname",
+ "userLogin": "ggazzo",
+ "milestone": "3.2.0",
+ "contributors": [
+ "ggazzo"
+ ]
+ },
+ {
+ "pr": "17238",
+ "title": "[FIX] Remove properties from users.info response",
+ "userLogin": "MarcosSpessatto",
+ "milestone": "3.2.0",
+ "contributors": [
+ "MarcosSpessatto",
+ "web-flow"
+ ]
+ },
+ {
+ "pr": "16286",
+ "title": "[NEW] Allow to send Agent custom fields through the Omnichannel CRM integration",
+ "userLogin": "renatobecker",
+ "milestone": "3.2.0",
+ "contributors": [
+ "renatobecker",
+ "web-flow"
+ ]
+ },
+ {
+ "pr": "17356",
+ "title": "[FIX] Spotify embed and collapsed",
+ "userLogin": "ffauvel",
+ "milestone": "3.2.0",
+ "contributors": [
+ "ffauvel",
+ "web-flow",
+ "gabriellsh"
+ ]
+ },
+ {
+ "pr": "17338",
+ "title": "Improve: Better Push Notification code",
+ "userLogin": "rodrigok",
+ "milestone": "3.2.0",
+ "contributors": [
+ "rodrigok"
+ ]
+ },
+ {
+ "pr": "17365",
+ "title": "LingoHub based on develop",
+ "userLogin": "engelgabriel",
+ "contributors": [
+ "sampaiodiego"
+ ]
+ },
+ {
+ "pr": "17290",
+ "title": "[FIX] Allow Screensharing in BBB Iframe",
+ "userLogin": "wolbernd",
+ "contributors": [
+ "wolbernd",
+ "web-flow"
+ ]
+ },
+ {
+ "pr": "16762",
+ "title": "[NEW] Make the header for rooms clickable",
+ "userLogin": "aKn1ghtOut",
+ "milestone": "3.2.0",
+ "contributors": [
+ "aKn1ghtOut",
+ "gabriellsh"
+ ]
+ },
+ {
+ "pr": "17355",
+ "title": "Regression: Import data pagination",
+ "userLogin": "ggazzo",
+ "milestone": "3.2.0",
+ "contributors": [
+ "ggazzo",
+ "tassoevan",
+ "web-flow"
+ ]
+ },
+ {
+ "pr": "17353",
+ "title": "[NEW] Allow to set a comment when forwarding Omnichannel chats",
+ "userLogin": "renatobecker",
+ "milestone": "3.2.0",
+ "contributors": [
+ "renatobecker",
+ "web-flow"
+ ]
+ },
+ {
+ "pr": "17320",
+ "title": "[FIX] Web Client memory leak caused by the Emoji rendering",
+ "userLogin": "ggazzo",
+ "milestone": "3.1.2",
+ "contributors": [
+ "ggazzo",
+ "web-flow"
+ ]
+ },
+ {
+ "pr": "17348",
+ "title": "[FIX] Omnichannel room info panel opening whenever a message is sent",
+ "userLogin": "renatobecker",
+ "milestone": "3.1.2",
+ "contributors": [
+ "renatobecker"
+ ]
+ },
+ {
+ "pr": "17249",
+ "title": "[FIX] New user added by admin doesn't receive random password email",
+ "userLogin": "pierre-lehnen-rc",
+ "milestone": "3.1.2",
+ "contributors": [
+ "pierre-lehnen-rc"
+ ]
+ },
+ {
+ "pr": "17323",
+ "title": "Bump https-proxy-agent from 2.2.1 to 2.2.4",
+ "userLogin": "dependabot[bot]",
+ "contributors": [
+ "dependabot[bot]",
+ "web-flow"
+ ]
+ },
+ {
+ "pr": "16683",
+ "title": "[NEW] Adds ability for Rocket.Chat Apps to create discussions",
+ "userLogin": "marceloschmidt",
+ "milestone": "3.2.0",
+ "contributors": [
+ "marceloschmidt",
+ "lolimay",
+ "web-flow",
+ "d-gubert"
+ ]
+ },
+ {
+ "pr": "17347",
+ "title": "[IMPROVE] Add possibility to sort the Omnichannel current chats list by column",
+ "userLogin": "MarcosSpessatto",
+ "milestone": "3.2.0",
+ "contributors": [
+ "MarcosSpessatto"
+ ]
+ },
+ {
+ "pr": "17289",
+ "title": "[IMPROVE] Redesign Administration > Import",
+ "userLogin": "tassoevan",
+ "milestone": "3.2.0",
+ "contributors": [
+ "tassoevan"
+ ]
+ },
+ {
+ "pr": "17322",
+ "title": "Mailer Scrollbar",
+ "userLogin": "gabriellsh",
+ "contributors": [
+ "gabriellsh"
+ ]
+ },
+ {
+ "pr": "17191",
+ "title": "[IMPROVE] Administration -> Mailer Rewrite.",
+ "userLogin": "gabriellsh",
+ "milestone": "3.2.0",
+ "contributors": [
+ "gabriellsh",
+ "ggazzo",
+ "web-flow"
+ ]
+ },
+ {
+ "pr": "17321",
+ "title": "Regression: Storybook",
+ "userLogin": "gabriellsh",
+ "contributors": [
+ "gabriellsh"
+ ]
+ },
+ {
+ "pr": "15690",
+ "title": "[NEW] Feature/custom oauth mail field and interpolation for mapped fields",
+ "userLogin": "benkroeger",
+ "milestone": "3.2.0",
+ "contributors": [
+ "sampaiodiego",
+ "benkroeger",
+ "web-flow"
+ ]
+ },
+ {
+ "pr": "17226",
+ "title": "[FIX] \"Invalid Invite\" message when registration is disabled",
+ "userLogin": "pierre-lehnen-rc",
+ "contributors": [
+ "pierre-lehnen-rc",
+ "web-flow"
+ ]
+ },
+ {
+ "pr": "16536",
+ "title": "[FIX] Red color error outline is not removed after password update on profile details",
+ "userLogin": "ashwaniYDV",
+ "milestone": "3.2.0",
+ "contributors": [
+ "ashwaniYDV",
+ "gabriellsh",
+ "web-flow"
+ ]
+ },
+ {
+ "pr": "8799",
+ "title": "[FIX] Change wording to start DM from info panel",
+ "userLogin": "bryandesrosiers",
+ "milestone": "3.2.0",
+ "contributors": [
+ "ggazzo"
+ ]
+ },
+ {
+ "pr": "17305",
+ "title": "New hooks for RouterContext",
+ "userLogin": "tassoevan",
+ "milestone": "3.2.0",
+ "contributors": [
+ "tassoevan"
+ ]
+ },
+ {
+ "pr": "17278",
+ "title": "[FIX] SAML assertion signature enforcement",
+ "userLogin": "pierre-lehnen-rc",
+ "milestone": "3.1.1",
+ "contributors": [
+ "pierre-lehnen-rc",
+ "sampaiodiego"
+ ]
+ },
+ {
+ "pr": "17302",
+ "title": "[FIX] LDAP users lose session on refresh",
+ "userLogin": "pierre-lehnen-rc",
+ "milestone": "3.1.1",
+ "contributors": [
+ "pierre-lehnen-rc"
+ ]
+ },
+ {
+ "pr": "17217",
+ "title": "[NEW] Add MMS support to Voxtelesys",
+ "userLogin": "john08burke",
+ "milestone": "3.2.0",
+ "contributors": [
+ "john08burke",
+ "renatobecker",
+ "web-flow"
+ ]
+ },
+ {
+ "pr": "17198",
+ "title": "[FIX] Popover component doesn't have scroll",
+ "userLogin": "Nikhil713",
+ "milestone": "3.2.0",
+ "contributors": [
+ "Nikhil713",
+ "gabriellsh",
+ "web-flow"
+ ]
+ },
+ {
+ "pr": "17288",
+ "title": " [FIX] Omnichannel SMS / WhatsApp integration errors due to missing location data",
+ "userLogin": "renatobecker",
+ "milestone": "3.1.1",
+ "contributors": [
+ "renatobecker",
+ "web-flow"
+ ]
+ },
+ {
+ "pr": "17299",
+ "title": "[FIX] User search on directory not working correctly",
+ "userLogin": "rodrigok",
+ "milestone": "3.1.1",
+ "contributors": [
+ "rodrigok"
+ ]
+ },
+ {
+ "pr": "16313",
+ "title": "[FIX] Can not save Unread Tray Icon Alert user preference",
+ "userLogin": "taiju271",
+ "milestone": "3.2.0",
+ "contributors": [
+ "taiju271",
+ "web-flow",
+ "gabriellsh"
+ ]
+ },
+ {
+ "pr": "15997",
+ "title": "[FIX] Variable rendering problem on Import recent history page",
+ "userLogin": "ritwizsinha",
+ "contributors": [
+ "ritwizsinha",
+ "gabriellsh",
+ "web-flow"
+ ]
+ },
+ {
+ "pr": "16215",
+ "title": "[FIX] Admin panel custom sounds, multiple sound playback fix and added single play/pause button",
+ "userLogin": "ashwaniYDV",
+ "contributors": [
+ "ashwaniYDV",
+ "web-flow"
+ ]
+ },
+ {
+ "pr": "17282",
+ "title": "[FIX] Discussions created from inside DMs were not working and some errors accessing recently created rooms",
+ "userLogin": "rodrigok",
+ "milestone": "3.1.1",
+ "contributors": [
+ "rodrigok",
+ "sampaiodiego"
+ ]
+ },
+ {
+ "pr": "16742",
+ "title": "[FIX] Translation for nl",
+ "userLogin": "CC007",
+ "contributors": [
+ "CC007",
+ "web-flow"
+ ]
+ },
+ {
+ "pr": "16534",
+ "title": "[FIX] No maxlength(120) defined for custom user status",
+ "userLogin": "ashwaniYDV",
+ "contributors": [
+ "ashwaniYDV",
+ "gabriellsh",
+ "web-flow"
+ ]
+ },
+ {
+ "pr": "16606",
+ "title": "[FIX] Fixed email sort button in directory -> users",
+ "userLogin": "ashwaniYDV",
+ "milestone": "3.2.0",
+ "contributors": [
+ "ashwaniYDV",
+ "ggazzo",
+ "web-flow"
+ ]
+ },
+ {
+ "pr": "16405",
+ "title": "[FIX] In Create a New Channel, input should be focused on channel name instead of invite users",
+ "userLogin": "ashwaniYDV",
+ "contributors": [
+ "ashwaniYDV",
+ "web-flow"
+ ]
+ },
+ {
+ "pr": "16236",
+ "title": "[FIX] Email not verified message",
+ "userLogin": "gabriellsh",
+ "milestone": "3.2.0",
+ "contributors": [
+ "gabriellsh",
+ "ggazzo",
+ "web-flow"
+ ]
+ },
+ {
+ "pr": "17283",
+ "title": "[FIX] Directory default tab",
+ "userLogin": "ggazzo",
+ "milestone": "3.1.1",
+ "contributors": [
+ "ggazzo"
+ ]
+ },
+ {
+ "pr": "16869",
+ "title": "[FIX] Update ru.i18n.json",
+ "userLogin": "1rV1N-git",
+ "contributors": [
+ "1rV1N-git",
+ "web-flow"
+ ]
+ },
+ {
+ "pr": "17286",
+ "title": "[FIX] Avatar on sidebar when showing real names",
+ "userLogin": "ggazzo",
+ "milestone": "3.1.1",
+ "contributors": [
+ "ggazzo"
+ ]
+ },
+ {
+ "pr": "17287",
+ "title": "Update Apps-Engine to stable version",
+ "userLogin": "d-gubert",
+ "milestone": "3.1.1",
+ "contributors": [
+ "d-gubert"
+ ]
+ },
+ {
+ "pr": "17055",
+ "title": "[NEW][ENTERPRISE] Auto close abandoned Omnichannel rooms",
+ "userLogin": "MarcosSpessatto",
+ "milestone": "3.2.0",
+ "contributors": [
+ "MarcosSpessatto",
+ "web-flow",
+ "renatobecker"
+ ]
+ },
+ {
+ "pr": "17285",
+ "title": "Static props for Administration route components",
+ "userLogin": "tassoevan",
+ "milestone": "3.2.0",
+ "contributors": [
+ "tassoevan"
+ ]
+ },
+ {
+ "pr": "16025",
+ "title": "[NEW] Default favorite channels",
+ "userLogin": "gabriellsh",
+ "milestone": "3.2.0",
+ "contributors": [
+ "gabriellsh",
+ "web-flow",
+ "ggazzo"
+ ]
+ },
+ {
+ "pr": "17280",
+ "title": "Apply $and helper to message template",
+ "userLogin": "tassoevan",
+ "milestone": "3.2.0",
+ "contributors": [
+ "tassoevan"
+ ]
+ },
+ {
+ "pr": "17222",
+ "title": "[NEW] Enable the IDP to choose the best authnContext",
+ "userLogin": "felipecrp",
+ "contributors": [
+ "felipecrp",
+ "pierre-lehnen-rc",
+ "web-flow"
+ ]
+ },
+ {
+ "pr": "17130",
+ "title": "[NEW] Support importing Slack threads",
+ "userLogin": "lpilz",
+ "contributors": [
+ "lpilz",
+ "web-flow"
+ ]
+ },
+ {
+ "pr": "17107",
+ "title": "Upgrade file storage packages",
+ "userLogin": "sampaiodiego",
+ "milestone": "3.2.0",
+ "contributors": [
+ "sampaiodiego",
+ "ggazzo",
+ "web-flow"
+ ]
+ },
+ {
+ "pr": "17274",
+ "title": "LingoHub based on develop",
+ "userLogin": "engelgabriel",
+ "contributors": [
+ "sampaiodiego",
+ "web-flow"
+ ]
+ },
+ {
+ "pr": "17275",
+ "title": "[FIX] 404 error when clicking an username",
+ "userLogin": "pierre-lehnen-rc",
+ "milestone": "3.1.1",
+ "contributors": [
+ "pierre-lehnen-rc"
+ ]
+ },
+ {
+ "pr": "16771",
+ "title": "[FIX] Global event click-message-link not fired",
+ "userLogin": "tassoevan",
+ "milestone": "3.1.0",
+ "contributors": [
+ "tassoevan"
+ ]
+ },
+ {
+ "pr": "16806",
+ "title": "[NEW] Add Color variable to left sidebar",
+ "userLogin": "MartinSchoeler",
+ "contributors": [
+ "MartinSchoeler"
+ ]
+ },
+ {
+ "pr": "16887",
+ "title": "[FIX] Search valid for emoji with dual name",
+ "userLogin": "subham103",
+ "contributors": [
+ "subham103"
+ ]
+ },
+ {
+ "pr": "16959",
+ "title": "[FIX] Threads: Hide Usernames hides Full names.",
+ "userLogin": "gabriellsh",
+ "contributors": [
+ "gabriellsh",
+ "web-flow"
+ ]
+ },
+ {
+ "pr": "17207",
+ "title": "[NEW] Buttons to check/uncheck all users and channels on import",
+ "userLogin": "pierre-lehnen-rc",
+ "contributors": [
+ "pierre-lehnen-rc",
+ "web-flow"
+ ]
+ },
+ {
+ "pr": "17261",
+ "title": "[IMPROVE] Move CSS imports to `/app` modules",
+ "userLogin": "tassoevan",
+ "milestone": "3.2.0",
+ "contributors": [
+ "tassoevan",
+ "ggazzo",
+ "web-flow"
+ ]
+ },
+ {
+ "pr": "17265",
+ "title": "[FIX] Unsafe React portals mount/unmount ",
+ "userLogin": "tassoevan",
+ "milestone": "3.2.0",
+ "contributors": [
+ "tassoevan"
+ ]
+ },
+ {
+ "pr": "17266",
+ "title": "[CHORE] Move polyfills to client/",
+ "userLogin": "tassoevan",
+ "milestone": "3.2.0",
+ "contributors": [
+ "tassoevan"
+ ]
+ },
+ {
+ "pr": "17241",
+ "title": "Merge master into develop & Set version to 3.2.0-develop",
+ "userLogin": "sampaiodiego",
+ "contributors": [
+ "sampaiodiego",
+ "web-flow",
+ "1rV1N-git",
+ "rodrigok",
+ "d-gubert",
+ "renatobecker"
+ ]
+ },
+ {
+ "pr": "17209",
+ "title": "[IMPROVE] Administration Pages root rewritten",
+ "userLogin": "tassoevan",
+ "contributors": [
+ "tassoevan",
+ "ggazzo",
+ "web-flow"
+ ]
+ },
+ {
+ "pr": "16642",
+ "title": "[IMPROVE] Increase decoupling between React components and Blaze templates",
+ "userLogin": "tassoevan",
+ "milestone": "3.1.0",
+ "contributors": [
+ "tassoevan",
+ "ggazzo",
+ "web-flow"
+ ]
+ }
+ ]
+ },
+ "3.2.0-rc.1": {
+ "node_version": "12.16.1",
+ "npm_version": "6.13.4",
+ "mongo_versions": [
+ "3.4",
+ "3.6",
+ "4.0"
+ ],
+ "pull_requests": [
+ {
+ "pr": "17391",
+ "title": "Regression: Fix mem usage with more than one argument",
+ "userLogin": "sampaiodiego",
+ "contributors": [
+ "sampaiodiego"
+ ]
+ },
+ {
+ "pr": "17378",
+ "title": "[FIX] 2FA not showing codes for Spanish translation",
+ "userLogin": "RavenSystem",
+ "milestone": "3.2.0",
+ "contributors": [
+ "RavenSystem",
+ "web-flow"
+ ]
+ }
+ ]
+ },
+ "3.2.0-rc.2": {
+ "pull_requests": [
+ {
+ "pr": "17453",
+ "title": "[FIX] LDAP error when trying to add room with spaces in the name",
+ "userLogin": "pierre-lehnen-rc",
+ "milestone": "3.1.2",
+ "contributors": [
+ "pierre-lehnen-rc"
+ ]
+ },
+ {
+ "pr": "17393",
+ "title": "Complement Guest role restrictions for Enterprise",
+ "userLogin": "pierre-lehnen-rc",
+ "contributors": [
+ "pierre-lehnen-rc",
+ "sampaiodiego",
+ "web-flow"
+ ]
+ },
+ {
+ "pr": "17430",
+ "title": "Regression: Add missing cacheKey to mem",
+ "userLogin": "sampaiodiego",
+ "milestone": "3.1.2",
+ "contributors": [
+ "sampaiodiego"
+ ]
+ },
+ {
+ "pr": "17428",
+ "title": "Remove `@typescript-eslint/explicit-function-return-type` rule",
+ "userLogin": "sampaiodiego",
+ "contributors": [
+ "sampaiodiego"
+ ]
+ },
+ {
+ "pr": "17422",
+ "title": "[FIX] Empty Incoming webhook script field ",
+ "userLogin": "ggazzo",
+ "milestone": "3.1.2",
+ "contributors": [
+ "ggazzo"
+ ]
+ },
+ {
+ "pr": "17417",
+ "title": "[FIX] LDAP Sync error",
+ "userLogin": "fastrde",
+ "milestone": "3.1.2",
+ "contributors": [
+ "fastrde"
+ ]
+ },
+ {
+ "pr": "17404",
+ "title": "[FIX] Bot Agents not being able to get Omnichannel Inquiries",
+ "userLogin": "renatobecker",
+ "milestone": "3.1.2",
+ "contributors": [
+ "renatobecker"
+ ]
+ },
+ {
+ "pr": "17406",
+ "title": "[FIX] Allowing blocking a user on channels",
+ "userLogin": "sampaiodiego",
+ "milestone": "3.1.2",
+ "contributors": [
+ "sampaiodiego"
+ ]
+ },
+ {
+ "pr": "17297",
+ "title": "Fix moving-to-a-single-codebase link in README",
+ "userLogin": "Krinkle",
+ "contributors": [
+ "Krinkle",
+ "web-flow"
+ ]
+ }
+ ]
+ },
"3.1.2": {
"pull_requests": [
+ {
+ "pr": "17454",
+ "title": "Release 3.1.2",
+ "userLogin": "sampaiodiego",
+ "contributors": [
+ "pierre-lehnen-rc",
+ "sampaiodiego",
+ "renatobecker",
+ "ggazzo",
+ "fastrde"
+ ]
+ },
+ {
+ "pr": "17453",
+ "title": "[FIX] LDAP error when trying to add room with spaces in the name",
+ "userLogin": "pierre-lehnen-rc",
+ "milestone": "3.1.2",
+ "contributors": [
+ "pierre-lehnen-rc"
+ ]
+ },
+ {
+ "pr": "17430",
+ "title": "Regression: Add missing cacheKey to mem",
+ "userLogin": "sampaiodiego",
+ "milestone": "3.1.2",
+ "contributors": [
+ "sampaiodiego"
+ ]
+ },
+ {
+ "pr": "17422",
+ "title": "[FIX] Empty Incoming webhook script field ",
+ "userLogin": "ggazzo",
+ "milestone": "3.1.2",
+ "contributors": [
+ "ggazzo"
+ ]
+ },
+ {
+ "pr": "17417",
+ "title": "[FIX] LDAP Sync error",
+ "userLogin": "fastrde",
+ "milestone": "3.1.2",
+ "contributors": [
+ "fastrde"
+ ]
+ },
+ {
+ "pr": "17404",
+ "title": "[FIX] Bot Agents not being able to get Omnichannel Inquiries",
+ "userLogin": "renatobecker",
+ "milestone": "3.1.2",
+ "contributors": [
+ "renatobecker"
+ ]
+ },
+ {
+ "pr": "17406",
+ "title": "[FIX] Allowing blocking a user on channels",
+ "userLogin": "sampaiodiego",
+ "milestone": "3.1.2",
+ "contributors": [
+ "sampaiodiego"
+ ]
+ },
+ {
+ "pr": "17391",
+ "title": "Regression: Fix mem usage with more than one argument",
+ "userLogin": "sampaiodiego",
+ "milestone": "3.1.2",
+ "contributors": [
+ "sampaiodiego"
+ ]
+ },
+ {
+ "pr": "17320",
+ "title": "[FIX] Web Client memory leak caused by the Emoji rendering",
+ "userLogin": "ggazzo",
+ "milestone": "3.1.2",
+ "contributors": [
+ "ggazzo",
+ "web-flow"
+ ]
+ },
+ {
+ "pr": "17348",
+ "title": "[FIX] Omnichannel room info panel opening whenever a message is sent",
+ "userLogin": "renatobecker",
+ "milestone": "3.1.2",
+ "contributors": [
+ "renatobecker"
+ ]
+ },
+ {
+ "pr": "17249",
+ "title": "[FIX] New user added by admin doesn't receive random password email",
+ "userLogin": "pierre-lehnen-rc",
+ "milestone": "3.1.2",
+ "contributors": [
+ "pierre-lehnen-rc"
+ ]
+ }
+ ]
+ },
+ "3.2.0": {
+ "pull_requests": [
+ {
+ "pr": "17454",
+ "title": "Release 3.1.2",
+ "userLogin": "sampaiodiego",
+ "contributors": [
+ "pierre-lehnen-rc",
+ "sampaiodiego",
+ "renatobecker",
+ "ggazzo",
+ "fastrde"
+ ]
+ },
{
"pr": "17453",
"title": "[FIX] LDAP error when trying to add room with spaces in the name",
diff --git a/.github/workflows/build_and_test.yml b/.github/workflows/build_and_test.yml
index 6d0c06c5f360..2d74d00f0e17 100644
--- a/.github/workflows/build_and_test.yml
+++ b/.github/workflows/build_and_test.yml
@@ -250,6 +250,14 @@ jobs:
steps:
- uses: actions/checkout@v1
+ - name: Free disk space
+ run: |
+ sudo swapoff -a
+ sudo rm -f /swapfile
+ sudo apt clean
+ docker rmi $(docker image ls -aq)
+ df -h
+
- name: Cache node modules
id: cache-nodemodules
uses: actions/cache@v1
diff --git a/.meteor/packages b/.meteor/packages
index e1895f9bc43e..84ee48c2bc9e 100644
--- a/.meteor/packages
+++ b/.meteor/packages
@@ -52,8 +52,8 @@ konecty:user-presence
deepwell:bootstrap-datepicker2
dispatch:run-as-user
-jalik:ufs
-jalik:ufs-gridfs
+jalik:ufs@1.0.2
+jalik:ufs-gridfs@1.0.2
jparker:gravatar
kadira:blaze-layout
kadira:flow-router
@@ -66,9 +66,7 @@ nooitaf:colors
ostrio:cookies
pauli:accounts-linkedin
raix:handlebar-helpers
-rocketchat:push
raix:ui-dropped-event
-todda00:friendly-slugs
rocketchat:tap-i18n
underscore@1.0.10
@@ -76,7 +74,7 @@ juliancwirko:postcss
littledata:synced-cron
edgee:slingshot
-jalik:ufs-local@0.2.5
+jalik:ufs-local@1.0.2
accounts-base@1.5.0
accounts-oauth@1.1.16
autoupdate@1.6.0
@@ -88,7 +86,6 @@ matb33:collection-hooks
meteorhacks:inject-initial
oauth@1.2.8
oauth2@1.2.1
-raix:eventemitter
routepolicy@1.1.0
sha@1.0.9
templating
diff --git a/.meteor/versions b/.meteor/versions
index ee1d117926a6..02e97dbc384a 100644
--- a/.meteor/versions
+++ b/.meteor/versions
@@ -55,9 +55,9 @@ htmljs@1.0.11
http@1.4.2
id-map@1.1.0
inter-process-messaging@0.1.0
-jalik:ufs@0.7.5
-jalik:ufs-gridfs@0.2.1
-jalik:ufs-local@0.2.9
+jalik:ufs@1.0.2
+jalik:ufs-gridfs@1.0.2
+jalik:ufs-local@1.0.2
jparker:crypto-core@0.1.0
jparker:crypto-md5@0.1.1
jparker:gravatar@0.5.1
@@ -77,7 +77,7 @@ littledata:synced-cron@1.5.1
livedata@1.0.18
localstorage@1.2.0
logging@1.1.20
-matb33:collection-hooks@0.9.1
+matb33:collection-hooks@1.0.1
mdg:validation-error@0.5.1
meteor@1.9.3
meteor-base@1.4.0
@@ -114,7 +114,6 @@ pauli:accounts-linkedin@5.0.0
pauli:linkedin-oauth@5.0.0
promise@0.11.2
raix:eventemitter@0.1.3
-raix:eventstate@0.0.4
raix:handlebar-helpers@0.2.5
raix:ui-dropped-event@0.0.7
random@1.1.0
@@ -127,7 +126,6 @@ rocketchat:i18n@0.0.1
rocketchat:livechat@0.0.1
rocketchat:mongo-config@0.0.1
rocketchat:oauth2-server@2.1.0
-rocketchat:push@3.3.1
rocketchat:streamer@1.1.0
rocketchat:tap-i18n@1.9.1
rocketchat:version@1.0.0
@@ -147,7 +145,6 @@ templating-compiler@1.3.3
templating-runtime@1.3.2
templating-tools@1.1.2
tmeasday:check-npm-versions@0.3.2
-todda00:friendly-slugs@0.6.0
tracker@1.2.0
twitter-oauth@1.2.0
typescript@3.7.5
diff --git a/.storybook/.babelrc b/.storybook/.babelrc
index 826e22a858c5..97ab12b7f950 100644
--- a/.storybook/.babelrc
+++ b/.storybook/.babelrc
@@ -13,6 +13,7 @@
"@babel/preset-flow"
],
"plugins": [
- "@babel/plugin-proposal-class-properties"
+ "@babel/plugin-proposal-class-properties",
+ "@babel/plugin-proposal-optional-chaining"
]
}
diff --git a/.storybook/config.js b/.storybook/config.js
index 420f7df4a6dc..c7fb95b21240 100644
--- a/.storybook/config.js
+++ b/.storybook/config.js
@@ -1,16 +1,12 @@
import { withKnobs } from '@storybook/addon-knobs';
-import { MINIMAL_VIEWPORTS, INITIAL_VIEWPORTS } from '@storybook/addon-viewport/dist/defaults';
+import { MINIMAL_VIEWPORTS } from '@storybook/addon-viewport/dist/defaults';
import { addDecorator, addParameters, configure } from '@storybook/react';
import { rocketChatDecorator } from './mocks/decorators';
addParameters({
viewport: {
- viewports: {
- ...MINIMAL_VIEWPORTS,
- ...INITIAL_VIEWPORTS,
- },
- defaultViewport: 'responsive',
+ viewports: MINIMAL_VIEWPORTS,
},
});
diff --git a/.storybook/mocks/decorators.js b/.storybook/mocks/decorators.js
index 8bf073a3b108..34a326f11077 100644
--- a/.storybook/mocks/decorators.js
+++ b/.storybook/mocks/decorators.js
@@ -7,7 +7,7 @@ export const rocketChatDecorator = (fn) => {
if (linkElement.id !== 'theme-styles') {
require('../../app/theme/client/main.css');
require('../../app/theme/client/vendor/fontello/css/fontello.css');
- require('../../client/rocketchat.font.css');
+ require('../../app/theme/client/rocketchat.font.css');
linkElement.setAttribute('id', 'theme-styles');
linkElement.setAttribute('rel', 'stylesheet');
linkElement.setAttribute('href', 'https://open.rocket.chat/theme.css');
diff --git a/.storybook/mocks/meteor.js b/.storybook/mocks/meteor.js
index e4a5761e50d2..d85c752a2b2b 100644
--- a/.storybook/mocks/meteor.js
+++ b/.storybook/mocks/meteor.js
@@ -64,6 +64,9 @@ export const check = () => {};
export const FlowRouter = {
route: () => {},
+ group: () => ({
+ route: () => {},
+ }),
};
export const BlazeLayout = {};
diff --git a/HISTORY.md b/HISTORY.md
index 9197d657fe1f..bc0b76f43333 100644
--- a/HISTORY.md
+++ b/HISTORY.md
@@ -1,6 +1,240 @@
+# 3.2.0
+`2020-04-27 · 19 🎉 · 10 🚀 · 34 🐛 · 19 🔍 · 34 👩💻👨💻`
+
+### 🎉 New features
+
+
+- **ENTERPRISE:** Restrict the permissions configuration for guest users ([#17333](https://github.com/RocketChat/Rocket.Chat/pull/17333))
+
+ The **Guest** role is blocked for edition on the EE version. This will allow the EE customers to receive licenses with extra seats for Guests for free. The CE version continues to have the Guest role configurable.
+
+- **ENTERPRISE:** Omnichannel queue priorities ([#17141](https://github.com/RocketChat/Rocket.Chat/pull/17141))
+
+- **ENTERPRISE:** Allows to set a group of departments accepted for forwarding chats ([#17335](https://github.com/RocketChat/Rocket.Chat/pull/17335))
+
+- **ENTERPRISE:** Auto close abandoned Omnichannel rooms ([#17055](https://github.com/RocketChat/Rocket.Chat/pull/17055))
+
+- Federation event for when users left rooms ([#17091](https://github.com/RocketChat/Rocket.Chat/pull/17091))
+
+- Better Push and Email Notification logic ([#17357](https://github.com/RocketChat/Rocket.Chat/pull/17357))
+
+ We are still using the same logic to define which notifications every new message will generate, it takes some servers' settings, users's preferences and subscriptions' settings in consideration to determine who will receive each notification type (desktop, audio, email and mobile push), but now it doesn't check the user's status (online, away, offline) for email and mobile push notifications but send those notifications to a new queue with the following rules:
+
+ - When the user is online the notification is scheduled to be sent in 120 seconds
+ - When the user is away the notification is scheduled to be sent in 120 seconds minus the amount of time he is away
+ - When the user is offline the notification is scheduled to be sent right away
+ - When the user reads a channel all the notifications for that user are removed (clear queue)
+ - When a notification is processed to be sent to a user and there are other scheduled notifications:
+ - All the scheduled notifications for that user are rescheduled to now
+ - The current notification goes back to the queue to be processed ordered by creation date
+
+- Error page when browser is not supported ([#17372](https://github.com/RocketChat/Rocket.Chat/pull/17372))
+
+- Add ability to set tags in the Omnichannel room closing dialog ([#17254](https://github.com/RocketChat/Rocket.Chat/pull/17254))
+
+- Allow to send Agent custom fields through the Omnichannel CRM integration ([#16286](https://github.com/RocketChat/Rocket.Chat/pull/16286))
+
+- Make the header for rooms clickable ([#16762](https://github.com/RocketChat/Rocket.Chat/pull/16762) by [@aKn1ghtOut](https://github.com/aKn1ghtOut))
+
+- Allow to set a comment when forwarding Omnichannel chats ([#17353](https://github.com/RocketChat/Rocket.Chat/pull/17353))
+
+- Adds ability for Rocket.Chat Apps to create discussions ([#16683](https://github.com/RocketChat/Rocket.Chat/pull/16683))
+
+- Feature/custom oauth mail field and interpolation for mapped fields ([#15690](https://github.com/RocketChat/Rocket.Chat/pull/15690) by [@benkroeger](https://github.com/benkroeger))
+
+- Add MMS support to Voxtelesys ([#17217](https://github.com/RocketChat/Rocket.Chat/pull/17217) by [@john08burke](https://github.com/john08burke))
+
+- Default favorite channels ([#16025](https://github.com/RocketChat/Rocket.Chat/pull/16025))
+
+- Enable the IDP to choose the best authnContext ([#17222](https://github.com/RocketChat/Rocket.Chat/pull/17222) by [@felipecrp](https://github.com/felipecrp))
+
+- Support importing Slack threads ([#17130](https://github.com/RocketChat/Rocket.Chat/pull/17130) by [@lpilz](https://github.com/lpilz))
+
+- Add Color variable to left sidebar ([#16806](https://github.com/RocketChat/Rocket.Chat/pull/16806))
+
+- Buttons to check/uncheck all users and channels on import ([#17207](https://github.com/RocketChat/Rocket.Chat/pull/17207))
+
+### 🚀 Improvements
+
+
+- Change the SAML metadata order to conform to XSD specification ([#15488](https://github.com/RocketChat/Rocket.Chat/pull/15488) by [@fcrespo82](https://github.com/fcrespo82))
+
+- Filter markdown in notifications ([#9995](https://github.com/RocketChat/Rocket.Chat/pull/9995) by [@c0dzilla](https://github.com/c0dzilla))
+
+- User gets UI feedback when message is pinned or unpinned ([#16056](https://github.com/RocketChat/Rocket.Chat/pull/16056) by [@ashwaniYDV](https://github.com/ashwaniYDV))
+
+- Add `file-title` and `file-desc` as new filter tag options on message search ([#16858](https://github.com/RocketChat/Rocket.Chat/pull/16858) by [@subham103](https://github.com/subham103))
+
+- Add possibility to sort the Omnichannel current chats list by column ([#17347](https://github.com/RocketChat/Rocket.Chat/pull/17347))
+
+- Redesign Administration > Import ([#17289](https://github.com/RocketChat/Rocket.Chat/pull/17289))
+
+- Administration -> Mailer Rewrite. ([#17191](https://github.com/RocketChat/Rocket.Chat/pull/17191))
+
+- Move CSS imports to `/app` modules ([#17261](https://github.com/RocketChat/Rocket.Chat/pull/17261))
+
+- Administration Pages root rewritten ([#17209](https://github.com/RocketChat/Rocket.Chat/pull/17209))
+
+- Increase decoupling between React components and Blaze templates ([#16642](https://github.com/RocketChat/Rocket.Chat/pull/16642))
+
+### 🐛 Bug fixes
+
+
+- CSV Importer fails when there are no users to import ([#16790](https://github.com/RocketChat/Rocket.Chat/pull/16790))
+
+- Import slack's multiple direct messages as direct rooms instead of private groups ([#17206](https://github.com/RocketChat/Rocket.Chat/pull/17206))
+
+- SAML Idp Initiated Logout Error ([#17324](https://github.com/RocketChat/Rocket.Chat/pull/17324))
+
+- Show active admin and user account menu item ([#17047](https://github.com/RocketChat/Rocket.Chat/pull/17047) by [@hullen](https://github.com/hullen))
+
+- Prevent user from getting stuck on login, if there is some bad fname ([#17331](https://github.com/RocketChat/Rocket.Chat/pull/17331))
+
+- Remove properties from users.info response ([#17238](https://github.com/RocketChat/Rocket.Chat/pull/17238))
+
+- Spotify embed and collapsed ([#17356](https://github.com/RocketChat/Rocket.Chat/pull/17356) by [@ffauvel](https://github.com/ffauvel))
+
+- Allow Screensharing in BBB Iframe ([#17290](https://github.com/RocketChat/Rocket.Chat/pull/17290) by [@wolbernd](https://github.com/wolbernd))
+
+- "Invalid Invite" message when registration is disabled ([#17226](https://github.com/RocketChat/Rocket.Chat/pull/17226))
+
+- Red color error outline is not removed after password update on profile details ([#16536](https://github.com/RocketChat/Rocket.Chat/pull/16536) by [@ashwaniYDV](https://github.com/ashwaniYDV))
+
+- Change wording to start DM from info panel ([#8799](https://github.com/RocketChat/Rocket.Chat/pull/8799))
+
+- SAML assertion signature enforcement ([#17278](https://github.com/RocketChat/Rocket.Chat/pull/17278))
+
+- LDAP users lose session on refresh ([#17302](https://github.com/RocketChat/Rocket.Chat/pull/17302))
+
+- Popover component doesn't have scroll ([#17198](https://github.com/RocketChat/Rocket.Chat/pull/17198) by [@Nikhil713](https://github.com/Nikhil713))
+
+- Omnichannel SMS / WhatsApp integration errors due to missing location data ([#17288](https://github.com/RocketChat/Rocket.Chat/pull/17288))
+
+- User search on directory not working correctly ([#17299](https://github.com/RocketChat/Rocket.Chat/pull/17299))
+
+- Can not save Unread Tray Icon Alert user preference ([#16313](https://github.com/RocketChat/Rocket.Chat/pull/16313) by [@taiju271](https://github.com/taiju271))
+
+- Variable rendering problem on Import recent history page ([#15997](https://github.com/RocketChat/Rocket.Chat/pull/15997) by [@ritwizsinha](https://github.com/ritwizsinha))
+
+- Admin panel custom sounds, multiple sound playback fix and added single play/pause button ([#16215](https://github.com/RocketChat/Rocket.Chat/pull/16215) by [@ashwaniYDV](https://github.com/ashwaniYDV))
+
+- Discussions created from inside DMs were not working and some errors accessing recently created rooms ([#17282](https://github.com/RocketChat/Rocket.Chat/pull/17282))
+
+- Translation for nl ([#16742](https://github.com/RocketChat/Rocket.Chat/pull/16742) by [@CC007](https://github.com/CC007))
+
+- No maxlength(120) defined for custom user status ([#16534](https://github.com/RocketChat/Rocket.Chat/pull/16534) by [@ashwaniYDV](https://github.com/ashwaniYDV))
+
+- Fixed email sort button in directory -> users ([#16606](https://github.com/RocketChat/Rocket.Chat/pull/16606) by [@ashwaniYDV](https://github.com/ashwaniYDV))
+
+- In Create a New Channel, input should be focused on channel name instead of invite users ([#16405](https://github.com/RocketChat/Rocket.Chat/pull/16405) by [@ashwaniYDV](https://github.com/ashwaniYDV))
+
+- Email not verified message ([#16236](https://github.com/RocketChat/Rocket.Chat/pull/16236))
+
+- Directory default tab ([#17283](https://github.com/RocketChat/Rocket.Chat/pull/17283))
+
+- Update ru.i18n.json ([#16869](https://github.com/RocketChat/Rocket.Chat/pull/16869) by [@1rV1N-git](https://github.com/1rV1N-git))
+
+- Avatar on sidebar when showing real names ([#17286](https://github.com/RocketChat/Rocket.Chat/pull/17286))
+
+- 404 error when clicking an username ([#17275](https://github.com/RocketChat/Rocket.Chat/pull/17275))
+
+- Global event click-message-link not fired ([#16771](https://github.com/RocketChat/Rocket.Chat/pull/16771))
+
+- Search valid for emoji with dual name ([#16887](https://github.com/RocketChat/Rocket.Chat/pull/16887) by [@subham103](https://github.com/subham103))
+
+- Threads: Hide Usernames hides Full names. ([#16959](https://github.com/RocketChat/Rocket.Chat/pull/16959))
+
+- Unsafe React portals mount/unmount ([#17265](https://github.com/RocketChat/Rocket.Chat/pull/17265))
+
+- 2FA not showing codes for Spanish translation ([#17378](https://github.com/RocketChat/Rocket.Chat/pull/17378) by [@RavenSystem](https://github.com/RavenSystem))
+
+
+🔍 Minor changes
+
+
+- Release 3.1.2 ([#17454](https://github.com/RocketChat/Rocket.Chat/pull/17454) by [@fastrde](https://github.com/fastrde))
+
+- Remove set as alias setting ([#16343](https://github.com/RocketChat/Rocket.Chat/pull/16343))
+
+- Improve: Better Push Notification code ([#17338](https://github.com/RocketChat/Rocket.Chat/pull/17338))
+
+- LingoHub based on develop ([#17365](https://github.com/RocketChat/Rocket.Chat/pull/17365))
+
+- Regression: Import data pagination ([#17355](https://github.com/RocketChat/Rocket.Chat/pull/17355))
+
+- Bump https-proxy-agent from 2.2.1 to 2.2.4 ([#17323](https://github.com/RocketChat/Rocket.Chat/pull/17323) by [@dependabot[bot]](https://github.com/dependabot[bot]))
+
+- Mailer Scrollbar ([#17322](https://github.com/RocketChat/Rocket.Chat/pull/17322))
+
+- Regression: Storybook ([#17321](https://github.com/RocketChat/Rocket.Chat/pull/17321))
+
+- New hooks for RouterContext ([#17305](https://github.com/RocketChat/Rocket.Chat/pull/17305))
+
+- Update Apps-Engine to stable version ([#17287](https://github.com/RocketChat/Rocket.Chat/pull/17287))
+
+- Static props for Administration route components ([#17285](https://github.com/RocketChat/Rocket.Chat/pull/17285))
+
+- Apply $and helper to message template ([#17280](https://github.com/RocketChat/Rocket.Chat/pull/17280))
+
+- Upgrade file storage packages ([#17107](https://github.com/RocketChat/Rocket.Chat/pull/17107))
+
+- LingoHub based on develop ([#17274](https://github.com/RocketChat/Rocket.Chat/pull/17274))
+
+- [CHORE] Move polyfills to client/ ([#17266](https://github.com/RocketChat/Rocket.Chat/pull/17266))
+
+- Merge master into develop & Set version to 3.2.0-develop ([#17241](https://github.com/RocketChat/Rocket.Chat/pull/17241) by [@1rV1N-git](https://github.com/1rV1N-git))
+
+- Complement Guest role restrictions for Enterprise ([#17393](https://github.com/RocketChat/Rocket.Chat/pull/17393))
+
+- Remove `@typescript-eslint/explicit-function-return-type` rule ([#17428](https://github.com/RocketChat/Rocket.Chat/pull/17428))
+
+- Fix moving-to-a-single-codebase link in README ([#17297](https://github.com/RocketChat/Rocket.Chat/pull/17297) by [@Krinkle](https://github.com/Krinkle))
+
+
+
+### 👩💻👨💻 Contributors 😍
+
+- [@1rV1N-git](https://github.com/1rV1N-git)
+- [@CC007](https://github.com/CC007)
+- [@Krinkle](https://github.com/Krinkle)
+- [@Nikhil713](https://github.com/Nikhil713)
+- [@RavenSystem](https://github.com/RavenSystem)
+- [@aKn1ghtOut](https://github.com/aKn1ghtOut)
+- [@ashwaniYDV](https://github.com/ashwaniYDV)
+- [@benkroeger](https://github.com/benkroeger)
+- [@c0dzilla](https://github.com/c0dzilla)
+- [@dependabot[bot]](https://github.com/dependabot[bot])
+- [@fastrde](https://github.com/fastrde)
+- [@fcrespo82](https://github.com/fcrespo82)
+- [@felipecrp](https://github.com/felipecrp)
+- [@ffauvel](https://github.com/ffauvel)
+- [@hullen](https://github.com/hullen)
+- [@john08burke](https://github.com/john08burke)
+- [@lpilz](https://github.com/lpilz)
+- [@ritwizsinha](https://github.com/ritwizsinha)
+- [@subham103](https://github.com/subham103)
+- [@taiju271](https://github.com/taiju271)
+- [@wolbernd](https://github.com/wolbernd)
+
+### 👩💻👨💻 Core Team 🤓
+
+- [@MarcosSpessatto](https://github.com/MarcosSpessatto)
+- [@MartinSchoeler](https://github.com/MartinSchoeler)
+- [@alansikora](https://github.com/alansikora)
+- [@d-gubert](https://github.com/d-gubert)
+- [@gabriellsh](https://github.com/gabriellsh)
+- [@ggazzo](https://github.com/ggazzo)
+- [@lolimay](https://github.com/lolimay)
+- [@marceloschmidt](https://github.com/marceloschmidt)
+- [@pierre-lehnen-rc](https://github.com/pierre-lehnen-rc)
+- [@renatobecker](https://github.com/renatobecker)
+- [@rodrigok](https://github.com/rodrigok)
+- [@sampaiodiego](https://github.com/sampaiodiego)
+- [@tassoevan](https://github.com/tassoevan)
+
# 3.1.2
-`2020-04-27 · 8 🐛 · 2 🔍 · 5 👩💻👨💻`
+`2020-04-27 · 8 🐛 · 3 🔍 · 5 👩💻👨💻`
### 🐛 Bug fixes
@@ -25,6 +259,8 @@
🔍 Minor changes
+- Release 3.1.2 ([#17454](https://github.com/RocketChat/Rocket.Chat/pull/17454) by [@fastrde](https://github.com/fastrde))
+
- Regression: Add missing cacheKey to mem ([#17430](https://github.com/RocketChat/Rocket.Chat/pull/17430))
- Regression: Fix mem usage with more than one argument ([#17391](https://github.com/RocketChat/Rocket.Chat/pull/17391))
diff --git a/README.md b/README.md
index 86ad92ff9efa..582baf0ec5c8 100644
--- a/README.md
+++ b/README.md
@@ -13,7 +13,7 @@
[![Code Climate](https://codeclimate.com/github/RocketChat/Rocket.Chat/badges/gpa.svg)](https://codeclimate.com/github/RocketChat/Rocket.Chat)
[![MIT License](https://img.shields.io/badge/license-MIT-blue.svg?style=flat)](https://github.com/RocketChat/Rocket.Chat/raw/master/LICENSE)
-* [**NEW!** Rocket.Chat Moving to a Single Codebase](#moving-to-one-codebase)
+* [**NEW!** Rocket.Chat Moving to a Single Codebase](#moving-to-a-single-codebase)
* [Community](#community)
* [Mobile Apps](#mobile-apps)
* [Desktop Apps](#desktop-apps)
diff --git a/app/action-links/client/index.js b/app/action-links/client/index.js
index f49166a5c811..34c929c096f9 100644
--- a/app/action-links/client/index.js
+++ b/app/action-links/client/index.js
@@ -1,6 +1,7 @@
import { actionLinks } from '../both/lib/actionLinks';
import './lib/actionLinks';
import './init';
+import './stylesheets/actionLinks.css';
export {
actionLinks,
diff --git a/app/api/server/lib/rooms.js b/app/api/server/lib/rooms.js
index 52c67f0dcdc6..94eb63685f39 100644
--- a/app/api/server/lib/rooms.js
+++ b/app/api/server/lib/rooms.js
@@ -18,6 +18,7 @@ export async function findAdminRooms({ uid, filter, types = [], pagination: { of
unmuted: 1,
ro: 1,
default: 1,
+ favorite: 1,
featured: 1,
topic: 1,
msgs: 1,
diff --git a/app/api/server/v1/channels.js b/app/api/server/v1/channels.js
index 682ff697e9ab..76af348d9a6c 100644
--- a/app/api/server/v1/channels.js
+++ b/app/api/server/v1/channels.js
@@ -758,7 +758,7 @@ API.v1.addRoute('channels.setDefault', { authRequired: true }, {
}
Meteor.runAsUser(this.userId, () => {
- Meteor.call('saveRoomSettings', findResult._id, 'default', this.bodyParams.default.toString());
+ Meteor.call('saveRoomSettings', findResult._id, 'default', ['true', '1'].includes(this.bodyParams.default.toString().toLowerCase()));
});
return API.v1.success({
diff --git a/app/api/server/v1/push.js b/app/api/server/v1/push.js
index 6c1c9959011a..9e3c8d59b325 100644
--- a/app/api/server/v1/push.js
+++ b/app/api/server/v1/push.js
@@ -1,7 +1,7 @@
import { Meteor } from 'meteor/meteor';
import { Random } from 'meteor/random';
-import { Push } from 'meteor/rocketchat:push';
+import { appTokensCollection } from '../../../push/server';
import { API } from '../api';
API.v1.addRoute('push.token', { authRequired: true }, {
@@ -47,7 +47,7 @@ API.v1.addRoute('push.token', { authRequired: true }, {
throw new Meteor.Error('error-token-param-not-valid', 'The required "token" body param is missing or invalid.');
}
- const affectedRecords = Push.appCollection.remove({
+ const affectedRecords = appTokensCollection.remove({
$or: [{
'token.apn': token,
}, {
diff --git a/app/api/server/v1/users.js b/app/api/server/v1/users.js
index 4b861eb11407..0598d8a430a2 100644
--- a/app/api/server/v1/users.js
+++ b/app/api/server/v1/users.js
@@ -16,7 +16,7 @@ import {
setUserAvatar,
saveCustomFields,
} from '../../../lib';
-import { getFullUserData, getFullUserDataById } from '../../../lib/server/functions/getFullUserData';
+import { getFullUserDataByIdOrUsername } from '../../../lib/server/functions/getFullUserData';
import { API } from '../api';
import { setStatusText } from '../../../lib/server';
import { findUsersToAutocomplete } from '../lib/users';
@@ -180,20 +180,12 @@ API.v1.addRoute('users.info', { authRequired: true }, {
get() {
const { username, userId } = this.requestParams();
const { fields } = this.parseJsonQuery();
- const params = {
- userId: this.userId,
- filter: username,
- limit: 1,
- };
- const result = userId
- ? getFullUserDataById({ userId: this.userId, filterId: userId })
- : getFullUserData(params);
+ const user = getFullUserDataByIdOrUsername({ userId: this.userId, filterId: userId, filterUsername: username });
- if (!result || result.count() !== 1) {
+ if (!user) {
return API.v1.failure('User not found.');
}
- const [user] = result.fetch();
const myself = user._id === this.userId;
if (fields.userRooms === 1 && (myself || hasPermission(this.userId, 'view-other-user-channels'))) {
user.rooms = Subscriptions.findByUserId(user._id, {
diff --git a/app/apps/client/admin/helpers.js b/app/apps/client/admin/helpers.js
index ca04c715fbc5..f78b4c661599 100644
--- a/app/apps/client/admin/helpers.js
+++ b/app/apps/client/admin/helpers.js
@@ -47,7 +47,7 @@ const promptCloudLogin = () => {
html: false,
}, (confirmed) => {
if (confirmed) {
- FlowRouter.go('cloud-config');
+ FlowRouter.go('cloud');
}
});
};
diff --git a/app/apps/client/admin/marketplace.js b/app/apps/client/admin/marketplace.js
index 0907080453d2..495bda2a90ac 100644
--- a/app/apps/client/admin/marketplace.js
+++ b/app/apps/client/admin/marketplace.js
@@ -254,7 +254,7 @@ Template.marketplace.helpers({
Template.marketplace.events({
'click .js-cloud-login'() {
- FlowRouter.go('cloud-config');
+ FlowRouter.go('cloud');
},
'submit .js-search-form'(event) {
event.stopPropagation();
diff --git a/app/apps/client/orchestrator.js b/app/apps/client/orchestrator.js
index 2252c886ce9d..54c614044fe0 100644
--- a/app/apps/client/orchestrator.js
+++ b/app/apps/client/orchestrator.js
@@ -4,7 +4,7 @@ import toastr from 'toastr';
import { AppWebsocketReceiver } from './communication';
import { APIClient } from '../../utils';
-import { AdminBox } from '../../ui-utils';
+import { registerAdminSidebarItem } from '../../ui-admin/client';
import { CachedCollectionManager } from '../../ui-cached-collection';
import { hasAtLeastOnePermission } from '../../authorization';
import { handleI18nResources } from './i18n';
@@ -51,14 +51,14 @@ class AppClientOrchestrator {
getAppClientManager = () => this._manager;
registerAdminMenuItems = () => {
- AdminBox.addOption({
+ registerAdminSidebarItem({
icon: 'cube',
href: 'apps',
i18nLabel: 'Apps',
permissionGranted: () => hasAtLeastOnePermission(['manage-apps']),
});
- AdminBox.addOption({
+ registerAdminSidebarItem({
icon: 'cube',
href: 'marketplace',
i18nLabel: 'Marketplace',
diff --git a/app/apps/client/routes.js b/app/apps/client/routes.js
index 914137e5f72f..66005d26b875 100644
--- a/app/apps/client/routes.js
+++ b/app/apps/client/routes.js
@@ -1,9 +1,10 @@
import { FlowRouter } from 'meteor/kadira:flow-router';
import { BlazeLayout } from 'meteor/kadira:blaze-layout';
+import { registerAdminRoute } from '../../ui-admin/client';
import { Apps } from './orchestrator';
-FlowRouter.route('/admin/apps/what-is-it', {
+registerAdminRoute('/apps/what-is-it', {
name: 'apps-what-is-it',
action: async () => {
// TODO: render loading indicator
@@ -26,32 +27,32 @@ const createAppsRouteAction = (centerTemplate) => async () => {
}
};
-FlowRouter.route('/admin/apps', {
+registerAdminRoute('/apps', {
name: 'apps',
action: createAppsRouteAction('apps'),
});
-FlowRouter.route('/admin/apps/install', {
+registerAdminRoute('/apps/install', {
name: 'app-install',
action: createAppsRouteAction('appInstall'),
});
-FlowRouter.route('/admin/apps/:appId', {
+registerAdminRoute('/apps/:appId', {
name: 'app-manage',
action: createAppsRouteAction('appManage'),
});
-FlowRouter.route('/admin/apps/:appId/logs', {
+registerAdminRoute('/apps/:appId/logs', {
name: 'app-logs',
action: createAppsRouteAction('appLogs'),
});
-FlowRouter.route('/admin/marketplace', {
+registerAdminRoute('/marketplace', {
name: 'marketplace',
action: createAppsRouteAction('marketplace'),
});
-FlowRouter.route('/admin/marketplace/:appId', {
+registerAdminRoute('/marketplace/:appId', {
name: 'marketplace-app',
action: createAppsRouteAction('appManage'),
});
diff --git a/app/apps/server/bridges/rooms.js b/app/apps/server/bridges/rooms.js
index d8bc4e76d4ac..a91d8b2105ad 100644
--- a/app/apps/server/bridges/rooms.js
+++ b/app/apps/server/bridges/rooms.js
@@ -1,8 +1,8 @@
-import { Meteor } from 'meteor/meteor';
import { RoomType } from '@rocket.chat/apps-engine/definition/rooms';
+import { Meteor } from 'meteor/meteor';
-import { Rooms, Subscriptions, Users } from '../../../models/server';
import { addUserToRoom } from '../../../lib/server/functions/addUserToRoom';
+import { Rooms, Subscriptions, Users } from '../../../models/server';
export class AppRoomBridge {
constructor(orch) {
@@ -120,4 +120,35 @@ export class AppRoomBridge {
addUserToRoom(rm._id, member);
}
}
+
+ async createDiscussion(room, parentMessage = null, reply = '', members = [], appId) {
+ this.orch.debugLog(`The App ${ appId } is creating a new discussion.`, room);
+
+ const rcRoom = this.orch.getConverters().get('rooms').convertAppRoom(room);
+
+ let rcMessage;
+ if (parentMessage) {
+ rcMessage = this.orch.getConverters().get('messages').convertAppMessage(parentMessage);
+ }
+
+ if (!rcRoom.prid || !Rooms.findOneById(rcRoom.prid)) {
+ throw new Error('There must be a parent room to create a discussion.');
+ }
+
+ const discussion = {
+ prid: rcRoom.prid,
+ t_name: rcRoom.fname,
+ pmid: rcMessage ? rcMessage._id : undefined,
+ reply: reply && reply.trim() !== '' ? reply : undefined,
+ users: members.length > 0 ? members : [],
+ };
+
+ let rid;
+ Meteor.runAsUser(room.creator.id, () => {
+ const info = Meteor.call('createDiscussion', discussion);
+ rid = info.rid;
+ });
+
+ return rid;
+ }
}
diff --git a/app/apps/server/converters/rooms.js b/app/apps/server/converters/rooms.js
index a511f471d256..8c8aedbe1448 100644
--- a/app/apps/server/converters/rooms.js
+++ b/app/apps/server/converters/rooms.js
@@ -91,6 +91,7 @@ export class AppRoomsConverter {
closedAt: room.closedAt,
lm: room.lastModifiedAt,
customFields: room.customFields,
+ prid: typeof room.parentRoom === 'undefined' ? undefined : room.parentRoom.id,
};
return Object.assign(newRoom, room._unmappedProperties_);
@@ -195,7 +196,17 @@ export class AppRoomsConverter {
return this.orch.getConverters().get('users').convertById(responseBy._id);
},
+ parentRoom: (room) => {
+ const { prid } = room;
+ if (!prid) {
+ return undefined;
+ }
+
+ delete room.prid;
+
+ return this.orch.getConverters().get('rooms').convertById(prid);
+ },
};
return transformMappedData(room, map);
diff --git a/app/authorization/client/hasPermission.js b/app/authorization/client/hasPermission.js
index 994c363428d9..a48549979c7a 100644
--- a/app/authorization/client/hasPermission.js
+++ b/app/authorization/client/hasPermission.js
@@ -3,11 +3,19 @@ import { Template } from 'meteor/templating';
import { ChatPermissions } from './lib/ChatPermissions';
import * as Models from '../../models';
+import { AuthorizationUtils } from '../lib/AuthorizationUtils';
function atLeastOne(permissions = [], scope, userId) {
userId = userId || Meteor.userId();
+ const user = Models.Users.findOneById(userId, { fields: { roles: 1 } });
return permissions.some((permissionId) => {
+ if (user && user.roles) {
+ if (AuthorizationUtils.isPermissionRestrictedForRoleList(permissionId, user.roles)) {
+ return false;
+ }
+ }
+
const permission = ChatPermissions.findOne(permissionId, { fields: { roles: 1 } });
const roles = (permission && permission.roles) || [];
@@ -23,8 +31,15 @@ function atLeastOne(permissions = [], scope, userId) {
function all(permissions = [], scope, userId) {
userId = userId || Meteor.userId();
+ const user = Models.Users.findOneById(userId, { fields: { roles: 1 } });
return permissions.every((permissionId) => {
+ if (user && user.roles) {
+ if (AuthorizationUtils.isPermissionRestrictedForRoleList(permissionId, user.roles)) {
+ return false;
+ }
+ }
+
const permission = ChatPermissions.findOne(permissionId, { fields: { roles: 1 } });
const roles = (permission && permission.roles) || [];
diff --git a/app/authorization/client/index.js b/app/authorization/client/index.js
index 7ff5353f1f79..ca382bacc82b 100644
--- a/app/authorization/client/index.js
+++ b/app/authorization/client/index.js
@@ -1,9 +1,11 @@
import { hasAllPermission, hasAtLeastOnePermission, hasPermission, userHasAllPermission } from './hasPermission';
import { hasRole } from './hasRole';
+import { AuthorizationUtils } from '../lib/AuthorizationUtils';
import './usersNameChanged';
import './requiresPermission.html';
import './route';
import './startup';
+import './stylesheets/permissions.css';
export {
hasAllPermission,
@@ -11,4 +13,5 @@ export {
hasRole,
hasPermission,
userHasAllPermission,
+ AuthorizationUtils,
};
diff --git a/app/authorization/client/route.js b/app/authorization/client/route.js
index 2415242d6eb9..95ccd464cc48 100644
--- a/app/authorization/client/route.js
+++ b/app/authorization/client/route.js
@@ -1,9 +1,9 @@
-import { FlowRouter } from 'meteor/kadira:flow-router';
import { BlazeLayout } from 'meteor/kadira:blaze-layout';
+import { registerAdminRoute } from '../../ui-admin/client';
import { t } from '../../utils/client';
-FlowRouter.route('/admin/permissions', {
+registerAdminRoute('/permissions', {
name: 'admin-permissions',
async action(/* params*/) {
await import('./views');
@@ -14,7 +14,7 @@ FlowRouter.route('/admin/permissions', {
},
});
-FlowRouter.route('/admin/permissions/:name?/edit', {
+registerAdminRoute('/permissions/:name?/edit', {
name: 'admin-permissions-edit',
async action(/* params*/) {
await import('./views');
@@ -26,7 +26,7 @@ FlowRouter.route('/admin/permissions/:name?/edit', {
},
});
-FlowRouter.route('/admin/permissions/new', {
+registerAdminRoute('/permissions/new', {
name: 'admin-permissions-new',
async action(/* params*/) {
await import('./views');
diff --git a/app/authorization/client/startup.js b/app/authorization/client/startup.js
index 2edf696d2856..9e437c26c050 100644
--- a/app/authorization/client/startup.js
+++ b/app/authorization/client/startup.js
@@ -2,8 +2,8 @@ import { Meteor } from 'meteor/meteor';
import { Tracker } from 'meteor/tracker';
import { hasAtLeastOnePermission } from './hasPermission';
+import { registerAdminSidebarItem } from '../../ui-admin/client';
import { CachedCollectionManager } from '../../ui-cached-collection';
-import { AdminBox } from '../../ui-utils/client/lib/AdminBox';
import { APIClient } from '../../utils/client';
import { Roles } from '../../models/client';
import { rolesStreamer } from './lib/streamer';
@@ -14,7 +14,7 @@ Meteor.startup(() => {
roles.forEach((role) => Roles.insert(role));
});
- AdminBox.addOption({
+ registerAdminSidebarItem({
href: 'admin-permissions',
i18nLabel: 'Permissions',
icon: 'lock',
diff --git a/app/authorization/client/views/permissions.html b/app/authorization/client/views/permissions.html
index 7f8d9c57e030..d332e471849d 100644
--- a/app/authorization/client/views/permissions.html
+++ b/app/authorization/client/views/permissions.html
@@ -27,7 +27,9 @@
{{permissionName permission}} [ID: {{permission._id}}] |
{{#each role in allRoles}}
-
+ {{#if isRolePermissionEnabled role permission}}
+
+ {{/if}}
|
{{else}}
diff --git a/app/authorization/client/views/permissions.js b/app/authorization/client/views/permissions.js
index 1ad319988a8e..291eaac4bcd5 100644
--- a/app/authorization/client/views/permissions.js
+++ b/app/authorization/client/views/permissions.js
@@ -1,16 +1,16 @@
+import { Meteor } from 'meteor/meteor';
import _ from 'underscore';
import s from 'underscore.string';
-import { Meteor } from 'meteor/meteor';
import { ReactiveDict } from 'meteor/reactive-dict';
import { Tracker } from 'meteor/tracker';
import { Template } from 'meteor/templating';
-import { Roles } from '../../../models';
+import { Roles } from '../../../models/client';
import { ChatPermissions } from '../lib/ChatPermissions';
import { hasAllPermission } from '../hasPermission';
import { t } from '../../../utils/client';
import { SideNav } from '../../../ui-utils/client/lib/SideNav';
-import { CONSTANTS } from '../../lib';
+import { CONSTANTS, AuthorizationUtils } from '../../lib';
import { hasAtLeastOnePermission } from '..';
@@ -179,6 +179,10 @@ Template.permissionsTable.helpers({
permissionDescription(permission) {
return t(`${ permission._id }_description`);
},
+
+ isRolePermissionEnabled(role, permission) {
+ return !AuthorizationUtils.isPermissionRestrictedForRole(permission._id, role._id);
+ },
});
Template.permissionsTable.events({
diff --git a/app/authorization/lib/AuthorizationUtils.ts b/app/authorization/lib/AuthorizationUtils.ts
new file mode 100644
index 000000000000..daa19828305c
--- /dev/null
+++ b/app/authorization/lib/AuthorizationUtils.ts
@@ -0,0 +1,56 @@
+import { Meteor } from 'meteor/meteor';
+
+const restrictedRolePermissions = new Map();
+
+export const AuthorizationUtils = class {
+ static addRolePermissionWhiteList(roleId: string, list: [string]): void {
+ if (!roleId) {
+ throw new Meteor.Error('invalid-param');
+ }
+
+ if (!list) {
+ throw new Meteor.Error('invalid-param');
+ }
+
+ if (!restrictedRolePermissions.has(roleId)) {
+ restrictedRolePermissions.set(roleId, new Set());
+ }
+
+ const rules = restrictedRolePermissions.get(roleId);
+
+ for (const permissionId of list) {
+ rules.add(permissionId);
+ }
+ }
+
+ static isPermissionRestrictedForRole(permissionId: string, roleId: string): boolean {
+ if (!roleId || !permissionId) {
+ throw new Meteor.Error('invalid-param');
+ }
+
+ if (!restrictedRolePermissions.has(roleId)) {
+ return false;
+ }
+
+ const rules = restrictedRolePermissions.get(roleId);
+ if (!rules || !rules.size) {
+ return false;
+ }
+
+ return !rules.has(permissionId);
+ }
+
+ static isPermissionRestrictedForRoleList(permissionId: string, roleList: [string]): boolean {
+ if (!roleList || !permissionId) {
+ throw new Meteor.Error('invalid-param');
+ }
+
+ for (const roleId of roleList) {
+ if (this.isPermissionRestrictedForRole(permissionId, roleId)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+};
diff --git a/app/authorization/lib.js b/app/authorization/lib/index.js
similarity index 76%
rename from app/authorization/lib.js
rename to app/authorization/lib/index.js
index e36c89a3a1c7..fd4f5f0cb288 100644
--- a/app/authorization/lib.js
+++ b/app/authorization/lib/index.js
@@ -6,3 +6,5 @@ export const getSettingPermissionId = function(settingId) {
export const CONSTANTS = {
SETTINGS_LEVEL: 'settings',
};
+
+export { AuthorizationUtils } from './AuthorizationUtils';
diff --git a/app/authorization/server/functions/hasPermission.js b/app/authorization/server/functions/hasPermission.js
index 6f5382496a22..0512c6a07e23 100644
--- a/app/authorization/server/functions/hasPermission.js
+++ b/app/authorization/server/functions/hasPermission.js
@@ -1,8 +1,13 @@
import mem from 'mem';
import { Permissions, Users, Subscriptions } from '../../../models/server/raw';
+import { AuthorizationUtils } from '../../lib/AuthorizationUtils';
const rolesHasPermission = mem(async (permission, roles) => {
+ if (AuthorizationUtils.isPermissionRestrictedForRoleList(permission, roles)) {
+ return false;
+ }
+
const result = await Permissions.findOne({ _id: permission, roles: { $in: roles } }, { projection: { _id: 1 } });
return !!result;
}, {
diff --git a/app/authorization/server/index.js b/app/authorization/server/index.js
index 80394b15fcfc..0ebc74f0ce77 100644
--- a/app/authorization/server/index.js
+++ b/app/authorization/server/index.js
@@ -14,6 +14,7 @@ import {
} from './functions/hasPermission';
import { hasRole } from './functions/hasRole';
import { removeUserFromRoles } from './functions/removeUserFromRoles';
+import { AuthorizationUtils } from '../lib/AuthorizationUtils';
import './methods/addPermissionToRole';
import './methods/addUserToRole';
import './methods/deleteRole';
@@ -36,4 +37,5 @@ export {
hasAllPermission,
hasAtLeastOnePermission,
hasPermission,
+ AuthorizationUtils,
};
diff --git a/app/authorization/server/methods/addPermissionToRole.js b/app/authorization/server/methods/addPermissionToRole.js
index 6879fe4009bc..5ca74ed3dbc9 100644
--- a/app/authorization/server/methods/addPermissionToRole.js
+++ b/app/authorization/server/methods/addPermissionToRole.js
@@ -2,10 +2,17 @@ import { Meteor } from 'meteor/meteor';
import { Permissions } from '../../../models/server';
import { hasPermission } from '../functions/hasPermission';
-import { CONSTANTS } from '../../lib';
+import { CONSTANTS, AuthorizationUtils } from '../../lib';
Meteor.methods({
'authorization:addPermissionToRole'(permissionId, role) {
+ if (AuthorizationUtils.isPermissionRestrictedForRole(permissionId, role)) {
+ throw new Meteor.Error('error-action-not-allowed', 'Permission is restricted', {
+ method: 'authorization:addPermissionToRole',
+ action: 'Adding_permission',
+ });
+ }
+
const uid = Meteor.userId();
const permission = Permissions.findOneById(permissionId);
diff --git a/app/authorization/server/startup.js b/app/authorization/server/startup.js
index 6781accad030..16b2f8906580 100644
--- a/app/authorization/server/startup.js
+++ b/app/authorization/server/startup.js
@@ -70,14 +70,14 @@ Meteor.startup(function() {
{ _id: 'unarchive-room', roles: ['admin'] },
{ _id: 'view-c-room', roles: ['admin', 'user', 'bot', 'app', 'anonymous'] },
{ _id: 'user-generate-access-token', roles: ['admin'] },
- { _id: 'view-d-room', roles: ['admin', 'user', 'bot', 'app'] },
+ { _id: 'view-d-room', roles: ['admin', 'user', 'bot', 'app', 'guest'] },
{ _id: 'view-full-other-user-info', roles: ['admin'] },
{ _id: 'view-history', roles: ['admin', 'user', 'anonymous'] },
{ _id: 'view-joined-room', roles: ['guest', 'bot', 'app', 'anonymous'] },
{ _id: 'view-join-code', roles: ['admin'] },
{ _id: 'view-logs', roles: ['admin'] },
{ _id: 'view-other-user-channels', roles: ['admin'] },
- { _id: 'view-p-room', roles: ['admin', 'user', 'anonymous'] },
+ { _id: 'view-p-room', roles: ['admin', 'user', 'anonymous', 'guest'] },
{ _id: 'view-privileged-setting', roles: ['admin'] },
{ _id: 'view-room-administration', roles: ['admin'] },
{ _id: 'view-statistics', roles: ['admin'] },
diff --git a/app/autotranslate/client/index.js b/app/autotranslate/client/index.js
index c1deaf68e645..f34d47269d43 100644
--- a/app/autotranslate/client/index.js
+++ b/app/autotranslate/client/index.js
@@ -2,5 +2,6 @@ import './lib/actionButton';
import './lib/tabBar';
import './views/autoTranslateFlexTab.html';
import './views/autoTranslateFlexTab';
+import './stylesheets/autotranslate.css';
export { AutoTranslate } from './lib/autotranslate';
diff --git a/app/channel-settings/client/index.js b/app/channel-settings/client/index.js
index 9f76f0a85cc7..42e3477bba2d 100644
--- a/app/channel-settings/client/index.js
+++ b/app/channel-settings/client/index.js
@@ -4,5 +4,6 @@ import './startup/trackSettingsChange';
import './views/channelSettings.html';
import './views/channelSettings';
import './views/Multiselect';
+import './stylesheets/channel-settings.css';
export { ChannelSettings } from './lib/ChannelSettings';
diff --git a/app/channel-settings/client/views/Multiselect.html b/app/channel-settings/client/views/Multiselect.html
deleted file mode 100644
index 15e2b1884545..000000000000
--- a/app/channel-settings/client/views/Multiselect.html
+++ /dev/null
@@ -1,3 +0,0 @@
-
-
-
diff --git a/app/channel-settings/client/views/Multiselect.js b/app/channel-settings/client/views/Multiselect.js
index 160901096f14..a63fa1981cd2 100644
--- a/app/channel-settings/client/views/Multiselect.js
+++ b/app/channel-settings/client/views/Multiselect.js
@@ -1,23 +1,12 @@
-import './Multiselect.html';
-import { Template } from 'meteor/templating';
-
-import { MultiSelectSettingInput } from '../../../../client/components/admin/settings/inputs/MultiSelectSettingInput';
-
-
-Template.Multiselect.onRendered(async function() {
- const { MeteorProvider } = await import('../../../../client/providers/MeteorProvider');
- const React = await import('react');
- const ReactDOM = await import('react-dom');
- this.container = this.firstNode;
- this.autorun(() => {
- ReactDOM.render(React.createElement(MeteorProvider, {
- children: React.createElement(MultiSelectSettingInput, Template.currentData()),
- }), this.container);
- });
-});
-
-
-Template.Multiselect.onDestroyed(async function() {
- const ReactDOM = await import('react-dom');
- this.container && ReactDOM.unmountComponentAtNode(this.container);
-});
+import { HTML } from 'meteor/htmljs';
+
+import { createTemplateForComponent } from '../../../../client/reactAdapters';
+
+createTemplateForComponent(
+ 'Multiselect',
+ () => import('../../../ui-admin/client/components/settings/inputs/MultiSelectSettingInput'),
+ {
+ // eslint-disable-next-line new-cap
+ renderContainerView: () => HTML.DIV({ class: 'rc-multiselect', style: 'display: flex;' }),
+ },
+);
diff --git a/app/channel-settings/server/methods/saveRoomSettings.js b/app/channel-settings/server/methods/saveRoomSettings.js
index adef8f1ed12c..49dae9c9ab66 100644
--- a/app/channel-settings/server/methods/saveRoomSettings.js
+++ b/app/channel-settings/server/methods/saveRoomSettings.js
@@ -17,7 +17,7 @@ import { saveRoomTokenpass } from '../functions/saveRoomTokens';
import { saveStreamingOptions } from '../functions/saveStreamingOptions';
import { RoomSettingsEnum, roomTypes } from '../../../utils';
-const fields = ['featured', 'roomName', 'roomTopic', 'roomAnnouncement', 'roomCustomFields', 'roomDescription', 'roomType', 'readOnly', 'reactWhenReadOnly', 'systemMessages', 'default', 'joinCode', 'tokenpass', 'streamingOptions', 'retentionEnabled', 'retentionMaxAge', 'retentionExcludePinned', 'retentionFilesOnly', 'retentionOverrideGlobal', 'encrypted'];
+const fields = ['featured', 'roomName', 'roomTopic', 'roomAnnouncement', 'roomCustomFields', 'roomDescription', 'roomType', 'readOnly', 'reactWhenReadOnly', 'systemMessages', 'default', 'joinCode', 'tokenpass', 'streamingOptions', 'retentionEnabled', 'retentionMaxAge', 'retentionExcludePinned', 'retentionFilesOnly', 'retentionOverrideGlobal', 'encrypted', 'favorite'];
Meteor.methods({
saveRoomSettings(rid, settings, value) {
const userId = Meteor.userId();
@@ -221,6 +221,9 @@ Meteor.methods({
case 'encrypted':
Rooms.saveEncryptedById(rid, value);
break;
+ case 'favorite':
+ Rooms.saveFavoriteById(rid, value.favorite, value.defaultValue);
+ break;
}
});
diff --git a/app/chatpal-search/client/index.js b/app/chatpal-search/client/index.js
index 6c169229e05a..8418c93acde9 100644
--- a/app/chatpal-search/client/index.js
+++ b/app/chatpal-search/client/index.js
@@ -5,3 +5,4 @@ import './template/admin';
import './template/result';
import './template/suggestion';
import './route';
+import './style.css';
diff --git a/app/chatpal-search/client/route.js b/app/chatpal-search/client/route.js
index cb0281953064..86c6ad276c49 100644
--- a/app/chatpal-search/client/route.js
+++ b/app/chatpal-search/client/route.js
@@ -1,9 +1,9 @@
-import { FlowRouter } from 'meteor/kadira:flow-router';
import { BlazeLayout } from 'meteor/kadira:blaze-layout';
+import { registerAdminRoute } from '../../ui-admin/client';
import { t } from '../../utils';
-FlowRouter.route('/admin/chatpal', {
+registerAdminRoute('/chatpal', {
name: 'chatpal-admin',
action() {
return BlazeLayout.render('main', {
diff --git a/app/cloud/client/index.js b/app/cloud/client/index.js
index 0165b0a25011..0c8025d22dbd 100644
--- a/app/cloud/client/index.js
+++ b/app/cloud/client/index.js
@@ -3,20 +3,19 @@ import './admin/cloud';
import './admin/cloudRegisterManually';
import { BlazeLayout } from 'meteor/kadira:blaze-layout';
-import { FlowRouter } from 'meteor/kadira:flow-router';
-import { AdminBox } from '../../ui-utils';
+import { registerAdminRoute, registerAdminSidebarItem } from '../../ui-admin/client';
import { hasAtLeastOnePermission } from '../../authorization';
-FlowRouter.route('/admin/cloud', {
- name: 'cloud-config',
+registerAdminRoute('/cloud', {
+ name: 'cloud',
async action() {
await import('./admin');
BlazeLayout.render('main', { center: 'cloud', old: true });
},
});
-FlowRouter.route('/admin/cloud/oauth-callback', {
+registerAdminRoute('/cloud/oauth-callback', {
name: 'cloud-oauth-callback',
async action() {
await import('./admin');
@@ -24,9 +23,9 @@ FlowRouter.route('/admin/cloud/oauth-callback', {
},
});
-AdminBox.addOption({
+registerAdminSidebarItem({
icon: 'cloud-plus',
- href: 'admin/cloud',
+ href: 'cloud',
i18nLabel: 'Connectivity_Services',
permissionGranted() {
return hasAtLeastOnePermission(['manage-cloud']);
diff --git a/app/colors/client/client.js b/app/colors/client/client.js
index 5f76a7c453aa..2f867b30963e 100644
--- a/app/colors/client/client.js
+++ b/app/colors/client/client.js
@@ -2,6 +2,7 @@ import s from 'underscore.string';
import { settings } from '../../settings';
import { callbacks } from '../../callbacks';
+import './style.css';
//
// HexColorPreview is a named function that will process Colors
diff --git a/app/custom-oauth/server/custom_oauth_server.js b/app/custom-oauth/server/custom_oauth_server.js
index 3f60a185b905..c37e43b62047 100644
--- a/app/custom-oauth/server/custom_oauth_server.js
+++ b/app/custom-oauth/server/custom_oauth_server.js
@@ -6,6 +6,7 @@ import { HTTP } from 'meteor/http';
import { ServiceConfiguration } from 'meteor/service-configuration';
import _ from 'underscore';
+import { normalizers, fromTemplate, renameInvalidProperties } from './transform_helpers';
import { mapRolesFromSSO, updateRolesFromSSO } from './oauth_helpers';
import { Logger } from '../../logger';
import { Users } from '../../models';
@@ -17,107 +18,6 @@ const logger = new Logger('CustomOAuth');
const Services = {};
const BeforeUpdateOrCreateUserFromExternalService = [];
-const normalizers = {
- // Set 'id' to '_id' for any sources that provide it
- _id(identity) {
- if (identity._id && !identity.id) {
- identity.id = identity._id;
- }
- },
-
- // Fix for Reddit
- redit(identity) {
- if (identity.result) {
- return identity.result;
- }
- },
-
- // Fix WordPress-like identities having 'ID' instead of 'id'
- wordpress(identity) {
- if (identity.ID && !identity.id) {
- identity.id = identity.ID;
- }
- },
-
- // Fix Auth0-like identities having 'user_id' instead of 'id'
- user_id(identity) {
- if (identity.user_id && !identity.id) {
- identity.id = identity.user_id;
- }
- },
-
- characterid(identity) {
- if (identity.CharacterID && !identity.id) {
- identity.id = identity.CharacterID;
- }
- },
-
- // Fix Dataporten having 'user.userid' instead of 'id'
- dataporten(identity) {
- if (identity.user && identity.user.userid && !identity.id) {
- if (identity.user.userid_sec && identity.user.userid_sec[0]) {
- identity.id = identity.user.userid_sec[0];
- } else {
- identity.id = identity.user.userid;
- }
- identity.email = identity.user.email;
- }
- },
-
- // Fix for Xenforo [BD]API plugin for 'user.user_id; instead of 'id'
- xenforo(identity) {
- if (identity.user && identity.user.user_id && !identity.id) {
- identity.id = identity.user.user_id;
- identity.email = identity.user.user_email;
- }
- },
-
- // Fix general 'phid' instead of 'id' from phabricator
- phabricator(identity) {
- if (identity.phid && !identity.id) {
- identity.id = identity.phid;
- }
- },
-
- // Fix Keycloak-like identities having 'sub' instead of 'id'
- kaycloak(identity) {
- if (identity.sub && !identity.id) {
- identity.id = identity.sub;
- }
- },
-
- // Fix OpenShift identities where id is in 'metadata' object
- openshift(identity) {
- if (!identity.id && identity.metadata && identity.metadata.uid) {
- identity.id = identity.metadata.uid;
- identity.name = identity.fullName;
- }
- },
-
- // Fix general 'userid' instead of 'id' from provider
- userid(identity) {
- if (identity.userid && !identity.id) {
- identity.id = identity.userid;
- }
- },
-
- // Fix Nextcloud provider
- nextcloud(identity) {
- if (!identity.id && identity.ocs && identity.ocs.data && identity.ocs.data.id) {
- identity.id = identity.ocs.data.id;
- identity.name = identity.ocs.data.displayname;
- identity.email = identity.ocs.data.email;
- }
- },
-
- // Fix when authenticating from a meteor app with 'emails' field
- meteor(identity) {
- if (!identity.email && (identity.emails && Array.isArray(identity.emails) && identity.emails.length >= 1)) {
- identity.email = identity.emails[0].address ? identity.emails[0].address : undefined;
- }
- },
-};
-
export class CustomOAuth {
constructor(name, options) {
logger.debug('Init CustomOAuth', name, options);
@@ -174,6 +74,7 @@ export class CustomOAuth {
this.tokenSentVia = options.tokenSentVia;
this.identityTokenSentVia = options.identityTokenSentVia;
this.usernameField = (options.usernameField || '').trim();
+ this.emailField = (options.emailField || '').trim();
this.nameField = (options.nameField || '').trim();
this.avatarField = (options.avatarField || '').trim();
this.mergeUsers = options.mergeUsers;
@@ -334,6 +235,10 @@ export class CustomOAuth {
identity.username = this.getUsername(identity);
}
+ if (this.emailField) {
+ identity.email = this.getEmail(identity);
+ }
+
if (this.avatarField) {
identity.avatarUrl = this.getAvatarUrl(identity);
}
@@ -344,7 +249,7 @@ export class CustomOAuth {
identity.name = this.getName(identity);
}
- return identity;
+ return renameInvalidProperties(identity);
}
retrieveCredential(credentialToken, credentialSecret) {
@@ -352,42 +257,56 @@ export class CustomOAuth {
}
getUsername(data) {
- let username = '';
+ try {
+ const value = fromTemplate(this.usernameField, data);
+
+ if (!value) {
+ throw new Meteor.Error('field_not_found', `Username field "${ this.usernameField }" not found in data`, data);
+ }
+ return value;
+ } catch (error) {
+ throw new Error('CustomOAuth: Failed to extract username', error.message);
+ }
+ }
- username = this.usernameField.split('.').reduce(function(prev, curr) {
- return prev ? prev[curr] : undefined;
- }, data);
+ getEmail(data) {
+ try {
+ const value = fromTemplate(this.emailField, data);
- if (!username) {
- throw new Meteor.Error('field_not_found', `Username field "${ this.usernameField }" not found in data`, data);
+ if (!value) {
+ throw new Meteor.Error('field_not_found', `Email field "${ this.emailField }" not found in data`, data);
+ }
+ return value;
+ } catch (error) {
+ throw new Error('CustomOAuth: Failed to extract email', error.message);
}
- return username;
}
getCustomName(data) {
- let customName = '';
+ try {
+ const value = fromTemplate(this.nameField, data);
- customName = this.nameField.split('.').reduce(function(prev, curr) {
- return prev ? prev[curr] : undefined;
- }, data);
+ if (!value) {
+ return this.getName(data);
+ }
- if (!customName) {
- return this.getName(data);
+ return value;
+ } catch (error) {
+ throw new Error('CustomOAuth: Failed to extract custom name', error.message);
}
-
- return customName;
}
getAvatarUrl(data) {
- const avatarUrl = this.avatarField.split('.').reduce(function(prev, curr) {
- return prev ? prev[curr] : undefined;
- }, data);
+ try {
+ const value = fromTemplate(this.avatarField, data);
- if (!avatarUrl) {
- logger.debug(`Avatar field "${ this.avatarField }" not found in data`, data);
+ if (!value) {
+ logger.debug(`Avatar field "${ this.avatarField }" not found in data`, data);
+ }
+ return value;
+ } catch (error) {
+ throw new Error('CustomOAuth: Failed to extract avatar url', error.message);
}
-
- return avatarUrl;
}
getName(identity) {
@@ -438,11 +357,15 @@ export class CustomOAuth {
}
if (this.usernameField) {
- user.username = this.getUsername(user.services[this.name]);
+ user.username = user.services[this.name].username;
+ }
+
+ if (this.emailField) {
+ user.email = user.services[this.name].email;
}
if (this.nameField) {
- user.name = this.getCustomName(user.services[this.name]);
+ user.name = user.services[this.name].name;
}
if (this.mergeRoles) {
diff --git a/app/custom-oauth/server/transform_helpers.js b/app/custom-oauth/server/transform_helpers.js
new file mode 100644
index 000000000000..ef9847ae0279
--- /dev/null
+++ b/app/custom-oauth/server/transform_helpers.js
@@ -0,0 +1,157 @@
+import _ from 'underscore';
+
+export const normalizers = {
+ // Set 'id' to '_id' for any sources that provide it
+ _id(identity) {
+ if (identity._id && !identity.id) {
+ identity.id = identity._id;
+ }
+ },
+
+ // Fix for Reddit
+ redit(identity) {
+ if (identity.result) {
+ return identity.result;
+ }
+ },
+
+ // Fix WordPress-like identities having 'ID' instead of 'id'
+ wordpress(identity) {
+ if (identity.ID && !identity.id) {
+ identity.id = identity.ID;
+ }
+ },
+
+ // Fix Auth0-like identities having 'user_id' instead of 'id'
+ user_id(identity) {
+ if (identity.user_id && !identity.id) {
+ identity.id = identity.user_id;
+ }
+ },
+
+ characterid(identity) {
+ if (identity.CharacterID && !identity.id) {
+ identity.id = identity.CharacterID;
+ }
+ },
+
+ // Fix Dataporten having 'user.userid' instead of 'id'
+ dataporten(identity) {
+ if (identity.user && identity.user.userid && !identity.id) {
+ if (identity.user.userid_sec && identity.user.userid_sec[0]) {
+ identity.id = identity.user.userid_sec[0];
+ } else {
+ identity.id = identity.user.userid;
+ }
+ identity.email = identity.user.email;
+ }
+ },
+
+ // Fix for Xenforo [BD]API plugin for 'user.user_id; instead of 'id'
+ xenforo(identity) {
+ if (identity.user && identity.user.user_id && !identity.id) {
+ identity.id = identity.user.user_id;
+ identity.email = identity.user.user_email;
+ }
+ },
+
+ // Fix general 'phid' instead of 'id' from phabricator
+ phabricator(identity) {
+ if (identity.phid && !identity.id) {
+ identity.id = identity.phid;
+ }
+ },
+
+ // Fix Keycloak-like identities having 'sub' instead of 'id'
+ kaycloak(identity) {
+ if (identity.sub && !identity.id) {
+ identity.id = identity.sub;
+ }
+ },
+
+ // Fix OpenShift identities where id is in 'metadata' object
+ openshift(identity) {
+ if (!identity.id && identity.metadata && identity.metadata.uid) {
+ identity.id = identity.metadata.uid;
+ identity.name = identity.fullName;
+ }
+ },
+
+ // Fix general 'userid' instead of 'id' from provider
+ userid(identity) {
+ if (identity.userid && !identity.id) {
+ identity.id = identity.userid;
+ }
+ },
+
+ // Fix Nextcloud provider
+ nextcloud(identity) {
+ if (!identity.id && identity.ocs && identity.ocs.data && identity.ocs.data.id) {
+ identity.id = identity.ocs.data.id;
+ identity.name = identity.ocs.data.displayname;
+ identity.email = identity.ocs.data.email;
+ }
+ },
+
+ // Fix when authenticating from a meteor app with 'emails' field
+ meteor(identity) {
+ if (!identity.email && (identity.emails && Array.isArray(identity.emails) && identity.emails.length >= 1)) {
+ identity.email = identity.emails[0].address ? identity.emails[0].address : undefined;
+ }
+ },
+};
+
+const IDENTITY_PROPNAME_FILTER = /(\.)/g;
+export const renameInvalidProperties = (input) => {
+ if (Array.isArray(input)) {
+ return input.map(renameInvalidProperties);
+ }
+ if (!_.isObject(input)) {
+ return input;
+ }
+
+ return Object.entries(input).reduce((result, [name, value]) => ({
+ ...result,
+ [name.replace(IDENTITY_PROPNAME_FILTER, '_')]: renameInvalidProperties(value),
+ }), {});
+};
+
+export const getNestedValue = (propertyPath, source) =>
+ propertyPath.split('.').reduce((prev, curr) => (prev ? prev[curr] : undefined), source);
+
+// /^(.+)@/::email
+const REGEXP_FROM_FORMULA = /^\/((?!\/::).*)\/::(.+)/;
+export const getRegexpMatch = (formula, data) => {
+ const regexAndPath = REGEXP_FROM_FORMULA.exec(formula);
+ if (!regexAndPath) {
+ return getNestedValue(formula, data);
+ }
+ if (regexAndPath.length !== 3) {
+ throw new Error(`expected array of length 3, got ${ regexAndPath.length }`);
+ }
+
+ const [, regexString, path] = regexAndPath;
+ const nestedValue = getNestedValue(path, data);
+ const regex = new RegExp(regexString);
+ const matches = regex.exec(nestedValue);
+
+ // regexp does not match nested value
+ if (!matches) {
+ return undefined;
+ }
+
+ // we only support regular expressions with a single capture group
+ const [, value] = matches;
+
+ // this could mean we return `undefined` (e.g. when capture group is empty)
+ return value;
+};
+
+const templateStringRegex = /{{((?:(?!}}).)+)}}/g;
+export const fromTemplate = (template, data) => {
+ if (!templateStringRegex.test(template)) {
+ return getNestedValue(template, data);
+ }
+
+ return template.replace(templateStringRegex, (fullMatch, match) => getRegexpMatch(match, data));
+};
diff --git a/app/custom-oauth/server/transform_helpers.tests.js b/app/custom-oauth/server/transform_helpers.tests.js
new file mode 100644
index 000000000000..ec1475780e68
--- /dev/null
+++ b/app/custom-oauth/server/transform_helpers.tests.js
@@ -0,0 +1,122 @@
+/* eslint-env mocha */
+
+import { expect } from 'chai';
+
+import {
+ normalizers,
+ fromTemplate,
+ renameInvalidProperties,
+ getNestedValue,
+ getRegexpMatch,
+} from './transform_helpers';
+
+
+const data = {
+ id: '123456',
+ 'invalid.property': true,
+ name: 'foo',
+ email: 'foo@bar.com',
+ nested: {
+ value: 'baz',
+ 'another.invalid.prop': true,
+ },
+ list: [{
+ 'invalid.property': 'in-array',
+ }],
+};
+
+describe('fromTemplate', () => {
+ const normalizedData = Object.values(normalizers).reduce((normalizedData, normalizer) => {
+ const result = { ...normalizedData };
+ normalizer({ ...result });
+
+ return result;
+ }, data);
+
+ it('returns match from regexp on top-level properties', () => {
+ const template = '{{/^foo@bar\.(.+)/::email}}';
+ const expected = 'com';
+ const result = fromTemplate(template, normalizedData);
+ expect(result).to.equal(expected);
+ });
+
+ it('returns match from regexp on nested properties', () => {
+ const template = '{{/^ba(.+)/::nested.value}}';
+ const expected = 'z';
+ const result = fromTemplate(template, normalizedData);
+ expect(result).to.equal(expected);
+ });
+
+ it('returns value from nested prop with plain syntax', () => {
+ const template = 'nested.value';
+ const expected = normalizedData.nested.value;
+ const result = fromTemplate(template, normalizedData);
+ expect(result).to.equal(expected);
+ });
+
+ it('returns value from nested prop with template syntax', () => {
+ const template = '{{nested.value}}';
+ const expected = normalizedData.nested.value;
+ const result = fromTemplate(template, normalizedData);
+ expect(result).to.equal(expected);
+ });
+
+ it('returns composed value from nested prop with template syntax', () => {
+ const template = '{{name}}.{{nested.value}}';
+ const expected = `${ normalizedData.name }.${ normalizedData.nested.value }`;
+ const result = fromTemplate(template, normalizedData);
+
+ expect(result).to.equal(expected);
+ });
+
+ it('returns composed string from multiple template chunks with static parts', () => {
+ const template = 'composed-{{name}}-at-{{nested.value}}-dot-{{/^foo@bar\.(.+)/::email}}-from-template';
+ const expected = 'composed-foo-at-baz-dot-com-from-template';
+ const result = fromTemplate(template, normalizedData);
+ expect(result).to.equal(expected);
+ });
+});
+
+describe('getRegexpMatch', () => {
+ it('returns nested value when formula is not in the regex::field form', () => {
+ const formula = 'nested.value';
+ expect(getRegexpMatch(formula, data)).to.equal(data.nested.value);
+ });
+
+ it("returns undefined when regex doesn't match", () => {
+ const formula = '/^foo@baz(.+)/::email';
+ expect(getRegexpMatch(formula, data)).to.be.undefined;
+ });
+
+ it("throws when regex isn't valid", () => {
+ const formula = '/+/::email';
+ expect(() => getRegexpMatch(formula, data)).to.throw();
+ });
+});
+
+describe('renameInvalidProperties', () => {
+ it('replaces . chars in field names with _', () => {
+ const result = renameInvalidProperties(data);
+
+ expect(result['invalid.property']).to.be.undefined;
+ expect(result.invalid_property).to.equal(data['invalid.property']);
+
+ expect(result.nested['invalid.property']).to.be.undefined;
+ expect(result.nested.invalid_property).to.equal(data.nested['invalid.property']);
+
+ result.list.forEach((item, idx) => {
+ expect(item['invalid.property']).to.be.undefined;
+ expect(item.invalid_property).to.equal(data.list[idx]['invalid.property']);
+ });
+ });
+});
+
+describe('getNestedValue', () => {
+ it("returns undefined when nested value doesn't exist", () => {
+ expect(getNestedValue('nested.does.not.exist', data)).to.be.undefined;
+ });
+
+ it('returns nested object property', () => {
+ expect(getNestedValue('nested.value', data)).to.equal(data.nested.value);
+ });
+});
diff --git a/app/custom-sounds/client/admin/adminSounds.html b/app/custom-sounds/client/admin/adminSounds.html
index b4ca2015025a..c9a20bd9c4fd 100644
--- a/app/custom-sounds/client/admin/adminSounds.html
+++ b/app/custom-sounds/client/admin/adminSounds.html
@@ -45,8 +45,11 @@
- {{>icon _id=_id icon="play" block="icon-play-circled"}}
- {{>icon _id=_id icon="pause" block="icon-pause-circled"}}
+ {{#if isPlaying _id}}
+ {{>icon _id=_id icon="pause" block="icon-pause-circled"}}
+ {{else}}
+ {{>icon _id=_id icon="play" block="icon-play-circled"}}
+ {{/if}}
{{>icon _id=_id icon="ban" block="icon-reset-circled"}}
|
diff --git a/app/custom-sounds/client/admin/adminSounds.js b/app/custom-sounds/client/admin/adminSounds.js
index e142bc85d60d..9de092e3c143 100644
--- a/app/custom-sounds/client/admin/adminSounds.js
+++ b/app/custom-sounds/client/admin/adminSounds.js
@@ -16,6 +16,9 @@ Template.adminSounds.helpers({
const instance = Template.instance();
return instance.filter && instance.filter.get();
},
+ isPlaying(_id) {
+ return Template.instance().isPlayingId.get() === _id;
+ },
customsounds() {
return Template.instance().sounds.get();
},
@@ -62,6 +65,7 @@ Template.adminSounds.onCreated(function() {
this.query = new ReactiveVar({});
this.isLoading = new ReactiveVar(false);
this.filter = new ReactiveVar('');
+ this.isPlayingId = new ReactiveVar('');
this.tabBar = new RocketChatTabBar();
this.tabBar.showGroup(FlowRouter.current().route.name);
@@ -138,18 +142,28 @@ Template.adminSounds.events({
t.filter.set(e.currentTarget.value);
t.offset.set(0);
},
- 'click .icon-play-circled'(e) {
+ 'click .icon-play-circled'(e, t) {
e.preventDefault();
e.stopPropagation();
CustomSounds.play(this._id);
+ const audio = document.getElementById(t.isPlayingId.get());
+ if (audio) {
+ audio.pause();
+ }
+ document.getElementById(this._id).onended = () => {
+ t.isPlayingId.set('');
+ this.onended = null;
+ };
+ t.isPlayingId.set(this._id);
},
- 'click .icon-pause-circled'(e) {
+ 'click .icon-pause-circled'(e, t) {
e.preventDefault();
e.stopPropagation();
const audio = document.getElementById(this._id);
if (audio && !audio.paused) {
audio.pause();
}
+ t.isPlayingId.set('');
},
'click .icon-reset-circled'(e) {
e.preventDefault();
diff --git a/app/custom-sounds/client/admin/route.js b/app/custom-sounds/client/admin/route.js
index 0150bef09169..0f181fda5787 100644
--- a/app/custom-sounds/client/admin/route.js
+++ b/app/custom-sounds/client/admin/route.js
@@ -1,7 +1,8 @@
-import { FlowRouter } from 'meteor/kadira:flow-router';
import { BlazeLayout } from 'meteor/kadira:blaze-layout';
-FlowRouter.route('/admin/custom-sounds', {
+import { registerAdminRoute } from '../../../ui-admin/client';
+
+registerAdminRoute('/custom-sounds', {
name: 'custom-sounds',
async action(/* params*/) {
await import('./views');
diff --git a/app/custom-sounds/client/admin/startup.js b/app/custom-sounds/client/admin/startup.js
index 5d469ce5d457..2647b17ea42d 100644
--- a/app/custom-sounds/client/admin/startup.js
+++ b/app/custom-sounds/client/admin/startup.js
@@ -1,7 +1,7 @@
-import { AdminBox } from '../../../ui-utils';
import { hasAtLeastOnePermission } from '../../../authorization';
+import { registerAdminSidebarItem } from '../../../ui-admin/client';
-AdminBox.addOption({
+registerAdminSidebarItem({
href: 'custom-sounds',
i18nLabel: 'Custom_Sounds',
icon: 'volume',
diff --git a/app/custom-sounds/client/index.js b/app/custom-sounds/client/index.js
index 918e3247240c..eb17166ef2b8 100644
--- a/app/custom-sounds/client/index.js
+++ b/app/custom-sounds/client/index.js
@@ -2,5 +2,6 @@ import './notifications/deleteCustomSound';
import './notifications/updateCustomSound';
import './admin/route';
import './admin/startup';
+import '../assets/stylesheets/customSoundsAdmin.css';
export { CustomSounds } from './lib/CustomSounds';
diff --git a/app/dolphin/client/index.js b/app/dolphin/client/index.js
index e44dbe195eff..96b9b5213228 100644
--- a/app/dolphin/client/index.js
+++ b/app/dolphin/client/index.js
@@ -1 +1,2 @@
import '../lib/common';
+import './login-button.css';
diff --git a/app/drupal/client/index.js b/app/drupal/client/index.js
index e44dbe195eff..96b9b5213228 100644
--- a/app/drupal/client/index.js
+++ b/app/drupal/client/index.js
@@ -1 +1,2 @@
import '../lib/common';
+import './login-button.css';
diff --git a/app/e2e/client/index.js b/app/e2e/client/index.js
index f4e81a3237eb..0564e11fa6b6 100644
--- a/app/e2e/client/index.js
+++ b/app/e2e/client/index.js
@@ -1 +1,3 @@
+import './stylesheets/e2e.css';
+
export * from './rocketchat.e2e';
diff --git a/app/e2e/client/stylesheets/e2e.less b/app/e2e/client/stylesheets/e2e.css
similarity index 100%
rename from app/e2e/client/stylesheets/e2e.less
rename to app/e2e/client/stylesheets/e2e.css
diff --git a/app/emoji-custom/client/admin/route.js b/app/emoji-custom/client/admin/route.js
index 169ff2bb637a..575e47831756 100644
--- a/app/emoji-custom/client/admin/route.js
+++ b/app/emoji-custom/client/admin/route.js
@@ -1,7 +1,8 @@
-import { FlowRouter } from 'meteor/kadira:flow-router';
import { BlazeLayout } from 'meteor/kadira:blaze-layout';
-FlowRouter.route('/admin/emoji-custom', {
+import { registerAdminRoute } from '../../../ui-admin/client';
+
+registerAdminRoute('/emoji-custom', {
name: 'emoji-custom',
async action(/* params*/) {
await import('./views');
diff --git a/app/emoji-custom/client/admin/startup.js b/app/emoji-custom/client/admin/startup.js
index 2845727615d4..851786b584f8 100644
--- a/app/emoji-custom/client/admin/startup.js
+++ b/app/emoji-custom/client/admin/startup.js
@@ -1,7 +1,7 @@
-import { AdminBox } from '../../../ui-utils';
import { hasPermission } from '../../../authorization';
+import { registerAdminSidebarItem } from '../../../ui-admin/client';
-AdminBox.addOption({
+registerAdminSidebarItem({
href: 'emoji-custom',
i18nLabel: 'Custom_Emoji',
icon: 'emoji',
diff --git a/app/emoji-custom/client/index.js b/app/emoji-custom/client/index.js
index 8b6f4fc9c0fc..1c143c92bd4c 100644
--- a/app/emoji-custom/client/index.js
+++ b/app/emoji-custom/client/index.js
@@ -3,3 +3,4 @@ import './notifications/deleteEmojiCustom';
import './notifications/updateEmojiCustom';
import './admin/startup';
import './admin/route';
+import '../assets/stylesheets/emojiCustomAdmin.css';
diff --git a/app/emoji-emojione/client/index.js b/app/emoji-emojione/client/index.js
index 8e716d1f414f..3bf47e3c8c5a 100644
--- a/app/emoji-emojione/client/index.js
+++ b/app/emoji-emojione/client/index.js
@@ -1 +1,2 @@
import '../lib/rocketchat';
+import './emojione-sprites.css';
diff --git a/app/emoji/client/emojiPicker.js b/app/emoji/client/emojiPicker.js
index 705ccb412410..7894ab736efc 100644
--- a/app/emoji/client/emojiPicker.js
+++ b/app/emoji/client/emojiPicker.js
@@ -58,9 +58,10 @@ function getEmojisBySearchTerm(searchTerm) {
if (searchRegExp.test(current)) {
const emojiObject = emoji.list[current];
- const { emojiPackage } = emojiObject;
+ const { emojiPackage, shortnames } = emojiObject;
let tone = '';
current = current.replace(/:/g, '');
+ const alias = shortnames[0] !== undefined ? shortnames[0].replace(/:/g, '') : shortnames[0];
if (actualTone > 0 && emoji.packages[emojiPackage].toneList.hasOwnProperty(emoji)) {
tone = `_tone${ actualTone }`;
@@ -71,7 +72,8 @@ function getEmojisBySearchTerm(searchTerm) {
for (const key in emoji.packages[emojiPackage].emojisByCategory) {
if (emoji.packages[emojiPackage].emojisByCategory.hasOwnProperty(key)) {
const contents = emoji.packages[emojiPackage].emojisByCategory[key];
- if (contents.indexOf(current) !== -1) {
+ const searchValArray = alias !== undefined ? alias.replace(/:/g, '').split('_') : alias;
+ if (contents.indexOf(current) !== -1 || (searchValArray !== undefined && searchValArray.includes(searchTerm))) {
emojiFound = true;
break;
}
diff --git a/app/federation/client/admin/dashboard.js b/app/federation/client/admin/dashboard.js
index e53325264d23..3d68975bd35b 100644
--- a/app/federation/client/admin/dashboard.js
+++ b/app/federation/client/admin/dashboard.js
@@ -3,10 +3,9 @@ import { Tracker } from 'meteor/tracker';
import { Template } from 'meteor/templating';
import { ReactiveVar } from 'meteor/reactive-var';
import { BlazeLayout } from 'meteor/kadira:blaze-layout';
-import { FlowRouter } from 'meteor/kadira:flow-router';
-import { AdminBox } from '../../../ui-utils';
import { hasRole } from '../../../authorization';
+import { registerAdminRoute, registerAdminSidebarItem } from '../../../ui-admin/client';
import './dashboard.html';
import './dashboard.css';
@@ -69,16 +68,16 @@ Template.dashboard.onRendered(() => {
// Route setup
-FlowRouter.route('/admin/federation-dashboard', {
+registerAdminRoute('/federation-dashboard', {
name: 'federation-dashboard',
action() {
BlazeLayout.render('main', { center: 'dashboard', old: true });
},
});
-AdminBox.addOption({
+registerAdminSidebarItem({
icon: 'discover',
- href: 'admin/federation-dashboard',
+ href: 'federation-dashboard',
i18nLabel: 'Federation Dashboard',
permissionGranted() {
return hasRole(Meteor.userId(), 'admin');
diff --git a/app/federation/server/endpoints/dispatch.js b/app/federation/server/endpoints/dispatch.js
index 77a405784316..43fd8641aa7b 100644
--- a/app/federation/server/endpoints/dispatch.js
+++ b/app/federation/server/endpoints/dispatch.js
@@ -155,6 +155,29 @@ const eventHandlers = {
return eventResult;
},
+ //
+ // ROOM_USER_LEFT
+ //
+ async [eventTypes.ROOM_USER_LEFT](event) {
+ const eventResult = await FederationRoomEvents.addEvent(event.context, event);
+
+ // If the event was successfully added, handle the event locally
+ if (eventResult.success) {
+ const { data: { roomId, user, domainsAfterRemoval } } = event;
+
+ // Remove the user's subscription
+ Subscriptions.removeByRoomIdAndUserId(roomId, user._id);
+
+ // Refresh the servers list
+ FederationServers.refreshServers();
+
+ // Update the room's federation property
+ Rooms.update({ _id: roomId }, { $set: { 'federation.domains': domainsAfterRemoval } });
+ }
+
+ return eventResult;
+ },
+
//
// ROOM_MESSAGE
//
@@ -422,13 +445,19 @@ API.v1.addRoute('federation.events.dispatch', { authRequired: false }, {
}
// If there was an error handling the event, take action
- if (!eventResult.success) {
- logger.server.debug(`federation.events.dispatch => Event has missing parents -> event=${ JSON.stringify(event, null, 2) }`);
+ if (!eventResult || !eventResult.success) {
+ try {
+ logger.server.debug(`federation.events.dispatch => Event has missing parents -> event=${ JSON.stringify(event, null, 2) }`);
- requestEventsFromLatest(event.origin, getFederationDomain(), contextDefinitions.defineType(event), event.context, eventResult.latestEventIds);
+ requestEventsFromLatest(event.origin, getFederationDomain(), contextDefinitions.defineType(event), event.context, eventResult.latestEventIds);
- // And stop handling the events
- break;
+ // And stop handling the events
+ break;
+ } catch (err) {
+ logger.server.error(() => `dispatch => event=${ JSON.stringify(event, null, 2) } eventResult=${ JSON.stringify(eventResult, null, 2) } error=${ err.toString() } ${ err.stack }`);
+
+ throw err;
+ }
}
/* eslint-enable no-await-in-loop */
diff --git a/app/federation/server/hooks/afterLeaveRoom.js b/app/federation/server/hooks/afterLeaveRoom.js
new file mode 100644
index 000000000000..5690a561a8a9
--- /dev/null
+++ b/app/federation/server/hooks/afterLeaveRoom.js
@@ -0,0 +1,53 @@
+import { FederationRoomEvents } from '../../../models/server';
+import { getFederatedRoomData, hasExternalDomain, isLocalUser } from '../functions/helpers';
+import { logger } from '../lib/logger';
+import { normalizers } from '../normalizers';
+import { getFederationDomain } from '../lib/getFederationDomain';
+import { dispatchEvent } from '../handler';
+
+async function afterLeaveRoom(user, room) {
+ const localDomain = getFederationDomain();
+
+ // If there are not federated users on this room, ignore it
+ if (!hasExternalDomain(room) && isLocalUser(user, localDomain)) {
+ return user;
+ }
+
+ logger.client.debug(() => `afterLeaveRoom => user=${ JSON.stringify(user, null, 2) } room=${ JSON.stringify(room, null, 2) }`);
+
+ const { users } = getFederatedRoomData(room);
+
+ try {
+ // Get the domains after leave
+ const domainsAfterLeave = users.map((u) => u.federation.origin);
+
+ //
+ // Normalize the room's federation status
+ //
+ const usersBeforeLeave = users;
+ usersBeforeLeave.push(user);
+
+ // Get the users domains
+ const domainsBeforeLeft = usersBeforeLeave.map((u) => u.federation.origin);
+
+ //
+ // Create the user left event
+ //
+ const normalizedSourceUser = normalizers.normalizeUser(user);
+
+ const userLeftEvent = await FederationRoomEvents.createUserLeftEvent(localDomain, room._id, normalizedSourceUser, domainsAfterLeave);
+
+ // Dispatch the events
+ dispatchEvent(domainsBeforeLeft, userLeftEvent);
+ } catch (err) {
+ logger.client.error('afterLeaveRoom => Could not make user leave:', err);
+ }
+
+ return user;
+}
+
+export const definition = {
+ hook: 'afterLeaveRoom',
+ callback: (roomOwner, room) => Promise.await(afterLeaveRoom(roomOwner, room)),
+ id: 'federation-after-leave-room',
+};
diff --git a/app/federation/server/hooks/afterRemoveFromRoom.js b/app/federation/server/hooks/afterRemoveFromRoom.js
index ad876ae36577..bcc5fb2baaad 100644
--- a/app/federation/server/hooks/afterRemoveFromRoom.js
+++ b/app/federation/server/hooks/afterRemoveFromRoom.js
@@ -42,7 +42,7 @@ async function afterRemoveFromRoom(involvedUsers, room) {
// Dispatch the events
dispatchEvent(domainsBeforeRemoval, removeUserEvent);
} catch (err) {
- logger.client.error('afterRemoveFromRoom => Could not add user:', err);
+ logger.client.error('afterRemoveFromRoom => Could not remove user:', err);
}
return involvedUsers;
diff --git a/app/federation/server/startup/registerCallbacks.js b/app/federation/server/startup/registerCallbacks.js
index 677b4daddcc2..6702140d038c 100644
--- a/app/federation/server/startup/registerCallbacks.js
+++ b/app/federation/server/startup/registerCallbacks.js
@@ -3,6 +3,7 @@ import { definition as afterAddedToRoomDef } from '../hooks/afterAddedToRoom';
import { definition as afterCreateDirectRoomDef } from '../hooks/afterCreateDirectRoom';
import { definition as afterCreateRoomDef } from '../hooks/afterCreateRoom';
import { definition as afterDeleteMessageDef } from '../hooks/afterDeleteMessage';
+import { definition as afterLeaveRoomDef } from '../hooks/afterLeaveRoom';
import { definition as afterMuteUserDef } from '../hooks/afterMuteUser';
import { definition as afterRemoveFromRoomDef } from '../hooks/afterRemoveFromRoom';
import { definition as afterSaveMessageDef } from '../hooks/afterSaveMessage';
@@ -15,6 +16,7 @@ registerCallback(afterAddedToRoomDef);
registerCallback(afterCreateDirectRoomDef);
registerCallback(afterCreateRoomDef);
registerCallback(afterDeleteMessageDef);
+registerCallback(afterLeaveRoomDef);
registerCallback(afterMuteUserDef);
registerCallback(beforeDeleteRoomDef);
registerCallback(afterSaveMessageDef);
diff --git a/app/github-enterprise/client/index.js b/app/github-enterprise/client/index.js
index e44dbe195eff..c43a3da658b4 100644
--- a/app/github-enterprise/client/index.js
+++ b/app/github-enterprise/client/index.js
@@ -1 +1,2 @@
import '../lib/common';
+import './github-enterprise-login-button.css';
diff --git a/app/gitlab/client/index.js b/app/gitlab/client/index.js
index e44dbe195eff..4af492503474 100644
--- a/app/gitlab/client/index.js
+++ b/app/gitlab/client/index.js
@@ -1 +1,2 @@
import '../lib/common';
+import './gitlab-login-button.css';
diff --git a/app/importer-csv/server/importer.js b/app/importer-csv/server/importer.js
index bac5254a8773..4e113e5c5726 100644
--- a/app/importer-csv/server/importer.js
+++ b/app/importer-csv/server/importer.js
@@ -86,9 +86,7 @@ export class CsvImporter extends Base {
const parsedUsers = this.csvParser(entry.getData().toString());
tempUsers = parsedUsers.map((u) => ({ id: Random.id(), username: u[0].trim(), email: u[1].trim(), name: u[2].trim() }));
- this.collection.insert({ import: this.importRecord._id, importer: this.name, type: 'users', users: tempUsers });
super.updateRecord({ 'count.users': tempUsers.length });
- super.addCountToTotal(tempUsers.length);
return increaseCount();
}
@@ -141,7 +139,8 @@ export class CsvImporter extends Base {
increaseCount();
});
- super.addCountToTotal(messagesCount);
+ this.collection.insert({ import: this.importRecord._id, importer: this.name, type: 'users', users: tempUsers });
+ super.addCountToTotal(messagesCount + tempUsers.length);
ImporterWebsocket.progressUpdated({ rate: 100 });
if (hasDirectMessages) {
diff --git a/app/importer-slack/server/importer.js b/app/importer-slack/server/importer.js
index 8cbfb213f575..5b0823423ef4 100644
--- a/app/importer-slack/server/importer.js
+++ b/app/importer-slack/server/importer.js
@@ -13,8 +13,9 @@ import {
} from '../../importer/server';
import { getUserAvatarURL } from '../../utils/lib/getUserAvatarURL';
import { Users, Rooms, Messages } from '../../models';
-import { insertMessage } from '../../lib';
+import { insertMessage, createDirectRoom } from '../../lib';
import { getValidRoomName } from '../../utils';
+import { settings } from '../../settings/lib/settings';
export class SlackImporter extends Base {
constructor(info, importRecord) {
@@ -399,11 +400,14 @@ export class SlackImporter extends Base {
msg: file.url_private_download || '',
_importFile: this.convertSlackFileToPendingFile(file),
};
+ if (message.thread_ts && (message.thread_ts !== message.ts)) {
+ msgObj.tmid = `slack-${ slackChannel.id }-${ message.thread_ts.replace(/\./g, '-') }`;
+ }
insertMessage(fileUser, msgObj, room, this._anyExistingSlackMessage);
});
}
- if (message.subtype) {
+ if (message.subtype && (message.subtype !== 'thread_broadcast')) {
this.processMessageSubType(message, room, msgDataDefaults, missedTypes);
} else {
const user = this.getRocketUserFromUserId(message.user);
@@ -419,6 +423,29 @@ export class SlackImporter extends Base {
},
};
+ if (message.thread_ts) {
+ if (message.thread_ts === message.ts) {
+ if (message.reply_users) {
+ msgObj.replies = [];
+ message.reply_users.forEach(function(item) {
+ msgObj.replies.push(item);
+ });
+ } else if (message.replies) {
+ msgObj.replies = [];
+ message.replies.forEach(function(item) {
+ msgObj.replies.push(item.user);
+ });
+ } else {
+ this.logger.warn(`Failed to import the parent comment, message: ${ msgDataDefaults._id }. Missing replies/reply_users field`);
+ }
+
+ msgObj.tcount = message.reply_count;
+ msgObj.tlm = new Date(parseInt(message.latest_reply.split('.')[0]) * 1000);
+ } else {
+ msgObj.tmid = `slack-${ slackChannel.id }-${ message.thread_ts.replace(/\./g, '-') }`;
+ }
+ }
+
if (message.edited) {
msgObj.editedAt = new Date(parseInt(message.edited.ts.split('.')[0]) * 1000);
const editedBy = this.getRocketUserFromUserId(message.edited.user);
@@ -526,13 +553,17 @@ export class SlackImporter extends Base {
return existingRoom;
}
- _getChannelUserList(channel) {
+ _getChannelUserList(channel, returnObject = false, includeCreator = false) {
return channel.members.reduce((ret, member) => {
- if (member !== channel.creator) {
+ if (includeCreator || member !== channel.creator) {
const user = this.getRocketUserFromUserId(member);
// Don't add bots to the room's member list; Since they are all replaced with rocket.cat, it could cause duplicated subscriptions
if (user && user.username && user._id !== 'rocket.cat') {
- ret.push(user.username);
+ if (returnObject) {
+ ret.push(user);
+ } else {
+ ret.push(user.username);
+ }
}
}
return ret;
@@ -599,7 +630,50 @@ export class SlackImporter extends Base {
}
_importMpims(startedByUserId, channelNames) {
- this._importPrivateGroupList(startedByUserId, this.mpims, channelNames);
+ if (!this.mpims || !this.mpims.channels) {
+ return;
+ }
+
+ const maxUsers = settings.get('DirectMesssage_maxUsers') || 1;
+
+ this.mpims.channels.forEach((channel) => {
+ if (!channel.do_import) {
+ this.addCountCompleted(1);
+ return;
+ }
+
+ channelNames.push(channel.name);
+
+ Meteor.runAsUser(startedByUserId, () => {
+ const users = this._getChannelUserList(channel, true, true);
+ const existingRoom = Rooms.findOneDirectRoomContainingAllUserIDs(users, { fields: { _id: 1 } });
+
+ if (existingRoom) {
+ channel.rocketId = existingRoom._id;
+ Rooms.update({ _id: channel.rocketId }, { $addToSet: { importIds: channel.id } });
+ } else {
+ const userId = this.getImportedRocketUserIdFromSlackUserId(channel.creator) || startedByUserId;
+ Meteor.runAsUser(userId, () => {
+ // If there are too many users for a direct room, then create a private group instead
+ if (users.length > maxUsers) {
+ const usernames = users.map((user) => user.username);
+ const group = Meteor.call('createPrivateGroup', channel.name, usernames);
+ channel.rocketId = group.rid;
+ return;
+ }
+
+ const newRoom = createDirectRoom(users);
+ channel.rocketId = newRoom._id;
+ });
+
+ this._updateImportedChannelTopicAndDescription(channel);
+ }
+
+ this.addCountCompleted(1);
+ });
+ });
+
+ this.collection.update({ _id: this.mpims._id }, { $set: { channels: this.mpims.channels } });
}
_importDMs(startedByUserId, channelNames) {
@@ -616,33 +690,26 @@ export class SlackImporter extends Base {
}
Meteor.runAsUser(startedByUserId, () => {
- const userId1 = this.getImportedRocketUserIdFromSlackUserId(channel.members[0]);
- const userId2 = this.getImportedRocketUserIdFromSlackUserId(channel.members[1]);
+ const user1 = this.getRocketUserFromUserId(channel.members[0]);
+ const user2 = this.getRocketUserFromUserId(channel.members[1]);
- const rid = [userId1, userId2].sort().join('');
- const existingRoom = Rooms.findOneById(rid);
+ const existingRoom = Rooms.findOneDirectRoomContainingAllUserIDs([user1, user2], { fields: { _id: 1 } });
if (existingRoom) {
channel.rocketId = existingRoom._id;
Rooms.update({ _id: channel.rocketId }, { $addToSet: { importIds: channel.id } });
} else {
- if (!userId1) {
+ if (!user1) {
this.logger.error(`DM creation: User not found for id ${ channel.members[0] } and channel id ${ channel.id }`);
return;
}
- if (!userId2) {
+ if (!user2) {
this.logger.error(`DM creation: User not found for id ${ channel.members[1] } and channel id ${ channel.id }`);
return;
}
- const user = this._getBasicUserData(userId2);
-
- if (!user) {
- return;
- }
-
- const roomInfo = Meteor.runAsUser(userId1, () => Meteor.call('createDirectMessage', user.username));
+ const roomInfo = Meteor.runAsUser(user1._id, () => Meteor.call('createDirectMessage', user2.username));
channel.rocketId = roomInfo.rid;
Rooms.update({ _id: channel.rocketId }, { $addToSet: { importIds: channel.id } });
}
@@ -765,6 +832,7 @@ export class SlackImporter extends Base {
try {
this._importUsers(startedByUserId);
+ super.updateProgress(ProgressStep.IMPORTING_CHANNELS);
this._importChannels(startedByUserId, channelNames);
this._importGroups(startedByUserId, channelNames);
this._importMpims(startedByUserId, channelNames);
diff --git a/app/importer/client/admin/adminImport.html b/app/importer/client/admin/adminImport.html
deleted file mode 100644
index 87fc49ab06d6..000000000000
--- a/app/importer/client/admin/adminImport.html
+++ /dev/null
@@ -1,55 +0,0 @@
-
-
- {{> header sectionName="Import" fullpage="true"}}
-
- {{#unless hasPermission 'run-import'}}
-
{{_ "You_are_not_authorized_to_view_this_page"}}
- {{else}}
- {{#if isPreparing}}
- {{> loading}}
- {{else}}
-
-
-
- {{#if anySuccessfulSlackImports}}
-
- {{/if}}
-
-
- {{#if canShowCurrentOperation}}
-
-
{{_ "Current_Import_Operation"}}
-
-
-
- {{> importOperationSummary operation}}
-
- {{#if canContinueOperation}}
-
- {{else}}
- {{#if canCheckOperationProgress}}
-
- {{/if}}
- {{/if}}
-
- {{/if}}
-
- {{#if history}}
-
-
{{_ "Recent_Import_History"}}
-
-
- {{#each history}}
- {{#if isNotCurrentOperation}}
-
- {{> importOperationSummary .}}
-
- {{/if}}
- {{/each}}
- {{/if}}
- {{/if}}
-
- {{/unless}}
-
-
-
diff --git a/app/importer/client/admin/adminImport.js b/app/importer/client/admin/adminImport.js
deleted file mode 100644
index ddbf8fc0f8d6..000000000000
--- a/app/importer/client/admin/adminImport.js
+++ /dev/null
@@ -1,136 +0,0 @@
-import { Tracker } from 'meteor/tracker';
-import { FlowRouter } from 'meteor/kadira:flow-router';
-import { Template } from 'meteor/templating';
-import { ReactiveVar } from 'meteor/reactive-var';
-import toastr from 'toastr';
-
-import { t, APIClient } from '../../../utils';
-import { SideNav } from '../../../ui-utils/client';
-import { ImportWaitingStates, ImportFileReadyStates, ImportPreparingStartedStates, ImportingStartedStates, ProgressStep } from '../../lib/ImporterProgressStep';
-
-import './adminImport.html';
-import './importOperationSummary.js';
-
-Template.adminImport.helpers({
- isPreparing() {
- return Template.instance().preparing.get();
- },
- history() {
- return Template.instance().history.get();
- },
-
- operation() {
- return Template.instance().operation.get();
- },
-
- isNotCurrentOperation() {
- const operation = Template.instance().operation.get();
- if (!operation) {
- return true;
- }
-
- return operation._id !== this._id || !operation.valid;
- },
-
- canShowCurrentOperation() {
- const operation = Template.instance().operation.get();
- return operation && operation.valid;
- },
-
- canContinueOperation() {
- const operation = Template.instance().operation.get();
- if (!operation || !operation.valid) {
- return false;
- }
-
- const possibleStatus = [ProgressStep.USER_SELECTION].concat(ImportWaitingStates).concat(ImportFileReadyStates).concat(ImportPreparingStartedStates);
- return possibleStatus.includes(operation.status);
- },
-
- canCheckOperationProgress() {
- const operation = Template.instance().operation.get();
- if (!operation || !operation.valid) {
- return false;
- }
-
- return ImportingStartedStates.includes(operation.status);
- },
-
- anySuccessfulSlackImports() {
- const history = Template.instance().history.get();
- if (!history) {
- return false;
- }
-
- for (const op of history) {
- if (op.importerKey === 'slack' && op.status === ProgressStep.DONE) {
- return true;
- }
- }
-
- return false;
- },
-});
-
-Template.adminImport.events({
- 'click .new-import-btn'() {
- FlowRouter.go('/admin/import/new');
- },
- 'click .download-slack-files-btn'(event, template) {
- template.preparing.set(true);
- APIClient.post('v1/downloadPendingFiles').then((data) => {
- template.preparing.set(false);
- if (data.count) {
- toastr.success(t('File_Downloads_Started'));
- FlowRouter.go('/admin/import/progress');
- } else {
- toastr.success(t('No_files_left_to_download'));
- }
- }).catch((error) => {
- template.preparing.set(false);
- if (error) {
- console.error(error);
- toastr.error(t('Failed_To_Download_Files'));
- }
- });
- },
- 'click .prepare-btn'() {
- FlowRouter.go('/admin/import/prepare');
- },
- 'click .progress-btn'() {
- FlowRouter.go('/admin/import/progress');
- },
-});
-
-Template.adminImport.onCreated(function() {
- const instance = this;
- this.preparing = new ReactiveVar(true);
- this.history = new ReactiveVar([]);
- this.operation = new ReactiveVar(false);
-
- APIClient.get('v1/getCurrentImportOperation').then((data) => {
- instance.operation.set(data.operation);
-
- APIClient.get('v1/getLatestImportOperations').then((data) => {
- instance.history.set(data);
- instance.preparing.set(false);
- }).catch((error) => {
- if (error) {
- toastr.error(t('Failed_To_Load_Import_History'));
- instance.preparing.set(false);
- }
- });
- }).catch((error) => {
- if (error) {
- toastr.error(t('Failed_To_Load_Import_Operation'));
- instance.preparing.set(false);
- }
- });
-});
-
-Template.adminImport.onRendered(() => {
- Tracker.afterFlush(() => {
- SideNav.setFlex('adminFlex');
- SideNav.openFlex();
- });
-});
diff --git a/app/importer/client/admin/adminImportNew.html b/app/importer/client/admin/adminImportNew.html
deleted file mode 100644
index 17e9230b5cb2..000000000000
--- a/app/importer/client/admin/adminImportNew.html
+++ /dev/null
@@ -1,83 +0,0 @@
-
-
- {{> header sectionName=pageTitle}}
-
-
{{_ "Back_to_imports"}}
-
- {{#unless hasPermission 'run-import'}}
-
{{_ "You_are_not_authorized_to_view_this_page"}}
- {{else}}
- {{#if isPreparing}}
- {{> loading}}
- {{else}}
-
-
-
-
- {{/if}}
-
- {{/unless}}
-
-
-
diff --git a/app/importer/client/admin/adminImportNew.js b/app/importer/client/admin/adminImportNew.js
deleted file mode 100644
index 2d643312dc1c..000000000000
--- a/app/importer/client/admin/adminImportNew.js
+++ /dev/null
@@ -1,158 +0,0 @@
-import { Tracker } from 'meteor/tracker';
-import { FlowRouter } from 'meteor/kadira:flow-router';
-import { Template } from 'meteor/templating';
-import { TAPi18n } from 'meteor/rocketchat:tap-i18n';
-import { ReactiveVar } from 'meteor/reactive-var';
-import toastr from 'toastr';
-
-import { t, APIClient } from '../../../utils';
-import { SideNav } from '../../../ui-utils/client';
-import { settings } from '../../../settings';
-import { showImporterException } from '../functions/showImporterException';
-
-import { Importers } from '..';
-
-import './adminImportNew.html';
-
-Template.adminImportNew.helpers({
- isPreparing() {
- return Template.instance().preparing.get();
- },
- importers() {
- return Importers.getAll();
- },
- pageTitle() {
- const importerKey = Template.instance().importType.get();
- if (!importerKey) {
- return t('Import_New_File');
- }
-
- const importer = Importers.get(importerKey);
- if (!importer) {
- return t('Import_New_File');
- }
-
- return TAPi18n.__('Importer_From_Description', { from: t(importer.name) });
- },
- importType() {
- return Template.instance().importType.get();
- },
- fileType() {
- return Template.instance().fileType.get();
- },
- isImporterSelected() {
- return Template.instance().importType.get();
- },
- isFileTypeSelected() {
- return Template.instance().fileType.get();
- },
- isUpload() {
- return Template.instance().fileType.get() === 'upload';
- },
- isPublicURL() {
- return Template.instance().fileType.get() === 'url';
- },
- isServerFile() {
- return Template.instance().fileType.get() === 'path';
- },
- fileSizeLimitMessage() {
- const maxFileSize = settings.get('FileUpload_MaxFileSize');
- let message;
-
- if (maxFileSize > 0) {
- const sizeInKb = maxFileSize / 1024;
- const sizeInMb = sizeInKb / 1024;
-
- let fileSizeMessage;
- if (sizeInMb > 0) {
- fileSizeMessage = TAPi18n.__('FileSize_MB', { fileSize: sizeInMb.toFixed(2) });
- } else if (sizeInKb > 0) {
- fileSizeMessage = TAPi18n.__('FileSize_KB', { fileSize: sizeInKb.toFixed(2) });
- } else {
- fileSizeMessage = TAPi18n.__('FileSize_Bytes', { fileSize: maxFileSize.toFixed(0) });
- }
-
- message = TAPi18n.__('Importer_Upload_FileSize_Message', { maxFileSize: fileSizeMessage });
- } else {
- message = TAPi18n.__('Importer_Upload_Unlimited_FileSize');
- }
-
- return message;
- },
-});
-
-Template.adminImportNew.events({
- 'change .file-type'(event, template) {
- template.fileType.set($('select[name=file-type]').val());
- },
- 'change .import-type'(event, template) {
- template.importType.set($('select[name=import-type]').val());
- },
-
- 'change .import-file-input'(event, template) {
- const importType = template.importType.get();
-
- const e = event.originalEvent || event;
- let { files } = e.target;
- if (!files || (files.length === 0)) {
- files = (e.dataTransfer != null ? e.dataTransfer.files : undefined) || [];
- }
-
- Array.from(files).forEach((file) => {
- template.preparing.set(true);
-
- const reader = new FileReader();
-
- reader.readAsDataURL(file);
- reader.onloadend = () => {
- APIClient.post('v1/uploadImportFile', {
- binaryContent: reader.result.split(';base64,')[1],
- contentType: file.type,
- fileName: file.name,
- importerKey: importType,
- }).then(() => {
- toastr.success(t('File_uploaded_successfully'));
- FlowRouter.go('/admin/import/prepare');
- }).catch((error) => {
- if (error) {
- showImporterException(error);
- template.preparing.set(false);
- }
- });
- };
- });
- },
-
- 'click .import-btn'(event, template) {
- const importType = template.importType.get();
- const fileUrl = $('.import-file-url').val();
-
- template.preparing.set(true);
-
- APIClient.post('v1/downloadPublicImportFile', {
- fileUrl,
- importerKey: importType,
- }).then(() => {
- toastr.success(t('Import_requested_successfully'));
- FlowRouter.go('/admin/import/prepare');
- }).catch((error) => {
- if (error) {
- showImporterException(error);
- template.preparing.set(false);
- }
- });
- },
-});
-
-Template.adminImportNew.onCreated(function() {
- this.preparing = new ReactiveVar(false);
- this.importType = new ReactiveVar('');
- this.fileType = new ReactiveVar('upload');
-});
-
-Template.adminImportNew.onRendered(() => {
- Tracker.afterFlush(() => {
- SideNav.setFlex('adminFlex');
- SideNav.openFlex();
- });
-});
diff --git a/app/importer/client/admin/adminImportPrepare.html b/app/importer/client/admin/adminImportPrepare.html
deleted file mode 100644
index 54f4fc2369f4..000000000000
--- a/app/importer/client/admin/adminImportPrepare.html
+++ /dev/null
@@ -1,71 +0,0 @@
-
-
- {{> header sectionName=pageTitle}}
-
- {{#unless hasPermission 'run-import'}}
-
{{_ "You_are_not_authorized_to_view_this_page"}}
- {{else}}
- {{#if isPreparing}}
- {{#if hasProgressRate}}
- {{ progressRate }}
- {{/if}}
-
- {{> loading}}
- {{else}}
-
{{_ "Back_to_imports"}}
-
-
-
{{_ "Actions"}}
-
-
-
-
-
-
-
-
-
{{_ "Messages"}}: {{message_count}}
-
-
- {{#if users.length}}
-
- {{/if}}
-
- {{#if channels.length}}
-
- {{/if}}
- {{/if}}
- {{/unless}}
-
-
-
diff --git a/app/importer/client/admin/adminImportPrepare.js b/app/importer/client/admin/adminImportPrepare.js
deleted file mode 100644
index f37dc66a4b80..000000000000
--- a/app/importer/client/admin/adminImportPrepare.js
+++ /dev/null
@@ -1,203 +0,0 @@
-import { Tracker } from 'meteor/tracker';
-import { FlowRouter } from 'meteor/kadira:flow-router';
-import { Template } from 'meteor/templating';
-import { ReactiveVar } from 'meteor/reactive-var';
-import toastr from 'toastr';
-
-import { t, APIClient } from '../../../utils';
-import { SideNav } from '../../../ui-utils/client';
-import { ProgressStep, ImportWaitingStates, ImportFileReadyStates, ImportPreparingStartedStates, ImportingStartedStates, ImportingErrorStates } from '../../lib/ImporterProgressStep';
-import { showImporterException } from '../functions/showImporterException';
-
-import { ImporterWebsocketReceiver } from '..';
-
-import './adminImportPrepare.html';
-
-Template.adminImportPrepare.helpers({
- isPreparing() {
- return Template.instance().preparing.get();
- },
- hasProgressRate() {
- return Template.instance().progressRate.get() !== false;
- },
- progressRate() {
- const rate = Template.instance().progressRate.get();
- if (rate) {
- return `${ rate }%`;
- }
-
- return '';
- },
- pageTitle() {
- return t('Importing_Data');
- },
- users() {
- return Template.instance().users.get();
- },
- channels() {
- return Template.instance().channels.get();
- },
- message_count() {
- return Template.instance().message_count.get();
- },
-});
-
-Template.adminImportPrepare.events({
- 'click .button.start'(event, template) {
- const btn = this;
- $(btn).prop('disabled', true);
- for (const user of Array.from(template.users.get())) {
- user.do_import = $(`[name='${ user.user_id }']`).is(':checked');
- }
-
- for (const channel of Array.from(template.channels.get())) {
- channel.do_import = $(`[name='${ channel.channel_id }']`).is(':checked');
- }
-
- APIClient.post('v1/startImport', { input: { users: template.users.get(), channels: template.channels.get() } }).then(() => {
- template.users.set([]);
- template.channels.set([]);
- return FlowRouter.go('/admin/import/progress');
- }).catch((error) => {
- if (error) {
- showImporterException(error, 'Failed_To_Start_Import');
- return FlowRouter.go('/admin/import');
- }
- });
- },
-
- 'click .button.uncheck-deleted-users'(event, template) {
- Array.from(template.users.get()).filter((user) => user.is_deleted).map((user) => {
- const box = $(`[name=${ user.user_id }]`);
- return box && box.length && box[0].checked && box.click();
- });
- },
-
- 'click .button.uncheck-archived-channels'(event, template) {
- Array.from(template.channels.get()).filter((channel) => channel.is_archived).map((channel) => {
- const box = $(`[name=${ channel.channel_id }]`);
- return box && box.length && box[0].checked && box.click();
- });
- },
-
-});
-
-function getImportFileData(template) {
- APIClient.get('v1/getImportFileData').then((data) => {
- if (!data) {
- console.warn('The importer is not set up correctly, as it did not return any data.');
- toastr.error(t('Importer_not_setup'));
- return FlowRouter.go('/admin/import');
- }
-
- if (data.waiting) {
- setTimeout(() => {
- getImportFileData(template);
- }, 1000);
- return;
- }
-
- if (data.step) {
- console.warn('Invalid file, contains `data.step`.', data);
- toastr.error(t('Failed_To_Load_Import_Data'));
- return FlowRouter.go('/admin/import');
- }
-
- template.users.set(data.users);
- template.channels.set(data.channels);
- template.message_count.set(data.message_count);
- template.preparing.set(false);
- template.progressRate.set(false);
- }).catch((error) => {
- if (error) {
- showImporterException(error, 'Failed_To_Load_Import_Data');
- return FlowRouter.go('/admin/import');
- }
- });
-}
-
-function loadOperation(template) {
- APIClient.get('v1/getCurrentImportOperation').then((data) => {
- const { operation } = data;
-
- if (!operation.valid) {
- return FlowRouter.go('/admin/import/new');
- }
-
- // If the import has already started, move to the progress screen
- if (ImportingStartedStates.includes(operation.status)) {
- return FlowRouter.go('/admin/import/progress');
- }
-
- // The getImportFileData method can handle it if the state is:
- // 1) ready to select the users,
- // 2) preparing
- // 3) ready to be prepared
-
- if (operation.status === ProgressStep.USER_SELECTION || ImportPreparingStartedStates.includes(operation.status) || ImportFileReadyStates.includes(operation.status)) {
- if (!template.callbackRegistered) {
- ImporterWebsocketReceiver.registerCallback(template.progressUpdated);
- template.callbackRegistered = true;
- }
-
- getImportFileData(template);
- return template.preparing.set(true);
- }
-
- // We're still waiting for a file... This shouldn't take long
- if (ImportWaitingStates.includes(operation.status)) {
- setTimeout(() => {
- loadOperation(template);
- }, 1000);
-
- return template.preparing.set(true);
- }
-
- if (ImportingErrorStates.includes(operation.status)) {
- toastr.error(t('Import_Operation_Failed'));
- return FlowRouter.go('/admin/import');
- }
-
- if (operation.status === ProgressStep.DONE) {
- return FlowRouter.go('/admin/import');
- }
-
- toastr.error(t('Unknown_Import_State'));
- return FlowRouter.go('/admin/import');
- }).catch((error) => {
- if (error) {
- toastr.error(t('Failed_To_Load_Import_Data'));
- return FlowRouter.go('/admin/import');
- }
- });
-}
-
-Template.adminImportPrepare.onCreated(function() {
- this.preparing = new ReactiveVar(true);
- this.progressRate = new ReactiveVar(false);
- this.callbackRegistered = false;
- this.users = new ReactiveVar([]);
- this.channels = new ReactiveVar([]);
- this.message_count = new ReactiveVar(0);
-
- this.progressUpdated = (progress) => {
- if ('rate' in progress) {
- const { rate } = progress;
- this.progressRate.set(rate);
- }
- };
-
- loadOperation(this);
-});
-
-Template.adminImportPrepare.onRendered(() => {
- Tracker.afterFlush(() => {
- SideNav.setFlex('adminFlex');
- SideNav.openFlex();
- });
-});
-
-Template.adminImportPrepare.onDestroyed(function() {
- this.callbackRegistered = false;
- ImporterWebsocketReceiver.unregisterCallback(this.progressUpdated);
-});
diff --git a/app/importer/client/admin/adminImportProgress.html b/app/importer/client/admin/adminImportProgress.html
deleted file mode 100644
index 05337b38b150..000000000000
--- a/app/importer/client/admin/adminImportProgress.html
+++ /dev/null
@@ -1,8 +0,0 @@
-
- {{> loading}}
- {{step}}
- {{completed}} / {{total}}
- {{progressRate}}
-
- {{_ "You_can_close_this_window_now"}}
-
diff --git a/app/importer/client/admin/adminImportProgress.js b/app/importer/client/admin/adminImportProgress.js
deleted file mode 100644
index cf59ab3c79cc..000000000000
--- a/app/importer/client/admin/adminImportProgress.js
+++ /dev/null
@@ -1,137 +0,0 @@
-import { ReactiveVar } from 'meteor/reactive-var';
-import { FlowRouter } from 'meteor/kadira:flow-router';
-import { Template } from 'meteor/templating';
-import toastr from 'toastr';
-
-import { t, handleError, APIClient } from '../../../utils';
-import { ProgressStep, ImportingStartedStates } from '../../lib/ImporterProgressStep';
-
-import { ImporterWebsocketReceiver } from '..';
-
-import './adminImportProgress.html';
-
-Template.adminImportProgress.helpers({
- step() {
- return Template.instance().step.get();
- },
- completed() {
- return Template.instance().completed.get();
- },
- total() {
- return Template.instance().total.get();
- },
- progressRate() {
- try {
- const instance = Template.instance();
- const completed = instance.completed.get();
- const total = instance.total.get();
-
- const rate = Math.floor(completed * 10000 / total) / 100;
-
- if (isNaN(rate)) {
- return '';
- }
-
- return `${ rate }%`;
- } catch {
- return '';
- }
- },
-});
-
-Template.adminImportProgress.onCreated(function() {
- const template = this;
- this.operation = new ReactiveVar(false);
- this.step = new ReactiveVar(t('Loading...'));
- this.completed = new ReactiveVar(0);
- this.total = new ReactiveVar(0);
-
- let importerKey = false;
-
- function _updateProgress(progress) {
- switch (progress.step) {
- case ProgressStep.DONE:
- toastr.success(t(progress.step[0].toUpperCase() + progress.step.slice(1)));
- return FlowRouter.go('/admin/import');
- case ProgressStep.ERROR:
- case ProgressStep.CANCELLED:
- toastr.error(t(progress.step[0].toUpperCase() + progress.step.slice(1)));
- return FlowRouter.go('/admin/import');
- default:
- template.step.set(t(progress.step[0].toUpperCase() + progress.step.slice(1)));
- if (progress.count.completed) {
- template.completed.set(progress.count.completed);
- }
- if (progress.count.total) {
- template.total.set(progress.count.total);
- }
- break;
- }
- }
-
- this.progressUpdated = function _progressUpdated(progress) {
- if (progress.key.toLowerCase() !== importerKey) {
- return;
- }
-
- _updateProgress(progress);
- };
-
- APIClient.get('v1/getCurrentImportOperation').then((data) => {
- const { operation } = data;
-
- if (!operation.valid) {
- return FlowRouter.go('/admin/import');
- }
-
- // If the import has not started, move to the prepare screen
- if (!ImportingStartedStates.includes(operation.status)) {
- return FlowRouter.go('/admin/import/prepare');
- }
-
- importerKey = operation.importerKey;
- template.operation.set(operation);
- if (operation.count) {
- if (operation.count.total) {
- template.total.set(operation.count.total);
- }
- if (operation.count.completed) {
- template.completed.set(operation.count.completed);
- }
- }
-
- APIClient.get('v1/getImportProgress').then((progress) => {
- if (!progress) {
- toastr.warning(t('Importer_not_in_progress'));
- return FlowRouter.go('/admin/import/prepare');
- }
-
- const whereTo = _updateProgress(progress);
-
- if (!whereTo) {
- ImporterWebsocketReceiver.registerCallback(template.progressUpdated);
- }
- }).catch((error) => {
- console.warn('Error on getting the import progress:', error);
-
- if (error) {
- handleError(error);
- } else {
- toastr.error(t('Failed_To_Load_Import_Data'));
- }
-
- return FlowRouter.go('/admin/import');
- });
- }).catch((error) => {
- if (error) {
- handleError(error);
- } else {
- toastr.error(t('Failed_To_Load_Import_Data'));
- }
- return FlowRouter.go('/admin/import');
- });
-});
-
-Template.adminImportProgress.onDestroyed(function() {
- ImporterWebsocketReceiver.unregisterCallback(this.progressUpdated);
-});
diff --git a/app/importer/client/admin/importOperationSummary.html b/app/importer/client/admin/importOperationSummary.html
index 2378a4ebddc8..c033a4ffe3b9 100644
--- a/app/importer/client/admin/importOperationSummary.html
+++ b/app/importer/client/admin/importOperationSummary.html
@@ -1,6 +1,6 @@
{{_ "Import_Type"}}:
- {{ type}} [{{ importerKey}}]
+ {{_ type}} [{{ importerKey}}]
{{_ "Last_Updated"}}:
diff --git a/app/importer/client/admin/importOperationSummary.js b/app/importer/client/admin/importOperationSummary.js
deleted file mode 100644
index 0fb5293a044a..000000000000
--- a/app/importer/client/admin/importOperationSummary.js
+++ /dev/null
@@ -1,136 +0,0 @@
-import { Template } from 'meteor/templating';
-
-import { t } from '../../../utils';
-
-import './importOperationSummary.html';
-
-Template.importOperationSummary.helpers({
- lastUpdated() {
- if (!this._updatedAt) {
- return '';
- }
-
- const date = new Date(this._updatedAt);
- return date.toLocaleString();
- },
-
- status() {
- if (!this.status) {
- return '';
- }
-
- return t(this.status.replace('importer_', 'importer_status_'));
- },
-
- fileName() {
- const fileName = this.file;
- if (!fileName) {
- return '';
- }
-
- // If the userid is inside the filename, remove it and anything before it
- const idx = fileName.indexOf(`_${ this.user }_`);
- if (idx >= 0) {
- return fileName.substring(idx + this.user.length + 2);
- }
-
- return fileName;
- },
-
- hasCounters() {
- return Boolean(this.count);
- },
-
- userCount() {
- if (this.count && this.count.users) {
- return this.count.users;
- }
-
- return 0;
- },
-
- channelCount() {
- if (this.count && this.count.channels) {
- return this.count.channels;
- }
-
- return 0;
- },
-
- messageCount() {
- if (this.count && this.count.messages) {
- return this.count.messages;
- }
-
- return 0;
- },
-
- totalCount() {
- if (this.count && this.count.total) {
- return this.count.total;
- }
-
- return 0;
- },
-
- hasErrors() {
- if (!this.fileData) {
- return false;
- }
-
- if (this.fileData.users) {
- for (const user of this.fileData.users) {
- if (user.is_email_taken) {
- return true;
- }
- if (user.error) {
- return true;
- }
- }
- }
-
- if (this.errors && this.errors.length > 0) {
- return true;
- }
-
- return false;
- },
-
- formatedError() {
- if (!this.error) {
- return '';
- }
-
- if (typeof this.error === 'string') {
- return this.error;
- }
-
- if (typeof this.error === 'object') {
- if (this.error.message) {
- return this.error.message;
- }
- if (this.error.error && typeof this.error.error === 'string') {
- return this.error.error;
- }
-
- try {
- const json = JSON.stringify(this.error);
- console.log(json);
- return json;
- } catch (e) {
- return t('Error');
- }
- }
-
- return this.error.toString();
- },
-
- messageTime() {
- if (!this.msg || !this.msg.ts) {
- return '';
- }
-
- const date = new Date(this.msg.ts);
- return date.toLocaleString();
- },
-});
diff --git a/app/importer/client/components/ImportHistoryPage.js b/app/importer/client/components/ImportHistoryPage.js
new file mode 100644
index 000000000000..60b5051ac1ed
--- /dev/null
+++ b/app/importer/client/components/ImportHistoryPage.js
@@ -0,0 +1,123 @@
+import { Button, ButtonGroup, Table } from '@rocket.chat/fuselage';
+import React, { useState, useEffect, useMemo } from 'react';
+
+import { Page } from '../../../../client/components/basic/Page';
+import { useTranslation } from '../../../../client/contexts/TranslationContext';
+import { useToastMessageDispatch } from '../../../../client/contexts/ToastMessagesContext';
+import { useRoute } from '../../../../client/contexts/RouterContext';
+import { useEndpoint } from '../../../../client/contexts/ServerContext';
+import { ProgressStep } from '../../lib/ImporterProgressStep';
+import ImportOperationSummary from './ImportOperationSummary';
+import { useSafely } from '../../../../client/hooks/useSafely';
+import { useMediaQuery } from '../../../ui/client/views/app/components/hooks';
+
+function ImportHistoryPage() {
+ const t = useTranslation();
+ const dispatchToastMessage = useToastMessageDispatch();
+
+ const [isLoading, setLoading] = useSafely(useState(true));
+ const [currentOperation, setCurrentOperation] = useSafely(useState());
+ const [latestOperations, setLatestOperations] = useSafely(useState([]));
+
+ const getCurrentImportOperation = useEndpoint('GET', 'getCurrentImportOperation');
+ const getLatestImportOperations = useEndpoint('GET', 'getLatestImportOperations');
+ const downloadPendingFiles = useEndpoint('POST', 'downloadPendingFiles');
+
+ const newImportRoute = useRoute('admin-import-new');
+ const importProgressRoute = useRoute('admin-import-progress');
+
+ useEffect(() => {
+ const loadData = async () => {
+ setLoading(true);
+
+ try {
+ const { operation } = await getCurrentImportOperation();
+ setCurrentOperation(operation);
+ } catch (error) {
+ dispatchToastMessage({ type: 'error', message: t('Failed_To_Load_Import_Operation') });
+ }
+
+ try {
+ const operations = await getLatestImportOperations();
+ setLatestOperations(operations);
+ } catch (error) {
+ dispatchToastMessage({ type: 'error', message: t('Failed_To_Load_Import_History') });
+ }
+
+ setLoading(false);
+ };
+
+ loadData();
+ }, []);
+
+ const hasAnySuccessfulSlackImport = useMemo(() =>
+ latestOperations?.some(({ importerKey, status }) => importerKey === 'slack' && status === ProgressStep.DONE), [latestOperations]);
+
+ const handleNewImportClick = () => {
+ newImportRoute.push();
+ };
+
+ const handleDownloadPendingFilesClick = async () => {
+ try {
+ setLoading(true);
+ const { count } = await downloadPendingFiles();
+
+ if (count) {
+ dispatchToastMessage({ type: 'info', message: t('No_files_left_to_download') });
+ setLoading(false);
+ return;
+ }
+
+ dispatchToastMessage({ type: 'info', message: t('File_Downloads_Started') });
+ importProgressRoute.push();
+ } catch (error) {
+ console.error(error);
+ dispatchToastMessage({ type: 'error', message: t('Failed_To_Download_Files') });
+ setLoading(false);
+ }
+ };
+
+ const small = useMediaQuery('(max-width: 768px)');
+
+ return
+
+
+
+ {hasAnySuccessfulSlackImport
+ && }
+
+
+
+
+
+
+ {t('Import_Type')}
+ {t('Last_Updated')}
+ {!small && <>{t('Last_Status')}
+ {t('File')}
+ {t('Counters')}>}
+
+ {!small &&
+ {t('Users')}
+ {t('Channels')}
+ {t('Messages')}
+ {t('Total')}
+ }
+
+
+ {isLoading
+ ? Array.from({ length: 20 }, (_, i) => )
+ : <>
+ {currentOperation?.valid && }
+ {latestOperations
+ ?.filter(({ _id }) => currentOperation?._id !== _id || !currentOperation?.valid)
+ // Forcing valid=false as the current API only accept preparation/progress over currentOperation
+ ?.map((operation) => )}
+ >}
+
+
+
+ ;
+}
+
+export default ImportHistoryPage;
diff --git a/app/importer/client/components/ImportHistoryPage.stories.js b/app/importer/client/components/ImportHistoryPage.stories.js
new file mode 100644
index 000000000000..dc5ba06b244a
--- /dev/null
+++ b/app/importer/client/components/ImportHistoryPage.stories.js
@@ -0,0 +1,10 @@
+import React from 'react';
+
+import ImportHistoryPage from './ImportHistoryPage';
+
+export default {
+ title: 'admin/import/ImportHistoryPage',
+ component: ImportHistoryPage,
+};
+
+export const _default = () => ;
diff --git a/app/importer/client/components/ImportOperationSummary.js b/app/importer/client/components/ImportOperationSummary.js
new file mode 100644
index 000000000000..c940aef934c9
--- /dev/null
+++ b/app/importer/client/components/ImportOperationSummary.js
@@ -0,0 +1,115 @@
+import { Skeleton, Table } from '@rocket.chat/fuselage';
+import React, { useMemo } from 'react';
+
+import { useTranslation } from '../../../../client/contexts/TranslationContext';
+import { useRoute } from '../../../../client/contexts/RouterContext';
+import { useFormatDateAndTime } from '../../../ui/client/views/app/components/hooks';
+import {
+ ImportWaitingStates,
+ ImportFileReadyStates,
+ ImportPreparingStartedStates,
+ ImportingStartedStates,
+ ProgressStep,
+} from '../../lib/ImporterProgressStep';
+
+function ImportOperationSummary({
+ type,
+ _updatedAt,
+ status,
+ file,
+ user,
+ small,
+ count: {
+ users = 0,
+ channels = 0,
+ messages = 0,
+ total = 0,
+ } = {
+ users: null,
+ channels: null,
+ messages: null,
+ total: null,
+ },
+ valid,
+}) {
+ const t = useTranslation();
+ const formatDateAndTime = useFormatDateAndTime();
+
+ const fileName = useMemo(() => {
+ if (!file) {
+ return '';
+ }
+
+ const fileName = file;
+
+ const userPattern = `_${ user }_`;
+ const idx = fileName.indexOf(userPattern);
+ if (idx >= 0) {
+ return fileName.slice(idx + userPattern.length);
+ }
+
+ return fileName;
+ }, [file, user]);
+
+ const canContinue = useMemo(() => valid && [
+ ProgressStep.USER_SELECTION,
+ ...ImportWaitingStates,
+ ...ImportFileReadyStates,
+ ...ImportPreparingStartedStates,
+ ].includes(status), [valid, status]);
+
+ const canCheckProgress = useMemo(() => valid && ImportingStartedStates.includes(status), [valid, status]);
+
+ const prepareImportRoute = useRoute('admin-import-prepare');
+ const importProgressRoute = useRoute('admin-import-progress');
+
+ const handleClick = () => {
+ if (canContinue) {
+ prepareImportRoute.push();
+ return;
+ }
+
+ if (canCheckProgress) {
+ importProgressRoute.push();
+ }
+ };
+
+ const hasAction = canContinue || canCheckProgress;
+
+ const props = hasAction ? {
+ tabIndex: 0,
+ role: 'link',
+ action: true,
+ onClick: handleClick,
+ } : {};
+
+ return
+ {type}
+ {formatDateAndTime(_updatedAt)}
+ {!small && <>{status && t(status.replace('importer_', 'importer_status_'))}
+ {fileName}
+ {users}
+ {channels}
+ {messages}
+ {total}
+ >}
+ ;
+}
+
+function ImportOperationSummarySkeleton({ small }) {
+ return
+
+
+ {!small && <>
+
+
+
+
+
+ >}
+ ;
+}
+
+ImportOperationSummary.Skeleton = ImportOperationSummarySkeleton;
+
+export default ImportOperationSummary;
diff --git a/app/importer/client/components/ImportOperationSummary.stories.js b/app/importer/client/components/ImportOperationSummary.stories.js
new file mode 100644
index 000000000000..ceab4c81dacd
--- /dev/null
+++ b/app/importer/client/components/ImportOperationSummary.stories.js
@@ -0,0 +1,18 @@
+import { Table } from '@rocket.chat/fuselage';
+import React from 'react';
+
+import ImportOperationSummary from './ImportOperationSummary';
+
+export default {
+ title: 'admin/import/ImportOperationSummary',
+ component: ImportOperationSummary,
+ decorators: [(fn) => ],
+};
+
+export const _default = () => ;
+
+export const skeleton = () => ;
diff --git a/app/importer/client/components/ImportProgressPage.js b/app/importer/client/components/ImportProgressPage.js
new file mode 100644
index 000000000000..ec8416cea6b9
--- /dev/null
+++ b/app/importer/client/components/ImportProgressPage.js
@@ -0,0 +1,138 @@
+import { Box, Margins, Throbber } from '@rocket.chat/fuselage';
+import React, { useEffect, useState, useMemo } from 'react';
+import s from 'underscore.string';
+
+import { useTranslation } from '../../../../client/contexts/TranslationContext';
+import { ProgressStep, ImportingStartedStates } from '../../lib/ImporterProgressStep';
+import { useToastMessageDispatch } from '../../../../client/contexts/ToastMessagesContext';
+import { ImporterWebsocketReceiver } from '../ImporterWebsocketReceiver';
+import { useSafely } from '../../../../client/hooks/useSafely';
+import { useEndpoint } from '../../../../client/contexts/ServerContext';
+import { useRoute } from '../../../../client/contexts/RouterContext';
+import { Page } from '../../../../client/components/basic/Page';
+
+function ImportProgressPage() {
+ const t = useTranslation();
+ const dispatchToastMessage = useToastMessageDispatch();
+
+ const [importerKey, setImporterKey] = useSafely(useState(null));
+ const [step, setStep] = useSafely(useState('Loading...'));
+ const [completed, setCompleted] = useSafely(useState(0));
+ const [total, setTotal] = useSafely(useState(0));
+
+ const getCurrentImportOperation = useEndpoint('GET', 'getCurrentImportOperation');
+ const getImportProgress = useEndpoint('GET', 'getImportProgress');
+
+ const importHistoryRoute = useRoute('admin-import');
+ const prepareImportRoute = useRoute('admin-import-prepare');
+
+ useEffect(() => {
+ const loadCurrentOperation = async () => {
+ try {
+ const { operation } = await getCurrentImportOperation();
+
+ if (!operation.valid) {
+ importHistoryRoute.push();
+ return;
+ }
+
+ if (!ImportingStartedStates.includes(operation.status)) {
+ prepareImportRoute.push();
+ return;
+ }
+
+ setImporterKey(operation.importerKey);
+ setCompleted(operation.count.completed);
+ setTotal(operation.count.total);
+ } catch (error) {
+ dispatchToastMessage({ type: 'error', message: error || t('Failed_To_Load_Import_Data') });
+ importHistoryRoute.push();
+ }
+ };
+
+ loadCurrentOperation();
+ }, []);
+
+ useEffect(() => {
+ if (!importerKey) {
+ return;
+ }
+
+ const handleProgressUpdated = ({ key, step, count: { completed = 0, total = 0 } = {} }) => {
+ if (key.toLowerCase() !== importerKey) {
+ return;
+ }
+
+ switch (step) {
+ case ProgressStep.DONE:
+ dispatchToastMessage({ type: 'success', message: t(step[0].toUpperCase() + step.slice(1)) });
+ importHistoryRoute.push();
+ return;
+
+ case ProgressStep.ERROR:
+ case ProgressStep.CANCELLED:
+ dispatchToastMessage({ type: 'error', message: t(step[0].toUpperCase() + step.slice(1)) });
+ importHistoryRoute.push();
+ return;
+
+ default:
+ setStep(step);
+ setCompleted(completed);
+ setTotal(total);
+ break;
+ }
+ };
+
+ const loadImportProgress = async () => {
+ try {
+ const progress = await getImportProgress();
+
+ if (!progress) {
+ dispatchToastMessage({ type: 'warning', message: t('Importer_not_in_progress') });
+ prepareImportRoute.push();
+ return;
+ }
+
+ ImporterWebsocketReceiver.registerCallback(handleProgressUpdated);
+ handleProgressUpdated(progress);
+ } catch (error) {
+ dispatchToastMessage({ type: 'error', message: error || t('Failed_To_Load_Import_Data') });
+ importHistoryRoute.push();
+ }
+ };
+
+ loadImportProgress();
+
+ return () => {
+ ImporterWebsocketReceiver.unregisterCallback(handleProgressUpdated);
+ };
+ }, [importerKey]);
+
+ const progressRate = useMemo(() => {
+ if (total === 0) {
+ return null;
+ }
+
+ return completed / total * 100;
+ });
+
+ return
+
+
+
+
+
+ {t(step[0].toUpperCase() + step.slice(1))}
+ {progressRate
+ ?
+
+ {completed}/{total} ({s.numberFormat(progressRate, 0) }%)
+
+ : }
+
+
+
+ ;
+}
+
+export default ImportProgressPage;
diff --git a/app/importer/client/components/ImportRoute.js b/app/importer/client/components/ImportRoute.js
new file mode 100644
index 000000000000..d44d70efa403
--- /dev/null
+++ b/app/importer/client/components/ImportRoute.js
@@ -0,0 +1,36 @@
+import React from 'react';
+
+import { usePermission } from '../../../../client/contexts/AuthorizationContext';
+import NotAuthorizedPage from '../../../ui-admin/client/components/NotAuthorizedPage';
+import ImportHistoryPage from './ImportHistoryPage';
+import NewImportPage from './NewImportPage';
+import PrepareImportPage from './PrepareImportPage';
+import ImportProgressPage from './ImportProgressPage';
+
+function ImportHistoryRoute({ page }) {
+ const canRunImport = usePermission('run-import');
+
+ if (!canRunImport) {
+ return ;
+ }
+
+ if (page === 'history') {
+ return ;
+ }
+
+ if (page === 'new') {
+ return ;
+ }
+
+ if (page === 'prepare') {
+ return ;
+ }
+
+ if (page === 'progress') {
+ return ;
+ }
+
+ return null;
+}
+
+export default ImportHistoryRoute;
diff --git a/app/importer/client/components/NewImportPage.js b/app/importer/client/components/NewImportPage.js
new file mode 100644
index 000000000000..242a44c4bfe1
--- /dev/null
+++ b/app/importer/client/components/NewImportPage.js
@@ -0,0 +1,247 @@
+import {
+ Box,
+ Button,
+ ButtonGroup,
+ Callout,
+ Chip,
+ Field,
+ Icon,
+ Margins,
+ Select,
+ InputBox,
+ TextInput,
+ Throbber,
+ UrlInput,
+} from '@rocket.chat/fuselage';
+import { useUniqueId } from '@rocket.chat/fuselage-hooks';
+import React, { useState, useMemo, useEffect } from 'react';
+
+import { Page } from '../../../../client/components/basic/Page';
+import { useTranslation } from '../../../../client/contexts/TranslationContext';
+import { useSetting } from '../../../../client/contexts/SettingsContext';
+import { useToastMessageDispatch } from '../../../../client/contexts/ToastMessagesContext';
+import { useRoute, useRouteParameter } from '../../../../client/contexts/RouterContext';
+import { showImporterException } from '../functions/showImporterException';
+import { useEndpoint } from '../../../../client/contexts/ServerContext';
+import { Importers } from '../index';
+import { useSafely } from '../../../../client/hooks/useSafely';
+import { useFormatMemorySize } from '../../../ui/client/views/app/components/hooks';
+
+function NewImportPage() {
+ const t = useTranslation();
+ const dispatchToastMessage = useToastMessageDispatch();
+
+ const [isLoading, setLoading] = useSafely(useState(false));
+ const [fileType, setFileType] = useSafely(useState('upload'));
+ const importerKey = useRouteParameter('importerKey');
+ const importer = useMemo(() => Importers.get(importerKey), [importerKey]);
+
+ const maxFileSize = useSetting('FileUpload_MaxFileSize');
+
+ const importHistoryRoute = useRoute('admin-import');
+ const newImportRoute = useRoute('admin-import-new');
+ const prepareImportRoute = useRoute('admin-import-prepare');
+
+ const uploadImportFile = useEndpoint('POST', 'uploadImportFile');
+ const downloadPublicImportFile = useEndpoint('POST', 'downloadPublicImportFile');
+
+ useEffect(() => {
+ if (importerKey && !importer) {
+ newImportRoute.replace();
+ }
+ }, [importerKey, !importer]);
+
+ const formatMemorySize = useFormatMemorySize();
+
+ const handleBackToImportsButtonClick = () => {
+ importHistoryRoute.push();
+ };
+
+ const handleImporterKeyChange = (importerKey) => {
+ newImportRoute.replace({ importerKey });
+ };
+
+ const handleFileTypeChange = (fileType) => {
+ setFileType(fileType);
+ };
+
+ const [files, setFiles] = useState([]);
+
+ const handleImportFileChange = async (event) => {
+ event = event.originalEvent || event;
+
+ let { files } = event.target;
+ if (!files || (files.length === 0)) {
+ files = (event.dataTransfer != null ? event.dataTransfer.files : undefined) || [];
+ }
+
+ setFiles(Array.from(files));
+ };
+
+ const handleFileUploadChipClick = (file) => () => {
+ setFiles((files) => files.filter((_file) => _file !== file));
+ };
+
+ const handleFileUploadImportButtonClick = async () => {
+ setLoading(true);
+
+ try {
+ await Promise.all(
+ Array.from(files, (file) => new Promise((resolve) => {
+ const reader = new FileReader();
+ reader.readAsDataURL(file);
+ reader.onloadend = async () => {
+ try {
+ await uploadImportFile({
+ binaryContent: reader.result.split(';base64,')[1],
+ contentType: file.type,
+ fileName: file.name,
+ importerKey,
+ });
+ dispatchToastMessage({ type: 'success', message: t('File_uploaded_successfully') });
+ } catch (error) {
+ showImporterException(error);
+ } finally {
+ resolve();
+ }
+ };
+ reader.onerror = () => resolve();
+ })),
+ );
+ prepareImportRoute.push();
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const [fileUrl, setFileUrl] = useSafely(useState(''));
+
+ const handleFileUrlChange = (event) => {
+ setFileUrl(event.currentTarget.value);
+ };
+
+ const handleFileUrlImportButtonClick = async () => {
+ setLoading(true);
+
+ try {
+ await downloadPublicImportFile({ importerKey, fileUrl });
+ dispatchToastMessage({ type: 'success', message: t('Import_requested_successfully') });
+ prepareImportRoute.push();
+ } catch (error) {
+ showImporterException(error);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const [filePath, setFilePath] = useSafely(useState(''));
+
+ const handleFilePathChange = (event) => {
+ setFilePath(event.currentTarget.value);
+ };
+
+ const handleFilePathImportButtonClick = async () => {
+ setLoading(true);
+
+ try {
+ await downloadPublicImportFile({ importerKey, fileUrl: filePath });
+ dispatchToastMessage({ type: 'success', message: t('Import_requested_successfully') });
+ prepareImportRoute.push();
+ } catch (error) {
+ showImporterException(error);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const importerKeySelectId = useUniqueId();
+ const fileTypeSelectId = useUniqueId();
+ const fileSourceInputId = useUniqueId();
+ const handleImportButtonClick = (fileType === 'upload' && handleFileUploadImportButtonClick)
+ || (fileType === 'url' && handleFileUrlImportButtonClick)
+ || (fileType === 'path' && handleFilePathImportButtonClick);
+
+ return
+
+
+
+ {importer && }
+
+
+
+
+
+
+ {t('Import_Type')}
+
+
+ {importer && {t('Importer_From_Description', { from: t(importer.name) })}}
+
+ {importer &&
+ {t('File_Type')}
+
+
+
+ }
+ {importer && <>
+ {fileType === 'upload' && <>
+ {maxFileSize > 0
+ ?
+ {t('Importer_Upload_FileSize_Message', { maxFileSize: formatMemorySize(maxFileSize) })}
+
+ :
+ {t('Importer_Upload_Unlimited_FileSize')}
+ }
+
+ {t('Importer_Source_File')}
+
+
+
+ {files?.length > 0 &&
+ {files.map((file, i) => {file.name})}
+ }
+
+ >}
+ {fileType === 'url' &&
+ {t('File_URL')}
+
+
+
+ }
+ {fileType === 'path' &&
+ {t('File_Path')}
+
+
+
+ }
+ >}
+
+
+
+ ;
+}
+
+export default NewImportPage;
diff --git a/app/importer/client/components/NewImportPage.stories.js b/app/importer/client/components/NewImportPage.stories.js
new file mode 100644
index 000000000000..029039865d07
--- /dev/null
+++ b/app/importer/client/components/NewImportPage.stories.js
@@ -0,0 +1,10 @@
+import React from 'react';
+
+import NewImportPage from './NewImportPage';
+
+export default {
+ title: 'admin/import/NewImportPage',
+ component: NewImportPage,
+};
+
+export const _default = () => ;
diff --git a/app/importer/client/components/PrepareImportPage.js b/app/importer/client/components/PrepareImportPage.js
new file mode 100644
index 000000000000..48e05b7c91e5
--- /dev/null
+++ b/app/importer/client/components/PrepareImportPage.js
@@ -0,0 +1,348 @@
+import {
+ Badge,
+ Box,
+ Button,
+ ButtonGroup,
+ CheckBox,
+ Icon,
+ Margins,
+ Table,
+ Tag,
+ Throbber,
+ Pagination,
+ Tabs,
+} from '@rocket.chat/fuselage';
+import React, { useEffect, useState, useMemo, useCallback } from 'react';
+import s from 'underscore.string';
+
+import { Page } from '../../../../client/components/basic/Page';
+import { useTranslation } from '../../../../client/contexts/TranslationContext';
+import { useToastMessageDispatch } from '../../../../client/contexts/ToastMessagesContext';
+import {
+ ProgressStep,
+ ImportWaitingStates,
+ ImportFileReadyStates,
+ ImportPreparingStartedStates,
+ ImportingStartedStates,
+ ImportingErrorStates,
+} from '../../lib/ImporterProgressStep';
+import { ImporterWebsocketReceiver } from '../ImporterWebsocketReceiver';
+import { showImporterException } from '../functions/showImporterException';
+import { useRoute } from '../../../../client/contexts/RouterContext';
+import { useSafely } from '../../../../client/hooks/useSafely';
+import { useEndpoint } from '../../../../client/contexts/ServerContext';
+import { useDebounce } from '../../../ui/client/views/app/components/hooks';
+
+const waitFor = (fn, predicate) => new Promise((resolve, reject) => {
+ const callPromise = () => {
+ fn().then((result) => {
+ if (predicate(result)) {
+ resolve(result);
+ return;
+ }
+
+ setTimeout(callPromise, 1000);
+ }, reject);
+ };
+
+ callPromise();
+});
+
+function PrepareUsers({ usersCount, users, setUsers }) {
+ const t = useTranslation();
+ const [current, setCurrent] = useState(0);
+ const [itemsPerPage, setItemsPerPage] = useState(25);
+ const showingResultsLabel = useCallback(({ count, current, itemsPerPage }) => t('Showing results %s - %s of %s', current + 1, Math.min(current + itemsPerPage, count), count), []);
+ const itemsPerPageLabel = useCallback(() => t('Items_per_page:'), []);
+
+ return <>
+
+
+
+
+ 0}
+ indeterminate={usersCount > 0 && usersCount !== users.length}
+ onChange={() => {
+ setUsers((users) => {
+ const hasCheckedDeletedUsers = users.some(({ is_deleted, do_import }) => is_deleted && do_import);
+ const isChecking = usersCount === 0;
+
+ if (isChecking) {
+ return users.map((user) => ({ ...user, do_import: true }));
+ }
+
+ if (hasCheckedDeletedUsers) {
+ return users.map((user) => (user.is_deleted ? { ...user, do_import: false } : user));
+ }
+
+ return users.map((user) => ({ ...user, do_import: false }));
+ });
+ }}
+ />
+
+ {t('Username')}
+ {t('Email')}
+
+
+
+
+ {users.slice(current, current + itemsPerPage).map((user) =>
+
+ {
+ const { checked } = event.currentTarget;
+ setUsers((users) =>
+ users.map((_user) => (_user === user ? { ..._user, do_import: checked } : _user)));
+ }}
+ />
+
+ {user.username}
+ {user.email}
+ {user.is_deleted && {t('Deleted')}}
+ )}
+
+
+
+ >;
+}
+
+function PrepareChannels({ channels, channelsCount, setChannels }) {
+ const t = useTranslation();
+ const [current, setCurrent] = useState(0);
+ const [itemsPerPage, setItemsPerPage] = useState(25);
+ const showingResultsLabel = useCallback(({ count, current, itemsPerPage }) => t('Showing results %s - %s of %s', current + 1, Math.min(current + itemsPerPage, count), count), []);
+ const itemsPerPageLabel = useCallback(() => t('Items_per_page:'), []);
+
+ return channels.length && <>
+
+
+
+ 0}
+ indeterminate={channelsCount > 0 && channelsCount !== channels.length}
+ onChange={() => {
+ setChannels((channels) => {
+ const hasCheckedArchivedChannels = channels.some(({ is_archived, do_import }) => is_archived && do_import);
+ const isChecking = channelsCount === 0;
+
+ if (isChecking) {
+ return channels.map((channel) => ({ ...channel, do_import: true }));
+ }
+
+ if (hasCheckedArchivedChannels) {
+ return channels.map((channel) => (channel.is_archived ? { ...channel, do_import: false } : channel));
+ }
+
+ return channels.map((channel) => ({ ...channel, do_import: false }));
+ });
+ }}
+ />
+
+ {t('Name')}
+
+
+
+
+ {channels.slice(current, current + itemsPerPage).map((channel) =>
+
+ {
+ const { checked } = event.currentTarget;
+ setChannels((channels) =>
+ channels.map((_channel) => (_channel === channel ? { ..._channel, do_import: checked } : _channel)));
+ }}
+ />
+
+ {channel.name}
+ {channel.is_archived && {t('Importer_Archived')}}
+ )}
+
+
+ >;
+}
+
+function PrepareImportPage() {
+ const t = useTranslation();
+ const dispatchToastMessage = useToastMessageDispatch();
+
+ const [isPreparing, setPreparing] = useSafely(useState(true));
+ const [progressRate, setProgressRate] = useSafely(useState(null));
+ const [status, setStatus] = useSafely(useState(null));
+ const [messageCount, setMessageCount] = useSafely(useState(0));
+ const [users, setUsers] = useState([]);
+ const [channels, setChannels] = useState([]);
+ const [isImporting, setImporting] = useSafely(useState(false));
+
+ const usersCount = useMemo(() => users.filter(({ do_import }) => do_import).length, [users]);
+ const channelsCount = useMemo(() => channels.filter(({ do_import }) => do_import).length, [channels]);
+
+ const importHistoryRoute = useRoute('admin-import');
+ const newImportRoute = useRoute('admin-import-new');
+ const importProgressRoute = useRoute('admin-import-progress');
+
+ const getImportFileData = useEndpoint('GET', 'getImportFileData');
+ const getCurrentImportOperation = useEndpoint('GET', 'getCurrentImportOperation');
+ const startImport = useEndpoint('POST', 'startImport');
+
+ useEffect(() => {
+ const handleProgressUpdated = ({ rate }) => {
+ setProgressRate(rate);
+ };
+
+ ImporterWebsocketReceiver.registerCallback(handleProgressUpdated);
+
+ return () => {
+ ImporterWebsocketReceiver.unregisterCallback(handleProgressUpdated);
+ };
+ }, []);
+
+ useEffect(() => {
+ const loadImportFileData = async () => {
+ try {
+ const data = await waitFor(getImportFileData, (data) => data && !data.waiting);
+
+ if (!data) {
+ dispatchToastMessage({ type: 'error', message: t('Importer_not_setup') });
+ importHistoryRoute.push();
+ return;
+ }
+
+ if (data.step) {
+ dispatchToastMessage({ type: 'error', message: t('Failed_To_Load_Import_Data') });
+ importHistoryRoute.push();
+ return;
+ }
+
+ setMessageCount(data.message_count);
+ setUsers(data.users.map((user) => ({ ...user, do_import: true })));
+ setChannels(data.channels.map((channel) => ({ ...channel, do_import: true })));
+ setPreparing(false);
+ setProgressRate(null);
+ } catch (error) {
+ showImporterException(error, 'Failed_To_Load_Import_Data');
+ importHistoryRoute.push();
+ }
+ };
+
+ const loadCurrentOperation = async () => {
+ try {
+ const { operation } = await waitFor(getCurrentImportOperation, ({ operation }) =>
+ operation.valid && !ImportWaitingStates.includes(operation.status));
+
+ if (!operation.valid) {
+ newImportRoute.push();
+ return;
+ }
+
+ if (ImportingStartedStates.includes(operation.status)) {
+ importProgressRoute.push();
+ return;
+ }
+
+ if (operation.status === ProgressStep.USER_SELECTION
+ || ImportPreparingStartedStates.includes(operation.status)
+ || ImportFileReadyStates.includes(operation.status)) {
+ setStatus(operation.status);
+ loadImportFileData();
+ return;
+ }
+
+ if (ImportingErrorStates.includes(operation.status)) {
+ dispatchToastMessage({ type: 'error', message: t('Import_Operation_Failed') });
+ importHistoryRoute.push();
+ return;
+ }
+
+ if (operation.status === ProgressStep.DONE) {
+ importHistoryRoute.push();
+ return;
+ }
+
+ dispatchToastMessage({ type: 'error', message: t('Unknown_Import_State') });
+ importHistoryRoute.push();
+ } catch (error) {
+ dispatchToastMessage({ type: 'error', message: t('Failed_To_Load_Import_Data') });
+ importHistoryRoute.push();
+ }
+ };
+
+ loadCurrentOperation();
+ }, []);
+
+ const handleBackToImportsButtonClick = () => {
+ importHistoryRoute.push();
+ };
+
+ const handleStartButtonClick = async () => {
+ setImporting(true);
+
+ try {
+ await startImport({ input: { users, channels } });
+ importProgressRoute.push();
+ } catch (error) {
+ showImporterException(error, 'Failed_To_Start_Import');
+ importHistoryRoute.push();
+ }
+ };
+
+ const [tab, setTab] = useState('users');
+ const handleTabClick = useMemo(() => (tab) => () => setTab(tab), []);
+
+ const statusDebounced = useDebounce(status, 100);
+ return
+
+
+
+
+
+
+
+
+
+ {statusDebounced && t(statusDebounced.replace('importer_', 'importer_status_'))}
+ {!isPreparing &&
+ {t('Users')} {usersCount}
+ {t('Channels')} {channelsCount}
+ {t('Messages')}{messageCount}
+ }
+
+ {isPreparing && <>
+ {progressRate
+ ?
+
+ {s.numberFormat(progressRate, 0) }%
+
+ : }
+ >}
+ {!isPreparing && tab === 'users' && }
+ {!isPreparing && tab === 'channels' && }
+
+
+
+ ;
+}
+
+export default PrepareImportPage;
diff --git a/app/importer/client/index.js b/app/importer/client/index.js
index 1ae15dfc877f..54b80e9bea16 100644
--- a/app/importer/client/index.js
+++ b/app/importer/client/index.js
@@ -1,46 +1,6 @@
-import { FlowRouter } from 'meteor/kadira:flow-router';
-import { BlazeLayout } from 'meteor/kadira:blaze-layout';
+import './routes';
-import { ImporterWebsocketReceiver } from './ImporterWebsocketReceiver';
-import { Importers } from '../lib/Importers';
-import { ImporterInfo } from '../lib/ImporterInfo';
-import { ProgressStep } from '../lib/ImporterProgressStep';
-
-FlowRouter.route('/admin/import', {
- name: 'admin-import',
- async action() {
- await import('./admin/adminImport');
- BlazeLayout.render('main', { center: 'adminImport' });
- },
-});
-
-FlowRouter.route('/admin/import/new', {
- name: 'admin-import-new',
- async action() {
- await import('./admin/adminImportNew');
- BlazeLayout.render('main', { center: 'adminImportNew' });
- },
-});
-
-FlowRouter.route('/admin/import/prepare', {
- name: 'admin-import-prepare',
- async action() {
- await import('./admin/adminImportPrepare');
- BlazeLayout.render('main', { center: 'adminImportPrepare' });
- },
-});
-
-FlowRouter.route('/admin/import/progress', {
- name: 'admin-import-progress',
- async action() {
- await import('./admin/adminImportProgress');
- BlazeLayout.render('main', { center: 'adminImportProgress' });
- },
-});
-
-export {
- Importers,
- ImporterInfo,
- ImporterWebsocketReceiver,
- ProgressStep,
-};
+export { Importers } from '../lib/Importers';
+export { ImporterInfo } from '../lib/ImporterInfo';
+export { ProgressStep } from '../lib/ImporterProgressStep';
+export { ImporterWebsocketReceiver } from './ImporterWebsocketReceiver';
diff --git a/app/importer/client/routes.js b/app/importer/client/routes.js
new file mode 100644
index 000000000000..875a6b2fa3d8
--- /dev/null
+++ b/app/importer/client/routes.js
@@ -0,0 +1,25 @@
+import { registerAdminRoute } from '../../ui-admin/client';
+
+registerAdminRoute('/import', {
+ name: 'admin-import',
+ lazyRouteComponent: () => import('./components/ImportRoute'),
+ props: { page: 'history' },
+});
+
+registerAdminRoute('/import/new/:importerKey?', {
+ name: 'admin-import-new',
+ lazyRouteComponent: () => import('./components/ImportRoute'),
+ props: { page: 'new' },
+});
+
+registerAdminRoute('/import/prepare', {
+ name: 'admin-import-prepare',
+ lazyRouteComponent: () => import('./components/ImportRoute'),
+ props: { page: 'prepare' },
+});
+
+registerAdminRoute('/import/progress', {
+ name: 'admin-import-progress',
+ lazyRouteComponent: () => import('./components/ImportRoute'),
+ props: { page: 'progress' },
+});
diff --git a/app/integrations/client/route.js b/app/integrations/client/route.js
index 18cd9743bfe6..fa97cf739741 100644
--- a/app/integrations/client/route.js
+++ b/app/integrations/client/route.js
@@ -1,11 +1,11 @@
-import { FlowRouter } from 'meteor/kadira:flow-router';
import { BlazeLayout } from 'meteor/kadira:blaze-layout';
+import { registerAdminRoute } from '../../ui-admin/client';
import { t } from '../../utils';
const dynamic = () => import('./views');
-FlowRouter.route('/admin/integrations', {
+registerAdminRoute('/integrations', {
name: 'admin-integrations',
async action() {
await dynamic();
@@ -16,7 +16,7 @@ FlowRouter.route('/admin/integrations', {
},
});
-FlowRouter.route('/admin/integrations/new', {
+registerAdminRoute('/integrations/new', {
name: 'admin-integrations-new',
async action() {
await dynamic();
@@ -27,7 +27,7 @@ FlowRouter.route('/admin/integrations/new', {
},
});
-FlowRouter.route('/admin/integrations/incoming/:id?', {
+registerAdminRoute('/integrations/incoming/:id?', {
name: 'admin-integrations-incoming',
async action(params) {
await dynamic();
@@ -40,7 +40,7 @@ FlowRouter.route('/admin/integrations/incoming/:id?', {
},
});
-FlowRouter.route('/admin/integrations/outgoing/:id?', {
+registerAdminRoute('/integrations/outgoing/:id?', {
name: 'admin-integrations-outgoing',
async action(params) {
await dynamic();
@@ -52,7 +52,7 @@ FlowRouter.route('/admin/integrations/outgoing/:id?', {
},
});
-FlowRouter.route('/admin/integrations/outgoing/:id?/history', {
+registerAdminRoute('/integrations/outgoing/:id?/history', {
name: 'admin-integrations-outgoing-history',
async action(params) {
await dynamic();
@@ -64,7 +64,7 @@ FlowRouter.route('/admin/integrations/outgoing/:id?/history', {
},
});
-FlowRouter.route('/admin/integrations/additional/zapier', {
+registerAdminRoute('/integrations/additional/zapier', {
name: 'admin-integrations-additional-zapier',
async action() {
await dynamic();
diff --git a/app/integrations/client/startup.js b/app/integrations/client/startup.js
index b4cb14574775..7ed84fba42f0 100644
--- a/app/integrations/client/startup.js
+++ b/app/integrations/client/startup.js
@@ -1,7 +1,7 @@
-import { AdminBox } from '../../ui-utils';
import { hasAtLeastOnePermission } from '../../authorization';
+import { registerAdminSidebarItem } from '../../ui-admin/client';
-AdminBox.addOption({
+registerAdminSidebarItem({
href: 'admin-integrations',
i18nLabel: 'Integrations',
icon: 'code',
diff --git a/app/invites/client/admin/route.js b/app/invites/client/admin/route.js
index f945611cced7..cb5dcb190f6a 100644
--- a/app/invites/client/admin/route.js
+++ b/app/invites/client/admin/route.js
@@ -1,7 +1,8 @@
-import { FlowRouter } from 'meteor/kadira:flow-router';
import { BlazeLayout } from 'meteor/kadira:blaze-layout';
-FlowRouter.route('/admin/invites', {
+import { registerAdminRoute } from '../../../ui-admin/client';
+
+registerAdminRoute('/invites', {
name: 'invites',
async action(/* params */) {
await import('./adminInvites');
diff --git a/app/invites/client/admin/startup.js b/app/invites/client/admin/startup.js
index b1fca12fb61b..a402aea6d144 100644
--- a/app/invites/client/admin/startup.js
+++ b/app/invites/client/admin/startup.js
@@ -1,7 +1,7 @@
-import { AdminBox } from '../../../ui-utils';
import { hasAtLeastOnePermission } from '../../../authorization';
+import { registerAdminSidebarItem } from '../../../ui-admin/client';
-AdminBox.addOption({
+registerAdminSidebarItem({
href: 'invites',
i18nLabel: 'Invites',
icon: 'user-plus',
diff --git a/app/lib/server/functions/addUserToDefaultChannels.js b/app/lib/server/functions/addUserToDefaultChannels.js
index bef1ba47620d..e0cdc6340507 100644
--- a/app/lib/server/functions/addUserToDefaultChannels.js
+++ b/app/lib/server/functions/addUserToDefaultChannels.js
@@ -14,6 +14,7 @@ export const addUserToDefaultChannels = function(user, silenced) {
unread: 1,
userMentions: 1,
groupMentions: 0,
+ ...room.favorite && { f: true },
});
// Insert user joined message
diff --git a/app/lib/server/functions/getFullUserData.js b/app/lib/server/functions/getFullUserData.js
index d3892bca9c51..cb75e4fa37ed 100644
--- a/app/lib/server/functions/getFullUserData.js
+++ b/app/lib/server/functions/getFullUserData.js
@@ -64,16 +64,32 @@ const getFields = (canViewAllInfo) => ({
...getCustomFields(canViewAllInfo),
});
-export function getFullUserDataById({ userId, filterId }) {
- const canViewAllInfo = userId === filterId || hasPermission(userId, 'view-full-other-user-info');
+const removePasswordInfo = (user) => {
+ if (user && user.services) {
+ delete user.services.password;
+ delete user.services.email;
+ delete user.services.resume;
+ delete user.services.emailCode;
+ delete user.services.cloud;
+ delete user.services.email2fa;
+ delete user.services.totp;
+ }
+ return user;
+};
+
+export function getFullUserDataByIdOrUsername({ userId, filterId, filterUsername }) {
+ const caller = Users.findOneById(userId, { fields: { username: 1 } });
+ const myself = userId === filterId || filterUsername === caller.username;
+ const canViewAllInfo = myself || hasPermission(userId, 'view-full-other-user-info');
const fields = getFields(canViewAllInfo);
const options = {
fields,
};
+ const user = Users.findOneByIdOrUsername(filterId || filterUsername, options);
- return Users.findById(filterId, options);
+ return myself ? user : removePasswordInfo(user);
}
export const getFullUserData = function({ userId, filter, limit: l }) {
diff --git a/app/lib/server/functions/notifications/email.js b/app/lib/server/functions/notifications/email.js
index 13a4652072d3..1640977bd0ed 100644
--- a/app/lib/server/functions/notifications/email.js
+++ b/app/lib/server/functions/notifications/email.js
@@ -110,7 +110,7 @@ const getButtonUrl = (room, subscription, message) => {
});
};
-export function sendEmail({ message, user, subscription, room, emailAddress, hasMentionToUser }) {
+export function getEmailData({ message, user, subscription, room, emailAddress, hasMentionToUser }) {
const username = settings.get('UI_Use_Real_Name') ? message.u.name || message.u.username : message.u.username;
let subjectKey = 'Offline_Mention_All_Email';
@@ -152,12 +152,20 @@ export function sendEmail({ message, user, subscription, room, emailAddress, has
}
metrics.notificationsSent.inc({ notification_type: 'email' });
- return Mailer.send(email);
+ return email;
+}
+
+export function sendEmailFromData(data) {
+ metrics.notificationsSent.inc({ notification_type: 'email' });
+ return Mailer.send(data);
+}
+
+export function sendEmail({ message, user, subscription, room, emailAddress, hasMentionToUser }) {
+ return sendEmailFromData(getEmailData({ message, user, subscription, room, emailAddress, hasMentionToUser }));
}
export function shouldNotifyEmail({
disableAllMessageNotifications,
- statusConnection,
emailNotifications,
isHighlighted,
hasMentionToUser,
@@ -170,11 +178,6 @@ export function shouldNotifyEmail({
return false;
}
- // use connected (don't need to send him an email)
- if (statusConnection === 'online') {
- return false;
- }
-
// user/room preference to nothing
if (emailNotifications === 'nothing') {
return false;
diff --git a/app/lib/server/functions/notifications/index.js b/app/lib/server/functions/notifications/index.js
index 6dbe07fb0402..8b247c8810a3 100644
--- a/app/lib/server/functions/notifications/index.js
+++ b/app/lib/server/functions/notifications/index.js
@@ -2,6 +2,7 @@ import { Meteor } from 'meteor/meteor';
import { TAPi18n } from 'meteor/rocketchat:tap-i18n';
import s from 'underscore.string';
+import { callbacks } from '../../../../callbacks';
import { settings } from '../../../../settings';
/**
@@ -22,7 +23,8 @@ export function parseMessageTextPerUser(messageText, message, receiver) {
return TAPi18n.__('Encrypted_message', { lng });
}
- return messageText;
+ // perform processing required before sending message as notification such as markdown filtering
+ return callbacks.run('renderNotification', messageText);
}
/**
diff --git a/app/lib/server/functions/notifications/mobile.js b/app/lib/server/functions/notifications/mobile.js
index 1e7a5d54849d..fdc5dda7b4df 100644
--- a/app/lib/server/functions/notifications/mobile.js
+++ b/app/lib/server/functions/notifications/mobile.js
@@ -3,16 +3,10 @@ import { Meteor } from 'meteor/meteor';
import { settings } from '../../../../settings';
import { Subscriptions } from '../../../../models';
import { roomTypes } from '../../../../utils';
-import { PushNotification } from '../../../../push-notifications/server';
const CATEGORY_MESSAGE = 'MESSAGE';
const CATEGORY_MESSAGE_NOREPLY = 'MESSAGE_NOREPLY';
-let alwaysNotifyMobileBoolean;
-settings.get('Notifications_Always_Notify_Mobile', (key, value) => {
- alwaysNotifyMobileBoolean = value;
-});
-
let SubscriptionRaw;
Meteor.startup(() => {
SubscriptionRaw = Subscriptions.model.rawCollection();
@@ -46,32 +40,25 @@ function enableNotificationReplyButton(room, username) {
return !room.muted.includes(username);
}
-export async function sendSinglePush({ room, message, userId, receiverUsername, senderUsername, senderName, notificationMessage }) {
+export async function getPushData({ room, message, userId, receiverUsername, senderUsername, senderName, notificationMessage }) {
let username = '';
if (settings.get('Push_show_username_room')) {
username = settings.get('UI_Use_Real_Name') === true ? senderName : senderUsername;
}
- PushNotification.send({
- roomId: message.rid,
+ return {
payload: {
- host: Meteor.absoluteUrl(),
- rid: message.rid,
sender: message.u,
type: room.t,
name: room.name,
messageType: message.t,
- messageId: message._id,
},
roomName: settings.get('Push_show_username_room') && roomTypes.getConfig(room.t).isGroupChat(room) ? `#${ roomTypes.getRoomName(room.t, room) }` : '',
username,
message: settings.get('Push_show_message') ? notificationMessage : ' ',
badge: await getBadgeCount(userId),
- usersTo: {
- userId,
- },
category: enableNotificationReplyButton(room, receiverUsername) ? CATEGORY_MESSAGE : CATEGORY_MESSAGE_NOREPLY,
- });
+ };
}
export function shouldNotifyMobile({
@@ -81,7 +68,6 @@ export function shouldNotifyMobile({
isHighlighted,
hasMentionToUser,
hasReplyToThread,
- statusConnection,
roomType,
}) {
if (disableAllMessageNotifications && mobilePushNotifications == null && !isHighlighted && !hasMentionToUser && !hasReplyToThread) {
@@ -92,10 +78,6 @@ export function shouldNotifyMobile({
return false;
}
- if (!alwaysNotifyMobileBoolean && statusConnection === 'online') {
- return false;
- }
-
if (!mobilePushNotifications) {
if (settings.get('Accounts_Default_User_Preferences_mobileNotifications') === 'all') {
return true;
diff --git a/app/lib/server/functions/saveUser.js b/app/lib/server/functions/saveUser.js
index dae0e6896d82..bceec8403c0a 100644
--- a/app/lib/server/functions/saveUser.js
+++ b/app/lib/server/functions/saveUser.js
@@ -10,6 +10,7 @@ import { getRoles, hasPermission } from '../../../authorization';
import { settings } from '../../../settings';
import { passwordPolicy } from '../lib/passwordPolicy';
import { validateEmailDomain } from '../lib';
+import { validateUserRoles } from '../../../../ee/app/authorization/server/validateUserRoles';
import { saveUserIdentity } from './saveUserIdentity';
import { checkEmailAvailability, checkUsernameAvailability, setUserAvatar, setEmail, setStatusText } from '.';
@@ -97,6 +98,10 @@ function validateUserData(userId, userData) {
});
}
+ if (userData.roles) {
+ validateUserRoles(userId, userData);
+ }
+
let nameValidation;
try {
diff --git a/app/lib/server/lib/processDirectEmail.js b/app/lib/server/lib/processDirectEmail.js
index 4489551698e6..deab36914c60 100644
--- a/app/lib/server/lib/processDirectEmail.js
+++ b/app/lib/server/lib/processDirectEmail.js
@@ -102,10 +102,6 @@ export const processDirectEmail = function(email) {
}
}
- if (message.alias == null && settings.get('Message_SetNameToAliasEnabled')) {
- message.alias = user.name;
- }
-
metrics.messagesSent.inc(); // TODO This line needs to be moved to it's proper place. See the comments on: https://github.com/RocketChat/Rocket.Chat/pull/5736
return _sendMessage(user, message, room);
diff --git a/app/lib/server/lib/sendNotificationsOnMessage.js b/app/lib/server/lib/sendNotificationsOnMessage.js
index 1d2e831c4441..834eac239b0e 100644
--- a/app/lib/server/lib/sendNotificationsOnMessage.js
+++ b/app/lib/server/lib/sendNotificationsOnMessage.js
@@ -7,10 +7,11 @@ import { callbacks } from '../../../callbacks/server';
import { Subscriptions, Users } from '../../../models/server';
import { roomTypes } from '../../../utils';
import { callJoinRoom, messageContainsHighlight, parseMessageTextPerUser, replaceMentionedUsernamesWithFullNames } from '../functions/notifications';
-import { sendEmail, shouldNotifyEmail } from '../functions/notifications/email';
-import { sendSinglePush, shouldNotifyMobile } from '../functions/notifications/mobile';
+import { getEmailData, shouldNotifyEmail } from '../functions/notifications/email';
+import { getPushData, shouldNotifyMobile } from '../functions/notifications/mobile';
import { notifyDesktopUser, shouldNotifyDesktop } from '../functions/notifications/desktop';
import { notifyAudioUser, shouldNotifyAudio } from '../functions/notifications/audio';
+import { Notification } from '../../../notification-queue/server/NotificationQueue';
let TroubleshootDisableNotifications;
@@ -115,6 +116,8 @@ export const sendNotification = async ({
});
}
+ const queueItems = [];
+
if (shouldNotifyMobile({
disableAllMessageNotifications,
mobilePushNotifications,
@@ -122,23 +125,24 @@ export const sendNotification = async ({
isHighlighted,
hasMentionToUser,
hasReplyToThread,
- statusConnection: receiver.statusConnection,
roomType,
})) {
- sendSinglePush({
- notificationMessage,
- room,
- message,
- userId: subscription.u._id,
- senderUsername: sender.username,
- senderName: sender.name,
- receiverUsername: receiver.username,
+ queueItems.push({
+ type: 'push',
+ data: await getPushData({
+ notificationMessage,
+ room,
+ message,
+ userId: subscription.u._id,
+ senderUsername: sender.username,
+ senderName: sender.name,
+ receiverUsername: receiver.username,
+ }),
});
}
if (receiver.emails && shouldNotifyEmail({
disableAllMessageNotifications,
- statusConnection: receiver.statusConnection,
emailNotifications,
isHighlighted,
hasMentionToUser,
@@ -148,13 +152,25 @@ export const sendNotification = async ({
})) {
receiver.emails.some((email) => {
if (email.verified) {
- sendEmail({ message, receiver, subscription, room, emailAddress: email.address, hasMentionToUser });
+ queueItems.push({
+ type: 'email',
+ data: getEmailData({ message, receiver, subscription, room, emailAddress: email.address, hasMentionToUser }),
+ });
return true;
}
return false;
});
}
+
+ if (queueItems.length) {
+ Notification.scheduleItem({
+ uid: subscription.u._id,
+ rid: room._id,
+ mid: message._id,
+ items: queueItems,
+ });
+ }
};
const project = {
@@ -263,7 +279,7 @@ export async function sendMessageNotifications(message, room, usersInThread = []
}
});
- // the find bellow is crucial. all subscription records returned will receive at least one kind of notification.
+ // the find below is crucial. All subscription records returned will receive at least one kind of notification.
// the query is defined by the server's default values and Notifications_Max_Room_Members setting.
const subscriptions = await Subscriptions.model.rawCollection().aggregate([
diff --git a/app/lib/server/methods/addOAuthService.js b/app/lib/server/methods/addOAuthService.js
index 49627f1e6f6b..4852e5af1e7f 100644
--- a/app/lib/server/methods/addOAuthService.js
+++ b/app/lib/server/methods/addOAuthService.js
@@ -37,6 +37,7 @@ Meteor.methods({
settings.add(`Accounts_OAuth_Custom-${ name }-button_label_color` , '#FFFFFF' , { type: 'string' , group: 'OAuth', section: `Custom OAuth: ${ name }`, i18nLabel: 'Accounts_OAuth_Custom_Button_Label_Color', persistent: true });
settings.add(`Accounts_OAuth_Custom-${ name }-button_color` , '#1d74f5' , { type: 'string' , group: 'OAuth', section: `Custom OAuth: ${ name }`, i18nLabel: 'Accounts_OAuth_Custom_Button_Color', persistent: true });
settings.add(`Accounts_OAuth_Custom-${ name }-username_field` , '' , { type: 'string' , group: 'OAuth', section: `Custom OAuth: ${ name }`, i18nLabel: 'Accounts_OAuth_Custom_Username_Field', persistent: true });
+ settings.add(`Accounts_OAuth_Custom-${ name }-email_field` , '' , { type: 'string' , group: 'OAuth', section: `Custom OAuth: ${ name }`, i18nLabel: 'Accounts_OAuth_Custom_Email_Field', persistent: true });
settings.add(`Accounts_OAuth_Custom-${ name }-name_field` , '' , { type: 'string' , group: 'OAuth', section: `Custom OAuth: ${ name }`, i18nLabel: 'Accounts_OAuth_Custom_Name_Field', persistent: true });
settings.add(`Accounts_OAuth_Custom-${ name }-avatar_field` , '' , { type: 'string' , group: 'OAuth', section: `Custom OAuth: ${ name }`, i18nLabel: 'Accounts_OAuth_Custom_Avatar_Field', persistent: true });
settings.add(`Accounts_OAuth_Custom-${ name }-roles_claim` , 'roles' , { type: 'string' , group: 'OAuth', section: `Custom OAuth: ${ name }`, i18nLabel: 'Accounts_OAuth_Custom_Roles_Claim', persistent: true });
diff --git a/app/lib/server/methods/removeOAuthService.js b/app/lib/server/methods/removeOAuthService.js
index 5a5f07fb69ca..4f83a1008bdb 100644
--- a/app/lib/server/methods/removeOAuthService.js
+++ b/app/lib/server/methods/removeOAuthService.js
@@ -35,6 +35,7 @@ Meteor.methods({
settings.removeById(`Accounts_OAuth_Custom-${ name }-button_color`);
settings.removeById(`Accounts_OAuth_Custom-${ name }-login_style`);
settings.removeById(`Accounts_OAuth_Custom-${ name }-username_field`);
+ settings.removeById(`Accounts_OAuth_Custom-${ name }-email_field`);
settings.removeById(`Accounts_OAuth_Custom-${ name }-name_field`);
settings.removeById(`Accounts_OAuth_Custom-${ name }-avatar_field`);
settings.removeById(`Accounts_OAuth_Custom-${ name }-roles_claim`);
diff --git a/app/lib/server/methods/sendMessage.js b/app/lib/server/methods/sendMessage.js
index d25284a3639f..be4ef0670d9c 100644
--- a/app/lib/server/methods/sendMessage.js
+++ b/app/lib/server/methods/sendMessage.js
@@ -51,7 +51,6 @@ export function executeSendMessage(uid, message) {
fields: {
username: 1,
type: 1,
- ...!!settings.get('Message_SetNameToAliasEnabled') && { name: 1 },
},
});
let { rid } = message;
@@ -69,9 +68,6 @@ export function executeSendMessage(uid, message) {
try {
const room = canSendMessage(rid, { uid, username: user.username, type: user.type });
- if (message.alias == null && settings.get('Message_SetNameToAliasEnabled')) {
- message.alias = user.name;
- }
metrics.messagesSent.inc(); // TODO This line needs to be moved to it's proper place. See the comments on: https://github.com/RocketChat/Rocket.Chat/pull/5736
return sendMessage(user, message, room, false);
diff --git a/app/lib/server/startup/oAuthServicesUpdate.js b/app/lib/server/startup/oAuthServicesUpdate.js
index 9e588f70ffaa..0374ad30c56f 100644
--- a/app/lib/server/startup/oAuthServicesUpdate.js
+++ b/app/lib/server/startup/oAuthServicesUpdate.js
@@ -47,6 +47,7 @@ function _OAuthServicesUpdate() {
data.tokenSentVia = settings.get(`${ service.key }-token_sent_via`);
data.identityTokenSentVia = settings.get(`${ service.key }-identity_token_sent_via`);
data.usernameField = settings.get(`${ service.key }-username_field`);
+ data.emailField = settings.get(`${ service.key }-email_field`);
data.nameField = settings.get(`${ service.key }-name_field`);
data.avatarField = settings.get(`${ service.key }-avatar_field`);
data.rolesClaim = settings.get(`${ service.key }-roles_claim`);
@@ -63,6 +64,7 @@ function _OAuthServicesUpdate() {
tokenSentVia: data.tokenSentVia,
identityTokenSentVia: data.identityTokenSentVia,
usernameField: data.usernameField,
+ emailField: data.emailField,
nameField: data.nameField,
avatarField: data.avatarField,
rolesClaim: data.rolesClaim,
diff --git a/app/lib/server/startup/settings.js b/app/lib/server/startup/settings.js
index 33bf91e477af..241db8151ea8 100644
--- a/app/lib/server/startup/settings.js
+++ b/app/lib/server/startup/settings.js
@@ -925,12 +925,6 @@ settings.addGroup('General', function() {
public: true,
i18nDescription: 'Notifications_Max_Room_Members_Description',
});
-
- this.add('Notifications_Always_Notify_Mobile', false, {
- type: 'boolean',
- public: true,
- i18nDescription: 'Notifications_Always_Notify_Mobile_Description',
- });
});
this.section('REST API', function() {
return this.add('API_User_Limit', 500, {
@@ -1062,11 +1056,6 @@ settings.addGroup('Message', function() {
type: 'boolean',
public: true,
});
- this.add('Message_SetNameToAliasEnabled', false, {
- type: 'boolean',
- public: false,
- i18nDescription: 'Message_SetNameToAliasEnabled_Description',
- });
this.add('Message_GroupingPeriod', 300, {
type: 'int',
public: true,
@@ -1183,33 +1172,7 @@ settings.addGroup('Push', function() {
public: true,
alert: 'Push_Setting_Requires_Restart_Alert',
});
- this.add('Push_debug', false, {
- type: 'boolean',
- public: true,
- alert: 'Push_Setting_Requires_Restart_Alert',
- enableQuery: {
- _id: 'Push_enable',
- value: true,
- },
- });
- this.add('Push_send_interval', 2000, {
- type: 'int',
- public: true,
- alert: 'Push_Setting_Requires_Restart_Alert',
- enableQuery: {
- _id: 'Push_enable',
- value: true,
- },
- });
- this.add('Push_send_batch_size', 100, {
- type: 'int',
- public: true,
- alert: 'Push_Setting_Requires_Restart_Alert',
- enableQuery: {
- _id: 'Push_enable',
- value: true,
- },
- });
+
this.add('Push_enable_gateway', true, {
type: 'boolean',
alert: 'Push_Setting_Requires_Restart_Alert',
diff --git a/app/livechat/client/index.js b/app/livechat/client/index.js
index 67a33ffb10eb..26013edf70a5 100644
--- a/app/livechat/client/index.js
+++ b/app/livechat/client/index.js
@@ -4,6 +4,8 @@ import './route';
import './ui';
import './hooks/onCreateRoomTabBar';
import './startup/notifyUnreadRooms';
+import './views/app/dialog/closeRoom';
+import './stylesheets/livechat.css';
import './views/sideNav/livechat';
import './views/sideNav/livechatFlex';
import './externalFrame';
diff --git a/app/livechat/client/stylesheets/livechat.less b/app/livechat/client/stylesheets/livechat.css
similarity index 96%
rename from app/livechat/client/stylesheets/livechat.less
rename to app/livechat/client/stylesheets/livechat.css
index 232c8cb93317..3a13815dbe11 100644
--- a/app/livechat/client/stylesheets/livechat.less
+++ b/app/livechat/client/stylesheets/livechat.css
@@ -1,9 +1,11 @@
-@header-min-height: 30px;
-@footer-min-height: 55px;
-@link-font-color: #008ce3;
-@primary-font-color: #444444;
-@secondary-font-color: #7f7f7f;
-@info-font-color: #aaaaaa;
+:root {
+ --header-min-height: 30px;
+ --footer-min-height: 55px;
+ --link-font-color: #008ce3;
+ --primary-font-color: #444444;
+ --secondary-font-color: #7f7f7f;
+ --info-font-color: #aaaaaa;
+}
.flex-list {
.active {
@@ -131,7 +133,7 @@
text-decoration: underline;
text-overflow: ellipsis;
- color: @secondary-font-color;
+ color: var(--secondary-font-color);
&:hover {
text-decoration: none;
@@ -172,7 +174,7 @@
.user-view {
li {
- color: @secondary-font-color;
+ color: var(--secondary-font-color);
font-size: 12px;
font-weight: 300;
diff --git a/app/livechat/client/views/app/dialog/closeRoom.html b/app/livechat/client/views/app/dialog/closeRoom.html
new file mode 100644
index 000000000000..0cb20ad48d85
--- /dev/null
+++ b/app/livechat/client/views/app/dialog/closeRoom.html
@@ -0,0 +1,72 @@
+
+
+
+
{{_ "Close_room_description"}}
+
+
+
+
+
+
+
diff --git a/app/livechat/client/views/app/dialog/closeRoom.js b/app/livechat/client/views/app/dialog/closeRoom.js
new file mode 100644
index 000000000000..2dac3bc7956a
--- /dev/null
+++ b/app/livechat/client/views/app/dialog/closeRoom.js
@@ -0,0 +1,190 @@
+import { Meteor } from 'meteor/meteor';
+import { Template } from 'meteor/templating';
+import { ReactiveVar } from 'meteor/reactive-var';
+import { TAPi18n } from 'meteor/rocketchat:tap-i18n';
+
+import { settings } from '../../../../../settings';
+import { modal } from '../../../../../ui-utils/client';
+import { APIClient, handleError, t } from '../../../../../utils';
+import { hasRole } from '../../../../../authorization';
+import './closeRoom.html';
+
+const validateRoomComment = (comment) => {
+ if (!settings.get('Livechat_request_comment_when_closing_conversation')) {
+ return true;
+ }
+
+ return comment?.length > 0;
+};
+
+const validateRoomTags = (tagsRequired, tags) => {
+ if (!tagsRequired) {
+ return true;
+ }
+
+ return tags?.length > 0;
+};
+
+const checkUserTagPermission = (availableUserTags = [], tag) => {
+ if (hasRole(Meteor.userId(), ['admin', 'livechat-manager'])) {
+ return true;
+ }
+
+ return availableUserTags.includes(tag);
+};
+
+Template.closeRoom.helpers({
+ invalidComment() {
+ return Template.instance().invalidComment.get();
+ },
+ tags() {
+ return Template.instance().tags.get();
+ },
+ invalidTags() {
+ return Template.instance().invalidTags.get();
+ },
+ availableUserTags() {
+ return Template.instance().availableUserTags.get();
+ },
+ tagsPlaceHolder() {
+ let placeholder = TAPi18n.__('Enter_a_tag');
+
+ if (!Template.instance().tagsRequired.get()) {
+ placeholder = placeholder.concat(`(${ TAPi18n.__('Optional') })`);
+ }
+
+ return placeholder;
+ },
+ hasAvailableTags() {
+ const tags = Template.instance().availableTags.get();
+ return tags?.length > 0;
+ },
+ canRemoveTag(availableUserTags, tag) {
+ return checkUserTagPermission(availableUserTags, tag);
+ },
+});
+
+Template.closeRoom.events({
+ async 'submit .close-room__content'(e, instance) {
+ e.preventDefault();
+ e.stopPropagation();
+
+ const comment = instance.$('#comment').val();
+ instance.invalidComment.set(!validateRoomComment(comment));
+ if (instance.invalidComment.get()) {
+ return;
+ }
+
+ const tagsRequired = instance.tagsRequired.get();
+ const tags = instance.tags.get();
+
+ instance.invalidTags.set(!validateRoomTags(tagsRequired, tags));
+ if (instance.invalidTags.get()) {
+ return;
+ }
+
+ Meteor.call('livechat:closeRoom', this.rid, comment, { clientAction: true, tags }, function(error/* , result*/) {
+ if (error) {
+ console.log(error);
+ return handleError(error);
+ }
+
+ modal.open({
+ title: t('Chat_closed'),
+ text: t('Chat_closed_successfully'),
+ type: 'success',
+ timer: 1000,
+ showConfirmButton: false,
+ });
+ });
+ },
+ 'click .remove-tag'(e, instance) {
+ e.stopPropagation();
+ e.preventDefault();
+
+ const tag = this.valueOf();
+ const availableTags = instance.availableTags.get();
+ const hasAvailableTags = availableTags?.length > 0;
+ const availableUserTags = instance.availableUserTags.get();
+ if (hasAvailableTags && !checkUserTagPermission(availableUserTags, tag)) {
+ return;
+ }
+
+ let tags = instance.tags.get();
+ tags = tags.filter((el) => el !== tag);
+ instance.tags.set(tags);
+ },
+ 'click #addTag'(e, instance) {
+ e.stopPropagation();
+ e.preventDefault();
+
+ if ($('#tagSelect').find(':selected').is(':disabled')) {
+ return;
+ }
+
+ const tags = [...instance.tags.get()];
+ const tagVal = $('#tagSelect').val();
+ if (tagVal === '' || tags.includes(tagVal)) {
+ return;
+ }
+
+ tags.push(tagVal);
+ instance.tags.set(tags);
+ $('#tagSelect').val('placeholder');
+ },
+ 'keydown #tagInput'(e, instance) {
+ if (e.which === 13) {
+ e.stopPropagation();
+ e.preventDefault();
+
+ const tags = [...instance.tags.get()];
+ const tagVal = $('#tagInput').val();
+ if (tagVal === '' || tags.includes(tagVal)) {
+ return;
+ }
+
+ tags.push(tagVal);
+ instance.tags.set(tags);
+ $('#tagInput').val('');
+ }
+ },
+});
+
+Template.closeRoom.onRendered(function() {
+ this.find('#comment').focus();
+});
+
+Template.closeRoom.onCreated(async function() {
+ this.tags = new ReactiveVar([]);
+ this.invalidComment = new ReactiveVar(false);
+ this.invalidTags = new ReactiveVar(false);
+ this.tagsRequired = new ReactiveVar(false);
+ this.availableTags = new ReactiveVar([]);
+ this.availableUserTags = new ReactiveVar([]);
+ this.agentDepartments = new ReactiveVar([]);
+
+ this.onEnterTag = () => this.invalidTags.set(!validateRoomTags(this.tagsRequired.get(), this.tags.get()));
+
+ const { rid } = Template.currentData();
+ const { room } = await APIClient.v1.get(`rooms.info?roomId=${ rid }`);
+ this.tags.set(room?.tags || []);
+
+ if (room?.departmentId) {
+ const { department } = await APIClient.v1.get(`livechat/department/${ room.departmentId }?includeAgents=false`);
+ this.tagsRequired.set(department?.requestTagBeforeClosingChat);
+ }
+
+ const uid = Meteor.userId();
+ const { departments } = await APIClient.v1.get(`livechat/agents/${ uid }/departments`);
+ const agentDepartments = departments.map((dept) => dept.departmentId);
+ this.agentDepartments.set(agentDepartments);
+
+ Meteor.call('livechat:getTagsList', (err, tagsList) => {
+ this.availableTags.set(tagsList);
+ const isAdmin = hasRole(uid, ['admin', 'livechat-manager']);
+ const availableTags = tagsList
+ .filter(({ departments }) => isAdmin || (departments.length === 0 || departments.some((i) => agentDepartments.includes(i))))
+ .map(({ name }) => name);
+ this.availableUserTags.set(availableTags);
+ });
+});
diff --git a/app/livechat/client/views/app/livechatCurrentChats.css b/app/livechat/client/views/app/livechatCurrentChats.css
new file mode 100644
index 000000000000..7345337d5979
--- /dev/null
+++ b/app/livechat/client/views/app/livechatCurrentChats.css
@@ -0,0 +1,25 @@
+.rc-table-content {
+ & .js-sort {
+ cursor: pointer;
+
+ &.is-sorting .table-fake-th .rc-icon {
+ opacity: 1;
+ }
+ }
+
+ & .table-fake-th {
+ color: #444444;
+
+ &:hover .rc-icon {
+ opacity: 1;
+ }
+
+ & .rc-icon {
+ transition: opacity 0.3s;
+
+ opacity: 0;
+
+ font-size: 1rem;
+ }
+ }
+}
diff --git a/app/livechat/client/views/app/livechatCurrentChats.html b/app/livechat/client/views/app/livechatCurrentChats.html
index c45ad535ab1b..df4720ba4234 100644
--- a/app/livechat/client/views/app/livechatCurrentChats.html
+++ b/app/livechat/client/views/app/livechatCurrentChats.html
@@ -127,24 +127,24 @@
{{#table fixed='true' onScroll=onTableScroll onResize=onTableResize onSort=onTableSort}}
-
- {{_ "Name"}}
+ |
+ {{_ "Name"}}{{> icon icon=(sortIcon 'fname')}}
|
-
- {{_ "Department"}}
+ |
+ {{_ "Department"}}{{> icon icon=(sortIcon 'departmentId')}}
|
-
- {{_ "Served_By"}}
+ |
+ {{_ "Served_By"}}{{> icon icon=(sortIcon 'servedBy.username')}}
|
-
- {{_ "Started_At"}}
+ |
+ {{_ "Started_At"}}{{> icon icon=(sortIcon 'ts')}}
|
-
- {{_ "Last_Message_At"}}
+ |
+ {{_ "Last_Message_At"}}{{> icon icon=(sortIcon 'lm')}}
|
-
- {{_ "Status"}}
+ |
+ {{_ "Status"}}{{> icon icon=(sortIcon 'open')}}
|
|
diff --git a/app/livechat/client/views/app/livechatCurrentChats.js b/app/livechat/client/views/app/livechatCurrentChats.js
index d5e017721360..b8493dfeab53 100644
--- a/app/livechat/client/views/app/livechatCurrentChats.js
+++ b/app/livechat/client/views/app/livechatCurrentChats.js
@@ -1,6 +1,7 @@
import 'moment-timezone';
import _ from 'underscore';
import moment from 'moment';
+import './livechatCurrentChats.css';
import { ReactiveVar } from 'meteor/reactive-var';
import { FlowRouter } from 'meteor/kadira:flow-router';
import { Template } from 'meteor/templating';
@@ -102,6 +103,25 @@ Template.livechatCurrentChats.helpers({
onSelectDepartments() {
return Template.instance().onSelectDepartments;
},
+ onTableSort() {
+ const { sortDirection, sortBy } = Template.instance();
+ return function(type) {
+ if (sortBy.get() === type) {
+ return sortDirection.set(sortDirection.get() === 'asc' ? 'desc' : 'asc');
+ }
+ sortBy.set(type);
+ sortDirection.set('asc');
+ };
+ },
+ sortBy(key) {
+ return Template.instance().sortBy.get() === key;
+ },
+ sortIcon(key) {
+ const { sortDirection, sortBy } = Template.instance();
+ return key === sortBy.get() && sortDirection.get() === 'asc'
+ ? 'sort-up'
+ : 'sort-down';
+ },
});
Template.livechatCurrentChats.events({
@@ -371,6 +391,8 @@ Template.livechatCurrentChats.onCreated(async function() {
this.customFields = new ReactiveVar([]);
this.tagFilters = new ReactiveVar([]);
this.selectedDepartments = new ReactiveVar([]);
+ this.sortBy = new ReactiveVar('ts');
+ this.sortDirection = new ReactiveVar('desc');
this.onSelectDepartments = ({ item: department }) => {
department.text = department.name;
@@ -387,9 +409,9 @@ Template.livechatCurrentChats.onCreated(async function() {
return acc;
}, '');
- const mountUrlWithParams = (filter, offset) => {
+ const mountUrlWithParams = (filter, offset, sort) => {
const { status, agents, department, from, to, tags, customFields, name: roomName } = filter;
- let url = `livechat/rooms?count=${ ROOMS_COUNT }&offset=${ offset }&sort={"ts": -1}`;
+ let url = `livechat/rooms?count=${ ROOMS_COUNT }&offset=${ offset }&sort=${ JSON.stringify(sort) }`;
const dateRange = {};
if (status) {
url += `&open=${ status === 'opened' }`;
@@ -452,9 +474,9 @@ Template.livechatCurrentChats.onCreated(async function() {
this.filter.set({});
};
- this.loadRooms = async (filter, offset) => {
+ this.loadRooms = async (filter, offset, sort) => {
this.isLoading.set(true);
- const { rooms, total } = await APIClient.v1.get(mountUrlWithParams(filter, offset));
+ const { rooms, total } = await APIClient.v1.get(mountUrlWithParams(filter, offset, sort));
this.total.set(total);
if (offset === 0) {
this.livechatRooms.set(rooms);
@@ -475,7 +497,8 @@ Template.livechatCurrentChats.onCreated(async function() {
this.autorun(async () => {
const filter = this.filter.get();
const offset = this.offset.get();
- this.loadRooms(filter, offset);
+ const { sortDirection, sortBy } = Template.instance();
+ this.loadRooms(filter, offset, { [sortBy.get()]: sortDirection.get() === 'asc' ? 1 : -1 });
});
Meteor.call('livechat:getCustomFields', (err, customFields) => {
diff --git a/app/livechat/client/views/app/livechatDepartmentForm.js b/app/livechat/client/views/app/livechatDepartmentForm.js
index 4af23b45ddcf..59bbcec12286 100644
--- a/app/livechat/client/views/app/livechatDepartmentForm.js
+++ b/app/livechat/client/views/app/livechatDepartmentForm.js
@@ -265,7 +265,7 @@ Template.livechatDepartmentForm.onCreated(async function() {
this.autorun(async () => {
const id = FlowRouter.getParam('_id');
if (id) {
- const { department, agents } = await APIClient.v1.get(`livechat/department/${ FlowRouter.getParam('_id') }`);
+ const { department, agents = [] } = await APIClient.v1.get(`livechat/department/${ FlowRouter.getParam('_id') }`);
this.department.set(department);
this.departmentAgents.set(agents);
this.chatClosingTags.set((department && department.chatClosingTags) || []);
diff --git a/app/livechat/client/views/app/tabbar/visitorEdit.html b/app/livechat/client/views/app/tabbar/visitorEdit.html
index 0d3afb3118be..306d1c310246 100644
--- a/app/livechat/client/views/app/tabbar/visitorEdit.html
+++ b/app/livechat/client/views/app/tabbar/visitorEdit.html
@@ -45,6 +45,9 @@ {{username}}
{{#with room}}
{{_ "Conversation" }}
+ {{#if customFieldsTemplate}}
+ {{> Template.dynamic template=customFieldsTemplate data=room }}
+ {{/if}}
{{/with}}