mattermost-mobile/app/screens/permalink/permalink.js
Elias Nahum 9f238d5ef4
Post List & post components refactored (#5409)
* Update transform to make Android's post list scroll smooth

* set start since metric when appStarted is false

* Refactor Formatted components

* Downgrade RNN to 7.13.0 & patch XCDYouTube to allow video playback

* Refactor Post list and all related components

* review suggestion rename hour12 to isMilitaryTime

* feedback review use aliases

* feedback review deconstruct actions in markdown_link

* feedback review simplify if/else statement in combined_used_activity

* Simplify if statement for consecutive posts

* Specify npm version to build iOS on CI

* Refactor network_indicator

* render Icon in file gallery with transparent background

* Increase timeout to scroll to bottom when posting a new message

* fix: scroll when tapping on the new messages bar

* fix: dismiss all modals

* fix navigation tests

* Handle dismissAllModals for iOS to prevent blank screens

* Prevent modal from dismissing when showing the thread screen in the stack

* Update app/components/image_viewport.tsx

Co-authored-by: Miguel Alatzar <migbot@users.noreply.github.com>

* Update app/utils/post.ts

Co-authored-by: Miguel Alatzar <migbot@users.noreply.github.com>

* Apply suggestions from code review

Co-authored-by: Miguel Alatzar <migbot@users.noreply.github.com>

* fix: rename selector and prop

* Fix XCDYouTube patch

* Fix posting from a thread in the right channel

* do not render reply bar on the thread screen

* close previous permalink before showing a new one

* move XCDYouTube patch to ios/patches folder

* closePermalink directly instead of using an onClose prop

Co-authored-by: Miguel Alatzar <migbot@users.noreply.github.com>
Co-authored-by: Miguel Alatzar <this.migbot@gmail.com>
2021-06-03 11:12:15 -07:00

498 lines
17 KiB
JavaScript

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {Text, TouchableOpacity, View} from 'react-native';
import {intlShape} from 'react-intl';
import * as Animatable from 'react-native-animatable';
import {Navigation} from 'react-native-navigation';
import {
resetToChannel,
dismissModal,
dismissAllModals,
popToRoot,
} from '@actions/navigation';
import CompassIcon from '@components/compass_icon';
import FormattedText from '@components/formatted_text';
import Loading from '@components/loading';
import PostList from '@components/post_list';
import PostListRetry from '@components/post_list_retry';
import SafeAreaView from '@components/safe_area_view';
import {General} from '@mm-redux/constants';
import EventEmitter from '@mm-redux/utils/event_emitter';
import {privateChannelJoinPrompt} from '@utils/channels';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import {PERMALINK} from '@constants/screen';
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({
addUserToTeam: PropTypes.func.isRequired,
closePermalink: PropTypes.func.isRequired,
getPostsAround: PropTypes.func.isRequired,
getPostThread: PropTypes.func.isRequired,
getChannel: PropTypes.func.isRequired,
getTeamByName: PropTypes.func.isRequired,
handleSelectChannel: PropTypes.func.isRequired,
handleTeamChange: PropTypes.func.isRequired,
joinChannel: PropTypes.func.isRequired,
removeUserFromTeam: PropTypes.func.isRequired,
selectPost: PropTypes.func.isRequired,
}).isRequired,
channelId: PropTypes.string,
channelIsArchived: PropTypes.bool,
channelName: PropTypes.string,
channelTeamId: PropTypes.string,
currentTeamId: PropTypes.string.isRequired,
currentUserId: PropTypes.string.isRequired,
focusedPostId: PropTypes.string.isRequired,
isPermalink: PropTypes.bool,
myChannelMemberships: PropTypes.object.isRequired,
myTeamMemberships: PropTypes.object.isRequired,
postIds: PropTypes.array,
team: PropTypes.object,
teamName: PropTypes.string,
theme: PropTypes.object.isRequired,
error: PropTypes.string,
};
static defaultProps = {
postIds: [],
};
static contextTypes = {
intl: intlShape.isRequired,
};
constructor(props) {
super(props);
const {error, postIds} = props;
let loading = true;
if (postIds && postIds.length >= 10) {
loading = false;
}
this.state = {
title: '',
loading,
joinChannelPromptVisible: false,
error: error || '',
retry: false,
};
}
componentDidMount() {
this.navigationEventListener = Navigation.events().bindComponent(this);
this.mounted = true;
if (this.state.loading && this.props.focusedPostId) {
this.initialLoad = true;
this.loadPosts();
}
}
componentDidUpdate() {
if (this.state.loading && this.props.focusedPostId && !this.initialLoad) {
this.loadPosts();
}
}
componentWillUnmount() {
this.mounted = false;
}
navigationButtonPressed({buttonId}) {
if (buttonId === 'backPress') {
this.handleClose();
}
}
setViewRef = (ref) => {
this.viewRef = ref;
}
handleClose = () => {
const {actions} = this.props;
if (this.viewRef) {
this.mounted = false;
this.viewRef.zoomOut().then(() => {
actions.selectPost('');
dismissModal();
actions.closePermalink();
});
}
};
handlePress = () => {
if (this.viewRef) {
this.viewRef.growOut().then(() => {
this.jumpToChannel(this.props.channelId);
});
}
};
jumpToChannel = async (channelId) => {
if (channelId) {
const {actions, channelId: currentChannelId, channelTeamId, currentTeamId} = this.props;
const {closePermalink, handleSelectChannel, handleTeamChange} = actions;
actions.selectPost('');
await dismissAllModals();
await popToRoot();
if (channelId === currentChannelId) {
EventEmitter.emit('reset_channel');
} else {
const passProps = {
disableTermsModal: true,
};
resetToChannel(passProps);
}
closePermalink();
if (channelTeamId && currentTeamId !== channelTeamId) {
handleTeamChange(channelTeamId);
}
handleSelectChannel(channelId);
}
};
loadPosts = async () => {
const {intl} = this.context;
const {actions, channelId, currentUserId, focusedPostId, isPermalink, postIds} = this.props;
const {formatMessage} = intl;
let focusChannelId = channelId;
if (this.mounted && !this.initialLoad) {
this.setState({loading: false});
}
if (focusedPostId) {
const post = await actions.getPostThread(focusedPostId, false);
if (post.error && (!postIds || !postIds.length)) {
const error = post.error.message.toLowerCase() === 'network request failed';
const connectionError = post.error.details?.message?.toLowerCase() === 'could not connect to the server.';
if (this.mounted && isPermalink && !error && !connectionError) {
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: 'permalink.error.link_not_found',
defaultMessage: 'Link Not Found',
}),
});
} else if (this.mounted) {
this.setState({error: post.error.message, retry: true, loading: false});
}
return;
}
if (!focusChannelId) {
const focusedPost = post.data && post.data.posts ? post.data.posts[focusedPostId] : null;
focusChannelId = focusedPost ? focusedPost.channel_id : '';
}
if (focusChannelId) {
const {teamName} = this.props;
let {team} = this.props;
let joinedNewTeam = false;
// teamname is only passed for permalinks and not post previews in mentions, search & pinned posts
if (teamName) {
if (!team) {
const teamResponse = await actions.getTeamByName(teamName);
if (teamResponse.error) {
this.setState({error: teamResponse.error.message, loading: false});
return;
}
team = teamResponse.data;
}
if (!this.props.myTeamMemberships[team.id]) {
const teamJoinResponse = await actions.addUserToTeam(team.id, currentUserId);
if (teamJoinResponse.error) {
this.setState({error: teamJoinResponse.error.message, loading: false});
return;
}
joinedNewTeam = true;
}
}
if (!this.props.myChannelMemberships[focusChannelId]) {
const {error: channelError, data: channel} = await actions.getChannel(focusChannelId);
if (channelError) {
this.setState({error: channelError.message, loading: false});
} else {
if (channel.type === General.PRIVATE_CHANNEL) {
this.setState({joinChannelPromptVisible: true});
const {join} = await privateChannelJoinPrompt(channel, this.context.intl);
if (!join) {
if (joinedNewTeam) {
await actions.removeUserFromTeam(team.id, currentUserId);
}
this.handleClose();
return;
}
this.setState({joinChannelPromptVisible: false});
}
// Join Open/Private channel
const channelJoinResponse = await actions.joinChannel(currentUserId, channel.team_id, channel.id);
if (channelJoinResponse.error) {
this.setState({error: channelJoinResponse.error.message, loading: false});
return;
}
}
}
}
await actions.getPostsAround(focusChannelId, focusedPostId, 10);
if (this.initialLoad) {
this.initialLoad = false;
this.setState({loading: false});
}
}
};
retry = () => {
if (this.mounted) {
this.initialLoad = false;
this.setState({loading: true, error: null, retry: false});
}
};
archivedIcon = () => {
const style = getStyleSheet(this.props.theme);
let icon = null;
if (this.props.channelIsArchived) {
icon = (
<Text>
<CompassIcon
name='archive-outline'
style={[style.archiveIcon]}
/>
{' '}
</Text>
);
}
return icon;
};
render() {
const {channelName, currentUserId, focusedPostId, postIds, theme} = this.props;
const {error, joinChannelPromptVisible, loading, retry, 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 = joinChannelPromptVisible ? null : <Loading color={theme.centerChannelColor}/>;
} else {
postList = (
<PostList
testID='permalink.post_list'
highlightPostId={focusedPostId}
indicateNewMessages={false}
shouldRenderReplyButton={false}
postIds={postIds}
currentUserId={currentUserId}
lastViewedAt={0}
highlightPinnedOrFlagged={false}
location={PERMALINK}
/>
);
}
return (
<SafeAreaView
backgroundColor='transparent'
excludeHeader={true}
footerColor='transparent'
>
<View
testID='permalink.screen'
style={style.container}
>
<Animatable.View
ref={this.setViewRef}
animation='zoomIn'
duration={200}
delay={0}
style={style.wrapper}
useNativeDriver={true}
>
<View
style={style.header}
>
<TouchableOpacity
style={style.close}
onPress={this.handleClose}
>
<CompassIcon
name='close'
size={20}
color={theme.centerChannelColor}
/>
</TouchableOpacity>
<View style={style.titleContainer}>
<Text
ellipsizeMode='tail'
numberOfLines={1}
style={style.title}
>
{this.archivedIcon()}
{title || channelName}
</Text>
</View>
</View>
<View style={style.dividerContainer}>
<View style={style.divider}/>
</View>
<View style={[style.postList, error ? style.bottom : null]}>
{postList}
</View>
{!error && !loading &&
<TouchableOpacity
style={[style.footer, style.bottom]}
onPress={this.handlePress}
>
<FormattedText
testID='permalink.search.jump'
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: {
backgroundColor: theme.centerChannelBg,
borderRadius: 6,
flex: 1,
margin: 10,
opacity: 0,
},
header: {
alignItems: 'center',
borderTopLeftRadius: 6,
borderTopRightRadius: 6,
flexDirection: 'row',
height: 44,
paddingRight: 16,
width: '100%',
},
dividerContainer: {
backgroundColor: theme.centerChannelBg,
},
divider: {
backgroundColor: changeOpacity(theme.centerChannelColor, 0.2),
height: 1,
},
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: {
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,
},
archiveIcon: {
color: theme.centerChannelColor,
fontSize: 16,
paddingRight: 20,
},
};
});