Move to mattermost-redux master branch (#484)

This commit is contained in:
enahum 2017-04-21 14:20:52 -03:00 committed by GitHub
parent 311cc4028d
commit 7a3c99c267
66 changed files with 880 additions and 806 deletions

View file

@ -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

View file

@ -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;

View file

@ -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);
};
}

View file

@ -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);
}

View file

@ -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) {

View file

@ -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);
};
}

View file

@ -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()
}
});
};

View file

@ -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);

View file

@ -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);
};

View file

@ -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 (
<View style={style.sectionWrapper}>
<View style={style.section}>
<FormattedText
id={localization[sectionId].id}
defaultMessage={localization[sectionId].defaultMessage}
style={style.sectionText}
/>
</View>
</View>
);
};
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 (
<TouchableOpacity
onPress={() => this.completeMention(data.username)}
style={style.row}
>
<View style={style.rowPicture}>
<ProfilePicture
user={data}
theme={this.props.theme}
size={20}
status={null}
/>
</View>
<Text style={style.rowUsername}>{`@${data.username}`}</Text>
{hasFullName && <Text style={style.rowUsername}>{' - '}</Text>}
{hasFullName && <Text style={style.rowFullname}>{`${data.first_name} ${data.last_name}`}</Text>}
</TouchableOpacity>
);
};
renderSpecialMentions = (data) => {
const style = getStyleFromTheme(this.props.theme);
return (
<TouchableOpacity
onPress={() => this.completeMention(data.completeHandle)}
style={style.row}
>
<View style={style.rowPicture}>
<Icon
name='users'
style={style.rowIcon}
/>
</View>
<Text style={style.textWrapper}>
<Text style={style.rowUsername}>{`@${data.completeHandle}`}</Text>
<Text style={style.rowUsername}>{' - '}</Text>
<FormattedText
id={data.id}
defaultMessage={data.defaultMessage}
values={data.values}
style={[style.rowFullname, {flex: 1}]}
/>
</Text>
</TouchableOpacity>
);
};
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 (
<View style={style.loading}>
<FormattedText
id='analytics.chart.loading": "Loading...'
defaultMessage='Loading...'
style={style.sectionText}
/>
</View>
);
}
return (
<ListView
keyboardShouldPersistTaps='always'
style={style.listView}
enableEmptySections={true}
dataSource={this.state.dataSource}
renderSectionHeader={this.renderSectionHeader}
renderRow={this.renderRow}
renderFooter={this.renderFooter}
pageSize={10}
initialListSize={10}
/>
);
}
}
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 (
<View style={style.sectionWrapper}>
<View style={style.section}>
<FormattedText
id={localization[sectionId].id}
defaultMessage={localization[sectionId].defaultMessage}
style={style.sectionText}
/>
</View>
</View>
);
}
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 (
<TouchableOpacity
onPress={() => this.completeMention(data.username)}
style={style.row}
>
<View style={style.rowPicture}>
<ProfilePicture
user={data}
theme={this.props.theme}
size={20}
status={null}
/>
</View>
<Text style={style.rowUsername}>{`@${data.username}`}</Text>
{hasFullName && <Text style={style.rowUsername}>{' - '}</Text>}
{hasFullName && <Text style={style.rowFullname}>{`${data.first_name} ${data.last_name}`}</Text>}
</TouchableOpacity>
);
}
renderSpecialMentions = (data) => {
const style = getStyleFromTheme(this.props.theme);
return (
<TouchableOpacity
onPress={() => this.completeMention(data.completeHandle)}
style={style.row}
>
<View style={style.rowPicture}>
<Icon
name='users'
style={style.rowIcon}
/>
</View>
<Text style={style.textWrapper}>
<Text style={style.rowUsername}>{`@${data.completeHandle}`}</Text>
<Text style={style.rowUsername}>{' - '}</Text>
<FormattedText
id={data.id}
defaultMessage={data.defaultMessage}
values={data.values}
style={[style.rowFullname, {flex: 1}]}
/>
</Text>
</TouchableOpacity>
);
}
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 (
<View style={style.loading}>
<FormattedText
id='analytics.chart.loading": "Loading...'
defaultMessage='Loading...'
style={style.sectionText}
/>
</View>
);
}
return (
<ListView
keyboardShouldPersistTaps='always'
style={style.listView}
enableEmptySections={true}
dataSource={this.state.dataSource}
renderSectionHeader={this.renderSectionHeader}
renderRow={this.renderRow}
renderFooter={this.renderFooter}
pageSize={10}
initialListSize={10}
/>
);
}
}

View file

@ -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)
};
}

View file

@ -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 (
<View style={style.sectionWrapper}>
<View style={style.section}>
<FormattedText
id={localization[sectionId].id}
defaultMessage={localization[sectionId].defaultMessage}
style={style.sectionText}
/>
</View>
</View>
);
};
renderRow = (data) => {
const style = getStyleFromTheme(this.props.theme);
return (
<TouchableOpacity
onPress={() => this.completeMention(data.name)}
style={style.row}
>
<Text style={style.rowDisplayName}>{data.display_name}</Text>
<Text style={style.rowName}>{` (~${data.name})`}</Text>
</TouchableOpacity>
);
};
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 (
<View style={style.loading}>
<FormattedText
id='analytics.chart.loading": "Loading...'
defaultMessage='Loading...'
style={style.sectionText}
/>
</View>
);
}
return (
<ListView
keyboardShouldPersistTaps='always'
style={style.listView}
enableEmptySections={true}
dataSource={this.state.dataSource}
renderSectionHeader={this.renderSectionHeader}
renderRow={this.renderRow}
pageSize={10}
initialListSize={10}
/>
);
}
}
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 (
<View style={style.sectionWrapper}>
<View style={style.section}>
<FormattedText
id={localization[sectionId].id}
defaultMessage={localization[sectionId].defaultMessage}
style={style.sectionText}
/>
</View>
</View>
);
}
renderRow = (data) => {
const style = getStyleFromTheme(this.props.theme);
return (
<TouchableOpacity
onPress={() => this.completeMention(data.name)}
style={style.row}
>
<Text style={style.rowDisplayName}>{data.display_name}</Text>
<Text style={style.rowName}>{` (~${data.name})`}</Text>
</TouchableOpacity>
);
}
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 (
<View style={style.loading}>
<FormattedText
id='analytics.chart.loading": "Loading...'
defaultMessage='Loading...'
style={style.sectionText}
/>
</View>
);
}
return (
<ListView
keyboardShouldPersistTaps='always'
style={style.listView}
enableEmptySections={true}
dataSource={this.state.dataSource}
renderSectionHeader={this.renderSectionHeader}
renderRow={this.renderRow}
pageSize={10}
initialListSize={10}
/>
);
}
}

View file

@ -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)
};
}

View file

@ -35,7 +35,7 @@ export default class Autocomplete extends Component {
this.setState({
cursorPosition: event.nativeEvent.selection.end
});
}
};
render() {
return (

View file

@ -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) => {

View file

@ -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
};
}

View file

@ -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 (
<Icon
name='globe'
style={[style.icon, unreadIcon, activeIcon, {fontSize: size}]}
/>
);
} else if (type === Constants.PRIVATE_CHANNEL) {
} else if (type === General.PRIVATE_CHANNEL) {
return (
<Icon
name='lock'
style={[style.icon, unreadIcon, activeIcon, {fontSize: size}]}
/>
);
} else if (type === Constants.GM_CHANNEL) {
} else if (type === General.GM_CHANNEL) {
return (
<View style={style.groupContainer}>
<View style={[style.groupBox, unreadGroupBox, activeGroupBox, {width: size, height: size}]}>
@ -70,7 +70,7 @@ function channelIcon(props) {
);
}
switch (status) {
case Constants.ONLINE:
case General.ONLINE:
return (
<View style={style.statusIcon}>
<OnlineStatus
@ -80,7 +80,7 @@ function channelIcon(props) {
/>
</View>
);
case Constants.AWAY:
case General.AWAY:
return (
<View style={style.statusIcon}>
<AwayStatus

View file

@ -1,42 +1,67 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {connect} from 'react-redux';
import React, {PropTypes, PureComponent} from 'react';
import {Text} from 'react-native';
import {StyleSheet, Text} from 'react-native';
import FormattedText from 'app/components/formatted_text';
import {getTheme} from 'app/selectors/preferences';
import {GlobalStyles} from 'app/styles';
import {makeStyleSheetFromTheme} from 'app/utils/theme';
export default class ErrorText extends PureComponent {
class ErrorText extends PureComponent {
static propTypes = {
error: PropTypes.oneOfType([PropTypes.string, PropTypes.object])
error: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
theme: PropTypes.object
};
static defaultProps = {
error: {}
error: {},
theme: {}
};
render() {
if (!this.props.error) {
const {error, theme} = this.props;
if (!error) {
return null;
}
if (this.props.error.hasOwnProperty('intl')) {
const {intl} = this.props.error;
const style = getStyleSheet(theme);
if (error.hasOwnProperty('intl')) {
const {intl} = error;
return (
<FormattedText
id={intl.id}
defaultMessage={intl.defaultMessage}
values={intl.values}
style={GlobalStyles.errorLabel}
style={[GlobalStyles.errorLabel, style.errorLabel]}
/>
);
}
return (
<Text style={GlobalStyles.errorLabel}>
{this.props.error.message || this.props.error}
<Text style={[GlobalStyles.errorLabel, style.errorLabel]}>
{error.message || error}
</Text>
);
}
}
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);

View file

@ -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];

View file

@ -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)
};

View file

@ -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 = (
<FormattedText
style={messageStyle}

View file

@ -12,23 +12,6 @@ import {makeStyleSheetFromTheme} from 'app/utils/theme';
import FormattedText from 'app/components/formatted_text';
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
}
});
});
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
}
});
});

View file

@ -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 (
<NewMessagesDivider
theme={this.props.theme}

View file

@ -44,7 +44,6 @@ class PostTextbox extends PureComponent {
intl: intlShape.isRequired,
onChangeText: PropTypes.func.isRequired,
rootId: PropTypes.string,
teamId: PropTypes.string.isRequired,
theme: PropTypes.object.isRequired,
typing: PropTypes.array.isRequired,
uploadFileRequestStatus: PropTypes.string.isRequired,
@ -147,7 +146,7 @@ class PostTextbox extends PureComponent {
message: this.props.value
};
this.props.actions.createPost(this.props.teamId, post, this.props.files);
this.props.actions.createPost(post, this.props.files);
this.handleTextChange('');
};
@ -185,7 +184,7 @@ class PostTextbox extends PureComponent {
// If user cancels it's considered
// an error and we have to catch it.
}
}
};
attachFileFromLibrary = async () => {
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;

View file

@ -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 {
<View style={{width: this.props.size + STATUS_BUFFER, height: this.props.size + STATUS_BUFFER}}>
<Image
style={{width: this.props.size, height: this.props.size, borderRadius: this.props.size / 2}}
source={{uri: pictureUrl}}
source={{uri: pictureUrl, headers: IMAGE_HEADERS}}
defaultSource={placeholder}
/>
{this.props.status &&

View file

@ -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;
};

View file

@ -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();

View file

@ -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
},

View file

@ -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) {

View file

@ -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:

View file

@ -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;

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;
}

View file

@ -39,7 +39,7 @@ export default class AccountNotifications extends PureComponent {
renderRightComponent: (props, emitter) => {
return <SaveNotificationsButton emitter={emitter}/>;
}
}
};
constructor(props) {
super(props);

View file

@ -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}
/>

View file

@ -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;
};

View file

@ -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),

View file

@ -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();
}

View file

@ -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);

View file

@ -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;

View file

@ -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 (
<TouchableHighlight onPress={action}>
@ -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;

View file

@ -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)

View file

@ -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}
/>
</View>
);

View file

@ -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,

View file

@ -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'});
}

View file

@ -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,

View file

@ -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)
};
}

View file

@ -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)
};

View file

@ -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() {

View file

@ -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
});

View file

@ -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 {

View file

@ -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);

View file

@ -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 = {

View file

@ -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,

View file

@ -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}
/>

View file

@ -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 = (
<FormattedText
id='mobile.routes.thread_dm'

View file

@ -4,7 +4,7 @@
import {batchActions} from 'redux-batched-actions';
import {AsyncStorage} from 'react-native';
import configureStore from 'mattermost-redux/store';
import {Constants, RequestStatus} from 'mattermost-redux/constants';
import {General, RequestStatus} from 'mattermost-redux/constants';
import {createBlacklistFilter} from 'redux-persist-transform-filter';
import {createTransform, persistStore} from 'redux-persist';
@ -20,16 +20,22 @@ function getAppReducer() {
const usersSetTransform = [
'profilesInChannel',
'profilesNotInChannel',
'profilesInTeam'
'profilesInTeam',
'profilesNotInTeam'
];
const teamSetTransform = [
'membersInTeam'
];
const channelSetTransform = [
'channelsInTeam'
];
const setTransforms = [
...usersSetTransform,
...teamSetTransform
...teamSetTransform,
...channelSetTransform
];
export default function configureAppStore(initialState) {
@ -73,7 +79,7 @@ export default function configureAppStore(initialState) {
persist: (store, options) => {
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
},
{

View file

@ -7,5 +7,5 @@ export function isValidUrl(url) {
}
export function stripTrailingSlashes(url) {
return url.replace(/\/+$/, '');
return url.replace(/\/+$/, '').trim();
}

View file

@ -5,5 +5,5 @@
"DefaultTheme": "default",
"ShowErrorsList": false,
"GooglePlaySenderId": "184930218130",
"MinServerVersion": "3.7.0"
"MinServerVersion": "3.8.0"
}

View file

@ -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"

View file

@ -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",