Add test notification tool (#8271)
* Add test notification menu * Reduce the minimum version for testing purposes * Address feedback * Add missing strings * Address feedback * Fix snapshots * Bump version limit and use correct link * Fix tests * Fix URL issues
This commit is contained in:
parent
83189afa88
commit
e0992c0bf6
22 changed files with 1935 additions and 8 deletions
|
|
@ -14,6 +14,7 @@ import {
|
|||
fetchNotificationData,
|
||||
backgroundNotification,
|
||||
openNotification,
|
||||
sendTestNotification,
|
||||
} from './notifications';
|
||||
|
||||
import type ServerDataOperator from '@database/operator/server_data_operator';
|
||||
|
|
@ -83,12 +84,18 @@ const mockClient = {
|
|||
getTeamMember: jest.fn((id: string, userId: string) => ({id: userId + '-' + id, user_id: userId === 'me' ? user1.id : userId, team_id: id, roles: ''})),
|
||||
getChannel: jest.fn((_channelId: string) => ({...channel, id: _channelId})),
|
||||
getChannelMember: jest.fn((_channelId: string, userId: string) => ({id: userId + '-' + _channelId, user_id: userId === 'me' ? user1.id : userId, channel_id: _channelId, roles: ''})),
|
||||
sendTestNotification: jest.fn(),
|
||||
};
|
||||
|
||||
beforeAll(() => {
|
||||
// eslint-disable-next-line
|
||||
// @ts-ignore
|
||||
NetworkManager.getClient = () => mockClient;
|
||||
NetworkManager.getClient = (url) => {
|
||||
if (serverUrl === url) {
|
||||
return mockClient;
|
||||
}
|
||||
throw new Error('invalid url');
|
||||
};
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
|
|
@ -187,3 +194,35 @@ describe('notifications', () => {
|
|||
expect(result.error).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('sendTestNotification', () => {
|
||||
it('calls client function and returns correctly', async () => {
|
||||
mockClient.sendTestNotification.mockResolvedValueOnce({status: 'OK'});
|
||||
const result = await sendTestNotification(serverUrl);
|
||||
expect(result.status).toBe('OK');
|
||||
expect(result.error).toBeUndefined();
|
||||
expect(mockClient.sendTestNotification).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('calls client function and returns correctly when error value', async () => {
|
||||
mockClient.sendTestNotification.mockRejectedValueOnce(new Error('some error'));
|
||||
const result = await sendTestNotification(serverUrl);
|
||||
expect(result.error).toBeTruthy();
|
||||
expect(mockClient.sendTestNotification).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('calls client function and returns error on throw', async () => {
|
||||
mockClient.sendTestNotification.mockImplementationOnce(() => {
|
||||
throw new Error('error');
|
||||
});
|
||||
const result = await sendTestNotification(serverUrl);
|
||||
expect(result.error).toBeTruthy();
|
||||
expect(mockClient.sendTestNotification).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('show error when wrong server url is used', async () => {
|
||||
const result = await sendTestNotification('bad server url');
|
||||
expect(result.error).toBeTruthy();
|
||||
expect(mockClient.sendTestNotification).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import {fetchMyTeam} from '@actions/remote/team';
|
|||
import {fetchAndSwitchToThread} from '@actions/remote/thread';
|
||||
import {ActionType} from '@constants';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import NetworkManager from '@managers/network_manager';
|
||||
import {getMyChannel, getChannelById} from '@queries/servers/channel';
|
||||
import {getCurrentTeamId} from '@queries/servers/system';
|
||||
import {getMyTeamById, prepareMyTeams} from '@queries/servers/team';
|
||||
|
|
@ -238,3 +239,14 @@ export const openNotification = async (serverUrl: string, notification: Notifica
|
|||
return {error};
|
||||
}
|
||||
};
|
||||
|
||||
export const sendTestNotification = async (serverUrl: string): Promise<{status?: 'OK'; error?: unknown}> => {
|
||||
try {
|
||||
const client = NetworkManager.getClient(serverUrl);
|
||||
const result = await client.sendTestNotification();
|
||||
return result;
|
||||
} catch (error) {
|
||||
forceLogoutIfNecessary(serverUrl, error);
|
||||
return {error};
|
||||
}
|
||||
};
|
||||
|
|
|
|||
18
app/client/rest/posts.test.ts
Normal file
18
app/client/rest/posts.test.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Client} from '.';
|
||||
|
||||
import type {APIClientInterface} from '@mattermost/react-native-network-client';
|
||||
|
||||
const mockAPIClient = {
|
||||
post: jest.fn(),
|
||||
} as any as APIClientInterface;
|
||||
|
||||
describe('sendTestNotification', () => {
|
||||
it('fetch the correct url with the correct method', () => {
|
||||
const client = new Client(mockAPIClient, 'serverUrl');
|
||||
client.sendTestNotification();
|
||||
expect(mockAPIClient.post).toHaveBeenCalledWith('/api/v4/notifications/test', expect.anything());
|
||||
});
|
||||
});
|
||||
|
|
@ -33,6 +33,7 @@ export interface ClientPostsMix {
|
|||
doPostActionWithCookie: (postId: string, actionId: string, actionCookie: string, selectedOption?: string) => Promise<any>;
|
||||
acknowledgePost: (postId: string, userId: string) => Promise<PostAcknowledgement>;
|
||||
unacknowledgePost: (postId: string, userId: string) => Promise<any>;
|
||||
sendTestNotification: () => Promise<{status: 'OK'}>;
|
||||
}
|
||||
|
||||
const ClientPosts = <TBase extends Constructor<ClientBase>>(superclass: TBase) => class extends superclass {
|
||||
|
|
@ -220,6 +221,13 @@ const ClientPosts = <TBase extends Constructor<ClientBase>>(superclass: TBase) =
|
|||
{method: 'delete'},
|
||||
);
|
||||
};
|
||||
|
||||
sendTestNotification = async () => {
|
||||
return this.doFetch(
|
||||
`${this.urlVersion}/notifications/test`,
|
||||
{method: 'post'},
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
export default ClientPosts;
|
||||
|
|
|
|||
520
app/components/section_notice/__snapshots__/index.test.tsx.snap
Normal file
520
app/components/section_notice/__snapshots__/index.test.tsx.snap
Normal file
|
|
@ -0,0 +1,520 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Section notice match snapshot 1`] = `
|
||||
<View
|
||||
style={
|
||||
[
|
||||
{
|
||||
"borderRadius": 4,
|
||||
"borderStyle": "solid",
|
||||
"borderWidth": 1,
|
||||
},
|
||||
{
|
||||
"backgroundColor": "rgba(93,137,234,0.08)",
|
||||
"borderColor": "rgba(93,137,234,0.16)",
|
||||
},
|
||||
]
|
||||
}
|
||||
testID="sectionNoticeContainer"
|
||||
>
|
||||
<View
|
||||
style={
|
||||
{
|
||||
"alignItems": "flex-start",
|
||||
"flexDirection": "row",
|
||||
"gap": 12,
|
||||
"padding": 16,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Icon
|
||||
name="information-outline"
|
||||
size={20}
|
||||
style={
|
||||
{
|
||||
"color": "#5d89ea",
|
||||
}
|
||||
}
|
||||
testID="sectionNoticeHeaderIcon"
|
||||
/>
|
||||
<View
|
||||
style={
|
||||
{
|
||||
"flex": 1,
|
||||
"flexDirection": "column",
|
||||
"gap": 8,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Text
|
||||
style={
|
||||
{
|
||||
"color": "#3f4350",
|
||||
"fontFamily": "OpenSans-SemiBold",
|
||||
"fontSize": 16,
|
||||
"fontWeight": "600",
|
||||
"lineHeight": 24,
|
||||
"margin": 0,
|
||||
}
|
||||
}
|
||||
>
|
||||
Some title
|
||||
</Text>
|
||||
<View
|
||||
style={
|
||||
[
|
||||
{
|
||||
"alignItems": "flex-start",
|
||||
"flexDirection": "row",
|
||||
"flexWrap": "wrap",
|
||||
},
|
||||
]
|
||||
}
|
||||
testID="markdown_paragraph"
|
||||
>
|
||||
<Text>
|
||||
<Text
|
||||
selectable={false}
|
||||
style={
|
||||
{
|
||||
"color": "#3f4350",
|
||||
"fontFamily": "OpenSans",
|
||||
"fontSize": 16,
|
||||
"fontWeight": "400",
|
||||
"lineHeight": 24,
|
||||
}
|
||||
}
|
||||
testID="markdown_text"
|
||||
>
|
||||
Some text
|
||||
</Text>
|
||||
</Text>
|
||||
</View>
|
||||
<View
|
||||
style={
|
||||
{
|
||||
"gap": 8,
|
||||
"marginTop": 8,
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
style={
|
||||
[
|
||||
{
|
||||
"overflow": "hidden",
|
||||
},
|
||||
{
|
||||
"borderRadius": 2,
|
||||
},
|
||||
undefined,
|
||||
false,
|
||||
]
|
||||
}
|
||||
testID="RNE_BUTTON_WRAPPER"
|
||||
>
|
||||
<View
|
||||
accessibilityRole="button"
|
||||
accessibilityState={
|
||||
{
|
||||
"busy": false,
|
||||
"checked": undefined,
|
||||
"disabled": false,
|
||||
"expanded": undefined,
|
||||
"selected": undefined,
|
||||
}
|
||||
}
|
||||
accessibilityValue={
|
||||
{
|
||||
"max": undefined,
|
||||
"min": undefined,
|
||||
"now": undefined,
|
||||
"text": undefined,
|
||||
}
|
||||
}
|
||||
accessible={true}
|
||||
collapsable={false}
|
||||
focusable={true}
|
||||
onClick={[Function]}
|
||||
onResponderGrant={[Function]}
|
||||
onResponderMove={[Function]}
|
||||
onResponderRelease={[Function]}
|
||||
onResponderTerminate={[Function]}
|
||||
onResponderTerminationRequest={[Function]}
|
||||
onStartShouldSetResponder={[Function]}
|
||||
style={
|
||||
{
|
||||
"opacity": 1,
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
style={
|
||||
{
|
||||
"alignItems": "center",
|
||||
"backgroundColor": "#1c58d9",
|
||||
"borderColor": "#2089dc",
|
||||
"borderRadius": 4,
|
||||
"borderWidth": 0,
|
||||
"flex": 0,
|
||||
"flexDirection": "row",
|
||||
"height": 40,
|
||||
"justifyContent": "center",
|
||||
"padding": 8,
|
||||
"paddingHorizontal": 20,
|
||||
"paddingVertical": 12,
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
style={
|
||||
[
|
||||
{
|
||||
"flexDirection": "row",
|
||||
},
|
||||
{
|
||||
"minHeight": 18,
|
||||
},
|
||||
]
|
||||
}
|
||||
>
|
||||
<Icon
|
||||
color="#ffffff"
|
||||
name="chevron-right"
|
||||
size={18}
|
||||
style={
|
||||
{
|
||||
"marginRight": 7,
|
||||
}
|
||||
}
|
||||
/>
|
||||
<Text
|
||||
numberOfLines={1}
|
||||
style={
|
||||
[
|
||||
[
|
||||
{
|
||||
"alignItems": "center",
|
||||
"fontFamily": "OpenSans-SemiBold",
|
||||
"fontWeight": "600",
|
||||
"justifyContent": "center",
|
||||
"padding": 1,
|
||||
"textAlignVertical": "center",
|
||||
},
|
||||
{
|
||||
"fontSize": 14,
|
||||
"lineHeight": 14,
|
||||
"marginTop": 3,
|
||||
},
|
||||
{
|
||||
"color": "#ffffff",
|
||||
},
|
||||
],
|
||||
undefined,
|
||||
]
|
||||
}
|
||||
>
|
||||
primary button
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
<View
|
||||
style={
|
||||
[
|
||||
{
|
||||
"overflow": "hidden",
|
||||
},
|
||||
{
|
||||
"borderRadius": 2,
|
||||
},
|
||||
undefined,
|
||||
false,
|
||||
]
|
||||
}
|
||||
testID="RNE_BUTTON_WRAPPER"
|
||||
>
|
||||
<View
|
||||
accessibilityRole="button"
|
||||
accessibilityState={
|
||||
{
|
||||
"busy": false,
|
||||
"checked": undefined,
|
||||
"disabled": false,
|
||||
"expanded": undefined,
|
||||
"selected": undefined,
|
||||
}
|
||||
}
|
||||
accessibilityValue={
|
||||
{
|
||||
"max": undefined,
|
||||
"min": undefined,
|
||||
"now": undefined,
|
||||
"text": undefined,
|
||||
}
|
||||
}
|
||||
accessible={true}
|
||||
collapsable={false}
|
||||
focusable={true}
|
||||
onClick={[Function]}
|
||||
onResponderGrant={[Function]}
|
||||
onResponderMove={[Function]}
|
||||
onResponderRelease={[Function]}
|
||||
onResponderTerminate={[Function]}
|
||||
onResponderTerminationRequest={[Function]}
|
||||
onStartShouldSetResponder={[Function]}
|
||||
style={
|
||||
{
|
||||
"opacity": 1,
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
style={
|
||||
{
|
||||
"alignItems": "center",
|
||||
"backgroundColor": "rgba(28,88,217,0.08)",
|
||||
"borderColor": "#2089dc",
|
||||
"borderRadius": 4,
|
||||
"borderWidth": 0,
|
||||
"flex": 0,
|
||||
"flexDirection": "row",
|
||||
"height": 40,
|
||||
"justifyContent": "center",
|
||||
"padding": 8,
|
||||
"paddingHorizontal": 20,
|
||||
"paddingVertical": 12,
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
style={
|
||||
[
|
||||
{
|
||||
"flexDirection": "row",
|
||||
},
|
||||
{
|
||||
"minHeight": 18,
|
||||
},
|
||||
]
|
||||
}
|
||||
>
|
||||
<Icon
|
||||
color="#1c58d9"
|
||||
name="chevron-right"
|
||||
size={18}
|
||||
style={
|
||||
{
|
||||
"marginRight": 7,
|
||||
}
|
||||
}
|
||||
/>
|
||||
<Text
|
||||
numberOfLines={1}
|
||||
style={
|
||||
[
|
||||
[
|
||||
{
|
||||
"alignItems": "center",
|
||||
"fontFamily": "OpenSans-SemiBold",
|
||||
"fontWeight": "600",
|
||||
"justifyContent": "center",
|
||||
"padding": 1,
|
||||
"textAlignVertical": "center",
|
||||
},
|
||||
{
|
||||
"fontSize": 14,
|
||||
"lineHeight": 14,
|
||||
"marginTop": 3,
|
||||
},
|
||||
{
|
||||
"color": "#1c58d9",
|
||||
},
|
||||
],
|
||||
undefined,
|
||||
]
|
||||
}
|
||||
>
|
||||
secondary button
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
<View
|
||||
style={
|
||||
[
|
||||
{
|
||||
"overflow": "hidden",
|
||||
},
|
||||
{
|
||||
"borderRadius": 2,
|
||||
},
|
||||
undefined,
|
||||
false,
|
||||
]
|
||||
}
|
||||
testID="RNE_BUTTON_WRAPPER"
|
||||
>
|
||||
<View
|
||||
accessibilityRole="button"
|
||||
accessibilityState={
|
||||
{
|
||||
"busy": false,
|
||||
"checked": undefined,
|
||||
"disabled": false,
|
||||
"expanded": undefined,
|
||||
"selected": undefined,
|
||||
}
|
||||
}
|
||||
accessibilityValue={
|
||||
{
|
||||
"max": undefined,
|
||||
"min": undefined,
|
||||
"now": undefined,
|
||||
"text": undefined,
|
||||
}
|
||||
}
|
||||
accessible={true}
|
||||
collapsable={false}
|
||||
focusable={true}
|
||||
onClick={[Function]}
|
||||
onResponderGrant={[Function]}
|
||||
onResponderMove={[Function]}
|
||||
onResponderRelease={[Function]}
|
||||
onResponderTerminate={[Function]}
|
||||
onResponderTerminationRequest={[Function]}
|
||||
onStartShouldSetResponder={[Function]}
|
||||
style={
|
||||
{
|
||||
"opacity": 1,
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
style={
|
||||
{
|
||||
"alignItems": "center",
|
||||
"backgroundColor": "transparent",
|
||||
"borderColor": "#2089dc",
|
||||
"borderRadius": 4,
|
||||
"borderWidth": 0,
|
||||
"flex": 0,
|
||||
"flexDirection": "row",
|
||||
"height": 40,
|
||||
"justifyContent": "center",
|
||||
"padding": 8,
|
||||
"paddingHorizontal": 20,
|
||||
"paddingVertical": 12,
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
style={
|
||||
[
|
||||
{
|
||||
"flexDirection": "row",
|
||||
},
|
||||
{
|
||||
"minHeight": 18,
|
||||
},
|
||||
]
|
||||
}
|
||||
>
|
||||
<Icon
|
||||
color="#1c58d9"
|
||||
name="chevron-right"
|
||||
size={18}
|
||||
style={
|
||||
{
|
||||
"marginRight": 7,
|
||||
}
|
||||
}
|
||||
/>
|
||||
<Text
|
||||
numberOfLines={1}
|
||||
style={
|
||||
[
|
||||
[
|
||||
{
|
||||
"alignItems": "center",
|
||||
"fontFamily": "OpenSans-SemiBold",
|
||||
"fontWeight": "600",
|
||||
"justifyContent": "center",
|
||||
"padding": 1,
|
||||
"textAlignVertical": "center",
|
||||
},
|
||||
{
|
||||
"fontSize": 14,
|
||||
"lineHeight": 14,
|
||||
"marginTop": 3,
|
||||
},
|
||||
{
|
||||
"color": "#1c58d9",
|
||||
},
|
||||
],
|
||||
undefined,
|
||||
]
|
||||
}
|
||||
>
|
||||
link button
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
<View
|
||||
accessibilityState={
|
||||
{
|
||||
"busy": undefined,
|
||||
"checked": undefined,
|
||||
"disabled": undefined,
|
||||
"expanded": undefined,
|
||||
"selected": undefined,
|
||||
}
|
||||
}
|
||||
accessibilityValue={
|
||||
{
|
||||
"max": undefined,
|
||||
"min": undefined,
|
||||
"now": undefined,
|
||||
"text": undefined,
|
||||
}
|
||||
}
|
||||
accessible={true}
|
||||
collapsable={false}
|
||||
focusable={true}
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
onResponderGrant={[Function]}
|
||||
onResponderMove={[Function]}
|
||||
onResponderRelease={[Function]}
|
||||
onResponderTerminate={[Function]}
|
||||
onResponderTerminationRequest={[Function]}
|
||||
onStartShouldSetResponder={[Function]}
|
||||
role="button"
|
||||
style={
|
||||
{
|
||||
"alignItems": "center",
|
||||
"height": 32,
|
||||
"justifyContent": "center",
|
||||
"position": "absolute",
|
||||
"right": 10,
|
||||
"top": 10,
|
||||
"width": 32,
|
||||
}
|
||||
}
|
||||
testID="sectionNoticeDismissButton"
|
||||
>
|
||||
<Icon
|
||||
name="close"
|
||||
size={18}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
`;
|
||||
211
app/components/section_notice/index.test.tsx
Normal file
211
app/components/section_notice/index.test.tsx
Normal file
|
|
@ -0,0 +1,211 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {type ComponentProps} from 'react';
|
||||
|
||||
import {fireEvent, renderWithEverything} from '@test/intl-test-helper';
|
||||
import TestHelper from '@test/test_helper';
|
||||
|
||||
import SectionNotice from '.';
|
||||
|
||||
import type Database from '@nozbe/watermelondb/Database';
|
||||
|
||||
function getBaseProps(): ComponentProps<typeof SectionNotice> {
|
||||
return {
|
||||
title: 'Some title',
|
||||
text: 'Some text',
|
||||
type: 'info',
|
||||
isDismissable: true,
|
||||
primaryButton: {
|
||||
onClick: jest.fn(),
|
||||
text: 'primary button',
|
||||
leadingIcon: 'chevron-left',
|
||||
trailingIcon: 'chevron-right',
|
||||
loading: true,
|
||||
},
|
||||
secondaryButton: {
|
||||
onClick: jest.fn(),
|
||||
text: 'secondary button',
|
||||
leadingIcon: 'chevron-left',
|
||||
trailingIcon: 'chevron-right',
|
||||
loading: true,
|
||||
},
|
||||
linkButton: {
|
||||
onClick: jest.fn(),
|
||||
text: 'link button',
|
||||
leadingIcon: 'chevron-left',
|
||||
trailingIcon: 'chevron-right',
|
||||
loading: true,
|
||||
},
|
||||
onDismissClick: jest.fn(),
|
||||
};
|
||||
}
|
||||
|
||||
describe('Section notice', () => {
|
||||
let database: Database;
|
||||
|
||||
beforeAll(async () => {
|
||||
const server = await TestHelper.setupServerDatabase();
|
||||
database = server.database;
|
||||
});
|
||||
|
||||
it('match snapshot', () => {
|
||||
const props = getBaseProps();
|
||||
const wrapper = renderWithEverything(<SectionNotice {...props}/>, {database});
|
||||
expect(wrapper.toJSON()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should show buttons only if defined', () => {
|
||||
const props = getBaseProps();
|
||||
const wrapper = renderWithEverything(<SectionNotice {...props}/>, {database});
|
||||
|
||||
const primaryText = props.primaryButton!.text;
|
||||
const secondaryText = props.secondaryButton!.text;
|
||||
const linkText = props.linkButton!.text;
|
||||
|
||||
expect(wrapper.getAllByRole('button')).toHaveLength(4);
|
||||
expect(wrapper.getByText(primaryText)).toBeVisible();
|
||||
expect(wrapper.getByText(secondaryText)).toBeVisible();
|
||||
expect(wrapper.getByText(linkText)).toBeVisible();
|
||||
expect(wrapper.getByTestId('sectionNoticeDismissButton')).toBeVisible();
|
||||
|
||||
props.primaryButton = undefined;
|
||||
wrapper.rerender(<SectionNotice {...props}/>);
|
||||
expect(wrapper.getAllByRole('button')).toHaveLength(3);
|
||||
expect(wrapper.queryByText(primaryText)).not.toBeVisible();
|
||||
expect(wrapper.getByText(secondaryText)).toBeVisible();
|
||||
expect(wrapper.getByText(linkText)).toBeVisible();
|
||||
expect(wrapper.getByTestId('sectionNoticeDismissButton')).toBeVisible();
|
||||
|
||||
props.secondaryButton = undefined;
|
||||
wrapper.rerender(<SectionNotice {...props}/>);
|
||||
expect(wrapper.getAllByRole('button')).toHaveLength(2);
|
||||
expect(wrapper.queryByText(primaryText)).not.toBeVisible();
|
||||
expect(wrapper.queryByText(secondaryText)).not.toBeVisible();
|
||||
expect(wrapper.getByText(linkText)).toBeVisible();
|
||||
expect(wrapper.getByTestId('sectionNoticeDismissButton')).toBeVisible();
|
||||
|
||||
props.linkButton = undefined;
|
||||
wrapper.rerender(<SectionNotice {...props}/>);
|
||||
expect(wrapper.getAllByRole('button')).toHaveLength(1);
|
||||
expect(wrapper.queryByText(primaryText)).not.toBeVisible();
|
||||
expect(wrapper.queryByText(secondaryText)).not.toBeVisible();
|
||||
expect(wrapper.queryByText(linkText)).not.toBeVisible();
|
||||
expect(wrapper.getByTestId('sectionNoticeDismissButton')).toBeVisible();
|
||||
|
||||
props.isDismissable = false;
|
||||
wrapper.rerender(<SectionNotice {...props}/>);
|
||||
expect(wrapper.queryAllByRole('button')).toHaveLength(0);
|
||||
expect(wrapper.queryByText(primaryText)).not.toBeVisible();
|
||||
expect(wrapper.queryByText(secondaryText)).not.toBeVisible();
|
||||
expect(wrapper.queryByText(linkText)).not.toBeVisible();
|
||||
expect(wrapper.queryByTestId('sectionNoticeDismissButton')).not.toBeVisible();
|
||||
});
|
||||
|
||||
it('should show the correct icon on each section type', () => {
|
||||
const props = getBaseProps();
|
||||
|
||||
props.type = 'info';
|
||||
const wrapper = renderWithEverything(<SectionNotice {...props}/>, {database});
|
||||
let icon = wrapper.getByTestId('sectionNoticeHeaderIcon');
|
||||
expect(icon).toBeVisible();
|
||||
expect(icon.props).toHaveProperty('name', 'information-outline');
|
||||
|
||||
props.type = 'danger';
|
||||
wrapper.rerender(<SectionNotice {...props}/>);
|
||||
icon = wrapper.getByTestId('sectionNoticeHeaderIcon');
|
||||
expect(icon).toBeVisible();
|
||||
expect(icon.props).toHaveProperty('name', 'alert-outline');
|
||||
|
||||
props.type = 'hint';
|
||||
wrapper.rerender(<SectionNotice {...props}/>);
|
||||
icon = wrapper.getByTestId('sectionNoticeHeaderIcon');
|
||||
expect(icon).toBeVisible();
|
||||
expect(icon.props).toHaveProperty('name', 'lightbulb-outline');
|
||||
|
||||
props.type = 'success';
|
||||
wrapper.rerender(<SectionNotice {...props}/>);
|
||||
icon = wrapper.getByTestId('sectionNoticeHeaderIcon');
|
||||
expect(icon).toBeVisible();
|
||||
expect(icon.props).toHaveProperty('name', 'check');
|
||||
|
||||
props.type = 'warning';
|
||||
wrapper.rerender(<SectionNotice {...props}/>);
|
||||
icon = wrapper.getByTestId('sectionNoticeHeaderIcon');
|
||||
expect(icon).toBeVisible();
|
||||
expect(icon.props).toHaveProperty('name', 'alert-outline');
|
||||
|
||||
props.type = 'welcome';
|
||||
wrapper.rerender(<SectionNotice {...props}/>);
|
||||
expect(wrapper.queryByTestId('sectionNoticeHeaderIcon')).not.toBeVisible();
|
||||
});
|
||||
|
||||
it('should have the correct background on each section type', () => {
|
||||
const props = getBaseProps();
|
||||
|
||||
props.type = 'info';
|
||||
const wrapper = renderWithEverything(<SectionNotice {...props}/>, {database});
|
||||
let container = wrapper.getByTestId('sectionNoticeContainer');
|
||||
expect(container).toHaveStyle({backgroundColor: 'rgba(93,137,234,0.08)'});
|
||||
|
||||
props.type = 'danger';
|
||||
wrapper.rerender(<SectionNotice {...props}/>);
|
||||
container = wrapper.getByTestId('sectionNoticeContainer');
|
||||
expect(container).toHaveStyle({backgroundColor: 'rgba(210,75,78,0.08)'});
|
||||
|
||||
props.type = 'hint';
|
||||
wrapper.rerender(<SectionNotice {...props}/>);
|
||||
container = wrapper.getByTestId('sectionNoticeContainer');
|
||||
expect(container).toHaveStyle({backgroundColor: 'rgba(93,137,234,0.08)'});
|
||||
|
||||
props.type = 'success';
|
||||
wrapper.rerender(<SectionNotice {...props}/>);
|
||||
container = wrapper.getByTestId('sectionNoticeContainer');
|
||||
expect(container).toHaveStyle({backgroundColor: 'rgba(61,184,135,0.08)'});
|
||||
|
||||
props.type = 'warning';
|
||||
wrapper.rerender(<SectionNotice {...props}/>);
|
||||
container = wrapper.getByTestId('sectionNoticeContainer');
|
||||
expect(container).toHaveStyle({backgroundColor: 'rgba(255,188,31,0.08)'});
|
||||
|
||||
props.type = 'welcome';
|
||||
wrapper.rerender(<SectionNotice {...props}/>);
|
||||
container = wrapper.getByTestId('sectionNoticeContainer');
|
||||
expect(container).toHaveStyle({backgroundColor: 'rgba(63,67,80,0.04)'});
|
||||
});
|
||||
|
||||
it('all buttons perform the expected action', () => {
|
||||
const props = getBaseProps();
|
||||
const wrapper = renderWithEverything(<SectionNotice {...props}/>, {database});
|
||||
|
||||
fireEvent.press(wrapper.getByText(props.primaryButton!.text));
|
||||
expect(props.primaryButton!.onClick).toHaveBeenCalled();
|
||||
expect(props.secondaryButton!.onClick).not.toHaveBeenCalled();
|
||||
expect(props.linkButton!.onClick).not.toHaveBeenCalled();
|
||||
expect(props.onDismissClick!).not.toHaveBeenCalled();
|
||||
|
||||
jest.clearAllMocks();
|
||||
|
||||
fireEvent.press(wrapper.getByText(props.secondaryButton!.text));
|
||||
expect(props.primaryButton!.onClick).not.toHaveBeenCalled();
|
||||
expect(props.secondaryButton!.onClick).toHaveBeenCalled();
|
||||
expect(props.linkButton!.onClick).not.toHaveBeenCalled();
|
||||
expect(props.onDismissClick!).not.toHaveBeenCalled();
|
||||
|
||||
jest.clearAllMocks();
|
||||
|
||||
fireEvent.press(wrapper.getByText(props.linkButton!.text));
|
||||
expect(props.primaryButton!.onClick).not.toHaveBeenCalled();
|
||||
expect(props.secondaryButton!.onClick).not.toHaveBeenCalled();
|
||||
expect(props.linkButton!.onClick).toHaveBeenCalled();
|
||||
expect(props.onDismissClick!).not.toHaveBeenCalled();
|
||||
|
||||
jest.clearAllMocks();
|
||||
|
||||
fireEvent.press(wrapper.getByTestId('sectionNoticeDismissButton'));
|
||||
expect(props.primaryButton!.onClick).not.toHaveBeenCalled();
|
||||
expect(props.secondaryButton!.onClick).not.toHaveBeenCalled();
|
||||
expect(props.linkButton!.onClick).not.toHaveBeenCalled();
|
||||
expect(props.onDismissClick!).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
228
app/components/section_notice/index.tsx
Normal file
228
app/components/section_notice/index.tsx
Normal file
|
|
@ -0,0 +1,228 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useMemo} from 'react';
|
||||
import {Pressable, Text, View} from 'react-native';
|
||||
|
||||
import {useTheme} from '@context/theme';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {typography} from '@utils/typography';
|
||||
|
||||
import CompassIcon from '../compass_icon';
|
||||
import Markdown from '../markdown';
|
||||
|
||||
import SectionNoticeButton from './section_notice_button';
|
||||
|
||||
type Props = {
|
||||
title: string;
|
||||
text?: string;
|
||||
primaryButton?: SectionNoticeButtonProps;
|
||||
secondaryButton?: SectionNoticeButtonProps;
|
||||
linkButton?: SectionNoticeButtonProps;
|
||||
type?: 'info' | 'success' | 'danger' | 'welcome' | 'warning' | 'hint';
|
||||
isDismissable?: boolean;
|
||||
onDismissClick?: () => void;
|
||||
}
|
||||
|
||||
const iconByType = {
|
||||
info: 'information-outline',
|
||||
hint: 'lightbulb-outline',
|
||||
success: 'check',
|
||||
danger: 'alert-outline',
|
||||
warning: 'alert-outline',
|
||||
welcome: undefined,
|
||||
};
|
||||
|
||||
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
|
||||
return {
|
||||
container: {
|
||||
borderWidth: 1,
|
||||
borderStyle: 'solid',
|
||||
borderRadius: 4,
|
||||
},
|
||||
content: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'flex-start',
|
||||
padding: 16,
|
||||
gap: 12,
|
||||
},
|
||||
body: {
|
||||
flexDirection: 'column',
|
||||
gap: 8,
|
||||
flex: 1,
|
||||
},
|
||||
actions: {
|
||||
marginTop: 8,
|
||||
gap: 8,
|
||||
},
|
||||
title: {
|
||||
margin: 0,
|
||||
color: theme.centerChannelColor,
|
||||
...typography('Body', 200, 'SemiBold'),
|
||||
},
|
||||
welcomeTitle: {
|
||||
margin: 0,
|
||||
color: theme.centerChannelColor,
|
||||
...typography('Heading', 400, 'SemiBold'),
|
||||
},
|
||||
baseText: {
|
||||
color: theme.centerChannelColor,
|
||||
...typography('Body', 200, 'Regular'),
|
||||
},
|
||||
infoText: {
|
||||
color: theme.centerChannelColor,
|
||||
},
|
||||
infoIcon: {
|
||||
color: theme.sidebarTextActiveBorder,
|
||||
},
|
||||
infoContainer: {
|
||||
borderColor: changeOpacity(theme.sidebarTextActiveBorder, 0.16),
|
||||
backgroundColor: changeOpacity(theme.sidebarTextActiveBorder, 0.08),
|
||||
},
|
||||
successText: {
|
||||
color: theme.centerChannelColor,
|
||||
},
|
||||
successIcon: {
|
||||
color: theme.onlineIndicator,
|
||||
},
|
||||
successContainer: {
|
||||
borderColor: changeOpacity(theme.onlineIndicator, 0.16),
|
||||
backgroundColor: changeOpacity(theme.onlineIndicator, 0.08),
|
||||
},
|
||||
dangerText: {
|
||||
color: theme.dndIndicator,
|
||||
},
|
||||
dangerIcon: {
|
||||
color: theme.sidebarTextActiveBorder,
|
||||
},
|
||||
dangerContainer: {
|
||||
borderColor: changeOpacity(theme.dndIndicator, 0.16),
|
||||
backgroundColor: changeOpacity(theme.dndIndicator, 0.08),
|
||||
},
|
||||
warningText: {
|
||||
color: theme.awayIndicator,
|
||||
},
|
||||
warningIcon: {
|
||||
color: theme.sidebarTextActiveBorder,
|
||||
},
|
||||
warningContainer: {
|
||||
borderColor: changeOpacity(theme.awayIndicator, 0.16),
|
||||
backgroundColor: changeOpacity(theme.awayIndicator, 0.08),
|
||||
},
|
||||
welcomeText: {
|
||||
color: theme.centerChannelColor,
|
||||
},
|
||||
welcomeIcon: {
|
||||
color: theme.centerChannelColor,
|
||||
},
|
||||
welcomeContainer: {
|
||||
borderColor: changeOpacity(theme.centerChannelColor, 0.08),
|
||||
backgroundColor: changeOpacity(theme.centerChannelColor, 0.04),
|
||||
},
|
||||
hintText: {
|
||||
color: theme.centerChannelColor,
|
||||
},
|
||||
hintIcon: {
|
||||
color: theme.sidebarTextActiveBorder,
|
||||
},
|
||||
hintContainer: {
|
||||
borderColor: changeOpacity(theme.sidebarTextActiveBorder, 0.16),
|
||||
backgroundColor: changeOpacity(theme.sidebarTextActiveBorder, 0.08),
|
||||
},
|
||||
dismissIcon: {
|
||||
position: 'absolute',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
right: 10,
|
||||
top: 10,
|
||||
width: 32,
|
||||
height: 32,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const SectionNotice = ({
|
||||
title,
|
||||
isDismissable,
|
||||
linkButton,
|
||||
onDismissClick,
|
||||
primaryButton,
|
||||
secondaryButton,
|
||||
text,
|
||||
type = 'info',
|
||||
}: Props) => {
|
||||
const theme = useTheme();
|
||||
const styles = getStyleFromTheme(theme);
|
||||
|
||||
const icon = iconByType[type];
|
||||
const showDismiss = Boolean(isDismissable && onDismissClick);
|
||||
const hasButtons = primaryButton || secondaryButton || linkButton;
|
||||
|
||||
const containerStyle = useMemo(() => [styles.container, styles[`${type}Container`]], [type]);
|
||||
const iconStyle = useMemo(() => styles[`${type}Icon`], [type]);
|
||||
return (
|
||||
<View
|
||||
style={containerStyle}
|
||||
testID={'sectionNoticeContainer'}
|
||||
>
|
||||
<View style={styles.content}>
|
||||
{icon && (
|
||||
<CompassIcon
|
||||
name={icon}
|
||||
style={iconStyle}
|
||||
size={20}
|
||||
testID='sectionNoticeHeaderIcon'
|
||||
/>
|
||||
)}
|
||||
<View style={styles.body}>
|
||||
<Text style={styles.title}>{title}</Text>
|
||||
{text && (
|
||||
<Markdown
|
||||
theme={theme}
|
||||
location=''
|
||||
baseTextStyle={styles.baseText}
|
||||
value={text}
|
||||
/>
|
||||
)}
|
||||
{hasButtons && (
|
||||
<View style={styles.actions}>
|
||||
{primaryButton && (
|
||||
<SectionNoticeButton
|
||||
button={primaryButton}
|
||||
emphasis='primary'
|
||||
/>
|
||||
)}
|
||||
{secondaryButton && (
|
||||
<SectionNoticeButton
|
||||
button={secondaryButton}
|
||||
emphasis='tertiary'
|
||||
/>
|
||||
)}
|
||||
{linkButton && (
|
||||
<SectionNoticeButton
|
||||
button={linkButton}
|
||||
emphasis='link'
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
{showDismiss && (
|
||||
<Pressable
|
||||
style={styles.dismissIcon}
|
||||
role={'button'}
|
||||
onPress={onDismissClick}
|
||||
testID={'sectionNoticeDismissButton'}
|
||||
>
|
||||
<CompassIcon
|
||||
name={'close'}
|
||||
size={18}
|
||||
/>
|
||||
</Pressable>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default SectionNotice;
|
||||
35
app/components/section_notice/section_notice_button.tsx
Normal file
35
app/components/section_notice/section_notice_button.tsx
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import {useTheme} from '@context/theme';
|
||||
|
||||
import Button from '../button';
|
||||
|
||||
type ButtonProps = {
|
||||
button: SectionNoticeButtonProps;
|
||||
emphasis: 'primary' | 'tertiary' | 'link';
|
||||
}
|
||||
|
||||
const SectionNoticeButton = ({
|
||||
button,
|
||||
emphasis,
|
||||
}: ButtonProps) => {
|
||||
const theme = useTheme();
|
||||
const leadingIcon = button.leadingIcon ? {iconName: button.leadingIcon, iconSize: 18} : {};
|
||||
const trailingIcon = button.trailingIcon ? {iconName: button.trailingIcon, iconSize: 18} : {};
|
||||
|
||||
return (
|
||||
<Button
|
||||
onPress={button.onClick}
|
||||
text={button.text}
|
||||
theme={theme}
|
||||
emphasis={emphasis}
|
||||
{...leadingIcon}
|
||||
{...trailingIcon}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default SectionNoticeButton;
|
||||
10
app/components/section_notice/types.d.ts
vendored
Normal file
10
app/components/section_notice/types.d.ts
vendored
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
type SectionNoticeButtonProps = {
|
||||
onClick: () => void;
|
||||
text: string;
|
||||
loading?: boolean;
|
||||
trailingIcon?: string;
|
||||
leadingIcon?: string;
|
||||
};
|
||||
|
|
@ -8,7 +8,7 @@ import {type Edge, SafeAreaView} from 'react-native-safe-area-context';
|
|||
import {useTheme} from '@context/theme';
|
||||
import {makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
const edges: Edge[] = ['left', 'right'];
|
||||
const edges: Edge[] = ['left', 'right', 'bottom'];
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
return {
|
||||
|
|
@ -18,6 +18,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
|||
},
|
||||
contentContainerStyle: {
|
||||
marginTop: 8,
|
||||
flexGrow: 1,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
|
|
|||
119
app/hooks/use_external_link.test.ts
Normal file
119
app/hooks/use_external_link.test.ts
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {renderHook} from '@testing-library/react-native';
|
||||
import {URL} from 'react-native-url-polyfill';
|
||||
|
||||
import {useExternalLink} from './use_external_link';
|
||||
|
||||
const baseCurrentUserId = 'someUserId';
|
||||
const baseTelemetryId = 'someTelemetryId';
|
||||
|
||||
function getBaseProps(): Parameters<typeof useExternalLink>[0] {
|
||||
return {
|
||||
userId: baseCurrentUserId,
|
||||
isCloud: true,
|
||||
telemetryId: baseTelemetryId,
|
||||
};
|
||||
}
|
||||
|
||||
describe('useExternalLink', () => {
|
||||
it('keep non mattermost links untouched', () => {
|
||||
const url = 'https://www.someLink.com/something?query1=2#anchor';
|
||||
const {result: {current: [href, queryParams]}} = renderHook(() => useExternalLink(getBaseProps(), url, 'some location', {utm_source: 'something'}));
|
||||
expect(href).toEqual(url);
|
||||
expect(queryParams).toEqual({});
|
||||
});
|
||||
|
||||
it('all base queries are set correctly', () => {
|
||||
const url = 'https://www.mattermost.com/some/url';
|
||||
const {result: {current: [href, queryParams]}} = renderHook(() => useExternalLink(getBaseProps(), url));
|
||||
const parsedLink = new URL(href);
|
||||
expect(parsedLink.searchParams.get('utm_source')).toBe('mattermost');
|
||||
expect(parsedLink.searchParams.get('utm_medium')).toBe('in-product-cloud');
|
||||
expect(parsedLink.searchParams.get('utm_content')).toBe('');
|
||||
expect(parsedLink.searchParams.get('uid')).toBe(baseCurrentUserId);
|
||||
expect(parsedLink.searchParams.get('sid')).toBe(baseTelemetryId);
|
||||
expect(queryParams.utm_source).toBe('mattermost');
|
||||
expect(queryParams.utm_medium).toBe('in-product-cloud');
|
||||
expect(queryParams.utm_content).toBe('');
|
||||
expect(queryParams.uid).toBe(baseCurrentUserId);
|
||||
expect(queryParams.sid).toBe(baseTelemetryId);
|
||||
expect(href.split('?')[0]).toBe(url);
|
||||
});
|
||||
|
||||
it('provided location is added to the params', () => {
|
||||
const url = 'https://www.mattermost.com/some/url';
|
||||
const location = 'someLocation';
|
||||
const {result: {current: [href, queryParams]}} = renderHook(() => useExternalLink(getBaseProps(), url, location));
|
||||
const parsedLink = new URL(href);
|
||||
expect(parsedLink.searchParams.get('utm_content')).toBe(location);
|
||||
expect(queryParams.utm_content).toBe(location);
|
||||
});
|
||||
|
||||
it('non cloud environments set the proper utm medium', () => {
|
||||
const url = 'https://www.mattermost.com/some/url';
|
||||
const stateProps = getBaseProps();
|
||||
stateProps.isCloud = false;
|
||||
const {result: {current: [href, queryParams]}} = renderHook(() => useExternalLink(stateProps, url));
|
||||
const parsedLink = new URL(href);
|
||||
expect(parsedLink.searchParams.get('utm_medium')).toBe('in-product');
|
||||
expect(queryParams.utm_medium).toBe('in-product');
|
||||
});
|
||||
|
||||
it('keep existing query parameters untouched', () => {
|
||||
const url = 'https://www.mattermost.com/some/url?myParameter=true';
|
||||
const {result: {current: [href, queryParams]}} = renderHook(() => useExternalLink(getBaseProps(), url));
|
||||
const parsedLink = new URL(href);
|
||||
expect(parsedLink.searchParams.get('myParameter')).toBe('true');
|
||||
expect(queryParams.myParameter).toBe('true');
|
||||
});
|
||||
|
||||
it('keep anchors untouched', () => {
|
||||
const url = 'https://www.mattermost.com/some/url?myParameter=true#myAnchor';
|
||||
const {result: {current: [href]}} = renderHook(() => useExternalLink(getBaseProps(), url));
|
||||
const parsedLink = new URL(href);
|
||||
expect(parsedLink.hash).toBe('#myAnchor');
|
||||
});
|
||||
|
||||
it('overwriting params gets preference over default params', () => {
|
||||
const url = 'https://www.mattermost.com/some/url';
|
||||
const location = 'someLocation';
|
||||
const expectedContent = 'someOtherLocation';
|
||||
const expectedSource = 'someOtherSource';
|
||||
const {result: {current: [href, queryParams]}} = renderHook(() => useExternalLink(getBaseProps(), url, location, {utm_content: expectedContent, utm_source: expectedSource}));
|
||||
const parsedLink = new URL(href);
|
||||
expect(parsedLink.searchParams.get('utm_content')).toBe(expectedContent);
|
||||
expect(queryParams.utm_content).toBe(expectedContent);
|
||||
expect(parsedLink.searchParams.get('utm_source')).toBe(expectedSource);
|
||||
expect(queryParams.utm_source).toBe(expectedSource);
|
||||
});
|
||||
|
||||
it('existing params gets preference over default and overwritten params', () => {
|
||||
const location = 'someLocation';
|
||||
const overwrittenContent = 'someOtherLocation';
|
||||
const overwrittenSource = 'someOtherSource';
|
||||
const expectedContent = 'differentLocation';
|
||||
const expectedSource = 'differentSource';
|
||||
const url = `https://www.mattermost.com/some/url?utm_content=${expectedContent}&utm_source=${expectedSource}`;
|
||||
|
||||
const {result: {current: [href, queryParams]}} = renderHook(() => useExternalLink(getBaseProps(), url, location, {utm_content: overwrittenContent, utm_source: overwrittenSource}));
|
||||
const parsedLink = new URL(href);
|
||||
expect(parsedLink.searchParams.get('utm_content')).toBe(expectedContent);
|
||||
expect(queryParams.utm_content).toBe(expectedContent);
|
||||
expect(parsedLink.searchParams.get('utm_source')).toBe(expectedSource);
|
||||
expect(queryParams.utm_source).toBe(expectedSource);
|
||||
});
|
||||
|
||||
it('results are stable between re-renders', () => {
|
||||
const url = 'https://www.mattermost.com/some/url';
|
||||
const overwriteQueryParams = {utm_content: 'overwrittenContent', utm_source: 'overwrittenSource'};
|
||||
|
||||
const {result, rerender} = renderHook(() => useExternalLink(getBaseProps(), url, 'someLocation', overwriteQueryParams));
|
||||
const [firstHref, firstParams] = result.current;
|
||||
rerender(undefined);
|
||||
const [secondHref, secondParams] = result.current;
|
||||
expect(firstHref).toBe(secondHref);
|
||||
expect(firstParams).toBe(secondParams);
|
||||
});
|
||||
});
|
||||
54
app/hooks/use_external_link.ts
Normal file
54
app/hooks/use_external_link.ts
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {useMemo} from 'react';
|
||||
import {URL, URLSearchParams} from 'react-native-url-polyfill';
|
||||
|
||||
export type ExternalLinkQueryParams = {
|
||||
utm_source?: string;
|
||||
utm_medium?: string;
|
||||
utm_campaign?: string;
|
||||
utm_content?: string;
|
||||
userId?: string;
|
||||
}
|
||||
|
||||
type StateProps = {
|
||||
userId: string;
|
||||
telemetryId: string;
|
||||
isCloud: boolean;
|
||||
}
|
||||
|
||||
// This mimics the behavior of webapp/channels/src/components/common/hooks/use_external_link.ts
|
||||
export function useExternalLink(
|
||||
{
|
||||
userId,
|
||||
telemetryId,
|
||||
isCloud,
|
||||
}: StateProps,
|
||||
href: string,
|
||||
location: string = '',
|
||||
overwriteQueryParams: ExternalLinkQueryParams = {},
|
||||
): [string, Record<string, string>] {
|
||||
return useMemo(() => {
|
||||
if (!href?.includes('mattermost.com')) {
|
||||
return [href, {}];
|
||||
}
|
||||
|
||||
const parsedUrl = new URL(href);
|
||||
|
||||
const existingURLSearchParams = parsedUrl.searchParams;
|
||||
const existingQueryParamsObj = Object.fromEntries(existingURLSearchParams.entries());
|
||||
const queryParams = {
|
||||
utm_source: 'mattermost',
|
||||
utm_medium: isCloud ? 'in-product-cloud' : 'in-product',
|
||||
utm_content: location,
|
||||
uid: userId,
|
||||
sid: telemetryId,
|
||||
...overwriteQueryParams,
|
||||
...existingQueryParamsObj,
|
||||
};
|
||||
parsedUrl.search = new URLSearchParams(queryParams).toString();
|
||||
|
||||
return [parsedUrl.toString(), queryParams];
|
||||
}, [href, isCloud, location, overwriteQueryParams, telemetryId, userId]);
|
||||
}
|
||||
|
|
@ -8,7 +8,7 @@ import {switchMap} from 'rxjs/operators';
|
|||
import {Preferences} from '@constants';
|
||||
import {getPreferenceValue} from '@helpers/api/preference';
|
||||
import {queryPreferencesByCategoryAndName} from '@queries/servers/preference';
|
||||
import {observeConfigBooleanValue} from '@queries/servers/system';
|
||||
import {observeConfigBooleanValue, observeConfigValue} from '@queries/servers/system';
|
||||
import {observeIsCRTEnabled} from '@queries/servers/thread';
|
||||
import {observeCurrentUser} from '@queries/servers/user';
|
||||
|
||||
|
|
@ -27,6 +27,7 @@ const enhanced = withObservables([], ({database}: WithDatabaseArgs) => {
|
|||
switchMap((preferences) => of$(getPreferenceValue<string>(preferences, Preferences.CATEGORIES.NOTIFICATIONS, Preferences.EMAIL_INTERVAL, Preferences.INTERVAL_NOT_SET))),
|
||||
),
|
||||
sendEmailNotifications: observeConfigBooleanValue(database, 'SendEmailNotifications'),
|
||||
serverVersion: observeConfigValue(database, 'Version'),
|
||||
};
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -15,6 +15,8 @@ import {popTopScreen} from '@screens/navigation';
|
|||
import {gotoSettingsScreen} from '@screens/settings/config';
|
||||
import {getEmailInterval, getEmailIntervalTexts, getNotificationProps} from '@utils/user';
|
||||
|
||||
import SendTestNotificationNotice from './send_test_notification_notice';
|
||||
|
||||
import type UserModel from '@typings/database/models/servers/user';
|
||||
import type {AvailableScreens} from '@typings/screens/navigation';
|
||||
|
||||
|
|
@ -45,6 +47,7 @@ type NotificationsProps = {
|
|||
enableEmailBatching: boolean;
|
||||
isCRTEnabled: boolean;
|
||||
sendEmailNotifications: boolean;
|
||||
serverVersion: string;
|
||||
}
|
||||
const Notifications = ({
|
||||
componentId,
|
||||
|
|
@ -54,6 +57,7 @@ const Notifications = ({
|
|||
enableEmailBatching,
|
||||
isCRTEnabled,
|
||||
sendEmailNotifications,
|
||||
serverVersion,
|
||||
}: NotificationsProps) => {
|
||||
const intl = useIntl();
|
||||
const serverUrl = useServerUrl();
|
||||
|
|
@ -75,7 +79,7 @@ const Notifications = ({
|
|||
const defaultMessage = isCRTEnabled ? 'Mentions' : 'Mentions and Replies';
|
||||
const title = intl.formatMessage({id, defaultMessage});
|
||||
gotoSettingsScreen(screen, title);
|
||||
}, [isCRTEnabled]);
|
||||
}, [intl, isCRTEnabled]);
|
||||
|
||||
const goToNotificationSettingsPush = useCallback(() => {
|
||||
const screen = Screens.SETTINGS_NOTIFICATION_PUSH;
|
||||
|
|
@ -85,7 +89,7 @@ const Notifications = ({
|
|||
});
|
||||
|
||||
gotoSettingsScreen(screen, title);
|
||||
}, []);
|
||||
}, [intl]);
|
||||
|
||||
const callsNotificationsOn = useMemo(() => Boolean(notifyProps?.calls_mobile_sound ? notifyProps.calls_mobile_sound === 'true' : notifyProps?.calls_desktop_sound === 'true'),
|
||||
[notifyProps]);
|
||||
|
|
@ -97,7 +101,7 @@ const Notifications = ({
|
|||
});
|
||||
|
||||
gotoSettingsScreen(screen, title);
|
||||
}, []);
|
||||
}, [intl]);
|
||||
|
||||
const goToNotificationAutoResponder = useCallback(() => {
|
||||
const screen = Screens.SETTINGS_NOTIFICATION_AUTO_RESPONDER;
|
||||
|
|
@ -106,13 +110,13 @@ const Notifications = ({
|
|||
defaultMessage: 'Automatic Replies',
|
||||
});
|
||||
gotoSettingsScreen(screen, title);
|
||||
}, []);
|
||||
}, [intl]);
|
||||
|
||||
const goToEmailSettings = useCallback(() => {
|
||||
const screen = Screens.SETTINGS_NOTIFICATION_EMAIL;
|
||||
const title = intl.formatMessage({id: 'notification_settings.email', defaultMessage: 'Email Notifications'});
|
||||
gotoSettingsScreen(screen, title);
|
||||
}, []);
|
||||
}, [intl]);
|
||||
|
||||
const close = useCallback(() => {
|
||||
popTopScreen(componentId);
|
||||
|
|
@ -161,6 +165,10 @@ const Notifications = ({
|
|||
testID='notification_settings.automatic_replies.option'
|
||||
/>
|
||||
)}
|
||||
<SendTestNotificationNotice
|
||||
serverVersion={serverVersion}
|
||||
userId={currentUser?.id || ''}
|
||||
/>
|
||||
</SettingContainer>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,344 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`SendTestNotificationNotice should match snapshot 1`] = `
|
||||
<View
|
||||
style={
|
||||
{
|
||||
"flex": 1,
|
||||
"justifyContent": "flex-end",
|
||||
"margin": 16,
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
style={
|
||||
[
|
||||
{
|
||||
"borderRadius": 4,
|
||||
"borderStyle": "solid",
|
||||
"borderWidth": 1,
|
||||
},
|
||||
{
|
||||
"backgroundColor": "rgba(93,137,234,0.08)",
|
||||
"borderColor": "rgba(93,137,234,0.16)",
|
||||
},
|
||||
]
|
||||
}
|
||||
testID="sectionNoticeContainer"
|
||||
>
|
||||
<View
|
||||
style={
|
||||
{
|
||||
"alignItems": "flex-start",
|
||||
"flexDirection": "row",
|
||||
"gap": 12,
|
||||
"padding": 16,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Icon
|
||||
name="lightbulb-outline"
|
||||
size={20}
|
||||
style={
|
||||
{
|
||||
"color": "#5d89ea",
|
||||
}
|
||||
}
|
||||
testID="sectionNoticeHeaderIcon"
|
||||
/>
|
||||
<View
|
||||
style={
|
||||
{
|
||||
"flex": 1,
|
||||
"flexDirection": "column",
|
||||
"gap": 8,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Text
|
||||
style={
|
||||
{
|
||||
"color": "#3f4350",
|
||||
"fontFamily": "OpenSans-SemiBold",
|
||||
"fontSize": 16,
|
||||
"fontWeight": "600",
|
||||
"lineHeight": 24,
|
||||
"margin": 0,
|
||||
}
|
||||
}
|
||||
>
|
||||
Troubleshooting notifications
|
||||
</Text>
|
||||
<View
|
||||
style={
|
||||
[
|
||||
{
|
||||
"alignItems": "flex-start",
|
||||
"flexDirection": "row",
|
||||
"flexWrap": "wrap",
|
||||
},
|
||||
]
|
||||
}
|
||||
testID="markdown_paragraph"
|
||||
>
|
||||
<Text>
|
||||
<Text
|
||||
selectable={false}
|
||||
style={
|
||||
{
|
||||
"color": "#3f4350",
|
||||
"fontFamily": "OpenSans",
|
||||
"fontSize": 16,
|
||||
"fontWeight": "400",
|
||||
"lineHeight": 24,
|
||||
}
|
||||
}
|
||||
testID="markdown_text"
|
||||
>
|
||||
Not receiving notifications? Start by sending a test notification to all your devices to check if they’re working as expected. If issues persist, explore ways to solve them with troubleshooting steps.
|
||||
</Text>
|
||||
</Text>
|
||||
</View>
|
||||
<View
|
||||
style={
|
||||
{
|
||||
"gap": 8,
|
||||
"marginTop": 8,
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
style={
|
||||
[
|
||||
{
|
||||
"overflow": "hidden",
|
||||
},
|
||||
{
|
||||
"borderRadius": 2,
|
||||
},
|
||||
undefined,
|
||||
false,
|
||||
]
|
||||
}
|
||||
testID="RNE_BUTTON_WRAPPER"
|
||||
>
|
||||
<View
|
||||
accessibilityRole="button"
|
||||
accessibilityState={
|
||||
{
|
||||
"busy": false,
|
||||
"checked": undefined,
|
||||
"disabled": false,
|
||||
"expanded": undefined,
|
||||
"selected": undefined,
|
||||
}
|
||||
}
|
||||
accessibilityValue={
|
||||
{
|
||||
"max": undefined,
|
||||
"min": undefined,
|
||||
"now": undefined,
|
||||
"text": undefined,
|
||||
}
|
||||
}
|
||||
accessible={true}
|
||||
collapsable={false}
|
||||
focusable={true}
|
||||
onClick={[Function]}
|
||||
onResponderGrant={[Function]}
|
||||
onResponderMove={[Function]}
|
||||
onResponderRelease={[Function]}
|
||||
onResponderTerminate={[Function]}
|
||||
onResponderTerminationRequest={[Function]}
|
||||
onStartShouldSetResponder={[Function]}
|
||||
style={
|
||||
{
|
||||
"opacity": 1,
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
style={
|
||||
{
|
||||
"alignItems": "center",
|
||||
"backgroundColor": "#1c58d9",
|
||||
"borderColor": "#2089dc",
|
||||
"borderRadius": 4,
|
||||
"borderWidth": 0,
|
||||
"flex": 0,
|
||||
"flexDirection": "row",
|
||||
"height": 40,
|
||||
"justifyContent": "center",
|
||||
"padding": 8,
|
||||
"paddingHorizontal": 20,
|
||||
"paddingVertical": 12,
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
style={
|
||||
{
|
||||
"flexDirection": "row",
|
||||
}
|
||||
}
|
||||
>
|
||||
<Text
|
||||
numberOfLines={1}
|
||||
style={
|
||||
[
|
||||
[
|
||||
{
|
||||
"alignItems": "center",
|
||||
"fontFamily": "OpenSans-SemiBold",
|
||||
"fontWeight": "600",
|
||||
"justifyContent": "center",
|
||||
"padding": 1,
|
||||
"textAlignVertical": "center",
|
||||
},
|
||||
{
|
||||
"fontSize": 14,
|
||||
"lineHeight": 14,
|
||||
"marginTop": 3,
|
||||
},
|
||||
{
|
||||
"color": "#ffffff",
|
||||
},
|
||||
],
|
||||
undefined,
|
||||
]
|
||||
}
|
||||
>
|
||||
Send a test notification
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
<View
|
||||
style={
|
||||
[
|
||||
{
|
||||
"overflow": "hidden",
|
||||
},
|
||||
{
|
||||
"borderRadius": 2,
|
||||
},
|
||||
undefined,
|
||||
false,
|
||||
]
|
||||
}
|
||||
testID="RNE_BUTTON_WRAPPER"
|
||||
>
|
||||
<View
|
||||
accessibilityRole="button"
|
||||
accessibilityState={
|
||||
{
|
||||
"busy": false,
|
||||
"checked": undefined,
|
||||
"disabled": false,
|
||||
"expanded": undefined,
|
||||
"selected": undefined,
|
||||
}
|
||||
}
|
||||
accessibilityValue={
|
||||
{
|
||||
"max": undefined,
|
||||
"min": undefined,
|
||||
"now": undefined,
|
||||
"text": undefined,
|
||||
}
|
||||
}
|
||||
accessible={true}
|
||||
collapsable={false}
|
||||
focusable={true}
|
||||
onClick={[Function]}
|
||||
onResponderGrant={[Function]}
|
||||
onResponderMove={[Function]}
|
||||
onResponderRelease={[Function]}
|
||||
onResponderTerminate={[Function]}
|
||||
onResponderTerminationRequest={[Function]}
|
||||
onStartShouldSetResponder={[Function]}
|
||||
style={
|
||||
{
|
||||
"opacity": 1,
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
style={
|
||||
{
|
||||
"alignItems": "center",
|
||||
"backgroundColor": "rgba(28,88,217,0.08)",
|
||||
"borderColor": "#2089dc",
|
||||
"borderRadius": 4,
|
||||
"borderWidth": 0,
|
||||
"flex": 0,
|
||||
"flexDirection": "row",
|
||||
"height": 40,
|
||||
"justifyContent": "center",
|
||||
"padding": 8,
|
||||
"paddingHorizontal": 20,
|
||||
"paddingVertical": 12,
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
style={
|
||||
[
|
||||
{
|
||||
"flexDirection": "row",
|
||||
},
|
||||
{
|
||||
"minHeight": 18,
|
||||
},
|
||||
]
|
||||
}
|
||||
>
|
||||
<Icon
|
||||
color="#1c58d9"
|
||||
name="open-in-new"
|
||||
size={18}
|
||||
style={
|
||||
{
|
||||
"marginRight": 7,
|
||||
}
|
||||
}
|
||||
/>
|
||||
<Text
|
||||
numberOfLines={1}
|
||||
style={
|
||||
[
|
||||
[
|
||||
{
|
||||
"alignItems": "center",
|
||||
"fontFamily": "OpenSans-SemiBold",
|
||||
"fontWeight": "600",
|
||||
"justifyContent": "center",
|
||||
"padding": 1,
|
||||
"textAlignVertical": "center",
|
||||
},
|
||||
{
|
||||
"fontSize": 14,
|
||||
"lineHeight": 14,
|
||||
"marginTop": 3,
|
||||
},
|
||||
{
|
||||
"color": "#1c58d9",
|
||||
},
|
||||
],
|
||||
undefined,
|
||||
]
|
||||
}
|
||||
>
|
||||
Troubleshooting docs
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
`;
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {withDatabase, withObservables} from '@nozbe/watermelondb/react';
|
||||
import {of as of$} from 'rxjs';
|
||||
import {switchMap} from 'rxjs/operators';
|
||||
|
||||
import {observeConfigValue, observeLicense} from '@queries/servers/system';
|
||||
|
||||
import SendTestNotificationNotice from './send_test_notification_notice';
|
||||
|
||||
import type {WithDatabaseArgs} from '@typings/database/database';
|
||||
|
||||
const enhanced = withObservables([], ({database}: WithDatabaseArgs) => {
|
||||
return {
|
||||
isCloud: observeLicense(database).pipe(switchMap((v) => of$(v?.Cloud === 'true'))),
|
||||
telemetryId: observeConfigValue(database, 'TelemetryId').pipe(switchMap((v) => of$(v || ''))),
|
||||
};
|
||||
});
|
||||
|
||||
export default withDatabase(enhanced(SendTestNotificationNotice));
|
||||
|
|
@ -0,0 +1,138 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import {sendTestNotification} from '@actions/remote/notifications';
|
||||
import {act, fireEvent, renderWithEverything} from '@test/intl-test-helper';
|
||||
import TestHelper from '@test/test_helper';
|
||||
import {logError} from '@utils/log';
|
||||
import {tryOpenURL} from '@utils/url';
|
||||
|
||||
import SendTestNotificationNotice from './send_test_notification_notice';
|
||||
|
||||
import type Database from '@nozbe/watermelondb/Database';
|
||||
|
||||
const version = '10.3.0';
|
||||
const oldVersion = '10.2.0';
|
||||
|
||||
jest.mock('@utils/url', () => {
|
||||
return {
|
||||
tryOpenURL: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('@actions/remote/notifications', () => {
|
||||
return {
|
||||
sendTestNotification: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('@utils/log', () => {
|
||||
return {
|
||||
logError: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
const mockedSendTestNotification = jest.mocked(sendTestNotification);
|
||||
|
||||
const allTimerExceptSetTimeout = [
|
||||
'Date' as const,
|
||||
'hrtime' as const,
|
||||
'nextTick' as const,
|
||||
'performance' as const,
|
||||
'queueMicrotask' as const,
|
||||
'requestAnimationFrame' as const,
|
||||
'cancelAnimationFrame' as const,
|
||||
'requestIdleCallback' as const,
|
||||
'cancelIdleCallback' as const,
|
||||
'setImmediate' as const,
|
||||
'clearImmediate' as const,
|
||||
'setInterval' as const,
|
||||
'clearInterval' as const,
|
||||
'clearTimeout' as const,
|
||||
];
|
||||
|
||||
function getBaseProps() {
|
||||
return {
|
||||
serverVersion: version,
|
||||
telemetryId: 'someId',
|
||||
userId: 'someUserId',
|
||||
isCloud: true,
|
||||
};
|
||||
}
|
||||
|
||||
describe('SendTestNotificationNotice', () => {
|
||||
let database: Database;
|
||||
|
||||
beforeAll(async () => {
|
||||
const server = await TestHelper.setupServerDatabase();
|
||||
database = server.database;
|
||||
});
|
||||
|
||||
it('should match snapshot', () => {
|
||||
const wrapper = renderWithEverything(<SendTestNotificationNotice {...getBaseProps()}/>, {database});
|
||||
expect(wrapper.toJSON()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should show nothing on older versions', () => {
|
||||
const props = getBaseProps();
|
||||
props.serverVersion = oldVersion;
|
||||
const wrapper = renderWithEverything(<SendTestNotificationNotice {...props}/>, {database});
|
||||
expect(wrapper.toJSON()).toBeNull();
|
||||
});
|
||||
|
||||
it('should open the correct url for troubleshooting docs', () => {
|
||||
const wrapper = renderWithEverything(<SendTestNotificationNotice {...getBaseProps()}/>, {database});
|
||||
fireEvent.press(wrapper.getByText('Troubleshooting docs'));
|
||||
expect(tryOpenURL).toHaveBeenCalledWith('https://mattermost.com/pl/troubleshoot-notifications?utm_source=mattermost&utm_medium=in-product-cloud&utm_content=&uid=someUserId&sid=someId');
|
||||
});
|
||||
|
||||
it('should call send notification action when the send notification button is clicked, and all states go through correctly', async () => {
|
||||
jest.useFakeTimers({doNotFake: allTimerExceptSetTimeout});
|
||||
const wrapper = renderWithEverything(<SendTestNotificationNotice {...getBaseProps()}/>, {database});
|
||||
|
||||
let resolve: ((value: any) => void) | undefined;
|
||||
mockedSendTestNotification.mockReturnValueOnce(new Promise((r) => {
|
||||
resolve = r;
|
||||
}));
|
||||
fireEvent.press(wrapper.getByText('Send a test notification'));
|
||||
expect(sendTestNotification).toHaveBeenCalled();
|
||||
expect(wrapper.getByText('Sending a test notification')).toBeVisible();
|
||||
|
||||
await act(() => {
|
||||
resolve?.({data: true});
|
||||
});
|
||||
expect(wrapper.getByText('Test notification sent')).toBeVisible();
|
||||
|
||||
act(() => {
|
||||
jest.advanceTimersByTime(3001);
|
||||
});
|
||||
expect(wrapper.getByText('Send a test notification')).toBeVisible();
|
||||
});
|
||||
|
||||
it('should call send notification action when the send notification button is clicked, and when it fails, all states go through correctly', async () => {
|
||||
jest.useFakeTimers({doNotFake: allTimerExceptSetTimeout});
|
||||
const wrapper = renderWithEverything(<SendTestNotificationNotice {...getBaseProps()}/>, {database});
|
||||
|
||||
let resolve: ((value: any) => void) | undefined;
|
||||
mockedSendTestNotification.mockReturnValueOnce(new Promise((r) => {
|
||||
resolve = r;
|
||||
}));
|
||||
fireEvent.press(wrapper.getByText('Send a test notification'));
|
||||
expect(sendTestNotification).toHaveBeenCalled();
|
||||
expect(wrapper.getByText('Sending a test notification')).toBeVisible();
|
||||
|
||||
await act(() => {
|
||||
resolve?.({error: 'some error'});
|
||||
});
|
||||
|
||||
expect(wrapper.getByText('Error sending test notification')).toBeVisible();
|
||||
expect(logError).toHaveBeenCalledWith({error: 'some error'});
|
||||
|
||||
act(() => {
|
||||
jest.advanceTimersByTime(3001);
|
||||
});
|
||||
expect(wrapper.getByText('Send a test notification')).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,139 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {View} from 'react-native';
|
||||
|
||||
import {sendTestNotification} from '@actions/remote/notifications';
|
||||
import SectionNotice from '@components/section_notice';
|
||||
import {useServerUrl} from '@context/server';
|
||||
import {useExternalLink} from '@hooks/use_external_link';
|
||||
import {isMinimumServerVersion} from '@utils/helpers';
|
||||
import {logError} from '@utils/log';
|
||||
import {tryOpenURL} from '@utils/url';
|
||||
|
||||
const TIME_TO_IDLE = 3000;
|
||||
|
||||
type Props = {
|
||||
serverVersion: string;
|
||||
userId: string;
|
||||
isCloud: boolean;
|
||||
telemetryId: string;
|
||||
}
|
||||
|
||||
type ButtonState = 'idle'|'sending'|'sent'|'error';
|
||||
|
||||
const styles = {
|
||||
wrapper: {
|
||||
flex: 1,
|
||||
justifyContent: 'flex-end' as const,
|
||||
margin: 16,
|
||||
},
|
||||
};
|
||||
|
||||
const SendTestNotificationNotice = ({
|
||||
serverVersion,
|
||||
userId,
|
||||
isCloud,
|
||||
telemetryId,
|
||||
}: Props) => {
|
||||
const intl = useIntl();
|
||||
const serverUrl = useServerUrl();
|
||||
const [buttonState, setButtonState] = useState<ButtonState>('idle');
|
||||
const isSending = useRef(false);
|
||||
const timeout = useRef<NodeJS.Timeout>();
|
||||
|
||||
const [href] = useExternalLink({
|
||||
userId,
|
||||
isCloud,
|
||||
telemetryId,
|
||||
}, 'https://mattermost.com/pl/troubleshoot-notifications');
|
||||
|
||||
const onGoToNotificationDocumentation = useCallback(() => {
|
||||
tryOpenURL(href);
|
||||
}, [href]);
|
||||
|
||||
const onSendTestNotificationClick = useCallback(async () => {
|
||||
if (isSending.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
isSending.current = true;
|
||||
setButtonState('sending');
|
||||
const result = await sendTestNotification(serverUrl);
|
||||
if (result.error) {
|
||||
logError(result);
|
||||
setButtonState('error');
|
||||
} else {
|
||||
setButtonState('sent');
|
||||
}
|
||||
timeout.current = setTimeout(() => {
|
||||
isSending.current = false;
|
||||
setButtonState('idle');
|
||||
}, TIME_TO_IDLE);
|
||||
}, [serverUrl]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
clearTimeout(timeout.current);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const primaryButton = useMemo(() => {
|
||||
let text;
|
||||
let icon;
|
||||
let loading;
|
||||
switch (buttonState) {
|
||||
case 'idle':
|
||||
text = intl.formatMessage({id: 'user_settings.notifications.test_notification.send_button.send', defaultMessage: 'Send a test notification'});
|
||||
break;
|
||||
case 'sending':
|
||||
text = intl.formatMessage({id: 'user_settings.notifications.test_notification.send_button.sending', defaultMessage: 'Sending a test notification'});
|
||||
loading = true;
|
||||
break;
|
||||
case 'sent':
|
||||
text = intl.formatMessage({id: 'user_settings.notifications.test_notification.send_button.sent', defaultMessage: 'Test notification sent'});
|
||||
icon = 'check';
|
||||
break;
|
||||
case 'error':
|
||||
text = intl.formatMessage({id: 'user_settings.notifications.test_notification.send_button.error', defaultMessage: 'Error sending test notification'});
|
||||
icon = 'alert-outline';
|
||||
}
|
||||
return {
|
||||
onClick: onSendTestNotificationClick,
|
||||
text,
|
||||
leadingIcon: icon,
|
||||
loading,
|
||||
};
|
||||
}, [buttonState, intl, onSendTestNotificationClick]);
|
||||
|
||||
const secondaryButton = useMemo(() => {
|
||||
return {
|
||||
onClick: onGoToNotificationDocumentation,
|
||||
text: intl.formatMessage({id: 'user_settings.notifications.test_notification.go_to_docs', defaultMessage: 'Troubleshooting docs'}),
|
||||
trailingIcon: 'open-in-new',
|
||||
};
|
||||
}, [intl, onGoToNotificationDocumentation]);
|
||||
|
||||
if (!isMinimumServerVersion(serverVersion, 10, 3)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={styles.wrapper}>
|
||||
<SectionNotice
|
||||
text={intl.formatMessage({
|
||||
id: 'user_settings.notifications.test_notification.body',
|
||||
defaultMessage: 'Not receiving notifications? Start by sending a test notification to all your devices to check if they’re working as expected. If issues persist, explore ways to solve them with troubleshooting steps.',
|
||||
})}
|
||||
title={intl.formatMessage({id: 'user_settings.notifications.test_notification.title', defaultMessage: 'Troubleshooting notifications'})}
|
||||
primaryButton={primaryButton}
|
||||
secondaryButton={secondaryButton}
|
||||
type='hint'
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default SendTestNotificationNotice;
|
||||
|
|
@ -1163,6 +1163,13 @@
|
|||
"unsupported_server.message": "Your server, {serverDisplayName}, is running an unsupported server version. You may experience compatibility issues that cause crashes or severe bugs breaking core functionality of the app. Please contact your System Administrator to upgrade your Mattermost server.",
|
||||
"unsupported_server.title": "Unsupported server version",
|
||||
"user_profile.custom_status": "Custom Status",
|
||||
"user_settings.notifications.test_notification.body": "Not receiving notifications? Start by sending a test notification to all your devices to check if they’re working as expected. If issues persist, explore ways to solve them with troubleshooting steps.",
|
||||
"user_settings.notifications.test_notification.go_to_docs": "Troubleshooting docs",
|
||||
"user_settings.notifications.test_notification.send_button.error": "Error sending test notification",
|
||||
"user_settings.notifications.test_notification.send_button.send": "Send a test notification",
|
||||
"user_settings.notifications.test_notification.send_button.sending": "Sending a test notification",
|
||||
"user_settings.notifications.test_notification.send_button.sent": "Test notification sent",
|
||||
"user_settings.notifications.test_notification.title": "Troubleshooting notifications",
|
||||
"user_status.away": "Away",
|
||||
"user_status.dnd": "Do Not Disturb",
|
||||
"user_status.offline": "Offline",
|
||||
|
|
|
|||
12
package-lock.json
generated
12
package-lock.json
generated
|
|
@ -91,6 +91,7 @@
|
|||
"react-native-shadow-2": "7.1.0",
|
||||
"react-native-share": "10.2.1",
|
||||
"react-native-svg": "15.4.0",
|
||||
"react-native-url-polyfill": "2.0.0",
|
||||
"react-native-vector-icons": "10.1.0",
|
||||
"react-native-video": "6.4.3",
|
||||
"react-native-walkthrough-tooltip": "1.6.0",
|
||||
|
|
@ -23755,6 +23756,17 @@
|
|||
"react-native": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-url-polyfill": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/react-native-url-polyfill/-/react-native-url-polyfill-2.0.0.tgz",
|
||||
"integrity": "sha512-My330Do7/DvKnEvwQc0WdcBnFPploYKp9CYlefDXzIdEaA+PAhDYllkvGeEroEzvc4Kzzj2O4yVdz8v6fjRvhA==",
|
||||
"dependencies": {
|
||||
"whatwg-url-without-unicode": "8.0.0-3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react-native": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-vector-icons": {
|
||||
"version": "10.1.0",
|
||||
"resolved": "https://registry.npmjs.org/react-native-vector-icons/-/react-native-vector-icons-10.1.0.tgz",
|
||||
|
|
|
|||
|
|
@ -92,6 +92,7 @@
|
|||
"react-native-shadow-2": "7.1.0",
|
||||
"react-native-share": "10.2.1",
|
||||
"react-native-svg": "15.4.0",
|
||||
"react-native-url-polyfill": "2.0.0",
|
||||
"react-native-vector-icons": "10.1.0",
|
||||
"react-native-video": "6.4.3",
|
||||
"react-native-walkthrough-tooltip": "1.6.0",
|
||||
|
|
|
|||
1
types/api/config.d.ts
vendored
1
types/api/config.d.ts
vendored
|
|
@ -188,6 +188,7 @@ interface ClientConfig {
|
|||
SiteURL: string;
|
||||
SupportEmail: string;
|
||||
TeammateNameDisplay: string;
|
||||
TelemetryId: string;
|
||||
TermsOfServiceLink: string;
|
||||
TimeBetweenUserTypingUpdatesMilliseconds: string;
|
||||
Version: string;
|
||||
|
|
|
|||
Loading…
Reference in a new issue