A React-based replacement for BlazeLayout (#21527)

pull/18906/head^2
Tasso Evangelista 4 years ago committed by GitHub
parent 191c3f8c2f
commit 84d547055e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      .meteor/packages
  2. 1
      .meteor/versions
  3. 2
      .storybook/mocks/meteor.js
  4. 5
      app/analytics/client/loadScript.js
  5. 8
      app/chatpal-search/client/route.js
  6. 2
      app/chatpal-search/client/template/admin.html
  7. 1
      app/mail-messages/client/index.js
  8. 13
      app/mail-messages/client/router.js
  9. 4
      app/mail-messages/client/startup.js
  10. 2
      app/mail-messages/client/views/index.js
  11. 14
      app/mail-messages/client/views/mailerUnsubscribe.html
  12. 5
      app/mail-messages/client/views/mailerUnsubscribe.js
  13. 4
      app/message-snippet/client/router.js
  14. 6
      app/oauth2-server-config/client/oauth/oauth2-client.js
  15. 11
      app/theme/client/imports/general/react-root.css
  16. 1
      app/theme/client/main.css
  17. 4
      app/threads/client/flextab/thread.html
  18. 4
      app/token-login/client/login_token_client.js
  19. 4
      app/ui-login/client/login/layout.js
  20. 4
      app/ui-login/client/routes.js
  21. 1
      app/ui-master/client/body.html
  22. 122
      app/ui-master/client/body.js
  23. 2
      app/ui-master/client/index.js
  24. 66
      app/ui-master/client/main.html
  25. 203
      app/ui-master/client/main.js
  26. 21
      app/ui-master/server/inject.js
  27. 4
      app/ui-sidenav/client/sideNav.html
  28. 4
      app/ui-utils/client/lib/AccountBox.js
  29. 61
      app/ui-utils/client/lib/openRoom.js
  30. 8
      app/ui/client/views/app/burger.js
  31. 4
      app/ui/client/views/app/invite.js
  32. 4
      app/ui/client/views/app/secretURL.js
  33. 4
      app/ui/client/views/cmsPage.js
  34. 2
      app/utils/lib/roomExit.js
  35. 2
      client/.eslintrc.js
  36. 2
      client/components/Message/Attachments/providers/AttachmentProvider.tsx
  37. 6
      client/components/ModalPortal.tsx
  38. 2
      client/contexts/ServerContext/methods.ts
  39. 1
      client/contexts/ServerContext/methods/mailer/unsubscribe.ts
  40. 17
      client/hooks/useWipeInitialPageLoading.js
  41. 52
      client/lib/appLayout.ts
  42. 80
      client/lib/createRouteGroup.ts
  43. 32
      client/lib/portals/blazeLayout.ts
  44. 44
      client/lib/portals/blazePortals.ts
  45. 52
      client/lib/portals/createTemplateForComponent.ts
  46. 89
      client/lib/portals/renderRouteComponent.ts
  47. 57
      client/providers/MeteorProvider.js
  48. 54
      client/providers/MeteorProvider.tsx
  49. 12
      client/providers/ModalProvider.tsx
  50. 2
      client/providers/OmnichannelProvider.tsx
  51. 26
      client/startup/appRoot.tsx
  52. 1
      client/startup/index.ts
  53. 82
      client/startup/routes.ts
  54. 21
      client/startup/setupWizard.ts
  55. 2
      client/startup/startup.ts
  56. 80
      client/templates.ts
  57. 7
      client/types/kadira-blaze-layout.d.ts
  58. 1
      client/types/meteor-htmljs.d.ts
  59. 9
      client/views/account/AccountRoute.js
  60. 22
      client/views/admin/AdministrationRouter.js
  61. 8
      client/views/admin/routes.js
  62. 15
      client/views/directory/DirectoryPage.js
  63. 67
      client/views/mailer/MailerUnsubscriptionPage.tsx
  64. 3
      client/views/notFound/NotFoundPage.js
  65. 24
      client/views/omnichannel/OmnichannelRouter.tsx
  66. 21
      client/views/omnichannel/directory/OmnichannelDirectoryPage.js
  67. 39
      client/views/root/AppLayout.tsx
  68. 30
      client/views/root/AppRoot.tsx
  69. 61
      client/views/root/BlazeLayoutWrapper.tsx
  70. 45
      client/views/root/BlazeTemplate.tsx
  71. 30
      client/views/root/DomNode.tsx
  72. 13
      client/views/root/PageLoading.tsx
  73. 3
      client/views/setupWizard/SetupWizardPage.js
  74. 6
      ee/app/auditing/client/routes.js
  75. 10
      ee/app/engagement-dashboard/client/components/EngagementDashboardRoute.js
  76. 6
      ee/app/engagement-dashboard/client/routes.js
  77. 2
      ee/client/.eslintrc.js

@ -50,7 +50,6 @@ dispatch:run-as-user
jalik:ufs@1.0.2
jalik:ufs-gridfs@1.0.2
jparker:gravatar
kadira:blaze-layout
kadira:flow-router
mizzao:timesync
mrt:reactive-store

@ -61,7 +61,6 @@ jparker:crypto-core@0.1.0
jparker:crypto-md5@0.1.1
jparker:gravatar@0.5.1
jquery@1.11.11
kadira:blaze-layout@2.3.0
kadira:flow-router@2.12.1
konecty:multiple-instances-status@1.1.0
konecty:user-presence@2.6.3

@ -78,8 +78,6 @@ export const FlowRouter = {
}),
};
export const BlazeLayout = {};
export const Session = {
get: () => {},
set: () => {},

@ -1,11 +1,10 @@
import { Meteor } from 'meteor/meteor';
import { Tracker } from 'meteor/tracker';
import { Template } from 'meteor/templating';
import { settings } from '../../settings';
Template.body.onRendered(() => {
Tracker.autorun((c) => {
Template.body.onRendered(function() {
this.autorun((c) => {
const piwikUrl = settings.get('PiwikAnalytics_enabled') && settings.get('PiwikAnalytics_url');
const piwikSiteId = piwikUrl && settings.get('PiwikAnalytics_siteId');
const piwikPrependDomain = piwikUrl && settings.get('PiwikAnalytics_prependDomain');

@ -1,13 +1,9 @@
import * as BlazeLayout from '../../../client/lib/portals/blazeLayout';
import { appLayout } from '../../../client/lib/appLayout';
import { registerAdminRoute } from '../../../client/views/admin';
import { t } from '../../utils';
registerAdminRoute('/chatpal', {
name: 'chatpal-admin',
action() {
return BlazeLayout.render('main', {
center: 'ChatpalAdmin',
pageTitle: t('Chatpal_AdminPage'),
});
return appLayout.render('main', { center: 'ChatpalAdmin' });
},
});

@ -3,7 +3,7 @@
<header class="rc-header">
<div class="rc-header__wrap">
<span class="rc-header__block">
{{_ pageTitle}}
{{_ "Chatpal_AdminPage"}}
</span>
</div>
</header>

@ -1,2 +1 @@
import './startup';
import './router';

@ -1,13 +0,0 @@
import { Meteor } from 'meteor/meteor';
import { FlowRouter } from 'meteor/kadira:flow-router';
import * as BlazeLayout from '../../../client/lib/portals/blazeLayout';
FlowRouter.route('/mailer/unsubscribe/:_id/:createdAt', {
name: 'mailer-unsubscribe',
async action(params) {
await import('./views');
Meteor.call('Mailer:unsubscribe', params._id, params.createdAt);
return BlazeLayout.render('mailerUnsubscribe');
},
});

@ -5,7 +5,5 @@ registerAdminSidebarItem({
href: 'admin-mailer',
i18nLabel: 'Mailer',
icon: 'mail',
permissionGranted() {
return hasAllPermission('access-mailer');
},
permissionGranted: () => hasAllPermission('access-mailer'),
});

@ -1,2 +0,0 @@
import './mailerUnsubscribe.html';
import './mailerUnsubscribe';

@ -1,14 +0,0 @@
<template name="mailerUnsubscribe">
<section class="rc-old full-page color-tertiary-font-color">
<div class="wrapper">
<header>
<a class="logo" href="/">
<img src="images/logo/logo.svg?v=3" />
</a>
</header>
<div class="cms-page content-background-color">
{{_ "You_have_successfully_unsubscribed"}}
</div>
</div>
</section>
</template>

@ -1,5 +0,0 @@
import { Template } from 'meteor/templating';
Template.mailerUnsubscribe.onRendered(function() {
return $('#initial-page-loading').remove();
});

@ -1,10 +1,10 @@
import { FlowRouter } from 'meteor/kadira:flow-router';
import * as BlazeLayout from '../../../client/lib/portals/blazeLayout';
import { appLayout } from '../../../client/lib/appLayout';
FlowRouter.route('/snippet/:snippetId/:snippetName', {
name: 'snippetView',
action() {
BlazeLayout.render('main', { center: 'snippetPage' });
appLayout.render('main', { center: 'snippetPage' });
},
});

@ -4,12 +4,12 @@ import { Template } from 'meteor/templating';
import { Accounts } from 'meteor/accounts-base';
import { ReactiveVar } from 'meteor/reactive-var';
import * as BlazeLayout from '../../../../client/lib/portals/blazeLayout';
import { appLayout } from '../../../../client/lib/appLayout';
import { APIClient } from '../../../utils/client';
FlowRouter.route('/oauth/authorize', {
action(params, queryParams) {
BlazeLayout.render('main', {
appLayout.render('main', {
center: 'authorize',
modal: true,
client_id: queryParams.client_id,
@ -22,7 +22,7 @@ FlowRouter.route('/oauth/authorize', {
FlowRouter.route('/oauth/error/:error', {
action(params) {
BlazeLayout.render('main', {
appLayout.render('main', {
center: 'oauth404',
modal: true,
error: params.error,

@ -0,0 +1,11 @@
#react-root {
position: relative;
display: flex;
overflow: visible;
flex-direction: column;
width: 100vw;
height: 100vh;
padding: 0;
}

@ -4,6 +4,7 @@
@import 'imports/general/base_old.css';
@import 'imports/general/base.css';
@import 'imports/general/animations.css';
@import 'imports/general/react-root.css';
/* Forms */
@import 'imports/general/forms.css';

@ -20,7 +20,9 @@
{{> messageBox messageBoxData}}
<footer class="thread-footer">
{{# with checkboxData }}
{{> Checkbox }}
<div style="display: flex;">
{{> Checkbox . }}
</div>
{{/with}}
<label for="sendAlso" class="thread-footer__text">{{ _ "Also_send_to_channel" }}</label>
</footer>

@ -2,7 +2,7 @@ import { Meteor } from 'meteor/meteor';
import { Accounts } from 'meteor/accounts-base';
import { FlowRouter } from 'meteor/kadira:flow-router';
import * as BlazeLayout from '../../../client/lib/portals/blazeLayout';
import { appLayout } from '../../../client/lib/appLayout';
Meteor.loginWithLoginToken = function(token) {
Accounts.callLoginMethod({
@ -20,7 +20,7 @@ Meteor.loginWithLoginToken = function(token) {
FlowRouter.route('/login-token/:token', {
name: 'tokenLogin',
action() {
BlazeLayout.render('loginLayout');
appLayout.render('loginLayout');
Meteor.loginWithLoginToken(this.getParam('token'));
},
});

@ -2,10 +2,6 @@ import { Template } from 'meteor/templating';
import { settings } from '../../../settings';
Template.loginLayout.onRendered(function() {
$('#initial-page-loading').remove();
});
Template.loginLayout.helpers({
backgroundUrl() {
const asset = settings.get('Assets_background');

@ -1,10 +1,10 @@
import { FlowRouter } from 'meteor/kadira:flow-router';
import * as BlazeLayout from '../../../client/lib/portals/blazeLayout';
import { appLayout } from '../../../client/lib/appLayout';
FlowRouter.route('/reset-password/:token', {
name: 'resetPassword',
action() {
BlazeLayout.render('loginLayout', { center: 'resetPassword' });
appLayout.render('loginLayout', { center: 'resetPassword' });
},
});

@ -0,0 +1 @@
<body class="color-primary-font-color"></body>

@ -0,0 +1,122 @@
import Clipboard from 'clipboard';
import s from 'underscore.string';
import { Meteor } from 'meteor/meteor';
import { Match } from 'meteor/check';
import { Session } from 'meteor/session';
import { Template } from 'meteor/templating';
import { t } from '../../utils/client';
import { chatMessages } from '../../ui';
import { Layout, modal, popover, fireGlobalEvent, RoomManager } from '../../ui-utils';
import { settings } from '../../settings';
import { ChatSubscription } from '../../models';
import './body.html';
Template.body.onRendered(function() {
new Clipboard('.clipboard');
$(document.body).on('keydown', function(e) {
const unread = Session.get('unread');
if (e.keyCode === 27 && (e.shiftKey === true || e.ctrlKey === true) && (unread != null) && unread !== '') {
e.preventDefault();
e.stopPropagation();
modal.open({
title: t('Clear_all_unreads_question'),
type: 'warning',
confirmButtonText: t('Yes_clear_all'),
showCancelButton: true,
cancelButtonText: t('Cancel'),
confirmButtonColor: '#DD6B55',
}, function() {
const subscriptions = ChatSubscription.find({
open: true,
}, {
fields: {
unread: 1,
alert: 1,
rid: 1,
t: 1,
name: 1,
ls: 1,
},
});
subscriptions.forEach((subscription) => {
if (subscription.alert || subscription.unread > 0) {
Meteor.call('readMessages', subscription.rid);
}
});
});
}
});
$(document.body).on('keydown', function(e) {
const { target } = e;
if (e.ctrlKey === true || e.metaKey === true) {
popover.close();
return;
}
if (!((e.keyCode > 45 && e.keyCode < 91) || e.keyCode === 8)) {
return;
}
if (/input|textarea|select/i.test(target.tagName)) {
return;
}
if (target.id === 'pswp') {
return;
}
popover.close();
if (document.querySelector('.rc-modal-wrapper dialog[open]')) {
return;
}
const inputMessage = chatMessages[RoomManager.openedRoom] && chatMessages[RoomManager.openedRoom].input;
if (!inputMessage) {
return;
}
inputMessage.focus();
});
const handleMessageLinkClick = (event) => {
const link = event.currentTarget;
if (link.origin === s.rtrim(Meteor.absoluteUrl(), '/') && /msg=([a-zA-Z0-9]+)/.test(link.search)) {
fireGlobalEvent('click-message-link', { link: link.pathname + link.search });
}
};
this.autorun(() => {
if (Layout.isEmbedded()) {
$(document.body).on('click', 'a', handleMessageLinkClick);
} else {
$(document.body).off('click', 'a', handleMessageLinkClick);
}
});
this.autorun(function(c) {
const w = window;
const d = document;
const script = 'script';
const l = 'dataLayer';
const i = settings.get('GoogleTagManager_id');
if (Match.test(i, String) && i.trim() !== '') {
c.stop();
return (function(w, d, s, l, i) {
w[l] = w[l] || [];
w[l].push({
'gtm.start': new Date().getTime(),
event: 'gtm.js',
});
const f = d.getElementsByTagName(s)[0];
const j = d.createElement(s);
const dl = l !== 'dataLayer' ? `&l=${ l }` : '';
j.async = true;
j.src = `//www.googletagmanager.com/gtm.js?id=${ i }${ dl }`;
return f.parentNode.insertBefore(j, f);
}(w, d, script, l, i));
}
});
});

@ -1,5 +1,5 @@
import './body';
import './loading';
import './error.html';
import './logoLayout.html';
import './main.html';
import './main';

@ -1,48 +1,42 @@
<body class="color-primary-font-color">
</body>
<template name="main">
{{#if subsReady}}
{{#unless showSetupWizard}}
{{#unless logged}}
{{#if useIframe}}
{{#if iframeUrl}}
<iframe src="{{iframeUrl}}" style="height: 100%; width: 100%;"></iframe>
{{/if}}
{{else}}
{{> loginLayout center="loginForm"}}
{{#unless logged}}
{{#if useIframe}}
{{#if iframeUrl}}
<iframe src="{{iframeUrl}}" style="height: 100%; width: 100%;"></iframe>
{{/if}}
{{ CustomScriptLoggedOut }}
{{else}}
{{#unless hasUsername}}
{{> username}}
{{> loginLayout center="loginForm"}}
{{/if}}
{{ CustomScriptLoggedOut }}
{{else}}
{{#unless hasUsername}}
{{> username}}
{{else}}
{{#if requirePasswordChange}}
{{> loginLayout center="resetPassword"}}
{{else}}
{{#if requirePasswordChange}}
{{> loginLayout center="resetPassword"}}
{{#if require2faSetup}}
<main id="rocket-chat" class="{{embeddedVersion}}">
<div class="rc-old main-content content-background-color">
{{> accountSecurity }}
</div>
</main>
{{else}}
{{#if require2faSetup}}
<main id="rocket-chat" class="{{embeddedVersion}}">
<div class="rc-old main-content content-background-color">
{{> accountSecurity }}
</div>
</main>
{{else}}
{{> videoCall overlay=true}}
<div id="user-card-popover"></div>
<div id="rocket-chat" class="{{embeddedVersion}} menu-nav">
{{#unless removeSidenav}}
{{> sideNav }}
{{/unless}}
<div class="{{#unless $eq old false}}rc-old{{/unless}} main-content content-background-color {{readReceiptsEnabled}}">
{{> Template.dynamic template=center}}
</div>
{{> videoCall overlay=true}}
<div id="rocket-chat" class="{{embeddedVersion}} menu-nav">
{{#unless removeSidenav}}
{{> sideNav }}
{{/unless}}
<div class="rc-old main-content content-background-color {{readReceiptsEnabled}}">
{{> Template.dynamic template=center}}
</div>
{{/if}}
</div>
{{/if}}
{{/unless}}
{{ CustomScriptLoggedIn }}
{{> photoswipe}}
{{/if}}
{{/unless}}
{{ CustomScriptLoggedIn }}
{{> photoswipe}}
{{/unless}}
{{else}}
{{> loading}}

@ -1,23 +1,20 @@
import Clipboard from 'clipboard';
import s from 'underscore.string';
import { Meteor } from 'meteor/meteor';
import { Match } from 'meteor/check';
import { Tracker } from 'meteor/tracker';
import { FlowRouter } from 'meteor/kadira:flow-router';
import { Session } from 'meteor/session';
import { Template } from 'meteor/templating';
import { t, getUserPreference } from '../../utils/client';
import { chatMessages } from '../../ui';
import { mainReady, Layout, iframeLogin, modal, popover, menu, fireGlobalEvent, RoomManager } from '../../ui-utils';
import { getUserPreference } from '../../utils/client';
import { mainReady, Layout, iframeLogin } from '../../ui-utils';
import { settings } from '../../settings';
import { CachedChatSubscription, Roles, ChatSubscription, Users } from '../../models';
import { CachedChatSubscription, Roles, Users } from '../../models';
import { CachedCollectionManager } from '../../ui-cached-collection';
import { hasRole } from '../../authorization';
import { tooltip } from '../../ui/client/components/tooltip';
import { callbacks } from '../../callbacks/client';
import { isSyncReady } from '../../../client/lib/userData';
import './main.html';
function executeCustomScript(script) {
eval(script);//eslint-disable-line
}
@ -31,142 +28,28 @@ function customScriptsOnLogout() {
callbacks.add('afterLogoutCleanUp', () => customScriptsOnLogout(), callbacks.priority.LOW, 'custom-script-on-logout');
Template.body.onRendered(function() {
new Clipboard('.clipboard');
$(document.body).on('keydown', function(e) {
const unread = Session.get('unread');
if (e.keyCode === 27 && (e.shiftKey === true || e.ctrlKey === true) && (unread != null) && unread !== '') {
e.preventDefault();
e.stopPropagation();
modal.open({
title: t('Clear_all_unreads_question'),
type: 'warning',
confirmButtonText: t('Yes_clear_all'),
showCancelButton: true,
cancelButtonText: t('Cancel'),
confirmButtonColor: '#DD6B55',
}, function() {
const subscriptions = ChatSubscription.find({
open: true,
}, {
fields: {
unread: 1,
alert: 1,
rid: 1,
t: 1,
name: 1,
ls: 1,
},
});
subscriptions.forEach((subscription) => {
if (subscription.alert || subscription.unread > 0) {
Meteor.call('readMessages', subscription.rid);
}
});
});
}
});
$(document.body).on('keydown', function(e) {
const { target } = e;
if (e.ctrlKey === true || e.metaKey === true) {
popover.close();
return;
}
if (!((e.keyCode > 45 && e.keyCode < 91) || e.keyCode === 8)) {
return;
}
if (/input|textarea|select/i.test(target.tagName)) {
return;
}
if (target.id === 'pswp') {
return;
}
popover.close();
if (document.querySelector('.rc-modal-wrapper dialog[open]')) {
return;
}
const inputMessage = chatMessages[RoomManager.openedRoom] && chatMessages[RoomManager.openedRoom].input;
if (!inputMessage) {
return;
}
inputMessage.focus();
});
const handleMessageLinkClick = (event) => {
const link = event.currentTarget;
if (link.origin === s.rtrim(Meteor.absoluteUrl(), '/') && /msg=([a-zA-Z0-9]+)/.test(link.search)) {
fireGlobalEvent('click-message-link', { link: link.pathname + link.search });
}
};
this.autorun(() => {
if (Layout.isEmbedded()) {
$(document.body).on('click', 'a', handleMessageLinkClick);
} else {
$(document.body).off('click', 'a', handleMessageLinkClick);
}
});
this.autorun(function(c) {
const w = window;
const d = document;
const script = 'script';
const l = 'dataLayer';
const i = settings.get('GoogleTagManager_id');
if (Match.test(i, String) && i.trim() !== '') {
c.stop();
return (function(w, d, s, l, i) {
w[l] = w[l] || [];
w[l].push({
'gtm.start': new Date().getTime(),
event: 'gtm.js',
});
const f = d.getElementsByTagName(s)[0];
const j = d.createElement(s);
const dl = l !== 'dataLayer' ? `&l=${ l }` : '';
j.async = true;
j.src = `//www.googletagmanager.com/gtm.js?id=${ i }${ dl }`;
return f.parentNode.insertBefore(j, f);
}(w, d, script, l, i));
}
});
});
Template.main.onCreated(function() {
tooltip.init();
});
Template.main.helpers({
removeSidenav() {
return Layout.isEmbedded() && !/^\/admin/.test(FlowRouter.current().route.path);
},
siteName() {
return settings.get('Site_Name');
},
logged() {
if (Meteor.userId() != null || (settings.get('Accounts_AllowAnonymousRead') === true && Session.get('forceLogin') !== true)) {
$('html').addClass('noscroll').removeClass('scroll');
removeSidenav: () => Layout.isEmbedded() && !/^\/admin/.test(FlowRouter.current().route.path),
logged: () => {
if (!!Meteor.userId() || (settings.get('Accounts_AllowAnonymousRead') === true && Session.get('forceLogin') !== true)) {
document.documentElement.classList.add('noscroll');
document.documentElement.classList.remove('scroll');
return true;
}
$('html').addClass('scroll').removeClass('noscroll');
document.documentElement.classList.add('scroll');
document.documentElement.classList.remove('noscroll');
return false;
},
useIframe() {
useIframe: () => {
const iframeEnabled = typeof iframeLogin !== 'undefined';
return iframeEnabled && iframeLogin.reactiveEnabled.get();
},
iframeUrl() {
iframeUrl: () => {
const iframeEnabled = typeof iframeLogin !== 'undefined';
return iframeEnabled && iframeLogin.reactiveIframeUrl.get();
},
subsReady() {
subsReady: () => {
const subscriptionsReady = CachedChatSubscription.ready.get();
const settingsReady = settings.cachedCollection.ready.get();
const ready = !Meteor.userId() || (isSyncReady.get() && subscriptionsReady && settingsReady);
@ -176,16 +59,16 @@ Template.main.helpers({
return ready;
},
hasUsername() {
hasUsername: () => {
const uid = Meteor.userId();
const user = uid && Users.findOne({ _id: uid }, { fields: { username: 1 } });
return (user && user.username) || (!uid && settings.get('Accounts_AllowAnonymousRead'));
},
requirePasswordChange() {
requirePasswordChange: () => {
const user = Meteor.user();
return user && user.requirePasswordChange === true;
},
require2faSetup() {
require2faSetup: () => {
const user = Meteor.user();
// User is already using 2fa
@ -196,70 +79,56 @@ Template.main.helpers({
const mandatoryRole = Roles.findOne({ _id: { $in: user.roles }, mandatory2fa: true });
return mandatoryRole !== undefined;
},
CustomScriptLoggedOut() {
CustomScriptLoggedOut: () => {
const script = settings.get('Custom_Script_Logged_Out') || '';
if (script.trim()) {
executeCustomScript(script);
}
},
CustomScriptLoggedIn() {
CustomScriptLoggedIn: () => {
const script = settings.get('Custom_Script_Logged_In') || '';
if (script.trim()) {
executeCustomScript(script);
}
},
embeddedVersion() {
embeddedVersion: () => {
if (Layout.isEmbedded()) {
return 'embedded-view';
}
},
showSetupWizard() {
const userId = Meteor.userId();
const Show_Setup_Wizard = settings.get('Show_Setup_Wizard');
return (!userId && Show_Setup_Wizard === 'pending') || (userId && hasRole(userId, 'admin') && Show_Setup_Wizard === 'in_progress');
},
readReceiptsEnabled() {
readReceiptsEnabled: () => {
if (settings.get('Message_Read_Receipt_Store_Users')) {
return 'read-receipts-enabled';
}
},
});
Template.main.events({
'click div.burger'() {
return menu.toggle();
},
Template.main.onCreated(function() {
tooltip.init();
});
Template.main.onRendered(function() {
$('#initial-page-loading').remove();
return Tracker.autorun(function() {
Tracker.autorun(function() {
const userId = Meteor.userId();
const Show_Setup_Wizard = settings.get('Show_Setup_Wizard');
if ((!userId && Show_Setup_Wizard === 'pending') || (userId && hasRole(userId, 'admin') && Show_Setup_Wizard === 'in_progress')) {
FlowRouter.go('setup-wizard');
}
if (getUserPreference(userId, 'hideUsernames')) {
$(document.body).on('mouseleave', 'button.thumb', function() {
return tooltip.hide();
});
return $(document.body).on('mouseenter', 'button.thumb', function(e) {
$(document.body).on('mouseenter', 'button.thumb', (e) => {
const avatarElem = $(e.currentTarget);
const username = avatarElem.attr('data-username');
if (username) {
e.stopPropagation();
return tooltip.showElement($('<span>').text(username), avatarElem);
tooltip.showElement($('<span>').text(username), avatarElem);
}
});
$(document.body).on('mouseleave', 'button.thumb', () => {
tooltip.hide();
});
return;
}
$(document.body).off('mouseenter', 'button.thumb');
return $(document.body).off('mouseleave', 'button.thumb');
$(document.body).off('mouseleave', 'button.thumb');
});
});
Meteor.startup(function() {
return fireGlobalEvent('startup', true);
});

@ -158,16 +158,19 @@ renderDynamicCssList();
settings.get(/theme-color-rc/i, () => renderDynamicCssList());
injectIntoBody('icons', Assets.getText('public/icons.svg'));
injectIntoBody('page-loading-div', `
<div id="initial-page-loading" class="page-loading">
<div class="loading-animation">
<div class="bounce bounce1"></div>
<div class="bounce bounce2"></div>
<div class="bounce bounce3"></div>
injectIntoBody('react-root', `
<div id="react-root">
<div class="page-loading">
<div class="loading-animation">
<div class="bounce bounce1"></div>
<div class="bounce bounce2"></div>
<div class="bounce bounce3"></div>
</div>
</div>
</div>`);
</div>
`);
injectIntoBody('icons', Assets.getText('public/icons.svg'));
settings.get('Accounts_ForgetUserSessionOnWindowClose', (key, value) => {
if (value) {

@ -7,7 +7,9 @@
</div>
</div>
<div class="rooms-list sidebar--custom-colors" aria-label="{{_ "Channels"}}" role="region">
{{> sidebarChats }}
<div style="display: flex; flex: 1 1 auto;">
{{> sidebarChats }}
</div>
</div>
<div class="wrapper-unread">
<div class="unread-rooms background-primary-action-color color-primary-action-contrast bottom-unread-rooms hidden">

@ -5,7 +5,7 @@ import { FlowRouter } from 'meteor/kadira:flow-router';
import { Session } from 'meteor/session';
import _ from 'underscore';
import * as BlazeLayout from '../../../../client/lib/portals/blazeLayout';
import { appLayout } from '../../../../client/lib/appLayout';
import { SideNav } from './SideNav';
export const AccountBox = (function() {
@ -78,7 +78,7 @@ export const AccountBox = (function() {
async action() {
await wait();
Session.set('openedRoom');
return BlazeLayout.render('main', routeConfig);
return appLayout.render('main', routeConfig);
},
triggersEnter: [
function() {

@ -1,13 +1,11 @@
import { Meteor } from 'meteor/meteor';
import { Tracker } from 'meteor/tracker';
import { Blaze } from 'meteor/blaze';
import { Template } from 'meteor/templating';
import { FlowRouter } from 'meteor/kadira:flow-router';
import { Session } from 'meteor/session';
import mem from 'mem';
import _ from 'underscore';
import { Random } from 'meteor/random';
import * as BlazeLayout from '../../../../client/lib/portals/blazeLayout';
import { appLayout } from '../../../../client/lib/appLayout';
import { Messages, ChatSubscription, Rooms } from '../../../models';
import { settings } from '../../../settings';
import { callbacks } from '../../../callbacks';
@ -15,40 +13,25 @@ import { roomTypes } from '../../../utils';
import { call, callMethod } from './callMethod';
import { RoomManager, fireGlobalEvent, RoomHistoryManager } from '..';
import { waitUntilWrapperExists } from './RoomHistoryManager';
import { createTemplateForComponent } from '../../../../client/lib/portals/createTemplateForComponent';
window.currentTracker = undefined;
// cleanup session when hot reloading
Session.set('openedRoom', null);
const getDomOfLoading = mem(function getDomOfLoading() {
const loadingDom = document.createElement('div');
const contentAsFunc = (content) => () => content;
const replaceCenterDomBy = (dom) => {
const roomNode = dom();
const template = Blaze._TemplateWith({ }, contentAsFunc(Template.loading));
Blaze.render(template, loadingDom);
return loadingDom;
});
const center = createTemplateForComponent(Random.id(), () => import('../../../../client/views/root/DomNode'), {
attachment: 'at-parent',
props: () => ({ node: roomNode }),
});
function replaceCenterDomBy(dom) {
document.dispatchEvent(new CustomEvent('main-content-destroyed'));
appLayout.render('main', { center });
return new Promise((resolve) => {
setTimeout(() => {
const mainNode = document.querySelector('.main-content');
if (mainNode) {
for (const child of Array.from(mainNode.children)) {
if (child) { mainNode.removeChild(child); }
}
const roomNode = dom();
mainNode.appendChild(roomNode);
return resolve([mainNode, roomNode]);
}
resolve(mainNode);
}, 0);
});
}
return roomNode;
};
const waitUntilRoomBeInserted = async (type, rid) => new Promise((resolve) => {
Tracker.autorun((c) => {
@ -65,7 +48,7 @@ export const openRoom = async function(type, name) {
window.currentTracker = Tracker.autorun(async function(c) {
const user = Meteor.user();
if ((user && user.username == null) || (user == null && settings.get('Accounts_AllowAnonymousRead') === false)) {
BlazeLayout.render('main');
appLayout.render('main');
return;
}
@ -84,16 +67,14 @@ export const openRoom = async function(type, name) {
if (RoomManager.open(type + name).ready() !== true) {
if (settings.get('Accounts_AllowAnonymousRead')) {
BlazeLayout.render('main');
appLayout.render('main');
}
await replaceCenterDomBy(() => getDomOfLoading());
appLayout.render('main', { center: 'loading' });
return;
}
BlazeLayout.render('main', {
center: 'loading',
});
appLayout.render('main', { center: 'loading' });
c.stop();
@ -101,12 +82,10 @@ export const openRoom = async function(type, name) {
window.currentTracker = undefined;
}
const [mainNode, roomDom] = await replaceCenterDomBy(() => RoomManager.getDomOfRoom(type + name, room._id, roomTypes.getConfig(type).mainTemplate));
const roomDom = replaceCenterDomBy(() => RoomManager.getDomOfRoom(type + name, room._id, roomTypes.getConfig(type).mainTemplate));
if (mainNode) {
const selector = await waitUntilWrapperExists('.messages-box .wrapper');
selector.scrollTop = roomDom.oldScrollTop;
}
const selector = await waitUntilWrapperExists('.messages-box .wrapper');
selector.scrollTop = roomDom.oldScrollTop;
Session.set('openedRoom', room._id);
RoomManager.openedRoom = room._id;
@ -147,7 +126,7 @@ export const openRoom = async function(type, name) {
}
}
Session.set('roomNotFound', { type, name, error });
return BlazeLayout.render('main', { center: 'roomNotFound' });
return appLayout.render('main', { center: 'roomNotFound' });
}
});
};

@ -3,7 +3,7 @@ import { Session } from 'meteor/session';
import { Template } from 'meteor/templating';
import { ChatSubscription } from '../../../../models/client';
import { Layout } from '../../../../ui-utils/client';
import { Layout, menu } from '../../../../ui-utils/client';
import { getUserPreference } from '../../../../utils';
Template.burger.helpers({
@ -52,3 +52,9 @@ Template.burger.helpers({
return Layout.isEmbedded();
},
});
Template.burger.events({
'click div.burger'() {
return menu.toggle();
},
});

@ -71,7 +71,3 @@ Template.invite.onCreated(function() {
}
});
});
Template.invite.onRendered(function() {
return $('#initial-page-loading').remove();
});

@ -31,7 +31,3 @@ Template.secretURL.onCreated(function() {
return this.hashIsValid.set(false);
});
});
Template.secretURL.onRendered(function() {
return $('#initial-page-loading').remove();
});

@ -27,7 +27,3 @@ Template.cmsPage.events({
return FlowRouter.go('/');
},
});
Template.cmsPage.onRendered(function() {
return $('#initial-page-loading').remove();
});

@ -46,7 +46,7 @@ export const roomExit = function() {
child.oldScrollTop = wrapper.scrollTop;
}
}
mainNode.removeChild(child);
// mainNode.removeChild(child);
}
});
});

@ -16,6 +16,7 @@ module.exports = {
},
],
'jsx-quotes': ['error', 'prefer-single'],
'new-cap': ['error', { capIsNewExceptions: ['HTML.Comment', 'HTML.DIV', 'SHA256'] }],
'prefer-arrow-callback': ['error', { allowNamedFunctions: true }],
'prettier/prettier': 2,
'react/display-name': 'error',
@ -82,6 +83,7 @@ module.exports = {
},
],
'jsx-quotes': ['error', 'prefer-single'],
'new-cap': ['error', { capIsNewExceptions: ['HTML.Comment', 'HTML.DIV', 'SHA256'] }],
'no-extra-parens': 'off',
'no-spaced-func': 'off',
'no-unused-vars': 'off',

@ -6,7 +6,7 @@ import { useLayout } from '../../../../contexts/LayoutContext';
import { useUserPreference } from '../../../../contexts/UserContext';
import { AttachmentContext, AttachmentContextValue } from '../context/AttachmentContext';
const AttachmentProvider: FC<{}> = ({ children }) => {
const AttachmentProvider: FC = ({ children }) => {
const { isMobile } = useLayout();
const reducedData = usePrefersReducedData();
const collapsedByDefault = !!useUserPreference<boolean>('collapseMediaByDefault');

@ -1,4 +1,4 @@
import { FunctionComponent, memo, useState } from 'react';
import { FC, memo, useState } from 'react';
import { createPortal } from 'react-dom';
const getModalRoot = (): Element => {
@ -14,9 +14,9 @@ const getModalRoot = (): Element => {
return newElement;
};
const ModalPortal: FunctionComponent = ({ children }) => {
const ModalPortal: FC = ({ children }) => {
const [modalRoot] = useState(getModalRoot);
return createPortal(children, modalRoot);
};
export default memo(ModalPortal);
export default memo<typeof ModalPortal>(ModalPortal);

@ -1,4 +1,5 @@
import { FollowMessageMethod } from './methods/followMessage';
import { UnsubscribeMethod as MailerUnsubscribeMethod } from './methods/mailer/unsubscribe';
import { RoomNameExistsMethod } from './methods/roomNameExists';
import { SaveRoomSettingsMethod } from './methods/saveRoomSettings';
import { SaveSettingsMethod } from './methods/saveSettings';
@ -129,6 +130,7 @@ export type ServerMethods = {
'updateOAuthApp': (...args: any[]) => any;
'updateOutgoingIntegration': (...args: any[]) => any;
'uploadCustomSound': (...args: any[]) => any;
'Mailer:unsubscribe': MailerUnsubscribeMethod;
};
export type ServerMethodName = keyof ServerMethods;

@ -0,0 +1 @@
export type UnsubscribeMethod = (_id: string, createdAt: string) => void;

@ -1,17 +0,0 @@
import { useLayoutEffect } from 'react';
export const useWipeInitialPageLoading = () => {
useLayoutEffect(() => {
const initialPageLoadingElement = document.getElementById('initial-page-loading');
if (!initialPageLoadingElement) {
return;
}
initialPageLoadingElement.style.display = 'none';
return () => {
initialPageLoadingElement.style.display = 'flex';
};
}, []);
};

@ -0,0 +1,52 @@
import { Emitter } from '@rocket.chat/emitter';
import { ComponentType } from 'react';
import { Subscription, Unsubscribe } from 'use-subscription';
type BlazeLayoutDescriptor = {
template: string;
data?: Record<string, unknown>;
};
type ComponentLayoutDescriptor<Props extends {} = {}> = {
component: ComponentType<Props>;
props?: Props;
};
type AppLayoutDescriptor = BlazeLayoutDescriptor | ComponentLayoutDescriptor | null;
class AppLayoutSubscription
extends Emitter<{ update: void }>
implements Subscription<AppLayoutDescriptor> {
private descriptor: AppLayoutDescriptor = null;
getCurrentValue = (): AppLayoutDescriptor => this.descriptor;
subscribe = (callback: () => void): Unsubscribe => this.on('update', callback);
setCurrentValue(descriptor: AppLayoutDescriptor): void {
this.descriptor = descriptor;
this.emit('update');
}
render: {
(template: string, data?: Record<string, unknown>): void;
(descriptor: BlazeLayoutDescriptor): void;
<Props = {}>(descriptor: ComponentLayoutDescriptor<Props>): void;
} = (
templateOrDescriptor: string | AppLayoutDescriptor,
data?: Record<string, unknown>,
): void => {
if (typeof templateOrDescriptor === 'string') {
this.setCurrentValue({ template: templateOrDescriptor, data });
return;
}
this.setCurrentValue(templateOrDescriptor);
};
reset = (): void => {
this.setCurrentValue(null);
};
}
export const appLayout = new AppLayoutSubscription();

@ -1,50 +1,74 @@
import { FlowRouter } from 'meteor/kadira:flow-router';
import type { ComponentType } from 'react';
import { Tracker } from 'meteor/tracker';
import { ComponentType, createElement, lazy, ReactNode } from 'react';
import { renderRouteComponent } from './portals/renderRouteComponent';
import { appLayout } from './appLayout';
import { createTemplateForComponent } from './portals/createTemplateForComponent';
type RouteRegister = {
(
path: string,
params: {
name: string;
lazyRouteComponent: () => Promise<ComponentType>;
props: Record<string, unknown>;
action: (params?: Record<string, string>, queryParams?: Record<string, string>) => void;
},
params: Parameters<typeof FlowRouter.route>[1] &
(
| {}
| {
lazyRouteComponent: () => Promise<{ default: ComponentType }>;
props: Record<string, unknown>;
}
),
): void;
};
export const createRouteGroup = (
name: string,
prefix: string,
importRouter: () => Promise<{ default: ComponentType }>,
importRouter: () => Promise<{
default: ComponentType<{
renderRoute?: () => ReactNode;
}>;
}>,
): RouteRegister => {
const routeGroup = FlowRouter.group({
name,
prefix,
});
const registerRoute: RouteRegister = (
path,
{ lazyRouteComponent, props, action, ...options },
) => {
routeGroup.route(path, {
...options,
action: (params, queryParams) => {
if (action) {
action(params, queryParams);
return;
}
renderRouteComponent(importRouter, {
template: 'main',
region: 'center',
propsFn: () => ({ lazyRouteComponent, ...options, params, queryParams, ...props }),
});
},
});
const registerRoute: RouteRegister = (path, options) => {
if ('lazyRouteComponent' in options) {
const { lazyRouteComponent, props, ...rest } = options;
const RouteComponent = lazy(lazyRouteComponent);
const renderRoute = (): ReactNode => createElement(RouteComponent, props);
routeGroup.route(path, {
...rest,
action() {
const center = createTemplateForComponent(
Tracker.nonreactive(() => FlowRouter.getRouteName()),
importRouter,
{
attachment: 'at-parent',
props: () => ({ renderRoute }),
},
);
appLayout.render('main', { center });
},
});
return;
}
routeGroup.route(path, options);
};
registerRoute('/', {
name: `${name}-index`,
action() {
const center = createTemplateForComponent(`${name}-index`, importRouter, {
attachment: 'at-parent',
});
appLayout.render('main', { center });
},
});
return registerRoute;
};

@ -1,32 +0,0 @@
import { Emitter } from '@rocket.chat/emitter';
import { Subscription, Unsubscribe } from 'use-subscription';
export type BlazeLayoutDescriptor = {
template: string;
regions?: { [region: string]: string };
};
class BlazeLayoutSubscription
extends Emitter<{ update: void }>
implements Subscription<BlazeLayoutDescriptor | null> {
private descriptor: BlazeLayoutDescriptor | null = null;
getCurrentValue = (): BlazeLayoutDescriptor | null => this.descriptor;
setCurrentValue(descriptor: BlazeLayoutDescriptor | null): void {
this.descriptor = descriptor;
this.emit('update');
}
subscribe = (callback: () => void): Unsubscribe => this.on('update', callback);
}
export const subscription = new BlazeLayoutSubscription();
export const render = (template: string, regions?: { [region: string]: string }): void => {
subscription.setCurrentValue({ template, regions });
};
export const reset = (): void => {
subscription.setCurrentValue(null);
};

@ -0,0 +1,44 @@
import { Emitter } from '@rocket.chat/emitter';
import { Random } from 'meteor/random';
import type { ReactNode } from 'react';
import type { Subscription, Unsubscribe } from 'use-subscription';
type BlazePortalEntry = {
key: string;
node: ReactNode;
};
class BlazePortalsSubscriptions
extends Emitter<{ update: void }>
implements Subscription<BlazePortalEntry[]> {
private map = new Map<Blaze.TemplateInstance, BlazePortalEntry>();
getCurrentValue = (): BlazePortalEntry[] => Array.from(this.map.values());
subscribe = (callback: () => void): Unsubscribe => this.on('update', callback);
register = (template: Blaze.TemplateInstance, node: ReactNode): void => {
const entry = this.map.get(template);
if (!entry) {
this.map.set(template, { key: Random.id(), node });
this.emit('update');
return;
}
if (entry.node === node) {
return;
}
this.map.set(template, { ...entry, node });
this.emit('update');
};
unregister = (template: Blaze.TemplateInstance): void => {
if (this.map.delete(template)) {
this.emit('update');
}
};
}
export const blazePortals = new BlazePortalsSubscriptions();

@ -4,42 +4,62 @@ import { ReactiveVar } from 'meteor/reactive-var';
import { Template } from 'meteor/templating';
import type { ComponentType, PropsWithoutRef } from 'react';
import { blazePortals } from './blazePortals';
import { createLazyPortal } from './createLazyPortal';
import { registerPortal } from './portalsSubscription';
const unregister = Symbol('unregister');
export const createTemplateForComponent = <Props extends {} = {}>(
name: string,
factory: () => Promise<{ default: ComponentType<Props> }>,
{
renderContainerView = (): unknown => HTML.DIV(), // eslint-disable-line new-cap
} = {},
options:
| {
renderContainerView?: () => unknown;
}
| {
attachment: 'at-parent';
props?: () => PropsWithoutRef<Props>;
} = {
renderContainerView: (): unknown => HTML.DIV(),
},
): string => {
if (Template[name]) {
return name;
}
const template = new Blaze.Template(name, renderContainerView);
template.onRendered(function (this: Blaze.TemplateInstance & Record<typeof unregister, unknown>) {
const renderFunction =
('renderContainerView' in options && options.renderContainerView) ||
('attachment' in options &&
options.attachment === 'at-parent' &&
((): unknown => HTML.Comment('anchor'))) ||
((): unknown => HTML.DIV());
const template = new Blaze.Template(name, renderFunction);
template.onRendered(function (this: Blaze.TemplateInstance) {
const props = new ReactiveVar(this.data as PropsWithoutRef<Props>);
this.autorun(() => {
props.set(Template.currentData());
props.set({
...('props' in options && typeof options.props === 'function' && options.props()),
...Template.currentData(),
});
});
const portal = createLazyPortal(factory, () => props.get(), this.firstNode as Element);
const container =
('renderContainerView' in options && (this.firstNode as Element)) ||
('attachment' in options &&
options.attachment === 'at-parent' &&
(this.firstNode as Node).parentElement) ||
null;
if (!this.firstNode) {
if (!container) {
return;
}
this[unregister] = registerPortal(this, portal);
const portal = createLazyPortal(factory, () => props.get(), container);
blazePortals.register(this, portal);
});
template.onDestroyed(function (
this: Blaze.TemplateInstance & Record<typeof unregister, () => void | undefined>,
) {
this[unregister]?.();
template.onDestroyed(function (this: Blaze.TemplateInstance) {
blazePortals.unregister(this);
});
Template[name] = template;

@ -1,89 +0,0 @@
import { Blaze } from 'meteor/blaze';
import { HTML } from 'meteor/htmljs';
import { FlowRouter } from 'meteor/kadira:flow-router';
import { Template } from 'meteor/templating';
import { Tracker } from 'meteor/tracker';
import type { ComponentType, PropsWithoutRef } from 'react';
import * as BlazeLayout from './blazeLayout';
import { createLazyElement } from './createLazyElement';
import { createLazyPortal } from './createLazyPortal';
import { portalsSubscription, registerPortal, unregisterPortal } from './portalsSubscription';
export const renderRouteComponent = <Props extends {} = {}>(
factory: () => Promise<{ default: ComponentType<Props> }>,
{
template,
region,
propsFn: getProps,
}: {
template?: string;
region?: string;
propsFn?: () => PropsWithoutRef<Props> | undefined;
} = {},
): void => {
const routeName = FlowRouter.getRouteName();
if (portalsSubscription.has(routeName)) {
return;
}
Tracker.autorun((computation) => {
if (routeName !== FlowRouter.getRouteName()) {
unregisterPortal(routeName);
computation.stop();
return;
}
if (!computation.firstRun) {
return;
}
if (!template || !region) {
BlazeLayout.reset();
const element = createLazyElement(factory, getProps);
if (routeName !== FlowRouter.getRouteName()) {
return;
}
registerPortal(routeName, element);
return;
}
if (!Template[routeName]) {
const blazeTemplate = new Blaze.Template(routeName, () => HTML.DIV()); // eslint-disable-line new-cap
blazeTemplate.onRendered(function (this: Blaze.TemplateInstance & { firstNode: Element }) {
const node = this.firstNode.parentElement;
if (!node) {
throw new Error();
}
this.firstNode.remove();
const portal = createLazyPortal(factory, getProps ?? ((): undefined => undefined), node);
if (routeName !== FlowRouter.getRouteName()) {
return;
}
registerPortal(routeName, portal);
const handleMainContentDestroyed = (): void => {
unregisterPortal(routeName);
document.removeEventListener('main-content-destroyed', handleMainContentDestroyed);
};
document.addEventListener('main-content-destroyed', handleMainContentDestroyed);
});
Template[routeName] = blazeTemplate;
}
Tracker.afterFlush(() => {
BlazeLayout.render(template, { [region]: routeName });
});
});
};

@ -1,57 +0,0 @@
import React from 'react';
import AttachmentProvider from '../components/Message/Attachments/providers/AttachmentProvider';
import AuthorizationProvider from './AuthorizationProvider';
import AvatarUrlProvider from './AvatarUrlProvider';
import ConnectionStatusProvider from './ConnectionStatusProvider';
import CustomSoundProvider from './CustomSoundProvider';
import LayoutProvider from './LayoutProvider';
import ModalProvider from './ModalProvider';
import OmnichannelProvider from './OmnichannelProvider';
import RouterProvider from './RouterProvider';
import ServerProvider from './ServerProvider';
import SessionProvider from './SessionProvider';
import SettingsProvider from './SettingsProvider';
import SidebarProvider from './SidebarProvider';
import ToastMessagesProvider from './ToastMessagesProvider';
import TranslationProvider from './TranslationProvider';
import UserProvider from './UserProvider';
function MeteorProvider({ children }) {
return (
<ConnectionStatusProvider>
<ServerProvider>
<RouterProvider>
<TranslationProvider>
<SessionProvider>
<SidebarProvider>
<ToastMessagesProvider>
<SettingsProvider>
<LayoutProvider>
<AvatarUrlProvider>
<CustomSoundProvider>
<UserProvider>
<AuthorizationProvider>
<OmnichannelProvider>
<ModalProvider>
{/* TODO move to RoomContext */}
<AttachmentProvider>{children}</AttachmentProvider>
</ModalProvider>
</OmnichannelProvider>
</AuthorizationProvider>
</UserProvider>
</CustomSoundProvider>
</AvatarUrlProvider>
</LayoutProvider>
</SettingsProvider>
</ToastMessagesProvider>
</SidebarProvider>
</SessionProvider>
</TranslationProvider>
</RouterProvider>
</ServerProvider>
</ConnectionStatusProvider>
);
}
export default MeteorProvider;

@ -0,0 +1,54 @@
import React, { FC } from 'react';
import AttachmentProvider from '../components/Message/Attachments/providers/AttachmentProvider';
import AuthorizationProvider from './AuthorizationProvider';
import AvatarUrlProvider from './AvatarUrlProvider';
import ConnectionStatusProvider from './ConnectionStatusProvider';
import CustomSoundProvider from './CustomSoundProvider';
import LayoutProvider from './LayoutProvider';
import ModalProvider from './ModalProvider';
import OmnichannelProvider from './OmnichannelProvider';
import RouterProvider from './RouterProvider';
import ServerProvider from './ServerProvider';
import SessionProvider from './SessionProvider';
import SettingsProvider from './SettingsProvider';
import SidebarProvider from './SidebarProvider';
import ToastMessagesProvider from './ToastMessagesProvider';
import TranslationProvider from './TranslationProvider';
import UserProvider from './UserProvider';
const MeteorProvider: FC = ({ children }) => (
<ConnectionStatusProvider>
<ServerProvider>
<RouterProvider>
<TranslationProvider>
<SessionProvider>
<SidebarProvider>
<ToastMessagesProvider>
<SettingsProvider>
<LayoutProvider>
<AvatarUrlProvider>
<CustomSoundProvider>
<UserProvider>
<AuthorizationProvider>
<OmnichannelProvider>
<ModalProvider>
<AttachmentProvider>{children}</AttachmentProvider>
</ModalProvider>
</OmnichannelProvider>
</AuthorizationProvider>
</UserProvider>
</CustomSoundProvider>
</AvatarUrlProvider>
</LayoutProvider>
</SettingsProvider>
</ToastMessagesProvider>
</SidebarProvider>
</SessionProvider>
</TranslationProvider>
</RouterProvider>
</ServerProvider>
</ConnectionStatusProvider>
);
export default MeteorProvider;

@ -1,14 +1,14 @@
import { Modal } from '@rocket.chat/fuselage';
import React, { useState, useMemo, memo } from 'react';
import React, { useState, useMemo, memo, FC, ComponentProps, ReactNode } from 'react';
import { modal } from '../../app/ui-utils/client/lib/modal';
import ModalPortal from '../components/ModalPortal';
import { ModalContext } from '../contexts/ModalContext';
function ModalProvider({ children }) {
const [currentModal, setCurrentModal] = useState(null);
const ModalProvider: FC = ({ children }) => {
const [currentModal, setCurrentModal] = useState<ReactNode>(null);
const contextValue = useMemo(
const contextValue = useMemo<ComponentProps<typeof ModalContext.Provider>['value']>(
() =>
Object.assign(modal, {
setModal: setCurrentModal,
@ -26,6 +26,6 @@ function ModalProvider({ children }) {
)}
</ModalContext.Provider>
);
}
};
export default memo(ModalProvider);
export default memo<typeof ModalProvider>(ModalProvider);

@ -141,4 +141,4 @@ const OmnichannelProvider: FC = ({ children }) => {
return <OmnichannelContext.Provider children={children} value={contextValue} />;
};
export default memo(OmnichannelProvider);
export default memo<typeof OmnichannelProvider>(OmnichannelProvider);

@ -1,20 +1,22 @@
import React, { lazy, Suspense } from 'react';
import { Meteor } from 'meteor/meteor';
import React from 'react';
import { render } from 'react-dom';
import AppRoot from '../views/root/AppRoot';
const createContainer = (): Element => {
const container = document.getElementById('react-root') ?? document.createElement('div');
container.id = 'react-root';
const container = document.getElementById('react-root');
if (!container) {
throw new Error('could not find the element #react-root on DOM tree');
}
document.body.insertBefore(container, document.body.firstChild);
return container;
};
const LazyAppRoot = lazy(() => import('../views/root/AppRoot'));
const container = createContainer();
render(
<Suspense fallback={null}>
<LazyAppRoot />
</Suspense>,
container,
);
Meteor.startup(() => {
const container = createContainer();
render(<AppRoot />, container);
});

@ -13,6 +13,7 @@ import './renderMessage';
import './renderNotification';
import './roomObserve';
import './routes';
import './setupWizard';
import './startup';
import './streamMessage';
import './theme';

@ -2,20 +2,21 @@ import { FlowRouter } from 'meteor/kadira:flow-router';
import { Meteor } from 'meteor/meteor';
import { Session } from 'meteor/session';
import { Tracker } from 'meteor/tracker';
import { lazy } from 'react';
import toastr from 'toastr';
import { KonchatNotification } from '../../app/ui/client';
import { handleError } from '../../app/utils/client';
import { IUser } from '../../definition/IUser';
import * as BlazeLayout from '../lib/portals/blazeLayout';
import { renderRouteComponent } from '../lib/portals/renderRouteComponent';
import { appLayout } from '../lib/appLayout';
import { createTemplateForComponent } from '../lib/portals/createTemplateForComponent';
FlowRouter.wait();
FlowRouter.route('/', {
name: 'index',
action() {
BlazeLayout.render('main', { center: 'loading' });
appLayout.render('main', { center: 'loading' });
if (!Meteor.userId()) {
return FlowRouter.go('home');
}
@ -65,66 +66,57 @@ FlowRouter.route('/home', {
}
}
BlazeLayout.render('main', { center: 'home' });
appLayout.render('main', { center: 'home' });
});
return;
}
BlazeLayout.render('main', { center: 'home' });
appLayout.render('main', { center: 'home' });
},
});
FlowRouter.route('/directory/:tab?', {
name: 'directory',
action: () => {
renderRouteComponent(() => import('../views/directory/DirectoryPage'), {
template: 'main',
region: 'center',
});
const DirectoryPage = createTemplateForComponent(
'DirectoryPage',
() => import('../views/directory/DirectoryPage'),
{ attachment: 'at-parent' },
);
appLayout.render('main', { center: DirectoryPage });
},
triggersExit: [
(): void => {
$('.main-content').addClass('rc-old');
},
],
});
FlowRouter.route('/omnichannel-directory/:tab?/:context?/:id?', {
name: 'omnichannel-directory',
action: () => {
renderRouteComponent(() => import('../views/omnichannel/directory/OmnichannelDirectoryPage'), {
template: 'main',
region: 'center',
});
const OmnichannelDirectoryPage = createTemplateForComponent(
'OmnichannelDirectoryPage',
() => import('../views/omnichannel/directory/OmnichannelDirectoryPage'),
{ attachment: 'at-parent' },
);
appLayout.render('main', { center: OmnichannelDirectoryPage });
},
triggersExit: [
(): void => {
$('.main-content').addClass('rc-old');
},
],
});
FlowRouter.route('/account/:group?', {
name: 'account',
action: () => {
renderRouteComponent(() => import('../views/account/AccountRoute'), {
template: 'main',
region: 'center',
});
const AccountRoute = createTemplateForComponent(
'AccountRoute',
() => import('../views/account/AccountRoute'),
{ attachment: 'at-parent' },
);
appLayout.render('main', { center: AccountRoute });
},
triggersExit: [
(): void => {
$('.main-content').addClass('rc-old');
},
],
});
FlowRouter.route('/terms-of-service', {
name: 'terms-of-service',
action: () => {
Session.set('cmsPage', 'Layout_Terms_of_Service');
BlazeLayout.render('cmsPage');
appLayout.render('cmsPage');
},
});
@ -132,7 +124,7 @@ FlowRouter.route('/privacy-policy', {
name: 'privacy-policy',
action: () => {
Session.set('cmsPage', 'Layout_Privacy_Policy');
BlazeLayout.render('cmsPage');
appLayout.render('cmsPage');
},
});
@ -140,7 +132,7 @@ FlowRouter.route('/legal-notice', {
name: 'legal-notice',
action: () => {
Session.set('cmsPage', 'Layout_Legal_Notice');
BlazeLayout.render('cmsPage');
appLayout.render('cmsPage');
},
});
@ -148,34 +140,44 @@ FlowRouter.route('/room-not-found/:type/:name', {
name: 'room-not-found',
action: ({ type, name } = {}) => {
Session.set('roomNotFound', { type, name });
BlazeLayout.render('main', { center: 'roomNotFound' });
appLayout.render('main', { center: 'roomNotFound' });
},
});
FlowRouter.route('/register/:hash', {
name: 'register-secret-url',
action: () => {
BlazeLayout.render('secretURL');
appLayout.render('secretURL');
},
});
FlowRouter.route('/invite/:hash', {
name: 'invite',
action: () => {
BlazeLayout.render('invite');
appLayout.render('invite');
},
});
FlowRouter.route('/setup-wizard/:step?', {
name: 'setup-wizard',
action: () => {
renderRouteComponent(() => import('../views/setupWizard/SetupWizardRoute'));
const SetupWizardRoute = lazy(() => import('../views/setupWizard/SetupWizardRoute'));
appLayout.render({ component: SetupWizardRoute });
},
});
FlowRouter.route('/mailer/unsubscribe/:_id/:createdAt', {
name: 'mailer-unsubscribe',
action: () => {
const MailerUnsubscriptionPage = lazy(() => import('../views/mailer/MailerUnsubscriptionPage'));
appLayout.render({ component: MailerUnsubscriptionPage });
},
});
FlowRouter.notFound = {
action: (): void => {
renderRouteComponent(() => import('../views/notFound/NotFoundPage'));
const NotFoundPage = lazy(() => import('../views/notFound/NotFoundPage'));
appLayout.render({ component: NotFoundPage });
},
};

@ -0,0 +1,21 @@
import { FlowRouter } from 'meteor/kadira:flow-router';
import { Meteor } from 'meteor/meteor';
import { Tracker } from 'meteor/tracker';
import { hasRole } from '../../app/authorization/client';
import { settings } from '../../app/settings/client';
Meteor.startup(() => {
Tracker.autorun(() => {
const userId = Meteor.userId();
const setupWizardState = settings.get('Show_Setup_Wizard');
const mustRedirect =
(!userId && setupWizardState === 'pending') ||
(!!userId && !!hasRole(userId, 'admin') && setupWizardState === 'in_progress');
if (mustRedirect) {
FlowRouter.go('setup-wizard');
}
});
});

@ -25,6 +25,8 @@ if (window.DISABLE_ANIMATION) {
}
Meteor.startup(() => {
fireGlobalEvent('startup', true);
Accounts.onLogout(() => Session.set('openedRoom', null));
TimeSync.loggingEnabled = false;

@ -1,5 +1,4 @@
import { HTML } from 'meteor/htmljs';
import { ComponentProps, ComponentType } from 'react';
import { createTemplateForComponent } from './lib/portals/createTemplateForComponent';
@ -21,28 +20,25 @@ createTemplateForComponent(
createTemplateForComponent(
'Checkbox',
(): Promise<{
default: ComponentType<ComponentProps<typeof import('@rocket.chat/fuselage').CheckBox>>;
}> => import('@rocket.chat/fuselage').then(({ CheckBox }) => ({ default: CheckBox })),
{
renderContainerView: () => HTML.DIV({ class: 'rcx-checkbox', style: 'display: flex;' }), // eslint-disable-line new-cap
async (): Promise<{ default: typeof import('@rocket.chat/fuselage').CheckBox }> => {
const { CheckBox } = await import('@rocket.chat/fuselage');
return { default: CheckBox };
},
);
createTemplateForComponent(
'ThreadComponent',
() => import('../app/threads/client/components/ThreadComponent'),
{
renderContainerView: () =>
HTML.DIV({ class: 'contextual-bar', style: 'display: flex; height: 100%;' }), // eslint-disable-line new-cap
attachment: 'at-parent',
},
);
createTemplateForComponent('RoomForeword', () => import('./components/RoomForeword'));
createTemplateForComponent('RoomForeword', () => import('./components/RoomForeword'), {
attachment: 'at-parent',
});
createTemplateForComponent(
'accountSecurity',
() => import('./views/account/security/AccountSecurityPage'),
{
attachment: 'at-parent',
},
);
createTemplateForComponent('messageLocation', () => import('./views/location/MessageLocation'));
@ -50,7 +46,7 @@ createTemplateForComponent('messageLocation', () => import('./views/location/Mes
createTemplateForComponent('sidebarHeader', () => import('./sidebar/header'));
createTemplateForComponent('sidebarChats', () => import('./sidebar/RoomList/index'), {
renderContainerView: () => HTML.DIV({ style: 'display: flex; flex: 1 1 auto;' }), // eslint-disable-line new-cap
attachment: 'at-parent',
});
createTemplateForComponent(
@ -58,7 +54,6 @@ createTemplateForComponent(
() => import('../app/ui-utils/client/lib/ReactionListContent'),
{
renderContainerView: () =>
// eslint-disable-next-line new-cap
HTML.DIV({
style:
'margin: -16px; height: 100%; display: flex; flex-direction: column; overflow: hidden;',
@ -70,35 +65,35 @@ createTemplateForComponent(
'omnichannelFlex',
() => import('./views/omnichannel/sidebar/OmnichannelSidebar'),
{
renderContainerView: () => HTML.DIV({ style: 'height: 100%; position: relative;' }), // eslint-disable-line new-cap
renderContainerView: () => HTML.DIV({ style: 'height: 100%; position: relative;' }),
},
);
createTemplateForComponent('auditPage', () => import('../ee/client/audit/AuditPage'), {
renderContainerView: () => HTML.DIV({ style: 'height: 100%;' }), // eslint-disable-line new-cap
attachment: 'at-parent',
});
createTemplateForComponent('auditLogPage', () => import('../ee/client/audit/AuditLogPage'), {
renderContainerView: () => HTML.DIV({ style: 'height: 100%;' }), // eslint-disable-line new-cap
attachment: 'at-parent',
});
createTemplateForComponent(
'DiscussionMessageList',
() => import('./views/room/contextualBar/Discussions'),
{
renderContainerView: () => HTML.DIV({ class: 'contextual-bar' }), // eslint-disable-line new-cap
renderContainerView: () => HTML.DIV({ class: 'contextual-bar' }),
},
);
createTemplateForComponent('ThreadsList', () => import('./views/room/contextualBar/Threads'), {
renderContainerView: () => HTML.DIV({ class: 'contextual-bar' }), // eslint-disable-line new-cap
renderContainerView: () => HTML.DIV({ class: 'contextual-bar' }),
});
createTemplateForComponent(
'ExportMessages',
() => import('./views/room/contextualBar/ExportMessages'),
{
renderContainerView: () => HTML.DIV({ class: 'contextual-bar' }), // eslint-disable-line new-cap
renderContainerView: () => HTML.DIV({ class: 'contextual-bar' }),
},
);
@ -106,19 +101,19 @@ createTemplateForComponent(
'KeyboardShortcuts',
() => import('./views/room/contextualBar/KeyboardShortcuts'),
{
renderContainerView: () => HTML.DIV({ class: 'contextual-bar' }), // eslint-disable-line new-cap
renderContainerView: () => HTML.DIV({ class: 'contextual-bar' }),
},
);
createTemplateForComponent('room', () => import('./views/room/Room'), {
renderContainerView: () => HTML.DIV({ style: 'height: 100%; position: relative;' }), // eslint-disable-line new-cap
renderContainerView: () => HTML.DIV({ style: 'height: 100%; position: relative;' }),
});
createTemplateForComponent(
'AutoTranslate',
() => import('./views/room/contextualBar/AutoTranslate'),
{
renderContainerView: () => HTML.DIV({ class: 'contextual-bar' }), // eslint-disable-line new-cap
renderContainerView: () => HTML.DIV({ class: 'contextual-bar' }),
},
);
@ -126,7 +121,7 @@ createTemplateForComponent(
'NotificationsPreferences',
() => import('./views/room/contextualBar/NotificationPreferences'),
{
renderContainerView: () => HTML.DIV({ class: 'contextual-bar' }), // eslint-disable-line new-cap
renderContainerView: () => HTML.DIV({ class: 'contextual-bar' }),
},
);
@ -134,7 +129,7 @@ createTemplateForComponent(
'InviteUsers',
() => import('./views/room/contextualBar/RoomMembers/InviteUsers'),
{
renderContainerView: () => HTML.DIV({ class: 'contextual-bar' }), // eslint-disable-line new-cap
renderContainerView: () => HTML.DIV({ class: 'contextual-bar' }),
},
);
@ -142,7 +137,7 @@ createTemplateForComponent(
'EditInvite',
() => import('./views/room/contextualBar/RoomMembers/EditInvite'),
{
renderContainerView: () => HTML.DIV({ class: 'contextual-bar' }), // eslint-disable-line new-cap
renderContainerView: () => HTML.DIV({ class: 'contextual-bar' }),
},
);
@ -150,35 +145,34 @@ createTemplateForComponent(
'AddUsers',
() => import('./views/room/contextualBar/RoomMembers/AddUsers'),
{
renderContainerView: () => HTML.DIV({ class: 'contextual-bar' }), // eslint-disable-line new-cap
renderContainerView: () => HTML.DIV({ class: 'contextual-bar' }),
},
);
createTemplateForComponent('membersList', () => import('./views/room/contextualBar/RoomMembers'), {
renderContainerView: () => HTML.DIV({ class: 'contextual-bar' }), // eslint-disable-line new-cap
renderContainerView: () => HTML.DIV({ class: 'contextual-bar' }),
});
createTemplateForComponent('OTR', () => import('./views/room/contextualBar/OTR'), {
renderContainerView: () => HTML.DIV({ class: 'contextual-bar' }), // eslint-disable-line new-cap
renderContainerView: () => HTML.DIV({ class: 'contextual-bar' }),
});
createTemplateForComponent(
'EditRoomInfo',
() => import('./views/room/contextualBar/Info/EditRoomInfo'),
{
renderContainerView: () => HTML.DIV({ class: 'contextual-bar' }), // eslint-disable-line new-cap
renderContainerView: () => HTML.DIV({ class: 'contextual-bar' }),
},
);
createTemplateForComponent('RoomInfo', () => import('./views/room/contextualBar/Info/RoomInfo'), {
renderContainerView: () => HTML.DIV({ class: 'contextual-bar' }), // eslint-disable-line new-cap
renderContainerView: () => HTML.DIV({ class: 'contextual-bar' }),
});
createTemplateForComponent(
'UserInfoWithData',
() => import('./views/room/contextualBar/UserInfo'),
{
// eslint-disable-next-line new-cap
renderContainerView: () => HTML.DIV({ class: 'contextual-bar' }),
},
);
@ -187,48 +181,42 @@ createTemplateForComponent(
'channelFilesList',
() => import('./views/room/contextualBar/RoomFiles'),
{
renderContainerView: () => HTML.DIV({ class: 'contextual-bar' }), // eslint-disable-line new-cap
renderContainerView: () => HTML.DIV({ class: 'contextual-bar' }),
},
);
createTemplateForComponent('RoomAnnouncement', () => import('./views/room/Announcement'), {
renderContainerView: () => HTML.DIV(), // eslint-disable-line new-cap
});
createTemplateForComponent('RoomAnnouncement', () => import('./views/room/Announcement'));
createTemplateForComponent(
'PruneMessages',
() => import('./views/room/contextualBar/PruneMessages'),
{
renderContainerView: () => HTML.DIV({ class: 'contextual-bar' }), // eslint-disable-line new-cap
renderContainerView: () => HTML.DIV({ class: 'contextual-bar' }),
},
);
createTemplateForComponent('Burger', () => import('./views/room/Header/Burger'), {
renderContainerView: () => HTML.DIV(), // eslint-disable-line new-cap
});
createTemplateForComponent('Burger', () => import('./views/room/Header/Burger'));
createTemplateForComponent(
'resetPassword',
() => import('./views/login/ResetPassword/ResetPassword'),
{
// eslint-disable-next-line new-cap
renderContainerView: () => HTML.DIV({ style: 'display: flex;' }),
},
);
createTemplateForComponent('ModalBlock', () => import('./views/blocks/ConnectedModalBlock'), {
// eslint-disable-next-line new-cap
renderContainerView: () => HTML.DIV({ style: 'display: flex; width: 100%; height: 100%;' }),
});
createTemplateForComponent('Blocks', () => import('./views/blocks/MessageBlock'));
createTemplateForComponent('adminFlex', () => import('./views/admin/sidebar/AdminSidebar'), {
renderContainerView: () => HTML.DIV({ style: 'height: 100%; position: relative;' }), // eslint-disable-line new-cap
renderContainerView: () => HTML.DIV({ style: 'height: 100%; position: relative;' }),
});
createTemplateForComponent('accountFlex', () => import('./views/account/AccountSidebar'), {
renderContainerView: () => HTML.DIV({ style: 'height: 100%; position: relative;' }), // eslint-disable-line new-cap
renderContainerView: () => HTML.DIV({ style: 'height: 100%; position: relative;' }),
});
createTemplateForComponent('SortList', () => import('./components/SortList'));

@ -1,7 +0,0 @@
declare module 'meteor/kadira:blaze-layout' {
namespace BlazeLayout {
function reset(): void;
function render(template: string, regions?: { [region: string]: string }): void;
function setRoot(selector: Element | string | null): void;
}
}

@ -1,5 +1,6 @@
declare module 'meteor/htmljs' {
namespace HTML {
function Comment(value: string): unknown;
function DIV(attributes?: Record<string, unknown>): unknown;
}
}

@ -3,7 +3,7 @@ import React, { useEffect } from 'react';
import { SideNav } from '../../../app/ui-utils/client';
import NotAuthorizedPage from '../../components/NotAuthorizedPage';
import { usePermission } from '../../contexts/AuthorizationContext';
import { useRouteParameter, useRoute } from '../../contexts/RouterContext';
import { useRouteParameter, useRoute, useCurrentRoute } from '../../contexts/RouterContext';
import { useSetting } from '../../contexts/SettingsContext';
import AccountIntegrationsPage from './AccountIntegrationsPage';
import AccountProfilePage from './AccountProfilePage';
@ -13,12 +13,17 @@ import AccountTokensPage from './tokens/AccountTokensPage';
import './sidebarItems';
const AccountRoute = () => {
const [routeName] = useCurrentRoute();
const page = useRouteParameter('group');
const router = useRoute('account');
useEffect(() => {
if (routeName !== 'account') {
return;
}
!page && router.push({ group: 'profile' });
}, [page, router]);
}, [routeName, page, router]);
useEffect(() => {
SideNav.setFlex('accountFlex');

@ -1,17 +1,27 @@
import React, { lazy, useMemo, Suspense } from 'react';
import React, { Suspense, useEffect } from 'react';
import PageSkeleton from '../../components/PageSkeleton';
import { useCurrentRoute, useRoute } from '../../contexts/RouterContext';
import SettingsProvider from '../../providers/SettingsProvider';
import AdministrationLayout from './AdministrationLayout';
function AdministrationRouter({ lazyRouteComponent, ...props }) {
const LazyRouteComponent = useMemo(() => lazy(lazyRouteComponent), [lazyRouteComponent]);
function AdministrationRouter({ renderRoute }) {
const [routeName] = useCurrentRoute();
const defaultRoute = useRoute('admin-info');
useEffect(() => {
if (routeName === 'admin-index') {
defaultRoute.push();
}
}, [defaultRoute, routeName]);
return (
<AdministrationLayout>
<SettingsProvider privileged>
<Suspense fallback={<PageSkeleton />}>
<LazyRouteComponent {...props} />
</Suspense>
{renderRoute ? (
<Suspense fallback={<PageSkeleton />}>{renderRoute()}</Suspense>
) : (
<PageSkeleton />
)}
</SettingsProvider>
</AdministrationLayout>
);

@ -6,14 +6,6 @@ export const registerAdminRoute = createRouteGroup('admin', '/admin', () =>
import('./AdministrationRouter'),
);
registerAdminRoute('/', {
triggersEnter: [
(context, redirect) => {
redirect('admin-info');
},
],
});
registerAdminRoute('/custom-sounds/:context?/:id?', {
name: 'custom-sounds',
lazyRouteComponent: () => import('./customSounds/AdminSoundsRoute'),

@ -2,7 +2,7 @@ import { Tabs } from '@rocket.chat/fuselage';
import React, { useEffect, useCallback } from 'react';
import Page from '../../components/Page';
import { useRoute, useRouteParameter } from '../../contexts/RouterContext';
import { useCurrentRoute, useRoute, useRouteParameter } from '../../contexts/RouterContext';
import { useSetting } from '../../contexts/SettingsContext';
import { useTranslation } from '../../contexts/TranslationContext';
import ChannelsTab from './ChannelsTab';
@ -13,19 +13,22 @@ function DirectoryPage() {
const t = useTranslation();
const defaultTab = useSetting('Accounts_Directory_DefaultView');
const federationEnabled = useSetting('FEDERATION_Enabled');
const [routeName] = useCurrentRoute();
const tab = useRouteParameter('tab');
const directoryRoute = useRoute('directory');
const handleTabClick = useCallback((tab) => () => directoryRoute.push({ tab }), [directoryRoute]);
useEffect(() => {
if (routeName !== 'directory') {
return;
}
if (!tab || (tab === 'external' && !federationEnabled)) {
return directoryRoute.replace({ tab: defaultTab });
}
}, [directoryRoute, tab, federationEnabled, defaultTab]);
}, [routeName, directoryRoute, tab, federationEnabled, defaultTab]);
const handleTabClick = useCallback((tab) => () => directoryRoute.push({ tab }), [directoryRoute]);
return (
<Page>

@ -0,0 +1,67 @@
import { Box, Callout, Throbber } from '@rocket.chat/fuselage';
import React, { FC, useEffect } from 'react';
import { useRouteParameter } from '../../contexts/RouterContext';
import { useAbsoluteUrl, useMethod } from '../../contexts/ServerContext';
import { useToastMessageDispatch } from '../../contexts/ToastMessagesContext';
import { useTranslation } from '../../contexts/TranslationContext';
import { AsyncState, AsyncStatePhase, useAsyncState } from '../../hooks/useAsyncState';
const useMailerUnsubscriptionState = (): AsyncState<boolean> => {
const { resolve, reject, ...unsubscribedState } = useAsyncState<boolean>();
const unsubscribe = useMethod('Mailer:unsubscribe');
const _id = useRouteParameter('_id');
const createdAt = useRouteParameter('createdAt');
const dispatchToastMessage = useToastMessageDispatch();
useEffect(() => {
const doUnsubscribe = async (_id: string, createdAt: string): Promise<void> => {
try {
await unsubscribe(_id, createdAt);
resolve(true);
} catch (error) {
dispatchToastMessage({ type: 'error', message: error });
reject(error);
}
};
if (!_id || !createdAt) {
return;
}
doUnsubscribe(_id, createdAt);
}, [resolve, reject, unsubscribe, _id, createdAt, dispatchToastMessage]);
return unsubscribedState;
};
const MailerUnsubscriptionPage: FC = () => {
const { phase, error } = useMailerUnsubscriptionState();
const t = useTranslation();
const absoluteUrl = useAbsoluteUrl();
return (
<section className='rc-old full-page color-tertiary-font-color'>
<div className='wrapper'>
<header>
<a className='logo' href={absoluteUrl('/')}>
<img src={absoluteUrl('/images/logo/logo.svg')} />
</a>
</header>
<Box color='default' marginInline='auto' marginBlock={16} maxWidth={800}>
{(phase === AsyncStatePhase.LOADING && <Throbber disabled />) ||
(phase === AsyncStatePhase.REJECTED && (
<Callout type='danger' title={error?.message} />
)) ||
(phase === AsyncStatePhase.RESOLVED && (
<Callout type='success' title={t('You_have_successfully_unsubscribed')} />
))}
</Box>
</div>
</section>
);
};
export default MailerUnsubscriptionPage;

@ -3,11 +3,8 @@ import React from 'react';
import { useRoute } from '../../contexts/RouterContext';
import { useTranslation } from '../../contexts/TranslationContext';
import { useWipeInitialPageLoading } from '../../hooks/useWipeInitialPageLoading';
function NotFoundPage() {
useWipeInitialPageLoading();
const t = useTranslation();
const homeRoute = useRoute('home');

@ -1,23 +1,31 @@
import React, { lazy, useMemo, Suspense, useEffect, FC, ComponentType } from 'react';
import React, { ReactNode, Suspense, useEffect, FC } from 'react';
import { SideNav } from '../../../app/ui-utils/client';
import PageSkeleton from '../../components/PageSkeleton';
import { useCurrentRoute, useRoute } from '../../contexts/RouterContext';
type OmnichannelRouterProps = {
lazyRouteComponent: () => Promise<{ default: ComponentType }>;
renderRoute?: () => ReactNode;
};
const OmnichannelRouter: FC<OmnichannelRouterProps> = ({ lazyRouteComponent, ...props }) => {
const LazyRouteComponent = useMemo(() => lazy(lazyRouteComponent), [lazyRouteComponent]);
const OmnichannelRouter: FC<OmnichannelRouterProps> = ({ renderRoute }) => {
const [routeName] = useCurrentRoute();
const defaultRoute = useRoute('omnichannel-current-chats');
useEffect(() => {
if (routeName === 'omnichannel-index') {
defaultRoute.push();
}
}, [defaultRoute, routeName]);
useEffect(() => {
SideNav.setFlex('omnichannelFlex');
SideNav.openFlex(() => undefined);
}, []);
return (
<Suspense fallback={<PageSkeleton />}>
<LazyRouteComponent {...props} />
</Suspense>
return renderRoute ? (
<Suspense fallback={<PageSkeleton />}>{renderRoute()}</Suspense>
) : (
<PageSkeleton />
);
};

@ -2,30 +2,35 @@ import { Tabs } from '@rocket.chat/fuselage';
import React, { useEffect, useCallback, useState } from 'react';
import Page from '../../../components/Page';
import { useRoute, useRouteParameter } from '../../../contexts/RouterContext';
import { useCurrentRoute, useRoute, useRouteParameter } from '../../../contexts/RouterContext';
import { useTranslation } from '../../../contexts/TranslationContext';
import ContextualBar from './ContextualBar';
import ChatTab from './chats/ChatTab';
import ContactTab from './contacts/ContactTab';
const OmnichannelDirectoryPage = () => {
const t = useTranslation();
const defaultTab = 'contacts';
const [routeName] = useCurrentRoute();
const tab = useRouteParameter('tab');
const directoryRoute = useRoute('omnichannel-directory');
useEffect(() => {
if (routeName !== 'omnichannel-directory') {
return;
}
if (!tab) {
return directoryRoute.replace({ tab: defaultTab });
}
}, [routeName, directoryRoute, tab, defaultTab]);
const handleTabClick = useCallback((tab) => () => directoryRoute.push({ tab }), [directoryRoute]);
const [contactReload, setContactReload] = useState();
const [chatReload, setChatReload] = useState();
useEffect(() => {
if (!tab) {
return directoryRoute.replace({ tab: defaultTab });
}
}, [directoryRoute, tab, defaultTab]);
const t = useTranslation();
return (
<Page flexDirection='row'>

@ -0,0 +1,39 @@
import React, { createElement, FC, Fragment, Suspense } from 'react';
import { useSubscription } from 'use-subscription';
import { appLayout } from '../../lib/appLayout';
import { blazePortals } from '../../lib/portals/blazePortals';
import BlazeTemplate from './BlazeTemplate';
import PageLoading from './PageLoading';
const AppLayout: FC = () => {
const descriptor = useSubscription(appLayout);
const portals = useSubscription(blazePortals);
if (descriptor === null) {
return null;
}
if ('template' in descriptor) {
return (
<>
<BlazeTemplate template={descriptor.template} data={descriptor.data} />
{portals.map(({ key, node }) => (
<Fragment key={key} children={node} />
))}
</>
);
}
if ('component' in descriptor) {
return (
<Suspense fallback={<PageLoading />}>
{createElement(descriptor.component, descriptor.props)}
</Suspense>
);
}
throw new Error('invalid app layout descriptor');
};
export default AppLayout;

@ -1,18 +1,24 @@
import React, { FC } from 'react';
import React, { FC, lazy, Suspense } from 'react';
import ConnectionStatusBar from '../../components/connectionStatus/ConnectionStatusBar';
import MeteorProvider from '../../providers/MeteorProvider';
import BannerRegion from '../banners/BannerRegion';
import BlazeLayoutWrapper from './BlazeLayoutWrapper';
import PortalsWrapper from './PortalsWrapper';
import PageLoading from './PageLoading';
const ConnectionStatusBar = lazy(
() => import('../../components/connectionStatus/ConnectionStatusBar'),
);
const MeteorProvider = lazy(() => import('../../providers/MeteorProvider'));
const BannerRegion = lazy(() => import('../banners/BannerRegion'));
const AppLayout = lazy(() => import('./AppLayout'));
const PortalsWrapper = lazy(() => import('./PortalsWrapper'));
const AppRoot: FC = () => (
<MeteorProvider>
<ConnectionStatusBar />
<BannerRegion />
<PortalsWrapper />
<BlazeLayoutWrapper />
</MeteorProvider>
<Suspense fallback={<PageLoading />}>
<MeteorProvider>
<ConnectionStatusBar />
<BannerRegion />
<AppLayout />
<PortalsWrapper />
</MeteorProvider>
</Suspense>
);
export default AppRoot;

@ -1,61 +0,0 @@
import { BlazeLayout } from 'meteor/kadira:blaze-layout';
import React, { FC, useLayoutEffect, useMemo, useRef, CSSProperties } from 'react';
import { useSubscription } from 'use-subscription';
import { subscription } from '../../lib/portals/blazeLayout';
let unmountCount = 0;
const BlazeLayoutWrapper: FC = () => {
const ref = useRef<HTMLDivElement>(null);
useLayoutEffect(() => {
if (!ref.current) {
return;
}
BlazeLayout.setRoot(ref.current);
return (): void => {
if (++unmountCount > 1) {
console.warn(
'It looks like BlazeLayoutWrapper is being remounted, droping template state out.',
);
}
BlazeLayout.reset();
BlazeLayout.setRoot(null);
};
}, []);
const descriptor = useSubscription(subscription);
useLayoutEffect(() => {
if (!descriptor) {
BlazeLayout.reset();
return;
}
BlazeLayout.render(descriptor.template, descriptor.regions);
}, [descriptor]);
const rootElementStyle = useMemo<CSSProperties>(
() =>
descriptor
? {
position: 'relative',
display: 'flex',
overflow: 'visible',
flexDirection: 'column',
width: '100vw',
height: '100vh',
padding: '0',
}
: { display: 'none' },
[descriptor],
);
return <div ref={ref} style={rootElementStyle} />;
};
export default BlazeLayoutWrapper;

@ -0,0 +1,45 @@
import { Blaze } from 'meteor/blaze';
import { ReactiveDict } from 'meteor/reactive-dict';
import { Template } from 'meteor/templating';
import React, { FC, useEffect, useRef } from 'react';
type BlazeTemplateProps = {
template: keyof typeof Template;
data?: Record<string, unknown>;
};
const hiddenStyle = { display: 'none' } as const;
const BlazeTemplate: FC<BlazeTemplateProps> = ({ template, data }) => {
const ref = useRef<HTMLDivElement>(null);
const dataRef = useRef(new ReactiveDict());
useEffect(() => {
if (data) {
dataRef.current.set(data);
}
});
useEffect(() => {
if (!ref.current || !ref.current.parentNode) {
return;
}
const data = dataRef.current;
const view = Blaze.renderWithData(
Template[template],
() => data.all(),
ref.current.parentNode,
ref.current,
);
return (): void => {
Blaze.remove(view);
};
}, [template]);
return <div ref={ref} style={hiddenStyle} />;
};
export default BlazeTemplate;

@ -0,0 +1,30 @@
import React, { FC, useLayoutEffect, useRef } from 'react';
type DomNodeProps = {
node: Node;
};
const hiddenStyle = { display: 'none' } as const;
const DomNode: FC<DomNodeProps> = ({ node }) => {
const ref = useRef<HTMLDivElement>(null);
useLayoutEffect(() => {
if (!ref.current || !ref.current.parentNode) {
return;
}
const container = ref.current.parentNode;
const sibling = ref.current;
container.insertBefore(node, sibling);
return (): void => {
container.removeChild(node);
};
}, [node]);
return <div ref={ref} style={hiddenStyle} />;
};
export default DomNode;

@ -0,0 +1,13 @@
import React, { FC } from 'react';
const PageLoading: FC = () => (
<div className='page-loading'>
<div className='loading-animation'>
<div className='bounce bounce1'></div>
<div className='bounce bounce2'></div>
<div className='bounce bounce3'></div>
</div>
</div>
);
export default PageLoading;

@ -4,7 +4,6 @@ import React from 'react';
import ScrollableContentWrapper from '../../components/ScrollableContentWrapper';
import { useTranslation } from '../../contexts/TranslationContext';
import { useWipeInitialPageLoading } from '../../hooks/useWipeInitialPageLoading';
import { finalStep } from './SetupWizardState';
import SideBar from './SideBar';
import AdminUserInformationStep from './steps/AdminUserInformationStep';
@ -13,8 +12,6 @@ import RegisterServerStep from './steps/RegisterServerStep';
import SettingsBasedStep from './steps/SettingsBasedStep';
function SetupWizardPage({ currentStep = 1 }) {
useWipeInitialPageLoading();
const t = useTranslation();
const small = useMediaQuery('(max-width: 760px)');

@ -1,17 +1,17 @@
import { FlowRouter } from 'meteor/kadira:flow-router';
import * as BlazeLayout from '../../../../client/lib/portals/blazeLayout';
import { appLayout } from '../../../../client/lib/appLayout';
FlowRouter.route('/audit', {
name: 'audit-home',
action() {
BlazeLayout.render('main', { center: 'auditPage' });
appLayout.render('main', { center: 'auditPage' });
},
});
FlowRouter.route('/audit-log', {
name: 'audit-log',
action() {
BlazeLayout.render('main', { center: 'auditLogPage' });
appLayout.render('main', { center: 'auditLogPage' });
},
});

@ -1,18 +1,22 @@
import React, { useEffect } from 'react';
import { useRoute, useRouteParameter } from '../../../../../client/contexts/RouterContext';
import { useCurrentRoute, useRoute, useRouteParameter } from '../../../../../client/contexts/RouterContext';
import { EngagementDashboardPage } from './EngagementDashboardPage';
export function EngagementDashboardRoute() {
const engagementDashboardRoute = useRoute('engagement-dashboard');
const [routeName] = useCurrentRoute();
const tab = useRouteParameter('tab');
useEffect(() => {
if (routeName !== 'engagement-dashboard') {
return;
}
if (!tab) {
engagementDashboardRoute.replace({ tab: 'users' });
}
}, [engagementDashboardRoute, tab]);
}, [routeName, engagementDashboardRoute, tab]);
return <EngagementDashboardPage
tab={tab}

@ -1,7 +1,8 @@
import { hasAllPermission } from '../../../../app/authorization';
import { registerAdminRoute, registerAdminSidebarItem } from '../../../../client/views/admin';
import { renderRouteComponent } from '../../../../client/lib/portals/renderRouteComponent';
import { hasLicense } from '../../license/client';
import { createTemplateForComponent } from '../../../../client/lib/portals/createTemplateForComponent';
import { appLayout } from '../../../../client/lib/appLayout';
registerAdminRoute('/engagement-dashboard/:tab?', {
name: 'engagement-dashboard',
@ -11,7 +12,8 @@ registerAdminRoute('/engagement-dashboard/:tab?', {
return;
}
renderRouteComponent(() => import('./components/EngagementDashboardRoute'), { template: 'main', region: 'center' });
const EngagementDashboardRoute = createTemplateForComponent('EngagementDashboardRoute', () => import('./components/EngagementDashboardRoute'), { attachment: 'at-parent' });
appLayout.render('main', { center: EngagementDashboardRoute });
},
});

@ -16,6 +16,7 @@ module.exports = {
},
],
'jsx-quotes': ['error', 'prefer-single'],
'new-cap': ['error', { capIsNewExceptions: ['HTML.Comment', 'HTML.DIV', 'SHA256'] }],
'prefer-arrow-callback': ['error', { allowNamedFunctions: true }],
'prettier/prettier': 2,
'react/display-name': 'error',
@ -82,6 +83,7 @@ module.exports = {
},
],
'jsx-quotes': ['error', 'prefer-single'],
'new-cap': ['error', { capIsNewExceptions: ['HTML.Comment', 'HTML.DIV', 'SHA256'] }],
'no-extra-parens': 'off',
'no-spaced-func': 'off',
'no-unused-vars': 'off',

Loading…
Cancel
Save