Update the API of React Hooks using Meteor's reactive system (#18226)

pull/18127/head
Tasso Evangelista 6 years ago committed by GitHub
parent d025656a6c
commit e4e3c3ad02
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 18
      .eslintrc
  2. 5
      app/threads/client/components/hooks/useUserRoom.js
  3. 5
      app/threads/client/components/hooks/useUserSubscription.js
  4. 16
      client/admin/integrations/IntegrationsRoute.js
  5. 10
      client/admin/sidebar/AdminSidebar.js
  6. 4
      client/components/RoomForeword.js
  7. 34
      client/contexts/AuthorizationContext.js
  8. 79
      client/contexts/AuthorizationContext.ts
  9. 18
      client/contexts/SessionContext.js
  10. 26
      client/contexts/SessionContext.ts
  11. 19
      client/contexts/UserContext.js
  12. 34
      client/contexts/UserContext.ts
  13. 31
      client/hooks/useForm.js
  14. 179
      client/hooks/useForm.ts
  15. 22
      client/hooks/useObservableValue.js
  16. 30
      client/hooks/useQuery.ts
  17. 28
      client/hooks/useReactiveSubscriptionFactory.ts
  18. 19
      client/hooks/useReactiveValue.js
  19. 35
      client/hooks/useReactiveValue.ts
  20. 22
      client/providers/AuthorizationProvider.js
  21. 31
      client/providers/AuthorizationProvider.tsx
  22. 10
      client/providers/ConnectionStatusProvider.js
  23. 24
      client/providers/EditableSettingsProvider.tsx
  24. 6
      client/providers/MeteorProvider.js
  25. 16
      client/providers/SessionProvider.js
  26. 17
      client/providers/SessionProvider.tsx
  27. 26
      client/providers/SettingsProvider.tsx
  28. 2
      client/providers/SidebarProvider.js
  29. 37
      client/providers/TranslationProvider.js
  30. 34
      client/providers/UserProvider.js
  31. 41
      client/providers/UserProvider.tsx
  32. 19
      client/providers/createObservableFromReactive.js
  33. 39
      client/providers/createReactiveSubscriptionFactory.ts

@ -77,6 +77,8 @@
"error",
"prefer-single"
],
"indent": "off",
"no-extra-parens": "off",
"react/jsx-uses-react": "error",
"react/jsx-uses-vars": "error",
"react/jsx-no-undef": "error",
@ -87,7 +89,21 @@
"@typescript-eslint/ban-ts-ignore": "off",
"@typescript-eslint/indent": [
"error",
"tab"
"tab",
{
"SwitchCase": 1
}
],
"@typescript-eslint/no-extra-parens": [
"error",
"all",
{
"conditionalAssign": true,
"nestedBinaryExpressions": false,
"returnAssign": true,
"ignoreJSX": "all",
"enforceForArrowConditionals": false
}
],
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/interface-name-prefix": [

@ -1,4 +1,7 @@
import { useCallback } from 'react';
import { useReactiveValue } from '../../../../../client/hooks/useReactiveValue';
import { Rooms } from '../../../../models/client';
export const useUserRoom = (rid, fields) => useReactiveValue(() => Rooms.findOne({ _id: rid }, { fields }), [rid, fields]);
export const useUserRoom = (rid, fields) =>
useReactiveValue(useCallback(() => Rooms.findOne({ _id: rid }, { fields }), [rid, fields]));

@ -1,4 +1,7 @@
import { useCallback } from 'react';
import { useReactiveValue } from '../../../../../client/hooks/useReactiveValue';
import { Subscriptions } from '../../../../models/client';
export const useUserSubscription = (rid, fields) => useReactiveValue(() => Subscriptions.findOne({ rid }, { fields }), [rid, fields]);
export const useUserSubscription = (rid, fields) =>
useReactiveValue(useCallback(() => Subscriptions.findOne({ rid }, { fields }), [rid, fields]));

@ -1,4 +1,4 @@
import React from 'react';
import React, { useMemo } from 'react';
import { useAtLeastOnePermission } from '../../contexts/AuthorizationContext';
import { useRouteParameter } from '../../contexts/RouterContext';
@ -9,12 +9,14 @@ import EditIntegrationsPage from './edit/EditIntegrationsPage';
import OutgoingWebhookHistoryPage from './edit/OutgoingWebhookHistoryPage';
function IntegrationsRoute() {
const canViewIntegrationsPage = useAtLeastOnePermission([
'manage-incoming-integrations',
'manage-outgoing-integrations',
'manage-own-incoming-integrations',
'manage-own-outgoing-integrations',
]);
const canViewIntegrationsPage = useAtLeastOnePermission(
useMemo(() => [
'manage-incoming-integrations',
'manage-outgoing-integrations',
'manage-own-incoming-integrations',
'manage-own-outgoing-integrations',
], []),
);
const context = useRouteParameter('context');

@ -74,7 +74,7 @@ const SidebarItemsAssembler = React.memo(({ items, currentPath }) => {
});
const AdminSidebarPages = React.memo(({ currentPath }) => {
const items = useReactiveValue(() => sidebarItems.get());
const items = useReactiveValue(useCallback(() => sidebarItems.get(), []));
return <Box display='flex' flexDirection='column' flexShrink={0} pb='x8'>
<SidebarItemsAssembler items={items} currentPath={currentPath}/>
@ -163,7 +163,13 @@ const AdminSidebarSettings = ({ currentPath }) => {
export default React.memo(function AdminSidebar() {
const t = useTranslation();
const canViewSettings = useAtLeastOnePermission(['view-privileged-setting', 'edit-privileged-setting', 'manage-selected-settings']);
const canViewSettings = useAtLeastOnePermission(
useMemo(() => [
'view-privileged-setting',
'edit-privileged-setting',
'manage-selected-settings',
], []),
);
const closeAdminFlex = useCallback(() => {
if (Layout.isEmbedded()) {

@ -1,4 +1,4 @@
import React from 'react';
import React, { useCallback } from 'react';
import { Avatar, Margins, Flex, Box, Tag } from '@rocket.chat/fuselage';
import { Rooms, Users } from '../../app/models/client';
@ -11,7 +11,7 @@ const RoomForeword = ({ _id: rid }) => {
const t = useTranslation();
const user = useUser();
const room = useReactiveValue(() => Rooms.findOne({ _id: rid }));
const room = useReactiveValue(useCallback(() => Rooms.findOne({ _id: rid }), [rid]));
if (room.t !== 'd') {
return t('Start_of_conversation');

@ -1,34 +0,0 @@
import { createContext, useCallback, useContext } from 'react';
import { useObservableValue } from '../hooks/useObservableValue';
export const AuthorizationContext = createContext({
hasPermission: () => {},
hasAtLeastOnePermission: () => {},
hasAllPermissions: () => {},
hasRole: () => {},
});
export const usePermission = (permission, scope) => {
const { hasPermission } = useContext(AuthorizationContext);
return useObservableValue(useCallback((listener) =>
hasPermission(permission, scope, listener), [hasPermission, permission, scope]));
};
export const useAtLeastOnePermission = (permissions, scope) => {
const { hasAtLeastOnePermission } = useContext(AuthorizationContext);
return useObservableValue(useCallback((listener) =>
hasAtLeastOnePermission(permissions, scope, listener), [hasAtLeastOnePermission, permissions, scope]));
};
export const useAllPermissions = (permissions, scope) => {
const { hasAllPermissions } = useContext(AuthorizationContext);
return useObservableValue(useCallback((listener) =>
hasAllPermissions(permissions, scope, listener), [hasAllPermissions, permissions, scope]));
};
export const useRole = (role) => {
const { hasRole } = useContext(AuthorizationContext);
return useObservableValue(useCallback((listener) =>
hasRole(role, listener), [hasRole, role]));
};

@ -0,0 +1,79 @@
import { createContext, useContext, useMemo } from 'react';
import { useSubscription, Subscription, Unsubscribe } from 'use-subscription';
export type AuthorizationContextValue = {
queryPermission(
permission: string | Mongo.ObjectID,
scope?: string | Mongo.ObjectID
): Subscription<boolean>;
queryAtLeastOnePermission(
permission: (string | Mongo.ObjectID)[],
scope?: string | Mongo.ObjectID
): Subscription<boolean>;
queryAllPermissions(
permission: (string | Mongo.ObjectID)[],
scope?: string | Mongo.ObjectID
): Subscription<boolean>;
queryRole(role: string | Mongo.ObjectID): Subscription<boolean>;
};
export const AuthorizationContext = createContext<AuthorizationContextValue>({
queryPermission: () => ({
getCurrentValue: (): boolean => false,
subscribe: (): Unsubscribe => (): void => undefined,
}),
queryAtLeastOnePermission: () => ({
getCurrentValue: (): boolean => false,
subscribe: (): Unsubscribe => (): void => undefined,
}),
queryAllPermissions: () => ({
getCurrentValue: (): boolean => false,
subscribe: (): Unsubscribe => (): void => undefined,
}),
queryRole: () => ({
getCurrentValue: (): boolean => false,
subscribe: (): Unsubscribe => (): void => undefined,
}),
});
export const usePermission = (
permission: string | Mongo.ObjectID,
scope?: string | Mongo.ObjectID,
): boolean => {
const { queryPermission } = useContext(AuthorizationContext);
const subscription = useMemo(
() => queryPermission(permission, scope),
[queryPermission, permission, scope],
);
return useSubscription(subscription);
};
export const useAtLeastOnePermission = (
permissions: (string | Mongo.ObjectID)[],
scope?: string | Mongo.ObjectID,
): boolean => {
const { queryAtLeastOnePermission } = useContext(AuthorizationContext);
const subscription = useMemo(
() => queryAtLeastOnePermission(permissions, scope),
[queryAtLeastOnePermission, permissions, scope],
);
return useSubscription(subscription);
};
export const useAllPermissions = (
permissions: (string | Mongo.ObjectID)[],
scope?: string | Mongo.ObjectID,
): boolean => {
const { queryAllPermissions } = useContext(AuthorizationContext);
const subscription = useMemo(
() => queryAllPermissions(permissions, scope),
[queryAllPermissions, permissions, scope],
);
return useSubscription(subscription);
};
export const useRole = (role: string | Mongo.ObjectID): boolean => {
const { queryRole } = useContext(AuthorizationContext);
const subscription = useMemo(() => queryRole(role), [queryRole, role]);
return useSubscription(subscription);
};

@ -1,18 +0,0 @@
import { createContext, useCallback, useContext } from 'react';
import { useObservableValue } from '../hooks/useObservableValue';
export const SessionContext = createContext({
get: () => {},
set: () => {},
});
export const useSession = (name) => {
const { get } = useContext(SessionContext);
return useObservableValue((listener) => get(name, listener));
};
export const useSessionDispatch = (name) => {
const { set } = useContext(SessionContext);
return useCallback((value) => set(name, value), [set, name]);
};

@ -0,0 +1,26 @@
import { createContext, useCallback, useContext, useMemo } from 'react';
import { useSubscription, Subscription, Unsubscribe } from 'use-subscription';
type SessionContextValue = {
query: (name: string) => Subscription<unknown>;
dispatch: (name: string, value: unknown) => void;
};
export const SessionContext = createContext<SessionContextValue>({
query: () => ({
getCurrentValue: (): undefined => undefined,
subscribe: (): Unsubscribe => (): void => undefined,
}),
dispatch: (): void => undefined,
});
export const useSession = (name: string): unknown => {
const { query } = useContext(SessionContext);
const subscription = useMemo(() => query(name), [query, name]);
return useSubscription(subscription);
};
export const useSessionDispatch = (name: string): ((name: string, value: unknown) => void) => {
const { dispatch } = useContext(SessionContext);
return useCallback((value) => dispatch(name, value), [dispatch, name]);
};

@ -1,19 +0,0 @@
import { createContext, useContext, useCallback } from 'react';
import { useObservableValue } from '../hooks/useObservableValue';
export const UserContext = createContext({
userId: null,
user: null,
loginWithPassword: async () => {},
getPreference: () => {},
});
export const useUserId = () => useContext(UserContext).userId;
export const useUser = () => useContext(UserContext).user;
export const useLoginWithPassword = () => useContext(UserContext).loginWithPassword;
export const useUserPreference = (key, defaultValue = undefined) => {
const { getPreference } = useContext(UserContext);
return useObservableValue(useCallback((listener) =>
getPreference(key, defaultValue, listener), [getPreference, key, defaultValue]));
};

@ -0,0 +1,34 @@
import { createContext, useContext, useMemo } from 'react';
import { useSubscription, Subscription, Unsubscribe } from 'use-subscription';
type UserContextValue = {
userId: string | null;
user: Meteor.User | null;
loginWithPassword: (user: string | object, password: string) => Promise<void>;
queryPreference: <T>(key: string | Mongo.ObjectID, defaultValue?: T) => Subscription<T | undefined>;
};
export const UserContext = createContext<UserContextValue>({
userId: null,
user: null,
loginWithPassword: async () => undefined,
queryPreference: () => ({
getCurrentValue: (): undefined => undefined,
subscribe: (): Unsubscribe => (): void => undefined,
}),
});
export const useUserId = (): string | Mongo.ObjectID | null =>
useContext(UserContext).userId;
export const useUser = (): Meteor.User | null =>
useContext(UserContext).user;
export const useLoginWithPassword = (): ((user: string | object, password: string) => Promise<void>) =>
useContext(UserContext).loginWithPassword;
export const useUserPreference = <T>(key: string | Mongo.ObjectID, defaultValue?: T): T | undefined => {
const { queryPreference } = useContext(UserContext);
const subscription = useMemo(() => queryPreference(key, defaultValue), [queryPreference, key, defaultValue]);
return useSubscription(subscription);
};

@ -1,31 +0,0 @@
/* eslint-disable react-hooks/rules-of-hooks */
import { useState, useCallback } from 'react';
import { capitalize } from '../helpers/capitalize';
const getValue = (e) => (e.currentTarget ? e.currentTarget.value : e);
export const useForm = (obj) => {
const resetCallbacks = [];
const hasUnsavedChanges = [];
// TODO: use useReducer hook as we can't assure that obj will have the same structure on each render
const ret = Object.keys(obj).sort().reduce((ret, key) => {
const value = obj[key];
const [data, setData] = useState(value);
ret.values = { ...ret.values, [key]: data };
ret.handlers = { ...ret.handlers, [`handle${ capitalize(key) }`]: useCallback(typeof value !== 'boolean' ? (e) => setData(getValue(e)) : () => setData(!data), [data]) };
hasUnsavedChanges.push(JSON.stringify(value) !== JSON.stringify(data));
resetCallbacks.push(() => setData(value));
return ret;
}, {});
ret.reset = () => {
resetCallbacks.forEach((reset) => reset());
};
ret.hasUnsavedChanges = hasUnsavedChanges.filter(Boolean).length > 0;
return ret;
};

@ -0,0 +1,179 @@
import { useCallback, useReducer, useMemo, ChangeEvent } from 'react';
import { capitalize } from '../helpers/capitalize';
type Field = {
name: string;
currentValue: unknown;
initialValue: unknown;
changed: boolean;
};
type FormState = {
fields: Field[];
values: Record<string, unknown>;
hasUnsavedChanges: boolean;
};
type FormAction = {
(prevState: FormState): FormState;
};
type UseFormReturnType = {
values: Record<string, unknown>;
handlers: Record<string, (eventOrValue: ChangeEvent | unknown) => void>;
hasUnsavedChanges: boolean;
commit: () => void;
reset: () => void;
};
const reduceForm = (state: FormState, action: FormAction): FormState => {
console.time('reduceForm');
const newState = action(state);
console.timeEnd('reduceForm');
return newState;
};
const initForm = (initialValues: Record<string, unknown>): FormState => {
const fields = [];
for (const [fieldName, initialValue] of Object.entries(initialValues)) {
fields.push({
name: fieldName,
currentValue: initialValue,
initialValue,
changed: false,
});
}
return {
fields,
values: { ...initialValues },
hasUnsavedChanges: false,
};
};
const valueChanged = (fieldName: string, newValue: unknown): FormAction =>
(state: FormState): FormState => {
let { fields } = state;
const field = fields.find(({ name }) => name === fieldName);
if (!field || field.currentValue === newValue) {
return state;
}
const newField = {
...field,
currentValue: newValue,
changed: JSON.stringify(newValue) !== JSON.stringify(field.initialValue),
};
fields = state.fields.map((field) => {
if (field.name === fieldName) {
return newField;
}
return field;
});
return {
...state,
fields,
values: {
...state.values,
[newField.name]: newField.currentValue,
},
hasUnsavedChanges: newField.changed || fields.some((field) => field.changed),
};
};
const formCommitted = (): FormAction =>
(state: FormState): FormState => ({
...state,
fields: state.fields.map((field) => ({
...field,
initialValue: field.currentValue,
changed: false,
})),
hasUnsavedChanges: false,
});
const formReset = (): FormAction =>
(state: FormState): FormState => ({
...state,
fields: state.fields.map((field) => ({
...field,
currentValue: field.initialValue,
changed: false,
})),
values: state.fields.reduce((values, field) => ({
...values,
[field.name]: field.initialValue,
}), {}),
hasUnsavedChanges: false,
});
const isChangeEvent = (x: any): x is ChangeEvent =>
(typeof x === 'object' || typeof x === 'function') && typeof x?.currentTarget !== 'undefined';
const getValue = (eventOrValue: ChangeEvent | unknown): unknown => {
if (!isChangeEvent(eventOrValue)) {
return eventOrValue;
}
const target = eventOrValue.currentTarget;
if (target instanceof HTMLTextAreaElement) {
return target.value;
}
if (target instanceof HTMLSelectElement) {
return target.value;
}
if (!(target instanceof HTMLInputElement)) {
return undefined;
}
if (target.type === 'checkbox' || target.type === 'radio') {
return target.checked;
}
return target.value;
};
export const useForm = (
initialValues: Record<string, unknown>,
onChange: ((...args: unknown[]) => void) = (): void => undefined,
): UseFormReturnType => {
const [state, dispatch] = useReducer(reduceForm, initialValues, initForm);
const commit = useCallback(() => {
dispatch(formCommitted());
}, []);
const reset = useCallback(() => {
dispatch(formReset());
}, []);
const handlers = useMemo(() => state.fields.reduce((handlers, { name, initialValue }) => ({
...handlers,
[`handle${ capitalize(name) }`]: (eventOrValue: ChangeEvent | unknown): void => {
const newValue = getValue(eventOrValue);
dispatch(valueChanged(name, newValue));
onChange({
initialValue,
value: newValue,
key: name,
});
},
}), {}), [onChange, state.fields]);
return {
handlers,
values: state.values,
hasUnsavedChanges: state.hasUnsavedChanges,
commit,
reset,
};
};

@ -1,22 +0,0 @@
import { useEffect, useState } from 'react';
export const useObservableValue = (getValue) => {
const [value, setValue] = useState(() => getValue());
useEffect(() => {
let mounted = true;
const unsubscribe = getValue((newValue) => {
if (mounted) {
setValue(newValue);
}
});
return () => {
mounted = false;
typeof unsubscribe === 'function' && unsubscribe();
};
}, [getValue]);
return value;
};

@ -1,30 +0,0 @@
import { Tracker } from 'meteor/tracker';
import { useCallback, useMemo, useRef } from 'react';
import { useSubscription } from 'use-subscription';
import { Mongo } from 'meteor/mongo';
const allQuery = {};
export const useQuery = <T>(collection: Mongo.Collection<T>, query: object = allQuery, options?: object): T[] => {
const queryHandle = useMemo(() => collection.find(query, options), [collection, query, options]);
const resultRef = useRef<T[]>([]);
resultRef.current = Tracker.nonreactive(() => queryHandle.fetch()) as unknown as T[];
const subscribe = useCallback((cb) => {
const computation = Tracker.autorun(() => {
resultRef.current = queryHandle.fetch();
cb(resultRef.current);
});
return (): void => {
computation.stop();
};
}, [queryHandle]);
const subscription = useMemo(() => ({
getCurrentValue: (): T[] => resultRef.current ?? [],
subscribe,
}), [subscribe]);
return useSubscription(subscription);
};

@ -1,28 +0,0 @@
import { Tracker } from 'meteor/tracker';
import { useCallback } from 'react';
import { Subscription, Unsubscribe } from 'use-subscription';
interface ISubscriptionFactory<T> {
(...args: any[]): Subscription<T>;
}
export const useReactiveSubscriptionFactory = <T>(fn: (...args: any[]) => T): ISubscriptionFactory<T> =>
useCallback<ISubscriptionFactory<T>>((...args: any[]) => {
const fnWithArgs = (): T => fn(...args);
return {
getCurrentValue: (): T => Tracker.nonreactive(fnWithArgs) as unknown as T,
subscribe: (callback): Unsubscribe => {
const computation = Tracker.autorun((c) => {
fnWithArgs();
if (!c.firstRun) {
callback();
}
});
return (): void => {
computation.stop();
};
},
};
}, [fn]);

@ -1,19 +0,0 @@
import { useState, useEffect } from 'react';
import { Tracker } from 'meteor/tracker';
export const useReactiveValue = (getValue, deps = []) => {
const [value, setValue] = useState(() => Tracker.nonreactive(getValue));
useEffect(() => {
const computation = Tracker.autorun(() => {
const newValue = getValue();
setValue(() => newValue);
});
return () => {
computation.stop();
};
}, deps);
return value;
};

@ -0,0 +1,35 @@
import { Tracker } from 'meteor/tracker';
import { useMemo } from 'react';
import { Subscription, Unsubscribe, useSubscription } from 'use-subscription';
export const useReactiveValue = <T>(computeCurrentValue: () => T): T => {
const subscription: Subscription<T> = useMemo(() => {
const callbacks = new Set<Unsubscribe>();
let currentValue: T;
const computation = Tracker.autorun(() => {
currentValue = computeCurrentValue();
callbacks.forEach((callback) => {
callback();
});
});
return {
getCurrentValue: (): T => currentValue,
subscribe: (callback): Unsubscribe => {
callbacks.add(callback);
return (): void => {
callbacks.delete(callback);
if (callbacks.size === 0) {
computation.stop();
}
};
},
};
}, [computeCurrentValue]);
return useSubscription(subscription);
};

@ -1,22 +0,0 @@
import React from 'react';
import { Meteor } from 'meteor/meteor';
import {
hasPermission,
hasAtLeastOnePermission,
hasAllPermission,
} from '../../app/authorization/client/hasPermission';
import { AuthorizationContext } from '../contexts/AuthorizationContext';
import { hasRole } from '../../app/authorization/client';
import { createObservableFromReactive } from './createObservableFromReactive';
const contextValue = {
hasPermission: createObservableFromReactive(hasPermission),
hasAtLeastOnePermission: createObservableFromReactive(hasAtLeastOnePermission),
hasAllPermission: createObservableFromReactive(hasAllPermission),
hasRole: createObservableFromReactive((role) => hasRole(Meteor.userId(), role)),
};
export function AuthorizationProvider({ children }) {
return <AuthorizationContext.Provider children={children} value={contextValue} />;
}

@ -0,0 +1,31 @@
import React, { FC } from 'react';
import { Meteor } from 'meteor/meteor';
import {
hasPermission,
hasAtLeastOnePermission,
hasAllPermission,
hasRole,
} from '../../app/authorization/client';
import { AuthorizationContext } from '../contexts/AuthorizationContext';
import { createReactiveSubscriptionFactory } from './createReactiveSubscriptionFactory';
const contextValue = {
queryPermission: createReactiveSubscriptionFactory(
(permission, scope) => hasPermission(permission, scope),
),
queryAtLeastOnePermission: createReactiveSubscriptionFactory(
(permissions, scope) => hasAtLeastOnePermission(permissions, scope),
),
queryAllPermissions: createReactiveSubscriptionFactory(
(permissions, scope) => hasAllPermission(permissions, scope),
),
queryRole: createReactiveSubscriptionFactory(
(role) => hasRole(Meteor.userId(), role),
),
};
const AuthorizationProvider: FC = ({ children }) =>
<AuthorizationContext.Provider children={children} value={contextValue} />;
export default AuthorizationProvider;

@ -4,11 +4,13 @@ import React from 'react';
import { ConnectionStatusContext } from '../contexts/ConnectionStatusContext';
import { useReactiveValue } from '../hooks/useReactiveValue';
const getStatus = () => ({
...Meteor.status(),
reconnect: Meteor.reconnect,
});
export function ConnectionStatusProvider({ children }) {
const status = useReactiveValue(() => ({
...Meteor.status(),
reconnect: Meteor.reconnect,
}), []);
const status = useReactiveValue(getStatus);
return <ConnectionStatusContext.Provider children={children} value={status} />;
}

@ -1,12 +1,12 @@
import { useMutableCallback } from '@rocket.chat/fuselage-hooks';
import { Mongo } from 'meteor/mongo';
import { Tracker } from 'meteor/tracker';
import React, { useEffect, useMemo, FunctionComponent, useCallback, useRef, MutableRefObject } from 'react';
import React, { useEffect, useMemo, FunctionComponent, useRef, MutableRefObject } from 'react';
import { SettingId, GroupId } from '../../definition/ISetting';
import { EditableSettingsContext, IEditableSetting, EditableSettingsContextValue } from '../contexts/EditableSettingsContext';
import { useSettings, SettingsContextQuery } from '../contexts/SettingsContext';
import { useReactiveSubscriptionFactory } from '../hooks/useReactiveSubscriptionFactory';
import { createReactiveSubscriptionFactory } from './createReactiveSubscriptionFactory';
const defaultQuery: SettingsContextQuery = {};
@ -34,12 +34,12 @@ const EditableSettingsProvider: FunctionComponent<EditableSettingsProviderProps>
settingsCollection.remove({ _id: { $nin: persistedSettings.map(({ _id }) => _id) } });
for (const { _id, ...fields } of persistedSettings) {
settingsCollection.upsert(_id, { $set: { ...fields } });
settingsCollection.upsert(_id, { $set: { ...fields }, $unset: { changed: true } });
}
}, [getSettingsCollection, persistedSettings]);
const queryEditableSetting = useReactiveSubscriptionFactory(
useCallback(
const queryEditableSetting = useMemo(
() => createReactiveSubscriptionFactory(
(_id: SettingId): IEditableSetting | undefined => {
const settingsCollection = getSettingsCollection();
@ -65,12 +65,12 @@ const EditableSettingsProvider: FunctionComponent<EditableSettingsProviderProps>
disabled: !queries.every((query) => settingsCollection.find(query).count() > 0),
};
},
[getSettingsCollection],
),
[getSettingsCollection],
);
const queryEditableSettings = useReactiveSubscriptionFactory(
useCallback(
const queryEditableSettings = useMemo(
() => createReactiveSubscriptionFactory(
(query = {}) => getSettingsCollection().find({
...('_id' in query) && { _id: { $in: query._id } },
...('group' in query) && { group: query.group },
@ -92,12 +92,12 @@ const EditableSettingsProvider: FunctionComponent<EditableSettingsProviderProps>
i18nLabel: 1,
},
}).fetch(),
[getSettingsCollection],
),
[getSettingsCollection],
);
const queryGroupSections = useReactiveSubscriptionFactory(
useCallback(
const queryGroupSections = useMemo(
() => createReactiveSubscriptionFactory(
(_id: GroupId) => Array.from(new Set(
getSettingsCollection().find({
group: _id,
@ -112,8 +112,8 @@ const EditableSettingsProvider: FunctionComponent<EditableSettingsProviderProps>
},
}).fetch().map(({ section }) => section || ''),
)),
[getSettingsCollection],
),
[getSettingsCollection],
);
const dispatch = useMutableCallback((changes: Partial<IEditableSetting>[]): void => {

@ -1,15 +1,15 @@
import React from 'react';
import { AuthorizationProvider } from './AuthorizationProvider';
import AuthorizationProvider from './AuthorizationProvider';
import { ConnectionStatusProvider } from './ConnectionStatusProvider';
import { RouterProvider } from './RouterProvider';
import { SessionProvider } from './SessionProvider';
import SessionProvider from './SessionProvider';
import SettingsProvider from './SettingsProvider';
import { ServerProvider } from './ServerProvider';
import { SidebarProvider } from './SidebarProvider';
import { TranslationProvider } from './TranslationProvider';
import { ToastMessagesProvider } from './ToastMessagesProvider';
import { UserProvider } from './UserProvider';
import UserProvider from './UserProvider';
import { AvatarUrlProvider } from './AvatarUrlProvider';
import { CustomSoundProvider } from './CustomSoundProvides';
import ModalProvider from './ModalProvider';

@ -1,16 +0,0 @@
import React from 'react';
import { Session } from 'meteor/session';
import { SessionContext } from '../contexts/SessionContext';
import { createObservableFromReactive } from './createObservableFromReactive';
const contextValue = {
get: createObservableFromReactive((name) => Session.get(name)),
set: (name, value) => {
Session.set(name, value);
},
};
export function SessionProvider({ children }) {
return <SessionContext.Provider children={children} value={contextValue} />;
}

@ -0,0 +1,17 @@
import React, { FC } from 'react';
import { Session } from 'meteor/session';
import { SessionContext } from '../contexts/SessionContext';
import { createReactiveSubscriptionFactory } from './createReactiveSubscriptionFactory';
const contextValue = {
query: createReactiveSubscriptionFactory<unknown>((name) => Session.get(name)),
dispatch: (name: string, value: unknown): void => {
Session.set(name, value);
},
};
const SessionProvider: FC = ({ children }) =>
<SessionContext.Provider children={children} value={contextValue} />;
export default SessionProvider;

@ -3,10 +3,10 @@ import React, { useCallback, useEffect, useMemo, useState, FunctionComponent } f
import { useMethod } from '../contexts/ServerContext';
import { SettingsContext, SettingsContextValue } from '../contexts/SettingsContext';
import { useReactiveSubscriptionFactory } from '../hooks/useReactiveSubscriptionFactory';
import { PrivateSettingsCachedCollection } from '../lib/settings/PrivateSettingsCachedCollection';
import { PublicSettingsCachedCollection } from '../lib/settings/PublicSettingsCachedCollection';
import { useAtLeastOnePermission } from '../contexts/AuthorizationContext';
import { createReactiveSubscriptionFactory } from './createReactiveSubscriptionFactory';
type SettingsProviderProps = {
readonly privileged?: boolean;
@ -16,11 +16,13 @@ const SettingsProvider: FunctionComponent<SettingsProviderProps> = ({
children,
privileged = false,
}) => {
const hasPrivilegedPermission = useAtLeastOnePermission([
'view-privileged-setting',
'edit-privileged-setting',
'manage-selected-settings',
]);
const hasPrivilegedPermission = useAtLeastOnePermission(
useMemo(() => [
'view-privileged-setting',
'edit-privileged-setting',
'manage-selected-settings',
], []),
);
const hasPrivateAccess = privileged && hasPrivilegedPermission;
@ -54,15 +56,15 @@ const SettingsProvider: FunctionComponent<SettingsProviderProps> = ({
};
}, [cachedCollection]);
const querySetting = useReactiveSubscriptionFactory(
useCallback(
const querySetting = useMemo(
() => createReactiveSubscriptionFactory(
(_id) => ({ ...cachedCollection.collection.findOne(_id) }),
[cachedCollection],
),
[cachedCollection],
);
const querySettings = useReactiveSubscriptionFactory(
useCallback(
const querySettings = useMemo(
() => createReactiveSubscriptionFactory(
(query = {}) => cachedCollection.collection.find({
...('_id' in query) && { _id: { $in: query._id } },
...('group' in query) && { group: query.group },
@ -83,8 +85,8 @@ const SettingsProvider: FunctionComponent<SettingsProviderProps> = ({
i18nLabel: 1,
},
}).fetch(),
[cachedCollection],
),
[cachedCollection],
);
const saveSettings = useMethod('saveSettings');

@ -9,7 +9,7 @@ const getOpen = () => menu.isOpen();
const setOpen = (open) => (open ? menu.open() : menu.close());
export function SidebarProvider({ children }) {
const contextValue = [useReactiveValue(getOpen, []), setOpen];
const contextValue = [useReactiveValue(getOpen), setOpen];
return <SidebarContext.Provider children={children} value={contextValue} />;
}

@ -1,4 +1,4 @@
import React, { useMemo, useCallback } from 'react';
import React, { useMemo } from 'react';
import { TAPi18n, TAPi18next } from 'meteor/rocketchat:tap-i18n';
import { TranslationContext } from '../contexts/TranslationContext';
@ -33,22 +33,27 @@ const createTranslateFunction = (language) => {
return translate;
};
export function TranslationProvider({ children }) {
const languages = useReactiveValue(() => {
const result = Object.entries(TAPi18n.getLanguages())
.map(([key, language]) => ({ ...language, key: key.toLowerCase() }))
.sort((a, b) => a.key - b.key);
result.unshift({
name: 'Default',
en: 'Default',
key: '',
});
const getLanguages = () => {
const result = Object.entries(TAPi18n.getLanguages())
.map(([key, language]) => ({ ...language, key: key.toLowerCase() }))
.sort((a, b) => a.key - b.key);
result.unshift({
name: 'Default',
en: 'Default',
key: '',
});
return result;
};
const getLanguage = () => TAPi18n.getLanguage();
return result;
}, []);
const language = useReactiveValue(() => TAPi18n.getLanguage());
const loadLanguage = useCallback((language) => TAPi18n._loadLanguage(language), []);
const loadLanguage = (language) => TAPi18n._loadLanguage(language);
export function TranslationProvider({ children }) {
const languages = useReactiveValue(getLanguages);
const language = useReactiveValue(getLanguage);
const translate = useMemo(() => createTranslateFunction(language), [language]);
return <TranslationContext.Provider

@ -1,34 +0,0 @@
import { Meteor } from 'meteor/meteor';
import React, { useCallback } from 'react';
import { getUserPreference } from '../../app/utils/client';
import { UserContext } from '../contexts/UserContext';
import { useReactiveValue } from '../hooks/useReactiveValue';
import { createObservableFromReactive } from './createObservableFromReactive';
const getPreference = createObservableFromReactive((key, defaultValue) =>
getUserPreference(Meteor.userId(), key, defaultValue));
export function UserProvider({ children }) {
const userId = useReactiveValue(() => Meteor.userId(), []);
const user = useReactiveValue(() => Meteor.user(), []);
const loginWithPassword = useCallback((user, password) => new Promise((resolve, reject) => {
Meteor.loginWithPassword(user, password, (error, result) => {
if (error) {
reject(error);
return;
}
resolve(result);
});
}), []);
const contextValue = useReactiveValue(() => ({
userId,
user,
loginWithPassword,
getPreference,
}), [userId, user, loginWithPassword]);
return <UserContext.Provider children={children} value={contextValue} />;
}

@ -0,0 +1,41 @@
import { Meteor } from 'meteor/meteor';
import React, { useMemo, FC } from 'react';
import { getUserPreference } from '../../app/utils/client';
import { UserContext } from '../contexts/UserContext';
import { useReactiveValue } from '../hooks/useReactiveValue';
import { createReactiveSubscriptionFactory } from './createReactiveSubscriptionFactory';
const getUserId = (): string | null => Meteor.userId();
const getUser = (): Meteor.User | null => Meteor.user();
const loginWithPassword = (user: string | object, password: string): Promise<void> =>
new Promise((resolve, reject) => {
Meteor.loginWithPassword(user, password, (error: Error | Meteor.Error | Meteor.TypedError | undefined) => {
if (error) {
reject(error);
return;
}
resolve();
});
});
const UserProvider: FC = ({ children }) => {
const userId = useReactiveValue(getUserId);
const user = useReactiveValue(getUser);
const contextValue = useMemo(() => ({
userId,
user,
loginWithPassword,
queryPreference: createReactiveSubscriptionFactory(
(key, defaultValue) => getUserPreference(userId, key, defaultValue),
),
}), [userId, user]);
return <UserContext.Provider children={children} value={contextValue} />;
};
export default UserProvider;

@ -1,19 +0,0 @@
import { Tracker } from 'meteor/tracker';
export const createObservableFromReactive = (fn) => (...fnArgs) => {
const args = fnArgs.slice(0, -1);
const listener = fnArgs.pop();
if (!listener) {
return Tracker.nonreactive(() => fn(...args));
}
const computation = Tracker.autorun(() => {
const value = fn(...args);
listener(value);
});
return () => {
computation.stop();
};
};

@ -0,0 +1,39 @@
import { Tracker } from 'meteor/tracker';
import { Subscription, Unsubscribe } from 'use-subscription';
interface ISubscriptionFactory<T> {
(...args: any[]): Subscription<T>;
}
export const createReactiveSubscriptionFactory = <T>(
computeCurrentValueWith: (...args: any[]) => T,
): ISubscriptionFactory<T> =>
(...args: any[]): Subscription<T> => {
const computeCurrentValue = (): T => computeCurrentValueWith(...args);
const callbacks = new Set<Unsubscribe>();
let currentValue: T;
const computation = Tracker.autorun(() => {
currentValue = computeCurrentValue();
callbacks.forEach((callback) => {
callback();
});
});
return {
getCurrentValue: (): T => currentValue,
subscribe: (callback): Unsubscribe => {
callbacks.add(callback);
return (): void => {
callbacks.delete(callback);
if (callbacks.size === 0) {
computation.stop();
}
};
},
};
};
Loading…
Cancel
Save