Skip to content

Commit

Permalink
feat: Add logs to feature config changes (#15639)
Browse files Browse the repository at this point in the history
  • Loading branch information
atomrc authored Aug 23, 2023
1 parent 1529aa2 commit a2d0f92
Show file tree
Hide file tree
Showing 6 changed files with 392 additions and 181 deletions.
2 changes: 2 additions & 0 deletions src/script/page/AppMain.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import {showUserModal, UserModal} from 'Components/Modals/UserModal';
import {useKoSubscribableChildren} from 'Util/ComponentUtil';

import {AppLock} from './AppLock';
import {FeatureConfigChangeNotifier} from './FeatureConfigChangeNotifier';
import {LeftSidebar} from './LeftSidebar';
import {MainContent} from './MainContent';
import {PanelEntity, PanelState, RightSidebar} from './RightSidebar';
Expand Down Expand Up @@ -243,6 +244,7 @@ const AppMain: FC<AppMainProps> = ({

<AppLock clientRepository={repositories.client} />
<WarningsContainer onRefresh={app.refresh} />
<FeatureConfigChangeNotifier teamState={teamState} />

<CallingContainer
multitasking={mainView.multitasking}
Expand Down
170 changes: 170 additions & 0 deletions src/script/page/FeatureConfigChangeNotifier.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
/*
* Wire
* Copyright (C) 2023 Wire Swiss GmbH
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see http://www.gnu.org/licenses/.
*
*/

import {act, render, waitFor} from '@testing-library/react';
import {FeatureStatus, FEATURE_KEY} from '@wireapp/api-client/lib/team/feature';

import {PrimaryModal} from 'Components/Modals/PrimaryModal';

import {FeatureConfigChangeNotifier} from './FeatureConfigChangeNotifier';

import {TeamState} from '../team/TeamState';

describe('FeatureConfigChangeNotifier', () => {
const showModalSpy = jest.spyOn(PrimaryModal, 'show');

beforeEach(() => {
showModalSpy.mockClear();
});

const baseConfig = {
[FEATURE_KEY.FILE_SHARING]: {
status: FeatureStatus.DISABLED,
},
[FEATURE_KEY.VIDEO_CALLING]: {
status: FeatureStatus.DISABLED,
},
[FEATURE_KEY.SELF_DELETING_MESSAGES]: {
status: FeatureStatus.DISABLED,
config: {enforcedTimeoutSeconds: 0},
},
[FEATURE_KEY.CONFERENCE_CALLING]: {
status: FeatureStatus.DISABLED,
},
[FEATURE_KEY.CONVERSATION_GUEST_LINKS]: {
status: FeatureStatus.DISABLED,
},
};

it.each([
[
FEATURE_KEY.FILE_SHARING,
'featureConfigChangeModalFileSharingDescriptionItemFileSharingEnabled',
'featureConfigChangeModalFileSharingDescriptionItemFileSharingDisabled',
],
[
FEATURE_KEY.VIDEO_CALLING,
'featureConfigChangeModalAudioVideoDescriptionItemCameraEnabled',
'featureConfigChangeModalAudioVideoDescriptionItemCameraDisabled',
],
[FEATURE_KEY.CONFERENCE_CALLING, 'featureConfigChangeModalConferenceCallingEnabled', undefined],
[
FEATURE_KEY.CONVERSATION_GUEST_LINKS,
'featureConfigChangeModalConversationGuestLinksDescriptionItemConversationGuestLinksEnabled',
'featureConfigChangeModalConversationGuestLinksDescriptionItemConversationGuestLinksDisabled',
],
] as const)('shows a modal when feature %s is turned on and off', async (feature, enabledString, disabledString) => {
const teamState = new TeamState();
render(<FeatureConfigChangeNotifier teamState={teamState} />);
act(() => {
teamState.teamFeatures(baseConfig);
});

act(() => {
teamState.teamFeatures({
...baseConfig,
[feature]: {
status: FeatureStatus.ENABLED,
},
});
});

await waitFor(() => {
expect(showModalSpy).toHaveBeenCalledTimes(1);
expect(showModalSpy).toHaveBeenCalledWith(PrimaryModal.type.ACKNOWLEDGE, {
text: expect.objectContaining({
htmlMessage: enabledString,
}),
});
});

act(() => {
teamState.teamFeatures({
...baseConfig,
[feature]: {
status: FeatureStatus.DISABLED,
},
});
});

if (!disabledString) {
expect(showModalSpy).toHaveBeenCalledTimes(1);
} else {
await waitFor(() => {
expect(showModalSpy).toHaveBeenCalledTimes(2);
expect(showModalSpy).toHaveBeenCalledWith(PrimaryModal.type.ACKNOWLEDGE, {
text: expect.objectContaining({
htmlMessage: disabledString,
}),
});
});
}
});

it.each([
[
{status: FeatureStatus.DISABLED, config: {enforcedTimeoutSeconds: 0}},
{status: FeatureStatus.ENABLED, config: {enforcedTimeoutSeconds: 10}},
'featureConfigChangeModalSelfDeletingMessagesDescriptionItemEnforced',
],
[
{status: FeatureStatus.DISABLED, config: {enforcedTimeoutSeconds: 0}},
{status: FeatureStatus.ENABLED, config: {enforcedTimeoutSeconds: 0}},
'featureConfigChangeModalSelfDeletingMessagesDescriptionItemEnabled',
],
[
{status: FeatureStatus.ENABLED, config: {enforcedTimeoutSeconds: 0}},
{status: FeatureStatus.ENABLED, config: {enforcedTimeoutSeconds: 10}},
'featureConfigChangeModalSelfDeletingMessagesDescriptionItemEnforced',
],
[
{status: FeatureStatus.ENABLED, config: {enforcedTimeoutSeconds: 10}},
{status: FeatureStatus.DISABLED, config: {enforcedTimeoutSeconds: 0}},
'featureConfigChangeModalSelfDeletingMessagesDescriptionItemDisabled',
],
])(
'indicates the config change when self deleting messages have changed (%s) to (%s)',
async (fromStatus, toStatus, expectedText) => {
const teamState = new TeamState();
render(<FeatureConfigChangeNotifier teamState={teamState} />);
act(() => {
teamState.teamFeatures({
...baseConfig,
[FEATURE_KEY.SELF_DELETING_MESSAGES]: fromStatus,
});
});

act(() => {
teamState.teamFeatures({
...baseConfig,
[FEATURE_KEY.SELF_DELETING_MESSAGES]: toStatus,
});
});

await waitFor(() => {
expect(showModalSpy).toHaveBeenCalledTimes(1);
expect(showModalSpy).toHaveBeenCalledWith(PrimaryModal.type.ACKNOWLEDGE, {
text: expect.objectContaining({
htmlMessage: expectedText,
}),
});
});
},
);
});
174 changes: 174 additions & 0 deletions src/script/page/FeatureConfigChangeNotifier.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
/*
* Wire
* Copyright (C) 2023 Wire Swiss GmbH
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see http://www.gnu.org/licenses/.
*
*/

import {useEffect, useRef} from 'react';

import {
FeatureList,
FeatureWithoutConfig,
Feature,
FEATURE_KEY,
FeatureStatus,
SelfDeletingTimeout,
} from '@wireapp/api-client/lib/team/feature/';

import {PrimaryModal} from 'Components/Modals/PrimaryModal';
import {useKoSubscribableChildren} from 'Util/ComponentUtil';
import {StringIdentifer, t} from 'Util/LocalizerUtil';
import {getLogger} from 'Util/Logger';
import {formatDuration} from 'Util/TimeUtil';

import {Config} from '../Config';
import {TeamState} from '../team/TeamState';

const featureNotifications: Partial<
Record<
FEATURE_KEY,
(
oldConfig?: Feature<any> | FeatureWithoutConfig,
newConfig?: Feature<any> | FeatureWithoutConfig,
) => undefined | {htmlMessage: string; title: StringIdentifer}
>
> = {
[FEATURE_KEY.FILE_SHARING]: (oldConfig, newConfig) => {
const status = wasTurnedOnOrOff(oldConfig, newConfig);
if (!status) {
return undefined;
}
return {
htmlMessage:
status === FeatureStatus.ENABLED
? t('featureConfigChangeModalFileSharingDescriptionItemFileSharingEnabled')
: t('featureConfigChangeModalFileSharingDescriptionItemFileSharingDisabled'),
title: 'featureConfigChangeModalFileSharingHeadline',
};
},
[FEATURE_KEY.VIDEO_CALLING]: (oldConfig, newConfig) => {
const status = wasTurnedOnOrOff(oldConfig, newConfig);
if (!status) {
return undefined;
}
return {
htmlMessage:
status === FeatureStatus.ENABLED
? t('featureConfigChangeModalAudioVideoDescriptionItemCameraEnabled')
: t('featureConfigChangeModalAudioVideoDescriptionItemCameraDisabled'),
title: 'featureConfigChangeModalAudioVideoHeadline',
};
},
[FEATURE_KEY.SELF_DELETING_MESSAGES]: (oldConfig, newConfig) => {
if (!oldConfig || !('config' in oldConfig) || !newConfig || !('config' in newConfig)) {
return undefined;
}
const previousTimeout = oldConfig?.config?.enforcedTimeoutSeconds * 1000;
const newTimeout = (newConfig?.config?.enforcedTimeoutSeconds ?? 0) * 1000;
const previousStatus = oldConfig?.status;
const newStatus = newConfig?.status;

const hasTimeoutChanged = previousTimeout !== newTimeout;
const isEnforced = newTimeout > SelfDeletingTimeout.OFF;
const hasStatusChanged = previousStatus !== newStatus;
const hasFeatureChanged = hasStatusChanged || hasTimeoutChanged;
const isFeatureEnabled = newStatus === FeatureStatus.ENABLED;

if (!hasFeatureChanged) {
return undefined;
}
return {
htmlMessage: isFeatureEnabled
? isEnforced
? t('featureConfigChangeModalSelfDeletingMessagesDescriptionItemEnforced', {
timeout: formatDuration(newTimeout).text,
})
: t('featureConfigChangeModalSelfDeletingMessagesDescriptionItemEnabled')
: t('featureConfigChangeModalSelfDeletingMessagesDescriptionItemDisabled'),
title: 'featureConfigChangeModalSelfDeletingMessagesHeadline',
};
},
[FEATURE_KEY.CONFERENCE_CALLING]: (oldConfig, newConfig) => {
const status = wasTurnedOnOrOff(oldConfig, newConfig);
if (!status || status === FeatureStatus.DISABLED) {
return undefined;
}
return {
htmlMessage: t('featureConfigChangeModalConferenceCallingEnabled'),
title: 'featureConfigChangeModalConferenceCallingTitle',
};
},
[FEATURE_KEY.CONVERSATION_GUEST_LINKS]: (oldConfig, newConfig) => {
const status = wasTurnedOnOrOff(oldConfig, newConfig);
if (!status) {
return undefined;
}
return {
htmlMessage:
status === FeatureStatus.ENABLED
? t('featureConfigChangeModalConversationGuestLinksDescriptionItemConversationGuestLinksEnabled')
: t('featureConfigChangeModalConversationGuestLinksDescriptionItemConversationGuestLinksDisabled'),
title: 'featureConfigChangeModalConversationGuestLinksHeadline',
};
},
};

function wasTurnedOnOrOff(oldConfig?: FeatureWithoutConfig, newConfig?: FeatureWithoutConfig): boolean | FeatureStatus {
if (oldConfig?.status && newConfig?.status && oldConfig.status !== newConfig.status) {
return newConfig.status;
}
return false;
}

const logger = getLogger('FeatureConfigChangeNotifier');
type Props = {
teamState: TeamState;
};

export function FeatureConfigChangeNotifier({teamState}: Props): null {
const {teamFeatures: config} = useKoSubscribableChildren(teamState, ['teamFeatures']);
const previousConfig = useRef<FeatureList>();

useEffect(() => {
const previous = previousConfig.current;
previousConfig.current = config;

if (previous && config) {
Object.entries(featureNotifications).forEach(([feature, getMessage]) => {
const featureKey = feature as FEATURE_KEY;
const message = getMessage(previous?.[featureKey], config[featureKey]);
if (!message) {
return;
}
logger.info(
`Detected feature config change for "${feature}" from "${JSON.stringify(
previous?.[featureKey],
)}" to "${JSON.stringify(config[featureKey])}"`,
);
PrimaryModal.show(PrimaryModal.type.ACKNOWLEDGE, {
text: {
htmlMessage: message.htmlMessage,
title: t(message.title, {
brandName: Config.getConfig().BRAND_NAME,
}),
},
});
});
}
}, [config]);

return null;
}
Loading

0 comments on commit a2d0f92

Please sign in to comment.