[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:
enahum 2018-03-07 14:48:31 +00:00 committed by Harrison Healey
parent 68fe35c74d
commit d0d9103857
28 changed files with 887 additions and 546 deletions

View file

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

View file

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

View file

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

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

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

View file

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

View file

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

View file

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

View file

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

View file

@ -181,3 +181,7 @@ const languages = {
export function getDisplayNameForLanguage(language) {
return languages[language.toLowerCase()] || '';
}
export function escapeRegex(text) {
return text.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&');
}

View file

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