Add support for review app (#6772)

* Add app review

* Use overlay instead of modal

* Add fixes for ios

* i18n-extract

* Add to milliseconds function

* Address review feedback

* Add try to queryGlobalValue

* added app review illustration

* add feedback illustration

* Add animations and feedback bot message

* Restrict reviews to build environment variable

* Fix bug with "dont ask anymore"

* Add check for only supported servers

* Add missing change

* Use for await

Co-authored-by: Daniel Espino <danielespino@MacBook-Pro-de-Daniel.local>
Co-authored-by: Matthew Birtch <mattbirtch@gmail.com>
This commit is contained in:
Daniel Espino García
2022-11-24 18:52:15 +01:00
committed by GitHub
parent 81dcfc817b
commit 5fae120826
47 changed files with 1144 additions and 78 deletions

View File

@@ -4,11 +4,11 @@
import {GLOBAL_IDENTIFIERS} from '@constants/database';
import DatabaseManager from '@database/manager';
export const storeDeviceToken = async (token: string, prepareRecordsOnly = false) => {
export const storeGlobal = async (id: string, value: unknown, prepareRecordsOnly = false) => {
try {
const {operator} = DatabaseManager.getAppDatabaseAndOperator();
return operator.handleGlobal({
globals: [{id: GLOBAL_IDENTIFIERS.DEVICE_TOKEN, value: token}],
globals: [{id, value}],
prepareRecordsOnly,
});
} catch (error) {
@@ -16,26 +16,26 @@ export const storeDeviceToken = async (token: string, prepareRecordsOnly = false
}
};
export const storeDeviceToken = async (token: string, prepareRecordsOnly = false) => {
return storeGlobal(GLOBAL_IDENTIFIERS.DEVICE_TOKEN, token, prepareRecordsOnly);
};
export const storeMultiServerTutorial = async (prepareRecordsOnly = false) => {
try {
const {operator} = DatabaseManager.getAppDatabaseAndOperator();
return operator.handleGlobal({
globals: [{id: GLOBAL_IDENTIFIERS.MULTI_SERVER_TUTORIAL, value: 'true'}],
prepareRecordsOnly,
});
} catch (error) {
return {error};
}
return storeGlobal(GLOBAL_IDENTIFIERS.MULTI_SERVER_TUTORIAL, 'true', prepareRecordsOnly);
};
export const storeProfileLongPressTutorial = async (prepareRecordsOnly = false) => {
try {
const {operator} = DatabaseManager.getAppDatabaseAndOperator();
return operator.handleGlobal({
globals: [{id: GLOBAL_IDENTIFIERS.PROFILE_LONG_PRESS_TUTORIAL, value: 'true'}],
prepareRecordsOnly,
});
} catch (error) {
return {error};
}
return storeGlobal(GLOBAL_IDENTIFIERS.PROFILE_LONG_PRESS_TUTORIAL, 'true', prepareRecordsOnly);
};
export const storeDontAskForReview = async (prepareRecordsOnly = false) => {
return storeGlobal(GLOBAL_IDENTIFIERS.DONT_ASK_FOR_REVIEW, 'true', prepareRecordsOnly);
};
export const storeLastAskForReview = async (prepareRecordsOnly = false) => {
return storeGlobal(GLOBAL_IDENTIFIERS.LAST_ASK_FOR_REVIEW, Date.now(), prepareRecordsOnly);
};
export const storeFirstLaunch = async (prepareRecordsOnly = false) => {
return storeGlobal(GLOBAL_IDENTIFIERS.FIRST_LAUNCH, Date.now(), prepareRecordsOnly);
};

View File

@@ -709,6 +709,29 @@ export async function switchToChannelByName(serverUrl: string, channelName: stri
}
}
export async function goToNPSChannel(serverUrl: string) {
let client: Client;
try {
client = NetworkManager.getClient(serverUrl);
} catch (error) {
return {error};
}
try {
const user = await client.getUserByUsername(General.NPS_PLUGIN_BOT_USERNAME);
const {data, error} = await createDirectChannel(serverUrl, user.id);
if (error || !data) {
throw error || new Error('channel not found');
}
await switchToChannelById(serverUrl, data.id, data.team_id);
} catch (error) {
forceLogoutIfNecessary(serverUrl, error as ClientErrorProps);
return {error};
}
return {};
}
export async function createDirectChannel(serverUrl: string, userId: string, displayName = '') {
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
if (!operator) {

View File

@@ -124,3 +124,4 @@ export const getRedirectLocation = async (serverUrl: string, link: string) => {
return {error};
}
};

30
app/actions/remote/nps.ts Normal file
View File

@@ -0,0 +1,30 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {General} from '@constants';
import NetworkManager from '@managers/network_manager';
export const isNPSEnabled = async (serverUrl: string) => {
try {
const client = NetworkManager.getClient(serverUrl);
const manifests = await client.getPluginsManifests();
for (const v of manifests) {
if (v.id === General.NPS_PLUGIN_ID) {
return true;
}
}
return false;
} catch (error) {
return false;
}
};
export const giveFeedbackAction = async (serverUrl: string) => {
try {
const client = NetworkManager.getClient(serverUrl);
const post = await client.npsGiveFeedbackAction();
return {post};
} catch (error) {
return {error};
}
};

View File

@@ -205,12 +205,16 @@ export default class ClientBase {
return `${this.urlVersion}/plugins`;
}
getPluginRoute(id: string) {
return `/plugins/${id}`;
}
getAppsProxyRoute() {
return '/plugins/com.mattermost.apps';
return this.getPluginRoute('com.mattermost.apps');
}
getCallsRoute() {
return `/plugins/${Calls.PluginId}`;
return this.getPluginRoute(Calls.PluginId);
}
doFetch = async (url: string, options: ClientOptions, returnDataOnly = true) => {

View File

@@ -3,6 +3,8 @@
import {ClientResponse, ClientResponseError, ProgressPromise, UploadRequestOptions} from '@mattermost/react-native-network-client';
import {toMilliseconds} from '@utils/datetime';
export interface ClientFilesMix {
getFileUrl: (fileId: string, timestamp: number) => string;
getFileThumbnailUrl: (fileId: string, timestamp: number) => string;
@@ -72,7 +74,7 @@ const ClientFiles = (superclass: any) => class extends superclass {
channel_id: channelId,
},
},
timeoutInterval: 3 * 60 * 1000, // 3 minutes
timeoutInterval: toMilliseconds({minutes: 3}),
};
const promise = this.apiClient.upload(url, file.localPath, options) as ProgressPromise<ClientResponse>;
promise.progress!(onProgress).then(onComplete).catch(onError);

View File

@@ -15,6 +15,7 @@ import ClientFiles, {ClientFilesMix} from './files';
import ClientGeneral, {ClientGeneralMix} from './general';
import ClientGroups, {ClientGroupsMix} from './groups';
import ClientIntegrations, {ClientIntegrationsMix} from './integrations';
import ClientNPS, {ClientNPSMix} from './nps';
import ClientPosts, {ClientPostsMix} from './posts';
import ClientPreferences, {ClientPreferencesMix} from './preferences';
import ClientTeams, {ClientTeamsMix} from './teams';
@@ -40,7 +41,8 @@ interface Client extends ClientBase,
ClientTosMix,
ClientUsersMix,
ClientCallsMix,
ClientPluginsMix
ClientPluginsMix,
ClientNPSMix
{}
class Client extends mix(ClientBase).with(
@@ -60,6 +62,7 @@ class Client extends mix(ClientBase).with(
ClientUsers,
ClientCalls,
ClientPlugins,
ClientNPS,
) {
// eslint-disable-next-line no-useless-constructor
constructor(apiClient: APIClientInterface, serverUrl: string, bearerToken?: string, csrfToken?: string) {

19
app/client/rest/nps.ts Normal file
View File

@@ -0,0 +1,19 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {General} from '@constants';
export interface ClientNPSMix {
npsGiveFeedbackAction: () => Promise<Post>;
}
const ClientNPS = (superclass: any) => class extends superclass {
npsGiveFeedbackAction = async () => {
return this.doFetch(
`${this.getPluginRoute(General.NPS_PLUGIN_ID)}/api/v1/give_feedback`,
{method: 'post'},
);
};
};
export default ClientNPS;

View File

@@ -0,0 +1,61 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useMemo} from 'react';
import {StyleProp, Text, TextStyle, ViewStyle} from 'react-native';
import RNButton from 'react-native-button';
import {buttonBackgroundStyle, buttonTextStyle} from '@utils/buttonStyles';
type Props = {
theme: Theme;
backgroundStyle?: StyleProp<ViewStyle>;
textStyle?: StyleProp<TextStyle>;
size?: ButtonSize;
emphasis?: ButtonEmphasis;
buttonType?: ButtonType;
buttonState?: ButtonState;
testID?: string;
onPress: () => void;
text: string;
}
const Button = ({
theme,
backgroundStyle,
textStyle,
size,
emphasis,
buttonType,
buttonState,
onPress,
text,
testID,
}: Props) => {
const bgStyle = useMemo(() => [
buttonBackgroundStyle(theme, size, emphasis, buttonType, buttonState),
backgroundStyle,
], [theme, backgroundStyle, size, emphasis, buttonType, buttonState]);
const txtStyle = useMemo(() => [
buttonTextStyle(theme, size, emphasis, buttonType),
textStyle,
], [theme, textStyle, size, emphasis, buttonType]);
return (
<RNButton
containerStyle={bgStyle}
onPress={onPress}
testID={testID}
>
<Text
style={txtStyle}
numberOfLines={1}
>
{text}
</Text>
</RNButton>
);
};
export default Button;

View File

@@ -5,6 +5,8 @@ import moment from 'moment-timezone';
import React, {useEffect, useState} from 'react';
import {Text, TextProps} from 'react-native';
import {toMilliseconds} from '@utils/datetime';
type FormattedRelativeTimeProps = TextProps & {
timezone?: UserTimezone | string;
value: number | string | Date;
@@ -24,7 +26,10 @@ const FormattedRelativeTime = ({timezone, value, updateIntervalInSeconds, ...pro
const [formattedTime, setFormattedTime] = useState(getFormattedRelativeTime);
useEffect(() => {
if (updateIntervalInSeconds) {
const interval = setInterval(() => setFormattedTime(getFormattedRelativeTime()), updateIntervalInSeconds * 1000);
const interval = setInterval(
() => setFormattedTime(getFormattedRelativeTime()),
toMilliseconds({seconds: updateIntervalInSeconds}),
);
return function cleanup() {
return clearInterval(interval);
};

View File

@@ -0,0 +1,197 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import * as React from 'react';
import Svg, {
Path,
Mask,
G,
Rect,
} from 'react-native-svg';
type Props = {
theme: Theme;
}
function ReviewAppIllustration({theme}: Props) {
return (
<Svg
width={222}
height={205}
viewBox='0 0 222 205'
fill='none'
>
<Path
d='M166.305 43.157c0-3.319 2.79-6.01 6.232-6.01h43.231c3.442 0 6.232 2.691 6.232 6.01v41.69c0 3.32-2.79 6.01-6.232 6.01h-43.231c-3.442 0-6.232-2.69-6.232-6.01v-41.69Z'
fill={theme.centerChannelColor}
fillOpacity={0.08}
/>
<Path
d='M194.309 76.927a.835.835 0 0 0-.517 0l-11.006 4.04a.914.914 0 0 1-.75-.118.7.7 0 0 1-.302-.618l.6-11.294a.854.854 0 0 0-.171-.496l-7.394-8.828a.652.652 0 0 1-.157-.313.63.63 0 0 1 .03-.346.913.913 0 0 1 .469-.496l11.435-2.968c.09 0 .178-.024.255-.067a.496.496 0 0 0 .184-.181l6.447-9.483a.775.775 0 0 1 .28-.24.804.804 0 0 1 1.009.24l6.447 9.483a.525.525 0 0 0 .171.224c.078.059.17.096.267.108l11.352 2.884c.124.04.238.105.334.19a.89.89 0 0 1 .223.305.64.64 0 0 1-.127.66l-7.394 8.828a.813.813 0 0 0-.175.495l.604 11.295a.65.65 0 0 1-.083.355.695.695 0 0 1-.263.263.86.86 0 0 1-.688.122L194.3 76.93l.009-.004Z'
fill='#FFBC1F'
/>
<Rect
x={95.794}
y={168.252}
width={19.722}
height={3.26}
rx={1.63}
fill='#8D93A5'
/>
<Path
fillRule='evenodd'
clipRule='evenodd'
d='M144.9 178.534c-2.206 2.177-5.351 3.846-8.8 3.846H75.773c-3.449 0-6.593-1.669-8.799-3.846-2.21-2.182-3.863-5.252-3.863-8.573V35.039c0-3.322 1.65-6.392 3.86-8.574 2.205-2.178 5.349-3.845 8.797-3.845H136.1c3.448 0 6.593 1.666 8.799 3.844 2.212 2.182 3.863 5.252 3.863 8.575v134.922c0 3.321-1.652 6.391-3.862 8.573Zm-8.8.586c4.641 0 9.281-4.585 9.281-9.159V35.039c0-4.58-4.64-9.159-9.281-9.159H75.768c-4.64 0-9.276 4.58-9.276 9.16V169.96c0 4.574 4.64 9.159 9.281 9.159H136.1Z'
fill='#363A45'
/>
<Path
d='M145.381 169.961c0 4.574-4.64 9.159-9.281 9.159H75.773c-4.64 0-9.28-4.585-9.28-9.159V35.039c0-4.58 4.634-9.159 9.275-9.159H136.1c4.641 0 9.281 4.58 9.281 9.16V169.96Z'
fill='#3F4350'
/>
<Path
d='M112.699 33.758c0 .451-.296.817-.671.817H99.851c-.37 0-.676-.377-.676-.817 0-.44.307-.814.676-.814h12.157c.374 0 .691.366.691.814Z'
fill='#8D93A5'
/>
<Path
fill={theme.centerChannelBg}
d='M69.873 42.726H142v120.092H69.873z'
/>
<Mask
id='a'
maskUnits='userSpaceOnUse'
x={69}
y={43}
width={134}
height={120}
>
<Path
fill='#FFFFFF'
d='M69.716 43.157h132.421v119.812H69.716z'
/>
</Mask>
<G mask='url(#a)'>
<Path
d='m89.041 85.762.504-14.438-1.145-5.718c2.096-5.199 5.176-4.835 7.066-5 4.306-.376 7.18-2.65 9.161-.64 1.077 1.203 2.291 6.115 2.485 9.934.138 2.02.871 3.742-.687 4.757a21.35 21.35 0 0 1-3.355 1.413l.572 10.807-14.6-1.115Z'
fill='#AD831F'
/>
<Path
d='M105.784 65.33a21.555 21.555 0 0 1 3.882 3.312c.355 1.512-3.962 2.285-3.962 2.285l.08-5.597Z'
fill='#AD831F'
/>
<Path
d='M98.489 76.644c1.527.161 3.072-.024 4.512-.54.538-.21-.996.728-1.546.916-.707.35-1.498.51-2.29.463-.55-.088-1.26-.883-.676-.839Z'
fill='#7A5600'
/>
<Path
d='M110.433 58.243a1.927 1.927 0 0 0-.966-.99 2.036 2.036 0 0 0-1.404-.113c-4.066.65-3.298-2.76-9.288-1.789-4.168.674-8.566 0-12.253 2.043a7.734 7.734 0 0 0-3.619 3.465 3.458 3.458 0 0 0-.21 2.424 3.58 3.58 0 0 0 1.447 1.992c.252.143.55.265.676.519.08.264.056.547-.069.794-.515 1.49-.973 3.312.183 4.416.642.618 1.718.905 1.97 1.744.137.463 0 .96.057 1.446.24 1.28 2.29 1.49 3.264.585a5.392 5.392 0 0 0 1.271-3.643c.103-1.159.137-2.494-.756-3.311-.412-.376-.996-.596-1.214-1.104a1.45 1.45 0 0 1 .046-.93c.118-.298.33-.552.607-.726a3.237 3.237 0 0 1 1.24-.505 3.29 3.29 0 0 1 1.346.036c.44.105.853.299 1.209.57.356.272.647.614.855 1.003.332.728.435 1.689 1.145 2.042.927.442 1.98-.552 2.107-1.534a16.322 16.322 0 0 0-.31-2.959c0-.993.527-2.207 1.558-2.207.34.017.677.084.996.198 2.023.55 4.157.603 6.207.155a6.497 6.497 0 0 0 2.817-1.104c.402-.284.716-.669.908-1.112.191-.444.254-.93.18-1.405Z'
fill='#4A2407'
/>
<Path
d='m102.577 64.403.058-.11c-3.058-.266-6.872-.497-9.483-.464-2.61.033-3.813.386-5.176 2.65-.229.385.436.661.676.275.27-.62.715-1.154 1.284-1.544a3.846 3.846 0 0 1 1.934-.664c1.328-.099 2.703 0 4.02 0 1.683 0 4.294.232 6.47.43.068-.198.137-.385.217-.573Z'
fill={theme.centerChannelBg}
/>
<Path
d='M102.978 66.555a2.144 2.144 0 0 1-.751-.944 2.064 2.064 0 0 1-.108-1.186c.161-1.16 1.054-1.998 1.981-1.877.499.137.922.456 1.181.889s.332.945.205 1.43c-.16 1.103-1.053 1.998-1.981 1.876a1.311 1.311 0 0 1-.527-.188Zm1.374-3.311a.687.687 0 0 0-.32-.11c-.63-.089-1.237.54-1.363 1.401-.126.861.286 1.634.916 1.722.63.089 1.225-.552 1.351-1.413.078-.299.064-.613-.04-.905a1.595 1.595 0 0 0-.544-.74v.045Z'
fill={theme.centerChannelBg}
/>
<Path
d='M97.344 84.824c29.774.22 29.694-2.473 32.683 31.382 2.989 33.854 3.035 35.08-26.408 36.945-19.628 1.248-31.217 8.831-32.236-1.567-1.03-10.597-1.683-10.288-3.436-28.292-3.973-39.837-3.298-38.711 29.397-38.468Z'
fill='#AD831F'
/>
<Path
d='M110.72 109.848c8.497 9.294 27.69 16.48 42.52 16.48a24.612 24.612 0 0 0 6.275-.773 21.401 21.401 0 0 0 3.653-1.313c12.208-5.751 15.838-23.501 18.495-29.594 3.275-7.495-.126-2.837 2.084-7.12.79-1.534 2.737-2.528 3.367-3.311.904-1.104-.825-2.594-7.352 1.666-10.307 6.624-9.162 19.969-19.308 21.371a15.88 15.88 0 0 1-3.836 0 25.19 25.19 0 0 1-3.378-.585c-18.105-4.305-19.64-20.796-37.459-21.47 0-.044-13.57 15.288-5.061 24.649ZM17.629 97.452c-.344.927 1.145 5.519 1.546 9.724.985 10.134 4.214 25.687 19.216 31.625a18.309 18.309 0 0 0 5.966 1.215c1.3.055 2.604-.004 3.894-.177 9.16-1.104 19.227-7.539 24.22-17.573 6.87-13.853 9.665-37.354 9.665-37.354-6.619 0-22.903-1.225-28.824 23.986a13.288 13.288 0 0 1-2.394 5.456 13.886 13.886 0 0 1-4.58 3.949 10.907 10.907 0 0 1-3.642 1.104 9.984 9.984 0 0 1-5.085-.698 9.604 9.604 0 0 1-3.996-3.11c-8.886-11.469-9.986-23.81-16.387-25.024-6.402-1.215 2.073 2.362.4 6.877Z'
fill='#AD831F'
/>
<Path
d='M115.781 85.155c17.819.673 19.354 17.165 37.459 21.47 1.889.483 3.839.713 5.794.684.836 5.883 1.443 11.8 2.027 17.728a24.098 24.098 0 0 1-7.821 1.247c-14.83 0-34.023-7.186-42.52-16.491-8.498-9.306 5.061-24.638 5.061-24.638ZM44.804 118.932c3.825-1.247 7.26-4.647 8.508-10.034 5.875-25.211 22.205-23.997 28.813-23.986 0 0-2.76 23.501-9.654 37.354-5.566 11.171-17.36 17.827-27.324 17.761a69.32 69.32 0 0 0-.343-21.095Z'
fill='#1E325C'
/>
<Path
d='M131.355 135.92c17.361 49.055 18.415 69.2 16.8 86.099-1.615 16.9-14.406 34.286-31.091 47.013-3.516 2.406-7.913 4.106-5.131 10.663 4.043 9.537-6.596-2.992-9.253-9.228-2.656-6.237 25.515-17.088 19.01-50.39-6.504-33.303-23.957-46.924-34.87-70.491-10.914-23.567 44.535-13.666 44.535-13.666Z'
fill={theme.buttonBg}
/>
<Path
d='M131.355 135.92c17.361 49.055 18.415 69.2 16.8 86.099-1.615 16.9-14.406 34.286-31.091 47.013-3.516 2.406-7.913 4.106-5.131 10.663 4.043 9.537-6.596-2.992-9.253-9.228-2.656-6.237 25.515-17.088 19.01-50.39-6.504-33.303-23.957-46.924-34.87-70.491-10.914-23.567 44.535-13.666 44.535-13.666Z'
fill='#000'
fillOpacity={0.16}
/>
<Path
d='M111.086 139.188c2.107 54.971-3.355 80.26-28.057 98.042-24.701 17.783-43.848 18.423-44.593 27.386-.744 8.964-2.977 6.91-3.847-2.285-1.145-12.484-6.55-3.212 22.159-24.56 28.71-21.348 15.15-77.599 13.742-94.267-1.409-16.668 40.596-4.316 40.596-4.316Z'
fill={theme.buttonBg}
/>
<Path
d='M38.081 255.466c14.407-6.844 29.385-14.472 36.073-29.418 7.444-16.634 4.272-35.322 3.802-52.785-.275-10.531 0-21.072.71-31.581 0-.474.802-.474.767 0-.607 9.659-.927 19.328-.755 29.009.171 9.681 1.145 19.361 1.145 29.064-.057 8.4-1.008 16.922-4.157 24.814-2.898 7.177-7.71 13.491-13.948 18.302-6.997 5.519-15.14 9.416-23.19 13.246-.504.199-.893-.442-.447-.651Z'
fill='#1E325C'
/>
<Path
d='M89.099 84.769a6.29 6.29 0 0 0 3.126 2.55c4.707 1.865 10.696 1.865 13.33-2.473 21.643 0 21.838.927 24.518 31.36 1.351 15.299 2.096 23.931-.195 28.976-11.944 4.978-25.675 6.623-38.191 8.963-5.451 1.015-10.925 1.876-16.41 2.638-2.222-.519-3.54-2.009-3.848-5.199-1.03-10.597-1.684-10.288-3.436-28.292-3.653-36.14-3.435-38.567 21.106-38.523Z'
fill={theme.centerChannelBg}
/>
<Path
d='M89.099 84.769a6.29 6.29 0 0 0 3.126 2.55c4.707 1.865 10.696 1.865 13.33-2.473 21.643 0 21.838.927 24.518 31.36 1.351 15.299 2.096 23.931-.195 28.976-11.944 4.978-25.675 6.623-38.191 8.963-5.451 1.015-10.925 1.876-16.41 2.638-2.222-.519-3.54-2.009-3.848-5.199-1.03-10.597-1.684-10.288-3.436-28.292-3.653-36.14-3.435-38.567 21.106-38.523Z'
fill={theme.centerChannelColor}
fillOpacity={0.08}
/>
<Path
d='M118.198 142.775a81.05 81.05 0 0 1-7.662-18.412 215.276 215.276 0 0 1-6.252-39.528c22.903 0 25.193-.806 27.976 30.212.138 1.501-1.672 4.194-1.145 5.729 4.432 11.987 20.304 24.284 18.998 27.044-1.82 3.951-8.772 8.168-17.521 11.48a77.847 77.847 0 0 1-6.985 1.368 104.902 104.902 0 0 0-7.409-17.893ZM67.764 145.546c-.653 7.34-1.511 13.621-.973 19.24 5.027-.806 10.077-1.314 15.15-1.656a100.117 100.117 0 0 0 9.826-35.511c.664-7.34 2.714-35.532 2.21-39.738-.32-2.825-3.974-.441-4.718-3.135-24.69 0-27.335 1.767-23.716 37.983.171 1.811 2.702 4.107 2.782 5.818.206 5.672.019 11.351-.56 16.999Z'
fill='#1E325C'
/>
<Path
d='M105.864 83.422c1.695 1.314 1.386.95.229 3.013-1.157 2.065-1.741 3.93-2.691 1.424-.951-2.505-1.077-2.108.16-3.598 1.237-1.49 1.443-1.512 2.302-.84ZM91.687 83.841c3.836 1.623 2.428 1.248 3.16 3.687 1.592 5.243-.63 2.462-6.069-.563-3.699-2.064-3.264-2.384-1.97-3.642 1.294-1.259.905-1.204 4.673.518 3.767 1.722-3.642-1.59.206 0ZM156.618 107.21a214.295 214.295 0 0 0 2.897 18.301 21.401 21.401 0 0 0 3.653-1.313 203.658 203.658 0 0 1-2.714-16.999 15.88 15.88 0 0 1-3.836.011Z'
fill={theme.buttonBg}
/>
</G>
<Path
d='M25.705 125.411c0-3.319 2.79-6.01 6.232-6.01h18.305c3.442 0 6.232 2.691 6.232 6.01v17.652c0 3.319-2.79 6.01-6.232 6.01H31.937c-3.442 0-6.232-2.691-6.232-6.01v-17.652Z'
fill={theme.centerChannelColor}
fillOpacity={0.08}
/>
<Path
d='M31.28 134.233s.33-3.798 2.412-5.711c2.081-1.914 4.073-3.181 8.275-3.428 4.202-.247 6.836 3.841 7.243 4.57.408.729 1.617 2.892 1.617 4.569 0 1.676.013 5.199-2.478 6.763-2.492 1.564-4.889 2.476-6.608 2.389-1.72-.086-6.066-.433-8.014-3.042-1.947-2.608-2.447-3.442-2.447-6.11Z'
fill='#FFBC1F'
/>
<Path
d='M41.394 127.066a3.612 3.612 0 0 1 2.646.289.125.125 0 0 0 .145-.017.115.115 0 0 0 .022-.14c-.37-.652-1.336-1.841-2.928-.329a.114.114 0 0 0-.037.067.119.119 0 0 0 .073.125c.025.01.053.011.079.005Z'
fill='#FFD470'
/>
<Path
d='M37.474 141.656c-1.019-1.004-2.376-2.281-2.24-6.648.136-4.368 1.223-5.914 2.001-7.075.419-.63 1.895-1.933 3.703-2.753-3.519.384-5.35 1.593-7.257 3.342-2.091 1.911-2.411 5.709-2.411 5.709 0 2.668.51 3.504 2.457 6.11 1.948 2.607 6.292 2.956 8.014 3.042-.002 0-3.248-.717-4.267-1.727Z'
fill='#CC8F00'
/>
<Path
d='M42.017 140.642s5.438.099 6.392-4.787a.64.64 0 0 0-.118-.519.681.681 0 0 0-.21-.183.72.72 0 0 0-.268-.087 54.746 54.746 0 0 0-12.165.177.698.698 0 0 0-.462.267.65.65 0 0 0-.116.506c.264 1.363 1.465 4.307 6.947 4.626Z'
fill='#6F370B'
/>
<Path
d='M42.607 140.287s-3.322.239-4.709-1.096l.168-.121c.179-.102.37-.181.57-.237a4.687 4.687 0 0 1 1.068-.165c.209-.01.418-.014.638 0 .22.002.44.033.652.091.21.065.412.152.603.257.285.144.536.343.736.586.157.199.252.436.274.685Z'
fill='#C43133'
/>
<Path
d='M36.961 135.389s4.755-.283 10.074-.174c0 0-.272 1.022-5.183 1.022-1.109 0-4.686.413-4.89-.848Z'
fill='#fff'
/>
<Path
d='M39.256 131.17c.052 1.169-.418 2.137-1.063 2.163-.644.026-1.198-.9-1.255-2.068-.056-1.169.419-2.135 1.063-2.161.644-.026 1.205.896 1.255 2.066ZM46.498 131.176c.046 1.022-.37 1.87-.931 1.894-.56.024-1.046-.787-1.094-1.807-.048-1.02.37-1.87.93-1.894.561-.024 1.049.785 1.095 1.807Z'
fill='#6F370B'
/>
<Path
d='M0 66.444c0-3.32 2.79-6.01 6.232-6.01h36.22c3.442 0 6.232 2.69 6.232 6.01v34.929c0 3.319-2.79 6.01-6.231 6.01H6.232c-3.442 0-6.232-2.691-6.232-6.01v-34.93Z'
fill={theme.centerChannelColor}
fillOpacity={0.08}
/>
<Path
d='M34.592 95.28s-3.856 1.454-4.48 1.671c-.623.217-2.508.983-2.956 1.126-.448.144-3.102 1.126-4.118 1.126-1.017 0-4.608-.4-4.608-.4l-2.133-.906-.754-1.126-.493-.719-1.647-1.416v-2.259l.262-.581-1.086-1.377-.345-2.034.748-1.634.19-.689-1.237-2.395.71-1.597s1.874-1.233 2.136-1.307a13.832 13.832 0 0 1 2.543 0c1.978.082 3.959.063 5.934-.056 1.444-.127 1.034-.902 1.034-.902l-1.475-3.108-.561-3.448s-.149-1.597.151-2.395c.3-.799 1.61-1.962 2.919-1.888 1.31.073.6 1.56.6 1.56l1.123 1.925s.748.668.861 1.052c.114.384.562 2.322.9 3.341.338 1.02 1.533 3.124 2.919 4.01 1.385.885 4.007 2.322 4.455 3.084.318.61.557 1.256.71 1.924l.345 2.339-.262 2.252-.758 3.124-1.627 1.704Z'
fill='#FFBC1F'
/>
<Path
d='M25.88 80.546s-3.704-2.65-3.445-9.689c0 0-1.496 5.56 1.199 9.803-.01-.004 1.485.247 2.247-.114ZM15.612 80.71s-2.499.732-2.647 2.279c-.148 1.546 0 2.566.948 3.244 0 0-2.995 3.34.344 5.516 0 0-2.047 3.725 1.847 4.26 0 0-1.278 3.097 5.9 2.903a16.222 16.222 0 0 0 7.478-2.131c1.034-.578 3.394-1.257 4.135-1.788.741-.531 2.809-2.275 3.046-5.713 0-1.985 0-3.846-.475-5.346 0 0 1.347 1.337 1.347 4.297s-.723 5.42-1.423 6.458c-.7 1.04-2.068 2.105-3.37 2.339-1.303.234-2.757.217-3.69.628-.935.411-3.447 1.38-5.442 1.574-2.118.127-4.244.03-6.34-.29-.849-.195-2.572-1.26-2.472-2.674 0 0-3.494-1.26-1.599-4.343 0 0-1.67-.822-1.67-3.12a2.91 2.91 0 0 1 .34-1.285c.212-.398.514-.745.882-1.014a3.355 3.355 0 0 1-1.25-1.306 3.227 3.227 0 0 1-.373-1.745c.166-2.158 2.437-2.967 4.484-2.743Z'
fill='#CC8F00'
/>
<Path
d='M27.838 73.31a4.725 4.725 0 0 1-2.007-1.786 4.521 4.521 0 0 1-.674-2.558c0-.361.917-.401 1.068 0 .152.4 1.523 3.782 1.613 4.343Z'
fill='#FFD470'
/>
</Svg>
);
}
export default ReviewAppIllustration;

View File

@@ -0,0 +1,49 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import * as React from 'react';
import Svg, {
Path,
Ellipse,
} from 'react-native-svg';
type Props = {
theme: Theme;
}
function ShareFeedbackIllustration({theme}: Props) {
return (
<Svg
width={172}
height={172}
viewBox='0 0 172 172'
fill='none'
>
<Path
d='M39.982 33h92.003a12.865 12.865 0 0 1 9.092 3.722 12.805 12.805 0 0 1 3.791 9.047v58.35a12.801 12.801 0 0 1-3.791 9.047 12.866 12.866 0 0 1-9.092 3.722h-13.577v21.841l-20.367-21.841H40.015a12.859 12.859 0 0 1-9.091-3.722 12.81 12.81 0 0 1-3.792-9.047v-58.35a12.799 12.799 0 0 1 3.78-9.035A12.852 12.852 0 0 1 39.982 33Z'
fill='#FFBC1F'
/>
<Path
d='M98.04 116.888H40.016a12.857 12.857 0 0 1-9.091-3.722 12.81 12.81 0 0 1-3.792-9.047V68.695s4.052 32.757 4.78 35.64c.727 2.884 2.172 7.198 9.015 7.913 6.843.716 57.114 4.64 57.114 4.64Z'
fill='#CC8F00'
/>
<Path
d='M117.408 66.603a8.3 8.3 0 0 0-4.604 1.393 8.255 8.255 0 0 0-1.256 12.725 8.302 8.302 0 0 0 12.752-1.253 8.263 8.263 0 0 0 .768-7.762 8.255 8.255 0 0 0-4.486-4.477 8.295 8.295 0 0 0-3.174-.626ZM85.983 66.603a8.3 8.3 0 0 0-4.605 1.393 8.275 8.275 0 0 0-3.052 3.712 8.255 8.255 0 0 0 1.797 9.013 8.304 8.304 0 0 0 9.032 1.793 8.285 8.285 0 0 0 3.72-3.046 8.258 8.258 0 0 0 1.396-4.595 8.243 8.243 0 0 0-2.424-5.851 8.277 8.277 0 0 0-5.864-2.42ZM54.592 66.603a8.3 8.3 0 0 0-4.607 1.388 8.274 8.274 0 0 0-3.057 3.71 8.254 8.254 0 0 0 1.79 9.017 8.294 8.294 0 0 0 9.032 1.797 8.284 8.284 0 0 0 3.722-3.046 8.258 8.258 0 0 0 1.397-4.596 8.246 8.246 0 0 0-2.42-5.847 8.278 8.278 0 0 0-5.857-2.423Z'
fill={theme.centerChannelBg}
/>
<Path
d='M135.32 57.433a25.992 25.992 0 0 0-4.65-9.077 26.044 26.044 0 0 0-7.788-6.597.902.902 0 0 1-.474-.994.897.897 0 0 1 .844-.708c5.8-.347 17.51.889 13.838 17.289a.912.912 0 0 1-1.77.087Z'
fill='#FFD470'
/>
<Ellipse
cx={86}
cy={148.925}
rx={52.528}
ry={4.075}
fill='#000'
fillOpacity={0.08}
/>
</Svg>
);
}
export default ShareFeedbackIllustration;

View File

@@ -1,7 +1,9 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
const RefreshConfigMillis = 20 * 60 * 1000; // Refresh config after 20 minutes
import {toMilliseconds} from '@utils/datetime';
const RefreshConfigMillis = toMilliseconds({minutes: 20});
const RequiredServer = {
FULL_VERSION: '6.3.0',

View File

@@ -73,6 +73,9 @@ export const SYSTEM_IDENTIFIERS = {
export const GLOBAL_IDENTIFIERS = {
DEVICE_TOKEN: 'deviceToken',
DONT_ASK_FOR_REVIEW: 'dontAskForReview',
FIRST_LAUNCH: 'firstLaunch',
LAST_ASK_FOR_REVIEW: 'lastAskForReview',
MULTI_SERVER_TUTORIAL: 'multiServerTutorial',
PROFILE_LONG_PRESS_TUTORIAL: 'profileLongPressTutorial',
};

View File

@@ -1,6 +1,8 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {toMilliseconds} from '@utils/datetime';
export const VALID_IMAGE_MIME_TYPES = [
'image/jpeg',
'image/jpeg',
@@ -43,6 +45,6 @@ export const Files: Record<string, string[]> = {
};
Files.DOCUMENT_TYPES = Files.WORD_TYPES.concat(Files.PDF_TYPES, Files.TEXT_TYPES);
export const PROGRESS_TIME_TO_STORE = 60000; // 60 * 1000 (60s)
export const PROGRESS_TIME_TO_STORE = toMilliseconds({seconds: 60});
export default Files;

View File

@@ -1,6 +1,8 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {toMilliseconds} from '@utils/datetime';
export default {
PAGE_SIZE_DEFAULT: 60,
POST_CHUNK_SIZE: 60,
@@ -36,4 +38,8 @@ export default {
AUTOCOMPLETE_SPLIT_CHARACTERS: ['.', '-', '_'],
CHANNEL_USER_ROLE: 'channel_user',
RESTRICT_DIRECT_MESSAGE_ANY: 'any',
TIME_TO_FIRST_REVIEW: toMilliseconds({days: 14}),
TIME_TO_NEXT_REVIEW: toMilliseconds({days: 90}),
NPS_PLUGIN_ID: 'com.mattermost.nps',
NPS_PLUGIN_BOT_USERNAME: 'feedbackbot',
};

View File

@@ -1,6 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {toMilliseconds} from '@utils/datetime';
import keyMirror from '@utils/key_mirror';
export const CERTIFICATE_ERRORS = keyMirror({
@@ -8,7 +9,7 @@ export const CERTIFICATE_ERRORS = keyMirror({
CLIENT_CERTIFICATE_MISSING: null,
});
export const DOWNLOAD_TIMEOUT = (1000 * 60) * 10; // 10 mins
export const DOWNLOAD_TIMEOUT = toMilliseconds({minutes: 10});
export default {
CERTIFICATE_ERRORS,

View File

@@ -1,6 +1,8 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {toMilliseconds} from '@utils/datetime';
export const PostTypes: Record<string, string> = {
CHANNEL_DELETED: 'system_channel_deleted',
CHANNEL_UNARCHIVED: 'system_channel_restored',
@@ -45,10 +47,10 @@ export enum PostPriorityType {
IMPORTANT = 'important',
}
export const POST_TIME_TO_FAIL = 10000;
export const POST_TIME_TO_FAIL = toMilliseconds({seconds: 10});
export default {
POST_COLLAPSE_TIMEOUT: 1000 * 60 * 5,
POST_COLLAPSE_TIMEOUT: toMilliseconds({minutes: 5}),
POST_TYPES: PostTypes,
USER_ACTIVITY_POST_TYPES: [
PostTypes.ADD_TO_CHANNEL,

View File

@@ -37,6 +37,7 @@ export const PERMALINK = 'Permalink';
export const PINNED_MESSAGES = 'PinnedMessages';
export const POST_OPTIONS = 'PostOptions';
export const REACTIONS = 'Reactions';
export const REVIEW_APP = 'ReviewApp';
export const SAVED_MESSAGES = 'SavedMessages';
export const SEARCH = 'Search';
export const SELECT_TEAM = 'SelectTeam';
@@ -53,6 +54,7 @@ export const SETTINGS_NOTIFICATION_AUTO_RESPONDER = 'SettingsNotificationAutoRes
export const SETTINGS_NOTIFICATION_EMAIL = 'SettingsNotificationEmail';
export const SETTINGS_NOTIFICATION_MENTION = 'SettingsNotificationMention';
export const SETTINGS_NOTIFICATION_PUSH = 'SettingsNotificationPush';
export const SHARE_FEEDBACK = 'ShareFeedback';
export const SNACK_BAR = 'SnackBar';
export const SSO = 'SSO';
export const TABLE = 'Table';
@@ -98,6 +100,7 @@ export default {
PINNED_MESSAGES,
POST_OPTIONS,
REACTIONS,
REVIEW_APP,
SAVED_MESSAGES,
SEARCH,
SELECT_TEAM,
@@ -114,6 +117,7 @@ export default {
SETTINGS_NOTIFICATION_EMAIL,
SETTINGS_NOTIFICATION_MENTION,
SETTINGS_NOTIFICATION_PUSH,
SHARE_FEEDBACK,
SNACK_BAR,
SSO,
TABLE,
@@ -153,6 +157,8 @@ export const OVERLAY_SCREENS = new Set<string>([
IN_APP_NOTIFICATION,
GALLERY,
SNACK_BAR,
REVIEW_APP,
SHARE_FEEDBACK,
]);
export const NOT_READY = [

View File

@@ -1,12 +1,12 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {EffectCallback, useEffect} from 'react';
import {useEffect} from 'react';
import {Navigation} from 'react-native-navigation';
const BACK_BUTTON = 'RNN.back';
const useBackNavigation = (callback: EffectCallback) => {
const useBackNavigation = (callback: () => void) => {
useEffect(() => {
const backListener = Navigation.events().registerNavigationButtonPressedListener(({buttonId}) => {
if (buttonId === BACK_BUTTON) {

View File

@@ -25,7 +25,7 @@ const initialNotificationTypes = [PushNotification.NOTIFICATION_TYPE.MESSAGE, Pu
export const initialLaunch = async () => {
const deepLinkUrl = await Linking.getInitialURL();
if (deepLinkUrl) {
launchAppFromDeepLink(deepLinkUrl);
launchAppFromDeepLink(deepLinkUrl, true);
return;
}
@@ -41,20 +41,20 @@ export const initialLaunch = async () => {
tapped = delivered.find((d) => (d as unknown as NotificationData).ack_id === notification?.payload.ack_id) == null;
}
if (initialNotificationTypes.includes(notification?.payload?.type) && tapped) {
launchAppFromNotification(convertToNotificationData(notification!));
launchAppFromNotification(convertToNotificationData(notification!), true);
return;
}
launchApp({launchType: Launch.Normal});
launchApp({launchType: Launch.Normal, coldStart: true});
};
const launchAppFromDeepLink = (deepLinkUrl: string) => {
const props = getLaunchPropsFromDeepLink(deepLinkUrl);
const launchAppFromDeepLink = (deepLinkUrl: string, coldStart = false) => {
const props = getLaunchPropsFromDeepLink(deepLinkUrl, coldStart);
launchApp(props);
};
const launchAppFromNotification = async (notification: NotificationWithData) => {
const props = await getLaunchPropsFromNotification(notification);
const launchAppFromNotification = async (notification: NotificationWithData, coldStart = false) => {
const props = await getLaunchPropsFromNotification(notification, coldStart);
launchApp(props);
};
@@ -184,10 +184,11 @@ export const relaunchApp = (props: LaunchProps, resetNavigation = false) => {
return launchApp(props, resetNavigation);
};
export const getLaunchPropsFromDeepLink = (deepLinkUrl: string): LaunchProps => {
export const getLaunchPropsFromDeepLink = (deepLinkUrl: string, coldStart = false): LaunchProps => {
const parsed = parseDeepLink(deepLinkUrl);
const launchProps: LaunchProps = {
launchType: Launch.DeepLink,
coldStart,
};
switch (parsed.type) {
@@ -219,9 +220,10 @@ export const getLaunchPropsFromDeepLink = (deepLinkUrl: string): LaunchProps =>
return launchProps;
};
export const getLaunchPropsFromNotification = async (notification: NotificationWithData): Promise<LaunchProps> => {
export const getLaunchPropsFromNotification = async (notification: NotificationWithData, coldStart = false): Promise<LaunchProps> => {
const launchProps: LaunchProps = {
launchType: Launch.Notification,
coldStart,
};
const {payload} = notification;

View File

@@ -7,9 +7,10 @@ import JailMonkey from 'jail-monkey';
import {Alert, AlertButton, AppState, AppStateStatus, Platform} from 'react-native';
import {DEFAULT_LOCALE, getTranslations, t} from '@i18n';
import {toMilliseconds} from '@utils/datetime';
import {getIOSAppGroupDetails} from '@utils/mattermost_managed';
const PROMPT_IN_APP_PIN_CODE_AFTER = 5 * 60 * 1000;
const PROMPT_IN_APP_PIN_CODE_AFTER = toMilliseconds({minutes: 5});
class ManagedApp {
backgroundSince = 0;

View File

@@ -14,10 +14,11 @@ import {General} from '@constants';
import DatabaseManager from '@database/manager';
import {getCurrentUserId} from '@queries/servers/system';
import {queryAllUsers} from '@queries/servers/user';
import {toMilliseconds} from '@utils/datetime';
import {logError} from '@utils/log';
const WAIT_TO_CLOSE = 15 * 1000;
const WAIT_UNTIL_NEXT = 20 * 1000;
const WAIT_TO_CLOSE = toMilliseconds({seconds: 15});
const WAIT_UNTIL_NEXT = toMilliseconds({seconds: 20});
class WebsocketManager {
private clients: Record<string, WebSocketClient> = {};

View File

@@ -5,6 +5,8 @@ import moment from 'moment-timezone';
import React, {useEffect, useState} from 'react';
import {Text, StyleProp, TextStyle} from 'react-native';
import {toMilliseconds} from '@utils/datetime';
type CallDurationProps = {
style: StyleProp<TextStyle>;
value: number;
@@ -34,7 +36,10 @@ const CallDuration = ({value, style, updateIntervalInSeconds}: CallDurationProps
const [formattedTime, setFormattedTime] = useState(() => getCallDuration());
useEffect(() => {
if (updateIntervalInSeconds) {
const interval = setInterval(() => setFormattedTime(getCallDuration()), updateIntervalInSeconds * 1000);
const interval = setInterval(
() => setFormattedTime(getCallDuration()),
toMilliseconds({seconds: updateIntervalInSeconds}),
);
return function cleanup() {
clearInterval(interval);
};

View File

@@ -21,6 +21,8 @@ import {
} from 'react-native-webrtc';
import stream from 'readable-stream';
import {toMilliseconds} from '@utils/datetime';
import type {ICEServersConfigs} from '@calls/types/calls';
const queueMicrotask = (callback: any) => {
@@ -55,8 +57,8 @@ function generateId(): string {
}
const MAX_BUFFERED_AMOUNT = 64 * 1024;
const ICECOMPLETE_TIMEOUT = 5 * 1000;
const CHANNEL_CLOSING_TIMEOUT = 5 * 1000;
const ICECOMPLETE_TIMEOUT = toMilliseconds({seconds: 5});
const CHANNEL_CLOSING_TIMEOUT = toMilliseconds({seconds: 5});
/**
* WebRTC peer connection. Same API as node core `net.Socket`, plus a few extra methods.

View File

@@ -21,20 +21,56 @@ export const getDeviceToken = async (appDatabase: Database): Promise<string> =>
}
};
export const observeMultiServerTutorial = (appDatabase: Database) => {
return appDatabase.get<GlobalModel>(GLOBAL).query(Q.where('id', GLOBAL_IDENTIFIERS.MULTI_SERVER_TUTORIAL), Q.take(1)).observe().pipe(
export const queryGlobalValue = (key: string) => {
try {
const {database} = DatabaseManager.getAppDatabaseAndOperator();
return database.get<GlobalModel>(GLOBAL).query(Q.where('id', key), Q.take(1));
} catch {
return undefined;
}
};
export const observeMultiServerTutorial = () => {
const query = queryGlobalValue(GLOBAL_IDENTIFIERS.MULTI_SERVER_TUTORIAL);
if (!query) {
return of$(false);
}
return query.observe().pipe(
switchMap((result) => (result.length ? result[0].observe() : of$(false))),
switchMap((v) => of$(Boolean(v))),
);
};
export const observeProfileLongPresTutorial = () => {
const appDatabase = DatabaseManager.appDatabase?.database;
if (!appDatabase) {
const query = queryGlobalValue(GLOBAL_IDENTIFIERS.PROFILE_LONG_PRESS_TUTORIAL);
if (!query) {
return of$(false);
}
return appDatabase.get<GlobalModel>(GLOBAL).query(Q.where('id', GLOBAL_IDENTIFIERS.PROFILE_LONG_PRESS_TUTORIAL), Q.take(1)).observe().pipe(
return query.observe().pipe(
switchMap((result) => (result.length ? result[0].observe() : of$(false))),
switchMap((v) => of$(Boolean(v))),
);
};
export const getLastAskedForReview = async () => {
const records = await queryGlobalValue(GLOBAL_IDENTIFIERS.LAST_ASK_FOR_REVIEW)?.fetch();
if (!records?.[0]?.value) {
return 0;
}
return records[0].value;
};
export const getDontAskForReview = async () => {
const records = await queryGlobalValue(GLOBAL_IDENTIFIERS.DONT_ASK_FOR_REVIEW)?.fetch();
return Boolean(records?.[0]?.value);
};
export const getFirstLaunch = async () => {
const records = await queryGlobalValue(GLOBAL_IDENTIFIERS.FIRST_LAUNCH)?.fetch();
if (!records?.[0]?.value) {
return 0;
}
return records[0].value;
};

View File

@@ -3,7 +3,11 @@
import {Database, Q} from '@nozbe/watermelondb';
import {SupportedServer} from '@constants';
import {MM_TABLES} from '@constants/database';
import DatabaseManager from '@database/manager';
import {getConfigValue} from '@queries/servers/system';
import {isMinimumServerVersion} from '@utils/helpers';
import type ServerModel from '@typings/database/models/app/servers';
@@ -56,3 +60,33 @@ export const queryServerName = async (appDatabase: Database, serverUrl: string)
return serverUrl;
}
};
export const areAllServersSupported = async () => {
let appDatabase;
try {
const databaseAndOperator = DatabaseManager.getAppDatabaseAndOperator();
appDatabase = databaseAndOperator.database;
} catch {
return false;
}
const servers = await queryAllServers(appDatabase);
for await (const s of servers) {
if (s.lastActiveAt) {
try {
const {database: serverDatabase} = DatabaseManager.getServerDatabaseAndOperator(s.url);
const version = await getConfigValue(serverDatabase, 'Version');
const {MAJOR_VERSION, MIN_VERSION, PATCH_VERSION} = SupportedServer;
const isSupportedServer = isMinimumServerVersion(version || '', MAJOR_VERSION, MIN_VERSION, PATCH_VERSION);
if (!isSupportedServer) {
return false;
}
} catch {
continue;
}
}
}
return true;
};

View File

@@ -16,7 +16,7 @@ import type ServersModel from '@typings/database/models/app/servers';
const enhance = withObservables(['highlight'], ({highlight, server}: {highlight: boolean; server: ServersModel}) => {
let tutorialWatched = of$(false);
if (highlight) {
tutorialWatched = observeMultiServerTutorial(server.database);
tutorialWatched = observeMultiServerTutorial();
}
const serverDatabase = DatabaseManager.serverDatabases[server.url]?.database;

View File

@@ -16,6 +16,7 @@ import {findChannels, popToRoot} from '@screens/navigation';
import NavigationStore from '@store/navigation_store';
import {alertChannelArchived, alertChannelRemove, alertTeamRemove} from '@utils/navigation';
import {notificationError} from '@utils/notification';
import {tryRunAppReview} from '@utils/reviews';
import Account from './account';
import ChannelList from './channel_list';
@@ -40,6 +41,14 @@ type HomeProps = LaunchProps & {
const Tab = createBottomTabNavigator();
// This is needed since the Database Provider is recreating this component
// when the database is changed (couldn't find exactly why), re-triggering
// the effect. This makes sure the rate logic is only handle on the first
// run. Most of the normal users won't see this issue, but on edge times
// (near the time you will see the rate dialog) will show when switching
// servers.
let hasRendered = false;
export default function HomeScreen(props: HomeProps) {
const theme = useTheme();
const intl = useIntl();
@@ -96,6 +105,15 @@ export default function HomeScreen(props: HomeProps) {
};
}, [intl.locale]);
// Init the rate app. Only run the effect on the first render
useEffect(() => {
if (hasRendered) {
return;
}
hasRendered = true;
tryRunAppReview(props.launchType, props.coldStart);
}, []);
return (
<>
<NavigationContainer

View File

@@ -161,6 +161,9 @@ Navigation.setLazyComponentRegistrator((screenName) => {
case Screens.REACTIONS:
screen = withServerDatabase(require('@screens/reactions').default);
break;
case Screens.REVIEW_APP:
screen = withServerDatabase(require('@screens/review_app').default);
break;
case Screens.SETTINGS:
screen = withServerDatabase(require('@screens/settings').default);
break;
@@ -197,6 +200,9 @@ Navigation.setLazyComponentRegistrator((screenName) => {
case Screens.SETTINGS_NOTIFICATION_PUSH:
screen = withServerDatabase(require('@screens/settings/notification_push').default);
break;
case Screens.SHARE_FEEDBACK:
screen = withServerDatabase(require('@screens/share_feedback').default);
break;
case Screens.SNACK_BAR: {
const snackBarScreen = withServerDatabase(require('@screens/snack_bar').default);
Navigation.registerComponent(Screens.SNACK_BAR, () =>

View File

@@ -77,7 +77,7 @@ export const loginAnimationOptions = () => {
};
};
export const bottomSheetModalOptions = (theme: Theme, closeButtonId?: string) => {
export const bottomSheetModalOptions = (theme: Theme, closeButtonId?: string): Options => {
if (closeButtonId) {
const closeButton = CompassIcon.getImageSourceSync('close', 24, theme.centerChannelColor);
const closeButtonTestId = `${closeButtonId.replace('close-', 'close.').replace(/-/g, '_')}.button`;
@@ -432,7 +432,7 @@ export async function dismissAllModalsAndPopToScreen(screenId: string, title: st
}
}
export function showModal(name: string, title: string, passProps = {}, options = {}) {
export function showModal(name: string, title: string, passProps = {}, options: Options = {}) {
if (!isScreenRegistered(name)) {
return;
}
@@ -485,7 +485,7 @@ export function showModal(name: string, title: string, passProps = {}, options =
});
}
export function showModalOverCurrentContext(name: string, passProps = {}, options = {}) {
export function showModalOverCurrentContext(name: string, passProps = {}, options: Options = {}) {
const title = '';
let animations;
switch (Platform.OS) {
@@ -599,7 +599,7 @@ export function setButtons(componentId: string, buttons: NavButtons = {leftButto
mergeNavigationOptions(componentId, options);
}
export function showOverlay(name: string, passProps = {}, options = {}) {
export function showOverlay(name: string, passProps = {}, options: Options = {}) {
if (!isScreenRegistered(name)) {
return;
}
@@ -694,6 +694,22 @@ export const showAppForm = async (form: AppForm) => {
showModal(Screens.APPS_FORM, form.title || '', passProps);
};
export const showReviewOverlay = (hasAskedBefore: boolean) => {
showOverlay(
Screens.REVIEW_APP,
{hasAskedBefore},
{overlay: {interceptTouchOutside: true}},
);
};
export const showShareFeedbackOverlay = () => {
showOverlay(
Screens.SHARE_FEEDBACK,
{},
{overlay: {interceptTouchOutside: true}},
);
};
export async function findChannels(title: string, theme: Theme) {
const options: Options = {};
const closeButtonId = 'close-find-channels';

View File

@@ -13,6 +13,7 @@ import {observePermissionForChannel, observePermissionForPost} from '@queries/se
import {observeConfigBooleanValue, observeConfigIntValue, observeConfigValue, observeLicense} from '@queries/servers/system';
import {observeIsCRTEnabled, observeThreadById} from '@queries/servers/thread';
import {observeCurrentUser} from '@queries/servers/user';
import {toMilliseconds} from '@utils/datetime';
import {isMinimumServerVersion} from '@utils/helpers';
import {isSystemMessage} from '@utils/post';
import {getPostIdsForCombinedUserActivityPost} from '@utils/post_list';
@@ -40,7 +41,7 @@ const observeCanEditPost = (database: Database, isOwner: boolean, post: PostMode
}
if (isLicensed && postEditTimeLimit !== -1) {
const timeLeft = (post.createAt + (postEditTimeLimit * 1000)) - Date.now();
const timeLeft = (post.createAt + toMilliseconds({seconds: postEditTimeLimit})) - Date.now();
if (timeLeft <= 0) {
return of$(false);
}

View File

@@ -0,0 +1,230 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useCallback, useMemo, useRef, useState} from 'react';
import {useIntl} from 'react-intl';
import {TouchableWithoutFeedback, View, Text, Alert, TouchableOpacity} from 'react-native';
import InAppReview from 'react-native-in-app-review';
import Animated, {runOnJS, SlideInDown, SlideOutDown} from 'react-native-reanimated';
import {storeDontAskForReview, storeLastAskForReview} from '@actions/app/global';
import {isNPSEnabled} from '@actions/remote/nps';
import Button from '@components/button';
import CompassIcon from '@components/compass_icon';
import ReviewAppIllustration from '@components/illustrations/review_app';
import {useServerUrl} from '@context/server';
import {useTheme} from '@context/theme';
import useBackNavigation from '@hooks/navigate_back';
import {dismissOverlay, showShareFeedbackOverlay} from '@screens/navigation';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import {typography} from '@utils/typography';
type Props = {
hasAskedBefore: boolean;
componentId: string;
}
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
root: {
flex: 1,
backgroundColor: changeOpacity('#000000', 0.50),
justifyContent: 'center',
alignItems: 'center',
},
container: {
flex: 1,
maxWidth: 680,
alignSelf: 'center',
alignContet: 'center',
alignItems: 'center',
justifyContent: 'center',
width: '100%',
flexDirection: 'row',
},
wrapper: {
backgroundColor: theme.centerChannelBg,
borderRadius: 12,
flex: 1,
margin: 10,
opacity: 1,
borderWidth: 1,
borderColor: changeOpacity(theme.centerChannelColor, 0.16),
},
content: {
marginHorizontal: 24,
marginBottom: 24,
alignItems: 'center',
},
buttonsWrapper: {
flexDirection: 'row',
width: '100%',
},
close: {
justifyContent: 'center',
height: 44,
width: 40,
paddingLeft: 16,
paddingTop: 16,
},
title: {
...typography('Heading', 600, 'SemiBold'),
color: theme.centerChannelColor,
marginTop: 0,
marginBottom: 8,
textAlign: 'center',
},
subtitle: {
...typography('Body', 200, 'Regular'),
color: changeOpacity(theme.centerChannelColor, 0.72),
marginBottom: 24,
textAlign: 'center',
},
leftButton: {
flex: 1,
marginRight: 5,
},
rightButton: {
flex: 1,
marginLeft: 5,
},
dontAsk: {
...typography('Body', 75, 'SemiBold'),
color: theme.buttonBg,
marginTop: 24,
},
}));
const ReviewApp = ({
hasAskedBefore,
componentId,
}: Props) => {
const intl = useIntl();
const theme = useTheme();
const styles = getStyleSheet(theme);
const serverUrl = useServerUrl();
const [show, setShow] = useState(true);
const executeAfterDone = useRef<() => void>(() => dismissOverlay(componentId));
const close = useCallback((afterDone: () => void) => {
executeAfterDone.current = afterDone;
storeLastAskForReview();
setShow(false);
}, []);
const onPressYes = useCallback(async () => {
close(async () => {
await dismissOverlay(componentId);
try {
// eslint-disable-next-line new-cap
await InAppReview.RequestInAppReview();
} catch (error) {
Alert.alert(
intl.formatMessage({id: 'rate.error.title', defaultMessage: 'Error'}),
intl.formatMessage({id: 'rate.error.text', defaultMessage: 'There has been an error while opening the review modal.'}),
);
}
});
}, [close, intl, componentId]);
const onPressNeedsWork = useCallback(async () => {
close(async () => {
await dismissOverlay(componentId);
if (await isNPSEnabled(serverUrl)) {
showShareFeedbackOverlay();
}
});
}, [close, componentId, serverUrl]);
const onPressDontAsk = useCallback(() => {
storeDontAskForReview();
close(async () => {
await dismissOverlay(componentId);
});
}, [close, intl, componentId]);
const onPressClose = useCallback(() => {
close(async () => {
await dismissOverlay(componentId);
});
}, [close, componentId]);
useBackNavigation(onPressClose);
const doAfterAnimation = useCallback(() => {
executeAfterDone.current();
}, []);
const slideOut = useMemo(() => SlideOutDown.withCallback((finished: boolean) => {
'worklet';
if (finished) {
runOnJS(doAfterAnimation)();
}
}), []);
return (
<View style={styles.root}>
<View
style={styles.container}
testID='rate_app.screen'
>
{show &&
<Animated.View
style={styles.wrapper}
entering={SlideInDown}
exiting={slideOut}
>
<TouchableOpacity
style={styles.close}
onPress={onPressClose}
>
<CompassIcon
name='close'
size={24}
color={changeOpacity(theme.centerChannelColor, 0.56)}
/>
</TouchableOpacity>
<View style={styles.content}>
<ReviewAppIllustration theme={theme}/>
<Text style={styles.title}>
{intl.formatMessage({id: 'rate.title', defaultMessage: 'Enjoying Mattermost?'})}
</Text>
<Text style={styles.subtitle}>
{intl.formatMessage({id: 'rate.subtitle', defaultMessage: 'Let us know what you think.'})}
</Text>
<View style={styles.buttonsWrapper}>
<Button
theme={theme}
size={'lg'}
emphasis={'tertiary'}
onPress={onPressNeedsWork}
text={intl.formatMessage({id: 'rate.button.needs_work', defaultMessage: 'Needs work'})}
backgroundStyle={styles.leftButton}
/>
<Button
theme={theme}
size={'lg'}
onPress={onPressYes}
text={intl.formatMessage({id: 'rate.button.yes', defaultMessage: 'Love it!'})}
backgroundStyle={styles.rightButton}
/>
</View>
{hasAskedBefore && (
<TouchableWithoutFeedback
onPress={onPressDontAsk}
>
<Text style={styles.dontAsk}>
{intl.formatMessage({id: 'rate.dont_ask_again', defaultMessage: 'Don\'t ask me again'})}
</Text>
</TouchableWithoutFeedback>
)}
</View>
</Animated.View>
}
</View>
</View>
);
};
export default ReviewApp;

View File

@@ -0,0 +1,187 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useCallback, useMemo, useRef, useState} from 'react';
import {useIntl} from 'react-intl';
import {View, Text, TouchableOpacity} from 'react-native';
import Animated, {runOnJS, SlideInDown, SlideOutDown} from 'react-native-reanimated';
import {goToNPSChannel} from '@actions/remote/channel';
import {giveFeedbackAction} from '@actions/remote/nps';
import Button from '@components/button';
import CompassIcon from '@components/compass_icon';
import ShareFeedbackIllustration from '@components/illustrations/share_feedback';
import {useServerUrl} from '@context/server';
import {useTheme} from '@context/theme';
import useBackNavigation from '@hooks/navigate_back';
import {dismissOverlay} from '@screens/navigation';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import {typography} from '@utils/typography';
type Props = {
componentId: string;
}
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
root: {
flex: 1,
backgroundColor: changeOpacity('#000000', 0.50),
justifyContent: 'center',
alignItems: 'center',
},
container: {
flex: 1,
maxWidth: 680,
alignSelf: 'center',
alignContet: 'center',
alignItems: 'center',
justifyContent: 'center',
width: '100%',
flexDirection: 'row',
},
wrapper: {
backgroundColor: theme.centerChannelBg,
borderRadius: 12,
flex: 1,
margin: 10,
opacity: 1,
borderWidth: 1,
borderColor: changeOpacity(theme.centerChannelColor, 0.16),
},
content: {
marginHorizontal: 24,
marginBottom: 24,
alignItems: 'center',
},
buttonsWrapper: {
flexDirection: 'row',
width: '100%',
},
close: {
justifyContent: 'center',
height: 44,
width: 40,
paddingLeft: 16,
paddingTop: 16,
},
title: {
...typography('Heading', 600, 'SemiBold'),
color: theme.centerChannelColor,
marginTop: 0,
marginBottom: 8,
textAlign: 'center',
},
subtitle: {
...typography('Body', 200, 'Regular'),
color: changeOpacity(theme.centerChannelColor, 0.72),
marginBottom: 24,
textAlign: 'center',
},
leftButton: {
flex: 1,
marginRight: 5,
},
rightButton: {
flex: 1,
marginLeft: 5,
},
}));
const ShareFeedback = ({
componentId,
}: Props) => {
const intl = useIntl();
const theme = useTheme();
const styles = getStyleSheet(theme);
const serverUrl = useServerUrl();
const [show, setShow] = useState(true);
const executeAfterDone = useRef<() => void>(() => dismissOverlay(componentId));
const close = useCallback((afterDone: () => void) => {
executeAfterDone.current = afterDone;
setShow(false);
}, []);
const onPressYes = useCallback(async () => {
close(async () => {
await dismissOverlay(componentId);
await goToNPSChannel(serverUrl);
giveFeedbackAction(serverUrl);
});
}, [close, intl, serverUrl]);
const onPressNo = useCallback(() => {
close(() => dismissOverlay(componentId));
}, [close, componentId]);
useBackNavigation(onPressNo);
const doAfterAnimation = useCallback(() => {
executeAfterDone.current();
}, []);
const slideOut = useMemo(() => SlideOutDown.withCallback((finished: boolean) => {
'worklet';
if (finished) {
runOnJS(doAfterAnimation)();
}
}), []);
return (
<View style={styles.root}>
<View
style={styles.container}
testID='rate_app.screen'
>
{show &&
<Animated.View
style={styles.wrapper}
entering={SlideInDown}
exiting={slideOut}
>
<TouchableOpacity
style={styles.close}
onPress={onPressNo}
>
<CompassIcon
name='close'
size={24}
color={changeOpacity(theme.centerChannelColor, 0.56)}
/>
</TouchableOpacity>
<View style={styles.content}>
<ShareFeedbackIllustration theme={theme}/>
<Text style={styles.title}>
{intl.formatMessage({id: 'share_feedback.title', defaultMessage: 'Would you share your feedback?'})}
</Text>
<Text style={styles.subtitle}>
{intl.formatMessage({id: 'share_feedback.subtitle', defaultMessage: 'We\'d love to hear how we can make your experience better.'})}
</Text>
<View style={styles.buttonsWrapper}>
<Button
theme={theme}
size={'lg'}
emphasis={'tertiary'}
onPress={onPressNo}
text={intl.formatMessage({id: 'share_feedback.button.no', defaultMessage: 'No, thanks'})}
backgroundStyle={styles.leftButton}
/>
<Button
theme={theme}
size={'lg'}
onPress={onPressYes}
text={intl.formatMessage({id: 'share_feedback.button.yes', defaultMessage: 'Yes'})}
backgroundStyle={styles.rightButton}
/>
</View>
</View>
</Animated.View>
}
</View>
</View>
);
};
export default ShareFeedback;

View File

@@ -5,22 +5,6 @@ import {StyleProp, StyleSheet, TextStyle, ViewStyle} from 'react-native';
import {blendColors, changeOpacity} from '@utils/theme';
type ButtonSize = 'xs' | 's' | 'm' | 'lg'
type ButtonEmphasis = 'primary' | 'secondary' | 'tertiary' | 'link'
type ButtonType = 'default' | 'destructive' | 'inverted' | 'disabled'
type ButtonState = 'default' | 'hover' | 'active' | 'focus'
type ButtonSizes = {
[key in ButtonSize]: ViewStyle
}
type BackgroundStyles = {
[key in ButtonEmphasis]: {
[ke in ButtonType]: {
[k in ButtonState]: ViewStyle
}
}
}
/**
* Returns the appropriate Style object for <View style={} />
*

View File

@@ -25,3 +25,10 @@ export function isYesterday(date: Date): boolean {
return isSameDate(date, yesterday);
}
export function toMilliseconds({days, hours, minutes, seconds}: {days?: number; hours?: number; minutes?: number; seconds?: number}) {
const totalHours = ((days || 0) * 24) + (hours || 0);
const totalMinutes = (totalHours * 60) + (minutes || 0);
const totalSeconds = (totalMinutes * 60) + (seconds || 0);
return totalSeconds * 1000;
}

View File

@@ -7,7 +7,15 @@ import {Navigation, Options} from 'react-native-navigation';
import {Screens} from '@constants';
export const appearanceControlledScreens = new Set([Screens.SERVER, Screens.LOGIN, Screens.FORGOT_PASSWORD, Screens.MFA, Screens.SSO]);
export const appearanceControlledScreens = new Set([
Screens.SERVER,
Screens.LOGIN,
Screens.FORGOT_PASSWORD,
Screens.MFA,
Screens.SSO,
Screens.REVIEW_APP,
Screens.SHARE_FEEDBACK,
]);
export function mergeNavigationOptions(componentId: string, options: Options) {
Navigation.mergeOptions(componentId, options);

View File

@@ -4,6 +4,7 @@
import moment from 'moment-timezone';
import {Post} from '@constants';
import {toMilliseconds} from '@utils/datetime';
import {isFromWebhook} from '@utils/post';
import type PostModel from '@typings/database/models/servers/post';
@@ -205,11 +206,11 @@ export function selectOrderedPosts(
// Push on a date header if the last post was on a different day than the current one
const postDate = new Date(post.createAt);
if (timezoneEnabled) {
const currentOffset = postDate.getTimezoneOffset() * 60 * 1000;
const currentOffset = toMilliseconds({minutes: postDate.getTimezoneOffset()});
if (currentTimezone) {
const zone = moment.tz.zone(currentTimezone);
if (zone) {
const timezoneOffset = zone.utcOffset(post.createAt) * 60 * 1000;
const timezoneOffset = toMilliseconds({minutes: zone.utcOffset(post.createAt)});
postDate.setTime(post.createAt + (currentOffset - timezoneOffset));
}
}

57
app/utils/reviews.ts Normal file
View File

@@ -0,0 +1,57 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import InAppReview from 'react-native-in-app-review';
import {storeFirstLaunch} from '@actions/app/global';
import LocalConfig from '@assets/config.json';
import {General, Launch} from '@constants';
import {getDontAskForReview, getFirstLaunch, getLastAskedForReview} from '@queries/app/global';
import {areAllServersSupported} from '@queries/app/servers';
import {showReviewOverlay} from '@screens/navigation';
export const tryRunAppReview = async (launchType: string, coldStart?: boolean) => {
if (!LocalConfig.ShowReview) {
return;
}
if (!coldStart) {
return;
}
if (launchType !== Launch.Normal) {
return;
}
if (!InAppReview.isAvailable()) {
return;
}
const supported = await areAllServersSupported();
if (!supported) {
return;
}
const dontAsk = await getDontAskForReview();
if (dontAsk) {
return;
}
const lastReviewed = await getLastAskedForReview();
if (lastReviewed) {
if (Date.now() - lastReviewed > General.TIME_TO_NEXT_REVIEW) {
showReviewOverlay(true);
}
return;
}
const firstLaunch = await getFirstLaunch();
if (!firstLaunch) {
storeFirstLaunch();
return;
}
if ((Date.now() - firstLaunch) > General.TIME_TO_FIRST_REVIEW) {
showReviewOverlay(false);
}
};

View File

@@ -35,5 +35,6 @@
"ShowSentryDebugOptions": false,
"CustomRequestHeaders": {}
"CustomRequestHeaders": {},
"ShowReview": false
}

View File

@@ -707,6 +707,13 @@
"post.reactions.title": "Reactions",
"posts_view.newMsg": "New Messages",
"public_link_copied": "Link copied to clipboard",
"rate.button.needs_work": "Needs work",
"rate.button.yes": "Love it!",
"rate.dont_ask_again": "Don't ask me again",
"rate.error.text": "There has been an error while opening the review modal.",
"rate.error.title": "Error",
"rate.subtitle": "Let us know what you think.",
"rate.title": "Enjoying Mattermost?",
"saved_messages.empty.paragraph": "To save something for later, long-press on a message and choose Save from the menu. Saved messages are only visible to you.",
"saved_messages.empty.title": "No saved messages yet",
"screen.mentions.subtitle": "Messages you've been mentioned in",
@@ -787,6 +794,10 @@
"settings.notice_text": "Mattermost is made possible by the open source software used in our {platform} and {mobile}.",
"settings.notifications": "Notifications",
"settings.save": "Save",
"share_feedback.button.no": "No, thanks",
"share_feedback.button.yes": "Yes",
"share_feedback.subtitle": "We'd love to hear how we can make your experience better.",
"share_feedback.title": "Would you share your feedback?",
"smobile.search.recent_title": "Recent searches in {teamName}",
"snack.bar.favorited.channel": "This channel was favorited",
"snack.bar.link.copied": "Link copied to clipboard",

View File

@@ -165,6 +165,11 @@ lane :configure do
json['SentryDsnAndroid'] = ENV['SENTRY_DSN_ANDROID']
end
# Configure show app review
if ENV['SHOW_REVIEW'] == 'true'
json['ShowReview'] = true
end
# Save the config.json file
save_json_as_file('../dist/assets/config.json', json)

View File

@@ -345,6 +345,8 @@ PODS:
- React
- react-native-image-picker (4.10.0):
- React-Core
- react-native-in-app-review (4.2.1):
- React-Core
- react-native-netinfo (9.3.6):
- React-Core
- react-native-network-client (1.0.0):
@@ -606,6 +608,7 @@ DEPENDENCIES:
- "react-native-emm (from `../node_modules/@mattermost/react-native-emm`)"
- react-native-hw-keyboard-event (from `../node_modules/react-native-hw-keyboard-event`)
- react-native-image-picker (from `../node_modules/react-native-image-picker`)
- react-native-in-app-review (from `../node_modules/react-native-in-app-review`)
- "react-native-netinfo (from `../node_modules/@react-native-community/netinfo`)"
- "react-native-network-client (from `../node_modules/@mattermost/react-native-network-client`)"
- react-native-notifications (from `../node_modules/react-native-notifications`)
@@ -753,6 +756,8 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native-hw-keyboard-event"
react-native-image-picker:
:path: "../node_modules/react-native-image-picker"
react-native-in-app-review:
:path: "../node_modules/react-native-in-app-review"
react-native-netinfo:
:path: "../node_modules/@react-native-community/netinfo"
react-native-network-client:
@@ -907,6 +912,7 @@ SPEC CHECKSUMS:
react-native-emm: c5b7dcffde34ce345fd0c16d66b587e611a3d31f
react-native-hw-keyboard-event: b517cefb8d5c659a38049c582de85ff43337dc53
react-native-image-picker: 4bc9ed38c8be255b515d8c88babbaf74973f91a8
react-native-in-app-review: a073f67c5f3392af6ea7fb383217cdb1aa2aa726
react-native-netinfo: f80db8cac2151405633324cb645c60af098ee461
react-native-network-client: 2b1370725abbbc1a7bfb1845d44bd3d4e73041b2
react-native-notifications: 83b4fd4a127a6c918fc846cae90da60f84819e44

11
package-lock.json generated
View File

@@ -67,6 +67,7 @@
"react-native-haptic-feedback": "1.14.0",
"react-native-hw-keyboard-event": "0.0.4",
"react-native-image-picker": "4.10.0",
"react-native-in-app-review": "4.2.1",
"react-native-incall-manager": "github:cpoile/react-native-incall-manager",
"react-native-keyboard-aware-scroll-view": "0.9.5",
"react-native-keyboard-tracking-view": "5.7.0",
@@ -18225,6 +18226,11 @@
"react-native": "*"
}
},
"node_modules/react-native-in-app-review": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/react-native-in-app-review/-/react-native-in-app-review-4.2.1.tgz",
"integrity": "sha512-Bny/ukRhkSSzlsbVpB3vsIuWYuOHBPlxguNwZ0TWK+7IQq8/vTRDf17y1P/4+jMIjBO0WNJCzBxHkXdnlhoTmQ=="
},
"node_modules/react-native-incall-manager": {
"version": "4.0.0",
"resolved": "git+ssh://git@github.com/cpoile/react-native-incall-manager.git#6b66ae7bab194c82573c7b3891b0ac3af71d424e",
@@ -35504,6 +35510,11 @@
"integrity": "sha512-QNK4ZnFLD+BdiM1QL1jh9GUALhRXm3MV97crhnTCSMv50H6E4tq3ozgfyxF5uKz0cECbNkWswg4sjgqZKEJtww==",
"requires": {}
},
"react-native-in-app-review": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/react-native-in-app-review/-/react-native-in-app-review-4.2.1.tgz",
"integrity": "sha512-Bny/ukRhkSSzlsbVpB3vsIuWYuOHBPlxguNwZ0TWK+7IQq8/vTRDf17y1P/4+jMIjBO0WNJCzBxHkXdnlhoTmQ=="
},
"react-native-incall-manager": {
"version": "git+ssh://git@github.com/cpoile/react-native-incall-manager.git#6b66ae7bab194c82573c7b3891b0ac3af71d424e",
"from": "react-native-incall-manager@github:cpoile/react-native-incall-manager",

View File

@@ -64,6 +64,7 @@
"react-native-haptic-feedback": "1.14.0",
"react-native-hw-keyboard-event": "0.0.4",
"react-native-image-picker": "4.10.0",
"react-native-in-app-review": "4.2.1",
"react-native-incall-manager": "github:cpoile/react-native-incall-manager",
"react-native-keyboard-aware-scroll-view": "0.9.5",
"react-native-keyboard-tracking-view": "5.7.0",

18
types/components/button.d.ts vendored Normal file
View File

@@ -0,0 +1,18 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
type ButtonSize = 'xs' | 's' | 'm' | 'lg'
type ButtonEmphasis = 'primary' | 'secondary' | 'tertiary' | 'link'
type ButtonType = 'default' | 'destructive' | 'inverted' | 'disabled'
type ButtonState = 'default' | 'hover' | 'active' | 'focus'
type ButtonSizes = {
[key in ButtonSize]: ViewStyle
}
type BackgroundStyles = {
[key in ButtonEmphasis]: {
[ke in ButtonType]: {
[k in ButtonState]: ViewStyle
}
}
}

View File

@@ -44,4 +44,5 @@ export interface LaunchProps {
serverUrl?: string;
displayName?: string;
time?: number;
coldStart?: boolean;
}