A React-based replacement for BlazeLayout (#21527)
parent
191c3f8c2f
commit
84d547055e
@ -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' }); |
||||
}, |
||||
}); |
||||
|
@ -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'); |
||||
}, |
||||
}); |
@ -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' }); |
||||
}, |
||||
}); |
||||
|
@ -0,0 +1,11 @@ |
||||
#react-root { |
||||
position: relative; |
||||
|
||||
display: flex; |
||||
overflow: visible; |
||||
flex-direction: column; |
||||
|
||||
width: 100vw; |
||||
height: 100vh; |
||||
padding: 0; |
||||
} |
@ -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'; |
||||
|
@ -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(); |
@ -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,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); |
||||
}); |
||||
|
@ -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'); |
||||
} |
||||
}); |
||||
}); |
@ -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; |
||||
} |
||||
} |
||||
|
@ -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; |
@ -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; |
@ -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' }); |
||||
}, |
||||
}); |
||||
|
Loading…
Reference in new issue