Compare commits
20 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
25563d1598 | ||
|
|
a84249a92f | ||
|
|
fc87d16578 | ||
|
|
3be3a15976 | ||
|
|
6d7370972d | ||
|
|
298666755c | ||
|
|
a736203f7c | ||
|
|
8468d81936 | ||
|
|
68aa016921 | ||
|
|
64581b8b2d | ||
|
|
dbf2eac879 | ||
|
|
fdac66adaf | ||
|
|
eb8d3655e1 | ||
|
|
c256c8f761 | ||
|
|
4cdf36c771 | ||
|
|
6aa27d18e3 | ||
|
|
2554cd5ae6 | ||
|
|
a4082402fe | ||
|
|
ad46cec886 | ||
|
|
d9e3bc9b6b |
100 changed files with 1807 additions and 614 deletions
|
|
@ -1,6 +1,6 @@
|
||||||
# Mattermost Mobile v2
|
# Mattermost Mobile v2
|
||||||
|
|
||||||
- **Minimum Server versions:** Current ESR version (9.5.0+)
|
- **Minimum Server versions:** Current ESR version (9.11.0+)
|
||||||
- **Supported iOS versions:** 13.4+
|
- **Supported iOS versions:** 13.4+
|
||||||
- **Supported Android versions:** 7.0+
|
- **Supported Android versions:** 7.0+
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -111,8 +111,8 @@ android {
|
||||||
applicationId "com.mattermost.rnbeta"
|
applicationId "com.mattermost.rnbeta"
|
||||||
minSdkVersion rootProject.ext.minSdkVersion
|
minSdkVersion rootProject.ext.minSdkVersion
|
||||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||||
versionCode 572
|
versionCode 582
|
||||||
versionName "2.22.0"
|
versionName "2.23.0"
|
||||||
testBuildType System.getProperty('testBuildType', 'debug')
|
testBuildType System.getProperty('testBuildType', 'debug')
|
||||||
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
|
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -160,7 +160,7 @@ export async function markPostAsDeleted(serverUrl: string, post: Post, prepareRe
|
||||||
p.message = '';
|
p.message = '';
|
||||||
p.messageSource = '';
|
p.messageSource = '';
|
||||||
p.metadata = null;
|
p.metadata = null;
|
||||||
p.props = undefined;
|
p.props = null;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!prepareRecordsOnly) {
|
if (!prepareRecordsOnly) {
|
||||||
|
|
|
||||||
|
|
@ -105,13 +105,13 @@ const mockClient = {
|
||||||
getUser: jest.fn((userId: string) => ({...user, id: userId})),
|
getUser: jest.fn((userId: string) => ({...user, id: userId})),
|
||||||
getMyChannels: jest.fn((teamId: string) => ([{id: channelId, name: 'channel1', creatorId: user.id, team_id: teamId}])),
|
getMyChannels: jest.fn((teamId: string) => ([{id: channelId, name: 'channel1', creatorId: user.id, team_id: teamId}])),
|
||||||
getMyChannelMembers: jest.fn(() => ([{id: user.id + '-' + channelId, user_id: user.id, channel_id: channelId, roles: ''}])),
|
getMyChannelMembers: jest.fn(() => ([{id: user.id + '-' + channelId, user_id: user.id, channel_id: channelId, roles: ''}])),
|
||||||
getCategories: jest.fn((userId: string, teamId: string) => ({categories: [{id: 'categoryid', channel_id: [channelId], team_id: teamId}], order: ['categoryid']})),
|
getCategories: jest.fn((userId: string, teamId: string) => ({categories: [{id: 'categoryid', channel_ids: [channelId], team_id: teamId}], order: ['categoryid']})),
|
||||||
viewMyChannel: jest.fn(),
|
viewMyChannel: jest.fn(),
|
||||||
getTeamByName: jest.fn((teamName: string) => ({id: teamId, name: teamName})),
|
getTeamByName: jest.fn((teamName: string) => ({id: teamId, name: teamName})),
|
||||||
getTeam: jest.fn((id: string) => ({id, name: 'teamname'})),
|
getTeam: jest.fn((id: string) => ({id, name: 'teamname'})),
|
||||||
addToTeam: jest.fn((teamId: string, userId: string) => ({id: userId + '-' + teamId, user_id: userId, team_id: teamId, roles: ''})),
|
addToTeam: jest.fn((teamId: string, userId: string) => ({id: userId + '-' + teamId, user_id: userId, team_id: teamId, roles: ''})),
|
||||||
getUserByUsername: jest.fn((username: string) => ({...user, id: 'userid2', username})),
|
getUserByUsername: jest.fn((username: string) => ({...user, id: 'userid2', username})),
|
||||||
createDirectChannel: jest.fn((userId1: string, userId2: string) => ({id: userId1 + '__' + userId2, team_id: '', type: 'D', display_name: 'displayname'})),
|
createDirectChannel: jest.fn((userIds: string[]) => ({id: userIds[0] + '__' + userIds[1], team_id: '', type: 'D', display_name: 'displayname'})),
|
||||||
getChannels: jest.fn((teamId: string) => ([{id: channelId, name: 'channel1', creatorId: user.id, team_id: teamId}])),
|
getChannels: jest.fn((teamId: string) => ([{id: channelId, name: 'channel1', creatorId: user.id, team_id: teamId}])),
|
||||||
getArchivedChannels: jest.fn((teamId: string) => ([{id: channelId + 'old', name: 'channel1old', creatorId: user.id, team_id: teamId, delete_at: 1}])),
|
getArchivedChannels: jest.fn((teamId: string) => ([{id: channelId + 'old', name: 'channel1old', creatorId: user.id, team_id: teamId, delete_at: 1}])),
|
||||||
createGroupChannel: jest.fn(() => ({id: 'groupid', team_id: '', type: 'G', display_name: 'displayname'})),
|
createGroupChannel: jest.fn(() => ({id: 'groupid', team_id: '', type: 'G', display_name: 'displayname'})),
|
||||||
|
|
@ -129,6 +129,11 @@ const mockClient = {
|
||||||
convertChannelToPrivate: jest.fn(),
|
convertChannelToPrivate: jest.fn(),
|
||||||
getGroupMessageMembersCommonTeams: jest.fn(() => ({id: teamId, name: 'teamname'})),
|
getGroupMessageMembersCommonTeams: jest.fn(() => ({id: teamId, name: 'teamname'})),
|
||||||
convertGroupMessageToPrivateChannel: jest.fn((channelId: string) => ({id: channelId, name: 'channel1', creatorId: user.id, type: 'P'})),
|
convertGroupMessageToPrivateChannel: jest.fn((channelId: string) => ({id: channelId, name: 'channel1', creatorId: user.id, type: 'P'})),
|
||||||
|
getPosts: jest.fn(() => ({
|
||||||
|
order: [],
|
||||||
|
posts: [],
|
||||||
|
previousPostId: '',
|
||||||
|
})),
|
||||||
};
|
};
|
||||||
|
|
||||||
const teamId = 'teamid1';
|
const teamId = 'teamid1';
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,7 @@ import type ChannelModel from '@typings/database/models/servers/channel';
|
||||||
import type {IntlShape} from 'react-intl';
|
import type {IntlShape} from 'react-intl';
|
||||||
|
|
||||||
export type MyChannelsRequest = {
|
export type MyChannelsRequest = {
|
||||||
|
teamId?: string;
|
||||||
categories?: CategoryWithChannels[];
|
categories?: CategoryWithChannels[];
|
||||||
channels?: Channel[];
|
channels?: Channel[];
|
||||||
memberships?: ChannelMembership[];
|
memberships?: ChannelMembership[];
|
||||||
|
|
@ -441,7 +442,7 @@ export async function fetchMyChannelsForTeam(serverUrl: string, teamId: string,
|
||||||
setTeamLoading(serverUrl, false);
|
setTeamLoading(serverUrl, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {channels, memberships, categories};
|
return {channels, memberships, categories, teamId};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logDebug('error on fetchMyChannelsForTeam', getFullErrorMessage(error));
|
logDebug('error on fetchMyChannelsForTeam', getFullErrorMessage(error));
|
||||||
if (!fetchOnly) {
|
if (!fetchOnly) {
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ import {fetchPostsForUnreadChannels} from '@actions/remote/post';
|
||||||
import {type MyPreferencesRequest, fetchMyPreferences} from '@actions/remote/preference';
|
import {type MyPreferencesRequest, fetchMyPreferences} from '@actions/remote/preference';
|
||||||
import {fetchRoles} from '@actions/remote/role';
|
import {fetchRoles} from '@actions/remote/role';
|
||||||
import {fetchConfigAndLicense, fetchDataRetentionPolicy} from '@actions/remote/systems';
|
import {fetchConfigAndLicense, fetchDataRetentionPolicy} from '@actions/remote/systems';
|
||||||
import {fetchMyTeams, fetchTeamsChannelsAndUnreadPosts, handleKickFromTeam, type MyTeamsRequest, updateCanJoinTeams} from '@actions/remote/team';
|
import {fetchMyTeams, fetchTeamsChannelsThreadsAndUnreadPosts, handleKickFromTeam, type MyTeamsRequest, updateCanJoinTeams} from '@actions/remote/team';
|
||||||
import {syncTeamThreads} from '@actions/remote/thread';
|
import {syncTeamThreads} from '@actions/remote/thread';
|
||||||
import {fetchMe, type MyUserRequest, updateAllUsersSince, autoUpdateTimezone} from '@actions/remote/user';
|
import {fetchMe, type MyUserRequest, updateAllUsersSince, autoUpdateTimezone} from '@actions/remote/user';
|
||||||
import {General, Preferences, Screens} from '@constants';
|
import {General, Preferences, Screens} from '@constants';
|
||||||
|
|
@ -66,9 +66,6 @@ export type EntryResponse = {
|
||||||
error: unknown;
|
error: unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
const FETCH_MISSING_DM_TIMEOUT = 2500;
|
|
||||||
export const FETCH_UNREADS_TIMEOUT = 2500;
|
|
||||||
|
|
||||||
export const entry = async (serverUrl: string, teamId?: string, channelId?: string, since = 0): Promise<EntryResponse> => {
|
export const entry = async (serverUrl: string, teamId?: string, channelId?: string, since = 0): Promise<EntryResponse> => {
|
||||||
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||||
const result = entryRest(serverUrl, teamId, channelId, since);
|
const result = entryRest(serverUrl, teamId, channelId, since);
|
||||||
|
|
@ -311,8 +308,7 @@ const fetchAlternateTeamData = async (
|
||||||
let initialTeamId = '';
|
let initialTeamId = '';
|
||||||
let chData;
|
let chData;
|
||||||
|
|
||||||
for (const teamId of availableTeamIds) {
|
for await (const teamId of availableTeamIds) {
|
||||||
// eslint-disable-next-line no-await-in-loop
|
|
||||||
chData = await fetchMyChannelsForTeam(serverUrl, teamId, includeDeleted, since, fetchOnly, false, isCRTEnabled);
|
chData = await fetchMyChannelsForTeam(serverUrl, teamId, includeDeleted, since, fetchOnly, false, isCRTEnabled);
|
||||||
const chError = chData.error;
|
const chError = chData.error;
|
||||||
if (isErrorWithStatusCode(chError) && chError.status_code === 403) {
|
if (isErrorWithStatusCode(chError) && chError.status_code === 403) {
|
||||||
|
|
@ -371,49 +367,64 @@ async function entryInitialChannelId(database: Database, requestedChannelId = ''
|
||||||
async function restDeferredAppEntryActions(
|
async function restDeferredAppEntryActions(
|
||||||
serverUrl: string, since: number, currentUserId: string, currentUserLocale: string, preferences: PreferenceType[] | undefined,
|
serverUrl: string, since: number, currentUserId: string, currentUserLocale: string, preferences: PreferenceType[] | undefined,
|
||||||
config: ClientConfig, license: ClientLicense | undefined, teamData: MyTeamsRequest, chData: MyChannelsRequest | undefined,
|
config: ClientConfig, license: ClientLicense | undefined, teamData: MyTeamsRequest, chData: MyChannelsRequest | undefined,
|
||||||
initialTeamId?: string, initialChannelId?: string) {
|
initialTeamId?: string, initialChannelId?: string,
|
||||||
setTimeout(async () => {
|
) {
|
||||||
if (chData?.channels?.length && chData.memberships?.length) {
|
const isCRTEnabled = (preferences && processIsCRTEnabled(preferences, config.CollapsedThreads, config.FeatureFlagCollapsedThreads, config.Version)) || false;
|
||||||
// defer fetching posts for unread channels on initial team
|
const directChannels = chData?.channels?.filter(isDMorGM);
|
||||||
fetchPostsForUnreadChannels(serverUrl, chData.channels, chData.memberships, initialChannelId);
|
const channelsToFetchProfiles = new Set<Channel>(directChannels);
|
||||||
}
|
|
||||||
}, FETCH_UNREADS_TIMEOUT);
|
// sidebar DM & GM profiles
|
||||||
|
if (channelsToFetchProfiles.size) {
|
||||||
|
const teammateDisplayNameSetting = getTeammateNameDisplaySetting(preferences || [], config.LockTeammateNameDisplay, config.TeammateNameDisplay, license);
|
||||||
|
fetchMissingDirectChannelsInfo(serverUrl, Array.from(channelsToFetchProfiles), currentUserLocale, teammateDisplayNameSetting, currentUserId);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateAllUsersSince(serverUrl, since);
|
||||||
|
updateCanJoinTeams(serverUrl);
|
||||||
|
|
||||||
// defer fetch channels and unread posts for other teams
|
// defer fetch channels and unread posts for other teams
|
||||||
if (teamData.teams?.length && teamData.memberships?.length) {
|
setTimeout(async () => {
|
||||||
fetchTeamsChannelsAndUnreadPosts(serverUrl, since, teamData.teams, teamData.memberships, initialTeamId);
|
if (chData?.channels?.length && chData.memberships?.length && initialTeamId) {
|
||||||
}
|
if (isCRTEnabled && initialTeamId) {
|
||||||
|
await syncTeamThreads(serverUrl, initialTeamId);
|
||||||
if (preferences && processIsCRTEnabled(preferences, config.CollapsedThreads, config.FeatureFlagCollapsedThreads, config.Version)) {
|
}
|
||||||
if (initialTeamId) {
|
fetchPostsForUnreadChannels(serverUrl, chData.channels, chData.memberships, initialChannelId);
|
||||||
await syncTeamThreads(serverUrl, initialTeamId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (teamData.teams?.length) {
|
if (teamData.teams?.length && teamData.memberships?.length) {
|
||||||
for await (const team of teamData.teams) {
|
const teamsOrder = preferences?.find((p) => p.category === Preferences.CATEGORIES.TEAMS_ORDER);
|
||||||
if (team.id !== initialTeamId) {
|
const sortedTeamIds = new Set(teamsOrder?.value.split(','));
|
||||||
// need to await here since GM/DM threads in different teams overlap
|
const membershipSet = new Set(teamData.memberships.map((m) => m.team_id));
|
||||||
await syncTeamThreads(serverUrl, team.id);
|
const teamMap = new Map(teamData.teams.map((t) => [t.id, t]));
|
||||||
}
|
if (initialTeamId) {
|
||||||
|
sortedTeamIds.delete(initialTeamId);
|
||||||
|
membershipSet.delete(initialTeamId);
|
||||||
|
teamMap.delete(initialTeamId);
|
||||||
|
}
|
||||||
|
|
||||||
|
let myTeams: Team[];
|
||||||
|
if (sortedTeamIds.size) {
|
||||||
|
const mySortedTeams = [...sortedTeamIds].
|
||||||
|
filter((id) => membershipSet.has(id) && teamMap.has(id)).
|
||||||
|
map((id) => teamMap.get(id)!);
|
||||||
|
const extraTeams = [...membershipSet].
|
||||||
|
filter((id) => !sortedTeamIds.has(id) && teamMap.get(id)).
|
||||||
|
map((id) => teamMap.get(id)!).
|
||||||
|
sort((a, b) => a.display_name.toLocaleLowerCase().localeCompare(b.display_name.toLocaleLowerCase()));
|
||||||
|
myTeams = [...mySortedTeams, ...extraTeams];
|
||||||
|
} else {
|
||||||
|
myTeams = teamData.teams.
|
||||||
|
sort((a, b) => a.display_name.toLocaleLowerCase().localeCompare(b.display_name.toLocaleLowerCase()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (myTeams.length) {
|
||||||
|
fetchTeamsChannelsThreadsAndUnreadPosts(serverUrl, since, myTeams, isCRTEnabled);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
|
|
||||||
updateCanJoinTeams(serverUrl);
|
|
||||||
await updateAllUsersSince(serverUrl, since);
|
|
||||||
|
|
||||||
// Fetch groups for current user
|
// Fetch groups for current user
|
||||||
fetchGroupsForMember(serverUrl, currentUserId);
|
fetchGroupsForMember(serverUrl, currentUserId);
|
||||||
|
|
||||||
// defer sidebar DM & GM profiles
|
|
||||||
setTimeout(async () => {
|
|
||||||
const directChannels = chData?.channels?.filter(isDMorGM);
|
|
||||||
const channelsToFetchProfiles = new Set<Channel>(directChannels);
|
|
||||||
if (channelsToFetchProfiles.size) {
|
|
||||||
const teammateDisplayNameSetting = getTeammateNameDisplaySetting(preferences || [], config.LockTeammateNameDisplay, config.TeammateNameDisplay, license);
|
|
||||||
fetchMissingDirectChannelsInfo(serverUrl, Array.from(channelsToFetchProfiles), currentUserLocale, teammateDisplayNameSetting, currentUserId);
|
|
||||||
}
|
|
||||||
}, FETCH_MISSING_DM_TIMEOUT);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const setExtraSessionProps = async (serverUrl: string) => {
|
export const setExtraSessionProps = async (serverUrl: string) => {
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,8 @@
|
||||||
|
|
||||||
/* eslint-disable max-lines */
|
/* eslint-disable max-lines */
|
||||||
|
|
||||||
|
import {chunk} from 'lodash';
|
||||||
|
|
||||||
import {markChannelAsUnread, updateLastPostAt} from '@actions/local/channel';
|
import {markChannelAsUnread, updateLastPostAt} from '@actions/local/channel';
|
||||||
import {addPostAcknowledgement, removePost, removePostAcknowledgement, storePostsForChannel} from '@actions/local/post';
|
import {addPostAcknowledgement, removePost, removePostAcknowledgement, storePostsForChannel} from '@actions/local/post';
|
||||||
import {addRecentReaction} from '@actions/local/reactions';
|
import {addRecentReaction} from '@actions/local/reactions';
|
||||||
|
|
@ -40,6 +42,13 @@ type PostsRequest = {
|
||||||
previousPostId?: string;
|
previousPostId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type PostsForChannel = PostsRequest & {
|
||||||
|
actionType?: string;
|
||||||
|
authors?: UserProfile[];
|
||||||
|
channelId?: string;
|
||||||
|
error?: unknown;
|
||||||
|
};
|
||||||
|
|
||||||
type PostsObjectsRequest = {
|
type PostsObjectsRequest = {
|
||||||
error?: unknown;
|
error?: unknown;
|
||||||
order?: string[];
|
order?: string[];
|
||||||
|
|
@ -71,7 +80,7 @@ export async function createPost(serverUrl: string, post: Partial<Post>, files:
|
||||||
const pendingPostId = post.pending_post_id || `${currentUserId}:${timestamp}`;
|
const pendingPostId = post.pending_post_id || `${currentUserId}:${timestamp}`;
|
||||||
|
|
||||||
const existing = await getPostById(database, pendingPostId);
|
const existing = await getPostById(database, pendingPostId);
|
||||||
if (existing && !existing.props.failed) {
|
if (existing && !existing.props?.failed) {
|
||||||
return {data: false};
|
return {data: false};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -231,7 +240,7 @@ export const retryFailedPost = async (serverUrl: string, post: PostModel) => {
|
||||||
// timestamps will remain the same as the initial attempt for createAt
|
// timestamps will remain the same as the initial attempt for createAt
|
||||||
// but updateAt will be use for the optimistic post UI
|
// but updateAt will be use for the optimistic post UI
|
||||||
post.prepareUpdate((p) => {
|
post.prepareUpdate((p) => {
|
||||||
p.props = newPost.props;
|
p.props = newPost.props || null;
|
||||||
p.updateAt = timestamp;
|
p.updateAt = timestamp;
|
||||||
});
|
});
|
||||||
await operator.batchRecords([post], 'retryFailedPost - first update');
|
await operator.batchRecords([post], 'retryFailedPost - first update');
|
||||||
|
|
@ -275,7 +284,7 @@ export const retryFailedPost = async (serverUrl: string, post: PostModel) => {
|
||||||
return {};
|
return {};
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function fetchPostsForChannel(serverUrl: string, channelId: string, fetchOnly = false) {
|
export async function fetchPostsForChannel(serverUrl: string, channelId: string, fetchOnly = false): Promise<PostsForChannel> {
|
||||||
try {
|
try {
|
||||||
if (!fetchOnly) {
|
if (!fetchOnly) {
|
||||||
EphemeralStore.addLoadingMessagesForChannel(serverUrl, channelId);
|
EphemeralStore.addLoadingMessagesForChannel(serverUrl, channelId);
|
||||||
|
|
@ -312,7 +321,7 @@ export async function fetchPostsForChannel(serverUrl: string, channelId: string,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {posts: data.posts, order: data.order, authors, actionType, previousPostId: data.previousPostId};
|
return {posts: data.posts, order: data.order, authors, actionType, previousPostId: data.previousPostId, channelId};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logDebug('error on fetchPostsForChannel', getFullErrorMessage(error));
|
logDebug('error on fetchPostsForChannel', getFullErrorMessage(error));
|
||||||
return {error};
|
return {error};
|
||||||
|
|
@ -323,15 +332,34 @@ export async function fetchPostsForChannel(serverUrl: string, channelId: string,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const fetchPostsForUnreadChannels = async (serverUrl: string, channels: Channel[], memberships: ChannelMembership[], excludeChannelId?: string) => {
|
export const fetchPostsForUnreadChannels = async (serverUrl: string, channels: Channel[], memberships: ChannelMembership[], excludeChannelId?: string, fetchOnly = false): Promise<PostsForChannel[]> => {
|
||||||
const promises = [];
|
const membersMap = new Map<string, ChannelMembership>();
|
||||||
for (const member of memberships) {
|
for (const member of memberships) {
|
||||||
const channel = channels.find((c) => c.id === member.channel_id);
|
membersMap.set(member.channel_id, member);
|
||||||
if (channel && !channel.delete_at && (channel.total_msg_count - member.msg_count) > 0 && channel.id !== excludeChannelId) {
|
|
||||||
promises.push(fetchPostsForChannel(serverUrl, channel.id));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return Promise.all(promises);
|
|
||||||
|
const unreadChannelIds = channels.reduce<string[]>((result, channel) => {
|
||||||
|
const member = membersMap.get(channel.id);
|
||||||
|
if (member && !channel.delete_at && (channel.total_msg_count - member.msg_count) > 0 && channel.id !== excludeChannelId) {
|
||||||
|
result.push(channel.id);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const postsForChannel: PostsForChannel[] = [];
|
||||||
|
|
||||||
|
// process 10 unread channels at a time
|
||||||
|
const chunks = chunk(unreadChannelIds, 10);
|
||||||
|
for await (const channelIds of chunks) {
|
||||||
|
const promises = [];
|
||||||
|
for (const channelId of channelIds) {
|
||||||
|
promises.push(fetchPostsForChannel(serverUrl, channelId, fetchOnly));
|
||||||
|
}
|
||||||
|
|
||||||
|
const results = await Promise.all(promises);
|
||||||
|
postsForChannel.push(...results);
|
||||||
|
}
|
||||||
|
return postsForChannel;
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function fetchPosts(serverUrl: string, channelId: string, page = 0, perPage = General.POST_CHUNK_SIZE, fetchOnly = false): Promise<PostsRequest> {
|
export async function fetchPosts(serverUrl: string, channelId: string, page = 0, perPage = General.POST_CHUNK_SIZE, fetchOnly = false): Promise<PostsRequest> {
|
||||||
|
|
|
||||||
|
|
@ -161,7 +161,7 @@ export const searchFiles = async (serverUrl: string, teamId: string, params: Fil
|
||||||
}, {});
|
}, {});
|
||||||
files.forEach((f) => {
|
files.forEach((f) => {
|
||||||
if (f.post_id) {
|
if (f.post_id) {
|
||||||
f.postProps = idToPost[f.post_id]?.props;
|
f.postProps = idToPost[f.post_id]?.props || {};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return {files, channels};
|
return {files, channels};
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,6 @@ import {
|
||||||
fetchAllTeams,
|
fetchAllTeams,
|
||||||
fetchTeamsForComponent,
|
fetchTeamsForComponent,
|
||||||
updateCanJoinTeams,
|
updateCanJoinTeams,
|
||||||
fetchTeamsChannelsAndUnreadPosts,
|
|
||||||
fetchTeamByName,
|
fetchTeamByName,
|
||||||
removeCurrentUserFromTeam,
|
removeCurrentUserFromTeam,
|
||||||
removeUserFromTeam,
|
removeUserFromTeam,
|
||||||
|
|
@ -26,6 +25,7 @@ import {
|
||||||
handleKickFromTeam,
|
handleKickFromTeam,
|
||||||
getTeamMembersByIds,
|
getTeamMembersByIds,
|
||||||
buildTeamIconUrl,
|
buildTeamIconUrl,
|
||||||
|
fetchTeamsChannelsThreadsAndUnreadPosts,
|
||||||
} from './team';
|
} from './team';
|
||||||
|
|
||||||
import type ServerDataOperator from '@database/operator/server_data_operator';
|
import type ServerDataOperator from '@database/operator/server_data_operator';
|
||||||
|
|
@ -81,12 +81,38 @@ const mockClient = {
|
||||||
getMyTeams: jest.fn(() => ([{id: teamId, name: 'team1'}])),
|
getMyTeams: jest.fn(() => ([{id: teamId, name: 'team1'}])),
|
||||||
getMyTeamMembers: jest.fn(() => ([{id: 'userid1-' + teamId, user_id: 'userid1', team_id: teamId, roles: ''}])),
|
getMyTeamMembers: jest.fn(() => ([{id: 'userid1-' + teamId, user_id: 'userid1', team_id: teamId, roles: ''}])),
|
||||||
getTeams: jest.fn(() => ([{id: teamId, name: 'team1'}])),
|
getTeams: jest.fn(() => ([{id: teamId, name: 'team1'}])),
|
||||||
getMyChannels: jest.fn((id: string) => ([{id: channelId, name: 'channel1', creatorId: user.id, team_id: id}])),
|
getMyChannels: jest.fn((id: string) => ([{id: channelId, name: 'channel1', creatorId: user.id, team_id: id, total_msg_count: 1}])),
|
||||||
getMyChannelMembers: jest.fn(() => ([{id: user.id + '-' + channelId, user_id: user.id, channel_id: channelId, roles: ''}])),
|
getMyChannelMembers: jest.fn(() => ([{id: user.id + '-' + channelId, user_id: user.id, channel_id: channelId, roles: '', msg_count: 0}])),
|
||||||
getCategories: jest.fn((userId: string, id: string) => ({categories: [{id: 'categoryid', channel_id: [channelId], team_id: id}], order: ['categoryid']})),
|
getCategories: jest.fn((userId: string, id: string) => ({categories: [{id: 'categoryid', channel_ids: [channelId], team_id: id}], order: ['categoryid']})),
|
||||||
getTeamByName: jest.fn((name: string) => ({id: teamId, name})),
|
getTeamByName: jest.fn((name: string) => ({id: teamId, name})),
|
||||||
removeFromTeam: jest.fn(),
|
removeFromTeam: jest.fn(),
|
||||||
getTeamMembersByIds: jest.fn((id: string, userIds: string[]) => (userIds.map((userId) => ({id: userId + '-' + id, user_id: userId, team_id: id, roles: ''})))),
|
getTeamMembersByIds: jest.fn((id: string, userIds: string[]) => (userIds.map((userId) => ({id: userId + '-' + id, user_id: userId, team_id: id, roles: ''})))),
|
||||||
|
getPosts: jest.fn(() => ({
|
||||||
|
order: ['yocj9xgkh78exk1uhx9yny1zxy', 'ad6yoisgh385fy7rkph49zpfqa', 'o5qqa4ntdigp7rbnf75f8hgeaw'],
|
||||||
|
posts: [{
|
||||||
|
channel_id: channelId,
|
||||||
|
create_at: 1726107604522,
|
||||||
|
delete_at: 0,
|
||||||
|
edit_at: 0,
|
||||||
|
hashtags: '',
|
||||||
|
id: 'yocj9xgkh78exk1uhx9yny1zxy',
|
||||||
|
is_pinned: false,
|
||||||
|
last_reply_at: 0,
|
||||||
|
message: 'Message ead863',
|
||||||
|
metadata: {},
|
||||||
|
original_id: '',
|
||||||
|
participants: null,
|
||||||
|
pending_post_id: '',
|
||||||
|
props: {},
|
||||||
|
remote_id: '',
|
||||||
|
reply_count: 0,
|
||||||
|
root_id: '',
|
||||||
|
type: '',
|
||||||
|
update_at: 1726107604522,
|
||||||
|
user_id: 'k479wsypafgjddz8cerqx4m1ha',
|
||||||
|
}],
|
||||||
|
previousPostId: '',
|
||||||
|
})),
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
|
|
@ -245,16 +271,25 @@ describe('teams', () => {
|
||||||
expect(result).toBeDefined();
|
expect(result).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('fetchTeamsChannelsAndUnreadPosts - handle not found database', async () => {
|
it('fetchTeamsChannelsThreadsAndUnreadPosts - handle not found database', async () => {
|
||||||
const result = await fetchTeamsChannelsAndUnreadPosts('foo', 0, [], []) as {error: unknown};
|
const result = await fetchTeamsChannelsThreadsAndUnreadPosts('foo', 0, []) as {error: unknown};
|
||||||
expect(result?.error).toBeDefined();
|
expect(result?.error).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('fetchTeamsChannelsAndUnreadPosts - base case', async () => {
|
it('fetchTeamsChannelsThreadsAndUnreadPosts - base case', async () => {
|
||||||
const result = await fetchTeamsChannelsAndUnreadPosts(serverUrl, 0, [team], [{team_id: teamId, user_id: 'userid1'} as TeamMembership]);
|
const result = await fetchTeamsChannelsThreadsAndUnreadPosts(
|
||||||
|
serverUrl, 0,
|
||||||
|
[team], true, false);
|
||||||
expect(result).toBeDefined();
|
expect(result).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('fetchTeamsChannelsThreadsAndUnreadPosts - fetch only case', async () => {
|
||||||
|
const result = await fetchTeamsChannelsThreadsAndUnreadPosts(
|
||||||
|
serverUrl, 0,
|
||||||
|
[team], true, true);
|
||||||
|
expect(result.models).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
it('fetchTeamByName - handle not found database', async () => {
|
it('fetchTeamByName - handle not found database', async () => {
|
||||||
const result = await fetchTeamByName('foo', '') as {error: unknown};
|
const result = await fetchTeamByName('foo', '') as {error: unknown};
|
||||||
expect(result?.error).toBeDefined();
|
expect(result?.error).toBeDefined();
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,17 @@
|
||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import {chunk} from 'lodash';
|
||||||
import {DeviceEventEmitter} from 'react-native';
|
import {DeviceEventEmitter} from 'react-native';
|
||||||
|
|
||||||
|
import {storeCategories} from '@actions/local/category';
|
||||||
|
import {storeMyChannelsForTeam} from '@actions/local/channel';
|
||||||
|
import {storePostsForChannel} from '@actions/local/post';
|
||||||
import {removeUserFromTeam as localRemoveUserFromTeam} from '@actions/local/team';
|
import {removeUserFromTeam as localRemoveUserFromTeam} from '@actions/local/team';
|
||||||
import {PER_PAGE_DEFAULT} from '@client/rest/constants';
|
import {PER_PAGE_DEFAULT} from '@client/rest/constants';
|
||||||
import {Events} from '@constants';
|
import {Events} from '@constants';
|
||||||
import DatabaseManager from '@database/manager';
|
import DatabaseManager from '@database/manager';
|
||||||
|
import {removeDuplicatesModels} from '@helpers/database';
|
||||||
import NetworkManager from '@managers/network_manager';
|
import NetworkManager from '@managers/network_manager';
|
||||||
import {getActiveServerUrl} from '@queries/app/servers';
|
import {getActiveServerUrl} from '@queries/app/servers';
|
||||||
import {prepareCategoriesAndCategoriesChannels} from '@queries/servers/categories';
|
import {prepareCategoriesAndCategoriesChannels} from '@queries/servers/categories';
|
||||||
|
|
@ -22,9 +27,10 @@ import {logDebug} from '@utils/log';
|
||||||
|
|
||||||
import {fetchMyChannelsForTeam, switchToChannelById} from './channel';
|
import {fetchMyChannelsForTeam, switchToChannelById} from './channel';
|
||||||
import {fetchGroupsForTeamIfConstrained} from './groups';
|
import {fetchGroupsForTeamIfConstrained} from './groups';
|
||||||
import {fetchPostsForChannel, fetchPostsForUnreadChannels} from './post';
|
import {fetchPostsForChannel, fetchPostsForUnreadChannels, type PostsForChannel} from './post';
|
||||||
import {fetchRolesIfNeeded} from './role';
|
import {fetchRolesIfNeeded} from './role';
|
||||||
import {forceLogoutIfNecessary} from './session';
|
import {forceLogoutIfNecessary} from './session';
|
||||||
|
import {syncThreadsIfNeeded} from './thread';
|
||||||
|
|
||||||
import type {Client} from '@client/rest';
|
import type {Client} from '@client/rest';
|
||||||
import type {Model} from '@nozbe/watermelondb';
|
import type {Model} from '@nozbe/watermelondb';
|
||||||
|
|
@ -312,24 +318,85 @@ export const updateCanJoinTeams = async (serverUrl: string) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const fetchTeamsChannelsAndUnreadPosts = async (serverUrl: string, since: number, teams: Team[], memberships: TeamMembership[], excludeTeamId?: string) => {
|
export const fetchTeamsChannelsThreadsAndUnreadPosts = async (
|
||||||
const database = DatabaseManager.serverDatabases[serverUrl]?.database;
|
serverUrl: string, since: number, teams: Team[],
|
||||||
if (!database) {
|
isCRTEnabled?: boolean, fetchOnly = false,
|
||||||
return {error: `${serverUrl} database not found`};
|
) => {
|
||||||
}
|
try {
|
||||||
|
const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||||
|
const result: Model[] = [];
|
||||||
|
|
||||||
const membershipSet = new Set(memberships.map((m) => m.team_id));
|
// process up to 15 teams at a time
|
||||||
const myTeams = teams.filter((t) => membershipSet.has(t.id) && t.id !== excludeTeamId);
|
const chunks = chunk(teams, 15);
|
||||||
|
for await (const myTeams of chunks) {
|
||||||
|
const promises = [];
|
||||||
|
for (const team of myTeams) {
|
||||||
|
promises.push(fetchMyChannelsForTeam(serverUrl, team.id, true, since, true, true, isCRTEnabled));
|
||||||
|
}
|
||||||
|
|
||||||
for await (const team of myTeams) {
|
const results = await Promise.all(promises);
|
||||||
const {channels, memberships: members} = await fetchMyChannelsForTeam(serverUrl, team.id, true, since, false, true);
|
const models: Model[] = [];
|
||||||
|
const channels: Channel[] = [];
|
||||||
|
const members: ChannelMembership[] = [];
|
||||||
|
|
||||||
if (channels?.length && members?.length) {
|
for await (const r of results) {
|
||||||
fetchPostsForUnreadChannels(serverUrl, channels, members);
|
if (!r.error && r.teamId && r.categories && r.channels && r.memberships) {
|
||||||
|
const {models: chModels} = await storeMyChannelsForTeam(serverUrl, r.teamId, r.channels, r.memberships, true, isCRTEnabled);
|
||||||
|
const {models: catModels} = await storeCategories(serverUrl, r.categories, true, true); // Re-sync
|
||||||
|
if (chModels?.length) {
|
||||||
|
models.push(...chModels);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (catModels?.length) {
|
||||||
|
models.push(...catModels);
|
||||||
|
}
|
||||||
|
|
||||||
|
channels.push(...r.channels);
|
||||||
|
members.push(...r.memberships);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const unreadPromise = fetchPostsForUnreadChannels(serverUrl, channels, members, undefined, true);
|
||||||
|
const threadsPromise = syncThreadsIfNeeded(serverUrl, isCRTEnabled ?? false, myTeams, true);
|
||||||
|
const postPromises: [Promise<PostsForChannel[]>, Promise<{models?: Model[]; error?: unknown}>] = [
|
||||||
|
unreadPromise,
|
||||||
|
threadsPromise,
|
||||||
|
];
|
||||||
|
|
||||||
|
const [unreadPosts, threads] = await Promise.all(postPromises);
|
||||||
|
|
||||||
|
for await (const data of unreadPosts) {
|
||||||
|
if (data.channelId && data.posts?.length && data.order?.length && data.actionType) {
|
||||||
|
const {models: prepared} = await storePostsForChannel(
|
||||||
|
serverUrl, data.channelId,
|
||||||
|
data.posts, data.order, data.previousPostId ?? '',
|
||||||
|
data.actionType, data.authors || [], true,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (prepared?.length) {
|
||||||
|
models.push(...prepared);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (threads.models) {
|
||||||
|
models.push(...threads.models);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!fetchOnly && models.length) {
|
||||||
|
await operator.batchRecords(removeDuplicatesModels(models), 'fetchTeamsChannelsThreadsAndUnreadPosts');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (models.length) {
|
||||||
|
result.push(...models);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return {};
|
return {error: undefined, models: result};
|
||||||
|
} catch (error) {
|
||||||
|
logDebug('error on fetchTeamsChannelsThreadsAndUnreadPosts', getFullErrorMessage(error));
|
||||||
|
return {error};
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function fetchTeamByName(serverUrl: string, teamName: string, fetchOnly = false) {
|
export async function fetchTeamByName(serverUrl: string, teamName: string, fetchOnly = false) {
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ import {
|
||||||
syncTeamThreads,
|
syncTeamThreads,
|
||||||
loadEarlierThreads,
|
loadEarlierThreads,
|
||||||
fetchAndSwitchToThread,
|
fetchAndSwitchToThread,
|
||||||
|
syncThreadsIfNeeded,
|
||||||
} from './thread';
|
} from './thread';
|
||||||
|
|
||||||
import type ServerDataOperator from '@database/operator/server_data_operator';
|
import type ServerDataOperator from '@database/operator/server_data_operator';
|
||||||
|
|
@ -150,7 +151,7 @@ describe('get threads', () => {
|
||||||
expect(result).toBeDefined();
|
expect(result).toBeDefined();
|
||||||
expect(result.error).toBeFalsy();
|
expect(result.error).toBeFalsy();
|
||||||
expect(result.models).toBeDefined();
|
expect(result.models).toBeDefined();
|
||||||
expect(result.models?.length).toBe(5); // 1 thread, 1 thread participant, 1 read thread in team, 1 unread thread in team, 1 team thread sync
|
expect(result.models?.length).toBe(4); // 1 thread, 1 thread participant, 1 latest thread in team, 1 team thread sync (the unread thread is actually the latest so it the duplicate is removed)
|
||||||
|
|
||||||
// Sync again after first sync
|
// Sync again after first sync
|
||||||
const result2 = await syncTeamThreads(serverUrl, team.id);
|
const result2 = await syncTeamThreads(serverUrl, team.id);
|
||||||
|
|
@ -160,6 +161,23 @@ describe('get threads', () => {
|
||||||
expect(result2.models?.length).toBe(1); // 1 team thread sync
|
expect(result2.models?.length).toBe(1); // 1 team thread sync
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('syncThreadsIfNeeded - handle error', async () => {
|
||||||
|
const result = await syncThreadsIfNeeded('foo', true, []);
|
||||||
|
expect(result).toBeDefined();
|
||||||
|
expect(result.error).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('syncThreadsIfNeeded - base case', async () => {
|
||||||
|
await operator.handleSystem({systems: [{id: SYSTEM_IDENTIFIERS.CURRENT_USER_ID, value: user1.id}], prepareRecordsOnly: false});
|
||||||
|
await operator.handleUsers({users: [user1], prepareRecordsOnly: false});
|
||||||
|
|
||||||
|
const result = await syncThreadsIfNeeded(serverUrl, true, [team]);
|
||||||
|
expect(result).toBeDefined();
|
||||||
|
expect(result.error).toBeFalsy();
|
||||||
|
expect(result.models).toBeDefined();
|
||||||
|
expect(result.models?.length).toBe(4); // 1 thread, 1 thread participant, 1 latest thread in team, 1 team thread sync (the unread thread is actually the latest so it the duplicate is removed)
|
||||||
|
});
|
||||||
|
|
||||||
it('loadEarlierThreads - handle error', async () => {
|
it('loadEarlierThreads - handle error', async () => {
|
||||||
const result = await loadEarlierThreads('foo', '', '');
|
const result = await loadEarlierThreads('foo', '', '');
|
||||||
expect(result).toBeDefined();
|
expect(result).toBeDefined();
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import {markTeamThreadsAsRead, markThreadAsViewed, processReceivedThreads, switc
|
||||||
import {fetchPostThread} from '@actions/remote/post';
|
import {fetchPostThread} from '@actions/remote/post';
|
||||||
import {General} from '@constants';
|
import {General} from '@constants';
|
||||||
import DatabaseManager from '@database/manager';
|
import DatabaseManager from '@database/manager';
|
||||||
|
import {removeDuplicatesModels} from '@helpers/database';
|
||||||
import PushNotifications from '@init/push_notifications';
|
import PushNotifications from '@init/push_notifications';
|
||||||
import AppsManager from '@managers/apps_manager';
|
import AppsManager from '@managers/apps_manager';
|
||||||
import NetworkManager from '@managers/network_manager';
|
import NetworkManager from '@managers/network_manager';
|
||||||
|
|
@ -13,7 +14,7 @@ import {getConfigValue, getCurrentChannelId, getCurrentTeamId} from '@queries/se
|
||||||
import {getIsCRTEnabled, getThreadById, getTeamThreadsSyncData} from '@queries/servers/thread';
|
import {getIsCRTEnabled, getThreadById, getTeamThreadsSyncData} from '@queries/servers/thread';
|
||||||
import {getCurrentUser} from '@queries/servers/user';
|
import {getCurrentUser} from '@queries/servers/user';
|
||||||
import {getFullErrorMessage} from '@utils/errors';
|
import {getFullErrorMessage} from '@utils/errors';
|
||||||
import {logDebug} from '@utils/log';
|
import {logDebug, logError} from '@utils/log';
|
||||||
import {showThreadFollowingSnackbar} from '@utils/snack_bar';
|
import {showThreadFollowingSnackbar} from '@utils/snack_bar';
|
||||||
import {getThreadsListEdges} from '@utils/thread';
|
import {getThreadsListEdges} from '@utils/thread';
|
||||||
|
|
||||||
|
|
@ -30,6 +31,7 @@ type FetchThreadsOptions = {
|
||||||
unread?: boolean;
|
unread?: boolean;
|
||||||
since?: number;
|
since?: number;
|
||||||
totalsOnly?: boolean;
|
totalsOnly?: boolean;
|
||||||
|
excludeDirect?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
enum Direction {
|
enum Direction {
|
||||||
|
|
@ -238,10 +240,10 @@ export const fetchThreads = async (
|
||||||
|
|
||||||
let currentPage = 0;
|
let currentPage = 0;
|
||||||
const fetchThreadsFunc = async (opts: FetchThreadsOptions) => {
|
const fetchThreadsFunc = async (opts: FetchThreadsOptions) => {
|
||||||
const {before, after, perPage = General.CRT_CHUNK_SIZE, deleted, unread, since} = opts;
|
const {before, after, perPage = General.CRT_CHUNK_SIZE, deleted, unread, since, excludeDirect = false} = opts;
|
||||||
|
|
||||||
currentPage++;
|
currentPage++;
|
||||||
const {threads} = await client.getThreads(currentUser.id, teamId, before, after, perPage, deleted, unread, since, false, version);
|
const {threads} = await client.getThreads(currentUser.id, teamId, before, after, perPage, deleted, unread, since, false, version, excludeDirect);
|
||||||
if (threads.length) {
|
if (threads.length) {
|
||||||
// Mark all fetched threads as following
|
// Mark all fetched threads as following
|
||||||
for (const thread of threads) {
|
for (const thread of threads) {
|
||||||
|
|
@ -277,7 +279,45 @@ export const fetchThreads = async (
|
||||||
return {error: false, threads: threadsData};
|
return {error: false, threads: threadsData};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const syncTeamThreads = async (serverUrl: string, teamId: string, prepareRecordsOnly = false) => {
|
export const syncThreadsIfNeeded = async (serverUrl: string, isCRTEnabled: boolean, teams?: Team[], fetchOnly = false) => {
|
||||||
|
try {
|
||||||
|
if (!isCRTEnabled) {
|
||||||
|
return {models: []};
|
||||||
|
}
|
||||||
|
|
||||||
|
const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||||
|
const promises = [];
|
||||||
|
const models: Model[][] = [];
|
||||||
|
|
||||||
|
if (teams?.length) {
|
||||||
|
for (const team of teams) {
|
||||||
|
promises.push(syncTeamThreads(serverUrl, team.id, true, true));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (promises.length) {
|
||||||
|
const results = await Promise.all(promises);
|
||||||
|
for (const r of results) {
|
||||||
|
if (r.models?.length) {
|
||||||
|
models.push(r.models);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const flat = models.flat();
|
||||||
|
if (!fetchOnly && flat.length) {
|
||||||
|
const uniqueArray = removeDuplicatesModels(flat);
|
||||||
|
await operator.batchRecords(uniqueArray, 'syncThreadsIfNeeded');
|
||||||
|
}
|
||||||
|
|
||||||
|
return {models: flat};
|
||||||
|
} catch (error) {
|
||||||
|
logError('syncThreadsIfNeeded: Error', error);
|
||||||
|
return {error, models: undefined};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const syncTeamThreads = async (serverUrl: string, teamId: string, excludeDirect = false, fetchOnly = false) => {
|
||||||
try {
|
try {
|
||||||
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||||
const syncData = await getTeamThreadsSyncData(database, teamId);
|
const syncData = await getTeamThreadsSyncData(database, teamId);
|
||||||
|
|
@ -299,13 +339,13 @@ export const syncTeamThreads = async (serverUrl: string, teamId: string, prepare
|
||||||
fetchThreads(
|
fetchThreads(
|
||||||
serverUrl,
|
serverUrl,
|
||||||
teamId,
|
teamId,
|
||||||
{unread: true},
|
{unread: true, excludeDirect},
|
||||||
Direction.Down,
|
Direction.Down,
|
||||||
),
|
),
|
||||||
fetchThreads(
|
fetchThreads(
|
||||||
serverUrl,
|
serverUrl,
|
||||||
teamId,
|
teamId,
|
||||||
{},
|
{excludeDirect},
|
||||||
undefined,
|
undefined,
|
||||||
1,
|
1,
|
||||||
),
|
),
|
||||||
|
|
@ -313,6 +353,9 @@ export const syncTeamThreads = async (serverUrl: string, teamId: string, prepare
|
||||||
if (allUnreadThreads.error || latestThreads.error) {
|
if (allUnreadThreads.error || latestThreads.error) {
|
||||||
return {error: allUnreadThreads.error || latestThreads.error};
|
return {error: allUnreadThreads.error || latestThreads.error};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const dedupe = new Set(latestThreads.threads?.map((t) => t.id));
|
||||||
|
|
||||||
if (latestThreads.threads?.length) {
|
if (latestThreads.threads?.length) {
|
||||||
// We are fetching the threads for the first time. We get "latest" and "earliest" values.
|
// We are fetching the threads for the first time. We get "latest" and "earliest" values.
|
||||||
const {earliestThread, latestThread} = getThreadsListEdges(latestThreads.threads);
|
const {earliestThread, latestThread} = getThreadsListEdges(latestThreads.threads);
|
||||||
|
|
@ -322,13 +365,14 @@ export const syncTeamThreads = async (serverUrl: string, teamId: string, prepare
|
||||||
threads.push(...latestThreads.threads);
|
threads.push(...latestThreads.threads);
|
||||||
}
|
}
|
||||||
if (allUnreadThreads.threads?.length) {
|
if (allUnreadThreads.threads?.length) {
|
||||||
threads.push(...allUnreadThreads.threads);
|
const unread = allUnreadThreads.threads.filter((u) => !dedupe.has(u.id));
|
||||||
|
threads.push(...unread);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const allNewThreads = await fetchThreads(
|
const allNewThreads = await fetchThreads(
|
||||||
serverUrl,
|
serverUrl,
|
||||||
teamId,
|
teamId,
|
||||||
{deleted: true, since: syncData.latest + 1},
|
{deleted: true, since: syncData.latest + 1, excludeDirect},
|
||||||
);
|
);
|
||||||
if (allNewThreads.error) {
|
if (allNewThreads.error) {
|
||||||
return {error: allNewThreads.error};
|
return {error: allNewThreads.error};
|
||||||
|
|
@ -361,7 +405,7 @@ export const syncTeamThreads = async (serverUrl: string, teamId: string, prepare
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!prepareRecordsOnly && models?.length) {
|
if (!fetchOnly && models?.length) {
|
||||||
try {
|
try {
|
||||||
await operator.batchRecords(models, 'syncTeamThreads');
|
await operator.batchRecords(models, 'syncTeamThreads');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
@ -372,7 +416,6 @@ export const syncTeamThreads = async (serverUrl: string, teamId: string, prepare
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {error: false, models};
|
return {error: false, models};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return {error};
|
return {error};
|
||||||
|
|
|
||||||
|
|
@ -314,11 +314,11 @@ export async function handleUserAddedToChannelEvent(serverUrl: string, msg: any)
|
||||||
}
|
}
|
||||||
|
|
||||||
const {posts, order, authors, actionType, previousPostId} = await fetchPostsForChannel(serverUrl, channelId, true);
|
const {posts, order, authors, actionType, previousPostId} = await fetchPostsForChannel(serverUrl, channelId, true);
|
||||||
if (posts?.length && order?.length) {
|
if (posts?.length && order?.length && actionType) {
|
||||||
const {models: prepared} = await storePostsForChannel(
|
const {models: prepared} = await storePostsForChannel(
|
||||||
serverUrl, channelId,
|
serverUrl, channelId,
|
||||||
posts, order, previousPostId ?? '',
|
posts, order, previousPostId ?? '',
|
||||||
actionType, authors, true,
|
actionType, authors || [], true,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (prepared?.length) {
|
if (prepared?.length) {
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ import {PER_PAGE_DEFAULT} from './constants';
|
||||||
import type ClientBase from './base';
|
import type ClientBase from './base';
|
||||||
|
|
||||||
export interface ClientThreadsMix {
|
export interface ClientThreadsMix {
|
||||||
getThreads: (userId: string, teamId: string, before?: string, after?: string, pageSize?: number, deleted?: boolean, unread?: boolean, since?: number, totalsOnly?: boolean, serverVersion?: string) => Promise<GetUserThreadsResponse>;
|
getThreads: (userId: string, teamId: string, before?: string, after?: string, pageSize?: number, deleted?: boolean, unread?: boolean, since?: number, totalsOnly?: boolean, serverVersion?: string, excludeDirect?: boolean) => Promise<GetUserThreadsResponse>;
|
||||||
getThread: (userId: string, teamId: string, threadId: string, extended?: boolean) => Promise<any>;
|
getThread: (userId: string, teamId: string, threadId: string, extended?: boolean) => Promise<any>;
|
||||||
markThreadAsRead: (userId: string, teamId: string, threadId: string, timestamp: number) => Promise<any>;
|
markThreadAsRead: (userId: string, teamId: string, threadId: string, timestamp: number) => Promise<any>;
|
||||||
markThreadAsUnread: (userId: string, teamId: string, threadId: string, postId: string) => Promise<any>;
|
markThreadAsUnread: (userId: string, teamId: string, threadId: string, postId: string) => Promise<any>;
|
||||||
|
|
@ -17,7 +17,7 @@ export interface ClientThreadsMix {
|
||||||
}
|
}
|
||||||
|
|
||||||
const ClientThreads = <TBase extends Constructor<ClientBase>>(superclass: TBase) => class extends superclass {
|
const ClientThreads = <TBase extends Constructor<ClientBase>>(superclass: TBase) => class extends superclass {
|
||||||
getThreads = async (userId: string, teamId: string, before = '', after = '', pageSize = PER_PAGE_DEFAULT, deleted = false, unread = false, since = 0, totalsOnly = false, serverVersion = '') => {
|
getThreads = async (userId: string, teamId: string, before = '', after = '', pageSize = PER_PAGE_DEFAULT, deleted = false, unread = false, since = 0, totalsOnly = false, serverVersion = '', excludeDirect = false) => {
|
||||||
const queryStringObj: Record<string, any> = {
|
const queryStringObj: Record<string, any> = {
|
||||||
extended: 'true',
|
extended: 'true',
|
||||||
before,
|
before,
|
||||||
|
|
@ -26,6 +26,7 @@ const ClientThreads = <TBase extends Constructor<ClientBase>>(superclass: TBase)
|
||||||
unread,
|
unread,
|
||||||
since,
|
since,
|
||||||
totalsOnly,
|
totalsOnly,
|
||||||
|
excludeDirect,
|
||||||
};
|
};
|
||||||
if (serverVersion && isMinimumServerVersion(serverVersion, 6, 0)) {
|
if (serverVersion && isMinimumServerVersion(serverVersion, 6, 0)) {
|
||||||
queryStringObj.per_page = pageSize;
|
queryStringObj.per_page = pageSize;
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ import {getUserById, observeTeammateNameDisplay} from '@queries/servers/user';
|
||||||
import {goToScreen} from '@screens/navigation';
|
import {goToScreen} from '@screens/navigation';
|
||||||
import {preventDoubleTap} from '@utils/tap';
|
import {preventDoubleTap} from '@utils/tap';
|
||||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||||
|
import {secureGetFromRecord} from '@utils/types';
|
||||||
import {displayUsername} from '@utils/user';
|
import {displayUsername} from '@utils/user';
|
||||||
|
|
||||||
import type {WithDatabaseArgs} from '@typings/database/database';
|
import type {WithDatabaseArgs} from '@typings/database/database';
|
||||||
|
|
@ -93,7 +94,7 @@ async function getItemName(serverUrl: string, selected: string, teammateNameDisp
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
const database = DatabaseManager.serverDatabases[serverUrl]?.database;
|
const database = secureGetFromRecord(DatabaseManager.serverDatabases, serverUrl)?.database;
|
||||||
|
|
||||||
switch (dataSource) {
|
switch (dataSource) {
|
||||||
case ViewConstants.DATA_SOURCE_USERS: {
|
case ViewConstants.DATA_SOURCE_USERS: {
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,7 @@ const Document = forwardRef<DocumentRef, DocumentProps>(({canDownloadFiles, chil
|
||||||
let exists = false;
|
let exists = false;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
path = decodeURIComponent(file.localPath || '');
|
path = file.localPath || '';
|
||||||
if (path) {
|
if (path) {
|
||||||
exists = await fileExists(path);
|
exists = await fileExists(path);
|
||||||
}
|
}
|
||||||
|
|
@ -103,7 +103,7 @@ const Document = forwardRef<DocumentRef, DocumentProps>(({canDownloadFiles, chil
|
||||||
|
|
||||||
const openDocument = useCallback(async () => {
|
const openDocument = useCallback(async () => {
|
||||||
if (!didCancel && !preview) {
|
if (!didCancel && !preview) {
|
||||||
let path = decodeURIComponent(file.localPath || '');
|
let path = file.localPath || '';
|
||||||
let exists = false;
|
let exists = false;
|
||||||
if (path) {
|
if (path) {
|
||||||
exists = await fileExists(path);
|
exists = await fileExists(path);
|
||||||
|
|
@ -116,7 +116,7 @@ const Document = forwardRef<DocumentRef, DocumentProps>(({canDownloadFiles, chil
|
||||||
setPreview(true);
|
setPreview(true);
|
||||||
setStatusBarColor('dark-content');
|
setStatusBarColor('dark-content');
|
||||||
FileViewer.open(path!.replace('file://', ''), {
|
FileViewer.open(path!.replace('file://', ''), {
|
||||||
displayName: decodeURIComponent(file.name),
|
displayName: file.name,
|
||||||
onDismiss: onDonePreviewingFile,
|
onDismiss: onDonePreviewingFile,
|
||||||
showOpenWithDialog: true,
|
showOpenWithDialog: true,
|
||||||
showAppsSuggestions: true,
|
showAppsSuggestions: true,
|
||||||
|
|
|
||||||
|
|
@ -79,7 +79,7 @@ const FileInfo = ({disabled, file, channelName, showDate, onPress}: FileInfoProp
|
||||||
ellipsizeMode='tail'
|
ellipsizeMode='tail'
|
||||||
style={style.fileName}
|
style={style.fileName}
|
||||||
>
|
>
|
||||||
{decodeURIComponent(file.name.trim())}
|
{file.name.trim()}
|
||||||
</Text>
|
</Text>
|
||||||
<View style={style.fileDownloadContainer}>
|
<View style={style.fileDownloadContainer}>
|
||||||
{channelName &&
|
{channelName &&
|
||||||
|
|
@ -98,7 +98,9 @@ const FileInfo = ({disabled, file, channelName, showDate, onPress}: FileInfoProp
|
||||||
</Text>
|
</Text>
|
||||||
{showDate && file.create_at != null && (
|
{showDate && file.create_at != null && (
|
||||||
<>
|
<>
|
||||||
{' • '}
|
<Text style={style.infoText}>
|
||||||
|
{' • '}
|
||||||
|
</Text>
|
||||||
<FormattedDate
|
<FormattedDate
|
||||||
style={style.infoText}
|
style={style.infoText}
|
||||||
format={FORMAT}
|
format={FORMAT}
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ type FilesProps = {
|
||||||
location: string;
|
location: string;
|
||||||
isReplyPost: boolean;
|
isReplyPost: boolean;
|
||||||
postId: string;
|
postId: string;
|
||||||
postProps: Record<string, any>;
|
postProps: Record<string, unknown>;
|
||||||
publicLinkEnabled: boolean;
|
publicLinkEnabled: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -46,9 +46,7 @@ const FormattedText = (props: FormattedTextProps) => {
|
||||||
// when the `message` is formatted. This allows the formatted
|
// when the `message` is formatted. This allows the formatted
|
||||||
// message to then be broken-up into parts with references to the
|
// message to then be broken-up into parts with references to the
|
||||||
// React Elements inserted back in.
|
// React Elements inserted back in.
|
||||||
Object.keys(values).forEach((name) => {
|
Object.entries(values).forEach(([name, value]) => {
|
||||||
const value = values[name];
|
|
||||||
|
|
||||||
if (isValidElement(value)) {
|
if (isValidElement(value)) {
|
||||||
const token = generateToken();
|
const token = generateToken();
|
||||||
tokenizedValues[name] = tokenDelimiter + token + tokenDelimiter;
|
tokenizedValues[name] = tokenDelimiter + token + tokenDelimiter;
|
||||||
|
|
|
||||||
|
|
@ -10,11 +10,38 @@ import {useServerUrl} from '@context/server';
|
||||||
import {t} from '@i18n';
|
import {t} from '@i18n';
|
||||||
import {alertErrorWithFallback} from '@utils/draft';
|
import {alertErrorWithFallback} from '@utils/draft';
|
||||||
import {preventDoubleTap} from '@utils/tap';
|
import {preventDoubleTap} from '@utils/tap';
|
||||||
|
import {secureGetFromRecord, isRecordOf} from '@utils/types';
|
||||||
|
|
||||||
import type ChannelModel from '@typings/database/models/servers/channel';
|
import type ChannelModel from '@typings/database/models/servers/channel';
|
||||||
import type TeamModel from '@typings/database/models/servers/team';
|
import type TeamModel from '@typings/database/models/servers/team';
|
||||||
|
|
||||||
export type ChannelMentions = Record<string, {id?: string; display_name: string; name?: string; team_name: string}>;
|
export type ChannelMentions = Record<string, {id?: string; display_name: string; name?: string; team_name?: string}>;
|
||||||
|
|
||||||
|
export function isChannelMentions(v: unknown): v is ChannelMentions {
|
||||||
|
return isRecordOf(v, (e) => {
|
||||||
|
if (typeof e !== 'object' || !e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!('display_name' in e) || typeof e.display_name !== 'string') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('team_name' in e && typeof e.team_name !== 'string') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('id' in e && typeof e.id !== 'string') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('name' in e && typeof e.name !== 'string') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
type ChannelMentionProps = {
|
type ChannelMentionProps = {
|
||||||
channelMentions?: ChannelMentions;
|
channelMentions?: ChannelMentions;
|
||||||
|
|
@ -40,7 +67,7 @@ function getChannelFromChannelName(name: string, channels: ChannelModel[], chann
|
||||||
});
|
});
|
||||||
|
|
||||||
while (channelName.length > 0) {
|
while (channelName.length > 0) {
|
||||||
if (channelsByName[channelName]) {
|
if (secureGetFromRecord(channelsByName, channelName)) {
|
||||||
return channelsByName[channelName];
|
return channelsByName[channelName];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,8 +12,6 @@ import ChannelMention from './channel_mention';
|
||||||
|
|
||||||
import type {WithDatabaseArgs} from '@typings/database/database';
|
import type {WithDatabaseArgs} from '@typings/database/database';
|
||||||
|
|
||||||
export type ChannelMentions = Record<string, {id?: string; display_name: string; name?: string; team_name: string}>;
|
|
||||||
|
|
||||||
const enhance = withObservables([], ({database}: WithDatabaseArgs) => {
|
const enhance = withObservables([], ({database}: WithDatabaseArgs) => {
|
||||||
const currentTeamId = observeCurrentTeamId(database);
|
const currentTeamId = observeCurrentTeamId(database);
|
||||||
const channels = currentTeamId.pipe(
|
const channels = currentTeamId.pipe(
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
class ErrorBoundary extends React.PureComponent<Props, State, any> {
|
class ErrorBoundary extends React.PureComponent<Props, State> {
|
||||||
constructor(props: Props) {
|
constructor(props: Props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {hasError: false};
|
this.state = {hasError: false};
|
||||||
|
|
@ -38,7 +38,6 @@ class ErrorBoundary extends React.PureComponent<Props, State, any> {
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
// eslint-disable-next-line react/prop-types
|
|
||||||
const {children, error, theme} = this.props;
|
const {children, error, theme} = this.props;
|
||||||
const {hasError} = this.state;
|
const {hasError} = this.state;
|
||||||
const style = getStyleSheet(theme);
|
const style = getStyleSheet(theme);
|
||||||
|
|
|
||||||
140
app/components/markdown/markdown.test.tsx
Normal file
140
app/components/markdown/markdown.test.tsx
Normal file
|
|
@ -0,0 +1,140 @@
|
||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import {screen} from '@testing-library/react-native';
|
||||||
|
import * as CommonMark from 'commonmark';
|
||||||
|
const {Node} = CommonMark;
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import {Preferences} from '@constants';
|
||||||
|
import {renderWithIntl} from '@test/intl-test-helper';
|
||||||
|
|
||||||
|
import Markdown from './markdown';
|
||||||
|
import * as Transforms from './transform';
|
||||||
|
|
||||||
|
const Parser = jest.requireActual('commonmark').Parser;
|
||||||
|
|
||||||
|
describe('Markdown', () => {
|
||||||
|
const baseProps: React.ComponentProps<typeof Markdown> = {
|
||||||
|
baseTextStyle: {},
|
||||||
|
enableInlineLatex: true,
|
||||||
|
enableLatex: true,
|
||||||
|
location: 'somewhere?',
|
||||||
|
maxNodes: 2000,
|
||||||
|
theme: Preferences.THEMES.denim,
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('error handling', () => {
|
||||||
|
test('should render Markdown normally', () => {
|
||||||
|
renderWithIntl(
|
||||||
|
<Markdown
|
||||||
|
{...baseProps}
|
||||||
|
value='This is a test'
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.getByText('This is a test')).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should catch errors when parsing Markdown', () => {
|
||||||
|
jest.spyOn(CommonMark, 'Parser').mockImplementation(() => {
|
||||||
|
const parser = new Parser();
|
||||||
|
parser.incorporateLine = () => {
|
||||||
|
throw new Error('test error');
|
||||||
|
};
|
||||||
|
return parser;
|
||||||
|
});
|
||||||
|
|
||||||
|
renderWithIntl(
|
||||||
|
<Markdown
|
||||||
|
{...baseProps}
|
||||||
|
value='This is a test'
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.queryByText('This is a text')).not.toBeVisible();
|
||||||
|
expect(screen.getByText('An error occurred while parsing this text')).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should catch errors when parsing Markdown', () => {
|
||||||
|
jest.spyOn(CommonMark, 'Parser').mockImplementation(() => {
|
||||||
|
const parser = new Parser();
|
||||||
|
parser.incorporateLine = () => {
|
||||||
|
throw new Error('test error');
|
||||||
|
};
|
||||||
|
return parser;
|
||||||
|
});
|
||||||
|
|
||||||
|
renderWithIntl(
|
||||||
|
<Markdown
|
||||||
|
{...baseProps}
|
||||||
|
value='This is a test'
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.queryByText('This is a text')).not.toBeVisible();
|
||||||
|
expect(screen.getByText('An error occurred while parsing this text')).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should catch errors when transforming Markdown', () => {
|
||||||
|
jest.spyOn(Transforms, 'highlightMentions').mockImplementation(() => {
|
||||||
|
throw new Error('test error');
|
||||||
|
});
|
||||||
|
|
||||||
|
renderWithIntl(
|
||||||
|
<Markdown
|
||||||
|
{...baseProps}
|
||||||
|
value='This is a test'
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.queryByText('This is a text')).not.toBeVisible();
|
||||||
|
expect(screen.getByText('An error occurred while parsing this text')).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should catch errors when rendering Markdown', () => {
|
||||||
|
jest.spyOn(CommonMark, 'Parser').mockImplementation(() => {
|
||||||
|
const parser = new Parser();
|
||||||
|
parser.parse = () => {
|
||||||
|
// This replicates what was returned by the parser to cause MM-61148
|
||||||
|
const document = new Node('document');
|
||||||
|
|
||||||
|
const paragraph = new Node('paragraph');
|
||||||
|
(paragraph as any)._string_content = 'some text';
|
||||||
|
|
||||||
|
document.appendChild(new Node('table'));
|
||||||
|
|
||||||
|
return document;
|
||||||
|
};
|
||||||
|
return parser;
|
||||||
|
});
|
||||||
|
|
||||||
|
renderWithIntl(
|
||||||
|
<Markdown
|
||||||
|
{...baseProps}
|
||||||
|
value='This is a test'
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.queryByText('This is a text')).not.toBeVisible();
|
||||||
|
expect(screen.getByText('An error occurred while rendering this text')).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should catch errors when with Latex', () => {
|
||||||
|
const value =
|
||||||
|
'```latex\n' +
|
||||||
|
'\\\\\\\\asdfasdfasdf\n' +
|
||||||
|
'```\n';
|
||||||
|
|
||||||
|
renderWithIntl(
|
||||||
|
<Markdown
|
||||||
|
{...baseProps}
|
||||||
|
value={value}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.queryByText('This is a text')).not.toBeVisible();
|
||||||
|
expect(screen.getByText('An error occurred while rendering this text')).toBeVisible();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -10,12 +10,14 @@ import {Dimensions, type GestureResponderEvent, Platform, type StyleProp, StyleS
|
||||||
import CompassIcon from '@components/compass_icon';
|
import CompassIcon from '@components/compass_icon';
|
||||||
import Emoji from '@components/emoji';
|
import Emoji from '@components/emoji';
|
||||||
import FormattedText from '@components/formatted_text';
|
import FormattedText from '@components/formatted_text';
|
||||||
|
import {logError} from '@utils/log';
|
||||||
import {computeTextStyle} from '@utils/markdown';
|
import {computeTextStyle} from '@utils/markdown';
|
||||||
import {blendColors, changeOpacity, concatStyles, makeStyleSheetFromTheme} from '@utils/theme';
|
import {blendColors, changeOpacity, concatStyles, makeStyleSheetFromTheme} from '@utils/theme';
|
||||||
|
import {typography} from '@utils/typography';
|
||||||
import {getScheme} from '@utils/url';
|
import {getScheme} from '@utils/url';
|
||||||
|
|
||||||
import AtMention from './at_mention';
|
import AtMention from './at_mention';
|
||||||
import ChannelMention, {type ChannelMentions} from './channel_mention';
|
import ChannelMention from './channel_mention';
|
||||||
import Hashtag from './hashtag';
|
import Hashtag from './hashtag';
|
||||||
import MarkdownBlockQuote from './markdown_block_quote';
|
import MarkdownBlockQuote from './markdown_block_quote';
|
||||||
import MarkdownCodeBlock from './markdown_code_block';
|
import MarkdownCodeBlock from './markdown_code_block';
|
||||||
|
|
@ -31,6 +33,7 @@ import MarkdownTableImage from './markdown_table_image';
|
||||||
import MarkdownTableRow, {type MarkdownTableRowProps} from './markdown_table_row';
|
import MarkdownTableRow, {type MarkdownTableRowProps} from './markdown_table_row';
|
||||||
import {addListItemIndices, combineTextNodes, highlightMentions, highlightWithoutNotification, highlightSearchPatterns, parseTaskLists, pullOutImages} from './transform';
|
import {addListItemIndices, combineTextNodes, highlightMentions, highlightWithoutNotification, highlightSearchPatterns, parseTaskLists, pullOutImages} from './transform';
|
||||||
|
|
||||||
|
import type {ChannelMentions} from './channel_mention/channel_mention';
|
||||||
import type {
|
import type {
|
||||||
MarkdownAtMentionRenderer, MarkdownBaseRenderer, MarkdownBlockStyles, MarkdownChannelMentionRenderer,
|
MarkdownAtMentionRenderer, MarkdownBaseRenderer, MarkdownBlockStyles, MarkdownChannelMentionRenderer,
|
||||||
MarkdownEmojiRenderer, MarkdownImageRenderer, MarkdownLatexRenderer, MarkdownTextStyles, SearchPattern, UserMentionKey, HighlightWithoutNotificationKey,
|
MarkdownEmojiRenderer, MarkdownImageRenderer, MarkdownLatexRenderer, MarkdownTextStyles, SearchPattern, UserMentionKey, HighlightWithoutNotificationKey,
|
||||||
|
|
@ -98,6 +101,10 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||||
color: editedColor,
|
color: editedColor,
|
||||||
opacity: editedOpacity,
|
opacity: editedOpacity,
|
||||||
},
|
},
|
||||||
|
errorMessage: {
|
||||||
|
color: theme.errorTextColor,
|
||||||
|
...typography('Body', 100),
|
||||||
|
},
|
||||||
maxNodesWarning: {
|
maxNodesWarning: {
|
||||||
color: theme.errorTextColor,
|
color: theme.errorTextColor,
|
||||||
},
|
},
|
||||||
|
|
@ -606,34 +613,74 @@ const Markdown = ({
|
||||||
|
|
||||||
const parser = useRef(new Parser({urlFilter, minimumHashtagLength})).current;
|
const parser = useRef(new Parser({urlFilter, minimumHashtagLength})).current;
|
||||||
const renderer = useMemo(createRenderer, [theme, textStyles]);
|
const renderer = useMemo(createRenderer, [theme, textStyles]);
|
||||||
let ast = parser.parse(value.toString());
|
|
||||||
|
|
||||||
ast = combineTextNodes(ast);
|
const errorLogged = useRef(false);
|
||||||
ast = addListItemIndices(ast);
|
|
||||||
ast = pullOutImages(ast);
|
|
||||||
ast = parseTaskLists(ast);
|
|
||||||
if (mentionKeys) {
|
|
||||||
ast = highlightMentions(ast, mentionKeys);
|
|
||||||
}
|
|
||||||
if (highlightKeys) {
|
|
||||||
ast = highlightWithoutNotification(ast, highlightKeys);
|
|
||||||
}
|
|
||||||
if (searchPatterns) {
|
|
||||||
ast = highlightSearchPatterns(ast, searchPatterns);
|
|
||||||
}
|
|
||||||
if (isEdited) {
|
|
||||||
const editIndicatorNode = new Node('edited_indicator');
|
|
||||||
if (ast.lastChild && ['heading', 'paragraph'].includes(ast.lastChild.type)) {
|
|
||||||
ast.appendChild(editIndicatorNode);
|
|
||||||
} else {
|
|
||||||
const node = new Node('paragraph');
|
|
||||||
node.appendChild(editIndicatorNode);
|
|
||||||
|
|
||||||
ast.appendChild(node);
|
let ast;
|
||||||
|
try {
|
||||||
|
ast = parser.parse(value.toString());
|
||||||
|
|
||||||
|
ast = combineTextNodes(ast);
|
||||||
|
ast = addListItemIndices(ast);
|
||||||
|
ast = pullOutImages(ast);
|
||||||
|
ast = parseTaskLists(ast);
|
||||||
|
if (mentionKeys) {
|
||||||
|
ast = highlightMentions(ast, mentionKeys);
|
||||||
}
|
}
|
||||||
|
if (highlightKeys) {
|
||||||
|
ast = highlightWithoutNotification(ast, highlightKeys);
|
||||||
|
}
|
||||||
|
if (searchPatterns) {
|
||||||
|
ast = highlightSearchPatterns(ast, searchPatterns);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isEdited) {
|
||||||
|
const editIndicatorNode = new Node('edited_indicator');
|
||||||
|
if (ast.lastChild && ['heading', 'paragraph'].includes(ast.lastChild.type)) {
|
||||||
|
ast.appendChild(editIndicatorNode);
|
||||||
|
} else {
|
||||||
|
const node = new Node('paragraph');
|
||||||
|
node.appendChild(editIndicatorNode);
|
||||||
|
|
||||||
|
ast.appendChild(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (!errorLogged.current) {
|
||||||
|
logError('An error occurred while parsing Markdown', e);
|
||||||
|
|
||||||
|
errorLogged.current = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormattedText
|
||||||
|
id='markdown.parse_error'
|
||||||
|
defaultMessage='An error occurred while parsing this text'
|
||||||
|
style={style.errorMessage}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return renderer.render(ast) as JSX.Element;
|
let output;
|
||||||
|
try {
|
||||||
|
output = renderer.render(ast);
|
||||||
|
} catch (e) {
|
||||||
|
if (!errorLogged.current) {
|
||||||
|
logError('An error occurred while rendering Markdown', e);
|
||||||
|
|
||||||
|
errorLogged.current = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormattedText
|
||||||
|
id='markdown.render_error'
|
||||||
|
defaultMessage='An error occurred while rendering this text'
|
||||||
|
style={style.errorMessage}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return output;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Markdown;
|
export default Markdown;
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,8 @@ import {bottomSheetSnapPoint} from '@utils/helpers';
|
||||||
import {calculateDimensions, getViewPortWidth, isGifTooLarge} from '@utils/images';
|
import {calculateDimensions, getViewPortWidth, isGifTooLarge} from '@utils/images';
|
||||||
import {getMarkdownImageSize} from '@utils/markdown';
|
import {getMarkdownImageSize} from '@utils/markdown';
|
||||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||||
import {normalizeProtocol, tryOpenURL} from '@utils/url';
|
import {secureGetFromRecord} from '@utils/types';
|
||||||
|
import {normalizeProtocol, safeDecodeURIComponent, tryOpenURL} from '@utils/url';
|
||||||
|
|
||||||
import type {GalleryItemType} from '@typings/screens/gallery';
|
import type {GalleryItemType} from '@typings/screens/gallery';
|
||||||
|
|
||||||
|
|
@ -80,7 +81,7 @@ const MarkdownImage = ({
|
||||||
const style = getStyleSheet(theme);
|
const style = getStyleSheet(theme);
|
||||||
const managedConfig = useManagedConfig<ManagedConfig>();
|
const managedConfig = useManagedConfig<ManagedConfig>();
|
||||||
const genericFileId = useRef(generateId('uid')).current;
|
const genericFileId = useRef(generateId('uid')).current;
|
||||||
const metadata = imagesMetadata?.[source] || Object.values(imagesMetadata || {})[0];
|
const metadata = secureGetFromRecord(imagesMetadata, source) || Object.values(imagesMetadata || {})[0];
|
||||||
const [failed, setFailed] = useState(isGifTooLarge(metadata));
|
const [failed, setFailed] = useState(isGifTooLarge(metadata));
|
||||||
const originalSize = getMarkdownImageSize(isReplyPost, isTablet, sourceSize, metadata, layoutWidth, layoutHeight);
|
const originalSize = getMarkdownImageSize(isReplyPost, isTablet, sourceSize, metadata, layoutWidth, layoutHeight);
|
||||||
const serverUrl = useServerUrl();
|
const serverUrl = useServerUrl();
|
||||||
|
|
@ -88,8 +89,8 @@ const MarkdownImage = ({
|
||||||
const uri = source.startsWith('/') ? serverUrl + source : source;
|
const uri = source.startsWith('/') ? serverUrl + source : source;
|
||||||
|
|
||||||
const fileInfo = useMemo(() => {
|
const fileInfo = useMemo(() => {
|
||||||
const link = decodeURIComponent(uri);
|
const decodedLink = safeDecodeURIComponent(uri);
|
||||||
let filename = parseUrl(link.substr(link.lastIndexOf('/'))).pathname.replace('/', '');
|
let filename = parseUrl(decodedLink.substr(decodedLink.lastIndexOf('/'))).pathname.replace('/', '');
|
||||||
let extension = metadata?.format || filename.split('.').pop();
|
let extension = metadata?.format || filename.split('.').pop();
|
||||||
if (extension === filename) {
|
if (extension === filename) {
|
||||||
const ext = filename.indexOf('.') === -1 ? '.png' : filename.substring(filename.lastIndexOf('.'));
|
const ext = filename.indexOf('.') === -1 ? '.png' : filename.substring(filename.lastIndexOf('.'));
|
||||||
|
|
@ -104,7 +105,7 @@ const MarkdownImage = ({
|
||||||
has_preview_image: true,
|
has_preview_image: true,
|
||||||
mime_type: lookupMimeType(filename),
|
mime_type: lookupMimeType(filename),
|
||||||
post_id: postId,
|
post_id: postId,
|
||||||
uri: link,
|
uri,
|
||||||
width: originalSize.width,
|
width: originalSize.width,
|
||||||
height: originalSize.height,
|
height: originalSize.height,
|
||||||
} as FileInfo;
|
} as FileInfo;
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,8 @@ import {useGalleryItem} from '@hooks/gallery';
|
||||||
import {fileToGalleryItem, openGalleryAtIndex} from '@utils/gallery';
|
import {fileToGalleryItem, openGalleryAtIndex} from '@utils/gallery';
|
||||||
import {generateId} from '@utils/general';
|
import {generateId} from '@utils/general';
|
||||||
import {calculateDimensions, isGifTooLarge} from '@utils/images';
|
import {calculateDimensions, isGifTooLarge} from '@utils/images';
|
||||||
|
import {secureGetFromRecord} from '@utils/types';
|
||||||
|
import {safeDecodeURIComponent} from '@utils/url';
|
||||||
|
|
||||||
import type {GalleryItemType} from '@typings/screens/gallery';
|
import type {GalleryItemType} from '@typings/screens/gallery';
|
||||||
|
|
||||||
|
|
@ -33,7 +35,7 @@ const style = StyleSheet.create({
|
||||||
});
|
});
|
||||||
|
|
||||||
const MarkTableImage = ({disabled, imagesMetadata, location, postId, serverURL, source}: MarkdownTableImageProps) => {
|
const MarkTableImage = ({disabled, imagesMetadata, location, postId, serverURL, source}: MarkdownTableImageProps) => {
|
||||||
const metadata = imagesMetadata[source];
|
const metadata = secureGetFromRecord(imagesMetadata, source);
|
||||||
const fileId = useRef(generateId('uid')).current;
|
const fileId = useRef(generateId('uid')).current;
|
||||||
const [failed, setFailed] = useState(isGifTooLarge(metadata));
|
const [failed, setFailed] = useState(isGifTooLarge(metadata));
|
||||||
const currentServerUrl = useServerUrl();
|
const currentServerUrl = useServerUrl();
|
||||||
|
|
@ -57,8 +59,9 @@ const MarkTableImage = ({disabled, imagesMetadata, location, postId, serverURL,
|
||||||
const getFileInfo = () => {
|
const getFileInfo = () => {
|
||||||
const height = metadata?.height || 0;
|
const height = metadata?.height || 0;
|
||||||
const width = metadata?.width || 0;
|
const width = metadata?.width || 0;
|
||||||
const link = decodeURIComponent(getImageSource());
|
const uri = getImageSource();
|
||||||
let filename = parseUrl(link.substr(link.lastIndexOf('/'))).pathname.replace('/', '');
|
const decodedLink = safeDecodeURIComponent(uri);
|
||||||
|
let filename = parseUrl(decodedLink.substring(decodedLink.lastIndexOf('/'))).pathname.replace('/', '');
|
||||||
let extension = filename.split('.').pop();
|
let extension = filename.split('.').pop();
|
||||||
|
|
||||||
if (extension === filename) {
|
if (extension === filename) {
|
||||||
|
|
@ -73,7 +76,7 @@ const MarkTableImage = ({disabled, imagesMetadata, location, postId, serverURL,
|
||||||
extension,
|
extension,
|
||||||
has_preview_image: true,
|
has_preview_image: true,
|
||||||
post_id: postId,
|
post_id: postId,
|
||||||
uri: link,
|
uri,
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
import React, {useCallback, useEffect} from 'react';
|
import React, {useCallback, useEffect, useMemo} from 'react';
|
||||||
import {useIntl} from 'react-intl';
|
import {useIntl} from 'react-intl';
|
||||||
import {Keyboard, type StyleProp, TouchableHighlight, View, type ViewStyle} from 'react-native';
|
import {Keyboard, type StyleProp, TouchableHighlight, View, type ViewStyle} from 'react-native';
|
||||||
|
|
||||||
|
|
@ -15,7 +15,9 @@ import {useIsTablet} from '@hooks/device';
|
||||||
import {bottomSheetModalOptions, showModal, showModalOverCurrentContext} from '@screens/navigation';
|
import {bottomSheetModalOptions, showModal, showModalOverCurrentContext} from '@screens/navigation';
|
||||||
import {emptyFunction} from '@utils/general';
|
import {emptyFunction} from '@utils/general';
|
||||||
import {getMarkdownTextStyles} from '@utils/markdown';
|
import {getMarkdownTextStyles} from '@utils/markdown';
|
||||||
|
import {isUserActivityProp} from '@utils/post_list';
|
||||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||||
|
import {secureGetFromRecord} from '@utils/types';
|
||||||
|
|
||||||
import LastUsers from './last_users';
|
import LastUsers from './last_users';
|
||||||
import {postTypeMessages} from './messages';
|
import {postTypeMessages} from './messages';
|
||||||
|
|
@ -70,13 +72,20 @@ const CombinedUserActivity = ({
|
||||||
const content = [];
|
const content = [];
|
||||||
const removedUserIds: string[] = [];
|
const removedUserIds: string[] = [];
|
||||||
|
|
||||||
|
const userActivity = useMemo(() => {
|
||||||
|
if (isUserActivityProp(post?.props?.user_activity)) {
|
||||||
|
return post?.props?.user_activity;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}, [post?.props?.user_activity]);
|
||||||
|
|
||||||
const getUsernames = (userIds: string[]) => {
|
const getUsernames = (userIds: string[]) => {
|
||||||
const someone = intl.formatMessage({id: 'channel_loader.someone', defaultMessage: 'Someone'});
|
const someone = intl.formatMessage({id: 'channel_loader.someone', defaultMessage: 'Someone'});
|
||||||
const you = intl.formatMessage({id: 'combined_system_message.you', defaultMessage: 'You'});
|
const you = intl.formatMessage({id: 'combined_system_message.you', defaultMessage: 'You'});
|
||||||
const usernamesValues = Object.values(usernamesById);
|
const usernamesValues = Object.values(usernamesById);
|
||||||
const usernames = userIds.reduce((acc: string[], id: string) => {
|
const usernames = userIds.reduce((acc: string[], id: string) => {
|
||||||
if (id !== currentUserId && id !== currentUsername) {
|
if (id !== currentUserId && id !== currentUsername) {
|
||||||
const name = usernamesById[id] ?? usernamesValues.find((n) => n === id);
|
const name = secureGetFromRecord(usernamesById, id) ?? usernamesValues.find((n) => n === id);
|
||||||
acc.push(name ? `@${name}` : someone);
|
acc.push(name ? `@${name}` : someone);
|
||||||
}
|
}
|
||||||
return acc;
|
return acc;
|
||||||
|
|
@ -107,12 +116,12 @@ const CombinedUserActivity = ({
|
||||||
}
|
}
|
||||||
}, [post, canDelete, isTablet, intl, location]);
|
}, [post, canDelete, isTablet, intl, location]);
|
||||||
|
|
||||||
const renderMessage = (postType: string, userIds: string[], actorId: string) => {
|
const renderMessage = (postType: string, userIds: string[], actorId?: string) => {
|
||||||
if (!post) {
|
if (!post) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
let actor = '';
|
let actor = '';
|
||||||
if (usernamesById[actorId]) {
|
if (actorId && secureGetFromRecord(usernamesById, actorId)) {
|
||||||
actor = `@${usernamesById[actorId]}`;
|
actor = `@${usernamesById[actorId]}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -141,11 +150,11 @@ const CombinedUserActivity = ({
|
||||||
const secondUser = usernames[1];
|
const secondUser = usernames[1];
|
||||||
let localeHolder;
|
let localeHolder;
|
||||||
if (numOthers === 0) {
|
if (numOthers === 0) {
|
||||||
localeHolder = postTypeMessages[postType].one;
|
localeHolder = secureGetFromRecord(postTypeMessages, postType)?.one;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
(userIds[0] === currentUserId || userIds[0] === currentUsername) &&
|
(userIds[0] === currentUserId || userIds[0] === currentUsername) &&
|
||||||
postTypeMessages[postType].one_you
|
secureGetFromRecord(postTypeMessages, postType)?.one_you
|
||||||
) {
|
) {
|
||||||
localeHolder = postTypeMessages[postType].one_you;
|
localeHolder = postTypeMessages[postType].one_you;
|
||||||
}
|
}
|
||||||
|
|
@ -153,7 +162,8 @@ const CombinedUserActivity = ({
|
||||||
localeHolder = postTypeMessages[postType].two;
|
localeHolder = postTypeMessages[postType].two;
|
||||||
}
|
}
|
||||||
|
|
||||||
const formattedMessage = intl.formatMessage(localeHolder, {firstUser, secondUser, actor});
|
// We default to empty string, but this should never happen
|
||||||
|
const formattedMessage = localeHolder ? intl.formatMessage(localeHolder, {firstUser, secondUser, actor}) : '';
|
||||||
return (
|
return (
|
||||||
<Markdown
|
<Markdown
|
||||||
channelId={post.channel_id}
|
channelId={post.channel_id}
|
||||||
|
|
@ -168,11 +178,12 @@ const CombinedUserActivity = ({
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!post) {
|
if (!userActivity) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const {allUserIds, allUsernames} = post.props.user_activity;
|
const allUserIds = userActivity.allUserIds;
|
||||||
|
const allUsernames = userActivity.allUsernames;
|
||||||
if (allUserIds.length) {
|
if (allUserIds.length) {
|
||||||
fetchMissingProfilesByIds(serverUrl, allUserIds);
|
fetchMissingProfilesByIds(serverUrl, allUserIds);
|
||||||
}
|
}
|
||||||
|
|
@ -180,14 +191,14 @@ const CombinedUserActivity = ({
|
||||||
if (allUsernames.length) {
|
if (allUsernames.length) {
|
||||||
fetchMissingProfilesByUsernames(serverUrl, allUsernames);
|
fetchMissingProfilesByUsernames(serverUrl, allUsernames);
|
||||||
}
|
}
|
||||||
}, [post?.props.user_activity.allUserIds, post?.props.user_activity.allUsernames]);
|
}, [userActivity?.allUserIds, userActivity?.allUsernames]);
|
||||||
|
|
||||||
if (!post) {
|
if (!post) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const itemTestID = `${testID}.${post.id}`;
|
const itemTestID = `${testID}.${post.id}`;
|
||||||
const {messageData} = post.props.user_activity;
|
const messageData = userActivity?.messageData || [];
|
||||||
for (const message of messageData) {
|
for (const message of messageData) {
|
||||||
const {postType, actorId} = message;
|
const {postType, actorId} = message;
|
||||||
const userIds = new Set<string>(message.userIds);
|
const userIds = new Set<string>(message.userIds);
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ import {queryPostsById} from '@queries/servers/post';
|
||||||
import {observePermissionForPost} from '@queries/servers/role';
|
import {observePermissionForPost} from '@queries/servers/role';
|
||||||
import {observeCurrentUserId} from '@queries/servers/system';
|
import {observeCurrentUserId} from '@queries/servers/system';
|
||||||
import {observeUser, queryUsersByIdsOrUsernames} from '@queries/servers/user';
|
import {observeUser, queryUsersByIdsOrUsernames} from '@queries/servers/user';
|
||||||
import {generateCombinedPost, getPostIdsForCombinedUserActivityPost} from '@utils/post_list';
|
import {generateCombinedPost, getPostIdsForCombinedUserActivityPost, isUserActivityProp} from '@utils/post_list';
|
||||||
|
|
||||||
import CombinedUserActivity from './combined_user_activity';
|
import CombinedUserActivity from './combined_user_activity';
|
||||||
|
|
||||||
|
|
@ -35,10 +35,11 @@ const withCombinedPosts = withObservables(['postId'], ({database, postId}: WithD
|
||||||
const usernamesById = post.pipe(
|
const usernamesById = post.pipe(
|
||||||
switchMap(
|
switchMap(
|
||||||
(p) => {
|
(p) => {
|
||||||
if (!p) {
|
const userActivity = isUserActivityProp(p?.props?.user_activity) ? p.props.user_activity : undefined;
|
||||||
|
if (!userActivity) {
|
||||||
return of$<Record<string, string>>({});
|
return of$<Record<string, string>>({});
|
||||||
}
|
}
|
||||||
return queryUsersByIdsOrUsernames(database, p.props.user_activity.allUserIds, p.props.user_activity.allUsernames).observeWithColumns(['username']).
|
return queryUsersByIdsOrUsernames(database, userActivity.allUserIds, userActivity.allUsernames).observeWithColumns(['username']).
|
||||||
pipe(
|
pipe(
|
||||||
// eslint-disable-next-line max-nested-callbacks
|
// eslint-disable-next-line max-nested-callbacks
|
||||||
switchMap((users) => {
|
switchMap((users) => {
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import FormattedText from '@components/formatted_text';
|
||||||
import Markdown from '@components/markdown';
|
import Markdown from '@components/markdown';
|
||||||
import {getMarkdownTextStyles} from '@utils/markdown';
|
import {getMarkdownTextStyles} from '@utils/markdown';
|
||||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||||
|
import {secureGetFromRecord} from '@utils/types';
|
||||||
|
|
||||||
import {postTypeMessages, systemMessages} from './messages';
|
import {postTypeMessages, systemMessages} from './messages';
|
||||||
|
|
||||||
|
|
@ -50,12 +51,14 @@ const LastUsers = ({actor, channelId, location, postType, theme, usernames}: Las
|
||||||
if (expanded) {
|
if (expanded) {
|
||||||
const lastIndex = usernames.length - 1;
|
const lastIndex = usernames.length - 1;
|
||||||
const lastUser = usernames[lastIndex];
|
const lastUser = usernames[lastIndex];
|
||||||
const expandedMessage = postTypeMessages[postType].many_expanded;
|
const expandedMessage = secureGetFromRecord(postTypeMessages, postType)?.many_expanded;
|
||||||
const formattedMessage = intl.formatMessage(expandedMessage, {
|
|
||||||
|
// We default to empty string, but this should never happen
|
||||||
|
const formattedMessage = expandedMessage ? intl.formatMessage(expandedMessage, {
|
||||||
users: usernames.slice(0, lastIndex).join(', '),
|
users: usernames.slice(0, lastIndex).join(', '),
|
||||||
lastUser,
|
lastUser,
|
||||||
actor,
|
actor,
|
||||||
});
|
}) : '';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Markdown
|
<Markdown
|
||||||
|
|
@ -72,6 +75,8 @@ const LastUsers = ({actor, channelId, location, postType, theme, usernames}: Las
|
||||||
const firstUser = usernames[0];
|
const firstUser = usernames[0];
|
||||||
const numOthers = usernames.length - 1;
|
const numOthers = usernames.length - 1;
|
||||||
|
|
||||||
|
const message = secureGetFromRecord(systemMessages, postType);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Text>
|
<Text>
|
||||||
<FormattedMarkdownText
|
<FormattedMarkdownText
|
||||||
|
|
@ -96,8 +101,8 @@ const LastUsers = ({actor, channelId, location, postType, theme, usernames}: Las
|
||||||
</Text>
|
</Text>
|
||||||
<FormattedMarkdownText
|
<FormattedMarkdownText
|
||||||
channelId={channelId}
|
channelId={channelId}
|
||||||
id={systemMessages[postType].id}
|
id={message?.id || ''}
|
||||||
defaultMessage={systemMessages[postType].defaultMessage}
|
defaultMessage={message?.defaultMessage || ''}
|
||||||
location={location}
|
location={location}
|
||||||
values={{actor}}
|
values={{actor}}
|
||||||
baseTextStyle={style.baseText}
|
baseTextStyle={style.baseText}
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ import {useServerUrl} from '@context/server';
|
||||||
import {useTheme} from '@context/theme';
|
import {useTheme} from '@context/theme';
|
||||||
import {openAsBottomSheet} from '@screens/navigation';
|
import {openAsBottomSheet} from '@screens/navigation';
|
||||||
import {preventDoubleTap} from '@utils/tap';
|
import {preventDoubleTap} from '@utils/tap';
|
||||||
|
import {ensureString} from '@utils/types';
|
||||||
|
|
||||||
import type PostModel from '@typings/database/models/servers/post';
|
import type PostModel from '@typings/database/models/servers/post';
|
||||||
import type UserModel from '@typings/database/models/servers/user';
|
import type UserModel from '@typings/database/models/servers/user';
|
||||||
|
|
@ -39,12 +40,15 @@ const Avatar = ({author, enablePostIconOverride, isAutoReponse, location, post}:
|
||||||
|
|
||||||
const fromWebHook = post.props?.from_webhook === 'true';
|
const fromWebHook = post.props?.from_webhook === 'true';
|
||||||
const iconOverride = enablePostIconOverride && post.props?.use_user_icon !== 'true';
|
const iconOverride = enablePostIconOverride && post.props?.use_user_icon !== 'true';
|
||||||
|
const propsIconUrl = ensureString(post.props?.override_icon_url);
|
||||||
|
const propsUsername = ensureString(post.props?.override_username);
|
||||||
|
|
||||||
if (fromWebHook && iconOverride) {
|
if (fromWebHook && iconOverride) {
|
||||||
const isEmoji = Boolean(post.props?.override_icon_emoji);
|
const isEmoji = Boolean(post.props?.override_icon_emoji);
|
||||||
const frameSize = ViewConstant.PROFILE_PICTURE_SIZE;
|
const frameSize = ViewConstant.PROFILE_PICTURE_SIZE;
|
||||||
const pictureSize = isEmoji ? ViewConstant.PROFILE_PICTURE_EMOJI_SIZE : ViewConstant.PROFILE_PICTURE_SIZE;
|
const pictureSize = isEmoji ? ViewConstant.PROFILE_PICTURE_EMOJI_SIZE : ViewConstant.PROFILE_PICTURE_SIZE;
|
||||||
const borderRadius = isEmoji ? 0 : ViewConstant.PROFILE_PICTURE_SIZE / 2;
|
const borderRadius = isEmoji ? 0 : ViewConstant.PROFILE_PICTURE_SIZE / 2;
|
||||||
const overrideIconUrl = buildAbsoluteUrl(serverUrl, post.props?.override_icon_url);
|
const overrideIconUrl = buildAbsoluteUrl(serverUrl, propsIconUrl);
|
||||||
|
|
||||||
let iconComponent: ReactNode;
|
let iconComponent: ReactNode;
|
||||||
if (overrideIconUrl) {
|
if (overrideIconUrl) {
|
||||||
|
|
@ -95,8 +99,8 @@ const Avatar = ({author, enablePostIconOverride, isAutoReponse, location, post}:
|
||||||
userId: author.id,
|
userId: author.id,
|
||||||
channelId: post.channelId,
|
channelId: post.channelId,
|
||||||
location,
|
location,
|
||||||
userIconOverride: post.props?.override_username,
|
userIconOverride: propsIconUrl,
|
||||||
usernameOverride: post.props?.override_icon_url,
|
usernameOverride: propsUsername,
|
||||||
};
|
};
|
||||||
|
|
||||||
Keyboard.dismiss();
|
Keyboard.dismiss();
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ import {useServerUrl} from '@context/server';
|
||||||
import {t} from '@i18n';
|
import {t} from '@i18n';
|
||||||
import {getMarkdownTextStyles} from '@utils/markdown';
|
import {getMarkdownTextStyles} from '@utils/markdown';
|
||||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||||
|
import {isStringArray} from '@utils/types';
|
||||||
|
|
||||||
import type PostModel from '@typings/database/models/servers/post';
|
import type PostModel from '@typings/database/models/servers/post';
|
||||||
import type UserModel from '@typings/database/models/servers/user';
|
import type UserModel from '@typings/database/models/servers/user';
|
||||||
|
|
@ -26,6 +27,39 @@ type AddMembersProps = {
|
||||||
theme: Theme;
|
theme: Theme;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type AddMemberPostProps = {
|
||||||
|
post_id: string;
|
||||||
|
not_in_channel_user_ids?: string[];
|
||||||
|
not_in_groups_usernames?: string[];
|
||||||
|
not_in_channel_usernames?: string[];
|
||||||
|
user_ids?: string[];
|
||||||
|
usernames?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isAddMemberProps(v: unknown): v is AddMemberPostProps {
|
||||||
|
if (typeof v !== 'object' || !v) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!('post_id' in v) || typeof v.post_id !== 'string') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (('not_in_channel_user_ids' in v) && !isStringArray(v.not_in_channel_user_ids)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (('not_in_groups_usernames' in v) && !isStringArray(v.not_in_groups_usernames)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (('not_in_channel_usernames' in v) && !isStringArray(v.not_in_channel_usernames)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||||
return {
|
return {
|
||||||
message: {
|
message: {
|
||||||
|
|
@ -41,20 +75,17 @@ const AddMembers = ({channelType, currentUser, location, post, theme}: AddMember
|
||||||
const styles = getStyleSheet(theme);
|
const styles = getStyleSheet(theme);
|
||||||
const textStyles = getMarkdownTextStyles(theme);
|
const textStyles = getMarkdownTextStyles(theme);
|
||||||
const serverUrl = useServerUrl();
|
const serverUrl = useServerUrl();
|
||||||
const postId: string = post.props.add_channel_member?.post_id;
|
if (!isAddMemberProps(post.props?.add_channel_member)) {
|
||||||
const noGroupsUsernames = post.props.add_channel_member?.not_in_groups_usernames;
|
|
||||||
let userIds: string[] = post.props.add_channel_member?.not_in_channel_user_ids;
|
|
||||||
let usernames: string[] = post.props.add_channel_member?.not_in_channel_usernames;
|
|
||||||
|
|
||||||
if (!postId || !channelType) {
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!userIds) {
|
const postId = post.props.add_channel_member.post_id;
|
||||||
userIds = post.props.add_channel_member?.user_ids;
|
const noGroupsUsernames = post.props.add_channel_member.not_in_groups_usernames || [];
|
||||||
}
|
const userIds = post.props.add_channel_member.not_in_channel_user_ids || post.props.add_channel_member.user_ids || [];
|
||||||
if (!usernames) {
|
const usernames = post.props.add_channel_member.not_in_channel_usernames || post.props.add_channel_member?.usernames || [];
|
||||||
usernames = post.props.add_channel_member?.usernames;
|
|
||||||
|
if (!postId || !channelType) {
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleAddChannelMember = () => {
|
const handleAddChannelMember = () => {
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ import {observeChannel} from '@queries/servers/channel';
|
||||||
import {observeCurrentTeamId} from '@queries/servers/system';
|
import {observeCurrentTeamId} from '@queries/servers/system';
|
||||||
import {showAppForm} from '@screens/navigation';
|
import {showAppForm} from '@screens/navigation';
|
||||||
import {createCallContext} from '@utils/apps';
|
import {createCallContext} from '@utils/apps';
|
||||||
import {getStatusColors} from '@utils/message_attachment_colors';
|
import {getStatusColors} from '@utils/message_attachment';
|
||||||
import {preventDoubleTap} from '@utils/tap';
|
import {preventDoubleTap} from '@utils/tap';
|
||||||
import {makeStyleSheetFromTheme, changeOpacity} from '@utils/theme';
|
import {makeStyleSheetFromTheme, changeOpacity} from '@utils/theme';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,9 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import {View} from 'react-native';
|
import {View} from 'react-native';
|
||||||
|
|
||||||
|
import {isAppBinding, validateBindings} from '@utils/apps';
|
||||||
|
import {isArrayOf} from '@utils/types';
|
||||||
|
|
||||||
import EmbeddedBinding from './embedded_binding';
|
import EmbeddedBinding from './embedded_binding';
|
||||||
|
|
||||||
import type PostModel from '@typings/database/models/servers/post';
|
import type PostModel from '@typings/database/models/servers/post';
|
||||||
|
|
@ -16,7 +19,7 @@ type Props = {
|
||||||
|
|
||||||
const EmbeddedBindings = ({location, post, theme}: Props) => {
|
const EmbeddedBindings = ({location, post, theme}: Props) => {
|
||||||
const content: React.ReactNode[] = [];
|
const content: React.ReactNode[] = [];
|
||||||
const embeds: AppBinding[] = post.props.app_bindings;
|
const embeds: AppBinding[] = isArrayOf<AppBinding>(post.props?.app_bindings, isAppBinding) ? validateBindings(post.props.app_bindings) : [];
|
||||||
|
|
||||||
embeds.forEach((embed, i) => {
|
embeds.forEach((embed, i) => {
|
||||||
content.push(
|
content.push(
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ import {openGalleryAtIndex} from '@utils/gallery';
|
||||||
import {generateId} from '@utils/general';
|
import {generateId} from '@utils/general';
|
||||||
import {calculateDimensions, getViewPortWidth, isGifTooLarge} from '@utils/images';
|
import {calculateDimensions, getViewPortWidth, isGifTooLarge} from '@utils/images';
|
||||||
import {changeOpacity} from '@utils/theme';
|
import {changeOpacity} from '@utils/theme';
|
||||||
|
import {secureGetFromRecord} from '@utils/types';
|
||||||
import {extractFilenameFromUrl, isImageLink, isValidUrl} from '@utils/url';
|
import {extractFilenameFromUrl, isImageLink, isValidUrl} from '@utils/url';
|
||||||
|
|
||||||
import type {GalleryItemType} from '@typings/screens/gallery';
|
import type {GalleryItemType} from '@typings/screens/gallery';
|
||||||
|
|
@ -55,7 +56,7 @@ const ImagePreview = ({expandedLink, isReplyPost, layoutWidth, link, location, m
|
||||||
const fileId = useRef(generateId('uid')).current;
|
const fileId = useRef(generateId('uid')).current;
|
||||||
const [imageUrl, setImageUrl] = useState(expandedLink || link);
|
const [imageUrl, setImageUrl] = useState(expandedLink || link);
|
||||||
const isTablet = useIsTablet();
|
const isTablet = useIsTablet();
|
||||||
const imageProps = metadata?.images?.[link];
|
const imageProps = secureGetFromRecord(metadata?.images, link);
|
||||||
const dimensions = calculateDimensions(imageProps?.height, imageProps?.width, layoutWidth || getViewPortWidth(isReplyPost, isTablet));
|
const dimensions = calculateDimensions(imageProps?.height, imageProps?.width, layoutWidth || getViewPortWidth(isReplyPost, isTablet));
|
||||||
|
|
||||||
const onError = useCallback(() => {
|
const onError = useCallback(() => {
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
|
import {isMessageAttachmentArray} from '@utils/message_attachment';
|
||||||
import {isYoutubeLink} from '@utils/url';
|
import {isYoutubeLink} from '@utils/url';
|
||||||
|
|
||||||
import EmbeddedBindings from './embedded_bindings';
|
import EmbeddedBindings from './embedded_bindings';
|
||||||
|
|
@ -31,7 +32,9 @@ const contentType: Record<string, string> = {
|
||||||
|
|
||||||
const Content = ({isReplyPost, layoutWidth, location, post, theme}: ContentProps) => {
|
const Content = ({isReplyPost, layoutWidth, location, post, theme}: ContentProps) => {
|
||||||
let type: string | undefined = post.metadata?.embeds?.[0].type;
|
let type: string | undefined = post.metadata?.embeds?.[0].type;
|
||||||
if (!type && post.props?.app_bindings?.length) {
|
|
||||||
|
const nAppBindings = Array.isArray(post.props?.app_bindings) ? post.props.app_bindings.length : 0;
|
||||||
|
if (!type && nAppBindings) {
|
||||||
type = contentType.app_bindings;
|
type = contentType.app_bindings;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -39,6 +42,8 @@ const Content = ({isReplyPost, layoutWidth, location, post, theme}: ContentProps
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const attachments = isMessageAttachmentArray(post.props?.attachments) ? post.props.attachments : [];
|
||||||
|
|
||||||
switch (contentType[type]) {
|
switch (contentType[type]) {
|
||||||
case contentType.image:
|
case contentType.image:
|
||||||
return (
|
return (
|
||||||
|
|
@ -74,10 +79,10 @@ const Content = ({isReplyPost, layoutWidth, location, post, theme}: ContentProps
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
case contentType.message_attachment:
|
case contentType.message_attachment:
|
||||||
if (post.props.attachments?.length) {
|
if (attachments.length) {
|
||||||
return (
|
return (
|
||||||
<MessageAttachments
|
<MessageAttachments
|
||||||
attachments={post.props.attachments}
|
attachments={attachments}
|
||||||
channelId={post.channelId}
|
channelId={post.channelId}
|
||||||
layoutWidth={layoutWidth}
|
layoutWidth={layoutWidth}
|
||||||
location={location}
|
location={location}
|
||||||
|
|
@ -89,7 +94,7 @@ const Content = ({isReplyPost, layoutWidth, location, post, theme}: ContentProps
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case contentType.app_bindings:
|
case contentType.app_bindings:
|
||||||
if (post.props.app_bindings?.length) {
|
if (nAppBindings) {
|
||||||
return (
|
return (
|
||||||
<EmbeddedBindings
|
<EmbeddedBindings
|
||||||
location={location}
|
location={location}
|
||||||
|
|
|
||||||
|
|
@ -6,9 +6,10 @@ import React, {useCallback, useRef} from 'react';
|
||||||
|
|
||||||
import {postActionWithCookie} from '@actions/remote/integrations';
|
import {postActionWithCookie} from '@actions/remote/integrations';
|
||||||
import {useServerUrl} from '@context/server';
|
import {useServerUrl} from '@context/server';
|
||||||
import {getStatusColors} from '@utils/message_attachment_colors';
|
import {getStatusColors} from '@utils/message_attachment';
|
||||||
import {preventDoubleTap} from '@utils/tap';
|
import {preventDoubleTap} from '@utils/tap';
|
||||||
import {makeStyleSheetFromTheme, changeOpacity} from '@utils/theme';
|
import {makeStyleSheetFromTheme, changeOpacity} from '@utils/theme';
|
||||||
|
import {secureGetFromRecord} from '@utils/types';
|
||||||
|
|
||||||
import ActionButtonText from './action_button_text';
|
import ActionButtonText from './action_button_text';
|
||||||
|
|
||||||
|
|
@ -65,7 +66,7 @@ const ActionButton = ({buttonColor, cookie, disabled, id, name, postId, theme}:
|
||||||
|
|
||||||
if (buttonColor) {
|
if (buttonColor) {
|
||||||
const STATUS_COLORS = getStatusColors(theme);
|
const STATUS_COLORS = getStatusColors(theme);
|
||||||
const hexColor = STATUS_COLORS[buttonColor] || theme[buttonColor] || buttonColor;
|
const hexColor = secureGetFromRecord(STATUS_COLORS, buttonColor) || secureGetFromRecord(theme, buttonColor) || buttonColor;
|
||||||
customButtonStyle = {borderColor: changeOpacity(hexColor, 0.25), backgroundColor: '#ffffff'};
|
customButtonStyle = {borderColor: changeOpacity(hexColor, 0.25), backgroundColor: '#ffffff'};
|
||||||
customButtonTextStyle = {color: hexColor};
|
customButtonTextStyle = {color: hexColor};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -57,7 +57,7 @@ const AttachmentImage = ({imageUrl, imageMetadata, layoutWidth, location, postId
|
||||||
const [error, setError] = useState(false);
|
const [error, setError] = useState(false);
|
||||||
const fileId = useRef(generateId('uid')).current;
|
const fileId = useRef(generateId('uid')).current;
|
||||||
const isTablet = useIsTablet();
|
const isTablet = useIsTablet();
|
||||||
const {height, width} = calculateDimensions(imageMetadata.height, imageMetadata.width, layoutWidth || getViewPortWidth(false, isTablet));
|
const {height, width} = calculateDimensions(imageMetadata.height, imageMetadata.width, layoutWidth || getViewPortWidth(false, isTablet, true));
|
||||||
const style = getStyleSheet(theme);
|
const style = getStyleSheet(theme);
|
||||||
|
|
||||||
const onError = useCallback(() => {
|
const onError = useCallback(() => {
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,9 @@ import React from 'react';
|
||||||
import {View} from 'react-native';
|
import {View} from 'react-native';
|
||||||
|
|
||||||
import {getMarkdownBlockStyles, getMarkdownTextStyles} from '@utils/markdown';
|
import {getMarkdownBlockStyles, getMarkdownTextStyles} from '@utils/markdown';
|
||||||
import {getStatusColors} from '@utils/message_attachment_colors';
|
import {getStatusColors} from '@utils/message_attachment';
|
||||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||||
|
import {secureGetFromRecord} from '@utils/types';
|
||||||
import {isValidUrl} from '@utils/url';
|
import {isValidUrl} from '@utils/url';
|
||||||
|
|
||||||
import AttachmentActions from './attachment_actions';
|
import AttachmentActions from './attachment_actions';
|
||||||
|
|
@ -62,7 +63,7 @@ export default function MessageAttachment({attachment, channelId, layoutWidth, l
|
||||||
if (attachment.color) {
|
if (attachment.color) {
|
||||||
if (attachment.color[0] === '#') {
|
if (attachment.color[0] === '#') {
|
||||||
borderStyle = {borderLeftColor: attachment.color};
|
borderStyle = {borderLeftColor: attachment.color};
|
||||||
} else if (STATUS_COLORS[attachment.color]) {
|
} else if (secureGetFromRecord(STATUS_COLORS, attachment.color)) {
|
||||||
borderStyle = {borderLeftColor: STATUS_COLORS[attachment.color]};
|
borderStyle = {borderLeftColor: STATUS_COLORS[attachment.color]};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ import {isTablet} from '@utils/helpers';
|
||||||
import {calculateDimensions} from '@utils/images';
|
import {calculateDimensions} from '@utils/images';
|
||||||
import {type BestImage, getNearestPoint} from '@utils/opengraph';
|
import {type BestImage, getNearestPoint} from '@utils/opengraph';
|
||||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||||
|
import {secureGetFromRecord} from '@utils/types';
|
||||||
import {extractFilenameFromUrl, isValidUrl} from '@utils/url';
|
import {extractFilenameFromUrl, isValidUrl} from '@utils/url';
|
||||||
|
|
||||||
import type {GalleryItemType} from '@typings/screens/gallery';
|
import type {GalleryItemType} from '@typings/screens/gallery';
|
||||||
|
|
@ -71,10 +72,7 @@ const OpengraphImage = ({isReplyPost, layoutWidth, location, metadata, openGraph
|
||||||
const imageUrl = (bestImage.secure_url || bestImage.url)!;
|
const imageUrl = (bestImage.secure_url || bestImage.url)!;
|
||||||
const imagesMetadata = metadata?.images;
|
const imagesMetadata = metadata?.images;
|
||||||
|
|
||||||
let ogImage;
|
let ogImage = secureGetFromRecord(imagesMetadata, imageUrl);
|
||||||
if (imagesMetadata && imagesMetadata[imageUrl]) {
|
|
||||||
ogImage = imagesMetadata[imageUrl];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!ogImage) {
|
if (!ogImage) {
|
||||||
ogImage = openGraphImages.find((i: BestImage) => i.url === imageUrl || i.secure_url === imageUrl);
|
ogImage = openGraphImages.find((i: BestImage) => i.url === imageUrl || i.secure_url === imageUrl);
|
||||||
|
|
|
||||||
|
|
@ -96,8 +96,11 @@ const Body = ({
|
||||||
let body;
|
let body;
|
||||||
let message;
|
let message;
|
||||||
|
|
||||||
|
const nBindings = Array.isArray(post.props?.app_bindings) ? post.props?.app_bindings.length : 0;
|
||||||
|
const nAttachments = Array.isArray(post.props?.attachments) ? post.props?.attachments.length : 0;
|
||||||
|
|
||||||
const isReplyPost = Boolean(post.rootId && (!isEphemeral || !hasBeenDeleted) && location !== THREAD);
|
const isReplyPost = Boolean(post.rootId && (!isEphemeral || !hasBeenDeleted) && location !== THREAD);
|
||||||
const hasContent = Boolean((post.metadata?.embeds?.length || (appsEnabled && post.props?.app_bindings?.length)) || post.props?.attachments?.length);
|
const hasContent = Boolean((post.metadata?.embeds?.length || (appsEnabled && nBindings)) || nAttachments);
|
||||||
|
|
||||||
const replyBarStyle = useCallback((): StyleProp<ViewStyle>|undefined => {
|
const replyBarStyle = useCallback((): StyleProp<ViewStyle>|undefined => {
|
||||||
if (!isReplyPost || (isCRTEnabled && location === Screens.PERMALINK)) {
|
if (!isReplyPost || (isCRTEnabled && location === Screens.PERMALINK)) {
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,12 @@
|
||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
import React, {useCallback, useState} from 'react';
|
import React, {useCallback, useMemo, useState} from 'react';
|
||||||
import {type LayoutChangeEvent, ScrollView, useWindowDimensions, View} from 'react-native';
|
import {type LayoutChangeEvent, ScrollView, useWindowDimensions, View} from 'react-native';
|
||||||
import Animated from 'react-native-reanimated';
|
import Animated from 'react-native-reanimated';
|
||||||
|
|
||||||
import Markdown from '@components/markdown';
|
import Markdown from '@components/markdown';
|
||||||
|
import {isChannelMentions} from '@components/markdown/channel_mention/channel_mention';
|
||||||
import {SEARCH} from '@constants/screens';
|
import {SEARCH} from '@constants/screens';
|
||||||
import {useShowMoreAnimatedStyle} from '@hooks/show_more';
|
import {useShowMoreAnimatedStyle} from '@hooks/show_more';
|
||||||
import {getMarkdownTextStyles, getMarkdownBlockStyles} from '@utils/markdown';
|
import {getMarkdownTextStyles, getMarkdownBlockStyles} from '@utils/markdown';
|
||||||
|
|
@ -69,6 +70,10 @@ const Message = ({currentUser, isHighlightWithoutNotificationLicensed, highlight
|
||||||
const onLayout = useCallback((event: LayoutChangeEvent) => setHeight(event.nativeEvent.layout.height), []);
|
const onLayout = useCallback((event: LayoutChangeEvent) => setHeight(event.nativeEvent.layout.height), []);
|
||||||
const onPress = () => setOpen(!open);
|
const onPress = () => setOpen(!open);
|
||||||
|
|
||||||
|
const channelMentions = useMemo(() => {
|
||||||
|
return isChannelMentions(post.props?.channel_mentions) ? post.props.channel_mentions : {};
|
||||||
|
}, [post.props?.channel_mentions]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Animated.View style={animatedStyle}>
|
<Animated.View style={animatedStyle}>
|
||||||
|
|
@ -86,7 +91,7 @@ const Message = ({currentUser, isHighlightWithoutNotificationLicensed, highlight
|
||||||
baseTextStyle={style.message}
|
baseTextStyle={style.message}
|
||||||
blockStyles={blockStyles}
|
blockStyles={blockStyles}
|
||||||
channelId={post.channelId}
|
channelId={post.channelId}
|
||||||
channelMentions={post.props?.channel_mentions}
|
channelMentions={channelMentions}
|
||||||
imagesMetadata={post.metadata?.images}
|
imagesMetadata={post.metadata?.images}
|
||||||
isEdited={isEdited}
|
isEdited={isEdited}
|
||||||
isReplyPost={isReplyPost}
|
isReplyPost={isReplyPost}
|
||||||
|
|
@ -100,7 +105,7 @@ const Message = ({currentUser, isHighlightWithoutNotificationLicensed, highlight
|
||||||
highlightKeys={isHighlightWithoutNotificationLicensed ? (currentUser?.highlightKeys ?? EMPTY_HIGHLIGHT_KEYS) : EMPTY_HIGHLIGHT_KEYS}
|
highlightKeys={isHighlightWithoutNotificationLicensed ? (currentUser?.highlightKeys ?? EMPTY_HIGHLIGHT_KEYS) : EMPTY_HIGHLIGHT_KEYS}
|
||||||
searchPatterns={searchPatterns}
|
searchPatterns={searchPatterns}
|
||||||
theme={theme}
|
theme={theme}
|
||||||
isUnsafeLinksPost={post.props.unsafe_links && post.props.unsafe_links !== ''}
|
isUnsafeLinksPost={Boolean(post.props?.unsafe_links && post.props.unsafe_links !== '')}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import {useTheme} from '@context/theme';
|
||||||
import {DEFAULT_LOCALE} from '@i18n';
|
import {DEFAULT_LOCALE} from '@i18n';
|
||||||
import {postUserDisplayName} from '@utils/post';
|
import {postUserDisplayName} from '@utils/post';
|
||||||
import {makeStyleSheetFromTheme} from '@utils/theme';
|
import {makeStyleSheetFromTheme} from '@utils/theme';
|
||||||
|
import {ensureString} from '@utils/types';
|
||||||
import {typography} from '@utils/typography';
|
import {typography} from '@utils/typography';
|
||||||
import {displayUsername, getUserCustomStatus, getUserTimezone, isCustomStatusExpired} from '@utils/user';
|
import {displayUsername, getUserCustomStatus, getUserTimezone, isCustomStatusExpired} from '@utils/user';
|
||||||
|
|
||||||
|
|
@ -96,6 +97,8 @@ const Header = (props: HeaderProps) => {
|
||||||
isCustomStatusEnabled && displayName && customStatus &&
|
isCustomStatusEnabled && displayName && customStatus &&
|
||||||
!(isSystemPost || author?.isBot || isAutoResponse || isWebHook),
|
!(isSystemPost || author?.isBot || isAutoResponse || isWebHook),
|
||||||
) && !isCustomStatusExpired(author) && Boolean(customStatus?.emoji);
|
) && !isCustomStatusExpired(author) && Boolean(customStatus?.emoji);
|
||||||
|
const userIconOverride = ensureString(post.props?.override_icon_url);
|
||||||
|
const usernameOverride = ensureString(post.props?.override_username);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
@ -109,9 +112,9 @@ const Header = (props: HeaderProps) => {
|
||||||
rootPostAuthor={rootAuthorDisplayName}
|
rootPostAuthor={rootAuthorDisplayName}
|
||||||
shouldRenderReplyButton={shouldRenderReplyButton}
|
shouldRenderReplyButton={shouldRenderReplyButton}
|
||||||
theme={theme}
|
theme={theme}
|
||||||
userIconOverride={post.props?.override_icon_url}
|
userIconOverride={userIconOverride}
|
||||||
userId={post.userId}
|
userId={post.userId}
|
||||||
usernameOverride={post.props?.override_username}
|
usernameOverride={usernameOverride}
|
||||||
showCustomStatusEmoji={showCustomStatusEmoji}
|
showCustomStatusEmoji={showCustomStatusEmoji}
|
||||||
customStatus={customStatus!}
|
customStatus={customStatus!}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import {useTheme} from '@context/theme';
|
||||||
import {t} from '@i18n';
|
import {t} from '@i18n';
|
||||||
import {getMarkdownTextStyles} from '@utils/markdown';
|
import {getMarkdownTextStyles} from '@utils/markdown';
|
||||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||||
|
import {secureGetFromRecord, ensureString} from '@utils/types';
|
||||||
import {typography} from '@utils/typography';
|
import {typography} from '@utils/typography';
|
||||||
|
|
||||||
import type PostModel from '@typings/database/models/servers/post';
|
import type PostModel from '@typings/database/models/servers/post';
|
||||||
|
|
@ -100,8 +101,8 @@ const renderHeaderChangeMessage = ({post, author, location, styles, intl, theme}
|
||||||
}
|
}
|
||||||
|
|
||||||
const username = renderUsername(author.username);
|
const username = renderUsername(author.username);
|
||||||
const oldHeader = post.props?.old_header;
|
const oldHeader = ensureString(post.props?.old_header);
|
||||||
const newHeader = post.props?.new_header;
|
const newHeader = ensureString(post.props?.new_header);
|
||||||
let localeHolder;
|
let localeHolder;
|
||||||
|
|
||||||
if (post.props?.new_header) {
|
if (post.props?.new_header) {
|
||||||
|
|
@ -143,12 +144,12 @@ const renderPurposeChangeMessage = ({post, author, location, styles, intl, theme
|
||||||
}
|
}
|
||||||
|
|
||||||
const username = renderUsername(author.username);
|
const username = renderUsername(author.username);
|
||||||
const oldPurpose = post.props?.old_purpose;
|
const oldPurpose = ensureString(post.props?.old_purpose);
|
||||||
const newPurpose = post.props?.new_purpose;
|
const newPurpose = ensureString(post.props?.new_purpose);
|
||||||
let localeHolder;
|
let localeHolder;
|
||||||
|
|
||||||
if (post.props?.new_purpose) {
|
if (newPurpose) {
|
||||||
if (post.props?.old_purpose) {
|
if (oldPurpose) {
|
||||||
localeHolder = {
|
localeHolder = {
|
||||||
id: t('mobile.system_message.update_channel_purpose_message.updated_from'),
|
id: t('mobile.system_message.update_channel_purpose_message.updated_from'),
|
||||||
defaultMessage: '{username} updated the channel purpose from: {oldPurpose} to: {newPurpose}',
|
defaultMessage: '{username} updated the channel purpose from: {oldPurpose} to: {newPurpose}',
|
||||||
|
|
@ -165,7 +166,7 @@ const renderPurposeChangeMessage = ({post, author, location, styles, intl, theme
|
||||||
|
|
||||||
values = {username, oldPurpose, newPurpose};
|
values = {username, oldPurpose, newPurpose};
|
||||||
return renderMessage({post, styles, intl, location, localeHolder, values, skipMarkdown: true, theme});
|
return renderMessage({post, styles, intl, location, localeHolder, values, skipMarkdown: true, theme});
|
||||||
} else if (post.props?.old_purpose) {
|
} else if (oldPurpose) {
|
||||||
localeHolder = {
|
localeHolder = {
|
||||||
id: t('mobile.system_message.update_channel_purpose_message.removed'),
|
id: t('mobile.system_message.update_channel_purpose_message.removed'),
|
||||||
defaultMessage: '{username} removed the channel purpose (was: {oldPurpose})',
|
defaultMessage: '{username} removed the channel purpose (was: {oldPurpose})',
|
||||||
|
|
@ -179,8 +180,8 @@ const renderPurposeChangeMessage = ({post, author, location, styles, intl, theme
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderDisplayNameChangeMessage = ({post, author, location, styles, intl, theme}: RenderersProps) => {
|
const renderDisplayNameChangeMessage = ({post, author, location, styles, intl, theme}: RenderersProps) => {
|
||||||
const oldDisplayName = post.props?.old_displayname;
|
const oldDisplayName = ensureString(post.props?.old_displayname);
|
||||||
const newDisplayName = post.props?.new_displayname;
|
const newDisplayName = ensureString(post.props?.new_displayname);
|
||||||
|
|
||||||
if (!(author?.username)) {
|
if (!(author?.username)) {
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -223,13 +224,13 @@ const renderUnarchivedMessage = ({post, author, location, styles, intl, theme}:
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderAddGuestToChannelMessage = ({post, location, styles, intl, theme}: RenderersProps, hideGuestTags: boolean) => {
|
const renderAddGuestToChannelMessage = ({post, location, styles, intl, theme}: RenderersProps, hideGuestTags: boolean) => {
|
||||||
if (!post.props.username || !post.props.addedUsername) {
|
const username = renderUsername(ensureString(post.props?.username));
|
||||||
|
const addedUsername = renderUsername(ensureString(post.props?.addedUsername));
|
||||||
|
|
||||||
|
if (!username || !addedUsername) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const username = renderUsername(post.props.username);
|
|
||||||
const addedUsername = renderUsername(post.props.addedUsername);
|
|
||||||
|
|
||||||
const localeHolder = hideGuestTags ? postTypeMessages[Post.POST_TYPES.ADD_TO_CHANNEL].one : {
|
const localeHolder = hideGuestTags ? postTypeMessages[Post.POST_TYPES.ADD_TO_CHANNEL].one : {
|
||||||
id: t('api.channel.add_guest.added'),
|
id: t('api.channel.add_guest.added'),
|
||||||
defaultMessage: '{addedUsername} added to the channel as a guest by {username}.',
|
defaultMessage: '{addedUsername} added to the channel as a guest by {username}.',
|
||||||
|
|
@ -240,11 +241,11 @@ const renderAddGuestToChannelMessage = ({post, location, styles, intl, theme}: R
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderGuestJoinChannelMessage = ({post, styles, location, intl, theme}: RenderersProps, hideGuestTags: boolean) => {
|
const renderGuestJoinChannelMessage = ({post, styles, location, intl, theme}: RenderersProps, hideGuestTags: boolean) => {
|
||||||
if (!post.props.username) {
|
const username = renderUsername(ensureString(post.props?.username));
|
||||||
|
if (!username) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const username = renderUsername(post.props.username);
|
|
||||||
const localeHolder = hideGuestTags ? postTypeMessages[Post.POST_TYPES.JOIN_CHANNEL].one : {
|
const localeHolder = hideGuestTags ? postTypeMessages[Post.POST_TYPES.JOIN_CHANNEL].one : {
|
||||||
id: t('api.channel.guest_join_channel.post_and_forget'),
|
id: t('api.channel.guest_join_channel.post_and_forget'),
|
||||||
defaultMessage: '{username} joined the channel as a guest.',
|
defaultMessage: '{username} joined the channel as a guest.',
|
||||||
|
|
@ -276,7 +277,7 @@ export const SystemMessage = ({post, location, author, hideGuestTags}: SystemMes
|
||||||
return renderAddGuestToChannelMessage({post, author, location, styles, intl, theme}, hideGuestTags);
|
return renderAddGuestToChannelMessage({post, author, location, styles, intl, theme}, hideGuestTags);
|
||||||
}
|
}
|
||||||
|
|
||||||
const renderer = systemMessageRenderers[post.type];
|
const renderer = secureGetFromRecord(systemMessageRenderers, post.type);
|
||||||
if (!renderer) {
|
if (!renderer) {
|
||||||
return (
|
return (
|
||||||
<Markdown
|
<Markdown
|
||||||
|
|
|
||||||
|
|
@ -101,7 +101,7 @@ const ProgressiveImage = ({
|
||||||
transition={300}
|
transition={300}
|
||||||
style={[StyleSheet.absoluteFill, imageStyle]}
|
style={[StyleSheet.absoluteFill, imageStyle]}
|
||||||
source={(showImage) ? {uri: imageUri} : undefined}
|
source={(showImage) ? {uri: imageUri} : undefined}
|
||||||
autoplay={false}
|
autoplay={true}
|
||||||
/>
|
/>
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -163,15 +163,15 @@ export default function SelectedUsers({
|
||||||
|
|
||||||
const users = useMemo(() => {
|
const users = useMemo(() => {
|
||||||
const u = [];
|
const u = [];
|
||||||
for (const id of Object.keys(selectedIds)) {
|
for (const [id, user] of Object.entries(selectedIds)) {
|
||||||
if (!selectedIds[id]) {
|
if (!user) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
u.push(
|
u.push(
|
||||||
<SelectedUser
|
<SelectedUser
|
||||||
key={id}
|
key={id}
|
||||||
user={selectedIds[id]}
|
user={user}
|
||||||
teammateNameDisplay={teammateNameDisplay}
|
teammateNameDisplay={teammateNameDisplay}
|
||||||
onRemove={onRemove}
|
onRemove={onRemove}
|
||||||
testID={`${testID}.selected_user`}
|
testID={`${testID}.selected_user`}
|
||||||
|
|
|
||||||
243
app/components/team_sidebar/team_list/index.test.tsx
Normal file
243
app/components/team_sidebar/team_list/index.test.tsx
Normal file
|
|
@ -0,0 +1,243 @@
|
||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import {render, waitFor} from '@testing-library/react-native';
|
||||||
|
import React from 'react';
|
||||||
|
import {of as of$} from 'rxjs';
|
||||||
|
|
||||||
|
import TeamList from './team_list';
|
||||||
|
|
||||||
|
import TeamListWrapper from './';
|
||||||
|
|
||||||
|
// Mock database queries
|
||||||
|
jest.mock('@queries/servers/team', () => {
|
||||||
|
const {of} = require('rxjs');
|
||||||
|
return {
|
||||||
|
queryMyTeams: jest.fn(() => of([])),
|
||||||
|
queryJoinedTeams: jest.fn(() => ({
|
||||||
|
observe: jest.fn(() => of([])),
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
jest.mock('@queries/servers/preference', () => {
|
||||||
|
const {of} = require('rxjs');
|
||||||
|
return {
|
||||||
|
queryPreferencesByCategoryAndName: jest.fn(() => ({
|
||||||
|
observe: jest.fn(() => of([])),
|
||||||
|
observeWithColumns: jest.fn(() => of([])),
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const {queryPreferencesByCategoryAndName} = require('@queries/servers/preference');
|
||||||
|
const {queryMyTeams, queryJoinedTeams} = require('@queries/servers/team');
|
||||||
|
|
||||||
|
// Mock WatermelonDB HOCs
|
||||||
|
jest.mock('@nozbe/watermelondb/react', () => ({
|
||||||
|
withDatabase: jest.fn((Component) => Component),
|
||||||
|
withObservables: jest.fn((_, observableMapper) => {
|
||||||
|
// eslint-disable-next-line react/display-name
|
||||||
|
return (Component: any) => (props: any) => {
|
||||||
|
const observables = observableMapper(props);
|
||||||
|
const mockedProps: Record<string, any> = {};
|
||||||
|
// eslint-disable-next-line guard-for-in
|
||||||
|
for (const key in observables) {
|
||||||
|
observables[key].subscribe((value: any) => {
|
||||||
|
mockedProps[key] = value;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Component
|
||||||
|
{...props}
|
||||||
|
{...mockedProps}
|
||||||
|
/>);
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('./team_list', () => {
|
||||||
|
return jest.fn(() => null); // Mock implementation for TeamList
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('withTeams HOC', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle valid data correctly', async () => {
|
||||||
|
queryMyTeams.mockReturnValue({
|
||||||
|
observe: jest.fn(() => of$([{id: '1', roles: 'team_user'}])),
|
||||||
|
});
|
||||||
|
|
||||||
|
queryJoinedTeams.mockReturnValue({
|
||||||
|
observe: jest.fn(() => of$([
|
||||||
|
{id: '1', displayName: 'Team 1'},
|
||||||
|
{id: '2', displayName: 'Team 2'},
|
||||||
|
])),
|
||||||
|
});
|
||||||
|
|
||||||
|
queryPreferencesByCategoryAndName.mockReturnValue({
|
||||||
|
observeWithColumns: jest.fn(() => of$([{value: '1,2'}])),
|
||||||
|
});
|
||||||
|
|
||||||
|
render(<TeamListWrapper/>);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(TeamList).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
myOrderedTeams: [{id: '1', roles: 'team_user'}],
|
||||||
|
}),
|
||||||
|
expect.anything(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle missing team IDs gracefully', async () => {
|
||||||
|
queryMyTeams.mockReturnValue({
|
||||||
|
observe: jest.fn(() => of$([{id: '1', roles: 'team_user'}])),
|
||||||
|
});
|
||||||
|
|
||||||
|
queryJoinedTeams.mockReturnValue({
|
||||||
|
observe: jest.fn(() => of$([
|
||||||
|
{id: '1', displayName: 'Team 1'},
|
||||||
|
{id: undefined, displayName: 'Team 2'},
|
||||||
|
])),
|
||||||
|
});
|
||||||
|
|
||||||
|
queryPreferencesByCategoryAndName.mockReturnValue({
|
||||||
|
observeWithColumns: jest.fn(() => of$([{value: '1,2'}])),
|
||||||
|
});
|
||||||
|
|
||||||
|
render(<TeamListWrapper/>);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(TeamList).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
myOrderedTeams: [{id: '1', roles: 'team_user'}],
|
||||||
|
}),
|
||||||
|
expect.anything(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle empty team order preferences gracefully', async () => {
|
||||||
|
queryMyTeams.mockReturnValue({
|
||||||
|
observe: jest.fn(() => of$([{id: '1', roles: 'team_user'}])),
|
||||||
|
});
|
||||||
|
|
||||||
|
queryJoinedTeams.mockReturnValue({
|
||||||
|
observe: jest.fn(() => of$([
|
||||||
|
{id: '1', displayName: 'Team 1'},
|
||||||
|
{id: '2', displayName: 'Team 2'},
|
||||||
|
])),
|
||||||
|
});
|
||||||
|
|
||||||
|
queryPreferencesByCategoryAndName.mockReturnValue({
|
||||||
|
observeWithColumns: jest.fn(() => of$([])),
|
||||||
|
});
|
||||||
|
|
||||||
|
render(<TeamListWrapper/>);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(TeamList).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
myOrderedTeams: [{id: '1', roles: 'team_user'}],
|
||||||
|
}),
|
||||||
|
expect.anything(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle undefined in team order preferences gracefully', async () => {
|
||||||
|
queryMyTeams.mockReturnValue({
|
||||||
|
observe: jest.fn(() => of$([
|
||||||
|
{id: '1', roles: 'team_user'},
|
||||||
|
{id: '2', roles: 'team_user'},
|
||||||
|
])),
|
||||||
|
});
|
||||||
|
|
||||||
|
queryJoinedTeams.mockReturnValue({
|
||||||
|
observe: jest.fn(() => of$([
|
||||||
|
{id: '1', displayName: 'Team 1'},
|
||||||
|
{id: '2', displayName: 'Team 2'},
|
||||||
|
])),
|
||||||
|
});
|
||||||
|
|
||||||
|
queryPreferencesByCategoryAndName.mockReturnValue({
|
||||||
|
observeWithColumns: jest.fn(() => of$([{value: '1,,2'}])),
|
||||||
|
});
|
||||||
|
|
||||||
|
render(<TeamListWrapper/>);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(TeamList).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
myOrderedTeams: [
|
||||||
|
{id: '1', roles: 'team_user'},
|
||||||
|
{id: '2', roles: 'team_user'},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
expect.anything(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle wrong data in team order preferences gracefully', async () => {
|
||||||
|
queryMyTeams.mockReturnValue({
|
||||||
|
observe: jest.fn(() => of$([
|
||||||
|
{id: '1', roles: 'team_user'},
|
||||||
|
{id: '2', roles: 'team_user'},
|
||||||
|
])),
|
||||||
|
});
|
||||||
|
|
||||||
|
queryJoinedTeams.mockReturnValue({
|
||||||
|
observe: jest.fn(() => of$([
|
||||||
|
{id: '1', displayName: 'Team 1'},
|
||||||
|
{id: '2', displayName: 'Team 2'},
|
||||||
|
])),
|
||||||
|
});
|
||||||
|
|
||||||
|
queryPreferencesByCategoryAndName.mockReturnValue({
|
||||||
|
observeWithColumns: jest.fn(() => of$([{value: '1,undefined,2'}])),
|
||||||
|
});
|
||||||
|
|
||||||
|
render(<TeamListWrapper/>);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(TeamList).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
myOrderedTeams: [
|
||||||
|
{id: '1', roles: 'team_user'},
|
||||||
|
{id: '2', roles: 'team_user'},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
expect.anything(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle empty teams list gracefully', async () => {
|
||||||
|
queryMyTeams.mockReturnValue({
|
||||||
|
observe: jest.fn(() => of$([])),
|
||||||
|
});
|
||||||
|
|
||||||
|
queryJoinedTeams.mockReturnValue({
|
||||||
|
observe: jest.fn(() => of$([])),
|
||||||
|
});
|
||||||
|
|
||||||
|
queryPreferencesByCategoryAndName.mockReturnValue({
|
||||||
|
observeWithColumns: jest.fn(() => of$([])),
|
||||||
|
});
|
||||||
|
|
||||||
|
render(<TeamListWrapper/>);
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(TeamList).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
myOrderedTeams: [],
|
||||||
|
}),
|
||||||
|
expect.anything(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -14,7 +14,6 @@ import {queryJoinedTeams, queryMyTeams} from '@queries/servers/team';
|
||||||
import TeamList from './team_list';
|
import TeamList from './team_list';
|
||||||
|
|
||||||
import type {WithDatabaseArgs} from '@typings/database/database';
|
import type {WithDatabaseArgs} from '@typings/database/database';
|
||||||
import type MyTeamModel from '@typings/database/models/servers/my_team';
|
|
||||||
|
|
||||||
const withTeams = withObservables([], ({database}: WithDatabaseArgs) => {
|
const withTeams = withObservables([], ({database}: WithDatabaseArgs) => {
|
||||||
const myTeams = queryMyTeams(database).observe();
|
const myTeams = queryMyTeams(database).observe();
|
||||||
|
|
@ -26,26 +25,27 @@ const withTeams = withObservables([], ({database}: WithDatabaseArgs) => {
|
||||||
switchMap((p) => (p.length ? of$(p[0].value.split(',')) : of$([]))),
|
switchMap((p) => (p.length ? of$(p[0].value.split(',')) : of$([]))),
|
||||||
);
|
);
|
||||||
const myOrderedTeams = combineLatest([myTeams, order, teamIds]).pipe(
|
const myOrderedTeams = combineLatest([myTeams, order, teamIds]).pipe(
|
||||||
map(([ts, o, tids]) => {
|
map(([memberships, o, teams]) => {
|
||||||
let ids: string[] = o;
|
const sortedTeamIds = new Set(o);
|
||||||
if (!o.length) {
|
const membershipMap = new Map(memberships.map((m) => [m.id, m]));
|
||||||
ids = tids.
|
|
||||||
|
if (sortedTeamIds.size) {
|
||||||
|
const mySortedTeams = [...sortedTeamIds].
|
||||||
|
filter((id) => id && membershipMap.has(id)).
|
||||||
|
map((id) => membershipMap.get(id)!);
|
||||||
|
|
||||||
|
const extraTeams = teams.
|
||||||
|
filter((t) => t.id && !sortedTeamIds.has(t.id) && membershipMap.has(t.id)).
|
||||||
sort((a, b) => a.displayName.toLocaleLowerCase().localeCompare(b.displayName.toLocaleLowerCase())).
|
sort((a, b) => a.displayName.toLocaleLowerCase().localeCompare(b.displayName.toLocaleLowerCase())).
|
||||||
map((t) => t.id);
|
map((t) => membershipMap.get(t.id)!);
|
||||||
|
|
||||||
|
return [...mySortedTeams, ...extraTeams];
|
||||||
}
|
}
|
||||||
|
|
||||||
const teamMap = ts.reduce<{[x: string]: MyTeamModel}>((result, team) => {
|
return teams.
|
||||||
result[team.id] = team;
|
filter((t) => t.id && membershipMap.has(t.id)).
|
||||||
return result;
|
sort((a, b) => a.displayName.toLocaleLowerCase().localeCompare(b.displayName.toLocaleLowerCase())).
|
||||||
}, {});
|
map((t) => membershipMap.get(t.id)!);
|
||||||
|
|
||||||
return ids.reduce<MyTeamModel[]>((result, id) => {
|
|
||||||
const t = teamMap[id];
|
|
||||||
if (t) {
|
|
||||||
result.push(t);
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}, []);
|
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ export const IMAGE_MIN_DIMENSION = 50;
|
||||||
export const MAX_GIF_SIZE = 100 * 1024 * 1024;
|
export const MAX_GIF_SIZE = 100 * 1024 * 1024;
|
||||||
export const VIEWPORT_IMAGE_OFFSET = 70;
|
export const VIEWPORT_IMAGE_OFFSET = 70;
|
||||||
export const VIEWPORT_IMAGE_REPLY_OFFSET = 11;
|
export const VIEWPORT_IMAGE_REPLY_OFFSET = 11;
|
||||||
|
export const VIEWPORT_IMAGE_ATTACHMENT_OFFSET = 31.5; // (2 * 12) MessageAttachment Padding + (2,5 + 5) AttachmentImage Padding
|
||||||
export const MAX_RESOLUTION = 7680 * 4320; // 8K, ~33MPX
|
export const MAX_RESOLUTION = 7680 * 4320; // 8K, ~33MPX
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
|
@ -15,4 +16,5 @@ export default {
|
||||||
MAX_RESOLUTION,
|
MAX_RESOLUTION,
|
||||||
VIEWPORT_IMAGE_OFFSET,
|
VIEWPORT_IMAGE_OFFSET,
|
||||||
VIEWPORT_IMAGE_REPLY_OFFSET,
|
VIEWPORT_IMAGE_REPLY_OFFSET,
|
||||||
|
VIEWPORT_IMAGE_ATTACHMENT_OFFSET,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import ThemeProvider from '@context/theme';
|
||||||
import UserLocaleProvider from '@context/user_locale';
|
import UserLocaleProvider from '@context/user_locale';
|
||||||
import DatabaseManager from '@database/manager';
|
import DatabaseManager from '@database/manager';
|
||||||
import {subscribeActiveServers} from '@database/subscription/servers';
|
import {subscribeActiveServers} from '@database/subscription/servers';
|
||||||
|
import {secureGetFromRecord} from '@utils/types';
|
||||||
|
|
||||||
import type {Database} from '@nozbe/watermelondb';
|
import type {Database} from '@nozbe/watermelondb';
|
||||||
import type ServersModel from '@typings/database/models/app/servers';
|
import type ServersModel from '@typings/database/models/app/servers';
|
||||||
|
|
@ -31,7 +32,7 @@ export function withServerDatabase<T extends JSX.IntrinsicAttributes>(Component:
|
||||||
|
|
||||||
if (server) {
|
if (server) {
|
||||||
const database =
|
const database =
|
||||||
DatabaseManager.serverDatabases[server.url]?.database;
|
secureGetFromRecord(DatabaseManager.serverDatabases, server.url)?.database;
|
||||||
|
|
||||||
if (database) {
|
if (database) {
|
||||||
setState({
|
setState({
|
||||||
|
|
|
||||||
|
|
@ -63,9 +63,12 @@ export const transformThreadRecord = ({action, database, value}: TransformerArgs
|
||||||
*/
|
*/
|
||||||
export const transformThreadParticipantRecord = ({action, database, value}: TransformerArgs): Promise<ThreadParticipantModel> => {
|
export const transformThreadParticipantRecord = ({action, database, value}: TransformerArgs): Promise<ThreadParticipantModel> => {
|
||||||
const raw = value.raw as ThreadParticipant;
|
const raw = value.raw as ThreadParticipant;
|
||||||
|
const record = value.record as ThreadParticipantModel;
|
||||||
|
const isCreateAction = action === OperationType.CREATE;
|
||||||
|
|
||||||
// id of participant comes from server response
|
// id of participant comes from server response
|
||||||
const fieldsMapper = (participant: ThreadParticipantModel) => {
|
const fieldsMapper = (participant: ThreadParticipantModel) => {
|
||||||
|
participant._raw.id = isCreateAction ? `${raw.thread_id}-${raw.id}` : record.id;
|
||||||
participant.threadId = raw.thread_id;
|
participant.threadId = raw.thread_id;
|
||||||
participant.userId = raw.id;
|
participant.userId = raw.id;
|
||||||
};
|
};
|
||||||
|
|
@ -81,8 +84,11 @@ export const transformThreadParticipantRecord = ({action, database, value}: Tran
|
||||||
|
|
||||||
export const transformThreadInTeamRecord = ({action, database, value}: TransformerArgs): Promise<ThreadInTeamModel> => {
|
export const transformThreadInTeamRecord = ({action, database, value}: TransformerArgs): Promise<ThreadInTeamModel> => {
|
||||||
const raw = value.raw as ThreadInTeam;
|
const raw = value.raw as ThreadInTeam;
|
||||||
|
const record = value.record as ThreadInTeamModel;
|
||||||
|
const isCreateAction = action === OperationType.CREATE;
|
||||||
|
|
||||||
const fieldsMapper = (threadInTeam: ThreadInTeamModel) => {
|
const fieldsMapper = (threadInTeam: ThreadInTeamModel) => {
|
||||||
|
threadInTeam._raw.id = isCreateAction ? `${raw.thread_id}-${raw.team_id}` : record.id;
|
||||||
threadInTeam.threadId = raw.thread_id;
|
threadInTeam.threadId = raw.thread_id;
|
||||||
threadInTeam.teamId = raw.team_id;
|
threadInTeam.teamId = raw.team_id;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
|
|
||||||
import {General} from '@constants';
|
import {General} from '@constants';
|
||||||
import {MENTIONS_REGEX} from '@constants/autocomplete';
|
import {MENTIONS_REGEX} from '@constants/autocomplete';
|
||||||
|
import {isMessageAttachmentArray} from '@utils/message_attachment';
|
||||||
|
|
||||||
export const getNeededAtMentionedUsernames = (usernames: Set<string>, posts: Post[], excludeUsername?: string) => {
|
export const getNeededAtMentionedUsernames = (usernames: Set<string>, posts: Post[], excludeUsername?: string) => {
|
||||||
const usernamesToLoad = new Set<string>();
|
const usernamesToLoad = new Set<string>();
|
||||||
|
|
@ -36,8 +37,9 @@ export const getNeededAtMentionedUsernames = (usernames: Set<string>, posts: Pos
|
||||||
// These correspond to the fields searched by getMentionsEnabledFields on the server
|
// These correspond to the fields searched by getMentionsEnabledFields on the server
|
||||||
findNeededUsernames(post.message);
|
findNeededUsernames(post.message);
|
||||||
|
|
||||||
if (post.props?.attachments) {
|
const attachments = isMessageAttachmentArray(post.props?.attachments) ? post.props.attachments : undefined;
|
||||||
for (const attachment of post.props.attachments) {
|
if (attachments) {
|
||||||
|
for (const attachment of attachments) {
|
||||||
findNeededUsernames(attachment.pretext);
|
findNeededUsernames(attachment.pretext);
|
||||||
findNeededUsernames(attachment.text);
|
findNeededUsernames(attachment.text);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import xRegExp from 'xregexp';
|
||||||
|
|
||||||
import {General} from '@constants';
|
import {General} from '@constants';
|
||||||
|
|
||||||
|
import type {Model} from '@nozbe/watermelondb';
|
||||||
import type ChannelModel from '@typings/database/models/servers/channel';
|
import type ChannelModel from '@typings/database/models/servers/channel';
|
||||||
import type MyChannelModel from '@typings/database/models/servers/my_channel';
|
import type MyChannelModel from '@typings/database/models/servers/my_channel';
|
||||||
|
|
||||||
|
|
@ -91,3 +92,15 @@ export const filterAndSortMyChannels = ([myChannels, channels, notifyProps]: Fil
|
||||||
// Matches letters from any alphabet and numbers
|
// Matches letters from any alphabet and numbers
|
||||||
const sqliteLikeStringRegex = xRegExp('[^\\p{L}\\p{Nd}]', 'g');
|
const sqliteLikeStringRegex = xRegExp('[^\\p{L}\\p{Nd}]', 'g');
|
||||||
export const sanitizeLikeString = (value: string) => value.replace(sqliteLikeStringRegex, '_');
|
export const sanitizeLikeString = (value: string) => value.replace(sqliteLikeStringRegex, '_');
|
||||||
|
|
||||||
|
export function removeDuplicatesModels(array: Model[]) {
|
||||||
|
const seen = new Set();
|
||||||
|
return array.filter((item) => {
|
||||||
|
const key = `${item.collection.table}-${item.id}`;
|
||||||
|
if (seen.has(key)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
seen.add(key);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -60,7 +60,7 @@ class NetworkManager {
|
||||||
sessionConfiguration: {
|
sessionConfiguration: {
|
||||||
allowsCellularAccess: true,
|
allowsCellularAccess: true,
|
||||||
waitsForConnectivity: false,
|
waitsForConnectivity: false,
|
||||||
httpMaximumConnectionsPerHost: 10,
|
httpMaximumConnectionsPerHost: 100,
|
||||||
cancelRequestsOnUnauthorized: true,
|
cancelRequestsOnUnauthorized: true,
|
||||||
},
|
},
|
||||||
retryPolicyConfiguration: {
|
retryPolicyConfiguration: {
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ const enhanced = withObservables(['post'], ({serverUrl, post, database}: OwnProp
|
||||||
);
|
);
|
||||||
|
|
||||||
// The call is not active, so return early with what we need to render the post.
|
// The call is not active, so return early with what we need to render the post.
|
||||||
if (post.props.end_at) {
|
if (post.props?.end_at) {
|
||||||
return {
|
return {
|
||||||
currentUser,
|
currentUser,
|
||||||
isMilitaryTime,
|
isMilitaryTime,
|
||||||
|
|
|
||||||
|
|
@ -194,12 +194,12 @@ describe('getCallPropsFromPost', () => {
|
||||||
|
|
||||||
const props = getCallPropsFromPost(post);
|
const props = getCallPropsFromPost(post);
|
||||||
|
|
||||||
expect(props.title).toBe(post.props.title);
|
expect(props.title).toBe(post.props?.title);
|
||||||
expect(props.start_at).toBe(post.props.start_at);
|
expect(props.start_at).toBe(post.props?.start_at);
|
||||||
expect(props.end_at).toBe(post.props.end_at);
|
expect(props.end_at).toBe(post.props?.end_at);
|
||||||
expect(props.recordings).toBe(post.props.recordings);
|
expect(props.recordings).toBe(post.props?.recordings);
|
||||||
expect(props.recording_files).toBe(post.props.recording_files);
|
expect(props.recording_files).toBe(post.props?.recording_files);
|
||||||
expect(props.transcriptions).toBe(post.props.transcriptions);
|
expect(props.transcriptions).toBe(post.props?.transcriptions);
|
||||||
expect(props.participants).toBe(post.props.participants);
|
expect(props.participants).toBe(post.props?.participants);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
import {makeCallsBaseAndBadgeRGB, rgbToCSS} from '@mattermost/calls';
|
import {makeCallsBaseAndBadgeRGB, rgbToCSS} from '@mattermost/calls';
|
||||||
|
import {type CallsConfig, type CallPostProps, isCaption, type Caption, isCallJobMetadata, type CallJobMetadata} from '@mattermost/calls/lib/types';
|
||||||
import {Alert} from 'react-native';
|
import {Alert} from 'react-native';
|
||||||
import {SelectedTrackType, TextTrackType, type ISO639_1, type SelectedTrack, type TextTracks} from 'react-native-video';
|
import {SelectedTrackType, TextTrackType, type ISO639_1, type SelectedTrack, type TextTracks} from 'react-native-video';
|
||||||
|
|
||||||
|
|
@ -9,6 +10,7 @@ import {buildFileUrl} from '@actions/remote/file';
|
||||||
import {Calls, Post} from '@constants';
|
import {Calls, Post} from '@constants';
|
||||||
import {NOTIFICATION_SUB_TYPE} from '@constants/push_notification';
|
import {NOTIFICATION_SUB_TYPE} from '@constants/push_notification';
|
||||||
import {isMinimumServerVersion} from '@utils/helpers';
|
import {isMinimumServerVersion} from '@utils/helpers';
|
||||||
|
import {ensureNumber, ensureString, isArrayOf, isRecordOf, isStringArray} from '@utils/types';
|
||||||
import {displayUsername} from '@utils/user';
|
import {displayUsername} from '@utils/user';
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
|
|
@ -17,7 +19,6 @@ import type {
|
||||||
CallsTheme,
|
CallsTheme,
|
||||||
CallsVersion,
|
CallsVersion,
|
||||||
} from '@calls/types/calls';
|
} from '@calls/types/calls';
|
||||||
import type {CallsConfig, Caption, CallPostProps} from '@mattermost/calls/lib/types';
|
|
||||||
import type PostModel from '@typings/database/models/servers/post';
|
import type PostModel from '@typings/database/models/servers/post';
|
||||||
import type UserModel from '@typings/database/models/servers/user';
|
import type UserModel from '@typings/database/models/servers/user';
|
||||||
import type {IntlShape} from 'react-intl';
|
import type {IntlShape} from 'react-intl';
|
||||||
|
|
@ -217,17 +218,17 @@ export function isCallsStartedMessage(payload?: NotificationData) {
|
||||||
return (payload?.message === 'You\'ve been invited to a call' || callsMessageRegex.test(payload?.message || ''));
|
return (payload?.message === 'You\'ve been invited to a call' || callsMessageRegex.test(payload?.message || ''));
|
||||||
}
|
}
|
||||||
|
|
||||||
export const hasCaptions = (postProps?: Record<string, any> & { captions?: Caption[] }): boolean => {
|
export const hasCaptions = (postProps?: Record<string, unknown>): boolean => {
|
||||||
return !(!postProps || !postProps.captions?.[0]);
|
return Boolean(isArrayOf<Caption>(postProps?.captions, isCaption) && postProps.captions[0]);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getTranscriptionUri = (serverUrl: string, postProps?: Record<string, any> & { captions?: Caption[] }): {
|
export const getTranscriptionUri = (serverUrl: string, postProps?: Record<string, unknown>): {
|
||||||
tracks?: TextTracks;
|
tracks?: TextTracks;
|
||||||
selected: SelectedTrack;
|
selected: SelectedTrack;
|
||||||
} => {
|
} => {
|
||||||
// Note: We're not using hasCaptions above because this tells typescript that the caption exists later.
|
// Note: We're not using hasCaptions above because this tells typescript that the caption exists later.
|
||||||
// We could use some fancy typescript to do the same, but it's not worth the complexity.
|
// We could use some fancy typescript to do the same, but it's not worth the complexity.
|
||||||
if (!postProps || !postProps.captions?.[0]) {
|
if (!isArrayOf<Caption>(postProps?.captions, isCaption) || !postProps.captions[0]) {
|
||||||
return {
|
return {
|
||||||
tracks: undefined,
|
tracks: undefined,
|
||||||
selected: {type: SelectedTrackType.DISABLED, value: ''},
|
selected: {type: SelectedTrackType.DISABLED, value: ''},
|
||||||
|
|
@ -247,20 +248,16 @@ export const getTranscriptionUri = (serverUrl: string, postProps?: Record<string
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
function isValidObject(v: any) {
|
|
||||||
return typeof v === 'object' && !Array.isArray(v) && v !== null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getCallPropsFromPost(post: PostModel | Post): CallPostProps {
|
export function getCallPropsFromPost(post: PostModel | Post): CallPostProps {
|
||||||
return {
|
return {
|
||||||
title: typeof post.props?.title === 'string' ? post.props.title : '',
|
title: ensureString(post.props?.title),
|
||||||
start_at: typeof post.props?.start_at === 'number' ? post.props.start_at : 0,
|
start_at: ensureNumber(post.props?.start_at),
|
||||||
end_at: typeof post.props?.end_at === 'number' ? post.props.end_at : 0,
|
end_at: ensureNumber(post.props?.end_at),
|
||||||
recordings: isValidObject(post.props?.recordings) ? post.props.recordings : {},
|
recordings: isRecordOf<CallJobMetadata>(post.props?.recordings, isCallJobMetadata) ? post.props.recordings : {},
|
||||||
transcriptions: isValidObject(post.props?.transcriptions) ? post.props.transcriptions : {},
|
transcriptions: isRecordOf<CallJobMetadata>(post.props?.transcriptions, isCallJobMetadata) ? post.props.transcriptions : {},
|
||||||
participants: Array.isArray(post.props?.participants) ? post.props.participants : [],
|
participants: isStringArray(post.props?.participants) ? post.props.participants : [],
|
||||||
|
|
||||||
// DEPRECATED
|
// DEPRECATED
|
||||||
recording_files: Array.isArray(post.props?.recording_files) ? post.props.recording_files : [],
|
recording_files: isStringArray(post.props?.recording_files) ? post.props.recording_files : [],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ import {buttonBackgroundStyle, buttonTextStyle} from '@utils/buttonStyles';
|
||||||
import {checkDialogElementForError, checkIfErrorsMatchElements} from '@utils/integrations';
|
import {checkDialogElementForError, checkIfErrorsMatchElements} from '@utils/integrations';
|
||||||
import {getMarkdownBlockStyles, getMarkdownTextStyles} from '@utils/markdown';
|
import {getMarkdownBlockStyles, getMarkdownTextStyles} from '@utils/markdown';
|
||||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||||
|
import {secureGetFromRecord} from '@utils/types';
|
||||||
|
|
||||||
import DialogIntroductionText from '../interactive_dialog/dialog_introduction_text';
|
import DialogIntroductionText from '../interactive_dialog/dialog_introduction_text';
|
||||||
import {buildNavigationButton, dismissModal, setButtons} from '../navigation';
|
import {buildNavigationButton, dismissModal, setButtons} from '../navigation';
|
||||||
|
|
@ -263,7 +264,7 @@ function AppsFormComponent({
|
||||||
elements?.forEach((element) => {
|
elements?.forEach((element) => {
|
||||||
const newError = checkDialogElementForError(
|
const newError = checkDialogElementForError(
|
||||||
element,
|
element,
|
||||||
element.name === form.submit_buttons ? button : values[element.name],
|
element.name === form.submit_buttons ? button : secureGetFromRecord(values, element.name),
|
||||||
);
|
);
|
||||||
if (newError) {
|
if (newError) {
|
||||||
hasErrors = true;
|
hasErrors = true;
|
||||||
|
|
@ -408,13 +409,17 @@ function AppsFormComponent({
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
{form.fields && form.fields.filter((f) => f.name !== form.submit_buttons).map((field) => {
|
{form.fields && form.fields.filter((f) => f.name !== form.submit_buttons).map((field) => {
|
||||||
|
const value = secureGetFromRecord(values, field.name);
|
||||||
|
if (!value) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<AppsFormField
|
<AppsFormField
|
||||||
field={field}
|
field={field}
|
||||||
key={field.name}
|
key={field.name}
|
||||||
name={field.name}
|
name={field.name}
|
||||||
errorText={errors[field.name]}
|
errorText={secureGetFromRecord(errors, field.name)}
|
||||||
value={values[field.name]}
|
value={value}
|
||||||
performLookup={performLookup}
|
performLookup={performLookup}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -308,7 +308,7 @@ const BookmarkFile = ({channelId, close, disabled, initialFile, maxFileSize, set
|
||||||
ellipsizeMode='tail'
|
ellipsizeMode='tail'
|
||||||
style={styles.filename}
|
style={styles.filename}
|
||||||
>
|
>
|
||||||
{decodeURIComponent(file.name.trim())}
|
{file.name.trim()}
|
||||||
</Text>
|
</Text>
|
||||||
{info}
|
{info}
|
||||||
</View>
|
</View>
|
||||||
|
|
|
||||||
|
|
@ -159,13 +159,13 @@ const ChannelBookmarkAddOrEdit = ({
|
||||||
...(bookmark || emptyBookmark),
|
...(bookmark || emptyBookmark),
|
||||||
owner_id: ownerId,
|
owner_id: ownerId,
|
||||||
channel_id: channelId,
|
channel_id: channelId,
|
||||||
display_name: decodeURIComponent(f.name),
|
display_name: f.name,
|
||||||
type: 'file',
|
type: 'file',
|
||||||
file_id: f.id,
|
file_id: f.id,
|
||||||
};
|
};
|
||||||
setBookmarkToSave(b);
|
setBookmarkToSave(b);
|
||||||
setFile(f);
|
setFile(f);
|
||||||
}, [bookmark, channelId, ownerId]);
|
}, [bookmark, channelId, ownerId, setBookmarkToSave]);
|
||||||
|
|
||||||
const setBookmarkDisplayName = useCallback((displayName: string) => {
|
const setBookmarkDisplayName = useCallback((displayName: string) => {
|
||||||
if (bookmark) {
|
if (bookmark) {
|
||||||
|
|
|
||||||
|
|
@ -68,8 +68,7 @@ const SkinSelector = ({onSelectSkin, selected, skins}: Props) => {
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
<View style={[styles.skins, isTablet && {marginRight: 10}]}>
|
<View style={[styles.skins, isTablet && {marginRight: 10}]}>
|
||||||
{Object.keys(skins).map((key) => {
|
{Object.entries(skins).map(([key, name]) => {
|
||||||
const name = skins[key];
|
|
||||||
return (
|
return (
|
||||||
<View
|
<View
|
||||||
key={name}
|
key={name}
|
||||||
|
|
|
||||||
|
|
@ -44,8 +44,7 @@ const styles = StyleSheet.create({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const skins = Object.keys(skinCodes).reduce<Record<string, string>>((result, value) => {
|
const skins = Object.entries(skinCodes).reduce<Record<string, string>>((result, [value, skin]) => {
|
||||||
const skin = skinCodes[value];
|
|
||||||
if (value === 'default') {
|
if (value === 'default') {
|
||||||
result[value] = 'hand';
|
result[value] = 'hand';
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -273,7 +273,7 @@ const DownloadWithAction = ({action, item, onDownloadSuccess, setAction, gallery
|
||||||
actionToExecute({
|
actionToExecute({
|
||||||
code: 200,
|
code: 200,
|
||||||
ok: true,
|
ok: true,
|
||||||
data: {path},
|
data: {path: path.replace('file://', '')},
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
if (item.type === 'avatar') {
|
if (item.type === 'avatar') {
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import {SafeAreaView, type Edge, useSafeAreaInsets} from 'react-native-safe-area
|
||||||
import {Events} from '@constants';
|
import {Events} from '@constants';
|
||||||
import {GALLERY_FOOTER_HEIGHT} from '@constants/gallery';
|
import {GALLERY_FOOTER_HEIGHT} from '@constants/gallery';
|
||||||
import {changeOpacity} from '@utils/theme';
|
import {changeOpacity} from '@utils/theme';
|
||||||
|
import {ensureString} from '@utils/types';
|
||||||
import {displayUsername} from '@utils/user';
|
import {displayUsername} from '@utils/user';
|
||||||
|
|
||||||
import Actions from './actions';
|
import Actions from './actions';
|
||||||
|
|
@ -71,14 +72,14 @@ const Footer = ({
|
||||||
|
|
||||||
let overrideIconUrl;
|
let overrideIconUrl;
|
||||||
if (enablePostIconOverride && post?.props?.use_user_icon !== 'true' && post?.props?.override_icon_url) {
|
if (enablePostIconOverride && post?.props?.use_user_icon !== 'true' && post?.props?.override_icon_url) {
|
||||||
overrideIconUrl = post.props.override_icon_url;
|
overrideIconUrl = ensureString(post.props.override_icon_url);
|
||||||
}
|
}
|
||||||
|
|
||||||
let userDisplayName;
|
let userDisplayName;
|
||||||
if (item.type === 'avatar') {
|
if (item.type === 'avatar') {
|
||||||
userDisplayName = item.name;
|
userDisplayName = item.name;
|
||||||
} else if (enablePostUsernameOverride && post?.props?.override_username) {
|
} else if (enablePostUsernameOverride && post?.props?.override_username) {
|
||||||
userDisplayName = post.props.override_username as string;
|
userDisplayName = ensureString(post.props.override_username);
|
||||||
} else {
|
} else {
|
||||||
userDisplayName = displayUsername(author, undefined, teammateNameDisplay);
|
userDisplayName = displayUsername(author, undefined, teammateNameDisplay);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ import {useIsTablet} from '@hooks/device';
|
||||||
import {dismissOverlay} from '@screens/navigation';
|
import {dismissOverlay} from '@screens/navigation';
|
||||||
import {preventDoubleTap} from '@utils/tap';
|
import {preventDoubleTap} from '@utils/tap';
|
||||||
import {changeOpacity} from '@utils/theme';
|
import {changeOpacity} from '@utils/theme';
|
||||||
|
import {secureGetFromRecord} from '@utils/types';
|
||||||
|
|
||||||
import Icon from './icon';
|
import Icon from './icon';
|
||||||
import Server from './server';
|
import Server from './server';
|
||||||
|
|
@ -148,7 +149,7 @@ const InAppNotification = ({componentId, serverName, serverUrl, notification}: I
|
||||||
// eslint-disable-next-line new-cap
|
// eslint-disable-next-line new-cap
|
||||||
const gesture = Gesture.Pan().activeOffsetY(-20).onStart(() => runOnJS(animateDismissOverlay)());
|
const gesture = Gesture.Pan().activeOffsetY(-20).onStart(() => runOnJS(animateDismissOverlay)());
|
||||||
|
|
||||||
const database = DatabaseManager.serverDatabases[serverUrl]?.database;
|
const database = secureGetFromRecord(DatabaseManager.serverDatabases, serverUrl)?.database;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<GestureHandlerRootView style={styles.gestureHandler}>
|
<GestureHandlerRootView style={styles.gestureHandler}>
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ import {
|
||||||
} from '@screens/navigation';
|
} from '@screens/navigation';
|
||||||
import {filterChannelsMatchingTerm} from '@utils/channel';
|
import {filterChannelsMatchingTerm} from '@utils/channel';
|
||||||
import {changeOpacity, getKeyboardAppearanceFromTheme, makeStyleSheetFromTheme} from '@utils/theme';
|
import {changeOpacity, getKeyboardAppearanceFromTheme, makeStyleSheetFromTheme} from '@utils/theme';
|
||||||
|
import {secureGetFromRecord} from '@utils/types';
|
||||||
import {typography} from '@utils/typography';
|
import {typography} from '@utils/typography';
|
||||||
|
|
||||||
import ChannelListRow from './channel_list_row';
|
import ChannelListRow from './channel_list_row';
|
||||||
|
|
@ -68,7 +69,8 @@ const extractItemKey = (dataSource: string, item: DataType): string => {
|
||||||
const toggleFromMap = <T extends DialogOption | Channel | UserProfile>(current: MultiselectSelectedMap, key: string, item: T): MultiselectSelectedMap => {
|
const toggleFromMap = <T extends DialogOption | Channel | UserProfile>(current: MultiselectSelectedMap, key: string, item: T): MultiselectSelectedMap => {
|
||||||
const newMap = {...current};
|
const newMap = {...current};
|
||||||
|
|
||||||
if (current[key]) {
|
const hasValue = Boolean(secureGetFromRecord<any>(current, key));
|
||||||
|
if (hasValue) {
|
||||||
delete newMap[key];
|
delete newMap[key];
|
||||||
} else {
|
} else {
|
||||||
newMap[key] = item;
|
newMap[key] = item;
|
||||||
|
|
@ -96,7 +98,7 @@ const filterSearchData = (source: string, searchData: DataTypeList, searchTerm:
|
||||||
const handleIdSelection = (dataSource: string, currentIds: {[id: string]: DataType}, item: DataType) => {
|
const handleIdSelection = (dataSource: string, currentIds: {[id: string]: DataType}, item: DataType) => {
|
||||||
const newSelectedIds = {...currentIds};
|
const newSelectedIds = {...currentIds};
|
||||||
const key = extractItemKey(dataSource, item);
|
const key = extractItemKey(dataSource, item);
|
||||||
const wasSelected = currentIds[key];
|
const wasSelected = secureGetFromRecord(currentIds, key);
|
||||||
|
|
||||||
if (wasSelected) {
|
if (wasSelected) {
|
||||||
Reflect.deleteProperty(newSelectedIds, key);
|
Reflect.deleteProperty(newSelectedIds, key);
|
||||||
|
|
@ -239,7 +241,9 @@ function IntegrationSelector(
|
||||||
} else {
|
} else {
|
||||||
setMultiselectSelected((current) => {
|
setMultiselectSelected((current) => {
|
||||||
const multiselectSelectedItems = {...current};
|
const multiselectSelectedItems = {...current};
|
||||||
delete multiselectSelectedItems[itemKey];
|
if (secureGetFromRecord<any>(multiselectSelectedItems, itemKey) !== undefined) {
|
||||||
|
delete multiselectSelectedItems[itemKey];
|
||||||
|
}
|
||||||
return multiselectSelectedItems;
|
return multiselectSelectedItems;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -470,7 +474,7 @@ function IntegrationSelector(
|
||||||
}, [multiselectSelected, theme, isMultiselect]);
|
}, [multiselectSelected, theme, isMultiselect]);
|
||||||
|
|
||||||
const renderOptionItem = useCallback((itemProps: any) => {
|
const renderOptionItem = useCallback((itemProps: any) => {
|
||||||
const itemSelected = Boolean(multiselectSelected[itemProps.item.value]);
|
const itemSelected = Boolean(secureGetFromRecord<any>(multiselectSelected, itemProps.item.value));
|
||||||
return (
|
return (
|
||||||
<OptionListRow
|
<OptionListRow
|
||||||
key={itemProps.id}
|
key={itemProps.id}
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
|
||||||
import {buildNavigationButton, dismissModal, setButtons} from '@screens/navigation';
|
import {buildNavigationButton, dismissModal, setButtons} from '@screens/navigation';
|
||||||
import {checkDialogElementForError, checkIfErrorsMatchElements} from '@utils/integrations';
|
import {checkDialogElementForError, checkIfErrorsMatchElements} from '@utils/integrations';
|
||||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||||
|
import {secureGetFromRecord} from '@utils/types';
|
||||||
|
|
||||||
import DialogElement from './dialog_element';
|
import DialogElement from './dialog_element';
|
||||||
import DialogIntroductionText from './dialog_introduction_text';
|
import DialogIntroductionText from './dialog_introduction_text';
|
||||||
|
|
@ -142,7 +143,7 @@ function InteractiveDialog({
|
||||||
let hasErrors = false;
|
let hasErrors = false;
|
||||||
if (elements) {
|
if (elements) {
|
||||||
elements.forEach((elem) => {
|
elements.forEach((elem) => {
|
||||||
const newError = checkDialogElementForError(elem, values[elem.name]);
|
const newError = checkDialogElementForError(elem, secureGetFromRecord(values, elem.name));
|
||||||
if (newError) {
|
if (newError) {
|
||||||
newErrors[elem.name] = intl.formatMessage({id: newError.id, defaultMessage: newError.defaultMessage}, newError.values);
|
newErrors[elem.name] = intl.formatMessage({id: newError.id, defaultMessage: newError.defaultMessage}, newError.values);
|
||||||
hasErrors = true;
|
hasErrors = true;
|
||||||
|
|
@ -244,6 +245,10 @@ function InteractiveDialog({
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
{Boolean(elements) && elements.map((e) => {
|
{Boolean(elements) && elements.map((e) => {
|
||||||
|
const value = secureGetFromRecord(values, e.name);
|
||||||
|
if (value === undefined) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<DialogElement
|
<DialogElement
|
||||||
key={'dialogelement' + e.name}
|
key={'dialogelement' + e.name}
|
||||||
|
|
@ -252,13 +257,13 @@ function InteractiveDialog({
|
||||||
type={e.type}
|
type={e.type}
|
||||||
subtype={e.subtype}
|
subtype={e.subtype}
|
||||||
helpText={e.help_text}
|
helpText={e.help_text}
|
||||||
errorText={errors[e.name]}
|
errorText={secureGetFromRecord(errors, e.name)}
|
||||||
placeholder={e.placeholder}
|
placeholder={e.placeholder}
|
||||||
maxLength={e.max_length}
|
maxLength={e.max_length}
|
||||||
dataSource={e.data_source}
|
dataSource={e.data_source}
|
||||||
optional={e.optional}
|
optional={e.optional}
|
||||||
options={e.options}
|
options={e.options}
|
||||||
value={values[e.name]}
|
value={value}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ import {dismissModal, setButtons} from '@screens/navigation';
|
||||||
import {isEmail} from '@utils/helpers';
|
import {isEmail} from '@utils/helpers';
|
||||||
import {mergeNavigationOptions} from '@utils/navigation';
|
import {mergeNavigationOptions} from '@utils/navigation';
|
||||||
import {makeStyleSheetFromTheme, changeOpacity} from '@utils/theme';
|
import {makeStyleSheetFromTheme, changeOpacity} from '@utils/theme';
|
||||||
|
import {secureGetFromRecord} from '@utils/types';
|
||||||
import {isGuest} from '@utils/user';
|
import {isGuest} from '@utils/user';
|
||||||
|
|
||||||
import Selection from './selection';
|
import Selection from './selection';
|
||||||
|
|
@ -183,7 +184,7 @@ export default function Invite({
|
||||||
const id = email ? item : (item as UserProfile).id;
|
const id = email ? item : (item as UserProfile).id;
|
||||||
const newSelectedIds = Object.assign({}, selectedIds);
|
const newSelectedIds = Object.assign({}, selectedIds);
|
||||||
|
|
||||||
if (!selectedIds[id]) {
|
if (!secureGetFromRecord(selectedIds, id)) {
|
||||||
newSelectedIds[id] = item;
|
newSelectedIds[id] = item;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -301,8 +302,9 @@ export default function Invite({
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const email of emails) {
|
for (const email of emails) {
|
||||||
if (membersWithError[email]) {
|
const error = secureGetFromRecord(membersWithError, email);
|
||||||
notSent.push({userId: email, reason: membersWithError[email]});
|
if (error) {
|
||||||
|
notSent.push({userId: email, reason: error});
|
||||||
} else {
|
} else {
|
||||||
sent.push({userId: email, reason: formatMessage({id: 'invite.summary.email_invite', defaultMessage: 'An invitation email has been sent'})});
|
sent.push({userId: email, reason: formatMessage({id: 'invite.summary.email_invite', defaultMessage: 'An invitation email has been sent'})});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import CompassIcon from '@components/compass_icon';
|
||||||
import UserItem from '@components/user_item';
|
import UserItem from '@components/user_item';
|
||||||
import {useTheme} from '@context/theme';
|
import {useTheme} from '@context/theme';
|
||||||
import {makeStyleSheetFromTheme, changeOpacity} from '@utils/theme';
|
import {makeStyleSheetFromTheme, changeOpacity} from '@utils/theme';
|
||||||
|
import {secureGetFromRecord} from '@utils/types';
|
||||||
import {typography} from '@utils/typography';
|
import {typography} from '@utils/typography';
|
||||||
|
|
||||||
import TextItem, {TextItemType} from './text_item';
|
import TextItem, {TextItemType} from './text_item';
|
||||||
|
|
@ -114,7 +115,7 @@ export default function SummaryReport({
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
{invites.map(({userId, reason}) => {
|
{invites.map(({userId, reason}) => {
|
||||||
const item = selectedIds[userId];
|
const item = secureGetFromRecord(selectedIds, userId);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View
|
<View
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ import {buttonBackgroundStyle, buttonTextStyle} from '@utils/buttonStyles';
|
||||||
import {closePermalink} from '@utils/permalink';
|
import {closePermalink} from '@utils/permalink';
|
||||||
import {preventDoubleTap} from '@utils/tap';
|
import {preventDoubleTap} from '@utils/tap';
|
||||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||||
|
import {secureGetFromRecord} from '@utils/types';
|
||||||
import {typography} from '@utils/typography';
|
import {typography} from '@utils/typography';
|
||||||
|
|
||||||
import PermalinkError from './permalink_error';
|
import PermalinkError from './permalink_error';
|
||||||
|
|
@ -176,7 +177,7 @@ function Permalink({
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const database = DatabaseManager.serverDatabases[serverUrl]?.database;
|
const database = secureGetFromRecord(DatabaseManager.serverDatabases, serverUrl)?.database;
|
||||||
if (!database) {
|
if (!database) {
|
||||||
setError({unreachable: true});
|
setError({unreachable: true});
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
import {AppBindingLocations, AppCallResponseTypes, AppFieldTypes} from '@constants/apps';
|
import {AppBindingLocations, AppCallResponseTypes, AppFieldTypes} from '@constants/apps';
|
||||||
|
|
||||||
import {generateId} from './general';
|
import {generateId} from './general';
|
||||||
|
import {isArrayOf, isStringArray} from './types';
|
||||||
|
|
||||||
export function cleanBinding(binding: AppBinding, topLocation: string): AppBinding|null {
|
export function cleanBinding(binding: AppBinding, topLocation: string): AppBinding|null {
|
||||||
return cleanBindingRec(binding, topLocation, 0);
|
return cleanBindingRec(binding, topLocation, 0);
|
||||||
|
|
@ -253,3 +254,297 @@ export const makeCallErrorResponse = (errMessage: string): AppCallResponse<any>
|
||||||
};
|
};
|
||||||
|
|
||||||
export const filterEmptyOptions = (option: AppSelectOption): boolean => Boolean(option.value && !option.value.match(/^[ \t]+$/));
|
export const filterEmptyOptions = (option: AppSelectOption): boolean => Boolean(option.value && !option.value.match(/^[ \t]+$/));
|
||||||
|
|
||||||
|
function isAppExpand(v: unknown): v is AppExpand {
|
||||||
|
if (typeof v !== 'object' || v === null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const expand = v as AppExpand;
|
||||||
|
|
||||||
|
if (expand.app !== undefined && typeof expand.app !== 'string') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (expand.acting_user !== undefined && typeof expand.acting_user !== 'string') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (expand.channel !== undefined && typeof expand.channel !== 'string') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (expand.config !== undefined && typeof expand.config !== 'string') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (expand.mentioned !== undefined && typeof expand.mentioned !== 'string') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (expand.parent_post !== undefined && typeof expand.parent_post !== 'string') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (expand.post !== undefined && typeof expand.post !== 'string') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (expand.root_post !== undefined && typeof expand.root_post !== 'string') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (expand.team !== undefined && typeof expand.team !== 'string') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (expand.user !== undefined && typeof expand.user !== 'string') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isAppCall(obj: unknown): obj is AppCall {
|
||||||
|
if (typeof obj !== 'object' || obj === null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const call = obj as AppCall;
|
||||||
|
|
||||||
|
if (typeof call.path !== 'string') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (call.expand !== undefined && !isAppExpand(call.expand)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Here we're assuming that 'state' can be of any type, so no type check for 'state'
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isAppFormValue(v: unknown): v is AppFormValue {
|
||||||
|
if (typeof v === 'string') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof v === 'boolean') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (v === null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return isAppSelectOption(v);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isAppSelectOption(v: unknown): v is AppSelectOption {
|
||||||
|
if (typeof v !== 'object' || v === null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const option = v as AppSelectOption;
|
||||||
|
|
||||||
|
if (typeof option.label !== 'string' || typeof option.value !== 'string') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (option.icon_data !== undefined && typeof option.icon_data !== 'string') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isAppField(v: unknown): v is AppField {
|
||||||
|
if (typeof v !== 'object' || v === null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const field = v as AppField;
|
||||||
|
|
||||||
|
if (typeof field.name !== 'string' || typeof field.type !== 'string') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (field.is_required !== undefined && typeof field.is_required !== 'boolean') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (field.readonly !== undefined && typeof field.readonly !== 'boolean') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (field.value !== undefined && !isAppFormValue(field.value)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (field.description !== undefined && typeof field.description !== 'string') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (field.label !== undefined && typeof field.label !== 'string') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (field.hint !== undefined && typeof field.hint !== 'string') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (field.position !== undefined && typeof field.position !== 'number') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (field.modal_label !== undefined && typeof field.modal_label !== 'string') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (field.refresh !== undefined && typeof field.refresh !== 'boolean') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (field.options !== undefined && !isArrayOf(field.options, isAppSelectOption)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (field.multiselect !== undefined && typeof field.multiselect !== 'boolean') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (field.lookup !== undefined && !isAppCall(field.lookup)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (field.subtype !== undefined && typeof field.subtype !== 'string') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (field.min_length !== undefined && typeof field.min_length !== 'number') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (field.max_length !== undefined && typeof field.max_length !== 'number') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isAppForm(v: unknown): v is AppForm {
|
||||||
|
if (typeof v !== 'object' || v === null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const form = v as AppForm;
|
||||||
|
|
||||||
|
if (form.title !== undefined && typeof form.title !== 'string') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (form.header !== undefined && typeof form.header !== 'string') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (form.footer !== undefined && typeof form.footer !== 'string') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (form.icon !== undefined && typeof form.icon !== 'string') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (form.submit_buttons !== undefined && typeof form.submit_buttons !== 'string') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (form.cancel_button !== undefined && typeof form.cancel_button !== 'boolean') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (form.submit_on_cancel !== undefined && typeof form.submit_on_cancel !== 'boolean') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (form.fields !== undefined && !isArrayOf(form.fields, isAppField)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (form.source !== undefined && !isAppCall(form.source)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (form.submit !== undefined && !isAppCall(form.submit)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (form.depends_on !== undefined && !isStringArray(form.depends_on)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isAppBinding(obj: unknown): obj is AppBinding {
|
||||||
|
if (typeof obj !== 'object' || obj === null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const binding = obj as AppBinding;
|
||||||
|
|
||||||
|
if (typeof binding.app_id !== 'string' || typeof binding.label !== 'string') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (binding.location !== undefined && typeof binding.location !== 'string') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (binding.icon !== undefined && typeof binding.icon !== 'string') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (binding.hint !== undefined && typeof binding.hint !== 'string') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (binding.description !== undefined && typeof binding.description !== 'string') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (binding.role_id !== undefined && typeof binding.role_id !== 'string') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (binding.depends_on_team !== undefined && typeof binding.depends_on_team !== 'boolean') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (binding.depends_on_channel !== undefined && typeof binding.depends_on_channel !== 'boolean') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (binding.depends_on_user !== undefined && typeof binding.depends_on_user !== 'boolean') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (binding.depends_on_post !== undefined && typeof binding.depends_on_post !== 'boolean') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (binding.bindings !== undefined && !isArrayOf(binding.bindings, isAppBinding)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (binding.form !== undefined && !isAppForm(binding.form)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (binding.submit !== undefined && !isAppCall(binding.submit)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import Permissions from 'react-native-permissions';
|
||||||
import {dismissBottomSheet} from '@screens/navigation';
|
import {dismissBottomSheet} from '@screens/navigation';
|
||||||
import {extractFileInfo, lookupMimeType} from '@utils/file';
|
import {extractFileInfo, lookupMimeType} from '@utils/file';
|
||||||
import {logWarning} from '@utils/log';
|
import {logWarning} from '@utils/log';
|
||||||
|
import {safeDecodeURIComponent} from '@utils/url';
|
||||||
|
|
||||||
import type {IntlShape} from 'react-intl';
|
import type {IntlShape} from 'react-intl';
|
||||||
|
|
||||||
|
|
@ -128,7 +129,7 @@ export default class FilePickerUtil {
|
||||||
const type = file.type || lookupMimeType(uri);
|
const type = file.type || lookupMimeType(uri);
|
||||||
let fileName = file.fileName;
|
let fileName = file.fileName;
|
||||||
if (type.includes('video/') && uri) {
|
if (type.includes('video/') && uri) {
|
||||||
fileName = decodeURIComponent(uri.split('\\').pop()?.split('/').pop() || '');
|
fileName = safeDecodeURIComponent(uri.split('\\').pop()?.split('/').pop() || '');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (uri) {
|
if (uri) {
|
||||||
|
|
|
||||||
|
|
@ -413,7 +413,7 @@ export async function extractFileInfo(files: Array<Asset | DocumentPickerRespons
|
||||||
} else {
|
} else {
|
||||||
const localPath = file.uri || '';
|
const localPath = file.uri || '';
|
||||||
try {
|
try {
|
||||||
const fileInfo = await getInfoAsync(decodeURIComponent(localPath), {size: true});
|
const fileInfo = await getInfoAsync(localPath, {size: true});
|
||||||
if ('size' in fileInfo) {
|
if ('size' in fileInfo) {
|
||||||
outFile.size = fileInfo.size || 0;
|
outFile.size = fileInfo.size || 0;
|
||||||
outFile.name = localPath.substring(localPath.lastIndexOf('/') + 1);
|
outFile.name = localPath.substring(localPath.lastIndexOf('/') + 1);
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ export const clampVelocity = (velocity: number, minVelocity: number, maxVelocity
|
||||||
return Math.max(Math.min(velocity, -minVelocity), -maxVelocity);
|
return Math.max(Math.min(velocity, -minVelocity), -maxVelocity);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const fileToGalleryItem = (file: FileInfo, authorId?: string, postProps?: Record<string, any>, lastPictureUpdate = 0): GalleryItemType => {
|
export const fileToGalleryItem = (file: FileInfo, authorId?: string, postProps?: Record<string, unknown>, lastPictureUpdate = 0): GalleryItemType => {
|
||||||
let type: GalleryItemType['type'] = 'file';
|
let type: GalleryItemType['type'] = 'file';
|
||||||
if (isVideo(file)) {
|
if (isVideo(file)) {
|
||||||
type = 'video';
|
type = 'video';
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,14 @@
|
||||||
import {Dimensions} from 'react-native';
|
import {Dimensions} from 'react-native';
|
||||||
|
|
||||||
import {View} from '@constants';
|
import {View} from '@constants';
|
||||||
import {IMAGE_MAX_HEIGHT, IMAGE_MIN_DIMENSION, MAX_GIF_SIZE, VIEWPORT_IMAGE_OFFSET, VIEWPORT_IMAGE_REPLY_OFFSET} from '@constants/image';
|
import {
|
||||||
|
IMAGE_MAX_HEIGHT,
|
||||||
|
IMAGE_MIN_DIMENSION,
|
||||||
|
MAX_GIF_SIZE,
|
||||||
|
VIEWPORT_IMAGE_ATTACHMENT_OFFSET,
|
||||||
|
VIEWPORT_IMAGE_OFFSET,
|
||||||
|
VIEWPORT_IMAGE_REPLY_OFFSET,
|
||||||
|
} from '@constants/image';
|
||||||
|
|
||||||
export const calculateDimensions = (height?: number, width?: number, viewPortWidth = 0, viewPortHeight = 0) => {
|
export const calculateDimensions = (height?: number, width?: number, viewPortWidth = 0, viewPortHeight = 0) => {
|
||||||
'worklet';
|
'worklet';
|
||||||
|
|
@ -47,7 +54,7 @@ export const calculateDimensions = (height?: number, width?: number, viewPortWid
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export function getViewPortWidth(isReplyPost: boolean, tabletOffset = false) {
|
export function getViewPortWidth(isReplyPost: boolean, tabletOffset = false, imageAttachmentOffset = false) {
|
||||||
const {width, height} = Dimensions.get('window');
|
const {width, height} = Dimensions.get('window');
|
||||||
let portraitPostWidth = Math.min(width, height) - VIEWPORT_IMAGE_OFFSET;
|
let portraitPostWidth = Math.min(width, height) - VIEWPORT_IMAGE_OFFSET;
|
||||||
|
|
||||||
|
|
@ -59,6 +66,10 @@ export function getViewPortWidth(isReplyPost: boolean, tabletOffset = false) {
|
||||||
portraitPostWidth -= VIEWPORT_IMAGE_REPLY_OFFSET;
|
portraitPostWidth -= VIEWPORT_IMAGE_REPLY_OFFSET;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (imageAttachmentOffset) {
|
||||||
|
portraitPostWidth -= VIEWPORT_IMAGE_ATTACHMENT_OFFSET;
|
||||||
|
}
|
||||||
|
|
||||||
return portraitPostWidth;
|
return portraitPostWidth;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
195
app/utils/message_attachment.ts
Normal file
195
app/utils/message_attachment.ts
Normal file
|
|
@ -0,0 +1,195 @@
|
||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import {isArrayOf} from './types';
|
||||||
|
|
||||||
|
export function getStatusColors(theme: Theme): Dictionary<string> {
|
||||||
|
return {
|
||||||
|
good: '#00c100',
|
||||||
|
warning: '#dede01',
|
||||||
|
danger: theme.errorTextColor,
|
||||||
|
default: theme.centerChannelColor,
|
||||||
|
primary: theme.buttonBg,
|
||||||
|
success: theme.onlineIndicator,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isMessageAttachmentArray(v: unknown): v is MessageAttachment[] {
|
||||||
|
return isArrayOf(v, isMessageAttachment);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isPostActionOption(v: unknown): v is PostActionOption {
|
||||||
|
if (typeof v !== 'object' || !v) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('text' in v && typeof v.text !== 'string') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('value' in v && typeof v.value !== 'string') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isPostAction(v: unknown): v is PostAction {
|
||||||
|
if (typeof v !== 'object' || !v) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!('id' in v)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof v.id !== 'string') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!('name' in v)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof v.name !== 'string') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('type' in v && typeof v.type !== 'string') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('disabled' in v && typeof v.disabled !== 'boolean') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('style' in v && typeof v.style !== 'string') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('data_source' in v && typeof v.data_source !== 'string') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('options' in v && !isArrayOf(v.options, isPostActionOption)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('default_option' in v && typeof v.default_option !== 'string') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('cookie' in v && typeof v.cookie !== 'string') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isMessageAttachmentField(v: unknown) {
|
||||||
|
if (typeof v !== 'object') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!v) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!('title' in v)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof v.title !== 'string') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!('value' in v)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof v.value === 'object' && v.value && 'toString' in v.value && typeof v.value.toString !== 'function') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('short' in v && typeof v.short !== 'boolean') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isMessageAttachment(v: unknown): v is MessageAttachment {
|
||||||
|
if (typeof v !== 'object' || !v) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('fallback' in v && typeof v.fallback !== 'string') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We may consider adding more validation to what color may be
|
||||||
|
if ('color' in v && typeof v.color !== 'string') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('pretext' in v && typeof v.pretext !== 'string') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('author_name' in v && typeof v.author_name !== 'string') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Where it is used, we are calling isUrlSafe. We could consider calling it here
|
||||||
|
if ('author_link' in v && typeof v.author_link !== 'string') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We may need more validation since this is going to be passed to an img src prop
|
||||||
|
if ('author_icon' in v && typeof v.author_icon !== 'string') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('title' in v && typeof v.title !== 'string') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Where it is used, we are calling isUrlSafe. We could consider calling it here
|
||||||
|
if ('title_link' in v && typeof v.title_link !== 'string') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('text' in v && typeof v.text !== 'string') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We may need more validation since this is going to be passed to an img src prop
|
||||||
|
if ('image_url' in v && typeof v.image_url !== 'string') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We may need more validation since this is going to be passed to an img src prop
|
||||||
|
if ('thumb_url' in v && typeof v.thumb_url !== 'string') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We are truncating if the size is more than some constant. We could check this here
|
||||||
|
if ('footer' in v && typeof v.footer !== 'string') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We may need more validation since this is going to be passed to an img src prop
|
||||||
|
if ('footer_icon' in v && typeof v.footer_icon !== 'string') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('fields' in v && v.fields !== null && !isArrayOf(v.fields, isMessageAttachmentField)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('actions' in v && !isArrayOf(v.actions, isPostAction)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
|
|
||||||
import {Preferences} from '@constants';
|
import {Preferences} from '@constants';
|
||||||
|
|
||||||
import {getStatusColors} from './message_attachment_colors';
|
import {getStatusColors} from './message_attachment';
|
||||||
|
|
||||||
describe('getStatusColors', () => {
|
describe('getStatusColors', () => {
|
||||||
const mockTheme = Preferences.THEMES.denim;
|
const mockTheme = Preferences.THEMES.denim;
|
||||||
|
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
||||||
// See LICENSE.txt for license information.
|
|
||||||
|
|
||||||
export function getStatusColors(theme: Theme): Dictionary<string> {
|
|
||||||
return {
|
|
||||||
good: '#00c100',
|
|
||||||
warning: '#dede01',
|
|
||||||
danger: theme.errorTextColor,
|
|
||||||
default: theme.centerChannelColor,
|
|
||||||
primary: theme.buttonBg,
|
|
||||||
success: theme.onlineIndicator,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
@ -85,7 +85,7 @@ describe('post utils', () => {
|
||||||
const post = {
|
const post = {
|
||||||
props: {
|
props: {
|
||||||
from_webhook: 'true',
|
from_webhook: 'true',
|
||||||
},
|
} as Record<string, unknown>,
|
||||||
} as PostModel;
|
} as PostModel;
|
||||||
|
|
||||||
const result = isFromWebhook(post);
|
const result = isFromWebhook(post);
|
||||||
|
|
@ -96,7 +96,7 @@ describe('post utils', () => {
|
||||||
const post = {
|
const post = {
|
||||||
props: {
|
props: {
|
||||||
from_webhook: 'false',
|
from_webhook: 'false',
|
||||||
},
|
} as Record<string, unknown>,
|
||||||
} as PostModel;
|
} as PostModel;
|
||||||
|
|
||||||
const result = isFromWebhook(post);
|
const result = isFromWebhook(post);
|
||||||
|
|
@ -149,7 +149,7 @@ describe('post utils', () => {
|
||||||
const post = {
|
const post = {
|
||||||
props: {
|
props: {
|
||||||
failed: true,
|
failed: true,
|
||||||
},
|
} as Record<string, unknown>,
|
||||||
pendingPostId: 'id',
|
pendingPostId: 'id',
|
||||||
id: 'id',
|
id: 'id',
|
||||||
updateAt: Date.now() - Post.POST_TIME_TO_FAIL - 1000,
|
updateAt: Date.now() - Post.POST_TIME_TO_FAIL - 1000,
|
||||||
|
|
@ -471,7 +471,7 @@ describe('post utils', () => {
|
||||||
props: {
|
props: {
|
||||||
from_webhook: 'true',
|
from_webhook: 'true',
|
||||||
override_username: 'webhook_user',
|
override_username: 'webhook_user',
|
||||||
},
|
} as Record<string, unknown>,
|
||||||
} as PostModel;
|
} as PostModel;
|
||||||
|
|
||||||
const result = postUserDisplayName(post, undefined, undefined, true);
|
const result = postUserDisplayName(post, undefined, undefined, true);
|
||||||
|
|
@ -482,7 +482,7 @@ describe('post utils', () => {
|
||||||
const post = {
|
const post = {
|
||||||
props: {
|
props: {
|
||||||
from_webhook: 'false',
|
from_webhook: 'false',
|
||||||
},
|
} as Record<string, unknown>,
|
||||||
} as PostModel;
|
} as PostModel;
|
||||||
const author = {
|
const author = {
|
||||||
username: 'user1',
|
username: 'user1',
|
||||||
|
|
@ -497,7 +497,7 @@ describe('post utils', () => {
|
||||||
const post = {
|
const post = {
|
||||||
props: {
|
props: {
|
||||||
from_webhook: 'false',
|
from_webhook: 'false',
|
||||||
},
|
} as Record<string, unknown>,
|
||||||
} as PostModel;
|
} as PostModel;
|
||||||
const author = {
|
const author = {
|
||||||
username: 'user1',
|
username: 'user1',
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import DatabaseManager from '@database/manager';
|
||||||
import {DEFAULT_LOCALE} from '@i18n';
|
import {DEFAULT_LOCALE} from '@i18n';
|
||||||
import {getUserById} from '@queries/servers/user';
|
import {getUserById} from '@queries/servers/user';
|
||||||
import {toMilliseconds} from '@utils/datetime';
|
import {toMilliseconds} from '@utils/datetime';
|
||||||
|
import {ensureString} from '@utils/types';
|
||||||
import {displayUsername, getUserIdFromChannelName} from '@utils/user';
|
import {displayUsername, getUserIdFromChannelName} from '@utils/user';
|
||||||
|
|
||||||
import type PostModel from '@typings/database/models/servers/post';
|
import type PostModel from '@typings/database/models/servers/post';
|
||||||
|
|
@ -36,7 +37,7 @@ export function areConsecutivePosts(post: PostModel, previousPost: PostModel) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isFromWebhook(post: PostModel | Post): boolean {
|
export function isFromWebhook(post: PostModel | Post): boolean {
|
||||||
return post.props && post.props.from_webhook === 'true';
|
return post.props?.from_webhook === 'true';
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isEdited(post: PostModel): boolean {
|
export function isEdited(post: PostModel): boolean {
|
||||||
|
|
@ -48,7 +49,7 @@ export function isPostEphemeral(post: PostModel): boolean {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isPostFailed(post: PostModel): boolean {
|
export function isPostFailed(post: PostModel): boolean {
|
||||||
return post.props?.failed || ((post.pendingPostId === post.id) && (Date.now() > post.updateAt + POST_TIME_TO_FAIL));
|
return Boolean(post.props?.failed) || ((post.pendingPostId === post.id) && (Date.now() > post.updateAt + POST_TIME_TO_FAIL));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isPostPendingOrFailed(post: PostModel): boolean {
|
export function isPostPendingOrFailed(post: PostModel): boolean {
|
||||||
|
|
@ -64,8 +65,13 @@ export function fromAutoResponder(post: PostModel): boolean {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function postUserDisplayName(post: PostModel, author?: UserModel, teammateNameDisplay?: string, enablePostUsernameOverride = false) {
|
export function postUserDisplayName(post: PostModel, author?: UserModel, teammateNameDisplay?: string, enablePostUsernameOverride = false) {
|
||||||
if (isFromWebhook(post) && post.props?.override_username && enablePostUsernameOverride) {
|
const overrideUsername = ensureString(post.props?.override_username);
|
||||||
return post.props.override_username;
|
if (
|
||||||
|
isFromWebhook(post) &&
|
||||||
|
enablePostUsernameOverride &&
|
||||||
|
overrideUsername
|
||||||
|
) {
|
||||||
|
return overrideUsername;
|
||||||
}
|
}
|
||||||
|
|
||||||
return displayUsername(author, author?.locale || DEFAULT_LOCALE, teammateNameDisplay, true);
|
return displayUsername(author, author?.locale || DEFAULT_LOCALE, teammateNameDisplay, true);
|
||||||
|
|
|
||||||
|
|
@ -319,7 +319,7 @@ describe('generateCombinedPost', () => {
|
||||||
const result = generateCombinedPost('combined-post-id', systemPosts);
|
const result = generateCombinedPost('combined-post-id', systemPosts);
|
||||||
|
|
||||||
// Ensure all post ids are included in the system_post_ids prop
|
// Ensure all post ids are included in the system_post_ids prop
|
||||||
expect(result.props.system_post_ids).toEqual(['post1', 'post2', 'post3']);
|
expect(result.props?.system_post_ids).toEqual(['post1', 'post2', 'post3']);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should include combined messages in the props object', () => {
|
it('should include combined messages in the props object', () => {
|
||||||
|
|
@ -331,7 +331,7 @@ describe('generateCombinedPost', () => {
|
||||||
const result = generateCombinedPost('combined-post-id', systemPosts);
|
const result = generateCombinedPost('combined-post-id', systemPosts);
|
||||||
|
|
||||||
// Ensure the messages prop includes all individual messages
|
// Ensure the messages prop includes all individual messages
|
||||||
expect(result.props.messages).toEqual(['Message 1', 'Message 2']);
|
expect(result.props?.messages).toEqual(['Message 1', 'Message 2']);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should set the post type to COMBINED_USER_ACTIVITY', () => {
|
it('should set the post type to COMBINED_USER_ACTIVITY', () => {
|
||||||
|
|
@ -387,7 +387,7 @@ describe('generateCombinedPost', () => {
|
||||||
const result = generateCombinedPost(combinedPostId, systemPosts);
|
const result = generateCombinedPost(combinedPostId, systemPosts);
|
||||||
|
|
||||||
// Extract the post types from the sorted messages in the combined post props
|
// Extract the post types from the sorted messages in the combined post props
|
||||||
const sortedPostTypes = result.props.user_activity_posts.map((post: PostModel) => post.type);
|
const sortedPostTypes = (result.props?.user_activity_posts as any).map((post: PostModel) => post.type);
|
||||||
|
|
||||||
// Expect the post types to be sorted based on their priorities in comparePostTypes
|
// Expect the post types to be sorted based on their priorities in comparePostTypes
|
||||||
expect(sortedPostTypes).toEqual([
|
expect(sortedPostTypes).toEqual([
|
||||||
|
|
@ -422,9 +422,9 @@ describe('generateCombinedPost', () => {
|
||||||
const result = generateCombinedPost(combinedPostId, systemPosts);
|
const result = generateCombinedPost(combinedPostId, systemPosts);
|
||||||
|
|
||||||
// Ensure user activities are combined for ADD_TO_TEAM
|
// Ensure user activities are combined for ADD_TO_TEAM
|
||||||
expect(result.props.user_activity_posts.length).toBe(2);
|
expect((result.props?.user_activity_posts as any).length).toBe(2);
|
||||||
expect(result.props.user_activity_posts[0].props.addedUserId).toBe('user1');
|
expect((result.props?.user_activity_posts as any)[0].props.addedUserId).toBe('user1');
|
||||||
expect(result.props.user_activity_posts[1].props.addedUserId).toBe('user2');
|
expect((result.props?.user_activity_posts as any)[1].props.addedUserId).toBe('user2');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should combine user activity for REMOVE_FROM_CHANNEL posts correctly', () => {
|
it('should combine user activity for REMOVE_FROM_CHANNEL posts correctly', () => {
|
||||||
|
|
@ -451,9 +451,9 @@ describe('generateCombinedPost', () => {
|
||||||
const result = generateCombinedPost(combinedPostId, systemPosts);
|
const result = generateCombinedPost(combinedPostId, systemPosts);
|
||||||
|
|
||||||
// Ensure user activities are combined for REMOVE_FROM_CHANNEL
|
// Ensure user activities are combined for REMOVE_FROM_CHANNEL
|
||||||
expect(result.props.user_activity_posts.length).toBe(2);
|
expect((result.props?.user_activity_posts as any).length).toBe(2);
|
||||||
expect(result.props.user_activity_posts[0].props.removedUserId).toBe('user3');
|
expect((result.props?.user_activity_posts as any)[0].props.removedUserId).toBe('user3');
|
||||||
expect(result.props.user_activity_posts[1].props.removedUserId).toBe('user4');
|
expect((result.props?.user_activity_posts as any)[1].props.removedUserId).toBe('user4');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle empty systemPosts gracefully', () => {
|
it('should handle empty systemPosts gracefully', () => {
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import moment from 'moment-timezone';
|
||||||
import {Post} from '@constants';
|
import {Post} from '@constants';
|
||||||
import {toMilliseconds} from '@utils/datetime';
|
import {toMilliseconds} from '@utils/datetime';
|
||||||
import {isFromWebhook} from '@utils/post';
|
import {isFromWebhook} from '@utils/post';
|
||||||
|
import {ensureString, isArrayOf, isStringArray} from '@utils/types';
|
||||||
|
|
||||||
import type {PostList, PostWithPrevAndNext} from '@typings/components/post_list';
|
import type {PostList, PostWithPrevAndNext} from '@typings/components/post_list';
|
||||||
import type PostModel from '@typings/database/models/servers/post';
|
import type PostModel from '@typings/database/models/servers/post';
|
||||||
|
|
@ -154,9 +155,11 @@ function isJoinLeavePostForUsername(post: PostModel, currentUsername: string): b
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (post.props.user_activity_posts) {
|
// We can be more lax with the types here because the recursive function only checks
|
||||||
|
// whether it is an array, or comparison with strings, so it should be safe enough.
|
||||||
|
if (Array.isArray(post.props.user_activity_posts)) {
|
||||||
for (const childPost of post.props.user_activity_posts as PostModel[]) {
|
for (const childPost of post.props.user_activity_posts as PostModel[]) {
|
||||||
if (isJoinLeavePostForUsername(childPost, currentUsername)) {
|
if (childPost && isJoinLeavePostForUsername(childPost, currentUsername)) {
|
||||||
// If any of the contained posts are for this user, the client will
|
// If any of the contained posts are for this user, the client will
|
||||||
// need to figure out how to render the post
|
// need to figure out how to render the post
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -264,8 +267,8 @@ function combineUserActivitySystemPost(systemPosts: PostModel[]) {
|
||||||
postType === Post.POST_TYPES.ADD_TO_CHANNEL ||
|
postType === Post.POST_TYPES.ADD_TO_CHANNEL ||
|
||||||
postType === Post.POST_TYPES.REMOVE_FROM_CHANNEL
|
postType === Post.POST_TYPES.REMOVE_FROM_CHANNEL
|
||||||
) {
|
) {
|
||||||
const userId = post.props.addedUserId || post.props.removedUserId;
|
const userId = ensureString(post.props?.addedUserId) || ensureString(post.props?.removedUserId);
|
||||||
const username = post.props.addedUsername || post.props.removedUsername;
|
const username = ensureString(post.props?.addedUsername) || ensureString(post.props?.removedUsername);
|
||||||
if (combinedPostType) {
|
if (combinedPostType) {
|
||||||
if (Array.isArray(combinedPostType[post.userId])) {
|
if (Array.isArray(combinedPostType[post.userId])) {
|
||||||
throw new Error('Invalid Post activity data');
|
throw new Error('Invalid Post activity data');
|
||||||
|
|
@ -384,3 +387,55 @@ export function shouldFilterJoinLeavePost(post: PostModel, showJoinLeave: boolea
|
||||||
// Don't filter out join/leave messages about the current user
|
// Don't filter out join/leave messages about the current user
|
||||||
return !isJoinLeavePostForUsername(post, currentUsername);
|
return !isJoinLeavePostForUsername(post, currentUsername);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type MessageData = {
|
||||||
|
actorId?: string;
|
||||||
|
postType: string;
|
||||||
|
userIds: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
function isMessageData(v: unknown): v is MessageData {
|
||||||
|
if (typeof v !== 'object' || !v) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (('actorId' in v) && typeof v.actorId !== 'string') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!('postType' in v) || typeof v.postType !== 'string') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!('userIds' in v) || !isStringArray(v.userIds)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UserActivityProp = {
|
||||||
|
allUserIds: string[];
|
||||||
|
allUsernames: string[];
|
||||||
|
messageData: MessageData[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isUserActivityProp(v: unknown): v is UserActivityProp {
|
||||||
|
if (typeof v !== 'object' || !v) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!('allUserIds' in v) || !isStringArray(v.allUserIds)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!('allUsernames' in v) || !isStringArray(v.allUsernames)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!('messageData' in v) || !isArrayOf(v.messageData, isMessageData)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
|
||||||
42
app/utils/types.ts
Normal file
42
app/utils/types.ts
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
export function isArrayOf<T>(v: unknown, check: (e: unknown) => boolean): v is T[] {
|
||||||
|
if (!Array.isArray(v)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return v.every(check);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isStringArray(v: unknown): v is string[] {
|
||||||
|
return isArrayOf(v, (e) => typeof e === 'string');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isRecordOf<T>(v: unknown, check: (e: unknown) => boolean): v is Record<string, T> {
|
||||||
|
if (typeof v !== 'object' || !v) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(Object.keys(v).every((k) => typeof k === 'string'))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(Object.values(v).every(check))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ensureString(v: unknown): string {
|
||||||
|
return typeof v === 'string' ? v : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ensureNumber(v: unknown): number {
|
||||||
|
return typeof v === 'number' ? v : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function secureGetFromRecord<T>(v: Record<string, T> | undefined, key: string) {
|
||||||
|
return typeof v === 'object' && v && Object.prototype.hasOwnProperty.call(v, key) ? v[key] : undefined;
|
||||||
|
}
|
||||||
29
app/utils/url/index.test.ts
Normal file
29
app/utils/url/index.test.ts
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
import {safeDecodeURIComponent} from './index';
|
||||||
|
|
||||||
|
describe('safeDecodeURIComponent', () => {
|
||||||
|
test('should decode a valid URI component', () => {
|
||||||
|
const encoded = 'Hello%20World';
|
||||||
|
const decoded = safeDecodeURIComponent(encoded);
|
||||||
|
expect(decoded).toBe('Hello World');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return the input if it is not a valid URI component', () => {
|
||||||
|
const invalidEncoded = '%E0%A4%A';
|
||||||
|
const result = safeDecodeURIComponent(invalidEncoded);
|
||||||
|
expect(result).toBe(invalidEncoded);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should decode a complex URI component', () => {
|
||||||
|
const encoded = 'Hello%20World%21%20How%20are%20you%3F';
|
||||||
|
const decoded = safeDecodeURIComponent(encoded);
|
||||||
|
expect(decoded).toBe('Hello World! How are you?');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return empty string if input is empty', () => {
|
||||||
|
const encoded = '';
|
||||||
|
const decoded = safeDecodeURIComponent(encoded);
|
||||||
|
expect(decoded).toBe('');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -252,3 +252,11 @@ export function extractFilenameFromUrl(url: string) {
|
||||||
const uri = urlParse(url);
|
const uri = urlParse(url);
|
||||||
return uri.pathname.split('/').pop();
|
return uri.pathname.split('/').pop();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function safeDecodeURIComponent(v: string) {
|
||||||
|
try {
|
||||||
|
return decodeURIComponent(v);
|
||||||
|
} catch {
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -454,6 +454,8 @@
|
||||||
"login.username": "Username",
|
"login.username": "Username",
|
||||||
"markdown.latex.error": "Latex render error",
|
"markdown.latex.error": "Latex render error",
|
||||||
"markdown.max_nodes.error": "This message is too long to by shown fully on a mobile device. Please view it on desktop or contact an admin to increase this limit.",
|
"markdown.max_nodes.error": "This message is too long to by shown fully on a mobile device. Please view it on desktop or contact an admin to increase this limit.",
|
||||||
|
"markdown.parse_error": "An error occurred while parsing this text",
|
||||||
|
"markdown.render_error": "An error occurred while rendering this text",
|
||||||
"mentions.empty.paragraph": "You'll see messages here when someone mentions you or uses terms you're monitoring.",
|
"mentions.empty.paragraph": "You'll see messages here when someone mentions you or uses terms you're monitoring.",
|
||||||
"mentions.empty.title": "No Mentions yet",
|
"mentions.empty.title": "No Mentions yet",
|
||||||
"mobile.about.appVersion": "App Version: {version} (Build {number})",
|
"mobile.about.appVersion": "App Version: {version} (Build {number})",
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
Requires Mattermost Server v9.5.0+. Older servers may not be able to connect or have unexpected behavior.
|
Requires Mattermost Server v9.11.0+. Older servers may not be able to connect or have unexpected behavior.
|
||||||
|
|
||||||
-------
|
-------
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
This version is compatible with Mattermost servers v9.5.0+.
|
This version is compatible with Mattermost servers v9.11.0+.
|
||||||
|
|
||||||
Please see [changelog](https://docs.mattermost.com/administration/mobile-changelog.html) for full release notes. If you're interested in helping beta test upcoming versions before they are released, please see our [documentation](https://github.com/mattermost/mattermost-mobile#testing).
|
Please see [changelog](https://docs.mattermost.com/administration/mobile-changelog.html) for full release notes. If you're interested in helping beta test upcoming versions before they are released, please see our [documentation](https://github.com/mattermost/mattermost-mobile#testing).
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1984,7 +1984,7 @@
|
||||||
CODE_SIGN_ENTITLEMENTS = Mattermost/Mattermost.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Mattermost/Mattermost.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||||
CURRENT_PROJECT_VERSION = 572;
|
CURRENT_PROJECT_VERSION = 582;
|
||||||
DEVELOPMENT_TEAM = UQ8HT4Q2XM;
|
DEVELOPMENT_TEAM = UQ8HT4Q2XM;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
HEADER_SEARCH_PATHS = "$(inherited)";
|
HEADER_SEARCH_PATHS = "$(inherited)";
|
||||||
|
|
@ -2026,7 +2026,7 @@
|
||||||
CODE_SIGN_ENTITLEMENTS = Mattermost/Mattermost.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Mattermost/Mattermost.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||||
CURRENT_PROJECT_VERSION = 572;
|
CURRENT_PROJECT_VERSION = 582;
|
||||||
DEVELOPMENT_TEAM = UQ8HT4Q2XM;
|
DEVELOPMENT_TEAM = UQ8HT4Q2XM;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
HEADER_SEARCH_PATHS = "$(inherited)";
|
HEADER_SEARCH_PATHS = "$(inherited)";
|
||||||
|
|
@ -2169,7 +2169,7 @@
|
||||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 572;
|
CURRENT_PROJECT_VERSION = 582;
|
||||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||||
DEVELOPMENT_TEAM = UQ8HT4Q2XM;
|
DEVELOPMENT_TEAM = UQ8HT4Q2XM;
|
||||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||||
|
|
@ -2219,7 +2219,7 @@
|
||||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
COPY_PHASE_STRIP = NO;
|
COPY_PHASE_STRIP = NO;
|
||||||
CURRENT_PROJECT_VERSION = 572;
|
CURRENT_PROJECT_VERSION = 582;
|
||||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||||
DEVELOPMENT_TEAM = UQ8HT4Q2XM;
|
DEVELOPMENT_TEAM = UQ8HT4Q2XM;
|
||||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@
|
||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>APPL</string>
|
<string>APPL</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>2.22.0</string>
|
<string>2.23.0</string>
|
||||||
<key>CFBundleSignature</key>
|
<key>CFBundleSignature</key>
|
||||||
<string>????</string>
|
<string>????</string>
|
||||||
<key>CFBundleURLTypes</key>
|
<key>CFBundleURLTypes</key>
|
||||||
|
|
@ -37,7 +37,7 @@
|
||||||
</dict>
|
</dict>
|
||||||
</array>
|
</array>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>572</string>
|
<string>582</string>
|
||||||
<key>ITSAppUsesNonExemptEncryption</key>
|
<key>ITSAppUsesNonExemptEncryption</key>
|
||||||
<false/>
|
<false/>
|
||||||
<key>LSRequiresIPhoneOS</key>
|
<key>LSRequiresIPhoneOS</key>
|
||||||
|
|
|
||||||
|
|
@ -19,9 +19,9 @@
|
||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>XPC!</string>
|
<string>XPC!</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>2.22.0</string>
|
<string>2.23.0</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>572</string>
|
<string>582</string>
|
||||||
<key>UIAppFonts</key>
|
<key>UIAppFonts</key>
|
||||||
<array>
|
<array>
|
||||||
<string>OpenSans-Bold.ttf</string>
|
<string>OpenSans-Bold.ttf</string>
|
||||||
|
|
|
||||||
|
|
@ -19,9 +19,9 @@
|
||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>XPC!</string>
|
<string>XPC!</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>2.22.0</string>
|
<string>2.23.0</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>572</string>
|
<string>582</string>
|
||||||
<key>NSExtension</key>
|
<key>NSExtension</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>NSExtensionPointIdentifier</key>
|
<key>NSExtensionPointIdentifier</key>
|
||||||
|
|
|
||||||
|
|
@ -1162,7 +1162,7 @@ PODS:
|
||||||
- Yoga
|
- Yoga
|
||||||
- react-native-netinfo (11.3.2):
|
- react-native-netinfo (11.3.2):
|
||||||
- React-Core
|
- React-Core
|
||||||
- react-native-network-client (1.7.2):
|
- react-native-network-client (1.7.3):
|
||||||
- Alamofire (~> 5.9.1)
|
- Alamofire (~> 5.9.1)
|
||||||
- DoubleConversion
|
- DoubleConversion
|
||||||
- glog
|
- glog
|
||||||
|
|
@ -2043,7 +2043,7 @@ SPEC CHECKSUMS:
|
||||||
react-native-emm: ecab44d78fb1cc7d7b7901914f48fb6309c46ff2
|
react-native-emm: ecab44d78fb1cc7d7b7901914f48fb6309c46ff2
|
||||||
react-native-image-picker: c3afe5472ef870d98a4b28415fc0b928161ee5f7
|
react-native-image-picker: c3afe5472ef870d98a4b28415fc0b928161ee5f7
|
||||||
react-native-netinfo: 076df4f9b07f6670acf4ce9a75aac8d34c2e2ccc
|
react-native-netinfo: 076df4f9b07f6670acf4ce9a75aac8d34c2e2ccc
|
||||||
react-native-network-client: 5173c47230b5f497cdef469cba8e6e1b3df687eb
|
react-native-network-client: 82ef7c000c5fd3bfd1ad9e78cd920af33040f9f3
|
||||||
react-native-notifications: 4601a5a8db4ced6ae7cfc43b44d35fe437ac50c4
|
react-native-notifications: 4601a5a8db4ced6ae7cfc43b44d35fe437ac50c4
|
||||||
react-native-paste-input: 011a9916ef3acf809a7da122847c61ca0f93a60e
|
react-native-paste-input: 011a9916ef3acf809a7da122847c61ca0f93a60e
|
||||||
react-native-performance: ff93f8af3b2ee9519fd7879896aa9b8b8272691d
|
react-native-performance: ff93f8af3b2ee9519fd7879896aa9b8b8272691d
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import React
|
||||||
@objc private var hasRegisteredLoad = false
|
@objc private var hasRegisteredLoad = false
|
||||||
|
|
||||||
deinit {
|
deinit {
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.sync {
|
||||||
guard let w = UIApplication.shared.delegate?.window, let window = w else { return }
|
guard let w = UIApplication.shared.delegate?.window, let window = w else { return }
|
||||||
window.removeObserver(self, forKeyPath: "frame")
|
window.removeObserver(self, forKeyPath: "frame")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
296
package-lock.json
generated
296
package-lock.json
generated
|
|
@ -1,12 +1,12 @@
|
||||||
{
|
{
|
||||||
"name": "mattermost-mobile",
|
"name": "mattermost-mobile",
|
||||||
"version": "2.22.0",
|
"version": "2.23.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "mattermost-mobile",
|
"name": "mattermost-mobile",
|
||||||
"version": "2.21.0",
|
"version": "2.22.0",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "Apache 2.0",
|
"license": "Apache 2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|
@ -17,12 +17,12 @@
|
||||||
"@formatjs/intl-numberformat": "8.10.3",
|
"@formatjs/intl-numberformat": "8.10.3",
|
||||||
"@formatjs/intl-pluralrules": "5.2.14",
|
"@formatjs/intl-pluralrules": "5.2.14",
|
||||||
"@gorhom/bottom-sheet": "4.6.4",
|
"@gorhom/bottom-sheet": "4.6.4",
|
||||||
"@mattermost/calls": "github:mattermost/calls-common#07607cf603f1e3f0c86ae248b2332e8da17f9cf8",
|
"@mattermost/calls": "github:mattermost/calls-common#030ff7c0a37ee9b0ccc5fae0b2ea9c0e1c08473f",
|
||||||
"@mattermost/compass-icons": "0.1.45",
|
"@mattermost/compass-icons": "0.1.45",
|
||||||
"@mattermost/hardware-keyboard": "file:./libraries/@mattermost/hardware-keyboard",
|
"@mattermost/hardware-keyboard": "file:./libraries/@mattermost/hardware-keyboard",
|
||||||
"@mattermost/keyboard-tracker": "file:./libraries/@mattermost/keyboard-tracker",
|
"@mattermost/keyboard-tracker": "file:./libraries/@mattermost/keyboard-tracker",
|
||||||
"@mattermost/react-native-emm": "1.5.0",
|
"@mattermost/react-native-emm": "1.5.0",
|
||||||
"@mattermost/react-native-network-client": "1.7.2",
|
"@mattermost/react-native-network-client": "1.7.3",
|
||||||
"@mattermost/react-native-paste-input": "0.8.0",
|
"@mattermost/react-native-paste-input": "0.8.0",
|
||||||
"@mattermost/react-native-turbo-log": "0.4.0",
|
"@mattermost/react-native-turbo-log": "0.4.0",
|
||||||
"@mattermost/rnshare": "file:./libraries/@mattermost/rnshare",
|
"@mattermost/rnshare": "file:./libraries/@mattermost/rnshare",
|
||||||
|
|
@ -42,7 +42,7 @@
|
||||||
"@stream-io/flat-list-mvcp": "0.10.3",
|
"@stream-io/flat-list-mvcp": "0.10.3",
|
||||||
"@voximplant/react-native-foreground-service": "3.0.2",
|
"@voximplant/react-native-foreground-service": "3.0.2",
|
||||||
"base-64": "1.0.0",
|
"base-64": "1.0.0",
|
||||||
"commonmark": "npm:@mattermost/commonmark@0.30.1-3",
|
"commonmark": "npm:@mattermost/commonmark@0.30.1-4",
|
||||||
"commonmark-react-renderer": "github:mattermost/commonmark-react-renderer#81b5d27509652bae50b4b510ede777dd3bd923cf",
|
"commonmark-react-renderer": "github:mattermost/commonmark-react-renderer#81b5d27509652bae50b4b510ede777dd3bd923cf",
|
||||||
"deep-equal": "2.2.3",
|
"deep-equal": "2.2.3",
|
||||||
"deepmerge": "4.3.1",
|
"deepmerge": "4.3.1",
|
||||||
|
|
@ -4533,26 +4533,12 @@
|
||||||
"integrity": "sha512-QdWi16+CHB9JYP7gma19OVVg0BFkvU8zNj9GjWorYI8Iv8FUxjOCcYRuAmX4s/h91e4e7BPsskc8cSrZYho9Ew==",
|
"integrity": "sha512-QdWi16+CHB9JYP7gma19OVVg0BFkvU8zNj9GjWorYI8Iv8FUxjOCcYRuAmX4s/h91e4e7BPsskc8cSrZYho9Ew==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cross-spawn": "^7.0.3"
|
"cross-spawn": "^7.0.6"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@expo/spawn-async/node_modules/cross-spawn": {
|
|
||||||
"version": "7.0.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
|
|
||||||
"integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"path-key": "^3.1.0",
|
|
||||||
"shebang-command": "^2.0.0",
|
|
||||||
"which": "^2.0.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@expo/vector-icons": {
|
"node_modules/@expo/vector-icons": {
|
||||||
"version": "14.0.2",
|
"version": "14.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/@expo/vector-icons/-/vector-icons-14.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/@expo/vector-icons/-/vector-icons-14.0.2.tgz",
|
||||||
|
|
@ -5851,11 +5837,11 @@
|
||||||
"node_modules/@mattermost/calls": {
|
"node_modules/@mattermost/calls": {
|
||||||
"name": "@mattermost/calls-common",
|
"name": "@mattermost/calls-common",
|
||||||
"version": "0.27.2",
|
"version": "0.27.2",
|
||||||
"resolved": "git+ssh://git@github.com/mattermost/calls-common.git#07607cf603f1e3f0c86ae248b2332e8da17f9cf8",
|
"resolved": "git+ssh://git@github.com/mattermost/calls-common.git#030ff7c0a37ee9b0ccc5fae0b2ea9c0e1c08473f",
|
||||||
"integrity": "sha512-aZIYwtgRVj8tdWJUPtAr4TPNa6tNM3lnRkC74CkSsFCpwZ1miwov0B4TLYG4Srj1rTgUqUwG3gQz4o5JHJ2fuQ==",
|
"integrity": "sha512-qtMUUGHrl6VOGgjyQ+Ft4YddWo5RV8MDK8zSaYRU/1MiiY/zqq5kW6nvuzMB2S0xAvPwYlcFEBfmM4QZrReD6w==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@msgpack/msgpack": "^3.0.0-beta2",
|
"@msgpack/msgpack": "3.0.0-beta2",
|
||||||
"fflate": "^0.8.2"
|
"fflate": "0.8.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@mattermost/calls/node_modules/@msgpack/msgpack": {
|
"node_modules/@mattermost/calls/node_modules/@msgpack/msgpack": {
|
||||||
|
|
@ -5908,9 +5894,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@mattermost/react-native-network-client": {
|
"node_modules/@mattermost/react-native-network-client": {
|
||||||
"version": "1.7.2",
|
"version": "1.7.3",
|
||||||
"resolved": "https://registry.npmjs.org/@mattermost/react-native-network-client/-/react-native-network-client-1.7.2.tgz",
|
"resolved": "https://registry.npmjs.org/@mattermost/react-native-network-client/-/react-native-network-client-1.7.3.tgz",
|
||||||
"integrity": "sha512-UaFhUaQkmb6Hq2LOyutiwL0HeMVHmhBBpgojLi83zXXXB5OgNWDgqbDBo6Y986c5TwhCy1/LxENdHBc+mI4RZA==",
|
"integrity": "sha512-gJSQ4Prf5jcbPlxVylhEtBGafSBYZSat+T21LIDHksGOfeY+jvPL3p8dS+cq+0D1AOHQ3tHNBkZ7RaY6IdUadw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"validator": "13.12.0",
|
"validator": "13.12.0",
|
||||||
|
|
@ -12057,9 +12043,9 @@
|
||||||
},
|
},
|
||||||
"node_modules/commonmark": {
|
"node_modules/commonmark": {
|
||||||
"name": "@mattermost/commonmark",
|
"name": "@mattermost/commonmark",
|
||||||
"version": "0.30.1-3",
|
"version": "0.30.1-4",
|
||||||
"resolved": "https://registry.npmjs.org/@mattermost/commonmark/-/commonmark-0.30.1-3.tgz",
|
"resolved": "https://registry.npmjs.org/@mattermost/commonmark/-/commonmark-0.30.1-4.tgz",
|
||||||
"integrity": "sha512-Kjsl/sZmb6R6PtpPVIifPfqBMrKs7z6Tukb1TJl/S0LfC5uNici3yol4waGxhGsJuvCF2kZqStwVQP7ieUbAgw==",
|
"integrity": "sha512-7O1xQfP3ILHf/+wjrxwF0rqowuvqQ7EY+SJOWsDaV+VXdj+lmft9yxkL0QPNYALinKFdPEj1gFtgR+pi4w8lzQ==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"entities": "~3.0.1",
|
"entities": "~3.0.1",
|
||||||
"mdurl": "~1.0.1",
|
"mdurl": "~1.0.1",
|
||||||
|
|
@ -12360,6 +12346,20 @@
|
||||||
"node-fetch": "^2.6.12"
|
"node-fetch": "^2.6.12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/cross-spawn": {
|
||||||
|
"version": "7.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||||
|
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"path-key": "^3.1.0",
|
||||||
|
"shebang-command": "^2.0.0",
|
||||||
|
"which": "^2.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/crypt": {
|
"node_modules/crypt": {
|
||||||
"version": "0.0.2",
|
"version": "0.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz",
|
||||||
|
|
@ -12669,29 +12669,13 @@
|
||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/default-gateway/node_modules/cross-spawn": {
|
|
||||||
"version": "6.0.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz",
|
|
||||||
"integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"nice-try": "^1.0.4",
|
|
||||||
"path-key": "^2.0.1",
|
|
||||||
"semver": "^5.5.0",
|
|
||||||
"shebang-command": "^1.2.0",
|
|
||||||
"which": "^1.2.9"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=4.8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/default-gateway/node_modules/execa": {
|
"node_modules/default-gateway/node_modules/execa": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz",
|
||||||
"integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==",
|
"integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cross-spawn": "^6.0.0",
|
"cross-spawn": "^7.0.6",
|
||||||
"get-stream": "^4.0.0",
|
"get-stream": "^4.0.0",
|
||||||
"is-stream": "^1.1.0",
|
"is-stream": "^1.1.0",
|
||||||
"npm-run-path": "^2.0.0",
|
"npm-run-path": "^2.0.0",
|
||||||
|
|
@ -12745,48 +12729,6 @@
|
||||||
"node": ">=4"
|
"node": ">=4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/default-gateway/node_modules/semver": {
|
|
||||||
"version": "5.7.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz",
|
|
||||||
"integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==",
|
|
||||||
"license": "ISC",
|
|
||||||
"bin": {
|
|
||||||
"semver": "bin/semver"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/default-gateway/node_modules/shebang-command": {
|
|
||||||
"version": "1.2.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz",
|
|
||||||
"integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"shebang-regex": "^1.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=0.10.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/default-gateway/node_modules/shebang-regex": {
|
|
||||||
"version": "1.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz",
|
|
||||||
"integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=0.10.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/default-gateway/node_modules/which": {
|
|
||||||
"version": "1.3.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
|
|
||||||
"integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==",
|
|
||||||
"license": "ISC",
|
|
||||||
"dependencies": {
|
|
||||||
"isexe": "^2.0.0"
|
|
||||||
},
|
|
||||||
"bin": {
|
|
||||||
"which": "bin/which"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/defaults": {
|
"node_modules/defaults": {
|
||||||
"version": "1.0.4",
|
"version": "1.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz",
|
||||||
|
|
@ -13510,7 +13452,7 @@
|
||||||
"@ungap/structured-clone": "^1.2.0",
|
"@ungap/structured-clone": "^1.2.0",
|
||||||
"ajv": "^6.12.4",
|
"ajv": "^6.12.4",
|
||||||
"chalk": "^4.0.0",
|
"chalk": "^4.0.0",
|
||||||
"cross-spawn": "^7.0.2",
|
"cross-spawn": "^7.0.6",
|
||||||
"debug": "^4.3.2",
|
"debug": "^4.3.2",
|
||||||
"doctrine": "^3.0.0",
|
"doctrine": "^3.0.0",
|
||||||
"escape-string-regexp": "^4.0.0",
|
"escape-string-regexp": "^4.0.0",
|
||||||
|
|
@ -14026,20 +13968,6 @@
|
||||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/eslint/node_modules/cross-spawn": {
|
|
||||||
"version": "7.0.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
|
|
||||||
"integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
|
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
|
||||||
"path-key": "^3.1.0",
|
|
||||||
"shebang-command": "^2.0.0",
|
|
||||||
"which": "^2.0.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/eslint/node_modules/eslint-scope": {
|
"node_modules/eslint/node_modules/eslint-scope": {
|
||||||
"version": "7.2.2",
|
"version": "7.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz",
|
||||||
|
|
@ -14340,7 +14268,7 @@
|
||||||
"resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz",
|
||||||
"integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==",
|
"integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cross-spawn": "^7.0.3",
|
"cross-spawn": "^7.0.6",
|
||||||
"get-stream": "^6.0.0",
|
"get-stream": "^6.0.0",
|
||||||
"human-signals": "^2.1.0",
|
"human-signals": "^2.1.0",
|
||||||
"is-stream": "^2.0.0",
|
"is-stream": "^2.0.0",
|
||||||
|
|
@ -14357,19 +14285,6 @@
|
||||||
"url": "https://github.com/sindresorhus/execa?sponsor=1"
|
"url": "https://github.com/sindresorhus/execa?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/execa/node_modules/cross-spawn": {
|
|
||||||
"version": "7.0.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
|
|
||||||
"integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
|
|
||||||
"dependencies": {
|
|
||||||
"path-key": "^3.1.0",
|
|
||||||
"shebang-command": "^2.0.0",
|
|
||||||
"which": "^2.0.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/exit": {
|
"node_modules/exit": {
|
||||||
"version": "0.1.2",
|
"version": "0.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz",
|
||||||
|
|
@ -15153,7 +15068,7 @@
|
||||||
"integrity": "sha512-PXUUyLqrR2XCWICfv6ukppP96sdFwWbNEnfEMt7jNsISjMsvaLNinAHNDYyvkyU+SZG2BTSbT5NjG+vZslfGTA==",
|
"integrity": "sha512-PXUUyLqrR2XCWICfv6ukppP96sdFwWbNEnfEMt7jNsISjMsvaLNinAHNDYyvkyU+SZG2BTSbT5NjG+vZslfGTA==",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cross-spawn": "^7.0.0",
|
"cross-spawn": "^7.0.6",
|
||||||
"signal-exit": "^4.0.1"
|
"signal-exit": "^4.0.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|
@ -15163,20 +15078,6 @@
|
||||||
"url": "https://github.com/sponsors/isaacs"
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/foreground-child/node_modules/cross-spawn": {
|
|
||||||
"version": "7.0.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
|
|
||||||
"integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"path-key": "^3.1.0",
|
|
||||||
"shebang-command": "^2.0.0",
|
|
||||||
"which": "^2.0.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/foreground-child/node_modules/signal-exit": {
|
"node_modules/foreground-child/node_modules/signal-exit": {
|
||||||
"version": "4.1.0",
|
"version": "4.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
|
||||||
|
|
@ -20964,11 +20865,6 @@
|
||||||
"integrity": "sha512-SrQrok4CATudVzBS7coSz26QRSmlK9TzzoFbeKfcPBUFPjcQM9Rqvr/DlJkOrwI/0KcgvMub1n1g5Jt9EgRn4A==",
|
"integrity": "sha512-SrQrok4CATudVzBS7coSz26QRSmlK9TzzoFbeKfcPBUFPjcQM9Rqvr/DlJkOrwI/0KcgvMub1n1g5Jt9EgRn4A==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/nice-try": {
|
|
||||||
"version": "1.0.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz",
|
|
||||||
"integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ=="
|
|
||||||
},
|
|
||||||
"node_modules/nocache": {
|
"node_modules/nocache": {
|
||||||
"version": "3.0.4",
|
"version": "3.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/nocache/-/nocache-3.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/nocache/-/nocache-3.0.4.tgz",
|
||||||
|
|
@ -21461,27 +21357,13 @@
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/os-locale/node_modules/cross-spawn": {
|
|
||||||
"version": "7.0.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
|
|
||||||
"integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
|
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
|
||||||
"path-key": "^3.1.0",
|
|
||||||
"shebang-command": "^2.0.0",
|
|
||||||
"which": "^2.0.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/os-locale/node_modules/execa": {
|
"node_modules/os-locale/node_modules/execa": {
|
||||||
"version": "4.1.0",
|
"version": "4.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/execa/-/execa-4.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/execa/-/execa-4.1.0.tgz",
|
||||||
"integrity": "sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA==",
|
"integrity": "sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA==",
|
||||||
"peer": true,
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cross-spawn": "^7.0.0",
|
"cross-spawn": "^7.0.6",
|
||||||
"get-stream": "^5.0.0",
|
"get-stream": "^5.0.0",
|
||||||
"human-signals": "^1.1.1",
|
"human-signals": "^1.1.1",
|
||||||
"is-stream": "^2.0.0",
|
"is-stream": "^2.0.0",
|
||||||
|
|
@ -21741,21 +21623,7 @@
|
||||||
"license": "0BSD",
|
"license": "0BSD",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ansi-escapes": "^4.3.2",
|
"ansi-escapes": "^4.3.2",
|
||||||
"cross-spawn": "^7.0.3"
|
"cross-spawn": "^7.0.6"
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/password-prompt/node_modules/cross-spawn": {
|
|
||||||
"version": "7.0.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
|
|
||||||
"integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"path-key": "^3.1.0",
|
|
||||||
"shebang-command": "^2.0.0",
|
|
||||||
"which": "^2.0.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 8"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/patch-package": {
|
"node_modules/patch-package": {
|
||||||
|
|
@ -21767,7 +21635,7 @@
|
||||||
"@yarnpkg/lockfile": "^1.1.0",
|
"@yarnpkg/lockfile": "^1.1.0",
|
||||||
"chalk": "^4.1.2",
|
"chalk": "^4.1.2",
|
||||||
"ci-info": "^3.7.0",
|
"ci-info": "^3.7.0",
|
||||||
"cross-spawn": "^7.0.3",
|
"cross-spawn": "^7.0.6",
|
||||||
"find-yarn-workspace-root": "^2.0.0",
|
"find-yarn-workspace-root": "^2.0.0",
|
||||||
"fs-extra": "^9.0.0",
|
"fs-extra": "^9.0.0",
|
||||||
"json-stable-stringify": "^1.0.2",
|
"json-stable-stringify": "^1.0.2",
|
||||||
|
|
@ -21837,20 +21705,6 @@
|
||||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/patch-package/node_modules/cross-spawn": {
|
|
||||||
"version": "7.0.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
|
|
||||||
"integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
|
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
|
||||||
"path-key": "^3.1.0",
|
|
||||||
"shebang-command": "^2.0.0",
|
|
||||||
"which": "^2.0.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/patch-package/node_modules/fs-extra": {
|
"node_modules/patch-package/node_modules/fs-extra": {
|
||||||
"version": "9.1.0",
|
"version": "9.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz",
|
||||||
|
|
@ -27392,29 +27246,13 @@
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/username/node_modules/cross-spawn": {
|
|
||||||
"version": "6.0.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz",
|
|
||||||
"integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==",
|
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
|
||||||
"nice-try": "^1.0.4",
|
|
||||||
"path-key": "^2.0.1",
|
|
||||||
"semver": "^5.5.0",
|
|
||||||
"shebang-command": "^1.2.0",
|
|
||||||
"which": "^1.2.9"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=4.8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/username/node_modules/execa": {
|
"node_modules/username/node_modules/execa": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz",
|
||||||
"integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==",
|
"integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==",
|
||||||
"peer": true,
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cross-spawn": "^6.0.0",
|
"cross-spawn": "^7.0.6",
|
||||||
"get-stream": "^4.0.0",
|
"get-stream": "^4.0.0",
|
||||||
"is-stream": "^1.1.0",
|
"is-stream": "^1.1.0",
|
||||||
"npm-run-path": "^2.0.0",
|
"npm-run-path": "^2.0.0",
|
||||||
|
|
@ -27482,48 +27320,6 @@
|
||||||
"node": ">=4"
|
"node": ">=4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/username/node_modules/semver": {
|
|
||||||
"version": "5.7.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz",
|
|
||||||
"integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==",
|
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
|
||||||
"semver": "bin/semver"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/username/node_modules/shebang-command": {
|
|
||||||
"version": "1.2.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz",
|
|
||||||
"integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==",
|
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
|
||||||
"shebang-regex": "^1.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=0.10.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/username/node_modules/shebang-regex": {
|
|
||||||
"version": "1.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz",
|
|
||||||
"integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==",
|
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
|
||||||
"node": ">=0.10.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/username/node_modules/which": {
|
|
||||||
"version": "1.3.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
|
|
||||||
"integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==",
|
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
|
||||||
"isexe": "^2.0.0"
|
|
||||||
},
|
|
||||||
"bin": {
|
|
||||||
"which": "bin/which"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/util-deprecate": {
|
"node_modules/util-deprecate": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||||
|
|
@ -27711,7 +27507,7 @@
|
||||||
"@webpack-cli/serve": "^1.7.0",
|
"@webpack-cli/serve": "^1.7.0",
|
||||||
"colorette": "^2.0.14",
|
"colorette": "^2.0.14",
|
||||||
"commander": "^7.0.0",
|
"commander": "^7.0.0",
|
||||||
"cross-spawn": "^7.0.3",
|
"cross-spawn": "^7.0.6",
|
||||||
"fastest-levenshtein": "^1.0.12",
|
"fastest-levenshtein": "^1.0.12",
|
||||||
"import-local": "^3.0.2",
|
"import-local": "^3.0.2",
|
||||||
"interpret": "^2.2.0",
|
"interpret": "^2.2.0",
|
||||||
|
|
@ -27761,20 +27557,6 @@
|
||||||
"node": ">= 10"
|
"node": ">= 10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/webpack-cli/node_modules/cross-spawn": {
|
|
||||||
"version": "7.0.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
|
|
||||||
"integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
|
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
|
||||||
"path-key": "^3.1.0",
|
|
||||||
"shebang-command": "^2.0.0",
|
|
||||||
"which": "^2.0.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/webpack-merge": {
|
"node_modules/webpack-merge": {
|
||||||
"version": "5.10.0",
|
"version": "5.10.0",
|
||||||
"resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.10.0.tgz",
|
"resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.10.0.tgz",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "mattermost-mobile",
|
"name": "mattermost-mobile",
|
||||||
"version": "2.22.0",
|
"version": "2.23.0",
|
||||||
"description": "Mattermost Mobile with React Native",
|
"description": "Mattermost Mobile with React Native",
|
||||||
"repository": "git@github.com:mattermost/mattermost-mobile.git",
|
"repository": "git@github.com:mattermost/mattermost-mobile.git",
|
||||||
"author": "Mattermost, Inc.",
|
"author": "Mattermost, Inc.",
|
||||||
|
|
@ -18,12 +18,12 @@
|
||||||
"@formatjs/intl-numberformat": "8.10.3",
|
"@formatjs/intl-numberformat": "8.10.3",
|
||||||
"@formatjs/intl-pluralrules": "5.2.14",
|
"@formatjs/intl-pluralrules": "5.2.14",
|
||||||
"@gorhom/bottom-sheet": "4.6.4",
|
"@gorhom/bottom-sheet": "4.6.4",
|
||||||
"@mattermost/calls": "github:mattermost/calls-common#07607cf603f1e3f0c86ae248b2332e8da17f9cf8",
|
"@mattermost/calls": "github:mattermost/calls-common#030ff7c0a37ee9b0ccc5fae0b2ea9c0e1c08473f",
|
||||||
"@mattermost/compass-icons": "0.1.45",
|
"@mattermost/compass-icons": "0.1.45",
|
||||||
"@mattermost/hardware-keyboard": "file:./libraries/@mattermost/hardware-keyboard",
|
"@mattermost/hardware-keyboard": "file:./libraries/@mattermost/hardware-keyboard",
|
||||||
"@mattermost/keyboard-tracker": "file:./libraries/@mattermost/keyboard-tracker",
|
"@mattermost/keyboard-tracker": "file:./libraries/@mattermost/keyboard-tracker",
|
||||||
"@mattermost/react-native-emm": "1.5.0",
|
"@mattermost/react-native-emm": "1.5.0",
|
||||||
"@mattermost/react-native-network-client": "1.7.2",
|
"@mattermost/react-native-network-client": "1.7.3",
|
||||||
"@mattermost/react-native-paste-input": "0.8.0",
|
"@mattermost/react-native-paste-input": "0.8.0",
|
||||||
"@mattermost/react-native-turbo-log": "0.4.0",
|
"@mattermost/react-native-turbo-log": "0.4.0",
|
||||||
"@mattermost/rnshare": "file:./libraries/@mattermost/rnshare",
|
"@mattermost/rnshare": "file:./libraries/@mattermost/rnshare",
|
||||||
|
|
@ -43,7 +43,7 @@
|
||||||
"@stream-io/flat-list-mvcp": "0.10.3",
|
"@stream-io/flat-list-mvcp": "0.10.3",
|
||||||
"@voximplant/react-native-foreground-service": "3.0.2",
|
"@voximplant/react-native-foreground-service": "3.0.2",
|
||||||
"base-64": "1.0.0",
|
"base-64": "1.0.0",
|
||||||
"commonmark": "npm:@mattermost/commonmark@0.30.1-3",
|
"commonmark": "npm:@mattermost/commonmark@0.30.1-4",
|
||||||
"commonmark-react-renderer": "github:mattermost/commonmark-react-renderer#81b5d27509652bae50b4b510ede777dd3bd923cf",
|
"commonmark-react-renderer": "github:mattermost/commonmark-react-renderer#81b5d27509652bae50b4b510ede777dd3bd923cf",
|
||||||
"deep-equal": "2.2.3",
|
"deep-equal": "2.2.3",
|
||||||
"deepmerge": "4.3.1",
|
"deepmerge": "4.3.1",
|
||||||
|
|
|
||||||
2
types/api/files.d.ts
vendored
2
types/api/files.d.ts
vendored
|
|
@ -22,7 +22,7 @@ type FileInfo = {
|
||||||
uri?: string;
|
uri?: string;
|
||||||
user_id: string;
|
user_id: string;
|
||||||
width: number;
|
width: number;
|
||||||
postProps?: Record<string, any>;
|
postProps?: Record<string, unknown>;
|
||||||
};
|
};
|
||||||
|
|
||||||
type FilesState = {
|
type FilesState = {
|
||||||
|
|
|
||||||
2
types/api/posts.d.ts
vendored
2
types/api/posts.d.ts
vendored
|
|
@ -71,7 +71,7 @@ type Post = {
|
||||||
message_source?: string;
|
message_source?: string;
|
||||||
type: PostType;
|
type: PostType;
|
||||||
participants?: null | UserProfile[]|string[];
|
participants?: null | UserProfile[]|string[];
|
||||||
props: Record<string, any>;
|
props: Record<string, unknown> | undefined;
|
||||||
hashtags: string;
|
hashtags: string;
|
||||||
pending_post_id: string;
|
pending_post_id: string;
|
||||||
reply_count: number;
|
reply_count: number;
|
||||||
|
|
|
||||||
|
|
@ -71,7 +71,7 @@ declare class PostModel extends Model {
|
||||||
userId: string;
|
userId: string;
|
||||||
|
|
||||||
/** props : Additional attributes for this props */
|
/** props : Additional attributes for this props */
|
||||||
props: any;
|
props: Record<string, unknown> | null;
|
||||||
|
|
||||||
/** drafts : Every draft associated with this Post */
|
/** drafts : Every draft associated with this Post */
|
||||||
drafts: Query<DraftModel>;
|
drafts: Query<DraftModel>;
|
||||||
|
|
|
||||||
|
|
@ -76,7 +76,7 @@ export type GalleryItemType = {
|
||||||
authorId?: string;
|
authorId?: string;
|
||||||
size?: number;
|
size?: number;
|
||||||
postId?: string;
|
postId?: string;
|
||||||
postProps?: Record<string, any> & {captions?: Caption[]};
|
postProps?: Record<string, unknown> & {captions?: Caption[]};
|
||||||
};
|
};
|
||||||
|
|
||||||
export type GalleryAction = 'none' | 'downloading' | 'copying' | 'sharing' | 'opening' | 'external';
|
export type GalleryAction = 'none' | 'downloading' | 'copying' | 'sharing' | 'opening' | 'external';
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue