* Improve sidebar performance on first load * Initial work for switch channel * Revert android changes * Split Sidebar per Platform * Fix waitForHydration executing the callback more than once * Fix custom emoji not showing on Android * Finalize Channel Switch * Enable Android Ram Bundles * Select the right team for lastChannelForTeam * Channel loading post indicator * Fix main sidebar base intl provider * Update mm-redux * No need to request configAndLicense on launch * Load channel member roles * Rename closeChannelDrawer to closeMainSidebar * do not throw errors when console is called while running tests * constant for LOADING_POSTS_HEIGHT * Remove show more if a long post is edited and no longer long * Update mm-redux#batch-actions branch * Code review * Clear notifications if channel was switched * Import Platform Co-authored-by: Miguel Alatzar <migbot@users.noreply.github.com> Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
This commit is contained in:
parent
a575f95797
commit
3383c93df8
77 changed files with 2361 additions and 2019 deletions
|
|
@ -1,9 +1,8 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Platform} from 'react-native';
|
||||
import {Keyboard, Platform} from 'react-native';
|
||||
import {Navigation} from 'react-native-navigation';
|
||||
|
||||
import merge from 'deepmerge';
|
||||
|
||||
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
|
||||
|
|
@ -20,38 +19,60 @@ function getThemeFromState() {
|
|||
export function resetToChannel(passProps = {}) {
|
||||
const theme = getThemeFromState();
|
||||
|
||||
Navigation.setRoot({
|
||||
root: {
|
||||
stack: {
|
||||
children: [{
|
||||
component: {
|
||||
name: 'Channel',
|
||||
passProps,
|
||||
options: {
|
||||
layout: {
|
||||
backgroundColor: 'transparent',
|
||||
},
|
||||
statusBar: {
|
||||
visible: true,
|
||||
},
|
||||
topBar: {
|
||||
visible: false,
|
||||
height: 0,
|
||||
backButton: {
|
||||
color: theme.sidebarHeaderTextColor,
|
||||
title: '',
|
||||
},
|
||||
background: {
|
||||
color: theme.sidebarHeaderBg,
|
||||
},
|
||||
title: {
|
||||
color: theme.sidebarHeaderTextColor,
|
||||
},
|
||||
},
|
||||
const stack = {
|
||||
children: [{
|
||||
component: {
|
||||
id: 'Channel',
|
||||
name: 'Channel',
|
||||
passProps,
|
||||
options: {
|
||||
layout: {
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
},
|
||||
statusBar: {
|
||||
visible: true,
|
||||
},
|
||||
topBar: {
|
||||
visible: false,
|
||||
height: 0,
|
||||
background: {
|
||||
color: theme.sidebarHeaderBg,
|
||||
},
|
||||
backButton: {
|
||||
visible: false,
|
||||
},
|
||||
},
|
||||
}],
|
||||
},
|
||||
},
|
||||
}],
|
||||
};
|
||||
|
||||
let platformStack = {stack};
|
||||
if (Platform.OS === 'android') {
|
||||
platformStack = {
|
||||
sideMenu: {
|
||||
left: {
|
||||
component: {
|
||||
id: 'MainSidebar',
|
||||
name: 'MainSidebar',
|
||||
},
|
||||
},
|
||||
center: {
|
||||
stack,
|
||||
},
|
||||
right: {
|
||||
component: {
|
||||
id: 'SettingsSidebar',
|
||||
name: 'SettingsSidebar',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
Navigation.setRoot({
|
||||
root: {
|
||||
...platformStack,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
@ -64,6 +85,7 @@ export function resetToSelectServer(allowOtherServers) {
|
|||
stack: {
|
||||
children: [{
|
||||
component: {
|
||||
id: 'SelectServer',
|
||||
name: 'SelectServer',
|
||||
passProps: {
|
||||
allowOtherServers,
|
||||
|
|
@ -121,6 +143,7 @@ export function resetToTeams(name, title, passProps = {}, options = {}) {
|
|||
stack: {
|
||||
children: [{
|
||||
component: {
|
||||
id: name,
|
||||
name,
|
||||
passProps,
|
||||
options: merge(defaultOptions, options),
|
||||
|
|
@ -138,6 +161,10 @@ export function goToScreen(name, title, passProps = {}, options = {}) {
|
|||
layout: {
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
},
|
||||
sideMenu: {
|
||||
left: {enabled: false},
|
||||
right: {enabled: false},
|
||||
},
|
||||
topBar: {
|
||||
animate: true,
|
||||
visible: true,
|
||||
|
|
@ -157,6 +184,7 @@ export function goToScreen(name, title, passProps = {}, options = {}) {
|
|||
|
||||
Navigation.push(componentId, {
|
||||
component: {
|
||||
id: name,
|
||||
name,
|
||||
passProps,
|
||||
options: merge(defaultOptions, options),
|
||||
|
|
@ -216,6 +244,7 @@ export function showModal(name, title, passProps = {}, options = {}) {
|
|||
stack: {
|
||||
children: [{
|
||||
component: {
|
||||
id: name,
|
||||
name,
|
||||
passProps,
|
||||
options: merge(defaultOptions, options),
|
||||
|
|
@ -295,23 +324,6 @@ export async function dismissAllModals(options = {}) {
|
|||
}
|
||||
}
|
||||
|
||||
export function peek(name, passProps = {}, options = {}) {
|
||||
const componentId = EphemeralStore.getNavigationTopComponentId();
|
||||
const defaultOptions = {
|
||||
preview: {
|
||||
commit: false,
|
||||
},
|
||||
};
|
||||
|
||||
Navigation.push(componentId, {
|
||||
component: {
|
||||
name,
|
||||
passProps,
|
||||
options: merge(defaultOptions, options),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function setButtons(componentId, buttons = {leftButtons: [], rightButtons: []}) {
|
||||
const options = {
|
||||
topBar: {
|
||||
|
|
@ -335,6 +347,7 @@ export function showOverlay(name, passProps, options = {}) {
|
|||
|
||||
Navigation.showOverlay({
|
||||
component: {
|
||||
id: name,
|
||||
name,
|
||||
passProps,
|
||||
options: merge(defaultOptions, options),
|
||||
|
|
@ -350,3 +363,77 @@ export async function dismissOverlay(componentId) {
|
|||
// this componentId to dismiss. We'll do nothing in this case.
|
||||
}
|
||||
}
|
||||
|
||||
export function openMainSideMenu() {
|
||||
if (Platform.OS === 'ios') {
|
||||
return;
|
||||
}
|
||||
|
||||
const componentId = EphemeralStore.getNavigationTopComponentId();
|
||||
|
||||
Keyboard.dismiss();
|
||||
Navigation.mergeOptions(componentId, {
|
||||
sideMenu: {
|
||||
left: {visible: true},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function closeMainSideMenu() {
|
||||
if (Platform.OS === 'ios') {
|
||||
return;
|
||||
}
|
||||
|
||||
const componentId = EphemeralStore.getNavigationTopComponentId();
|
||||
|
||||
Keyboard.dismiss();
|
||||
Navigation.mergeOptions(componentId, {
|
||||
sideMenu: {
|
||||
left: {visible: false},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function enableMainSideMenu(enabled, visible = true) {
|
||||
if (Platform.OS === 'ios') {
|
||||
return;
|
||||
}
|
||||
|
||||
const componentId = EphemeralStore.getNavigationTopComponentId();
|
||||
|
||||
Navigation.mergeOptions(componentId, {
|
||||
sideMenu: {
|
||||
left: {enabled, visible},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function openSettingsSideMenu() {
|
||||
if (Platform.OS === 'ios') {
|
||||
return;
|
||||
}
|
||||
|
||||
const componentId = EphemeralStore.getNavigationTopComponentId();
|
||||
|
||||
Keyboard.dismiss();
|
||||
Navigation.mergeOptions(componentId, {
|
||||
sideMenu: {
|
||||
right: {visible: true},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function closeSettingsSideMenu() {
|
||||
if (Platform.OS === 'ios') {
|
||||
return;
|
||||
}
|
||||
|
||||
const componentId = EphemeralStore.getNavigationTopComponentId();
|
||||
|
||||
Keyboard.dismiss();
|
||||
Navigation.mergeOptions(componentId, {
|
||||
sideMenu: {
|
||||
right: {visible: false},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,11 +37,12 @@ describe('app/actions/navigation', () => {
|
|||
stack: {
|
||||
children: [{
|
||||
component: {
|
||||
id: 'Channel',
|
||||
name: 'Channel',
|
||||
passProps,
|
||||
options: {
|
||||
layout: {
|
||||
backgroundColor: 'transparent',
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
},
|
||||
statusBar: {
|
||||
visible: true,
|
||||
|
|
@ -50,15 +51,11 @@ describe('app/actions/navigation', () => {
|
|||
visible: false,
|
||||
height: 0,
|
||||
backButton: {
|
||||
color: theme.sidebarHeaderTextColor,
|
||||
title: '',
|
||||
visible: false,
|
||||
},
|
||||
background: {
|
||||
color: theme.sidebarHeaderBg,
|
||||
},
|
||||
title: {
|
||||
color: theme.sidebarHeaderTextColor,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -80,6 +77,7 @@ describe('app/actions/navigation', () => {
|
|||
stack: {
|
||||
children: [{
|
||||
component: {
|
||||
id: 'SelectServer',
|
||||
name: 'SelectServer',
|
||||
passProps: {
|
||||
allowOtherServers,
|
||||
|
|
@ -141,6 +139,7 @@ describe('app/actions/navigation', () => {
|
|||
stack: {
|
||||
children: [{
|
||||
component: {
|
||||
id: name,
|
||||
name,
|
||||
passProps,
|
||||
options: merge(defaultOptions, options),
|
||||
|
|
@ -161,6 +160,10 @@ describe('app/actions/navigation', () => {
|
|||
layout: {
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
},
|
||||
sideMenu: {
|
||||
left: {enabled: false},
|
||||
right: {enabled: false},
|
||||
},
|
||||
topBar: {
|
||||
animate: true,
|
||||
visible: true,
|
||||
|
|
@ -180,6 +183,7 @@ describe('app/actions/navigation', () => {
|
|||
|
||||
const expectedLayout = {
|
||||
component: {
|
||||
id: name,
|
||||
name,
|
||||
passProps,
|
||||
options: merge(defaultOptions, options),
|
||||
|
|
@ -241,6 +245,7 @@ describe('app/actions/navigation', () => {
|
|||
stack: {
|
||||
children: [{
|
||||
component: {
|
||||
id: name,
|
||||
name,
|
||||
passProps,
|
||||
options: merge(defaultOptions, options),
|
||||
|
|
@ -317,6 +322,7 @@ describe('app/actions/navigation', () => {
|
|||
stack: {
|
||||
children: [{
|
||||
component: {
|
||||
id: name,
|
||||
name,
|
||||
passProps,
|
||||
options: merge(showModalOptions, defaultOptions),
|
||||
|
|
@ -372,6 +378,7 @@ describe('app/actions/navigation', () => {
|
|||
stack: {
|
||||
children: [{
|
||||
component: {
|
||||
id: showSearchModalName,
|
||||
name: showSearchModalName,
|
||||
passProps: showSearchModalPassProps,
|
||||
options: merge(defaultOptions, showSearchModalOptions),
|
||||
|
|
@ -398,27 +405,6 @@ describe('app/actions/navigation', () => {
|
|||
expect(dismissAllModals).toHaveBeenCalledWith(options);
|
||||
});
|
||||
|
||||
test('peek should call Navigation.push', async () => {
|
||||
const push = jest.spyOn(Navigation, 'push');
|
||||
|
||||
const defaultOptions = {
|
||||
preview: {
|
||||
commit: false,
|
||||
},
|
||||
};
|
||||
|
||||
const expectedLayout = {
|
||||
component: {
|
||||
name,
|
||||
passProps,
|
||||
options: merge(defaultOptions, options),
|
||||
},
|
||||
};
|
||||
|
||||
await NavigationActions.peek(name, passProps, options);
|
||||
expect(push).toHaveBeenCalledWith(topComponentId, expectedLayout);
|
||||
});
|
||||
|
||||
test('mergeNavigationOptions should call Navigation.mergeOptions', () => {
|
||||
const mergeOptions = jest.spyOn(Navigation, 'mergeOptions');
|
||||
|
||||
|
|
@ -454,6 +440,7 @@ describe('app/actions/navigation', () => {
|
|||
|
||||
const expectedLayout = {
|
||||
component: {
|
||||
id: name,
|
||||
name,
|
||||
passProps,
|
||||
options: merge(defaultOptions, options),
|
||||
|
|
|
|||
|
|
@ -5,15 +5,13 @@ import {batchActions} from 'redux-batched-actions';
|
|||
|
||||
import {ViewTypes} from 'app/constants';
|
||||
|
||||
import {UserTypes} from 'mattermost-redux/action_types';
|
||||
import {UserTypes, ChannelTypes} from 'mattermost-redux/action_types';
|
||||
import {
|
||||
fetchMyChannelsAndMembers,
|
||||
getChannelByNameAndTeamName,
|
||||
markChannelAsRead,
|
||||
markChannelAsViewed,
|
||||
leaveChannel as serviceLeaveChannel,
|
||||
selectChannel,
|
||||
getChannelStats,
|
||||
} from 'mattermost-redux/actions/channels';
|
||||
import {
|
||||
getPosts,
|
||||
|
|
@ -23,19 +21,22 @@ import {
|
|||
} 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';
|
||||
import {getTeamMembersByIds, selectTeam} from 'mattermost-redux/actions/teams';
|
||||
import {getProfilesInChannel} from 'mattermost-redux/actions/users';
|
||||
import {Client4} from 'mattermost-redux/client';
|
||||
import {General, Preferences} from 'mattermost-redux/constants';
|
||||
import {getPostIdsInChannel} from 'mattermost-redux/selectors/entities/posts';
|
||||
import {
|
||||
getChannel,
|
||||
getCurrentChannelId,
|
||||
getMyChannelMember,
|
||||
getRedirectChannelNameForTeam,
|
||||
getChannelsNameMapInTeam,
|
||||
isManuallyUnread,
|
||||
} from 'mattermost-redux/selectors/entities/channels';
|
||||
import {getCurrentTeamId, getTeamByName} from 'mattermost-redux/selectors/entities/teams';
|
||||
import {getConfig} from 'mattermost-redux/selectors/entities/general';
|
||||
import {getMyPreferences} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {getCurrentUserId, getUserIdsInChannels, getUsers} from 'mattermost-redux/selectors/entities/users';
|
||||
import {getTeamByName} from 'mattermost-redux/selectors/entities/teams';
|
||||
|
||||
import {getChannelReachable} from 'app/selectors/channel';
|
||||
|
||||
|
|
@ -54,15 +55,12 @@ import {getLastCreateAt} from 'mattermost-redux/utils/post_utils';
|
|||
import {getPreferencesByCategory} from 'mattermost-redux/utils/preference_utils';
|
||||
|
||||
import {INSERT_TO_COMMENT, INSERT_TO_DRAFT} from 'app/constants/post_textbox';
|
||||
import {isDirectChannelVisible, isGroupChannelVisible} from 'app/utils/channels';
|
||||
import {isDirectChannelVisible, isGroupChannelVisible, isDirectMessageVisible, isGroupMessageVisible, isDirectChannelAutoClosed} from 'app/utils/channels';
|
||||
import {buildPreference} from 'app/utils/preferences';
|
||||
|
||||
const MAX_POST_TRIES = 3;
|
||||
import {forceLogoutIfNecessary} from './user';
|
||||
|
||||
export function loadChannelsIfNecessary(teamId) {
|
||||
return async (dispatch) => {
|
||||
await dispatch(fetchMyChannelsAndMembers(teamId));
|
||||
};
|
||||
}
|
||||
const MAX_RETRIES = 3;
|
||||
|
||||
export function loadChannelsByTeamName(teamName, errorHandler) {
|
||||
return async (dispatch, getState) => {
|
||||
|
|
@ -190,22 +188,10 @@ export function loadPostsIfNecessaryWithRetry(channelId) {
|
|||
const time = Date.now();
|
||||
|
||||
let loadMorePostsVisible = true;
|
||||
let received;
|
||||
let postAction;
|
||||
if (!postsIds || postsIds.length < ViewTypes.POST_VISIBILITY_CHUNK_SIZE) {
|
||||
// Get the first page of posts if it appears we haven't gotten it yet, like the webapp
|
||||
received = await retryGetPostsAction(getPosts(channelId), dispatch, getState);
|
||||
|
||||
if (received?.order) {
|
||||
const count = received.order.length;
|
||||
loadMorePostsVisible = count >= ViewTypes.POST_VISIBILITY_CHUNK_SIZE;
|
||||
actions.push({
|
||||
type: ViewTypes.SET_INITIAL_POST_COUNT,
|
||||
data: {
|
||||
channelId,
|
||||
count,
|
||||
},
|
||||
});
|
||||
}
|
||||
postAction = getPosts(channelId);
|
||||
} else {
|
||||
const lastConnectAt = state.websocket?.lastConnectAt || 0;
|
||||
const lastGetPosts = state.views.channel.lastGetPosts[channelId];
|
||||
|
|
@ -221,27 +207,22 @@ export function loadPostsIfNecessaryWithRetry(channelId) {
|
|||
since = getLastCreateAt(postsForChannel);
|
||||
}
|
||||
|
||||
received = await retryGetPostsAction(getPostsSince(channelId, since), dispatch, getState);
|
||||
|
||||
if (received?.order) {
|
||||
const count = received.order.length;
|
||||
loadMorePostsVisible = postsIds.length + count >= ViewTypes.POST_VISIBILITY_CHUNK_SIZE;
|
||||
actions.push({
|
||||
type: ViewTypes.SET_INITIAL_POST_COUNT,
|
||||
data: {
|
||||
channelId,
|
||||
count: postsIds.length + count,
|
||||
},
|
||||
});
|
||||
}
|
||||
postAction = getPostsSince(channelId, since);
|
||||
}
|
||||
|
||||
const received = await retryGetPostsAction(postAction, dispatch, getState);
|
||||
|
||||
if (received) {
|
||||
actions.push({
|
||||
type: ViewTypes.RECEIVED_POSTS_FOR_CHANNEL_AT_TIME,
|
||||
channelId,
|
||||
time,
|
||||
});
|
||||
|
||||
if (received?.order) {
|
||||
const count = received.order.length;
|
||||
loadMorePostsVisible = count >= ViewTypes.POST_VISIBILITY_CHUNK_SIZE;
|
||||
}
|
||||
}
|
||||
|
||||
actions.push(setLoadMorePostsVisible(loadMorePostsVisible));
|
||||
|
|
@ -249,7 +230,7 @@ export function loadPostsIfNecessaryWithRetry(channelId) {
|
|||
};
|
||||
}
|
||||
|
||||
export async function retryGetPostsAction(action, dispatch, getState, maxTries = MAX_POST_TRIES) {
|
||||
export async function retryGetPostsAction(action, dispatch, getState, maxTries = MAX_RETRIES) {
|
||||
for (let i = 0; i < maxTries; i++) {
|
||||
const {data} = await dispatch(action); // eslint-disable-line no-await-in-loop
|
||||
|
||||
|
|
@ -352,7 +333,6 @@ export function selectDefaultChannel(teamId) {
|
|||
|
||||
const channelsInTeam = getChannelsNameMapInTeam(state, teamId);
|
||||
const channel = getChannelByNameSelector(channelsInTeam, getRedirectChannelNameForTeam(state, teamId));
|
||||
|
||||
let channelId;
|
||||
if (channel) {
|
||||
channelId = channel.id;
|
||||
|
|
@ -372,37 +352,31 @@ export function selectDefaultChannel(teamId) {
|
|||
|
||||
export function handleSelectChannel(channelId, fromPushNotification = false) {
|
||||
return async (dispatch, getState) => {
|
||||
const dt = Date.now();
|
||||
const state = getState();
|
||||
const channel = getChannel(state, channelId);
|
||||
const currentTeamId = getCurrentTeamId(state);
|
||||
const currentChannelId = getCurrentChannelId(state);
|
||||
const sameChannel = channelId === currentChannelId;
|
||||
const member = getMyChannelMember(state, channelId);
|
||||
|
||||
dispatch(setLoadMorePostsVisible(true));
|
||||
const {channels, myMembers} = state.entities.channels;
|
||||
const {currentTeamId} = state.entities.teams;
|
||||
const channel = channels[channelId];
|
||||
const member = myMembers[channelId];
|
||||
|
||||
// If the app is open from push notification, we already fetched the posts.
|
||||
if (!fromPushNotification) {
|
||||
dispatch(loadPostsIfNecessaryWithRetry(channelId));
|
||||
}
|
||||
|
||||
let previousChannelId;
|
||||
if (!fromPushNotification && !sameChannel) {
|
||||
previousChannelId = currentChannelId;
|
||||
if (channel && member) {
|
||||
dispatch({
|
||||
type: ChannelTypes.SELECT_CHANNEL,
|
||||
data: channelId,
|
||||
extra: {
|
||||
channel,
|
||||
member,
|
||||
teamId: currentTeamId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const actions = [
|
||||
selectChannel(channelId),
|
||||
getChannelStats(channelId),
|
||||
setChannelDisplayName(channel.display_name),
|
||||
setInitialPostVisibility(channelId),
|
||||
setChannelLoading(false),
|
||||
setLastChannelForTeam(currentTeamId, channelId),
|
||||
selectChannelWithMember(channelId, channel, member),
|
||||
];
|
||||
|
||||
dispatch(batchActions(actions));
|
||||
dispatch(markChannelViewedAndRead(channelId, previousChannelId));
|
||||
console.log('channel switch in', channel?.display_name, (Date.now() - dt), 'ms'); //eslint-disable-line
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -608,8 +582,7 @@ export function setChannelDisplayName(displayName) {
|
|||
export function increasePostVisibility(channelId, postId) {
|
||||
return async (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const {loadingPosts, postVisibility} = state.views.channel;
|
||||
const currentPostVisibility = postVisibility[channelId] || 0;
|
||||
const {loadingPosts} = state.views.channel;
|
||||
|
||||
if (loadingPosts[channelId]) {
|
||||
return true;
|
||||
|
|
@ -620,20 +593,6 @@ export function increasePostVisibility(channelId, postId) {
|
|||
return true;
|
||||
}
|
||||
|
||||
// Check if we already have the posts that we want to show
|
||||
const loadedPostCount = state.views.channel.postCountInChannel[channelId] || 0;
|
||||
const desiredPostVisibility = currentPostVisibility + ViewTypes.POST_VISIBILITY_CHUNK_SIZE;
|
||||
|
||||
if (loadedPostCount >= desiredPostVisibility) {
|
||||
// We already have the posts, so we just need to show them
|
||||
dispatch(batchActions([
|
||||
doIncreasePostVisibility(channelId),
|
||||
setLoadMorePostsVisible(true),
|
||||
]));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
telemetry.reset();
|
||||
telemetry.start(['posts:loading']);
|
||||
|
||||
|
|
@ -658,18 +617,6 @@ export function increasePostVisibility(channelId, postId) {
|
|||
const count = result.order.length;
|
||||
hasMorePost = count >= pageSize;
|
||||
|
||||
actions.push({
|
||||
type: ViewTypes.INCREASE_POST_COUNT,
|
||||
data: {
|
||||
channelId,
|
||||
count,
|
||||
},
|
||||
});
|
||||
|
||||
// make sure to increment the posts visibility
|
||||
// only if we got results
|
||||
actions.push(doIncreasePostVisibility(channelId));
|
||||
|
||||
actions.push(setLoadMorePostsVisible(hasMorePost));
|
||||
}
|
||||
|
||||
|
|
@ -681,24 +628,6 @@ export function increasePostVisibility(channelId, postId) {
|
|||
};
|
||||
}
|
||||
|
||||
export function increasePostVisibilityByOne(channelId) {
|
||||
return (dispatch) => {
|
||||
dispatch({
|
||||
type: ViewTypes.INCREASE_POST_VISIBILITY,
|
||||
data: channelId,
|
||||
amount: 1,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function doIncreasePostVisibility(channelId) {
|
||||
return {
|
||||
type: ViewTypes.INCREASE_POST_VISIBILITY,
|
||||
data: channelId,
|
||||
amount: ViewTypes.POST_VISIBILITY_CHUNK_SIZE,
|
||||
};
|
||||
}
|
||||
|
||||
function setLoadMorePostsVisible(visible) {
|
||||
return {
|
||||
type: ViewTypes.SET_LOAD_MORE_POSTS_VISIBLE,
|
||||
|
|
@ -706,26 +635,202 @@ function setLoadMorePostsVisible(visible) {
|
|||
};
|
||||
}
|
||||
|
||||
function setInitialPostVisibility(channelId) {
|
||||
return {
|
||||
type: ViewTypes.SET_INITIAL_POST_VISIBILITY,
|
||||
data: channelId,
|
||||
export function loadChannelsForTeam(teamId) {
|
||||
return async (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const currentUserId = getCurrentUserId(state);
|
||||
|
||||
if (currentUserId) {
|
||||
const data = {sync: true, teamId};
|
||||
for (let i = 0; i < MAX_RETRIES; i++) {
|
||||
try {
|
||||
console.log('Fetching channels attempt', (i + 1)); //eslint-disable-line no-console
|
||||
const [channels, channelMembers] = await Promise.all([ //eslint-disable-line no-await-in-loop
|
||||
Client4.getMyChannels(teamId),
|
||||
Client4.getMyChannelMembers(teamId),
|
||||
]);
|
||||
|
||||
data.channels = channels;
|
||||
data.channelMembers = channelMembers;
|
||||
break;
|
||||
} catch (error) {
|
||||
const result = await dispatch(forceLogoutIfNecessary(error)); //eslint-disable-line no-await-in-loop
|
||||
if (result || i === MAX_RETRIES) {
|
||||
return {error};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (data.channels) {
|
||||
const roles = new Set();
|
||||
const members = data.channelMembers;
|
||||
for (const member of members) {
|
||||
for (const role of member.roles.split(' ')) {
|
||||
roles.add(role);
|
||||
}
|
||||
}
|
||||
|
||||
if (roles.size > 0) {
|
||||
dispatch(loadRolesIfNeeded(roles));
|
||||
}
|
||||
|
||||
// Fetch needed profiles from channel creators and direct channels
|
||||
dispatch(loadSidebarDirectMessagesProfiles(data));
|
||||
|
||||
dispatch({
|
||||
type: ChannelTypes.RECEIVED_MY_CHANNELS_WITH_MEMBERS,
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
return {data};
|
||||
}
|
||||
|
||||
return {error: 'Cannot fetch channels without a current user'};
|
||||
};
|
||||
}
|
||||
|
||||
function setLastChannelForTeam(teamId, channelId) {
|
||||
return {
|
||||
type: ViewTypes.SET_LAST_CHANNEL_FOR_TEAM,
|
||||
teamId,
|
||||
channelId,
|
||||
export function loadSidebarDirectMessagesProfiles(data) {
|
||||
return async (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const {channels, channelMembers} = data;
|
||||
const currentUserId = getCurrentUserId(state);
|
||||
const usersInChannel = getUserIdsInChannels(state);
|
||||
const directChannels = Object.values(channels).filter((c) => c.type === General.DM_CHANNEL || c.type === General.GM_CHANNEL);
|
||||
const prefs = [];
|
||||
const promises = []; //only fetch profiles that we don't have and the Direct channel should be visible
|
||||
|
||||
// Prepare preferences and start fetching profiles to batch them
|
||||
directChannels.forEach((c) => {
|
||||
const profilesInChannel = Array.from(usersInChannel[c.id] || []).filter((u) => u.id !== currentUserId);
|
||||
switch (c.type) {
|
||||
case General.DM_CHANNEL: {
|
||||
const dm = fetchDirectMessageProfileIfNeeded(state, c, channelMembers, profilesInChannel, prefs);
|
||||
|
||||
if (dm) {
|
||||
promises.push(dispatch(dm));
|
||||
}
|
||||
break;
|
||||
}
|
||||
case General.GM_CHANNEL: {
|
||||
const gm = fetchGroupMessageProfilesIfNeeded(state, c, channelMembers, profilesInChannel, prefs);
|
||||
|
||||
if (gm) {
|
||||
promises.push(dispatch(gm));
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Save preferences if there are any changes
|
||||
if (prefs.length) {
|
||||
dispatch(savePreferences(currentUserId, prefs));
|
||||
}
|
||||
|
||||
getProfilesFromPromises(dispatch, promises, directChannels);
|
||||
|
||||
return {data: true};
|
||||
};
|
||||
}
|
||||
|
||||
function selectChannelWithMember(channelId, channel, member) {
|
||||
return {
|
||||
type: ViewTypes.SELECT_CHANNEL_WITH_MEMBER,
|
||||
data: channelId,
|
||||
channel,
|
||||
member,
|
||||
export function getUsersInChannel(channelId) {
|
||||
return async (dispatch, getState) => {
|
||||
try {
|
||||
const state = getState();
|
||||
const currentUserId = getCurrentUserId(state);
|
||||
const profiles = await Client4.getProfilesInChannel(channelId);
|
||||
|
||||
// When fetching profiles in channels we exclude our own user
|
||||
const users = profiles.filter((p) => p.id !== currentUserId);
|
||||
const data = {
|
||||
channelId,
|
||||
users,
|
||||
};
|
||||
|
||||
return {data};
|
||||
} catch (error) {
|
||||
return {error};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async function getProfilesFromPromises(dispatch, promiseArray, directChannels) {
|
||||
// Get the profiles returned by the promises and retry those that failed
|
||||
let promises = promiseArray;
|
||||
for (let i = 0; i < MAX_RETRIES; i++) {
|
||||
if (!promises.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await Promise.all(promises); //eslint-disable-line no-await-in-loop
|
||||
const failed = [];
|
||||
|
||||
result.forEach((p, index) => {
|
||||
if (p.error) {
|
||||
failed.push(directChannels[index].id);
|
||||
}
|
||||
});
|
||||
|
||||
dispatch({
|
||||
type: UserTypes.RECEIVED_BATCHED_PROFILES_IN_CHANNEL,
|
||||
data: result,
|
||||
});
|
||||
|
||||
if (failed.length) {
|
||||
promises = failed.map((id) => dispatch(getUsersInChannel(id))); //eslint-disable-line no-loop-func
|
||||
continue;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
function fetchDirectMessageProfileIfNeeded(state, channel, channelMembers, profilesInChannel, newPreferences) {
|
||||
const currentUserId = getCurrentUserId(state);
|
||||
const preferences = getMyPreferences(state);
|
||||
const users = getUsers(state);
|
||||
const config = getConfig(state);
|
||||
const currentChannelId = getCurrentChannelId(state);
|
||||
const otherUserId = getUserIdFromChannelName(currentUserId, channel.name);
|
||||
const otherUser = users[otherUserId];
|
||||
const dmVisible = isDirectMessageVisible(preferences, channel.id);
|
||||
const dmAutoClosed = isDirectChannelAutoClosed(config, preferences, channel.id, channel.last_post_at, otherUser?.delete_at, currentChannelId); //eslint-disable-line camelcase
|
||||
const dmIsUnread = channelMembers[channel.id]?.mention_count > 0; //eslint-disable-line camelcase
|
||||
const dmFetchProfile = dmIsUnread || (dmVisible && !dmAutoClosed);
|
||||
|
||||
// when then DM is hidden but has new messages
|
||||
if ((!dmVisible || dmAutoClosed) && dmIsUnread) {
|
||||
newPreferences.push(buildPreference(Preferences.CATEGORY_DIRECT_CHANNEL_SHOW, currentUserId, otherUserId));
|
||||
newPreferences.push(buildPreference(Preferences.CATEGORY_CHANNEL_OPEN_TIME, currentUserId, channel.id, Date.now().toString()));
|
||||
}
|
||||
|
||||
if (dmFetchProfile && !profilesInChannel.includes(otherUserId) && otherUserId !== currentUserId) {
|
||||
return getUsersInChannel(channel.id);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function fetchGroupMessageProfilesIfNeeded(state, channel, channelMembers, profilesInChannel, newPreferences) {
|
||||
const currentUserId = getCurrentUserId(state);
|
||||
const preferences = getMyPreferences(state);
|
||||
const config = getConfig(state);
|
||||
const gmVisible = isGroupMessageVisible(preferences, channel.id);
|
||||
const gmAutoClosed = isDirectChannelAutoClosed(config, preferences, channel.id, channel.last_post_at);
|
||||
const channelMember = channelMembers[channel.id];
|
||||
const gmIsUnread = channelMember?.mention_count > 0 || channelMember?.msg_count < channel.total_msg_count; //eslint-disable-line camelcase
|
||||
const gmFetchProfile = gmIsUnread || (gmVisible && !gmAutoClosed);
|
||||
|
||||
// when then GM is hidden but has new messages
|
||||
if ((!gmVisible || gmAutoClosed) && gmIsUnread) {
|
||||
newPreferences.push(buildPreference(Preferences.CATEGORY_GROUP_CHANNEL_SHOW, currentUserId, channel.id));
|
||||
newPreferences.push(buildPreference(Preferences.CATEGORY_CHANNEL_OPEN_TIME, currentUserId, channel.id, Date.now().toString()));
|
||||
}
|
||||
|
||||
if (gmFetchProfile && !profilesInChannel.length) {
|
||||
return getUsersInChannel(channel.id);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
|
@ -5,7 +5,7 @@ import configureStore from 'redux-mock-store';
|
|||
import thunk from 'redux-thunk';
|
||||
|
||||
import initialState from 'app/initial_state';
|
||||
import {ViewTypes} from 'app/constants';
|
||||
import {ChannelTypes} from 'mattermost-redux/action_types';
|
||||
import testHelper from 'test/test_helper';
|
||||
|
||||
import * as ChannelActions from 'app/actions/views/channel';
|
||||
|
|
@ -116,14 +116,19 @@ describe('Actions.Views.Channel', () => {
|
|||
},
|
||||
channels: {
|
||||
currentChannelId,
|
||||
channels: {
|
||||
'channel-id': {id: 'channel-id', display_name: 'Test Channel'},
|
||||
},
|
||||
myMembers: {
|
||||
'channel-id': {channel_id: 'channel-id', user_id: currentUserId, mention_count: 0, msg_count: 0},
|
||||
},
|
||||
},
|
||||
teams: {
|
||||
currentTeamId,
|
||||
teams: {
|
||||
currentTeamId,
|
||||
currentTeams: {
|
||||
[currentTeamId]: {
|
||||
name: currentTeamName,
|
||||
},
|
||||
[currentTeamId]: {
|
||||
id: currentTeamId,
|
||||
name: currentTeamName,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -147,14 +152,13 @@ describe('Actions.Views.Channel', () => {
|
|||
const receivedChannel = storeActions.some((action) => action.type === MOCK_RECEIVE_CHANNEL_TYPE);
|
||||
expect(receivedChannel).toBe(true);
|
||||
|
||||
const storeBatchActions = storeActions.filter(({type}) => type === 'BATCHING_REDUCER.BATCH');
|
||||
const selectedChannel = storeBatchActions[0].payload.some((action) => action.type === MOCK_SELECT_CHANNEL_TYPE);
|
||||
const selectedChannel = storeActions.some(({type}) => type === MOCK_RECEIVE_CHANNEL_TYPE);
|
||||
expect(selectedChannel).toBe(true);
|
||||
});
|
||||
|
||||
test('handleSelectChannelByName failure from null currentTeamName', async () => {
|
||||
const failStoreObj = {...storeObj};
|
||||
failStoreObj.entities.teams.teams.currentTeamId = 'not-in-current-teams';
|
||||
failStoreObj.entities.teams.currentTeamId = 'not-in-current-teams';
|
||||
store = mockStore(failStoreObj);
|
||||
|
||||
await store.dispatch(handleSelectChannelByName(currentChannelName, null));
|
||||
|
|
@ -168,6 +172,7 @@ describe('Actions.Views.Channel', () => {
|
|||
});
|
||||
|
||||
test('handleSelectChannelByName failure from no permission to channel', async () => {
|
||||
store = mockStore({...storeObj});
|
||||
actions.getChannelByNameAndTeamName = jest.fn(() => {
|
||||
return {
|
||||
type: 'MOCK_ERROR',
|
||||
|
|
@ -283,29 +288,38 @@ describe('Actions.Views.Channel', () => {
|
|||
[`not-${currentChannelId}`, false],
|
||||
];
|
||||
test.each(handleSelectChannelCases)('handleSelectChannel dispatches selectChannelWithMember', async (channelId, fromPushNotification) => {
|
||||
store = mockStore({...storeObj});
|
||||
const testObj = {...storeObj};
|
||||
testObj.entities.teams.currentTeamId = currentTeamId;
|
||||
store = mockStore(testObj);
|
||||
|
||||
await store.dispatch(handleSelectChannel(channelId, fromPushNotification));
|
||||
const storeActions = store.getActions();
|
||||
const storeBatchActions = storeActions.find(({type}) => type === 'BATCHING_REDUCER.BATCH');
|
||||
const selectChannelWithMember = storeBatchActions.payload.find(({type}) => type === ViewTypes.SELECT_CHANNEL_WITH_MEMBER);
|
||||
const selectChannelWithMember = storeActions.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);
|
||||
|
||||
const expectedSelectChannelWithMember = {
|
||||
type: ViewTypes.SELECT_CHANNEL_WITH_MEMBER,
|
||||
type: ChannelTypes.SELECT_CHANNEL,
|
||||
data: channelId,
|
||||
channel: {
|
||||
data: channelId,
|
||||
},
|
||||
member: {
|
||||
data: {
|
||||
member: {},
|
||||
extra: {
|
||||
channel: {
|
||||
id: channelId,
|
||||
display_name: 'Test Channel',
|
||||
},
|
||||
member: {
|
||||
channel_id: channelId,
|
||||
user_id: currentUserId,
|
||||
mention_count: 0,
|
||||
msg_count: 0,
|
||||
},
|
||||
teamId: currentTeamId,
|
||||
},
|
||||
|
||||
};
|
||||
expect(selectChannelWithMember).toStrictEqual(expectedSelectChannelWithMember);
|
||||
if (channelId.includes('not')) {
|
||||
expect(selectChannelWithMember).toBe(undefined);
|
||||
} else {
|
||||
expect(selectChannelWithMember).toStrictEqual(expectedSelectChannelWithMember);
|
||||
}
|
||||
expect(viewedAction).not.toBe(null);
|
||||
expect(readAction).not.toBe(null);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ export function loadConfigAndLicense() {
|
|||
if (currentUserId) {
|
||||
if (config.DataRetentionEnableMessageDeletion && config.DataRetentionEnableMessageDeletion === 'true' &&
|
||||
license.IsLicensed === 'true' && license.DataRetention === 'true') {
|
||||
getDataRetentionPolicy()(dispatch, getState);
|
||||
dispatch(getDataRetentionPolicy());
|
||||
} else {
|
||||
dispatch({type: GeneralTypes.RECEIVED_DATA_RETENTION_POLICY, data: {}});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,185 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {UserTypes} from 'mattermost-redux/action_types';
|
||||
import {GeneralTypes, UserTypes} from 'mattermost-redux/action_types';
|
||||
import {getDataRetentionPolicy} from 'mattermost-redux/actions/general';
|
||||
import * as HelperActions from 'mattermost-redux/actions/helpers';
|
||||
import {loadRolesIfNeeded} from 'mattermost-redux/actions/roles';
|
||||
import {autoUpdateTimezone} from 'mattermost-redux/actions/timezone';
|
||||
import {Client4} from 'mattermost-redux/client';
|
||||
import {General} from 'mattermost-redux/constants';
|
||||
import {getConfig, getLicense} from 'mattermost-redux/selectors/entities/general';
|
||||
import {isTimezoneEnabled} from 'mattermost-redux/selectors/entities/timezone';
|
||||
import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
|
||||
|
||||
import {setAppCredentials} from 'app/init/credentials';
|
||||
import {setCSRFFromCookie} from 'app/utils/security';
|
||||
import {getDeviceTimezoneAsync} from 'app/utils/timezone';
|
||||
|
||||
const HTTP_UNAUTHORIZED = 401;
|
||||
|
||||
export function completeLogin(user, deviceToken) {
|
||||
return async (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const config = getConfig(state);
|
||||
const license = getLicense(state);
|
||||
const token = Client4.getToken();
|
||||
const url = Client4.getUrl();
|
||||
|
||||
setCSRFFromCookie(url);
|
||||
setAppCredentials(deviceToken, user.id, token, url);
|
||||
|
||||
// Set timezone
|
||||
const enableTimezone = isTimezoneEnabled(state);
|
||||
if (enableTimezone) {
|
||||
const timezone = await getDeviceTimezoneAsync();
|
||||
dispatch(autoUpdateTimezone(timezone));
|
||||
}
|
||||
|
||||
// Data retention
|
||||
if (config.DataRetentionEnableMessageDeletion && config.DataRetentionEnableMessageDeletion === 'true' &&
|
||||
license.IsLicensed === 'true' && license.DataRetention === 'true') {
|
||||
dispatch(getDataRetentionPolicy());
|
||||
} else {
|
||||
dispatch({type: GeneralTypes.RECEIVED_DATA_RETENTION_POLICY, data: {}});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function loadMe(user, deviceToken) {
|
||||
return async (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const data = {user};
|
||||
const deviceId = state.entities?.general?.deviceToken;
|
||||
|
||||
try {
|
||||
if (deviceId && !deviceToken) {
|
||||
await Client4.attachDevice(deviceId);
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
data.user = await Client4.getMe();
|
||||
}
|
||||
} catch (error) {
|
||||
dispatch(forceLogoutIfNecessary(error));
|
||||
return {error};
|
||||
}
|
||||
|
||||
try {
|
||||
Client4.setUserId(data.user.id);
|
||||
Client4.setUserRoles(data.user.roles);
|
||||
|
||||
// Execute all other requests in parallel
|
||||
const teamsRequest = Client4.getMyTeams();
|
||||
const teamMembersRequest = Client4.getMyTeamMembers();
|
||||
const teamUnreadRequest = Client4.getMyTeamUnreads();
|
||||
const preferencesRequest = Client4.getMyPreferences();
|
||||
const configRequest = Client4.getClientConfigOld();
|
||||
|
||||
const [teams, teamMembers, teamUnreads, preferences, config] = await Promise.all([
|
||||
teamsRequest,
|
||||
teamMembersRequest,
|
||||
teamUnreadRequest,
|
||||
preferencesRequest,
|
||||
configRequest,
|
||||
]);
|
||||
|
||||
data.teams = teams;
|
||||
data.teamMembers = teamMembers;
|
||||
data.teamUnreads = teamUnreads;
|
||||
data.preferences = preferences;
|
||||
data.config = config;
|
||||
data.url = Client4.getUrl();
|
||||
|
||||
dispatch({
|
||||
type: UserTypes.LOGIN,
|
||||
data,
|
||||
});
|
||||
|
||||
const roles = new Set();
|
||||
for (const role of data.user.roles.split(' ')) {
|
||||
roles.add(role);
|
||||
}
|
||||
for (const teamMember of teamMembers) {
|
||||
for (const role of teamMember.roles.split(' ')) {
|
||||
roles.add(role);
|
||||
}
|
||||
}
|
||||
if (roles.size > 0) {
|
||||
dispatch(loadRolesIfNeeded(roles));
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('login error', error.stack); // eslint-disable-line no-console
|
||||
return {error};
|
||||
}
|
||||
|
||||
return {data};
|
||||
};
|
||||
}
|
||||
|
||||
export function login(loginId, password, mfaToken, ldapOnly = false) {
|
||||
return async (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const deviceToken = state.entities?.general?.deviceToken;
|
||||
let user;
|
||||
|
||||
try {
|
||||
user = await Client4.login(loginId, password, mfaToken, deviceToken, ldapOnly);
|
||||
} catch (error) {
|
||||
return {error};
|
||||
}
|
||||
|
||||
const result = await dispatch(loadMe(user));
|
||||
|
||||
if (!result.error) {
|
||||
dispatch(completeLogin(user, deviceToken));
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
}
|
||||
|
||||
export function ssoLogin(token) {
|
||||
return async (dispatch) => {
|
||||
Client4.setToken(token);
|
||||
const result = await dispatch(loadMe());
|
||||
|
||||
if (!result.error) {
|
||||
dispatch(completeLogin(result.data.user));
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
}
|
||||
|
||||
export function logout(skipServerLogout = false) {
|
||||
return async (dispatch) => {
|
||||
if (!skipServerLogout) {
|
||||
try {
|
||||
Client4.logout();
|
||||
} catch {
|
||||
// Do nothing
|
||||
}
|
||||
}
|
||||
|
||||
dispatch({type: UserTypes.LOGOUT_SUCCESS});
|
||||
};
|
||||
}
|
||||
|
||||
export function forceLogoutIfNecessary(error) {
|
||||
return async (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const currentUserId = getCurrentUserId(state);
|
||||
|
||||
if (currentUserId && error.status_code === HTTP_UNAUTHORIZED && error.url && !error.url.includes('/login')) {
|
||||
dispatch(logout(true));
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
}
|
||||
|
||||
export function setCurrentUserStatusOffline() {
|
||||
return (dispatch, getState) => {
|
||||
const currentUserId = getCurrentUserId(getState());
|
||||
|
|
@ -18,3 +193,5 @@ export function setCurrentUserStatusOffline() {
|
|||
});
|
||||
};
|
||||
}
|
||||
|
||||
HelperActions.forceLogoutIfNecessary = forceLogoutIfNecessary;
|
||||
|
|
@ -9,7 +9,6 @@ import {
|
|||
StyleSheet,
|
||||
Text,
|
||||
} from 'react-native';
|
||||
import FastImage from 'react-native-fast-image';
|
||||
|
||||
import CustomPropTypes from 'app/constants/custom_prop_types';
|
||||
import ImageCacheManager from 'app/utils/image_cache_manager';
|
||||
|
|
@ -143,7 +142,7 @@ export default class Emoji extends React.PureComponent {
|
|||
}
|
||||
|
||||
return (
|
||||
<FastImage
|
||||
<Image
|
||||
key={key}
|
||||
style={[customEmojiStyle, {width, height}]}
|
||||
source={{uri: imageUrl}}
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ export default class AttachmentText extends PureComponent {
|
|||
metadata: PropTypes.object,
|
||||
onPermalinkPress: PropTypes.func,
|
||||
textStyles: PropTypes.object.isRequired,
|
||||
theme: PropTypes.object,
|
||||
value: PropTypes.string,
|
||||
};
|
||||
|
||||
|
|
@ -68,8 +69,9 @@ export default class AttachmentText extends PureComponent {
|
|||
hasThumbnail,
|
||||
metadata,
|
||||
onPermalinkPress,
|
||||
value,
|
||||
textStyles,
|
||||
theme,
|
||||
value,
|
||||
} = this.props;
|
||||
const {collapsed, isLongText, maxHeight} = this.state;
|
||||
|
||||
|
|
@ -103,6 +105,7 @@ export default class AttachmentText extends PureComponent {
|
|||
<ShowMoreButton
|
||||
onPress={this.toggleCollapseState}
|
||||
showMore={collapsed}
|
||||
theme={theme}
|
||||
/>
|
||||
}
|
||||
</View>
|
||||
|
|
|
|||
|
|
@ -95,6 +95,7 @@ export default class MessageAttachment extends PureComponent {
|
|||
onPermalinkPress={onPermalinkPress}
|
||||
textStyles={textStyles}
|
||||
value={attachment.text}
|
||||
theme={theme}
|
||||
/>
|
||||
<AttachmentFields
|
||||
baseTextStyle={baseTextStyle}
|
||||
|
|
|
|||
|
|
@ -320,7 +320,7 @@ export default class Post extends PureComponent {
|
|||
underlayColor={changeOpacity(theme.centerChannelColor, 0.1)}
|
||||
cancelTouchOnPanning={true}
|
||||
>
|
||||
<React.Fragment>
|
||||
<>
|
||||
<PostPreHeader
|
||||
isConsecutive={mergeMessage}
|
||||
isFlagged={isFlagged}
|
||||
|
|
@ -355,7 +355,7 @@ export default class Post extends PureComponent {
|
|||
/>
|
||||
</View>
|
||||
</View>
|
||||
</React.Fragment>
|
||||
</>
|
||||
</TouchableWithFeedback>
|
||||
</View>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -431,6 +431,7 @@ export default class PostBody extends PureComponent {
|
|||
<ShowMoreButton
|
||||
highlight={highlight}
|
||||
onPress={this.openLongPost}
|
||||
theme={theme}
|
||||
/>
|
||||
}
|
||||
{this.renderPostAdditionalContent(blockStyles, messageStyle, textStyles)}
|
||||
|
|
|
|||
|
|
@ -92,7 +92,6 @@ describe('PostBody', () => {
|
|||
event.nativeEvent.layout.height = wrapper.state('maxHeight') - 1;
|
||||
instance.measurePost(event);
|
||||
expect(wrapper.state('isLongPost')).toEqual(false);
|
||||
|
||||
event.nativeEvent.layout.height = wrapper.state('maxHeight') + 1;
|
||||
instance.measurePost(event);
|
||||
expect(wrapper.state('isLongPost')).toEqual(true);
|
||||
|
|
|
|||
|
|
@ -20,10 +20,11 @@ exports[`PostList setting channel deep link 1`] = `
|
|||
"channel-id",
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
]
|
||||
}
|
||||
horizontal={false}
|
||||
initialNumToRender={15}
|
||||
initialNumToRender={7}
|
||||
inverted={true}
|
||||
keyExtractor={[Function]}
|
||||
keyboardDismissMode="interactive"
|
||||
|
|
@ -34,7 +35,7 @@ exports[`PostList setting channel deep link 1`] = `
|
|||
"minIndexForVisible": 0,
|
||||
}
|
||||
}
|
||||
maxToRenderPerBatch={16}
|
||||
maxToRenderPerBatch={10}
|
||||
numColumns={1}
|
||||
onContentSizeChange={[Function]}
|
||||
onEndReachedThreshold={2}
|
||||
|
|
@ -56,8 +57,13 @@ exports[`PostList setting channel deep link 1`] = `
|
|||
removeClippedSubviews={true}
|
||||
renderItem={[Function]}
|
||||
scrollEventThrottle={60}
|
||||
style={
|
||||
Object {
|
||||
"flex": 1,
|
||||
}
|
||||
}
|
||||
updateCellsBatchingPeriod={50}
|
||||
windowSize={21}
|
||||
windowSize={50}
|
||||
/>
|
||||
`;
|
||||
|
||||
|
|
@ -81,10 +87,11 @@ exports[`PostList setting permalink deep link 1`] = `
|
|||
"channel-id",
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
]
|
||||
}
|
||||
horizontal={false}
|
||||
initialNumToRender={15}
|
||||
initialNumToRender={7}
|
||||
inverted={true}
|
||||
keyExtractor={[Function]}
|
||||
keyboardDismissMode="interactive"
|
||||
|
|
@ -95,7 +102,7 @@ exports[`PostList setting permalink deep link 1`] = `
|
|||
"minIndexForVisible": 0,
|
||||
}
|
||||
}
|
||||
maxToRenderPerBatch={16}
|
||||
maxToRenderPerBatch={10}
|
||||
numColumns={1}
|
||||
onContentSizeChange={[Function]}
|
||||
onEndReachedThreshold={2}
|
||||
|
|
@ -117,8 +124,13 @@ exports[`PostList setting permalink deep link 1`] = `
|
|||
removeClippedSubviews={true}
|
||||
renderItem={[Function]}
|
||||
scrollEventThrottle={60}
|
||||
style={
|
||||
Object {
|
||||
"flex": 1,
|
||||
}
|
||||
}
|
||||
updateCellsBatchingPeriod={50}
|
||||
windowSize={21}
|
||||
windowSize={50}
|
||||
/>
|
||||
`;
|
||||
|
||||
|
|
@ -142,10 +154,11 @@ exports[`PostList should match snapshot 1`] = `
|
|||
"channel-id",
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
]
|
||||
}
|
||||
horizontal={false}
|
||||
initialNumToRender={15}
|
||||
initialNumToRender={7}
|
||||
inverted={true}
|
||||
keyExtractor={[Function]}
|
||||
keyboardDismissMode="interactive"
|
||||
|
|
@ -156,7 +169,7 @@ exports[`PostList should match snapshot 1`] = `
|
|||
"minIndexForVisible": 0,
|
||||
}
|
||||
}
|
||||
maxToRenderPerBatch={16}
|
||||
maxToRenderPerBatch={10}
|
||||
numColumns={1}
|
||||
onContentSizeChange={[Function]}
|
||||
onEndReachedThreshold={2}
|
||||
|
|
@ -178,7 +191,12 @@ exports[`PostList should match snapshot 1`] = `
|
|||
removeClippedSubviews={true}
|
||||
renderItem={[Function]}
|
||||
scrollEventThrottle={60}
|
||||
style={
|
||||
Object {
|
||||
"flex": 1,
|
||||
}
|
||||
}
|
||||
updateCellsBatchingPeriod={50}
|
||||
windowSize={21}
|
||||
windowSize={50}
|
||||
/>
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -24,7 +24,8 @@ import {t} from 'app/utils/i18n';
|
|||
import DateHeader from './date_header';
|
||||
import NewMessagesDivider from './new_messages_divider';
|
||||
|
||||
const INITIAL_BATCH_TO_RENDER = 15;
|
||||
const INITIAL_BATCH_TO_RENDER = 7;
|
||||
const LOADING_POSTS_HEIGHT = 53;
|
||||
const SCROLL_UP_MULTIPLIER = 3.5;
|
||||
const SCROLL_POSITION_CONFIG = {
|
||||
|
||||
|
|
@ -55,6 +56,7 @@ export default class PostList extends PureComponent {
|
|||
isSearchResult: PropTypes.bool,
|
||||
lastPostIndex: PropTypes.number.isRequired,
|
||||
lastViewedAt: PropTypes.number, // Used by container // eslint-disable-line no-unused-prop-types
|
||||
loadMorePostsVisible: PropTypes.bool,
|
||||
onLoadMoreUp: PropTypes.func,
|
||||
onHashtagPress: PropTypes.func,
|
||||
onPermalinkPress: PropTypes.func,
|
||||
|
|
@ -129,7 +131,7 @@ export default class PostList extends PureComponent {
|
|||
this.shouldScrollToBottom = false;
|
||||
}
|
||||
|
||||
if (!this.hasDoneInitialScroll && this.props.initialIndex > 0 && this.state.contentHeight) {
|
||||
if (!this.hasDoneInitialScroll && this.props.initialIndex > 0 && this.state.contentHeight > LOADING_POSTS_HEIGHT) {
|
||||
this.scrollToInitialIndexIfNeeded(this.props.initialIndex);
|
||||
}
|
||||
|
||||
|
|
@ -138,7 +140,7 @@ export default class PostList extends PureComponent {
|
|||
this.props.postIds.length &&
|
||||
this.state.contentHeight &&
|
||||
this.state.contentHeight < this.state.postListHeight &&
|
||||
this.props.extraData
|
||||
!this.props.extraData
|
||||
) {
|
||||
this.loadToFillContent();
|
||||
}
|
||||
|
|
@ -169,7 +171,7 @@ export default class PostList extends PureComponent {
|
|||
handleContentSizeChange = (contentWidth, contentHeight) => {
|
||||
if (this.state.contentHeight !== contentHeight) {
|
||||
this.setState({contentHeight}, () => {
|
||||
if (this.state.postListHeight && contentHeight < this.state.postListHeight && this.props.extraData) {
|
||||
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();
|
||||
}
|
||||
|
|
@ -392,11 +394,13 @@ export default class PostList extends PureComponent {
|
|||
};
|
||||
|
||||
flatListScrollToIndex = (index) => {
|
||||
this.flatListRef.current.scrollToIndex({
|
||||
animated: false,
|
||||
index,
|
||||
viewOffset: 0,
|
||||
viewPosition: 1, // 0 is at bottom
|
||||
this.animationFrameInitialIndex = requestAnimationFrame(() => {
|
||||
this.flatListRef.current.scrollToIndex({
|
||||
animated: false,
|
||||
index,
|
||||
viewOffset: 0,
|
||||
viewPosition: 1, // 0 is at bottom
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -455,7 +459,9 @@ export default class PostList extends PureComponent {
|
|||
render() {
|
||||
const {
|
||||
channelId,
|
||||
extraData,
|
||||
highlightPostId,
|
||||
loadMorePostsVisible,
|
||||
postIds,
|
||||
refreshing,
|
||||
scrollViewNativeID,
|
||||
|
|
@ -476,9 +482,10 @@ export default class PostList extends PureComponent {
|
|||
<FlatList
|
||||
key={`recyclerFor-${channelId}-${hasPostsKey}`}
|
||||
ref={this.flatListRef}
|
||||
style={{flex: 1}}
|
||||
contentContainerStyle={styles.postListContent}
|
||||
data={postIds}
|
||||
extraData={this.makeExtraData(channelId, highlightPostId, this.props.extraData)}
|
||||
extraData={this.makeExtraData(channelId, highlightPostId, extraData, loadMorePostsVisible)}
|
||||
initialNumToRender={INITIAL_BATCH_TO_RENDER}
|
||||
inverted={true}
|
||||
keyboardDismissMode={'interactive'}
|
||||
|
|
@ -486,7 +493,6 @@ export default class PostList extends PureComponent {
|
|||
keyExtractor={this.keyExtractor}
|
||||
ListFooterComponent={this.props.renderFooter}
|
||||
maintainVisibleContentPosition={SCROLL_POSITION_CONFIG}
|
||||
maxToRenderPerBatch={INITIAL_BATCH_TO_RENDER + 1}
|
||||
onContentSizeChange={this.handleContentSizeChange}
|
||||
onLayout={this.handleLayout}
|
||||
onScroll={this.handleScroll}
|
||||
|
|
@ -496,6 +502,7 @@ export default class PostList extends PureComponent {
|
|||
scrollEventThrottle={60}
|
||||
refreshControl={refreshControl}
|
||||
nativeID={scrollViewNativeID}
|
||||
windowSize={50}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -109,7 +109,7 @@ describe('PostList', () => {
|
|||
instance.loadToFillContent = jest.fn();
|
||||
|
||||
wrapper.setProps({
|
||||
extraData: true,
|
||||
extraData: false,
|
||||
});
|
||||
expect(instance.loadToFillContent).toHaveBeenCalledTimes(0);
|
||||
|
||||
|
|
@ -120,7 +120,7 @@ describe('PostList', () => {
|
|||
expect(instance.loadToFillContent).toHaveBeenCalledTimes(1);
|
||||
|
||||
wrapper.setProps({
|
||||
extraData: false,
|
||||
extraData: true,
|
||||
});
|
||||
|
||||
expect(instance.loadToFillContent).toHaveBeenCalledTimes(1);
|
||||
|
|
|
|||
|
|
@ -1,16 +1,145 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {connect} from 'react-redux';
|
||||
import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Text, View} from 'react-native';
|
||||
import LinearGradient from 'react-native-linear-gradient';
|
||||
|
||||
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import FormattedText from 'app/components/formatted_text';
|
||||
import TouchableWithFeedback from 'app/components/touchable_with_feedback';
|
||||
import {t} from 'app/utils/i18n';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
|
||||
import ShowMoreButton from './show_more_button';
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
theme: getTheme(state),
|
||||
export default class ShowMoreButton extends PureComponent {
|
||||
static propTypes = {
|
||||
highlight: PropTypes.bool,
|
||||
onPress: PropTypes.func.isRequired,
|
||||
showMore: PropTypes.bool.isRequired,
|
||||
theme: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
showMore: true,
|
||||
};
|
||||
|
||||
renderButton(showMore, style) {
|
||||
let sign = '+';
|
||||
let textId = t('post_info.message.show_more');
|
||||
let textMessage = 'Show More';
|
||||
if (!showMore) {
|
||||
sign = '-';
|
||||
textId = t('post_info.message.show_less');
|
||||
textMessage = 'Show Less';
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={style.button}>
|
||||
<Text style={style.sign}>{sign}</Text>
|
||||
<FormattedText
|
||||
id={textId}
|
||||
defaultMessage={textMessage}
|
||||
style={style.text}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {highlight, showMore, theme} = this.props;
|
||||
const style = getStyleSheet(theme, showMore);
|
||||
|
||||
let gradientColors = [
|
||||
changeOpacity(theme.centerChannelBg, 0),
|
||||
changeOpacity(theme.centerChannelBg, 0.75),
|
||||
theme.centerChannelBg,
|
||||
];
|
||||
if (highlight) {
|
||||
gradientColors = [
|
||||
changeOpacity(theme.mentionHighlightBg, 0),
|
||||
changeOpacity(theme.mentionHighlightBg, 0.15),
|
||||
changeOpacity(theme.mentionHighlightBg, 0.5),
|
||||
];
|
||||
}
|
||||
|
||||
return (
|
||||
<View>
|
||||
{showMore &&
|
||||
<LinearGradient
|
||||
colors={gradientColors}
|
||||
locations={[0, 0.7, 1]}
|
||||
style={style.gradient}
|
||||
/>
|
||||
}
|
||||
<View style={style.container}>
|
||||
<View style={style.dividerLeft}/>
|
||||
<TouchableWithFeedback
|
||||
onPress={this.props.onPress}
|
||||
style={style.buttonContainer}
|
||||
type={'opacity'}
|
||||
>
|
||||
{this.renderButton(showMore, style)}
|
||||
</TouchableWithFeedback>
|
||||
<View style={style.dividerRight}/>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(ShowMoreButton);
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme, showMore) => {
|
||||
return {
|
||||
gradient: {
|
||||
flex: 1,
|
||||
height: 50,
|
||||
position: 'absolute',
|
||||
top: -50,
|
||||
width: '100%',
|
||||
},
|
||||
container: {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
position: 'relative',
|
||||
top: showMore ? -7.5 : 10,
|
||||
marginBottom: 10,
|
||||
},
|
||||
dividerLeft: {
|
||||
backgroundColor: changeOpacity(theme.centerChannelColor, 0.2),
|
||||
flex: 1,
|
||||
height: 1,
|
||||
marginRight: 10,
|
||||
},
|
||||
buttonContainer: {
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
borderColor: changeOpacity(theme.centerChannelColor, 0.2),
|
||||
borderRadius: 4,
|
||||
borderWidth: 1,
|
||||
height: 37,
|
||||
paddingHorizontal: 10,
|
||||
},
|
||||
button: {
|
||||
alignItems: 'center',
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
},
|
||||
sign: {
|
||||
color: theme.linkColor,
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
marginRight: 8,
|
||||
},
|
||||
text: {
|
||||
color: theme.linkColor,
|
||||
fontSize: 13,
|
||||
fontWeight: '600',
|
||||
},
|
||||
dividerRight: {
|
||||
backgroundColor: changeOpacity(theme.centerChannelColor, 0.2),
|
||||
flex: 1,
|
||||
height: 1,
|
||||
marginLeft: 10,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,145 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Text, View} from 'react-native';
|
||||
import LinearGradient from 'react-native-linear-gradient';
|
||||
|
||||
import FormattedText from 'app/components/formatted_text';
|
||||
import TouchableWithFeedback from 'app/components/touchable_with_feedback';
|
||||
import {t} from 'app/utils/i18n';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
|
||||
export default class ShowMoreButton extends PureComponent {
|
||||
static propTypes = {
|
||||
highlight: PropTypes.bool,
|
||||
onPress: PropTypes.func.isRequired,
|
||||
showMore: PropTypes.bool.isRequired,
|
||||
theme: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
showMore: true,
|
||||
};
|
||||
|
||||
renderButton(showMore, style) {
|
||||
let sign = '+';
|
||||
let textId = t('post_info.message.show_more');
|
||||
let textMessage = 'Show More';
|
||||
if (!showMore) {
|
||||
sign = '-';
|
||||
textId = t('post_info.message.show_less');
|
||||
textMessage = 'Show Less';
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={style.button}>
|
||||
<Text style={style.sign}>{sign}</Text>
|
||||
<FormattedText
|
||||
id={textId}
|
||||
defaultMessage={textMessage}
|
||||
style={style.text}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {highlight, showMore, theme} = this.props;
|
||||
const style = getStyleSheet(theme, showMore);
|
||||
|
||||
let gradientColors = [
|
||||
changeOpacity(theme.centerChannelBg, 0),
|
||||
changeOpacity(theme.centerChannelBg, 0.75),
|
||||
theme.centerChannelBg,
|
||||
];
|
||||
if (highlight) {
|
||||
gradientColors = [
|
||||
changeOpacity(theme.mentionHighlightBg, 0),
|
||||
changeOpacity(theme.mentionHighlightBg, 0.15),
|
||||
changeOpacity(theme.mentionHighlightBg, 0.5),
|
||||
];
|
||||
}
|
||||
|
||||
return (
|
||||
<View>
|
||||
{showMore &&
|
||||
<LinearGradient
|
||||
colors={gradientColors}
|
||||
locations={[0, 0.7, 1]}
|
||||
style={style.gradient}
|
||||
/>
|
||||
}
|
||||
<View style={style.container}>
|
||||
<View style={style.dividerLeft}/>
|
||||
<TouchableWithFeedback
|
||||
onPress={this.props.onPress}
|
||||
style={style.buttonContainer}
|
||||
type={'opacity'}
|
||||
>
|
||||
{this.renderButton(showMore, style)}
|
||||
</TouchableWithFeedback>
|
||||
<View style={style.dividerRight}/>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme, showMore) => {
|
||||
return {
|
||||
gradient: {
|
||||
flex: 1,
|
||||
height: 50,
|
||||
position: 'absolute',
|
||||
top: -50,
|
||||
width: '100%',
|
||||
},
|
||||
container: {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
position: 'relative',
|
||||
top: showMore ? -7.5 : 10,
|
||||
marginBottom: 10,
|
||||
},
|
||||
dividerLeft: {
|
||||
backgroundColor: changeOpacity(theme.centerChannelColor, 0.2),
|
||||
flex: 1,
|
||||
height: 1,
|
||||
marginRight: 10,
|
||||
},
|
||||
buttonContainer: {
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
borderColor: changeOpacity(theme.centerChannelColor, 0.2),
|
||||
borderRadius: 4,
|
||||
borderWidth: 1,
|
||||
height: 37,
|
||||
paddingHorizontal: 10,
|
||||
},
|
||||
button: {
|
||||
alignItems: 'center',
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
},
|
||||
sign: {
|
||||
color: theme.linkColor,
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
marginRight: 8,
|
||||
},
|
||||
text: {
|
||||
color: theme.linkColor,
|
||||
fontSize: 13,
|
||||
fontWeight: '600',
|
||||
},
|
||||
dividerRight: {
|
||||
backgroundColor: changeOpacity(theme.centerChannelColor, 0.2),
|
||||
flex: 1,
|
||||
height: 1,
|
||||
marginLeft: 10,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
|
@ -8,7 +8,7 @@ import LinearGradient from 'react-native-linear-gradient';
|
|||
import Preferences from 'mattermost-redux/constants/preferences';
|
||||
|
||||
import TouchableWithFeedback from 'app/components/touchable_with_feedback';
|
||||
import ShowMoreButton from './show_more_button';
|
||||
import ShowMoreButton from './index';
|
||||
|
||||
describe('ShowMoreButton', () => {
|
||||
const baseProps = {
|
||||
|
|
|
|||
|
|
@ -214,7 +214,7 @@ export default class DrawerLayout extends Component {
|
|||
// 1 - main | tablet
|
||||
// 2 - overlay
|
||||
// 3 - android main drawer | settings drawer
|
||||
const drawerZIndex = drawerPosition === 'left' && Platform.OS === 'ios' ? 0 : 3
|
||||
const drawerZIndex = drawerPosition === 'left' ? 0 : 3
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
|
|
@ -262,7 +262,7 @@ export default class DrawerLayout extends Component {
|
|||
}
|
||||
|
||||
const mainStyles = [styles.main]
|
||||
if (drawerPosition === 'left' && Platform.OS === 'ios') {
|
||||
if (drawerPosition === 'left') {
|
||||
/* Drawer styles */
|
||||
let outputRange;
|
||||
|
||||
|
|
@ -410,10 +410,7 @@ export default class DrawerLayout extends Component {
|
|||
return true;
|
||||
}
|
||||
} else {
|
||||
const filter = Platform.select({
|
||||
ios: moveX > 0 && dx > 35,
|
||||
android: moveX <= 35 && dx > 0
|
||||
});
|
||||
const filter = moveX > 0 && dx > 35;
|
||||
if (filter) {
|
||||
this._isClosing = false;
|
||||
return true;
|
||||
|
|
@ -447,7 +444,7 @@ export default class DrawerLayout extends Component {
|
|||
};
|
||||
|
||||
_panResponderMove = (e: EventType, { moveX, dx }: PanResponderEventType) => {
|
||||
const useDx = Platform.OS === 'ios' && this.getDrawerPosition() === 'left' && !this._isClosing;
|
||||
const useDx = this.getDrawerPosition() === 'left' && !this._isClosing;
|
||||
let openValue = this._getOpenValueForX(useDx ? dx : moveX);
|
||||
|
||||
if (this._isClosing) {
|
||||
|
|
|
|||
|
|
@ -33,7 +33,6 @@ export default class ChannelsList extends PureComponent {
|
|||
onSelectChannel: PropTypes.func.isRequired,
|
||||
onShowTeams: PropTypes.func.isRequired,
|
||||
theme: PropTypes.object.isRequired,
|
||||
drawerOpened: PropTypes.bool,
|
||||
isLandscape: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -11,9 +11,7 @@ import Swiper from 'app/components/swiper';
|
|||
export default class DrawerSwiper extends Component {
|
||||
static propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
drawerOpened: PropTypes.bool,
|
||||
drawerWidth: PropTypes.number.isRequired,
|
||||
hasSafeAreaInsets: PropTypes.bool,
|
||||
onPageSelected: PropTypes.func,
|
||||
showTeams: PropTypes.bool.isRequired,
|
||||
theme: PropTypes.object.isRequired,
|
||||
|
|
@ -30,8 +28,7 @@ export default class DrawerSwiper extends Component {
|
|||
const {drawerWidth, showTeams, theme} = this.props;
|
||||
return nextProps.drawerWidth !== drawerWidth ||
|
||||
nextProps.showTeams !== showTeams ||
|
||||
nextProps.theme !== theme ||
|
||||
nextProps.drawerOpened !== this.props.drawerOpened;
|
||||
nextProps.theme !== theme;
|
||||
}
|
||||
|
||||
runOnLayout = (shouldRun = true) => {
|
||||
|
|
|
|||
|
|
@ -8,12 +8,13 @@ import {joinChannel} from 'mattermost-redux/actions/channels';
|
|||
import {getTeams} from 'mattermost-redux/actions/teams';
|
||||
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {getCurrentTeamId, getMyTeamsCount} from 'mattermost-redux/selectors/entities/teams';
|
||||
import {getCurrentUser} from 'mattermost-redux/selectors/entities/users';
|
||||
|
||||
import {setChannelDisplayName, setChannelLoading} from 'app/actions/views/channel';
|
||||
import {setChannelDisplayName, handleSelectChannel} from 'app/actions/views/channel';
|
||||
import {makeDirectChannel} from 'app/actions/views/more_dms';
|
||||
import telemetry from 'app/telemetry';
|
||||
|
||||
import MainSidebar from './main_sidebar.js';
|
||||
import MainSidebar from './main_sidebar';
|
||||
|
||||
export function logChannelSwitch(channelId, currentChannelId) {
|
||||
return (dispatch, getState) => {
|
||||
|
|
@ -34,11 +35,12 @@ export function logChannelSwitch(channelId, currentChannelId) {
|
|||
}
|
||||
|
||||
function mapStateToProps(state) {
|
||||
const {currentUserId} = state.entities.users;
|
||||
const currentUser = getCurrentUser(state);
|
||||
|
||||
return {
|
||||
locale: currentUser?.locale,
|
||||
currentTeamId: getCurrentTeamId(state),
|
||||
currentUserId,
|
||||
currentUserId: currentUser?.id,
|
||||
teamsCount: getMyTeamsCount(state),
|
||||
theme: getTheme(state),
|
||||
};
|
||||
|
|
@ -52,7 +54,7 @@ function mapDispatchToProps(dispatch) {
|
|||
logChannelSwitch,
|
||||
makeDirectChannel,
|
||||
setChannelDisplayName,
|
||||
setChannelLoading,
|
||||
handleSelectChannel,
|
||||
}, dispatch),
|
||||
};
|
||||
}
|
||||
|
|
|
|||
93
app/components/sidebars/main/main_sidebar.android.js
Normal file
93
app/components/sidebars/main/main_sidebar.android.js
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {Dimensions, Keyboard} from 'react-native';
|
||||
import {IntlProvider} from 'react-intl';
|
||||
|
||||
import EventEmitter from 'mattermost-redux/utils/event_emitter';
|
||||
|
||||
import {closeMainSideMenu, enableMainSideMenu} from 'app/actions/navigation';
|
||||
import {NavigationTypes} from 'app/constants';
|
||||
import {getTranslations} from 'app/i18n';
|
||||
|
||||
import MainSidebarBase from './main_sidebar_base';
|
||||
|
||||
export default class MainSidebarAndroid extends MainSidebarBase {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
deviceWidth: Dimensions.get('window').width,
|
||||
opened: false,
|
||||
searching: false,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
super.componentDidMount();
|
||||
|
||||
EventEmitter.on(NavigationTypes.MAIN_SIDEBAR_DID_CLOSE, this.handleSidebarDidClose);
|
||||
EventEmitter.on(NavigationTypes.MAIN_SIDEBAR_DID_OPEN, this.handleSidebarDidOpen);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
super.componentWillUnmount();
|
||||
|
||||
EventEmitter.off(NavigationTypes.MAIN_SIDEBAR_DID_CLOSE, this.handleSidebarDidClose);
|
||||
EventEmitter.off(NavigationTypes.MAIN_SIDEBAR_DID_OPEN, this.handleSidebarDidOpen);
|
||||
}
|
||||
|
||||
closeMainSidebar = () => {
|
||||
closeMainSideMenu();
|
||||
};
|
||||
|
||||
handleDimensions = ({window}) => {
|
||||
if (this.mounted) {
|
||||
this.setState({deviceWidth: window.width});
|
||||
}
|
||||
};
|
||||
|
||||
handleSidebarDidClose = () => {
|
||||
this.setState({searching: false, opened: false}, () => {
|
||||
enableMainSideMenu(true, false);
|
||||
this.resetDrawer(true);
|
||||
Keyboard.dismiss();
|
||||
});
|
||||
};
|
||||
|
||||
handleSidebarDidOpen = () => {
|
||||
this.setState({opened: true});
|
||||
}
|
||||
|
||||
onPageSelected = (index) => {
|
||||
this.swiperIndex = index;
|
||||
|
||||
if (this.state.opened) {
|
||||
enableMainSideMenu(index !== 0, true);
|
||||
}
|
||||
};
|
||||
|
||||
setProviderRef = (ref) => {
|
||||
this.providerRef = ref;
|
||||
}
|
||||
|
||||
render() {
|
||||
const locale = this.props.locale;
|
||||
|
||||
if (!locale) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<IntlProvider
|
||||
key={locale}
|
||||
locale={locale}
|
||||
ref={this.setProviderRef}
|
||||
messages={getTranslations(locale)}
|
||||
>
|
||||
{this.renderNavigationView(this.state.deviceWidth)}
|
||||
</IntlProvider>
|
||||
);
|
||||
}
|
||||
}
|
||||
135
app/components/sidebars/main/main_sidebar.ios.js
Normal file
135
app/components/sidebars/main/main_sidebar.ios.js
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {Dimensions, Keyboard} from 'react-native';
|
||||
import {intlShape} from 'react-intl';
|
||||
import AsyncStorage from '@react-native-community/async-storage';
|
||||
|
||||
import EventEmitter from 'mattermost-redux/utils/event_emitter';
|
||||
|
||||
import DrawerLayout, {DRAWER_INITIAL_OFFSET, TABLET_WIDTH} from 'app/components/sidebars/drawer_layout';
|
||||
import {DeviceTypes, NavigationTypes} from 'app/constants';
|
||||
import mattermostManaged from 'app/mattermost_managed';
|
||||
|
||||
import MainSidebarBase from './main_sidebar_base';
|
||||
|
||||
export default class MainSidebarIOS extends MainSidebarBase {
|
||||
static contextTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.drawerRef = React.createRef();
|
||||
this.state = {
|
||||
deviceWidth: Dimensions.get('window').width,
|
||||
openDrawerOffset: DRAWER_INITIAL_OFFSET,
|
||||
drawerOpened: false,
|
||||
searching: false,
|
||||
isSplitView: false,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
super.componentDidMount();
|
||||
|
||||
this.handleDimensions({window: Dimensions.get('window')});
|
||||
this.handlePermanentSidebar();
|
||||
EventEmitter.on(DeviceTypes.PERMANENT_SIDEBAR_SETTINGS, this.handlePermanentSidebar);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
super.componentWillUnmount();
|
||||
|
||||
EventEmitter.off(DeviceTypes.PERMANENT_SIDEBAR_SETTINGS, this.handlePermanentSidebar);
|
||||
}
|
||||
|
||||
closeMainSidebar = () => {
|
||||
if (this.state.drawerOpened && this.drawerRef?.current) {
|
||||
this.drawerRef.current.closeDrawer();
|
||||
} else if (this.drawerSwiper && DeviceTypes.IS_TABLET) {
|
||||
this.resetDrawer(true);
|
||||
}
|
||||
};
|
||||
|
||||
handleDimensions = ({window}) => {
|
||||
if (this.mounted) {
|
||||
if (DeviceTypes.IS_TABLET) {
|
||||
mattermostManaged.isRunningInSplitView().then((result) => {
|
||||
const isSplitView = Boolean(result.isSplitView);
|
||||
this.setState({isSplitView});
|
||||
});
|
||||
}
|
||||
|
||||
if (this.state.openDrawerOffset !== 0) {
|
||||
let openDrawerOffset = DRAWER_INITIAL_OFFSET;
|
||||
if ((window.width > window.height) || DeviceTypes.IS_TABLET) {
|
||||
openDrawerOffset = window.width * 0.5;
|
||||
}
|
||||
|
||||
this.setState({openDrawerOffset, deviceWidth: window.width});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
handlePermanentSidebar = async () => {
|
||||
if (DeviceTypes.IS_TABLET && this.mounted) {
|
||||
const enabled = await AsyncStorage.getItem(DeviceTypes.PERMANENT_SIDEBAR_SETTINGS);
|
||||
this.setState({permanentSidebar: enabled === 'true'});
|
||||
}
|
||||
};
|
||||
|
||||
handleDrawerClose = () => {
|
||||
this.setState({
|
||||
drawerOpened: false,
|
||||
searching: false,
|
||||
});
|
||||
this.resetDrawer();
|
||||
Keyboard.dismiss();
|
||||
};
|
||||
|
||||
handleDrawerOpen = () => {
|
||||
this.setState({
|
||||
drawerOpened: true,
|
||||
});
|
||||
};
|
||||
|
||||
open = () => {
|
||||
EventEmitter.emit(NavigationTypes.BLUR_POST_TEXTBOX);
|
||||
|
||||
if (this.drawerRef?.current) {
|
||||
this.drawerRef.current.openDrawer();
|
||||
}
|
||||
};
|
||||
|
||||
onPageSelected = (index) => {
|
||||
this.swiperIndex = index;
|
||||
|
||||
if (this.drawerRef?.current) {
|
||||
this.drawerRef.current.canClose = this.swiperIndex !== 0;
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const {children} = this.props;
|
||||
const {deviceWidth, openDrawerOffset} = this.state;
|
||||
const isTablet = DeviceTypes.IS_TABLET && !this.state.isSplitView && this.state.permanentSidebar;
|
||||
const drawerWidth = DeviceTypes.IS_TABLET ? TABLET_WIDTH : (deviceWidth - openDrawerOffset);
|
||||
|
||||
return (
|
||||
<DrawerLayout
|
||||
ref={this.drawerRef}
|
||||
renderNavigationView={this.renderNavigationView}
|
||||
onDrawerClose={this.handleDrawerClose}
|
||||
onDrawerOpen={this.handleDrawerOpen}
|
||||
drawerWidth={drawerWidth}
|
||||
useNativeAnimations={true}
|
||||
isTablet={isTablet}
|
||||
>
|
||||
{children}
|
||||
</DrawerLayout>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,424 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {Component} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
BackHandler,
|
||||
Dimensions,
|
||||
Keyboard,
|
||||
StyleSheet,
|
||||
View,
|
||||
} from 'react-native';
|
||||
import {intlShape} from 'react-intl';
|
||||
import AsyncStorage from '@react-native-community/async-storage';
|
||||
|
||||
import {General, WebsocketEvents} from 'mattermost-redux/constants';
|
||||
import EventEmitter from 'mattermost-redux/utils/event_emitter';
|
||||
|
||||
import SafeAreaView from 'app/components/safe_area_view';
|
||||
import DrawerLayout, {DRAWER_INITIAL_OFFSET, TABLET_WIDTH} from 'app/components/sidebars/drawer_layout';
|
||||
import {DeviceTypes} from 'app/constants';
|
||||
import mattermostManaged from 'app/mattermost_managed';
|
||||
import tracker from 'app/utils/time_tracker';
|
||||
import {t} from 'app/utils/i18n';
|
||||
|
||||
import ChannelsList from './channels_list';
|
||||
import DrawerSwiper from './drawer_swiper';
|
||||
import TeamsList from './teams_list';
|
||||
|
||||
import telemetry from 'app/telemetry';
|
||||
|
||||
export default class ChannelSidebar extends Component {
|
||||
static propTypes = {
|
||||
actions: PropTypes.shape({
|
||||
getTeams: PropTypes.func.isRequired,
|
||||
logChannelSwitch: PropTypes.func.isRequired,
|
||||
makeDirectChannel: PropTypes.func.isRequired,
|
||||
setChannelDisplayName: PropTypes.func.isRequired,
|
||||
setChannelLoading: PropTypes.func.isRequired,
|
||||
joinChannel: PropTypes.func.isRequired,
|
||||
}).isRequired,
|
||||
blurPostTextBox: PropTypes.func.isRequired,
|
||||
children: PropTypes.node,
|
||||
currentTeamId: PropTypes.string.isRequired,
|
||||
currentUserId: PropTypes.string.isRequired,
|
||||
teamsCount: PropTypes.number.isRequired,
|
||||
theme: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
static contextTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.swiperIndex = 1;
|
||||
this.drawerRef = React.createRef();
|
||||
this.channelListRef = React.createRef();
|
||||
this.state = {
|
||||
deviceWidth: Dimensions.get('window').width,
|
||||
show: false,
|
||||
openDrawerOffset: DRAWER_INITIAL_OFFSET,
|
||||
drawerOpened: false,
|
||||
searching: false,
|
||||
isSplitView: false,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.mounted = true;
|
||||
this.props.actions.getTeams();
|
||||
this.handleDimensions({window: Dimensions.get('window')});
|
||||
this.handlePermanentSidebar();
|
||||
EventEmitter.on('close_channel_drawer', this.closeChannelDrawer);
|
||||
EventEmitter.on('renderDrawer', this.handleShowDrawerContent);
|
||||
EventEmitter.on(WebsocketEvents.CHANNEL_UPDATED, this.handleUpdateTitle);
|
||||
EventEmitter.on(DeviceTypes.PERMANENT_SIDEBAR_SETTINGS, this.handlePermanentSidebar);
|
||||
BackHandler.addEventListener('hardwareBackPress', this.handleAndroidBack);
|
||||
Dimensions.addEventListener('change', this.handleDimensions);
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps, nextState) {
|
||||
const {currentTeamId, teamsCount, theme} = this.props;
|
||||
const {deviceWidth, openDrawerOffset, isSplitView, permanentSidebar, show, searching} = this.state;
|
||||
|
||||
if (nextState.openDrawerOffset !== openDrawerOffset || nextState.show !== show || nextState.searching !== searching || nextState.deviceWidth !== deviceWidth) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return nextProps.currentTeamId !== currentTeamId ||
|
||||
nextProps.teamsCount !== teamsCount ||
|
||||
nextProps.theme !== theme ||
|
||||
nextState.isSplitView !== isSplitView ||
|
||||
nextState.permanentSidebar !== permanentSidebar;
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.mounted = false;
|
||||
EventEmitter.off('close_channel_drawer', this.closeChannelDrawer);
|
||||
EventEmitter.off(WebsocketEvents.CHANNEL_UPDATED, this.handleUpdateTitle);
|
||||
EventEmitter.off('renderDrawer', this.handleShowDrawerContent);
|
||||
EventEmitter.off(DeviceTypes.PERMANENT_SIDEBAR_SETTINGS, this.handlePermanentSidebar);
|
||||
BackHandler.removeEventListener('hardwareBackPress', this.handleAndroidBack);
|
||||
Dimensions.removeEventListener('change', this.handleDimensions);
|
||||
}
|
||||
|
||||
handleAndroidBack = () => {
|
||||
if (this.state.drawerOpened && this.drawerRef?.current) {
|
||||
this.drawerRef.current.closeDrawer();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
handleDimensions = ({window}) => {
|
||||
if (this.mounted) {
|
||||
if (DeviceTypes.IS_TABLET) {
|
||||
mattermostManaged.isRunningInSplitView().then((result) => {
|
||||
const isSplitView = Boolean(result.isSplitView);
|
||||
this.setState({isSplitView});
|
||||
});
|
||||
}
|
||||
|
||||
if (this.state.openDrawerOffset !== 0) {
|
||||
let openDrawerOffset = DRAWER_INITIAL_OFFSET;
|
||||
if ((window.width > window.height) || DeviceTypes.IS_TABLET) {
|
||||
openDrawerOffset = window.width * 0.5;
|
||||
}
|
||||
|
||||
this.setState({openDrawerOffset, deviceWidth: window.width});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
handlePermanentSidebar = async () => {
|
||||
if (DeviceTypes.IS_TABLET && this.mounted) {
|
||||
const enabled = await AsyncStorage.getItem(DeviceTypes.PERMANENT_SIDEBAR_SETTINGS);
|
||||
this.setState({permanentSidebar: enabled === 'true'});
|
||||
}
|
||||
};
|
||||
|
||||
handleShowDrawerContent = () => {
|
||||
requestAnimationFrame(() => this.setState({show: true}));
|
||||
};
|
||||
|
||||
closeChannelDrawer = () => {
|
||||
if (this.state.drawerOpened && this.drawerRef?.current) {
|
||||
this.drawerRef.current.closeDrawer();
|
||||
} else if (this.drawerSwiper && DeviceTypes.IS_TABLET) {
|
||||
this.resetDrawer(true);
|
||||
}
|
||||
};
|
||||
|
||||
drawerSwiperRef = (ref) => {
|
||||
this.drawerSwiper = ref;
|
||||
};
|
||||
|
||||
handleDrawerClose = () => {
|
||||
this.setState({
|
||||
drawerOpened: false,
|
||||
searching: false,
|
||||
});
|
||||
this.resetDrawer();
|
||||
Keyboard.dismiss();
|
||||
};
|
||||
|
||||
handleDrawerOpen = () => {
|
||||
this.setState({
|
||||
drawerOpened: true,
|
||||
});
|
||||
};
|
||||
|
||||
handleUpdateTitle = (channel) => {
|
||||
let channelName = '';
|
||||
if (channel.display_name) {
|
||||
channelName = channel.display_name;
|
||||
}
|
||||
this.props.actions.setChannelDisplayName(channelName);
|
||||
};
|
||||
|
||||
openChannelSidebar = () => {
|
||||
this.props.blurPostTextBox();
|
||||
|
||||
if (this.drawerRef?.current) {
|
||||
this.drawerRef.current.openDrawer();
|
||||
}
|
||||
};
|
||||
|
||||
selectChannel = (channel, currentChannelId, closeDrawer = true) => {
|
||||
const {logChannelSwitch, setChannelLoading} = this.props.actions;
|
||||
|
||||
logChannelSwitch(channel.id, currentChannelId);
|
||||
|
||||
tracker.channelSwitch = Date.now();
|
||||
|
||||
if (closeDrawer) {
|
||||
telemetry.start(['channel:close_drawer']);
|
||||
this.closeChannelDrawer();
|
||||
setChannelLoading(channel.id !== currentChannelId);
|
||||
}
|
||||
|
||||
if (!channel) {
|
||||
const utils = require('app/utils/general');
|
||||
const {intl} = this.context;
|
||||
|
||||
const unableToJoinMessage = {
|
||||
id: t('mobile.open_unknown_channel.error'),
|
||||
defaultMessage: "We couldn't join the channel. Please reset the cache and try again.",
|
||||
};
|
||||
const erroMessage = {};
|
||||
|
||||
utils.alertErrorWithFallback(intl, erroMessage, unableToJoinMessage);
|
||||
setChannelLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
EventEmitter.emit('switch_channel', channel, currentChannelId);
|
||||
};
|
||||
|
||||
joinChannel = (channel, currentChannelId) => {
|
||||
const {intl} = this.context;
|
||||
const {
|
||||
actions,
|
||||
currentTeamId,
|
||||
currentUserId,
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
joinChannel,
|
||||
makeDirectChannel,
|
||||
setChannelLoading,
|
||||
} = actions;
|
||||
|
||||
this.closeChannelDrawer();
|
||||
setChannelLoading(channel.id !== currentChannelId);
|
||||
|
||||
setTimeout(async () => {
|
||||
const displayValue = {displayName: channel.display_name};
|
||||
const utils = require('app/utils/general');
|
||||
|
||||
let result;
|
||||
if (channel.type === General.DM_CHANNEL) {
|
||||
result = await makeDirectChannel(channel.id, false);
|
||||
|
||||
if (result.error) {
|
||||
const dmFailedMessage = {
|
||||
id: t('mobile.open_dm.error'),
|
||||
defaultMessage: "We couldn't open a direct message with {displayName}. Please check your connection and try again.",
|
||||
};
|
||||
utils.alertErrorWithFallback(intl, result.error, dmFailedMessage, displayValue);
|
||||
}
|
||||
} else {
|
||||
result = await joinChannel(currentUserId, currentTeamId, channel.id);
|
||||
|
||||
if (result.error || !result.data || !result.data.channel) {
|
||||
const joinFailedMessage = {
|
||||
id: t('mobile.join_channel.error'),
|
||||
defaultMessage: "We couldn't join the channel {displayName}. Please check your connection and try again.",
|
||||
};
|
||||
utils.alertErrorWithFallback(intl, result.error, joinFailedMessage, displayValue);
|
||||
}
|
||||
}
|
||||
|
||||
if (result.error || (!result.data && !result.data.channel)) {
|
||||
setChannelLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
this.selectChannel(result.data.channel || result.data, currentChannelId, false);
|
||||
});
|
||||
}, 200);
|
||||
};
|
||||
|
||||
onPageSelected = (index) => {
|
||||
this.swiperIndex = index;
|
||||
|
||||
if (this.drawerRef?.current) {
|
||||
this.drawerRef.current.canClose = this.swiperIndex !== 0;
|
||||
}
|
||||
};
|
||||
|
||||
onSearchEnds = () => {
|
||||
this.setState({searching: false});
|
||||
};
|
||||
|
||||
onSearchStart = () => {
|
||||
if (this.drawerRef?.current) {
|
||||
this.drawerRef.current.canClose = false;
|
||||
}
|
||||
this.setState({searching: true});
|
||||
};
|
||||
|
||||
showTeams = () => {
|
||||
if (this.drawerSwiper && this.props.teamsCount > 1) {
|
||||
this.drawerSwiper.showTeamsPage();
|
||||
}
|
||||
};
|
||||
|
||||
resetDrawer = () => {
|
||||
if (this.drawerSwiper) {
|
||||
this.drawerSwiper.resetPage();
|
||||
}
|
||||
|
||||
if (this.drawerRef?.current) {
|
||||
this.drawerRef.current.canClose = true;
|
||||
}
|
||||
|
||||
if (this.channelListRef?.current) {
|
||||
this.channelListRef.current.cancelSearch();
|
||||
}
|
||||
};
|
||||
|
||||
renderNavigationView = (drawerWidth) => {
|
||||
const {
|
||||
teamsCount,
|
||||
theme,
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
show,
|
||||
openDrawerOffset,
|
||||
searching,
|
||||
} = this.state;
|
||||
|
||||
if (!show) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const hasSafeAreaInsets = DeviceTypes.IS_IPHONE_WITH_INSETS || mattermostManaged.hasSafeAreaInsets;
|
||||
const multipleTeams = teamsCount > 1;
|
||||
const showTeams = !searching && multipleTeams;
|
||||
if (this.drawerSwiper) {
|
||||
if (multipleTeams) {
|
||||
this.drawerSwiper.runOnLayout();
|
||||
this.drawerSwiper.scrollToInitial();
|
||||
} else if (!openDrawerOffset) {
|
||||
this.drawerSwiper.scrollToStart();
|
||||
}
|
||||
}
|
||||
|
||||
const lists = [];
|
||||
if (multipleTeams) {
|
||||
const teamsList = (
|
||||
<View
|
||||
key='teamsList'
|
||||
style={style.swiperContent}
|
||||
>
|
||||
<TeamsList
|
||||
closeChannelDrawer={this.closeChannelDrawer}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
lists.push(teamsList);
|
||||
}
|
||||
|
||||
lists.push(
|
||||
<View
|
||||
key='channelsList'
|
||||
style={style.swiperContent}
|
||||
>
|
||||
<ChannelsList
|
||||
ref={this.channelListRef}
|
||||
onSelectChannel={this.selectChannel}
|
||||
onJoinChannel={this.joinChannel}
|
||||
onShowTeams={this.showTeams}
|
||||
onSearchStart={this.onSearchStart}
|
||||
onSearchEnds={this.onSearchEnds}
|
||||
theme={theme}
|
||||
drawerOpened={this.state.drawerOpened}
|
||||
/>
|
||||
</View>,
|
||||
);
|
||||
|
||||
return (
|
||||
<SafeAreaView
|
||||
excludeFooter={true}
|
||||
navBarBackgroundColor={theme.sidebarBg}
|
||||
backgroundColor={theme.sidebarHeaderBg}
|
||||
footerColor={theme.sidebarBg}
|
||||
>
|
||||
<DrawerSwiper
|
||||
ref={this.drawerSwiperRef}
|
||||
onPageSelected={this.onPageSelected}
|
||||
showTeams={showTeams}
|
||||
drawerOpened={this.state.drawerOpened}
|
||||
drawerWidth={drawerWidth}
|
||||
hasSafeAreaInsets={hasSafeAreaInsets}
|
||||
>
|
||||
{lists}
|
||||
</DrawerSwiper>
|
||||
</SafeAreaView>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const {children} = this.props;
|
||||
const {deviceWidth, openDrawerOffset} = this.state;
|
||||
const isTablet = DeviceTypes.IS_TABLET && !this.state.isSplitView && this.state.permanentSidebar;
|
||||
const drawerWidth = DeviceTypes.IS_TABLET ? TABLET_WIDTH : (deviceWidth - openDrawerOffset);
|
||||
|
||||
return (
|
||||
<DrawerLayout
|
||||
ref={this.drawerRef}
|
||||
renderNavigationView={this.renderNavigationView}
|
||||
onDrawerClose={this.handleDrawerClose}
|
||||
onDrawerOpen={this.handleDrawerOpen}
|
||||
drawerWidth={drawerWidth}
|
||||
useNativeAnimations={true}
|
||||
isTablet={isTablet}
|
||||
>
|
||||
{children}
|
||||
</DrawerLayout>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const style = StyleSheet.create({
|
||||
swiperContent: {
|
||||
flex: 1,
|
||||
},
|
||||
});
|
||||
|
|
@ -2,14 +2,13 @@
|
|||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {Platform} from 'react-native';
|
||||
import {shallow} from 'enzyme';
|
||||
|
||||
import Preferences from 'mattermost-redux/constants/preferences';
|
||||
|
||||
import {DeviceTypes} from 'app/constants';
|
||||
|
||||
import MainSidebar from './main_sidebar';
|
||||
import MainSidebar from './main_sidebar.ios';
|
||||
|
||||
jest.mock('react-intl');
|
||||
|
||||
|
|
@ -85,8 +84,6 @@ describe('MainSidebar', () => {
|
|||
});
|
||||
|
||||
test('should render main sidebar below PostList for iOS', () => {
|
||||
Platform.OS = 'ios';
|
||||
|
||||
const wrapper = shallow(
|
||||
<MainSidebar {...baseProps}/>,
|
||||
);
|
||||
|
|
@ -94,15 +91,4 @@ describe('MainSidebar', () => {
|
|||
const drawerStyle = drawer.props().style.reduce((acc, obj) => ({...acc, ...obj}));
|
||||
expect(drawerStyle).toHaveProperty('zIndex', 0);
|
||||
});
|
||||
|
||||
test('should render main sidebar above PostList for android', () => {
|
||||
Platform.OS = 'android';
|
||||
|
||||
const wrapper = shallow(
|
||||
<MainSidebar {...baseProps}/>,
|
||||
);
|
||||
const drawer = wrapper.dive().childAt(1);
|
||||
const drawerStyle = drawer.props().style.reduce((acc, obj) => ({...acc, ...obj}));
|
||||
expect(drawerStyle).toHaveProperty('zIndex', 3);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
301
app/components/sidebars/main/main_sidebar_base.js
Normal file
301
app/components/sidebars/main/main_sidebar_base.js
Normal file
|
|
@ -0,0 +1,301 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {Component} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
Dimensions,
|
||||
Platform,
|
||||
StyleSheet,
|
||||
View,
|
||||
} from 'react-native';
|
||||
|
||||
import {General, WebsocketEvents} from 'mattermost-redux/constants';
|
||||
import EventEmitter from 'mattermost-redux/utils/event_emitter';
|
||||
|
||||
import SafeAreaView from 'app/components/safe_area_view';
|
||||
import {NavigationTypes} from 'app/constants';
|
||||
import tracker from 'app/utils/time_tracker';
|
||||
import {t} from 'app/utils/i18n';
|
||||
|
||||
import ChannelsList from './channels_list';
|
||||
import DrawerSwiper from './drawer_swiper';
|
||||
import TeamsList from './teams_list';
|
||||
|
||||
import telemetry from 'app/telemetry';
|
||||
|
||||
export default class MainSidebarBase extends Component {
|
||||
static propTypes = {
|
||||
actions: PropTypes.shape({
|
||||
getTeams: PropTypes.func.isRequired,
|
||||
handleSelectChannel: PropTypes.func,
|
||||
joinChannel: PropTypes.func.isRequired,
|
||||
logChannelSwitch: PropTypes.func.isRequired,
|
||||
makeDirectChannel: PropTypes.func.isRequired,
|
||||
setChannelDisplayName: PropTypes.func.isRequired,
|
||||
}).isRequired,
|
||||
children: PropTypes.node,
|
||||
currentTeamId: PropTypes.string.isRequired,
|
||||
currentUserId: PropTypes.string,
|
||||
locale: PropTypes.string,
|
||||
teamsCount: PropTypes.number.isRequired,
|
||||
theme: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.swiperIndex = 1;
|
||||
this.channelListRef = React.createRef();
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.mounted = true;
|
||||
this.props.actions.getTeams();
|
||||
EventEmitter.on(NavigationTypes.CLOSE_MAIN_SIDEBAR, this.closeMainSidebar);
|
||||
EventEmitter.on(WebsocketEvents.CHANNEL_UPDATED, this.handleUpdateTitle);
|
||||
Dimensions.addEventListener('change', this.handleDimensions);
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps, nextState) {
|
||||
const {currentTeamId, teamsCount, theme} = this.props;
|
||||
const {deviceWidth, openDrawerOffset, isSplitView, permanentSidebar, searching} = this.state;
|
||||
|
||||
if (nextState.openDrawerOffset !== openDrawerOffset && Platform.OS === 'ios') {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (nextState.searching !== searching || nextState.deviceWidth !== deviceWidth) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const condition = nextProps.currentTeamId !== currentTeamId ||
|
||||
nextProps.teamsCount !== teamsCount ||
|
||||
nextProps.theme !== theme;
|
||||
|
||||
if (Platform.OS === 'ios') {
|
||||
return condition ||
|
||||
nextState.isSplitView !== isSplitView ||
|
||||
nextState.permanentSidebar !== permanentSidebar;
|
||||
}
|
||||
|
||||
return condition;
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.mounted = false;
|
||||
EventEmitter.off(NavigationTypes.CLOSE_MAIN_SIDEBAR, this.closeMainSidebar);
|
||||
EventEmitter.off(WebsocketEvents.CHANNEL_UPDATED, this.handleUpdateTitle);
|
||||
Dimensions.removeEventListener('change', this.handleDimensions);
|
||||
}
|
||||
|
||||
drawerSwiperRef = (ref) => {
|
||||
this.drawerSwiper = ref;
|
||||
};
|
||||
|
||||
getIntl = () => {
|
||||
const {intl} = this.providerRef ? this.providerRef.getChildContext() : this.context;
|
||||
return intl;
|
||||
};
|
||||
|
||||
handleUpdateTitle = (channel) => {
|
||||
let channelName = '';
|
||||
if (channel.display_name) {
|
||||
channelName = channel.display_name;
|
||||
}
|
||||
this.props.actions.setChannelDisplayName(channelName);
|
||||
};
|
||||
|
||||
joinChannel = async (channel, currentChannelId) => {
|
||||
const intl = this.getIntl();
|
||||
const {
|
||||
actions,
|
||||
currentTeamId,
|
||||
currentUserId,
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
joinChannel,
|
||||
makeDirectChannel,
|
||||
} = actions;
|
||||
|
||||
this.closeMainSidebar();
|
||||
|
||||
const displayValue = {displayName: channel.display_name};
|
||||
const utils = require('app/utils/general');
|
||||
|
||||
let result;
|
||||
if (channel.type === General.DM_CHANNEL) {
|
||||
result = await makeDirectChannel(channel.id, false);
|
||||
|
||||
if (result.error) {
|
||||
const dmFailedMessage = {
|
||||
id: t('mobile.open_dm.error'),
|
||||
defaultMessage: "We couldn't open a direct message with {displayName}. Please check your connection and try again.",
|
||||
};
|
||||
utils.alertErrorWithFallback(intl, result.error, dmFailedMessage, displayValue);
|
||||
}
|
||||
} else {
|
||||
result = await joinChannel(currentUserId, currentTeamId, channel.id);
|
||||
|
||||
if (result.error || !result.data || !result.data.channel) {
|
||||
const joinFailedMessage = {
|
||||
id: t('mobile.join_channel.error'),
|
||||
defaultMessage: "We couldn't join the channel {displayName}. Please check your connection and try again.",
|
||||
};
|
||||
utils.alertErrorWithFallback(intl, result.error, joinFailedMessage, displayValue);
|
||||
}
|
||||
}
|
||||
|
||||
if (result.error || (!result.data && !result.data.channel)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.selectChannel(result.data.channel || result.data, currentChannelId, false);
|
||||
};
|
||||
|
||||
onSearchEnds = () => {
|
||||
this.setState({searching: false});
|
||||
};
|
||||
|
||||
onSearchStart = () => {
|
||||
if (this.drawerRef?.current) {
|
||||
this.drawerRef.current.canClose = false;
|
||||
}
|
||||
this.setState({searching: true});
|
||||
};
|
||||
|
||||
showTeams = () => {
|
||||
if (this.drawerSwiper && this.props.teamsCount > 1) {
|
||||
this.drawerSwiper.showTeamsPage();
|
||||
}
|
||||
};
|
||||
|
||||
resetDrawer = () => {
|
||||
if (this.drawerSwiper) {
|
||||
this.drawerSwiper.resetPage();
|
||||
}
|
||||
|
||||
if (this.drawerRef?.current) {
|
||||
this.drawerRef.current.canClose = true;
|
||||
}
|
||||
|
||||
if (this.channelListRef?.current) {
|
||||
this.channelListRef.current.cancelSearch();
|
||||
}
|
||||
};
|
||||
|
||||
renderNavigationView = (drawerWidth) => {
|
||||
const {
|
||||
teamsCount,
|
||||
theme,
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
openDrawerOffset,
|
||||
searching,
|
||||
} = this.state;
|
||||
|
||||
const offset = Platform.select({android: 60, ios: 0});
|
||||
const multipleTeams = teamsCount > 1;
|
||||
const showTeams = !searching && multipleTeams;
|
||||
if (this.drawerSwiper) {
|
||||
if (multipleTeams) {
|
||||
this.drawerSwiper.runOnLayout();
|
||||
this.drawerSwiper.scrollToInitial();
|
||||
} else if (!openDrawerOffset) {
|
||||
this.drawerSwiper.scrollToStart();
|
||||
}
|
||||
}
|
||||
|
||||
const lists = [];
|
||||
if (multipleTeams) {
|
||||
const teamsList = (
|
||||
<View
|
||||
key='teamsList'
|
||||
style={style.swiperContent}
|
||||
>
|
||||
<TeamsList
|
||||
closeMainSidebar={this.closeMainSidebar}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
lists.push(teamsList);
|
||||
}
|
||||
|
||||
lists.push(
|
||||
<View
|
||||
key='channelsList'
|
||||
style={style.swiperContent}
|
||||
>
|
||||
<ChannelsList
|
||||
ref={this.channelListRef}
|
||||
onSelectChannel={this.selectChannel}
|
||||
onJoinChannel={this.joinChannel}
|
||||
onShowTeams={this.showTeams}
|
||||
onSearchStart={this.onSearchStart}
|
||||
onSearchEnds={this.onSearchEnds}
|
||||
theme={theme}
|
||||
/>
|
||||
</View>,
|
||||
);
|
||||
|
||||
return (
|
||||
<SafeAreaView
|
||||
excludeFooter={true}
|
||||
navBarBackgroundColor={theme.sidebarBg}
|
||||
backgroundColor={theme.sidebarHeaderBg}
|
||||
footerColor={theme.sidebarBg}
|
||||
>
|
||||
<DrawerSwiper
|
||||
ref={this.drawerSwiperRef}
|
||||
onPageSelected={this.onPageSelected}
|
||||
showTeams={showTeams}
|
||||
drawerOpened={this.state.drawerOpened}
|
||||
drawerWidth={drawerWidth - offset}
|
||||
>
|
||||
{lists}
|
||||
</DrawerSwiper>
|
||||
</SafeAreaView>
|
||||
);
|
||||
};
|
||||
|
||||
selectChannel = (channel, currentChannelId, closeDrawer = true) => {
|
||||
const {logChannelSwitch, handleSelectChannel} = this.props.actions;
|
||||
|
||||
logChannelSwitch(channel.id, currentChannelId);
|
||||
|
||||
tracker.channelSwitch = Date.now();
|
||||
|
||||
if (closeDrawer) {
|
||||
telemetry.start(['channel:close_drawer']);
|
||||
this.closeMainSidebar();
|
||||
}
|
||||
|
||||
if (!channel) {
|
||||
const utils = require('app/utils/general');
|
||||
const intl = this.getIntl();
|
||||
|
||||
const unableToJoinMessage = {
|
||||
id: t('mobile.open_unknown_channel.error'),
|
||||
defaultMessage: "We couldn't join the channel. Please reset the cache and try again.",
|
||||
};
|
||||
const erroMessage = {};
|
||||
|
||||
utils.alertErrorWithFallback(intl, erroMessage, unableToJoinMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
handleSelectChannel(channel.id);
|
||||
};
|
||||
|
||||
render() {
|
||||
return; // eslint-disable-line no-useless-return
|
||||
}
|
||||
}
|
||||
|
||||
const style = StyleSheet.create({
|
||||
swiperContent: {
|
||||
flex: 1,
|
||||
},
|
||||
});
|
||||
|
|
@ -38,7 +38,7 @@ export default class TeamsList extends PureComponent {
|
|||
actions: PropTypes.shape({
|
||||
handleTeamChange: PropTypes.func.isRequired,
|
||||
}).isRequired,
|
||||
closeChannelDrawer: PropTypes.func.isRequired,
|
||||
closeMainSidebar: PropTypes.func.isRequired,
|
||||
currentTeamId: PropTypes.string.isRequired,
|
||||
hasOtherJoinableTeams: PropTypes.bool,
|
||||
teamIds: PropTypes.array.isRequired,
|
||||
|
|
@ -66,7 +66,7 @@ export default class TeamsList extends PureComponent {
|
|||
}
|
||||
|
||||
selectTeam = (teamId) => {
|
||||
const {actions, closeChannelDrawer, currentTeamId} = this.props;
|
||||
const {actions, closeMainSidebar, currentTeamId} = this.props;
|
||||
|
||||
if (teamId !== currentTeamId) {
|
||||
telemetry.reset();
|
||||
|
|
@ -80,7 +80,7 @@ export default class TeamsList extends PureComponent {
|
|||
actions.handleTeamChange(teamId);
|
||||
}
|
||||
|
||||
closeChannelDrawer();
|
||||
closeMainSidebar();
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -4,10 +4,12 @@
|
|||
import {bindActionCreators} from 'redux';
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {logout, setStatus} from 'mattermost-redux/actions/users';
|
||||
import {setStatus} from 'mattermost-redux/actions/users';
|
||||
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {getCurrentUser, getStatusForUserId} from 'mattermost-redux/selectors/entities/users';
|
||||
|
||||
import {logout} from 'app/actions/views/user';
|
||||
|
||||
import SettingsSidebar from './settings_sidebar';
|
||||
|
||||
function mapStateToProps(state) {
|
||||
|
|
@ -16,6 +18,7 @@ function mapStateToProps(state) {
|
|||
|
||||
return {
|
||||
currentUser,
|
||||
locale: currentUser?.locale,
|
||||
status,
|
||||
theme: getTheme(state),
|
||||
};
|
||||
|
|
|
|||
112
app/components/sidebars/settings/settings_sidebar.android.js
Normal file
112
app/components/sidebars/settings/settings_sidebar.android.js
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {IntlProvider} from 'react-intl';
|
||||
import {View} from 'react-native';
|
||||
|
||||
import {closeSettingsSideMenu} from 'app/actions/navigation';
|
||||
import {getTranslations} from 'app/i18n';
|
||||
import {preventDoubleTap} from 'app/utils/tap';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
|
||||
import SettingsSidebarBase from './settings_sidebar_base';
|
||||
|
||||
export default class SettingsDrawerAndroid extends SettingsSidebarBase {
|
||||
confirmReset = (status) => {
|
||||
const {intl} = this.providerRef.getChildContext();
|
||||
this.confirmResetBase(status, intl);
|
||||
};
|
||||
|
||||
closeSettingsSidebar = () => {
|
||||
closeSettingsSideMenu();
|
||||
};
|
||||
|
||||
goToEditProfile = preventDoubleTap(() => {
|
||||
const {intl} = this.providerRef.getChildContext();
|
||||
this.goToEditProfileScreen(intl);
|
||||
});
|
||||
|
||||
goToFlagged = preventDoubleTap(() => {
|
||||
const {intl} = this.providerRef.getChildContext();
|
||||
this.goToFlaggedScreen(intl);
|
||||
});
|
||||
|
||||
goToMentions = preventDoubleTap(() => {
|
||||
const {intl} = this.providerRef.getChildContext();
|
||||
this.goToMentionsScreen(intl);
|
||||
});
|
||||
|
||||
goToUserProfile = preventDoubleTap(() => {
|
||||
const {intl} = this.providerRef.getChildContext();
|
||||
this.goToUserProfileScreen(intl);
|
||||
});
|
||||
|
||||
goToSettings = preventDoubleTap(() => {
|
||||
const {intl} = this.providerRef.getChildContext();
|
||||
this.goToSettingsScreeen(intl);
|
||||
});
|
||||
|
||||
renderNavigationView = () => {
|
||||
const {theme} = this.props;
|
||||
const style = getStyleSheet(theme);
|
||||
|
||||
return (
|
||||
<View style={style.sidebar}>
|
||||
{this.renderOptions(style)}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
setProviderRef = (ref) => {
|
||||
this.providerRef = ref;
|
||||
}
|
||||
|
||||
render() {
|
||||
const locale = this.props.locale;
|
||||
|
||||
if (!locale) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<IntlProvider
|
||||
key={locale}
|
||||
locale={locale}
|
||||
ref={this.setProviderRef}
|
||||
messages={getTranslations(locale)}
|
||||
>
|
||||
{this.renderNavigationView()}
|
||||
</IntlProvider>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
return {
|
||||
sidebar: {
|
||||
flex: 1,
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
},
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: changeOpacity(theme.centerChannelColor, 0.03),
|
||||
},
|
||||
wrapper: {
|
||||
paddingTop: 0,
|
||||
},
|
||||
block: {
|
||||
borderBottomColor: changeOpacity(theme.centerChannelColor, 0.1),
|
||||
borderBottomWidth: 1,
|
||||
borderTopColor: changeOpacity(theme.centerChannelColor, 0.1),
|
||||
borderTopWidth: 1,
|
||||
},
|
||||
divider: {
|
||||
backgroundColor: changeOpacity(theme.centerChannelColor, 0.1),
|
||||
height: 1,
|
||||
},
|
||||
separator: {
|
||||
marginTop: 35,
|
||||
},
|
||||
};
|
||||
});
|
||||
178
app/components/sidebars/settings/settings_sidebar.ios.js
Normal file
178
app/components/sidebars/settings/settings_sidebar.ios.js
Normal file
|
|
@ -0,0 +1,178 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {intlShape} from 'react-intl';
|
||||
import {Dimensions, Keyboard, View} from 'react-native';
|
||||
|
||||
import EventEmitter from 'mattermost-redux/utils/event_emitter';
|
||||
|
||||
import SafeAreaView from 'app/components/safe_area_view';
|
||||
import DrawerLayout, {DRAWER_INITIAL_OFFSET, TABLET_WIDTH} from 'app/components/sidebars/drawer_layout';
|
||||
import {DeviceTypes, NavigationTypes} from 'app/constants';
|
||||
import {preventDoubleTap} from 'app/utils/tap';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
|
||||
import SettingsSidebarBase from './settings_sidebar_base';
|
||||
|
||||
export default class SettingsDrawer extends SettingsSidebarBase {
|
||||
static contextTypes = {
|
||||
intl: intlShape,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
deviceWidth: Dimensions.get('window').width,
|
||||
openDrawerOffset: DRAWER_INITIAL_OFFSET,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
super.componentDidMount();
|
||||
|
||||
this.handleDimensions({window: Dimensions.get('window')});
|
||||
Dimensions.addEventListener('change', this.handleDimensions);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
super.componentWillUnmount();
|
||||
|
||||
Dimensions.removeEventListener('change', this.handleDimensions);
|
||||
}
|
||||
|
||||
setDrawerRef = (ref) => {
|
||||
this.drawerRef = ref;
|
||||
}
|
||||
|
||||
confirmReset = (status) => {
|
||||
const {intl} = this.context;
|
||||
this.confirmResetBase(status, intl);
|
||||
};
|
||||
|
||||
closeSettingsSidebar = () => {
|
||||
if (this.drawerRef && this.drawerOpened) {
|
||||
this.drawerRef.closeDrawer();
|
||||
}
|
||||
};
|
||||
|
||||
open = () => {
|
||||
EventEmitter.emit(NavigationTypes.BLUR_POST_TEXTBOX);
|
||||
|
||||
if (this.drawerRef && !this.drawerOpened) {
|
||||
this.drawerRef.openDrawer();
|
||||
}
|
||||
};
|
||||
|
||||
handleDrawerClose = () => {
|
||||
this.drawerOpened = false;
|
||||
Keyboard.dismiss();
|
||||
};
|
||||
|
||||
handleDrawerOpen = () => {
|
||||
this.drawerOpened = true;
|
||||
Keyboard.dismiss();
|
||||
};
|
||||
|
||||
handleDimensions = ({window}) => {
|
||||
if (this.mounted) {
|
||||
if (this.state.openDrawerOffset !== 0) {
|
||||
let openDrawerOffset = DRAWER_INITIAL_OFFSET;
|
||||
if ((window.width > window.height) || DeviceTypes.IS_TABLET) {
|
||||
openDrawerOffset = window.width * 0.5;
|
||||
}
|
||||
|
||||
this.setState({openDrawerOffset, deviceWidth: window.width});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
goToEditProfile = preventDoubleTap(() => {
|
||||
const {intl} = this.context;
|
||||
this.goToEditProfileScreen(intl);
|
||||
});
|
||||
|
||||
goToFlagged = preventDoubleTap(() => {
|
||||
const {intl} = this.context;
|
||||
this.goToFlaggedScreen(intl);
|
||||
});
|
||||
|
||||
goToMentions = preventDoubleTap(() => {
|
||||
const {intl} = this.context;
|
||||
this.goToMentionsScreen(intl);
|
||||
});
|
||||
|
||||
goToUserProfile = preventDoubleTap(() => {
|
||||
const {intl} = this.context;
|
||||
this.goToUserProfileScreen(intl);
|
||||
});
|
||||
|
||||
goToSettings = preventDoubleTap(() => {
|
||||
const {intl} = this.context;
|
||||
this.goToSettingsScreeen(intl);
|
||||
});
|
||||
|
||||
renderNavigationView = () => {
|
||||
const {theme} = this.props;
|
||||
const style = getStyleSheet(theme);
|
||||
|
||||
return (
|
||||
<SafeAreaView
|
||||
backgroundColor={theme.centerChannelBg}
|
||||
navBarBackgroundColor={theme.centerChannelBg}
|
||||
footerColor={theme.centerChannelBg}
|
||||
footerComponent={<View style={style.container}/>}
|
||||
headerComponent={<View style={style.container}/>}
|
||||
theme={theme}
|
||||
>
|
||||
{this.renderOptions(style)}
|
||||
</SafeAreaView>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const {children} = this.props;
|
||||
const {deviceWidth, openDrawerOffset} = this.state;
|
||||
const drawerWidth = DeviceTypes.IS_TABLET ? TABLET_WIDTH : (deviceWidth - openDrawerOffset);
|
||||
|
||||
return (
|
||||
<DrawerLayout
|
||||
ref={this.setDrawerRef}
|
||||
renderNavigationView={this.renderNavigationView}
|
||||
onDrawerClose={this.handleDrawerClose}
|
||||
onDrawerOpen={this.handleDrawerOpen}
|
||||
drawerPosition='right'
|
||||
drawerWidth={drawerWidth}
|
||||
useNativeAnimations={true}
|
||||
>
|
||||
{children}
|
||||
</DrawerLayout>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
return {
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: changeOpacity(theme.centerChannelColor, 0.03),
|
||||
},
|
||||
wrapper: {
|
||||
paddingTop: 0,
|
||||
},
|
||||
block: {
|
||||
borderBottomColor: changeOpacity(theme.centerChannelColor, 0.1),
|
||||
borderBottomWidth: 1,
|
||||
borderTopColor: changeOpacity(theme.centerChannelColor, 0.1),
|
||||
borderTopWidth: 1,
|
||||
},
|
||||
divider: {
|
||||
backgroundColor: changeOpacity(theme.centerChannelColor, 0.1),
|
||||
height: 1,
|
||||
},
|
||||
separator: {
|
||||
marginTop: 35,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
|
@ -1,417 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {intlShape} from 'react-intl';
|
||||
import {
|
||||
BackHandler,
|
||||
Dimensions,
|
||||
InteractionManager,
|
||||
Keyboard,
|
||||
ScrollView,
|
||||
View,
|
||||
} from 'react-native';
|
||||
import MaterialIcon from 'react-native-vector-icons/MaterialIcons';
|
||||
|
||||
import {General} from 'mattermost-redux/constants';
|
||||
import EventEmitter from 'mattermost-redux/utils/event_emitter';
|
||||
|
||||
import SafeAreaView from 'app/components/safe_area_view';
|
||||
import DrawerLayout, {DRAWER_INITIAL_OFFSET, TABLET_WIDTH} from 'app/components/sidebars/drawer_layout';
|
||||
import UserStatus from 'app/components/user_status';
|
||||
import {DeviceTypes, NavigationTypes} from 'app/constants';
|
||||
import {confirmOutOfOfficeDisabled} from 'app/utils/status';
|
||||
import {preventDoubleTap} from 'app/utils/tap';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
import {t} from 'app/utils/i18n';
|
||||
import {showModal, showModalOverCurrentContext, dismissModal} from 'app/actions/navigation';
|
||||
|
||||
import DrawerItem from './drawer_item';
|
||||
import UserInfo from './user_info';
|
||||
import StatusLabel from './status_label';
|
||||
|
||||
export default class SettingsDrawer extends PureComponent {
|
||||
static propTypes = {
|
||||
actions: PropTypes.shape({
|
||||
logout: PropTypes.func.isRequired,
|
||||
setStatus: PropTypes.func.isRequired,
|
||||
}).isRequired,
|
||||
blurPostTextBox: PropTypes.func.isRequired,
|
||||
children: PropTypes.node,
|
||||
currentUser: PropTypes.object.isRequired,
|
||||
status: PropTypes.string,
|
||||
theme: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
currentUser: {},
|
||||
status: 'offline',
|
||||
};
|
||||
|
||||
static contextTypes = {
|
||||
intl: intlShape,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
MaterialIcon.getImageSource('close', 20, props.theme.sidebarHeaderTextColor).then((source) => {
|
||||
this.closeButton = source;
|
||||
});
|
||||
|
||||
this.state = {
|
||||
deviceWidth: Dimensions.get('window').width,
|
||||
openDrawerOffset: DRAWER_INITIAL_OFFSET,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.mounted = true;
|
||||
this.handleDimensions({window: Dimensions.get('window')});
|
||||
EventEmitter.on('close_settings_sidebar', this.closeSettingsSidebar);
|
||||
BackHandler.addEventListener('hardwareBackPress', this.handleAndroidBack);
|
||||
Dimensions.addEventListener('change', this.handleDimensions);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.mounted = false;
|
||||
EventEmitter.off('close_settings_sidebar', this.closeSettingsSidebar);
|
||||
BackHandler.removeEventListener('hardwareBackPress', this.handleAndroidBack);
|
||||
Dimensions.removeEventListener('change', this.handleDimensions);
|
||||
}
|
||||
|
||||
setDrawerRef = (ref) => {
|
||||
this.drawerRef = ref;
|
||||
}
|
||||
|
||||
confirmReset = (status) => {
|
||||
const {intl} = this.context;
|
||||
confirmOutOfOfficeDisabled(intl, status, this.updateStatus);
|
||||
};
|
||||
|
||||
closeSettingsSidebar = () => {
|
||||
if (this.drawerRef && this.drawerOpened) {
|
||||
this.drawerRef.closeDrawer();
|
||||
}
|
||||
};
|
||||
|
||||
openSettingsSidebar = () => {
|
||||
this.props.blurPostTextBox();
|
||||
|
||||
if (this.drawerRef && !this.drawerOpened) {
|
||||
this.drawerRef.openDrawer();
|
||||
}
|
||||
};
|
||||
|
||||
handleAndroidBack = () => {
|
||||
if (this.statusModal) {
|
||||
this.statusModal = false;
|
||||
return false;
|
||||
} else if (this.drawerRef && this.drawerOpened) {
|
||||
this.drawerRef.closeDrawer();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
handleDrawerClose = () => {
|
||||
this.drawerOpened = false;
|
||||
Keyboard.dismiss();
|
||||
};
|
||||
|
||||
handleDrawerOpen = () => {
|
||||
this.drawerOpened = true;
|
||||
Keyboard.dismiss();
|
||||
};
|
||||
|
||||
handleDimensions = ({window}) => {
|
||||
if (this.mounted) {
|
||||
if (this.state.openDrawerOffset !== 0) {
|
||||
let openDrawerOffset = DRAWER_INITIAL_OFFSET;
|
||||
if ((window.width > window.height) || DeviceTypes.IS_TABLET) {
|
||||
openDrawerOffset = window.width * 0.5;
|
||||
}
|
||||
|
||||
this.setState({openDrawerOffset, deviceWidth: window.width});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
handleSetStatus = preventDoubleTap(() => {
|
||||
const items = [{
|
||||
action: () => this.setStatus(General.ONLINE),
|
||||
text: {
|
||||
id: t('mobile.set_status.online'),
|
||||
defaultMessage: 'Online',
|
||||
},
|
||||
}, {
|
||||
action: () => this.setStatus(General.AWAY),
|
||||
text: {
|
||||
id: t('mobile.set_status.away'),
|
||||
defaultMessage: 'Away',
|
||||
},
|
||||
}, {
|
||||
action: () => this.setStatus(General.DND),
|
||||
text: {
|
||||
id: t('mobile.set_status.dnd'),
|
||||
defaultMessage: 'Do Not Disturb',
|
||||
},
|
||||
}, {
|
||||
action: () => this.setStatus(General.OFFLINE),
|
||||
text: {
|
||||
id: t('mobile.set_status.offline'),
|
||||
defaultMessage: 'Offline',
|
||||
},
|
||||
}];
|
||||
|
||||
this.statusModal = true;
|
||||
showModalOverCurrentContext('OptionsModal', {items});
|
||||
});
|
||||
|
||||
goToEditProfile = preventDoubleTap(() => {
|
||||
const {currentUser} = this.props;
|
||||
const {formatMessage} = this.context.intl;
|
||||
const commandType = 'ShowModal';
|
||||
|
||||
this.openModal(
|
||||
'EditProfile',
|
||||
formatMessage({id: 'mobile.routes.edit_profile', defaultMessage: 'Edit Profile'}),
|
||||
{currentUser, commandType},
|
||||
);
|
||||
});
|
||||
|
||||
goToFlagged = preventDoubleTap(() => {
|
||||
const {formatMessage} = this.context.intl;
|
||||
|
||||
this.openModal(
|
||||
'FlaggedPosts',
|
||||
formatMessage({id: 'search_header.title3', defaultMessage: 'Flagged Posts'}),
|
||||
);
|
||||
});
|
||||
|
||||
goToMentions = preventDoubleTap(() => {
|
||||
const {intl} = this.context;
|
||||
|
||||
this.openModal(
|
||||
'RecentMentions',
|
||||
intl.formatMessage({id: 'search_header.title2', defaultMessage: 'Recent Mentions'}),
|
||||
);
|
||||
});
|
||||
|
||||
goToUserProfile = preventDoubleTap(() => {
|
||||
const userId = this.props.currentUser.id;
|
||||
const {formatMessage} = this.context.intl;
|
||||
|
||||
this.openModal(
|
||||
'UserProfile',
|
||||
formatMessage({id: 'mobile.routes.user_profile', defaultMessage: 'Profile'}),
|
||||
{userId, fromSettings: true},
|
||||
);
|
||||
});
|
||||
|
||||
goToSettings = preventDoubleTap(() => {
|
||||
const {intl} = this.context;
|
||||
|
||||
this.openModal(
|
||||
'Settings',
|
||||
intl.formatMessage({id: 'mobile.routes.settings', defaultMessage: 'Settings'}),
|
||||
);
|
||||
});
|
||||
|
||||
logout = preventDoubleTap(() => {
|
||||
const {logout} = this.props.actions;
|
||||
this.closeSettingsSidebar();
|
||||
logout();
|
||||
});
|
||||
|
||||
openModal = (screen, title, passProps) => {
|
||||
this.closeSettingsSidebar();
|
||||
|
||||
const options = {
|
||||
topBar: {
|
||||
leftButtons: [{
|
||||
id: 'close-settings',
|
||||
icon: this.closeButton,
|
||||
}],
|
||||
},
|
||||
};
|
||||
|
||||
InteractionManager.runAfterInteractions(() => {
|
||||
showModal(screen, title, passProps, options);
|
||||
});
|
||||
};
|
||||
|
||||
updateStatus = (status) => {
|
||||
const {currentUser: {id: currentUserId}} = this.props;
|
||||
this.props.actions.setStatus({
|
||||
user_id: currentUserId,
|
||||
status,
|
||||
});
|
||||
};
|
||||
|
||||
setStatus = (status) => {
|
||||
const {status: currentUserStatus} = this.props;
|
||||
|
||||
if (currentUserStatus === General.OUT_OF_OFFICE) {
|
||||
dismissModal();
|
||||
this.closeSettingsSidebar();
|
||||
this.confirmReset(status);
|
||||
return;
|
||||
}
|
||||
this.updateStatus(status);
|
||||
EventEmitter.emit(NavigationTypes.NAVIGATION_CLOSE_MODAL);
|
||||
};
|
||||
|
||||
renderUserStatusIcon = (userId) => {
|
||||
return (
|
||||
<UserStatus
|
||||
size={18}
|
||||
userId={userId}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
renderUserStatusLabel = (userId) => {
|
||||
return (
|
||||
<StatusLabel userId={userId}/>
|
||||
);
|
||||
};
|
||||
|
||||
renderNavigationView = () => {
|
||||
const {currentUser, theme} = this.props;
|
||||
const style = getStyleSheet(theme);
|
||||
|
||||
return (
|
||||
<SafeAreaView
|
||||
backgroundColor={theme.centerChannelBg}
|
||||
navBarBackgroundColor={theme.centerChannelBg}
|
||||
footerColor={theme.centerChannelBg}
|
||||
footerComponent={<View style={style.container}/>}
|
||||
headerComponent={<View style={style.container}/>}
|
||||
theme={theme}
|
||||
>
|
||||
<View style={style.container}>
|
||||
<ScrollView
|
||||
alwaysBounceVertical={false}
|
||||
contentContainerStyle={style.wrapper}
|
||||
>
|
||||
<UserInfo
|
||||
onPress={this.goToUserProfile}
|
||||
user={currentUser}
|
||||
/>
|
||||
<View style={style.block}>
|
||||
<DrawerItem
|
||||
labelComponent={this.renderUserStatusLabel(currentUser.id)}
|
||||
leftComponent={this.renderUserStatusIcon(currentUser.id)}
|
||||
separator={false}
|
||||
onPress={this.handleSetStatus}
|
||||
theme={theme}
|
||||
/>
|
||||
</View>
|
||||
<View style={style.separator}/>
|
||||
<View style={style.block}>
|
||||
<DrawerItem
|
||||
defaultMessage='Recent Mentions'
|
||||
i18nId='search_header.title2'
|
||||
iconName='ios-at'
|
||||
iconType='ion'
|
||||
onPress={this.goToMentions}
|
||||
separator={true}
|
||||
theme={theme}
|
||||
/>
|
||||
<DrawerItem
|
||||
defaultMessage='Flagged Posts'
|
||||
i18nId='search_header.title3'
|
||||
iconName='ios-flag'
|
||||
iconType='ion'
|
||||
onPress={this.goToFlagged}
|
||||
separator={false}
|
||||
theme={theme}
|
||||
/>
|
||||
</View>
|
||||
<View style={style.separator}/>
|
||||
<View style={style.block}>
|
||||
<DrawerItem
|
||||
defaultMessage='Edit Profile'
|
||||
i18nId='mobile.routes.edit_profile'
|
||||
iconName='ios-person'
|
||||
iconType='ion'
|
||||
onPress={this.goToEditProfile}
|
||||
separator={true}
|
||||
theme={theme}
|
||||
/>
|
||||
<DrawerItem
|
||||
defaultMessage='Settings'
|
||||
i18nId='mobile.routes.settings'
|
||||
iconName='ios-options'
|
||||
iconType='ion'
|
||||
onPress={this.goToSettings}
|
||||
separator={false}
|
||||
theme={theme}
|
||||
/>
|
||||
</View>
|
||||
<View style={style.separator}/>
|
||||
<View style={style.block}>
|
||||
<DrawerItem
|
||||
centered={true}
|
||||
defaultMessage='Logout'
|
||||
i18nId='sidebar_right_menu.logout'
|
||||
isDestructor={true}
|
||||
onPress={this.logout}
|
||||
separator={false}
|
||||
theme={theme}
|
||||
/>
|
||||
</View>
|
||||
</ScrollView>
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const {children} = this.props;
|
||||
const {deviceWidth, openDrawerOffset} = this.state;
|
||||
const drawerWidth = DeviceTypes.IS_TABLET ? TABLET_WIDTH : (deviceWidth - openDrawerOffset);
|
||||
|
||||
return (
|
||||
<DrawerLayout
|
||||
ref={this.setDrawerRef}
|
||||
renderNavigationView={this.renderNavigationView}
|
||||
onDrawerClose={this.handleDrawerClose}
|
||||
onDrawerOpen={this.handleDrawerOpen}
|
||||
drawerPosition='right'
|
||||
drawerWidth={drawerWidth}
|
||||
useNativeAnimations={true}
|
||||
>
|
||||
{children}
|
||||
</DrawerLayout>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
return {
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: changeOpacity(theme.centerChannelColor, 0.03),
|
||||
},
|
||||
wrapper: {
|
||||
paddingTop: 0,
|
||||
},
|
||||
block: {
|
||||
borderBottomColor: changeOpacity(theme.centerChannelColor, 0.1),
|
||||
borderBottomWidth: 1,
|
||||
borderTopColor: changeOpacity(theme.centerChannelColor, 0.1),
|
||||
borderTopWidth: 1,
|
||||
},
|
||||
divider: {
|
||||
backgroundColor: changeOpacity(theme.centerChannelColor, 0.1),
|
||||
height: 1,
|
||||
},
|
||||
separator: {
|
||||
marginTop: 35,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
|
@ -1,57 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {shallow} from 'enzyme';
|
||||
|
||||
import Preferences from 'mattermost-redux/constants/preferences';
|
||||
|
||||
import SettingsSidebar from './settings_sidebar';
|
||||
|
||||
jest.mock('react-intl');
|
||||
|
||||
jest.mock('react-native-vector-icons/MaterialIcons', () => ({
|
||||
getImageSource: jest.fn().mockResolvedValue(null),
|
||||
}));
|
||||
|
||||
describe('SettingsSidebar', () => {
|
||||
const baseProps = {
|
||||
actions: {
|
||||
logout: jest.fn(),
|
||||
setStatus: jest.fn(),
|
||||
},
|
||||
blurPostTextBox: jest.fn(),
|
||||
currentUser: {},
|
||||
status: 'online',
|
||||
theme: Preferences.THEMES.default,
|
||||
};
|
||||
|
||||
test('Android back button should dismiss status modal first', () => {
|
||||
const wrapper = shallow(<SettingsSidebar {...baseProps}/>);
|
||||
|
||||
const instance = wrapper.instance();
|
||||
|
||||
// Mocking the reference the DrawerLayout as the component is not really being mounted
|
||||
instance.drawerRef = {
|
||||
closeDrawer: jest.fn(() => {
|
||||
instance.drawerOpened = false;
|
||||
}),
|
||||
};
|
||||
|
||||
// simulate that the drawer is opened
|
||||
instance.drawerOpened = true;
|
||||
|
||||
// simulate that the status modal is opened
|
||||
instance.statusModal = true;
|
||||
|
||||
// this simulates the first tap on the back button that closes the status modal
|
||||
let backButtonResult = instance.handleAndroidBack();
|
||||
expect(backButtonResult).toBe(false);
|
||||
expect(instance.drawerOpened).toBe(true);
|
||||
|
||||
// this simulates a second tap on the back button that closes the drawer
|
||||
backButtonResult = instance.handleAndroidBack();
|
||||
expect(backButtonResult).toBe(true);
|
||||
expect(instance.drawerOpened).toBe(false);
|
||||
});
|
||||
});
|
||||
278
app/components/sidebars/settings/settings_sidebar_base.js
Normal file
278
app/components/sidebars/settings/settings_sidebar_base.js
Normal file
|
|
@ -0,0 +1,278 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {InteractionManager, ScrollView, View} from 'react-native';
|
||||
import MaterialIcon from 'react-native-vector-icons/MaterialIcons';
|
||||
|
||||
import {General} from 'mattermost-redux/constants';
|
||||
import EventEmitter from 'mattermost-redux/utils/event_emitter';
|
||||
|
||||
import {showModal, showModalOverCurrentContext, dismissModal} from 'app/actions/navigation';
|
||||
import UserStatus from 'app/components/user_status';
|
||||
import {NavigationTypes} from 'app/constants';
|
||||
import {confirmOutOfOfficeDisabled} from 'app/utils/status';
|
||||
import {preventDoubleTap} from 'app/utils/tap';
|
||||
import {t} from 'app/utils/i18n';
|
||||
|
||||
import DrawerItem from './drawer_item';
|
||||
import UserInfo from './user_info';
|
||||
import StatusLabel from './status_label';
|
||||
|
||||
export default class SettingsSidebarBase extends PureComponent {
|
||||
static propTypes = {
|
||||
actions: PropTypes.shape({
|
||||
logout: PropTypes.func.isRequired,
|
||||
setStatus: PropTypes.func.isRequired,
|
||||
}).isRequired,
|
||||
currentUser: PropTypes.object.isRequired,
|
||||
status: PropTypes.string,
|
||||
theme: PropTypes.object.isRequired,
|
||||
locale: PropTypes.string,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
currentUser: {},
|
||||
status: 'offline',
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
MaterialIcon.getImageSource('close', 20, props.theme.sidebarHeaderTextColor).then((source) => {
|
||||
this.closeButton = source;
|
||||
});
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.mounted = true;
|
||||
EventEmitter.on(NavigationTypes.CLOSE_SETTINGS_SIDEBAR, this.closeSettingsSidebar);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.mounted = false;
|
||||
EventEmitter.off(NavigationTypes.CLOSE_SETTINGS_SIDEBAR, this.closeSettingsSidebar);
|
||||
}
|
||||
|
||||
confirmResetBase = (status, intl) => {
|
||||
confirmOutOfOfficeDisabled(intl, status, this.updateStatus);
|
||||
};
|
||||
|
||||
handleSetStatus = preventDoubleTap(() => {
|
||||
const items = [{
|
||||
action: () => this.setStatus(General.ONLINE),
|
||||
text: {
|
||||
id: t('mobile.set_status.online'),
|
||||
defaultMessage: 'Online',
|
||||
},
|
||||
}, {
|
||||
action: () => this.setStatus(General.AWAY),
|
||||
text: {
|
||||
id: t('mobile.set_status.away'),
|
||||
defaultMessage: 'Away',
|
||||
},
|
||||
}, {
|
||||
action: () => this.setStatus(General.DND),
|
||||
text: {
|
||||
id: t('mobile.set_status.dnd'),
|
||||
defaultMessage: 'Do Not Disturb',
|
||||
},
|
||||
}, {
|
||||
action: () => this.setStatus(General.OFFLINE),
|
||||
text: {
|
||||
id: t('mobile.set_status.offline'),
|
||||
defaultMessage: 'Offline',
|
||||
},
|
||||
}];
|
||||
|
||||
this.statusModal = true;
|
||||
showModalOverCurrentContext('OptionsModal', {items});
|
||||
});
|
||||
|
||||
goToEditProfileScreen = (intl) => {
|
||||
const {currentUser} = this.props;
|
||||
const commandType = 'ShowModal';
|
||||
|
||||
this.openModal(
|
||||
'EditProfile',
|
||||
intl.formatMessage({id: 'mobile.routes.edit_profile', defaultMessage: 'Edit Profile'}),
|
||||
{currentUser, commandType},
|
||||
);
|
||||
};
|
||||
|
||||
goToFlaggedScreen = (intl) => {
|
||||
this.openModal(
|
||||
'FlaggedPosts',
|
||||
intl.formatMessage({id: 'search_header.title3', defaultMessage: 'Flagged Posts'}),
|
||||
);
|
||||
};
|
||||
|
||||
goToMentionsScreen = (intl) => {
|
||||
this.openModal(
|
||||
'RecentMentions',
|
||||
intl.formatMessage({id: 'search_header.title2', defaultMessage: 'Recent Mentions'}),
|
||||
);
|
||||
};
|
||||
|
||||
goToUserProfileScreen = (intl) => {
|
||||
const userId = this.props.currentUser.id;
|
||||
|
||||
this.openModal(
|
||||
'UserProfile',
|
||||
intl.formatMessage({id: 'mobile.routes.user_profile', defaultMessage: 'Profile'}),
|
||||
{userId, fromSettings: true},
|
||||
);
|
||||
};
|
||||
|
||||
goToSettingsScreeen = (intl) => {
|
||||
this.openModal(
|
||||
'Settings',
|
||||
intl.formatMessage({id: 'mobile.routes.settings', defaultMessage: 'Settings'}),
|
||||
);
|
||||
};
|
||||
|
||||
logout = preventDoubleTap(() => {
|
||||
const {logout} = this.props.actions;
|
||||
this.closeSettingsSidebar();
|
||||
logout();
|
||||
});
|
||||
|
||||
openModal = (screen, title, passProps) => {
|
||||
this.closeSettingsSidebar();
|
||||
|
||||
const options = {
|
||||
topBar: {
|
||||
leftButtons: [{
|
||||
id: 'close-settings',
|
||||
icon: this.closeButton,
|
||||
}],
|
||||
},
|
||||
};
|
||||
|
||||
InteractionManager.runAfterInteractions(() => {
|
||||
showModal(screen, title, passProps, options);
|
||||
});
|
||||
};
|
||||
|
||||
updateStatus = (status) => {
|
||||
const {currentUser: {id: currentUserId}} = this.props;
|
||||
this.props.actions.setStatus({
|
||||
user_id: currentUserId,
|
||||
status,
|
||||
});
|
||||
};
|
||||
|
||||
setStatus = (status) => {
|
||||
const {status: currentUserStatus} = this.props;
|
||||
|
||||
if (currentUserStatus === General.OUT_OF_OFFICE) {
|
||||
dismissModal();
|
||||
this.closeSettingsSidebar();
|
||||
this.confirmReset(status);
|
||||
return;
|
||||
}
|
||||
this.updateStatus(status);
|
||||
EventEmitter.emit(NavigationTypes.NAVIGATION_CLOSE_MODAL);
|
||||
};
|
||||
|
||||
renderUserStatusIcon = (userId) => {
|
||||
return (
|
||||
<UserStatus
|
||||
size={18}
|
||||
userId={userId}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
renderUserStatusLabel = (userId) => {
|
||||
return (
|
||||
<StatusLabel userId={userId}/>
|
||||
);
|
||||
};
|
||||
|
||||
renderOptions = (style) => {
|
||||
const {currentUser, theme} = this.props;
|
||||
|
||||
return (
|
||||
<View style={style.container}>
|
||||
<ScrollView
|
||||
alwaysBounceVertical={false}
|
||||
contentContainerStyle={style.wrapper}
|
||||
>
|
||||
<UserInfo
|
||||
onPress={this.goToUserProfile}
|
||||
user={currentUser}
|
||||
/>
|
||||
<View style={style.block}>
|
||||
<DrawerItem
|
||||
labelComponent={this.renderUserStatusLabel(currentUser.id)}
|
||||
leftComponent={this.renderUserStatusIcon(currentUser.id)}
|
||||
separator={false}
|
||||
onPress={this.handleSetStatus}
|
||||
theme={theme}
|
||||
/>
|
||||
</View>
|
||||
<View style={style.separator}/>
|
||||
<View style={style.block}>
|
||||
<DrawerItem
|
||||
defaultMessage='Recent Mentions'
|
||||
i18nId='search_header.title2'
|
||||
iconName='ios-at'
|
||||
iconType='ion'
|
||||
onPress={this.goToMentions}
|
||||
separator={true}
|
||||
theme={theme}
|
||||
/>
|
||||
<DrawerItem
|
||||
defaultMessage='Flagged Posts'
|
||||
i18nId='search_header.title3'
|
||||
iconName='ios-flag'
|
||||
iconType='ion'
|
||||
onPress={this.goToFlagged}
|
||||
separator={false}
|
||||
theme={theme}
|
||||
/>
|
||||
</View>
|
||||
<View style={style.separator}/>
|
||||
<View style={style.block}>
|
||||
<DrawerItem
|
||||
defaultMessage='Edit Profile'
|
||||
i18nId='mobile.routes.edit_profile'
|
||||
iconName='ios-person'
|
||||
iconType='ion'
|
||||
onPress={this.goToEditProfile}
|
||||
separator={true}
|
||||
theme={theme}
|
||||
/>
|
||||
<DrawerItem
|
||||
defaultMessage='Settings'
|
||||
i18nId='mobile.routes.settings'
|
||||
iconName='ios-options'
|
||||
iconType='ion'
|
||||
onPress={this.goToSettings}
|
||||
separator={false}
|
||||
theme={theme}
|
||||
/>
|
||||
</View>
|
||||
<View style={style.separator}/>
|
||||
<View style={style.block}>
|
||||
<DrawerItem
|
||||
centered={true}
|
||||
defaultMessage='Logout'
|
||||
i18nId='sidebar_right_menu.logout'
|
||||
isDestructor={true}
|
||||
onPress={this.logout}
|
||||
separator={false}
|
||||
theme={theme}
|
||||
/>
|
||||
</View>
|
||||
</ScrollView>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
return; // eslint-disable-line no-useless-return
|
||||
}
|
||||
}
|
||||
|
|
@ -10,6 +10,11 @@ const NavigationTypes = keyMirror({
|
|||
RESTART_APP: null,
|
||||
NAVIGATION_ERROR_TEAMS: null,
|
||||
NAVIGATION_SHOW_OVERLAY: null,
|
||||
CLOSE_MAIN_SIDEBAR: null,
|
||||
MAIN_SIDEBAR_DID_CLOSE: null,
|
||||
MAIN_SIDEBAR_DID_OPEN: null,
|
||||
CLOSE_SETTINGS_SIDEBAR: null,
|
||||
BLUR_POST_TEXTBOX: null,
|
||||
});
|
||||
|
||||
export default NavigationTypes;
|
||||
|
|
|
|||
|
|
@ -62,7 +62,6 @@ const ViewTypes = keyMirror({
|
|||
SET_CHANNEL_RETRY_FAILED: null,
|
||||
SET_CHANNEL_DISPLAY_NAME: null,
|
||||
|
||||
SET_LAST_CHANNEL_FOR_TEAM: null,
|
||||
REMOVE_LAST_CHANNEL_FOR_TEAM: null,
|
||||
|
||||
GITLAB: null,
|
||||
|
|
@ -70,14 +69,10 @@ const ViewTypes = keyMirror({
|
|||
SAML: null,
|
||||
|
||||
SET_INITIAL_POST_VISIBILITY: null,
|
||||
INCREASE_POST_VISIBILITY: null,
|
||||
RECEIVED_FOCUSED_POST: null,
|
||||
LOADING_POSTS: null,
|
||||
SET_LOAD_MORE_POSTS_VISIBLE: null,
|
||||
|
||||
SET_INITIAL_POST_COUNT: null,
|
||||
INCREASE_POST_COUNT: null,
|
||||
|
||||
RECEIVED_POSTS_FOR_CHANNEL_AT_TIME: null,
|
||||
|
||||
SET_LAST_UPGRADE_CHECK: null,
|
||||
|
|
@ -94,7 +89,6 @@ const ViewTypes = keyMirror({
|
|||
|
||||
SELECTED_ACTION_MENU: null,
|
||||
SUBMIT_ATTACHMENT_MENU_ACTION: null,
|
||||
SELECT_CHANNEL_WITH_MEMBER: null,
|
||||
|
||||
PORTRAIT: null,
|
||||
LANDSCAPE: null,
|
||||
|
|
|
|||
|
|
@ -5,10 +5,13 @@ import {Linking} from 'react-native';
|
|||
import {Navigation} from 'react-native-navigation';
|
||||
import {Provider} from 'react-redux';
|
||||
|
||||
import {loadMe} from 'mattermost-redux/actions/users';
|
||||
import EventEmitter from 'mattermost-redux/utils/event_emitter';
|
||||
|
||||
import {loadMe} from 'app/actions/views/user';
|
||||
|
||||
import {resetToChannel, resetToSelectServer} from 'app/actions/navigation';
|
||||
import {setDeepLinkURL, loadConfigAndLicense} from 'app/actions/views/root';
|
||||
import {setDeepLinkURL} from 'app/actions/views/root';
|
||||
import {NavigationTypes} from 'app/constants';
|
||||
import {getAppCredentials} from 'app/init/credentials';
|
||||
import emmProvider from 'app/init/emm_provider';
|
||||
import 'app/init/device';
|
||||
|
|
@ -49,7 +52,6 @@ const launchApp = (credentials) => {
|
|||
|
||||
if (credentials) {
|
||||
waitForHydration(store, async () => {
|
||||
await store.dispatch(loadConfigAndLicense());
|
||||
store.dispatch(loadMe());
|
||||
resetToChannel({skipMetrics: true});
|
||||
});
|
||||
|
|
@ -86,9 +88,23 @@ Navigation.events().registerAppLaunchedListener(() => {
|
|||
// Keep track of the latest componentId to appear
|
||||
Navigation.events().registerComponentDidAppearListener(({componentId}) => {
|
||||
EphemeralStore.addNavigationComponentId(componentId);
|
||||
|
||||
switch (componentId) {
|
||||
case 'MainSidebar':
|
||||
EventEmitter.emit(NavigationTypes.MAIN_SIDEBAR_DID_OPEN, this.handleSidebarDidOpen);
|
||||
EventEmitter.emit(Navigation.BLUR_POST_TEXTBOX);
|
||||
break;
|
||||
case 'SettingsSidebar':
|
||||
EventEmitter.emit(NavigationTypes.BLUR_POST_TEXTBOX);
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
Navigation.events().registerComponentDidDisappearListener(({componentId}) => {
|
||||
EphemeralStore.removeNavigationComponentId(componentId);
|
||||
|
||||
if (componentId === 'MainSidebar') {
|
||||
EventEmitter.emit(NavigationTypes.MAIN_SIDEBAR_DID_CLOSE);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -15,6 +15,8 @@ function displayName(state = '', action) {
|
|||
switch (action.type) {
|
||||
case ViewTypes.SET_CHANNEL_DISPLAY_NAME:
|
||||
return action.displayName || '';
|
||||
case ChannelTypes.SELECT_CHANNEL:
|
||||
return '';
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
|
@ -257,60 +259,6 @@ function retryFailed(state = false, action) {
|
|||
}
|
||||
}
|
||||
|
||||
function postVisibility(state = {}, action) {
|
||||
switch (action.type) {
|
||||
case ViewTypes.SET_INITIAL_POST_VISIBILITY: {
|
||||
const nextState = {...state};
|
||||
nextState[action.data] = ViewTypes.POST_VISIBILITY_CHUNK_SIZE;
|
||||
return nextState;
|
||||
}
|
||||
case ViewTypes.INCREASE_POST_VISIBILITY: {
|
||||
const nextState = {...state};
|
||||
if (nextState[action.data]) {
|
||||
nextState[action.data] += action.amount;
|
||||
} else {
|
||||
nextState[action.data] = action.amount;
|
||||
}
|
||||
return nextState;
|
||||
}
|
||||
case ViewTypes.RECEIVED_FOCUSED_POST: {
|
||||
const nextState = {...state};
|
||||
nextState[action.channelId] = ViewTypes.POST_VISIBILITY_CHUNK_SIZE;
|
||||
return nextState;
|
||||
}
|
||||
case PostTypes.RECEIVED_NEW_POST: {
|
||||
if (action.data.id === action.data.pending_post_id) {
|
||||
const nextState = {...state};
|
||||
nextState[action.data.channel_id] += 1;
|
||||
return nextState;
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
function postCountInChannel(state = {}, action) {
|
||||
switch (action.type) {
|
||||
case ViewTypes.SET_INITIAL_POST_COUNT: {
|
||||
const {channelId, count} = action.data;
|
||||
const nextState = {...state};
|
||||
nextState[channelId] = count;
|
||||
return nextState;
|
||||
}
|
||||
case ViewTypes.INCREASE_POST_COUNT: {
|
||||
const {channelId, count} = action.data;
|
||||
const nextState = {...state};
|
||||
nextState[channelId] += count;
|
||||
return nextState;
|
||||
}
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
function loadingPosts(state = {}, action) {
|
||||
switch (action.type) {
|
||||
case ViewTypes.LOADING_POSTS: {
|
||||
|
|
@ -338,6 +286,8 @@ function lastGetPosts(state = {}, action) {
|
|||
|
||||
function loadMorePostsVisible(state = true, action) {
|
||||
switch (action.type) {
|
||||
case ChannelTypes.SELECT_CHANNEL:
|
||||
return true;
|
||||
case ViewTypes.SET_LOAD_MORE_POSTS_VISIBLE:
|
||||
return action.data;
|
||||
|
||||
|
|
@ -348,12 +298,14 @@ function loadMorePostsVisible(state = true, action) {
|
|||
|
||||
function lastChannelViewTime(state = {}, action) {
|
||||
switch (action.type) {
|
||||
case ViewTypes.SELECT_CHANNEL_WITH_MEMBER: {
|
||||
if (action.member) {
|
||||
case ChannelTypes.SELECT_CHANNEL: {
|
||||
if (action.extra?.member) {
|
||||
const {member} = action.extra;
|
||||
const nextState = {...state};
|
||||
nextState[action.data] = action.member.last_viewed_at;
|
||||
nextState[action.data] = member.last_viewed_at;
|
||||
return nextState;
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
|
|
@ -369,9 +321,8 @@ function lastChannelViewTime(state = {}, action) {
|
|||
|
||||
function keepChannelIdAsUnread(state = null, action) {
|
||||
switch (action.type) {
|
||||
case ViewTypes.SELECT_CHANNEL_WITH_MEMBER: {
|
||||
const member = action.member;
|
||||
const channel = action.channel;
|
||||
case ChannelTypes.SELECT_CHANNEL: {
|
||||
const {channel, member} = action.extra;
|
||||
|
||||
if (!member || !channel) {
|
||||
return state;
|
||||
|
|
@ -410,8 +361,6 @@ export default combineReducers({
|
|||
drafts,
|
||||
loading,
|
||||
refreshing,
|
||||
postCountInChannel,
|
||||
postVisibility,
|
||||
loadingPosts,
|
||||
lastGetPosts,
|
||||
retryFailed,
|
||||
|
|
|
|||
|
|
@ -1,10 +1,7 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {PostTypes} from 'mattermost-redux/action_types';
|
||||
|
||||
import channelReducer from './channel';
|
||||
import {ViewTypes} from 'app/constants';
|
||||
|
||||
describe('Reducers.channel', () => {
|
||||
const initialState = {
|
||||
|
|
@ -12,8 +9,6 @@ describe('Reducers.channel', () => {
|
|||
drafts: {},
|
||||
loading: false,
|
||||
refreshing: false,
|
||||
postCountInChannel: {},
|
||||
postVisibility: {},
|
||||
loadingPosts: {},
|
||||
lastGetPosts: {},
|
||||
retryFailed: false,
|
||||
|
|
@ -29,8 +24,6 @@ describe('Reducers.channel', () => {
|
|||
drafts: {},
|
||||
loading: false,
|
||||
refreshing: false,
|
||||
postCountInChannel: {},
|
||||
postVisibility: {},
|
||||
loadingPosts: {},
|
||||
lastGetPosts: {},
|
||||
retryFailed: false,
|
||||
|
|
@ -43,110 +36,4 @@ describe('Reducers.channel', () => {
|
|||
|
||||
expect(nextState).toEqual(initialState);
|
||||
});
|
||||
|
||||
test('should set the postVisibility amount for a channel', () => {
|
||||
const channelId = 'channel_id';
|
||||
const amount = 15;
|
||||
const nextState = channelReducer(
|
||||
{
|
||||
displayName: '',
|
||||
drafts: {},
|
||||
loading: false,
|
||||
refreshing: false,
|
||||
postCountInChannel: {},
|
||||
postVisibility: {},
|
||||
loadingPosts: {},
|
||||
lastGetPosts: {},
|
||||
retryFailed: false,
|
||||
loadMorePostsVisible: true,
|
||||
lastChannelViewTime: {},
|
||||
keepChannelIdAsUnread: null,
|
||||
},
|
||||
{
|
||||
type: ViewTypes.INCREASE_POST_VISIBILITY,
|
||||
data: channelId,
|
||||
amount,
|
||||
},
|
||||
);
|
||||
|
||||
expect(nextState).toEqual({
|
||||
...initialState,
|
||||
postVisibility: {
|
||||
[channelId]: amount,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('should increase the postVisibility amount for a channel', () => {
|
||||
const channelId = 'channel_id';
|
||||
const amount = 15;
|
||||
const nextState = channelReducer(
|
||||
{
|
||||
displayName: '',
|
||||
drafts: {},
|
||||
loading: false,
|
||||
refreshing: false,
|
||||
postCountInChannel: {},
|
||||
postVisibility: {
|
||||
[channelId]: amount,
|
||||
},
|
||||
loadingPosts: {},
|
||||
lastGetPosts: {},
|
||||
retryFailed: false,
|
||||
loadMorePostsVisible: true,
|
||||
lastChannelViewTime: {},
|
||||
keepChannelIdAsUnread: null,
|
||||
},
|
||||
{
|
||||
type: ViewTypes.INCREASE_POST_VISIBILITY,
|
||||
data: channelId,
|
||||
amount,
|
||||
},
|
||||
);
|
||||
|
||||
expect(nextState).toEqual({
|
||||
...initialState,
|
||||
postVisibility: {
|
||||
[channelId]: 2 * amount,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('should increase the postVisibility amount for a channel by one after creating a post', () => {
|
||||
const channelId = 'channel_id';
|
||||
const amount = 15;
|
||||
const state = {
|
||||
...initialState,
|
||||
postVisibility: {
|
||||
[channelId]: amount,
|
||||
},
|
||||
};
|
||||
|
||||
const receiveOtherPostAction = {
|
||||
type: PostTypes.RECEIVED_NEW_POST,
|
||||
data: {
|
||||
channel_id: channelId,
|
||||
id: 'post-id',
|
||||
pending_post_id: 'pending-post-id',
|
||||
},
|
||||
};
|
||||
let nextState = channelReducer(state, receiveOtherPostAction);
|
||||
expect(nextState).toEqual(state);
|
||||
|
||||
const receiveCreatedPostAction = {
|
||||
type: PostTypes.RECEIVED_NEW_POST,
|
||||
data: {
|
||||
channel_id: channelId,
|
||||
id: 'post-id',
|
||||
pending_post_id: 'post-id',
|
||||
},
|
||||
};
|
||||
nextState = channelReducer(state, receiveCreatedPostAction);
|
||||
expect(nextState).toEqual({
|
||||
...state,
|
||||
postVisibility: {
|
||||
[channelId]: amount + 1,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
// See LICENSE.txt for license information.
|
||||
|
||||
import {combineReducers} from 'redux';
|
||||
import {TeamTypes} from 'mattermost-redux/action_types';
|
||||
import {ChannelTypes, TeamTypes} from 'mattermost-redux/action_types';
|
||||
|
||||
import {ViewTypes} from 'app/constants';
|
||||
|
||||
|
|
@ -15,35 +15,40 @@ function lastTeamId(state = '', action) {
|
|||
}
|
||||
}
|
||||
|
||||
function setLastChannelForTeam(state, teamId, channel) {
|
||||
if (!channel?.id) {
|
||||
return state;
|
||||
}
|
||||
|
||||
const team = state[channel.team_id || teamId];
|
||||
const channelIds = [];
|
||||
|
||||
if (team) {
|
||||
channelIds.push(...team);
|
||||
const index = channelIds.indexOf(channel.id);
|
||||
if (index === -1) {
|
||||
channelIds.unshift(channel.id);
|
||||
channelIds.slice(0, 5);
|
||||
} else {
|
||||
channelIds.splice(index, 1);
|
||||
channelIds.unshift(channel.id);
|
||||
}
|
||||
} else {
|
||||
channelIds.push(channel.id);
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
[teamId]: channelIds,
|
||||
};
|
||||
}
|
||||
|
||||
function lastChannelForTeam(state = {}, action) {
|
||||
switch (action.type) {
|
||||
case ViewTypes.SET_LAST_CHANNEL_FOR_TEAM: {
|
||||
const team = state[action.teamId];
|
||||
const channelIds = [];
|
||||
|
||||
if (!action.channelId) {
|
||||
return state;
|
||||
}
|
||||
|
||||
if (team) {
|
||||
channelIds.push(...team);
|
||||
const index = channelIds.indexOf(action.channelId);
|
||||
if (index === -1) {
|
||||
channelIds.unshift(action.channelId);
|
||||
channelIds.slice(0, 5);
|
||||
} else {
|
||||
channelIds.splice(index, 1);
|
||||
channelIds.unshift(action.channelId);
|
||||
}
|
||||
} else {
|
||||
channelIds.push(action.channelId);
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
[action.teamId]: channelIds,
|
||||
};
|
||||
case ChannelTypes.SELECT_CHANNEL: {
|
||||
return setLastChannelForTeam(state, action.extra?.teamId, action.extra?.channel);
|
||||
}
|
||||
|
||||
case ViewTypes.REMOVE_LAST_CHANNEL_FOR_TEAM: {
|
||||
const {data} = action;
|
||||
const team = state[data.teamId];
|
||||
|
|
|
|||
|
|
@ -2,30 +2,50 @@
|
|||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {Dimensions, View} from 'react-native';
|
||||
import {View} from 'react-native';
|
||||
|
||||
import ChannelLoader from 'app/components/channel_loader';
|
||||
import EventEmitter from 'mattermost-redux/utils/event_emitter';
|
||||
|
||||
import {openMainSideMenu, openSettingsSideMenu} from 'app/actions/navigation';
|
||||
import KeyboardLayout from 'app/components/layout/keyboard_layout';
|
||||
import InteractiveDialogController from 'app/components/interactive_dialog_controller';
|
||||
import NetworkIndicator from 'app/components/network_indicator';
|
||||
import PostTextbox from 'app/components/post_textbox';
|
||||
import {NavigationTypes} from 'app/constants';
|
||||
import {makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
|
||||
import LocalConfig from 'assets/config';
|
||||
|
||||
import ChannelNavBar from './channel_nav_bar';
|
||||
import ChannelPostList from './channel_post_list';
|
||||
|
||||
import ChannelBase, {ClientUpgradeListener, style} from './channel_base';
|
||||
import ChannelBase, {ClientUpgradeListener} from './channel_base';
|
||||
|
||||
export default class ChannelAndroid extends ChannelBase {
|
||||
render() {
|
||||
const {height} = Dimensions.get('window');
|
||||
openMainSidebar = () => {
|
||||
EventEmitter.emit(NavigationTypes.BLUR_POST_TEXTBOX);
|
||||
openMainSideMenu();
|
||||
};
|
||||
|
||||
const channelLoaderStyle = [style.channelLoader, {height}];
|
||||
openSettingsSidebar = () => {
|
||||
EventEmitter.emit(NavigationTypes.BLUR_POST_TEXTBOX);
|
||||
openSettingsSideMenu();
|
||||
};
|
||||
|
||||
render() {
|
||||
const {theme} = this.props;
|
||||
const channelLoadingOrFailed = this.renderLoadingOrFailedChannel();
|
||||
if (channelLoadingOrFailed) {
|
||||
return channelLoadingOrFailed;
|
||||
}
|
||||
|
||||
const style = getStyleFromTheme(theme);
|
||||
const drawerContent = (
|
||||
<>
|
||||
<NetworkIndicator/>
|
||||
<ChannelNavBar
|
||||
openChannelDrawer={this.openChannelSidebar}
|
||||
openSettingsDrawer={this.openSettingsSidebar}
|
||||
openMainSidebar={this.openMainSidebar}
|
||||
openSettingsSidebar={this.openSettingsSidebar}
|
||||
onPress={this.goToChannelInfo}
|
||||
/>
|
||||
<KeyboardLayout>
|
||||
|
|
@ -37,14 +57,31 @@ export default class ChannelAndroid extends ChannelBase {
|
|||
screenId={this.props.componentId}
|
||||
/>
|
||||
</KeyboardLayout>
|
||||
<ChannelLoader
|
||||
height={height}
|
||||
style={channelLoaderStyle}
|
||||
/>
|
||||
{LocalConfig.EnableMobileClientUpgrade && <ClientUpgradeListener/>}
|
||||
</>
|
||||
);
|
||||
|
||||
return this.renderChannel(drawerContent);
|
||||
return (
|
||||
<>
|
||||
<View style={style.backdrop}>
|
||||
{drawerContent}
|
||||
</View>
|
||||
<InteractiveDialogController
|
||||
theme={theme}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
|
||||
return {
|
||||
backdrop: {
|
||||
flex: 1,
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
},
|
||||
flex: {
|
||||
flex: 1,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
|
@ -2,20 +2,22 @@
|
|||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {Dimensions, View} from 'react-native';
|
||||
import {View} from 'react-native';
|
||||
import {KeyboardTrackingView} from 'react-native-keyboard-tracking-view';
|
||||
|
||||
import Autocomplete, {AUTOCOMPLETE_MAX_HEIGHT} from 'app/components/autocomplete';
|
||||
import ChannelLoader from 'app/components/channel_loader';
|
||||
import InteractiveDialogController from 'app/components/interactive_dialog_controller';
|
||||
import MainSidebar from 'app/components/sidebars/main';
|
||||
import NetworkIndicator from 'app/components/network_indicator';
|
||||
import PostTextbox from 'app/components/post_textbox';
|
||||
import SafeAreaView from 'app/components/safe_area_view';
|
||||
import SettingsSidebar from 'app/components/sidebars/settings';
|
||||
import StatusBar from 'app/components/status_bar';
|
||||
import {DeviceTypes} from 'app/constants';
|
||||
import {makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
|
||||
import LocalConfig from 'assets/config';
|
||||
|
||||
import ChannelBase, {ClientUpgradeListener, style} from './channel_base';
|
||||
import ChannelBase, {ClientUpgradeListener} from './channel_base';
|
||||
import ChannelNavBar from './channel_nav_bar';
|
||||
import ChannelPostList from './channel_post_list';
|
||||
|
||||
|
|
@ -24,23 +26,47 @@ const CHANNEL_POST_TEXTBOX_CURSOR_CHANGE = 'onChannelTextBoxCursorChange';
|
|||
const CHANNEL_POST_TEXTBOX_VALUE_CHANGE = 'onChannelTextBoxValueChange';
|
||||
|
||||
export default class ChannelIOS extends ChannelBase {
|
||||
render() {
|
||||
const {height} = Dimensions.get('window');
|
||||
const {currentChannelId} = this.props;
|
||||
mainSidebarRef = (ref) => {
|
||||
if (ref) {
|
||||
this.mainSidebar = ref;
|
||||
}
|
||||
};
|
||||
|
||||
const channelLoaderStyle = [style.channelLoader, {height}];
|
||||
if ((DeviceTypes.IS_IPHONE_WITH_INSETS || DeviceTypes.IS_TABLET)) {
|
||||
channelLoaderStyle.push(style.iOSHomeIndicator);
|
||||
settingsSidebarRef = (ref) => {
|
||||
if (ref) {
|
||||
this.settingsSidebar = ref;
|
||||
}
|
||||
};
|
||||
|
||||
openMainSidebar = () => {
|
||||
if (this.mainSidebar) {
|
||||
this.mainSidebar.open();
|
||||
}
|
||||
};
|
||||
|
||||
openSettingsSidebar = () => {
|
||||
if (this.settingsSidebar) {
|
||||
this.settingsSidebar.open();
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const {currentChannelId, theme} = this.props;
|
||||
|
||||
const channelLoadingOrFailed = this.renderLoadingOrFailedChannel();
|
||||
if (channelLoadingOrFailed) {
|
||||
return channelLoadingOrFailed;
|
||||
}
|
||||
|
||||
const style = getStyle(theme);
|
||||
const drawerContent = (
|
||||
<React.Fragment>
|
||||
<>
|
||||
<SafeAreaView>
|
||||
<StatusBar/>
|
||||
<NetworkIndicator/>
|
||||
<ChannelNavBar
|
||||
openChannelDrawer={this.openChannelSidebar}
|
||||
openSettingsDrawer={this.openSettingsSidebar}
|
||||
openMainSidebar={this.openMainSidebar}
|
||||
openSettingsSidebar={this.openSettingsSidebar}
|
||||
onPress={this.goToChannelInfo}
|
||||
/>
|
||||
<ChannelPostList
|
||||
|
|
@ -54,10 +80,6 @@ export default class ChannelIOS extends ChannelBase {
|
|||
valueEvent={CHANNEL_POST_TEXTBOX_VALUE_CHANGE}
|
||||
/>
|
||||
</View>
|
||||
<ChannelLoader
|
||||
height={height}
|
||||
style={channelLoaderStyle}
|
||||
/>
|
||||
{LocalConfig.EnableMobileClientUpgrade && <ClientUpgradeListener/>}
|
||||
</SafeAreaView>
|
||||
<KeyboardTrackingView
|
||||
|
|
@ -72,9 +94,30 @@ export default class ChannelIOS extends ChannelBase {
|
|||
screenId={this.props.componentId}
|
||||
/>
|
||||
</KeyboardTrackingView>
|
||||
</React.Fragment>
|
||||
</>
|
||||
);
|
||||
|
||||
return this.renderChannel(drawerContent);
|
||||
return (
|
||||
<MainSidebar ref={this.mainSidebarRef}>
|
||||
<SettingsSidebar ref={this.settingsSidebarRef}>
|
||||
<View style={style.backdrop}>
|
||||
{drawerContent}
|
||||
</View>
|
||||
</SettingsSidebar>
|
||||
<InteractiveDialogController
|
||||
theme={theme}
|
||||
/>
|
||||
</MainSidebar>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const getStyle = makeStyleSheetFromTheme((theme) => ({
|
||||
backdrop: {
|
||||
flex: 1,
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
},
|
||||
flex: {
|
||||
flex: 1,
|
||||
},
|
||||
}));
|
||||
|
|
@ -13,22 +13,16 @@ import MaterialIcon from 'react-native-vector-icons/MaterialIcons';
|
|||
|
||||
import EventEmitter from 'mattermost-redux/utils/event_emitter';
|
||||
|
||||
import EmptyToolbar from 'app/components/start/empty_toolbar';
|
||||
import InteractiveDialogController from 'app/components/interactive_dialog_controller';
|
||||
import MainSidebar from 'app/components/sidebars/main';
|
||||
import {showModal, showModalOverCurrentContext} from 'app/actions/navigation';
|
||||
import SafeAreaView from 'app/components/safe_area_view';
|
||||
import SettingsSidebar from 'app/components/sidebars/settings';
|
||||
|
||||
import {preventDoubleTap} from 'app/utils/tap';
|
||||
import {makeStyleSheetFromTheme, setNavigatorStyles} from 'app/utils/theme';
|
||||
import EmptyToolbar from 'app/components/start/empty_toolbar';
|
||||
import {NavigationTypes} from 'app/constants';
|
||||
import PushNotifications from 'app/push_notifications';
|
||||
import EphemeralStore from 'app/store/ephemeral_store';
|
||||
import tracker from 'app/utils/time_tracker';
|
||||
import telemetry from 'app/telemetry';
|
||||
import {
|
||||
showModal,
|
||||
showModalOverCurrentContext,
|
||||
} from 'app/actions/navigation';
|
||||
import {preventDoubleTap} from 'app/utils/tap';
|
||||
import {setNavigatorStyles} from 'app/utils/theme';
|
||||
import tracker from 'app/utils/time_tracker';
|
||||
|
||||
import LocalConfig from 'assets/config';
|
||||
|
||||
|
|
@ -37,8 +31,8 @@ export let ClientUpgradeListener;
|
|||
export default class ChannelBase extends PureComponent {
|
||||
static propTypes = {
|
||||
actions: PropTypes.shape({
|
||||
loadChannelsIfNecessary: PropTypes.func.isRequired,
|
||||
loadProfilesAndTeamMembersForDMSidebar: PropTypes.func.isRequired,
|
||||
loadChannelsForTeam: PropTypes.func.isRequired,
|
||||
markChannelViewedAndRead: PropTypes.func.isRequired,
|
||||
selectDefaultTeam: PropTypes.func.isRequired,
|
||||
selectInitialChannel: PropTypes.func.isRequired,
|
||||
recordLoadTime: PropTypes.func.isRequired,
|
||||
|
|
@ -46,7 +40,6 @@ export default class ChannelBase extends PureComponent {
|
|||
}).isRequired,
|
||||
componentId: PropTypes.string.isRequired,
|
||||
currentChannelId: PropTypes.string,
|
||||
channelsRequestFailed: PropTypes.bool,
|
||||
currentTeamId: PropTypes.string,
|
||||
isLandscape: PropTypes.bool,
|
||||
theme: PropTypes.object.isRequired,
|
||||
|
|
@ -69,18 +62,19 @@ export default class ChannelBase extends PureComponent {
|
|||
this.postTextbox = React.createRef();
|
||||
this.keyboardTracker = React.createRef();
|
||||
|
||||
MaterialIcon.getImageSource('close', 20, props.theme.sidebarHeaderTextColor).then((source) => {
|
||||
this.closeButton = source;
|
||||
});
|
||||
|
||||
setNavigatorStyles(props.componentId, props.theme);
|
||||
|
||||
this.state = {
|
||||
channelsRequestFailed: false,
|
||||
};
|
||||
|
||||
if (LocalConfig.EnableMobileClientUpgrade && !ClientUpgradeListener) {
|
||||
ClientUpgradeListener = require('app/components/client_upgrade_listener').default;
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
EventEmitter.on(NavigationTypes.BLUR_POST_TEXTBOX, this.blurPostTextBox);
|
||||
EventEmitter.on('leave_team', this.handleLeaveTeam);
|
||||
|
||||
if (this.props.currentTeamId) {
|
||||
|
|
@ -91,7 +85,10 @@ export default class ChannelBase extends PureComponent {
|
|||
|
||||
if (this.props.currentChannelId) {
|
||||
PushNotifications.clearChannelNotifications(this.props.currentChannelId);
|
||||
this.props.actions.getChannelStats(this.props.currentChannelId);
|
||||
requestAnimationFrame(() => {
|
||||
this.props.actions.getChannelStats(this.props.currentChannelId);
|
||||
this.props.actions.markChannelViewedAndRead(this.props.currentChannelId);
|
||||
});
|
||||
}
|
||||
|
||||
if (tracker.initialLoad && !this.props.skipMetrics) {
|
||||
|
|
@ -102,56 +99,46 @@ export default class ChannelBase extends PureComponent {
|
|||
this.showTermsOfServiceModal();
|
||||
}
|
||||
|
||||
EventEmitter.emit('renderDrawer');
|
||||
|
||||
if (!this.props.skipMetrics) {
|
||||
telemetry.end(['start:channel_screen']);
|
||||
}
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (this.props.theme !== nextProps.theme) {
|
||||
setNavigatorStyles(this.props.componentId, nextProps.theme);
|
||||
|
||||
EphemeralStore.allNavigationComponentIds.forEach((componentId) => {
|
||||
setNavigatorStyles(componentId, nextProps.theme);
|
||||
});
|
||||
}
|
||||
|
||||
if (nextProps.currentTeamId && this.props.currentTeamId !== nextProps.currentTeamId) {
|
||||
this.loadChannels(nextProps.currentTeamId);
|
||||
}
|
||||
|
||||
if (nextProps.currentChannelId !== this.props.currentChannelId &&
|
||||
nextProps.currentTeamId === this.props.currentTeamId) {
|
||||
PushNotifications.clearChannelNotifications(nextProps.currentChannelId);
|
||||
}
|
||||
|
||||
if (nextProps.currentChannelId !== this.props.currentChannelId) {
|
||||
this.props.actions.getChannelStats(nextProps.currentChannelId);
|
||||
}
|
||||
|
||||
if (LocalConfig.EnableMobileClientUpgrade && !ClientUpgradeListener) {
|
||||
ClientUpgradeListener = require('app/components/client_upgrade_listener').default;
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (tracker.teamSwitch) {
|
||||
this.props.actions.recordLoadTime('Switch Team', 'teamSwitch');
|
||||
}
|
||||
|
||||
// When the team changes emit the event to render the drawer content
|
||||
if (this.props.currentChannelId && !prevProps.currentChannelId) {
|
||||
EventEmitter.emit('renderDrawer');
|
||||
if (this.props.theme !== prevProps.theme) {
|
||||
setNavigatorStyles(this.props.componentId, this.props.theme);
|
||||
|
||||
EphemeralStore.allNavigationComponentIds.forEach((componentId) => {
|
||||
setNavigatorStyles(componentId, this.props.theme);
|
||||
});
|
||||
}
|
||||
|
||||
if (this.props.currentTeamId && this.props.currentTeamId !== prevProps.currentTeamId) {
|
||||
this.loadChannels(this.props.currentTeamId);
|
||||
}
|
||||
|
||||
if (this.props.currentChannelId && this.props.currentChannelId !== prevProps.currentChannelId) {
|
||||
this.updateNativeScrollView();
|
||||
const previousChannelId = EphemeralStore.appStartedFromPushNotification ? null : prevProps.currentChannelId;
|
||||
PushNotifications.clearChannelNotifications(this.props.currentChannelId);
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
this.props.actions.markChannelViewedAndRead(this.props.currentChannelId, previousChannelId);
|
||||
this.props.actions.getChannelStats(this.props.currentChannelId);
|
||||
this.updateNativeScrollView();
|
||||
});
|
||||
}
|
||||
|
||||
if (LocalConfig.EnableMobileClientUpgrade && !ClientUpgradeListener) {
|
||||
ClientUpgradeListener = require('app/components/client_upgrade_listener').default;
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
EventEmitter.off(NavigationTypes.BLUR_POST_TEXTBOX, this.blurPostTextBox);
|
||||
EventEmitter.off('leave_team', this.handleLeaveTeam);
|
||||
}
|
||||
|
||||
|
|
@ -161,58 +148,48 @@ export default class ChannelBase extends PureComponent {
|
|||
}
|
||||
};
|
||||
|
||||
channelSidebarRef = (ref) => {
|
||||
if (ref) {
|
||||
this.channelSidebar = ref;
|
||||
}
|
||||
};
|
||||
|
||||
settingsSidebarRef = (ref) => {
|
||||
if (ref) {
|
||||
this.settingsSidebar = ref;
|
||||
}
|
||||
};
|
||||
|
||||
showTermsOfServiceModal = async () => {
|
||||
const {intl} = this.context;
|
||||
const {theme} = this.props;
|
||||
const closeButton = await MaterialIcon.getImageSource('close', 20, theme.sidebarHeaderTextColor);
|
||||
const screen = 'TermsOfService';
|
||||
const passProps = {closeButton};
|
||||
const title = intl.formatMessage({id: 'mobile.tos_link', defaultMessage: 'Terms of Service'});
|
||||
const options = {
|
||||
layout: {
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
},
|
||||
topBar: {
|
||||
visible: true,
|
||||
height: null,
|
||||
title: {
|
||||
color: theme.sidebarHeaderTextColor,
|
||||
text: title,
|
||||
MaterialIcon.getImageSource('close', 20, theme.sidebarHeaderTextColor).then((closeButton) => {
|
||||
const passProps = {closeButton};
|
||||
const options = {
|
||||
layout: {
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
},
|
||||
},
|
||||
};
|
||||
topBar: {
|
||||
visible: true,
|
||||
height: null,
|
||||
title: {
|
||||
color: theme.sidebarHeaderTextColor,
|
||||
text: title,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
showModalOverCurrentContext(screen, passProps, options);
|
||||
showModalOverCurrentContext(screen, passProps, options);
|
||||
});
|
||||
};
|
||||
|
||||
goToChannelInfo = preventDoubleTap(() => {
|
||||
const {intl} = this.context;
|
||||
const {theme} = this.props;
|
||||
const screen = 'ChannelInfo';
|
||||
const title = intl.formatMessage({id: 'mobile.routes.channelInfo', defaultMessage: 'Info'});
|
||||
const options = {
|
||||
topBar: {
|
||||
leftButtons: [{
|
||||
id: 'close-info',
|
||||
icon: this.closeButton,
|
||||
}],
|
||||
},
|
||||
};
|
||||
MaterialIcon.getImageSource('close', 20, theme.sidebarHeaderTextColor).then((source) => {
|
||||
const options = {
|
||||
topBar: {
|
||||
leftButtons: [{
|
||||
id: 'close-info',
|
||||
icon: source,
|
||||
}],
|
||||
},
|
||||
};
|
||||
|
||||
Keyboard.dismiss();
|
||||
Keyboard.dismiss();
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
showModal(screen, title, null, options);
|
||||
});
|
||||
});
|
||||
|
|
@ -228,14 +205,12 @@ export default class ChannelBase extends PureComponent {
|
|||
};
|
||||
|
||||
loadChannels = (teamId) => {
|
||||
const {
|
||||
loadChannelsIfNecessary,
|
||||
loadProfilesAndTeamMembersForDMSidebar,
|
||||
selectInitialChannel,
|
||||
} = this.props.actions;
|
||||
|
||||
loadChannelsIfNecessary(teamId).then(() => {
|
||||
loadProfilesAndTeamMembersForDMSidebar(teamId);
|
||||
const {loadChannelsForTeam, selectInitialChannel} = this.props.actions;
|
||||
loadChannelsForTeam(teamId).then((result) => {
|
||||
if (result?.error) {
|
||||
this.setState({channelsRequestFailed: true});
|
||||
return;
|
||||
}
|
||||
|
||||
if (EphemeralStore.appStartedFromPushNotification) {
|
||||
EphemeralStore.appStartedFromPushNotification = false;
|
||||
|
|
@ -245,36 +220,18 @@ export default class ChannelBase extends PureComponent {
|
|||
});
|
||||
};
|
||||
|
||||
openChannelSidebar = () => {
|
||||
if (this.channelSidebar) {
|
||||
this.channelSidebar.openChannelSidebar();
|
||||
}
|
||||
};
|
||||
|
||||
openSettingsSidebar = () => {
|
||||
if (this.settingsSidebar) {
|
||||
this.settingsSidebar.openSettingsSidebar();
|
||||
}
|
||||
};
|
||||
|
||||
retryLoadChannels = () => {
|
||||
this.loadChannels(this.props.currentTeamId);
|
||||
};
|
||||
|
||||
updateNativeScrollView = () => {
|
||||
if (this.keyboardTracker?.current) {
|
||||
this.keyboardTracker.current.resetScrollView(this.props.currentChannelId);
|
||||
}
|
||||
};
|
||||
|
||||
renderChannel(drawerContent) {
|
||||
renderLoadingOrFailedChannel() {
|
||||
const {
|
||||
channelsRequestFailed,
|
||||
currentChannelId,
|
||||
isLandscape,
|
||||
theme,
|
||||
} = this.props;
|
||||
|
||||
const {channelsRequestFailed} = this.state;
|
||||
if (!currentChannelId) {
|
||||
if (channelsRequestFailed) {
|
||||
const FailedNetworkAction = require('app/components/failed_network_action').default;
|
||||
|
|
@ -312,27 +269,15 @@ export default class ChannelBase extends PureComponent {
|
|||
);
|
||||
}
|
||||
|
||||
const baseStyle = getStyleFromTheme(theme);
|
||||
return (
|
||||
<MainSidebar
|
||||
ref={this.channelSidebarRef}
|
||||
blurPostTextBox={this.blurPostTextBox}
|
||||
>
|
||||
<SettingsSidebar
|
||||
ref={this.settingsSidebarRef}
|
||||
blurPostTextBox={this.blurPostTextBox}
|
||||
>
|
||||
<View style={baseStyle.backdrop}>
|
||||
{drawerContent}
|
||||
</View>
|
||||
</SettingsSidebar>
|
||||
<InteractiveDialogController
|
||||
theme={theme}
|
||||
/>
|
||||
</MainSidebar>
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
updateNativeScrollView = () => {
|
||||
if (this.keyboardTracker?.current) {
|
||||
this.keyboardTracker.current.resetScrollView(this.props.currentChannelId);
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
// Overriden in channel.android.js and channel.ios.js
|
||||
// but defined here for channel_base.test.js
|
||||
|
|
@ -340,25 +285,8 @@ export default class ChannelBase extends PureComponent {
|
|||
}
|
||||
}
|
||||
|
||||
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
|
||||
return {
|
||||
backdrop: {
|
||||
flex: 1,
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
export const style = StyleSheet.create({
|
||||
flex: {
|
||||
flex: 1,
|
||||
},
|
||||
channelLoader: {
|
||||
position: 'absolute',
|
||||
width: '100%',
|
||||
flex: 1,
|
||||
},
|
||||
iOSHomeIndicator: {
|
||||
paddingBottom: 50,
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -21,12 +21,12 @@ describe('ChannelBase', () => {
|
|||
const componentIds = ['component-1', 'component-2', 'component-3'];
|
||||
const baseProps = {
|
||||
actions: {
|
||||
loadChannelsIfNecessary: jest.fn(),
|
||||
loadProfilesAndTeamMembersForDMSidebar: jest.fn(),
|
||||
getChannelStats: jest.fn(),
|
||||
loadChannelsForTeam: jest.fn(),
|
||||
markChannelViewedAndRead: jest.fn(),
|
||||
recordLoadTime: jest.fn(),
|
||||
selectDefaultTeam: jest.fn(),
|
||||
selectInitialChannel: jest.fn(),
|
||||
recordLoadTime: jest.fn(),
|
||||
getChannelStats: jest.fn(),
|
||||
},
|
||||
componentId: channelBaseComponentId,
|
||||
theme: Preferences.THEMES.default,
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ exports[`ChannelNavBar should match, full snapshot 1`] = `
|
|||
}
|
||||
>
|
||||
<Connect(ChannelDrawerButton)
|
||||
openDrawer={[MockFunction]}
|
||||
openSidebar={[MockFunction]}
|
||||
visible={true}
|
||||
/>
|
||||
<Connect(ChannelTitle)
|
||||
|
|
@ -58,7 +58,7 @@ exports[`ChannelNavBar should match, full snapshot 1`] = `
|
|||
}
|
||||
/>
|
||||
<Connect(SettingDrawerButton)
|
||||
openDrawer={[MockFunction]}
|
||||
openSidebar={[MockFunction]}
|
||||
/>
|
||||
</View>
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ import telemetry from 'app/telemetry';
|
|||
|
||||
export default class ChannelDrawerButton extends PureComponent {
|
||||
static propTypes = {
|
||||
openDrawer: PropTypes.func.isRequired,
|
||||
openSidebar: PropTypes.func.isRequired,
|
||||
badgeCount: PropTypes.number,
|
||||
theme: PropTypes.object,
|
||||
visible: PropTypes.bool,
|
||||
|
|
@ -57,7 +57,7 @@ export default class ChannelDrawerButton extends PureComponent {
|
|||
|
||||
handlePress = preventDoubleTap(() => {
|
||||
telemetry.start(['channel:open_drawer']);
|
||||
this.props.openDrawer();
|
||||
this.props.openSidebar();
|
||||
});
|
||||
|
||||
render() {
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ jest.mock('react-native-notifications', () => {
|
|||
|
||||
describe('ChannelDrawerButton', () => {
|
||||
const baseProps = {
|
||||
openDrawer: jest.fn(),
|
||||
openSidebar: jest.fn(),
|
||||
badgeCount: 0,
|
||||
theme: Preferences.THEMES.default,
|
||||
visible: false,
|
||||
|
|
|
|||
|
|
@ -29,8 +29,8 @@ const {
|
|||
export default class ChannelNavBar extends PureComponent {
|
||||
static propTypes = {
|
||||
isLandscape: PropTypes.bool.isRequired,
|
||||
openChannelDrawer: PropTypes.func.isRequired,
|
||||
openSettingsDrawer: PropTypes.func.isRequired,
|
||||
openMainSidebar: PropTypes.func.isRequired,
|
||||
openSettingsSidebar: PropTypes.func.isRequired,
|
||||
onPress: PropTypes.func.isRequired,
|
||||
theme: PropTypes.object.isRequired,
|
||||
};
|
||||
|
|
@ -72,7 +72,7 @@ export default class ChannelNavBar extends PureComponent {
|
|||
|
||||
render() {
|
||||
const {isLandscape, onPress, theme} = this.props;
|
||||
const {openChannelDrawer, openSettingsDrawer} = this.props;
|
||||
const {openMainSidebar, openSettingsSidebar} = this.props;
|
||||
const style = getStyleFromTheme(theme);
|
||||
|
||||
let height;
|
||||
|
|
@ -107,7 +107,7 @@ export default class ChannelNavBar extends PureComponent {
|
|||
return (
|
||||
<View style={[style.header, padding(isLandscape), {height}]}>
|
||||
<ChannelDrawerButton
|
||||
openDrawer={openChannelDrawer}
|
||||
openSidebar={openMainSidebar}
|
||||
visible={drawerButtonVisible}
|
||||
/>
|
||||
<ChannelTitle
|
||||
|
|
@ -117,7 +117,7 @@ export default class ChannelNavBar extends PureComponent {
|
|||
<ChannelSearchButton
|
||||
theme={theme}
|
||||
/>
|
||||
<SettingDrawerButton openDrawer={openSettingsDrawer}/>
|
||||
<SettingDrawerButton openSidebar={openSettingsSidebar}/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,8 +15,8 @@ jest.mock('react-intl');
|
|||
describe('ChannelNavBar', () => {
|
||||
const baseProps = {
|
||||
isLandscape: false,
|
||||
openChannelDrawer: jest.fn(),
|
||||
openSettingsDrawer: jest.fn(),
|
||||
openMainSidebar: jest.fn(),
|
||||
openSettingsSidebar: jest.fn(),
|
||||
onPress: jest.fn(),
|
||||
theme: Preferences.THEMES.default,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ import {makeStyleSheetFromTheme} from 'app/utils/theme';
|
|||
|
||||
class SettingDrawerButton extends PureComponent {
|
||||
static propTypes = {
|
||||
openDrawer: PropTypes.func.isRequired,
|
||||
openSidebar: PropTypes.func.isRequired,
|
||||
theme: PropTypes.object,
|
||||
};
|
||||
|
||||
|
|
@ -26,7 +26,7 @@ class SettingDrawerButton extends PureComponent {
|
|||
};
|
||||
|
||||
handlePress = preventDoubleTap(() => {
|
||||
this.props.openDrawer();
|
||||
this.props.openSidebar();
|
||||
});
|
||||
|
||||
render() {
|
||||
|
|
|
|||
|
|
@ -11,12 +11,10 @@ import {
|
|||
|
||||
import {getLastPostIndex} from 'mattermost-redux/utils/post_list';
|
||||
import EventEmitter from 'mattermost-redux/utils/event_emitter';
|
||||
import {WebsocketEvents} from 'mattermost-redux/constants';
|
||||
|
||||
import AnnouncementBanner from 'app/components/announcement_banner';
|
||||
import PostList from 'app/components/post_list';
|
||||
import RetryBarIndicator from 'app/components/retry_bar_indicator';
|
||||
import {ViewTypes} from 'app/constants';
|
||||
import tracker from 'app/utils/time_tracker';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
import telemetry from 'app/telemetry';
|
||||
|
|
@ -31,7 +29,6 @@ export default class ChannelPostList extends PureComponent {
|
|||
loadPostsIfNecessaryWithRetry: PropTypes.func.isRequired,
|
||||
loadThreadIfNecessary: PropTypes.func.isRequired,
|
||||
increasePostVisibility: PropTypes.func.isRequired,
|
||||
increasePostVisibilityByOne: PropTypes.func.isRequired,
|
||||
selectPost: PropTypes.func.isRequired,
|
||||
recordLoadTime: PropTypes.func.isRequired,
|
||||
refreshChannelWithRetry: PropTypes.func.isRequired,
|
||||
|
|
@ -43,7 +40,6 @@ export default class ChannelPostList extends PureComponent {
|
|||
lastViewedAt: PropTypes.number,
|
||||
loadMorePostsVisible: PropTypes.bool.isRequired,
|
||||
postIds: PropTypes.array,
|
||||
postVisibility: PropTypes.number,
|
||||
refreshing: PropTypes.bool.isRequired,
|
||||
theme: PropTypes.object.isRequired,
|
||||
updateNativeScrollView: PropTypes.func,
|
||||
|
|
@ -51,16 +47,11 @@ export default class ChannelPostList extends PureComponent {
|
|||
|
||||
static defaultProps = {
|
||||
postIds: [],
|
||||
postVisibility: ViewTypes.POST_VISIBILITY_CHUNK_SIZE,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
visiblePostIds: this.getVisiblePostIds(props),
|
||||
};
|
||||
|
||||
this.contentHeight = 0;
|
||||
|
||||
this.isLoadingMoreBottom = false;
|
||||
|
|
@ -69,29 +60,14 @@ export default class ChannelPostList extends PureComponent {
|
|||
|
||||
componentDidMount() {
|
||||
EventEmitter.on('goToThread', this.goToThread);
|
||||
EventEmitter.on(WebsocketEvents.INCREASE_POST_VISIBILITY_BY_ONE, this.increasePostVisibilityByOne);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
const {postIds: nextPostIds} = nextProps;
|
||||
|
||||
let visiblePostIds = this.state.visiblePostIds;
|
||||
if (nextPostIds !== this.props.postIds || nextProps.postVisibility !== this.props.postVisibility) {
|
||||
visiblePostIds = this.getVisiblePostIds(nextProps);
|
||||
}
|
||||
|
||||
if (this.props.channelId !== nextProps.channelId) {
|
||||
this.isLoadingMoreTop = false;
|
||||
}
|
||||
|
||||
if (this.state.visiblePostIds !== visiblePostIds) {
|
||||
this.setState({visiblePostIds});
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (prevProps.channelId !== this.props.channelId && tracker.channelSwitch) {
|
||||
this.props.actions.recordLoadTime('Switch Channel', 'channelSwitch');
|
||||
if (this.props.channelId !== prevProps.channelId) {
|
||||
this.isLoadingMoreTop = false;
|
||||
if (tracker.channelSwitch) {
|
||||
this.props.actions.recordLoadTime('Switch Channel', 'channelSwitch');
|
||||
}
|
||||
}
|
||||
|
||||
if (!prevProps.postIds?.length && this.props.postIds?.length > 0 && this.props.updateNativeScrollView) {
|
||||
|
|
@ -102,13 +78,8 @@ export default class ChannelPostList extends PureComponent {
|
|||
|
||||
componentWillUnmount() {
|
||||
EventEmitter.off('goToThread', this.goToThread);
|
||||
EventEmitter.off(WebsocketEvents.INCREASE_POST_VISIBILITY_BY_ONE, this.increasePostVisibilityByOne);
|
||||
}
|
||||
|
||||
getVisiblePostIds = (props) => {
|
||||
return props.postIds ? props.postIds.slice(0, props.postVisibility) : [];
|
||||
};
|
||||
|
||||
goToThread = (post) => {
|
||||
telemetry.start(['post_list:thread']);
|
||||
const {actions, channelId} = this.props;
|
||||
|
|
@ -130,18 +101,13 @@ export default class ChannelPostList extends PureComponent {
|
|||
});
|
||||
};
|
||||
|
||||
increasePostVisibilityByOne = () => {
|
||||
const {actions, channelId} = this.props;
|
||||
actions.increasePostVisibilityByOne(channelId);
|
||||
}
|
||||
|
||||
loadMorePostsTop = () => {
|
||||
const {actions, channelId} = this.props;
|
||||
const {actions, channelId, postIds} = this.props;
|
||||
if (!this.isLoadingMoreTop) {
|
||||
this.isLoadingMoreTop = true;
|
||||
actions.increasePostVisibility(
|
||||
channelId,
|
||||
this.state.visiblePostIds[this.state.visiblePostIds.length - 1],
|
||||
postIds[postIds.length - 1],
|
||||
).then((hasMore) => {
|
||||
this.isLoadingMoreTop = !hasMore;
|
||||
});
|
||||
|
|
@ -190,15 +156,14 @@ export default class ChannelPostList extends PureComponent {
|
|||
channelRefreshingFailed,
|
||||
currentUserId,
|
||||
lastViewedAt,
|
||||
loadMorePostsVisible,
|
||||
postIds,
|
||||
refreshing,
|
||||
theme,
|
||||
} = this.props;
|
||||
|
||||
const {visiblePostIds} = this.state;
|
||||
let component;
|
||||
|
||||
if (visiblePostIds.length === 0 && channelRefreshingFailed) {
|
||||
if (postIds.length === 0 && channelRefreshingFailed) {
|
||||
const FailedNetworkAction = require('app/components/failed_network_action').default;
|
||||
|
||||
component = (
|
||||
|
|
@ -210,9 +175,9 @@ export default class ChannelPostList extends PureComponent {
|
|||
} else {
|
||||
component = (
|
||||
<PostList
|
||||
postIds={visiblePostIds}
|
||||
lastPostIndex={Platform.OS === 'android' ? getLastPostIndex(visiblePostIds) : -1}
|
||||
extraData={loadMorePostsVisible}
|
||||
postIds={postIds}
|
||||
lastPostIndex={Platform.OS === 'android' ? getLastPostIndex(postIds) : -1}
|
||||
extraData={postIds.length === 0}
|
||||
onLoadMoreUp={this.loadMorePostsTop}
|
||||
onPostPress={this.goToThread}
|
||||
onRefresh={actions.setChannelRefreshing}
|
||||
|
|
@ -224,6 +189,7 @@ export default class ChannelPostList extends PureComponent {
|
|||
renderFooter={this.renderFooter}
|
||||
refreshing={refreshing}
|
||||
scrollViewNativeID={channelId}
|
||||
loadMorePostsVisible={this.props.loadMorePostsVisible}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,40 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {shallow} from 'enzyme';
|
||||
|
||||
import Preferences from 'mattermost-redux/constants/preferences';
|
||||
import EventEmitter from 'mattermost-redux/utils/event_emitter';
|
||||
import {WebsocketEvents} from 'mattermost-redux/constants';
|
||||
|
||||
import ChannelPostList from './channel_post_list';
|
||||
|
||||
describe('ChannelPostList', () => {
|
||||
const baseProps = {
|
||||
actions: {
|
||||
loadPostsIfNecessaryWithRetry: jest.fn(),
|
||||
loadThreadIfNecessary: jest.fn(),
|
||||
increasePostVisibility: jest.fn(),
|
||||
increasePostVisibilityByOne: jest.fn(),
|
||||
selectPost: jest.fn(),
|
||||
recordLoadTime: jest.fn(),
|
||||
refreshChannelWithRetry: jest.fn(),
|
||||
},
|
||||
channelId: 'channel-id',
|
||||
loadMorePostsVisible: false,
|
||||
refreshing: false,
|
||||
theme: Preferences.THEMES.default,
|
||||
};
|
||||
|
||||
test('should call increasePostVisibilityByOne', () => {
|
||||
shallow(
|
||||
<ChannelPostList {...baseProps}/>,
|
||||
);
|
||||
|
||||
expect(baseProps.actions.increasePostVisibilityByOne).toHaveBeenCalledTimes(0);
|
||||
|
||||
EventEmitter.emit(WebsocketEvents.INCREASE_POST_VISIBILITY_BY_ONE);
|
||||
expect(baseProps.actions.increasePostVisibilityByOne).toHaveBeenCalledWith(baseProps.channelId);
|
||||
});
|
||||
});
|
||||
|
|
@ -14,7 +14,6 @@ import {
|
|||
loadPostsIfNecessaryWithRetry,
|
||||
loadThreadIfNecessary,
|
||||
increasePostVisibility,
|
||||
increasePostVisibilityByOne,
|
||||
refreshChannelWithRetry,
|
||||
} from 'app/actions/views/channel';
|
||||
import {recordLoadTime} from 'app/actions/views/root';
|
||||
|
|
@ -31,8 +30,7 @@ function mapStateToProps(state) {
|
|||
channelRefreshingFailed,
|
||||
currentUserId: getCurrentUserId(state),
|
||||
deviceHeight: state.device.dimension.deviceHeight,
|
||||
postIds: getPostIdsInCurrentChannel(state),
|
||||
postVisibility: state.views.channel.postVisibility[channelId],
|
||||
postIds: getPostIdsInCurrentChannel(state) || [],
|
||||
lastViewedAt: state.views.channel.lastChannelViewTime[channelId],
|
||||
loadMorePostsVisible: state.views.channel.loadMorePostsVisible,
|
||||
refreshing: state.views.channel.refreshing,
|
||||
|
|
@ -47,7 +45,6 @@ function mapDispatchToProps(dispatch) {
|
|||
loadPostsIfNecessaryWithRetry,
|
||||
loadThreadIfNecessary,
|
||||
increasePostVisibility,
|
||||
increasePostVisibilityByOne,
|
||||
selectPost,
|
||||
recordLoadTime,
|
||||
refreshChannelWithRetry,
|
||||
|
|
|
|||
|
|
@ -4,8 +4,7 @@
|
|||
import {bindActionCreators} from 'redux';
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {startPeriodicStatusUpdates, stopPeriodicStatusUpdates, logout} from 'mattermost-redux/actions/users';
|
||||
import {RequestStatus} from 'mattermost-redux/constants';
|
||||
import {startPeriodicStatusUpdates, stopPeriodicStatusUpdates} from 'mattermost-redux/actions/users';
|
||||
import {getCurrentChannelId} from 'mattermost-redux/selectors/entities/channels';
|
||||
import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams';
|
||||
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
|
||||
|
|
@ -13,22 +12,20 @@ import {shouldShowTermsOfService} from 'mattermost-redux/selectors/entities/user
|
|||
import {getChannelStats} from 'mattermost-redux/actions/channels';
|
||||
|
||||
import {
|
||||
loadChannelsIfNecessary,
|
||||
loadProfilesAndTeamMembersForDMSidebar,
|
||||
loadChannelsForTeam,
|
||||
selectInitialChannel,
|
||||
markChannelViewedAndRead,
|
||||
} from 'app/actions/views/channel';
|
||||
import {connection} from 'app/actions/device';
|
||||
import {recordLoadTime} from 'app/actions/views/root';
|
||||
import {logout} from 'app/actions/views/user';
|
||||
import {selectDefaultTeam} from 'app/actions/views/select_team';
|
||||
import {isLandscape} from 'app/selectors/device';
|
||||
|
||||
import Channel from './channel';
|
||||
|
||||
function mapStateToProps(state) {
|
||||
const {myChannels: channelsRequest} = state.requests.channels;
|
||||
|
||||
return {
|
||||
channelsRequestFailed: channelsRequest.status === RequestStatus.FAILURE,
|
||||
currentTeamId: getCurrentTeamId(state),
|
||||
currentChannelId: getCurrentChannelId(state),
|
||||
isLandscape: isLandscape(state),
|
||||
|
|
@ -42,9 +39,9 @@ function mapDispatchToProps(dispatch) {
|
|||
actions: bindActionCreators({
|
||||
getChannelStats,
|
||||
connection,
|
||||
loadChannelsIfNecessary,
|
||||
loadProfilesAndTeamMembersForDMSidebar,
|
||||
loadChannelsForTeam,
|
||||
logout,
|
||||
markChannelViewedAndRead,
|
||||
selectDefaultTeam,
|
||||
selectInitialChannel,
|
||||
recordLoadTime,
|
||||
|
|
|
|||
|
|
@ -13,9 +13,10 @@ import {Navigation} from 'react-native-navigation';
|
|||
import {General, RequestStatus} from 'mattermost-redux/constants';
|
||||
import EventEmitter from 'mattermost-redux/utils/event_emitter';
|
||||
|
||||
import EditChannelInfo from 'app/components/edit_channel_info';
|
||||
import {setNavigatorStyles} from 'app/utils/theme';
|
||||
import {popTopScreen, dismissModal, setButtons} from 'app/actions/navigation';
|
||||
import EditChannelInfo from 'app/components/edit_channel_info';
|
||||
import {NavigationTypes} from 'app/constants';
|
||||
import {setNavigatorStyles} from 'app/utils/theme';
|
||||
|
||||
export default class CreateChannel extends PureComponent {
|
||||
static propTypes = {
|
||||
|
|
@ -87,7 +88,7 @@ export default class CreateChannel extends PureComponent {
|
|||
this.setState({error: null, creating: true});
|
||||
break;
|
||||
case RequestStatus.SUCCESS:
|
||||
EventEmitter.emit('close_channel_drawer');
|
||||
EventEmitter.emit(NavigationTypes.CLOSE_MAIN_SIDEBAR);
|
||||
InteractionManager.runAfterInteractions(() => {
|
||||
this.emitCreating(false);
|
||||
this.setState({error: null, creating: false});
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ import {General, RequestStatus} from 'mattermost-redux/constants';
|
|||
import EventEmitter from 'mattermost-redux/utils/event_emitter';
|
||||
|
||||
import EditChannelInfo from 'app/components/edit_channel_info';
|
||||
import {ViewTypes} from 'app/constants';
|
||||
import {NavigationTypes, ViewTypes} from 'app/constants';
|
||||
import {setNavigatorStyles} from 'app/utils/theme';
|
||||
import {cleanUpUrlable} from 'app/utils/url';
|
||||
import {t} from 'app/utils/i18n';
|
||||
|
|
@ -151,7 +151,7 @@ export default class EditChannel extends PureComponent {
|
|||
this.emitUpdating(true);
|
||||
break;
|
||||
case RequestStatus.SUCCESS:
|
||||
EventEmitter.emit('close_channel_drawer');
|
||||
EventEmitter.emit(NavigationTypes.CLOSE_MAIN_SIDEBAR);
|
||||
InteractionManager.runAfterInteractions(() => {
|
||||
this.emitUpdating(false);
|
||||
this.close();
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {Platform} from 'react-native';
|
||||
import {Navigation} from 'react-native-navigation';
|
||||
import {gestureHandlerRootHOC} from 'react-native-gesture-handler';
|
||||
|
||||
|
|
@ -78,4 +79,9 @@ export function registerScreens(store, Provider) {
|
|||
Navigation.registerComponent('TimezoneSettings', () => wrapper(require('app/screens/settings/timezone').default), () => require('app/screens/settings/timezone').default);
|
||||
Navigation.registerComponent('ErrorTeamsList', () => wrapper(require('app/screens/error_teams_list').default), () => require('app/screens/error_teams_list').default);
|
||||
Navigation.registerComponent('UserProfile', () => wrapper(require('app/screens/user_profile').default), () => require('app/screens/user_profile').default);
|
||||
|
||||
if (Platform.OS === 'android') {
|
||||
Navigation.registerComponentWithRedux('MainSidebar', () => require('app/components/sidebars/main').default, Provider, store);
|
||||
Navigation.registerComponentWithRedux('SettingsSidebar', () => require('app/components/sidebars/settings').default, Provider, store);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
import {bindActionCreators} from 'redux';
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {login} from 'mattermost-redux/actions/users';
|
||||
import {login} from 'app/actions/views/user';
|
||||
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {getConfig, getLicense} from 'mattermost-redux/selectors/entities/general';
|
||||
import {isLandscape} from 'app/selectors/device';
|
||||
|
|
@ -13,12 +13,10 @@ import LoginActions from 'app/actions/views/login';
|
|||
import Login from './login.js';
|
||||
|
||||
function mapStateToProps(state) {
|
||||
const {login: loginRequest} = state.requests.users;
|
||||
const config = getConfig(state);
|
||||
const license = getLicense(state);
|
||||
return {
|
||||
...state.views.login,
|
||||
loginRequest,
|
||||
config,
|
||||
license,
|
||||
theme: getTheme(state),
|
||||
|
|
|
|||
|
|
@ -19,8 +19,6 @@ import {
|
|||
import Button from 'react-native-button';
|
||||
import {KeyboardAwareScrollView} from 'react-native-keyboard-aware-scroll-view';
|
||||
|
||||
import {RequestStatus} from 'mattermost-redux/constants';
|
||||
|
||||
import {paddingHorizontal as padding} from 'app/components/safe_area_view/iphone_x_spacing';
|
||||
import ErrorText from 'app/components/error_text';
|
||||
import FormattedText from 'app/components/formatted_text';
|
||||
|
|
@ -46,12 +44,10 @@ export default class Login extends PureComponent {
|
|||
scheduleExpiredNotification: PropTypes.func.isRequired,
|
||||
login: PropTypes.func.isRequired,
|
||||
}).isRequired,
|
||||
theme: PropTypes.object,
|
||||
config: PropTypes.object.isRequired,
|
||||
license: PropTypes.object.isRequired,
|
||||
loginId: PropTypes.string.isRequired,
|
||||
password: PropTypes.string.isRequired,
|
||||
loginRequest: PropTypes.object.isRequired,
|
||||
isLandscape: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
|
|
@ -64,6 +60,7 @@ export default class Login extends PureComponent {
|
|||
|
||||
this.state = {
|
||||
error: null,
|
||||
isLoading: false,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -73,14 +70,6 @@ export default class Login extends PureComponent {
|
|||
setMfaPreflightDone(false);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (this.props.loginRequest.status === RequestStatus.STARTED && nextProps.loginRequest.status === RequestStatus.SUCCESS) {
|
||||
this.props.actions.handleSuccessfulLogin().then(this.goToChannel);
|
||||
} else if (this.props.loginRequest.status !== nextProps.loginRequest.status && nextProps.loginRequest.status !== RequestStatus.STARTED) {
|
||||
this.setState({isLoading: false});
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
Dimensions.removeEventListener('change', this.orientationDidChange);
|
||||
}
|
||||
|
|
@ -100,7 +89,7 @@ export default class Login extends PureComponent {
|
|||
const screen = 'MFA';
|
||||
const title = intl.formatMessage({id: 'mobile.routes.mfa', defaultMessage: 'Multi-factor Authentication'});
|
||||
|
||||
goToScreen(screen, title);
|
||||
goToScreen(screen, title, {onMfaComplete: this.checkLoginResponse});
|
||||
};
|
||||
|
||||
blur = () => {
|
||||
|
|
@ -178,16 +167,32 @@ export default class Login extends PureComponent {
|
|||
};
|
||||
|
||||
signIn = () => {
|
||||
const {actions, loginId, loginRequest, password} = this.props;
|
||||
if (loginRequest.status !== RequestStatus.STARTED) {
|
||||
actions.login(loginId.toLowerCase(), password).then(this.checkLoginResponse);
|
||||
const {actions, loginId, password} = this.props;
|
||||
const {isLoading} = this.state;
|
||||
if (isLoading) {
|
||||
actions.login(loginId.toLowerCase(), password).
|
||||
then(this.checkLoginResponse);
|
||||
}
|
||||
};
|
||||
|
||||
checkLoginResponse = (data) => {
|
||||
if (mfaExpectedErrors.includes(data?.error?.server_error_id)) { // eslint-disable-line camelcase
|
||||
this.goToMfa();
|
||||
this.setState({isLoading: false});
|
||||
return false;
|
||||
}
|
||||
|
||||
if (data?.error) {
|
||||
this.setState({
|
||||
error: this.getLoginErrorMessage(data.error),
|
||||
isLoading: false,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
this.setState({isLoading: false});
|
||||
resetToChannel();
|
||||
return true;
|
||||
};
|
||||
|
||||
createLoginPlaceholder() {
|
||||
|
|
@ -223,15 +228,14 @@ export default class Login extends PureComponent {
|
|||
return '';
|
||||
}
|
||||
|
||||
getLoginErrorMessage = () => {
|
||||
getLoginErrorMessage = (error) => {
|
||||
return (
|
||||
this.getServerErrorForLogin() ||
|
||||
this.getServerErrorForLogin(error) ||
|
||||
this.state.error
|
||||
);
|
||||
};
|
||||
|
||||
getServerErrorForLogin = () => {
|
||||
const {error} = this.props.loginRequest;
|
||||
getServerErrorForLogin = (error) => {
|
||||
if (!error) {
|
||||
return null;
|
||||
}
|
||||
|
|
@ -295,7 +299,7 @@ export default class Login extends PureComponent {
|
|||
}
|
||||
|
||||
render() {
|
||||
const isLoading = this.props.loginRequest.status === RequestStatus.STARTED || this.state.isLoading;
|
||||
const {isLoading} = this.state;
|
||||
|
||||
let proceed;
|
||||
if (isLoading) {
|
||||
|
|
@ -373,7 +377,7 @@ export default class Login extends PureComponent {
|
|||
defaultMessage='All team communication in one place, searchable and accessible anywhere'
|
||||
/>
|
||||
</View>
|
||||
<ErrorText error={this.getLoginErrorMessage()}/>
|
||||
<ErrorText error={this.state.error}/>
|
||||
<TextInput
|
||||
ref={this.loginRef}
|
||||
value={this.props.loginId}
|
||||
|
|
|
|||
|
|
@ -3,8 +3,6 @@
|
|||
|
||||
import React from 'react';
|
||||
|
||||
import {RequestStatus} from 'mattermost-redux/constants';
|
||||
|
||||
import FormattedText from 'app/components/formatted_text';
|
||||
|
||||
import {shallowWithIntl} from 'test/intl-test-helper';
|
||||
|
|
@ -79,43 +77,6 @@ describe('Login', () => {
|
|||
expect(wrapper.find(FormattedText).find({id: 'login.forgot'}).exists()).toBe(false);
|
||||
});
|
||||
|
||||
test('should send the user to the login screen after login', (done) => {
|
||||
const resetToChannel = jest.spyOn(NavigationActions, 'resetToChannel').mockImplementation(() => done());
|
||||
|
||||
let props = {
|
||||
...baseProps,
|
||||
loginRequest: {
|
||||
status: RequestStatus.NOT_STARTED,
|
||||
},
|
||||
};
|
||||
|
||||
props.actions.handleSuccessfulLogin.mockImplementation(() => Promise.resolve());
|
||||
|
||||
const wrapper = shallowWithIntl(<Login {...props}/>);
|
||||
|
||||
expect(resetToChannel).not.toHaveBeenCalled();
|
||||
|
||||
props = {
|
||||
...props,
|
||||
loginRequest: {
|
||||
status: RequestStatus.STARTED,
|
||||
},
|
||||
};
|
||||
wrapper.setProps(props);
|
||||
|
||||
expect(resetToChannel).not.toHaveBeenCalled();
|
||||
|
||||
props = {
|
||||
...props,
|
||||
loginRequest: {
|
||||
status: RequestStatus.SUCCESS,
|
||||
},
|
||||
};
|
||||
wrapper.setProps(props);
|
||||
|
||||
// This test times out if resetToChannel hasn't been called
|
||||
});
|
||||
|
||||
test('should go to MFA screen when login response returns MFA error', () => {
|
||||
const goToScreen = jest.spyOn(NavigationActions, 'goToScreen');
|
||||
|
||||
|
|
@ -132,6 +93,7 @@ describe('Login', () => {
|
|||
toHaveBeenCalledWith(
|
||||
'MFA',
|
||||
'Multi-factor Authentication',
|
||||
{onMfaComplete: wrapper.instance().checkLoginResponse},
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -4,17 +4,15 @@
|
|||
import {bindActionCreators} from 'redux';
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {login} from 'mattermost-redux/actions/users';
|
||||
import {login} from 'app/actions/views/user';
|
||||
|
||||
import Mfa from './mfa';
|
||||
|
||||
function mapStateToProps(state) {
|
||||
const {login: loginRequest} = state.requests.users;
|
||||
const {loginId, password} = state.views.login;
|
||||
return {
|
||||
loginId,
|
||||
password,
|
||||
loginRequest,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -15,8 +15,6 @@ import {
|
|||
} from 'react-native';
|
||||
import Button from 'react-native-button';
|
||||
|
||||
import {RequestStatus} from 'mattermost-redux/constants';
|
||||
|
||||
import ErrorText from 'app/components/error_text';
|
||||
import FormattedText from 'app/components/formatted_text';
|
||||
import StatusBar from 'app/components/status_bar';
|
||||
|
|
@ -34,7 +32,7 @@ export default class Mfa extends PureComponent {
|
|||
}).isRequired,
|
||||
loginId: PropTypes.string.isRequired,
|
||||
password: PropTypes.string.isRequired,
|
||||
loginRequest: PropTypes.object.isRequired,
|
||||
onMfaComplete: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
|
|
@ -43,6 +41,7 @@ export default class Mfa extends PureComponent {
|
|||
this.state = {
|
||||
token: '',
|
||||
error: null,
|
||||
isLoading: false,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -52,14 +51,6 @@ export default class Mfa extends PureComponent {
|
|||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
// In case the login is successful the previous scene (login) will take care of the transition
|
||||
if (prevProps.loginRequest.status === RequestStatus.STARTED &&
|
||||
this.props.loginRequest.status === RequestStatus.FAILURE) {
|
||||
popTopScreen();
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (Platform.OS === 'android') {
|
||||
Keyboard.removeListener('keyboardDidHide', this.handleAndroidKeyboard);
|
||||
|
|
@ -86,8 +77,11 @@ export default class Mfa extends PureComponent {
|
|||
};
|
||||
|
||||
submit = preventDoubleTap(() => {
|
||||
const {actions, loginId, password, onMfaComplete} = this.props;
|
||||
const {token} = this.state;
|
||||
|
||||
Keyboard.dismiss();
|
||||
if (!this.state.token) {
|
||||
if (!token) {
|
||||
this.setState({
|
||||
error: {
|
||||
intl: {
|
||||
|
|
@ -99,11 +93,17 @@ export default class Mfa extends PureComponent {
|
|||
return;
|
||||
}
|
||||
setMfaPreflightDone(true);
|
||||
this.props.actions.login(this.props.loginId, this.props.password, this.state.token);
|
||||
this.setState({isLoading: true});
|
||||
actions.login(loginId, password, token).then(() => {
|
||||
if (!onMfaComplete()) {
|
||||
popTopScreen();
|
||||
}
|
||||
this.setState({isLoading: false});
|
||||
});
|
||||
});
|
||||
|
||||
render() {
|
||||
const isLoading = this.props.loginRequest.status === RequestStatus.STARTED;
|
||||
const {isLoading} = this.state;
|
||||
|
||||
let proceed;
|
||||
if (isLoading) {
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import KeyboardLayout from 'app/components/layout/keyboard_layout';
|
|||
import Loading from 'app/components/loading';
|
||||
import SearchBar from 'app/components/search_bar';
|
||||
import StatusBar from 'app/components/status_bar';
|
||||
import {NavigationTypes} from 'app/constants';
|
||||
import {alertErrorWithFallback, emptyFunction} from 'app/utils/general';
|
||||
import {goToScreen, dismissModal, setButtons} from 'app/actions/navigation';
|
||||
import {
|
||||
|
|
@ -271,7 +272,7 @@ export default class MoreChannels extends PureComponent {
|
|||
}
|
||||
await actions.handleSelectChannel(id);
|
||||
|
||||
EventEmitter.emit('close_channel_drawer');
|
||||
EventEmitter.emit(NavigationTypes.CLOSE_MAIN_SIDEBAR);
|
||||
requestAnimationFrame(() => {
|
||||
this.close();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import KeyboardLayout from 'app/components/layout/keyboard_layout';
|
|||
import Loading from 'app/components/loading';
|
||||
import SearchBar from 'app/components/search_bar';
|
||||
import StatusBar from 'app/components/status_bar';
|
||||
import {NavigationTypes} from 'app/constants';
|
||||
import {alertErrorWithFallback} from 'app/utils/general';
|
||||
import {createProfilesSections, loadingText} from 'app/utils/member_list';
|
||||
import {
|
||||
|
|
@ -321,7 +322,7 @@ export default class MoreDirectMessages extends PureComponent {
|
|||
}
|
||||
|
||||
if (success) {
|
||||
EventEmitter.emit('close_channel_drawer');
|
||||
EventEmitter.emit(NavigationTypes.CLOSE_MAIN_SIDEBAR);
|
||||
requestAnimationFrame(() => {
|
||||
this.close();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -21,11 +21,11 @@ import {isDirectChannel} from 'mattermost-redux/utils/channel_utils';
|
|||
import EventEmitter from 'mattermost-redux/utils/event_emitter';
|
||||
import {displayUsername} from 'mattermost-redux/utils/user_utils';
|
||||
|
||||
import {popToRoot, dismissAllModals, dismissOverlay} from 'app/actions/navigation';
|
||||
import FormattedText from 'app/components/formatted_text';
|
||||
import ProfilePicture from 'app/components/profile_picture';
|
||||
import {changeOpacity} from 'app/utils/theme';
|
||||
import {NavigationTypes} from 'app/constants';
|
||||
import {popToRoot, dismissAllModals, dismissOverlay} from 'app/actions/navigation';
|
||||
import {changeOpacity} from 'app/utils/theme';
|
||||
|
||||
import logo from 'assets/images/icon.png';
|
||||
import webhookIcon from 'assets/images/icons/webhook.jpg';
|
||||
|
|
@ -143,8 +143,8 @@ export default class Notification extends PureComponent {
|
|||
|
||||
const {actions, notification} = this.props;
|
||||
|
||||
EventEmitter.emit('close_channel_drawer');
|
||||
EventEmitter.emit('close_settings_sidebar');
|
||||
EventEmitter.emit(NavigationTypes.CLOSE_MAIN_SIDEABR);
|
||||
EventEmitter.emit(NavigationTypes.CLOSE_SETTINGS_SIDEBAR);
|
||||
InteractionManager.runAfterInteractions(() => {
|
||||
this.dismissOverlay();
|
||||
if (!notification.localNotification) {
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ import StatusBar from 'app/components/status_bar';
|
|||
import CustomList from 'app/components/custom_list';
|
||||
import {paddingHorizontal as padding} from 'app/components/safe_area_view/iphone_x_spacing';
|
||||
import TeamIcon from 'app/components/team_icon';
|
||||
import {NavigationTypes} from 'app/constants';
|
||||
import {resetToChannel, dismissModal} from 'app/actions/navigation';
|
||||
import {preventDoubleTap} from 'app/utils/tap';
|
||||
import {changeOpacity, makeStyleSheetFromTheme, setNavigatorStyles} from 'app/utils/theme';
|
||||
|
|
@ -158,7 +159,7 @@ export default class SelectTeam extends PureComponent {
|
|||
if (userWithoutTeams) {
|
||||
this.goToChannelView();
|
||||
} else {
|
||||
EventEmitter.emit('close_channel_drawer');
|
||||
EventEmitter.emit(NavigationTypes.CLOSE_MAIN_SIDEBAR);
|
||||
InteractionManager.runAfterInteractions(() => {
|
||||
this.close();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -4,10 +4,7 @@
|
|||
import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {intlShape} from 'react-intl';
|
||||
import {
|
||||
Platform,
|
||||
View,
|
||||
} from 'react-native';
|
||||
import {Platform, View} from 'react-native';
|
||||
|
||||
import {DeviceTypes} from 'app/constants';
|
||||
import StatusBar from 'app/components/status_bar';
|
||||
|
|
@ -111,7 +108,7 @@ export default class DisplaySettings extends PureComponent {
|
|||
}
|
||||
|
||||
let sidebar;
|
||||
if (DeviceTypes.IS_TABLET) {
|
||||
if (DeviceTypes.IS_TABLET && Platform.OS === 'ios') {
|
||||
sidebar = (
|
||||
<SettingsItem
|
||||
defaultMessage='Sidebar'
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import {shallow} from 'enzyme';
|
|||
import Preferences from 'mattermost-redux/constants/preferences';
|
||||
|
||||
import {DeviceTypes} from 'app/constants';
|
||||
import MainSidebar from 'app/components/sidebars/main/main_sidebar';
|
||||
import MainSidebar from 'app/components/sidebars/main/main_sidebar.ios';
|
||||
import SidebarSettings from './index';
|
||||
|
||||
jest.mock('react-intl');
|
||||
|
|
|
|||
|
|
@ -4,12 +4,11 @@
|
|||
import {bindActionCreators} from 'redux';
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {handleSuccessfulLogin, scheduleExpiredNotification} from 'app/actions/views/login';
|
||||
import {scheduleExpiredNotification} from 'app/actions/views/login';
|
||||
import {ssoLogin} from 'app/actions/views/user';
|
||||
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {isLandscape} from 'app/selectors/device';
|
||||
|
||||
import {setStoreFromLocalData} from 'mattermost-redux/actions/general';
|
||||
|
||||
import SSO from './sso';
|
||||
|
||||
function mapStateToProps(state) {
|
||||
|
|
@ -24,8 +23,7 @@ function mapDispatchToProps(dispatch) {
|
|||
return {
|
||||
actions: bindActionCreators({
|
||||
scheduleExpiredNotification,
|
||||
handleSuccessfulLogin,
|
||||
setStoreFromLocalData,
|
||||
ssoLogin,
|
||||
}, dispatch),
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -61,16 +61,15 @@ const oneLoginFormScalingJS = `
|
|||
|
||||
class SSO extends PureComponent {
|
||||
static propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
theme: PropTypes.object,
|
||||
serverUrl: PropTypes.string.isRequired,
|
||||
ssoType: PropTypes.string.isRequired,
|
||||
actions: PropTypes.shape({
|
||||
scheduleExpiredNotification: PropTypes.func.isRequired,
|
||||
handleSuccessfulLogin: PropTypes.func.isRequired,
|
||||
setStoreFromLocalData: PropTypes.func.isRequired,
|
||||
ssoLogin: PropTypes.func.isRequired,
|
||||
}).isRequired,
|
||||
intl: intlShape.isRequired,
|
||||
isLandscape: PropTypes.bool.isRequired,
|
||||
serverUrl: PropTypes.string.isRequired,
|
||||
ssoType: PropTypes.string.isRequired,
|
||||
theme: PropTypes.object,
|
||||
};
|
||||
|
||||
useWebkit = true;
|
||||
|
|
@ -158,15 +157,17 @@ class SSO extends PureComponent {
|
|||
if (token) {
|
||||
this.setState({renderWebView: false});
|
||||
const {
|
||||
handleSuccessfulLogin,
|
||||
setStoreFromLocalData,
|
||||
ssoLogin,
|
||||
} = this.props.actions;
|
||||
|
||||
Client4.setToken(token);
|
||||
setStoreFromLocalData({url: Client4.getUrl(), token}).
|
||||
then(handleSuccessfulLogin).
|
||||
then(this.goToChannel).
|
||||
catch(this.onLoadEndError);
|
||||
ssoLogin(token).then((result) => {
|
||||
if (result.error) {
|
||||
this.onLoadEndError(result.error);
|
||||
return;
|
||||
}
|
||||
this.goToChannel();
|
||||
});
|
||||
} else if (this.webView && !this.state.error) {
|
||||
this.webView.injectJavaScript(postMessageJS);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import {createTransform, persistStore} from 'redux-persist';
|
|||
import merge from 'deepmerge';
|
||||
|
||||
import {ErrorTypes, GeneralTypes} from 'mattermost-redux/action_types';
|
||||
import {General, RequestStatus} from 'mattermost-redux/constants';
|
||||
import {General} from 'mattermost-redux/constants';
|
||||
import {getConfig} from 'mattermost-redux/selectors/entities/general';
|
||||
import configureStore from 'mattermost-redux/store';
|
||||
import EventEmitter from 'mattermost-redux/utils/event_emitter';
|
||||
|
|
@ -62,7 +62,7 @@ export default function configureAppStore(initialState) {
|
|||
['typing'],
|
||||
);
|
||||
|
||||
const channelViewBlackList = {loading: true, refreshing: true, loadingPosts: true, postVisibility: true, retryFailed: true, loadMorePostsVisible: true};
|
||||
const channelViewBlackList = {loading: true, refreshing: true, loadingPosts: true, retryFailed: true, loadMorePostsVisible: true};
|
||||
const channelViewBlackListFilter = createTransform(
|
||||
(inboundState) => {
|
||||
const channel = {};
|
||||
|
|
@ -206,39 +206,7 @@ export default function configureAppStore(initialState) {
|
|||
setSiteUrl(config.SiteURL);
|
||||
}
|
||||
|
||||
if ((state.requests.users.logout.status === RequestStatus.SUCCESS || state.requests.users.logout.status === RequestStatus.FAILURE) && !purging) {
|
||||
purging = true;
|
||||
|
||||
await persistor.purge();
|
||||
|
||||
store.dispatch(batchActions([
|
||||
{
|
||||
type: General.OFFLINE_STORE_RESET,
|
||||
data: initialState,
|
||||
},
|
||||
{
|
||||
type: General.STORE_REHYDRATION_COMPLETE,
|
||||
},
|
||||
{
|
||||
type: ViewTypes.SERVER_URL_CHANGED,
|
||||
serverUrl: state.entities.general.credentials.url || state.views.selectServer.serverUrl,
|
||||
},
|
||||
{
|
||||
type: GeneralTypes.RECEIVED_APP_DEVICE_TOKEN,
|
||||
data: state.entities.general.deviceToken,
|
||||
},
|
||||
]));
|
||||
|
||||
// When logging out remove the data stored in the bucket
|
||||
mattermostBucket.removePreference('cert');
|
||||
mattermostBucket.removePreference('emm');
|
||||
mattermostBucket.removeFile('entities');
|
||||
setSiteUrl(null);
|
||||
|
||||
setTimeout(() => {
|
||||
purging = false;
|
||||
}, 500);
|
||||
} else if (state.views.root.purge && !purging) {
|
||||
if (state.views.root.purge && !purging) {
|
||||
purging = true;
|
||||
|
||||
await persistor.purge();
|
||||
|
|
|
|||
|
|
@ -14,3 +14,56 @@ export function isGroupChannelVisible(myPreferences, channel) {
|
|||
const gm = myPreferences[`${Preferences.CATEGORY_GROUP_CHANNEL_SHOW}--${channel.id}`];
|
||||
return gm && gm.value === 'true';
|
||||
}
|
||||
|
||||
export function isFavoriteChannel(preferences, channelId) {
|
||||
const fav = preferences[`${Preferences.CATEGORY_FAVORITE_CHANNEL}--${channelId}`];
|
||||
return fav ? fav.value === 'true' : false;
|
||||
}
|
||||
|
||||
// New to replace the ones above
|
||||
export function isDirectMessageVisible(preferences, channelId) {
|
||||
const dm = preferences[`${Preferences.CATEGORY_DIRECT_CHANNEL_SHOW}--${channelId}`];
|
||||
return dm ? dm.value === 'true' : true;
|
||||
}
|
||||
|
||||
export function isGroupMessageVisible(preferences, channelId) {
|
||||
const gm = preferences[`${Preferences.CATEGORY_GROUP_CHANNEL_SHOW}--${channelId}`];
|
||||
return gm ? gm.value === 'true' : true;
|
||||
}
|
||||
|
||||
export function isDirectChannelAutoClosed(config, preferences, channelId, channelActivity, channelArchiveTime = 0, currentChannelId = '') {
|
||||
// When the config is not set or is a favorite channel
|
||||
if (config.CloseUnusedDirectMessages !== 'true' || isFavoriteChannel(preferences, channelId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const cutoff = new Date().getTime() - (7 * 24 * 60 * 60 * 1000);
|
||||
const viewTimePref = preferences[`${Preferences.CATEGORY_CHANNEL_APPROXIMATE_VIEW_TIME}--${channelId}`];
|
||||
const viewTime = viewTimePref ? parseInt(viewTimePref.value || 0, 10) : 0;
|
||||
|
||||
if (viewTime > cutoff) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const openTimePref = preferences[`${Preferences.CATEGORY_CHANNEL_OPEN_TIME}--${channelId}`];
|
||||
const openTime = openTimePref ? parseInt(openTimePref.value || 0, 10) : 0;
|
||||
|
||||
// Only close archived channels when not being viewed
|
||||
if (channelId !== currentChannelId && channelArchiveTime && channelArchiveTime > openTime) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const autoClose = preferences[`${Preferences.CATEGORY_SIDEBAR_SETTINGS}--close_unused_direct_messages`];
|
||||
if (!autoClose || autoClose.value === 'after_seven_days') {
|
||||
if (channelActivity && channelActivity > cutoff) {
|
||||
return false;
|
||||
}
|
||||
if (openTime > cutoff) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return !channelActivity || channelActivity < cutoff;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
11
app/utils/preferences.js
Normal file
11
app/utils/preferences.js
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
export function buildPreference(category, userId, name, value = 'true') {
|
||||
return {
|
||||
user_id: userId,
|
||||
category,
|
||||
name,
|
||||
value,
|
||||
};
|
||||
}
|
||||
|
|
@ -17,7 +17,7 @@ import {
|
|||
} from 'app/actions/views/root';
|
||||
import {dismissAllModals, popToRoot} from 'app/actions/navigation';
|
||||
|
||||
import {ViewTypes} from 'app/constants';
|
||||
import {NavigationTypes, ViewTypes} from 'app/constants';
|
||||
import {getLocalizedMessage} from 'app/i18n';
|
||||
import {getCurrentServerUrl, getAppCredentials} from 'app/init/credentials';
|
||||
import PushNotifications from 'app/push_notifications';
|
||||
|
|
@ -53,8 +53,8 @@ class PushNotificationUtils {
|
|||
// if we have a componentId means that the app is already initialized
|
||||
const componentId = EphemeralStore.getNavigationTopComponentId();
|
||||
if (componentId) {
|
||||
EventEmitter.emit('close_channel_drawer');
|
||||
EventEmitter.emit('close_settings_sidebar');
|
||||
EventEmitter.emit(NavigationTypes.CLOSE_MAIN_SIDEBAR);
|
||||
EventEmitter.emit(NavigationTypes.CLOSE_SETTINGS_SIDEBAR);
|
||||
|
||||
await dismissAllModals();
|
||||
await popToRoot();
|
||||
|
|
@ -80,7 +80,7 @@ class PushNotificationUtils {
|
|||
if (foreground) {
|
||||
EventEmitter.emit(ViewTypes.NOTIFICATION_IN_APP, notification);
|
||||
} else if (userInteraction && !notification?.data?.localNotification) {
|
||||
EventEmitter.emit('close_channel_drawer');
|
||||
EventEmitter.emit(NavigationTypes.CLOSE_MAIN_SIDEBAR);
|
||||
if (getState().views.root.hydrationComplete) { //TODO: Replace when realm is ready
|
||||
setTimeout(() => {
|
||||
this.loadFromNotification(notification);
|
||||
|
|
|
|||
4
package-lock.json
generated
4
package-lock.json
generated
|
|
@ -9974,8 +9974,8 @@
|
|||
}
|
||||
},
|
||||
"mattermost-redux": {
|
||||
"version": "github:mattermost/mattermost-redux#a0e7a1cbedb2ec051e0d2411fd47ee19bb49d709",
|
||||
"from": "github:mattermost/mattermost-redux#a0e7a1cbedb2ec051e0d2411fd47ee19bb49d709",
|
||||
"version": "github:mattermost/mattermost-redux#d7e1109753a3cc1a4e79e8dfe34e149625e9443c",
|
||||
"from": "github:mattermost/mattermost-redux#d7e1109753a3cc1a4e79e8dfe34e149625e9443c",
|
||||
"requires": {
|
||||
"core-js": "3.1.4",
|
||||
"form-data": "2.5.1",
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@
|
|||
"intl": "1.2.5",
|
||||
"jail-monkey": "2.3.1",
|
||||
"jsc-android": "241213.2.0",
|
||||
"mattermost-redux": "github:mattermost/mattermost-redux#a0e7a1cbedb2ec051e0d2411fd47ee19bb49d709",
|
||||
"mattermost-redux": "github:mattermost/mattermost-redux#d7e1109753a3cc1a4e79e8dfe34e149625e9443c",
|
||||
"mime-db": "1.43.0",
|
||||
"moment-timezone": "0.5.27",
|
||||
"prop-types": "15.7.2",
|
||||
|
|
|
|||
|
|
@ -156,7 +156,6 @@ jest.mock('app/actions/navigation', () => ({
|
|||
showModal: jest.fn(),
|
||||
showModalOverCurrentContext: jest.fn(),
|
||||
showSearchModal: jest.fn(),
|
||||
peek: jest.fn(),
|
||||
setButtons: jest.fn(),
|
||||
showOverlay: jest.fn(),
|
||||
mergeNavigationOptions: jest.fn(),
|
||||
|
|
@ -195,12 +194,6 @@ beforeEach(() => {
|
|||
errors = [];
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (logs.length > 0 || warns.length > 0 || errors.length > 0) {
|
||||
throw new Error('Unexpected console logs' + logs + warns + errors);
|
||||
}
|
||||
});
|
||||
|
||||
jest.mock('rn-fetch-blob', () => ({
|
||||
fs: {
|
||||
dirs: {
|
||||
|
|
|
|||
Loading…
Reference in a new issue