[MM-9374] Open Permalinks in app (#1449)
* [MM-9374] Open Permalinks in app * Feedback review * Upgrade mattermost-redux * Fix eslint * Navigation fixes * Fixing style of merged lines
This commit is contained in:
parent
68fe35c74d
commit
d0d9103857
28 changed files with 887 additions and 546 deletions
|
|
@ -19,6 +19,7 @@ import {getTeamMembersByIds} from 'mattermost-redux/actions/teams';
|
|||
import {getProfilesInChannel} from 'mattermost-redux/actions/users';
|
||||
import {General, Preferences} from 'mattermost-redux/constants';
|
||||
import {getCurrentChannelId} from 'mattermost-redux/selectors/entities/channels';
|
||||
import {getTeamByName} from 'mattermost-redux/selectors/entities/teams';
|
||||
|
||||
import {
|
||||
getChannelByName,
|
||||
|
|
@ -40,6 +41,18 @@ export function loadChannelsIfNecessary(teamId) {
|
|||
};
|
||||
}
|
||||
|
||||
export function loadChannelsByTeamName(teamName) {
|
||||
return async (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const {currentTeamId} = state.entities.teams;
|
||||
const team = getTeamByName(state, teamName);
|
||||
|
||||
if (team && team.id !== currentTeamId) {
|
||||
await dispatch(fetchMyChannelsAndMembers(team.id));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function loadProfilesAndTeamMembersForDMSidebar(teamId) {
|
||||
return async (dispatch, getState) => {
|
||||
const state = getState();
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Clipboard, Text} from 'react-native';
|
||||
import {Clipboard, Platform, Text} from 'react-native';
|
||||
import {intlShape} from 'react-intl';
|
||||
|
||||
import CustomPropTypes from 'app/constants/custom_prop_types';
|
||||
|
|
@ -49,8 +49,7 @@ export default class AtMention extends React.PureComponent {
|
|||
goToUserProfile = () => {
|
||||
const {navigator, theme} = this.props;
|
||||
const {intl} = this.context;
|
||||
|
||||
navigator.push({
|
||||
const options = {
|
||||
screen: 'UserProfile',
|
||||
title: intl.formatMessage({id: 'mobile.routes.user_profile', defaultMessage: 'Profile'}),
|
||||
animated: true,
|
||||
|
|
@ -64,7 +63,13 @@ export default class AtMention extends React.PureComponent {
|
|||
navBarButtonColor: theme.sidebarHeaderTextColor,
|
||||
screenBackgroundColor: theme.centerChannelBg,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
if (Platform.OS === 'ios') {
|
||||
navigator.push(options);
|
||||
} else {
|
||||
navigator.showModal(options);
|
||||
}
|
||||
};
|
||||
|
||||
getUserDetailsFromMentionName(props) {
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
Platform,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
|
|
@ -30,8 +31,7 @@ class ChannelIntro extends PureComponent {
|
|||
|
||||
goToUserProfile = (userId) => {
|
||||
const {intl, navigator, theme} = this.props;
|
||||
|
||||
navigator.push({
|
||||
const options = {
|
||||
screen: 'UserProfile',
|
||||
title: intl.formatMessage({id: 'mobile.routes.user_profile', defaultMessage: 'Profile'}),
|
||||
animated: true,
|
||||
|
|
@ -45,7 +45,13 @@ class ChannelIntro extends PureComponent {
|
|||
navBarButtonColor: theme.sidebarHeaderTextColor,
|
||||
screenBackgroundColor: theme.centerChannelBg,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
if (Platform.OS === 'ios') {
|
||||
navigator.push(options);
|
||||
} else {
|
||||
navigator.showModal(options);
|
||||
}
|
||||
};
|
||||
|
||||
getDisplayName = (member) => {
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ export default class Markdown extends PureComponent {
|
|||
isSearchResult: PropTypes.bool,
|
||||
navigator: PropTypes.object.isRequired,
|
||||
onLongPress: PropTypes.func,
|
||||
onPermalinkPress: PropTypes.func,
|
||||
onPostPress: PropTypes.func,
|
||||
textStyles: PropTypes.object,
|
||||
theme: PropTypes.object.isRequired,
|
||||
|
|
@ -320,6 +321,7 @@ export default class Markdown extends PureComponent {
|
|||
<MarkdownLink
|
||||
href={href}
|
||||
onLongPress={this.props.onLongPress}
|
||||
onPermalinkPress={this.props.onPermalinkPress}
|
||||
>
|
||||
{children}
|
||||
</MarkdownLink>
|
||||
|
|
|
|||
17
app/components/markdown/markdown_link/index.js
Normal file
17
app/components/markdown/markdown_link/index.js
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {getConfig, getCurrentUrl} from 'mattermost-redux/selectors/entities/general';
|
||||
|
||||
import MarkdownLink from './markdown_link';
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
serverURL: getCurrentUrl(state),
|
||||
siteURL: getConfig(state).SiteURL,
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(MarkdownLink);
|
||||
|
|
@ -12,6 +12,8 @@ import mattermostManaged from 'app/mattermost_managed';
|
|||
|
||||
import Config from 'assets/config';
|
||||
|
||||
import {escapeRegex} from 'app/utils/markdown';
|
||||
import {preventDoubleTap} from 'app/utils/tap';
|
||||
import {normalizeProtocol} from 'app/utils/url';
|
||||
|
||||
export default class MarkdownLink extends PureComponent {
|
||||
|
|
@ -19,29 +21,45 @@ export default class MarkdownLink extends PureComponent {
|
|||
children: CustomPropTypes.Children.isRequired,
|
||||
href: PropTypes.string.isRequired,
|
||||
onLongPress: PropTypes.func,
|
||||
onPermalinkPress: PropTypes.func,
|
||||
serverURL: PropTypes.string.isRequired,
|
||||
siteURL: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
onLongPress: () => true,
|
||||
onPermalinkPress: () => true,
|
||||
};
|
||||
|
||||
static contextTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
handlePress = () => {
|
||||
const url = normalizeProtocol(this.props.href);
|
||||
handlePress = preventDoubleTap(() => {
|
||||
const {href, onPermalinkPress, serverURL, siteURL} = this.props;
|
||||
const url = normalizeProtocol(href);
|
||||
|
||||
if (!url) {
|
||||
return;
|
||||
}
|
||||
|
||||
Linking.canOpenURL(url).then((supported) => {
|
||||
if (supported) {
|
||||
Linking.openURL(url);
|
||||
}
|
||||
});
|
||||
};
|
||||
const pattern = new RegExp('^' + escapeRegex(serverURL) + '\\/([^\\/]+)\\/pl\\/(\\w+)');
|
||||
const sitePattern = new RegExp('^' + escapeRegex(siteURL) + '\\/([^\\/]+)\\/pl\\/(\\w+)');
|
||||
const match = url.match(pattern) || url.match(sitePattern);
|
||||
|
||||
if (!match) {
|
||||
Linking.canOpenURL(url).then((supported) => {
|
||||
if (supported) {
|
||||
Linking.openURL(url);
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const teamName = match[1];
|
||||
const postId = match[2];
|
||||
onPermalinkPress(postId, teamName);
|
||||
});
|
||||
|
||||
parseLinkLiteral = (literal) => {
|
||||
let nextLiteral = literal;
|
||||
|
|
@ -54,7 +72,7 @@ export default class MarkdownLink extends PureComponent {
|
|||
const parsed = urlParse(nextLiteral, {});
|
||||
|
||||
return parsed.href;
|
||||
}
|
||||
};
|
||||
|
||||
parseChildren = () => {
|
||||
return Children.map(this.props.children, (child) => {
|
||||
|
|
@ -6,6 +6,7 @@ import PropTypes from 'prop-types';
|
|||
import {
|
||||
Alert,
|
||||
Clipboard,
|
||||
Platform,
|
||||
View,
|
||||
ViewPropTypes,
|
||||
} from 'react-native';
|
||||
|
|
@ -55,6 +56,7 @@ class Post extends PureComponent {
|
|||
license: PropTypes.object.isRequired,
|
||||
managedConfig: PropTypes.object.isRequired,
|
||||
navigator: PropTypes.object,
|
||||
onPermalinkPress: PropTypes.func,
|
||||
roles: PropTypes.string,
|
||||
shouldRenderReplyButton: PropTypes.bool,
|
||||
showFullDate: PropTypes.bool,
|
||||
|
|
@ -107,7 +109,7 @@ class Post extends PureComponent {
|
|||
|
||||
goToUserProfile = () => {
|
||||
const {intl, navigator, post, theme} = this.props;
|
||||
navigator.push({
|
||||
const options = {
|
||||
screen: 'UserProfile',
|
||||
title: intl.formatMessage({id: 'mobile.routes.user_profile', defaultMessage: 'Profile'}),
|
||||
animated: true,
|
||||
|
|
@ -121,7 +123,13 @@ class Post extends PureComponent {
|
|||
navBarButtonColor: theme.sidebarHeaderTextColor,
|
||||
screenBackgroundColor: theme.centerChannelBg,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
if (Platform.OS === 'ios') {
|
||||
navigator.push(options);
|
||||
} else {
|
||||
navigator.showModal(options);
|
||||
}
|
||||
};
|
||||
|
||||
autofillUserMention = (username) => {
|
||||
|
|
@ -260,7 +268,6 @@ class Post extends PureComponent {
|
|||
|
||||
handlePress = preventDoubleTap(() => {
|
||||
const {
|
||||
isSearchResult,
|
||||
onPress,
|
||||
post,
|
||||
} = this.props;
|
||||
|
|
@ -268,7 +275,7 @@ class Post extends PureComponent {
|
|||
if (!getToolTipVisible()) {
|
||||
if (onPress && post.state !== Posts.POST_DELETED && !isSystemMessage(post) && !isPostPendingOrFailed(post)) {
|
||||
onPress(post);
|
||||
} else if (!isSearchResult && isPostEphemeral(post)) {
|
||||
} else if (isPostEphemeral(post) || post.state === Posts.POST_DELETED) {
|
||||
this.onRemovePost(post);
|
||||
}
|
||||
}
|
||||
|
|
@ -320,9 +327,7 @@ class Post extends PureComponent {
|
|||
};
|
||||
|
||||
viewUserProfile = preventDoubleTap(() => {
|
||||
const {isSearchResult} = this.props;
|
||||
|
||||
if (!isSearchResult && !getToolTipVisible()) {
|
||||
if (!getToolTipVisible()) {
|
||||
this.goToUserProfile();
|
||||
}
|
||||
});
|
||||
|
|
@ -355,6 +360,7 @@ class Post extends PureComponent {
|
|||
highlight,
|
||||
isLastReply,
|
||||
isSearchResult,
|
||||
onPermalinkPress,
|
||||
post,
|
||||
renderReplies,
|
||||
shouldRenderReplyButton,
|
||||
|
|
@ -412,6 +418,7 @@ class Post extends PureComponent {
|
|||
onCopyPermalink={this.handleCopyPermalink}
|
||||
onCopyText={this.handleCopyText}
|
||||
onFailedPostPress={this.handleFailedPostPress}
|
||||
onPermalinkPress={onPermalinkPress}
|
||||
onPostDelete={this.handlePostDelete}
|
||||
onPostEdit={this.handlePostEdit}
|
||||
onPress={this.handlePress}
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@ class PostBody extends PureComponent {
|
|||
onCopyPermalink: PropTypes.func,
|
||||
onCopyText: PropTypes.func,
|
||||
onFailedPostPress: PropTypes.func,
|
||||
onPermalinkPress: PropTypes.func,
|
||||
onPostDelete: PropTypes.func,
|
||||
onPostEdit: PropTypes.func,
|
||||
onPress: PropTypes.func,
|
||||
|
|
@ -151,6 +152,7 @@ class PostBody extends PureComponent {
|
|||
message,
|
||||
navigator,
|
||||
onFailedPostPress,
|
||||
onPermalinkPress,
|
||||
onPostDelete,
|
||||
onPostEdit,
|
||||
onPress,
|
||||
|
|
@ -169,7 +171,7 @@ class PostBody extends PureComponent {
|
|||
const isPendingOrFailedPost = isPending || isFailed;
|
||||
|
||||
// we should check for the user roles and permissions
|
||||
if (!isPendingOrFailedPost && !isSearchResult && !isSystemMessage && !isPostEphemeral) {
|
||||
if (!isPendingOrFailedPost && !isSystemMessage && !isPostEphemeral) {
|
||||
actions.push({
|
||||
text: formatMessage({id: 'mobile.post_info.add_reaction', defaultMessage: 'Add Reaction'}),
|
||||
onPress: this.props.onAddReaction,
|
||||
|
|
@ -240,6 +242,7 @@ class PostBody extends PureComponent {
|
|||
isSearchResult={isSearchResult}
|
||||
navigator={navigator}
|
||||
onLongPress={this.showOptionsContext}
|
||||
onPermalinkPress={onPermalinkPress}
|
||||
onPostPress={onPress}
|
||||
textStyles={textStyles}
|
||||
value={message}
|
||||
|
|
@ -250,61 +253,36 @@ class PostBody extends PureComponent {
|
|||
}
|
||||
|
||||
if (!hasBeenDeleted) {
|
||||
if (isSearchResult) {
|
||||
body = (
|
||||
<TouchableHighlight
|
||||
onHideUnderlay={this.handleHideUnderlay}
|
||||
onPress={onPress}
|
||||
onShowUnderlay={this.handleShowUnderlay}
|
||||
underlayColor='transparent'
|
||||
>
|
||||
<View>
|
||||
{messageComponent}
|
||||
<PostBodyAdditionalContent
|
||||
baseTextStyle={messageStyle}
|
||||
blockStyles={blockStyles}
|
||||
navigator={navigator}
|
||||
message={message}
|
||||
postId={postId}
|
||||
postProps={postProps}
|
||||
textStyles={textStyles}
|
||||
isReplyPost={isReplyPost}
|
||||
/>
|
||||
{this.renderFileAttachments()}
|
||||
</View>
|
||||
</TouchableHighlight>
|
||||
);
|
||||
} else {
|
||||
body = (
|
||||
<OptionsContext
|
||||
actions={actions}
|
||||
ref='options'
|
||||
onPress={onPress}
|
||||
toggleSelected={toggleSelected}
|
||||
cancelText={formatMessage({id: 'channel_modal.cancel', defaultMessage: 'Cancel'})}
|
||||
>
|
||||
{messageComponent}
|
||||
<PostBodyAdditionalContent
|
||||
baseTextStyle={messageStyle}
|
||||
blockStyles={blockStyles}
|
||||
navigator={navigator}
|
||||
message={message}
|
||||
postId={postId}
|
||||
postProps={postProps}
|
||||
textStyles={textStyles}
|
||||
onLongPress={this.showOptionsContext}
|
||||
isReplyPost={isReplyPost}
|
||||
/>
|
||||
{this.renderFileAttachments()}
|
||||
{hasReactions &&
|
||||
<Reactions
|
||||
postId={postId}
|
||||
onAddReaction={this.props.onAddReaction}
|
||||
/>
|
||||
}
|
||||
</OptionsContext>
|
||||
);
|
||||
}
|
||||
body = (
|
||||
<OptionsContext
|
||||
actions={actions}
|
||||
ref='options'
|
||||
onPress={onPress}
|
||||
toggleSelected={toggleSelected}
|
||||
cancelText={formatMessage({id: 'channel_modal.cancel', defaultMessage: 'Cancel'})}
|
||||
>
|
||||
{messageComponent}
|
||||
<PostBodyAdditionalContent
|
||||
baseTextStyle={messageStyle}
|
||||
blockStyles={blockStyles}
|
||||
navigator={navigator}
|
||||
message={message}
|
||||
postId={postId}
|
||||
postProps={postProps}
|
||||
textStyles={textStyles}
|
||||
onLongPress={this.showOptionsContext}
|
||||
isReplyPost={isReplyPost}
|
||||
onPermalinkPress={onPermalinkPress}
|
||||
/>
|
||||
{this.renderFileAttachments()}
|
||||
{!isSearchResult && hasReactions &&
|
||||
<Reactions
|
||||
postId={postId}
|
||||
onAddReaction={this.props.onAddReaction}
|
||||
/>
|
||||
}
|
||||
</OptionsContext>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ export default class PostBodyAdditionalContent extends PureComponent {
|
|||
message: PropTypes.string.isRequired,
|
||||
navigator: PropTypes.object.isRequired,
|
||||
onLongPress: PropTypes.func,
|
||||
onPermalinkPress: PropTypes.func,
|
||||
openGraphData: PropTypes.object,
|
||||
postId: PropTypes.string.isRequired,
|
||||
postProps: PropTypes.object.isRequired,
|
||||
|
|
@ -160,6 +161,7 @@ export default class PostBodyAdditionalContent extends PureComponent {
|
|||
baseTextStyle,
|
||||
blockStyles,
|
||||
navigator,
|
||||
onPermalinkPress,
|
||||
textStyles,
|
||||
theme,
|
||||
} = this.props;
|
||||
|
|
@ -176,6 +178,7 @@ export default class PostBodyAdditionalContent extends PureComponent {
|
|||
textStyles={textStyles}
|
||||
theme={theme}
|
||||
onLongPress={this.props.onLongPress}
|
||||
onPermalinkPress={onPermalinkPress}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,11 +4,12 @@
|
|||
import {bindActionCreators} from 'redux';
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {refreshChannelWithRetry} from 'app/actions/views/channel';
|
||||
import {makePreparePostIdsForPostList, START_OF_NEW_MESSAGES} from 'app/selectors/post_list';
|
||||
|
||||
import {selectFocusedPostId} from 'mattermost-redux/actions/posts';
|
||||
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
|
||||
|
||||
import {loadChannelsByTeamName, refreshChannelWithRetry} from 'app/actions/views/channel';
|
||||
import {makePreparePostIdsForPostList, START_OF_NEW_MESSAGES} from 'app/selectors/post_list';
|
||||
|
||||
import PostList from './post_list';
|
||||
|
||||
function makeMapStateToProps() {
|
||||
|
|
@ -31,7 +32,9 @@ function makeMapStateToProps() {
|
|||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators({
|
||||
loadChannelsByTeamName,
|
||||
refreshChannelWithRetry,
|
||||
selectFocusedPostId,
|
||||
}, dispatch),
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,10 +4,10 @@
|
|||
import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
FlatList,
|
||||
InteractionManager,
|
||||
Platform,
|
||||
StyleSheet,
|
||||
FlatList,
|
||||
} from 'react-native';
|
||||
|
||||
import ChannelIntro from 'app/components/channel_intro';
|
||||
|
|
@ -15,6 +15,7 @@ import Post from 'app/components/post';
|
|||
import {DATE_LINE, START_OF_NEW_MESSAGES} from 'app/selectors/post_list';
|
||||
import mattermostManaged from 'app/mattermost_managed';
|
||||
import {makeExtraData} from 'app/utils/list_view';
|
||||
import {changeOpacity} from 'app/utils/theme';
|
||||
|
||||
import DateHeader from './date_header';
|
||||
import LoadMorePosts from './load_more_posts';
|
||||
|
|
@ -30,7 +31,9 @@ const DATE_HEADER_HEIGHT = 28;
|
|||
export default class PostList extends PureComponent {
|
||||
static propTypes = {
|
||||
actions: PropTypes.shape({
|
||||
loadChannelsByTeamName: PropTypes.func.isRequired,
|
||||
refreshChannelWithRetry: PropTypes.func.isRequired,
|
||||
selectFocusedPostId: PropTypes.func.isRequired,
|
||||
}).isRequired,
|
||||
channelId: PropTypes.string,
|
||||
currentUserId: PropTypes.string,
|
||||
|
|
@ -42,6 +45,7 @@ export default class PostList extends PureComponent {
|
|||
loadMore: PropTypes.func,
|
||||
measureCellLayout: PropTypes.bool,
|
||||
navigator: PropTypes.object,
|
||||
onPermalinkPress: PropTypes.func,
|
||||
onPostPress: PropTypes.func,
|
||||
onRefresh: PropTypes.func,
|
||||
postIds: PropTypes.array.isRequired,
|
||||
|
|
@ -94,13 +98,51 @@ export default class PostList extends PureComponent {
|
|||
mattermostManaged.removeEventListener(this.listenerId);
|
||||
}
|
||||
|
||||
handleClosePermalink = () => {
|
||||
const {actions} = this.props;
|
||||
actions.selectFocusedPostId('');
|
||||
this.showingPermalink = false;
|
||||
};
|
||||
|
||||
handlePermalinkPress = (postId, teamName) => {
|
||||
this.props.actions.loadChannelsByTeamName(teamName);
|
||||
this.showPermalinkView(postId);
|
||||
};
|
||||
|
||||
showPermalinkView = (postId) => {
|
||||
const {actions, navigator} = this.props;
|
||||
|
||||
actions.selectFocusedPostId(postId);
|
||||
|
||||
if (!this.showingPermalink) {
|
||||
const options = {
|
||||
screen: 'Permalink',
|
||||
animationType: 'none',
|
||||
backButtonTitle: '',
|
||||
navigatorStyle: {
|
||||
navBarHidden: true,
|
||||
screenBackgroundColor: changeOpacity('#000', 0.2),
|
||||
modalPresentationStyle: 'overCurrentContext',
|
||||
},
|
||||
passProps: {
|
||||
isPermalink: true,
|
||||
onClose: this.handleClosePermalink,
|
||||
onPermalinkPress: this.handlePermalinkPress,
|
||||
},
|
||||
};
|
||||
|
||||
this.showingPermalink = true;
|
||||
navigator.showModal(options);
|
||||
}
|
||||
};
|
||||
|
||||
scrollToBottomOffset = () => {
|
||||
InteractionManager.runAfterInteractions(() => {
|
||||
if (this.refs.list) {
|
||||
this.refs.list.scrollToOffset({offset: 0, animated: false});
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
getMeasurementOffset = (index) => {
|
||||
const orderedKeys = Object.keys(this.itemMeasurements).sort((a, b) => {
|
||||
|
|
@ -117,13 +159,12 @@ export default class PostList extends PureComponent {
|
|||
}).slice(0, index);
|
||||
|
||||
return orderedKeys.map((i) => this.itemMeasurements[i]).reduce((a, b) => a + b, 0);
|
||||
}
|
||||
};
|
||||
|
||||
scrollListToMessageOffset = () => {
|
||||
const index = this.moreNewMessages ? this.props.postIds.length - 1 : this.newMessagesIndex;
|
||||
|
||||
if (index !== -1) {
|
||||
let offset = this.getMeasurementOffset(index);
|
||||
let offset = this.getMeasurementOffset(index) - (3 * this.itemMeasurements[index]);
|
||||
const windowHeight = this.state.postListHeight;
|
||||
|
||||
if (offset < windowHeight) {
|
||||
|
|
@ -148,7 +189,7 @@ export default class PostList extends PureComponent {
|
|||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
setManagedConfig = async (config) => {
|
||||
let nextConfig = config;
|
||||
|
|
@ -191,7 +232,7 @@ export default class PostList extends PureComponent {
|
|||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
renderItem = ({item, index}) => {
|
||||
if (item === START_OF_NEW_MESSAGES) {
|
||||
|
|
@ -260,6 +301,7 @@ export default class PostList extends PureComponent {
|
|||
renderReplies={renderReplies}
|
||||
isSearchResult={isSearchResult}
|
||||
shouldRenderReplyButton={shouldRenderReplyButton}
|
||||
onPermalinkPress={this.handlePermalinkPress}
|
||||
onPress={onPostPress}
|
||||
navigator={navigator}
|
||||
managedConfig={managedConfig}
|
||||
|
|
@ -296,7 +338,7 @@ export default class PostList extends PureComponent {
|
|||
this.setState({
|
||||
postListHeight: height,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ function mapStateToProps(state, ownProps) {
|
|||
|
||||
const currentChannel = getCurrentChannel(state);
|
||||
let deactivatedChannel = false;
|
||||
if (currentChannel.type === General.DM_CHANNEL) {
|
||||
if (currentChannel && currentChannel.type === General.DM_CHANNEL) {
|
||||
const teammate = getChannelMembersForDm(state, currentChannel);
|
||||
if (teammate.length && teammate[0].delete_at) {
|
||||
deactivatedChannel = true;
|
||||
|
|
@ -36,7 +36,7 @@ function mapStateToProps(state, ownProps) {
|
|||
}
|
||||
|
||||
return {
|
||||
channelId: ownProps.channelId || currentChannel.id,
|
||||
channelId: ownProps.channelId || (currentChannel ? currentChannel.id : ''),
|
||||
canUploadFiles: canUploadFilesOnMobile(state),
|
||||
channelIsLoading: state.views.channel.loading,
|
||||
currentUserId: getCurrentUserId(state),
|
||||
|
|
|
|||
|
|
@ -1,40 +0,0 @@
|
|||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import {bindActionCreators} from 'redux';
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {makeGetPostIdsAroundPost} from 'mattermost-redux/selectors/entities/posts';
|
||||
import {getPostsAfter, getPostsBefore, getPostThread} from 'mattermost-redux/actions/posts';
|
||||
import {makeGetChannel} from 'mattermost-redux/selectors/entities/channels';
|
||||
import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
|
||||
|
||||
import SearchPreview from './search_preview';
|
||||
|
||||
function makeMapStateToProps() {
|
||||
const getPostIdsAroundPost = makeGetPostIdsAroundPost();
|
||||
const getChannel = makeGetChannel();
|
||||
|
||||
return function mapStateToProps(state, ownProps) {
|
||||
const channel = getChannel(state, {id: ownProps.channelId});
|
||||
const postIds = getPostIdsAroundPost(state, ownProps.focusedPostId, ownProps.channelId, {postsBeforeCount: 5, postsAfterCount: 5});
|
||||
|
||||
return {
|
||||
channelName: channel ? channel.display_name : '',
|
||||
currentUserId: getCurrentUserId(state),
|
||||
postIds,
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators({
|
||||
getPostsAfter,
|
||||
getPostsBefore,
|
||||
getPostThread,
|
||||
}, dispatch),
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(makeMapStateToProps, mapDispatchToProps, null, {withRef: true})(SearchPreview);
|
||||
|
|
@ -1,287 +0,0 @@
|
|||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
Platform,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
} from 'react-native';
|
||||
import * as Animatable from 'react-native-animatable';
|
||||
import MaterialIcon from 'react-native-vector-icons/MaterialIcons';
|
||||
|
||||
import FormattedText from 'app/components/formatted_text';
|
||||
import Loading from 'app/components/loading';
|
||||
import PostList from 'app/components/post_list';
|
||||
import PostListRetry from 'app/components/post_list_retry';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
|
||||
Animatable.initializeRegistryWithDefinitions({
|
||||
growOut: {
|
||||
from: {
|
||||
opacity: 1,
|
||||
scale: 1,
|
||||
},
|
||||
0.5: {
|
||||
opacity: 1,
|
||||
scale: 3,
|
||||
},
|
||||
to: {
|
||||
opacity: 0,
|
||||
scale: 5,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export default class SearchPreview extends PureComponent {
|
||||
static propTypes = {
|
||||
actions: PropTypes.shape({
|
||||
getPostsAfter: PropTypes.func.isRequired,
|
||||
getPostsBefore: PropTypes.func.isRequired,
|
||||
getPostThread: PropTypes.func.isRequired,
|
||||
}).isRequired,
|
||||
channelId: PropTypes.string,
|
||||
channelName: PropTypes.string,
|
||||
currentUserId: PropTypes.string.isRequired,
|
||||
focusedPostId: PropTypes.string.isRequired,
|
||||
navigator: PropTypes.object,
|
||||
onClose: PropTypes.func,
|
||||
onPress: PropTypes.func,
|
||||
postIds: PropTypes.array,
|
||||
theme: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
postIds: [],
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
const {postIds} = props;
|
||||
let show = false;
|
||||
if (postIds && postIds.length >= 10) {
|
||||
show = true;
|
||||
}
|
||||
|
||||
this.state = {
|
||||
show,
|
||||
error: false,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
if (!this.state.show) {
|
||||
this.loadPosts();
|
||||
}
|
||||
}
|
||||
|
||||
handleClose = () => {
|
||||
if (this.refs.view) {
|
||||
this.refs.view.zoomOut().then(() => {
|
||||
if (this.props.onClose) {
|
||||
this.props.onClose();
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
handlePress = () => {
|
||||
const {channelId, channelName, onPress} = this.props;
|
||||
|
||||
if (this.refs.view) {
|
||||
this.refs.view.growOut().then(() => {
|
||||
if (onPress) {
|
||||
onPress(channelId, channelName);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
loadPosts = async () => {
|
||||
const {actions, channelId, focusedPostId} = this.props;
|
||||
|
||||
const result = await Promise.all([
|
||||
actions.getPostThread(focusedPostId, false),
|
||||
actions.getPostsBefore(channelId, focusedPostId, 0, 5),
|
||||
actions.getPostsAfter(channelId, focusedPostId, 0, 5),
|
||||
]);
|
||||
|
||||
const error = result.some((res) => Boolean(res.error));
|
||||
this.setState({show: true, error});
|
||||
};
|
||||
|
||||
retry = () => {
|
||||
this.setState({show: false, error: false});
|
||||
this.loadPosts();
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
channelName,
|
||||
currentUserId,
|
||||
focusedPostId,
|
||||
navigator,
|
||||
postIds,
|
||||
theme,
|
||||
} = this.props;
|
||||
const style = getStyleSheet(theme);
|
||||
|
||||
let postList;
|
||||
if (this.state.error) {
|
||||
postList = (
|
||||
<PostListRetry
|
||||
retry={this.retry}
|
||||
theme={theme}
|
||||
/>
|
||||
);
|
||||
} else if (this.state.show) {
|
||||
postList = (
|
||||
<PostList
|
||||
highlightPostId={focusedPostId}
|
||||
indicateNewMessages={false}
|
||||
isSearchResult={true}
|
||||
shouldRenderReplyButton={false}
|
||||
renderReplies={false}
|
||||
postIds={postIds}
|
||||
currentUserId={currentUserId}
|
||||
lastViewedAt={0}
|
||||
navigator={navigator}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
postList = <Loading/>;
|
||||
}
|
||||
|
||||
return (
|
||||
<View
|
||||
style={style.container}
|
||||
>
|
||||
<Animatable.View
|
||||
ref='view'
|
||||
animation='zoomIn'
|
||||
duration={200}
|
||||
delay={0}
|
||||
style={style.wrapper}
|
||||
useNativeDriver={true}
|
||||
>
|
||||
<View
|
||||
style={style.header}
|
||||
>
|
||||
<TouchableOpacity
|
||||
style={style.close}
|
||||
onPress={this.handleClose}
|
||||
>
|
||||
<MaterialIcon
|
||||
name='close'
|
||||
size={20}
|
||||
color={theme.centerChannelColor}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
<View style={style.titleContainer}>
|
||||
<Text
|
||||
ellipsizeMode='tail'
|
||||
numberOfLines={1}
|
||||
style={style.title}
|
||||
>
|
||||
{channelName}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View style={style.postList}>
|
||||
{postList}
|
||||
</View>
|
||||
<TouchableOpacity
|
||||
style={style.footer}
|
||||
onPress={this.handlePress}
|
||||
>
|
||||
<FormattedText
|
||||
id='mobile.search.jump'
|
||||
defautMessage='JUMP'
|
||||
style={style.jump}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</Animatable.View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
return {
|
||||
container: {
|
||||
position: 'absolute',
|
||||
backgroundColor: changeOpacity('#000', 0.3),
|
||||
height: '100%',
|
||||
top: 0,
|
||||
left: 0,
|
||||
zIndex: 10,
|
||||
width: '100%',
|
||||
},
|
||||
wrapper: {
|
||||
flex: 1,
|
||||
marginBottom: 10,
|
||||
marginHorizontal: 10,
|
||||
opacity: 0,
|
||||
...Platform.select({
|
||||
android: {
|
||||
marginTop: 10,
|
||||
},
|
||||
ios: {
|
||||
marginTop: 20,
|
||||
},
|
||||
}),
|
||||
},
|
||||
header: {
|
||||
alignItems: 'center',
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
borderBottomColor: changeOpacity(theme.centerChannelColor, 0.2),
|
||||
borderBottomWidth: 1,
|
||||
borderTopLeftRadius: 6,
|
||||
borderTopRightRadius: 6,
|
||||
flexDirection: 'row',
|
||||
height: 44,
|
||||
paddingRight: 16,
|
||||
width: '100%',
|
||||
},
|
||||
close: {
|
||||
justifyContent: 'center',
|
||||
height: 44,
|
||||
width: 40,
|
||||
paddingLeft: 7,
|
||||
},
|
||||
titleContainer: {
|
||||
alignItems: 'center',
|
||||
flex: 1,
|
||||
paddingRight: 40,
|
||||
},
|
||||
title: {
|
||||
color: theme.centerChannelColor,
|
||||
fontSize: 17,
|
||||
fontWeight: '600',
|
||||
},
|
||||
postList: {
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
flex: 1,
|
||||
},
|
||||
footer: {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
backgroundColor: theme.buttonBg,
|
||||
borderBottomLeftRadius: 6,
|
||||
borderBottomRightRadius: 6,
|
||||
flexDirection: 'row',
|
||||
height: 44,
|
||||
paddingRight: 16,
|
||||
width: '100%',
|
||||
},
|
||||
jump: {
|
||||
color: theme.buttonColor,
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
textAlignVertical: 'center',
|
||||
},
|
||||
};
|
||||
});
|
||||
|
|
@ -17,12 +17,23 @@ export default class SlackAttachments extends PureComponent {
|
|||
postId: PropTypes.string.isRequired,
|
||||
navigator: PropTypes.object.isRequired,
|
||||
onLongPress: PropTypes.func.isRequired,
|
||||
onPermalinkPress: PropTypes.func,
|
||||
theme: PropTypes.object,
|
||||
textStyles: PropTypes.object,
|
||||
};
|
||||
|
||||
render() {
|
||||
const {attachments, baseTextStyle, blockStyles, navigator, onLongPress, postId, theme, textStyles} = this.props;
|
||||
const {
|
||||
attachments,
|
||||
baseTextStyle,
|
||||
blockStyles,
|
||||
navigator,
|
||||
onLongPress,
|
||||
onPermalinkPress,
|
||||
postId,
|
||||
theme,
|
||||
textStyles,
|
||||
} = this.props;
|
||||
const content = [];
|
||||
|
||||
attachments.forEach((attachment, i) => {
|
||||
|
|
@ -34,6 +45,7 @@ export default class SlackAttachments extends PureComponent {
|
|||
key={'att_' + i}
|
||||
navigator={navigator}
|
||||
onLongPress={onLongPress}
|
||||
onPermalinkPress={onPermalinkPress}
|
||||
postId={postId}
|
||||
theme={theme}
|
||||
textStyles={textStyles}
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ export default class SlackAttachment extends PureComponent {
|
|||
navigator: PropTypes.object.isRequired,
|
||||
postId: PropTypes.string.isRequired,
|
||||
onLongPress: PropTypes.func.isRequired,
|
||||
onPermalinkPress: PropTypes.func,
|
||||
theme: PropTypes.object,
|
||||
textStyles: PropTypes.object,
|
||||
};
|
||||
|
|
@ -103,6 +104,7 @@ export default class SlackAttachment extends PureComponent {
|
|||
baseTextStyle,
|
||||
blockStyles,
|
||||
navigator,
|
||||
onPermalinkPress,
|
||||
textStyles,
|
||||
} = this.props;
|
||||
const fields = attachment.fields;
|
||||
|
|
@ -159,6 +161,7 @@ export default class SlackAttachment extends PureComponent {
|
|||
value={(field.value || '')}
|
||||
navigator={navigator}
|
||||
onLongPress={this.props.onLongPress}
|
||||
onPermalinkPress={onPermalinkPress}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
|
@ -212,6 +215,7 @@ export default class SlackAttachment extends PureComponent {
|
|||
blockStyles,
|
||||
textStyles,
|
||||
navigator,
|
||||
onPermalinkPress,
|
||||
theme,
|
||||
} = this.props;
|
||||
|
||||
|
|
@ -228,6 +232,7 @@ export default class SlackAttachment extends PureComponent {
|
|||
value={attachment.pretext}
|
||||
navigator={navigator}
|
||||
onLongPress={this.props.onLongPress}
|
||||
onPermalinkPress={onPermalinkPress}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
|
|
@ -341,6 +346,7 @@ export default class SlackAttachment extends PureComponent {
|
|||
value={this.state.text}
|
||||
navigator={navigator}
|
||||
onLongPress={this.props.onLongPress}
|
||||
onPermalinkPress={onPermalinkPress}
|
||||
/>
|
||||
{moreLess}
|
||||
</View>
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {injectIntl, intlShape} from 'react-intl';
|
||||
import {intlShape} from 'react-intl';
|
||||
import {
|
||||
Alert,
|
||||
Platform,
|
||||
|
|
@ -22,9 +22,20 @@ import EventEmitter from 'mattermost-redux/utils/event_emitter';
|
|||
import ChannelInfoHeader from './channel_info_header';
|
||||
import ChannelInfoRow from './channel_info_row';
|
||||
|
||||
class ChannelInfo extends PureComponent {
|
||||
export default class ChannelInfo extends PureComponent {
|
||||
static propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
actions: PropTypes.shape({
|
||||
closeDMChannel: PropTypes.func.isRequired,
|
||||
closeGMChannel: PropTypes.func.isRequired,
|
||||
deleteChannel: PropTypes.func.isRequired,
|
||||
getChannelStats: PropTypes.func.isRequired,
|
||||
leaveChannel: PropTypes.func.isRequired,
|
||||
loadChannelsByTeamName: PropTypes.func.isRequired,
|
||||
favoriteChannel: PropTypes.func.isRequired,
|
||||
unfavoriteChannel: PropTypes.func.isRequired,
|
||||
getCustomEmojisInText: PropTypes.func.isRequired,
|
||||
selectFocusedPostId: PropTypes.func.isRequired,
|
||||
}),
|
||||
canDeleteChannel: PropTypes.bool.isRequired,
|
||||
currentChannel: PropTypes.object.isRequired,
|
||||
currentChannelCreatorName: PropTypes.string,
|
||||
|
|
@ -36,23 +47,17 @@ class ChannelInfo extends PureComponent {
|
|||
isFavorite: PropTypes.bool.isRequired,
|
||||
canManageUsers: PropTypes.bool.isRequired,
|
||||
canEditChannel: PropTypes.bool.isRequired,
|
||||
actions: PropTypes.shape({
|
||||
closeDMChannel: PropTypes.func.isRequired,
|
||||
closeGMChannel: PropTypes.func.isRequired,
|
||||
deleteChannel: PropTypes.func.isRequired,
|
||||
getChannelStats: PropTypes.func.isRequired,
|
||||
leaveChannel: PropTypes.func.isRequired,
|
||||
favoriteChannel: PropTypes.func.isRequired,
|
||||
unfavoriteChannel: PropTypes.func.isRequired,
|
||||
getCustomEmojisInText: PropTypes.func.isRequired,
|
||||
}),
|
||||
};
|
||||
|
||||
static contextTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
isFavorite: this.props.isFavorite,
|
||||
isFavorite: props.isFavorite,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -82,7 +87,8 @@ class ChannelInfo extends PureComponent {
|
|||
};
|
||||
|
||||
goToChannelAddMembers = preventDoubleTap(() => {
|
||||
const {intl, navigator, theme} = this.props;
|
||||
const {intl} = this.context;
|
||||
const {navigator, theme} = this.props;
|
||||
navigator.push({
|
||||
backButtonTitle: '',
|
||||
screen: 'ChannelAddMembers',
|
||||
|
|
@ -98,7 +104,8 @@ class ChannelInfo extends PureComponent {
|
|||
});
|
||||
|
||||
goToChannelMembers = preventDoubleTap(() => {
|
||||
const {canManageUsers, intl, navigator, theme} = this.props;
|
||||
const {intl} = this.context;
|
||||
const {canManageUsers, navigator, theme} = this.props;
|
||||
const id = canManageUsers ? 'channel_header.manageMembers' : 'channel_header.viewMembers';
|
||||
const defaultMessage = canManageUsers ? 'Manage Members' : 'View Members';
|
||||
|
||||
|
|
@ -117,7 +124,8 @@ class ChannelInfo extends PureComponent {
|
|||
});
|
||||
|
||||
handleChannelEdit = preventDoubleTap(() => {
|
||||
const {intl, navigator, theme} = this.props;
|
||||
const {intl} = this.context;
|
||||
const {navigator, theme} = this.props;
|
||||
const id = 'mobile.channel_info.edit';
|
||||
const defaultMessage = 'Edit Channel';
|
||||
|
||||
|
|
@ -144,7 +152,7 @@ class ChannelInfo extends PureComponent {
|
|||
};
|
||||
|
||||
handleDeleteOrLeave = preventDoubleTap((eventType) => {
|
||||
const {formatMessage} = this.props.intl;
|
||||
const {formatMessage} = this.context.intl;
|
||||
const channel = this.props.currentChannel;
|
||||
const term = channel.type === General.OPEN_CHANNEL ?
|
||||
formatMessage({id: 'mobile.channel_info.publicChannel', defaultMessage: 'Public Channel'}) :
|
||||
|
|
@ -173,7 +181,7 @@ class ChannelInfo extends PureComponent {
|
|||
const result = await this.props.actions.deleteChannel(channel.id);
|
||||
if (result.error) {
|
||||
alertErrorWithFallback(
|
||||
this.props.intl,
|
||||
this.context.intl,
|
||||
result.error,
|
||||
{
|
||||
id: 'mobile.channel_info.delete_failed',
|
||||
|
|
@ -234,6 +242,44 @@ class ChannelInfo extends PureComponent {
|
|||
toggleFavorite(currentChannel.id);
|
||||
};
|
||||
|
||||
handleClosePermalink = () => {
|
||||
const {actions} = this.props;
|
||||
actions.selectFocusedPostId('');
|
||||
this.showingPermalink = false;
|
||||
};
|
||||
|
||||
handlePermalinkPress = (postId, teamName) => {
|
||||
this.props.actions.loadChannelsByTeamName(teamName);
|
||||
this.showPermalinkView(postId);
|
||||
};
|
||||
|
||||
showPermalinkView = (postId) => {
|
||||
const {actions, navigator} = this.props;
|
||||
|
||||
actions.selectFocusedPostId(postId);
|
||||
|
||||
if (!this.showingPermalink) {
|
||||
const options = {
|
||||
screen: 'Permalink',
|
||||
animationType: 'none',
|
||||
backButtonTitle: '',
|
||||
navigatorStyle: {
|
||||
navBarHidden: true,
|
||||
screenBackgroundColor: changeOpacity('#000', 0.2),
|
||||
modalPresentationStyle: 'overCurrentContext',
|
||||
},
|
||||
passProps: {
|
||||
isPermalink: true,
|
||||
onClose: this.handleClosePermalink,
|
||||
onPermalinkPress: this.handlePermalinkPress,
|
||||
},
|
||||
};
|
||||
|
||||
this.showingPermalink = true;
|
||||
navigator.showModal(options);
|
||||
}
|
||||
};
|
||||
|
||||
renderViewOrManageMembersRow = () => {
|
||||
const channel = this.props.currentChannel;
|
||||
const isDirectMessage = channel.type === General.DM_CHANNEL;
|
||||
|
|
@ -301,6 +347,7 @@ class ChannelInfo extends PureComponent {
|
|||
header={currentChannel.header}
|
||||
memberCount={currentChannelMemberCount}
|
||||
navigator={navigator}
|
||||
onPermalinkPress={this.handlePermalinkPress}
|
||||
purpose={currentChannel.purpose}
|
||||
status={status}
|
||||
theme={theme}
|
||||
|
|
@ -443,5 +490,3 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
|||
},
|
||||
};
|
||||
});
|
||||
|
||||
export default injectIntl(ChannelInfo);
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ export default class ChannelInfoHeader extends React.PureComponent {
|
|||
displayName: PropTypes.string.isRequired,
|
||||
header: PropTypes.string,
|
||||
navigator: PropTypes.object.isRequired,
|
||||
onPermalinkPress: PropTypes.func,
|
||||
purpose: PropTypes.string,
|
||||
status: PropTypes.string,
|
||||
theme: PropTypes.object.isRequired,
|
||||
|
|
@ -37,6 +38,7 @@ export default class ChannelInfoHeader extends React.PureComponent {
|
|||
header,
|
||||
memberCount,
|
||||
navigator,
|
||||
onPermalinkPress,
|
||||
purpose,
|
||||
status,
|
||||
theme,
|
||||
|
|
@ -75,6 +77,7 @@ export default class ChannelInfoHeader extends React.PureComponent {
|
|||
/>
|
||||
<Markdown
|
||||
navigator={navigator}
|
||||
onPermalinkPress={onPermalinkPress}
|
||||
baseTextStyle={style.detail}
|
||||
textStyles={textStyles}
|
||||
blockStyles={blockStyles}
|
||||
|
|
@ -91,6 +94,7 @@ export default class ChannelInfoHeader extends React.PureComponent {
|
|||
/>
|
||||
<Markdown
|
||||
navigator={navigator}
|
||||
onPermalinkPress={onPermalinkPress}
|
||||
baseTextStyle={style.detail}
|
||||
textStyles={textStyles}
|
||||
blockStyles={blockStyles}
|
||||
|
|
|
|||
|
|
@ -8,11 +8,13 @@ import {
|
|||
closeDMChannel,
|
||||
closeGMChannel,
|
||||
leaveChannel,
|
||||
loadChannelsByTeamName,
|
||||
} from 'app/actions/views/channel';
|
||||
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
|
||||
|
||||
import {getCustomEmojisInText} from 'mattermost-redux/actions/emojis';
|
||||
import {favoriteChannel, getChannelStats, deleteChannel, unfavoriteChannel} from 'mattermost-redux/actions/channels';
|
||||
import {selectFocusedPostId} from 'mattermost-redux/actions/posts';
|
||||
import {General} from 'mattermost-redux/constants';
|
||||
import {
|
||||
getCurrentChannel,
|
||||
|
|
@ -68,9 +70,11 @@ function mapDispatchToProps(dispatch) {
|
|||
deleteChannel,
|
||||
getChannelStats,
|
||||
leaveChannel,
|
||||
loadChannelsByTeamName,
|
||||
favoriteChannel,
|
||||
unfavoriteChannel,
|
||||
getCustomEmojisInText,
|
||||
selectFocusedPostId,
|
||||
}, dispatch),
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ import NotificationSettingsMentions from 'app/screens/settings/notification_sett
|
|||
import NotificationSettingsMentionsKeywords from 'app/screens/settings/notification_settings_mentions_keywords';
|
||||
import NotificationSettingsMobile from 'app/screens/settings/notification_settings_mobile';
|
||||
import OptionsModal from 'app/screens/options_modal';
|
||||
import Permalink from 'app/screens/permalink';
|
||||
import Root from 'app/screens/root';
|
||||
import SSO from 'app/screens/sso';
|
||||
import Search from 'app/screens/search';
|
||||
|
|
@ -91,6 +92,7 @@ export function registerScreens(store, Provider) {
|
|||
Navigation.registerComponent('NotificationSettingsMentionsKeywords', () => wrapWithContextProvider(NotificationSettingsMentionsKeywords), store, Provider);
|
||||
Navigation.registerComponent('NotificationSettingsMobile', () => wrapWithContextProvider(NotificationSettingsMobile), store, Provider);
|
||||
Navigation.registerComponent('OptionsModal', () => wrapWithContextProvider(OptionsModal), store, Provider);
|
||||
Navigation.registerComponent('Permalink', () => wrapWithContextProvider(Permalink), store, Provider);
|
||||
Navigation.registerComponent('Root', () => Root, store, Provider);
|
||||
Navigation.registerComponent('Search', () => wrapWithContextProvider(Search), store, Provider);
|
||||
Navigation.registerComponent('SelectServer', () => wrapWithContextProvider(SelectServer), store, Provider);
|
||||
|
|
|
|||
76
app/screens/permalink/index.js
Normal file
76
app/screens/permalink/index.js
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import {bindActionCreators} from 'redux';
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {getChannel as getChannelAction, joinChannel, markChannelAsRead, markChannelAsViewed} from 'mattermost-redux/actions/channels';
|
||||
import {getPostsAfter, getPostsBefore, getPostThread, selectPost} from 'mattermost-redux/actions/posts';
|
||||
import {getMyChannelMemberships, makeGetChannel} from 'mattermost-redux/selectors/entities/channels';
|
||||
import {makeGetPostIdsAroundPost, getPost} from 'mattermost-redux/selectors/entities/posts';
|
||||
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams';
|
||||
import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
|
||||
|
||||
import {
|
||||
handleSelectChannel,
|
||||
loadThreadIfNecessary,
|
||||
setChannelDisplayName,
|
||||
setChannelLoading,
|
||||
} from 'app/actions/views/channel';
|
||||
import {handleTeamChange} from 'app/actions/views/select_team';
|
||||
|
||||
import Permalink from './permalink';
|
||||
|
||||
function makeMapStateToProps() {
|
||||
const getPostIdsAroundPost = makeGetPostIdsAroundPost();
|
||||
const getChannel = makeGetChannel();
|
||||
|
||||
return function mapStateToProps(state) {
|
||||
const {currentFocusedPostId} = state.entities.posts;
|
||||
const post = getPost(state, currentFocusedPostId);
|
||||
const channel = post ? getChannel(state, {id: post.channel_id}) : null;
|
||||
let postIds;
|
||||
|
||||
if (channel && channel.id) {
|
||||
postIds = getPostIdsAroundPost(state, currentFocusedPostId, channel.id, {
|
||||
postsBeforeCount: 10,
|
||||
postsAfterCount: 10,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
channelId: channel ? channel.id : '',
|
||||
channelName: channel ? channel.display_name : '',
|
||||
channelTeamId: channel ? channel.team_id : '',
|
||||
currentTeamId: getCurrentTeamId(state),
|
||||
currentUserId: getCurrentUserId(state),
|
||||
focusedPostId: currentFocusedPostId,
|
||||
myMembers: getMyChannelMemberships(state),
|
||||
postIds,
|
||||
theme: getTheme(state),
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators({
|
||||
getPostsAfter,
|
||||
getPostsBefore,
|
||||
getPostThread,
|
||||
getChannel: getChannelAction,
|
||||
handleSelectChannel,
|
||||
handleTeamChange,
|
||||
joinChannel,
|
||||
loadThreadIfNecessary,
|
||||
markChannelAsRead,
|
||||
markChannelAsViewed,
|
||||
selectPost,
|
||||
setChannelDisplayName,
|
||||
setChannelLoading,
|
||||
}, dispatch),
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(makeMapStateToProps, mapDispatchToProps, null, {withRef: true})(Permalink);
|
||||
455
app/screens/permalink/permalink.js
Normal file
455
app/screens/permalink/permalink.js
Normal file
|
|
@ -0,0 +1,455 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
InteractionManager,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
} from 'react-native';
|
||||
import {intlShape} from 'react-intl';
|
||||
import * as Animatable from 'react-native-animatable';
|
||||
import MaterialIcon from 'react-native-vector-icons/MaterialIcons';
|
||||
|
||||
import {General} from 'mattermost-redux/constants';
|
||||
|
||||
import FormattedText from 'app/components/formatted_text';
|
||||
import Loading from 'app/components/loading';
|
||||
import PostList from 'app/components/post_list';
|
||||
import PostListRetry from 'app/components/post_list_retry';
|
||||
import SafeAreaView from 'app/components/safe_area_view';
|
||||
import {preventDoubleTap} from 'app/utils/tap';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
|
||||
Animatable.initializeRegistryWithDefinitions({
|
||||
growOut: {
|
||||
from: {
|
||||
opacity: 1,
|
||||
scale: 1,
|
||||
},
|
||||
0.5: {
|
||||
opacity: 1,
|
||||
scale: 3,
|
||||
},
|
||||
to: {
|
||||
opacity: 0,
|
||||
scale: 5,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export default class Permalink extends PureComponent {
|
||||
static propTypes = {
|
||||
actions: PropTypes.shape({
|
||||
getPostsAfter: PropTypes.func.isRequired,
|
||||
getPostsBefore: PropTypes.func.isRequired,
|
||||
getPostThread: PropTypes.func.isRequired,
|
||||
getChannel: PropTypes.func.isRequired,
|
||||
handleSelectChannel: PropTypes.func.isRequired,
|
||||
handleTeamChange: PropTypes.func.isRequired,
|
||||
joinChannel: PropTypes.func.isRequired,
|
||||
loadThreadIfNecessary: PropTypes.func.isRequired,
|
||||
markChannelAsRead: PropTypes.func.isRequired,
|
||||
markChannelAsViewed: PropTypes.func.isRequired,
|
||||
selectPost: PropTypes.func.isRequired,
|
||||
setChannelDisplayName: PropTypes.func.isRequired,
|
||||
setChannelLoading: PropTypes.func.isRequired,
|
||||
}).isRequired,
|
||||
channelId: PropTypes.string,
|
||||
channelName: PropTypes.string,
|
||||
channelTeamId: PropTypes.string,
|
||||
currentTeamId: PropTypes.string.isRequired,
|
||||
currentUserId: PropTypes.string.isRequired,
|
||||
focusedPostId: PropTypes.string.isRequired,
|
||||
isPermalink: PropTypes.bool,
|
||||
myMembers: PropTypes.object.isRequired,
|
||||
navigator: PropTypes.object,
|
||||
onClose: PropTypes.func,
|
||||
onPermalinkPress: PropTypes.func,
|
||||
onPress: PropTypes.func,
|
||||
postIds: PropTypes.array,
|
||||
theme: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
onPress: () => true,
|
||||
postIds: [],
|
||||
};
|
||||
|
||||
static contextTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
const {postIds, channelName} = props;
|
||||
let loading = false;
|
||||
|
||||
if (postIds && postIds.length >= 10) {
|
||||
loading = true;
|
||||
}
|
||||
|
||||
this.state = {
|
||||
title: channelName,
|
||||
loading,
|
||||
error: '',
|
||||
retry: false,
|
||||
};
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
if (!this.state.loading) {
|
||||
this.loadPosts(this.props);
|
||||
}
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (this.props.channelName !== nextProps.channelName) {
|
||||
this.setState({title: nextProps.channelName});
|
||||
}
|
||||
|
||||
if (this.props.focusedPostId !== nextProps.focusedPostId) {
|
||||
this.setState({loading: false});
|
||||
if (nextProps.postIds && nextProps.postIds.length < 10) {
|
||||
this.loadPosts(nextProps);
|
||||
} else {
|
||||
this.setState({loading: true});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
goToThread = preventDoubleTap((post) => {
|
||||
const {actions, navigator, theme} = this.props;
|
||||
const channelId = post.channel_id;
|
||||
const rootId = (post.root_id || post.id);
|
||||
|
||||
actions.loadThreadIfNecessary(rootId, channelId);
|
||||
actions.selectPost(rootId);
|
||||
|
||||
const options = {
|
||||
screen: 'Thread',
|
||||
animated: true,
|
||||
backButtonTitle: '',
|
||||
navigatorStyle: {
|
||||
navBarTextColor: theme.sidebarHeaderTextColor,
|
||||
navBarBackgroundColor: theme.sidebarHeaderBg,
|
||||
navBarButtonColor: theme.sidebarHeaderTextColor,
|
||||
screenBackgroundColor: theme.centerChannelBg,
|
||||
},
|
||||
passProps: {
|
||||
channelId,
|
||||
rootId,
|
||||
},
|
||||
};
|
||||
|
||||
navigator.push(options);
|
||||
});
|
||||
|
||||
handleClose = () => {
|
||||
const {actions, navigator, onClose} = this.props;
|
||||
if (this.refs.view) {
|
||||
this.refs.view.zoomOut().then(() => {
|
||||
actions.selectPost('');
|
||||
navigator.dismissModal({animationType: 'none'});
|
||||
|
||||
if (onClose) {
|
||||
onClose();
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
jumpToChannel = (channelId, channelDisplayName) => {
|
||||
if (channelId) {
|
||||
const {actions, channelTeamId, currentTeamId, navigator, onClose, theme} = this.props;
|
||||
const currentChannelId = this.props.channelId;
|
||||
const {
|
||||
handleSelectChannel,
|
||||
handleTeamChange,
|
||||
markChannelAsRead,
|
||||
setChannelLoading,
|
||||
setChannelDisplayName,
|
||||
markChannelAsViewed,
|
||||
} = actions;
|
||||
|
||||
actions.selectPost('');
|
||||
|
||||
if (onClose) {
|
||||
onClose();
|
||||
}
|
||||
|
||||
navigator.resetTo({
|
||||
screen: 'Channel',
|
||||
animated: true,
|
||||
animationType: 'fade',
|
||||
navigatorStyle: {
|
||||
navBarHidden: true,
|
||||
statusBarHidden: false,
|
||||
statusBarHideWithNavBar: false,
|
||||
screenBackgroundColor: theme.centerChannelBg,
|
||||
},
|
||||
});
|
||||
|
||||
if (channelTeamId && currentTeamId !== channelTeamId) {
|
||||
handleTeamChange(channelTeamId, false);
|
||||
}
|
||||
|
||||
setChannelLoading(channelId !== currentChannelId);
|
||||
setChannelDisplayName(channelDisplayName);
|
||||
handleSelectChannel(channelId);
|
||||
|
||||
InteractionManager.runAfterInteractions(async () => {
|
||||
markChannelAsRead(channelId, currentChannelId);
|
||||
if (channelId !== currentChannelId) {
|
||||
markChannelAsViewed(currentChannelId);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
handlePress = () => {
|
||||
const {channelId, channelName} = this.props;
|
||||
|
||||
if (this.refs.view) {
|
||||
this.refs.view.growOut().then(() => {
|
||||
this.jumpToChannel(channelId, channelName);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
loadPosts = async (props) => {
|
||||
const {intl} = this.context;
|
||||
const {actions, channelId, currentUserId, focusedPostId, isPermalink, postIds} = props;
|
||||
const {formatMessage} = intl;
|
||||
let focusChannelId = channelId;
|
||||
|
||||
const post = await actions.getPostThread(focusedPostId, false);
|
||||
if (post.error && (!postIds || !postIds.length)) {
|
||||
if (isPermalink && post.error.message.toLowerCase() !== 'network request failed') {
|
||||
this.setState({
|
||||
error: formatMessage({
|
||||
id: 'permalink.error.access',
|
||||
defaultMessage: 'Permalink belongs to a deleted message or to a channel to which you do not have access.',
|
||||
}),
|
||||
title: formatMessage({
|
||||
id: 'mobile.search.no_results',
|
||||
defaultMessage: 'No Results Found',
|
||||
}),
|
||||
});
|
||||
} else {
|
||||
this.setState({error: post.error.message, retry: true});
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!channelId) {
|
||||
focusChannelId = post.data.posts[focusedPostId].channel_id;
|
||||
if (!this.props.myMembers[focusChannelId]) {
|
||||
const {data: channel} = await actions.getChannel(focusChannelId);
|
||||
if (channel && channel.type === General.OPEN_CHANNEL) {
|
||||
await actions.joinChannel(currentUserId, channel.team_id, channel.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.all([
|
||||
actions.getPostsBefore(focusChannelId, focusedPostId, 0, 10),
|
||||
actions.getPostsAfter(focusChannelId, focusedPostId, 0, 10),
|
||||
]);
|
||||
|
||||
this.setState({loading: true});
|
||||
};
|
||||
|
||||
retry = () => {
|
||||
this.setState({loading: false, error: null, retry: false});
|
||||
this.loadPosts(this.props);
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
currentUserId,
|
||||
focusedPostId,
|
||||
navigator,
|
||||
onPermalinkPress,
|
||||
postIds,
|
||||
theme,
|
||||
} = this.props;
|
||||
const {error, retry, loading, title} = this.state;
|
||||
const style = getStyleSheet(theme);
|
||||
|
||||
let postList;
|
||||
if (retry) {
|
||||
postList = (
|
||||
<PostListRetry
|
||||
retry={this.retry}
|
||||
theme={theme}
|
||||
/>
|
||||
);
|
||||
} else if (error) {
|
||||
postList = (
|
||||
<View style={style.errorContainer}>
|
||||
<Text style={style.errorText}>
|
||||
{error}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
} else if (loading) {
|
||||
postList = (
|
||||
<PostList
|
||||
highlightPostId={focusedPostId}
|
||||
indicateNewMessages={false}
|
||||
isSearchResult={false}
|
||||
shouldRenderReplyButton={false}
|
||||
renderReplies={true}
|
||||
onPermalinkPress={onPermalinkPress}
|
||||
onPostPress={this.goToThread}
|
||||
postIds={postIds}
|
||||
currentUserId={currentUserId}
|
||||
lastViewedAt={0}
|
||||
navigator={navigator}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
postList = <Loading/>;
|
||||
}
|
||||
|
||||
return (
|
||||
<SafeAreaView
|
||||
backgroundColor='transparent'
|
||||
excludeHeader={true}
|
||||
footerColor='transparent'
|
||||
forceTop={44}
|
||||
>
|
||||
<View
|
||||
style={style.container}
|
||||
>
|
||||
<Animatable.View
|
||||
ref='view'
|
||||
animation='zoomIn'
|
||||
duration={200}
|
||||
delay={0}
|
||||
style={style.wrapper}
|
||||
useNativeDriver={true}
|
||||
>
|
||||
<View
|
||||
style={style.header}
|
||||
>
|
||||
<TouchableOpacity
|
||||
style={style.close}
|
||||
onPress={this.handleClose}
|
||||
>
|
||||
<MaterialIcon
|
||||
name='close'
|
||||
size={20}
|
||||
color={theme.centerChannelColor}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
<View style={style.titleContainer}>
|
||||
<Text
|
||||
ellipsizeMode='tail'
|
||||
numberOfLines={1}
|
||||
style={style.title}
|
||||
>
|
||||
{title}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View style={[style.postList, error ? style.bottom : null]}>
|
||||
{postList}
|
||||
</View>
|
||||
{!error && loading &&
|
||||
<TouchableOpacity
|
||||
style={[style.footer, style.bottom]}
|
||||
onPress={this.handlePress}
|
||||
>
|
||||
<FormattedText
|
||||
id='mobile.search.jump'
|
||||
defautMessage='Jump to recent messages'
|
||||
style={style.jump}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
}
|
||||
</Animatable.View>
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
return {
|
||||
container: {
|
||||
flex: 1,
|
||||
marginTop: 20,
|
||||
},
|
||||
wrapper: {
|
||||
flex: 1,
|
||||
margin: 10,
|
||||
opacity: 0,
|
||||
},
|
||||
header: {
|
||||
alignItems: 'center',
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
borderBottomColor: changeOpacity(theme.centerChannelColor, 0.2),
|
||||
borderBottomWidth: 1,
|
||||
borderTopLeftRadius: 6,
|
||||
borderTopRightRadius: 6,
|
||||
flexDirection: 'row',
|
||||
height: 44,
|
||||
paddingRight: 16,
|
||||
width: '100%',
|
||||
},
|
||||
close: {
|
||||
justifyContent: 'center',
|
||||
height: 44,
|
||||
width: 40,
|
||||
paddingLeft: 7,
|
||||
},
|
||||
titleContainer: {
|
||||
alignItems: 'center',
|
||||
flex: 1,
|
||||
paddingRight: 40,
|
||||
},
|
||||
title: {
|
||||
color: theme.centerChannelColor,
|
||||
fontSize: 17,
|
||||
fontWeight: '600',
|
||||
},
|
||||
postList: {
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
flex: 1,
|
||||
},
|
||||
bottom: {
|
||||
borderBottomLeftRadius: 6,
|
||||
borderBottomRightRadius: 6,
|
||||
},
|
||||
footer: {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
backgroundColor: theme.buttonBg,
|
||||
flexDirection: 'row',
|
||||
height: 43,
|
||||
paddingRight: 16,
|
||||
width: '100%',
|
||||
},
|
||||
jump: {
|
||||
color: theme.buttonColor,
|
||||
fontSize: 15,
|
||||
fontWeight: '600',
|
||||
textAlignVertical: 'center',
|
||||
},
|
||||
errorContainer: {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: 15,
|
||||
},
|
||||
errorText: {
|
||||
color: changeOpacity(theme.centerChannelColor, 0.4),
|
||||
fontSize: 15,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
|
@ -4,19 +4,13 @@
|
|||
import {bindActionCreators} from 'redux';
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {markChannelAsRead, markChannelAsViewed} from 'mattermost-redux/actions/channels';
|
||||
import {selectPost} from 'mattermost-redux/actions/posts';
|
||||
import {selectFocusedPostId, selectPost} from 'mattermost-redux/actions/posts';
|
||||
import {clearSearch, removeSearchTerms, searchPosts} from 'mattermost-redux/actions/search';
|
||||
import {getCurrentChannelId} from 'mattermost-redux/selectors/entities/channels';
|
||||
import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams';
|
||||
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
|
||||
|
||||
import {
|
||||
handleSelectChannel,
|
||||
loadThreadIfNecessary,
|
||||
setChannelDisplayName,
|
||||
setChannelLoading,
|
||||
} from 'app/actions/views/channel';
|
||||
import {loadChannelsByTeamName, loadThreadIfNecessary} from 'app/actions/views/channel';
|
||||
import {isLandscape} from 'app/selectors/device';
|
||||
import {handleSearchDraftChanged} from 'app/actions/views/search';
|
||||
|
||||
|
|
@ -44,15 +38,12 @@ function mapDispatchToProps(dispatch) {
|
|||
actions: bindActionCreators({
|
||||
clearSearch,
|
||||
handleSearchDraftChanged,
|
||||
handleSelectChannel,
|
||||
loadChannelsByTeamName,
|
||||
loadThreadIfNecessary,
|
||||
markChannelAsRead,
|
||||
markChannelAsViewed,
|
||||
removeSearchTerms,
|
||||
selectFocusedPostId,
|
||||
searchPosts,
|
||||
selectPost,
|
||||
setChannelDisplayName,
|
||||
setChannelLoading,
|
||||
}, dispatch),
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,10 +3,9 @@
|
|||
|
||||
import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {injectIntl, intlShape} from 'react-intl';
|
||||
import {intlShape} from 'react-intl';
|
||||
import {
|
||||
Keyboard,
|
||||
InteractionManager,
|
||||
Platform,
|
||||
SectionList,
|
||||
Text,
|
||||
|
|
@ -26,7 +25,6 @@ import Loading from 'app/components/loading';
|
|||
import PostListRetry from 'app/components/post_list_retry';
|
||||
import SafeAreaView from 'app/components/safe_area_view';
|
||||
import SearchBar from 'app/components/search_bar';
|
||||
import SearchPreview from 'app/components/search_preview';
|
||||
import StatusBar from 'app/components/status_bar';
|
||||
import mattermostManaged from 'app/mattermost_managed';
|
||||
import {preventDoubleTap} from 'app/utils/tap';
|
||||
|
|
@ -42,21 +40,20 @@ const MODIFIER_LABEL_HEIGHT = 58;
|
|||
const SEARCHING = 'searching';
|
||||
const NO_RESULTS = 'no results';
|
||||
|
||||
class Search extends PureComponent {
|
||||
export default class Search extends PureComponent {
|
||||
static propTypes = {
|
||||
actions: PropTypes.shape({
|
||||
clearSearch: PropTypes.func.isRequired,
|
||||
handleSearchDraftChanged: PropTypes.func.isRequired,
|
||||
loadChannelsByTeamName: PropTypes.func.isRequired,
|
||||
loadThreadIfNecessary: PropTypes.func.isRequired,
|
||||
markChannelAsRead: PropTypes.func.isRequired,
|
||||
markChannelAsViewed: PropTypes.func.isRequired,
|
||||
removeSearchTerms: PropTypes.func.isRequired,
|
||||
searchPosts: PropTypes.func.isRequired,
|
||||
selectFocusedPostId: PropTypes.func.isRequired,
|
||||
selectPost: PropTypes.func.isRequired,
|
||||
}).isRequired,
|
||||
currentTeamId: PropTypes.string.isRequired,
|
||||
currentChannelId: PropTypes.string.isRequired,
|
||||
intl: intlShape.isRequired,
|
||||
isLandscape: PropTypes.bool.isRequired,
|
||||
navigator: PropTypes.object,
|
||||
postIds: PropTypes.array,
|
||||
|
|
@ -70,6 +67,10 @@ class Search extends PureComponent {
|
|||
recent: [],
|
||||
};
|
||||
|
||||
static contextTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
|
|
@ -77,9 +78,6 @@ class Search extends PureComponent {
|
|||
this.isX = DeviceInfo.getModel() === 'iPhone X';
|
||||
this.state = {
|
||||
channelName: '',
|
||||
focusedChannelId: null,
|
||||
focusedPostId: null,
|
||||
preview: false,
|
||||
value: '',
|
||||
managedConfig: {},
|
||||
};
|
||||
|
|
@ -160,6 +158,17 @@ class Search extends PureComponent {
|
|||
navigator.push(options);
|
||||
};
|
||||
|
||||
handleClosePermalink = () => {
|
||||
const {actions} = this.props;
|
||||
actions.selectFocusedPostId('');
|
||||
this.showingPermalink = false;
|
||||
};
|
||||
|
||||
handlePermalinkPress = (postId, teamName) => {
|
||||
this.props.actions.loadChannelsByTeamName(teamName);
|
||||
this.showPermalinkView(postId, true);
|
||||
};
|
||||
|
||||
handleSelectionChange = (event) => {
|
||||
if (this.autocomplete) {
|
||||
this.autocomplete.getWrappedInstance().handleSelectionChange(event);
|
||||
|
|
@ -201,10 +210,6 @@ class Search extends PureComponent {
|
|||
return item.id || item;
|
||||
};
|
||||
|
||||
onFocus = () => {
|
||||
this.scrollToTop();
|
||||
};
|
||||
|
||||
onNavigatorEvent = (event) => {
|
||||
if (event.id === 'backPress') {
|
||||
if (this.state.preview) {
|
||||
|
|
@ -218,11 +223,34 @@ class Search extends PureComponent {
|
|||
previewPost = (post) => {
|
||||
Keyboard.dismiss();
|
||||
|
||||
this.setState({
|
||||
preview: true,
|
||||
focusedChannelId: post.channel_id,
|
||||
focusedPostId: post.id,
|
||||
});
|
||||
this.showPermalinkView(post.id, false);
|
||||
};
|
||||
|
||||
showPermalinkView = (postId, isPermalink) => {
|
||||
const {actions, navigator} = this.props;
|
||||
|
||||
actions.selectFocusedPostId(postId);
|
||||
|
||||
if (!this.showingPermalink) {
|
||||
const options = {
|
||||
screen: 'Permalink',
|
||||
animationType: 'none',
|
||||
backButtonTitle: '',
|
||||
navigatorStyle: {
|
||||
navBarHidden: true,
|
||||
screenBackgroundColor: changeOpacity('#000', 0.2),
|
||||
modalPresentationStyle: 'overCurrentContext',
|
||||
},
|
||||
passProps: {
|
||||
isPermalink,
|
||||
onClose: this.handleClosePermalink,
|
||||
onPermalinkPress: this.handlePermalinkPress,
|
||||
},
|
||||
};
|
||||
|
||||
this.showingPermalink = true;
|
||||
navigator.showModal(options);
|
||||
}
|
||||
};
|
||||
|
||||
removeSearchTerms = preventDoubleTap((item) => {
|
||||
|
|
@ -290,6 +318,7 @@ class Search extends PureComponent {
|
|||
previewPost={this.previewPost}
|
||||
goToThread={this.goToThread}
|
||||
navigator={this.props.navigator}
|
||||
onPermalinkPress={this.handlePermalinkPress}
|
||||
managedConfig={managedConfig}
|
||||
/>
|
||||
{separator}
|
||||
|
|
@ -440,60 +469,20 @@ class Search extends PureComponent {
|
|||
const {terms, isOrSearch} = recent;
|
||||
this.handleTextChanged(terms);
|
||||
this.search(terms, isOrSearch);
|
||||
Keyboard.dismiss();
|
||||
});
|
||||
|
||||
handleClosePreview = () => {
|
||||
this.setState({
|
||||
preview: false,
|
||||
focusedChannelId: null,
|
||||
focusedPostId: null,
|
||||
});
|
||||
};
|
||||
|
||||
handleJumpToChannel = (channelId, channelDisplayName) => {
|
||||
if (channelId) {
|
||||
const {actions, currentChannelId} = this.props;
|
||||
const {
|
||||
handleSelectChannel,
|
||||
markChannelAsRead,
|
||||
setChannelLoading,
|
||||
setChannelDisplayName,
|
||||
markChannelAsViewed,
|
||||
} = actions;
|
||||
|
||||
setChannelLoading(channelId !== currentChannelId);
|
||||
setChannelDisplayName(channelDisplayName);
|
||||
|
||||
InteractionManager.runAfterInteractions(() => {
|
||||
handleSelectChannel(channelId);
|
||||
requestAnimationFrame(() => {
|
||||
// mark the channel as viewed after all the frame has flushed
|
||||
markChannelAsRead(channelId, currentChannelId);
|
||||
if (channelId !== currentChannelId) {
|
||||
markChannelAsViewed(currentChannelId);
|
||||
}
|
||||
});
|
||||
|
||||
this.props.navigator.dismissModal({animationType: 'slide-down'});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
intl,
|
||||
isLandscape,
|
||||
navigator,
|
||||
postIds,
|
||||
recent,
|
||||
searchingStatus,
|
||||
theme,
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
preview,
|
||||
value,
|
||||
} = this.state;
|
||||
const {intl} = this.context;
|
||||
const {value} = this.state;
|
||||
const style = getStyleFromTheme(theme);
|
||||
const sections = [{
|
||||
data: [{
|
||||
|
|
@ -522,7 +511,7 @@ class Search extends PureComponent {
|
|||
sections.push({
|
||||
data: recent,
|
||||
key: 'recent',
|
||||
title: intl.formatMessage({id: 'mobile.search.recentTitle', defaultMessage: 'Recent Searches'}),
|
||||
title: intl.formatMessage({id: 'mobile.search.recent_title', defaultMessage: 'Recent Searches'}),
|
||||
renderItem: this.renderRecentItem,
|
||||
keyExtractor: this.keyRecentExtractor,
|
||||
ItemSeparatorComponent: this.renderRecentSeparator,
|
||||
|
|
@ -583,23 +572,6 @@ class Search extends PureComponent {
|
|||
});
|
||||
}
|
||||
|
||||
let previewComponent;
|
||||
if (preview) {
|
||||
const {focusedChannelId, focusedPostId} = this.state;
|
||||
|
||||
previewComponent = (
|
||||
<SearchPreview
|
||||
ref='preview'
|
||||
channelId={focusedChannelId}
|
||||
focusedPostId={focusedPostId}
|
||||
navigator={navigator}
|
||||
onClose={this.handleClosePreview}
|
||||
onPress={this.handleJumpToChannel}
|
||||
theme={theme}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const searchBarInput = {
|
||||
backgroundColor: changeOpacity(theme.sidebarHeaderTextColor, 0.2),
|
||||
color: theme.sidebarHeaderTextColor,
|
||||
|
|
@ -651,7 +623,6 @@ class Search extends PureComponent {
|
|||
isSearch={true}
|
||||
value={value}
|
||||
/>
|
||||
{previewComponent}
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
);
|
||||
|
|
@ -778,4 +749,3 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
|
|||
};
|
||||
});
|
||||
|
||||
export default injectIntl(Search);
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ export default class SearchResultPost extends PureComponent {
|
|||
goToThread: PropTypes.func.isRequired,
|
||||
managedConfig: PropTypes.object.isRequired,
|
||||
navigator: PropTypes.object.isRequired,
|
||||
onPermalinkPress: PropTypes.func.isRequired,
|
||||
postId: PropTypes.string.isRequired,
|
||||
previewPost: PropTypes.func.isRequired,
|
||||
};
|
||||
|
|
@ -26,6 +27,7 @@ export default class SearchResultPost extends PureComponent {
|
|||
postComponentProps.onReply = this.props.goToThread;
|
||||
postComponentProps.shouldRenderReplyButton = true;
|
||||
postComponentProps.managedConfig = this.props.managedConfig;
|
||||
postComponentProps.onPermalinkPress = this.props.onPermalinkPress;
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@
|
|||
import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
Platform,
|
||||
ScrollView,
|
||||
Text,
|
||||
View,
|
||||
|
|
@ -47,17 +46,20 @@ class UserProfile extends PureComponent {
|
|||
}
|
||||
|
||||
close = () => {
|
||||
const {navigator} = this.props;
|
||||
const {navigator, theme} = this.props;
|
||||
|
||||
navigator.popToRoot({
|
||||
navigator.resetTo({
|
||||
screen: 'Channel',
|
||||
animated: true,
|
||||
navigatorStyle: {
|
||||
animated: true,
|
||||
animationType: 'fade',
|
||||
navBarHidden: true,
|
||||
statusBarHidden: false,
|
||||
statusBarHideWithNavBar: false,
|
||||
screenBackgroundColor: theme.centerChannelBg,
|
||||
},
|
||||
});
|
||||
|
||||
if (Platform.OS === 'android') {
|
||||
navigator.dismissModal({
|
||||
animationType: 'slide-down',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
displaySendMessageOption = () => {
|
||||
|
|
|
|||
|
|
@ -181,3 +181,7 @@ const languages = {
|
|||
export function getDisplayNameForLanguage(language) {
|
||||
return languages[language.toLowerCase()] || '';
|
||||
}
|
||||
|
||||
export function escapeRegex(text) {
|
||||
return text.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2166,8 +2166,9 @@
|
|||
"mobile.search.from_modifier_title": "username",
|
||||
"mobile.search.in_modifier_description": "to find posts in specific channels",
|
||||
"mobile.search.in_modifier_title": "channel-name",
|
||||
"mobile.search.jump": "JUMP",
|
||||
"mobile.search.jump": "Jump to recent messages",
|
||||
"mobile.search.no_results": "No Results Found",
|
||||
"mobile.search.recent_title": "Recent Searches",
|
||||
"mobile.select_team.choose": "Your teams:",
|
||||
"mobile.select_team.join_open": "Open teams you can join",
|
||||
"mobile.select_team.no_teams": "There are no available teams for you to join.",
|
||||
|
|
|
|||
Loading…
Reference in a new issue