Fix Posts cutoff (#2071)

* Remove PixelRatio from emoji

* Fix removeSubclippedViews in emoji picker

* Add system emojis to the store handler

* Pass element to whyDidYouUpdate util

* Fix post avoidable re-renders

* Fix emoji on Android

* Feedback review
This commit is contained in:
Elias Nahum 2018-09-04 11:53:15 -03:00 committed by GitHub
parent fd99e0489c
commit 82dc62dfc6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 69 additions and 124 deletions

View file

@ -14,13 +14,6 @@ import {
import CustomPropTypes from 'app/constants/custom_prop_types';
import ImageCacheManager from 'app/utils/image_cache_manager';
const scaleEmojiBasedOnDevice = (size) => {
if (Platform.OS === 'ios') {
return size * 1.1; // slightly larger emojis look better on ios
}
return size * PixelRatio.get();
};
export default class Emoji extends React.PureComponent {
static propTypes = {
@ -60,30 +53,28 @@ export default class Emoji extends React.PureComponent {
this.state = {
imageUrl: null,
originalWidth: 0,
originalHeight: 0,
};
}
componentWillMount() {
const {displayTextOnly, imageUrl} = this.props;
this.mounted = true;
if (!this.props.displayTextOnly && this.props.imageUrl) {
ImageCacheManager.cache(this.props.imageUrl, this.props.imageUrl, this.updateImageHeight);
if (!displayTextOnly && imageUrl) {
ImageCacheManager.cache(imageUrl, imageUrl, this.setImageUrl);
}
}
componentWillReceiveProps(nextProps) {
if (nextProps.emojiName !== this.props.emojiName) {
const {displayTextOnly, emojiName, imageUrl} = nextProps;
if (emojiName !== this.props.emojiName) {
this.setState({
imageUrl: null,
originalWidth: 0,
originalHeight: 0,
});
}
if (!nextProps.displayTextOnly && nextProps.imageUrl &&
nextProps.imageUrl !== this.props.imageUrl) {
ImageCacheManager.cache(nextProps.imageUrl, nextProps.imageUrl, this.updateImageHeight);
if (!displayTextOnly && imageUrl &&
imageUrl !== this.props.imageUrl) {
ImageCacheManager.cache(imageUrl, imageUrl, this.setImageUrl);
}
}
@ -91,21 +82,15 @@ export default class Emoji extends React.PureComponent {
this.mounted = false;
}
updateImageHeight = (imageUrl) => {
setImageUrl = (imageUrl) => {
let prefix = '';
if (Platform.OS === 'android') {
prefix = 'file://';
}
const uri = `${prefix}${imageUrl}`;
Image.getSize(uri, (originalWidth, originalHeight) => {
if (this.mounted) {
this.setState({
imageUrl: uri,
originalWidth,
originalHeight,
});
}
this.setState({
imageUrl: uri,
});
};
@ -122,26 +107,15 @@ export default class Emoji extends React.PureComponent {
if (!size && textStyle) {
const flatten = StyleSheet.flatten(textStyle);
fontSize = flatten.fontSize;
size = scaleEmojiBasedOnDevice(fontSize);
size = fontSize * (Platform.OS === 'android' ? PixelRatio.get() : 1);
}
if (displayTextOnly) {
return <Text style={textStyle}>{literal}</Text>;
}
let width = size;
let height = size;
let {originalHeight, originalWidth} = this.state;
originalHeight = scaleEmojiBasedOnDevice(originalHeight);
originalWidth = scaleEmojiBasedOnDevice(originalWidth);
if (originalHeight && originalWidth) {
if (originalWidth > originalHeight) {
height = (size * originalHeight) / originalWidth;
} else if (originalWidth < originalHeight) {
// This may cause text to reflow, but its impossible to add a horizontal margin
width = (size * originalWidth) / originalHeight;
}
}
const width = size;
const height = size;
let marginTop = 0;
if (textStyle) {

View file

@ -407,6 +407,7 @@ export default class EmojiPicker extends PureComponent {
renderItem={this.flatListRenderItem}
pageSize={10}
initialListSize={10}
removeClippedSubviews={true}
/>
);
} else {
@ -558,6 +559,7 @@ const getStyleSheetFromTheme = makeStyleSheetFromTheme((theme) => {
borderLeftColor: changeOpacity(theme.centerChannelColor, 0.2),
borderRightWidth: 1,
borderRightColor: changeOpacity(theme.centerChannelColor, 0.2),
overflow: 'hidden',
},
listView: {
backgroundColor: theme.centerChannelBg,

View file

@ -64,7 +64,7 @@ export default class EmojiPickerRow extends Component {
/>
</TouchableOpacity>
);
}
};
render() {
const {emojiGutter, items} = this.props;
@ -86,6 +86,7 @@ const styles = StyleSheet.create({
emoji: {
alignItems: 'center',
justifyContent: 'center',
overflow: 'hidden',
},
emojiLeft: {
marginLeft: 0,

View file

@ -8,7 +8,7 @@ import RNBottomSheet from 'react-native-bottom-sheet';
export default class OptionsContext extends PureComponent {
static propTypes = {
actions: PropTypes.array,
getPostActions: PropTypes.func,
cancelText: PropTypes.string,
children: PropTypes.node.isRequired,
onPress: PropTypes.func.isRequired,
@ -16,13 +16,13 @@ export default class OptionsContext extends PureComponent {
};
static defaultProps = {
actions: [],
getPostActions: () => [],
cancelText: 'Cancel',
};
show = (additionalAction) => {
const {actions, cancelText} = this.props;
const nextActions = [...actions];
const {getPostActions, cancelText} = this.props;
const nextActions = getPostActions();
if (additionalAction && !additionalAction.nativeEvent && additionalAction.text) {
const copyPostIndex = nextActions.findIndex((action) => action.copyPost);
nextActions.splice(copyPostIndex + 1, 0, additionalAction);
@ -45,11 +45,11 @@ export default class OptionsContext extends PureComponent {
};
handleHideUnderlay = () => {
this.props.toggleSelected(false, this.props.actions.length > 0);
this.props.toggleSelected(false, this.props.getPostActions().length > 0);
};
handleShowUnderlay = () => {
this.props.toggleSelected(true, this.props.actions.length > 0);
this.props.toggleSelected(true, this.props.getPostActions().length > 0);
};
render() {

View file

@ -8,14 +8,14 @@ import ToolTip from 'app/components/tooltip';
export default class OptionsContext extends PureComponent {
static propTypes = {
actions: PropTypes.array,
getPostActions: PropTypes.func,
children: PropTypes.node.isRequired,
onPress: PropTypes.func.isRequired,
toggleSelected: PropTypes.func.isRequired,
};
static defaultProps = {
actions: [],
getPostActions: () => [],
additionalActions: [],
};
@ -23,16 +23,10 @@ export default class OptionsContext extends PureComponent {
super(props);
this.state = {
actions: props.actions,
actions: props.getPostActions(),
};
}
componentWillReceiveProps(nextProps) {
if (this.props.actions !== nextProps.actions) {
this.setState({actions: nextProps.actions});
}
}
handleHideUnderlay = () => {
if (!this.isShowing) {
this.props.toggleSelected(false, false);
@ -45,11 +39,11 @@ export default class OptionsContext extends PureComponent {
handleHide = () => {
this.isShowing = false;
this.props.toggleSelected(false, this.props.actions.length > 0);
this.props.toggleSelected(false, this.props.getPostActions().length > 0);
};
handleShow = () => {
this.isShowing = this.props.actions.length > 0;
this.isShowing = this.props.getPostActions().length > 0;
this.props.toggleSelected(true, this.isShowing);
};
@ -59,12 +53,12 @@ export default class OptionsContext extends PureComponent {
}
this.setState({
actions: this.props.actions,
actions: this.props.getPostActions(),
});
};
show = (additionalAction) => {
const nextActions = [...this.props.actions];
const nextActions = this.props.getPostActions();
if (additionalAction && additionalAction.text && !additionalAction.nativeEvent) {
const copyPostIndex = nextActions.findIndex((action) => action.copyPost);
nextActions.splice(copyPostIndex + 1, 0, additionalAction);
@ -80,7 +74,7 @@ export default class OptionsContext extends PureComponent {
};
handlePress = () => {
this.props.toggleSelected(false, this.props.actions.length > 0);
this.props.toggleSelected(false, this.props.getPostActions().length > 0);
this.props.onPress();
};

View file

@ -5,6 +5,7 @@ import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import {createPost, deletePost, removePost} from 'mattermost-redux/actions/posts';
import {General, Posts} from 'mattermost-redux/constants';
import {getCurrentChannelId, isCurrentChannelReadOnly} from 'mattermost-redux/selectors/entities/channels';
import {getPost, makeGetCommentCountForPost} from 'mattermost-redux/selectors/entities/posts';
import {getCurrentUserId, getCurrentUserRoles} from 'mattermost-redux/selectors/entities/users';
@ -16,8 +17,6 @@ import {getConfig, getLicense} from 'mattermost-redux/selectors/entities/general
import {insertToDraft, setPostTooltipVisible} from 'app/actions/views/channel';
import {addReaction} from 'app/actions/views/emoji';
import {getDimensions} from 'app/selectors/device';
import {Posts} from 'mattermost-redux/constants';
import Post from './post';
@ -78,33 +77,36 @@ function makeMapStateToProps() {
}
}
}
const {deviceWidth} = getDimensions(state);
const isAdmin = checkIsAdmin(roles);
const isSystemAdmin = checkIsSystemAdmin(roles);
let canDelete = false;
let canEdit = false;
let canEditUntil = -1;
if (post) {
canDelete = canDeletePost(state, config, license, currentTeamId, currentChannelId, currentUserId, post, isAdmin, isSystemAdmin);
canEdit = canEditPost(state, config, license, currentTeamId, currentChannelId, currentUserId, post);
if (canEdit && license.IsLicensed === 'true' &&
(config.AllowEditPost === General.ALLOW_EDIT_POST_TIME_LIMIT || (config.PostEditTimeLimit !== -1 && config.PostEditTimeLimit !== '-1'))
) {
canEditUntil = post.create_at + (config.PostEditTimeLimit * 1000);
}
}
return {
channelIsReadOnly: isCurrentChannelReadOnly(state),
config,
canDelete,
canEdit,
canEditUntil,
currentTeamUrl: getCurrentTeamUrl(state),
currentUserId,
deviceWidth,
post,
isFirstReply,
isLastReply,
consecutivePost: isConsecutivePost(state, ownProps),
hasComments: getCommentCountForPost(state, {post}) > 0,
commentedOnPost,
license,
theme: getTheme(state),
isFlagged: isPostFlagged(post.id, myPreferences),
};

View file

@ -25,9 +25,8 @@ import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
import {getToolTipVisible} from 'app/utils/tooltip';
import {Posts} from 'mattermost-redux/constants';
import DelayedAction from 'mattermost-redux/utils/delayed_action';
import EventEmitter from 'mattermost-redux/utils/event_emitter';
import {editDisable, isPostEphemeral, isPostPendingOrFailed, isSystemMessage} from 'mattermost-redux/utils/post_utils';
import {isPostEphemeral, isPostPendingOrFailed, isSystemMessage} from 'mattermost-redux/utils/post_utils';
import Config from 'assets/config';
@ -41,10 +40,8 @@ export default class Post extends PureComponent {
removePost: PropTypes.func.isRequired,
}).isRequired,
channelIsReadOnly: PropTypes.bool,
config: PropTypes.object.isRequired,
currentTeamUrl: PropTypes.string.isRequired,
currentUserId: PropTypes.string.isRequired,
deviceWidth: PropTypes.number.isRequired,
highlight: PropTypes.bool,
style: ViewPropTypes.style,
post: PropTypes.object,
@ -56,10 +53,10 @@ export default class Post extends PureComponent {
hasComments: PropTypes.bool,
isSearchResult: PropTypes.bool,
commentedOnPost: PropTypes.object,
license: PropTypes.object.isRequired,
managedConfig: PropTypes.object.isRequired,
navigator: PropTypes.object,
canEdit: PropTypes.bool.isRequired,
canEditUntil: PropTypes.number.isRequired,
canDelete: PropTypes.bool.isRequired,
onPermalinkPress: PropTypes.func,
shouldRenderReplyButton: PropTypes.bool,
@ -83,37 +80,6 @@ export default class Post extends PureComponent {
intl: intlShape.isRequired,
};
constructor(props) {
super(props);
const {config, license, currentUserId, post} = props;
this.editDisableAction = new DelayedAction(this.handleEditDisable);
if (post) {
editDisable(config, license, currentUserId, post, this.editDisableAction);
}
this.state = {
canEdit: this.props.canEdit,
};
}
componentWillReceiveProps(nextProps) {
if (nextProps.config !== this.props.config ||
nextProps.license !== this.props.license ||
nextProps.currentUserId !== this.props.currentUserId ||
nextProps.post !== this.props.post) {
const {config, license, currentUserId, post} = nextProps;
editDisable(config, license, currentUserId, post, this.editDisableAction);
this.setState({
canEdit: nextProps.canEdit,
});
}
}
componentWillUnmount() {
this.editDisableAction.cancel();
}
goToUserProfile = () => {
const {intl} = this.context;
const {navigator, post, theme} = this.props;
@ -165,7 +131,6 @@ export default class Post extends PureComponent {
text: formatMessage({id: 'post_info.del', defaultMessage: 'Delete'}),
style: 'destructive',
onPress: () => {
this.editDisableAction.cancel();
actions.deletePost(post);
if (post.user_id === currentUserId) {
actions.removePost(post);
@ -479,7 +444,8 @@ export default class Post extends PureComponent {
<PostBody
ref={'postBody'}
canDelete={this.props.canDelete}
canEdit={this.state.canEdit}
canEdit={this.props.canEdit}
canEditUntil={this.props.canEditUntil}
highlight={highlight}
channelIsReadOnly={channelIsReadOnly}
isSearchResult={isSearchResult}

View file

@ -40,6 +40,7 @@ export default class PostBody extends PureComponent {
canAddReaction: PropTypes.bool,
canDelete: PropTypes.bool,
canEdit: PropTypes.bool,
canEditUntil: PropTypes.number.isRequired,
channelIsReadOnly: PropTypes.bool.isRequired,
fileIds: PropTypes.array,
hasBeenDeleted: PropTypes.bool,
@ -119,6 +120,7 @@ export default class PostBody extends PureComponent {
const {formatMessage} = this.context.intl;
const {
canEdit,
canEditUntil,
canDelete,
canAddReaction,
channelIsReadOnly,
@ -168,7 +170,7 @@ export default class PostBody extends PureComponent {
}
}
if (canEdit) {
if (canEdit && (canEditUntil === -1 || canEditUntil > Date.now())) {
actions.push({text: formatMessage({id: 'post_info.edit', defaultMessage: 'Edit'}), onPress: onPostEdit});
}
@ -435,7 +437,7 @@ export default class PostBody extends PureComponent {
body = (
<View style={style.messageBody}>
<OptionsContext
actions={this.getPostActions()}
getPostActions={this.getPostActions}
ref='options'
onPress={onPress}
toggleSelected={toggleSelected}

View file

@ -215,7 +215,7 @@ export default class PostHeader extends PureComponent {
}
return (
<View>
<React.Fragment>
<View style={[style.postInfoContainer, (isPendingOrFailedPost && style.pendingPost)]}>
<View style={{flexDirection: 'row', flex: 1}}>
{this.getDisplayName(style)}
@ -253,7 +253,7 @@ export default class PostHeader extends PureComponent {
{this.renderCommentedOnMessage(style)}
</View>
}
</View>
</React.Fragment>
);
}
}

View file

@ -234,9 +234,11 @@ export default class PostList extends PureComponent {
nextConfig = await mattermostManaged.getLocalConfig();
}
this.setState({
managedConfig: nextConfig,
});
if (Object.keys(nextConfig).length) {
this.setState({
managedConfig: nextConfig,
});
}
};
keyExtractor = (item) => {

View file

@ -28,9 +28,10 @@ function withLayout(WrappedComponent) {
};
render() {
const {index, onLayoutCalled, shouldCallOnLayout, ...otherProps} = this.props; //eslint-disable-line no-unused-vars
return (
<View onLayout={this.onLayout}>
<WrappedComponent {...this.props}/>
<WrappedComponent {...otherProps}/>
</View>
);
}

View file

@ -11,6 +11,7 @@ import {
import DeviceInfo from 'react-native-device-info';
import {setSystemEmojis} from 'mattermost-redux/actions/emojis';
import {Client4} from 'mattermost-redux/client';
import EventEmitter from 'mattermost-redux/utils/event_emitter';
@ -56,15 +57,14 @@ const lazyLoadReplyPushNotifications = () => {
*/
export default class Entry extends PureComponent {
static propTypes = {
config: PropTypes.object,
theme: PropTypes.object,
navigator: PropTypes.object,
isLandscape: PropTypes.bool,
hydrationComplete: PropTypes.bool,
enableTimezone: PropTypes.bool,
deviceTimezone: PropTypes.string,
initializeModules: PropTypes.func.isRequired,
actions: PropTypes.shape({
autoUpdateTimezone: PropTypes.func.isRequired,
setDeviceToken: PropTypes.func.isRequired,
}).isRequired,
};
@ -132,6 +132,7 @@ export default class Entry extends PureComponent {
this.setAppCredentials();
this.setStartupThemes();
this.handleNotification();
this.loadSystemEmojis();
if (Platform.OS === 'android') {
this.launchForAndroid();
@ -226,6 +227,11 @@ export default class Entry extends PureComponent {
}
};
loadSystemEmojis = () => {
const EmojiIndicesByAlias = require('app/utils/emojis').EmojiIndicesByAlias;
setSystemEmojis(EmojiIndicesByAlias);
};
renderLogin = () => {
const SelectServer = lazyLoadSelectServer();
const props = {

View file

@ -6,7 +6,6 @@ import {connect} from 'react-redux';
import {setDeviceToken} from 'mattermost-redux/actions/general';
import {autoUpdateTimezone} from 'mattermost-redux/actions/timezone';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {getConfig} from 'mattermost-redux/selectors/entities/general';
import {isLandscape} from 'app/selectors/device';
import {getDeviceTimezone, isTimezoneEnabled} from 'app/utils/timezone';
@ -16,16 +15,12 @@ const lazyLoadEntry = () => {
};
function mapStateToProps(state) {
const config = getConfig(state);
const enableTimezone = isTimezoneEnabled(state);
const deviceTimezone = getDeviceTimezone();
return {
config,
theme: getTheme(state),
isLandscape: isLandscape(state),
hydrationComplete: state.views.root.hydrationComplete,
enableTimezone,
deviceTimezone,
};
@ -34,8 +29,8 @@ function mapStateToProps(state) {
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
setDeviceToken,
autoUpdateTimezone,
setDeviceToken,
}, dispatch),
};
}

View file

@ -40,10 +40,10 @@ function deepDiff(o1, o2, p) {
}
}
function whyDidYouUpdate(prevProps, prevState) {
function whyDidYouUpdate(theClass, prevProps, prevState) {
deepDiff({props: prevProps, state: prevState},
{props: this.props, state: this.state},
this.constructor.name);
{props: theClass.props, state: theClass.state},
theClass.constructor.name);
}
export default whyDidYouUpdate;