diff --git a/app/actions/navigation/index.js b/app/actions/navigation/index.js
index cfbacdb44..48ae3da72 100644
--- a/app/actions/navigation/index.js
+++ b/app/actions/navigation/index.js
@@ -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},
+ },
+ });
+}
diff --git a/app/actions/navigation/index.test.js b/app/actions/navigation/index.test.js
index cdb5d93cc..ad0cf0014 100644
--- a/app/actions/navigation/index.test.js
+++ b/app/actions/navigation/index.test.js
@@ -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),
diff --git a/app/actions/views/channel.js b/app/actions/views/channel.js
index be1451f93..bc5c4e1cd 100644
--- a/app/actions/views/channel.js
+++ b/app/actions/views/channel.js
@@ -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;
+}
\ No newline at end of file
diff --git a/app/actions/views/channel.test.js b/app/actions/views/channel.test.js
index ff8e8269b..de48887e8 100644
--- a/app/actions/views/channel.test.js
+++ b/app/actions/views/channel.test.js
@@ -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);
});
diff --git a/app/actions/views/root.js b/app/actions/views/root.js
index 6bed1877d..8778248c3 100644
--- a/app/actions/views/root.js
+++ b/app/actions/views/root.js
@@ -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: {}});
}
diff --git a/app/actions/views/user.js b/app/actions/views/user.js
index ef631e891..1cd9a9e1e 100644
--- a/app/actions/views/user.js
+++ b/app/actions/views/user.js
@@ -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;
\ No newline at end of file
diff --git a/app/components/emoji/emoji.js b/app/components/emoji/emoji.js
index 06fae7006..9ef918112 100644
--- a/app/components/emoji/emoji.js
+++ b/app/components/emoji/emoji.js
@@ -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 (
-
}
diff --git a/app/components/message_attachments/message_attachment.js b/app/components/message_attachments/message_attachment.js
index 143fc63dc..1c750b9b0 100644
--- a/app/components/message_attachments/message_attachment.js
+++ b/app/components/message_attachments/message_attachment.js
@@ -95,6 +95,7 @@ export default class MessageAttachment extends PureComponent {
onPermalinkPress={onPermalinkPress}
textStyles={textStyles}
value={attachment.text}
+ theme={theme}
/>
-
+ <>
-
+ >
);
diff --git a/app/components/post_body/post_body.js b/app/components/post_body/post_body.js
index 0ddd542f0..c7da95d09 100644
--- a/app/components/post_body/post_body.js
+++ b/app/components/post_body/post_body.js
@@ -431,6 +431,7 @@ export default class PostBody extends PureComponent {
}
{this.renderPostAdditionalContent(blockStyles, messageStyle, textStyles)}
diff --git a/app/components/post_body/post_body.test.js b/app/components/post_body/post_body.test.js
index c8cb6d92a..ac62b6db6 100644
--- a/app/components/post_body/post_body.test.js
+++ b/app/components/post_body/post_body.test.js
@@ -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);
diff --git a/app/components/post_list/__snapshots__/post_list.test.js.snap b/app/components/post_list/__snapshots__/post_list.test.js.snap
index 353451061..81847c7eb 100644
--- a/app/components/post_list/__snapshots__/post_list.test.js.snap
+++ b/app/components/post_list/__snapshots__/post_list.test.js.snap
@@ -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}
/>
`;
diff --git a/app/components/post_list/post_list.js b/app/components/post_list/post_list.js
index d588c34c9..1876233b5 100644
--- a/app/components/post_list/post_list.js
+++ b/app/components/post_list/post_list.js
@@ -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 {
);
}
diff --git a/app/components/post_list/post_list.test.js b/app/components/post_list/post_list.test.js
index 9f4deca49..eb33aee35 100644
--- a/app/components/post_list/post_list.test.js
+++ b/app/components/post_list/post_list.test.js
@@ -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);
diff --git a/app/components/show_more_button/index.js b/app/components/show_more_button/index.js
index 0002f53a2..010d772ad 100644
--- a/app/components/show_more_button/index.js
+++ b/app/components/show_more_button/index.js
@@ -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 (
+
+ {sign}
+
+
+ );
+ }
+
+ 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 (
+
+ {showMore &&
+
+ }
+
+
+
+ {this.renderButton(showMore, style)}
+
+
+
+
+ );
+ }
}
-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,
+ },
+ };
+});
diff --git a/app/components/show_more_button/show_more_button.js b/app/components/show_more_button/show_more_button.js
deleted file mode 100644
index 010d772ad..000000000
--- a/app/components/show_more_button/show_more_button.js
+++ /dev/null
@@ -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 (
-
- {sign}
-
-
- );
- }
-
- 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 (
-
- {showMore &&
-
- }
-
-
-
- {this.renderButton(showMore, style)}
-
-
-
-
- );
- }
-}
-
-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,
- },
- };
-});
diff --git a/app/components/show_more_button/show_more_button.test.js b/app/components/show_more_button/show_more_button.test.js
index 1903c77fb..145b419f6 100644
--- a/app/components/show_more_button/show_more_button.test.js
+++ b/app/components/show_more_button/show_more_button.test.js
@@ -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 = {
diff --git a/app/components/sidebars/drawer_layout.js b/app/components/sidebars/drawer_layout.js
index 3275fbfb2..b4f4f3691 100644
--- a/app/components/sidebars/drawer_layout.js
+++ b/app/components/sidebars/drawer_layout.js
@@ -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 (
@@ -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) {
diff --git a/app/components/sidebars/main/channels_list/channels_list.js b/app/components/sidebars/main/channels_list/channels_list.js
index 10b38e5e9..01f03acf1 100644
--- a/app/components/sidebars/main/channels_list/channels_list.js
+++ b/app/components/sidebars/main/channels_list/channels_list.js
@@ -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,
};
diff --git a/app/components/sidebars/main/drawer_swiper/drawer_swiper.js b/app/components/sidebars/main/drawer_swiper/drawer_swiper.js
index cdbdbe0a7..67c8a93c8 100644
--- a/app/components/sidebars/main/drawer_swiper/drawer_swiper.js
+++ b/app/components/sidebars/main/drawer_swiper/drawer_swiper.js
@@ -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) => {
diff --git a/app/components/sidebars/main/index.js b/app/components/sidebars/main/index.js
index 0dd39209a..5c252fd45 100644
--- a/app/components/sidebars/main/index.js
+++ b/app/components/sidebars/main/index.js
@@ -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),
};
}
diff --git a/app/components/sidebars/main/main_sidebar.android.js b/app/components/sidebars/main/main_sidebar.android.js
new file mode 100644
index 000000000..1652eb7ce
--- /dev/null
+++ b/app/components/sidebars/main/main_sidebar.android.js
@@ -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 (
+
+ {this.renderNavigationView(this.state.deviceWidth)}
+
+ );
+ }
+}
diff --git a/app/components/sidebars/main/main_sidebar.ios.js b/app/components/sidebars/main/main_sidebar.ios.js
new file mode 100644
index 000000000..96ede9833
--- /dev/null
+++ b/app/components/sidebars/main/main_sidebar.ios.js
@@ -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 (
+
+ {children}
+
+ );
+ }
+}
\ No newline at end of file
diff --git a/app/components/sidebars/main/main_sidebar.js b/app/components/sidebars/main/main_sidebar.js
deleted file mode 100644
index 28bc96b92..000000000
--- a/app/components/sidebars/main/main_sidebar.js
+++ /dev/null
@@ -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 = (
-
-
-
- );
- lists.push(teamsList);
- }
-
- lists.push(
-
-
- ,
- );
-
- return (
-
-
- {lists}
-
-
- );
- };
-
- 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 (
-
- {children}
-
- );
- }
-}
-
-const style = StyleSheet.create({
- swiperContent: {
- flex: 1,
- },
-});
diff --git a/app/components/sidebars/main/main_sidebar.test.js b/app/components/sidebars/main/main_sidebar.test.js
index 584dc0734..31759ac14 100644
--- a/app/components/sidebars/main/main_sidebar.test.js
+++ b/app/components/sidebars/main/main_sidebar.test.js
@@ -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(
,
);
@@ -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(
- ,
- );
- const drawer = wrapper.dive().childAt(1);
- const drawerStyle = drawer.props().style.reduce((acc, obj) => ({...acc, ...obj}));
- expect(drawerStyle).toHaveProperty('zIndex', 3);
- });
});
diff --git a/app/components/sidebars/main/main_sidebar_base.js b/app/components/sidebars/main/main_sidebar_base.js
new file mode 100644
index 000000000..c1fb66983
--- /dev/null
+++ b/app/components/sidebars/main/main_sidebar_base.js
@@ -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 = (
+
+
+
+ );
+ lists.push(teamsList);
+ }
+
+ lists.push(
+
+
+ ,
+ );
+
+ return (
+
+
+ {lists}
+
+
+ );
+ };
+
+ 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,
+ },
+});
\ No newline at end of file
diff --git a/app/components/sidebars/main/teams_list/teams_list.js b/app/components/sidebars/main/teams_list/teams_list.js
index f685e392f..4ee33802b 100644
--- a/app/components/sidebars/main/teams_list/teams_list.js
+++ b/app/components/sidebars/main/teams_list/teams_list.js
@@ -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();
});
};
diff --git a/app/components/sidebars/settings/index.js b/app/components/sidebars/settings/index.js
index db0d83db5..f404ca4cd 100644
--- a/app/components/sidebars/settings/index.js
+++ b/app/components/sidebars/settings/index.js
@@ -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),
};
diff --git a/app/components/sidebars/settings/settings_sidebar.android.js b/app/components/sidebars/settings/settings_sidebar.android.js
new file mode 100644
index 000000000..2f7118d3e
--- /dev/null
+++ b/app/components/sidebars/settings/settings_sidebar.android.js
@@ -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 (
+
+ {this.renderOptions(style)}
+
+ );
+ };
+
+ setProviderRef = (ref) => {
+ this.providerRef = ref;
+ }
+
+ render() {
+ const locale = this.props.locale;
+
+ if (!locale) {
+ return null;
+ }
+
+ return (
+
+ {this.renderNavigationView()}
+
+ );
+ }
+}
+
+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,
+ },
+ };
+});
diff --git a/app/components/sidebars/settings/settings_sidebar.ios.js b/app/components/sidebars/settings/settings_sidebar.ios.js
new file mode 100644
index 000000000..7455f7c0d
--- /dev/null
+++ b/app/components/sidebars/settings/settings_sidebar.ios.js
@@ -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 (
+ }
+ headerComponent={}
+ theme={theme}
+ >
+ {this.renderOptions(style)}
+
+ );
+ };
+
+ render() {
+ const {children} = this.props;
+ const {deviceWidth, openDrawerOffset} = this.state;
+ const drawerWidth = DeviceTypes.IS_TABLET ? TABLET_WIDTH : (deviceWidth - openDrawerOffset);
+
+ return (
+
+ {children}
+
+ );
+ }
+}
+
+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,
+ },
+ };
+});
\ No newline at end of file
diff --git a/app/components/sidebars/settings/settings_sidebar.js b/app/components/sidebars/settings/settings_sidebar.js
deleted file mode 100644
index 0d6225658..000000000
--- a/app/components/sidebars/settings/settings_sidebar.js
+++ /dev/null
@@ -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 (
-
- );
- };
-
- renderUserStatusLabel = (userId) => {
- return (
-
- );
- };
-
- renderNavigationView = () => {
- const {currentUser, theme} = this.props;
- const style = getStyleSheet(theme);
-
- return (
- }
- headerComponent={}
- theme={theme}
- >
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- );
- };
-
- render() {
- const {children} = this.props;
- const {deviceWidth, openDrawerOffset} = this.state;
- const drawerWidth = DeviceTypes.IS_TABLET ? TABLET_WIDTH : (deviceWidth - openDrawerOffset);
-
- return (
-
- {children}
-
- );
- }
-}
-
-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,
- },
- };
-});
diff --git a/app/components/sidebars/settings/settings_sidebar.test.js b/app/components/sidebars/settings/settings_sidebar.test.js
deleted file mode 100644
index 8dc98a9d6..000000000
--- a/app/components/sidebars/settings/settings_sidebar.test.js
+++ /dev/null
@@ -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();
-
- 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);
- });
-});
diff --git a/app/components/sidebars/settings/settings_sidebar_base.js b/app/components/sidebars/settings/settings_sidebar_base.js
new file mode 100644
index 000000000..79e9b83cc
--- /dev/null
+++ b/app/components/sidebars/settings/settings_sidebar_base.js
@@ -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 (
+
+ );
+ };
+
+ renderUserStatusLabel = (userId) => {
+ return (
+
+ );
+ };
+
+ renderOptions = (style) => {
+ const {currentUser, theme} = this.props;
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+ };
+
+ render() {
+ return; // eslint-disable-line no-useless-return
+ }
+}
diff --git a/app/constants/navigation.js b/app/constants/navigation.js
index 618dc5b3f..df458f39f 100644
--- a/app/constants/navigation.js
+++ b/app/constants/navigation.js
@@ -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;
diff --git a/app/constants/view.js b/app/constants/view.js
index 9bb077a31..60a186651 100644
--- a/app/constants/view.js
+++ b/app/constants/view.js
@@ -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,
diff --git a/app/mattermost.js b/app/mattermost.js
index 612a2edde..9e2fc2e28 100644
--- a/app/mattermost.js
+++ b/app/mattermost.js
@@ -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);
+ }
});
});
diff --git a/app/reducers/views/channel.js b/app/reducers/views/channel.js
index c54eba511..340c6e518 100644
--- a/app/reducers/views/channel.js
+++ b/app/reducers/views/channel.js
@@ -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,
diff --git a/app/reducers/views/channel.test.js b/app/reducers/views/channel.test.js
index 4adc02bf5..d2ae8b724 100644
--- a/app/reducers/views/channel.test.js
+++ b/app/reducers/views/channel.test.js
@@ -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,
- },
- });
- });
});
diff --git a/app/reducers/views/team.js b/app/reducers/views/team.js
index c89a89593..6eca847ca 100644
--- a/app/reducers/views/team.js
+++ b/app/reducers/views/team.js
@@ -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];
diff --git a/app/screens/channel/channel.android.js b/app/screens/channel/channel.android.js
index 73fde034f..1a8b2e7fb 100644
--- a/app/screens/channel/channel.android.js
+++ b/app/screens/channel/channel.android.js
@@ -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 = (
<>
@@ -37,14 +57,31 @@ export default class ChannelAndroid extends ChannelBase {
screenId={this.props.componentId}
/>
-
{LocalConfig.EnableMobileClientUpgrade && }
>
);
- return this.renderChannel(drawerContent);
+ return (
+ <>
+
+ {drawerContent}
+
+
+ >
+ );
}
}
+
+const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
+ return {
+ backdrop: {
+ flex: 1,
+ backgroundColor: theme.centerChannelBg,
+ },
+ flex: {
+ flex: 1,
+ },
+ };
+});
\ No newline at end of file
diff --git a/app/screens/channel/channel.ios.js b/app/screens/channel/channel.ios.js
index 9a92542fe..4fc33eaa1 100644
--- a/app/screens/channel/channel.ios.js
+++ b/app/screens/channel/channel.ios.js
@@ -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 = (
-
+ <>
-
{LocalConfig.EnableMobileClientUpgrade && }
-
+ >
);
- return this.renderChannel(drawerContent);
+ return (
+
+
+
+ {drawerContent}
+
+
+
+
+ );
}
}
+
+export const getStyle = makeStyleSheetFromTheme((theme) => ({
+ backdrop: {
+ flex: 1,
+ backgroundColor: theme.centerChannelBg,
+ },
+ flex: {
+ flex: 1,
+ },
+}));
\ No newline at end of file
diff --git a/app/screens/channel/channel_base.js b/app/screens/channel/channel_base.js
index 1889b52d2..22016084d 100644
--- a/app/screens/channel/channel_base.js
+++ b/app/screens/channel/channel_base.js
@@ -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 (
-
-
-
- {drawerContent}
-
-
-
-
- );
+ 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,
- },
});
diff --git a/app/screens/channel/channel_base.test.js b/app/screens/channel/channel_base.test.js
index 75406e91d..d18768649 100644
--- a/app/screens/channel/channel_base.test.js
+++ b/app/screens/channel/channel_base.test.js
@@ -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,
diff --git a/app/screens/channel/channel_nav_bar/__snapshots__/channel_nav_bar.test.js.snap b/app/screens/channel/channel_nav_bar/__snapshots__/channel_nav_bar.test.js.snap
index 329721d83..d7b345368 100644
--- a/app/screens/channel/channel_nav_bar/__snapshots__/channel_nav_bar.test.js.snap
+++ b/app/screens/channel/channel_nav_bar/__snapshots__/channel_nav_bar.test.js.snap
@@ -19,7 +19,7 @@ exports[`ChannelNavBar should match, full snapshot 1`] = `
}
>
`;
diff --git a/app/screens/channel/channel_nav_bar/channel_drawer_button/channel_drawer_button.js b/app/screens/channel/channel_nav_bar/channel_drawer_button/channel_drawer_button.js
index d1e9b7192..89bc63074 100644
--- a/app/screens/channel/channel_nav_bar/channel_drawer_button/channel_drawer_button.js
+++ b/app/screens/channel/channel_nav_bar/channel_drawer_button/channel_drawer_button.js
@@ -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() {
diff --git a/app/screens/channel/channel_nav_bar/channel_drawer_button/channel_drawer_button.test.js b/app/screens/channel/channel_nav_bar/channel_drawer_button/channel_drawer_button.test.js
index 45ab66b41..614b26653 100644
--- a/app/screens/channel/channel_nav_bar/channel_drawer_button/channel_drawer_button.test.js
+++ b/app/screens/channel/channel_nav_bar/channel_drawer_button/channel_drawer_button.test.js
@@ -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,
diff --git a/app/screens/channel/channel_nav_bar/channel_nav_bar.js b/app/screens/channel/channel_nav_bar/channel_nav_bar.js
index 731d9e2ea..a47d11728 100644
--- a/app/screens/channel/channel_nav_bar/channel_nav_bar.js
+++ b/app/screens/channel/channel_nav_bar/channel_nav_bar.js
@@ -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 (
-
+
);
}
diff --git a/app/screens/channel/channel_nav_bar/channel_nav_bar.test.js b/app/screens/channel/channel_nav_bar/channel_nav_bar.test.js
index d693f9206..88a63b1e6 100644
--- a/app/screens/channel/channel_nav_bar/channel_nav_bar.test.js
+++ b/app/screens/channel/channel_nav_bar/channel_nav_bar.test.js
@@ -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,
};
diff --git a/app/screens/channel/channel_nav_bar/settings_drawer_button.js b/app/screens/channel/channel_nav_bar/settings_drawer_button.js
index 6f0becaa1..2a2a52bf1 100644
--- a/app/screens/channel/channel_nav_bar/settings_drawer_button.js
+++ b/app/screens/channel/channel_nav_bar/settings_drawer_button.js
@@ -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() {
diff --git a/app/screens/channel/channel_post_list/channel_post_list.js b/app/screens/channel/channel_post_list/channel_post_list.js
index 752105eed..41428f84d 100644
--- a/app/screens/channel/channel_post_list/channel_post_list.js
+++ b/app/screens/channel/channel_post_list/channel_post_list.js
@@ -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 = (
);
}
diff --git a/app/screens/channel/channel_post_list/channel_post_list.test.js b/app/screens/channel/channel_post_list/channel_post_list.test.js
deleted file mode 100644
index 7ea4e9847..000000000
--- a/app/screens/channel/channel_post_list/channel_post_list.test.js
+++ /dev/null
@@ -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(
- ,
- );
-
- expect(baseProps.actions.increasePostVisibilityByOne).toHaveBeenCalledTimes(0);
-
- EventEmitter.emit(WebsocketEvents.INCREASE_POST_VISIBILITY_BY_ONE);
- expect(baseProps.actions.increasePostVisibilityByOne).toHaveBeenCalledWith(baseProps.channelId);
- });
-});
diff --git a/app/screens/channel/channel_post_list/index.js b/app/screens/channel/channel_post_list/index.js
index 97d15b748..5fe9097fb 100644
--- a/app/screens/channel/channel_post_list/index.js
+++ b/app/screens/channel/channel_post_list/index.js
@@ -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,
diff --git a/app/screens/channel/index.js b/app/screens/channel/index.js
index 228353389..638da338e 100644
--- a/app/screens/channel/index.js
+++ b/app/screens/channel/index.js
@@ -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,
diff --git a/app/screens/create_channel/create_channel.js b/app/screens/create_channel/create_channel.js
index 2a55c286d..36a790cc7 100644
--- a/app/screens/create_channel/create_channel.js
+++ b/app/screens/create_channel/create_channel.js
@@ -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});
diff --git a/app/screens/edit_channel/edit_channel.js b/app/screens/edit_channel/edit_channel.js
index e4b43ca9e..da5505f1b 100644
--- a/app/screens/edit_channel/edit_channel.js
+++ b/app/screens/edit_channel/edit_channel.js
@@ -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();
diff --git a/app/screens/index.js b/app/screens/index.js
index 8a61a7ac2..963e814a7 100644
--- a/app/screens/index.js
+++ b/app/screens/index.js
@@ -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);
+ }
}
diff --git a/app/screens/login/index.js b/app/screens/login/index.js
index 8b1165225..280365ee4 100644
--- a/app/screens/login/index.js
+++ b/app/screens/login/index.js
@@ -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),
diff --git a/app/screens/login/login.js b/app/screens/login/login.js
index 2b009d6e0..ebc4dd9a2 100644
--- a/app/screens/login/login.js
+++ b/app/screens/login/login.js
@@ -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'
/>
-
+
{
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();
-
- 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},
);
});
diff --git a/app/screens/mfa/index.js b/app/screens/mfa/index.js
index 4707c0bd2..caf034c41 100644
--- a/app/screens/mfa/index.js
+++ b/app/screens/mfa/index.js
@@ -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,
};
}
diff --git a/app/screens/mfa/mfa.js b/app/screens/mfa/mfa.js
index b76cf78ed..c10359416 100644
--- a/app/screens/mfa/mfa.js
+++ b/app/screens/mfa/mfa.js
@@ -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) {
diff --git a/app/screens/more_channels/more_channels.js b/app/screens/more_channels/more_channels.js
index 85a2e3c21..804f3cd5a 100644
--- a/app/screens/more_channels/more_channels.js
+++ b/app/screens/more_channels/more_channels.js
@@ -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();
});
diff --git a/app/screens/more_dms/more_dms.js b/app/screens/more_dms/more_dms.js
index cbdcda742..53c8d015b 100644
--- a/app/screens/more_dms/more_dms.js
+++ b/app/screens/more_dms/more_dms.js
@@ -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();
});
diff --git a/app/screens/notification/notification.js b/app/screens/notification/notification.js
index 00c504b64..6284b4b98 100644
--- a/app/screens/notification/notification.js
+++ b/app/screens/notification/notification.js
@@ -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) {
diff --git a/app/screens/select_team/select_team.js b/app/screens/select_team/select_team.js
index d2c65a731..cf9c5747d 100644
--- a/app/screens/select_team/select_team.js
+++ b/app/screens/select_team/select_team.js
@@ -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();
});
diff --git a/app/screens/settings/display_settings/display_settings.js b/app/screens/settings/display_settings/display_settings.js
index 2734a5971..c96822c87 100644
--- a/app/screens/settings/display_settings/display_settings.js
+++ b/app/screens/settings/display_settings/display_settings.js
@@ -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 = (
{
+ if (result.error) {
+ this.onLoadEndError(result.error);
+ return;
+ }
+ this.goToChannel();
+ });
} else if (this.webView && !this.state.error) {
this.webView.injectJavaScript(postMessageJS);
}
diff --git a/app/store/store.js b/app/store/store.js
index a3f11ec07..2e245ab6e 100644
--- a/app/store/store.js
+++ b/app/store/store.js
@@ -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();
diff --git a/app/utils/channels.js b/app/utils/channels.js
index dfffc71c0..8a4d8a3b0 100644
--- a/app/utils/channels.js
+++ b/app/utils/channels.js
@@ -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;
+}
\ No newline at end of file
diff --git a/app/utils/preferences.js b/app/utils/preferences.js
new file mode 100644
index 000000000..0883bd308
--- /dev/null
+++ b/app/utils/preferences.js
@@ -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,
+ };
+}
\ No newline at end of file
diff --git a/app/utils/push_notifications.js b/app/utils/push_notifications.js
index 2bb1e26d3..33b5c23e4 100644
--- a/app/utils/push_notifications.js
+++ b/app/utils/push_notifications.js
@@ -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);
diff --git a/package-lock.json b/package-lock.json
index efa100aea..776fddb54 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -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",
diff --git a/package.json b/package.json
index 3e470441a..f31587073 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/test/setup.js b/test/setup.js
index c246e182f..ed1962968 100644
--- a/test/setup.js
+++ b/test/setup.js
@@ -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: {