forked from Ivasoft/mattermost-mobile
Gekidou - Account Screen (#5708)
* Added DrawerItem component
* WIP Account Screen
* Added react-native-paper
* Added StatusLabel Component
* Extracted i18n
* TS fix DrawerItem component
* WIP Account Screen
* Added server name label under log out
* Updated translation
* WIP
* Fixes the Offline text style
* Added Metropolis fonts
* WIP
* Typo clean up
* WIP
* WIP
* WIP
* Added server display name
* Writing OpenSans properly
* WIP
* WIP
* Added OptionsModal
* Opening OptionsModal
* Added translation keys
* Writes status to local db
* Fix missing translation
* Fix OptionModal not dismissing
* Pushing status to server
* Refactored
* Added CustomStatusExpiry component
* Added sub components
* Added CustomLabel
* CustomStatus WIP
* Added Custom Status screen WIP
* WIP - unsetCustomStatus and CustomStatus constant
* WIP
* WIP
* WIP
* WIP
* WIP
* WIP
* WIP
* Retrieving RecentCustomStatuses from Preferences table
* WIP
* WIP
* WIP
* Added Clear After Modal
* WIP - Transations
* WIP
* Done with showing modal cst
* wip
* Clear After Modal - DONE
* fix
* Added missing API calls
* wip
* Causing screen refresh
* wip
* WIP
* WIP
* WIP
* Code clean up
* Added OOO alert box
* Refactored Options-Item
* Refactored OptionsModalList component
* Opening 'status' in BottomSheet instead of OptionsModal
* AddReaction screen - WIP
* Add Reaction screen - WIP
* Added EmojiPickerRow
* Added @components/emoji_picker - WIP
* Emoji Picker - WIP
* WIP
* WIP
* WIP
* SectionList - WIP
* Installed react-native-section_list_get_item_layout
* Adding API calls - WIP
* WIP
* Search Bar component - WIP
* WIP
* WIP
* WIP
* Rendering Emoticons now - have to tackle some fixmes
* Code clean up
* Code clean up - WIP
* Code clean up
* WIP
* Major clean up
* wip
* WIP
* Fix rendering issue with SectionIcons and SearchBar
* Tackled the CustomEmojiPage
* Code clean up
* WIP
* Done with loading User Profiles for Custom Emoji
* Code clean up
* Code Clean up
* Fix screen Account
* Added missing sql file for IOS Pod
* Updated Podfile.lock
* Using queryConfig instead of queryCommonSystemValues
* Fix - Custom status
* Fix - Custom Status - Error
* Fix - Clear Pass Status - WIP
* Fix - Custom Status Clear
* Need to fix CST clear
* WIP
* Status clear - working
* Using catchError operator
* remove unnecessary prop
* Status BottomSheet now has colored indicators
* Added KeyboardTrackingView from 'react-native-keyboard-tracking-view'
* Code clean up
* WIP
* code clean up
* Added a safety check
* Fix - Display suggestions
* Code clean up based on PR Review
* Code clean up
* Code clean up
* Code clean up
* Corrections
* Fix tsc
* TS fix
* Removed unnecessary prop
* Fix SearchBar Ts
* Updated tests
* Delete search_bar.test.js.snap
* Merge branch 'gekidou' into gekidou_account_screen
* Revert "Merge branch 'gekidou' into gekidou_account_screen"
This reverts commit 5defc31321.
* Fix fonts
* Refactor home account screen
* fix theme provider
* refactor bottom sheet
* remove paper provider
* update drawer item snapshots
* Remove options modal screen
* remove react-native-ui-lib dependency
* Refactor & fix custom status & navigation (including tablet)
* Refactor emoji picker
Co-authored-by: Avinash Lingaloo <>
Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
This commit is contained in:
43
app/actions/local/reactions.ts
Normal file
43
app/actions/local/reactions.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {MM_TABLES, SYSTEM_IDENTIFIERS} from '@constants/database';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {safeParseJSON} from '@utils/helpers';
|
||||
|
||||
import type SystemModel from '@typings/database/models/servers/system';
|
||||
|
||||
const MAXIMUM_RECENT_EMOJI = 27;
|
||||
|
||||
export const addRecentReaction = async (serverUrl: string, emojiName: string, prepareRecordsOnly = false) => {
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (!operator) {
|
||||
return {error: `${serverUrl} database not found`};
|
||||
}
|
||||
|
||||
let recent: string[] = [];
|
||||
try {
|
||||
const emojis = await operator.database.get<SystemModel>(MM_TABLES.SERVER.SYSTEM).find(SYSTEM_IDENTIFIERS.RECENT_REACTIONS);
|
||||
recent.push(...(safeParseJSON(emojis.value) as string[] || []));
|
||||
} catch {
|
||||
// no previous values.. continue
|
||||
}
|
||||
|
||||
try {
|
||||
const recentEmojis = new Set(recent);
|
||||
if (recentEmojis.has(emojiName)) {
|
||||
recentEmojis.delete(emojiName);
|
||||
}
|
||||
recent = Array.from(recentEmojis);
|
||||
recent.unshift(emojiName);
|
||||
return operator.handleSystem({
|
||||
systems: [{
|
||||
id: SYSTEM_IDENTIFIERS.RECENT_REACTIONS,
|
||||
value: JSON.stringify(recent.slice(0, MAXIMUM_RECENT_EMOJI)),
|
||||
}],
|
||||
prepareRecordsOnly,
|
||||
});
|
||||
} catch (error) {
|
||||
return {error};
|
||||
}
|
||||
};
|
||||
92
app/actions/local/user.ts
Normal file
92
app/actions/local/user.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {SYSTEM_IDENTIFIERS} from '@constants/database';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {queryRecentCustomStatuses} from '@queries/servers/system';
|
||||
import {queryUserById} from '@queries/servers/user';
|
||||
|
||||
import {addRecentReaction} from './reactions';
|
||||
|
||||
import type Model from '@nozbe/watermelondb/Model';
|
||||
import type UserModel from '@typings/database/models/servers/user';
|
||||
|
||||
export const updateLocalCustomStatus = async (serverUrl: string, user: UserModel, customStatus?: UserCustomStatus) => {
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (!operator) {
|
||||
return {error: `${serverUrl} database not found`};
|
||||
}
|
||||
|
||||
const models: Model[] = [];
|
||||
const currentProps = {...user.props, customStatus: customStatus || {}};
|
||||
const userModel = user.prepareUpdate((u: UserModel) => {
|
||||
u.props = currentProps;
|
||||
});
|
||||
|
||||
models.push(userModel);
|
||||
if (customStatus) {
|
||||
const recent = await updateRecentCustomStatuses(serverUrl, customStatus, true);
|
||||
if (Array.isArray(recent)) {
|
||||
models.push(...recent);
|
||||
}
|
||||
|
||||
if (customStatus.emoji) {
|
||||
const recentEmojis = await addRecentReaction(serverUrl, customStatus.emoji, true);
|
||||
if (Array.isArray(recentEmojis)) {
|
||||
models.push(...recentEmojis);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await operator.batchRecords(models);
|
||||
return {};
|
||||
};
|
||||
|
||||
export const updateRecentCustomStatuses = async (serverUrl: string, customStatus: UserCustomStatus, prepareRecordsOnly = false, remove = false) => {
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (!operator) {
|
||||
return {error: `${serverUrl} database not found`};
|
||||
}
|
||||
|
||||
const recent = await queryRecentCustomStatuses(operator.database);
|
||||
const recentStatuses = (recent ? recent.value : []) as UserCustomStatus[];
|
||||
const index = recentStatuses.findIndex((cs) => (
|
||||
cs.emoji === customStatus.emoji &&
|
||||
cs.text === customStatus.text &&
|
||||
cs.duration === customStatus.duration
|
||||
));
|
||||
|
||||
if (index !== -1) {
|
||||
recentStatuses.splice(index, 1);
|
||||
}
|
||||
|
||||
if (!remove) {
|
||||
recentStatuses.unshift(customStatus);
|
||||
}
|
||||
|
||||
return operator.handleSystem({
|
||||
systems: [{
|
||||
id: SYSTEM_IDENTIFIERS.RECENT_CUSTOM_STATUS,
|
||||
value: JSON.stringify(recentStatuses),
|
||||
}],
|
||||
prepareRecordsOnly,
|
||||
});
|
||||
};
|
||||
|
||||
export const updateUserPresence = async (serverUrl: string, userStatus: UserStatus) => {
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (!operator) {
|
||||
return {error: `${serverUrl} database not found`};
|
||||
}
|
||||
|
||||
const user = await queryUserById(operator.database, userStatus.user_id);
|
||||
if (user) {
|
||||
user.prepareUpdate((record) => {
|
||||
record.status = userStatus.status;
|
||||
});
|
||||
await operator.batchRecords([user]);
|
||||
}
|
||||
|
||||
return {};
|
||||
};
|
||||
|
||||
71
app/actions/remote/custom_emoji.ts
Normal file
71
app/actions/remote/custom_emoji.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {forceLogoutIfNecessary} from '@actions/remote/session';
|
||||
import {Client} from '@client/rest';
|
||||
import {Emoji, General} from '@constants';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import NetworkManager from '@init/network_manager';
|
||||
import {queryCustomEmojisByName} from '@queries/servers/custom_emoji';
|
||||
|
||||
export const fetchCustomEmojis = async (serverUrl: string, page = 0, perPage = General.PAGE_SIZE_DEFAULT, sort = Emoji.SORT_BY_NAME) => {
|
||||
let client: Client;
|
||||
try {
|
||||
client = NetworkManager.getClient(serverUrl);
|
||||
} catch (error) {
|
||||
return {error};
|
||||
}
|
||||
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (!operator) {
|
||||
return {error: `${serverUrl} database not found`};
|
||||
}
|
||||
|
||||
try {
|
||||
const data = await client.getCustomEmojis(page, perPage, sort);
|
||||
if (data.length) {
|
||||
await operator.handleCustomEmojis({
|
||||
emojis: data,
|
||||
prepareRecordsOnly: false,
|
||||
});
|
||||
}
|
||||
|
||||
return {data};
|
||||
} catch (error) {
|
||||
forceLogoutIfNecessary(serverUrl, error as ClientErrorProps);
|
||||
return {error};
|
||||
}
|
||||
};
|
||||
|
||||
export const searchCustomEmojis = async (serverUrl: string, term: string) => {
|
||||
let client: Client;
|
||||
try {
|
||||
client = NetworkManager.getClient(serverUrl);
|
||||
} catch (error) {
|
||||
return {error};
|
||||
}
|
||||
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (!operator) {
|
||||
return {error: `${serverUrl} database not found`};
|
||||
}
|
||||
|
||||
try {
|
||||
const data = await client.searchCustomEmoji(term);
|
||||
if (data.length) {
|
||||
const names = data.map((c) => c.name);
|
||||
const exist = await queryCustomEmojisByName(operator.database, names);
|
||||
const emojis = data.filter((d) => exist.findIndex((c) => c.name === d.name));
|
||||
if (emojis.length) {
|
||||
await operator.handleCustomEmojis({
|
||||
emojis,
|
||||
prepareRecordsOnly: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
return {data};
|
||||
} catch (error) {
|
||||
forceLogoutIfNecessary(serverUrl, error as ClientErrorProps);
|
||||
return {error};
|
||||
}
|
||||
};
|
||||
@@ -1,9 +1,10 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Q} from '@nozbe/watermelondb';
|
||||
import {Model, Q} from '@nozbe/watermelondb';
|
||||
|
||||
import {MM_TABLES, SYSTEM_IDENTIFIERS} from '@constants/database';
|
||||
import {addRecentReaction} from '@actions/local/reactions';
|
||||
import {MM_TABLES} from '@constants/database';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import NetworkManager from '@init/network_manager';
|
||||
import {queryCurrentUserId} from '@queries/servers/system';
|
||||
@@ -11,7 +12,6 @@ import {queryCurrentUserId} from '@queries/servers/system';
|
||||
import {forceLogoutIfNecessary} from './session';
|
||||
|
||||
import type {Client} from '@client/rest';
|
||||
import type SystemModel from '@typings/database/models/servers/system';
|
||||
|
||||
export const addReaction = async (serverUrl: string, postId: string, emojiName: string) => {
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
@@ -29,16 +29,25 @@ export const addReaction = async (serverUrl: string, postId: string, emojiName:
|
||||
try {
|
||||
const currentUserId = await queryCurrentUserId(operator.database);
|
||||
const reaction = await client.addReaction(currentUserId, postId, emojiName);
|
||||
const models: Model[] = [];
|
||||
|
||||
await operator.handleReactions({
|
||||
const reactions = await operator.handleReactions({
|
||||
postsReactions: [{
|
||||
post_id: postId,
|
||||
reactions: [reaction],
|
||||
}],
|
||||
prepareRecordsOnly: false,
|
||||
prepareRecordsOnly: true,
|
||||
});
|
||||
models.push(...reactions);
|
||||
|
||||
addRecentReaction(serverUrl, emojiName);
|
||||
const recent = await addRecentReaction(serverUrl, emojiName, true);
|
||||
if (Array.isArray(recent)) {
|
||||
models.push(...recent);
|
||||
}
|
||||
|
||||
if (models.length) {
|
||||
await operator.batchRecords(models);
|
||||
}
|
||||
|
||||
return {reaction};
|
||||
} catch (error) {
|
||||
@@ -83,33 +92,3 @@ export const removeReaction = async (serverUrl: string, postId: string, emojiNam
|
||||
return {error};
|
||||
}
|
||||
};
|
||||
|
||||
export const addRecentReaction = async (serverUrl: string, emojiName: string) => {
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (!operator) {
|
||||
return {error: `${serverUrl} database not found`};
|
||||
}
|
||||
|
||||
const recent = [];
|
||||
try {
|
||||
const emojis = await operator.database.get(MM_TABLES.SERVER.SYSTEM).find(SYSTEM_IDENTIFIERS.RECENT_REACTIONS) as SystemModel;
|
||||
recent.push(...emojis.value);
|
||||
} catch {
|
||||
// no previous values.. continue
|
||||
}
|
||||
|
||||
try {
|
||||
recent.unshift(emojiName);
|
||||
await operator.handleSystem({
|
||||
systems: [{
|
||||
id: SYSTEM_IDENTIFIERS.RECENT_REACTIONS,
|
||||
value: JSON.stringify(recent),
|
||||
}],
|
||||
prepareRecordsOnly: false,
|
||||
});
|
||||
|
||||
return {error: undefined};
|
||||
} catch (error) {
|
||||
return {error};
|
||||
}
|
||||
};
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
import {Q} from '@nozbe/watermelondb';
|
||||
|
||||
import {updateRecentCustomStatuses, updateUserPresence} from '@actions/local/user';
|
||||
import {fetchRolesIfNeeded} from '@actions/remote/role';
|
||||
import {Database, General} from '@constants';
|
||||
import DatabaseManager from '@database/manager';
|
||||
@@ -322,7 +323,7 @@ export const fetchStatusByIds = async (serverUrl: string, userIds: string[], fet
|
||||
const users = await database.get(Database.MM_TABLES.SERVER.USER).query(Q.where('id', Q.oneOf(userIds))).fetch() as UserModel[];
|
||||
for (const user of users) {
|
||||
const status = statuses.find((s) => s.user_id === user.id);
|
||||
user.prepareSatus(status?.status || General.OFFLINE);
|
||||
user.prepareStatus(status?.status || General.OFFLINE);
|
||||
}
|
||||
|
||||
await operator.batchRecords(users);
|
||||
@@ -357,7 +358,6 @@ export const fetchUsersByIds = async (serverUrl: string, userIds: string[], fetc
|
||||
const exisingUsers = await queryUsersById(operator.database, userIds);
|
||||
const usersToLoad = userIds.filter((id) => (id !== currentUserId && !exisingUsers.find((u) => u.id === id)));
|
||||
const users = await client.getProfilesByIds([...new Set(usersToLoad)]);
|
||||
|
||||
if (!fetchOnly) {
|
||||
await operator.handleUsers({
|
||||
users,
|
||||
@@ -408,7 +408,7 @@ export const fetchUsersByUsernames = async (serverUrl: string, usernames: string
|
||||
}
|
||||
};
|
||||
|
||||
export const fetchMissinProfilesByIds = async (serverUrl: string, userIds: string[]) => {
|
||||
export const fetchMissingProfilesByIds = async (serverUrl: string, userIds: string[]) => {
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (!operator) {
|
||||
return {error: `${serverUrl} database not found`};
|
||||
@@ -427,7 +427,7 @@ export const fetchMissinProfilesByIds = async (serverUrl: string, userIds: strin
|
||||
}
|
||||
};
|
||||
|
||||
export const fetchMissinProfilesByUsernames = async (serverUrl: string, usernames: string[]) => {
|
||||
export const fetchMissingProfilesByUsernames = async (serverUrl: string, usernames: string[]) => {
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (!operator) {
|
||||
return {error: `${serverUrl} database not found`};
|
||||
@@ -445,3 +445,82 @@ export const fetchMissinProfilesByUsernames = async (serverUrl: string, username
|
||||
return {error};
|
||||
}
|
||||
};
|
||||
|
||||
export const setStatus = async (serverUrl: string, status: UserStatus) => {
|
||||
let client: Client;
|
||||
try {
|
||||
client = NetworkManager.getClient(serverUrl);
|
||||
} catch (error) {
|
||||
return {error};
|
||||
}
|
||||
|
||||
try {
|
||||
const data = await client.updateStatus(status);
|
||||
updateUserPresence(serverUrl, status);
|
||||
|
||||
return {
|
||||
data,
|
||||
};
|
||||
} catch (error: any) {
|
||||
forceLogoutIfNecessary(serverUrl, error);
|
||||
return {error};
|
||||
}
|
||||
};
|
||||
|
||||
export const updateCustomStatus = async (serverUrl: string, user: UserModel, customStatus: UserCustomStatus) => {
|
||||
let client: Client;
|
||||
try {
|
||||
client = NetworkManager.getClient(serverUrl);
|
||||
} catch (error) {
|
||||
return {error};
|
||||
}
|
||||
|
||||
try {
|
||||
await client.updateCustomStatus(customStatus);
|
||||
return {data: true};
|
||||
} catch (error) {
|
||||
return {error};
|
||||
}
|
||||
};
|
||||
|
||||
export const removeRecentCustomStatus = async (serverUrl: string, customStatus: UserCustomStatus) => {
|
||||
let client: Client;
|
||||
try {
|
||||
client = NetworkManager.getClient(serverUrl);
|
||||
} catch (error) {
|
||||
return {error};
|
||||
}
|
||||
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (!operator) {
|
||||
return {error: `${serverUrl} database not found`};
|
||||
}
|
||||
|
||||
updateRecentCustomStatuses(serverUrl, customStatus, false, true);
|
||||
|
||||
try {
|
||||
await client.removeRecentCustomStatus(customStatus);
|
||||
} catch (error) {
|
||||
return {error};
|
||||
}
|
||||
|
||||
return {data: true};
|
||||
};
|
||||
|
||||
export const unsetCustomStatus = async (serverUrl: string) => {
|
||||
let client: Client;
|
||||
|
||||
try {
|
||||
client = NetworkManager.getClient(serverUrl);
|
||||
} catch (error) {
|
||||
return {error};
|
||||
}
|
||||
|
||||
try {
|
||||
await client.unsetCustomStatus();
|
||||
} catch (error) {
|
||||
return {error};
|
||||
}
|
||||
|
||||
return {data: true};
|
||||
};
|
||||
|
||||
@@ -41,6 +41,9 @@ export interface ClientUsersMix {
|
||||
getStatusesByIds: (userIds: string[]) => Promise<UserStatus[]>;
|
||||
getStatus: (userId: string) => Promise<UserStatus>;
|
||||
updateStatus: (status: UserStatus) => Promise<UserStatus>;
|
||||
updateCustomStatus: (customStatus: UserCustomStatus) => Promise<{status: string}>;
|
||||
unsetCustomStatus: () => Promise<{status: string}>;
|
||||
removeRecentCustomStatus: (customStatus: UserCustomStatus) => Promise<{status: string}>;
|
||||
}
|
||||
|
||||
const ClientUsers = (superclass: any) => class extends superclass {
|
||||
@@ -380,6 +383,27 @@ const ClientUsers = (superclass: any) => class extends superclass {
|
||||
{method: 'put', body: status},
|
||||
);
|
||||
};
|
||||
|
||||
updateCustomStatus = async (customStatus: UserCustomStatus) => {
|
||||
return this.doFetch(
|
||||
`${this.getUserRoute('me')}/status/custom`,
|
||||
{method: 'put', body: customStatus},
|
||||
);
|
||||
};
|
||||
|
||||
unsetCustomStatus = async () => {
|
||||
return this.doFetch(
|
||||
`${this.getUserRoute('me')}/status/custom`,
|
||||
{method: 'delete'},
|
||||
);
|
||||
};
|
||||
|
||||
removeRecentCustomStatus = async (customStatus: UserCustomStatus) => {
|
||||
return this.doFetch(
|
||||
`${this.getUserRoute('me')}/status/custom/recent/delete`,
|
||||
{method: 'post', body: customStatus},
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
export default ClientUsers;
|
||||
|
||||
@@ -4,8 +4,18 @@ exports[`components/custom_status/custom_status_emoji should match snapshot 1`]
|
||||
<Text
|
||||
testID="custom_status_emoji.calendar"
|
||||
>
|
||||
<Text>
|
||||
|
||||
<Text
|
||||
style={
|
||||
Array [
|
||||
undefined,
|
||||
Object {
|
||||
"color": "#000",
|
||||
"fontSize": 16,
|
||||
},
|
||||
]
|
||||
}
|
||||
>
|
||||
📆
|
||||
</Text>
|
||||
</Text>
|
||||
`;
|
||||
@@ -14,8 +24,18 @@ exports[`components/custom_status/custom_status_emoji should match snapshot with
|
||||
<Text
|
||||
testID="custom_status_emoji.calendar"
|
||||
>
|
||||
<Text>
|
||||
|
||||
<Text
|
||||
style={
|
||||
Array [
|
||||
undefined,
|
||||
Object {
|
||||
"color": "#000",
|
||||
"fontSize": 34,
|
||||
},
|
||||
]
|
||||
}
|
||||
>
|
||||
📆
|
||||
</Text>
|
||||
</Text>
|
||||
`;
|
||||
|
||||
@@ -5,6 +5,7 @@ import Database from '@nozbe/watermelondb/Database';
|
||||
import React from 'react';
|
||||
|
||||
import CustomStatusEmoji from '@components/custom_status/custom_status_emoji';
|
||||
import {CustomStatusDuration} from '@constants';
|
||||
import {renderWithEverything} from '@test/intl-test-helper';
|
||||
import TestHelper from '@test/test_helper';
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ interface ComponentProps {
|
||||
testID?: string;
|
||||
}
|
||||
|
||||
const CustomStatusEmoji = ({customStatus, emojiSize, style, testID}: ComponentProps) => {
|
||||
const CustomStatusEmoji = ({customStatus, emojiSize = 16, style, testID}: ComponentProps) => {
|
||||
const testIdPrefix = testID ? `${testID}.` : '';
|
||||
return (
|
||||
<Text
|
||||
@@ -28,8 +28,4 @@ const CustomStatusEmoji = ({customStatus, emojiSize, style, testID}: ComponentPr
|
||||
);
|
||||
};
|
||||
|
||||
CustomStatusEmoji.defaultProps = {
|
||||
emojiSize: 16,
|
||||
};
|
||||
|
||||
export default CustomStatusEmoji;
|
||||
|
||||
141
app/components/custom_status/custom_status_expiry.tsx
Normal file
141
app/components/custom_status/custom_status_expiry.tsx
Normal file
@@ -0,0 +1,141 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Q} from '@nozbe/watermelondb';
|
||||
import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider';
|
||||
import withObservables from '@nozbe/with-observables';
|
||||
import moment, {Moment} from 'moment-timezone';
|
||||
import React from 'react';
|
||||
import {Text, TextStyle} from 'react-native';
|
||||
import {of as of$} from 'rxjs';
|
||||
import {switchMap} from 'rxjs/operators';
|
||||
|
||||
import {getUserTimezone} from '@actions/local/timezone';
|
||||
import FormattedDate from '@components/formatted_date';
|
||||
import FormattedText from '@components/formatted_text';
|
||||
import FormattedTime from '@components/formatted_time';
|
||||
import {Preferences} from '@constants';
|
||||
import {MM_TABLES} from '@constants/database';
|
||||
import {getPreferenceAsBool} from '@helpers/api/preference';
|
||||
import {getCurrentMomentForTimezone} from '@utils/helpers';
|
||||
import {makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
import type {WithDatabaseArgs} from '@typings/database/database';
|
||||
import type PreferenceModel from '@typings/database/models/servers/preference';
|
||||
import type UserModel from '@typings/database/models/servers/user';
|
||||
|
||||
type Props = {
|
||||
currentUser: UserModel;
|
||||
isMilitaryTime: boolean;
|
||||
showPrefix?: boolean;
|
||||
showTimeCompulsory?: boolean;
|
||||
showToday?: boolean;
|
||||
testID?: string;
|
||||
textStyles?: TextStyle;
|
||||
theme: Theme;
|
||||
time: Date | Moment;
|
||||
withinBrackets?: boolean;
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
return {
|
||||
text: {
|
||||
fontSize: 15,
|
||||
color: theme.centerChannelColor,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const CustomStatusExpiry = ({currentUser, isMilitaryTime, showPrefix, showTimeCompulsory, showToday, testID = '', textStyles = {}, theme, time, withinBrackets}: Props) => {
|
||||
const userTimezone = getUserTimezone(currentUser);
|
||||
const timezone = userTimezone.useAutomaticTimezone ? userTimezone.automaticTimezone : userTimezone.manualTimezone;
|
||||
const styles = getStyleSheet(theme);
|
||||
const currentMomentTime = getCurrentMomentForTimezone(timezone);
|
||||
const expiryMomentTime = timezone ? moment(time).tz(timezone) : moment(time);
|
||||
const plusSixDaysEndTime = currentMomentTime.clone().add(6, 'days').endOf('day');
|
||||
const tomorrowEndTime = currentMomentTime.clone().add(1, 'day').endOf('day');
|
||||
const todayEndTime = currentMomentTime.clone().endOf('day');
|
||||
const isCurrentYear = currentMomentTime.get('y') === expiryMomentTime.get('y');
|
||||
|
||||
let dateComponent;
|
||||
if ((showToday && expiryMomentTime.isBefore(todayEndTime)) || expiryMomentTime.isSame(todayEndTime)) {
|
||||
dateComponent = (
|
||||
<FormattedText
|
||||
id='custom_status.expiry_time.today'
|
||||
defaultMessage='Today'
|
||||
/>
|
||||
);
|
||||
} else if (expiryMomentTime.isAfter(todayEndTime) && expiryMomentTime.isSameOrBefore(tomorrowEndTime)) {
|
||||
dateComponent = (
|
||||
<FormattedText
|
||||
id='custom_status.expiry_time.tomorrow'
|
||||
defaultMessage='Tomorrow'
|
||||
/>
|
||||
);
|
||||
} else if (expiryMomentTime.isAfter(tomorrowEndTime)) {
|
||||
let format = 'dddd';
|
||||
if (expiryMomentTime.isAfter(plusSixDaysEndTime) && isCurrentYear) {
|
||||
format = 'MMM DD';
|
||||
} else if (!isCurrentYear) {
|
||||
format = 'MMM DD, YYYY';
|
||||
}
|
||||
|
||||
dateComponent = (
|
||||
<FormattedDate
|
||||
format={format}
|
||||
timezone={timezone}
|
||||
value={expiryMomentTime.toDate()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const useTime = showTimeCompulsory || !(expiryMomentTime.isSame(todayEndTime) || expiryMomentTime.isAfter(tomorrowEndTime));
|
||||
|
||||
return (
|
||||
<Text
|
||||
testID={testID}
|
||||
style={[styles.text, textStyles]}
|
||||
>
|
||||
{withinBrackets && '('}
|
||||
{showPrefix && (
|
||||
<FormattedText
|
||||
id='custom_status.expiry.until'
|
||||
defaultMessage='Until'
|
||||
/>
|
||||
)}
|
||||
{showPrefix && ' '}
|
||||
{dateComponent}
|
||||
{useTime && dateComponent && (
|
||||
<>
|
||||
{' '}
|
||||
<FormattedText
|
||||
id='custom_status.expiry.at'
|
||||
defaultMessage='at'
|
||||
/>
|
||||
{' '}
|
||||
</>
|
||||
)}
|
||||
{useTime && (
|
||||
<FormattedTime
|
||||
isMilitaryTime={isMilitaryTime}
|
||||
timezone={timezone || ''}
|
||||
value={expiryMomentTime.toDate()}
|
||||
/>
|
||||
)}
|
||||
{withinBrackets && ')'}
|
||||
</Text>
|
||||
);
|
||||
};
|
||||
|
||||
const enhanced = withObservables([], ({database}: WithDatabaseArgs) => ({
|
||||
isMilitaryTime: database.get<PreferenceModel>(MM_TABLES.SERVER.PREFERENCE).
|
||||
query(
|
||||
Q.where('category', Preferences.CATEGORY_DISPLAY_SETTINGS),
|
||||
).observe().pipe(
|
||||
switchMap(
|
||||
(preferences) => of$(getPreferenceAsBool(preferences, Preferences.CATEGORY_DISPLAY_SETTINGS, 'use_military_time', false)),
|
||||
),
|
||||
),
|
||||
}));
|
||||
|
||||
export default withDatabase(enhanced(CustomStatusExpiry));
|
||||
@@ -4,11 +4,10 @@
|
||||
import React from 'react';
|
||||
import {Text, TextStyle} from 'react-native';
|
||||
|
||||
import FormattedText from '@components/formatted_text';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
interface ComponentProps {
|
||||
text: string | typeof FormattedText;
|
||||
text: string | React.ReactNode;
|
||||
theme: Theme;
|
||||
textStyle?: TextStyle;
|
||||
ellipsizeMode?: 'head' | 'middle' | 'tail' | 'clip';
|
||||
|
||||
220
app/components/drawer_item/__snapshots__/index.test.tsx.snap
Normal file
220
app/components/drawer_item/__snapshots__/index.test.tsx.snap
Normal file
@@ -0,0 +1,220 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`DrawerItem should match snapshot 1`] = `
|
||||
<View
|
||||
onMoveShouldSetResponder={[Function]}
|
||||
onMoveShouldSetResponderCapture={[Function]}
|
||||
onResponderEnd={[Function]}
|
||||
onResponderGrant={[Function]}
|
||||
onResponderMove={[Function]}
|
||||
onResponderReject={[Function]}
|
||||
onResponderRelease={[Function]}
|
||||
onResponderStart={[Function]}
|
||||
onResponderTerminate={[Function]}
|
||||
onResponderTerminationRequest={[Function]}
|
||||
onStartShouldSetResponder={[Function]}
|
||||
onStartShouldSetResponderCapture={[Function]}
|
||||
testID="test-id"
|
||||
>
|
||||
<View
|
||||
accessible={true}
|
||||
focusable={true}
|
||||
onClick={[Function]}
|
||||
onResponderGrant={[Function]}
|
||||
onResponderMove={[Function]}
|
||||
onResponderRelease={[Function]}
|
||||
onResponderTerminate={[Function]}
|
||||
onResponderTerminationRequest={[Function]}
|
||||
onStartShouldSetResponder={[Function]}
|
||||
>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "#ffffff",
|
||||
"flexDirection": "row",
|
||||
"minHeight": 50,
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"height": 50,
|
||||
"justifyContent": "center",
|
||||
"marginLeft": 5,
|
||||
"width": 45,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Icon
|
||||
name="icon-name"
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"color": "rgba(63,67,80,0.64)",
|
||||
"fontSize": 24,
|
||||
},
|
||||
Object {
|
||||
"color": "#d24b4e",
|
||||
},
|
||||
]
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"flex": 1,
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"flex": 1,
|
||||
"justifyContent": "center",
|
||||
"paddingBottom": 14,
|
||||
"paddingTop": 14,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Text
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"color": "rgba(63,67,80,0.5)",
|
||||
"fontSize": 17,
|
||||
"includeFontPadding": false,
|
||||
"textAlignVertical": "center",
|
||||
},
|
||||
Object {
|
||||
"color": "#d24b4e",
|
||||
},
|
||||
Object {
|
||||
"textAlign": "center",
|
||||
"textAlignVertical": "center",
|
||||
},
|
||||
]
|
||||
}
|
||||
>
|
||||
default message
|
||||
</Text>
|
||||
</View>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "rgba(63,67,80,0.2)",
|
||||
"height": 1,
|
||||
}
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
`;
|
||||
|
||||
exports[`DrawerItem should match snapshot without separator and centered false 1`] = `
|
||||
<View
|
||||
onMoveShouldSetResponder={[Function]}
|
||||
onMoveShouldSetResponderCapture={[Function]}
|
||||
onResponderEnd={[Function]}
|
||||
onResponderGrant={[Function]}
|
||||
onResponderMove={[Function]}
|
||||
onResponderReject={[Function]}
|
||||
onResponderRelease={[Function]}
|
||||
onResponderStart={[Function]}
|
||||
onResponderTerminate={[Function]}
|
||||
onResponderTerminationRequest={[Function]}
|
||||
onStartShouldSetResponder={[Function]}
|
||||
onStartShouldSetResponderCapture={[Function]}
|
||||
testID="test-id"
|
||||
>
|
||||
<View
|
||||
accessible={true}
|
||||
focusable={true}
|
||||
onClick={[Function]}
|
||||
onResponderGrant={[Function]}
|
||||
onResponderMove={[Function]}
|
||||
onResponderRelease={[Function]}
|
||||
onResponderTerminate={[Function]}
|
||||
onResponderTerminationRequest={[Function]}
|
||||
onStartShouldSetResponder={[Function]}
|
||||
>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "#ffffff",
|
||||
"flexDirection": "row",
|
||||
"minHeight": 50,
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"height": 50,
|
||||
"justifyContent": "center",
|
||||
"marginLeft": 5,
|
||||
"width": 45,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Icon
|
||||
name="icon-name"
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"color": "rgba(63,67,80,0.64)",
|
||||
"fontSize": 24,
|
||||
},
|
||||
Object {
|
||||
"color": "#d24b4e",
|
||||
},
|
||||
]
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"flex": 1,
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"flex": 1,
|
||||
"justifyContent": "center",
|
||||
"paddingBottom": 14,
|
||||
"paddingTop": 14,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Text
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"color": "rgba(63,67,80,0.5)",
|
||||
"fontSize": 17,
|
||||
"includeFontPadding": false,
|
||||
"textAlignVertical": "center",
|
||||
},
|
||||
Object {
|
||||
"color": "#d24b4e",
|
||||
},
|
||||
Object {},
|
||||
]
|
||||
}
|
||||
>
|
||||
default message
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
`;
|
||||
42
app/components/drawer_item/index.test.tsx
Normal file
42
app/components/drawer_item/index.test.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import {Preferences} from '@constants';
|
||||
import {renderWithIntl} from '@test/intl-test-helper';
|
||||
|
||||
import DrawerItem from './';
|
||||
|
||||
describe('DrawerItem', () => {
|
||||
const baseProps = {
|
||||
onPress: () => null,
|
||||
testID: 'test-id',
|
||||
centered: true,
|
||||
defaultMessage: 'default message',
|
||||
i18nId: 'i18-id',
|
||||
iconName: 'icon-name',
|
||||
isDestructor: true,
|
||||
separator: true,
|
||||
theme: Preferences.THEMES.denim,
|
||||
};
|
||||
|
||||
test('should match snapshot', () => {
|
||||
const wrapper = renderWithIntl(<DrawerItem {...baseProps}/>);
|
||||
|
||||
expect(wrapper.toJSON()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should match snapshot without separator and centered false', () => {
|
||||
const props = {
|
||||
...baseProps,
|
||||
centered: false,
|
||||
separator: false,
|
||||
};
|
||||
const wrapper = renderWithIntl(
|
||||
<DrawerItem {...props}/>,
|
||||
);
|
||||
|
||||
expect(wrapper.toJSON()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
149
app/components/drawer_item/index.tsx
Normal file
149
app/components/drawer_item/index.tsx
Normal file
@@ -0,0 +1,149 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {ReactNode} from 'react';
|
||||
import {Platform, View} from 'react-native';
|
||||
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import FormattedText from '@components/formatted_text';
|
||||
import TouchableWithFeedback from '@components/touchable_with_feedback';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
type DrawerItemProps = {
|
||||
centered?: boolean;
|
||||
defaultMessage?: string;
|
||||
i18nId?: string;
|
||||
iconName?: string;
|
||||
isDestructor?: boolean;
|
||||
labelComponent?: ReactNode;
|
||||
leftComponent?: ReactNode;
|
||||
onPress: () => void;
|
||||
separator?: boolean;
|
||||
testID: string;
|
||||
theme: Theme;
|
||||
};
|
||||
|
||||
const DrawerItem = (props: DrawerItemProps) => {
|
||||
const {
|
||||
centered,
|
||||
defaultMessage = '',
|
||||
i18nId,
|
||||
iconName,
|
||||
isDestructor = false,
|
||||
labelComponent,
|
||||
leftComponent,
|
||||
onPress,
|
||||
separator = true,
|
||||
testID,
|
||||
theme,
|
||||
} = props;
|
||||
|
||||
const style = getStyleSheet(theme);
|
||||
|
||||
const destructor: any = {};
|
||||
if (isDestructor) {
|
||||
destructor.color = theme.errorTextColor;
|
||||
}
|
||||
|
||||
let divider;
|
||||
if (separator) {
|
||||
divider = (<View style={style.divider}/>);
|
||||
}
|
||||
|
||||
let icon;
|
||||
if (leftComponent) {
|
||||
icon = leftComponent;
|
||||
} else if (iconName) {
|
||||
icon = (
|
||||
<CompassIcon
|
||||
name={iconName}
|
||||
style={[style.icon, destructor]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
let label;
|
||||
if (labelComponent) {
|
||||
label = labelComponent;
|
||||
} else if (i18nId) {
|
||||
label = (
|
||||
<FormattedText
|
||||
id={i18nId}
|
||||
defaultMessage={defaultMessage}
|
||||
style={[
|
||||
style.label,
|
||||
destructor,
|
||||
centered ? style.centerLabel : {},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<TouchableWithFeedback
|
||||
testID={testID}
|
||||
onPress={onPress}
|
||||
underlayColor={changeOpacity(theme.centerChannelColor, Platform.select({android: 0.1, ios: 0.3}) || 0.3)}
|
||||
>
|
||||
<View style={style.container}>
|
||||
{icon && (
|
||||
<View style={style.iconContainer}>
|
||||
{icon}
|
||||
</View>
|
||||
)}
|
||||
<View style={style.wrapper}>
|
||||
<View style={style.labelContainer}>
|
||||
{label}
|
||||
</View>
|
||||
{divider}
|
||||
</View>
|
||||
</View>
|
||||
</TouchableWithFeedback>
|
||||
);
|
||||
};
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
return {
|
||||
container: {
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
flexDirection: 'row',
|
||||
minHeight: 50,
|
||||
},
|
||||
iconContainer: {
|
||||
width: 45,
|
||||
height: 50,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginLeft: 5,
|
||||
},
|
||||
icon: {
|
||||
color: changeOpacity(theme.centerChannelColor, 0.64),
|
||||
fontSize: 24,
|
||||
},
|
||||
wrapper: {
|
||||
flex: 1,
|
||||
},
|
||||
labelContainer: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
paddingTop: 14,
|
||||
paddingBottom: 14,
|
||||
},
|
||||
centerLabel: {
|
||||
textAlign: 'center',
|
||||
textAlignVertical: 'center',
|
||||
},
|
||||
label: {
|
||||
color: changeOpacity(theme.centerChannelColor, 0.5),
|
||||
fontSize: 17,
|
||||
textAlignVertical: 'center',
|
||||
includeFontPadding: false,
|
||||
},
|
||||
divider: {
|
||||
backgroundColor: changeOpacity(theme.centerChannelColor, 0.2),
|
||||
height: 1,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
export default DrawerItem;
|
||||
@@ -11,7 +11,6 @@ import {
|
||||
StyleSheet,
|
||||
Text,
|
||||
TextStyle,
|
||||
View,
|
||||
} from 'react-native';
|
||||
import FastImage, {ImageStyle} from 'react-native-fast-image';
|
||||
import {of as of$} from 'rxjs';
|
||||
@@ -102,7 +101,7 @@ const Emoji = (props: Props) => {
|
||||
|
||||
return (
|
||||
<Text
|
||||
style={[textStyle, {fontSize: size}]}
|
||||
style={[textStyle, {fontSize: size, color: '#000'}]}
|
||||
testID={testID}
|
||||
>
|
||||
{code}
|
||||
@@ -118,15 +117,13 @@ const Emoji = (props: Props) => {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<View style={Platform.select({ios: {flex: 1, justifyContent: 'center'}})}>
|
||||
<FastImage
|
||||
key={key}
|
||||
source={image}
|
||||
style={[customEmojiStyle, {width, height}]}
|
||||
resizeMode={FastImage.resizeMode.contain}
|
||||
testID={testID}
|
||||
/>
|
||||
</View>
|
||||
<FastImage
|
||||
key={key}
|
||||
source={image}
|
||||
style={[customEmojiStyle, {width, height}]}
|
||||
resizeMode={FastImage.resizeMode.contain}
|
||||
testID={testID}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -139,31 +136,27 @@ const Emoji = (props: Props) => {
|
||||
const key = Platform.OS === 'android' ? (`${imageUrl}-${height}-${width}`) : null;
|
||||
|
||||
return (
|
||||
<View style={Platform.select({ios: {flex: 1, justifyContent: 'center'}})}>
|
||||
<FastImage
|
||||
key={key}
|
||||
style={[customEmojiStyle, {width, height}]}
|
||||
source={{uri: imageUrl}}
|
||||
resizeMode={FastImage.resizeMode.contain}
|
||||
testID={testID}
|
||||
/>
|
||||
</View>
|
||||
<FastImage
|
||||
key={key}
|
||||
style={[customEmojiStyle, {width, height}]}
|
||||
source={{uri: imageUrl}}
|
||||
resizeMode={FastImage.resizeMode.contain}
|
||||
testID={testID}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const withSystemIds = withObservables([], ({database}: WithDatabaseArgs) => ({
|
||||
enableCustomEmoji: database.get(MM_TABLES.SERVER.SYSTEM).findAndObserve(SYSTEM_IDENTIFIERS.CONFIG).pipe(
|
||||
switchMap((config: SystemModel) => of$(config.value.EnableCustomEmoji)),
|
||||
),
|
||||
}));
|
||||
const withCustomEmojis = withObservables(['emojiName'], ({database, emojiName}: WithDatabaseArgs & {emojiName: string}) => {
|
||||
const hasEmojiBuiltIn = EmojiIndicesByAlias.has(emojiName);
|
||||
|
||||
const withCustomEmojis = withObservables(['enableCustomEmoji', 'emojiName'], ({enableCustomEmoji, database, emojiName}: WithDatabaseArgs & {enableCustomEmoji: string; emojiName: string}) => {
|
||||
const displayTextOnly = enableCustomEmoji !== 'true';
|
||||
const displayTextOnly = hasEmojiBuiltIn ? of$(false) : database.get<SystemModel>(MM_TABLES.SERVER.SYSTEM).findAndObserve(SYSTEM_IDENTIFIERS.CONFIG).pipe(
|
||||
switchMap((config) => of$(config.value.EnableCustomEmoji !== 'true')),
|
||||
);
|
||||
|
||||
return {
|
||||
displayTextOnly: of$(displayTextOnly),
|
||||
customEmojis: database.get(MM_TABLES.SERVER.CUSTOM_EMOJI).query(Q.where('name', emojiName)).observe(),
|
||||
displayTextOnly,
|
||||
customEmojis: hasEmojiBuiltIn ? of$([]) : database.get(MM_TABLES.SERVER.CUSTOM_EMOJI).query(Q.where('name', emojiName)).observe(),
|
||||
};
|
||||
});
|
||||
|
||||
export default withDatabase(withSystemIds(withCustomEmojis(Emoji)));
|
||||
export default withDatabase(withCustomEmojis(Emoji));
|
||||
|
||||
63
app/components/emoji_picker/filtered/emoji_item.tsx
Normal file
63
app/components/emoji_picker/filtered/emoji_item.tsx
Normal file
@@ -0,0 +1,63 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {memo, useCallback} from 'react';
|
||||
import {Text, TouchableOpacity, View} from 'react-native';
|
||||
|
||||
import Emoji from '@components/emoji';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
type TouchableEmojiProps = {
|
||||
name: string;
|
||||
onEmojiPress: (emojiName: string) => void;
|
||||
}
|
||||
|
||||
const getStyleSheetFromTheme = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
return {
|
||||
container: {
|
||||
height: 40,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: 8,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: changeOpacity(theme.centerChannelColor, 0.2),
|
||||
overflow: 'hidden',
|
||||
},
|
||||
emojiContainer: {
|
||||
marginRight: 5,
|
||||
},
|
||||
emoji: {
|
||||
color: '#000',
|
||||
},
|
||||
emojiText: {
|
||||
fontSize: 13,
|
||||
color: theme.centerChannelColor,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const EmojiTouchable = ({name, onEmojiPress}: TouchableEmojiProps) => {
|
||||
const theme = useTheme();
|
||||
const style = getStyleSheetFromTheme(theme);
|
||||
|
||||
const onPress = useCallback(() => onEmojiPress(name), []);
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
onPress={onPress}
|
||||
style={style.container}
|
||||
>
|
||||
<View style={style.emojiContainer}>
|
||||
<Emoji
|
||||
emojiName={name}
|
||||
textStyle={style.emoji}
|
||||
size={20}
|
||||
/>
|
||||
</View>
|
||||
<Text style={style.emojiText}>{`:${name}:`}</Text>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(EmojiTouchable);
|
||||
102
app/components/emoji_picker/filtered/index.tsx
Normal file
102
app/components/emoji_picker/filtered/index.tsx
Normal file
@@ -0,0 +1,102 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import Fuse from 'fuse.js';
|
||||
import React, {useCallback, useMemo} from 'react';
|
||||
import {FlatList} from 'react-native';
|
||||
|
||||
import {Emojis, EmojiIndicesByAlias} from '@utils/emoji';
|
||||
import {compareEmojis, getSkin} from '@utils/emoji/helpers';
|
||||
|
||||
import EmojiItem from './emoji_item';
|
||||
import NoResults from './no_results';
|
||||
|
||||
import type CustomEmojiModel from '@typings/database/models/servers/custom_emoji';
|
||||
|
||||
type Props = {
|
||||
customEmojis: CustomEmojiModel[];
|
||||
skinTone: string;
|
||||
searchTerm: string;
|
||||
onEmojiPress: (emojiName: string) => void;
|
||||
};
|
||||
|
||||
const EmojiFiltered = ({customEmojis, skinTone, searchTerm, onEmojiPress}: Props) => {
|
||||
const emojis = useMemo(() => {
|
||||
const emoticons = new Set<string>();
|
||||
for (const [key, index] of EmojiIndicesByAlias.entries()) {
|
||||
const skin = getSkin(Emojis[index]);
|
||||
if (!skin || skin === skinTone) {
|
||||
emoticons.add(key);
|
||||
}
|
||||
}
|
||||
|
||||
for (const custom of customEmojis) {
|
||||
emoticons.add(custom.name);
|
||||
}
|
||||
|
||||
return Array.from(emoticons);
|
||||
}, [skinTone, customEmojis]);
|
||||
|
||||
const fuse = useMemo(() => {
|
||||
const options = {findAllMatches: true, ignoreLocation: true, includeMatches: true, shouldSort: false, includeScore: true};
|
||||
return new Fuse(emojis, options);
|
||||
}, [emojis]);
|
||||
|
||||
const data = useMemo(() => {
|
||||
const searchTermLowerCase = searchTerm.toLowerCase();
|
||||
|
||||
if (!searchTerm) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const sorter = (a: string, b: string) => {
|
||||
return compareEmojis(a, b, searchTermLowerCase);
|
||||
};
|
||||
|
||||
const fuzz = fuse.search(searchTermLowerCase);
|
||||
|
||||
if (fuzz) {
|
||||
const results = fuzz.reduce((values, r) => {
|
||||
const score = r?.score === undefined ? 1 : r.score;
|
||||
const v = r?.matches?.[0]?.value;
|
||||
if (score < 0.2 && v) {
|
||||
values.push(v);
|
||||
}
|
||||
|
||||
return values;
|
||||
}, [] as string[]);
|
||||
|
||||
return results.sort(sorter);
|
||||
}
|
||||
|
||||
return [];
|
||||
}, [fuse, searchTerm]);
|
||||
|
||||
const keyExtractor = useCallback((item) => item, []);
|
||||
|
||||
const renderItem = useCallback(({item}) => {
|
||||
return (
|
||||
<EmojiItem
|
||||
onEmojiPress={onEmojiPress}
|
||||
name={item}
|
||||
/>
|
||||
);
|
||||
}, []);
|
||||
|
||||
if (!data.length) {
|
||||
return <NoResults searchTerm={searchTerm}/>;
|
||||
}
|
||||
|
||||
return (
|
||||
<FlatList
|
||||
data={data}
|
||||
initialNumToRender={30}
|
||||
keyboardShouldPersistTaps='always'
|
||||
keyExtractor={keyExtractor}
|
||||
renderItem={renderItem}
|
||||
removeClippedSubviews={false}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default EmojiFiltered;
|
||||
90
app/components/emoji_picker/filtered/no_results.tsx
Normal file
90
app/components/emoji_picker/filtered/no_results.tsx
Normal file
@@ -0,0 +1,90 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {Text, View} from 'react-native';
|
||||
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
type Props = {
|
||||
searchTerm: string;
|
||||
}
|
||||
|
||||
const getStyleSheetFromTheme = makeStyleSheetFromTheme((theme) => {
|
||||
return {
|
||||
flex: {
|
||||
flex: 1,
|
||||
},
|
||||
flexCenter: {
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
notFoundIcon: {
|
||||
backgroundColor: changeOpacity(theme.centerChannelColor, 0.04),
|
||||
width: 120,
|
||||
height: 120,
|
||||
borderRadius: 60,
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
notFoundText: {
|
||||
color: theme.centerChannelColor,
|
||||
marginTop: 16,
|
||||
},
|
||||
notFoundText20: {
|
||||
fontSize: 20,
|
||||
fontWeight: '600',
|
||||
},
|
||||
notFoundText15: {
|
||||
fontSize: 15,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const NoResults = ({searchTerm}: Props) => {
|
||||
const theme = useTheme();
|
||||
const intl = useIntl();
|
||||
|
||||
const styles = getStyleSheetFromTheme(theme);
|
||||
const title = intl.formatMessage(
|
||||
{
|
||||
id: 'mobile.emoji_picker.search.not_found_title',
|
||||
defaultMessage: 'No results found for "{searchTerm}"',
|
||||
},
|
||||
{
|
||||
searchTerm,
|
||||
},
|
||||
);
|
||||
|
||||
const description = intl.formatMessage({
|
||||
id: 'mobile.emoji_picker.search.not_found_description',
|
||||
defaultMessage: 'Check the spelling or try another search.',
|
||||
});
|
||||
|
||||
return (
|
||||
<View style={[styles.flex, styles.flexCenter]}>
|
||||
<View style={styles.flexCenter}>
|
||||
<View style={styles.notFoundIcon}>
|
||||
<CompassIcon
|
||||
name='magnify'
|
||||
size={72}
|
||||
color={theme.buttonBg}
|
||||
/>
|
||||
</View>
|
||||
<Text style={[styles.notFoundText, styles.notFoundText20]}>
|
||||
{title}
|
||||
</Text>
|
||||
<Text style={[styles.notFoundText, styles.notFoundText15]}>
|
||||
{description}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default NoResults;
|
||||
172
app/components/emoji_picker/index.tsx
Normal file
172
app/components/emoji_picker/index.tsx
Normal file
@@ -0,0 +1,172 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Q} from '@nozbe/watermelondb';
|
||||
import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider';
|
||||
import withObservables from '@nozbe/with-observables';
|
||||
import React, {useCallback, useState} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {LayoutChangeEvent, Platform, View} from 'react-native';
|
||||
import {Edge, SafeAreaView} from 'react-native-safe-area-context';
|
||||
import {of as of$} from 'rxjs';
|
||||
import {catchError, switchMap} from 'rxjs/operators';
|
||||
|
||||
import {searchCustomEmojis} from '@actions/remote/custom_emoji';
|
||||
import SearchBar from '@components/search_bar';
|
||||
import {Preferences} from '@constants';
|
||||
import {MM_TABLES, SYSTEM_IDENTIFIERS} from '@constants/database';
|
||||
import {useServerUrl} from '@context/server_url';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {debounce} from '@helpers/api/general';
|
||||
import {safeParseJSON} from '@utils/helpers';
|
||||
import {changeOpacity, getKeyboardAppearanceFromTheme, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
import EmojiFiltered from './filtered';
|
||||
import EmojiSections from './sections';
|
||||
|
||||
import type {WithDatabaseArgs} from '@typings/database/database';
|
||||
import type CustomEmojiModel from '@typings/database/models/servers/custom_emoji';
|
||||
import type PreferenceModel from '@typings/database/models/servers/preference';
|
||||
import type SystemModel from '@typings/database/models/servers/system';
|
||||
|
||||
export const SCROLLVIEW_NATIVE_ID = 'emojiSelector';
|
||||
const edges: Edge[] = ['bottom', 'left', 'right'];
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||
flex: {
|
||||
flex: 1,
|
||||
},
|
||||
container: {
|
||||
flex: 1,
|
||||
marginHorizontal: 12,
|
||||
},
|
||||
searchBar: {
|
||||
backgroundColor: changeOpacity(theme.centerChannelColor, 0.2),
|
||||
paddingVertical: 5,
|
||||
...Platform.select({
|
||||
ios: {
|
||||
paddingLeft: 8,
|
||||
},
|
||||
}),
|
||||
height: 50,
|
||||
},
|
||||
searchBarInput: {
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
color: theme.centerChannelColor,
|
||||
fontSize: 13,
|
||||
},
|
||||
}));
|
||||
|
||||
type Props = {
|
||||
customEmojis: CustomEmojiModel[];
|
||||
customEmojisEnabled: boolean;
|
||||
onEmojiPress: (emoji: string) => void;
|
||||
recentEmojis: string[];
|
||||
skinTone: string;
|
||||
testID?: string;
|
||||
}
|
||||
|
||||
const EmojiPicker = ({customEmojis, customEmojisEnabled, onEmojiPress, recentEmojis, skinTone, testID = ''}: Props) => {
|
||||
const theme = useTheme();
|
||||
const intl = useIntl();
|
||||
const serverUrl = useServerUrl();
|
||||
const [width, setWidth] = useState(0);
|
||||
const [searchTerm, setSearchTerm] = useState<string|undefined>();
|
||||
const styles = getStyleSheet(theme);
|
||||
const onLayout = useCallback(({nativeEvent}: LayoutChangeEvent) => setWidth(nativeEvent.layout.width), []);
|
||||
const onCancelSearch = useCallback(() => setSearchTerm(undefined), []);
|
||||
const onChangeSearchTerm = useCallback((text) => {
|
||||
setSearchTerm(text);
|
||||
searchCustom(text);
|
||||
}, []);
|
||||
const searchCustom = debounce((text: string) => {
|
||||
if (text && text.length > 1) {
|
||||
searchCustomEmojis(serverUrl, text);
|
||||
}
|
||||
}, 500);
|
||||
|
||||
let EmojiList: React.ReactNode = null;
|
||||
if (searchTerm) {
|
||||
EmojiList = (
|
||||
<EmojiFiltered
|
||||
customEmojis={customEmojis}
|
||||
skinTone={skinTone}
|
||||
searchTerm={searchTerm}
|
||||
onEmojiPress={onEmojiPress}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
EmojiList = (
|
||||
<EmojiSections
|
||||
customEmojis={customEmojis}
|
||||
customEmojisEnabled={customEmojisEnabled}
|
||||
onEmojiPress={onEmojiPress}
|
||||
recentEmojis={recentEmojis}
|
||||
skinTone={skinTone}
|
||||
width={width}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<SafeAreaView
|
||||
style={styles.flex}
|
||||
edges={edges}
|
||||
>
|
||||
<View
|
||||
style={styles.searchBar}
|
||||
testID={testID}
|
||||
>
|
||||
<SearchBar
|
||||
autoCapitalize='none'
|
||||
backgroundColor='transparent'
|
||||
cancelTitle={intl.formatMessage({id: 'mobile.post.cancel', defaultMessage: 'Cancel'})}
|
||||
inputHeight={33}
|
||||
inputStyle={styles.searchBarInput}
|
||||
keyboardAppearance={getKeyboardAppearanceFromTheme(theme)}
|
||||
onCancelButtonPress={onCancelSearch}
|
||||
onChangeText={onChangeSearchTerm}
|
||||
placeholder={intl.formatMessage({id: 'search_bar.search', defaultMessage: 'Search'})}
|
||||
placeholderTextColor={changeOpacity(theme.centerChannelColor, 0.5)}
|
||||
testID={`${testID}.search_bar`}
|
||||
tintColorDelete={changeOpacity(theme.centerChannelColor, 0.5)}
|
||||
tintColorSearch={changeOpacity(theme.centerChannelColor, 0.8)}
|
||||
titleCancelColor={theme.centerChannelColor}
|
||||
value={searchTerm}
|
||||
/>
|
||||
</View>
|
||||
<View
|
||||
style={styles.container}
|
||||
onLayout={onLayout}
|
||||
>
|
||||
{Boolean(width) &&
|
||||
<>
|
||||
{EmojiList}
|
||||
</>
|
||||
}
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
);
|
||||
};
|
||||
|
||||
const enhanced = withObservables([], ({database}: WithDatabaseArgs) => ({
|
||||
customEmojisEnabled: database.get<SystemModel>(MM_TABLES.SERVER.SYSTEM).
|
||||
findAndObserve(SYSTEM_IDENTIFIERS.CONFIG).pipe(
|
||||
switchMap((config) => of$(config.value.EnableCustomEmoji === 'true')),
|
||||
),
|
||||
customEmojis: database.get<CustomEmojiModel>(MM_TABLES.SERVER.CUSTOM_EMOJI).query().observe(),
|
||||
recentEmojis: database.get<SystemModel>(MM_TABLES.SERVER.SYSTEM).
|
||||
findAndObserve(SYSTEM_IDENTIFIERS.RECENT_REACTIONS).
|
||||
pipe(
|
||||
switchMap((recent) => of$(safeParseJSON(recent.value) as string[])),
|
||||
catchError(() => of$([])),
|
||||
),
|
||||
skinTone: database.get<PreferenceModel>(MM_TABLES.SERVER.PREFERENCE).query(
|
||||
Q.where('category', Preferences.CATEGORY_EMOJI),
|
||||
Q.where('name', Preferences.EMOJI_SKINTONE),
|
||||
).observe().pipe(
|
||||
switchMap((prefs) => of$(prefs?.[0]?.value ?? 'default')),
|
||||
),
|
||||
}));
|
||||
|
||||
export default withDatabase(enhanced(EmojiPicker));
|
||||
53
app/components/emoji_picker/sections/icons_bar/icon.tsx
Normal file
53
app/components/emoji_picker/sections/icons_bar/icon.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback} from 'react';
|
||||
import {TouchableOpacity} from 'react-native';
|
||||
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import {preventDoubleTap} from '@utils/tap';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
type Props = {
|
||||
currentIndex: number;
|
||||
icon: string;
|
||||
index: number;
|
||||
scrollToIndex: (index: number) => void;
|
||||
theme: Theme;
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||
container: {
|
||||
alignItems: 'center',
|
||||
flex: 1,
|
||||
height: 35,
|
||||
justifyContent: 'center',
|
||||
zIndex: 10,
|
||||
},
|
||||
icon: {
|
||||
color: changeOpacity(theme.centerChannelColor, 0.4),
|
||||
},
|
||||
selected: {
|
||||
color: theme.centerChannelColor,
|
||||
},
|
||||
}));
|
||||
|
||||
const SectionIcon = ({currentIndex, icon, index, scrollToIndex, theme}: Props) => {
|
||||
const style = getStyleSheet(theme);
|
||||
const onPress = useCallback(preventDoubleTap(() => scrollToIndex(index)), []);
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
onPress={onPress}
|
||||
style={style.container}
|
||||
>
|
||||
<CompassIcon
|
||||
name={icon}
|
||||
size={20}
|
||||
style={[style.icon, currentIndex === index ? style.selected : undefined]}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
};
|
||||
|
||||
export default SectionIcon;
|
||||
75
app/components/emoji_picker/sections/icons_bar/index.tsx
Normal file
75
app/components/emoji_picker/sections/icons_bar/index.tsx
Normal file
@@ -0,0 +1,75 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {View} from 'react-native';
|
||||
import {KeyboardTrackingView} from 'react-native-keyboard-tracking-view';
|
||||
|
||||
import {useTheme} from '@context/theme';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
import SectionIcon from './icon';
|
||||
|
||||
export const SCROLLVIEW_NATIVE_ID = 'emojiSelector';
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||
container: {
|
||||
bottom: 10,
|
||||
height: 35,
|
||||
position: 'absolute',
|
||||
width: '100%',
|
||||
},
|
||||
background: {
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
},
|
||||
pane: {
|
||||
flexDirection: 'row',
|
||||
borderRadius: 10,
|
||||
paddingHorizontal: 10,
|
||||
width: '100%',
|
||||
borderColor: changeOpacity(theme.centerChannelColor, 0.3),
|
||||
backgroundColor: changeOpacity(theme.centerChannelColor, 0.1),
|
||||
borderWidth: 1,
|
||||
justifyContent: 'space-between',
|
||||
},
|
||||
}));
|
||||
|
||||
export type SectionIconType = {
|
||||
key: string;
|
||||
icon: string;
|
||||
}
|
||||
|
||||
type Props = {
|
||||
currentIndex: number;
|
||||
sections: SectionIconType[];
|
||||
scrollToIndex: (index: number) => void;
|
||||
}
|
||||
|
||||
const EmojiSectionBar = ({currentIndex, sections, scrollToIndex}: Props) => {
|
||||
const theme = useTheme();
|
||||
const styles = getStyleSheet(theme);
|
||||
return (
|
||||
<KeyboardTrackingView
|
||||
scrollViewNativeID={SCROLLVIEW_NATIVE_ID}
|
||||
normalList={true}
|
||||
style={styles.container}
|
||||
>
|
||||
<View style={styles.background}>
|
||||
<View style={styles.pane}>
|
||||
{sections.map((section, index) => (
|
||||
<SectionIcon
|
||||
currentIndex={currentIndex}
|
||||
key={section.key}
|
||||
icon={section.icon}
|
||||
index={index}
|
||||
scrollToIndex={scrollToIndex}
|
||||
theme={theme}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
</KeyboardTrackingView>
|
||||
);
|
||||
};
|
||||
|
||||
export default EmojiSectionBar;
|
||||
242
app/components/emoji_picker/sections/index.tsx
Normal file
242
app/components/emoji_picker/sections/index.tsx
Normal file
@@ -0,0 +1,242 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {chunk} from 'lodash';
|
||||
import React, {useCallback, useMemo, useRef, useState} from 'react';
|
||||
import {NativeScrollEvent, NativeSyntheticEvent, SectionList, StyleSheet, View} from 'react-native';
|
||||
import sectionListGetItemLayout from 'react-native-section-list-get-item-layout';
|
||||
|
||||
import {fetchCustomEmojis} from '@actions/remote/custom_emoji';
|
||||
import {EMOJIS_PER_PAGE} from '@constants/emoji';
|
||||
import {useServerUrl} from '@context/server_url';
|
||||
import {CategoryNames, EmojiIndicesByCategory, CategoryTranslations, CategoryMessage} from '@utils/emoji';
|
||||
import {fillEmoji} from '@utils/emoji/helpers';
|
||||
|
||||
import EmojiSectionBar, {SCROLLVIEW_NATIVE_ID, SectionIconType} from './icons_bar';
|
||||
import SectionFooter from './section_footer';
|
||||
import SectionHeader, {SECTION_HEADER_HEIGHT} from './section_header';
|
||||
import TouchableEmoji from './touchable_emoji';
|
||||
|
||||
import type CustomEmojiModel from '@typings/database/models/servers/custom_emoji';
|
||||
|
||||
export const EMOJI_SIZE = 30;
|
||||
export const EMOJI_GUTTER = 8;
|
||||
|
||||
const ICONS: Record<string, string> = {
|
||||
recent: 'clock-outline',
|
||||
'smileys-emotion': 'emoticon-happy-outline',
|
||||
'people-body': 'eye-outline',
|
||||
'animals-nature': 'leaf-outline',
|
||||
'food-drink': 'food-apple',
|
||||
'travel-places': 'airplane-variant',
|
||||
activities: 'basketball',
|
||||
objects: 'lightbulb-outline',
|
||||
symbols: 'heart-outline',
|
||||
flags: 'flag-outline',
|
||||
custom: 'emoticon-custom-outline',
|
||||
};
|
||||
|
||||
const categoryToI18n: Record<string, CategoryTranslation> = {};
|
||||
const getItemLayout = sectionListGetItemLayout({
|
||||
getItemHeight: () => (EMOJI_SIZE + (EMOJI_GUTTER * 2)),
|
||||
getSectionHeaderHeight: () => SECTION_HEADER_HEIGHT,
|
||||
});
|
||||
|
||||
const styles = StyleSheet.create(({
|
||||
row: {
|
||||
flexDirection: 'row',
|
||||
marginBottom: EMOJI_GUTTER,
|
||||
},
|
||||
emoji: {
|
||||
height: EMOJI_SIZE + EMOJI_GUTTER,
|
||||
marginHorizontal: 7,
|
||||
width: EMOJI_SIZE + EMOJI_GUTTER,
|
||||
},
|
||||
}));
|
||||
|
||||
type Props = {
|
||||
customEmojis: CustomEmojiModel[];
|
||||
customEmojisEnabled: boolean;
|
||||
onEmojiPress: (emoji: string) => void;
|
||||
recentEmojis: string[];
|
||||
skinTone: string;
|
||||
width: number;
|
||||
}
|
||||
|
||||
CategoryNames.forEach((name: string) => {
|
||||
if (CategoryTranslations.has(name) && CategoryMessage.has(name)) {
|
||||
categoryToI18n[name] = {
|
||||
id: CategoryTranslations.get(name)!,
|
||||
defaultMessage: CategoryMessage.get(name)!,
|
||||
icon: ICONS[name],
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
const EmojiSections = ({customEmojis, customEmojisEnabled, onEmojiPress, recentEmojis, skinTone, width}: Props) => {
|
||||
const serverUrl = useServerUrl();
|
||||
const list = useRef<SectionList<EmojiSection>>();
|
||||
const [sectionIndex, setSectionIndex] = useState(0);
|
||||
const [customEmojiPage, setCustomEmojiPage] = useState(0);
|
||||
const [fetchingCustomEmojis, setFetchingCustomEmojis] = useState(false);
|
||||
const [loadedAllCustomEmojis, setLoadedAllCustomEmojis] = useState(false);
|
||||
|
||||
const sections: EmojiSection[] = useMemo(() => {
|
||||
if (!width) {
|
||||
return [];
|
||||
}
|
||||
const chunkSize = Math.floor(width / (EMOJI_SIZE + EMOJI_GUTTER));
|
||||
|
||||
return CategoryNames.map((category) => {
|
||||
const emojiIndices = EmojiIndicesByCategory.get(skinTone)?.get(category);
|
||||
|
||||
let data: EmojiAlias[][];
|
||||
switch (category) {
|
||||
case 'custom': {
|
||||
const builtInCustom = emojiIndices.map(fillEmoji);
|
||||
|
||||
// eslint-disable-next-line max-nested-callbacks
|
||||
const custom = customEmojisEnabled ? customEmojis.map((ce) => ({
|
||||
aliases: [],
|
||||
name: ce.name,
|
||||
short_name: '',
|
||||
})) : [];
|
||||
|
||||
data = chunk<EmojiAlias>(builtInCustom.concat(custom), chunkSize);
|
||||
break;
|
||||
}
|
||||
case 'recent':
|
||||
// eslint-disable-next-line max-nested-callbacks
|
||||
data = chunk<EmojiAlias>(recentEmojis.map((emoji) => ({
|
||||
aliases: [],
|
||||
name: emoji,
|
||||
short_name: '',
|
||||
})), chunkSize);
|
||||
break;
|
||||
default:
|
||||
data = chunk(emojiIndices.map(fillEmoji), chunkSize);
|
||||
break;
|
||||
}
|
||||
|
||||
return {
|
||||
...categoryToI18n[category],
|
||||
data,
|
||||
key: category,
|
||||
};
|
||||
}).filter((s: EmojiSection) => s.data.length);
|
||||
}, [skinTone, customEmojis, customEmojisEnabled, width]);
|
||||
|
||||
const sectionIcons: SectionIconType[] = useMemo(() => {
|
||||
return sections.map((s) => ({
|
||||
key: s.key,
|
||||
icon: s.icon,
|
||||
}));
|
||||
}, [sections]);
|
||||
|
||||
const emojiSectionsByOffset = useMemo(() => {
|
||||
let lastOffset = 0;
|
||||
return sections.map((s) => {
|
||||
const start = lastOffset;
|
||||
const nextOffset = s.data.length * (EMOJI_SIZE + (EMOJI_GUTTER * 2));
|
||||
lastOffset += nextOffset;
|
||||
return start;
|
||||
});
|
||||
}, [sections]);
|
||||
|
||||
const onLoadMoreCustomEmojis = useCallback(async () => {
|
||||
if (!customEmojisEnabled || fetchingCustomEmojis || loadedAllCustomEmojis) {
|
||||
return;
|
||||
}
|
||||
setFetchingCustomEmojis(true);
|
||||
const {data, error} = await fetchCustomEmojis(serverUrl, customEmojiPage, EMOJIS_PER_PAGE);
|
||||
if (data?.length) {
|
||||
setCustomEmojiPage(customEmojiPage + 1);
|
||||
} else if (!error && (data && data.length < EMOJIS_PER_PAGE)) {
|
||||
setLoadedAllCustomEmojis(true);
|
||||
}
|
||||
|
||||
setFetchingCustomEmojis(false);
|
||||
}, [customEmojiPage, customEmojisEnabled, loadedAllCustomEmojis, fetchingCustomEmojis]);
|
||||
|
||||
const onScroll = useCallback((e: NativeSyntheticEvent<NativeScrollEvent>) => {
|
||||
const {contentOffset} = e.nativeEvent;
|
||||
let nextIndex = emojiSectionsByOffset.findIndex(
|
||||
(offset) => contentOffset.y <= offset,
|
||||
);
|
||||
|
||||
if (nextIndex === -1) {
|
||||
nextIndex = emojiSectionsByOffset.length - 1;
|
||||
} else if (nextIndex !== 0) {
|
||||
nextIndex -= 1;
|
||||
}
|
||||
|
||||
if (nextIndex !== sectionIndex) {
|
||||
setSectionIndex(nextIndex);
|
||||
}
|
||||
}, [emojiSectionsByOffset, sectionIndex]);
|
||||
|
||||
const scrollToIndex = (index: number) => {
|
||||
list.current?.scrollToLocation({sectionIndex: index, itemIndex: 0, animated: false, viewOffset: 0});
|
||||
setSectionIndex(index);
|
||||
};
|
||||
|
||||
const renderSectionHeader = useCallback(({section}) => {
|
||||
return (
|
||||
<SectionHeader section={section as EmojiSection}/>
|
||||
);
|
||||
}, []);
|
||||
|
||||
const renderFooter = useMemo(() => {
|
||||
return fetchingCustomEmojis ? <SectionFooter/> : null;
|
||||
}, [fetchingCustomEmojis]);
|
||||
|
||||
const renderItem = useCallback(({item}) => {
|
||||
return (
|
||||
<View style={styles.row}>
|
||||
{item.map((emoji: EmojiAlias) => {
|
||||
return (
|
||||
<TouchableEmoji
|
||||
key={emoji.name}
|
||||
name={emoji.name}
|
||||
onEmojiPress={onEmojiPress}
|
||||
size={EMOJI_SIZE}
|
||||
style={styles.emoji}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<SectionList
|
||||
getItemLayout={getItemLayout}
|
||||
initialNumToRender={20}
|
||||
keyboardDismissMode='interactive'
|
||||
keyboardShouldPersistTaps='always'
|
||||
ListFooterComponent={renderFooter}
|
||||
maxToRenderPerBatch={20}
|
||||
nativeID={SCROLLVIEW_NATIVE_ID}
|
||||
onEndReached={onLoadMoreCustomEmojis}
|
||||
onEndReachedThreshold={2}
|
||||
onScroll={onScroll}
|
||||
|
||||
// @ts-expect-error ref
|
||||
ref={list}
|
||||
renderItem={renderItem}
|
||||
renderSectionHeader={renderSectionHeader}
|
||||
sections={sections}
|
||||
contentContainerStyle={{paddingBottom: 50}}
|
||||
windowSize={100}
|
||||
/>
|
||||
<EmojiSectionBar
|
||||
currentIndex={sectionIndex}
|
||||
scrollToIndex={scrollToIndex}
|
||||
sections={sectionIcons}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default EmojiSections;
|
||||
30
app/components/emoji_picker/sections/section_footer.tsx
Normal file
30
app/components/emoji_picker/sections/section_footer.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {memo} from 'react';
|
||||
import {ActivityIndicator, View} from 'react-native';
|
||||
|
||||
import {useTheme} from '@context/theme';
|
||||
import {makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
const Footer = () => {
|
||||
const theme = useTheme();
|
||||
const styles = getStyleSheetFromTheme(theme);
|
||||
|
||||
return (
|
||||
<View style={styles.loading}>
|
||||
<ActivityIndicator color={theme.centerChannelColor}/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const getStyleSheetFromTheme = makeStyleSheetFromTheme(() => {
|
||||
return {
|
||||
loading: {
|
||||
flex: 1,
|
||||
alignItems: 'center',
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
export default memo(Footer);
|
||||
50
app/components/emoji_picker/sections/section_header.tsx
Normal file
50
app/components/emoji_picker/sections/section_header.tsx
Normal file
@@ -0,0 +1,50 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {memo} from 'react';
|
||||
import {View} from 'react-native';
|
||||
|
||||
import FormattedText from '@components/formatted_text';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
type Props = {
|
||||
section: EmojiSection;
|
||||
}
|
||||
|
||||
export const SECTION_HEADER_HEIGHT = 28;
|
||||
|
||||
const getStyleSheetFromTheme = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
return {
|
||||
sectionTitleContainer: {
|
||||
height: SECTION_HEADER_HEIGHT,
|
||||
justifyContent: 'center',
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
},
|
||||
sectionTitle: {
|
||||
color: changeOpacity(theme.centerChannelColor, 0.2),
|
||||
fontSize: 15,
|
||||
fontWeight: '700',
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const SectionHeader = ({section}: Props) => {
|
||||
const theme = useTheme();
|
||||
const styles = getStyleSheetFromTheme(theme);
|
||||
|
||||
return (
|
||||
<View
|
||||
style={styles.sectionTitleContainer}
|
||||
key={section.id}
|
||||
>
|
||||
<FormattedText
|
||||
style={styles.sectionTitle}
|
||||
id={section.id}
|
||||
defaultMessage={section.icon}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(SectionHeader);
|
||||
35
app/components/emoji_picker/sections/touchable_emoji.tsx
Normal file
35
app/components/emoji_picker/sections/touchable_emoji.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback} from 'react';
|
||||
import {StyleProp, ViewStyle} from 'react-native';
|
||||
|
||||
import Emoji from '@components/emoji';
|
||||
import TouchableWithFeedback from '@components/touchable_with_feedback';
|
||||
import {preventDoubleTap} from '@utils/tap';
|
||||
|
||||
type Props = {
|
||||
name: string;
|
||||
onEmojiPress: (emoji: string) => void;
|
||||
size?: number;
|
||||
style: StyleProp<ViewStyle>;
|
||||
}
|
||||
|
||||
const TouchableEmoji = ({name, onEmojiPress, size = 30, style}: Props) => {
|
||||
const onPress = useCallback(preventDoubleTap(() => onEmojiPress(name)), []);
|
||||
|
||||
return (
|
||||
<TouchableWithFeedback
|
||||
onPress={onPress}
|
||||
style={style}
|
||||
type={'opacity'}
|
||||
>
|
||||
<Emoji
|
||||
emojiName={name}
|
||||
size={size}
|
||||
/>
|
||||
</TouchableWithFeedback>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(TouchableEmoji);
|
||||
@@ -5,7 +5,7 @@ import React, {useEffect} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {Keyboard, StyleProp, View, ViewStyle} from 'react-native';
|
||||
|
||||
import {fetchMissinProfilesByIds, fetchMissinProfilesByUsernames} from '@actions/remote/user';
|
||||
import {fetchMissingProfilesByIds, fetchMissingProfilesByUsernames} from '@actions/remote/user';
|
||||
import Markdown from '@components/markdown';
|
||||
import SystemAvatar from '@components/system_avatar';
|
||||
import SystemHeader from '@components/system_header';
|
||||
@@ -70,11 +70,11 @@ const CombinedUserActivity = ({
|
||||
|
||||
const loadUserProfiles = () => {
|
||||
if (allUserIds.length) {
|
||||
fetchMissinProfilesByIds(serverUrl, allUserIds);
|
||||
fetchMissingProfilesByIds(serverUrl, allUserIds);
|
||||
}
|
||||
|
||||
if (allUsernames.length) {
|
||||
fetchMissinProfilesByUsernames(serverUrl, allUsernames);
|
||||
fetchMissingProfilesByUsernames(serverUrl, allUsernames);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ const getStyleSheet = (scale: number, th: Theme) => {
|
||||
moreImagesText: {
|
||||
color: theme.sidebarHeaderTextColor,
|
||||
fontSize: Math.round(PixelRatio.roundToNearestPixel(24 * scale)),
|
||||
fontFamily: 'Open Sans',
|
||||
fontFamily: 'OpenSans',
|
||||
textAlign: 'center',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -59,7 +59,7 @@ const Reaction = ({count, emojiName, highlight, onPress, onLongPress, theme}: Re
|
||||
<Emoji
|
||||
emojiName={emojiName}
|
||||
size={20}
|
||||
textStyle={{color: 'black', fontWeight: 'bold'}}
|
||||
textStyle={{color: '#000'}}
|
||||
customEmojiStyle={styles.customEmojiStyle}
|
||||
testID={`reaction.emoji.${emojiName}`}
|
||||
/>
|
||||
|
||||
@@ -103,7 +103,7 @@ const Header = (props: HeaderProps) => {
|
||||
theme={theme}
|
||||
userId={post.userId}
|
||||
/>
|
||||
{showCustomStatusEmoji && customStatusExpired && Boolean(customStatus?.emoji) && (
|
||||
{showCustomStatusEmoji && !customStatusExpired && Boolean(customStatus?.emoji) && (
|
||||
<CustomStatusEmoji
|
||||
customStatus={customStatus!}
|
||||
style={style.customStatusEmoji}
|
||||
|
||||
44
app/components/search_bar/components/clear_icon.tsx
Normal file
44
app/components/search_bar/components/clear_icon.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {memo, useCallback} from 'react';
|
||||
import {Platform} from 'react-native';
|
||||
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
|
||||
type ClearIconProps = {
|
||||
deleteIconSizeAndroid: number;
|
||||
onClear: () => void;
|
||||
placeholderTextColor: string;
|
||||
searchClearButtonTestID: string;
|
||||
tintColorDelete: string;
|
||||
titleCancelColor: string;
|
||||
}
|
||||
|
||||
const ClearIcon = ({deleteIconSizeAndroid, onClear, placeholderTextColor, searchClearButtonTestID, tintColorDelete, titleCancelColor}: ClearIconProps) => {
|
||||
const onPressClear = useCallback(() => onClear(), []);
|
||||
|
||||
if (Platform.OS === 'ios') {
|
||||
return (
|
||||
<CompassIcon
|
||||
testID={searchClearButtonTestID}
|
||||
name='close-circle'
|
||||
size={18}
|
||||
style={{color: tintColorDelete || 'grey'}}
|
||||
onPress={onPressClear}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<CompassIcon
|
||||
testID={searchClearButtonTestID}
|
||||
name='close'
|
||||
size={deleteIconSizeAndroid}
|
||||
color={titleCancelColor || placeholderTextColor}
|
||||
onPress={onPressClear}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(ClearIcon);
|
||||
53
app/components/search_bar/components/search_icon.tsx
Normal file
53
app/components/search_bar/components/search_icon.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {Platform, TouchableWithoutFeedback, ViewStyle} from 'react-native';
|
||||
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
|
||||
type SearchIconProps = {
|
||||
backArrowSize: number;
|
||||
clearIconColorAndroid: string;
|
||||
iOSStyle: ViewStyle;
|
||||
onCancel: () => void;
|
||||
searchCancelButtonTestID: string;
|
||||
searchIconColor: string;
|
||||
searchIconSize: number;
|
||||
showArrow: boolean;
|
||||
}
|
||||
|
||||
const SearchIcon = ({backArrowSize, clearIconColorAndroid, iOSStyle, onCancel, searchCancelButtonTestID, searchIconColor, searchIconSize, showArrow}: SearchIconProps) => {
|
||||
if (Platform.OS === 'ios') {
|
||||
return (
|
||||
<CompassIcon
|
||||
name='magnify'
|
||||
size={24}
|
||||
style={iOSStyle}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (showArrow) {
|
||||
return (
|
||||
<TouchableWithoutFeedback onPress={onCancel}>
|
||||
<CompassIcon
|
||||
testID={searchCancelButtonTestID}
|
||||
name='arrow-left'
|
||||
size={backArrowSize}
|
||||
color={clearIconColorAndroid}
|
||||
/>
|
||||
</TouchableWithoutFeedback>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<CompassIcon
|
||||
name='magnify'
|
||||
size={searchIconSize}
|
||||
color={searchIconColor}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default SearchIcon;
|
||||
316
app/components/search_bar/index.tsx
Normal file
316
app/components/search_bar/index.tsx
Normal file
@@ -0,0 +1,316 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {PureComponent} from 'react';
|
||||
import {IntlShape} from 'react-intl';
|
||||
import {
|
||||
Animated,
|
||||
InteractionManager,
|
||||
Keyboard,
|
||||
StyleSheet,
|
||||
View,
|
||||
Platform,
|
||||
ViewStyle,
|
||||
ReturnKeyTypeOptions,
|
||||
KeyboardTypeOptions,
|
||||
NativeSyntheticEvent,
|
||||
TextInputSelectionChangeEventData,
|
||||
TextStyle,
|
||||
} from 'react-native';
|
||||
import {SearchBar} from 'react-native-elements';
|
||||
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
|
||||
import ClearIcon from './components/clear_icon';
|
||||
import SearchIcon from './components/search_icon';
|
||||
import {getSearchStyles} from './styles';
|
||||
|
||||
const LEFT_COMPONENT_INITIAL_POSITION = Platform.OS === 'ios' ? 7 : 0;
|
||||
|
||||
type SearchProps = {
|
||||
autoCapitalize?: 'none' | 'sentences' | 'words' | 'characters' | undefined;
|
||||
autoFocus?: boolean;
|
||||
backArrowSize?: number;
|
||||
backgroundColor: string;
|
||||
blurOnSubmit?: boolean;
|
||||
cancelButtonStyle?: ViewStyle;
|
||||
cancelTitle?: string;
|
||||
containerHeight?: number;
|
||||
containerStyle?: ViewStyle;
|
||||
deleteIconSize?: number;
|
||||
editable?: boolean;
|
||||
inputHeight: number;
|
||||
inputStyle?: TextStyle;
|
||||
intl?: IntlShape;
|
||||
keyboardAppearance?: 'default' | 'light' | 'dark' | undefined;
|
||||
keyboardShouldPersist?: boolean;
|
||||
keyboardType?: KeyboardTypeOptions | undefined;
|
||||
leftComponent?: JSX.Element;
|
||||
onBlur?: () => void;
|
||||
onCancelButtonPress: (text?: string) => void;
|
||||
onChangeText: (text: string) => void;
|
||||
onFocus?: () => void;
|
||||
onSearchButtonPress?: (value: string) => void;
|
||||
onSelectionChange?: (e: NativeSyntheticEvent<TextInputSelectionChangeEventData>) => void;
|
||||
placeholder?: string;
|
||||
placeholderTextColor?: string;
|
||||
returnKeyType?: ReturnKeyTypeOptions | undefined;
|
||||
searchBarRightMargin?: number;
|
||||
searchIconSize?: number;
|
||||
selectionColor?: string;
|
||||
showArrow?: boolean;
|
||||
showCancel?: boolean;
|
||||
testID: string;
|
||||
tintColorDelete: string;
|
||||
tintColorSearch: string;
|
||||
titleCancelColor: string;
|
||||
value?: string;
|
||||
};
|
||||
|
||||
type SearchState = {
|
||||
leftComponentWidth: number;
|
||||
}
|
||||
|
||||
export default class Search extends PureComponent<SearchProps, SearchState> {
|
||||
static defaultProps = {
|
||||
backArrowSize: 24,
|
||||
blurOnSubmit: false,
|
||||
containerHeight: 40,
|
||||
deleteIconSize: 20,
|
||||
editable: true,
|
||||
keyboardShouldPersist: false,
|
||||
keyboardType: 'default',
|
||||
onBlur: () => true,
|
||||
onSelectionChange: () => true,
|
||||
placeholderTextColor: 'grey',
|
||||
returnKeyType: 'search',
|
||||
searchBarRightMargin: 0,
|
||||
searchIconSize: 24,
|
||||
showArrow: false,
|
||||
showCancel: true,
|
||||
value: '',
|
||||
};
|
||||
|
||||
private readonly leftComponentAnimated: Animated.Value;
|
||||
private readonly searchContainerAnimated: Animated.Value;
|
||||
private searchContainerRef: any;
|
||||
private inputKeywordRef: any;
|
||||
private readonly searchStyle: any;
|
||||
|
||||
constructor(props: SearchProps | Readonly<SearchProps>) {
|
||||
super(props);
|
||||
this.state = {
|
||||
leftComponentWidth: 0,
|
||||
};
|
||||
|
||||
this.leftComponentAnimated = new Animated.Value(LEFT_COMPONENT_INITIAL_POSITION);
|
||||
this.searchContainerAnimated = new Animated.Value(0);
|
||||
|
||||
const {backgroundColor, cancelButtonStyle, containerHeight, inputHeight, inputStyle, placeholderTextColor, searchBarRightMargin, tintColorDelete, tintColorSearch, titleCancelColor} = props;
|
||||
|
||||
this.searchStyle = getSearchStyles(backgroundColor, cancelButtonStyle, containerHeight!, inputHeight, inputStyle, placeholderTextColor!, searchBarRightMargin!, tintColorDelete, tintColorSearch, titleCancelColor);
|
||||
}
|
||||
|
||||
setSearchContainerRef = (ref: any) => {
|
||||
this.searchContainerRef = ref;
|
||||
};
|
||||
|
||||
setInputKeywordRef = (ref: any) => {
|
||||
this.inputKeywordRef = ref;
|
||||
};
|
||||
|
||||
blur = () => {
|
||||
this.inputKeywordRef.blur();
|
||||
};
|
||||
|
||||
focus = () => {
|
||||
this.inputKeywordRef.focus();
|
||||
};
|
||||
|
||||
onBlur = async () => {
|
||||
this.props?.onBlur?.();
|
||||
|
||||
if (this.props.leftComponent) {
|
||||
await this.collapseAnimation();
|
||||
}
|
||||
};
|
||||
|
||||
onLeftComponentLayout = (event: { nativeEvent: { layout: { width: any } } }) => {
|
||||
const leftComponentWidth = event.nativeEvent.layout.width;
|
||||
this.setState({leftComponentWidth});
|
||||
};
|
||||
|
||||
onSearch = async () => {
|
||||
const {keyboardShouldPersist, onSearchButtonPress, value} = this.props;
|
||||
if (!keyboardShouldPersist) {
|
||||
await Keyboard.dismiss();
|
||||
}
|
||||
|
||||
if (value) {
|
||||
onSearchButtonPress?.(value);
|
||||
}
|
||||
};
|
||||
|
||||
onChangeText = (text: string) => {
|
||||
const {onChangeText} = this.props;
|
||||
if (onChangeText) {
|
||||
onChangeText(text);
|
||||
}
|
||||
};
|
||||
|
||||
onFocus = () => {
|
||||
const {leftComponent, onFocus} = this.props;
|
||||
InteractionManager.runAfterInteractions(async () => {
|
||||
onFocus?.();
|
||||
if (leftComponent) {
|
||||
await this.expandAnimation();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
onClear = () => {
|
||||
this.focus();
|
||||
this.props.onChangeText('');
|
||||
};
|
||||
|
||||
onCancel = () => {
|
||||
const {onCancelButtonPress} = this.props;
|
||||
|
||||
Keyboard.dismiss();
|
||||
InteractionManager.runAfterInteractions(() => {
|
||||
return onCancelButtonPress?.();
|
||||
});
|
||||
};
|
||||
|
||||
onSelectionChange = (event: NativeSyntheticEvent<TextInputSelectionChangeEventData>) => {
|
||||
const {onSelectionChange} = this.props;
|
||||
onSelectionChange?.(event);
|
||||
};
|
||||
|
||||
expandAnimation = () => {
|
||||
return new Promise((resolve) => {
|
||||
Animated.parallel([
|
||||
Animated.timing(this.leftComponentAnimated, {toValue: -115, duration: 200} as Animated.TimingAnimationConfig),
|
||||
Animated.timing(this.searchContainerAnimated, {toValue: this.state.leftComponentWidth * -1, duration: 200} as Animated.TimingAnimationConfig),
|
||||
]).start(resolve);
|
||||
});
|
||||
};
|
||||
|
||||
collapseAnimation = () => {
|
||||
return new Promise((resolve) => {
|
||||
Animated.parallel([
|
||||
Animated.timing(this.leftComponentAnimated, {toValue: LEFT_COMPONENT_INITIAL_POSITION, duration: 200} as Animated.TimingAnimationConfig),
|
||||
Animated.timing(this.searchContainerAnimated, {toValue: 0, duration: 200} as Animated.TimingAnimationConfig),
|
||||
]).start(resolve);
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const {autoCapitalize, autoFocus, backArrowSize, blurOnSubmit, cancelTitle, deleteIconSize, editable, intl, keyboardAppearance, keyboardType, leftComponent, placeholder, placeholderTextColor, returnKeyType, searchIconSize, selectionColor, showArrow, showCancel, testID, tintColorDelete, tintColorSearch, titleCancelColor, value} = this.props;
|
||||
|
||||
const searchClearButtonTestID = `${testID}.search.clear.button`;
|
||||
const searchCancelButtonTestID = `${testID}.search.cancel.button`;
|
||||
const searchInputTestID = `${testID}.search.input`;
|
||||
|
||||
const {cancelButtonPropStyle, containerStyle, inputContainerStyle, inputTextStyle, searchBarStyle, styles} = this.searchStyle;
|
||||
|
||||
return (
|
||||
<View
|
||||
testID={testID}
|
||||
style={[searchBarStyle.container, this.props.containerStyle]}
|
||||
>
|
||||
{leftComponent && (
|
||||
<Animated.View
|
||||
style={[styles.leftComponent, {left: this.leftComponentAnimated}]}
|
||||
onLayout={this.onLeftComponentLayout}
|
||||
>
|
||||
{leftComponent}
|
||||
</Animated.View>
|
||||
)}
|
||||
<Animated.View
|
||||
style={[
|
||||
styles.fullWidth,
|
||||
searchBarStyle.searchBarWrapper,
|
||||
{marginLeft: this.searchContainerAnimated},
|
||||
]}
|
||||
>
|
||||
<SearchBar
|
||||
testID={searchInputTestID}
|
||||
autoCapitalize={autoCapitalize}
|
||||
autoCorrect={false}
|
||||
autoFocus={autoFocus}
|
||||
blurOnSubmit={blurOnSubmit}
|
||||
cancelButtonProps={cancelButtonPropStyle}
|
||||
cancelButtonTitle={cancelTitle || intl?.formatMessage({id: 'mobile.post.cancel', defaultMessage: 'Cancel'}) || ''}
|
||||
cancelIcon={
|
||||
|
||||
// Making sure the icon won't change depending on whether the input is in focus on Android devices
|
||||
Platform.OS === 'android' && (
|
||||
<CompassIcon
|
||||
testID={searchCancelButtonTestID}
|
||||
name='arrow-left'
|
||||
size={25}
|
||||
color={searchBarStyle.clearIconColorAndroid}
|
||||
onPress={this.onCancel}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
// @ts-expect-error: The clearIcon can also accept a ReactElement
|
||||
clearIcon={
|
||||
<ClearIcon
|
||||
deleteIconSizeAndroid={deleteIconSize!}
|
||||
onClear={this.onClear}
|
||||
placeholderTextColor={placeholderTextColor!}
|
||||
searchClearButtonTestID={searchClearButtonTestID}
|
||||
tintColorDelete={tintColorDelete}
|
||||
titleCancelColor={titleCancelColor}
|
||||
/>
|
||||
}
|
||||
containerStyle={containerStyle}
|
||||
disableFullscreenUI={true}
|
||||
editable={editable}
|
||||
enablesReturnKeyAutomatically={true}
|
||||
inputContainerStyle={inputContainerStyle}
|
||||
inputStyle={inputTextStyle}
|
||||
keyboardAppearance={keyboardAppearance}
|
||||
keyboardType={keyboardType}
|
||||
leftIconContainerStyle={styles.leftIcon}
|
||||
placeholder={placeholder || intl?.formatMessage({id: 'search_bar.search', defaultMessage: 'Search'}) || ''}
|
||||
placeholderTextColor={placeholderTextColor}
|
||||
platform={Platform.OS as 'ios' | 'android'}
|
||||
onBlur={this.onBlur}
|
||||
onCancel={this.onCancel}
|
||||
|
||||
// @ts-expect-error: The TS definition for this SearchBar is messed up
|
||||
onChangeText={this.onChangeText}
|
||||
onClear={this.onClear}
|
||||
onFocus={this.onFocus}
|
||||
onSelectionChange={this.onSelectionChange}
|
||||
onSubmitEditing={this.onSearch}
|
||||
|
||||
// @ts-expect-error: The searchIcon can also accept a ReactElement
|
||||
searchIcon={
|
||||
<SearchIcon
|
||||
searchIconColor={tintColorSearch || placeholderTextColor!}
|
||||
searchIconSize={searchIconSize!}
|
||||
clearIconColorAndroid={titleCancelColor || placeholderTextColor!}
|
||||
backArrowSize={backArrowSize!}
|
||||
searchCancelButtonTestID={searchCancelButtonTestID}
|
||||
onCancel={this.onCancel}
|
||||
showArrow={showArrow!}
|
||||
iOSStyle={StyleSheet.flatten([styles.fullWidth, searchBarStyle.searchIcon])}
|
||||
/>
|
||||
}
|
||||
selectionColor={selectionColor}
|
||||
showCancel={showCancel!}
|
||||
ref={this.setInputKeywordRef}
|
||||
returnKeyType={returnKeyType}
|
||||
underlineColorAndroid='transparent'
|
||||
value={value!}
|
||||
/>
|
||||
</Animated.View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
125
app/components/search_bar/styles.ts
Normal file
125
app/components/search_bar/styles.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Platform, StyleSheet, TextStyle, ViewStyle} from 'react-native';
|
||||
|
||||
export const getSearchBarStyle = (backgroundColor: string, cancelButtonStyle: ViewStyle | undefined, containerHeight: number, inputHeight: number, inputStyle: TextStyle | undefined, placeholderTextColor: string, searchBarRightMargin: number, tintColorDelete: string, tintColorSearch: string, titleCancelColor: string) => ({
|
||||
cancelButtonText: {
|
||||
...cancelButtonStyle,
|
||||
color: titleCancelColor,
|
||||
},
|
||||
container: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'flex-start',
|
||||
alignItems: 'center',
|
||||
height: containerHeight,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
clearIconColorIos: tintColorDelete || 'grey',
|
||||
clearIconColorAndroid: titleCancelColor || placeholderTextColor,
|
||||
inputStyle: {
|
||||
...inputStyle,
|
||||
backgroundColor: 'transparent',
|
||||
height: inputHeight,
|
||||
},
|
||||
inputContainer: {
|
||||
backgroundColor: inputStyle?.backgroundColor,
|
||||
height: inputHeight,
|
||||
},
|
||||
searchBarWrapper: {
|
||||
marginRight: searchBarRightMargin,
|
||||
height: Platform.select({
|
||||
ios: inputHeight || containerHeight - 10,
|
||||
android: inputHeight,
|
||||
}),
|
||||
},
|
||||
searchBarContainer: {
|
||||
backgroundColor,
|
||||
},
|
||||
searchIcon: {
|
||||
color: tintColorSearch || placeholderTextColor,
|
||||
top: 8,
|
||||
},
|
||||
searchIconColor: tintColorSearch || placeholderTextColor,
|
||||
});
|
||||
|
||||
export const getStyles = () => StyleSheet.create({
|
||||
defaultColor: {
|
||||
color: 'grey',
|
||||
},
|
||||
fullWidth: {
|
||||
flex: 1,
|
||||
},
|
||||
inputContainer: {
|
||||
borderRadius: Platform.select({
|
||||
ios: 2,
|
||||
android: 0,
|
||||
}),
|
||||
},
|
||||
inputMargin: {
|
||||
marginLeft: 4,
|
||||
paddingTop: 0,
|
||||
marginTop: Platform.select({
|
||||
ios: 0,
|
||||
android: 8,
|
||||
}),
|
||||
},
|
||||
leftIcon: {
|
||||
marginLeft: 4,
|
||||
width: 30,
|
||||
},
|
||||
searchContainer: {
|
||||
paddingTop: 0,
|
||||
paddingBottom: 0,
|
||||
},
|
||||
text: {
|
||||
fontSize: Platform.select({
|
||||
ios: 14,
|
||||
android: 15,
|
||||
}),
|
||||
color: '#fff',
|
||||
},
|
||||
leftComponent: {
|
||||
position: 'relative',
|
||||
marginLeft: 2,
|
||||
},
|
||||
});
|
||||
|
||||
export const getSearchStyles = (backgroundColor: string, cancelButtonStyle: ViewStyle | undefined, containerHeight: number, inputHeight: number, inputStyle: TextStyle | undefined, placeholderTextColor: string, searchBarRightMargin: number, tintColorDelete: string, tintColorSearch: string, titleCancelColor: string) => {
|
||||
const searchBarStyle = getSearchBarStyle(backgroundColor, cancelButtonStyle, containerHeight, inputHeight, inputStyle, placeholderTextColor, searchBarRightMargin, tintColorDelete, tintColorSearch, titleCancelColor);
|
||||
|
||||
const styles = getStyles();
|
||||
|
||||
const inputTextStyle = {
|
||||
...styles.text,
|
||||
...styles.inputMargin,
|
||||
...searchBarStyle.inputStyle,
|
||||
};
|
||||
|
||||
const inputContainerStyle = {
|
||||
...styles.inputContainer,
|
||||
...searchBarStyle.inputContainer,
|
||||
};
|
||||
|
||||
const containerStyle = {
|
||||
...styles.searchContainer,
|
||||
...styles.fullWidth,
|
||||
...searchBarStyle.searchBarContainer,
|
||||
};
|
||||
|
||||
const cancelButtonPropStyle = {
|
||||
buttonTextStyle: {
|
||||
...styles.text,
|
||||
...searchBarStyle.cancelButtonText,
|
||||
},
|
||||
};
|
||||
|
||||
return {
|
||||
cancelButtonPropStyle,
|
||||
containerStyle,
|
||||
inputContainerStyle,
|
||||
inputTextStyle,
|
||||
searchBarStyle,
|
||||
styles,
|
||||
};
|
||||
};
|
||||
@@ -2,7 +2,7 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback} from 'react';
|
||||
import {StyleProp, Text, View, ViewStyle} from 'react-native';
|
||||
import {StyleProp, Text, TextStyle, View, ViewStyle} from 'react-native';
|
||||
import FastImage, {ImageStyle, Source} from 'react-native-fast-image';
|
||||
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
@@ -15,7 +15,9 @@ import {isValidUrl} from '@utils/url';
|
||||
type SlideUpPanelProps = {
|
||||
destructive?: boolean;
|
||||
icon?: string | Source;
|
||||
imageStyles?: StyleProp<TextStyle>;
|
||||
onPress: () => void;
|
||||
textStyles?: TextStyle;
|
||||
testID?: string;
|
||||
text: string;
|
||||
}
|
||||
@@ -32,14 +34,13 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
color: '#D0021B',
|
||||
},
|
||||
row: {
|
||||
flex: 1,
|
||||
width: '100%',
|
||||
flexDirection: 'row',
|
||||
},
|
||||
iconContainer: {
|
||||
alignItems: 'center',
|
||||
height: 50,
|
||||
justifyContent: 'center',
|
||||
width: 60,
|
||||
marginRight: 10,
|
||||
},
|
||||
noIconContainer: {
|
||||
height: 50,
|
||||
@@ -61,15 +62,10 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
opacity: 0.9,
|
||||
letterSpacing: -0.45,
|
||||
},
|
||||
footer: {
|
||||
marginHorizontal: 17,
|
||||
borderBottomWidth: 0.5,
|
||||
borderBottomColor: changeOpacity(theme.centerChannelColor, 0.2),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const SlideUpPanelItem = ({destructive, icon, onPress, testID, text}: SlideUpPanelProps) => {
|
||||
const SlideUpPanelItem = ({destructive, icon, imageStyles, onPress, testID, text, textStyles}: SlideUpPanelProps) => {
|
||||
const theme = useTheme();
|
||||
const handleOnPress = useCallback(preventDoubleTap(onPress, 500), []);
|
||||
const style = getStyleSheet(theme);
|
||||
@@ -77,7 +73,7 @@ const SlideUpPanelItem = ({destructive, icon, onPress, testID, text}: SlideUpPan
|
||||
let image;
|
||||
let iconStyle: StyleProp<ViewStyle> = [style.iconContainer];
|
||||
if (icon) {
|
||||
const imageStyle: StyleProp<ImageStyle> = [style.icon];
|
||||
const imageStyle: StyleProp<ImageStyle> = [style.icon, imageStyles];
|
||||
if (destructive) {
|
||||
imageStyle.push(style.destructive);
|
||||
}
|
||||
@@ -105,27 +101,22 @@ const SlideUpPanelItem = ({destructive, icon, onPress, testID, text}: SlideUpPan
|
||||
}
|
||||
|
||||
return (
|
||||
<View
|
||||
testID={testID}
|
||||
<TouchableWithFeedback
|
||||
onPress={handleOnPress}
|
||||
style={style.container}
|
||||
testID={testID}
|
||||
type='native'
|
||||
underlayColor={changeOpacity(theme.centerChannelColor, 0.5)}
|
||||
>
|
||||
<TouchableWithFeedback
|
||||
onPress={handleOnPress}
|
||||
style={style.row}
|
||||
type='native'
|
||||
underlayColor={changeOpacity(theme.centerChannelColor, 0.5)}
|
||||
>
|
||||
<View style={style.row}>
|
||||
{Boolean(image) &&
|
||||
<View style={iconStyle}>{image}</View>
|
||||
}
|
||||
<View style={style.textContainer}>
|
||||
<Text style={[style.text, destructive ? style.destructive : null]}>{text}</Text>
|
||||
</View>
|
||||
<View style={style.row}>
|
||||
{Boolean(image) &&
|
||||
<View style={iconStyle}>{image}</View>
|
||||
}
|
||||
<View style={style.textContainer}>
|
||||
<Text style={[style.text, destructive ? style.destructive : null, textStyles]}>{text}</Text>
|
||||
</View>
|
||||
</TouchableWithFeedback>
|
||||
<View style={style.footer}/>
|
||||
</View>
|
||||
</View>
|
||||
</TouchableWithFeedback>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
66
app/components/status_label/index.tsx
Normal file
66
app/components/status_label/index.tsx
Normal file
@@ -0,0 +1,66 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {TextStyle} from 'react-native';
|
||||
|
||||
import FormattedText from '@components/formatted_text';
|
||||
import {General} from '@constants';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {t} from '@i18n';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
type StatusLabelProps = {
|
||||
status?: string;
|
||||
labelStyle?: TextStyle;
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
return {
|
||||
label: {
|
||||
color: changeOpacity(theme.centerChannelColor, 0.5),
|
||||
fontSize: 17,
|
||||
textAlignVertical: 'center',
|
||||
includeFontPadding: false,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const StatusLabel = ({status = General.OFFLINE, labelStyle}: StatusLabelProps) => {
|
||||
const theme = useTheme();
|
||||
const style = getStyleSheet(theme);
|
||||
|
||||
let i18nId = t('status_dropdown.set_offline');
|
||||
let defaultMessage = 'Offline';
|
||||
|
||||
switch (status) {
|
||||
case General.AWAY:
|
||||
i18nId = t('status_dropdown.set_away');
|
||||
defaultMessage = 'Away';
|
||||
break;
|
||||
case General.DND:
|
||||
i18nId = t('status_dropdown.set_dnd');
|
||||
defaultMessage = 'Do Not Disturb';
|
||||
break;
|
||||
case General.ONLINE:
|
||||
i18nId = t('status_dropdown.set_online');
|
||||
defaultMessage = 'Online';
|
||||
break;
|
||||
}
|
||||
|
||||
if (status === General.OUT_OF_OFFICE) {
|
||||
i18nId = t('status_dropdown.set_ooo');
|
||||
defaultMessage = 'Out Of Office';
|
||||
}
|
||||
|
||||
return (
|
||||
<FormattedText
|
||||
id={i18nId}
|
||||
defaultMessage={defaultMessage}
|
||||
style={[style.label, labelStyle]}
|
||||
testID={`user_status.label.${status}`}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default StatusLabel;
|
||||
78
app/components/tablet_title/index.tsx
Normal file
78
app/components/tablet_title/index.tsx
Normal file
@@ -0,0 +1,78 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {Platform, Text, View} from 'react-native';
|
||||
|
||||
import TouchableWithFeedback from '@components/touchable_with_feedback';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
type Props = {
|
||||
action?: string;
|
||||
onPress: () => void;
|
||||
title: string;
|
||||
testID: string;
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||
actionContainer: {
|
||||
alignItems: 'flex-end',
|
||||
justifyContent: 'center',
|
||||
marginRight: 20,
|
||||
},
|
||||
action: {
|
||||
color: theme.buttonBg,
|
||||
fontFamily: 'OpenSans-Semibold',
|
||||
fontSize: 16,
|
||||
lineHeight: 24,
|
||||
},
|
||||
container: {
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: changeOpacity(theme.centerChannelColor, 0.08),
|
||||
flexDirection: 'row',
|
||||
height: 34,
|
||||
width: '100%',
|
||||
},
|
||||
titleContainer: {
|
||||
alignItems: 'center',
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
},
|
||||
title: {
|
||||
color: theme.centerChannelColor,
|
||||
fontFamily: 'OpenSans-Semibold',
|
||||
fontSize: 18,
|
||||
lineHeight: 24,
|
||||
},
|
||||
}));
|
||||
|
||||
const TabletTitle = ({action, onPress, testID, title}: Props) => {
|
||||
const theme = useTheme();
|
||||
const styles = getStyleSheet(theme);
|
||||
|
||||
return (
|
||||
<>
|
||||
<View style={styles.container}>
|
||||
<View style={styles.titleContainer}>
|
||||
<Text style={styles.title}>{title}</Text>
|
||||
</View>
|
||||
{Boolean(action) &&
|
||||
<View style={styles.actionContainer}>
|
||||
<TouchableWithFeedback
|
||||
onPress={onPress}
|
||||
type={Platform.select({android: 'native', ios: 'opacity'})}
|
||||
testID={testID}
|
||||
underlayColor={changeOpacity(theme.centerChannelColor, 0.1)}
|
||||
>
|
||||
<Text style={styles.action}>{action}</Text>
|
||||
</TouchableWithFeedback>
|
||||
</View>
|
||||
}
|
||||
</View>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default TabletTitle;
|
||||
@@ -4,16 +4,15 @@
|
||||
/* eslint-disable new-cap */
|
||||
|
||||
import React, {memo} from 'react';
|
||||
import {TouchableOpacity, TouchableWithoutFeedback, View, StyleProp, ViewStyle} from 'react-native';
|
||||
import {Touchable, TouchableOpacity, TouchableWithoutFeedback, View, StyleProp, ViewStyle} from 'react-native';
|
||||
import {TouchableNativeFeedback} from 'react-native-gesture-handler';
|
||||
|
||||
type TouchableProps = {
|
||||
type TouchableProps = Touchable & {
|
||||
testID: string;
|
||||
children: React.ReactNode | React.ReactNode[];
|
||||
underlayColor: string;
|
||||
type: 'native' | 'opacity' | 'none';
|
||||
style?: StyleProp<ViewStyle>;
|
||||
[x: string]: any;
|
||||
}
|
||||
|
||||
const TouchableWithFeedbackAndroid = ({testID, children, underlayColor, type = 'native', ...props}: TouchableProps) => {
|
||||
@@ -23,7 +22,7 @@ const TouchableWithFeedbackAndroid = ({testID, children, underlayColor, type = '
|
||||
<TouchableNativeFeedback
|
||||
testID={testID}
|
||||
{...props}
|
||||
style={[props.style, {flex: undefined, flexDirection: undefined, width: '100%', height: '100%'}]}
|
||||
style={[props.style]}
|
||||
background={TouchableNativeFeedback.Ripple(underlayColor || '#fff', false)}
|
||||
>
|
||||
<View>
|
||||
|
||||
@@ -2,14 +2,13 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {memo} from 'react';
|
||||
import {PanResponder, TouchableHighlight, TouchableOpacity, TouchableWithoutFeedback, View} from 'react-native';
|
||||
import {PanResponder, Touchable, TouchableHighlight, TouchableOpacity, TouchableWithoutFeedback, View} from 'react-native';
|
||||
|
||||
type TouchableProps = {
|
||||
type TouchableProps = Touchable & {
|
||||
cancelTouchOnPanning: boolean;
|
||||
children: React.ReactNode | React.ReactNode[];
|
||||
testID: string;
|
||||
type: 'native' | 'opacity' | 'none';
|
||||
[x: string]: any;
|
||||
}
|
||||
|
||||
const TouchableWithFeedbackIOS = ({testID, children, type = 'native', cancelTouchOnPanning, ...props}: TouchableProps) => {
|
||||
|
||||
61
app/constants/custom_status.ts
Normal file
61
app/constants/custom_status.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {t} from '@i18n';
|
||||
|
||||
export enum CustomStatusDuration {
|
||||
DONT_CLEAR = '',
|
||||
THIRTY_MINUTES = 'thirty_minutes',
|
||||
ONE_HOUR = 'one_hour',
|
||||
FOUR_HOURS = 'four_hours',
|
||||
TODAY = 'today',
|
||||
THIS_WEEK = 'this_week',
|
||||
DATE_AND_TIME = 'date_and_time',
|
||||
}
|
||||
|
||||
const {
|
||||
DONT_CLEAR,
|
||||
THIRTY_MINUTES,
|
||||
ONE_HOUR,
|
||||
FOUR_HOURS,
|
||||
TODAY,
|
||||
THIS_WEEK,
|
||||
DATE_AND_TIME,
|
||||
} = CustomStatusDuration;
|
||||
|
||||
export const CST = {
|
||||
[DONT_CLEAR]: {
|
||||
id: t('custom_status.expiry_dropdown.dont_clear'),
|
||||
defaultMessage: "Don't clear",
|
||||
},
|
||||
[THIRTY_MINUTES]: {
|
||||
id: t('custom_status.expiry_dropdown.thirty_minutes'),
|
||||
defaultMessage: '30 minutes',
|
||||
},
|
||||
[ONE_HOUR]: {
|
||||
id: t('custom_status.expiry_dropdown.one_hour'),
|
||||
defaultMessage: '1 hour',
|
||||
},
|
||||
[FOUR_HOURS]: {
|
||||
id: t('custom_status.expiry_dropdown.four_hours'),
|
||||
defaultMessage: '4 hours',
|
||||
},
|
||||
[TODAY]: {
|
||||
id: t('custom_status.expiry_dropdown.today'),
|
||||
defaultMessage: 'Today',
|
||||
},
|
||||
[THIS_WEEK]: {
|
||||
id: t('custom_status.expiry_dropdown.this_week'),
|
||||
defaultMessage: 'This week',
|
||||
},
|
||||
[DATE_AND_TIME]: {
|
||||
id: t('custom_status.expiry_dropdown.date_and_time'),
|
||||
defaultMessage: 'Date and Time',
|
||||
},
|
||||
};
|
||||
|
||||
export const CUSTOM_STATUS_TEXT_CHARACTER_LIMIT = 100;
|
||||
|
||||
export const SET_CUSTOM_STATUS_FAILURE = 'set_custom_status_failure';
|
||||
|
||||
export const CUSTOM_STATUS_TIME_PICKER_INTERVALS_IN_MINUTES = 30;
|
||||
@@ -57,6 +57,7 @@ export const SYSTEM_IDENTIFIERS = {
|
||||
INTEGRATION_TRIGGER_ID: 'IntegreationTriggerId',
|
||||
LICENSE: 'license',
|
||||
WEBSOCKET: 'WebSocket',
|
||||
RECENT_CUSTOM_STATUS: 'recentCustomStatus',
|
||||
};
|
||||
|
||||
export const GLOBAL_IDENTIFIERS = {
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
|
||||
export const ALL_EMOJIS = 'all_emojis';
|
||||
export const MAX_ALLOWED_REACTIONS = 40;
|
||||
export const SORT_BY_NAME = 'name';
|
||||
export const EMOJIS_PER_PAGE = 200;
|
||||
|
||||
// reEmoji matches an emoji (eg. :taco:) at the start of a string.
|
||||
export const reEmoji = /^:([a-z0-9_\-+]+):\B/i;
|
||||
@@ -13,8 +15,8 @@ export const reEmoticon = /^(?:(:-?\))|(;-?\))|(:o)|(:-o)|(:-?])|(:-?d)|(x-d)|(:
|
||||
// reMain matches some amount of plain text, starting at the beginning of the string and hopefully stopping right
|
||||
// before the next emoji by looking for any character that could start an emoji (:, ;, x, or <)
|
||||
export const reMain = /^[\s\S]+?(?=[:;x<]|$)/i;
|
||||
|
||||
export default {
|
||||
ALL_EMOJIS,
|
||||
MAX_ALLOWED_REACTIONS,
|
||||
SORT_BY_NAME,
|
||||
};
|
||||
|
||||
8
app/constants/events.ts
Normal file
8
app/constants/events.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import keyMirror from '@utils/key_mirror';
|
||||
|
||||
export default keyMirror({
|
||||
ACCOUNT_SELECT_TABLET_VIEW: null,
|
||||
});
|
||||
@@ -4,9 +4,12 @@
|
||||
import ActionType from './action_type';
|
||||
import Apps from './apps';
|
||||
import Attachment from './attachment';
|
||||
import {CustomStatusDuration} from './custom_status';
|
||||
import Database from './database';
|
||||
import DeepLink from './deep_linking';
|
||||
import Device from './device';
|
||||
import Emoji from './emoji';
|
||||
import Events from './events';
|
||||
import Files from './files';
|
||||
import General from './general';
|
||||
import List from './list';
|
||||
@@ -22,11 +25,14 @@ import WebsocketEvents from './websocket';
|
||||
|
||||
export {
|
||||
ActionType,
|
||||
Attachment,
|
||||
Apps,
|
||||
Attachment,
|
||||
CustomStatusDuration,
|
||||
Database,
|
||||
DeepLink,
|
||||
Device,
|
||||
Emoji,
|
||||
Events,
|
||||
Files,
|
||||
General,
|
||||
List,
|
||||
|
||||
@@ -6,6 +6,7 @@ const Preferences: Record<string, any> = {
|
||||
CATEGORY_CHANNEL_APPROXIMATE_VIEW_TIME: 'channel_approximate_view_time',
|
||||
CATEGORY_DIRECT_CHANNEL_SHOW: 'direct_channel_show',
|
||||
CATEGORY_GROUP_CHANNEL_SHOW: 'group_channel_show',
|
||||
CATEGORY_EMOJI: 'emoji',
|
||||
CATEGORY_FLAGGED_POST: 'flagged_post',
|
||||
CATEGORY_FAVORITE_CHANNEL: 'favorite_channel',
|
||||
CATEGORY_AUTO_RESET_MANUAL_STATUS: 'auto_reset_manual_status',
|
||||
@@ -28,6 +29,7 @@ const Preferences: Record<string, any> = {
|
||||
DISPLAY_PREFER_NICKNAME: 'nickname_full_name',
|
||||
DISPLAY_PREFER_FULL_NAME: 'full_name',
|
||||
DISPLAY_PREFER_USERNAME: 'username',
|
||||
EMOJI_SKINTONE: 'emoji_skintone',
|
||||
MENTION_KEYS: 'mention_keys',
|
||||
USE_MILITARY_TIME: 'use_military_time',
|
||||
CATEGORY_SIDEBAR_SETTINGS: 'sidebar_settings',
|
||||
|
||||
@@ -2,12 +2,15 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
export const ABOUT = 'About';
|
||||
export const EMOJI_PICKER = 'AddReaction';
|
||||
export const APP_FORM = 'AppForm';
|
||||
export const BOTTOM_SHEET = 'BottomSheet';
|
||||
export const CHANNEL = 'Channel';
|
||||
export const CUSTOM_STATUS_CLEAR_AFTER = 'CustomStatusClearAfter';
|
||||
export const CUSTOM_STATUS = 'CustomStatus';
|
||||
export const FORGOT_PASSWORD = 'ForgotPassword';
|
||||
export const INTEGRATION_SELECTOR = 'IntegrationSelector';
|
||||
export const HOME = 'Home';
|
||||
export const INTEGRATION_SELECTOR = 'IntegrationSelector';
|
||||
export const LOGIN = 'Login';
|
||||
export const LOGIN_OPTIONS = 'LoginOptions';
|
||||
export const MAIN_SIDEBAR = 'MainSidebar';
|
||||
@@ -21,12 +24,15 @@ export const THREAD = 'Thread';
|
||||
|
||||
export default {
|
||||
ABOUT,
|
||||
EMOJI_PICKER,
|
||||
APP_FORM,
|
||||
BOTTOM_SHEET,
|
||||
CHANNEL,
|
||||
CUSTOM_STATUS_CLEAR_AFTER,
|
||||
CUSTOM_STATUS,
|
||||
FORGOT_PASSWORD,
|
||||
INTEGRATION_SELECTOR,
|
||||
HOME,
|
||||
INTEGRATION_SELECTOR,
|
||||
LOGIN,
|
||||
LOGIN_OPTIONS,
|
||||
MAIN_SIDEBAR,
|
||||
|
||||
@@ -5,17 +5,19 @@ import {Q} from '@nozbe/watermelondb';
|
||||
import withObservables from '@nozbe/with-observables';
|
||||
import React, {ComponentType, createContext, useEffect} from 'react';
|
||||
import {Appearance, EventSubscription} from 'react-native';
|
||||
import {of as of$} from 'rxjs';
|
||||
import {catchError, switchMap} from 'rxjs/operators';
|
||||
|
||||
import {Preferences} from '@constants';
|
||||
import {MM_TABLES, SYSTEM_IDENTIFIERS} from '@constants/database';
|
||||
import EphemeralStore from '@store/ephemeral_store';
|
||||
import {setNavigationStackStyles} from '@utils/theme';
|
||||
import {setNavigationStackStyles, setThemeDefaults} from '@utils/theme';
|
||||
|
||||
import type {PreferenceModel, SystemModel} from '@database/models/server';
|
||||
import type Database from '@nozbe/watermelondb/Database';
|
||||
|
||||
type Props = {
|
||||
currentTeamId: SystemModel[];
|
||||
currentTeamId?: string;
|
||||
children: React.ReactNode;
|
||||
themes: PreferenceModel[];
|
||||
}
|
||||
@@ -38,12 +40,11 @@ const {Consumer, Provider} = ThemeContext;
|
||||
|
||||
const ThemeProvider = ({currentTeamId, children, themes}: Props) => {
|
||||
const getTheme = (): Theme => {
|
||||
if (currentTeamId.length) {
|
||||
const teamId = currentTeamId[0]?.value;
|
||||
const teamTheme = themes.find((t) => t.name === teamId) || themes[0];
|
||||
if (currentTeamId) {
|
||||
const teamTheme = themes.find((t) => t.name === currentTeamId) || themes[0];
|
||||
if (teamTheme?.value) {
|
||||
try {
|
||||
const theme = JSON.parse(teamTheme.value) as Theme;
|
||||
const theme = setThemeDefaults(JSON.parse(teamTheme.value) as Theme);
|
||||
EphemeralStore.theme = theme;
|
||||
requestAnimationFrame(() => {
|
||||
setNavigationStackStyles(theme);
|
||||
@@ -87,7 +88,10 @@ export function useTheme(): Theme {
|
||||
}
|
||||
|
||||
const enhancedThemeProvider = withObservables([], ({database}: {database: Database}) => ({
|
||||
currentTeamId: database.get(SYSTEM).query(Q.where('id', SYSTEM_IDENTIFIERS.CURRENT_TEAM_ID)).observe(),
|
||||
currentTeamId: database.get<SystemModel>(SYSTEM).findAndObserve(SYSTEM_IDENTIFIERS.CURRENT_TEAM_ID).pipe(
|
||||
switchMap((row) => of$(row.value)),
|
||||
catchError(() => of$(undefined)),
|
||||
),
|
||||
themes: database.get(PREFERENCE).query(Q.where('category', Preferences.CATEGORY_THEME)).observeWithColumns(['value']),
|
||||
}));
|
||||
|
||||
|
||||
@@ -15,21 +15,22 @@ import type ServersModel from '@typings/database/models/app/servers';
|
||||
type State = {
|
||||
database: Database;
|
||||
serverUrl: string;
|
||||
}
|
||||
};
|
||||
|
||||
const {SERVERS} = MM_TABLES.APP;
|
||||
|
||||
export function withServerDatabase<T>(
|
||||
Component: ComponentType<T>,
|
||||
): ComponentType<T> {
|
||||
export function withServerDatabase<T>(Component: ComponentType<T>): ComponentType<T> {
|
||||
return function ServerDatabaseComponent(props) {
|
||||
const [state, setState] = useState<State|undefined>();
|
||||
const [state, setState] = useState<State | undefined>();
|
||||
const db = DatabaseManager.appDatabase?.database;
|
||||
|
||||
const observer = (servers: ServersModel[]) => {
|
||||
const server = servers.reduce((a, b) => (b.lastActiveAt > a.lastActiveAt ? b : a));
|
||||
const server = servers.reduce((a, b) =>
|
||||
(b.lastActiveAt > a.lastActiveAt ? b : a),
|
||||
);
|
||||
|
||||
const serverDatabase = DatabaseManager.serverDatabases[server?.url]?.database;
|
||||
const serverDatabase =
|
||||
DatabaseManager.serverDatabases[server?.url]?.database;
|
||||
|
||||
setState({
|
||||
database: serverDatabase,
|
||||
@@ -54,9 +55,7 @@ export function withServerDatabase<T>(
|
||||
}
|
||||
|
||||
return (
|
||||
<DatabaseProvider
|
||||
database={(state.database)}
|
||||
>
|
||||
<DatabaseProvider database={state.database}>
|
||||
<ServerUrlProvider url={state.serverUrl}>
|
||||
<ThemeProvider database={state.database}>
|
||||
<Component {...props}/>
|
||||
|
||||
@@ -136,7 +136,7 @@ export default class UserModel extends Model {
|
||||
/** teams : All the team that this user is part of */
|
||||
@children(TEAM_MEMBERSHIP) teams!: TeamMembershipModel[];
|
||||
|
||||
prepareSatus = (status: string) => {
|
||||
prepareStatus = (status: string) => {
|
||||
this.prepareUpdate((u) => {
|
||||
u.status = status;
|
||||
});
|
||||
|
||||
@@ -8,7 +8,7 @@ import {Notifications} from 'react-native-notifications';
|
||||
import {appEntry, upgradeEntry} from '@actions/remote/entry';
|
||||
import {Screens} from '@constants';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {getActiveServerUrl, getServerCredentials} from '@init/credentials';
|
||||
import {getActiveServerUrl, getServerCredentials, removeServerCredentials} from '@init/credentials';
|
||||
import {queryThemeForCurrentTeam} from '@queries/servers/preference';
|
||||
import {queryCurrentUserId} from '@queries/servers/system';
|
||||
import {goToScreen, resetToHome, resetToSelectServer} from '@screens/navigation';
|
||||
@@ -43,7 +43,7 @@ const launchAppFromNotification = (notification: NotificationWithData) => {
|
||||
};
|
||||
|
||||
const launchApp = async (props: LaunchProps, resetNavigation = true) => {
|
||||
let serverUrl;
|
||||
let serverUrl: string | undefined;
|
||||
switch (props?.launchType) {
|
||||
case LaunchType.DeepLink:
|
||||
if (props.extra?.type !== DeepLinkType.Invalid) {
|
||||
@@ -85,7 +85,11 @@ const launchApp = async (props: LaunchProps, resetNavigation = true) => {
|
||||
`An error ocurred while upgrading the app to the new version.\n\nDetails: ${result.error}\n\nThe app will now quit.`,
|
||||
[{
|
||||
text: 'OK',
|
||||
onPress: () => Emm.exitApp(),
|
||||
onPress: async () => {
|
||||
await DatabaseManager.destroyServerDatabase(serverUrl!);
|
||||
await removeServerCredentials(serverUrl!);
|
||||
Emm.exitApp();
|
||||
},
|
||||
}],
|
||||
);
|
||||
return;
|
||||
|
||||
24
app/queries/servers/custom_emoji.ts
Normal file
24
app/queries/servers/custom_emoji.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Database, Q} from '@nozbe/watermelondb';
|
||||
|
||||
import {MM_TABLES} from '@constants/database';
|
||||
|
||||
import type CustomEmojiModel from '@typings/database/models/servers/custom_emoji';
|
||||
|
||||
export const queryAllCustomEmojis = async (database: Database): Promise<CustomEmojiModel[]> => {
|
||||
try {
|
||||
return database.get<CustomEmojiModel>(MM_TABLES.SERVER.CUSTOM_EMOJI).query().fetch();
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
export const queryCustomEmojisByName = async (database: Database, names: string[]): Promise<CustomEmojiModel[]> => {
|
||||
try {
|
||||
return database.get<CustomEmojiModel>(MM_TABLES.SERVER.CUSTOM_EMOJI).query(Q.where('name', Q.oneOf(names))).fetch();
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
@@ -23,10 +23,13 @@ export const prepareMyPreferences = (operator: ServerDataOperator, preferences:
|
||||
};
|
||||
|
||||
export const queryPreferencesByCategoryAndName = (database: Database, category: string, name: string) => {
|
||||
return database.get(MM_TABLES.SERVER.PREFERENCE).query(
|
||||
Q.where('category', category),
|
||||
Q.where('name', name),
|
||||
).fetch() as Promise<PreferenceModel[]>;
|
||||
return database.
|
||||
get<PreferenceModel>(MM_TABLES.SERVER.PREFERENCE).
|
||||
query(
|
||||
Q.where('category', category),
|
||||
Q.where('name', name),
|
||||
).
|
||||
fetch();
|
||||
};
|
||||
|
||||
export const queryThemeForCurrentTeam = async (database: Database) => {
|
||||
|
||||
@@ -90,6 +90,15 @@ export const queryConfig = async (serverDatabase: Database) => {
|
||||
}
|
||||
};
|
||||
|
||||
export const queryRecentCustomStatuses = async (serverDatabase: Database) => {
|
||||
try {
|
||||
const recent = await serverDatabase.get<SystemModel>(SYSTEM).find(SYSTEM_IDENTIFIERS.RECENT_CUSTOM_STATUS);
|
||||
return recent;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
export const queryExpandedLinks = async (serverDatabase: Database) => {
|
||||
try {
|
||||
const expandedLinks = await serverDatabase.get(SYSTEM).find(SYSTEM_IDENTIFIERS.EXPANDED_LINKS) as SystemModel;
|
||||
|
||||
@@ -2,39 +2,55 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {ReactNode, useEffect, useRef} from 'react';
|
||||
import {BackHandler, DeviceEventEmitter, StyleSheet, useWindowDimensions, View} from 'react-native';
|
||||
import {BackHandler, DeviceEventEmitter, Keyboard, StyleSheet, useWindowDimensions, View} from 'react-native';
|
||||
import {State, TapGestureHandler} from 'react-native-gesture-handler';
|
||||
import {Navigation as RNN} from 'react-native-navigation';
|
||||
import Animated from 'react-native-reanimated';
|
||||
import RNBottomSheet from 'reanimated-bottom-sheet';
|
||||
|
||||
import {Navigation} from '@constants';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@app/utils/theme';
|
||||
import {Device, Navigation} from '@constants';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {useSplitView} from '@hooks/device';
|
||||
import {dismissModal} from '@screens/navigation';
|
||||
import {hapticFeedback} from '@utils/general';
|
||||
|
||||
import Indicator from './indicator';
|
||||
|
||||
type SlideUpPanelProps = {
|
||||
closeButtonId?: string;
|
||||
initialSnapIndex?: number;
|
||||
renderContent: () => ReactNode;
|
||||
snapPoints?: Array<string | number>;
|
||||
}
|
||||
|
||||
const BottomSheet = ({initialSnapIndex = 0, renderContent, snapPoints = ['90%', '50%', 50]}: SlideUpPanelProps) => {
|
||||
const BottomSheet = ({closeButtonId, initialSnapIndex = 0, renderContent, snapPoints = ['90%', '50%', 50]}: SlideUpPanelProps) => {
|
||||
const sheetRef = useRef<RNBottomSheet>(null);
|
||||
const dimensions = useWindowDimensions();
|
||||
const isSplitView = useSplitView();
|
||||
const isTablet = Device.IS_TABLET && !isSplitView;
|
||||
const theme = useTheme();
|
||||
const lastSnap = snapPoints.length - 1;
|
||||
|
||||
useEffect(() => {
|
||||
const listener = DeviceEventEmitter.addListener(Navigation.NAVIGATION_CLOSE_MODAL, () => sheetRef.current?.snapTo(lastSnap));
|
||||
const listener = DeviceEventEmitter.addListener(Navigation.NAVIGATION_CLOSE_MODAL, () => {
|
||||
if (sheetRef.current) {
|
||||
sheetRef.current.snapTo(lastSnap);
|
||||
} else {
|
||||
dismissModal();
|
||||
}
|
||||
});
|
||||
|
||||
return () => listener.remove();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const listener = BackHandler.addEventListener('hardwareBackPress', () => {
|
||||
sheetRef.current?.snapTo(1);
|
||||
if (sheetRef.current) {
|
||||
sheetRef.current.snapTo(1);
|
||||
} else {
|
||||
dismissModal();
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
@@ -43,9 +59,20 @@ const BottomSheet = ({initialSnapIndex = 0, renderContent, snapPoints = ['90%',
|
||||
|
||||
useEffect(() => {
|
||||
hapticFeedback();
|
||||
Keyboard.dismiss();
|
||||
sheetRef.current?.snapTo(initialSnapIndex);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const navigationEvents = RNN.events().registerNavigationButtonPressedListener(({buttonId}) => {
|
||||
if (closeButtonId && buttonId === closeButtonId) {
|
||||
dismissModal();
|
||||
}
|
||||
});
|
||||
|
||||
return () => navigationEvents.remove();
|
||||
}, []);
|
||||
|
||||
const renderBackdrop = () => {
|
||||
return (
|
||||
<TapGestureHandler
|
||||
@@ -69,9 +96,11 @@ const BottomSheet = ({initialSnapIndex = 0, renderContent, snapPoints = ['90%',
|
||||
style={{
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
opacity: 1,
|
||||
padding: 16,
|
||||
paddingHorizontal: 16,
|
||||
paddingBottom: isTablet ? 20 : 16,
|
||||
paddingTop: isTablet ? 0 : 16,
|
||||
height: '100%',
|
||||
width: Math.min(dimensions.width, 450),
|
||||
width: isTablet ? '100%' : Math.min(dimensions.width, 450),
|
||||
alignSelf: 'center',
|
||||
}}
|
||||
>
|
||||
@@ -79,6 +108,16 @@ const BottomSheet = ({initialSnapIndex = 0, renderContent, snapPoints = ['90%',
|
||||
</View>
|
||||
);
|
||||
|
||||
if (isTablet) {
|
||||
const styles = getStyleSheet(theme);
|
||||
return (
|
||||
<>
|
||||
<View style={styles.separator}/>
|
||||
{renderContainer()}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<RNBottomSheet
|
||||
@@ -97,4 +136,14 @@ const BottomSheet = ({initialSnapIndex = 0, renderContent, snapPoints = ['90%',
|
||||
);
|
||||
};
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
return {
|
||||
separator: {
|
||||
height: 1,
|
||||
borderTopWidth: 1,
|
||||
borderColor: changeOpacity(theme.centerChannelColor, 0.08),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
export default BottomSheet;
|
||||
|
||||
102
app/screens/custom_status/components/clear_after.tsx
Normal file
102
app/screens/custom_status/components/clear_after.tsx
Normal file
@@ -0,0 +1,102 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {Text, TouchableOpacity, View} from 'react-native';
|
||||
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import CustomStatusExpiry from '@components/custom_status/custom_status_expiry';
|
||||
import FormattedText from '@components/formatted_text';
|
||||
import {CustomStatusDuration, CST} from '@constants/custom_status';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
import type UserModel from '@typings/database/models/servers/user';
|
||||
import type {Moment} from 'moment-timezone';
|
||||
|
||||
type Props = {
|
||||
currentUser: UserModel;
|
||||
duration: CustomStatusDuration;
|
||||
onOpenClearAfterModal: () => void;
|
||||
theme: Theme;
|
||||
expiresAt: Moment;
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
return {
|
||||
rightIcon: {
|
||||
position: 'absolute',
|
||||
right: 18,
|
||||
color: changeOpacity(theme.centerChannelColor, 0.5),
|
||||
},
|
||||
expiryTimeLabel: {
|
||||
fontSize: 17,
|
||||
paddingLeft: 16,
|
||||
textAlignVertical: 'center',
|
||||
color: theme.centerChannelColor,
|
||||
},
|
||||
inputContainer: {
|
||||
justifyContent: 'center',
|
||||
height: 48,
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
},
|
||||
expiryTime: {
|
||||
position: 'absolute',
|
||||
right: 42,
|
||||
color: changeOpacity(theme.centerChannelColor, 0.5),
|
||||
},
|
||||
customStatusExpiry: {
|
||||
color: changeOpacity(theme.centerChannelColor, 0.5),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const ClearAfter = ({currentUser, duration, expiresAt, onOpenClearAfterModal, theme}: Props) => {
|
||||
const intl = useIntl();
|
||||
const style = getStyleSheet(theme);
|
||||
|
||||
const renderClearAfterTime = () => {
|
||||
if (duration && duration === CustomStatusDuration.DATE_AND_TIME) {
|
||||
return (
|
||||
<View style={style.expiryTime}>
|
||||
<CustomStatusExpiry
|
||||
currentUser={currentUser}
|
||||
textStyles={style.customStatusExpiry}
|
||||
theme={theme}
|
||||
time={expiresAt.toDate()}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<FormattedText
|
||||
id={CST[duration].id}
|
||||
defaultMessage={CST[duration].defaultMessage}
|
||||
style={style.expiryTime}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
testID={'custom_status.clear_after.action'}
|
||||
onPress={onOpenClearAfterModal}
|
||||
>
|
||||
<View
|
||||
testID={`custom_status.duration.${duration}`}
|
||||
style={style.inputContainer}
|
||||
>
|
||||
<Text style={style.expiryTimeLabel}>{intl.formatMessage({id: 'mobile.custom_status.clear_after', defaultMessage: 'Clear After'})}</Text>
|
||||
{renderClearAfterTime()}
|
||||
<CompassIcon
|
||||
name='chevron-right'
|
||||
size={24}
|
||||
style={style.rightIcon}
|
||||
/>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
};
|
||||
|
||||
export default ClearAfter;
|
||||
60
app/screens/custom_status/components/custom_status_emoji.tsx
Normal file
60
app/screens/custom_status/components/custom_status_emoji.tsx
Normal file
@@ -0,0 +1,60 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {Text, TouchableOpacity} from 'react-native';
|
||||
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import Emoji from '@components/emoji';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
type Props = {
|
||||
onPress: () => void;
|
||||
isStatusSet: boolean;
|
||||
emoji?: string;
|
||||
theme: Theme;
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
return {
|
||||
iconContainer: {
|
||||
position: 'absolute',
|
||||
left: 14,
|
||||
top: 10,
|
||||
},
|
||||
icon: {
|
||||
color: changeOpacity(theme.centerChannelColor, 0.64),
|
||||
},
|
||||
emoji: {
|
||||
color: theme.centerChannelColor,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const CustomStatusEmoji = ({emoji, isStatusSet, onPress, theme}: Props) => {
|
||||
const style = getStyleSheet(theme);
|
||||
return (
|
||||
<TouchableOpacity
|
||||
testID={`custom_status.emoji.${isStatusSet ? (emoji || 'speech_balloon') : 'default'}`}
|
||||
onPress={onPress}
|
||||
style={style.iconContainer}
|
||||
>
|
||||
{isStatusSet ? (
|
||||
<Text style={style.emoji}>
|
||||
<Emoji
|
||||
emojiName={emoji || 'speech_balloon'}
|
||||
size={20}
|
||||
/>
|
||||
</Text>
|
||||
) : (
|
||||
<CompassIcon
|
||||
name='emoticon-happy-outline'
|
||||
size={24}
|
||||
style={style.icon}
|
||||
/>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
);
|
||||
};
|
||||
|
||||
export default CustomStatusEmoji;
|
||||
103
app/screens/custom_status/components/custom_status_input.tsx
Normal file
103
app/screens/custom_status/components/custom_status_input.tsx
Normal file
@@ -0,0 +1,103 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {TextInput, View} from 'react-native';
|
||||
|
||||
import ClearButton from '@components/custom_status/clear_button';
|
||||
import {CUSTOM_STATUS_TEXT_CHARACTER_LIMIT} from '@constants/custom_status';
|
||||
import {changeOpacity, getKeyboardAppearanceFromTheme, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
import CustomStatusEmoji from './custom_status_emoji';
|
||||
|
||||
type Props = {
|
||||
emoji?: string;
|
||||
isStatusSet: boolean;
|
||||
onChangeText: (value: string) => void;
|
||||
onClearHandle: () => void;
|
||||
onOpenEmojiPicker: () => void;
|
||||
text?: string;
|
||||
theme: Theme;
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
return {
|
||||
divider: {
|
||||
backgroundColor: changeOpacity(theme.centerChannelColor, 0.2),
|
||||
height: 1,
|
||||
marginRight: 16,
|
||||
marginLeft: 52,
|
||||
},
|
||||
clearButton: {
|
||||
position: 'absolute',
|
||||
top: 3,
|
||||
right: 14,
|
||||
},
|
||||
input: {
|
||||
alignSelf: 'stretch',
|
||||
color: theme.centerChannelColor,
|
||||
width: '100%',
|
||||
fontSize: 17,
|
||||
paddingHorizontal: 52,
|
||||
textAlignVertical: 'center',
|
||||
height: '100%',
|
||||
},
|
||||
inputContainer: {
|
||||
justifyContent: 'center',
|
||||
height: 48,
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const CustomStatusInput = ({emoji, isStatusSet, onChangeText, onClearHandle, onOpenEmojiPicker, text, theme}: Props) => {
|
||||
const style = getStyleSheet(theme);
|
||||
const intl = useIntl();
|
||||
const placeholder = intl.formatMessage({id: 'custom_status.set_status', defaultMessage: 'Set a Status'});
|
||||
|
||||
return (
|
||||
<View style={style.inputContainer}>
|
||||
<TextInput
|
||||
testID='custom_status.input'
|
||||
autoCapitalize='none'
|
||||
autoCorrect={false}
|
||||
blurOnSubmit={false}
|
||||
disableFullscreenUI={true}
|
||||
keyboardAppearance={getKeyboardAppearanceFromTheme(theme)}
|
||||
keyboardType='default'
|
||||
maxLength={CUSTOM_STATUS_TEXT_CHARACTER_LIMIT}
|
||||
onChangeText={onChangeText}
|
||||
placeholder={placeholder}
|
||||
placeholderTextColor={changeOpacity(theme.centerChannelColor, 0.5)}
|
||||
returnKeyType='go'
|
||||
style={style.input}
|
||||
secureTextEntry={false}
|
||||
underlineColorAndroid='transparent'
|
||||
value={text}
|
||||
/>
|
||||
{isStatusSet && (
|
||||
<View style={style.divider}/>
|
||||
)}
|
||||
<CustomStatusEmoji
|
||||
emoji={emoji}
|
||||
isStatusSet={isStatusSet}
|
||||
onPress={onOpenEmojiPicker}
|
||||
theme={theme}
|
||||
/>
|
||||
{isStatusSet ? (
|
||||
<View
|
||||
style={style.clearButton}
|
||||
testID='custom_status.input.clear.button'
|
||||
>
|
||||
<ClearButton
|
||||
handlePress={onClearHandle}
|
||||
theme={theme}
|
||||
/>
|
||||
</View>
|
||||
) : null}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default CustomStatusInput;
|
||||
@@ -0,0 +1,144 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {Text, TouchableOpacity, View} from 'react-native';
|
||||
|
||||
import ClearButton from '@components/custom_status/clear_button';
|
||||
import CustomStatusText from '@components/custom_status/custom_status_text';
|
||||
import Emoji from '@components/emoji';
|
||||
import {CST, CustomStatusDuration} from '@constants/custom_status';
|
||||
import {preventDoubleTap} from '@utils/tap';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
type Props = {
|
||||
duration: CustomStatusDuration;
|
||||
emoji?: string;
|
||||
expires_at?: string;
|
||||
handleClear?: (status: UserCustomStatus) => void;
|
||||
handleSuggestionClick: (status: UserCustomStatus) => void;
|
||||
isExpirySupported: boolean;
|
||||
separator: boolean;
|
||||
text?: string;
|
||||
theme: Theme;
|
||||
};
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
return {
|
||||
container: {
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
flexDirection: 'row',
|
||||
minHeight: 50,
|
||||
},
|
||||
iconContainer: {
|
||||
width: 45,
|
||||
height: 46,
|
||||
left: 14,
|
||||
top: 12,
|
||||
marginRight: 6,
|
||||
color: theme.centerChannelColor,
|
||||
},
|
||||
wrapper: {
|
||||
flex: 1,
|
||||
},
|
||||
textContainer: {
|
||||
paddingTop: 14,
|
||||
paddingBottom: 14,
|
||||
justifyContent: 'center',
|
||||
width: '70%',
|
||||
flex: 1,
|
||||
},
|
||||
clearButtonContainer: {
|
||||
position: 'absolute',
|
||||
top: 4,
|
||||
right: 13,
|
||||
},
|
||||
divider: {
|
||||
backgroundColor: changeOpacity(theme.centerChannelColor, 0.2),
|
||||
height: 1,
|
||||
marginRight: 16,
|
||||
},
|
||||
customStatusDuration: {
|
||||
color: changeOpacity(theme.centerChannelColor, 0.6),
|
||||
fontSize: 15,
|
||||
},
|
||||
customStatusText: {
|
||||
color: theme.centerChannelColor,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const CustomStatusSuggestion = ({duration, emoji, expires_at, handleClear, handleSuggestionClick, isExpirySupported, separator, text, theme}: Props) => {
|
||||
const style = getStyleSheet(theme);
|
||||
const intl = useIntl();
|
||||
|
||||
const handleClick = useCallback(preventDoubleTap(() => {
|
||||
handleSuggestionClick({emoji, text, duration});
|
||||
}), []);
|
||||
|
||||
const handleSuggestionClear = useCallback(() => {
|
||||
if (handleClear) {
|
||||
handleClear({emoji, text, duration, expires_at});
|
||||
}
|
||||
}, []);
|
||||
|
||||
const showCustomStatus = Boolean(duration && duration !== CustomStatusDuration.DATE_AND_TIME && isExpirySupported);
|
||||
|
||||
const clearButton =
|
||||
handleClear && expires_at ? (
|
||||
<View style={style.clearButtonContainer}>
|
||||
<ClearButton
|
||||
handlePress={handleSuggestionClear}
|
||||
theme={theme}
|
||||
iconName='close'
|
||||
size={18}
|
||||
testID='custom_status_suggestion.clear.button'
|
||||
/>
|
||||
</View>
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
testID={`custom_status_suggestion.${text}`}
|
||||
onPress={handleClick}
|
||||
>
|
||||
<View style={style.container}>
|
||||
{emoji && (
|
||||
<Text style={style.iconContainer}>
|
||||
<Emoji
|
||||
emojiName={emoji}
|
||||
size={20}
|
||||
/>
|
||||
</Text>
|
||||
)}
|
||||
<View style={style.wrapper}>
|
||||
<View style={style.textContainer}>
|
||||
{Boolean(text) && (
|
||||
<View>
|
||||
<CustomStatusText
|
||||
text={text}
|
||||
theme={theme}
|
||||
textStyle={style.customStatusText}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
{showCustomStatus && (
|
||||
<View style={{paddingTop: 5}}>
|
||||
<CustomStatusText
|
||||
text={intl.formatMessage(CST[duration])}
|
||||
theme={theme}
|
||||
textStyle={style.customStatusDuration}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
{clearButton}
|
||||
{separator && <View style={style.divider}/>}
|
||||
</View>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
};
|
||||
|
||||
export default CustomStatusSuggestion;
|
||||
@@ -0,0 +1,107 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {IntlShape} from 'react-intl';
|
||||
import {View} from 'react-native';
|
||||
|
||||
import FormattedText from '@components/formatted_text';
|
||||
import {CustomStatusDuration} from '@constants/custom_status';
|
||||
import {t} from '@i18n';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
import CustomStatusSuggestion from './custom_status_suggestion';
|
||||
|
||||
type Props = {
|
||||
intl: IntlShape;
|
||||
isExpirySupported: boolean;
|
||||
onHandleCustomStatusSuggestionClick: (status: UserCustomStatus) => void;
|
||||
recentCustomStatuses: UserCustomStatus[];
|
||||
theme: Theme;
|
||||
};
|
||||
|
||||
type DefaultUserCustomStatus = {
|
||||
emoji: string;
|
||||
message: string;
|
||||
messageDefault: string;
|
||||
durationDefault: string;
|
||||
};
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
return {
|
||||
separator: {
|
||||
marginTop: 32,
|
||||
},
|
||||
title: {
|
||||
fontSize: 17,
|
||||
marginBottom: 12,
|
||||
color: changeOpacity(theme.centerChannelColor, 0.5),
|
||||
marginLeft: 16,
|
||||
},
|
||||
block: {
|
||||
borderBottomColor: changeOpacity(theme.centerChannelColor, 0.1),
|
||||
borderBottomWidth: 1,
|
||||
borderTopColor: changeOpacity(theme.centerChannelColor, 0.1),
|
||||
borderTopWidth: 1,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const defaultCustomStatusSuggestions: DefaultUserCustomStatus[] = [
|
||||
{emoji: 'calendar', message: t('custom_status.suggestions.in_a_meeting'), messageDefault: 'In a meeting', durationDefault: CustomStatusDuration.ONE_HOUR},
|
||||
{emoji: 'hamburger', message: t('custom_status.suggestions.out_for_lunch'), messageDefault: 'Out for lunch', durationDefault: CustomStatusDuration.THIRTY_MINUTES},
|
||||
{emoji: 'sneezing_face', message: t('custom_status.suggestions.out_sick'), messageDefault: 'Out sick', durationDefault: CustomStatusDuration.TODAY},
|
||||
{emoji: 'house', message: t('custom_status.suggestions.working_from_home'), messageDefault: 'Working from home', durationDefault: CustomStatusDuration.TODAY},
|
||||
{emoji: 'palm_tree', message: t('custom_status.suggestions.on_a_vacation'), messageDefault: 'On a vacation', durationDefault: CustomStatusDuration.THIS_WEEK},
|
||||
];
|
||||
|
||||
const CustomStatusSuggestions = ({
|
||||
intl,
|
||||
isExpirySupported,
|
||||
onHandleCustomStatusSuggestionClick,
|
||||
recentCustomStatuses,
|
||||
theme,
|
||||
}: Props) => {
|
||||
const style = getStyleSheet(theme);
|
||||
const recentCustomStatusTexts = recentCustomStatuses.map((status: UserCustomStatus) => status.text);
|
||||
|
||||
const customStatusSuggestions = defaultCustomStatusSuggestions.
|
||||
map((status) => ({
|
||||
emoji: status.emoji,
|
||||
text: intl.formatMessage({id: status.message, defaultMessage: status.messageDefault}),
|
||||
duration: status.durationDefault,
|
||||
})).
|
||||
filter((status: UserCustomStatus) => !recentCustomStatusTexts.includes(status.text)).
|
||||
map((status: UserCustomStatus, index: number, arr: UserCustomStatus[]) => (
|
||||
<CustomStatusSuggestion
|
||||
key={status.text}
|
||||
handleSuggestionClick={onHandleCustomStatusSuggestionClick} // this.handleCustomStatusSuggestionClick
|
||||
emoji={status.emoji}
|
||||
text={status.text}
|
||||
theme={theme}
|
||||
separator={index !== arr.length - 1}
|
||||
duration={status.duration}
|
||||
isExpirySupported={isExpirySupported}
|
||||
/>
|
||||
));
|
||||
|
||||
if (customStatusSuggestions.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<View style={style.separator}/>
|
||||
<View testID='custom_status.suggestions'>
|
||||
<FormattedText
|
||||
id={t('custom_status.suggestions.title')}
|
||||
defaultMessage='SUGGESTIONS'
|
||||
style={style.title}
|
||||
/>
|
||||
<View style={style.block}>{customStatusSuggestions}</View>
|
||||
</View>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default CustomStatusSuggestions;
|
||||
@@ -0,0 +1,77 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {View} from 'react-native';
|
||||
|
||||
import FormattedText from '@components/formatted_text';
|
||||
import {t} from '@i18n';
|
||||
import CustomStatusSuggestion from '@screens/custom_status/components/custom_status_suggestion';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
type Props = {
|
||||
isExpirySupported: boolean;
|
||||
onHandleClear: (status: UserCustomStatus) => void;
|
||||
onHandleSuggestionClick: (status: UserCustomStatus) => void;
|
||||
recentCustomStatuses: UserCustomStatus[];
|
||||
theme: Theme;
|
||||
}
|
||||
|
||||
const RecentCustomStatuses = ({isExpirySupported, onHandleClear, onHandleSuggestionClick, recentCustomStatuses, theme}: Props) => {
|
||||
const style = getStyleSheet(theme);
|
||||
|
||||
if (recentCustomStatuses.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<View style={style.separator}/>
|
||||
<View testID='custom_status.recents'>
|
||||
<FormattedText
|
||||
id={t('custom_status.suggestions.recent_title')}
|
||||
defaultMessage='RECENT'
|
||||
style={style.title}
|
||||
/>
|
||||
<View style={style.block}>
|
||||
{recentCustomStatuses.map((status: UserCustomStatus, index: number) => (
|
||||
<CustomStatusSuggestion
|
||||
key={`${status.text}-${index.toString()}`}
|
||||
handleSuggestionClick={onHandleSuggestionClick}
|
||||
handleClear={onHandleClear}
|
||||
emoji={status?.emoji}
|
||||
text={status?.text}
|
||||
theme={theme}
|
||||
separator={index !== recentCustomStatuses.length - 1}
|
||||
duration={status.duration}
|
||||
expires_at={status.expires_at}
|
||||
isExpirySupported={isExpirySupported}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
</View >
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
return {
|
||||
separator: {
|
||||
marginTop: 32,
|
||||
},
|
||||
title: {
|
||||
fontSize: 17,
|
||||
marginBottom: 12,
|
||||
color: changeOpacity(theme.centerChannelColor, 0.5),
|
||||
marginLeft: 16,
|
||||
},
|
||||
block: {
|
||||
borderBottomColor: changeOpacity(theme.centerChannelColor, 0.1),
|
||||
borderBottomWidth: 1,
|
||||
borderTopColor: changeOpacity(theme.centerChannelColor, 0.1),
|
||||
borderTopWidth: 1,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
export default RecentCustomStatuses;
|
||||
425
app/screens/custom_status/index.tsx
Normal file
425
app/screens/custom_status/index.tsx
Normal file
@@ -0,0 +1,425 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider';
|
||||
import withObservables from '@nozbe/with-observables';
|
||||
import moment, {Moment} from 'moment-timezone';
|
||||
import React from 'react';
|
||||
import {injectIntl, IntlShape} from 'react-intl';
|
||||
import {BackHandler, DeviceEventEmitter, Keyboard, KeyboardAvoidingView, Platform, ScrollView, View} from 'react-native';
|
||||
import {EventSubscription, Navigation, NavigationButtonPressedEvent, NavigationComponent, NavigationComponentProps} from 'react-native-navigation';
|
||||
import {Edge, SafeAreaView} from 'react-native-safe-area-context';
|
||||
import {of as of$} from 'rxjs';
|
||||
import {switchMap, catchError} from 'rxjs/operators';
|
||||
|
||||
import {updateLocalCustomStatus} from '@actions/local/user';
|
||||
import {removeRecentCustomStatus, updateCustomStatus, unsetCustomStatus} from '@actions/remote/user';
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import StatusBar from '@components/status_bar';
|
||||
import TabletTitle from '@components/tablet_title';
|
||||
import {CustomStatusDuration, Events, Screens} from '@constants';
|
||||
import {SET_CUSTOM_STATUS_FAILURE} from '@constants/custom_status';
|
||||
import {MM_TABLES, SYSTEM_IDENTIFIERS} from '@constants/database';
|
||||
import {withServerUrl} from '@context/server_url';
|
||||
import {withTheme} from '@context/theme';
|
||||
import {dismissModal, goToScreen, mergeNavigationOptions, showModal} from '@screens/navigation';
|
||||
import {getCurrentMomentForTimezone, getRoundedTime, isCustomStatusExpirySupported, safeParseJSON} from '@utils/helpers';
|
||||
import {preventDoubleTap} from '@utils/tap';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {
|
||||
getTimezone,
|
||||
getUserCustomStatus,
|
||||
isCustomStatusExpired as verifyExpiredStatus,
|
||||
} from '@utils/user';
|
||||
|
||||
import ClearAfter from './components/clear_after';
|
||||
import CustomStatusInput from './components/custom_status_input';
|
||||
import CustomStatusSuggestions from './components/custom_status_suggestions';
|
||||
import RecentCustomStatuses from './components/recent_custom_statuses';
|
||||
|
||||
import type {WithDatabaseArgs} from '@typings/database/database';
|
||||
import type SystemModel from '@typings/database/models/servers/system';
|
||||
import type UserModel from '@typings/database/models/servers/user';
|
||||
|
||||
interface Props extends NavigationComponentProps {
|
||||
customStatusExpirySupported: boolean;
|
||||
currentUser: UserModel;
|
||||
intl: IntlShape;
|
||||
isModal?: boolean;
|
||||
isTablet?: boolean;
|
||||
recentCustomStatuses: UserCustomStatus[];
|
||||
serverUrl: string;
|
||||
theme: Theme;
|
||||
}
|
||||
|
||||
type State = {
|
||||
emoji?: string;
|
||||
text?: string;
|
||||
duration: CustomStatusDuration;
|
||||
expires_at: Moment;
|
||||
};
|
||||
|
||||
const {SERVER: {SYSTEM, USER}} = MM_TABLES;
|
||||
const {DONT_CLEAR, THIRTY_MINUTES, ONE_HOUR, FOUR_HOURS, TODAY, THIS_WEEK, DATE_AND_TIME} = CustomStatusDuration;
|
||||
const DEFAULT_DURATION: CustomStatusDuration = TODAY;
|
||||
const BTN_UPDATE_STATUS = 'update-custom-status';
|
||||
const edges: Edge[] = ['bottom', 'left', 'right'];
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
return {
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: changeOpacity(theme.centerChannelColor, 0.03),
|
||||
},
|
||||
contentContainerStyle: {
|
||||
height: '99%',
|
||||
},
|
||||
scrollView: {
|
||||
flex: 1,
|
||||
paddingTop: 32,
|
||||
},
|
||||
separator: {
|
||||
marginTop: 32,
|
||||
},
|
||||
block: {
|
||||
borderBottomColor: changeOpacity(theme.centerChannelColor, 0.1),
|
||||
borderBottomWidth: 1,
|
||||
borderTopColor: changeOpacity(theme.centerChannelColor, 0.1),
|
||||
borderTopWidth: 1,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
class CustomStatusModal extends NavigationComponent<Props, State> {
|
||||
private navigationEventListener: EventSubscription | undefined;
|
||||
private isCustomStatusExpired: boolean | undefined;
|
||||
private backListener: EventSubscription | undefined;
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
const {intl, theme, componentId} = props;
|
||||
|
||||
mergeNavigationOptions(componentId, {
|
||||
topBar: {
|
||||
rightButtons: [
|
||||
{
|
||||
enabled: true,
|
||||
id: BTN_UPDATE_STATUS,
|
||||
showAsAction: 'always',
|
||||
testID: 'custom_status.done.button',
|
||||
text: intl.formatMessage({id: 'mobile.custom_status.modal_confirm', defaultMessage: 'Done'}),
|
||||
color: theme.sidebarHeaderTextColor,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
this.setUp();
|
||||
}
|
||||
|
||||
setUp = () => {
|
||||
const {currentUser} = this.props;
|
||||
const userTimezone = getTimezone(currentUser.timezone);
|
||||
|
||||
const customStatus = this.getCustomStatus();
|
||||
|
||||
this.isCustomStatusExpired = verifyExpiredStatus(currentUser);
|
||||
|
||||
const currentTime = getCurrentMomentForTimezone(userTimezone ?? '');
|
||||
|
||||
let initialCustomExpiryTime: Moment = getRoundedTime(currentTime);
|
||||
const isCurrentCustomStatusSet = !this.isCustomStatusExpired && (customStatus?.text || customStatus?.emoji);
|
||||
if (isCurrentCustomStatusSet && customStatus?.duration === DATE_AND_TIME && customStatus?.expires_at) {
|
||||
initialCustomExpiryTime = moment(customStatus?.expires_at);
|
||||
}
|
||||
|
||||
this.state = {
|
||||
duration: isCurrentCustomStatusSet ? customStatus?.duration ?? DONT_CLEAR : DEFAULT_DURATION,
|
||||
emoji: isCurrentCustomStatusSet ? customStatus?.emoji : '',
|
||||
expires_at: initialCustomExpiryTime,
|
||||
text: isCurrentCustomStatusSet ? customStatus?.text : '',
|
||||
};
|
||||
};
|
||||
|
||||
getCustomStatus = () => {
|
||||
const {currentUser} = this.props;
|
||||
return getUserCustomStatus(currentUser);
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
this.navigationEventListener = Navigation.events().bindComponent(this);
|
||||
this.backListener = BackHandler.addEventListener('hardwareBackPress', this.onBackPress);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.navigationEventListener?.remove();
|
||||
this.backListener?.remove();
|
||||
}
|
||||
|
||||
navigationButtonPressed({buttonId}: NavigationButtonPressedEvent) {
|
||||
switch (buttonId) {
|
||||
case BTN_UPDATE_STATUS:
|
||||
this.handleSetStatus();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
onBackPress = () => {
|
||||
if (this.props.isTablet) {
|
||||
DeviceEventEmitter.emit(Events.ACCOUNT_SELECT_TABLET_VIEW, '');
|
||||
} else {
|
||||
dismissModal();
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
handleSetStatus = async () => {
|
||||
const {customStatusExpirySupported, currentUser, serverUrl} = this.props;
|
||||
const {emoji, text, duration} = this.state;
|
||||
const customStatus = this.getCustomStatus();
|
||||
|
||||
const isStatusSet = emoji || text;
|
||||
if (isStatusSet) {
|
||||
let isStatusSame = customStatus?.emoji === emoji && customStatus?.text === text && customStatus?.duration === duration;
|
||||
const expiresAt = this.calculateExpiryTime(duration);
|
||||
if (isStatusSame && duration === DATE_AND_TIME) {
|
||||
isStatusSame = customStatus?.expires_at === expiresAt;
|
||||
}
|
||||
|
||||
if (!isStatusSame) {
|
||||
const status: UserCustomStatus = {
|
||||
emoji: emoji || 'speech_balloon',
|
||||
text: text?.trim(),
|
||||
duration: DONT_CLEAR,
|
||||
};
|
||||
|
||||
if (customStatusExpirySupported) {
|
||||
status.duration = duration;
|
||||
status.expires_at = expiresAt;
|
||||
}
|
||||
const {error} = await updateCustomStatus(serverUrl, currentUser, status);
|
||||
if (error) {
|
||||
DeviceEventEmitter.emit(SET_CUSTOM_STATUS_FAILURE);
|
||||
return;
|
||||
}
|
||||
|
||||
updateLocalCustomStatus(serverUrl, currentUser, status);
|
||||
|
||||
this.setState({
|
||||
duration: status.duration,
|
||||
emoji: status.emoji,
|
||||
expires_at: moment(status.expires_at),
|
||||
text: status.text,
|
||||
});
|
||||
}
|
||||
} else if (customStatus?.emoji) {
|
||||
const unsetResponse = await unsetCustomStatus(serverUrl);
|
||||
|
||||
if (unsetResponse?.data) {
|
||||
updateLocalCustomStatus(serverUrl, currentUser, undefined);
|
||||
}
|
||||
}
|
||||
Keyboard.dismiss();
|
||||
if (this.props.isTablet) {
|
||||
DeviceEventEmitter.emit(Events.ACCOUNT_SELECT_TABLET_VIEW, '');
|
||||
} else {
|
||||
dismissModal();
|
||||
}
|
||||
};
|
||||
|
||||
calculateExpiryTime = (duration: CustomStatusDuration): string => {
|
||||
const {currentUser} = this.props;
|
||||
const userTimezone = getTimezone(currentUser.timezone);
|
||||
const currentTime = getCurrentMomentForTimezone(userTimezone);
|
||||
|
||||
const {expires_at} = this.state;
|
||||
switch (duration) {
|
||||
case THIRTY_MINUTES:
|
||||
return currentTime.add(30, 'minutes').seconds(0).milliseconds(0).toISOString();
|
||||
case ONE_HOUR:
|
||||
return currentTime.add(1, 'hour').seconds(0).milliseconds(0).toISOString();
|
||||
case FOUR_HOURS:
|
||||
return currentTime.add(4, 'hours').seconds(0).milliseconds(0).toISOString();
|
||||
case TODAY:
|
||||
return currentTime.endOf('day').toISOString();
|
||||
case THIS_WEEK:
|
||||
return currentTime.endOf('week').toISOString();
|
||||
case DATE_AND_TIME:
|
||||
return expires_at.toISOString();
|
||||
case DONT_CLEAR:
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
handleTextChange = (text: string) => {
|
||||
this.setState({text});
|
||||
};
|
||||
|
||||
handleRecentCustomStatusClear = (status: UserCustomStatus) => removeRecentCustomStatus(this.props.serverUrl, status);
|
||||
|
||||
clearHandle = () => this.setState({emoji: '', text: '', duration: DEFAULT_DURATION});
|
||||
|
||||
handleCustomStatusSuggestionClick = (status: UserCustomStatus) => {
|
||||
const {emoji, text, duration} = status;
|
||||
this.setState({emoji, text, duration});
|
||||
};
|
||||
|
||||
handleRecentCustomStatusSuggestionClick = (status: UserCustomStatus) => {
|
||||
const {emoji, text, duration} = status;
|
||||
this.setState({emoji, text, duration: duration || DONT_CLEAR});
|
||||
if (duration === DATE_AND_TIME) {
|
||||
this.openClearAfterModal();
|
||||
}
|
||||
};
|
||||
|
||||
openEmojiPicker = preventDoubleTap(() => {
|
||||
const {theme, intl} = this.props;
|
||||
CompassIcon.getImageSource('close', 24, theme.sidebarHeaderTextColor).then((source) => {
|
||||
const screen = Screens.EMOJI_PICKER;
|
||||
const title = intl.formatMessage({id: 'mobile.custom_status.choose_emoji', defaultMessage: 'Choose an emoji'});
|
||||
const passProps = {closeButton: source, onEmojiPress: this.handleEmojiClick};
|
||||
|
||||
showModal(screen, title, passProps);
|
||||
});
|
||||
});
|
||||
|
||||
handleEmojiClick = (emoji: string) => {
|
||||
this.setState({emoji});
|
||||
};
|
||||
|
||||
handleClearAfterClick = (duration: CustomStatusDuration, expires_at: string) =>
|
||||
this.setState({
|
||||
duration,
|
||||
expires_at: duration === DATE_AND_TIME && expires_at ? moment(expires_at) : this.state.expires_at,
|
||||
});
|
||||
|
||||
openClearAfterModal = async () => {
|
||||
const {intl, theme} = this.props;
|
||||
const screen = Screens.CUSTOM_STATUS_CLEAR_AFTER;
|
||||
const title = intl.formatMessage({id: 'mobile.custom_status.clear_after.title', defaultMessage: 'Clear Custom Status After'});
|
||||
const passProps = {
|
||||
handleClearAfterClick: this.handleClearAfterClick,
|
||||
initialDuration: this.state.duration,
|
||||
intl,
|
||||
theme,
|
||||
};
|
||||
|
||||
if (this.props.isTablet) {
|
||||
showModal(screen, title, passProps);
|
||||
} else {
|
||||
goToScreen(screen, title, passProps);
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const {duration, emoji, expires_at, text} = this.state;
|
||||
const {customStatusExpirySupported, currentUser, intl, recentCustomStatuses, theme} = this.props;
|
||||
const isStatusSet = Boolean(emoji || text);
|
||||
const style = getStyleSheet(theme);
|
||||
|
||||
return (
|
||||
<>
|
||||
{this.props.isTablet &&
|
||||
<TabletTitle
|
||||
action={intl.formatMessage({id: 'mobile.custom_status.modal_confirm', defaultMessage: 'Done'})}
|
||||
onPress={this.handleSetStatus}
|
||||
testID='custom_status.done.button'
|
||||
title={intl.formatMessage({id: 'mobile.routes.custom_status', defaultMessage: 'Set a Status'})}
|
||||
/>
|
||||
}
|
||||
<SafeAreaView
|
||||
edges={edges}
|
||||
style={style.container}
|
||||
testID='custom_status.screen'
|
||||
>
|
||||
<KeyboardAvoidingView
|
||||
behavior='padding'
|
||||
enabled={Platform.OS === 'ios'}
|
||||
keyboardVerticalOffset={100}
|
||||
contentContainerStyle={style.contentContainerStyle}
|
||||
>
|
||||
<ScrollView
|
||||
bounces={false}
|
||||
keyboardDismissMode='none'
|
||||
keyboardShouldPersistTaps='always'
|
||||
>
|
||||
<StatusBar theme={theme}/>
|
||||
<View style={style.scrollView}>
|
||||
<View style={style.block}>
|
||||
<CustomStatusInput
|
||||
emoji={emoji}
|
||||
isStatusSet={isStatusSet}
|
||||
onChangeText={this.handleTextChange}
|
||||
onClearHandle={this.clearHandle}
|
||||
onOpenEmojiPicker={this.openEmojiPicker}
|
||||
text={text}
|
||||
theme={theme}
|
||||
/>
|
||||
{isStatusSet && customStatusExpirySupported && (
|
||||
<ClearAfter
|
||||
currentUser={currentUser}
|
||||
duration={duration}
|
||||
expiresAt={expires_at}
|
||||
onOpenClearAfterModal={this.openClearAfterModal}
|
||||
theme={theme}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
{recentCustomStatuses.length > 0 && (
|
||||
<RecentCustomStatuses
|
||||
isExpirySupported={customStatusExpirySupported}
|
||||
onHandleClear={this.handleRecentCustomStatusClear}
|
||||
onHandleSuggestionClick={this.handleRecentCustomStatusSuggestionClick}
|
||||
recentCustomStatuses={recentCustomStatuses}
|
||||
theme={theme}
|
||||
/>
|
||||
)
|
||||
}
|
||||
<CustomStatusSuggestions
|
||||
intl={intl}
|
||||
isExpirySupported={customStatusExpirySupported}
|
||||
onHandleCustomStatusSuggestionClick={this.handleCustomStatusSuggestionClick}
|
||||
recentCustomStatuses={recentCustomStatuses}
|
||||
theme={theme}
|
||||
/>
|
||||
</View>
|
||||
<View style={style.separator}/>
|
||||
</ScrollView>
|
||||
</KeyboardAvoidingView>
|
||||
</SafeAreaView>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const augmentCSM = injectIntl(withTheme(withServerUrl(CustomStatusModal)));
|
||||
|
||||
const enhancedCSM = withObservables([], ({database}: WithDatabaseArgs) => {
|
||||
const config = database.
|
||||
get<SystemModel>(SYSTEM).
|
||||
findAndObserve(SYSTEM_IDENTIFIERS.CONFIG);
|
||||
return {
|
||||
currentUser: database.
|
||||
get<SystemModel>(SYSTEM).
|
||||
findAndObserve(SYSTEM_IDENTIFIERS.CURRENT_USER_ID).
|
||||
pipe(
|
||||
switchMap((id) => database.get<UserModel>(USER).findAndObserve(id.value)),
|
||||
),
|
||||
customStatusExpirySupported: config.pipe(
|
||||
switchMap((cfg) => of$(isCustomStatusExpirySupported((cfg.value as ClientConfig).Version))),
|
||||
),
|
||||
recentCustomStatuses: database.
|
||||
get<SystemModel>(SYSTEM).
|
||||
findAndObserve(SYSTEM_IDENTIFIERS.RECENT_CUSTOM_STATUS).pipe(
|
||||
switchMap(
|
||||
(recentStatuses) => of$(
|
||||
safeParseJSON(recentStatuses.value) as unknown as UserCustomStatus[],
|
||||
),
|
||||
),
|
||||
catchError(() => of$([])),
|
||||
),
|
||||
};
|
||||
});
|
||||
|
||||
export default withDatabase(enhancedCSM(augmentCSM));
|
||||
@@ -0,0 +1,142 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import moment, {Moment} from 'moment';
|
||||
import React, {useCallback} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {View, TouchableOpacity} from 'react-native';
|
||||
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import CustomStatusExpiry from '@components/custom_status/custom_status_expiry';
|
||||
import CustomStatusText from '@components/custom_status/custom_status_text';
|
||||
import {CustomStatusDuration, CST} from '@constants/custom_status';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {preventDoubleTap} from '@utils/tap';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
import DateTimePicker from './date_time_selector';
|
||||
|
||||
import type UserModel from '@typings/database/models/servers/user';
|
||||
|
||||
type Props = {
|
||||
currentUser: UserModel;
|
||||
duration: CustomStatusDuration;
|
||||
expiryTime?: string;
|
||||
handleItemClick: (duration: CustomStatusDuration, expiresAt: string) => void;
|
||||
|
||||
isSelected: boolean;
|
||||
separator: boolean;
|
||||
showDateTimePicker?: boolean;
|
||||
showExpiryTime?: boolean;
|
||||
|
||||
};
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
return {
|
||||
container: {
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
padding: 10,
|
||||
},
|
||||
textContainer: {
|
||||
marginLeft: 5,
|
||||
marginBottom: 2,
|
||||
alignItems: 'center',
|
||||
width: '70%',
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
position: 'relative',
|
||||
},
|
||||
rightPosition: {
|
||||
position: 'absolute',
|
||||
right: 14,
|
||||
},
|
||||
divider: {
|
||||
backgroundColor: changeOpacity(theme.centerChannelColor, 0.2),
|
||||
height: 1,
|
||||
marginHorizontal: 16,
|
||||
},
|
||||
button: {
|
||||
borderRadius: 1000,
|
||||
color: theme.buttonBg,
|
||||
},
|
||||
customStatusExpiry: {
|
||||
color: theme.linkColor,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const ClearAfterMenuItem = ({currentUser, duration, expiryTime = '', handleItemClick, isSelected, separator, showDateTimePicker = false, showExpiryTime = false}: Props) => {
|
||||
const theme = useTheme();
|
||||
const intl = useIntl();
|
||||
const style = getStyleSheet(theme);
|
||||
|
||||
const expiryMenuItems: { [key in CustomStatusDuration]: string } = {
|
||||
[CustomStatusDuration.DONT_CLEAR]: intl.formatMessage(CST[CustomStatusDuration.DONT_CLEAR]),
|
||||
[CustomStatusDuration.THIRTY_MINUTES]: intl.formatMessage(CST[CustomStatusDuration.THIRTY_MINUTES]),
|
||||
[CustomStatusDuration.ONE_HOUR]: intl.formatMessage(CST[CustomStatusDuration.ONE_HOUR]),
|
||||
[CustomStatusDuration.FOUR_HOURS]: intl.formatMessage(CST[CustomStatusDuration.FOUR_HOURS]),
|
||||
[CustomStatusDuration.TODAY]: intl.formatMessage(CST[CustomStatusDuration.TODAY]),
|
||||
[CustomStatusDuration.THIS_WEEK]: intl.formatMessage(CST[CustomStatusDuration.THIS_WEEK]),
|
||||
[CustomStatusDuration.DATE_AND_TIME]: intl.formatMessage({id: 'custom_status.expiry_dropdown.custom', defaultMessage: 'Custom'}),
|
||||
};
|
||||
|
||||
const handleClick = preventDoubleTap(() => {
|
||||
handleItemClick(duration, expiryTime);
|
||||
});
|
||||
|
||||
const handleCustomExpiresAtChange = useCallback((expiresAt: Moment) => {
|
||||
handleItemClick(duration, expiresAt.toISOString());
|
||||
}, [handleItemClick, duration]);
|
||||
|
||||
return (
|
||||
<View>
|
||||
<TouchableOpacity
|
||||
testID={`clear_after.menu_item.${duration}`}
|
||||
onPress={handleClick}
|
||||
>
|
||||
<View style={style.container}>
|
||||
<View style={style.textContainer}>
|
||||
<CustomStatusText
|
||||
text={expiryMenuItems[duration]}
|
||||
theme={theme}
|
||||
textStyle={{color: theme.centerChannelColor}}
|
||||
/>
|
||||
{isSelected && (
|
||||
<View style={style.rightPosition}>
|
||||
<CompassIcon
|
||||
name={'check'}
|
||||
size={24}
|
||||
style={style.button}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
{showExpiryTime && expiryTime !== '' && (
|
||||
<View style={style.rightPosition}>
|
||||
<CustomStatusExpiry
|
||||
currentUser={currentUser}
|
||||
theme={theme}
|
||||
time={moment(expiryTime).toDate()}
|
||||
textStyles={style.customStatusExpiry}
|
||||
showTimeCompulsory={true}
|
||||
showToday={true}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
{separator && <View style={style.divider}/>}
|
||||
</TouchableOpacity>
|
||||
{showDateTimePicker && (
|
||||
<DateTimePicker
|
||||
currentUser={currentUser}
|
||||
theme={theme}
|
||||
handleChange={handleCustomExpiresAtChange}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default ClearAfterMenuItem;
|
||||
@@ -0,0 +1,131 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Q} from '@nozbe/watermelondb';
|
||||
import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider';
|
||||
import withObservables from '@nozbe/with-observables';
|
||||
import DateTimePicker from '@react-native-community/datetimepicker';
|
||||
import moment, {Moment} from 'moment-timezone';
|
||||
import React, {useState} from 'react';
|
||||
import {View, Button, Platform} from 'react-native';
|
||||
import {of as of$} from 'rxjs';
|
||||
import {switchMap} from 'rxjs/operators';
|
||||
|
||||
import {Preferences} from '@constants';
|
||||
import {CUSTOM_STATUS_TIME_PICKER_INTERVALS_IN_MINUTES} from '@constants/custom_status';
|
||||
import {MM_TABLES} from '@constants/database';
|
||||
import {getPreferenceAsBool} from '@helpers/api/preference';
|
||||
import {getCurrentMomentForTimezone, getRoundedTime, getUtcOffsetForTimeZone} from '@utils/helpers';
|
||||
import {makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {getTimezone} from '@utils/user';
|
||||
|
||||
import type {WithDatabaseArgs} from '@typings/database/database';
|
||||
import type PreferenceModel from '@typings/database/models/servers/preference';
|
||||
import type UserModel from '@typings/database/models/servers/user';
|
||||
|
||||
type Props = {
|
||||
currentUser: UserModel;
|
||||
isMilitaryTime: boolean;
|
||||
theme: Theme;
|
||||
handleChange: (currentDate: Moment) => void;
|
||||
}
|
||||
|
||||
type AndroidMode = 'date' | 'time';
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
return {
|
||||
container: {
|
||||
flex: 1,
|
||||
paddingTop: 10,
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
},
|
||||
buttonContainer: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-evenly',
|
||||
marginBottom: 10,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const DateTimeSelector = ({currentUser, handleChange, isMilitaryTime, theme}: Props) => {
|
||||
const styles = getStyleSheet(theme);
|
||||
const timezone = getTimezone(currentUser.timezone);
|
||||
const currentTime = getCurrentMomentForTimezone(timezone);
|
||||
const timezoneOffSetInMinutes = timezone ? getUtcOffsetForTimeZone(timezone) : undefined;
|
||||
const minimumDate = getRoundedTime(currentTime);
|
||||
const [date, setDate] = useState<Moment>(minimumDate);
|
||||
const [mode, setMode] = useState<AndroidMode>('date');
|
||||
const [show, setShow] = useState<boolean>(false);
|
||||
|
||||
const onChange = (_: React.ChangeEvent<HTMLInputElement>, selectedDate: Date) => {
|
||||
const currentDate = selectedDate || date;
|
||||
setShow(Platform.OS === 'ios');
|
||||
if (moment(currentDate).isAfter(minimumDate)) {
|
||||
setDate(moment(currentDate));
|
||||
handleChange(moment(currentDate));
|
||||
}
|
||||
};
|
||||
|
||||
const showMode = (currentMode: AndroidMode) => {
|
||||
setShow(true);
|
||||
setMode(currentMode);
|
||||
};
|
||||
|
||||
const showDatepicker = () => {
|
||||
showMode('date');
|
||||
handleChange(moment(date));
|
||||
};
|
||||
|
||||
const showTimepicker = () => {
|
||||
showMode('time');
|
||||
handleChange(moment(date));
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<View style={styles.buttonContainer}>
|
||||
<Button
|
||||
testID={'clear_after.menu_item.date_and_time.button.date'}
|
||||
onPress={showDatepicker}
|
||||
title='Select Date'
|
||||
color={theme.buttonBg}
|
||||
/>
|
||||
<Button
|
||||
testID={'clear_after.menu_item.date_and_time.button.time'}
|
||||
onPress={showTimepicker}
|
||||
title='Select Time'
|
||||
color={theme.buttonBg}
|
||||
/>
|
||||
</View>
|
||||
{show && (
|
||||
<DateTimePicker
|
||||
testID='clear_after.date_time_picker'
|
||||
value={date.toDate()}
|
||||
mode={mode}
|
||||
is24Hour={isMilitaryTime}
|
||||
display={Platform.OS === 'ios' ? 'spinner' : 'default'}
|
||||
onChange={onChange}
|
||||
textColor={theme.centerChannelColor}
|
||||
minimumDate={minimumDate.toDate()}
|
||||
minuteInterval={CUSTOM_STATUS_TIME_PICKER_INTERVALS_IN_MINUTES}
|
||||
timeZoneOffsetInMinutes={timezoneOffSetInMinutes}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const enhanced = withObservables([], ({database}: WithDatabaseArgs) => ({
|
||||
isMilitaryTime: database.get<PreferenceModel>(MM_TABLES.SERVER.PREFERENCE).
|
||||
query(
|
||||
Q.where('category', Preferences.CATEGORY_DISPLAY_SETTINGS),
|
||||
).observe().pipe(
|
||||
switchMap(
|
||||
(preferences) => of$(getPreferenceAsBool(preferences, Preferences.CATEGORY_DISPLAY_SETTINGS, 'use_military_time', false)),
|
||||
),
|
||||
),
|
||||
}));
|
||||
|
||||
export default withDatabase(enhanced(DateTimeSelector));
|
||||
219
app/screens/custom_status_clear_after/index.tsx
Normal file
219
app/screens/custom_status_clear_after/index.tsx
Normal file
@@ -0,0 +1,219 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider';
|
||||
import withObservables from '@nozbe/with-observables';
|
||||
import React from 'react';
|
||||
import {injectIntl, IntlShape} from 'react-intl';
|
||||
import {BackHandler, NativeEventSubscription, SafeAreaView, StatusBar, View} from 'react-native';
|
||||
import {KeyboardAwareScrollView} from 'react-native-keyboard-aware-scroll-view';
|
||||
import {
|
||||
Navigation,
|
||||
NavigationButtonPressedEvent,
|
||||
NavigationComponent,
|
||||
NavigationComponentProps,
|
||||
Options,
|
||||
} from 'react-native-navigation';
|
||||
import {switchMap} from 'rxjs/operators';
|
||||
|
||||
import {CustomStatusDuration} from '@constants/custom_status';
|
||||
import {MM_TABLES, SYSTEM_IDENTIFIERS} from '@constants/database';
|
||||
import {dismissModal, mergeNavigationOptions, popTopScreen} from '@screens/navigation';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
import ClearAfterMenuItem from './components/clear_after_menu_item';
|
||||
|
||||
import type {WithDatabaseArgs} from '@typings/database/database';
|
||||
import type SystemModel from '@typings/database/models/servers/system';
|
||||
import type UserModel from '@typings/database/models/servers/user';
|
||||
|
||||
interface Props extends NavigationComponentProps {
|
||||
currentUser: UserModel;
|
||||
handleClearAfterClick: (duration: CustomStatusDuration, expiresAt: string) => void;
|
||||
initialDuration: CustomStatusDuration;
|
||||
intl: IntlShape;
|
||||
isModal?: boolean;
|
||||
theme: Theme;
|
||||
}
|
||||
|
||||
type State = {
|
||||
duration: CustomStatusDuration;
|
||||
expiresAt: string;
|
||||
showExpiryTime: boolean;
|
||||
}
|
||||
|
||||
const {SERVER: {SYSTEM, USER}} = MM_TABLES;
|
||||
const CLEAR_AFTER = 'update-custom-status-clear-after';
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
return {
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: changeOpacity(theme.centerChannelColor, 0.03),
|
||||
},
|
||||
scrollView: {
|
||||
flex: 1,
|
||||
paddingTop: 32,
|
||||
paddingBottom: 32,
|
||||
},
|
||||
block: {
|
||||
borderBottomColor: changeOpacity(theme.centerChannelColor, 0.1),
|
||||
borderBottomWidth: 1,
|
||||
borderTopColor: changeOpacity(theme.centerChannelColor, 0.1),
|
||||
borderTopWidth: 1,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
class ClearAfterModal extends NavigationComponent<Props, State> {
|
||||
private backListener: NativeEventSubscription | undefined;
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
const options: Options = {
|
||||
topBar: {
|
||||
rightButtons: [{
|
||||
color: props.theme.sidebarHeaderTextColor,
|
||||
enabled: true,
|
||||
id: CLEAR_AFTER,
|
||||
showAsAction: 'always',
|
||||
testID: 'clear_after.done.button',
|
||||
text: props.intl.formatMessage({
|
||||
id: 'mobile.custom_status.modal_confirm',
|
||||
defaultMessage: 'Done',
|
||||
}),
|
||||
}],
|
||||
},
|
||||
};
|
||||
|
||||
mergeNavigationOptions(props.componentId, options);
|
||||
|
||||
this.state = {
|
||||
duration: props.initialDuration,
|
||||
expiresAt: '',
|
||||
showExpiryTime: false,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
Navigation.events().bindComponent(this);
|
||||
this.backListener = BackHandler.addEventListener('hardwareBackPress', this.onBackPress);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.backListener?.remove();
|
||||
}
|
||||
|
||||
navigationButtonPressed({buttonId}: NavigationButtonPressedEvent) {
|
||||
switch (buttonId) {
|
||||
case CLEAR_AFTER:
|
||||
this.onDone();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
onBackPress = () => {
|
||||
if (this.props.isModal) {
|
||||
dismissModal();
|
||||
} else {
|
||||
popTopScreen();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
onDone = () => {
|
||||
const {handleClearAfterClick, isModal} = this.props;
|
||||
handleClearAfterClick(this.state.duration, this.state.expiresAt);
|
||||
if (isModal) {
|
||||
dismissModal();
|
||||
return;
|
||||
}
|
||||
|
||||
popTopScreen();
|
||||
};
|
||||
|
||||
handleItemClick = (duration: CustomStatusDuration, expiresAt: string) =>
|
||||
this.setState({
|
||||
duration,
|
||||
expiresAt,
|
||||
showExpiryTime: duration === CustomStatusDuration.DATE_AND_TIME && expiresAt !== '',
|
||||
});
|
||||
|
||||
renderClearAfterMenu = () => {
|
||||
const {currentUser, theme} = this.props;
|
||||
const style = getStyleSheet(theme);
|
||||
|
||||
const {duration} = this.state;
|
||||
|
||||
const clearAfterMenu = Object.values(CustomStatusDuration).map(
|
||||
(item, index, arr) => {
|
||||
if (index === arr.length - 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<ClearAfterMenuItem
|
||||
currentUser={currentUser}
|
||||
duration={item}
|
||||
handleItemClick={this.handleItemClick}
|
||||
isSelected={duration === item}
|
||||
key={item}
|
||||
separator={index !== arr.length - 2}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
if (clearAfterMenu.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<View testID='clear_after.menu'>
|
||||
<View style={style.block}>{clearAfterMenu}</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const {currentUser, theme} = this.props;
|
||||
const style = getStyleSheet(theme);
|
||||
const {duration, expiresAt, showExpiryTime} = this.state;
|
||||
return (
|
||||
<SafeAreaView
|
||||
style={style.container}
|
||||
testID='clear_after.screen'
|
||||
>
|
||||
<StatusBar/>
|
||||
<KeyboardAwareScrollView bounces={false}>
|
||||
<View style={style.scrollView}>
|
||||
{this.renderClearAfterMenu()}
|
||||
</View>
|
||||
<View style={style.block}>
|
||||
<ClearAfterMenuItem
|
||||
currentUser={currentUser}
|
||||
duration={CustomStatusDuration.DATE_AND_TIME}
|
||||
expiryTime={expiresAt}
|
||||
handleItemClick={this.handleItemClick}
|
||||
isSelected={duration === CustomStatusDuration.DATE_AND_TIME && expiresAt === ''}
|
||||
separator={false}
|
||||
showDateTimePicker={duration === CustomStatusDuration.DATE_AND_TIME}
|
||||
showExpiryTime={showExpiryTime}
|
||||
/>
|
||||
</View>
|
||||
</KeyboardAwareScrollView>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const enhancedCAM = withObservables([], ({database}: WithDatabaseArgs) => ({
|
||||
currentUser: database.get<SystemModel>(SYSTEM).
|
||||
findAndObserve(SYSTEM_IDENTIFIERS.CURRENT_USER_ID).
|
||||
pipe(
|
||||
switchMap((id) => database.get<UserModel>(USER).findAndObserve(id.value)),
|
||||
),
|
||||
}));
|
||||
|
||||
export default withDatabase(enhancedCAM(injectIntl(ClearAfterModal)));
|
||||
56
app/screens/emoji_picker/index.tsx
Normal file
56
app/screens/emoji_picker/index.tsx
Normal file
@@ -0,0 +1,56 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback, useEffect} from 'react';
|
||||
import {Keyboard} from 'react-native';
|
||||
import {Navigation} from 'react-native-navigation';
|
||||
|
||||
import EmojiPicker from '@app/components/emoji_picker';
|
||||
import {dismissModal, setButtons} from '@screens/navigation';
|
||||
|
||||
type Props = {
|
||||
componentId: string;
|
||||
onEmojiPress: (emoji: string) => void;
|
||||
closeButton: never;
|
||||
};
|
||||
|
||||
const EmojiPickerScreen = ({closeButton, componentId, onEmojiPress}: Props) => {
|
||||
useEffect(() => {
|
||||
setButtons(componentId, {
|
||||
leftButtons: [
|
||||
{
|
||||
icon: closeButton,
|
||||
id: 'close-add-reaction',
|
||||
testID: 'close.add_reaction.button',
|
||||
},
|
||||
] as unknown as never[],
|
||||
rightButtons: [],
|
||||
});
|
||||
|
||||
const unsubscribe = Navigation.events().registerComponentListener({
|
||||
navigationButtonPressed: ({buttonId}: { buttonId: string }) => {
|
||||
if (buttonId === 'close-add-reaction') {
|
||||
close();
|
||||
}
|
||||
},
|
||||
}, componentId);
|
||||
|
||||
return () => {
|
||||
unsubscribe.remove();
|
||||
};
|
||||
}, []);
|
||||
|
||||
const close = useCallback(() => {
|
||||
Keyboard.dismiss();
|
||||
dismissModal();
|
||||
}, []);
|
||||
|
||||
const handleEmojiPress = useCallback((emoji: string) => {
|
||||
onEmojiPress(emoji);
|
||||
close();
|
||||
}, []);
|
||||
|
||||
return <EmojiPicker onEmojiPress={handleEmojiPress}/>;
|
||||
};
|
||||
|
||||
export default EmojiPickerScreen;
|
||||
@@ -0,0 +1,92 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import moment from 'moment-timezone';
|
||||
import React from 'react';
|
||||
import {View} from 'react-native';
|
||||
|
||||
import ClearButton from '@components/custom_status/clear_button';
|
||||
import CustomStatusExpiry from '@components/custom_status/custom_status_expiry';
|
||||
import FormattedText from '@components/formatted_text';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
import CustomStatusText from './custom_status_text';
|
||||
|
||||
import type UserModel from '@typings/database/models/servers/user';
|
||||
|
||||
type CustomLabelProps = {
|
||||
customStatus: UserCustomStatus;
|
||||
isCustomStatusExpirySupported: boolean;
|
||||
isStatusSet: boolean;
|
||||
showRetryMessage: boolean;
|
||||
theme: Theme;
|
||||
currentUser: UserModel;
|
||||
onClearCustomStatus: () => void;
|
||||
};
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
return {
|
||||
clearButton: {
|
||||
position: 'absolute',
|
||||
top: 4,
|
||||
right: 14,
|
||||
},
|
||||
customStatusTextContainer: {
|
||||
width: '70%',
|
||||
},
|
||||
customStatusExpiryText: {
|
||||
paddingTop: 3,
|
||||
fontSize: 15,
|
||||
color: changeOpacity(theme.centerChannelColor, 0.35),
|
||||
},
|
||||
retryMessage: {
|
||||
color: theme.errorTextColor,
|
||||
paddingBottom: 25,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const CustomLabel = ({currentUser, customStatus, isCustomStatusExpirySupported, isStatusSet, onClearCustomStatus, showRetryMessage, theme}: CustomLabelProps) => {
|
||||
const style = getStyleSheet(theme);
|
||||
|
||||
return (
|
||||
<>
|
||||
<View style={style.customStatusTextContainer}>
|
||||
<CustomStatusText
|
||||
theme={theme}
|
||||
isStatusSet={Boolean(isStatusSet)}
|
||||
customStatus={customStatus}
|
||||
/>
|
||||
{Boolean(isStatusSet && isCustomStatusExpirySupported && customStatus?.duration) && (
|
||||
<CustomStatusExpiry
|
||||
currentUser={currentUser}
|
||||
time={moment(customStatus?.expires_at)}
|
||||
theme={theme}
|
||||
textStyles={style.customStatusExpiryText}
|
||||
withinBrackets={true}
|
||||
showPrefix={true}
|
||||
testID={'custom_status.expiry'}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
{showRetryMessage && (
|
||||
<FormattedText
|
||||
id={'custom_status.failure_message'}
|
||||
defaultMessage='Failed to update status. Try again'
|
||||
style={style.retryMessage}
|
||||
/>
|
||||
)}
|
||||
{isStatusSet && (
|
||||
<View style={style.clearButton}>
|
||||
<ClearButton
|
||||
handlePress={onClearCustomStatus}
|
||||
theme={theme}
|
||||
testID='settings.sidebar.custom_status.action.clear'
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default CustomLabel;
|
||||
@@ -0,0 +1,48 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {View} from 'react-native';
|
||||
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import Emoji from '@components/emoji';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
type CustomStatusEmojiProps = {
|
||||
emoji?: string;
|
||||
isStatusSet: boolean;
|
||||
theme: Theme;
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
return {
|
||||
customStatusIcon: {
|
||||
color: changeOpacity(theme.centerChannelColor, 0.64),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const CustomStatusEmoji = ({emoji, isStatusSet, theme}: CustomStatusEmojiProps) => {
|
||||
const styles = getStyleSheet(theme);
|
||||
|
||||
return (
|
||||
<View
|
||||
testID={`custom_status.emoji.${isStatusSet ? emoji : 'default'}`}
|
||||
>
|
||||
{isStatusSet && emoji ? (
|
||||
<Emoji
|
||||
emojiName={emoji}
|
||||
size={20}
|
||||
/>
|
||||
) : (
|
||||
<CompassIcon
|
||||
name='emoticon-happy-outline'
|
||||
size={24}
|
||||
style={styles.customStatusIcon}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default CustomStatusEmoji;
|
||||
@@ -0,0 +1,46 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import CustomText from '@components/custom_status/custom_status_text';
|
||||
import FormattedText from '@components/formatted_text';
|
||||
import {makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
type CustomStatusTextProps = {
|
||||
customStatus?: UserCustomStatus;
|
||||
isStatusSet: boolean;
|
||||
theme: Theme;
|
||||
};
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||
text: {
|
||||
color: theme.centerChannelColor,
|
||||
},
|
||||
}));
|
||||
|
||||
const CustomStatusText = ({isStatusSet, customStatus, theme}: CustomStatusTextProps) => {
|
||||
let text: React.ReactNode | string;
|
||||
|
||||
text = (
|
||||
<FormattedText
|
||||
id='mobile.routes.custom_status'
|
||||
defaultMessage='Set a Status'
|
||||
/>
|
||||
);
|
||||
|
||||
if (isStatusSet && customStatus?.text) {
|
||||
text = customStatus.text;
|
||||
}
|
||||
|
||||
const styles = getStyleSheet(theme);
|
||||
return (
|
||||
<CustomText
|
||||
text={text}
|
||||
theme={theme}
|
||||
textStyle={styles.text}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default CustomStatusText;
|
||||
@@ -0,0 +1,97 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback, useEffect, useState} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {DeviceEventEmitter} from 'react-native';
|
||||
|
||||
import {updateLocalCustomStatus} from '@actions/local/user';
|
||||
import {unsetCustomStatus} from '@actions/remote/user';
|
||||
import DrawerItem from '@components/drawer_item';
|
||||
import {Events, Screens} from '@constants';
|
||||
import {SET_CUSTOM_STATUS_FAILURE} from '@constants/custom_status';
|
||||
import {useServerUrl} from '@context/server_url';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {showModal} from '@screens/navigation';
|
||||
import {preventDoubleTap} from '@utils/tap';
|
||||
import {getUserCustomStatus, isCustomStatusExpired as checkCustomStatusIsExpired} from '@utils/user';
|
||||
|
||||
import CustomLabel from './custom_label';
|
||||
import CustomStatusEmoji from './custom_status_emoji';
|
||||
|
||||
import type UserModel from '@typings/database/models/servers/user';
|
||||
|
||||
type CustomStatusProps = {
|
||||
isCustomStatusExpirySupported: boolean;
|
||||
isTablet: boolean;
|
||||
currentUser: UserModel;
|
||||
}
|
||||
|
||||
const CustomStatus = ({isCustomStatusExpirySupported, isTablet, currentUser}: CustomStatusProps) => {
|
||||
const theme = useTheme();
|
||||
const intl = useIntl();
|
||||
const serverUrl = useServerUrl();
|
||||
const [showRetryMessage, setShowRetryMessage] = useState<boolean>(false);
|
||||
const customStatus = getUserCustomStatus(currentUser);
|
||||
const isCustomStatusExpired = checkCustomStatusIsExpired(currentUser);
|
||||
const isStatusSet = !isCustomStatusExpired && (customStatus?.text || customStatus?.emoji);
|
||||
|
||||
useEffect(() => {
|
||||
const onSetCustomStatusError = () => {
|
||||
setShowRetryMessage(true);
|
||||
};
|
||||
|
||||
const listener = DeviceEventEmitter.addListener(SET_CUSTOM_STATUS_FAILURE, onSetCustomStatusError);
|
||||
|
||||
return () => listener.remove();
|
||||
}, []);
|
||||
|
||||
const clearCustomStatus = useCallback(preventDoubleTap(async () => {
|
||||
setShowRetryMessage(false);
|
||||
|
||||
const {error} = await unsetCustomStatus(serverUrl);
|
||||
if (error) {
|
||||
setShowRetryMessage(true);
|
||||
return;
|
||||
}
|
||||
|
||||
updateLocalCustomStatus(serverUrl, currentUser, undefined);
|
||||
}), []);
|
||||
|
||||
const goToCustomStatusScreen = useCallback(preventDoubleTap(() => {
|
||||
if (isTablet) {
|
||||
DeviceEventEmitter.emit(Events.ACCOUNT_SELECT_TABLET_VIEW, Screens.CUSTOM_STATUS);
|
||||
} else {
|
||||
showModal(Screens.CUSTOM_STATUS, intl.formatMessage({id: 'mobile.routes.custom_status', defaultMessage: 'Set a Status'}));
|
||||
}
|
||||
setShowRetryMessage(false);
|
||||
}), [isTablet]);
|
||||
|
||||
return (
|
||||
<DrawerItem
|
||||
testID='settings.sidebar.custom_status.action'
|
||||
labelComponent={
|
||||
<CustomLabel
|
||||
currentUser={currentUser}
|
||||
theme={theme}
|
||||
customStatus={customStatus!}
|
||||
isCustomStatusExpirySupported={isCustomStatusExpirySupported}
|
||||
isStatusSet={Boolean(isStatusSet)}
|
||||
onClearCustomStatus={clearCustomStatus}
|
||||
showRetryMessage={showRetryMessage}
|
||||
/>
|
||||
}
|
||||
leftComponent={
|
||||
<CustomStatusEmoji
|
||||
emoji={customStatus?.emoji}
|
||||
isStatusSet={Boolean(isStatusSet)}
|
||||
theme={theme}
|
||||
/>}
|
||||
separator={false}
|
||||
onPress={goToCustomStatusScreen}
|
||||
theme={theme}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default CustomStatus;
|
||||
105
app/screens/home/account/components/options/index.tsx
Normal file
105
app/screens/home/account/components/options/index.tsx
Normal file
@@ -0,0 +1,105 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {useWindowDimensions, View} from 'react-native';
|
||||
import {Shadow} from 'react-native-neomorph-shadows';
|
||||
|
||||
import {View as ViewConstants} from '@constants';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
import CustomStatus from './custom_status';
|
||||
import Logout from './logout';
|
||||
import SavedMessages from './saved_messages';
|
||||
import Settings from './settings';
|
||||
import UserPresence from './user_presence';
|
||||
import YourProfile from './your_profile';
|
||||
|
||||
import type UserModel from '@typings/database/models/servers/user';
|
||||
|
||||
type AccountScreenProps = {
|
||||
user: UserModel;
|
||||
enableCustomUserStatuses: boolean;
|
||||
isCustomStatusExpirySupported: boolean;
|
||||
isTablet: boolean;
|
||||
theme: Theme;
|
||||
};
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
return {
|
||||
container: {
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
flex: 1,
|
||||
borderTopRightRadius: 12,
|
||||
borderTopLeftRadius: 12,
|
||||
paddingTop: 12,
|
||||
},
|
||||
divider: {
|
||||
backgroundColor: changeOpacity(theme.centerChannelColor, 0.2),
|
||||
height: 1,
|
||||
marginHorizontal: 15,
|
||||
},
|
||||
menuLabel: {
|
||||
color: theme.centerChannelColor,
|
||||
fontSize: 16,
|
||||
lineHeight: 24,
|
||||
fontFamily: 'OpenSans',
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const AccountOptions = ({user, enableCustomUserStatuses, isCustomStatusExpirySupported, isTablet, theme}: AccountScreenProps) => {
|
||||
const styles = getStyleSheet(theme);
|
||||
const dimensions = useWindowDimensions();
|
||||
const width = dimensions.width - (isTablet ? ViewConstants.TABLET.SIDEBAR_WIDTH : 0);
|
||||
|
||||
return (
|
||||
<Shadow
|
||||
style={{
|
||||
height: dimensions.height,
|
||||
width,
|
||||
shadowColor: 'rgba(61, 60, 64, 0.08)',
|
||||
shadowOffset: {width: 0, height: -2},
|
||||
shadowOpacity: 1,
|
||||
shadowRadius: 6,
|
||||
}}
|
||||
>
|
||||
<View style={styles.container}>
|
||||
<UserPresence
|
||||
currentUser={user}
|
||||
style={styles.menuLabel}
|
||||
theme={theme}
|
||||
/>
|
||||
{enableCustomUserStatuses &&
|
||||
<CustomStatus
|
||||
isCustomStatusExpirySupported={isCustomStatusExpirySupported}
|
||||
isTablet={isTablet}
|
||||
currentUser={user}
|
||||
/>}
|
||||
<View style={styles.divider}/>
|
||||
<YourProfile
|
||||
isTablet={isTablet}
|
||||
style={styles.menuLabel}
|
||||
theme={theme}
|
||||
/>
|
||||
<SavedMessages
|
||||
isTablet={isTablet}
|
||||
style={styles.menuLabel}
|
||||
theme={theme}
|
||||
/>
|
||||
<Settings
|
||||
isTablet={isTablet}
|
||||
style={styles.menuLabel}
|
||||
theme={theme}
|
||||
/>
|
||||
<View style={styles.divider}/>
|
||||
<Logout
|
||||
style={styles.menuLabel}
|
||||
theme={theme}
|
||||
/>
|
||||
</View>
|
||||
</Shadow>
|
||||
);
|
||||
};
|
||||
|
||||
export default AccountOptions;
|
||||
81
app/screens/home/account/components/options/logout/index.tsx
Normal file
81
app/screens/home/account/components/options/logout/index.tsx
Normal file
@@ -0,0 +1,81 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback, useEffect, useState} from 'react';
|
||||
import {TextStyle, View} from 'react-native';
|
||||
|
||||
import {logout} from '@actions/remote/session';
|
||||
import DrawerItem from '@components/drawer_item';
|
||||
import FormattedText from '@components/formatted_text';
|
||||
import {useServerUrl} from '@context/server_url';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {queryServer} from '@queries/app/servers';
|
||||
import {preventDoubleTap} from '@utils/tap';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
type Props = {
|
||||
style: TextStyle;
|
||||
theme: Theme;
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||
label: {
|
||||
color: theme.dndIndicator,
|
||||
marginTop: 5,
|
||||
},
|
||||
logOutFrom: {
|
||||
color: changeOpacity(theme.centerChannelColor, 0.64),
|
||||
fontSize: 12,
|
||||
height: 30,
|
||||
lineHeight: 16,
|
||||
fontFamily: 'OpenSans',
|
||||
},
|
||||
}));
|
||||
|
||||
const Settings = ({style, theme}: Props) => {
|
||||
const styles = getStyleSheet(theme);
|
||||
const serverUrl = useServerUrl();
|
||||
const [serverName, setServerName] = useState(serverUrl);
|
||||
const onLogout = useCallback(preventDoubleTap(() => {
|
||||
logout(serverUrl);
|
||||
}), []);
|
||||
|
||||
useEffect(() => {
|
||||
const appDatabase = DatabaseManager.appDatabase?.database;
|
||||
if (appDatabase) {
|
||||
queryServer(appDatabase, serverUrl).then((server) => {
|
||||
if (server) {
|
||||
setServerName(server.displayName);
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [serverUrl]);
|
||||
|
||||
return (
|
||||
<DrawerItem
|
||||
testID='account.logout.action'
|
||||
labelComponent={(
|
||||
<View>
|
||||
<FormattedText
|
||||
id='account.logout'
|
||||
defaultMessage='Log out'
|
||||
style={[style, styles.label]}
|
||||
/>
|
||||
<FormattedText
|
||||
id={'account.logout_from'}
|
||||
defaultMessage={'Log out of {serverName}'}
|
||||
values={{serverName}}
|
||||
style={styles.logOutFrom}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
iconName='exit-to-app'
|
||||
isDestructor={true}
|
||||
onPress={onLogout}
|
||||
separator={false}
|
||||
theme={theme}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default Settings;
|
||||
@@ -0,0 +1,40 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback} from 'react';
|
||||
import {TextStyle} from 'react-native';
|
||||
|
||||
import DrawerItem from '@components/drawer_item';
|
||||
import FormattedText from '@components/formatted_text';
|
||||
import {preventDoubleTap} from '@utils/tap';
|
||||
|
||||
type Props = {
|
||||
isTablet: boolean;
|
||||
style: TextStyle;
|
||||
theme: Theme;
|
||||
}
|
||||
|
||||
const SavedMessages = ({isTablet, style, theme}: Props) => {
|
||||
const openSavedMessages = useCallback(preventDoubleTap(() => {
|
||||
// TODO: Open Saved messages screen in either a screen or in line for tablets
|
||||
}), [isTablet]);
|
||||
|
||||
return (
|
||||
<DrawerItem
|
||||
testID='account.saved_messages.action'
|
||||
labelComponent={
|
||||
<FormattedText
|
||||
id='account.saved_messages'
|
||||
defaultMessage='Saved Messages'
|
||||
style={style}
|
||||
/>
|
||||
}
|
||||
iconName='bookmark-outline'
|
||||
onPress={openSavedMessages}
|
||||
separator={false}
|
||||
theme={theme}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default SavedMessages;
|
||||
@@ -0,0 +1,40 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback} from 'react';
|
||||
import {TextStyle} from 'react-native';
|
||||
|
||||
import DrawerItem from '@components/drawer_item';
|
||||
import FormattedText from '@components/formatted_text';
|
||||
import {preventDoubleTap} from '@utils/tap';
|
||||
|
||||
type Props = {
|
||||
isTablet: boolean;
|
||||
style: TextStyle;
|
||||
theme: Theme;
|
||||
}
|
||||
|
||||
const Settings = ({isTablet, style, theme}: Props) => {
|
||||
const openSettings = useCallback(preventDoubleTap(() => {
|
||||
// TODO: Open Saved messages screen in either a screen or in line for tablets
|
||||
}), [isTablet]);
|
||||
|
||||
return (
|
||||
<DrawerItem
|
||||
testID='account.settings.action'
|
||||
labelComponent={
|
||||
<FormattedText
|
||||
id='account.settings'
|
||||
defaultMessage='Settings'
|
||||
style={style}
|
||||
/>
|
||||
}
|
||||
iconName='settings-outline'
|
||||
onPress={openSettings}
|
||||
separator={false}
|
||||
theme={theme}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default Settings;
|
||||
@@ -0,0 +1,140 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {DeviceEventEmitter, TextStyle} from 'react-native';
|
||||
|
||||
import {setStatus} from '@actions/remote/user';
|
||||
import {useServerUrl} from '@app/context/server_url';
|
||||
import DrawerItem from '@components/drawer_item';
|
||||
import SlideUpPanelItem, {ITEM_HEIGHT} from '@components/slide_up_panel_item';
|
||||
import StatusLabel from '@components/status_label';
|
||||
import UserStatusIndicator from '@components/user_status';
|
||||
import {Navigation} from '@constants';
|
||||
import General from '@constants/general';
|
||||
import {bottomSheet, dismissModal} from '@screens/navigation';
|
||||
import {preventDoubleTap} from '@utils/tap';
|
||||
import {changeOpacity} from '@utils/theme';
|
||||
import {confirmOutOfOfficeDisabled} from '@utils/user';
|
||||
|
||||
import type UserModel from '@typings/database/models/servers/user';
|
||||
|
||||
type Props = {
|
||||
currentUser: UserModel;
|
||||
style: TextStyle;
|
||||
theme: Theme;
|
||||
};
|
||||
|
||||
const {OUT_OF_OFFICE, OFFLINE, AWAY, ONLINE, DND} = General;
|
||||
|
||||
const UserStatus = ({currentUser, style, theme}: Props) => {
|
||||
const intl = useIntl();
|
||||
const serverUrl = useServerUrl();
|
||||
|
||||
const handleSetStatus = useCallback(preventDoubleTap(() => {
|
||||
const renderContent = () => {
|
||||
return (
|
||||
<>
|
||||
<SlideUpPanelItem
|
||||
icon='check-circle'
|
||||
imageStyles={{color: theme.onlineIndicator}}
|
||||
onPress={() => setUserStatus(ONLINE)}
|
||||
testID='user_status.bottom_sheet.online'
|
||||
text={intl.formatMessage({
|
||||
id: 'mobile.set_status.online',
|
||||
defaultMessage: 'Online',
|
||||
})}
|
||||
textStyles={style}
|
||||
/>
|
||||
<SlideUpPanelItem
|
||||
icon='clock'
|
||||
imageStyles={{color: theme.awayIndicator}}
|
||||
onPress={() => setUserStatus(AWAY)}
|
||||
testID='user_status.bottom_sheet.away'
|
||||
text={intl.formatMessage({
|
||||
id: 'mobile.set_status.away',
|
||||
defaultMessage: 'Away',
|
||||
})}
|
||||
textStyles={style}
|
||||
/>
|
||||
<SlideUpPanelItem
|
||||
icon='minus-circle'
|
||||
imageStyles={{color: theme.dndIndicator}}
|
||||
onPress={() => setUserStatus(DND)}
|
||||
testID='user_status.bottom_sheet.dnd'
|
||||
text={intl.formatMessage({
|
||||
id: 'mobile.set_status.dnd',
|
||||
defaultMessage: 'Do Not Disturb',
|
||||
})}
|
||||
textStyles={style}
|
||||
/>
|
||||
<SlideUpPanelItem
|
||||
icon='circle-outline'
|
||||
imageStyles={{color: changeOpacity('#B8B8B8', 0.64)}}
|
||||
onPress={() => setUserStatus(OFFLINE)}
|
||||
testID='user_status.bottom_sheet.offline'
|
||||
text={intl.formatMessage({
|
||||
id: 'mobile.set_status.offline',
|
||||
defaultMessage: 'Offline',
|
||||
})}
|
||||
textStyles={style}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
bottomSheet({
|
||||
closeButtonId: 'close-set-user-status',
|
||||
renderContent,
|
||||
snapPoints: [(5 * ITEM_HEIGHT) + 10, 10],
|
||||
title: intl.formatMessage({id: 'account.user_status.title', defaultMessage: 'User Presence'}),
|
||||
theme,
|
||||
});
|
||||
}), [theme]);
|
||||
|
||||
const updateStatus = useCallback((status: string) => {
|
||||
const userStatus = {
|
||||
user_id: currentUser.id,
|
||||
status,
|
||||
manual: true,
|
||||
last_activity_at: Date.now(),
|
||||
};
|
||||
|
||||
setStatus(serverUrl, userStatus);
|
||||
}, []);
|
||||
|
||||
const setUserStatus = useCallback((status: string) => {
|
||||
if (currentUser.status === OUT_OF_OFFICE) {
|
||||
dismissModal();
|
||||
return confirmOutOfOfficeDisabled(intl, status, updateStatus);
|
||||
}
|
||||
|
||||
updateStatus(status);
|
||||
DeviceEventEmitter.emit(Navigation.NAVIGATION_CLOSE_MODAL);
|
||||
return null;
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<DrawerItem
|
||||
testID='account.status.action'
|
||||
labelComponent={
|
||||
<StatusLabel
|
||||
labelStyle={style}
|
||||
status={currentUser.status}
|
||||
/>
|
||||
}
|
||||
leftComponent={
|
||||
<UserStatusIndicator
|
||||
size={24}
|
||||
status={currentUser.status}
|
||||
/>
|
||||
}
|
||||
separator={false}
|
||||
onPress={handleSetStatus}
|
||||
theme={theme}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default UserStatus;
|
||||
@@ -0,0 +1,40 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback} from 'react';
|
||||
import {TextStyle} from 'react-native';
|
||||
|
||||
import DrawerItem from '@components/drawer_item';
|
||||
import FormattedText from '@components/formatted_text';
|
||||
import {preventDoubleTap} from '@utils/tap';
|
||||
|
||||
type Props = {
|
||||
isTablet: boolean;
|
||||
style: TextStyle;
|
||||
theme: Theme;
|
||||
}
|
||||
|
||||
const YourProfile = ({isTablet, style, theme}: Props) => {
|
||||
const openProfile = useCallback(preventDoubleTap(() => {
|
||||
// TODO: Open Profile screen in either a screen or in line for tablets
|
||||
}), [isTablet]);
|
||||
|
||||
return (
|
||||
<DrawerItem
|
||||
testID='account.your_profile.action'
|
||||
labelComponent={
|
||||
<FormattedText
|
||||
id='account.your_profile'
|
||||
defaultMessage='Your Profile'
|
||||
style={style}
|
||||
/>
|
||||
}
|
||||
iconName='account-outline'
|
||||
onPress={openProfile}
|
||||
separator={false}
|
||||
theme={theme}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default YourProfile;
|
||||
45
app/screens/home/account/components/tablet_view/index.ts
Normal file
45
app/screens/home/account/components/tablet_view/index.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useEffect, useState} from 'react';
|
||||
import {DeviceEventEmitter} from 'react-native';
|
||||
|
||||
import {Events, Screens} from '@constants';
|
||||
import CustomStatus from '@screens/custom_status';
|
||||
|
||||
type SelectedView = {
|
||||
id: string;
|
||||
Component: any;
|
||||
}
|
||||
|
||||
const TabletView: Record<string, React.ReactNode> = {
|
||||
[Screens.CUSTOM_STATUS]: CustomStatus,
|
||||
};
|
||||
|
||||
const AccountTabletView = () => {
|
||||
const [selected, setSelected] = useState<SelectedView | undefined>();
|
||||
|
||||
useEffect(() => {
|
||||
const listener = DeviceEventEmitter.addListener(Events.ACCOUNT_SELECT_TABLET_VIEW, (id: string) => {
|
||||
const component = TabletView[id];
|
||||
let tabletView: SelectedView | undefined;
|
||||
if (component) {
|
||||
tabletView = {
|
||||
Component: component,
|
||||
id,
|
||||
};
|
||||
}
|
||||
setSelected(tabletView);
|
||||
});
|
||||
|
||||
return () => listener.remove();
|
||||
}, []);
|
||||
|
||||
if (!selected) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return React.createElement(selected.Component, {componentId: selected.id, isTablet: true});
|
||||
};
|
||||
|
||||
export default AccountTabletView;
|
||||
73
app/screens/home/account/components/user_info/index.tsx
Normal file
73
app/screens/home/account/components/user_info/index.tsx
Normal file
@@ -0,0 +1,73 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {Text, View} from 'react-native';
|
||||
|
||||
import ProfilePicture from '@components/profile_picture';
|
||||
import {makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
import type UserModel from '@typings/database/models/servers/user';
|
||||
|
||||
type Props = {
|
||||
user: UserModel;
|
||||
showFullName: boolean;
|
||||
theme: Theme;
|
||||
};
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
return {
|
||||
container: {
|
||||
backgroundColor: theme.sidebarBg,
|
||||
paddingBottom: 20,
|
||||
top: 0,
|
||||
paddingTop: 22,
|
||||
paddingLeft: 20,
|
||||
},
|
||||
statusStyle: {
|
||||
right: 10,
|
||||
bottom: 10,
|
||||
borderColor: theme.sidebarBg,
|
||||
backgroundColor: theme.sidebarBg,
|
||||
},
|
||||
textFullName: {
|
||||
fontSize: 28,
|
||||
lineHeight: 36,
|
||||
color: theme.sidebarText,
|
||||
fontFamily: 'Metropolis-SemiBold',
|
||||
marginTop: 16,
|
||||
},
|
||||
textUserName: {
|
||||
fontSize: 16,
|
||||
lineHeight: 24,
|
||||
color: theme.sidebarText,
|
||||
fontFamily: 'OpenSans',
|
||||
marginTop: 4,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const AccountUserInfo = ({user, showFullName, theme}: Props) => {
|
||||
const styles = getStyleSheet(theme);
|
||||
const nickName = user.nickname ? ` (${user.nickname})` : '';
|
||||
const title = `${user.firstName} ${user.lastName}${nickName}`;
|
||||
const userName = `@${user.username}`;
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<ProfilePicture
|
||||
size={120}
|
||||
iconSize={28}
|
||||
showStatus={true}
|
||||
author={user}
|
||||
testID={'account.profile_picture'}
|
||||
statusStyle={styles.statusStyle}
|
||||
statusSize={24}
|
||||
/>
|
||||
{showFullName && <Text style={styles.textFullName}>{title}</Text>}
|
||||
<Text style={showFullName ? styles.textUserName : styles.textFullName}>{`${userName}`}</Text>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default AccountUserInfo;
|
||||
@@ -1,17 +1,87 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider';
|
||||
import withObservables from '@nozbe/with-observables';
|
||||
import {useRoute} from '@react-navigation/native';
|
||||
import React, {useCallback, useState} from 'react';
|
||||
import {Text} from 'react-native';
|
||||
import {ScrollView, StatusBar, View} from 'react-native';
|
||||
import Animated, {useAnimatedStyle, withTiming} from 'react-native-reanimated';
|
||||
import {SafeAreaView} from 'react-native-safe-area-context';
|
||||
import {Edge, SafeAreaView, useSafeAreaInsets} from 'react-native-safe-area-context';
|
||||
import {of as of$} from 'rxjs';
|
||||
import {switchMap} from 'rxjs/operators';
|
||||
import tinycolor from 'tinycolor2';
|
||||
|
||||
const AccountScreen = () => {
|
||||
import {Device, View as ViewConstants} from '@constants';
|
||||
import {MM_TABLES, SYSTEM_IDENTIFIERS} from '@constants/database';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {useSplitView} from '@hooks/device';
|
||||
import {isCustomStatusExpirySupported, isMinimumServerVersion} from '@utils/helpers';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
import AccountOptions from './components/options';
|
||||
import AccountTabletView from './components/tablet_view';
|
||||
import AccountUserInfo from './components/user_info';
|
||||
|
||||
import type {WithDatabaseArgs} from '@typings/database/database';
|
||||
import type SystemModel from '@typings/database/models/servers/system';
|
||||
import type UserModel from '@typings/database/models/servers/user';
|
||||
|
||||
type AccountScreenProps = {
|
||||
currentUser: UserModel;
|
||||
customStatusExpirySupported: boolean;
|
||||
enableCustomUserStatuses: boolean;
|
||||
showFullName: boolean;
|
||||
};
|
||||
|
||||
const {SERVER: {SYSTEM, USER}} = MM_TABLES;
|
||||
const edges: Edge[] = ['bottom', 'left', 'right'];
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
return {
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: theme.sidebarBg,
|
||||
},
|
||||
flex: {
|
||||
flex: 1,
|
||||
},
|
||||
flexRow: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
},
|
||||
divider: {
|
||||
backgroundColor: changeOpacity(theme.centerChannelColor, 0.2),
|
||||
height: 1,
|
||||
marginHorizontal: 15,
|
||||
},
|
||||
tabletContainer: {
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
flex: 1,
|
||||
},
|
||||
tabletDivider: {
|
||||
borderLeftWidth: 1,
|
||||
borderLeftColor: changeOpacity(theme.centerChannelColor, 0.16),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const AccountScreen = ({currentUser, enableCustomUserStatuses, customStatusExpirySupported, showFullName}: AccountScreenProps) => {
|
||||
const theme = useTheme();
|
||||
const [start, setStart] = useState(false);
|
||||
const route = useRoute();
|
||||
const isSplitView = useSplitView();
|
||||
const insets = useSafeAreaInsets();
|
||||
const isTablet = Device.IS_TABLET && !isSplitView;
|
||||
const barStyle = tinycolor(theme.sidebarBg).isDark() ? 'light-content' : 'dark-content';
|
||||
let tabletSidebarStyle;
|
||||
if (isTablet) {
|
||||
const {TABLET} = ViewConstants;
|
||||
tabletSidebarStyle = {maxWidth: TABLET.SIDEBAR_WIDTH};
|
||||
}
|
||||
|
||||
const params = route.params! as {direction: string};
|
||||
const toLeft = params.direction === 'left';
|
||||
const [start, setStart] = useState(false);
|
||||
|
||||
const onLayout = useCallback(() => {
|
||||
setStart(true);
|
||||
@@ -31,18 +101,83 @@ const AccountScreen = () => {
|
||||
};
|
||||
}, [start]);
|
||||
|
||||
const styles = getStyleSheet(theme);
|
||||
|
||||
return (
|
||||
<SafeAreaView
|
||||
style={{flex: 1, backgroundColor: 'green'}}
|
||||
edges={edges}
|
||||
style={styles.container}
|
||||
>
|
||||
<View style={[{height: insets.top, flexDirection: 'row'}]}>
|
||||
<View style={[styles.container, tabletSidebarStyle]}/>
|
||||
{isTablet && <View style={styles.tabletContainer}/>}
|
||||
</View>
|
||||
<StatusBar
|
||||
barStyle={barStyle}
|
||||
backgroundColor='rgba(20, 33, 62, 0.42)'
|
||||
/>
|
||||
<Animated.View
|
||||
onLayout={onLayout}
|
||||
style={[{flex: 1, justifyContent: 'center', alignItems: 'center'}, animated]}
|
||||
style={[styles.flexRow, animated]}
|
||||
>
|
||||
<Text style={{fontSize: 20, color: '#fff'}}>{'Account Screen'}</Text>
|
||||
<ScrollView
|
||||
contentContainerStyle={styles.flex}
|
||||
alwaysBounceVertical={false}
|
||||
style={tabletSidebarStyle}
|
||||
>
|
||||
<AccountUserInfo
|
||||
user={currentUser}
|
||||
showFullName={showFullName}
|
||||
theme={theme}
|
||||
/>
|
||||
<AccountOptions
|
||||
enableCustomUserStatuses={enableCustomUserStatuses}
|
||||
isCustomStatusExpirySupported={customStatusExpirySupported}
|
||||
isTablet={isTablet}
|
||||
user={currentUser}
|
||||
theme={theme}
|
||||
/>
|
||||
</ScrollView>
|
||||
{isTablet &&
|
||||
<View style={[styles.tabletContainer, styles.tabletDivider]}>
|
||||
<AccountTabletView/>
|
||||
</View>
|
||||
}
|
||||
</Animated.View>
|
||||
</SafeAreaView>
|
||||
);
|
||||
};
|
||||
|
||||
export default AccountScreen;
|
||||
const withUserConfig = withObservables([], ({database}: WithDatabaseArgs) => {
|
||||
const config = database.
|
||||
get<SystemModel>(SYSTEM).
|
||||
findAndObserve(SYSTEM_IDENTIFIERS.CONFIG);
|
||||
const showFullName = config.pipe((switchMap((cfg) => of$((cfg.value as ClientConfig).ShowFullName === 'true'))));
|
||||
const enableCustomUserStatuses = config.pipe((switchMap((cfg) => {
|
||||
const ClientConfig = cfg.value as ClientConfig;
|
||||
return of$(ClientConfig.EnableCustomUserStatuses === 'true' && isMinimumServerVersion(ClientConfig.Version, 5, 36));
|
||||
})));
|
||||
const version = config.pipe(
|
||||
switchMap((cfg) => of$((cfg.value as ClientConfig).Version)),
|
||||
);
|
||||
const customStatusExpirySupported = config.pipe(
|
||||
switchMap((cfg) => of$(isCustomStatusExpirySupported((cfg.value as ClientConfig).Version))),
|
||||
);
|
||||
|
||||
return {
|
||||
currentUser: database.
|
||||
get(SYSTEM).
|
||||
findAndObserve(SYSTEM_IDENTIFIERS.CURRENT_USER_ID).
|
||||
pipe(
|
||||
switchMap((id: SystemModel) =>
|
||||
database.get(USER).findAndObserve(id.value),
|
||||
),
|
||||
),
|
||||
enableCustomUserStatuses,
|
||||
customStatusExpirySupported,
|
||||
showFullName,
|
||||
version,
|
||||
};
|
||||
});
|
||||
|
||||
export default withDatabase(withUserConfig(AccountScreen));
|
||||
|
||||
@@ -76,7 +76,7 @@ function TabBar({state, descriptors, navigation, theme}: BottomTabBarProps & {th
|
||||
return {
|
||||
transform: [{translateX}],
|
||||
};
|
||||
}, [state.index]);
|
||||
}, [state.index, tabWidth]);
|
||||
|
||||
const animatedStyle = useAnimatedStyle(() => {
|
||||
if (visible === undefined) {
|
||||
|
||||
@@ -54,18 +54,24 @@ Navigation.setLazyComponentRegistrator((screenName) => {
|
||||
case Screens.ABOUT:
|
||||
screen = withServerDatabase(require('@screens/about').default);
|
||||
break;
|
||||
// case 'AddReaction':
|
||||
// screen = require('@screens/add_reaction').default;
|
||||
// break;
|
||||
// case 'AdvancedSettings':
|
||||
// screen = require('@screens/settings/advanced_settings').default;
|
||||
// break;
|
||||
case Screens.BOTTOM_SHEET:
|
||||
screen = require('@screens/bottom_sheet').default;
|
||||
screen = withServerDatabase(require('@screens/bottom_sheet').default);
|
||||
break;
|
||||
case Screens.CHANNEL:
|
||||
screen = withServerDatabase(require('@screens/channel').default);
|
||||
break;
|
||||
case Screens.CUSTOM_STATUS:
|
||||
screen = withServerDatabase(require('@screens/custom_status').default);
|
||||
break;
|
||||
case Screens.CUSTOM_STATUS_CLEAR_AFTER:
|
||||
screen = withServerDatabase(require('@screens/custom_status_clear_after').default);
|
||||
break;
|
||||
case Screens.EMOJI_PICKER:
|
||||
screen = withServerDatabase(require('@screens/emoji_picker').default);
|
||||
break;
|
||||
// case 'ChannelAddMembers':
|
||||
// screen = require('@screens/channel_add_members').default;
|
||||
// break;
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {Text, View} from 'react-native';
|
||||
|
||||
const Modal = () => {
|
||||
return (
|
||||
<View style={{backgroundColor: '#ffffff', flex: 1}}>
|
||||
<Text>{'Modal screen'}</Text>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default Modal;
|
||||
@@ -2,21 +2,29 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import merge from 'deepmerge';
|
||||
import {Appearance, DeviceEventEmitter, Keyboard, Platform} from 'react-native';
|
||||
import {Appearance, DeviceEventEmitter, NativeModules, Platform} from 'react-native';
|
||||
import {Navigation, Options, OptionsModalPresentationStyle} from 'react-native-navigation';
|
||||
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import {Device, Preferences, Screens} from '@constants';
|
||||
import NavigationConstants from '@constants/navigation';
|
||||
import EphemeralStore from '@store/ephemeral_store';
|
||||
import {changeOpacity} from '@utils/theme';
|
||||
|
||||
import type {LaunchProps} from '@typings/launch';
|
||||
|
||||
const {MattermostManaged} = NativeModules;
|
||||
const isRunningInSplitView = MattermostManaged.isRunningInSplitView;
|
||||
|
||||
Navigation.setDefaultOptions({
|
||||
layout: {
|
||||
|
||||
//@ts-expect-error all not defined in type definition
|
||||
orientation: [Device.IS_TABLET ? 'all' : 'portrait'],
|
||||
},
|
||||
statusBar: {
|
||||
backgroundColor: 'rgba(20, 33, 62, 0.42)',
|
||||
},
|
||||
});
|
||||
|
||||
function getThemeFromState() {
|
||||
@@ -261,7 +269,10 @@ export function showModal(name: string, title: string, passProps = {}, options =
|
||||
component: {
|
||||
id: name,
|
||||
name,
|
||||
passProps,
|
||||
passProps: {
|
||||
...passProps,
|
||||
isModal: true,
|
||||
},
|
||||
options: merge(defaultOptions, options),
|
||||
},
|
||||
}],
|
||||
@@ -353,14 +364,15 @@ export async function dismissModal(options = {}) {
|
||||
return;
|
||||
}
|
||||
|
||||
const componentId = EphemeralStore.getNavigationTopComponentId();
|
||||
|
||||
try {
|
||||
await Navigation.dismissModal(componentId, options);
|
||||
EphemeralStore.removeNavigationModal(componentId);
|
||||
} catch (error) {
|
||||
// RNN returns a promise rejection if there is no modal to
|
||||
// dismiss. We'll do nothing in this case.
|
||||
const componentId = EphemeralStore.getNavigationTopModalId();
|
||||
if (componentId) {
|
||||
try {
|
||||
await Navigation.dismissModal(componentId, options);
|
||||
EphemeralStore.removeNavigationModal(componentId);
|
||||
} catch (error) {
|
||||
// RNN returns a promise rejection if there is no modal to
|
||||
// dismiss. We'll do nothing in this case.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -421,68 +433,46 @@ export async function dismissOverlay(componentId: string) {
|
||||
}
|
||||
}
|
||||
|
||||
export function openMainSideMenu() {
|
||||
if (Platform.OS === 'ios') {
|
||||
return;
|
||||
}
|
||||
|
||||
const componentId = EphemeralStore.getNavigationTopComponentId();
|
||||
|
||||
Keyboard.dismiss();
|
||||
Navigation.mergeOptions(componentId, {
|
||||
sideMenu: {
|
||||
left: {visible: true},
|
||||
},
|
||||
});
|
||||
type BottomSheetArgs = {
|
||||
closeButtonId: string;
|
||||
renderContent: () => JSX.Element;
|
||||
snapPoints: number[];
|
||||
theme: Theme;
|
||||
title: string;
|
||||
}
|
||||
|
||||
export function closeMainSideMenu() {
|
||||
if (Platform.OS === 'ios') {
|
||||
return;
|
||||
export async function bottomSheet({title, renderContent, snapPoints, theme, closeButtonId}: BottomSheetArgs) {
|
||||
const {isSplitView} = await isRunningInSplitView();
|
||||
const isTablet = Device.IS_TABLET && !isSplitView;
|
||||
|
||||
if (isTablet) {
|
||||
const closeButton = CompassIcon.getImageSourceSync('close', 24, theme.centerChannelColor);
|
||||
showModal(Screens.BOTTOM_SHEET, title, {
|
||||
closeButtonId,
|
||||
renderContent,
|
||||
snapPoints,
|
||||
}, {
|
||||
modalPresentationStyle: OptionsModalPresentationStyle.formSheet,
|
||||
swipeToDismiss: true,
|
||||
topBar: {
|
||||
leftButtons: [{
|
||||
id: closeButtonId,
|
||||
icon: closeButton,
|
||||
testID: closeButtonId,
|
||||
}],
|
||||
leftButtonColor: changeOpacity(theme.centerChannelColor, 0.56),
|
||||
background: {
|
||||
color: theme.centerChannelBg,
|
||||
},
|
||||
title: {
|
||||
color: theme.centerChannelColor,
|
||||
},
|
||||
},
|
||||
});
|
||||
} else {
|
||||
showModalOverCurrentContext(Screens.BOTTOM_SHEET, {
|
||||
renderContent,
|
||||
snapPoints,
|
||||
}, {swipeToDismiss: true});
|
||||
}
|
||||
|
||||
Keyboard.dismiss();
|
||||
Navigation.mergeOptions(Screens.CHANNEL, {
|
||||
sideMenu: {
|
||||
left: {visible: false},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function enableMainSideMenu(enabled: boolean, visible = true) {
|
||||
if (Platform.OS === 'ios') {
|
||||
return;
|
||||
}
|
||||
|
||||
Navigation.mergeOptions(Screens.CHANNEL, {
|
||||
sideMenu: {
|
||||
left: {enabled, visible},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function openSettingsSideMenu() {
|
||||
if (Platform.OS === 'ios') {
|
||||
return;
|
||||
}
|
||||
|
||||
Keyboard.dismiss();
|
||||
Navigation.mergeOptions(Screens.CHANNEL, {
|
||||
sideMenu: {
|
||||
right: {visible: true},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function closeSettingsSideMenu() {
|
||||
if (Platform.OS === 'ios') {
|
||||
return;
|
||||
}
|
||||
|
||||
Keyboard.dismiss();
|
||||
Navigation.mergeOptions(Screens.CHANNEL, {
|
||||
sideMenu: {
|
||||
right: {visible: false},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -11,6 +11,10 @@ class EphemeralStore {
|
||||
return this.navigationComponentIdStack[0];
|
||||
}
|
||||
|
||||
getNavigationTopModalId = () => {
|
||||
return this.navigationModalStack[0];
|
||||
}
|
||||
|
||||
clearNavigationComponents = () => {
|
||||
this.navigationComponentIdStack = [];
|
||||
this.navigationModalStack = [];
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
|
||||
import emojiRegex from 'emoji-regex';
|
||||
|
||||
import SystemModel from '@database/models/server/system';
|
||||
|
||||
import {Emojis, EmojiIndicesByAlias} from './';
|
||||
|
||||
const RE_NAMED_EMOJI = /(:([a-zA-Z0-9_+-]+):)/g;
|
||||
@@ -111,9 +113,9 @@ const defaultComparisonRule = (aName: string, bName: string) => {
|
||||
return aName.localeCompare(bName);
|
||||
};
|
||||
|
||||
const thumbsDownComparisonRule = (other: string) => (other === 'thumbsup' || other === '+1' ? 1 : 0);
|
||||
const thumbsDownComparisonRule = (other: string) => (other.startsWith('thumbsup') || other.startsWith('+1') ? 1 : 0);
|
||||
|
||||
const thumbsUpComparisonRule = (other: string) => (other === 'thumbsdown' || other === '-1' ? -1 : 0);
|
||||
const thumbsUpComparisonRule = (other: string) => (other.startsWith('thumbsdown') || other.startsWith('-1') ? -1 : 0);
|
||||
|
||||
type Comparators = Record<string, ((other: string) => number)>;
|
||||
|
||||
@@ -125,8 +127,9 @@ const customComparisonRules: Comparators = {
|
||||
};
|
||||
|
||||
function doDefaultComparison(aName: string, bName: string) {
|
||||
if (customComparisonRules[aName]) {
|
||||
return customComparisonRules[aName](bName) || defaultComparisonRule(aName, bName);
|
||||
const rule = aName.split('_')[0];
|
||||
if (customComparisonRules[rule]) {
|
||||
return customComparisonRules[rule](bName) || defaultComparisonRule(aName, bName);
|
||||
}
|
||||
|
||||
return defaultComparisonRule(aName, bName);
|
||||
@@ -188,3 +191,29 @@ export function compareEmojis(emojiA: string | Partial<EmojiType>, emojiB: strin
|
||||
|
||||
return doDefaultComparison(aName!, bName!);
|
||||
}
|
||||
|
||||
export const isCustomEmojiEnabled = (config: ClientConfig | SystemModel) => {
|
||||
if (config instanceof SystemModel) {
|
||||
return config?.value?.EnableCustomEmoji === 'true';
|
||||
}
|
||||
|
||||
return config?.EnableCustomEmoji === 'true';
|
||||
};
|
||||
|
||||
export function fillEmoji(index: number) {
|
||||
const emoji = Emojis[index];
|
||||
return {
|
||||
name: 'short_name' in emoji ? emoji.short_name : emoji.name,
|
||||
aliases: 'short_names' in emoji ? emoji.short_names : [],
|
||||
};
|
||||
}
|
||||
|
||||
export function getSkin(emoji: any) {
|
||||
if ('skin_variations' in emoji) {
|
||||
return 'default';
|
||||
}
|
||||
if ('skins' in emoji) {
|
||||
return emoji.skins && emoji.skins[0];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,12 +1,15 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import moment, {Moment} from 'moment-timezone';
|
||||
|
||||
import {CUSTOM_STATUS_TIME_PICKER_INTERVALS_IN_MINUTES} from '@constants/custom_status';
|
||||
|
||||
// isMinimumServerVersion will return true if currentVersion is equal to higher or than
|
||||
// the provided minimum version. A non-equal major version will ignore minor and dot
|
||||
// versions, and a non-equal minor version will ignore dot version.
|
||||
// currentVersion is a string, e.g '4.6.0'
|
||||
// minMajorVersion, minMinorVersion, minDotVersion are integers
|
||||
|
||||
export const isMinimumServerVersion = (currentVersion: string, minMajorVersion = 0, minMinorVersion = 0, minDotVersion = 0): boolean => {
|
||||
if (!currentVersion || typeof currentVersion !== 'string') {
|
||||
return false;
|
||||
@@ -74,7 +77,7 @@ export function isEmail(email: string): boolean {
|
||||
return (/^[^ ,@]+@[^ ,@]+$/).test(email);
|
||||
}
|
||||
|
||||
export function safeParseJSON(rawJson: string | Record<string, unknown>) {
|
||||
export function safeParseJSON(rawJson: string | Record<string, unknown> | unknown[]) {
|
||||
let data = rawJson;
|
||||
try {
|
||||
if (typeof rawJson == 'string') {
|
||||
@@ -86,3 +89,33 @@ export function safeParseJSON(rawJson: string | Record<string, unknown>) {
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
export function getCurrentMomentForTimezone(timezone: string) {
|
||||
return timezone ? moment.tz(timezone) : moment();
|
||||
}
|
||||
|
||||
export function getUtcOffsetForTimeZone(timezone: string) {
|
||||
return moment.tz(timezone).utcOffset();
|
||||
}
|
||||
|
||||
export function isCustomStatusExpirySupported(version: string) {
|
||||
return isMinimumServerVersion(version, 5, 37);
|
||||
}
|
||||
|
||||
export function toTitleCase(str: string) {
|
||||
function doTitleCase(txt: string) {
|
||||
return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();
|
||||
}
|
||||
return str.replace(/\w\S*/g, doTitleCase);
|
||||
}
|
||||
|
||||
export function getRoundedTime(value: Moment) {
|
||||
const roundedTo = CUSTOM_STATUS_TIME_PICKER_INTERVALS_IN_MINUTES;
|
||||
const start = moment(value);
|
||||
const diff = start.minute() % roundedTo;
|
||||
if (diff === 0) {
|
||||
return value;
|
||||
}
|
||||
const remainder = roundedTo - diff;
|
||||
return start.add(remainder, 'm').seconds(0).milliseconds(0);
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
import {StyleSheet} from 'react-native';
|
||||
import tinyColor from 'tinycolor2';
|
||||
|
||||
import {Preferences, Screens} from '@constants';
|
||||
import {mergeNavigationOptions} from '@screens/navigation';
|
||||
import EphemeralStore from '@store/ephemeral_store';
|
||||
|
||||
@@ -119,7 +120,9 @@ export function setNavigatorStyles(componentId: string, theme: Theme) {
|
||||
|
||||
export function setNavigationStackStyles(theme: Theme) {
|
||||
EphemeralStore.allNavigationComponentIds.forEach((componentId) => {
|
||||
setNavigatorStyles(componentId, theme);
|
||||
if (componentId !== Screens.BOTTOM_SHEET) {
|
||||
setNavigatorStyles(componentId, theme);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -206,3 +209,56 @@ export function blendColors(background: string, foreground: string, opacity: num
|
||||
|
||||
return `rgba(${red},${green},${blue},${alpha})`;
|
||||
}
|
||||
|
||||
const themeTypeMap: ThemeTypeMap = {
|
||||
Mattermost: 'denim',
|
||||
Organization: 'sapphire',
|
||||
'Mattermost Dark': 'indigo',
|
||||
'Windows Dark': 'onyx',
|
||||
Denim: 'denim',
|
||||
Sapphire: 'sapphire',
|
||||
Quartz: 'quartz',
|
||||
Indigo: 'indigo',
|
||||
Onyx: 'onyx',
|
||||
custom: 'custom',
|
||||
};
|
||||
|
||||
// setThemeDefaults will set defaults on the theme for any unset properties.
|
||||
export function setThemeDefaults(theme: Theme): Theme {
|
||||
const themes = Preferences.THEMES as Record<ThemeKey, Theme>;
|
||||
const defaultTheme = themes.denim;
|
||||
|
||||
const processedTheme = {...theme};
|
||||
|
||||
// If this is a system theme, return the source theme object matching the theme preference type
|
||||
if (theme.type && theme.type !== 'custom' && Object.keys(themeTypeMap).includes(theme.type)) {
|
||||
return Preferences.THEMES[themeTypeMap[theme.type]];
|
||||
}
|
||||
|
||||
for (const key of Object.keys(defaultTheme)) {
|
||||
if (theme[key]) {
|
||||
// Fix a case where upper case theme colours are rendered as black
|
||||
processedTheme[key] = theme[key]?.toLowerCase();
|
||||
}
|
||||
}
|
||||
|
||||
for (const property in defaultTheme) {
|
||||
if (property === 'type' || (property === 'sidebarTeamBarBg' && theme.sidebarHeaderBg)) {
|
||||
continue;
|
||||
}
|
||||
if (theme[property] == null) {
|
||||
processedTheme[property] = defaultTheme[property];
|
||||
}
|
||||
|
||||
// Backwards compatability with old name
|
||||
if (!theme.mentionBg && theme.mentionBj) {
|
||||
processedTheme.mentionBg = theme.mentionBj;
|
||||
}
|
||||
}
|
||||
|
||||
if (!theme.sidebarTeamBarBg && theme.sidebarHeaderBg) {
|
||||
processedTheme.sidebarTeamBarBg = blendColors(theme.sidebarHeaderBg, '#000000', 0.2, true);
|
||||
}
|
||||
|
||||
return processedTheme as Theme;
|
||||
}
|
||||
|
||||
@@ -2,14 +2,18 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import moment from 'moment-timezone';
|
||||
import {Alert} from 'react-native';
|
||||
|
||||
import {General, Preferences} from '@constants';
|
||||
import {CustomStatusDuration} from '@constants/custom_status';
|
||||
import {UserModel} from '@database/models/server';
|
||||
import {DEFAULT_LOCALE, getLocalizedMessage, t} from '@i18n';
|
||||
import {toTitleCase} from '@utils/helpers';
|
||||
|
||||
import type GroupModel from '@typings/database/models/servers/group';
|
||||
import type GroupMembershipModel from '@typings/database/models/servers/group_membership';
|
||||
import type {UserMentionKey} from '@typings/global/markdown';
|
||||
import type {IntlShape} from 'react-intl';
|
||||
|
||||
export function displayUsername(user?: UserProfile | UserModel, locale?: string, teammateDisplayNameSetting?: string, useFallbackUsername = true) {
|
||||
let name = useFallbackUsername ? getLocalizedMessage(locale || DEFAULT_LOCALE, t('channel_loader.someone'), 'Someone') : '';
|
||||
@@ -166,12 +170,16 @@ export const getTimezone = (timezone: UserTimezone | null) => {
|
||||
return timezone.manualTimezone;
|
||||
};
|
||||
|
||||
export const getUserCustomStatus = (user: UserModel) => {
|
||||
if (user.props?.customStatus) {
|
||||
return user.props.customStatus as UserCustomStatus;
|
||||
}
|
||||
export const getUserCustomStatus = (user: UserModel): UserCustomStatus | undefined => {
|
||||
try {
|
||||
if (typeof user.props?.customStatus === 'string') {
|
||||
return JSON.parse(user.props.customStatus) as UserCustomStatus;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
return user.props?.customStatus;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
export function isCustomStatusExpired(user: UserModel) {
|
||||
@@ -190,3 +198,43 @@ export function isCustomStatusExpired(user: UserModel) {
|
||||
const currentTime = timezone ? moment.tz(timezone) : moment();
|
||||
return currentTime.isSameOrAfter(expiryTime);
|
||||
}
|
||||
|
||||
export function confirmOutOfOfficeDisabled(intl: IntlShape, status: string, updateStatus: (status: string) => void) {
|
||||
const userStatusId = 'modal.manual_status.auto_responder.message_' + status;
|
||||
t('modal.manual_status.auto_responder.message_');
|
||||
t('modal.manual_status.auto_responder.message_away');
|
||||
t('modal.manual_status.auto_responder.message_dnd');
|
||||
t('modal.manual_status.auto_responder.message_offline');
|
||||
t('modal.manual_status.auto_responder.message_online');
|
||||
|
||||
let translatedStatus;
|
||||
if (status === 'dnd') {
|
||||
translatedStatus = intl.formatMessage({
|
||||
id: 'mobile.set_status.dnd',
|
||||
defaultMessage: 'Do Not Disturb',
|
||||
});
|
||||
} else {
|
||||
translatedStatus = intl.formatMessage({
|
||||
id: `mobile.set_status.${status}`,
|
||||
defaultMessage: toTitleCase(status),
|
||||
});
|
||||
}
|
||||
|
||||
Alert.alert(
|
||||
intl.formatMessage({
|
||||
id: 'mobile.reset_status.title_ooo',
|
||||
defaultMessage: 'Disable "Out Of Office"?',
|
||||
}),
|
||||
intl.formatMessage({
|
||||
id: userStatusId,
|
||||
defaultMessage: 'Would you like to switch your status to "{status}" and disable Automatic Replies?',
|
||||
}, {status: translatedStatus}),
|
||||
[{
|
||||
text: intl.formatMessage({id: 'mobile.reset_status.alert_cancel', defaultMessage: 'Cancel'}),
|
||||
style: 'cancel',
|
||||
}, {
|
||||
text: intl.formatMessage({id: 'mobile.reset_status.alert_ok', defaultMessage: 'OK'}),
|
||||
onPress: () => updateStatus(status),
|
||||
}],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -10,6 +10,12 @@
|
||||
"about.teamEditiont0": "Team Edition",
|
||||
"about.teamEditiont1": "Enterprise Edition",
|
||||
"about.title": "About {appTitle}",
|
||||
"account.logout": "Log out",
|
||||
"account.logout_from": "Log out of {serverName}",
|
||||
"account.saved_messages": "Saved Messages",
|
||||
"account.settings": "Settings",
|
||||
"account.user_status.title": "User Presence",
|
||||
"account.your_profile": "Your Profile",
|
||||
"api.channel.add_guest.added": "{addedUsername} added to the channel as a guest by {username}.",
|
||||
"api.channel.add_member.added": "{addedUsername} added to the channel by {username}.",
|
||||
"api.channel.guest_join_channel.post_and_forget": "{username} joined the channel as a guest.",
|
||||
@@ -58,6 +64,27 @@
|
||||
"combined_system_message.removed_from_team.one_you": "You were **removed from the team**.",
|
||||
"combined_system_message.removed_from_team.two": "{firstUser} and {secondUser} were **removed from the team**.",
|
||||
"combined_system_message.you": "You",
|
||||
"custom_status.expiry_dropdown.custom": "Custom",
|
||||
"custom_status.expiry_dropdown.date_and_time": "Date and Time",
|
||||
"custom_status.expiry_dropdown.dont_clear": "Don't clear",
|
||||
"custom_status.expiry_dropdown.four_hours": "4 hours",
|
||||
"custom_status.expiry_dropdown.one_hour": "1 hour",
|
||||
"custom_status.expiry_dropdown.thirty_minutes": "30 minutes",
|
||||
"custom_status.expiry_dropdown.this_week": "This week",
|
||||
"custom_status.expiry_dropdown.today": "Today",
|
||||
"custom_status.expiry_time.today": "Today",
|
||||
"custom_status.expiry_time.tomorrow": "Tomorrow",
|
||||
"custom_status.expiry.at": "at",
|
||||
"custom_status.expiry.until": "Until",
|
||||
"custom_status.failure_message": "Failed to update status. Try again",
|
||||
"custom_status.set_status": "Set a Status",
|
||||
"custom_status.suggestions.in_a_meeting": "In a meeting",
|
||||
"custom_status.suggestions.on_a_vacation": "On a vacation",
|
||||
"custom_status.suggestions.out_for_lunch": "Out for lunch",
|
||||
"custom_status.suggestions.out_sick": "Out sick",
|
||||
"custom_status.suggestions.recent_title": "RECENT",
|
||||
"custom_status.suggestions.title": "SUGGESTIONS",
|
||||
"custom_status.suggestions.working_from_home": "Working from home",
|
||||
"date_separator.today": "Today",
|
||||
"date_separator.yesterday": "Yesterday",
|
||||
"emoji_picker.activities": "Activities",
|
||||
@@ -126,12 +153,18 @@
|
||||
"mobile.components.select_server_view.enterServerUrl": "Enter Server URL",
|
||||
"mobile.components.select_server_view.proceed": "Proceed",
|
||||
"mobile.components.select_server_view.siteUrlPlaceholder": "https://mattermost.example.com",
|
||||
"mobile.custom_status.choose_emoji": "Choose an emoji",
|
||||
"mobile.custom_status.clear_after": "Clear After",
|
||||
"mobile.custom_status.clear_after.title": "Clear Custom Status After",
|
||||
"mobile.custom_status.modal_confirm": "Done",
|
||||
"mobile.document_preview.failed_description": "An error occurred while opening the document. Please make sure you have a {fileType} viewer installed and try again.\n",
|
||||
"mobile.document_preview.failed_title": "Open Document failed",
|
||||
"mobile.downloader.disabled_description": "File downloads are disabled on this server. Please contact your System Admin for more details.\n",
|
||||
"mobile.downloader.disabled_title": "Download disabled",
|
||||
"mobile.downloader.failed_description": "An error occurred while downloading the file. Please check your internet connection and try again.\n",
|
||||
"mobile.downloader.failed_title": "Download failed",
|
||||
"mobile.emoji_picker.search.not_found_description": "Check the spelling or try another search.",
|
||||
"mobile.emoji_picker.search.not_found_title": "No results found for \"{searchTerm}\"",
|
||||
"mobile.error_handler.button": "Relaunch",
|
||||
"mobile.error_handler.description": "\nTap relaunch to open the app again. After restart, you can report the problem from the settings menu.\n",
|
||||
"mobile.error_handler.title": "Unexpected error occurred",
|
||||
@@ -177,8 +210,12 @@
|
||||
"mobile.push_notification_reply.title": "Reply",
|
||||
"mobile.request.invalid_request_method": "Invalid request method",
|
||||
"mobile.request.invalid_response": "Received invalid response from the server.",
|
||||
"mobile.reset_status.alert_cancel": "Cancel",
|
||||
"mobile.reset_status.alert_ok": "OK",
|
||||
"mobile.reset_status.title_ooo": "Disable \"Out Of Office\"?",
|
||||
"mobile.routes.code": "{language} Code",
|
||||
"mobile.routes.code.noLanguage": "Code",
|
||||
"mobile.routes.custom_status": "Set a Status",
|
||||
"mobile.routes.login": "Login",
|
||||
"mobile.routes.loginOptions": "Login Chooser",
|
||||
"mobile.routes.mfa": "Multi-factor Authentication",
|
||||
@@ -197,6 +234,10 @@
|
||||
"mobile.server_url.empty": "Please enter a valid server URL",
|
||||
"mobile.server_url.invalid_format": "URL must start with http:// or https://",
|
||||
"mobile.session_expired": "Session Expired: Please log in to continue receiving notifications. Sessions for {siteName} are configured to expire every {daysCount, number} {daysCount, plural, one {day} other {days}}.",
|
||||
"mobile.set_status.away": "Away",
|
||||
"mobile.set_status.dnd": "Do Not Disturb",
|
||||
"mobile.set_status.offline": "Offline",
|
||||
"mobile.set_status.online": "Online",
|
||||
"mobile.system_message.channel_archived_message": "{username} archived the channel",
|
||||
"mobile.system_message.channel_unarchived_message": "{username} unarchived the channel",
|
||||
"mobile.system_message.update_channel_displayname_message_and_forget.updated_from": "{username} updated the channel display name from: {oldDisplayName} to: {newDisplayName}",
|
||||
@@ -212,6 +253,11 @@
|
||||
"mobile.unsupported_server.title": "Unsupported server version",
|
||||
"mobile.youtube_playback_error.description": "An error occurred while trying to play the YouTube video.\nDetails: {details}",
|
||||
"mobile.youtube_playback_error.title": "YouTube playback error",
|
||||
"modal.manual_status.auto_responder.message_": "Would you like to switch your status to \"{status}\" and disable Automatic Replies?",
|
||||
"modal.manual_status.auto_responder.message_away": "Would you like to switch your status to \"Away\" and disable Automatic Replies?",
|
||||
"modal.manual_status.auto_responder.message_dnd": "Would you like to switch your status to \"Do Not Disturb\" and disable Automatic Replies?",
|
||||
"modal.manual_status.auto_responder.message_offline": "Would you like to switch your status to \"Offline\" and disable Automatic Replies?",
|
||||
"modal.manual_status.auto_responder.message_online": "Would you like to switch your status to \"Online\" and disable Automatic Replies?",
|
||||
"password_form.title": "Password Reset",
|
||||
"password_send.checkInbox": "Please check your inbox.",
|
||||
"password_send.description": "To reset your password, enter the email address you used to sign up",
|
||||
@@ -237,8 +283,14 @@
|
||||
"post_info.system": "System",
|
||||
"post_message_view.edited": "(edited)",
|
||||
"posts_view.newMsg": "New Messages",
|
||||
"search_bar.search": "Search",
|
||||
"signup.email": "Email and Password",
|
||||
"signup.google": "Google Apps",
|
||||
"signup.office365": "Office 365",
|
||||
"status_dropdown.set_away": "Away",
|
||||
"status_dropdown.set_dnd": "Do Not Disturb",
|
||||
"status_dropdown.set_offline": "Offline",
|
||||
"status_dropdown.set_online": "Online",
|
||||
"status_dropdown.set_ooo": "Out Of Office",
|
||||
"web.root.signup_info": "All team communication in one place, searchable and accessible anywhere"
|
||||
}
|
||||
|
||||
BIN
assets/fonts/Metropolis-SemiBold.ttf
Normal file
BIN
assets/fonts/Metropolis-SemiBold.ttf
Normal file
Binary file not shown.
@@ -8,7 +8,7 @@ module.exports = {
|
||||
],
|
||||
env: {
|
||||
production: {
|
||||
plugins: ['transform-remove-console'],
|
||||
plugins: ['transform-remove-console', 'react-native-paper/babel'],
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
|
||||
6
index.ts
6
index.ts
@@ -13,7 +13,7 @@ import {initialLaunch} from './app/init/launch';
|
||||
import ManagedApp from './app/init/managed_app';
|
||||
import NetworkManager from './app/init/network_manager';
|
||||
import PushNotifications from './app/init/push_notifications';
|
||||
import {registerScreens} from './app/screens/index';
|
||||
import {registerScreens} from './app/screens';
|
||||
import EphemeralStore from './app/store/ephemeral_store';
|
||||
import setFontFamily from './app/utils/font_family';
|
||||
|
||||
@@ -81,5 +81,9 @@ function componentDidAppearListener({componentId}: ComponentDidAppearEvent) {
|
||||
function componentDidDisappearListener({componentId}: ComponentDidDisappearEvent) {
|
||||
if (componentId !== Screens.HOME) {
|
||||
EphemeralStore.removeNavigationComponentId(componentId);
|
||||
|
||||
if (EphemeralStore.getNavigationTopComponentId() === Screens.HOME) {
|
||||
DeviceEventEmitter.emit('tabBarVisible', true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,12 +7,9 @@
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
0111A42B7F264BCF8CBDE3ED /* OpenSans-ExtraBoldItalic.ttf in Resources */ = {isa = PBXBuildFile; fileRef = FBBEC29EE2D3418D9AC33BD5 /* OpenSans-ExtraBoldItalic.ttf */; };
|
||||
0C0D24F53F254F75869E5951 /* OpenSans-Italic.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 41F3AFE83AAF4B74878AB78A /* OpenSans-Italic.ttf */; };
|
||||
13B07FBC1A68108700A75B9A /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB01A68108700A75B9A /* AppDelegate.m */; };
|
||||
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; };
|
||||
13B07FC11A68108700A75B9A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB71A68108700A75B9A /* main.m */; };
|
||||
2D5296A8926B4D7FBAF2D6E2 /* OpenSans-Light.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 6561AEAC21CC40B8A72ABB93 /* OpenSans-Light.ttf */; };
|
||||
4953BF602368AE8600593328 /* SwimeProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4953BF5F2368AE8600593328 /* SwimeProxy.swift */; };
|
||||
49AE36FF26D4455800EF4E52 /* Gekidou in Frameworks */ = {isa = PBXBuildFile; productRef = 49AE36FE26D4455800EF4E52 /* Gekidou */; };
|
||||
49AE370126D4455D00EF4E52 /* Gekidou in Frameworks */ = {isa = PBXBuildFile; productRef = 49AE370026D4455D00EF4E52 /* Gekidou */; };
|
||||
@@ -20,9 +17,6 @@
|
||||
49B4C050230C981C006E919E /* libUploadAttachments.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7FABE04522137F2A00D0F595 /* libUploadAttachments.a */; };
|
||||
531BEBC72513E93C00BC05B1 /* compass-icons.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 531BEBC52513E93C00BC05B1 /* compass-icons.ttf */; };
|
||||
536CC6C323E79287002C478C /* RNNotificationEventHandler+HandleReplyAction.m in Sources */ = {isa = PBXBuildFile; fileRef = 536CC6C123E79287002C478C /* RNNotificationEventHandler+HandleReplyAction.m */; };
|
||||
55C6561DDBBA45929D88B6D1 /* OpenSans-BoldItalic.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 32AC3D4EA79E44738A6E9766 /* OpenSans-BoldItalic.ttf */; };
|
||||
62A8448264674B4D95A5A7C2 /* OpenSans-Semibold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = C78A387124874496AD2C1466 /* OpenSans-Semibold.ttf */; };
|
||||
69AC753E496743BABB7A7124 /* OpenSans-SemiboldItalic.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 0E617BF0F36D4E738F51D169 /* OpenSans-SemiboldItalic.ttf */; };
|
||||
6C9B1EFD6561083917AF06CF /* libPods-Mattermost.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 8DEEFB3ED6175724A2653247 /* libPods-Mattermost.a */; };
|
||||
7F0F4B0A24BA173900E14C60 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 7F0F4B0924BA173900E14C60 /* LaunchScreen.storyboard */; };
|
||||
7F151D3E221B062700FAD8F3 /* RuntimeUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F151D3D221B062700FAD8F3 /* RuntimeUtils.swift */; };
|
||||
@@ -31,9 +25,6 @@
|
||||
7F240A23220D3A2300637665 /* MattermostShare.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 7F240A19220D3A2300637665 /* MattermostShare.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||
7F240ADB220E089300637665 /* Item.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F240ADA220E089300637665 /* Item.swift */; };
|
||||
7F240ADD220E094A00637665 /* TeamsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F240ADC220E094A00637665 /* TeamsViewController.swift */; };
|
||||
7F25B62D270F717F00F32373 /* Metropolis-Light.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 7F25B626270F666D00F32373 /* Metropolis-Light.ttf */; };
|
||||
7F25B62E270F718300F32373 /* Metropolis-Regular.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 7F25B628270F666D00F32373 /* Metropolis-Regular.ttf */; };
|
||||
7F25B632270F825700F32373 /* Metropolis-Semibold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 7F25B631270F824E00F32373 /* Metropolis-Semibold.ttf */; };
|
||||
7F292A711E8AB73400A450A3 /* SplashScreenResource in Resources */ = {isa = PBXBuildFile; fileRef = 7F292A701E8AB73400A450A3 /* SplashScreenResource */; };
|
||||
7F581D35221ED5C60099E66B /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F581D34221ED5C60099E66B /* NotificationService.swift */; };
|
||||
7F581D39221ED5C60099E66B /* NotificationService.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 7F581D32221ED5C60099E66B /* NotificationService.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||
@@ -44,14 +35,23 @@
|
||||
7FABDFC22211A39000D0F595 /* Section.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FABDFC12211A39000D0F595 /* Section.swift */; };
|
||||
7FABE00A2212650600D0F595 /* ChannelsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FABE0092212650600D0F595 /* ChannelsViewController.swift */; };
|
||||
7FABE0562213884700D0F595 /* libUploadAttachments.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7FABE04522137F2A00D0F595 /* libUploadAttachments.a */; };
|
||||
7FB4224026DED2E40063B0EE /* compass-icons.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 531BEBC52513E93C00BC05B1 /* compass-icons.ttf */; };
|
||||
7FB31F812710995B0032E2E5 /* Metropolis-Light.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 7F25B626270F666D00F32373 /* Metropolis-Light.ttf */; };
|
||||
7FB31F822710996D0032E2E5 /* Metropolis-Regular.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 7F25B628270F666D00F32373 /* Metropolis-Regular.ttf */; };
|
||||
7FB31F832710996D0032E2E5 /* Metropolis-Semibold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 7F25B631270F824E00F32373 /* Metropolis-Semibold.ttf */; };
|
||||
7FB31F842710996D0032E2E5 /* OpenSans-Bold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = D4B1B363C2414DA19C1AC521 /* OpenSans-Bold.ttf */; };
|
||||
7FB31F852710996D0032E2E5 /* OpenSans-BoldItalic.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 32AC3D4EA79E44738A6E9766 /* OpenSans-BoldItalic.ttf */; };
|
||||
7FB31F862710996D0032E2E5 /* OpenSans-ExtraBold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 3647DF63D6764CF093375861 /* OpenSans-ExtraBold.ttf */; };
|
||||
7FB31F872710996D0032E2E5 /* OpenSans-ExtraBoldItalic.ttf in Resources */ = {isa = PBXBuildFile; fileRef = FBBEC29EE2D3418D9AC33BD5 /* OpenSans-ExtraBoldItalic.ttf */; };
|
||||
7FB31F882710996D0032E2E5 /* OpenSans-Italic.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 41F3AFE83AAF4B74878AB78A /* OpenSans-Italic.ttf */; };
|
||||
7FB31F892710996D0032E2E5 /* OpenSans-Light.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 6561AEAC21CC40B8A72ABB93 /* OpenSans-Light.ttf */; };
|
||||
7FB31F8A2710996D0032E2E5 /* OpenSans-LightItalic.ttf in Resources */ = {isa = PBXBuildFile; fileRef = BE17F630DB5D41FD93F32D22 /* OpenSans-LightItalic.ttf */; };
|
||||
7FB31F8B2710996D0032E2E5 /* OpenSans-Regular.ttf in Resources */ = {isa = PBXBuildFile; fileRef = BC977883E2624E05975CA65B /* OpenSans-Regular.ttf */; };
|
||||
7FB31F8C2710996D0032E2E5 /* OpenSans-Semibold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = C78A387124874496AD2C1466 /* OpenSans-Semibold.ttf */; };
|
||||
7FB31F8D2710996D0032E2E5 /* OpenSans-SemiboldItalic.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 0E617BF0F36D4E738F51D169 /* OpenSans-SemiboldItalic.ttf */; };
|
||||
7FB31F8E2710996D0032E2E5 /* compass-icons.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 531BEBC52513E93C00BC05B1 /* compass-icons.ttf */; };
|
||||
7FCEFB9326B7934F006DC1DE /* SDWebImageDownloaderOperation+Swizzle.m in Sources */ = {isa = PBXBuildFile; fileRef = 7FCEFB9226B7934F006DC1DE /* SDWebImageDownloaderOperation+Swizzle.m */; };
|
||||
7FEB109D1F61019C0039A015 /* MattermostManaged.m in Sources */ = {isa = PBXBuildFile; fileRef = 7FEB109A1F61019C0039A015 /* MattermostManaged.m */; };
|
||||
84E3264B229834C30055068A /* Config.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E325FF229834C30055068A /* Config.swift */; };
|
||||
9358B95F95184EE0A4DCE629 /* OpenSans-Bold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = D4B1B363C2414DA19C1AC521 /* OpenSans-Bold.ttf */; };
|
||||
A08D512E7ADC40CCAD055A9E /* OpenSans-Regular.ttf in Resources */ = {isa = PBXBuildFile; fileRef = BC977883E2624E05975CA65B /* OpenSans-Regular.ttf */; };
|
||||
D719A67137964F08BE47A5FC /* OpenSans-ExtraBold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 3647DF63D6764CF093375861 /* OpenSans-ExtraBold.ttf */; };
|
||||
F083DB472349411A8E6E7AAD /* OpenSans-LightItalic.ttf in Resources */ = {isa = PBXBuildFile; fileRef = BE17F630DB5D41FD93F32D22 /* OpenSans-LightItalic.ttf */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
@@ -603,23 +603,23 @@
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
7FB4224026DED2E40063B0EE /* compass-icons.ttf in Resources */,
|
||||
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */,
|
||||
7F0F4B0A24BA173900E14C60 /* LaunchScreen.storyboard in Resources */,
|
||||
7F292A711E8AB73400A450A3 /* SplashScreenResource in Resources */,
|
||||
7F25B62D270F717F00F32373 /* Metropolis-Light.ttf in Resources */,
|
||||
7F25B62E270F718300F32373 /* Metropolis-Regular.ttf in Resources */,
|
||||
7F25B632270F825700F32373 /* Metropolis-Semibold.ttf in Resources */,
|
||||
9358B95F95184EE0A4DCE629 /* OpenSans-Bold.ttf in Resources */,
|
||||
55C6561DDBBA45929D88B6D1 /* OpenSans-BoldItalic.ttf in Resources */,
|
||||
D719A67137964F08BE47A5FC /* OpenSans-ExtraBold.ttf in Resources */,
|
||||
0111A42B7F264BCF8CBDE3ED /* OpenSans-ExtraBoldItalic.ttf in Resources */,
|
||||
0C0D24F53F254F75869E5951 /* OpenSans-Italic.ttf in Resources */,
|
||||
2D5296A8926B4D7FBAF2D6E2 /* OpenSans-Light.ttf in Resources */,
|
||||
F083DB472349411A8E6E7AAD /* OpenSans-LightItalic.ttf in Resources */,
|
||||
A08D512E7ADC40CCAD055A9E /* OpenSans-Regular.ttf in Resources */,
|
||||
62A8448264674B4D95A5A7C2 /* OpenSans-Semibold.ttf in Resources */,
|
||||
69AC753E496743BABB7A7124 /* OpenSans-SemiboldItalic.ttf in Resources */,
|
||||
7FB31F812710995B0032E2E5 /* Metropolis-Light.ttf in Resources */,
|
||||
7FB31F822710996D0032E2E5 /* Metropolis-Regular.ttf in Resources */,
|
||||
7FB31F832710996D0032E2E5 /* Metropolis-Semibold.ttf in Resources */,
|
||||
7FB31F842710996D0032E2E5 /* OpenSans-Bold.ttf in Resources */,
|
||||
7FB31F852710996D0032E2E5 /* OpenSans-BoldItalic.ttf in Resources */,
|
||||
7FB31F862710996D0032E2E5 /* OpenSans-ExtraBold.ttf in Resources */,
|
||||
7FB31F872710996D0032E2E5 /* OpenSans-ExtraBoldItalic.ttf in Resources */,
|
||||
7FB31F882710996D0032E2E5 /* OpenSans-Italic.ttf in Resources */,
|
||||
7FB31F892710996D0032E2E5 /* OpenSans-Light.ttf in Resources */,
|
||||
7FB31F8A2710996D0032E2E5 /* OpenSans-LightItalic.ttf in Resources */,
|
||||
7FB31F8B2710996D0032E2E5 /* OpenSans-Regular.ttf in Resources */,
|
||||
7FB31F8C2710996D0032E2E5 /* OpenSans-Semibold.ttf in Resources */,
|
||||
7FB31F8D2710996D0032E2E5 /* OpenSans-SemiboldItalic.ttf in Resources */,
|
||||
7FB31F8E2710996D0032E2E5 /* compass-icons.ttf in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
||||
@@ -362,6 +362,8 @@ PODS:
|
||||
- React-Core
|
||||
- RNCMaskedView (0.1.11):
|
||||
- React
|
||||
- RNDateTimePicker (3.5.2):
|
||||
- React-Core
|
||||
- RNDeviceInfo (8.3.3):
|
||||
- React-Core
|
||||
- RNDevMenu (4.0.2):
|
||||
@@ -516,6 +518,7 @@ DEPENDENCIES:
|
||||
- "RNCAsyncStorage (from `../node_modules/@react-native-community/async-storage`)"
|
||||
- "RNCClipboard (from `../node_modules/@react-native-community/clipboard`)"
|
||||
- "RNCMaskedView (from `../node_modules/@react-native-community/masked-view`)"
|
||||
- "RNDateTimePicker (from `../node_modules/@react-native-community/datetimepicker`)"
|
||||
- RNDeviceInfo (from `../node_modules/react-native-device-info`)
|
||||
- RNDevMenu (from `../node_modules/react-native-dev-menu`)
|
||||
- RNFastImage (from `../node_modules/react-native-fast-image`)
|
||||
@@ -670,6 +673,8 @@ EXTERNAL SOURCES:
|
||||
:path: "../node_modules/@react-native-community/clipboard"
|
||||
RNCMaskedView:
|
||||
:path: "../node_modules/@react-native-community/masked-view"
|
||||
RNDateTimePicker:
|
||||
:path: "../node_modules/@react-native-community/datetimepicker"
|
||||
RNDeviceInfo:
|
||||
:path: "../node_modules/react-native-device-info"
|
||||
RNDevMenu:
|
||||
@@ -785,6 +790,7 @@ SPEC CHECKSUMS:
|
||||
RNCAsyncStorage: b03032fdbdb725bea0bd9e5ec5a7272865ae7398
|
||||
RNCClipboard: 41d8d918092ae8e676f18adada19104fa3e68495
|
||||
RNCMaskedView: 0e1bc4bfa8365eba5fbbb71e07fbdc0555249489
|
||||
RNDateTimePicker: 7658208086d86d09e1627b5c34ba0cf237c60140
|
||||
RNDeviceInfo: cc7de0772378f85d8f36ae439df20f05c590a651
|
||||
RNDevMenu: fd325b5554b61fe7f48d9205a3877cf5ee88cd7c
|
||||
RNFastImage: 1f2cab428712a4baaf78d6169eaec7f622556dd7
|
||||
|
||||
103
package-lock.json
generated
103
package-lock.json
generated
@@ -25,6 +25,7 @@
|
||||
"@react-native-community/async-storage": "1.12.1",
|
||||
"@react-native-community/cameraroll": "4.0.4",
|
||||
"@react-native-community/clipboard": "1.5.1",
|
||||
"@react-native-community/datetimepicker": "3.5.2",
|
||||
"@react-native-community/masked-view": "0.1.11",
|
||||
"@react-native-community/netinfo": "6.0.2",
|
||||
"@react-native-cookies/cookies": "6.0.11",
|
||||
@@ -66,11 +67,13 @@
|
||||
"react-native-navigation": "7.21.0",
|
||||
"react-native-neomorph-shadows": "1.1.2",
|
||||
"react-native-notifications": "4.1.2",
|
||||
"react-native-paper": "4.9.2",
|
||||
"react-native-permissions": "3.0.5",
|
||||
"react-native-reanimated": "2.3.0-beta.2",
|
||||
"react-native-redash": "16.2.2",
|
||||
"react-native-safe-area-context": "3.3.2",
|
||||
"react-native-screens": "3.8.0",
|
||||
"react-native-section-list-get-item-layout": "2.2.3",
|
||||
"react-native-share": "7.1.1",
|
||||
"react-native-slider": "0.11.0",
|
||||
"react-native-svg": "12.1.1",
|
||||
@@ -103,6 +106,7 @@
|
||||
"@types/commonmark-react-renderer": "4.3.1",
|
||||
"@types/deep-equal": "1.0.1",
|
||||
"@types/jest": "27.0.2",
|
||||
"@types/lodash": "4.14.175",
|
||||
"@types/react": "17.0.26",
|
||||
"@types/react-intl": "3.0.0",
|
||||
"@types/react-native": "0.65.3",
|
||||
@@ -4277,6 +4281,14 @@
|
||||
"react-native": ">=0.57.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@react-native-community/datetimepicker": {
|
||||
"version": "3.5.2",
|
||||
"resolved": "https://registry.npmjs.org/@react-native-community/datetimepicker/-/datetimepicker-3.5.2.tgz",
|
||||
"integrity": "sha512-TWRuAtr/DnrEcRewqvXMLea2oB+YF+SbtuYLHguALLxNJQLl/RFB7aTNZeF+OoH75zKFqtXECXV1/uxQUpA+sg==",
|
||||
"dependencies": {
|
||||
"invariant": "^2.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@react-native-community/eslint-config": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@react-native-community/eslint-config/-/eslint-config-3.0.1.tgz",
|
||||
@@ -4998,6 +5010,12 @@
|
||||
"integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/lodash": {
|
||||
"version": "4.14.175",
|
||||
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.175.tgz",
|
||||
"integrity": "sha512-XmdEOrKQ8a1Y/yxQFOMbC47G/V2VDO1GvMRnl4O75M4GW/abC5tnfzadQYkqEveqRM1dEJGFFegfPNA2vvx2iw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/mime-db": {
|
||||
"version": "1.43.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/mime-db/-/mime-db-1.43.1.tgz",
|
||||
@@ -19503,6 +19521,41 @@
|
||||
"react-native": ">=0.25.1"
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-paper": {
|
||||
"version": "4.9.2",
|
||||
"resolved": "https://registry.npmjs.org/react-native-paper/-/react-native-paper-4.9.2.tgz",
|
||||
"integrity": "sha512-J7FRsd0YblQawtuj9I46F//apZHadsCKk6jWpc6njFTYdgUeCdkR8KgEto7cp2WxbcGNELx7KGwPQ4zAgX746A==",
|
||||
"dependencies": {
|
||||
"@callstack/react-theme-provider": "^3.0.6",
|
||||
"color": "^3.1.2",
|
||||
"react-native-iphone-x-helper": "^1.3.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "*",
|
||||
"react-native": "*",
|
||||
"react-native-vector-icons": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-paper/node_modules/@callstack/react-theme-provider": {
|
||||
"version": "3.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@callstack/react-theme-provider/-/react-theme-provider-3.0.6.tgz",
|
||||
"integrity": "sha512-wwKMXfmklfogpalNZT0W+jh76BIquiYUiQHOaPmt/PCyCEP/E6rP+e7Uie6mBZrfkea9WJYJ+mus6r+45JAEhg==",
|
||||
"dependencies": {
|
||||
"deepmerge": "^3.2.0",
|
||||
"hoist-non-react-statics": "^3.3.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-paper/node_modules/deepmerge": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-3.3.0.tgz",
|
||||
"integrity": "sha512-GRQOafGHwMHpjPx9iCvTgpu9NojZ49q794EEL94JVEw6VaeA8XTUyBKvAkOOjBX9oJNiV6G3P+T+tihFjo2TqA==",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-permissions": {
|
||||
"version": "3.0.5",
|
||||
"resolved": "https://registry.npmjs.org/react-native-permissions/-/react-native-permissions-3.0.5.tgz",
|
||||
@@ -19580,6 +19633,11 @@
|
||||
"react-native": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-section-list-get-item-layout": {
|
||||
"version": "2.2.3",
|
||||
"resolved": "https://registry.npmjs.org/react-native-section-list-get-item-layout/-/react-native-section-list-get-item-layout-2.2.3.tgz",
|
||||
"integrity": "sha512-fzCW5SiYP6qCZyDHebaElHonIFr8NFrZK9JDkxFLnpxMJih4d+HQ4rHyOs0Z4Gb/FjyCVbRH7RtEnjeQ0XffMg=="
|
||||
},
|
||||
"node_modules/react-native-share": {
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/react-native-share/-/react-native-share-7.1.1.tgz",
|
||||
@@ -27599,6 +27657,14 @@
|
||||
"integrity": "sha512-AHAmrkLEH5UtPaDiRqoULERHh3oNv7Dgs0bTC0hO5Z2GdNokAMPT5w8ci8aMcRemcwbtdHjxChgtjbeA38GBdA==",
|
||||
"requires": {}
|
||||
},
|
||||
"@react-native-community/datetimepicker": {
|
||||
"version": "3.5.2",
|
||||
"resolved": "https://registry.npmjs.org/@react-native-community/datetimepicker/-/datetimepicker-3.5.2.tgz",
|
||||
"integrity": "sha512-TWRuAtr/DnrEcRewqvXMLea2oB+YF+SbtuYLHguALLxNJQLl/RFB7aTNZeF+OoH75zKFqtXECXV1/uxQUpA+sg==",
|
||||
"requires": {
|
||||
"invariant": "^2.2.4"
|
||||
}
|
||||
},
|
||||
"@react-native-community/eslint-config": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@react-native-community/eslint-config/-/eslint-config-3.0.1.tgz",
|
||||
@@ -28226,6 +28292,12 @@
|
||||
"integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=",
|
||||
"dev": true
|
||||
},
|
||||
"@types/lodash": {
|
||||
"version": "4.14.175",
|
||||
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.175.tgz",
|
||||
"integrity": "sha512-XmdEOrKQ8a1Y/yxQFOMbC47G/V2VDO1GvMRnl4O75M4GW/abC5tnfzadQYkqEveqRM1dEJGFFegfPNA2vvx2iw==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/mime-db": {
|
||||
"version": "1.43.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/mime-db/-/mime-db-1.43.1.tgz",
|
||||
@@ -39670,6 +39742,32 @@
|
||||
"integrity": "sha512-InYHtQMt47FTvi8siiI9s/2hB0z3Dq+t9mCrCkzLSqbQvZyjMEErKWjK0cT8Lk9sjxQCTNFpuMAhT5AIc0yzfw==",
|
||||
"requires": {}
|
||||
},
|
||||
"react-native-paper": {
|
||||
"version": "4.9.2",
|
||||
"resolved": "https://registry.npmjs.org/react-native-paper/-/react-native-paper-4.9.2.tgz",
|
||||
"integrity": "sha512-J7FRsd0YblQawtuj9I46F//apZHadsCKk6jWpc6njFTYdgUeCdkR8KgEto7cp2WxbcGNELx7KGwPQ4zAgX746A==",
|
||||
"requires": {
|
||||
"@callstack/react-theme-provider": "^3.0.6",
|
||||
"color": "^3.1.2",
|
||||
"react-native-iphone-x-helper": "^1.3.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@callstack/react-theme-provider": {
|
||||
"version": "3.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@callstack/react-theme-provider/-/react-theme-provider-3.0.6.tgz",
|
||||
"integrity": "sha512-wwKMXfmklfogpalNZT0W+jh76BIquiYUiQHOaPmt/PCyCEP/E6rP+e7Uie6mBZrfkea9WJYJ+mus6r+45JAEhg==",
|
||||
"requires": {
|
||||
"deepmerge": "^3.2.0",
|
||||
"hoist-non-react-statics": "^3.3.0"
|
||||
}
|
||||
},
|
||||
"deepmerge": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-3.3.0.tgz",
|
||||
"integrity": "sha512-GRQOafGHwMHpjPx9iCvTgpu9NojZ49q794EEL94JVEw6VaeA8XTUyBKvAkOOjBX9oJNiV6G3P+T+tihFjo2TqA=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"react-native-permissions": {
|
||||
"version": "3.0.5",
|
||||
"resolved": "https://registry.npmjs.org/react-native-permissions/-/react-native-permissions-3.0.5.tgz",
|
||||
@@ -39721,6 +39819,11 @@
|
||||
"warn-once": "^0.1.0"
|
||||
}
|
||||
},
|
||||
"react-native-section-list-get-item-layout": {
|
||||
"version": "2.2.3",
|
||||
"resolved": "https://registry.npmjs.org/react-native-section-list-get-item-layout/-/react-native-section-list-get-item-layout-2.2.3.tgz",
|
||||
"integrity": "sha512-fzCW5SiYP6qCZyDHebaElHonIFr8NFrZK9JDkxFLnpxMJih4d+HQ4rHyOs0Z4Gb/FjyCVbRH7RtEnjeQ0XffMg=="
|
||||
},
|
||||
"react-native-share": {
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/react-native-share/-/react-native-share-7.1.1.tgz",
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
"@react-native-community/async-storage": "1.12.1",
|
||||
"@react-native-community/cameraroll": "4.0.4",
|
||||
"@react-native-community/clipboard": "1.5.1",
|
||||
"@react-native-community/datetimepicker": "3.5.2",
|
||||
"@react-native-community/masked-view": "0.1.11",
|
||||
"@react-native-community/netinfo": "6.0.2",
|
||||
"@react-native-cookies/cookies": "6.0.11",
|
||||
@@ -64,11 +65,13 @@
|
||||
"react-native-navigation": "7.21.0",
|
||||
"react-native-neomorph-shadows": "1.1.2",
|
||||
"react-native-notifications": "4.1.2",
|
||||
"react-native-paper": "4.9.2",
|
||||
"react-native-permissions": "3.0.5",
|
||||
"react-native-reanimated": "2.3.0-beta.2",
|
||||
"react-native-redash": "16.2.2",
|
||||
"react-native-safe-area-context": "3.3.2",
|
||||
"react-native-screens": "3.8.0",
|
||||
"react-native-section-list-get-item-layout": "2.2.3",
|
||||
"react-native-share": "7.1.1",
|
||||
"react-native-slider": "0.11.0",
|
||||
"react-native-svg": "12.1.1",
|
||||
@@ -101,6 +104,7 @@
|
||||
"@types/commonmark-react-renderer": "4.3.1",
|
||||
"@types/deep-equal": "1.0.1",
|
||||
"@types/jest": "27.0.2",
|
||||
"@types/lodash": "4.14.175",
|
||||
"@types/react": "17.0.26",
|
||||
"@types/react-intl": "3.0.0",
|
||||
"@types/react-native": "0.65.3",
|
||||
|
||||
11
types/api/preferences.d.ts
vendored
11
types/api/preferences.d.ts
vendored
@@ -1,13 +1,17 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
type ThemeKey = 'denim' | 'sapphire' | 'quartz' | 'indigo' | 'onyx';
|
||||
type LegacyThemeKey = 'default' | 'organization' | 'mattermostDark' | 'windows10';
|
||||
|
||||
type LegacyThemeType = 'Mattermost' | 'Organization' | 'Mattermost Dark' | 'Windows Dark';
|
||||
|
||||
type ThemeKey = 'denim' | 'sapphire' | 'quartz' | 'indigo' | 'onyx' | 'custom';
|
||||
|
||||
type ThemeType = 'Denim' | 'Sapphire' | 'Quartz' | 'Indigo' | 'Onyx' | 'custom';
|
||||
|
||||
type Theme = {
|
||||
[key: string]: string | undefined;
|
||||
type?: string;
|
||||
type?: ThemeType | LegacyThemeType;
|
||||
sidebarBg: string;
|
||||
sidebarText: string;
|
||||
sidebarUnreadText: string;
|
||||
@@ -16,6 +20,7 @@ type Theme = {
|
||||
sidebarTextActiveColor: string;
|
||||
sidebarHeaderBg: string;
|
||||
sidebarHeaderTextColor: string;
|
||||
sidebarTeamBarBg: string;
|
||||
onlineIndicator: string;
|
||||
awayIndicator: string;
|
||||
dndIndicator: string;
|
||||
@@ -32,3 +37,5 @@ type Theme = {
|
||||
mentionHighlightLink: string;
|
||||
codeTheme: string;
|
||||
};
|
||||
|
||||
type ThemeTypeMap = Record<ThemeType | LegacyThemeType, ThemeKey>;
|
||||
|
||||
10
types/api/users.d.ts
vendored
10
types/api/users.d.ts
vendored
@@ -90,13 +90,3 @@ type UserCustomStatus = {
|
||||
expires_at?: string;
|
||||
duration?: CustomStatusDuration;
|
||||
};
|
||||
|
||||
enum CustomStatusDuration {
|
||||
DONT_CLEAR = '',
|
||||
THIRTY_MINUTES = 'thirty_minutes',
|
||||
ONE_HOUR = 'one_hour',
|
||||
FOUR_HOURS = 'four_hours',
|
||||
TODAY = 'today',
|
||||
THIS_WEEK = 'this_week',
|
||||
DATE_AND_TIME = 'date_and_time',
|
||||
}
|
||||
|
||||
4
types/database/models/servers/user.d.ts
vendored
4
types/database/models/servers/user.d.ts
vendored
@@ -91,6 +91,6 @@ export default class UserModel extends Model {
|
||||
/** teams : All the team that this user is part of */
|
||||
teams: Query<TeamMembershipModel>;
|
||||
|
||||
/** prepareSatus: Prepare the model to update the user status in a batch operation */
|
||||
prepareSatus: (status: string) => void;
|
||||
/** prepareStatus: Prepare the model to update the user status in a batch operation */
|
||||
prepareStatus: (status: string) => void;
|
||||
}
|
||||
|
||||
4
types/modules/react-native-keyboard-tracking-view.d.ts
vendored
Normal file
4
types/modules/react-native-keyboard-tracking-view.d.ts
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
declare module 'react-native-keyboard-tracking-view'
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user