From 7a3c99c267a6c75c03b6d7c5617c0ae945cf7f44 Mon Sep 17 00:00:00 2001 From: enahum Date: Fri, 21 Apr 2017 14:20:52 -0300 Subject: [PATCH] Move to mattermost-redux master branch (#484) --- README.md | 4 +- app/actions/navigation/index.js | 6 +- app/actions/views/account_notifications.js | 8 +- app/actions/views/channel.js | 60 ++- app/actions/views/channel_members.js | 4 +- app/actions/views/file_upload.js | 5 +- app/actions/views/login.js | 8 +- app/actions/views/more_dms.js | 3 +- app/actions/views/select_team.js | 4 +- .../autocomplete/at_mention/at_mention.js | 507 +++++++++--------- .../autocomplete/at_mention/index.js | 13 +- .../channel_mention/channel_mention.js | 394 +++++++------- .../autocomplete/channel_mention/index.js | 17 +- app/components/autocomplete/index.js | 2 +- .../channel_drawer_list.js | 6 +- app/components/channel_drawer_list/index.js | 4 +- app/components/channel_icon.js | 12 +- app/components/error_text.js | 47 +- .../file_attachment_image.js | 14 +- app/components/post/index.js | 5 +- app/components/post/post.js | 21 +- app/components/post_list/load_more_posts.js | 34 +- app/components/post_list/post_list.js | 7 +- app/components/post_textbox/post_textbox.js | 11 +- .../profile_picture/profile_picture.js | 8 +- .../push_notification/push_notification.js | 4 +- app/components/root/root.js | 12 +- app/initial_state.js | 15 +- app/mattermost.js | 4 +- app/reducers/navigation/index.js | 4 +- app/reducers/views/channel.js | 6 +- app/reducers/views/connection.js | 4 +- app/reducers/views/i18n.js | 11 +- app/reducers/views/login.js | 6 +- app/reducers/views/options_modal.js | 8 +- app/reducers/views/root.js | 4 +- app/reducers/views/team.js | 4 +- app/reducers/views/thread.js | 6 +- .../account_notifications.js | 2 +- app/scenes/channel/channel.js | 8 +- .../channel_post_list/channel_post_list.js | 20 +- app/scenes/channel/channel_post_list/index.js | 6 +- .../channel_add_members.js | 58 +- app/scenes/channel_drawer/channel_drawer.js | 4 +- app/scenes/channel_info/channel_info.js | 28 +- app/scenes/channel_info/channel_info_row.js | 56 +- app/scenes/channel_info/index.js | 7 +- app/scenes/channel_members/channel_members.js | 67 ++- app/scenes/channel_members/index.js | 2 - app/scenes/create_channel/create_channel.js | 8 +- app/scenes/load_team/index.js | 2 +- app/scenes/more_channels/create_button.js | 4 +- app/scenes/more_channels/index.js | 10 +- app/scenes/more_channels/more_channels.js | 24 +- app/scenes/more_dms/more_dms.js | 20 +- app/scenes/root/root.js | 14 +- app/scenes/saml/saml.js | 4 +- app/scenes/select_server/select_server.js | 4 +- app/scenes/thread/index.js | 13 +- app/scenes/thread/thread.js | 2 - app/scenes/thread/thread_title.js | 4 +- app/store/index.js | 16 +- app/utils/url.js | 2 +- assets/base/config.json | 2 +- assets/base/themes.json | 4 + package.json | 3 +- 66 files changed, 880 insertions(+), 806 deletions(-) diff --git a/README.md b/README.md index 225ce73a6..3bda46620 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Mattermost Mobile (unreleased) -**Supported Server Versions:** Master (no released versions are currently supported) +**Supported Server Versions:** 3.8+ (no released versions are currently supported) This is an unreleased project for replacing the Mattermost iOS and Android apps with new mobile apps using React Native and Redux. The project is not yet stable, and the instructions are for internal use currently (i.e. probably out-of-date until we stablize). @@ -49,7 +49,7 @@ Follow the [React Native Getting Started Guide](https://facebook.github.io/react - General requirements - - XCode 8.1 + - XCode 8.3 - Install watchman $ brew install watchman diff --git a/app/actions/navigation/index.js b/app/actions/navigation/index.js index 8dfcbb164..5c03e0818 100644 --- a/app/actions/navigation/index.js +++ b/app/actions/navigation/index.js @@ -3,7 +3,7 @@ import {NavigationTypes} from 'app/constants'; import Routes from 'app/navigation/routes'; -import {Constants} from 'mattermost-redux/constants'; +import {General} from 'mattermost-redux/constants'; import {selectPost} from 'mattermost-redux/actions/posts'; export function closeDrawers() { @@ -90,11 +90,11 @@ export function goToCreateChannel(channelType) { let type; let route; switch (channelType) { - case Constants.OPEN_CHANNEL: + case General.OPEN_CHANNEL: type = NavigationTypes.NAVIGATION_PUSH; route = Routes.CreatePublicChannel; break; - case Constants.PRIVATE_CHANNEL: + case General.PRIVATE_CHANNEL: type = NavigationTypes.NAVIGATION_MODAL; route = Routes.CreatePrivateChannel; break; diff --git a/app/actions/views/account_notifications.js b/app/actions/views/account_notifications.js index eed033d23..9e15a19a8 100644 --- a/app/actions/views/account_notifications.js +++ b/app/actions/views/account_notifications.js @@ -1,7 +1,7 @@ // Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -import {updateUserNotifyProps} from 'mattermost-redux/actions/users'; +import {updateMe} from 'mattermost-redux/actions/users'; import {Preferences} from 'mattermost-redux/constants'; import {savePreferences} from 'mattermost-redux/actions/preferences'; @@ -9,6 +9,7 @@ export function handleUpdateUserNotifyProps(notifyProps) { return async (dispatch, getState) => { const state = getState(); const config = state.entities.general.config; + const {currentUserId} = state.entities.users; const {interval, ...otherProps} = notifyProps; @@ -21,9 +22,10 @@ export function handleUpdateUserNotifyProps(notifyProps) { value: interval }]; - savePreferences(emailInterval)(dispatch, getState); + savePreferences(currentUserId, emailInterval)(dispatch, getState); } - updateUserNotifyProps({...otherProps, email})(dispatch, getState); + const props = {...otherProps, email}; + updateMe({notify_props: props})(dispatch, getState); }; } diff --git a/app/actions/views/channel.js b/app/actions/views/channel.js index 04872de8f..1d39535b5 100644 --- a/app/actions/views/channel.js +++ b/app/actions/views/channel.js @@ -5,6 +5,7 @@ import {batchActions} from 'redux-batched-actions'; import {ViewTypes} from 'app/constants'; +import {UserTypes} from 'mattermost-redux/action_types'; import { fetchMyChannelsAndMembers, getChannelStats, @@ -17,7 +18,7 @@ import {getFilesForPost} from 'mattermost-redux/actions/files'; import {savePreferences, deletePreferences} from 'mattermost-redux/actions/preferences'; import {getTeamMembersByIds} from 'mattermost-redux/actions/teams'; import {getProfilesInChannel} from 'mattermost-redux/actions/users'; -import {Constants, Preferences, UsersTypes} from 'mattermost-redux/constants'; +import {General, Posts, Preferences} from 'mattermost-redux/constants'; import { getChannelByName, getDirectChannelName, @@ -27,6 +28,7 @@ import { isDirectChannel, isGroupChannel } from 'mattermost-redux/utils/channel_utils'; +import {getLastUpdateAt} from 'mattermost-redux/utils/post_utils'; import {getPreferencesByCategory} from 'mattermost-redux/utils/preference_utils'; export function loadChannelsIfNecessary(teamId) { @@ -100,7 +102,7 @@ export function loadProfilesAndTeamMembersForDMSidebar(teamId) { }); if (prefs.length) { - savePreferences(prefs)(dispatch, getState); + savePreferences(currentUserId, prefs)(dispatch, getState); } for (const [key, pref] of dmPrefs) { @@ -118,7 +120,7 @@ export function loadProfilesAndTeamMembersForDMSidebar(teamId) { if (loadProfilesForChannels.length) { for (let i = 0; i < loadProfilesForChannels.length; i++) { const channelId = loadProfilesForChannels[i]; - getProfilesInChannel(teamId, channelId, 0)(dispatch, getState); + getProfilesInChannel(channelId, 0)(dispatch, getState); } } @@ -137,7 +139,7 @@ export function loadProfilesAndTeamMembersForDMSidebar(teamId) { const channel = getChannelByName(channels, channelName); if (channel) { actions.push({ - type: UsersTypes.RECEIVED_PROFILE_IN_CHANNEL, + type: UserTypes.RECEIVED_PROFILE_IN_CHANNEL, data: {user_id: members[i]}, id: channel.id }); @@ -153,30 +155,32 @@ export function loadProfilesAndTeamMembersForDMSidebar(teamId) { export function loadPostsIfNecessary(channel) { return async (dispatch, getState) => { const state = getState(); - const postsInChannel = state.entities.posts.postsByChannel[channel.id]; - - // Make sure we include a team id for DM channels - const teamId = channel.team_id || state.entities.teams.currentTeamId; + const {posts, postsInChannel} = state.entities.posts; + const postsIds = postsInChannel[channel.id]; // Get the first page of posts if it appears we haven't gotten it yet, like the webapp - if (!postsInChannel || postsInChannel.length < Constants.POST_CHUNK_SIZE) { - return getPosts(teamId, channel.id)(dispatch, getState); + if (!postsIds || postsIds.length < Posts.POST_CHUNK_SIZE) { + return getPosts(channel.id)(dispatch, getState); } - const latestPostInChannel = state.entities.posts.posts[postsInChannel[0]]; + const postsForChannel = postsIds.map((id) => posts[id]); + const latestPostTime = getLastUpdateAt(postsForChannel); - return getPostsSince(teamId, channel.id, latestPostInChannel.create_at)(dispatch, getState); + if (latestPostTime) { + return getPostsSince(channel.id, latestPostTime)(dispatch, getState); + } + + return null; }; } export function loadFilesForPostIfNecessary(post) { return async (dispatch, getState) => { - const {files, teams} = getState().entities; + const {files} = getState().entities; const fileIdsForPost = files.fileIdsByPostId[post.id]; if (!fileIdsForPost) { - const {currentTeamId} = teams; - await getFilesForPost(currentTeamId, post.channel_id, post.id)(dispatch, getState); + await getFilesForPost(post.id)(dispatch, getState); } }; } @@ -189,10 +193,10 @@ export function selectInitialChannel(teamId) { const currentChannel = channels[currentChannelId]; const {myPreferences} = state.entities.preferences; - const isDMVisible = currentChannel && currentChannel.type === Constants.DM_CHANNEL && + const isDMVisible = currentChannel && currentChannel.type === General.DM_CHANNEL && isDirectChannelVisible(currentUserId, myPreferences, currentChannel); - const isGMVisible = currentChannel && currentChannel.type === Constants.GM_CHANNEL && + const isGMVisible = currentChannel && currentChannel.type === General.GM_CHANNEL && isGroupChannelVisible(myPreferences, currentChannel); if (currentChannel && myMembers[currentChannelId] && @@ -201,7 +205,7 @@ export function selectInitialChannel(teamId) { return; } - const channel = Object.values(channels).find((c) => c.team_id === teamId && c.name === Constants.DEFAULT_CHANNEL); + const channel = Object.values(channels).find((c) => c.team_id === teamId && c.name === General.DEFAULT_CHANNEL); if (channel) { await handleSelectChannel(channel.id)(dispatch, getState); } else { @@ -223,7 +227,7 @@ export function handleSelectChannel(channelId) { teamId: currentTeamId, channelId }); - getChannelStats(currentTeamId, channelId)(dispatch, getState); + getChannelStats(channelId)(dispatch, getState); selectChannel(channelId)(dispatch, getState); }; } @@ -250,7 +254,7 @@ export function toggleDMChannel(otherUserId, visible) { value: visible }]; - savePreferences(dm)(dispatch, getState); + savePreferences(currentUserId, dm)(dispatch, getState); }; } @@ -266,7 +270,7 @@ export function toggleGMChannel(channelId, visible) { value: visible }]; - savePreferences(gm)(dispatch, getState); + savePreferences(currentUserId, gm)(dispatch, getState); }; } @@ -303,9 +307,9 @@ export function closeGMChannel(channel) { export function closeDirectChannel(channel) { return async (dispatch, getState) => { switch (channel.type) { - case Constants.DM_CHANNEL: + case General.DM_CHANNEL: return closeDMChannel(channel)(dispatch, getState); - case Constants.GM_CHANNEL: + case General.GM_CHANNEL: return closeGMChannel(channel)(dispatch, getState); } @@ -318,12 +322,12 @@ export function markFavorite(channelId) { const {currentUserId} = getState().entities.users; const fav = [{ user_id: currentUserId, - category: Constants.CATEGORY_FAVORITE_CHANNEL, + category: Preferences.CATEGORY_FAVORITE_CHANNEL, name: channelId, value: 'true' }]; - savePreferences(fav)(dispatch, getState); + savePreferences(currentUserId, fav)(dispatch, getState); }; } @@ -332,18 +336,18 @@ export function unmarkFavorite(channelId) { const {currentUserId} = getState().entities.users; const fav = [{ user_id: currentUserId, - category: Constants.CATEGORY_FAVORITE_CHANNEL, + category: Preferences.CATEGORY_FAVORITE_CHANNEL, name: channelId }]; - deletePreferences(fav)(dispatch, getState); + deletePreferences(currentUserId, fav)(dispatch, getState); }; } export function leaveChannel(channel, reset = false) { return async (dispatch, getState) => { const {currentTeamId} = getState().entities.teams; - await serviceLeaveChannel(currentTeamId, channel.id)(dispatch, getState); + await serviceLeaveChannel(channel.id)(dispatch, getState); if (channel.isCurrent || reset) { await selectInitialChannel(currentTeamId)(dispatch, getState); } diff --git a/app/actions/views/channel_members.js b/app/actions/views/channel_members.js index 051b74a12..68473cb15 100644 --- a/app/actions/views/channel_members.js +++ b/app/actions/views/channel_members.js @@ -3,10 +3,10 @@ import {removeChannelMember} from 'mattermost-redux/actions/channels'; -export function handleRemoveChannelMembers(teamId, channelId, members) { +export function handleRemoveChannelMembers(channelId, members) { return async (dispatch, getState) => { try { - const requests = members.map((m) => dispatch(removeChannelMember(teamId, channelId, m, getState))); + const requests = members.map((m) => dispatch(removeChannelMember(channelId, m, getState))); await Promise.all(requests); } catch (error) { diff --git a/app/actions/views/file_upload.js b/app/actions/views/file_upload.js index 665c3a6b1..b90716b93 100644 --- a/app/actions/views/file_upload.js +++ b/app/actions/views/file_upload.js @@ -9,11 +9,10 @@ import {lookupMimeType} from 'mattermost-redux/utils/file_utils'; import {generateId} from 'app/utils/file'; import {ViewTypes} from 'app/constants'; -export function handleUploadFiles(files, rootId, requestId) { +export function handleUploadFiles(files, rootId) { return async (dispatch, getState) => { const state = getState(); - const teamId = state.entities.teams.currentTeamId; const channelId = state.entities.channels.currentChannelId; const formData = new FormData(); const clientIds = []; @@ -48,7 +47,7 @@ export function handleUploadFiles(files, rootId, requestId) { rootId }); - await uploadFile(teamId, channelId, formData, formBoundary, rootId, requestId)(dispatch, getState); + await uploadFile(channelId, rootId, formData, formBoundary)(dispatch, getState); }; } diff --git a/app/actions/views/login.js b/app/actions/views/login.js index 816bc0e8f..3c583e826 100644 --- a/app/actions/views/login.js +++ b/app/actions/views/login.js @@ -1,9 +1,9 @@ // Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -import {GeneralTypes} from 'mattermost-redux/constants'; +import {GeneralTypes} from 'mattermost-redux/action_types'; import {ViewTypes} from 'app/constants'; -import Client from 'mattermost-redux/client'; +import {Client4} from 'mattermost-redux/client'; export function handleLoginIdChanged(loginId) { return async (dispatch, getState) => { @@ -28,8 +28,8 @@ export function handleSuccessfulLogin() { dispatch({ type: GeneralTypes.RECEIVED_APP_CREDENTIALS, data: { - url: Client.getUrl(), - token: Client.getToken() + url: Client4.getUrl(), + token: Client4.getToken() } }); }; diff --git a/app/actions/views/more_dms.js b/app/actions/views/more_dms.js index 57986cc51..5955131b8 100644 --- a/app/actions/views/more_dms.js +++ b/app/actions/views/more_dms.js @@ -13,7 +13,6 @@ export function makeDirectChannel(otherUserId) { const channelName = getDirectChannelName(currentUserId, otherUserId); const {channels, myMembers} = state.entities.channels; const channel = Object.values(channels).find((c) => c.name === channelName); - const {currentTeamId} = state.entities.teams; getProfilesByIds([otherUserId])(dispatch, getState); getStatusesByIds([otherUserId])(dispatch, getState); @@ -22,7 +21,7 @@ export function makeDirectChannel(otherUserId) { toggleDMChannel(otherUserId, 'true')(dispatch, getState); handleSelectChannel(channel.id)(dispatch, getState); } else { - const created = await createDirectChannel(currentTeamId, currentUserId, otherUserId)(dispatch, getState); + const created = await createDirectChannel(currentUserId, otherUserId)(dispatch, getState); if (created) { toggleDMChannel(otherUserId, 'true')(dispatch, getState); handleSelectChannel(created.id)(dispatch, getState); diff --git a/app/actions/views/select_team.js b/app/actions/views/select_team.js index 0eee5ce27..320b93f4b 100644 --- a/app/actions/views/select_team.js +++ b/app/actions/views/select_team.js @@ -3,7 +3,7 @@ import {batchActions} from 'redux-batched-actions'; -import {ChannelTypes, TeamsTypes} from 'mattermost-redux/constants'; +import {ChannelTypes, TeamTypes} from 'mattermost-redux/action_types'; export function handleTeamChange(team) { return async (dispatch, getState) => { @@ -16,7 +16,7 @@ export function handleTeamChange(team) { const lastChannelId = state.views.team.lastChannelForTeam[team.id] || ''; dispatch(batchActions([ - {type: TeamsTypes.SELECT_TEAM, data: team.id}, + {type: TeamTypes.SELECT_TEAM, data: team.id}, {type: ChannelTypes.SELECT_CHANNEL, data: lastChannelId} ]), getState); }; diff --git a/app/components/autocomplete/at_mention/at_mention.js b/app/components/autocomplete/at_mention/at_mention.js index 4034a8748..bfaa74378 100644 --- a/app/components/autocomplete/at_mention/at_mention.js +++ b/app/components/autocomplete/at_mention/at_mention.js @@ -19,6 +19,262 @@ import {RequestStatus} from 'mattermost-redux/constants'; const AT_MENTION_REGEX = /\B(@([^@\r\n\s]*))$/i; +export default class AtMention extends Component { + static propTypes = { + currentUserId: PropTypes.string.isRequired, + currentChannelId: PropTypes.string.isRequired, + currentTeamId: PropTypes.string.isRequired, + cursorPosition: PropTypes.number.isRequired, + defaultChannel: PropTypes.object.isRequired, + autocompleteUsersInCurrentChannel: PropTypes.object.isRequired, + postDraft: PropTypes.string, + requestStatus: PropTypes.string.isRequired, + theme: PropTypes.object.isRequired, + onChangeText: PropTypes.func.isRequired, + actions: PropTypes.shape({ + autocompleteUsers: PropTypes.func.isRequired + }) + }; + + static defaultProps = { + autocompleteUsersInCurrentChannel: {}, + postDraft: '' + }; + + constructor(props) { + super(props); + + const ds = new ListView.DataSource({ + sectionHeaderHasChanged: (s1, s2) => s1 !== s2, + rowHasChanged: (r1, r2) => r1 !== r2 + }); + const data = {}; + this.state = { + active: false, + dataSource: ds.cloneWithRowsAndSections(data) + }; + } + + componentWillReceiveProps(nextProps) { + const match = nextProps.postDraft.substring(0, nextProps.cursorPosition).match(AT_MENTION_REGEX); + + if (!match || this.state.mentionComplete) { + this.setState({ + active: false, + matchTerm: null, + mentionComplete: false + }); + return; + } + + const matchTerm = match[2]; + + if (matchTerm !== this.state.matchTerm) { + this.setState({ + matchTerm + }); + + const {currentTeamId, currentChannelId} = this.props; + this.props.actions.autocompleteUsers(matchTerm, currentTeamId, currentChannelId); + } + + if (nextProps.requestStatus !== RequestStatus.STARTED) { + const membersInChannel = this.filter(nextProps.autocompleteUsersInCurrentChannel.inChannel, matchTerm) || []; + const membersOutOfChannel = this.filter(nextProps.autocompleteUsersInCurrentChannel.outChannel, matchTerm) || []; + + let data = {}; + if (membersInChannel.length > 0) { + data = Object.assign({}, data, {inChannel: membersInChannel}); + } + if (this.checkSpecialMentions(matchTerm)) { + data = Object.assign({}, data, {specialMentions: this.getSpecialMentions()}); + } + if (membersOutOfChannel.length > 0) { + data = Object.assign({}, data, {notInChannel: membersOutOfChannel}); + } + + this.setState({ + active: data.hasOwnProperty('inChannel') || data.hasOwnProperty('specialMentions') || data.hasOwnProperty('notInChannel'), + dataSource: this.state.dataSource.cloneWithRowsAndSections(data) + }); + } + } + + filter = (profiles, matchTerm) => { + return profiles.filter((p) => { + return ((p.id !== this.props.currentUserId) && ( + p.username.toLowerCase().includes(matchTerm) || p.email.toLowerCase().includes(matchTerm) || + p.first_name.toLowerCase().includes(matchTerm) || p.last_name.toLowerCase().includes(matchTerm))); + }); + }; + + getSpecialMentions = () => { + return [{ + completeHandle: 'all', + id: 'suggestion.mention.all', + defaultMessage: 'Notifies everyone in the channel, use in {townsquare} to notify the whole team', + values: { + townsquare: this.props.defaultChannel.display_name + } + }, { + completeHandle: 'channel', + id: 'suggestion.mention.channel', + defaultMessage: 'Notifies everyone in the channel' + }, { + completeHandle: 'here', + id: 'suggestions.mention.here', + defaultMessage: 'Notifies everyone in the channel and online' + }]; + }; + + checkSpecialMentions = (term) => { + return this.getSpecialMentions().filter((m) => m.completeHandle.startsWith(term)).length > 0; + }; + + completeMention = (mention) => { + const mentionPart = this.props.postDraft.substring(0, this.props.cursorPosition); + + let completedDraft = mentionPart.replace(AT_MENTION_REGEX, `@${mention} `); + if (this.props.postDraft.length > this.props.cursorPosition) { + completedDraft += this.props.postDraft.substring(this.props.cursorPosition); + } + + this.props.onChangeText(completedDraft); + this.setState({ + active: false, + mentionComplete: true + }); + }; + + renderSectionHeader = (sectionData, sectionId) => { + const style = getStyleFromTheme(this.props.theme); + + const localization = { + inChannel: { + id: 'suggestion.mention.members', + defaultMessage: 'Channel Members' + }, + notInChannel: { + id: 'suggestion.mention.nonmembers', + defaultMessage: 'Not in Channel' + }, + specialMentions: { + id: 'suggestion.mention.special', + defaultMessage: 'Special Mentions' + } + }; + + return ( + + + + + + ); + }; + + renderRow = (data, sectionId) => { + if (sectionId === 'specialMentions') { + return this.renderSpecialMentions(data); + } + + const style = getStyleFromTheme(this.props.theme); + const hasFullName = data.first_name.length > 0 && data.last_name.length > 0; + + return ( + this.completeMention(data.username)} + style={style.row} + > + + + + {`@${data.username}`} + {hasFullName && {' - '}} + {hasFullName && {`${data.first_name} ${data.last_name}`}} + + ); + }; + + renderSpecialMentions = (data) => { + const style = getStyleFromTheme(this.props.theme); + + return ( + this.completeMention(data.completeHandle)} + style={style.row} + > + + + + + {`@${data.completeHandle}`} + {' - '} + + + + ); + }; + + render() { + const {autocompleteUsersInCurrentChannel, requestStatus} = this.props; + if (!this.state.active && (requestStatus !== RequestStatus.STARTED || requestStatus !== RequestStatus.SUCCESS)) { + // If we are not in an active state return null so nothing is rendered + // other components are not blocked. + return null; + } + + const style = getStyleFromTheme(this.props.theme); + + if ( + !autocompleteUsersInCurrentChannel.inChannel && + !autocompleteUsersInCurrentChannel.outChannel && + requestStatus === RequestStatus.STARTED + ) { + return ( + + + + ); + } + + return ( + + ); + } +} + const getStyleFromTheme = makeStyleSheetFromTheme((theme) => { return StyleSheet.create({ section: { @@ -90,254 +346,3 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => { } }); }); - -export default class AtMention extends Component { - static propTypes = { - currentUserId: PropTypes.string.isRequired, - currentChannelId: PropTypes.string.isRequired, - currentTeamId: PropTypes.string.isRequired, - cursorPosition: PropTypes.number.isRequired, - defaultChannel: PropTypes.object.isRequired, - autocompleteUsersInCurrentChannel: PropTypes.object.isRequired, - postDraft: PropTypes.string, - requestStatus: PropTypes.string.isRequired, - theme: PropTypes.object.isRequired, - onChangeText: PropTypes.func.isRequired, - actions: PropTypes.shape({ - autocompleteUsersInChannel: PropTypes.func.isRequired - }) - } - - static defaultProps = { - autocompleteUsersInCurrentChannel: {}, - postDraft: '' - } - - constructor(props) { - super(props); - - const ds = new ListView.DataSource({ - sectionHeaderHasChanged: (s1, s2) => s1 !== s2, - rowHasChanged: (r1, r2) => r1 !== r2 - }); - const data = {}; - this.state = { - active: false, - dataSource: ds.cloneWithRowsAndSections(data) - }; - } - - componentWillReceiveProps(nextProps) { - const match = nextProps.postDraft.substring(0, nextProps.cursorPosition).match(AT_MENTION_REGEX); - - if (!match || this.state.mentionComplete) { - this.setState({ - active: false, - matchTerm: null, - mentionComplete: false - }); - return; - } - - const matchTerm = match[2]; - - if (matchTerm !== this.state.matchTerm) { - this.setState({ - matchTerm - }); - - const {currentTeamId, currentChannelId} = this.props; - this.props.actions.autocompleteUsersInChannel(currentTeamId, currentChannelId, matchTerm); - } - - if (nextProps.requestStatus !== RequestStatus.STARTED) { - let membersInChannel = nextProps.autocompleteUsersInCurrentChannel.in_channel || []; - const membersOutOfChannel = nextProps.autocompleteUsersInCurrentChannel.out_of_channel || []; - - // filter the current user out - membersInChannel = membersInChannel.filter((m) => m.id !== this.props.currentUserId); - - let data = {}; - if (membersInChannel.length > 0) { - data = Object.assign({}, data, {inChannel: membersInChannel}); - } - if (this.checkSpecialMentions(matchTerm)) { - data = Object.assign({}, data, {specialMentions: this.getSpecialMentions()}); - } - if (membersOutOfChannel.length > 0) { - data = Object.assign({}, data, {notInChannel: membersOutOfChannel}); - } - - this.setState({ - active: data.hasOwnProperty('inChannel') || data.hasOwnProperty('specialMentions') || data.hasOwnProperty('notInChannel'), - dataSource: this.state.dataSource.cloneWithRowsAndSections(data) - }); - } - } - - getSpecialMentions = () => { - return [{ - completeHandle: 'all', - id: 'suggestion.mention.all', - defaultMessage: 'Notifies everyone in the channel, use in {townsquare} to notify the whole team', - values: { - townsquare: this.props.defaultChannel.display_name - } - }, { - completeHandle: 'channel', - id: 'suggestion.mention.channel', - defaultMessage: 'Notifies everyone in the channel' - }, { - completeHandle: 'here', - id: 'suggestions.mention.here', - defaultMessage: 'Notifies everyone in the channel and online' - }]; - } - - checkSpecialMentions = (term) => { - return this.getSpecialMentions().filter((m) => m.completeHandle.startsWith(term)).length > 0; - } - - completeMention = (mention) => { - const mentionPart = this.props.postDraft.substring(0, this.props.cursorPosition); - - let completedDraft = mentionPart.replace(AT_MENTION_REGEX, `@${mention} `); - if (this.props.postDraft.length > this.props.cursorPosition) { - completedDraft += this.props.postDraft.substring(this.props.cursorPosition); - } - - this.props.onChangeText(completedDraft); - this.setState({ - active: false, - mentionComplete: true - }); - } - - renderSectionHeader = (sectionData, sectionId) => { - const style = getStyleFromTheme(this.props.theme); - - const localization = { - inChannel: { - id: 'suggestion.mention.members', - defaultMessage: 'Channel Members' - }, - notInChannel: { - id: 'suggestion.mention.nonmembers', - defaultMessage: 'Not in Channel' - }, - specialMentions: { - id: 'suggestion.mention.special', - defaultMessage: 'Special Mentions' - } - }; - - return ( - - - - - - ); - } - - renderRow = (data, sectionId) => { - if (sectionId === 'specialMentions') { - return this.renderSpecialMentions(data); - } - - const style = getStyleFromTheme(this.props.theme); - const hasFullName = data.first_name.length > 0 && data.last_name.length > 0; - - return ( - this.completeMention(data.username)} - style={style.row} - > - - - - {`@${data.username}`} - {hasFullName && {' - '}} - {hasFullName && {`${data.first_name} ${data.last_name}`}} - - ); - } - - renderSpecialMentions = (data) => { - const style = getStyleFromTheme(this.props.theme); - - return ( - this.completeMention(data.completeHandle)} - style={style.row} - > - - - - - {`@${data.completeHandle}`} - {' - '} - - - - ); - } - - render() { - const {autocompleteUsersInCurrentChannel, requestStatus} = this.props; - if (!this.state.active && (requestStatus !== RequestStatus.STARTED || requestStatus !== RequestStatus.SUCCESS)) { - // If we are not in an active state return null so nothing is rendered - // other components are not blocked. - return null; - } - - const style = getStyleFromTheme(this.props.theme); - - if ( - !autocompleteUsersInCurrentChannel.in_channel && - !autocompleteUsersInCurrentChannel.out_of_channel && - requestStatus === RequestStatus.STARTED - ) { - return ( - - - - ); - } - - return ( - - ); - } -} diff --git a/app/components/autocomplete/at_mention/index.js b/app/components/autocomplete/at_mention/index.js index 1428d0042..ce2d17273 100644 --- a/app/components/autocomplete/at_mention/index.js +++ b/app/components/autocomplete/at_mention/index.js @@ -5,9 +5,9 @@ import {bindActionCreators} from 'redux'; import {connect} from 'react-redux'; import {getTheme} from 'app/selectors/preferences'; -import {autocompleteUsersInChannel} from 'mattermost-redux/actions/users'; +import {autocompleteUsers} from 'mattermost-redux/actions/users'; import {getDefaultChannel} from 'mattermost-redux/selectors/entities/channels'; -import {getAutocompleteUsersInCurrentChannel} from 'mattermost-redux/selectors/entities/users'; +import {getProfilesInCurrentChannel, getProfilesNotInCurrentChannel} from 'mattermost-redux/selectors/entities/users'; import AtMention from './at_mention'; @@ -28,8 +28,11 @@ function mapStateToProps(state, ownProps) { currentTeamId: state.entities.teams.currentTeamId, defaultChannel: getDefaultChannel(state), postDraft, - autocompleteUsersInCurrentChannel: getAutocompleteUsersInCurrentChannel(state), - requestStatus: state.requests.users.autocompleteUsersInChannel.status, + autocompleteUsersInCurrentChannel: { + inChannel: getProfilesInCurrentChannel(state), + outChannel: getProfilesNotInCurrentChannel(state) + }, + requestStatus: state.requests.users.autocompleteUsers.status, theme: getTheme(state) }; } @@ -37,7 +40,7 @@ function mapStateToProps(state, ownProps) { function mapDispatchToProps(dispatch) { return { actions: bindActionCreators({ - autocompleteUsersInChannel + autocompleteUsers }, dispatch) }; } diff --git a/app/components/autocomplete/channel_mention/channel_mention.js b/app/components/autocomplete/channel_mention/channel_mention.js index c8369c986..f8612bbff 100644 --- a/app/components/autocomplete/channel_mention/channel_mention.js +++ b/app/components/autocomplete/channel_mention/channel_mention.js @@ -17,6 +17,205 @@ import {RequestStatus} from 'mattermost-redux/constants'; const CHANNEL_MENTION_REGEX = /\B(~([^~\r\n]*))$/i; +export default class ChannelMention extends Component { + static propTypes = { + currentChannelId: PropTypes.string.isRequired, + currentTeamId: PropTypes.string.isRequired, + cursorPosition: PropTypes.number.isRequired, + autocompleteChannels: PropTypes.object.isRequired, + postDraft: PropTypes.string, + requestStatus: PropTypes.string.isRequired, + theme: PropTypes.object.isRequired, + onChangeText: PropTypes.func.isRequired, + actions: PropTypes.shape({ + searchMoreChannels: PropTypes.func.isRequired + }) + }; + + static defaultProps = { + postDraft: '' + }; + + constructor(props) { + super(props); + + const ds = new ListView.DataSource({ + sectionHeaderHasChanged: (s1, s2) => s1 !== s2, + rowHasChanged: (r1, r2) => r1 !== r2 + }); + + this.state = { + active: false, + dataSource: ds.cloneWithRowsAndSections(props.autocompleteChannels) + }; + } + + componentWillReceiveProps(nextProps) { + const match = nextProps.postDraft.substring(0, nextProps.cursorPosition).match(CHANNEL_MENTION_REGEX); + + // If not match or if user clicked on a channel + if (!match || this.state.mentionComplete) { + const nextState = { + active: false, + mentionComplete: false + }; + + // Handle the case where the user typed a ~ first and then backspaced + if (nextProps.postDraft.length < this.props.postDraft.length) { + nextState.matchTerm = null; + } + + this.setState(nextState); + return; + } + + const matchTerm = match[2]; + const myChannels = this.filter(nextProps.autocompleteChannels.myChannels, matchTerm); + const otherChannels = this.filter(nextProps.autocompleteChannels.otherChannels, matchTerm); + + // Show loading indicator on first pull for channels + if (nextProps.requestStatus === RequestStatus.STARTED && ((myChannels.length === 0 && otherChannels.length === 0) || matchTerm === '')) { + this.setState({ + active: true, + loading: true + }); + return; + } + + // Still matching the same term that didn't return any results + if (match[0].startsWith(`~${this.state.matchTerm}`) && (myChannels.length === 0 && otherChannels.length === 0)) { + this.setState({ + active: false + }); + return; + } + + if (matchTerm !== this.state.matchTerm) { + this.setState({ + matchTerm + }); + + const {currentTeamId} = this.props; + this.props.actions.searchMoreChannels(currentTeamId, matchTerm); + return; + } + + if (nextProps.requestStatus !== RequestStatus.STARTED && this.props.autocompleteChannels !== nextProps.autocompleteChannels) { + let data = {}; + if (myChannels.length > 0) { + data = Object.assign({}, data, {myChannels}); + } + if (otherChannels.length > 0) { + data = Object.assign({}, data, {otherChannels}); + } + + this.setState({ + active: true, + loading: false, + dataSource: this.state.dataSource.cloneWithRowsAndSections(data) + }); + } + } + + filter = (channels, matchTerm) => { + return channels.filter((c) => c.name.includes(matchTerm) || c.display_name.includes(matchTerm)); + }; + + completeMention = (mention) => { + const mentionPart = this.props.postDraft.substring(0, this.props.cursorPosition); + + let completedDraft = mentionPart.replace(CHANNEL_MENTION_REGEX, `~${mention} `); + if (this.props.postDraft.length > this.props.cursorPosition) { + completedDraft += this.props.postDraft.substring(this.props.cursorPosition); + } + + this.props.onChangeText(completedDraft); + this.setState({ + active: false, + mentionComplete: true, + matchTerm: `${mention} ` + }); + }; + + renderSectionHeader = (sectionData, sectionId) => { + const style = getStyleFromTheme(this.props.theme); + + const localization = { + myChannels: { + id: 'suggestion.mention.channels', + defaultMessage: 'My Channels' + }, + otherChannels: { + id: 'suggestion.mention.morechannels', + defaultMessage: 'Other Channels' + } + }; + + return ( + + + + + + ); + }; + + renderRow = (data) => { + const style = getStyleFromTheme(this.props.theme); + + return ( + this.completeMention(data.name)} + style={style.row} + > + {data.display_name} + {` (~${data.name})`} + + ); + }; + + render() { + if (!this.state.active) { + // If we are not in an active state return null so nothing is rendered + // other components are not blocked. + return null; + } + + const {requestStatus, theme} = this.props; + + const style = getStyleFromTheme(theme); + + if (this.state.loading && requestStatus === RequestStatus.STARTED) { + return ( + + + + ); + } + + return ( + + ); + } +} + const getStyleFromTheme = makeStyleSheetFromTheme((theme) => { return StyleSheet.create({ section: { @@ -73,198 +272,3 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => { } }); }); - -export default class ChannelMention extends Component { - static propTypes = { - currentChannelId: PropTypes.string.isRequired, - currentTeamId: PropTypes.string.isRequired, - cursorPosition: PropTypes.number.isRequired, - autocompleteChannels: PropTypes.object.isRequired, - postDraft: PropTypes.string, - requestStatus: PropTypes.string.isRequired, - theme: PropTypes.object.isRequired, - onChangeText: PropTypes.func.isRequired, - actions: PropTypes.shape({ - autocompleteChannels: PropTypes.func.isRequired - }) - } - - static defaultProps = { - postDraft: '' - } - - constructor(props) { - super(props); - - const ds = new ListView.DataSource({ - sectionHeaderHasChanged: (s1, s2) => s1 !== s2, - rowHasChanged: (r1, r2) => r1 !== r2 - }); - - this.state = { - active: false, - dataSource: ds.cloneWithRowsAndSections(props.autocompleteChannels) - }; - } - - componentWillReceiveProps(nextProps) { - const match = nextProps.postDraft.substring(0, nextProps.cursorPosition).match(CHANNEL_MENTION_REGEX); - - // If not match or if user clicked on a channel - if (!match || this.state.mentionComplete) { - const nextState = { - active: false, - mentionComplete: false - }; - - // Handle the case where the user typed a ~ first and then backspaced - if (nextProps.postDraft.length < this.props.postDraft.length) { - nextState.matchTerm = null; - } - - this.setState(nextState); - return; - } - - const matchTerm = match[2]; - const myChannels = nextProps.autocompleteChannels.myChannels; - const otherChannels = nextProps.autocompleteChannels.otherChannels; - - // Show loading indicator on first pull for channels - if (nextProps.requestStatus === RequestStatus.STARTED && ((myChannels.length === 0 && otherChannels.length === 0) || matchTerm === '')) { - this.setState({ - active: true, - loading: true - }); - return; - } - - // Still matching the same term that didn't return any results - if (match[0].startsWith(`~${this.state.matchTerm}`) && (myChannels.length === 0 && otherChannels.length === 0)) { - this.setState({ - active: false - }); - return; - } - - if (matchTerm !== this.state.matchTerm) { - this.setState({ - matchTerm - }); - - const {currentTeamId} = this.props; - this.props.actions.autocompleteChannels(currentTeamId, matchTerm); - return; - } - - if (nextProps.requestStatus !== RequestStatus.STARTED && this.props.autocompleteChannels !== nextProps.autocompleteChannels) { - let data = {}; - if (myChannels.length > 0) { - data = Object.assign({}, data, {myChannels}); - } - if (otherChannels.length > 0) { - data = Object.assign({}, data, {otherChannels}); - } - - this.setState({ - active: true, - loading: false, - dataSource: this.state.dataSource.cloneWithRowsAndSections(data) - }); - } - } - - completeMention = (mention) => { - const mentionPart = this.props.postDraft.substring(0, this.props.cursorPosition); - - let completedDraft = mentionPart.replace(CHANNEL_MENTION_REGEX, `~${mention} `); - if (this.props.postDraft.length > this.props.cursorPosition) { - completedDraft += this.props.postDraft.substring(this.props.cursorPosition); - } - - this.props.onChangeText(completedDraft); - this.setState({ - active: false, - mentionComplete: true, - matchTerm: `${mention} ` - }); - } - - renderSectionHeader = (sectionData, sectionId) => { - const style = getStyleFromTheme(this.props.theme); - - const localization = { - myChannels: { - id: 'suggestion.mention.channels', - defaultMessage: 'My Channels' - }, - otherChannels: { - id: 'suggestion.mention.morechannels', - defaultMessage: 'Other Channels' - } - }; - - return ( - - - - - - ); - } - - renderRow = (data) => { - const style = getStyleFromTheme(this.props.theme); - - return ( - this.completeMention(data.name)} - style={style.row} - > - {data.display_name} - {` (~${data.name})`} - - ); - } - - render() { - if (!this.state.active) { - // If we are not in an active state return null so nothing is rendered - // other components are not blocked. - return null; - } - - const {requestStatus, theme} = this.props; - - const style = getStyleFromTheme(theme); - - if (this.state.loading && requestStatus === RequestStatus.STARTED) { - return ( - - - - ); - } - - return ( - - ); - } -} diff --git a/app/components/autocomplete/channel_mention/index.js b/app/components/autocomplete/channel_mention/index.js index 5a6b6cd99..025590766 100644 --- a/app/components/autocomplete/channel_mention/index.js +++ b/app/components/autocomplete/channel_mention/index.js @@ -4,9 +4,11 @@ import {bindActionCreators} from 'redux'; import {connect} from 'react-redux'; -import {autocompleteChannels} from 'mattermost-redux/actions/channels'; +import {searchMoreChannels} from 'mattermost-redux/actions/channels'; +import {General} from 'mattermost-redux/constants'; +import {getMyChannels, getOtherChannels} from 'mattermost-redux/selectors/entities/channels'; + import {getTheme} from 'app/selectors/preferences'; -import {getAutocompleteChannelWithSections} from 'mattermost-redux/selectors/entities/channels'; import ChannelMention from './channel_mention'; @@ -20,13 +22,18 @@ function mapStateToProps(state, ownProps) { postDraft = state.views.channel.drafts[currentChannelId].draft; } + const autocompleteChannels = { + myChannels: getMyChannels(state).filter((c) => c.type !== General.DM_CHANNEL && c.type !== General.GM_CHANNEL), + otherChannels: getOtherChannels(state) + }; + return { ...ownProps, currentChannelId, currentTeamId: state.entities.teams.currentTeamId, postDraft, - autocompleteChannels: getAutocompleteChannelWithSections(state), - requestStatus: state.requests.channels.autocompleteChannels.status, + autocompleteChannels, + requestStatus: state.requests.channels.getChannels.status, theme: getTheme(state) }; } @@ -34,7 +41,7 @@ function mapStateToProps(state, ownProps) { function mapDispatchToProps(dispatch) { return { actions: bindActionCreators({ - autocompleteChannels + searchMoreChannels }, dispatch) }; } diff --git a/app/components/autocomplete/index.js b/app/components/autocomplete/index.js index c2625e168..11a289553 100644 --- a/app/components/autocomplete/index.js +++ b/app/components/autocomplete/index.js @@ -35,7 +35,7 @@ export default class Autocomplete extends Component { this.setState({ cursorPosition: event.nativeEvent.selection.end }); - } + }; render() { return ( diff --git a/app/components/channel_drawer_list/channel_drawer_list.js b/app/components/channel_drawer_list/channel_drawer_list.js index c5f892924..00c4b073b 100644 --- a/app/components/channel_drawer_list/channel_drawer_list.js +++ b/app/components/channel_drawer_list/channel_drawer_list.js @@ -19,7 +19,7 @@ import FormattedText from 'app/components/formatted_text'; import {preventDoubleTap} from 'app/utils/tap'; import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme'; -import {Constants} from 'mattermost-redux/constants'; +import {General} from 'mattermost-redux/constants'; import ChannelDrawerItem from './channel_drawer_item'; import UnreadIndicator from './unread_indicator'; @@ -132,7 +132,7 @@ class ChannelDrawerList extends Component { mentions = member.mention_count; unreadCount = channel.total_msg_count - member.msg_count; - if (member.notify_props && member.notify_props.mark_unread === Constants.MENTION) { + if (member.notify_props && member.notify_props.mark_unread === General.MENTION) { unreadCount = 0; } } @@ -178,7 +178,7 @@ class ChannelDrawerList extends Component { }; createPrivateChannel = () => { - this.props.actions.goToCreateChannel(Constants.PRIVATE_CHANNEL); + this.props.actions.goToCreateChannel(General.PRIVATE_CHANNEL); }; buildData = (props) => { diff --git a/app/components/channel_drawer_list/index.js b/app/components/channel_drawer_list/index.js index ac6e0a47a..463d32b1a 100644 --- a/app/components/channel_drawer_list/index.js +++ b/app/components/channel_drawer_list/index.js @@ -20,7 +20,7 @@ import { unmarkFavorite } from 'app/actions/views/channel'; -import {Constants} from 'mattermost-redux/constants'; +import {General} from 'mattermost-redux/constants'; import {getCurrentUserId, getCurrentUserRoles} from 'mattermost-redux/selectors/entities/users'; import {showCreateOption} from 'mattermost-redux/utils/channel_utils'; import {isAdmin, isSystemAdmin} from 'mattermost-redux/utils/user_utils'; @@ -32,7 +32,7 @@ function mapStateToProps(state, ownProps) { const roles = getCurrentUserId(state) ? getCurrentUserRoles(state) : ''; return { - canCreatePrivateChannels: showCreateOption(config, license, Constants.PRIVATE_CHANNEL, isAdmin(roles), isSystemAdmin(roles)), + canCreatePrivateChannels: showCreateOption(config, license, General.PRIVATE_CHANNEL, isAdmin(roles), isSystemAdmin(roles)), ...ownProps }; } diff --git a/app/components/channel_icon.js b/app/components/channel_icon.js index 28560871f..4559bb6a0 100644 --- a/app/components/channel_icon.js +++ b/app/components/channel_icon.js @@ -11,7 +11,7 @@ import Icon from 'react-native-vector-icons/FontAwesome'; import {OnlineStatus, AwayStatus, OfflineStatus} from 'app/components/status_icons'; -import {Constants} from 'mattermost-redux/constants'; +import {General} from 'mattermost-redux/constants'; import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme'; @@ -44,21 +44,21 @@ function channelIcon(props) { activeGroup = style.groupInfo; } - if (type === Constants.OPEN_CHANNEL) { + if (type === General.OPEN_CHANNEL) { return ( ); - } else if (type === Constants.PRIVATE_CHANNEL) { + } else if (type === General.PRIVATE_CHANNEL) { return ( ); - } else if (type === Constants.GM_CHANNEL) { + } else if (type === General.GM_CHANNEL) { return ( @@ -70,7 +70,7 @@ function channelIcon(props) { ); } switch (status) { - case Constants.ONLINE: + case General.ONLINE: return ( ); - case Constants.AWAY: + case General.AWAY: return ( ); } return ( - - {this.props.error.message || this.props.error} + + {error.message || error} ); } } + +const getStyleSheet = makeStyleSheetFromTheme((theme) => { + return StyleSheet.create({ + errorLabel: { + color: (theme.errorTextColor || '#DA4A4A') + } + }); +}); + +function mapStateToProps(state, ownProps) { + return { + ...ownProps, + theme: getTheme(state) + }; +} + +export default connect(mapStateToProps)(ErrorText); diff --git a/app/components/file_attachment_list/file_attachment_image.js b/app/components/file_attachment_list/file_attachment_image.js index 34a8c233f..4c45a4d8c 100644 --- a/app/components/file_attachment_list/file_attachment_image.js +++ b/app/components/file_attachment_list/file_attachment_image.js @@ -13,7 +13,7 @@ import { StyleSheet } from 'react-native'; -import Client from 'mattermost-redux/client'; +import {Client4} from 'mattermost-redux/client'; import imageIcon from 'assets/images/icons/image.png'; @@ -25,6 +25,8 @@ const IMAGE_SIZE = { Thumbnail: 'thumbnail' }; +const IMAGE_HEADERS = Client4.getOptions().headers; + export default class FileAttachmentImage extends PureComponent { static propTypes = { addFileToFetchCache: PropTypes.func.isRequired, @@ -103,14 +105,14 @@ export default class FileAttachmentImage extends PureComponent { switch (imageSize) { case IMAGE_SIZE.Fullsize: - return Client.getFileUrl(file.id, this.state.timestamp); + return Client4.getFileUrl(file.id, this.state.timestamp); case IMAGE_SIZE.Preview: - return Client.getFilePreviewUrl(file.id, this.state.timestamp); + return Client4.getFilePreviewUrl(file.id, this.state.timestamp); case IMAGE_SIZE.Thumbnail: default: - return Client.getFileThumbnailUrl(file.id, this.state.timestamp); + return Client4.getFileThumbnailUrl(file.id, this.state.timestamp); } - } + }; render() { const { @@ -131,7 +133,7 @@ export default class FileAttachmentImage extends PureComponent { if (this.state.retry === 4) { source = imageIcon; } else if (file.id) { - source = {uri: this.handleGetImageURL()}; + source = {uri: this.handleGetImageURL(), headers: IMAGE_HEADERS}; } const isInFetchCache = fetchCache[source.uri]; diff --git a/app/components/post/index.js b/app/components/post/index.js index 5a459a502..8db11c3d3 100644 --- a/app/components/post/index.js +++ b/app/components/post/index.js @@ -3,9 +3,8 @@ import {connect} from 'react-redux'; import {bindActionCreators} from 'redux'; -import {deletePost, flagPost, unflagPost} from 'mattermost-redux/actions/posts'; +import {deletePost, flagPost, removePost, unflagPost} from 'mattermost-redux/actions/posts'; import {getMyPreferences} from 'mattermost-redux/selectors/entities/preferences'; -import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams'; import {makeGetCommentCountForPost} from 'mattermost-redux/selectors/entities/posts'; import {getCurrentUserId, getCurrentUserRoles, getUser} from 'mattermost-redux/selectors/entities/users'; import {isPostFlagged} from 'mattermost-redux/utils/post_utils'; @@ -30,7 +29,6 @@ function makeMapStateToProps() { config, commentCount: getCommentCountForPost(state, ownProps), commentedOnDisplayName: displayUsername(commentedOnUser, myPreferences), - currentTeamId: getCurrentTeamId(state), currentUserId: getCurrentUserId(state), displayName: displayUsername(user, myPreferences), isFlagged: isPostFlagged(ownProps.post.id, myPreferences), @@ -49,6 +47,7 @@ function mapDispatchToProps(dispatch) { flagPost, goToUserProfile, openEditPostModal, + removePost, unflagPost }, dispatch) }; diff --git a/app/components/post/post.js b/app/components/post/post.js index 7f9760c3a..389e817e4 100644 --- a/app/components/post/post.js +++ b/app/components/post/post.js @@ -27,7 +27,7 @@ import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme'; import webhookIcon from 'assets/images/icons/webhook.jpg'; -import {Constants} from 'mattermost-redux/constants'; +import {Posts} from 'mattermost-redux/constants'; import DelayedAction from 'mattermost-redux/utils/delayed_action'; import {canDeletePost, canEditPost, isSystemMessage} from 'mattermost-redux/utils/post_utils'; import {isAdmin, isSystemAdmin} from 'mattermost-redux/utils/user_utils'; @@ -38,7 +38,6 @@ class Post extends PureComponent { static propTypes = { config: PropTypes.object.isRequired, commentCount: PropTypes.number.isRequired, - currentTeamId: PropTypes.string.isRequired, currentUserId: PropTypes.string.isRequired, intl: intlShape.isRequired, style: View.propTypes.style, @@ -60,6 +59,7 @@ class Post extends PureComponent { flagPost: PropTypes.func.isRequired, goToUserProfile: PropTypes.func.isRequired, openEditPostModal: PropTypes.func.isRequired, + removePost: PropTypes.func.isRequired, unflagPost: PropTypes.func.isRequired }).isRequired }; @@ -93,7 +93,7 @@ class Post extends PureComponent { handlePostDelete = () => { const {formatMessage} = this.props.intl; - const {currentTeamId, post, actions} = this.props; + const {post, actions} = this.props; Alert.alert( formatMessage({id: 'mobile.post.delete_title', defaultMessage: 'Delete Post'}), @@ -106,7 +106,7 @@ class Post extends PureComponent { style: 'destructive', onPress: () => { this.editDisableAction.cancel(); - actions.deletePost(currentTeamId, post); + actions.deletePost(post); } }] ); @@ -119,8 +119,10 @@ class Post extends PureComponent { handlePress = () => { const {post, onPress} = this.props; - if (onPress && post.state !== Constants.POST_DELETED && !isSystemMessage(post)) { + if (onPress && post.state !== Posts.POST_DELETED && !isSystemMessage(post)) { preventDoubleTap(onPress, null, post); + } else if (post.state === Posts.POST_DELETED) { + preventDoubleTap(this.onRemovePost, this, post); } }; @@ -130,6 +132,11 @@ class Post extends PureComponent { } }; + onRemovePost = (post) => { + const {removePost} = this.props.actions; + removePost(post); + }; + showOptionsContext = () => { if (Platform.OS === 'ios') { return this.refs.tooltip.show(); @@ -236,13 +243,13 @@ class Post extends PureComponent { actions.push({text: formatMessage({id: 'post_info.edit', defaultMessage: 'Edit'}), onPress: () => this.handlePostEdit()}); } - if (this.state.canDelete && post.state !== Constants.POST_DELETED) { + if (this.state.canDelete && post.state !== Posts.POST_DELETED) { actions.push({text: formatMessage({id: 'post_info.del', defaultMessage: 'Delete'}), onPress: () => this.handlePostDelete()}); } let messageContainer; let message; - if (post.state === Constants.POST_DELETED) { + if (post.state === Posts.POST_DELETED) { message = ( { - return StyleSheet.create({ - container: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'center', - height: 28, - marginVertical: 10 - }, - text: { - fontSize: 14, - fontWeight: '600', - color: theme.linkColor - } - }); -}); - export default class LoadMorePosts extends PureComponent { static propTypes = { loading: PropTypes.bool.isRequired, @@ -71,3 +54,20 @@ export default class LoadMorePosts extends PureComponent { ); } } + +const getStyleSheet = makeStyleSheetFromTheme((theme) => { + return StyleSheet.create({ + container: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + height: 28, + marginVertical: 10 + }, + text: { + fontSize: 14, + fontWeight: '600', + color: theme.linkColor + } + }); +}); diff --git a/app/components/post_list/post_list.js b/app/components/post_list/post_list.js index 3d90222f2..dc756313c 100644 --- a/app/components/post_list/post_list.js +++ b/app/components/post_list/post_list.js @@ -7,7 +7,8 @@ import { StyleSheet, View } from 'react-native'; -import {Constants} from 'mattermost-redux/constants'; + +import {General, Posts} from 'mattermost-redux/constants'; import {addDatesToPostList} from 'mattermost-redux/utils/post_utils'; import ChannelIntro from 'app/components/channel_intro'; @@ -77,7 +78,7 @@ export default class PostList extends Component { const firstPostHasRendered = channel.total_msg_count ? posts.length > 0 : true; const messageCount = channel.total_msg_count - posts.length; - if (channelIsLoading || !firstPostHasRendered || messageCount > Constants.POST_CHUNK_SIZE) { + if (channelIsLoading || !firstPostHasRendered || messageCount > Posts.POST_CHUNK_SIZE) { return null; } @@ -92,7 +93,7 @@ export default class PostList extends Component { if (row instanceof Date) { return this.renderDateHeader(row); } - if (row === Constants.START_OF_NEW_MESSAGES) { + if (row === General.START_OF_NEW_MESSAGES) { return ( { try { @@ -200,11 +199,11 @@ class PostTextbox extends PureComponent { // If user cancels it's considered // an error and we have to catch it. } - } + }; uploadFiles = (images) => { this.props.actions.handleUploadFiles(images, this.props.rootId); - } + }; showFileAttachmentOptions = () => { this.blur(); @@ -227,7 +226,7 @@ class PostTextbox extends PureComponent { }; this.props.actions.showOptionsModal(options); - } + }; renderTyping = () => { const {typing} = this.props; diff --git a/app/components/profile_picture/profile_picture.js b/app/components/profile_picture/profile_picture.js index 5b2dc282f..61928e6ef 100644 --- a/app/components/profile_picture/profile_picture.js +++ b/app/components/profile_picture/profile_picture.js @@ -9,7 +9,7 @@ import {makeStyleSheetFromTheme} from 'app/utils/theme'; import placeholder from 'assets/images/profile.jpg'; -import Client from 'mattermost-redux/client'; +import {Client4} from 'mattermost-redux/client'; const statusToIcon = { online: 'check', @@ -21,6 +21,8 @@ const STATUS_BUFFER = Platform.select({ android: 2 }); +const IMAGE_HEADERS = Client4.getOptions().headers; + export default class ProfilePicture extends React.PureComponent { static propTypes = { size: React.PropTypes.number, @@ -53,7 +55,7 @@ export default class ProfilePicture extends React.PureComponent { let pictureUrl; if (this.props.user) { - pictureUrl = Client.getProfilePictureUrl(this.props.user.id, this.props.user.last_picture_update); + pictureUrl = Client4.getProfilePictureUrl(this.props.user.id, this.props.user.last_picture_update); } let statusIcon; @@ -85,7 +87,7 @@ export default class ProfilePicture extends React.PureComponent { {this.props.status && diff --git a/app/components/push_notification/push_notification.js b/app/components/push_notification/push_notification.js index 6cc6e3542..741c0c6ef 100644 --- a/app/components/push_notification/push_notification.js +++ b/app/components/push_notification/push_notification.js @@ -10,7 +10,7 @@ import {changeOpacity} from 'app/utils/theme'; import icon from 'assets/images/icon.png'; import {GooglePlaySenderId} from 'assets/config.json'; -import {Constants} from 'mattermost-redux/constants'; +import {General} from 'mattermost-redux/constants'; export default class PushNotification extends PureComponent { static propTypes = { @@ -63,7 +63,7 @@ export default class PushNotification extends PureComponent { }; onRegisterDevice = (data) => { - const prefix = Platform.OS === 'ios' ? Constants.PUSH_NOTIFY_APPLE_REACT_NATIVE : Constants.PUSH_NOTIFY_ANDROID_REACT_NATIVE; + const prefix = Platform.OS === 'ios' ? General.PUSH_NOTIFY_APPLE_REACT_NATIVE : General.PUSH_NOTIFY_ANDROID_REACT_NATIVE; this.props.actions.setDeviceToken(`${prefix}:${data.token}`); this.isConfigured = true; }; diff --git a/app/components/root/root.js b/app/components/root/root.js index 95a4f1276..3a41269ed 100644 --- a/app/components/root/root.js +++ b/app/components/root/root.js @@ -20,8 +20,8 @@ import {getTranslations} from 'app/i18n'; import Config from 'assets/config'; -import Client from 'mattermost-redux/client'; -import {Constants} from 'mattermost-redux/constants'; +import {Client4} from 'mattermost-redux/client'; +import {General} from 'mattermost-redux/constants'; import EventEmitter from 'mattermost-redux/utils/event_emitter'; export default class Root extends Component { @@ -45,8 +45,8 @@ export default class Root extends Component { componentDidMount() { this.props.actions.setAppState(AppState.currentState === 'active'); AppState.addEventListener('change', this.handleAppStateChange); - EventEmitter.on(Constants.CONFIG_CHANGED, this.handleConfigChanged); - Client.setUserAgent(DeviceInfo.getUserAgent()); + EventEmitter.on(General.CONFIG_CHANGED, this.handleConfigChanged); + Client4.setUserAgent(DeviceInfo.getUserAgent()); if (Platform.OS === 'android') { BackAndroid.addEventListener('hardwareBackPress', this.handleAndroidBack); @@ -55,7 +55,7 @@ export default class Root extends Component { componentWillUnmount() { AppState.removeEventListener('change', this.handleAppStateChange); - EventEmitter.off(Constants.CONFIG_CHANGED, this.handleConfigChanged); + EventEmitter.off(General.CONFIG_CHANGED, this.handleConfigChanged); if (Platform.OS === 'android') { BackAndroid.removeEventListener('hardwareBackPress', this.handleAndroidBack); @@ -116,7 +116,7 @@ export default class Root extends Component { handleVersionUpgrade = async () => { const {closeDrawers, logout, unrenderDrawer} = this.props.actions; - Client.serverVersion = ''; + Client4.serverVersion = ''; closeDrawers(); unrenderDrawer(); diff --git a/app/initial_state.js b/app/initial_state.js index 1fd7f706c..07e7811cf 100644 --- a/app/initial_state.js +++ b/app/initial_state.js @@ -30,8 +30,7 @@ const state = { teams: {}, myMembers: {}, membersInTeam: {}, - stats: {}, - openTeamIds: new Set() + stats: {} }, channels: { currentChannelId: '', @@ -41,7 +40,7 @@ const state = { }, posts: { posts: {}, - postsByChannel: {}, + postsInChannel: {}, selectedPostId: '', currentFocusedPostId: '' }, @@ -93,10 +92,6 @@ const state = { status: 'not_started', error: null }, - getMoreChannels: { - status: 'not_started', - error: null - }, getChannelStats: { status: 'not_started', error: null @@ -141,7 +136,7 @@ const state = { status: 'not_started', error: null }, - getPost: { + getPostThread: { status: 'not_started', error: null }, @@ -163,11 +158,11 @@ const state = { } }, teams: { - allTeams: { + getMyTeams: { status: 'not_started', error: null }, - getAllTeamListings: { + getTeams: { status: 'not_started', error: null }, diff --git a/app/mattermost.js b/app/mattermost.js index 74954d900..7ec56cb1b 100644 --- a/app/mattermost.js +++ b/app/mattermost.js @@ -16,7 +16,7 @@ const store = configureStore(initialState); export default class Mattermost extends React.Component { state = { hydrated: false - } + }; componentWillMount() { this.unsubscribeFromStore = store.subscribe(this.listenForHydration); @@ -31,7 +31,7 @@ export default class Mattermost extends React.Component { hydrated: true }); } - } + }; render() { if (!this.state.hydrated) { diff --git a/app/reducers/navigation/index.js b/app/reducers/navigation/index.js index 1ac3f4de9..302ed6b64 100644 --- a/app/reducers/navigation/index.js +++ b/app/reducers/navigation/index.js @@ -3,7 +3,7 @@ import {NavigationExperimental} from 'react-native'; -import {UsersTypes} from 'mattermost-redux/constants'; +import {UserTypes} from 'mattermost-redux/action_types'; import {NavigationTypes} from 'app/constants'; import Routes from 'app/navigation/routes'; @@ -220,7 +220,7 @@ export default function(state = initialState, action) { }; } - case UsersTypes.LOGOUT_SUCCESS: + case UserTypes.LOGOUT_SUCCESS: return NavigationExperimental.StateUtils.reset(state, initialState.routes, initialState.index); default: diff --git a/app/reducers/views/channel.js b/app/reducers/views/channel.js index e7c134abd..a1eb5a28e 100644 --- a/app/reducers/views/channel.js +++ b/app/reducers/views/channel.js @@ -2,7 +2,7 @@ // See License.txt for license information. import {combineReducers} from 'redux'; -import {ChannelTypes, FilesTypes, UsersTypes} from 'mattermost-redux/constants'; +import {ChannelTypes, FileTypes, UserTypes} from 'mattermost-redux/action_types'; import {ViewTypes} from 'app/constants'; @@ -53,7 +53,7 @@ function drafts(state = {}, action) { [action.channelId]: Object.assign({}, state[action.channelId], {files}) }; } - case FilesTypes.RECEIVED_UPLOAD_FILES: { + case FileTypes.RECEIVED_UPLOAD_FILES: { if (action.rootId || !state[action.channelId].files) { return state; } @@ -128,7 +128,7 @@ function appInitializing(state = true, action) { switch (action.type) { case ViewTypes.APPLICATION_INITIALIZED: return false; - case UsersTypes.RESET_LOGOUT_STATE: + case UserTypes.RESET_LOGOUT_STATE: return true; default: return state; diff --git a/app/reducers/views/connection.js b/app/reducers/views/connection.js index 0ae906c9c..76fbb34a9 100644 --- a/app/reducers/views/connection.js +++ b/app/reducers/views/connection.js @@ -1,7 +1,7 @@ // Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -import {UsersTypes} from 'mattermost-redux/constants'; +import {UserTypes} from 'mattermost-redux/action_types'; import {ViewTypes} from 'app/constants'; export default function connection(state = true, action) { @@ -9,7 +9,7 @@ export default function connection(state = true, action) { case ViewTypes.CONNECTION_CHANGED: return action.data; - case UsersTypes.LOGOUT_SUCCESS: + case UserTypes.LOGOUT_SUCCESS: return true; } diff --git a/app/reducers/views/i18n.js b/app/reducers/views/i18n.js index 580abbb98..e39772062 100644 --- a/app/reducers/views/i18n.js +++ b/app/reducers/views/i18n.js @@ -5,14 +5,15 @@ import {combineReducers} from 'redux'; import Config from 'assets/config.json'; -import {UsersTypes} from 'mattermost-redux/constants'; +import {UserTypes} from 'mattermost-redux/action_types'; function locale(state = Config.DefaultLocale, action) { switch (action.type) { - case UsersTypes.RECEIVED_ME: - return action.data.locale; - - case UsersTypes.LOGOUT_SUCCESS: + case UserTypes.RECEIVED_ME: { + const data = action.data || action.payload; + return data.locale; + } + case UserTypes.LOGOUT_SUCCESS: return Config.DefaultLocale; } diff --git a/app/reducers/views/login.js b/app/reducers/views/login.js index 9b59d0d34..2e8e67330 100644 --- a/app/reducers/views/login.js +++ b/app/reducers/views/login.js @@ -2,14 +2,14 @@ // See License.txt for license information. import {combineReducers} from 'redux'; -import {UsersTypes} from 'mattermost-redux/constants'; +import {UserTypes} from 'mattermost-redux/action_types'; import {ViewTypes} from 'app/constants'; function loginId(state = '', action) { switch (action.type) { case ViewTypes.LOGIN_ID_CHANGED: return action.loginId.trim(); - case UsersTypes.LOGOUT_SUCCESS: + case UserTypes.LOGOUT_SUCCESS: return ''; default: return state; @@ -20,7 +20,7 @@ function password(state = '', action) { switch (action.type) { case ViewTypes.PASSWORD_CHANGED: return action.password; - case UsersTypes.LOGOUT_SUCCESS: + case UserTypes.LOGOUT_SUCCESS: return ''; default: return state; diff --git a/app/reducers/views/options_modal.js b/app/reducers/views/options_modal.js index 06eacfa2c..dff5152c7 100644 --- a/app/reducers/views/options_modal.js +++ b/app/reducers/views/options_modal.js @@ -3,14 +3,14 @@ import {combineReducers} from 'redux'; -import {UsersTypes} from 'mattermost-redux/constants'; +import {UserTypes} from 'mattermost-redux/action_types'; import {ViewTypes} from 'app/constants'; function title(state = '', action) { switch (action.type) { case ViewTypes.OPTIONS_MODAL_CHANGED: return action.data.title; - case UsersTypes.LOGOUT_SUCCESS: + case UserTypes.LOGOUT_SUCCESS: return ''; default: return state; @@ -21,7 +21,7 @@ function options(state = [], action) { switch (action.type) { case ViewTypes.OPTIONS_MODAL_CHANGED: return action.data.options; - case UsersTypes.LOGOUT_SUCCESS: + case UserTypes.LOGOUT_SUCCESS: return []; default: return state; @@ -32,7 +32,7 @@ function visible(state = false, action) { switch (action.type) { case ViewTypes.OPTIONS_MODAL_CHANGED: return action.data.visible; - case UsersTypes.LOGOUT_SUCCESS: + case UserTypes.LOGOUT_SUCCESS: return false; default: return state; diff --git a/app/reducers/views/root.js b/app/reducers/views/root.js index 523aaba1a..183b7ff13 100644 --- a/app/reducers/views/root.js +++ b/app/reducers/views/root.js @@ -2,11 +2,11 @@ // See License.txt for license information. import {combineReducers} from 'redux'; -import {Constants} from 'mattermost-redux/constants'; +import {General} from 'mattermost-redux/constants'; function hydrationComplete(state = false, action) { switch (action.type) { - case Constants.STORE_REHYDRATION_COMPLETE: + case General.STORE_REHYDRATION_COMPLETE: return true; default: return state; diff --git a/app/reducers/views/team.js b/app/reducers/views/team.js index fbe5aac1c..c5d9184e9 100644 --- a/app/reducers/views/team.js +++ b/app/reducers/views/team.js @@ -2,13 +2,13 @@ // See License.txt for license information. import {combineReducers} from 'redux'; -import {TeamsTypes} from 'mattermost-redux/constants'; +import {TeamTypes} from 'mattermost-redux/action_types'; import {ViewTypes} from 'app/constants'; function lastTeamId(state = '', action) { switch (action.type) { - case TeamsTypes.SELECT_TEAM: + case TeamTypes.SELECT_TEAM: return action.data; default: return state; diff --git a/app/reducers/views/thread.js b/app/reducers/views/thread.js index eb5dd1ac8..f894a093d 100644 --- a/app/reducers/views/thread.js +++ b/app/reducers/views/thread.js @@ -2,7 +2,7 @@ // See License.txt for license information. import {combineReducers} from 'redux'; -import {FilesTypes, PostsTypes} from 'mattermost-redux/constants'; +import {FileTypes, PostTypes} from 'mattermost-redux/action_types'; import {ViewTypes} from 'app/constants'; @@ -21,7 +21,7 @@ function drafts(state = {}, action) { files: action.files } }; - case PostsTypes.RECEIVED_POST_SELECTED: { + case PostTypes.RECEIVED_POST_SELECTED: { let data = {...state}; if (!data[action.data]) { @@ -52,7 +52,7 @@ function drafts(state = {}, action) { [action.rootId]: Object.assign({}, state[action.rootId], {files}) }; } - case FilesTypes.RECEIVED_UPLOAD_FILES: { + case FileTypes.RECEIVED_UPLOAD_FILES: { if (!action.rootId) { return state; } diff --git a/app/scenes/account_notifications/account_notifications.js b/app/scenes/account_notifications/account_notifications.js index 96fe86c90..5a0dfae3f 100644 --- a/app/scenes/account_notifications/account_notifications.js +++ b/app/scenes/account_notifications/account_notifications.js @@ -39,7 +39,7 @@ export default class AccountNotifications extends PureComponent { renderRightComponent: (props, emitter) => { return ; } - } + }; constructor(props) { super(props); diff --git a/app/scenes/channel/channel.js b/app/scenes/channel/channel.js index 210bfae19..90e932674 100644 --- a/app/scenes/channel/channel.js +++ b/app/scenes/channel/channel.js @@ -15,7 +15,7 @@ import OfflineIndicator from 'app/components/offline_indicator'; import PostTextbox from 'app/components/post_textbox'; import {preventDoubleTap} from 'app/utils/tap'; -import {Constants, RequestStatus} from 'mattermost-redux/constants'; +import {RequestStatus} from 'mattermost-redux/constants'; import EventEmitter from 'mattermost-redux/utils/event_emitter'; import ChannelDrawerButton from './channel_drawer_button'; @@ -159,11 +159,6 @@ export default class Channel extends PureComponent { ); } - let teamId = currentChannel.team_id; - if (currentChannel.type === Constants.DM_CHANNEL || currentChannel.type === Constants.GM_CHANNEL) { - teamId = currentTeam.id; - } - const channelDraft = this.props.drafts[this.props.currentChannel.id]; return ( @@ -179,7 +174,6 @@ export default class Channel extends PureComponent { ref={this.attachPostTextbox} files={channelDraft.files} value={channelDraft.draft} - teamId={teamId} channelId={currentChannel.id} onChangeText={this.handleDraftChanged} /> diff --git a/app/scenes/channel/channel_post_list/channel_post_list.js b/app/scenes/channel/channel_post_list/channel_post_list.js index f2f85a031..0446141a5 100644 --- a/app/scenes/channel/channel_post_list/channel_post_list.js +++ b/app/scenes/channel/channel_post_list/channel_post_list.js @@ -8,7 +8,7 @@ import { View } from 'react-native'; -import {Constants, RequestStatus} from 'mattermost-redux/constants'; +import {General, Posts, RequestStatus} from 'mattermost-redux/constants'; import ChannelLoader from 'app/components/channel_loader'; import PostList from 'app/components/post_list'; @@ -26,7 +26,6 @@ export default class ChannelPostList extends PureComponent { }).isRequired, applicationInitializing: PropTypes.bool.isRequired, channel: PropTypes.object.isRequired, - currentTeamId: PropTypes.string.isRequired, channelIsLoading: PropTypes.bool, myMember: PropTypes.object.isRequired, postsRequests: PropTypes.shape({ @@ -60,9 +59,9 @@ export default class ChannelPostList extends PureComponent { const didMorePostsLoad = this.didPostsLoad(nextProps, 'getPostsBefore'); let hasFirstPost = false; if (didInitialPostsLoad) { - hasFirstPost = nextProps.posts.length < Constants.POST_CHUNK_SIZE; + hasFirstPost = nextProps.posts.length < Posts.POST_CHUNK_SIZE; } else if (didMorePostsLoad) { - hasFirstPost = (nextProps.posts.length - this.props.posts.length) < Constants.POST_CHUNK_SIZE; + hasFirstPost = (nextProps.posts.length - this.props.posts.length) < General.POST_CHUNK_SIZE; } if (hasFirstPost) { this.setState({hasFirstPost}); @@ -95,19 +94,12 @@ export default class ChannelPostList extends PureComponent { } loadMorePosts = () => { - const {currentTeamId, channel, posts, postsRequests} = this.props; - const {team_id: teamId, id: channelId} = channel; + const {channel, posts, postsRequests} = this.props; + const {id: channelId} = channel; const oldestPost = posts[posts.length - 1]; const {didInitialPostsLoad, hasFirstPost} = this.state; if (didInitialPostsLoad && !hasFirstPost && oldestPost && postsRequests.getPostsBefore.status !== RequestStatus.STARTED) { - let postsForTeamId = teamId; - switch (channel.type) { - case Constants.DM_CHANNEL: - case Constants.GM_CHANNEL: - postsForTeamId = currentTeamId; - break; - } - return this.props.actions.getPostsBefore(postsForTeamId, channelId, oldestPost.id); + return this.props.actions.getPostsBefore(channelId, oldestPost.id); } return null; }; diff --git a/app/scenes/channel/channel_post_list/index.js b/app/scenes/channel/channel_post_list/index.js index 7ee23f8ec..ebe2e6dc9 100644 --- a/app/scenes/channel/channel_post_list/index.js +++ b/app/scenes/channel/channel_post_list/index.js @@ -6,8 +6,7 @@ import {connect} from 'react-redux'; import {createSelector} from 'reselect'; import {getPostsBefore} from 'mattermost-redux/actions/posts'; import {getAllPosts, getPostsInCurrentChannel} from 'mattermost-redux/selectors/entities/posts'; -import {getCurrentChannelMembership} from 'mattermost-redux/selectors/entities/channels'; -import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams'; +import {getMyCurrentChannelMembership} from 'mattermost-redux/selectors/entities/channels'; import {goToThread} from 'app/actions/navigation'; import {loadPostsIfNecessary} from 'app/actions/views/channel'; @@ -69,9 +68,8 @@ function mapStateToProps(state, ownProps) { return { ...ownProps, applicationInitializing: state.views.channel.appInitializing, - currentTeamId: getCurrentTeamId(state), channelIsLoading: state.views.channel.loading, - myMember: getCurrentChannelMembership(state), + myMember: getMyCurrentChannelMembership(state), postsRequests: state.requests.posts, posts: getPostsInCurrentChannelWithReplyProps(state), theme: getTheme(state), diff --git a/app/scenes/channel_add_members/channel_add_members.js b/app/scenes/channel_add_members/channel_add_members.js index 0c8e28548..db631c7ed 100644 --- a/app/scenes/channel_add_members/channel_add_members.js +++ b/app/scenes/channel_add_members/channel_add_members.js @@ -18,8 +18,9 @@ import SearchBar from 'app/components/search_bar'; import {createMembersSections, loadingText, renderMemberRow} from 'app/utils/member_list'; import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme'; -import {Constants, RequestStatus} from 'mattermost-redux/constants'; +import {General, RequestStatus} from 'mattermost-redux/constants'; import EventEmitter from 'mattermost-redux/utils/event_emitter'; +import {filterProfiles} from 'mattermost-redux/utils/user_utils'; class ChannelAddMembers extends PureComponent { static propTypes = { @@ -61,11 +62,21 @@ class ChannelAddMembers extends PureComponent { } }; - state = { - canSelect: true, - profiles: [], - selectedMembers: {} - }; + constructor(props) { + super(props); + + this.searchTimeoutId = 0; + + this.state = { + canSelect: true, + next: true, + page: 0, + profiles: [], + searching: false, + selectedMembers: {}, + showNoResults: false + }; + } componentWillMount() { this.props.subscribeToHeaderEvent('add_members', this.handleAddMembersPress); @@ -96,15 +107,13 @@ class ChannelAddMembers extends PureComponent { const {loadMoreRequestStatus} = this.props; if (loadMoreRequestStatus === RequestStatus.STARTED && nextProps.loadMoreRequestStatus === RequestStatus.SUCCESS) { - this.setState({profiles: nextProps.membersNotInChannel}); + const {page} = this.state; + const profiles = nextProps.membersNotInChannel.splice(0, (page + 1) * General.PROFILE_CHUNK_SIZE); + this.setState({profiles, showNoResults: true}); } else if (this.state.searching && nextProps.searchRequestStatus === RequestStatus.SUCCESS) { - const results = nextProps.membersNotInChannel.filter((p) => { - const {term} = this.state; - return p.username.toLowerCase().includes(term) || p.email.toLowerCase().includes(term) || - p.first_name.toLowerCase().includes(term) || p.last_name.toLowerCase().includes(term); - }); - this.setState({profiles: results}); + const results = filterProfiles(nextProps.membersNotInChannel, this.state.term); + this.setState({profiles: results, showNoResults: true}); } const {addChannelMemberStatus} = nextProps; @@ -145,9 +154,21 @@ class ChannelAddMembers extends PureComponent { }; loadMoreMembers = () => { - const {currentChannelMemberCount, currentTeamMemberCount, membersNotInChannel, loadMoreRequestStatus} = this.props; - if (loadMoreRequestStatus !== RequestStatus.STARTED && membersNotInChannel.length < (currentTeamMemberCount - currentChannelMemberCount - 1)) { - this.props.actions.getProfilesNotInChannel(this.props.currentTeam.id, this.props.currentChannel.id, this.props.membersNotInChannel.length); + const {actions, loadMoreRequestStatus, currentChannel, currentTeam} = this.props; + const {next, searching} = this.state; + let {page} = this.state; + if (loadMoreRequestStatus !== RequestStatus.STARTED && next && !searching) { + page = page + 1; + actions.getProfilesNotInChannel(currentTeam.id, currentChannel.id, page, General.PROFILE_CHUNK_SIZE). + then((data) => { + if (data && data.length) { + this.setState({ + page + }); + } else { + this.setState({next: false}); + } + }); } }; @@ -183,14 +204,15 @@ class ChannelAddMembers extends PureComponent { searchProfiles = (event) => { const term = event.nativeEvent.text.toLowerCase(); + const {actions, currentChannel, currentTeam} = this.props; if (term) { this.setState({searching: true, term}); clearTimeout(this.searchTimeoutId); this.searchTimeoutId = setTimeout(() => { - this.props.actions.searchProfiles(term, {not_in_channel_id: this.props.currentChannel.id}); - }, Constants.SEARCH_TIMEOUT_MILLISECONDS); + actions.searchProfiles(term, {not_in_channel_id: currentChannel.id, team_id: currentTeam.id}); + }, General.SEARCH_TIMEOUT_MILLISECONDS); } else { this.cancelSearch(); } diff --git a/app/scenes/channel_drawer/channel_drawer.js b/app/scenes/channel_drawer/channel_drawer.js index c5c2f315b..888bc9b93 100644 --- a/app/scenes/channel_drawer/channel_drawer.js +++ b/app/scenes/channel_drawer/channel_drawer.js @@ -42,9 +42,9 @@ export default class ChannelDrawer extends PureComponent { viewChannel } = actions; - markChannelAsRead(currentChannel.id); + markChannelAsRead(id, currentChannel.id); setChannelLoading(); - viewChannel(currentTeam.id, id); + viewChannel(currentTeam.id); closeDrawers(); InteractionManager.runAfterInteractions(() => { handleSelectChannel(id); diff --git a/app/scenes/channel_info/channel_info.js b/app/scenes/channel_info/channel_info.js index a4b4f2467..e111d6f54 100644 --- a/app/scenes/channel_info/channel_info.js +++ b/app/scenes/channel_info/channel_info.js @@ -13,7 +13,7 @@ import { import {preventDoubleTap} from 'app/utils/tap'; import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme'; -import {Constants} from 'mattermost-redux/constants'; +import {General} from 'mattermost-redux/constants'; import ChannelInfoHeader from './channel_info_header'; import ChannelInfoRow from './channel_info_row'; @@ -22,7 +22,6 @@ class ChannelInfo extends PureComponent { static propTypes = { intl: intlShape.isRequired, canDeleteChannel: PropTypes.bool.isRequired, - currentTeamId: PropTypes.string.isRequired, currentChannel: PropTypes.object.isRequired, currentChannelCreatorName: PropTypes.string, currentChannelMemberCount: PropTypes.number, @@ -30,7 +29,6 @@ class ChannelInfo extends PureComponent { theme: PropTypes.object.isRequired, isCurrent: PropTypes.bool.isRequired, isFavorite: PropTypes.bool.isRequired, - leaveChannelRequest: PropTypes.object.isRequired, canManageUsers: PropTypes.bool.isRequired, actions: PropTypes.shape({ closeDMChannel: PropTypes.func.isRequired, @@ -55,7 +53,7 @@ class ChannelInfo extends PureComponent { } componentDidMount() { - this.props.actions.getChannelStats(this.props.currentTeamId, this.props.currentChannel.id); + this.props.actions.getChannelStats(this.props.currentChannel.id); } componentWillReceiveProps(nextProps) { @@ -68,7 +66,7 @@ class ChannelInfo extends PureComponent { handleDeleteOrLeave(eventType) { const {formatMessage} = this.props.intl; const channel = this.props.currentChannel; - const term = channel.type === Constants.OPEN_CHANNEL ? + const term = channel.type === General.OPEN_CHANNEL ? formatMessage({id: 'mobile.channel_info.publicChannel', defaultMessage: 'Public Channel'}) : formatMessage({id: 'mobile.channel_info.privateChannel', defaultMessage: 'Private Channel'}); let title; @@ -90,7 +88,7 @@ class ChannelInfo extends PureComponent { defaultMessage: 'Are you sure you want to delete the {term} {name}?' }; onPressAction = () => { - this.props.actions.deleteChannel(channel.team_id, channel.id).then(this.props.actions.goBack); + this.props.actions.deleteChannel(channel.id).then(this.props.actions.goBack); }; } @@ -118,10 +116,10 @@ class ChannelInfo extends PureComponent { const {closeDMChannel, closeGMChannel, goBack} = this.props.actions; switch (channel.type) { - case Constants.DM_CHANNEL: + case General.DM_CHANNEL: closeDMChannel(channel).then(goBack); break; - case Constants.GM_CHANNEL: + case General.GM_CHANNEL: closeGMChannel(channel).then(goBack); break; } @@ -137,17 +135,17 @@ class ChannelInfo extends PureComponent { renderLeaveOrDeleteChannelRow() { const channel = this.props.currentChannel; - const isDefaultChannel = channel.name === Constants.DEFAULT_CHANNEL; - const isDirectMessage = channel.type === Constants.DM_CHANNEL; - const isGroupMessage = channel.type === Constants.GM_CHANNEL; + const isDefaultChannel = channel.name === General.DEFAULT_CHANNEL; + const isDirectMessage = channel.type === General.DM_CHANNEL; + const isGroupMessage = channel.type === General.GM_CHANNEL; return !isDefaultChannel && !isDirectMessage && !isGroupMessage; } renderCloseDirect() { const channel = this.props.currentChannel; - const isDirectMessage = channel.type === Constants.DM_CHANNEL; - const isGroupMessage = channel.type === Constants.GM_CHANNEL; + const isDirectMessage = channel.type === General.DM_CHANNEL; + const isGroupMessage = channel.type === General.GM_CHANNEL; return isDirectMessage || isGroupMessage; } @@ -168,11 +166,11 @@ class ChannelInfo extends PureComponent { let i18nId; let defaultMessage; switch (currentChannel.type) { - case Constants.DM_CHANNEL: + case General.DM_CHANNEL: i18nId = 'mobile.channel_list.closeDM'; defaultMessage = 'Close Direct Message'; break; - case Constants.GM_CHANNEL: + case General.GM_CHANNEL: i18nId = 'mobile.channel_list.closeGM'; defaultMessage = 'Close Group Message'; break; diff --git a/app/scenes/channel_info/channel_info_row.js b/app/scenes/channel_info/channel_info_row.js index 270fa711d..be0fd143b 100644 --- a/app/scenes/channel_info/channel_info_row.js +++ b/app/scenes/channel_info/channel_info_row.js @@ -14,34 +14,6 @@ import Icon from 'react-native-vector-icons/FontAwesome'; import FormattedText from 'app/components/formatted_text'; import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme'; -const getStyleSheet = makeStyleSheetFromTheme((theme) => { - return StyleSheet.create({ - container: { - backgroundColor: theme.centerChannelBg, - paddingHorizontal: 15, - flexDirection: 'row', - alignItems: 'center' - }, - detail: { - marginHorizontal: 15, - color: changeOpacity(theme.centerChannelColor, 0.5), - fontSize: 15 - }, - label: { - flex: 1, - marginLeft: 15, - fontSize: 15, - paddingVertical: 15 - }, - leftIcon: { - width: 17 - }, - rightIcon: { - color: changeOpacity(theme.centerChannelColor, 0.5) - } - }); -}); - function createTouchableComponent(children, action) { return ( @@ -115,4 +87,32 @@ channelInfoRow.defaultProps = { shouldRender: true }; +const getStyleSheet = makeStyleSheetFromTheme((theme) => { + return StyleSheet.create({ + container: { + backgroundColor: theme.centerChannelBg, + paddingHorizontal: 15, + flexDirection: 'row', + alignItems: 'center' + }, + detail: { + marginHorizontal: 15, + color: changeOpacity(theme.centerChannelColor, 0.5), + fontSize: 15 + }, + label: { + flex: 1, + marginLeft: 15, + fontSize: 15, + paddingVertical: 15 + }, + leftIcon: { + width: 17 + }, + rightIcon: { + color: changeOpacity(theme.centerChannelColor, 0.5) + } + }); +}); + export default channelInfoRow; diff --git a/app/scenes/channel_info/index.js b/app/scenes/channel_info/index.js index aefbe5cd3..997a6ed6b 100644 --- a/app/scenes/channel_info/index.js +++ b/app/scenes/channel_info/index.js @@ -15,7 +15,7 @@ import navigationSceneConnect from 'app/scenes/navigationSceneConnect'; import {getTheme} from 'app/selectors/preferences'; import {getChannelStats, deleteChannel} from 'mattermost-redux/actions/channels'; -import {Constants} from 'mattermost-redux/constants'; +import {General} from 'mattermost-redux/constants'; import { getCurrentChannel, getCurrentChannelStats, @@ -38,11 +38,10 @@ function mapStateToProps(state, ownProps) { const favoriteChannels = getChannelsByCategory(state).favoriteChannels.map((f) => f.id); const isCurrent = currentChannel.id === state.entities.channels.currentChannelId; const isFavorite = favoriteChannels.indexOf(currentChannel.id) > -1; - const leaveChannelRequest = state.requests.channels.leaveChannel; const roles = getCurrentUserRoles(state); let status; - if (currentChannel.type === Constants.DM_CHANNEL) { + if (currentChannel.type === General.DM_CHANNEL) { const teammateId = getUserIdFromChannelName(currentUserId, currentChannel.name); status = getStatusForUserId(state, teammateId); } @@ -50,13 +49,11 @@ function mapStateToProps(state, ownProps) { return { ...ownProps, canDeleteChannel: showDeleteOption(config, license, currentChannel, isAdmin(roles), isSystemAdmin(roles), isChannelAdmin(roles)), - currentTeamId: state.entities.teams.currentTeamId, currentChannel, currentChannelCreatorName, currentChannelMemberCount, isCurrent, isFavorite, - leaveChannelRequest, status, theme: getTheme(state), canManageUsers: canManageChannelMembers(state) diff --git a/app/scenes/channel_members/channel_members.js b/app/scenes/channel_members/channel_members.js index 859e2d9a8..6e5c0f745 100644 --- a/app/scenes/channel_members/channel_members.js +++ b/app/scenes/channel_members/channel_members.js @@ -16,11 +16,11 @@ import MemberList from 'app/components/custom_list'; import SearchBar from 'app/components/search_bar'; import {createMembersSections, loadingText} from 'app/utils/member_list'; import MemberListRow from 'app/components/custom_list/member_list_row'; -import {displayUsername} from 'mattermost-redux/utils/user_utils'; import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme'; -import {Constants, RequestStatus} from 'mattermost-redux/constants'; +import {General, RequestStatus} from 'mattermost-redux/constants'; import EventEmitter from 'mattermost-redux/utils/event_emitter'; +import {displayUsername, filterProfiles} from 'mattermost-redux/utils/user_utils'; import ChannelMembersTitle from './channel_members_title'; import RemoveMemberButton from './remove_member_button'; @@ -33,7 +33,6 @@ class ChannelMembers extends PureComponent { currentChannelMembers: PropTypes.array.isRequired, currentChannelMemberCount: PropTypes.number.isRequired, currentUserId: PropTypes.string.isRequired, - currentTeam: PropTypes.object, preferences: PropTypes.object, requestStatus: PropTypes.string, searchRequestStatus: PropTypes.string, @@ -58,11 +57,21 @@ class ChannelMembers extends PureComponent { } }; - state = { - canSelect: true, - profiles: [], - selectedMembers: {} - }; + constructor(props) { + super(props); + + this.searchTimeoutId = 0; + + this.state = { + canSelect: true, + next: true, + page: 0, + profiles: [], + searching: false, + selectedMembers: {}, + showNoResults: false + }; + } componentWillMount() { this.props.subscribeToHeaderEvent('remove_members', this.handleRemoveMembersPress); @@ -74,7 +83,7 @@ class ChannelMembers extends PureComponent { } InteractionManager.runAfterInteractions(() => { - this.props.actions.getProfilesInChannel(this.props.currentTeam.id, this.props.currentChannel.id, 0); + this.props.actions.getProfilesInChannel(this.props.currentChannel.id, 0); }); this.emitCanRemoveMembers(false); @@ -92,15 +101,13 @@ class ChannelMembers extends PureComponent { const {requestStatus} = this.props; if (requestStatus === RequestStatus.STARTED && nextProps.requestStatus === RequestStatus.SUCCESS) { - this.setState({profiles: nextProps.currentChannelMembers}); + const {page} = this.state; + const profiles = nextProps.currentChannelMembers.splice(0, (page + 1) * General.PROFILE_CHUNK_SIZE); + this.setState({profiles, showNoResults: true}); } else if (this.state.searching && nextProps.searchRequestStatus === RequestStatus.SUCCESS) { - const results = nextProps.currentChannelMembers.filter((p) => { - const {term} = this.state; - return p.username.toLowerCase().includes(term) || p.email.toLowerCase().includes(term) || - p.first_name.toLowerCase().includes(term) || p.last_name.toLowerCase().includes(term); - }); - this.setState({profiles: results}); + const results = filterProfiles(nextProps.currentChannelMembers, this.state.term); + this.setState({profiles: results, showNoResults: true}); } const {removeMembersStatus} = nextProps; @@ -165,13 +172,26 @@ class ChannelMembers extends PureComponent { }; removeMembers = (membersToRemove) => { - const {actions, currentTeam, currentChannel} = this.props; - actions.handleRemoveChannelMembers(currentTeam.id, currentChannel.id, membersToRemove); + const {actions, currentChannel} = this.props; + actions.handleRemoveChannelMembers(currentChannel.id, membersToRemove); }; loadMoreMembers = () => { - if (this.props.requestStatus !== 'started' && this.props.currentChannelMembers.length < this.props.currentChannelMemberCount - 1) { - this.props.actions.getProfilesInChannel(this.props.currentTeam.id, this.props.currentChannel.id, this.props.currentChannelMembers.length); + const {actions, requestStatus, currentChannel} = this.props; + const {next, searching} = this.state; + let {page} = this.state; + if (requestStatus !== RequestStatus.STARTED && next && !searching) { + page = page + 1; + actions.getProfilesInChannel(currentChannel.id, page, General.PROFILE_CHUNK_SIZE). + then((data) => { + if (data && data.length) { + this.setState({ + page + }); + } else { + this.setState({next: false}); + } + }); } }; @@ -213,7 +233,7 @@ class ChannelMembers extends PureComponent { this.searchTimeoutId = setTimeout(() => { this.props.actions.searchProfiles(term, {in_channel_id: this.props.currentChannel.id}); - }, Constants.SEARCH_TIMEOUT_MILLISECONDS); + }, General.SEARCH_TIMEOUT_MILLISECONDS); } else { this.cancelSearch(); } @@ -257,8 +277,8 @@ class ChannelMembers extends PureComponent { render() { const {canManageUsers, intl, preferences, requestStatus, searchRequestStatus, theme} = this.props; const {formatMessage} = intl; - const {profiles, searching} = this.state; - const isLoading = (requestStatus === RequestStatus.STARTED) || + const {profiles, searching, showNoResults} = this.state; + const isLoading = (requestStatus === RequestStatus.STARTED) || (requestStatus.status === RequestStatus.NOT_STARTED) || (searchRequestStatus === RequestStatus.STARTED); const more = searching ? () => true : this.loadMoreMembers; const style = getStyleFromTheme(theme); @@ -294,6 +314,7 @@ class ChannelMembers extends PureComponent { onRowSelect={this.handleRowSelect} renderRow={this.renderMemberRow} createSections={createMembersSections} + showNoResults={showNoResults} /> ); diff --git a/app/scenes/channel_members/index.js b/app/scenes/channel_members/index.js index 4c913c402..3852b7477 100644 --- a/app/scenes/channel_members/index.js +++ b/app/scenes/channel_members/index.js @@ -10,7 +10,6 @@ import {handleRemoveChannelMembers} from 'app/actions/views/channel_members'; import {getTheme} from 'app/selectors/preferences'; import {getCurrentChannel, getCurrentChannelStats, canManageChannelMembers} from 'mattermost-redux/selectors/entities/channels'; import {getMyPreferences} from 'mattermost-redux/selectors/entities/preferences'; -import {getCurrentTeam} from 'mattermost-redux/selectors/entities/teams'; import {getProfilesInCurrentChannel} from 'mattermost-redux/selectors/entities/users'; import {getProfilesInChannel, searchProfiles} from 'mattermost-redux/actions/users'; @@ -25,7 +24,6 @@ function mapStateToProps(state) { currentChannelMembers: getProfilesInCurrentChannel(state), currentChannelMemberCount, currentUserId: state.entities.users.currentUserId, - currentTeam: getCurrentTeam(state), preferences: getMyPreferences(state), requestStatus: state.requests.users.getProfilesInChannel.status, searchRequestStatus: state.requests.users.searchProfiles.status, diff --git a/app/scenes/create_channel/create_channel.js b/app/scenes/create_channel/create_channel.js index 187dcbfc7..ee82dc420 100644 --- a/app/scenes/create_channel/create_channel.js +++ b/app/scenes/create_channel/create_channel.js @@ -20,7 +20,7 @@ import TextInputWithLocalizedPlaceholder from 'app/components/text_input_with_lo import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme'; -import {Constants, RequestStatus} from 'mattermost-redux/constants'; +import {General, RequestStatus} from 'mattermost-redux/constants'; import EventEmitter from 'mattermost-redux/utils/event_emitter'; import ActionButton from 'app/components/action_button'; @@ -41,7 +41,7 @@ class CreateChannel extends PureComponent { }; static defaultProps = { - channelType: Constants.OPEN_CHANNEL + channelType: General.OPEN_CHANNEL }; static navigationProps = { @@ -191,9 +191,9 @@ class CreateChannel extends PureComponent { const style = getStyleSheet(theme); let term; - if (channelType === Constants.OPEN_CHANNEL) { + if (channelType === General.OPEN_CHANNEL) { term = formatMessage({id: 'channel_modal.channel', defaultMessage: 'Channel'}); - } else if (channelType === Constants.PRIVATE_CHANNEL) { + } else if (channelType === General.PRIVATE_CHANNEL) { term = formatMessage({id: 'channel_modal.group', defaultMessage: 'Group'}); } diff --git a/app/scenes/load_team/index.js b/app/scenes/load_team/index.js index 83365c380..b1238b9d5 100644 --- a/app/scenes/load_team/index.js +++ b/app/scenes/load_team/index.js @@ -18,7 +18,7 @@ function mapStateToProps(state) { return { config: state.entities.general.config, theme: getTheme(state), - teamsRequest: state.requests.teams.allTeams, + teamsRequest: state.requests.teams.getMyTeams, teams: state.entities.teams.teams, currentTeam: getCurrentTeam(state), myMembers: state.entities.teams.myMembers, diff --git a/app/scenes/more_channels/create_button.js b/app/scenes/more_channels/create_button.js index c7fd39e00..de522de64 100644 --- a/app/scenes/more_channels/create_button.js +++ b/app/scenes/more_channels/create_button.js @@ -8,7 +8,7 @@ import ActionButton from 'app/components/action_button'; import {getTheme} from 'app/selectors/preferences'; -import {Constants} from 'mattermost-redux/constants'; +import {General} from 'mattermost-redux/constants'; import {getCurrentUserRoles} from 'mattermost-redux/selectors/entities/users'; import {showCreateOption} from 'mattermost-redux/utils/channel_utils'; import {isAdmin, isSystemAdmin} from 'mattermost-redux/utils/user_utils'; @@ -46,7 +46,7 @@ function mapStateToProps(state) { const roles = getCurrentUserRoles(state); return { - canCreateChannels: showCreateOption(config, license, Constants.PRIVATE_CHANNEL, isAdmin(roles), isSystemAdmin(roles)), + canCreateChannels: showCreateOption(config, license, General.PRIVATE_CHANNEL, isAdmin(roles), isSystemAdmin(roles)), theme: getTheme(state) }; } diff --git a/app/scenes/more_channels/index.js b/app/scenes/more_channels/index.js index 4676cfa7a..10e6a5b07 100644 --- a/app/scenes/more_channels/index.js +++ b/app/scenes/more_channels/index.js @@ -7,17 +7,17 @@ import navigationSceneConnect from '../navigationSceneConnect'; import {closeDrawers, goBack, goToCreateChannel} from 'app/actions/navigation'; import {getTheme} from 'app/selectors/preferences'; -import {getMoreChannels as getMoreChannelsSelector} from 'mattermost-redux/selectors/entities/channels'; +import {getOtherChannels} from 'mattermost-redux/selectors/entities/channels'; import {handleSelectChannel} from 'app/actions/views/channel'; -import {getMoreChannels, joinChannel, searchMoreChannels} from 'mattermost-redux/actions/channels'; +import {getChannels, joinChannel, searchMoreChannels} from 'mattermost-redux/actions/channels'; import MoreChannels from './more_channels'; function mapStateToProps(state) { const {currentUserId} = state.entities.users; const {currentTeamId} = state.entities.teams; - const {getMoreChannels: requestStatus} = state.requests.channels; - const channels = getMoreChannelsSelector(state); + const {getChannels: requestStatus} = state.requests.channels; + const channels = getOtherChannels(state); return { currentUserId, @@ -36,7 +36,7 @@ function mapDispatchToProps(dispatch) { handleSelectChannel, goToCreateChannel, joinChannel, - getMoreChannels, + getChannels, searchMoreChannels }, dispatch) }; diff --git a/app/scenes/more_channels/more_channels.js b/app/scenes/more_channels/more_channels.js index 8c71e57b0..a991be78e 100644 --- a/app/scenes/more_channels/more_channels.js +++ b/app/scenes/more_channels/more_channels.js @@ -19,7 +19,7 @@ import Loading from 'app/components/loading'; import SearchBar from 'app/components/search_bar'; import {makeStyleSheetFromTheme, changeOpacity} from 'app/utils/theme'; -import {Constants, RequestStatus} from 'mattermost-redux/constants'; +import {General, RequestStatus} from 'mattermost-redux/constants'; import EventEmitter from 'mattermost-redux/utils/event_emitter'; import CreateButton from './create_button'; @@ -40,7 +40,7 @@ class MoreChannels extends PureComponent { handleSelectChannel: PropTypes.func.isRequired, goToCreateChannel: PropTypes.func.isRequired, joinChannel: PropTypes.func.isRequired, - getMoreChannels: PropTypes.func.isRequired, + getChannels: PropTypes.func.isRequired, searchMoreChannels: PropTypes.func.isRequired }).isRequired }; @@ -71,7 +71,7 @@ class MoreChannels extends PureComponent { this.searchTimeoutId = 0; this.state = { - channels: props.channels, + channels: props.channels.splice(0, General.CHANNELS_CHUNK_SIZE), page: 0, adding: false, next: true, @@ -94,7 +94,7 @@ class MoreChannels extends PureComponent { } else if (requestStatus.status === RequestStatus.STARTED && nextProps.requestStatus.status === RequestStatus.SUCCESS) { const {page} = this.state; - const channels = nextProps.channels.splice(0, (page + 1) * Constants.CHANNELS_CHUNK_SIZE); + const channels = nextProps.channels.splice(0, (page + 1) * General.CHANNELS_CHUNK_SIZE); this.setState({channels, showNoResults: true}); } } @@ -107,7 +107,7 @@ class MoreChannels extends PureComponent { // set the timeout to 400 cause is the time that the modal takes to open // Somehow interactionManager doesn't care setTimeout(() => { - this.props.actions.getMoreChannels(this.props.currentTeamId, 0); + this.props.actions.getChannels(this.props.currentTeamId, 0); }, 400); } @@ -152,14 +152,14 @@ class MoreChannels extends PureComponent { this.searchTimeoutId = setTimeout(() => { this.props.actions.searchMoreChannels(this.props.currentTeamId, term); - }, Constants.SEARCH_TIMEOUT_MILLISECONDS); + }, General.SEARCH_TIMEOUT_MILLISECONDS); } else { this.cancelSearch(); } }; cancelSearch = () => { - this.props.actions.getMoreChannels(this.props.currentTeamId, 0); + this.props.actions.getChannels(this.props.currentTeamId, 0); this.setState({ term: null, searching: false, @@ -171,12 +171,12 @@ class MoreChannels extends PureComponent { let {page} = this.state; if (this.props.requestStatus.status !== RequestStatus.STARTED && this.state.next && !this.state.searching) { page = page + 1; - this.props.actions.getMoreChannels( + this.props.actions.getChannels( this.props.currentTeamId, - page * Constants.CHANNELS_CHUNK_SIZE, - Constants.CHANNELS_CHUNK_SIZE). + page, + General.CHANNELS_CHUNK_SIZE). then((data) => { - if (Object.keys(data).length) { + if (data && data.length) { this.setState({ page }); @@ -225,7 +225,7 @@ class MoreChannels extends PureComponent { }; onCreateChannel = async () => { - this.props.actions.goToCreateChannel(Constants.OPEN_CHANNEL); + this.props.actions.goToCreateChannel(General.OPEN_CHANNEL); }; render() { diff --git a/app/scenes/more_dms/more_dms.js b/app/scenes/more_dms/more_dms.js index 7d4881c86..dde0c848b 100644 --- a/app/scenes/more_dms/more_dms.js +++ b/app/scenes/more_dms/more_dms.js @@ -17,9 +17,11 @@ import Loading from 'app/components/loading'; import MemberList from 'app/components/custom_list'; import SearchBar from 'app/components/search_bar'; import {createMembersSections, loadingText, renderMemberRow} from 'app/utils/member_list'; -import {Constants, RequestStatus} from 'mattermost-redux/constants'; import {makeStyleSheetFromTheme, changeOpacity} from 'app/utils/theme'; +import {General, RequestStatus} from 'mattermost-redux/constants'; +import {filterProfiles} from 'mattermost-redux/utils/user_utils'; + class MoreDirectMessages extends PureComponent { static propTypes = { intl: intlShape.isRequired, @@ -62,7 +64,7 @@ class MoreDirectMessages extends PureComponent { this.searchTimeoutId = 0; this.state = { - profiles: props.profiles, + profiles: props.profiles.splice(0, General.PROFILE_CHUNK_SIZE), page: 0, adding: false, next: true, @@ -80,15 +82,11 @@ class MoreDirectMessages extends PureComponent { if (requestStatus.status === RequestStatus.STARTED && nextProps.requestStatus.status === RequestStatus.SUCCESS) { const {page} = this.state; - const profiles = nextProps.profiles.splice(0, (page + 1) * Constants.PROFILE_CHUNK_SIZE); + const profiles = nextProps.profiles.splice(0, (page + 1) * General.PROFILE_CHUNK_SIZE); this.setState({profiles, showNoResults: true}); } else if (this.state.searching && nextProps.searchRequest.status === RequestStatus.SUCCESS) { - const results = nextProps.profiles.filter((p) => { - const {term} = this.state; - return p.username.toLowerCase().includes(term) || p.email.toLowerCase().includes(term) || - p.first_name.toLowerCase().includes(term) || p.last_name.toLowerCase().includes(term); - }); + const results = filterProfiles(nextProps.profiles, this.state.term); this.setState({profiles: results, showNoResults: true}); } } @@ -134,7 +132,7 @@ class MoreDirectMessages extends PureComponent { this.searchTimeoutId = setTimeout(() => { this.props.actions.searchProfiles(term); - }, Constants.SEARCH_TIMEOUT_MILLISECONDS); + }, General.SEARCH_TIMEOUT_MILLISECONDS); } else { this.cancelSearch(); } @@ -153,9 +151,9 @@ class MoreDirectMessages extends PureComponent { let {page} = this.state; if (this.props.requestStatus.status !== RequestStatus.STARTED && this.state.next && !this.state.searching) { page = page + 1; - this.props.actions.getProfiles(page * Constants.PROFILE_CHUNK_SIZE, Constants.PROFILE_CHUNK_SIZE). + this.props.actions.getProfiles(page, General.PROFILE_CHUNK_SIZE). then((data) => { - if (Object.keys(data).length) { + if (data && data.length) { this.setState({ page }); diff --git a/app/scenes/root/root.js b/app/scenes/root/root.js index 2fc26eacd..727c7f9a2 100644 --- a/app/scenes/root/root.js +++ b/app/scenes/root/root.js @@ -3,12 +3,14 @@ import React, {PropTypes, PureComponent} from 'react'; import Orientation from 'react-native-orientation'; -import Loading from 'app/components/loading'; - -import Client from 'mattermost-redux/client'; -import {RequestStatus} from 'mattermost-redux/constants'; import SplashScreen from 'react-native-smart-splash-screen'; +import {Client4} from 'mattermost-redux/client'; +import {RequestStatus} from 'mattermost-redux/constants'; + +import Loading from 'app/components/loading'; +import {stripTrailingSlashes} from 'app/utils/url'; + export default class Root extends PureComponent { static propTypes = { credentials: PropTypes.object, @@ -59,8 +61,8 @@ export default class Root extends PureComponent { // Will probably need to make this optimistic since we // assume that the stored token is good. if (loginRequest.status === RequestStatus.NOT_STARTED) { - Client.setToken(credentials.token); - Client.setUrl(credentials.url); + Client4.setToken(credentials.token); + Client4.setUrl(stripTrailingSlashes(credentials.url)); loadMe().then(goToLoadTeam).catch(goToLoadTeam); } else { diff --git a/app/scenes/saml/saml.js b/app/scenes/saml/saml.js index c34915907..cf68f4691 100644 --- a/app/scenes/saml/saml.js +++ b/app/scenes/saml/saml.js @@ -8,7 +8,7 @@ import { } from 'react-native'; import CookieManager from 'react-native-cookies'; -import Client from 'mattermost-redux/client'; +import {Client4} from 'mattermost-redux/client'; export default class Saml extends PureComponent { static propTypes = { @@ -40,7 +40,7 @@ export default class Saml extends PureComponent { goToLoadTeam } = this.props.actions; - Client.setToken(token); + Client4.setToken(token); handleSuccessfulLogin(). then(setStoreFromLocalData.bind(null, {url: this.props.serverUrl, token})). then(goToLoadTeam); diff --git a/app/scenes/select_server/select_server.js b/app/scenes/select_server/select_server.js index d94d2db54..a5d1c333b 100644 --- a/app/scenes/select_server/select_server.js +++ b/app/scenes/select_server/select_server.js @@ -16,7 +16,7 @@ import { import Button from 'react-native-button'; import {RequestStatus} from 'mattermost-redux/constants'; -import Client from 'mattermost-redux/client'; +import {Client4} from 'mattermost-redux/client'; import ErrorText from 'app/components/error_text'; import FormattedText from 'app/components/formatted_text'; @@ -83,7 +83,7 @@ export default class SelectServer extends PureComponent { Keyboard.dismiss(); if (isValidUrl(url)) { - Client.setUrl(stripTrailingSlashes(url)); + Client4.setUrl(stripTrailingSlashes(url)); await this.props.actions.getPing(); } else { error = { diff --git a/app/scenes/thread/index.js b/app/scenes/thread/index.js index aa3dde39d..46a923788 100644 --- a/app/scenes/thread/index.js +++ b/app/scenes/thread/index.js @@ -8,8 +8,7 @@ import {selectPost} from 'mattermost-redux/actions/posts'; import {makeGetPostsForThread} from 'mattermost-redux/selectors/entities/posts'; import {getTheme} from 'app/selectors/preferences'; -import {getCurrentChannelMembership} from 'mattermost-redux/selectors/entities/channels'; -import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams'; +import {getMyCurrentChannelMembership} from 'mattermost-redux/selectors/entities/channels'; import navigationSceneConnect from '../navigationSceneConnect'; import Thread from './thread'; @@ -21,20 +20,12 @@ function makeMapStateToProps() { return function mapStateToProps(state, ownProps) { const posts = getPostsForThread(state, ownProps); - - let teamId = state.entities.channels.channels[ownProps.channelId].team_id; - if (!teamId) { - // We can't make a post without a team id, so get it from the current team - teamId = getCurrentTeamId(state); - } - const threadDraft = state.views.thread.drafts[ownProps.rootId]; return { ...ownProps, - teamId, channelId: ownProps.channelId, - myMember: getCurrentChannelMembership(state), + myMember: getMyCurrentChannelMembership(state), rootId: ownProps.rootId, draft: threadDraft.draft, files: threadDraft.files, diff --git a/app/scenes/thread/thread.js b/app/scenes/thread/thread.js index cfda4e600..62499bbf2 100644 --- a/app/scenes/thread/thread.js +++ b/app/scenes/thread/thread.js @@ -16,7 +16,6 @@ export default class Thread extends PureComponent { handleCommentDraftChanged: PropTypes.func.isRequired, selectPost: PropTypes.func.isRequired }).isRequired, - teamId: PropTypes.string.isRequired, channelId: PropTypes.string.isRequired, myMember: PropTypes.object.isRequired, files: PropTypes.array, @@ -68,7 +67,6 @@ export default class Thread extends PureComponent { rootId={this.props.rootId} value={this.props.draft} files={this.props.files} - teamId={this.props.teamId} channelId={this.props.channelId} onChangeText={this.handleDraftChanged} /> diff --git a/app/scenes/thread/thread_title.js b/app/scenes/thread/thread_title.js index 4b37cd434..380fc3bdc 100644 --- a/app/scenes/thread/thread_title.js +++ b/app/scenes/thread/thread_title.js @@ -7,7 +7,7 @@ import { View } from 'react-native'; -import {Constants} from 'mattermost-redux/constants'; +import {General} from 'mattermost-redux/constants'; import {getCurrentChannel} from 'mattermost-redux/selectors/entities/channels'; import {getTheme} from 'app/selectors/preferences'; @@ -16,7 +16,7 @@ import FormattedText from 'app/components/formatted_text'; function ThreadTitle(props) { const {currentChannel, theme} = props; let label; - if (currentChannel.type === Constants.DM_CHANNEL) { + if (currentChannel.type === General.DM_CHANNEL) { label = ( { const persistor = persistStore(store, {storage: AsyncStorage, ...options}, () => { store.dispatch({ - type: Constants.STORE_REHYDRATION_COMPLETE, + type: General.STORE_REHYDRATION_COMPLETE, complete: true }); }); @@ -90,7 +96,7 @@ export default function configureAppStore(initialState) { store.dispatch(batchActions([ { - type: Constants.OFFLINE_STORE_RESET, + type: General.OFFLINE_STORE_RESET, data: initialState }, { diff --git a/app/utils/url.js b/app/utils/url.js index 91fe9d5e9..b5c0b96df 100644 --- a/app/utils/url.js +++ b/app/utils/url.js @@ -7,5 +7,5 @@ export function isValidUrl(url) { } export function stripTrailingSlashes(url) { - return url.replace(/\/+$/, ''); + return url.replace(/\/+$/, '').trim(); } diff --git a/assets/base/config.json b/assets/base/config.json index ed2971609..bdebd5764 100644 --- a/assets/base/config.json +++ b/assets/base/config.json @@ -5,5 +5,5 @@ "DefaultTheme": "default", "ShowErrorsList": false, "GooglePlaySenderId": "184930218130", - "MinServerVersion": "3.7.0" + "MinServerVersion": "3.8.0" } diff --git a/assets/base/themes.json b/assets/base/themes.json index 02ce1d93a..5c6ecaf93 100644 --- a/assets/base/themes.json +++ b/assets/base/themes.json @@ -18,6 +18,7 @@ "linkColor": "#2f81b7", "buttonBg": "#1dacfc", "buttonColor": "#FFFFFF", + "errorTextColor": "#a94442", "mentionHighlightBg": "#fff2bb", "mentionHighlightLink": "#2f81b7", "codeTheme": "github" @@ -41,6 +42,7 @@ "linkColor": "#2389d7", "buttonBg": "#23A2FF", "buttonColor": "#FFFFFF", + "errorTextColor": "#a94442", "mentionHighlightBg": "#fff2bb", "mentionHighlightLink": "#2f81b7", "codeTheme": "github" @@ -64,6 +66,7 @@ "linkColor": "#A4FFEB", "buttonBg": "#4CBBA4", "buttonColor": "#FFFFFF", + "errorTextColor": "#ff6461", "mentionHighlightBg": "#984063", "mentionHighlightLink": "#A4FFEB", "codeTheme": "solarized-dark" @@ -87,6 +90,7 @@ "linkColor": "#0D93FF", "buttonBg": "#0177e7", "buttonColor": "#FFFFFF", + "errorTextColor": "#ff6461", "mentionHighlightBg": "#784098", "mentionHighlightLink": "#A4FFEB", "codeTheme": "monokai" diff --git a/package.json b/package.json index 693519606..2ca79999b 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "deep-equal": "1.0.1", "harmony-reflect": "1.5.1", "intl": "1.2.5", - "mattermost-redux": "mattermost/mattermost-redux#release-3.7", + "mattermost-redux": "mattermost/mattermost-redux#master", "react": "15.4.2", "react-intl": "2.2.3", "react-native": "0.42.3", @@ -42,6 +42,7 @@ "babel-eslint": "7.2.1", "babel-plugin-module-resolver": "2.7.0", "babel-preset-es2015": "6.24.0", + "babel-preset-stage-0": "6.22.0", "babel-preset-react-native": "1.9.1", "babel-register": "6.24.0", "chai": "3.5.0",