refactor(client): Move banner orchestration to `BannerRegion` effects (#28414)

pull/28371/head^2
Tasso Evangelista 3 years ago committed by GitHub
parent b9e4b18d10
commit 0d286a5597
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      apps/meteor/.eslintcache
  2. 40
      apps/meteor/app/version-check/client/index.js
  3. 7
      apps/meteor/app/version-check/server/methods/banner_dismiss.ts
  4. 1
      apps/meteor/client/importPackages.ts
  5. 18
      apps/meteor/client/lib/appLayout.tsx
  6. 73
      apps/meteor/client/startup/banners.ts
  7. 1
      apps/meteor/client/startup/index.ts
  8. 2
      apps/meteor/client/startup/routes.tsx
  9. 5
      apps/meteor/client/views/banners/BannerRegion.tsx
  10. 14
      apps/meteor/client/views/banners/hooks/useDismissUserBannerMutation.ts
  11. 75
      apps/meteor/client/views/banners/hooks/useRemoteBanners.ts
  12. 44
      apps/meteor/client/views/banners/hooks/useUserBanners.ts
  13. 8
      apps/meteor/client/views/root/AppRoot.tsx
  14. 12
      packages/core-typings/src/IUser.ts

File diff suppressed because one or more lines are too long

@ -1,40 +0,0 @@
import { Meteor } from 'meteor/meteor';
import { Tracker } from 'meteor/tracker';
import { TAPi18n } from 'meteor/rocketchat:tap-i18n';
import * as banners from '../../../client/lib/banners';
Meteor.startup(function () {
Tracker.autorun(() => {
const user = Meteor.user();
if (user && Object.keys(user.banners || {}).length > 0) {
const firstBanner = Object.values(user.banners)
.filter((b) => b.read !== true)
.sort((a, b) => b.priority - a.priority)[0];
if (!firstBanner) {
return;
}
firstBanner.textArguments = firstBanner.textArguments || [];
banners.open({
id: firstBanner.id,
title: TAPi18n.__(firstBanner.title),
text: TAPi18n.__(firstBanner.text, ...firstBanner.textArguments),
modifiers: firstBanner.modifiers,
action() {
if (firstBanner.link) {
window.open(firstBanner.link, '_system');
}
},
onClose() {
Meteor.call('banner/dismiss', {
id: firstBanner.id,
});
},
});
}
});
});

@ -2,6 +2,13 @@ import { Meteor } from 'meteor/meteor';
import { Users } from '../../../models/server';
declare module '@rocket.chat/ui-contexts' {
// eslint-disable-next-line @typescript-eslint/naming-convention
interface ServerMethods {
'banner/dismiss'({ id }: { id: string }): Promise<void>;
}
}
Meteor.methods({
'banner/dismiss'({ id }) {
if (!Meteor.userId()) {

@ -53,7 +53,6 @@ import '../app/webrtc/client';
import '../app/wordpress/client';
import '../app/meteor-accounts-saml/client';
import '../app/e2e/client';
import '../app/version-check/client';
import '../app/discussion/client';
import '../app/threads/client';
import '../app/user-status/client';

@ -1,5 +1,11 @@
import { Emitter } from '@rocket.chat/emitter';
import type { ReactElement } from 'react';
import React, { lazy } from 'react';
const ConnectionStatusBar = lazy(() => import('../components/connectionStatus/ConnectionStatusBar'));
const BannerRegion = lazy(() => import('../views/banners/BannerRegion'));
const PortalsWrapper = lazy(() => import('../views/root/PortalsWrapper'));
const ModalRegion = lazy(() => import('../views/modal/ModalRegion'));
type AppLayoutDescriptor = ReactElement | null;
@ -16,6 +22,18 @@ class AppLayoutSubscription extends Emitter<{ update: void }> {
}
render(element: ReactElement): void {
this.setCurrentValue(
<>
<ConnectionStatusBar />
<BannerRegion />
{element}
<PortalsWrapper />
<ModalRegion />
</>,
);
}
renderStandalone(element: ReactElement): void {
this.setCurrentValue(element);
}
}

@ -1,73 +0,0 @@
import { BannerPlatform } from '@rocket.chat/core-typings';
import { FlowRouter } from 'meteor/kadira:flow-router';
import { Meteor } from 'meteor/meteor';
import { Tracker } from 'meteor/tracker';
import { Notifications } from '../../app/notifications/client';
import { APIClient } from '../../app/utils/client';
import * as banners from '../lib/banners';
const fetchInitialBanners = async (): Promise<void> => {
const response = await APIClient.get('/v1/banners', {
platform: BannerPlatform.Web,
});
for (const banner of response.banners) {
banners.open({
...banner.view,
viewId: banner.view.viewId || banner._id,
});
}
};
const handleBanner = async (event: { bannerId: string }): Promise<void> => {
const response = await APIClient.get(`/v1/banners/${event.bannerId}`, {
platform: BannerPlatform.Web,
});
if (!response.banners.length) {
return banners.closeById(event.bannerId);
}
for (const banner of response.banners) {
banners.open({
...banner.view,
viewId: banner.view.viewId || banner._id,
});
}
};
const watchBanners = (): (() => void) => {
fetchInitialBanners();
Notifications.onLogged('banner-changed', handleBanner);
return (): void => {
Notifications.unLogged(handleBanner);
banners.clear();
};
};
Meteor.startup(() => {
let unwatchBanners: () => void | undefined;
Tracker.autorun(() => {
unwatchBanners?.();
if (!Meteor.userId()) {
return;
}
if (Tracker.nonreactive(() => FlowRouter.getRouteName()) === 'setup-wizard') {
Tracker.autorun((c) => {
if (FlowRouter.getRouteName() !== 'setup-wizard') {
unwatchBanners = Tracker.nonreactive(watchBanners);
c.stop();
}
});
return;
}
unwatchBanners = Tracker.nonreactive(watchBanners);
});
});

@ -3,7 +3,6 @@ import './absoluteUrl';
import './actionButtons';
import './afterLogoutCleanUp';
import './appRoot';
import './banners';
import './callbacks';
import './contextualBar';
import './customOAuth';

@ -202,7 +202,7 @@ FlowRouter.route('/invite/:hash', {
FlowRouter.route('/setup-wizard/:step?', {
name: 'setup-wizard',
action: () => {
appLayout.render(<SetupWizardRoute />);
appLayout.renderStandalone(<SetupWizardRoute />);
},
});

@ -5,10 +5,15 @@ import { useSyncExternalStore } from 'use-sync-external-store/shim';
import * as banners from '../../lib/banners';
import LegacyBanner from './LegacyBanner';
import UiKitBanner from './UiKitBanner';
import { useRemoteBanners } from './hooks/useRemoteBanners';
import { useUserBanners } from './hooks/useUserBanners';
const BannerRegion = (): ReactElement | null => {
const payload = useSyncExternalStore(...banners.firstSubscription);
useRemoteBanners();
useUserBanners();
if (!payload) {
return null;
}

@ -0,0 +1,14 @@
import { useMethod, useToastMessageDispatch } from '@rocket.chat/ui-contexts';
import { useMutation } from '@tanstack/react-query';
export const useDismissUserBannerMutation = () => {
const dismissBanner = useMethod('banner/dismiss');
const dispatchToastMessage = useToastMessageDispatch();
return useMutation(dismissBanner, {
onError: (error) => {
dispatchToastMessage({ type: 'error', message: error });
},
});
};

@ -0,0 +1,75 @@
import { BannerPlatform } from '@rocket.chat/core-typings';
import type { IBanner, Serialized, UiKitBannerPayload } from '@rocket.chat/core-typings';
import { useEndpoint, useStream, useUserId, ServerContext } from '@rocket.chat/ui-contexts';
import { useContext, useEffect } from 'react';
import * as banners from '../../../lib/banners';
export const useRemoteBanners = () => {
const uid = useUserId();
const serverContext = useContext(ServerContext);
const getBanners = useEndpoint('GET', '/v1/banners');
const subscribeToNotifyLoggedIn = useStream('notify-logged');
useEffect(() => {
if (!uid) {
return;
}
const controller = new AbortController();
const { signal } = controller;
const mapBanner = (banner: Serialized<IBanner>): UiKitBannerPayload => ({
...banner.view,
viewId: banner.view.viewId || banner._id,
});
const fetchInitialBanners = async (): Promise<void> => {
const response = await getBanners({
platform: BannerPlatform.Web,
});
if (signal?.aborted) {
return;
}
response.banners.forEach((banner) => {
banners.open(mapBanner(banner));
});
};
const handleBannerChange = async (event: { bannerId: string }): Promise<void> => {
const response = await serverContext.callEndpoint({
method: 'GET',
pathPattern: '/v1/banners/:id',
keys: { id: event.bannerId },
params: { platform: BannerPlatform.Web },
});
if (signal?.aborted) {
return;
}
if (!response.banners.length) {
return banners.closeById(event.bannerId);
}
response.banners.forEach((banner) => {
banners.open(mapBanner(banner));
});
};
fetchInitialBanners();
const unsubscribeFromBannerChanged = subscribeToNotifyLoggedIn('banner-changed', handleBannerChange);
return () => {
controller.abort();
unsubscribeFromBannerChanged();
banners.clear();
};
}, [getBanners, serverContext, subscribeToNotifyLoggedIn, uid]);
};

@ -0,0 +1,44 @@
import { useUser } from '@rocket.chat/ui-contexts';
import { TAPi18n } from 'meteor/rocketchat:tap-i18n';
import { useEffect } from 'react';
import * as banners from '../../../lib/banners';
import { useDismissUserBannerMutation } from './useDismissUserBannerMutation';
export const useUserBanners = () => {
const user = useUser();
const dismissBannerMutation = useDismissUserBannerMutation();
useEffect(() => {
if (!user?.banners || Object.keys(user.banners).length === 0) {
return;
}
const firstBanner = Object.values(user.banners)
.filter((b) => b.read !== true)
.sort((a, b) => b.priority - a.priority)[0];
if (!firstBanner) {
return;
}
banners.open({
id: firstBanner.id,
title: TAPi18n.__(firstBanner.title),
text: TAPi18n.__(firstBanner.text, {
postProcess: 'sprintf',
sprintf: firstBanner.textArguments ?? [],
}),
modifiers: firstBanner.modifiers,
action() {
if (firstBanner.link) {
window.open(firstBanner.link, '_system');
}
},
onClose() {
dismissBannerMutation.mutate({ id: firstBanner.id });
},
});
}, [dismissBannerMutation, user]);
};

@ -6,23 +6,15 @@ import { queryClient } from '../../lib/queryClient';
import OutermostErrorBoundary from './OutermostErrorBoundary';
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 ModalRegion = lazy(() => import('../modal/ModalRegion'));
const AppRoot = (): ReactElement => (
<OutermostErrorBoundary>
<Suspense fallback={<PageLoading />}>
<QueryClientProvider client={queryClient}>
<MeteorProvider>
<ConnectionStatusBar />
<BannerRegion />
<AppLayout />
<PortalsWrapper />
<ModalRegion />
</MeteorProvider>
</QueryClientProvider>
</Suspense>

@ -158,6 +158,18 @@ export interface IUser extends IRocketChatRecord {
avatarUrl?: string;
searchedServerNames?: string[];
};
banners?: {
[key: string]: {
id: string;
priority: number;
title: string;
text: string;
textArguments?: string[];
modifiers: ('large' | 'danger')[];
link: string;
read?: boolean;
};
};
}
export interface IRegisterUser extends IUser {

Loading…
Cancel
Save