forked from Ivasoft/mattermost-mobile
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:
committed by
GitHub
parent
81dcfc817b
commit
5fae120826
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -124,3 +124,4 @@ export const getRedirectLocation = async (serverUrl: string, link: string) => {
|
||||
return {error};
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
30
app/actions/remote/nps.ts
Normal file
30
app/actions/remote/nps.ts
Normal 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};
|
||||
}
|
||||
};
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
19
app/client/rest/nps.ts
Normal 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;
|
||||
61
app/components/button/index.tsx
Normal file
61
app/components/button/index.tsx
Normal 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;
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
197
app/components/illustrations/review_app.tsx
Normal file
197
app/components/illustrations/review_app.tsx
Normal 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;
|
||||
49
app/components/illustrations/share_feedback.tsx
Normal file
49
app/components/illustrations/share_feedback.tsx
Normal 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;
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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',
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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> = {};
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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, () =>
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
230
app/screens/review_app/index.tsx
Normal file
230
app/screens/review_app/index.tsx
Normal 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;
|
||||
187
app/screens/share_feedback/index.tsx
Normal file
187
app/screens/share_feedback/index.tsx
Normal 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;
|
||||
@@ -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={} />
|
||||
*
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
57
app/utils/reviews.ts
Normal 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);
|
||||
}
|
||||
};
|
||||
@@ -35,5 +35,6 @@
|
||||
|
||||
"ShowSentryDebugOptions": false,
|
||||
|
||||
"CustomRequestHeaders": {}
|
||||
"CustomRequestHeaders": {},
|
||||
"ShowReview": false
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
11
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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
18
types/components/button.d.ts
vendored
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -44,4 +44,5 @@ export interface LaunchProps {
|
||||
serverUrl?: string;
|
||||
displayName?: string;
|
||||
time?: number;
|
||||
coldStart?: boolean;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user