MM-23230 Batch post actions and fine tune postlint (#4042)
* PostList optimizations on FlatList * Stop scroll to index if there is an interaction * Fix unit tests * MM-23176 Fix crash due to scrollToIndex out of range * Batch mark channel as read action * Fine tune post list * Batch actions for getting posts * Update app/utils/push_notifications.js Co-Authored-By: Miguel Alatzar <migbot@users.noreply.github.com> * Update app/actions/views/channel.js Co-Authored-By: Miguel Alatzar <migbot@users.noreply.github.com> * Pass state as arg to markAsViewedAndReadBatch Co-authored-by: Miguel Alatzar <migbot@users.noreply.github.com>
This commit is contained in:
parent
e8150fa7cc
commit
ec4dfb65b2
14 changed files with 558 additions and 132 deletions
|
|
@ -13,12 +13,6 @@ import {
|
|||
markChannelAsViewed,
|
||||
leaveChannel as serviceLeaveChannel,
|
||||
} from 'mattermost-redux/actions/channels';
|
||||
import {
|
||||
getPosts,
|
||||
getPostsBefore,
|
||||
getPostsSince,
|
||||
getPostThread,
|
||||
} from 'mattermost-redux/actions/posts';
|
||||
import {getFilesForPost} from 'mattermost-redux/actions/files';
|
||||
import {savePreferences} from 'mattermost-redux/actions/preferences';
|
||||
import {loadRolesIfNeeded} from 'mattermost-redux/actions/roles';
|
||||
|
|
@ -56,6 +50,7 @@ import telemetry from 'app/telemetry';
|
|||
import {isDirectChannelVisible, isGroupChannelVisible, isDirectMessageVisible, isGroupMessageVisible, isDirectChannelAutoClosed} from 'app/utils/channels';
|
||||
import {buildPreference} from 'app/utils/preferences';
|
||||
|
||||
import {getPosts, getPostsBefore, getPostsSince, getPostThread} from './post';
|
||||
import {forceLogoutIfNecessary} from './user';
|
||||
|
||||
const MAX_RETRIES = 3;
|
||||
|
|
@ -359,7 +354,8 @@ export function handleSelectChannel(channelId) {
|
|||
dispatch(loadPostsIfNecessaryWithRetry(channelId));
|
||||
|
||||
if (channel && currentChannelId !== channelId) {
|
||||
dispatch({
|
||||
const actions = markAsViewedAndReadBatch(state, channelId, currentChannelId);
|
||||
actions.push({
|
||||
type: ChannelTypes.SELECT_CHANNEL,
|
||||
data: channelId,
|
||||
extra: {
|
||||
|
|
@ -368,11 +364,10 @@ export function handleSelectChannel(channelId) {
|
|||
teamId: channel.team_id || currentTeamId,
|
||||
},
|
||||
});
|
||||
|
||||
dispatch(markChannelViewedAndRead(channelId, currentChannelId));
|
||||
dispatch(batchActions(actions));
|
||||
}
|
||||
|
||||
console.log('channel switch to', channel?.display_name, (Date.now() - dt), 'ms'); //eslint-disable-line
|
||||
console.log('channel switch to', channel?.display_name, channelId, (Date.now() - dt), 'ms'); //eslint-disable-line
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -435,6 +430,81 @@ export function markChannelViewedAndRead(channelId, previousChannelId, markOnSer
|
|||
};
|
||||
}
|
||||
|
||||
function markAsViewedAndReadBatch(state, channelId, prevChannelId = '', markOnServer = true) {
|
||||
const actions = [];
|
||||
const {channels, myMembers} = state.entities.channels;
|
||||
const channel = channels[channelId];
|
||||
const member = myMembers[channelId];
|
||||
const prevMember = myMembers[prevChannelId];
|
||||
const prevChanManuallyUnread = isManuallyUnread(state, prevChannelId);
|
||||
const prevChannel = (!prevChanManuallyUnread && prevChannelId) ? channels[prevChannelId] : null; // May be null since prevChannelId is optional
|
||||
|
||||
if (markOnServer) {
|
||||
Client4.viewMyChannel(channelId, prevChanManuallyUnread ? '' : prevChannelId);
|
||||
}
|
||||
|
||||
if (member) {
|
||||
actions.push({
|
||||
type: ChannelTypes.RECEIVED_MY_CHANNEL_MEMBER,
|
||||
data: {...member, last_viewed_at: Date.now()},
|
||||
});
|
||||
|
||||
if (isManuallyUnread(state, channelId)) {
|
||||
actions.push({
|
||||
type: ChannelTypes.REMOVE_MANUALLY_UNREAD,
|
||||
data: {channelId},
|
||||
});
|
||||
}
|
||||
|
||||
if (channel) {
|
||||
actions.push({
|
||||
type: ChannelTypes.DECREMENT_UNREAD_MSG_COUNT,
|
||||
data: {
|
||||
teamId: channel.team_id,
|
||||
channelId,
|
||||
amount: channel.total_msg_count - member.msg_count,
|
||||
},
|
||||
}, {
|
||||
type: ChannelTypes.DECREMENT_UNREAD_MENTION_COUNT,
|
||||
data: {
|
||||
teamId: channel.team_id,
|
||||
channelId,
|
||||
amount: member.mention_count,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (prevMember) {
|
||||
if (!prevChanManuallyUnread) {
|
||||
actions.push({
|
||||
type: ChannelTypes.RECEIVED_MY_CHANNEL_MEMBER,
|
||||
data: {...prevMember, last_viewed_at: Date.now()},
|
||||
});
|
||||
}
|
||||
|
||||
if (prevChannel) {
|
||||
actions.push({
|
||||
type: ChannelTypes.DECREMENT_UNREAD_MSG_COUNT,
|
||||
data: {
|
||||
teamId: prevChannel.team_id,
|
||||
channelId: prevChannelId,
|
||||
amount: prevChannel.total_msg_count - prevMember.msg_count,
|
||||
},
|
||||
}, {
|
||||
type: ChannelTypes.DECREMENT_UNREAD_MENTION_COUNT,
|
||||
data: {
|
||||
teamId: prevChannel.team_id,
|
||||
channelId: prevChannelId,
|
||||
amount: prevMember.mention_count,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return actions;
|
||||
}
|
||||
|
||||
export function markChannelViewedAndReadOnReconnect(channelId) {
|
||||
return (dispatch, getState) => {
|
||||
if (isManuallyUnread(getState(), channelId)) {
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@ describe('Actions.Views.Channel', () => {
|
|||
type: MOCK_SELECT_CHANNEL_TYPE,
|
||||
data: 'selected-channel-id',
|
||||
});
|
||||
const postActions = require('mattermost-redux/actions/posts');
|
||||
const postActions = require('./post');
|
||||
postActions.getPostsSince = jest.fn(() => {
|
||||
return {
|
||||
type: MOCK_RECEIVED_POSTS_SINCE,
|
||||
|
|
@ -116,6 +116,7 @@ describe('Actions.Views.Channel', () => {
|
|||
},
|
||||
channels: {
|
||||
currentChannelId,
|
||||
manuallyUnread: {},
|
||||
channels: {
|
||||
'channel-id': {id: 'channel-id', display_name: 'Test Channel'},
|
||||
'channel-id-2': {id: 'channel-id-2', display_name: 'Test Channel'},
|
||||
|
|
@ -296,7 +297,8 @@ describe('Actions.Views.Channel', () => {
|
|||
|
||||
await store.dispatch(handleSelectChannel(channelId));
|
||||
const storeActions = store.getActions();
|
||||
const selectChannelWithMember = storeActions.find(({type}) => type === ChannelTypes.SELECT_CHANNEL);
|
||||
const storeBatchActions = storeActions.find(({type}) => type === 'BATCHING_REDUCER.BATCH');
|
||||
const selectChannelWithMember = storeBatchActions?.payload.find(({type}) => type === ChannelTypes.SELECT_CHANNEL);
|
||||
const viewedAction = storeActions.find(({type}) => type === MOCK_CHANNEL_MARK_AS_VIEWED);
|
||||
const readAction = storeActions.find(({type}) => type === MOCK_CHANNEL_MARK_AS_READ);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,11 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {addReaction as serviceAddReaction} from 'mattermost-redux/actions/posts';
|
||||
import {batchActions} from 'redux-batched-actions';
|
||||
|
||||
import {EmojiTypes} from 'mattermost-redux/action_types';
|
||||
import {addReaction as serviceAddReaction, getNeededCustomEmojis} from 'mattermost-redux/actions/posts';
|
||||
import {Client4} from 'mattermost-redux/client';
|
||||
import {getPostIdsInCurrentChannel, makeGetPostIdsForThread} from 'mattermost-redux/selectors/entities/posts';
|
||||
|
||||
import {ViewTypes} from 'app/constants';
|
||||
|
|
@ -42,3 +46,55 @@ export function incrementEmojiPickerPage() {
|
|||
return {data: true};
|
||||
};
|
||||
}
|
||||
|
||||
export function getEmojisInPosts(posts) {
|
||||
return async (dispatch, getState) => {
|
||||
const state = getState();
|
||||
|
||||
// Do not wait for this as they need to be loaded one by one
|
||||
const emojisToLoad = getNeededCustomEmojis(state, posts);
|
||||
|
||||
if (emojisToLoad?.size > 0) {
|
||||
const promises = emojisToLoad.map((name) => getCustomEmojiByName(name));
|
||||
const result = await Promise.all(promises);
|
||||
const actions = [];
|
||||
const data = [];
|
||||
|
||||
result.forEach((emoji, index) => {
|
||||
const name = emojisToLoad[index];
|
||||
|
||||
if (emoji) {
|
||||
switch (emoji) {
|
||||
case 404:
|
||||
actions.push({type: EmojiTypes.CUSTOM_EMOJI_DOES_NOT_EXIST, data: name});
|
||||
break;
|
||||
default:
|
||||
data.push(emoji);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (data.length) {
|
||||
actions.push({type: EmojiTypes.RECEIVED_CUSTOM_EMOJIS, data});
|
||||
}
|
||||
|
||||
if (actions.length) {
|
||||
dispatch(batchActions(actions));
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async function getCustomEmojiByName(name) {
|
||||
try {
|
||||
const data = await Client4.getCustomEmojiByName(name);
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
if (error.status_code === 404) {
|
||||
return 404;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
|
@ -1,13 +1,28 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {batchActions} from 'redux-batched-actions';
|
||||
|
||||
import {UserTypes} from 'mattermost-redux/action_types';
|
||||
import {
|
||||
doPostAction,
|
||||
getNeededAtMentionedUsernames,
|
||||
receivedNewPost,
|
||||
receivedPosts,
|
||||
receivedPostsBefore,
|
||||
receivedPostsInChannel,
|
||||
receivedPostsSince,
|
||||
receivedPostsInThread,
|
||||
} from 'mattermost-redux/actions/posts';
|
||||
import {Client4} from 'mattermost-redux/client';
|
||||
import {Posts} from 'mattermost-redux/constants';
|
||||
import {doPostAction, receivedNewPost} from 'mattermost-redux/actions/posts';
|
||||
import {removeUserFromList} from 'mattermost-redux/utils/user_utils';
|
||||
|
||||
import {ViewTypes} from 'app/constants';
|
||||
|
||||
import {generateId} from 'app/utils/file';
|
||||
|
||||
import {getEmojisInPosts} from './emoji';
|
||||
|
||||
export function sendAddToChannelEphemeralPost(user, addedUsername, message, channelId, postRootId = '') {
|
||||
return async (dispatch) => {
|
||||
const timestamp = Date.now();
|
||||
|
|
@ -58,3 +73,261 @@ export function selectAttachmentMenuAction(postId, actionId, text, value) {
|
|||
dispatch(doPostAction(postId, actionId, value));
|
||||
};
|
||||
}
|
||||
|
||||
export function getPosts(channelId, page = 0, perPage = Posts.POST_CHUNK_SIZE) {
|
||||
return async (dispatch) => {
|
||||
try {
|
||||
const data = await Client4.getPosts(channelId, page, perPage);
|
||||
const posts = Object.values(data.posts);
|
||||
|
||||
if (posts?.length) {
|
||||
const actions = [
|
||||
receivedPosts(data),
|
||||
receivedPostsInChannel(data, channelId, page === 0, data.prev_post_id === ''),
|
||||
];
|
||||
|
||||
const additional = await dispatch(getPostsAdditionalDataBatch(posts));
|
||||
if (additional.length) {
|
||||
actions.push(...additional);
|
||||
}
|
||||
|
||||
dispatch(batchActions(actions));
|
||||
}
|
||||
|
||||
return {data};
|
||||
} catch (error) {
|
||||
return {error};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function getPostsSince(channelId, since) {
|
||||
return async (dispatch) => {
|
||||
try {
|
||||
const data = await Client4.getPostsSince(channelId, since);
|
||||
const posts = Object.values(data.posts);
|
||||
|
||||
if (posts?.length) {
|
||||
const actions = [
|
||||
receivedPosts(data),
|
||||
receivedPostsSince(data, channelId),
|
||||
];
|
||||
|
||||
const additional = await dispatch(getPostsAdditionalDataBatch(posts));
|
||||
if (additional.length) {
|
||||
actions.push(...additional);
|
||||
}
|
||||
|
||||
dispatch(batchActions(actions));
|
||||
}
|
||||
|
||||
return {data};
|
||||
} catch (error) {
|
||||
return {error};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function getPostsBefore(channelId, postId, page = 0, perPage = Posts.POST_CHUNK_SIZE) {
|
||||
return async (dispatch) => {
|
||||
try {
|
||||
const data = await Client4.getPostsBefore(channelId, postId, page, perPage);
|
||||
const posts = Object.values(data.posts);
|
||||
|
||||
if (posts?.length) {
|
||||
const actions = [
|
||||
receivedPosts(data),
|
||||
receivedPostsBefore(data, channelId, postId, data.prev_post_id === ''),
|
||||
];
|
||||
|
||||
const additional = await dispatch(getPostsAdditionalDataBatch(posts));
|
||||
if (additional.length) {
|
||||
actions.push(...additional);
|
||||
}
|
||||
|
||||
dispatch(batchActions(actions));
|
||||
}
|
||||
|
||||
return {data};
|
||||
} catch (error) {
|
||||
return {error};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function getPostThread(rootId) {
|
||||
return async (dispatch) => {
|
||||
try {
|
||||
const data = await Client4.getPostThread(rootId);
|
||||
const posts = Object.values(data.posts);
|
||||
|
||||
if (posts.length) {
|
||||
const actions = [
|
||||
receivedPosts(data),
|
||||
receivedPostsInThread(data, rootId),
|
||||
];
|
||||
|
||||
const additional = await dispatch(getPostsAdditionalDataBatch(posts));
|
||||
if (additional.length) {
|
||||
actions.push(...additional);
|
||||
}
|
||||
|
||||
dispatch(batchActions(actions));
|
||||
}
|
||||
|
||||
return {data};
|
||||
} catch (error) {
|
||||
return {error};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function getPostsAround(channelId, postId, perPage = Posts.POST_CHUNK_SIZE / 2) {
|
||||
return async (dispatch) => {
|
||||
try {
|
||||
const [before, thread, after] = await Promise.all([
|
||||
Client4.getPostsBefore(channelId, postId, 0, perPage),
|
||||
Client4.getPostThread(postId),
|
||||
Client4.getPostsAfter(channelId, postId, 0, perPage),
|
||||
]);
|
||||
|
||||
const data = {
|
||||
posts: {
|
||||
...after.posts,
|
||||
...thread.posts,
|
||||
...before.posts,
|
||||
},
|
||||
order: [ // Remember that the order is newest posts first
|
||||
...after.order,
|
||||
postId,
|
||||
...before.order,
|
||||
],
|
||||
next_post_id: after.next_post_id,
|
||||
prev_post_id: before.prev_post_id,
|
||||
};
|
||||
|
||||
const posts = Object.values(data.posts);
|
||||
|
||||
if (posts?.length) {
|
||||
const actions = [
|
||||
receivedPosts(data),
|
||||
receivedPostsInChannel(data, channelId, after.next_post_id === '', before.prev_post_id === ''),
|
||||
];
|
||||
|
||||
const additional = await dispatch(getPostsAdditionalDataBatch(posts));
|
||||
if (additional.length) {
|
||||
actions.push(...additional);
|
||||
}
|
||||
|
||||
dispatch(batchActions(actions));
|
||||
}
|
||||
|
||||
return {data};
|
||||
} catch (error) {
|
||||
return {error};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function getPostsAdditionalDataBatch(posts = []) {
|
||||
return async (dispatch, getState) => {
|
||||
const actions = [];
|
||||
|
||||
if (!posts.length) {
|
||||
return actions;
|
||||
}
|
||||
|
||||
// Custom Emojis used in the posts
|
||||
// Do not wait for this as they need to be loaded one by one
|
||||
dispatch(getEmojisInPosts(posts));
|
||||
|
||||
try {
|
||||
const state = getState();
|
||||
const promises = [];
|
||||
const promiseTrace = [];
|
||||
const extra = dispatch(profilesStatusesAndToLoadFromPosts(posts));
|
||||
|
||||
if (extra?.userIds.length) {
|
||||
promises.push(Client4.getProfilesByIds(extra.userIds));
|
||||
promiseTrace.push('ids');
|
||||
}
|
||||
|
||||
if (extra?.usernames.length) {
|
||||
promises.push(Client4.getProfilesByUsernames(extra.usernames));
|
||||
promiseTrace.push('usernames');
|
||||
}
|
||||
|
||||
if (extra?.statuses.length) {
|
||||
promises.push(Client4.getStatusesByIds(extra.statuses));
|
||||
promiseTrace.push('statuses');
|
||||
}
|
||||
|
||||
if (promises.length) {
|
||||
const result = await Promise.all(promises);
|
||||
result.forEach((p, index) => {
|
||||
if (p.length) {
|
||||
const type = promiseTrace[index];
|
||||
switch (type) {
|
||||
case 'statuses':
|
||||
actions.push({
|
||||
type: UserTypes.RECEIVED_STATUSES,
|
||||
data: p,
|
||||
});
|
||||
break;
|
||||
default: {
|
||||
const {currentUserId} = state.entities.users;
|
||||
|
||||
removeUserFromList(currentUserId, p);
|
||||
actions.push({
|
||||
type: UserTypes.RECEIVED_PROFILES_LIST,
|
||||
data: p,
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
return actions;
|
||||
};
|
||||
}
|
||||
|
||||
function profilesStatusesAndToLoadFromPosts(posts = []) {
|
||||
return (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const {currentUserId, profiles, statuses} = state.entities.users;
|
||||
|
||||
// Profiles of users mentioned in the posts
|
||||
const usernamesToLoad = getNeededAtMentionedUsernames(state, posts);
|
||||
|
||||
// Statuses and profiles of the users who made the posts
|
||||
const userIdsToLoad = new Set();
|
||||
const statusesToLoad = new Set();
|
||||
|
||||
posts.forEach((post) => {
|
||||
const userId = post.user_id;
|
||||
|
||||
if (!statuses[userId]) {
|
||||
statusesToLoad.add(userId);
|
||||
}
|
||||
|
||||
if (userId === currentUserId) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!profiles[userId]) {
|
||||
userIdsToLoad.add(userId);
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
usernames: Array.from(usernamesToLoad),
|
||||
userIds: Array.from(userIdsToLoad),
|
||||
statuses: Array.from(statusesToLoad),
|
||||
};
|
||||
};
|
||||
}
|
||||
|
|
@ -14,7 +14,7 @@ exports[`PostList setting channel deep link 1`] = `
|
|||
"post-id-2",
|
||||
]
|
||||
}
|
||||
disableVirtualization={false}
|
||||
disableVirtualization={true}
|
||||
extraData={
|
||||
Array [
|
||||
"channel-id",
|
||||
|
|
@ -24,18 +24,19 @@ exports[`PostList setting channel deep link 1`] = `
|
|||
]
|
||||
}
|
||||
horizontal={false}
|
||||
initialNumToRender={7}
|
||||
initialNumToRender={10}
|
||||
inverted={true}
|
||||
keyExtractor={[Function]}
|
||||
keyboardDismissMode="interactive"
|
||||
keyboardShouldPersistTaps="handled"
|
||||
listKey="recyclerFor-channel-id"
|
||||
maintainVisibleContentPosition={
|
||||
Object {
|
||||
"autoscrollToTopThreshold": 60,
|
||||
"minIndexForVisible": 0,
|
||||
}
|
||||
}
|
||||
maxToRenderPerBatch={10}
|
||||
maxToRenderPerBatch={5}
|
||||
numColumns={1}
|
||||
onContentSizeChange={[Function]}
|
||||
onEndReachedThreshold={2}
|
||||
|
|
@ -55,7 +56,7 @@ exports[`PostList setting channel deep link 1`] = `
|
|||
tintColor="#3d3c40"
|
||||
/>
|
||||
}
|
||||
removeClippedSubviews={true}
|
||||
removeClippedSubviews={false}
|
||||
renderItem={[Function]}
|
||||
scrollEventThrottle={60}
|
||||
style={
|
||||
|
|
@ -64,7 +65,7 @@ exports[`PostList setting channel deep link 1`] = `
|
|||
}
|
||||
}
|
||||
updateCellsBatchingPeriod={50}
|
||||
windowSize={50}
|
||||
windowSize={11}
|
||||
/>
|
||||
`;
|
||||
|
||||
|
|
@ -82,7 +83,7 @@ exports[`PostList setting permalink deep link 1`] = `
|
|||
"post-id-2",
|
||||
]
|
||||
}
|
||||
disableVirtualization={false}
|
||||
disableVirtualization={true}
|
||||
extraData={
|
||||
Array [
|
||||
"channel-id",
|
||||
|
|
@ -92,18 +93,19 @@ exports[`PostList setting permalink deep link 1`] = `
|
|||
]
|
||||
}
|
||||
horizontal={false}
|
||||
initialNumToRender={7}
|
||||
initialNumToRender={10}
|
||||
inverted={true}
|
||||
keyExtractor={[Function]}
|
||||
keyboardDismissMode="interactive"
|
||||
keyboardShouldPersistTaps="handled"
|
||||
listKey="recyclerFor-channel-id"
|
||||
maintainVisibleContentPosition={
|
||||
Object {
|
||||
"autoscrollToTopThreshold": 60,
|
||||
"minIndexForVisible": 0,
|
||||
}
|
||||
}
|
||||
maxToRenderPerBatch={10}
|
||||
maxToRenderPerBatch={5}
|
||||
numColumns={1}
|
||||
onContentSizeChange={[Function]}
|
||||
onEndReachedThreshold={2}
|
||||
|
|
@ -123,7 +125,7 @@ exports[`PostList setting permalink deep link 1`] = `
|
|||
tintColor="#3d3c40"
|
||||
/>
|
||||
}
|
||||
removeClippedSubviews={true}
|
||||
removeClippedSubviews={false}
|
||||
renderItem={[Function]}
|
||||
scrollEventThrottle={60}
|
||||
style={
|
||||
|
|
@ -132,7 +134,7 @@ exports[`PostList setting permalink deep link 1`] = `
|
|||
}
|
||||
}
|
||||
updateCellsBatchingPeriod={50}
|
||||
windowSize={50}
|
||||
windowSize={11}
|
||||
/>
|
||||
`;
|
||||
|
||||
|
|
@ -150,7 +152,7 @@ exports[`PostList should match snapshot 1`] = `
|
|||
"post-id-2",
|
||||
]
|
||||
}
|
||||
disableVirtualization={false}
|
||||
disableVirtualization={true}
|
||||
extraData={
|
||||
Array [
|
||||
"channel-id",
|
||||
|
|
@ -160,18 +162,19 @@ exports[`PostList should match snapshot 1`] = `
|
|||
]
|
||||
}
|
||||
horizontal={false}
|
||||
initialNumToRender={7}
|
||||
initialNumToRender={10}
|
||||
inverted={true}
|
||||
keyExtractor={[Function]}
|
||||
keyboardDismissMode="interactive"
|
||||
keyboardShouldPersistTaps="handled"
|
||||
listKey="recyclerFor-channel-id"
|
||||
maintainVisibleContentPosition={
|
||||
Object {
|
||||
"autoscrollToTopThreshold": 60,
|
||||
"minIndexForVisible": 0,
|
||||
}
|
||||
}
|
||||
maxToRenderPerBatch={10}
|
||||
maxToRenderPerBatch={5}
|
||||
numColumns={1}
|
||||
onContentSizeChange={[Function]}
|
||||
onEndReachedThreshold={2}
|
||||
|
|
@ -191,7 +194,7 @@ exports[`PostList should match snapshot 1`] = `
|
|||
tintColor="#3d3c40"
|
||||
/>
|
||||
}
|
||||
removeClippedSubviews={true}
|
||||
removeClippedSubviews={false}
|
||||
renderItem={[Function]}
|
||||
scrollEventThrottle={60}
|
||||
style={
|
||||
|
|
@ -200,6 +203,6 @@ exports[`PostList should match snapshot 1`] = `
|
|||
}
|
||||
}
|
||||
updateCellsBatchingPeriod={50}
|
||||
windowSize={50}
|
||||
windowSize={11}
|
||||
/>
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Alert, FlatList, RefreshControl, StyleSheet} from 'react-native';
|
||||
import {Alert, FlatList, InteractionManager, RefreshControl, StyleSheet} from 'react-native';
|
||||
import {intlShape} from 'react-intl';
|
||||
|
||||
import EventEmitter from 'mattermost-redux/utils/event_emitter';
|
||||
|
|
@ -24,8 +24,7 @@ import {t} from 'app/utils/i18n';
|
|||
import DateHeader from './date_header';
|
||||
import NewMessagesDivider from './new_messages_divider';
|
||||
|
||||
const INITIAL_BATCH_TO_RENDER = 7;
|
||||
const LOADING_POSTS_HEIGHT = 53;
|
||||
const INITIAL_BATCH_TO_RENDER = 10;
|
||||
const SCROLL_UP_MULTIPLIER = 3.5;
|
||||
const SCROLL_POSITION_CONFIG = {
|
||||
|
||||
|
|
@ -90,20 +89,18 @@ export default class PostList extends PureComponent {
|
|||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.hasDoneInitialScroll = false;
|
||||
this.cancelScrollToIndex = false;
|
||||
this.contentOffsetY = 0;
|
||||
this.contentHeight = 0;
|
||||
this.hasDoneInitialScroll = false;
|
||||
this.shouldScrollToBottom = false;
|
||||
this.cancelScrollToIndex = false;
|
||||
this.makeExtraData = makeExtraData();
|
||||
this.flatListRef = React.createRef();
|
||||
|
||||
this.state = {
|
||||
postListHeight: 0,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const {actions, deepLinkURL} = this.props;
|
||||
const {actions, deepLinkURL, highlightPostId, initialIndex} = this.props;
|
||||
|
||||
EventEmitter.on('scroll-to-bottom', this.handleSetScrollToBottom);
|
||||
|
||||
|
|
@ -112,6 +109,11 @@ export default class PostList extends PureComponent {
|
|||
this.handleDeepLink(deepLinkURL);
|
||||
actions.setDeepLinkURL('');
|
||||
}
|
||||
|
||||
// Scroll to highlighted post for permalinks
|
||||
if (!this.hasDoneInitialScroll && initialIndex > 0 && !this.cancelScrollToIndex && highlightPostId) {
|
||||
this.scrollToInitialIndexIfNeeded(initialIndex);
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
|
|
@ -132,16 +134,16 @@ export default class PostList extends PureComponent {
|
|||
this.shouldScrollToBottom = false;
|
||||
}
|
||||
|
||||
if (!this.hasDoneInitialScroll && this.props.initialIndex > 0 && this.state.contentHeight > LOADING_POSTS_HEIGHT) {
|
||||
if (!this.hasDoneInitialScroll && this.props.initialIndex > 0 && !this.cancelScrollToIndex) {
|
||||
this.scrollToInitialIndexIfNeeded(this.props.initialIndex);
|
||||
}
|
||||
|
||||
if (
|
||||
this.props.channelId === prevProps.channelId &&
|
||||
this.props.postIds.length &&
|
||||
this.state.contentHeight &&
|
||||
this.state.contentHeight < this.state.postListHeight &&
|
||||
!this.props.extraData
|
||||
this.contentHeight &&
|
||||
this.contentHeight < this.postListHeight &&
|
||||
this.props.extraData
|
||||
) {
|
||||
this.loadToFillContent();
|
||||
}
|
||||
|
|
@ -176,14 +178,12 @@ export default class PostList extends PureComponent {
|
|||
this.showingPermalink = false;
|
||||
};
|
||||
|
||||
handleContentSizeChange = (contentWidth, contentHeight) => {
|
||||
if (this.state.contentHeight !== contentHeight) {
|
||||
this.setState({contentHeight}, () => {
|
||||
if (this.state.postListHeight && contentHeight < this.state.postListHeight && !this.props.extraData && contentHeight > LOADING_POSTS_HEIGHT) {
|
||||
// We still have less than 1 screen of posts loaded with more to get, so load more
|
||||
this.props.onLoadMoreUp();
|
||||
}
|
||||
});
|
||||
handleContentSizeChange = (contentWidth, contentHeight, forceLoad) => {
|
||||
this.contentHeight = contentHeight;
|
||||
const loadMore = forceLoad || !this.props.extraData;
|
||||
if (this.postListHeight && contentHeight < this.postListHeight && loadMore) {
|
||||
// We still have less than 1 screen of posts loaded with more to get, so load more
|
||||
this.props.onLoadMoreUp();
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -194,7 +194,7 @@ export default class PostList extends PureComponent {
|
|||
|
||||
if (match) {
|
||||
if (match.type === DeepLinkTypes.CHANNEL) {
|
||||
this.props.actions.handleSelectChannelByName(match.channelName, match.teamName, this.errorBadChannel);
|
||||
this.props.actions.handleSelectChannelByName(match.channelName, match.teamName, this.permalinkBadChannel);
|
||||
} else if (match.type === DeepLinkTypes.PERMALINK) {
|
||||
this.handlePermalinkPress(match.postId, match.teamName);
|
||||
}
|
||||
|
|
@ -215,31 +215,11 @@ export default class PostList extends PureComponent {
|
|||
|
||||
handleLayout = (event) => {
|
||||
const {height} = event.nativeEvent.layout;
|
||||
if (this.state.postListHeight !== height) {
|
||||
this.setState({postListHeight: height});
|
||||
if (this.postListHeight !== height) {
|
||||
this.postListHeight = height;
|
||||
}
|
||||
};
|
||||
|
||||
errorBadTeam = () => {
|
||||
const {intl} = this.context;
|
||||
const message = {
|
||||
id: t('mobile.server_link.unreachable_team.error'),
|
||||
defaultMessage: 'This link belongs to a deleted team or to a team to which you do not have access.',
|
||||
};
|
||||
|
||||
alertErrorWithFallback(intl, {}, message);
|
||||
};
|
||||
|
||||
errorBadChannel = () => {
|
||||
const {intl} = this.context;
|
||||
const message = {
|
||||
id: t('mobile.server_link.unreachable_channel.error'),
|
||||
defaultMessage: 'This link belongs to a deleted channel or to a channel to which you do not have access.',
|
||||
};
|
||||
|
||||
alertErrorWithFallback(intl, {}, message);
|
||||
};
|
||||
|
||||
handlePermalinkPress = (postId, teamName) => {
|
||||
telemetry.start(['post_list:permalink']);
|
||||
const {actions, onPermalinkPress} = this.props;
|
||||
|
|
@ -247,7 +227,7 @@ export default class PostList extends PureComponent {
|
|||
if (onPermalinkPress) {
|
||||
onPermalinkPress(postId, true);
|
||||
} else {
|
||||
actions.loadChannelsByTeamName(teamName, this.errorBadTeam);
|
||||
actions.loadChannelsByTeamName(teamName, this.permalinkBadTeam);
|
||||
this.showPermalinkView(postId);
|
||||
}
|
||||
};
|
||||
|
|
@ -272,14 +252,12 @@ export default class PostList extends PureComponent {
|
|||
const pageOffsetY = event.nativeEvent.contentOffset.y;
|
||||
if (pageOffsetY > 0) {
|
||||
const contentHeight = event.nativeEvent.contentSize.height;
|
||||
const direction = (this.contentOffsetY < pageOffsetY) ?
|
||||
ListTypes.VISIBILITY_SCROLL_UP :
|
||||
ListTypes.VISIBILITY_SCROLL_DOWN;
|
||||
const direction = (this.contentOffsetY < pageOffsetY) ? ListTypes.VISIBILITY_SCROLL_UP : ListTypes.VISIBILITY_SCROLL_DOWN;
|
||||
|
||||
this.contentOffsetY = pageOffsetY;
|
||||
if (
|
||||
direction === ListTypes.VISIBILITY_SCROLL_UP &&
|
||||
(contentHeight - pageOffsetY) < (this.state.postListHeight * SCROLL_UP_MULTIPLIER)
|
||||
(contentHeight - pageOffsetY) < (this.postListHeight * SCROLL_UP_MULTIPLIER)
|
||||
) {
|
||||
this.props.onLoadMoreUp();
|
||||
}
|
||||
|
|
@ -290,15 +268,25 @@ export default class PostList extends PureComponent {
|
|||
this.cancelScrollToIndex = true;
|
||||
}
|
||||
|
||||
handleScrollToIndexFailed = () => {
|
||||
handleScrollToIndexFailed = (info) => {
|
||||
this.animationFrameIndexFailed = requestAnimationFrame(() => {
|
||||
if (this.props.initialIndex > 0 && this.state.contentHeight > 0) {
|
||||
if (this.props.initialIndex > 0 && this.contentHeight > 0) {
|
||||
this.hasDoneInitialScroll = false;
|
||||
this.scrollToInitialIndexIfNeeded(this.props.initialIndex);
|
||||
if (info.highestMeasuredFrameIndex) {
|
||||
this.scrollToInitialIndexIfNeeded(info.highestMeasuredFrameIndex);
|
||||
} else {
|
||||
this.scrollAfterInteraction = InteractionManager.runAfterInteractions(() => {
|
||||
this.scrollToInitialIndexIfNeeded(info.index);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
handleScrollBeginDrag = () => {
|
||||
this.cancelScrollToIndex = true;
|
||||
};
|
||||
|
||||
handleSetScrollToBottom = () => {
|
||||
this.shouldScrollToBottom = true;
|
||||
}
|
||||
|
|
@ -310,10 +298,30 @@ export default class PostList extends PureComponent {
|
|||
|
||||
loadToFillContent = () => {
|
||||
this.fillContentTimer = setTimeout(() => {
|
||||
this.handleContentSizeChange(0, this.state.contentHeight);
|
||||
this.handleContentSizeChange(0, this.contentHeight, true);
|
||||
});
|
||||
};
|
||||
|
||||
permalinkBadTeam = () => {
|
||||
const {intl} = this.context;
|
||||
const message = {
|
||||
id: t('mobile.server_link.unreachable_team.error'),
|
||||
defaultMessage: 'This link belongs to a deleted team or to a team to which you do not have access.',
|
||||
};
|
||||
|
||||
alertErrorWithFallback(intl, {}, message);
|
||||
};
|
||||
|
||||
permalinkBadChannel = () => {
|
||||
const {intl} = this.context;
|
||||
const message = {
|
||||
id: t('mobile.server_link.unreachable_channel.error'),
|
||||
defaultMessage: 'This link belongs to a deleted channel or to a channel to which you do not have access.',
|
||||
};
|
||||
|
||||
alertErrorWithFallback(intl, {}, message);
|
||||
};
|
||||
|
||||
renderItem = ({item, index}) => {
|
||||
const {
|
||||
highlightPinnedOrFlagged,
|
||||
|
|
@ -397,19 +405,15 @@ export default class PostList extends PureComponent {
|
|||
);
|
||||
};
|
||||
|
||||
scrollToBottom = () => {
|
||||
this.scrollToBottomTimer = setTimeout(() => {
|
||||
if (this.flatListRef.current) {
|
||||
this.flatListRef.current.scrollToOffset({offset: 0, animated: true});
|
||||
}
|
||||
}, 250);
|
||||
};
|
||||
|
||||
resetPostList = () => {
|
||||
this.contentOffsetY = 0;
|
||||
this.hasDoneInitialScroll = false;
|
||||
this.cancelScrollToIndex = false;
|
||||
|
||||
if (this.scrollAfterInteraction) {
|
||||
this.scrollAfterInteraction.cancel();
|
||||
}
|
||||
|
||||
if (this.animationFrameIndexFailed) {
|
||||
cancelAnimationFrame(this.animationFrameIndexFailed);
|
||||
}
|
||||
|
|
@ -430,11 +434,19 @@ export default class PostList extends PureComponent {
|
|||
clearTimeout(this.scrollToInitialTimer);
|
||||
}
|
||||
|
||||
if (this.state.contentHeight !== 0) {
|
||||
this.setState({contentHeight: 0});
|
||||
if (this.contentHeight !== 0) {
|
||||
this.contentHeight = 0;
|
||||
}
|
||||
}
|
||||
|
||||
scrollToBottom = () => {
|
||||
this.scrollToBottomTimer = setTimeout(() => {
|
||||
if (this.flatListRef.current) {
|
||||
this.flatListRef.current.scrollToOffset({offset: 0, animated: true});
|
||||
}
|
||||
}, 250);
|
||||
};
|
||||
|
||||
scrollToIndex = (index) => {
|
||||
this.animationFrameInitialIndex = requestAnimationFrame(() => {
|
||||
if (this.flatListRef.current && index > 0 && index <= this.getItemCount()) {
|
||||
|
|
@ -443,15 +455,18 @@ export default class PostList extends PureComponent {
|
|||
});
|
||||
};
|
||||
|
||||
scrollToInitialIndexIfNeeded = (index, count = 0) => {
|
||||
scrollToInitialIndexIfNeeded = (index) => {
|
||||
if (!this.hasDoneInitialScroll) {
|
||||
if (index > 0 && index <= this.getItemCount()) {
|
||||
this.hasDoneInitialScroll = true;
|
||||
this.scrollToIndex(index);
|
||||
} else if (count < 3) {
|
||||
this.scrollToInitialTimer = setTimeout(() => {
|
||||
this.scrollToInitialIndexIfNeeded(index, count + 1);
|
||||
}, 300);
|
||||
|
||||
if (index !== this.props.initialIndex) {
|
||||
this.hasDoneInitialScroll = false;
|
||||
this.scrollToInitialTimer = setTimeout(() => {
|
||||
this.scrollToInitialIndexIfNeeded(this.props.initialIndex);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
@ -499,40 +514,44 @@ export default class PostList extends PureComponent {
|
|||
tintColor={theme.centerChannelColor}
|
||||
/>);
|
||||
|
||||
const hasPostsKey = postIds.length ? 'true' : 'false';
|
||||
|
||||
return (
|
||||
<FlatList
|
||||
key={`recyclerFor-${channelId}-${hasPostsKey}`}
|
||||
ref={this.flatListRef}
|
||||
style={{flex: 1}}
|
||||
contentContainerStyle={styles.postListContent}
|
||||
data={postIds}
|
||||
disableVirtualization={true}
|
||||
extraData={this.makeExtraData(channelId, highlightPostId, extraData, loadMorePostsVisible)}
|
||||
initialNumToRender={INITIAL_BATCH_TO_RENDER}
|
||||
inverted={true}
|
||||
key={`recyclerFor-${channelId}`}
|
||||
keyboardDismissMode={'interactive'}
|
||||
keyboardShouldPersistTaps={'handled'}
|
||||
keyExtractor={this.keyExtractor}
|
||||
ListFooterComponent={this.props.renderFooter}
|
||||
listKey={`recyclerFor-${channelId}`}
|
||||
maintainVisibleContentPosition={SCROLL_POSITION_CONFIG}
|
||||
maxToRenderPerBatch={5}
|
||||
nativeID={scrollViewNativeID}
|
||||
onContentSizeChange={this.handleContentSizeChange}
|
||||
onLayout={this.handleLayout}
|
||||
onScroll={this.handleScroll}
|
||||
onScrollBeginDrag={this.handleScrollBeginDrag}
|
||||
onScrollToIndexFailed={this.handleScrollToIndexFailed}
|
||||
removeClippedSubviews={true}
|
||||
ref={this.flatListRef}
|
||||
refreshControl={refreshControl}
|
||||
removeClippedSubviews={false}
|
||||
renderItem={this.renderItem}
|
||||
scrollEventThrottle={60}
|
||||
refreshControl={refreshControl}
|
||||
nativeID={scrollViewNativeID}
|
||||
windowSize={50}
|
||||
style={styles.flex}
|
||||
windowSize={11}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
flex: {
|
||||
flex: 1,
|
||||
},
|
||||
postListContent: {
|
||||
paddingTop: 5,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -113,12 +113,8 @@ describe('PostList', () => {
|
|||
});
|
||||
expect(instance.loadToFillContent).toHaveBeenCalledTimes(0);
|
||||
|
||||
wrapper.setState({
|
||||
postListHeight: 500,
|
||||
contentHeight: 200,
|
||||
});
|
||||
expect(instance.loadToFillContent).toHaveBeenCalledTimes(1);
|
||||
|
||||
instance.postListHeight = 500;
|
||||
instance.contentHeight = 200;
|
||||
wrapper.setProps({
|
||||
extraData: true,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import DeepLinkTypes from './deep_linking';
|
|||
import DeviceTypes from './device';
|
||||
import ListTypes from './list';
|
||||
import NavigationTypes from './navigation';
|
||||
import Types from './types';
|
||||
import ViewTypes, {UpgradeTypes} from './view';
|
||||
|
||||
export {
|
||||
|
|
@ -13,5 +14,6 @@ export {
|
|||
ListTypes,
|
||||
NavigationTypes,
|
||||
UpgradeTypes,
|
||||
Types,
|
||||
ViewTypes,
|
||||
};
|
||||
|
|
|
|||
7
app/constants/types.js
Normal file
7
app/constants/types.js
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
export default {
|
||||
EMPTY_ARRAY: [],
|
||||
EMTPY_OBJECT: {},
|
||||
};
|
||||
|
|
@ -177,7 +177,7 @@ export default class ChannelPostList extends PureComponent {
|
|||
<PostList
|
||||
postIds={postIds}
|
||||
lastPostIndex={Platform.OS === 'android' ? getLastPostIndex(postIds) : -1}
|
||||
extraData={postIds.length === 0}
|
||||
extraData={postIds.length !== 0}
|
||||
onLoadMoreUp={this.loadMorePostsTop}
|
||||
onPostPress={this.goToThread}
|
||||
onRefresh={actions.setChannelRefreshing}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import {
|
|||
refreshChannelWithRetry,
|
||||
} from 'app/actions/views/channel';
|
||||
import {recordLoadTime} from 'app/actions/views/root';
|
||||
import {Types} from 'app/constants';
|
||||
import {isLandscape} from 'app/selectors/device';
|
||||
|
||||
import ChannelPostList from './channel_post_list';
|
||||
|
|
@ -30,7 +31,7 @@ function mapStateToProps(state) {
|
|||
channelRefreshingFailed,
|
||||
currentUserId: getCurrentUserId(state),
|
||||
deviceHeight: state.device.dimension.deviceHeight,
|
||||
postIds: getPostIdsInCurrentChannel(state) || [],
|
||||
postIds: getPostIdsInCurrentChannel(state) || Types.EMPTY_ARRAY,
|
||||
lastViewedAt: state.views.channel.lastChannelViewTime[channelId],
|
||||
loadMorePostsVisible: state.views.channel.loadMorePostsVisible,
|
||||
refreshing: state.views.channel.refreshing,
|
||||
|
|
|
|||
|
|
@ -5,23 +5,20 @@ import {bindActionCreators} from 'redux';
|
|||
import {connect} from 'react-redux';
|
||||
|
||||
import {getChannel as getChannelAction, joinChannel} from 'mattermost-redux/actions/channels';
|
||||
import {
|
||||
getPostsAround,
|
||||
getPostThread,
|
||||
selectPost,
|
||||
} from 'mattermost-redux/actions/posts';
|
||||
import {selectPost} from 'mattermost-redux/actions/posts';
|
||||
import {makeGetChannel, getMyChannelMemberships} from 'mattermost-redux/selectors/entities/channels';
|
||||
import {makeGetPostIdsAroundPost, getPost} from 'mattermost-redux/selectors/entities/posts';
|
||||
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams';
|
||||
import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
|
||||
import {isLandscape} from 'app/selectors/device';
|
||||
|
||||
import {
|
||||
handleSelectChannel,
|
||||
loadThreadIfNecessary,
|
||||
} from 'app/actions/views/channel';
|
||||
import {getPostsAround, getPostThread} from 'app/actions/views/post';
|
||||
import {handleTeamChange} from 'app/actions/views/select_team';
|
||||
import {isLandscape} from 'app/selectors/device';
|
||||
|
||||
import Permalink from './permalink';
|
||||
|
||||
|
|
|
|||
|
|
@ -5,17 +5,17 @@ import {Platform} from 'react-native';
|
|||
import DeviceInfo from 'react-native-device-info';
|
||||
|
||||
import {setDeviceToken} from 'mattermost-redux/actions/general';
|
||||
import {getPosts} from 'mattermost-redux/actions/posts';
|
||||
import {Client4} from 'mattermost-redux/client';
|
||||
import {General} from 'mattermost-redux/constants';
|
||||
import EventEmitter from 'mattermost-redux/utils/event_emitter';
|
||||
|
||||
import {markChannelViewedAndRead, retryGetPostsAction} from 'app/actions/views/channel';
|
||||
import {dismissAllModals, popToRoot} from 'app/actions/navigation';
|
||||
import {getPosts} from 'app/actions/views/post';
|
||||
import {
|
||||
createPostForNotificationReply,
|
||||
loadFromPushNotification,
|
||||
} from 'app/actions/views/root';
|
||||
import {dismissAllModals, popToRoot} from 'app/actions/navigation';
|
||||
|
||||
import {NavigationTypes, ViewTypes} from 'app/constants';
|
||||
import {getLocalizedMessage} from 'app/i18n';
|
||||
|
|
|
|||
18
package-lock.json
generated
18
package-lock.json
generated
|
|
@ -2938,9 +2938,9 @@
|
|||
"integrity": "sha512-1dVNHT76Uu5N3eJNTYcvxee+jzX4Z9lfciqRRHCU27ihbUcYi+iSc2iml5Ke1LXe1SyJCLA0+14Jh4tXJgOppA=="
|
||||
},
|
||||
"@hapi/hoek": {
|
||||
"version": "8.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-8.5.0.tgz",
|
||||
"integrity": "sha512-7XYT10CZfPsH7j9F1Jmg1+d0ezOux2oM2GfArAzLwWe4mE2Dr3hVjsAL6+TFY49RRJlCdJDMw3nJsLFroTc8Kw=="
|
||||
"version": "8.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-8.5.1.tgz",
|
||||
"integrity": "sha512-yN7kbciD87WzLGc5539Tn0sApjyiGHAJgKvG9W8C7O+6c7qmoQMfVs0W4bX17eqz6C78QJqqFrtgdK5EWf6Qow=="
|
||||
},
|
||||
"@hapi/joi": {
|
||||
"version": "15.1.1",
|
||||
|
|
@ -3659,9 +3659,9 @@
|
|||
}
|
||||
},
|
||||
"acorn": {
|
||||
"version": "7.1.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-7.1.0.tgz",
|
||||
"integrity": "sha512-kL5CuoXA/dgxlBbVrflsflzQ3PAas7RYZB52NOm/6839iVYJgKMJ3cQJD+t2i5+qFa8h3MDpEOJiS64E8JLnSQ==",
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-7.1.1.tgz",
|
||||
"integrity": "sha512-add7dgA5ppRPxCFJoAGfMDi7PIBXq1RtGo7BhbLaxwrXPOmw8gq48Y9ozT01hUKy9byMjlR20EJhu5zlkErEkg==",
|
||||
"dev": true
|
||||
},
|
||||
"acorn-globals": {
|
||||
|
|
@ -9549,9 +9549,9 @@
|
|||
"integrity": "sha1-kYiJ6hP40KQufFVyUO7nE63JXDU="
|
||||
},
|
||||
"kind-of": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz",
|
||||
"integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA=="
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
|
||||
"integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw=="
|
||||
},
|
||||
"klaw": {
|
||||
"version": "1.3.1",
|
||||
|
|
|
|||
Loading…
Reference in a new issue