parent
c01e8080f0
commit
a11aad3a5a
54 changed files with 1084 additions and 1043 deletions
|
|
@ -10,7 +10,7 @@ import {reEmoji, reEmoticon, reMain} from '@constants/emoji';
|
|||
import {getEmoticonName} from '@utils/emoji_utils';
|
||||
|
||||
export default function ButtonBindingText({message, style}: {message: string; style: StyleProp<any>}) {
|
||||
const components = [] as JSX.Element[];
|
||||
const components = [] as React.ReactNode[];
|
||||
|
||||
let text = message;
|
||||
while (text) {
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ export default function EmbeddedSubBindings(props: Props) {
|
|||
return null;
|
||||
}
|
||||
|
||||
const content = [] as JSX.Element[];
|
||||
const content = [] as React.ReactNode[];
|
||||
|
||||
bindings.forEach((binding) => {
|
||||
if (!binding.app_id || !binding.call) {
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ export default function EmbeddedBindings(props: Props) {
|
|||
theme,
|
||||
textStyles,
|
||||
} = props;
|
||||
const content = [] as JSX.Element[];
|
||||
const content = [] as React.ReactNode[];
|
||||
|
||||
embeds.forEach((embed, i) => {
|
||||
content.push(
|
||||
|
|
|
|||
|
|
@ -1,117 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
Platform,
|
||||
StyleSheet,
|
||||
Text,
|
||||
} from 'react-native';
|
||||
import FastImage from 'react-native-fast-image';
|
||||
|
||||
export default class Emoji extends React.PureComponent {
|
||||
static propTypes = {
|
||||
|
||||
/*
|
||||
* Emoji text name.
|
||||
*/
|
||||
emojiName: PropTypes.string.isRequired,
|
||||
|
||||
/*
|
||||
* Image URL for the emoji.
|
||||
*/
|
||||
imageUrl: PropTypes.string.isRequired,
|
||||
|
||||
/*
|
||||
* Set if this is a custom emoji.
|
||||
*/
|
||||
isCustomEmoji: PropTypes.bool.isRequired,
|
||||
|
||||
/*
|
||||
* Set to render only the text and no image.
|
||||
*/
|
||||
displayTextOnly: PropTypes.bool,
|
||||
literal: PropTypes.string,
|
||||
size: PropTypes.number,
|
||||
textStyle: PropTypes.oneOfType([PropTypes.object, PropTypes.number, PropTypes.array]),
|
||||
unicode: PropTypes.string,
|
||||
customEmojiStyle: PropTypes.oneOfType([PropTypes.object, PropTypes.number, PropTypes.array]),
|
||||
testID: PropTypes.string,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
customEmojis: new Map(),
|
||||
literal: '',
|
||||
imageUrl: '',
|
||||
isCustomEmoji: false,
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
customEmojiStyle,
|
||||
displayTextOnly,
|
||||
imageUrl,
|
||||
literal,
|
||||
unicode,
|
||||
testID,
|
||||
textStyle,
|
||||
} = this.props;
|
||||
|
||||
let size = this.props.size;
|
||||
let fontSize = size;
|
||||
if (!size && textStyle) {
|
||||
const flatten = StyleSheet.flatten(textStyle);
|
||||
fontSize = flatten.fontSize;
|
||||
size = fontSize;
|
||||
}
|
||||
|
||||
if (displayTextOnly) {
|
||||
return (
|
||||
<Text
|
||||
style={textStyle}
|
||||
testID={testID}
|
||||
>
|
||||
{literal}
|
||||
</Text>);
|
||||
}
|
||||
|
||||
const width = size;
|
||||
const height = size;
|
||||
|
||||
// Android can't change the size of an image after its first render, so
|
||||
// force a new image to be rendered when the size changes
|
||||
const key = Platform.OS === 'android' ? (`${imageUrl}-${height}-${width}`) : null;
|
||||
|
||||
if (unicode && !imageUrl) {
|
||||
const codeArray = unicode.split('-');
|
||||
const code = codeArray.reduce((acc, c) => {
|
||||
return acc + String.fromCodePoint(parseInt(c, 16));
|
||||
}, '');
|
||||
|
||||
return (
|
||||
<Text
|
||||
style={[textStyle, {fontSize: size}]}
|
||||
testID={testID}
|
||||
>
|
||||
{code}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
if (!imageUrl) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<FastImage
|
||||
key={key}
|
||||
style={[customEmojiStyle, {width, height}]}
|
||||
source={{uri: imageUrl}}
|
||||
onError={this.onError}
|
||||
resizeMode={FastImage.resizeMode.contain}
|
||||
testID={testID}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
116
app/components/emoji/emoji.tsx
Normal file
116
app/components/emoji/emoji.tsx
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {
|
||||
Platform,
|
||||
StyleProp,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TextStyle,
|
||||
} from 'react-native';
|
||||
import FastImage, {ImageStyle} from 'react-native-fast-image';
|
||||
|
||||
type Props = {
|
||||
|
||||
/*
|
||||
* Emoji text name.
|
||||
*/
|
||||
emojiName: string;
|
||||
|
||||
/*
|
||||
* Image URL for the emoji.
|
||||
*/
|
||||
imageUrl: string;
|
||||
|
||||
/*
|
||||
* Set if this is a custom emoji.
|
||||
*/
|
||||
isCustomEmoji: boolean;
|
||||
|
||||
/*
|
||||
* Set to render only the text and no image.
|
||||
*/
|
||||
displayTextOnly?: boolean;
|
||||
literal?: string;
|
||||
size?: number;
|
||||
textStyle?: StyleProp<TextStyle>;
|
||||
unicode?: string;
|
||||
customEmojiStyle?: StyleProp<ImageStyle>;
|
||||
testID?: string;
|
||||
}
|
||||
|
||||
const Emoji: React.FC<Props> = (props: Props) => {
|
||||
const {
|
||||
customEmojiStyle,
|
||||
displayTextOnly,
|
||||
imageUrl,
|
||||
literal,
|
||||
unicode,
|
||||
testID,
|
||||
textStyle,
|
||||
} = props;
|
||||
|
||||
let size = props.size;
|
||||
let fontSize = size;
|
||||
if (!size && textStyle) {
|
||||
const flatten = StyleSheet.flatten(textStyle);
|
||||
fontSize = flatten.fontSize;
|
||||
size = fontSize;
|
||||
}
|
||||
|
||||
if (displayTextOnly) {
|
||||
return (
|
||||
<Text
|
||||
style={textStyle}
|
||||
testID={testID}
|
||||
>
|
||||
{literal}
|
||||
</Text>);
|
||||
}
|
||||
|
||||
const width = size;
|
||||
const height = size;
|
||||
|
||||
if (unicode && !imageUrl) {
|
||||
const codeArray = unicode.split('-');
|
||||
const code = codeArray.reduce((acc, c) => {
|
||||
return acc + String.fromCodePoint(parseInt(c, 16));
|
||||
}, '');
|
||||
|
||||
return (
|
||||
<Text
|
||||
style={[textStyle, {fontSize: size}]}
|
||||
testID={testID}
|
||||
>
|
||||
{code}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
if (!imageUrl) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Android can't change the size of an image after its first render, so
|
||||
// force a new image to be rendered when the size changes
|
||||
const key = Platform.OS === 'android' ? (`${imageUrl}-${height}-${width}`) : null;
|
||||
|
||||
return (
|
||||
<FastImage
|
||||
key={key}
|
||||
style={[customEmojiStyle, {width, height}]}
|
||||
source={{uri: imageUrl}}
|
||||
resizeMode={FastImage.resizeMode.contain}
|
||||
testID={testID}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
Emoji.defaultProps = {
|
||||
literal: '',
|
||||
imageUrl: '',
|
||||
isCustomEmoji: false,
|
||||
};
|
||||
|
||||
export default Emoji;
|
||||
|
|
@ -8,12 +8,17 @@ import {getCurrentUserId} from '@mm-redux/selectors/entities/users';
|
|||
import {getConfig} from '@mm-redux/selectors/entities/general';
|
||||
import {Client4} from '@client/rest';
|
||||
import {isMinimumServerVersion} from '@mm-redux/utils/helpers';
|
||||
import {GlobalState} from '@mm-redux/types/store';
|
||||
|
||||
import {BuiltInEmojis, EmojiIndicesByAlias, Emojis} from 'app/utils/emojis';
|
||||
import {BuiltInEmojis, EmojiIndicesByAlias, Emojis} from '@utils/emojis';
|
||||
|
||||
import Emoji from './emoji';
|
||||
|
||||
function mapStateToProps(state, ownProps) {
|
||||
type OwnProps = {
|
||||
emojiName: string;
|
||||
}
|
||||
|
||||
function mapStateToProps(state: GlobalState, ownProps: OwnProps) {
|
||||
const config = getConfig(state);
|
||||
const emojiName = ownProps.emojiName;
|
||||
const customEmojis = getCustomEmojisByName(state);
|
||||
|
|
@ -24,7 +29,7 @@ function mapStateToProps(state, ownProps) {
|
|||
let isCustomEmoji = false;
|
||||
let displayTextOnly = false;
|
||||
if (EmojiIndicesByAlias.has(emojiName) || BuiltInEmojis.includes(emojiName)) {
|
||||
const emoji = Emojis[EmojiIndicesByAlias.get(emojiName)];
|
||||
const emoji = Emojis[EmojiIndicesByAlias.get(emojiName)!];
|
||||
unicode = emoji.filename;
|
||||
if (BuiltInEmojis.includes(emojiName)) {
|
||||
if (serverUrl) {
|
||||
|
|
@ -35,7 +40,7 @@ function mapStateToProps(state, ownProps) {
|
|||
}
|
||||
} else if (customEmojis.has(emojiName) && serverUrl) {
|
||||
const emoji = customEmojis.get(emojiName);
|
||||
imageUrl = Client4.getCustomEmojiImageUrl(emoji.id);
|
||||
imageUrl = Client4.getCustomEmojiImageUrl(emoji!.id);
|
||||
isCustomEmoji = true;
|
||||
} else {
|
||||
displayTextOnly = state.entities.emojis.nonExistentEmoji.has(emojiName) ||
|
||||
|
|
@ -10,7 +10,7 @@ import {
|
|||
} from 'react-native';
|
||||
import shallowEqual from 'shallow-equals';
|
||||
|
||||
import Emoji from 'app/components/emoji';
|
||||
import Emoji from '@components/emoji';
|
||||
|
||||
export default class EmojiPickerRow extends Component {
|
||||
static propTypes = {
|
||||
|
|
|
|||
|
|
@ -13,10 +13,10 @@ import {
|
|||
|
||||
import * as Utils from '@mm-redux/utils/file_utils';
|
||||
|
||||
import TouchableWithFeedback from 'app/components/touchable_with_feedback';
|
||||
import {isDocument, isImage} from 'app/utils/file';
|
||||
import {calculateDimensions} from 'app/utils/images';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
import TouchableWithFeedback from '@components/touchable_with_feedback';
|
||||
import {isDocument, isImage} from '@utils/file';
|
||||
import {calculateDimensions} from '@utils/images';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
import FileAttachmentDocument from './file_attachment_document';
|
||||
import FileAttachmentIcon from './file_attachment_icon';
|
||||
|
|
|
|||
|
|
@ -11,13 +11,13 @@ import {
|
|||
View,
|
||||
} from 'react-native';
|
||||
|
||||
import AtMention from 'app/components/at_mention';
|
||||
import ChannelLink from 'app/components/channel_link';
|
||||
import Emoji from 'app/components/emoji';
|
||||
import FormattedText from 'app/components/formatted_text';
|
||||
import Hashtag from 'app/components/markdown/hashtag';
|
||||
import {blendColors, concatStyles, makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
import {getScheme} from 'app/utils/url';
|
||||
import AtMention from '@components/at_mention';
|
||||
import ChannelLink from '@components/channel_link';
|
||||
import Emoji from '@components/emoji';
|
||||
import FormattedText from '@components/formatted_text';
|
||||
import Hashtag from '@components/markdown/hashtag';
|
||||
import {blendColors, concatStyles, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {getScheme} from '@utils/url';
|
||||
|
||||
import MarkdownBlockQuote from './markdown_block_quote';
|
||||
import MarkdownCodeBlock from './markdown_code_block';
|
||||
|
|
|
|||
|
|
@ -7,9 +7,9 @@ import React, {PureComponent} from 'react';
|
|||
import {Platform, Text, View} from 'react-native';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import Emoji from 'app/components/emoji';
|
||||
import FormattedText from 'app/components/formatted_text';
|
||||
import {blendColors, concatStyles, makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
import Emoji from '@components/emoji';
|
||||
import FormattedText from '@components/formatted_text';
|
||||
import {blendColors, concatStyles, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
export default class MarkdownEmoji extends PureComponent {
|
||||
static propTypes = {
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import {calculateDimensions, isGifTooLarge, openGalleryAtIndex} from '@utils/ima
|
|||
import {generateId} from '@utils/file';
|
||||
|
||||
import type {PostImage} from '@mm-redux/types/posts';
|
||||
import {FileInfo} from '@mm-redux/types/files';
|
||||
|
||||
type MarkdownTableImageProps = {
|
||||
disable: boolean;
|
||||
|
|
@ -78,8 +79,11 @@ const MarkTableImage = ({disable, imagesMetadata, postId, serverURL, source}: Ma
|
|||
return;
|
||||
}
|
||||
|
||||
const files = [getFileInfo()];
|
||||
openGalleryAtIndex(0, files);
|
||||
const file = getFileInfo() as FileInfo;
|
||||
if (!file) {
|
||||
return;
|
||||
}
|
||||
openGalleryAtIndex(0, [file]);
|
||||
}, []);
|
||||
|
||||
const onLoadFailed = useCallback(() => {
|
||||
|
|
|
|||
|
|
@ -27,7 +27,9 @@ exports[`AttachmentFooter it matches snapshot when both footer and footer_icon a
|
|||
}
|
||||
/>
|
||||
<Text
|
||||
ellipsizeMode="tail"
|
||||
key="footer_text"
|
||||
numberOfLines={1}
|
||||
style={
|
||||
Object {
|
||||
"color": "rgba(61,60,64,0.5)",
|
||||
|
|
@ -51,7 +53,9 @@ exports[`AttachmentFooter it matches snapshot when footer text is provided 1`] =
|
|||
}
|
||||
>
|
||||
<Text
|
||||
ellipsizeMode="tail"
|
||||
key="footer_text"
|
||||
numberOfLines={1}
|
||||
style={
|
||||
Object {
|
||||
"color": "rgba(61,60,64,0.5)",
|
||||
|
|
@ -67,43 +71,3 @@ exports[`AttachmentFooter it matches snapshot when footer text is provided 1`] =
|
|||
exports[`AttachmentFooter it matches snapshot when no footer is provided 1`] = `""`;
|
||||
|
||||
exports[`AttachmentFooter it matches snapshot when only the footer icon is provided 1`] = `""`;
|
||||
|
||||
exports[`AttachmentFooter it matches snapshot when the footer is longer than the maximum length 1`] = `
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"flex": 1,
|
||||
"flexDirection": "row",
|
||||
"marginTop": 5,
|
||||
}
|
||||
}
|
||||
>
|
||||
<FastImage
|
||||
key="footer_icon"
|
||||
source={
|
||||
Object {
|
||||
"uri": "https://images.com/image.png",
|
||||
}
|
||||
}
|
||||
style={
|
||||
Object {
|
||||
"height": 12,
|
||||
"marginRight": 5,
|
||||
"marginTop": 1,
|
||||
"width": 12,
|
||||
}
|
||||
}
|
||||
/>
|
||||
<Text
|
||||
key="footer_text"
|
||||
style={
|
||||
Object {
|
||||
"color": "rgba(61,60,64,0.5)",
|
||||
"fontSize": 11,
|
||||
}
|
||||
}
|
||||
>
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa…
|
||||
</Text>
|
||||
</View>
|
||||
`;
|
||||
|
|
@ -3,9 +3,11 @@
|
|||
|
||||
import React from 'react';
|
||||
import {shallow} from 'enzyme';
|
||||
|
||||
import ActionButton from './action_button';
|
||||
import {changeOpacity} from 'app/utils/theme';
|
||||
import {getStatusColors} from 'app/utils/message_attachment_colors';
|
||||
|
||||
import {changeOpacity} from '@utils/theme';
|
||||
import {getStatusColors} from '@utils/message_attachment_colors';
|
||||
|
||||
import Preferences from '@mm-redux/constants/preferences';
|
||||
|
||||
|
|
@ -106,7 +108,6 @@ describe('ActionButton', () => {
|
|||
cookie: '',
|
||||
name: buttonConfig.name,
|
||||
postId: buttonConfig.id,
|
||||
buttonColor: buttonConfig.style,
|
||||
theme: Preferences.THEMES.default,
|
||||
actions: {
|
||||
doPostActionWithCookie: jest.fn(),
|
||||
|
|
@ -2,31 +2,33 @@
|
|||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Button from 'react-native-button';
|
||||
|
||||
import {preventDoubleTap} from 'app/utils/tap';
|
||||
import {makeStyleSheetFromTheme, changeOpacity} from 'app/utils/theme';
|
||||
import {getStatusColors} from 'app/utils/message_attachment_colors';
|
||||
import ActionButtonText from './action_button_text';
|
||||
|
||||
export default class ActionButton extends PureComponent {
|
||||
static propTypes = {
|
||||
actions: PropTypes.shape({
|
||||
doPostActionWithCookie: PropTypes.func.isRequired,
|
||||
}).isRequired,
|
||||
id: PropTypes.string.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
postId: PropTypes.string.isRequired,
|
||||
theme: PropTypes.object.isRequired,
|
||||
cookie: PropTypes.string,
|
||||
disabled: PropTypes.bool,
|
||||
buttonColor: PropTypes.string,
|
||||
};
|
||||
import {preventDoubleTap} from '@utils/tap';
|
||||
import {makeStyleSheetFromTheme, changeOpacity} from '@utils/theme';
|
||||
import {getStatusColors} from '@utils/message_attachment_colors';
|
||||
|
||||
import {Theme} from '@mm-redux/types/preferences';
|
||||
import {ActionResult} from '@mm-redux/types/actions';
|
||||
|
||||
type Props = {
|
||||
actions: {
|
||||
doPostActionWithCookie: (postId: string, actionId: string, actionCookie: string, selectedOption?: string) => Promise<ActionResult>;
|
||||
};
|
||||
id: string;
|
||||
name: string;
|
||||
postId: string;
|
||||
theme: Theme,
|
||||
cookie?: string,
|
||||
disabled?: boolean,
|
||||
buttonColor?: string,
|
||||
}
|
||||
export default class ActionButton extends PureComponent<Props> {
|
||||
handleActionPress = preventDoubleTap(() => {
|
||||
const {actions, id, postId, cookie} = this.props;
|
||||
actions.doPostActionWithCookie(postId, id, cookie);
|
||||
actions.doPostActionWithCookie(postId, id, cookie || '');
|
||||
}, 4000);
|
||||
|
||||
render() {
|
||||
|
|
@ -58,7 +60,7 @@ export default class ActionButton extends PureComponent {
|
|||
}
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
const STATUS_COLORS = getStatusColors(theme);
|
||||
return {
|
||||
button: {
|
||||
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
import React from 'react';
|
||||
import {shallow} from 'enzyme';
|
||||
|
||||
import ActionButtonText from './action_button_text';
|
||||
|
||||
describe('ActionButtonText emojis', () => {
|
||||
|
|
@ -3,13 +3,14 @@
|
|||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Text, View, StyleSheet} from 'react-native';
|
||||
import Emoji from 'app/components/emoji';
|
||||
import {getEmoticonName} from 'app/utils/emoji_utils';
|
||||
import {reEmoji, reEmoticon, reMain} from 'app/constants/emoji';
|
||||
import {Text, View, StyleSheet, StyleProp, TextStyle} from 'react-native';
|
||||
|
||||
export default function ActionButtonText({message, style}) {
|
||||
const components = [];
|
||||
import Emoji from '@components/emoji';
|
||||
import {reEmoji, reEmoticon, reMain} from '@constants/emoji';
|
||||
import {getEmoticonName} from '@utils/emoji_utils';
|
||||
|
||||
export default function ActionButtonText({message, style}: {message: string; style: StyleProp<TextStyle>}) {
|
||||
const components = [] as React.ReactNode[];
|
||||
|
||||
let text = message;
|
||||
while (text) {
|
||||
|
|
@ -49,6 +50,9 @@ export default function ActionButtonText({message, style}) {
|
|||
// This is plain text, so capture as much text as possible until we hit the next possible emoji. Note that
|
||||
// reMain always captures at least one character, so text will always be getting shorter
|
||||
match = text.match(reMain);
|
||||
if (!match) {
|
||||
continue;
|
||||
}
|
||||
|
||||
components.push(
|
||||
<Text
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
// 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 {doPostActionWithCookie} from '@mm-redux/actions/posts';
|
||||
import {getTheme} from '@mm-redux/selectors/entities/preferences';
|
||||
|
||||
import ActionButton from './action_button';
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
theme: getTheme(state),
|
||||
};
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators({
|
||||
doPostActionWithCookie,
|
||||
}, dispatch),
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ActionButton);
|
||||
32
app/components/message_attachments/action_button/index.ts
Normal file
32
app/components/message_attachments/action_button/index.ts
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {ActionCreatorsMapObject, bindActionCreators, Dispatch} from 'redux';
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import ActionButton from './action_button';
|
||||
|
||||
import {doPostActionWithCookie} from '@mm-redux/actions/posts';
|
||||
import {getTheme} from '@mm-redux/selectors/entities/preferences';
|
||||
import {GlobalState} from '@mm-redux/types/store';
|
||||
import {ActionFunc, ActionResult} from '@mm-redux/types/actions';
|
||||
|
||||
function mapStateToProps(state: GlobalState) {
|
||||
return {
|
||||
theme: getTheme(state),
|
||||
};
|
||||
}
|
||||
|
||||
type Actions = {
|
||||
doPostActionWithCookie: (postId: string, actionId: string, actionCookie: string, selectedOption?: string | undefined) => Promise<ActionResult>;
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch: Dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators<ActionCreatorsMapObject<ActionFunc>, Actions>({
|
||||
doPostActionWithCookie,
|
||||
}, dispatch),
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ActionButton);
|
||||
|
|
@ -1,81 +0,0 @@
|
|||
// 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 AutocompleteSelector from 'app/components/autocomplete_selector';
|
||||
|
||||
export default class ActionMenu extends PureComponent {
|
||||
static propTypes = {
|
||||
actions: PropTypes.shape({
|
||||
selectAttachmentMenuAction: PropTypes.func.isRequired,
|
||||
}).isRequired,
|
||||
id: PropTypes.string.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
dataSource: PropTypes.string,
|
||||
defaultOption: PropTypes.string,
|
||||
options: PropTypes.arrayOf(PropTypes.object),
|
||||
postId: PropTypes.string.isRequired,
|
||||
selected: PropTypes.object,
|
||||
disabled: PropTypes.bool,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
let selected;
|
||||
if (props.defaultOption && props.options) {
|
||||
selected = props.options.find((option) => option.value === props.defaultOption);
|
||||
}
|
||||
|
||||
this.state = {
|
||||
selected,
|
||||
};
|
||||
}
|
||||
|
||||
static getDerivedStateFromProps(props, state) {
|
||||
if (props.selected && props.selected !== state.selected) {
|
||||
return {
|
||||
selected: props.selected,
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
handleSelect = (selected) => {
|
||||
if (!selected) {
|
||||
return;
|
||||
}
|
||||
|
||||
const {
|
||||
actions,
|
||||
id,
|
||||
postId,
|
||||
} = this.props;
|
||||
|
||||
actions.selectAttachmentMenuAction(postId, id, selected.text, selected.value);
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
name,
|
||||
dataSource,
|
||||
options,
|
||||
disabled,
|
||||
} = this.props;
|
||||
const {selected} = this.state;
|
||||
|
||||
return (
|
||||
<AutocompleteSelector
|
||||
placeholder={name}
|
||||
dataSource={dataSource}
|
||||
options={options}
|
||||
selected={selected}
|
||||
onSelected={this.handleSelect}
|
||||
disabled={disabled}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -29,7 +29,7 @@ describe('ActionMenu', () => {
|
|||
test('should start with nothing selected when no default is selected', () => {
|
||||
const wrapper = shallow(<ActionMenu {...baseProps}/>);
|
||||
|
||||
expect(wrapper.state('selected')).toBeUndefined();
|
||||
expect(wrapper.props().selected).toBeUndefined();
|
||||
});
|
||||
|
||||
test('should set selected based on default option', () => {
|
||||
|
|
@ -39,7 +39,7 @@ describe('ActionMenu', () => {
|
|||
};
|
||||
const wrapper = shallow(<ActionMenu {...props}/>);
|
||||
|
||||
expect(wrapper.state('selected')).toBe(props.options[1]);
|
||||
expect(wrapper.props().selected).toBe(props.options[1]);
|
||||
});
|
||||
|
||||
test('should start with previous value selected', () => {
|
||||
|
|
@ -50,7 +50,7 @@ describe('ActionMenu', () => {
|
|||
};
|
||||
const wrapper = shallow(<ActionMenu {...props}/>);
|
||||
|
||||
expect(wrapper.state('selected')).toBe(props.selected);
|
||||
expect(wrapper.props().selected).toBe(props.selected);
|
||||
});
|
||||
|
||||
test('disabled works', () => {
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import AutocompleteSelector from '@components/autocomplete_selector';
|
||||
import {PostActionOption} from '@mm-redux/types/integration_actions';
|
||||
|
||||
type Props = {
|
||||
actions: {
|
||||
selectAttachmentMenuAction: (postId: string, actionId: string, text: string, value: string) => void;
|
||||
};
|
||||
id: string;
|
||||
name: string;
|
||||
dataSource?: string;
|
||||
defaultOption?: string;
|
||||
options?: PostActionOption[];
|
||||
postId: string;
|
||||
selected?: PostActionOption;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const ActionMenu: React.FC<Props> = (props: Props) => {
|
||||
let selected: PostActionOption | undefined;
|
||||
if (props.defaultOption && props.options) {
|
||||
selected = props.options.find((option) => option.value === props.defaultOption);
|
||||
}
|
||||
|
||||
if (props.selected) {
|
||||
selected = props.selected;
|
||||
}
|
||||
|
||||
const handleSelect = (selectedItem?: PostActionOption) => {
|
||||
if (!selectedItem) {
|
||||
return;
|
||||
}
|
||||
|
||||
const {
|
||||
actions,
|
||||
id,
|
||||
postId,
|
||||
} = props;
|
||||
|
||||
actions.selectAttachmentMenuAction(postId, id, selectedItem.text, selectedItem.value);
|
||||
};
|
||||
|
||||
const {
|
||||
name,
|
||||
dataSource,
|
||||
options,
|
||||
disabled,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<AutocompleteSelector
|
||||
placeholder={name}
|
||||
dataSource={dataSource}
|
||||
options={options}
|
||||
selected={selected}
|
||||
onSelected={handleSelect}
|
||||
disabled={disabled}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default ActionMenu;
|
||||
|
|
@ -1,14 +1,21 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {bindActionCreators} from 'redux';
|
||||
import {bindActionCreators, Dispatch} from 'redux';
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {selectAttachmentMenuAction} from 'app/actions/views/post';
|
||||
import {GlobalState} from '@mm-redux/types/store';
|
||||
|
||||
import {selectAttachmentMenuAction} from '@actions/views/post';
|
||||
|
||||
import ActionMenu from './action_menu';
|
||||
|
||||
function mapStateToProps(state, ownProps) {
|
||||
type OwnProps = {
|
||||
postId: string;
|
||||
id: string;
|
||||
}
|
||||
|
||||
function mapStateToProps(state: GlobalState, ownProps: OwnProps) {
|
||||
const actions = state.views.post.submittedMenuActions[ownProps.postId];
|
||||
const selected = actions?.[ownProps.id];
|
||||
|
||||
|
|
@ -17,7 +24,7 @@ function mapStateToProps(state, ownProps) {
|
|||
};
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
function mapDispatchToProps(dispatch: Dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators({
|
||||
selectAttachmentMenuAction,
|
||||
|
|
@ -1,67 +0,0 @@
|
|||
// 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 ActionMenu from './action_menu';
|
||||
import ActionButton from './action_button';
|
||||
|
||||
export default class AttachmentActions extends PureComponent {
|
||||
static propTypes = {
|
||||
actions: PropTypes.array,
|
||||
postId: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
actions,
|
||||
postId,
|
||||
} = this.props;
|
||||
|
||||
if (!actions?.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const content = [];
|
||||
|
||||
actions.forEach((action) => {
|
||||
if (!action.id || !action.name) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (action.type) {
|
||||
case 'select':
|
||||
content.push(
|
||||
<ActionMenu
|
||||
key={action.id}
|
||||
id={action.id}
|
||||
name={action.name}
|
||||
dataSource={action.data_source}
|
||||
defaultOption={action.default_option}
|
||||
options={action.options}
|
||||
postId={postId}
|
||||
disabled={action.disabled}
|
||||
/>,
|
||||
);
|
||||
break;
|
||||
case 'button':
|
||||
default:
|
||||
content.push(
|
||||
<ActionButton
|
||||
key={action.id}
|
||||
id={action.id}
|
||||
cookie={action.cookie}
|
||||
name={action.name}
|
||||
postId={postId}
|
||||
disabled={action.disabled}
|
||||
buttonColor={action.style}
|
||||
/>,
|
||||
);
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
return content.length ? content : null;
|
||||
}
|
||||
}
|
||||
65
app/components/message_attachments/attachment_actions.tsx
Normal file
65
app/components/message_attachments/attachment_actions.tsx
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import ActionMenu from './action_menu';
|
||||
import ActionButton from './action_button';
|
||||
|
||||
import {PostAction} from '@mm-redux/types/integration_actions';
|
||||
|
||||
type Props = {
|
||||
actions?: PostAction[];
|
||||
postId: string;
|
||||
}
|
||||
export default function AttachmentActions(props: Props) {
|
||||
const {
|
||||
actions,
|
||||
postId,
|
||||
} = props;
|
||||
|
||||
if (!actions?.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const content = [] as React.ReactNode[];
|
||||
|
||||
actions.forEach((action) => {
|
||||
if (!action.id || !action.name) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (action.type) {
|
||||
case 'select':
|
||||
content.push(
|
||||
<ActionMenu
|
||||
key={action.id}
|
||||
id={action.id}
|
||||
name={action.name}
|
||||
dataSource={action.data_source}
|
||||
defaultOption={action.default_option}
|
||||
options={action.options}
|
||||
postId={postId}
|
||||
disabled={action.disabled}
|
||||
/>,
|
||||
);
|
||||
break;
|
||||
case 'button':
|
||||
default:
|
||||
content.push(
|
||||
<ActionButton
|
||||
key={action.id}
|
||||
id={action.id}
|
||||
cookie={action.cookie}
|
||||
name={action.name}
|
||||
postId={postId}
|
||||
disabled={action.disabled}
|
||||
buttonColor={action.style}
|
||||
/>,
|
||||
);
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
return content.length ? (<>{content}</>) : null;
|
||||
}
|
||||
|
|
@ -5,19 +5,18 @@ import React, {PureComponent} from 'react';
|
|||
import {Alert, Text, View} from 'react-native';
|
||||
import FastImage from 'react-native-fast-image';
|
||||
import {intlShape} from 'react-intl';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {tryOpenURL} from '@utils/url';
|
||||
import {Theme} from '@mm-redux/types/preferences';
|
||||
|
||||
export default class AttachmentAuthor extends PureComponent {
|
||||
static propTypes = {
|
||||
icon: PropTypes.string,
|
||||
link: PropTypes.string,
|
||||
name: PropTypes.string,
|
||||
theme: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
type Props = {
|
||||
icon?: string;
|
||||
link?: string;
|
||||
name?: string;
|
||||
theme: Theme;
|
||||
}
|
||||
export default class AttachmentAuthor extends PureComponent<Props> {
|
||||
static contextTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
|
@ -81,7 +80,7 @@ export default class AttachmentAuthor extends PureComponent {
|
|||
}
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
return {
|
||||
container: {
|
||||
flex: 1,
|
||||
|
|
@ -1,142 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {PureComponent} from 'react';
|
||||
import {Text, View} from 'react-native';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import Markdown from '@components/markdown';
|
||||
import {makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
export default class AttachmentFields extends PureComponent {
|
||||
static propTypes = {
|
||||
baseTextStyle: PropTypes.oneOfType([PropTypes.object, PropTypes.number, PropTypes.array]),
|
||||
blockStyles: PropTypes.object.isRequired,
|
||||
fields: PropTypes.array,
|
||||
metadata: PropTypes.object,
|
||||
onPermalinkPress: PropTypes.func,
|
||||
textStyles: PropTypes.object.isRequired,
|
||||
theme: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
baseTextStyle,
|
||||
blockStyles,
|
||||
fields,
|
||||
metadata,
|
||||
onPermalinkPress,
|
||||
textStyles,
|
||||
theme,
|
||||
} = this.props;
|
||||
|
||||
if (!fields?.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const style = getStyleSheet(theme);
|
||||
const fieldTables = [];
|
||||
|
||||
let fieldInfos = [];
|
||||
let rowPos = 0;
|
||||
let lastWasLong = false;
|
||||
let nrTables = 0;
|
||||
|
||||
fields.forEach((field, i) => {
|
||||
if (rowPos === 2 || !(field.short === true) || lastWasLong) {
|
||||
fieldTables.push(
|
||||
<View
|
||||
key={`attachment__table__${nrTables}`}
|
||||
style={style.field}
|
||||
>
|
||||
{fieldInfos}
|
||||
</View>,
|
||||
);
|
||||
fieldInfos = [];
|
||||
rowPos = 0;
|
||||
nrTables += 1;
|
||||
lastWasLong = false;
|
||||
}
|
||||
|
||||
fieldInfos.push(
|
||||
<View
|
||||
style={style.flex}
|
||||
key={`attachment__field-${i}__${nrTables}`}
|
||||
>
|
||||
{Boolean(field.title) && (
|
||||
<View
|
||||
style={style.headingContainer}
|
||||
key={`attachment__field-caption-${i}__${nrTables}`}
|
||||
>
|
||||
<View>
|
||||
<Text style={style.heading}>
|
||||
{field.title}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
<View
|
||||
style={style.flex}
|
||||
key={`attachment__field-${i}__${nrTables}`}
|
||||
>
|
||||
<Markdown
|
||||
baseTextStyle={baseTextStyle}
|
||||
textStyles={textStyles}
|
||||
blockStyles={blockStyles}
|
||||
disableGallery={true}
|
||||
imagesMetadata={metadata?.images}
|
||||
value={(field.value || '')}
|
||||
onPermalinkPress={onPermalinkPress}
|
||||
/>
|
||||
</View>
|
||||
</View>,
|
||||
);
|
||||
|
||||
rowPos += 1;
|
||||
lastWasLong = !(field.short === true);
|
||||
});
|
||||
|
||||
if (fieldInfos.length > 0) { // Flush last fields
|
||||
fieldTables.push(
|
||||
<View
|
||||
key={`attachment__table__${nrTables}`}
|
||||
style={style.table}
|
||||
>
|
||||
{fieldInfos}
|
||||
</View>,
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View>
|
||||
{fieldTables}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
return {
|
||||
field: {
|
||||
alignSelf: 'stretch',
|
||||
flexDirection: 'row',
|
||||
},
|
||||
flex: {
|
||||
flex: 1,
|
||||
},
|
||||
headingContainer: {
|
||||
alignSelf: 'stretch',
|
||||
flexDirection: 'row',
|
||||
marginBottom: 5,
|
||||
marginTop: 10,
|
||||
},
|
||||
heading: {
|
||||
color: theme.centerChannelColor,
|
||||
fontWeight: '600',
|
||||
},
|
||||
table: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
},
|
||||
};
|
||||
});
|
||||
145
app/components/message_attachments/attachment_fields.tsx
Normal file
145
app/components/message_attachments/attachment_fields.tsx
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {StyleProp, Text, TextStyle, View, ViewStyle} from 'react-native';
|
||||
|
||||
import Markdown from '@components/markdown';
|
||||
import {makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
import {Theme} from '@mm-redux/types/preferences';
|
||||
import {MessageAttachmentField} from '@mm-redux/types/message_attachments';
|
||||
import {PostMetadata} from '@mm-redux/types/posts';
|
||||
|
||||
type Props = {
|
||||
baseTextStyle: StyleProp<TextStyle>,
|
||||
blockStyles?: StyleProp<ViewStyle>[],
|
||||
fields?: MessageAttachmentField[],
|
||||
metadata?: PostMetadata,
|
||||
onPermalinkPress?: () => void,
|
||||
textStyles?: StyleProp<TextStyle>[],
|
||||
theme: Theme,
|
||||
}
|
||||
|
||||
export default function AttachmentFields(props: Props) {
|
||||
const {
|
||||
baseTextStyle,
|
||||
blockStyles,
|
||||
fields,
|
||||
metadata,
|
||||
onPermalinkPress,
|
||||
textStyles,
|
||||
theme,
|
||||
} = props;
|
||||
|
||||
if (!fields?.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const style = getStyleSheet(theme);
|
||||
const fieldTables = [];
|
||||
|
||||
let fieldInfos = [] as React.ReactNode[];
|
||||
let rowPos = 0;
|
||||
let lastWasLong = false;
|
||||
let nrTables = 0;
|
||||
|
||||
fields.forEach((field, i) => {
|
||||
if (rowPos === 2 || !(field.short === true) || lastWasLong) {
|
||||
fieldTables.push(
|
||||
<View
|
||||
key={`attachment__table__${nrTables}`}
|
||||
style={style.field}
|
||||
>
|
||||
{fieldInfos}
|
||||
</View>,
|
||||
);
|
||||
fieldInfos = [];
|
||||
rowPos = 0;
|
||||
nrTables += 1;
|
||||
lastWasLong = false;
|
||||
}
|
||||
|
||||
fieldInfos.push(
|
||||
<View
|
||||
style={style.flex}
|
||||
key={`attachment__field-${i}__${nrTables}`}
|
||||
>
|
||||
{Boolean(field.title) && (
|
||||
<View
|
||||
style={style.headingContainer}
|
||||
key={`attachment__field-caption-${i}__${nrTables}`}
|
||||
>
|
||||
<View>
|
||||
<Text style={style.heading}>
|
||||
{field.title}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
<View
|
||||
style={style.flex}
|
||||
key={`attachment__field-${i}__${nrTables}`}
|
||||
>
|
||||
<Markdown
|
||||
|
||||
//TODO: remove conversion when markdown is migrated to typescript
|
||||
baseTextStyle={baseTextStyle as any}
|
||||
textStyles={textStyles}
|
||||
blockStyles={blockStyles}
|
||||
disableGallery={true}
|
||||
imagesMetadata={metadata?.images}
|
||||
value={(field.value || '')}
|
||||
onPermalinkPress={onPermalinkPress}
|
||||
/>
|
||||
</View>
|
||||
</View>,
|
||||
);
|
||||
|
||||
rowPos += 1;
|
||||
lastWasLong = !(field.short === true);
|
||||
});
|
||||
|
||||
if (fieldInfos.length > 0) { // Flush last fields
|
||||
fieldTables.push(
|
||||
<View
|
||||
key={`attachment__table__${nrTables}`}
|
||||
style={style.table}
|
||||
>
|
||||
{fieldInfos}
|
||||
</View>,
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View>
|
||||
{fieldTables}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
return {
|
||||
field: {
|
||||
alignSelf: 'stretch',
|
||||
flexDirection: 'row',
|
||||
},
|
||||
flex: {
|
||||
flex: 1,
|
||||
},
|
||||
headingContainer: {
|
||||
alignSelf: 'stretch',
|
||||
flexDirection: 'row',
|
||||
marginBottom: 5,
|
||||
marginTop: 10,
|
||||
},
|
||||
heading: {
|
||||
color: theme.centerChannelColor,
|
||||
fontWeight: '600',
|
||||
},
|
||||
table: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
},
|
||||
};
|
||||
});
|
||||
|
|
@ -1,78 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {PureComponent} from 'react';
|
||||
import {Text, View, Platform} from 'react-native';
|
||||
import FastImage from 'react-native-fast-image';
|
||||
import PropTypes from 'prop-types';
|
||||
import truncate from 'lodash/truncate';
|
||||
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
import {MAX_ATTACHMENT_FOOTER_LENGTH} from 'app/constants/attachment';
|
||||
|
||||
export default class AttachmentFooter extends PureComponent {
|
||||
static propTypes = {
|
||||
text: PropTypes.string,
|
||||
icon: PropTypes.string,
|
||||
theme: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
text,
|
||||
icon,
|
||||
theme,
|
||||
} = this.props;
|
||||
|
||||
if (!text) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const style = getStyleSheet(theme);
|
||||
|
||||
return (
|
||||
<View style={style.container}>
|
||||
{Boolean(icon) &&
|
||||
<FastImage
|
||||
source={{uri: icon}}
|
||||
key='footer_icon'
|
||||
style={style.icon}
|
||||
/>
|
||||
}
|
||||
<Text
|
||||
key='footer_text'
|
||||
style={style.text}
|
||||
>
|
||||
{truncate(text, {length: MAX_ATTACHMENT_FOOTER_LENGTH, omission: '…'})}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
return {
|
||||
container: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
marginTop: 5,
|
||||
},
|
||||
icon: {
|
||||
height: 12,
|
||||
width: 12,
|
||||
marginRight: 5,
|
||||
...Platform.select({
|
||||
ios: {
|
||||
marginTop: 1,
|
||||
},
|
||||
android: {
|
||||
marginTop: 2,
|
||||
},
|
||||
}),
|
||||
},
|
||||
text: {
|
||||
color: changeOpacity(theme.centerChannelColor, 0.5),
|
||||
fontSize: 11,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
|
@ -4,7 +4,6 @@
|
|||
import React from 'react';
|
||||
import {shallow} from 'enzyme';
|
||||
|
||||
import {MAX_ATTACHMENT_FOOTER_LENGTH} from 'app/constants/attachment';
|
||||
import AttachmentFooter from './attachment_footer';
|
||||
|
||||
import Preferences from '@mm-redux/constants/preferences';
|
||||
|
|
@ -51,14 +50,4 @@ describe('AttachmentFooter', () => {
|
|||
const wrapper = shallow(<AttachmentFooter {...baseProps}/>);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('it matches snapshot when the footer is longer than the maximum length', () => {
|
||||
const props = {
|
||||
...baseProps,
|
||||
text: 'a'.repeat(MAX_ATTACHMENT_FOOTER_LENGTH + 1),
|
||||
};
|
||||
|
||||
const wrapper = shallow(<AttachmentFooter {...props}/>);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
77
app/components/message_attachments/attachment_footer.tsx
Normal file
77
app/components/message_attachments/attachment_footer.tsx
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {Text, View, Platform} from 'react-native';
|
||||
import FastImage from 'react-native-fast-image';
|
||||
|
||||
import {Theme} from '@mm-redux/types/preferences';
|
||||
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
type Props = {
|
||||
text?: string;
|
||||
icon?: string;
|
||||
theme: Theme;
|
||||
}
|
||||
|
||||
export default function AttachmentFooter(props: Props) {
|
||||
const {
|
||||
text,
|
||||
icon,
|
||||
theme,
|
||||
} = props;
|
||||
|
||||
if (!text) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const style = getStyleSheet(theme);
|
||||
|
||||
return (
|
||||
<View style={style.container}>
|
||||
{Boolean(icon) &&
|
||||
<FastImage
|
||||
source={{uri: icon}}
|
||||
key='footer_icon'
|
||||
style={style.icon}
|
||||
/>
|
||||
}
|
||||
<Text
|
||||
key='footer_text'
|
||||
style={style.text}
|
||||
ellipsizeMode='tail'
|
||||
numberOfLines={1}
|
||||
>
|
||||
{text}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
return {
|
||||
container: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
marginTop: 5,
|
||||
},
|
||||
icon: {
|
||||
height: 12,
|
||||
width: 12,
|
||||
marginRight: 5,
|
||||
...Platform.select({
|
||||
ios: {
|
||||
marginTop: 1,
|
||||
},
|
||||
android: {
|
||||
marginTop: 2,
|
||||
},
|
||||
}),
|
||||
},
|
||||
text: {
|
||||
color: changeOpacity(theme.centerChannelColor, 0.5),
|
||||
fontSize: 11,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
|
@ -6,7 +6,7 @@ import React from 'react';
|
|||
|
||||
import Preferences from '@mm-redux/constants/preferences';
|
||||
|
||||
import AttachmentImage from './index';
|
||||
import AttachmentImage, {State, Props} from './index';
|
||||
|
||||
describe('AttachmentImage', () => {
|
||||
const baseProps = {
|
||||
|
|
@ -15,7 +15,7 @@ describe('AttachmentImage', () => {
|
|||
imageMetadata: {width: 32, height: 32},
|
||||
imageUrl: 'https://images.com/image.png',
|
||||
theme: Preferences.THEMES.default,
|
||||
};
|
||||
} as Props;
|
||||
|
||||
test('it matches snapshot', () => {
|
||||
const wrapper = shallow(<AttachmentImage {...baseProps}/>);
|
||||
|
|
@ -23,7 +23,7 @@ describe('AttachmentImage', () => {
|
|||
});
|
||||
|
||||
test('it sets state based on props', () => {
|
||||
const wrapper = shallow(<AttachmentImage {...baseProps}/>);
|
||||
const wrapper = shallow<AttachmentImage, Props, State>(<AttachmentImage {...baseProps}/>);
|
||||
|
||||
const state = wrapper.state();
|
||||
expect(state.hasImage).toBe(true);
|
||||
|
|
@ -32,8 +32,11 @@ describe('AttachmentImage', () => {
|
|||
});
|
||||
|
||||
test('it does not render image if no imageUrl is provided', () => {
|
||||
const props = {...baseProps, imageUrl: null, imageMetadata: null};
|
||||
const wrapper = shallow(<AttachmentImage {...props}/>);
|
||||
const props = {...baseProps};
|
||||
delete props.imageUrl;
|
||||
delete props.imageMetadata;
|
||||
|
||||
const wrapper = shallow<AttachmentImage, Props, State>(<AttachmentImage {...props}/>);
|
||||
|
||||
const state = wrapper.state();
|
||||
expect(state.hasImage).toBe(false);
|
||||
|
|
@ -41,7 +44,7 @@ describe('AttachmentImage', () => {
|
|||
});
|
||||
|
||||
test('it updates image when imageUrl prop changes', () => {
|
||||
const wrapper = shallow(<AttachmentImage {...baseProps}/>);
|
||||
const wrapper = shallow<AttachmentImage, Props, State>(<AttachmentImage {...baseProps}/>);
|
||||
|
||||
wrapper.setProps({
|
||||
imageUrl: 'https://someothersite.com/picture.png',
|
||||
|
|
@ -58,7 +61,7 @@ describe('AttachmentImage', () => {
|
|||
});
|
||||
|
||||
test('it does not update image when an unrelated prop changes', () => {
|
||||
const wrapper = shallow(<AttachmentImage {...baseProps}/>);
|
||||
const wrapper = shallow<AttachmentImage, Props, State>(<AttachmentImage {...baseProps}/>);
|
||||
|
||||
wrapper.setProps({
|
||||
theme: {...Preferences.THEMES.default},
|
||||
|
|
@ -2,7 +2,6 @@
|
|||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {View} from 'react-native';
|
||||
|
||||
import ProgressiveImage from '@components/progressive_image';
|
||||
|
|
@ -11,20 +10,37 @@ import {generateId} from '@utils/file';
|
|||
import {isGifTooLarge, openGalleryAtIndex, calculateDimensions} from '@utils/images';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
import {Theme} from '@mm-redux/types/preferences';
|
||||
import {PostImage} from '@mm-redux/types/posts';
|
||||
import {FileInfo} from '@mm-redux/types/files';
|
||||
|
||||
const VIEWPORT_IMAGE_OFFSET = 100;
|
||||
const VIEWPORT_IMAGE_CONTAINER_OFFSET = 10;
|
||||
|
||||
export default class AttachmentImage extends PureComponent {
|
||||
static propTypes = {
|
||||
deviceHeight: PropTypes.number.isRequired,
|
||||
deviceWidth: PropTypes.number.isRequired,
|
||||
imageMetadata: PropTypes.object,
|
||||
imageUrl: PropTypes.string,
|
||||
postId: PropTypes.string,
|
||||
theme: PropTypes.object.isRequired,
|
||||
};
|
||||
export type Props = {
|
||||
deviceHeight: number;
|
||||
deviceWidth: number;
|
||||
imageMetadata?: PostImage;
|
||||
imageUrl?: string;
|
||||
postId?: string;
|
||||
theme: Theme;
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
export type State = {
|
||||
hasImage: boolean;
|
||||
imageUri: string | null;
|
||||
originalHeight?: number;
|
||||
originalWidth?: number;
|
||||
height?: number;
|
||||
width?: number;
|
||||
}
|
||||
|
||||
export default class AttachmentImage extends PureComponent<Props, State> {
|
||||
private fileId: string;
|
||||
private mounted = false;
|
||||
private maxImageWidth = 0;
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this.fileId = generateId();
|
||||
|
|
@ -48,7 +64,7 @@ export default class AttachmentImage extends PureComponent {
|
|||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
componentDidUpdate(prevProps: Props) {
|
||||
if (this.props.imageUrl && (prevProps.imageUrl !== this.props.imageUrl)) {
|
||||
this.setImageUrl(this.props.imageUrl);
|
||||
}
|
||||
|
|
@ -62,6 +78,16 @@ export default class AttachmentImage extends PureComponent {
|
|||
originalWidth,
|
||||
} = this.state;
|
||||
|
||||
if (!imageUrl) {
|
||||
return {
|
||||
id: this.fileId,
|
||||
post_id: postId,
|
||||
uri,
|
||||
width: originalWidth,
|
||||
height: originalHeight,
|
||||
} as FileInfo;
|
||||
}
|
||||
|
||||
let filename = imageUrl.substring(imageUrl.lastIndexOf('/') + 1, imageUrl.indexOf('?') === -1 ? imageUrl.length : imageUrl.indexOf('?'));
|
||||
const extension = filename.split('.').pop();
|
||||
|
||||
|
|
@ -70,7 +96,7 @@ export default class AttachmentImage extends PureComponent {
|
|||
filename = `${filename}${ext}`;
|
||||
}
|
||||
|
||||
return {
|
||||
const out = {
|
||||
id: this.fileId,
|
||||
name: filename,
|
||||
extension,
|
||||
|
|
@ -79,7 +105,8 @@ export default class AttachmentImage extends PureComponent {
|
|||
uri,
|
||||
width: originalWidth,
|
||||
height: originalHeight,
|
||||
};
|
||||
} as FileInfo;
|
||||
return out;
|
||||
}
|
||||
|
||||
handlePreviewImage = () => {
|
||||
|
|
@ -87,7 +114,7 @@ export default class AttachmentImage extends PureComponent {
|
|||
openGalleryAtIndex(0, files);
|
||||
};
|
||||
|
||||
setImageDimensions = (imageUri, dimensions, originalWidth, originalHeight) => {
|
||||
setImageDimensions = (imageUri: string | null, dimensions: {width?: number; height?: number;}, originalWidth: number, originalHeight: number) => {
|
||||
if (this.mounted) {
|
||||
this.setState({
|
||||
...dimensions,
|
||||
|
|
@ -98,12 +125,12 @@ export default class AttachmentImage extends PureComponent {
|
|||
}
|
||||
};
|
||||
|
||||
setImageDimensionsFromMeta = (imageUri, imageMetadata) => {
|
||||
setImageDimensionsFromMeta = (imageUri: string | null, imageMetadata: PostImage) => {
|
||||
const dimensions = calculateDimensions(imageMetadata.height, imageMetadata.width, this.maxImageWidth);
|
||||
this.setImageDimensions(imageUri, dimensions, imageMetadata.width, imageMetadata.height);
|
||||
};
|
||||
|
||||
setImageUrl = (imageURL) => {
|
||||
setImageUrl = (imageURL: string) => {
|
||||
const {imageMetadata} = this.props;
|
||||
|
||||
if (imageMetadata) {
|
||||
|
|
@ -158,7 +185,7 @@ export default class AttachmentImage extends PureComponent {
|
|||
}
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
return {
|
||||
container: {
|
||||
marginTop: 5,
|
||||
|
|
@ -1,54 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {PureComponent} from 'react';
|
||||
import {StyleSheet, View} from 'react-native';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import Markdown from '@components/markdown';
|
||||
|
||||
export default class AttachmentPreText extends PureComponent {
|
||||
static propTypes = {
|
||||
baseTextStyle: PropTypes.oneOfType([PropTypes.object, PropTypes.number, PropTypes.array]),
|
||||
blockStyles: PropTypes.object.isRequired,
|
||||
metadata: PropTypes.object,
|
||||
onPermalinkPress: PropTypes.func,
|
||||
textStyles: PropTypes.object.isRequired,
|
||||
value: PropTypes.string,
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
baseTextStyle,
|
||||
blockStyles,
|
||||
metadata,
|
||||
onPermalinkPress,
|
||||
value,
|
||||
textStyles,
|
||||
} = this.props;
|
||||
|
||||
if (!value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={style.container}>
|
||||
<Markdown
|
||||
baseTextStyle={baseTextStyle}
|
||||
textStyles={textStyles}
|
||||
blockStyles={blockStyles}
|
||||
disableGallery={true}
|
||||
imagesMetadata={metadata?.images}
|
||||
value={value}
|
||||
onPermalinkPress={onPermalinkPress}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const style = StyleSheet.create({
|
||||
container: {
|
||||
marginTop: 5,
|
||||
},
|
||||
});
|
||||
54
app/components/message_attachments/attachment_pretext.tsx
Normal file
54
app/components/message_attachments/attachment_pretext.tsx
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {StyleProp, StyleSheet, TextStyle, View, ViewStyle} from 'react-native';
|
||||
|
||||
import Markdown from '@components/markdown';
|
||||
|
||||
import {PostMetadata} from '@mm-redux/types/posts';
|
||||
|
||||
type Props = {
|
||||
baseTextStyle: StyleProp<TextStyle>;
|
||||
blockStyles?: StyleProp<ViewStyle>[];
|
||||
metadata?: PostMetadata;
|
||||
onPermalinkPress?: () => void;
|
||||
textStyles?: StyleProp<TextStyle>[];
|
||||
value?: string;
|
||||
}
|
||||
export default function AttachmentPreText(props: Props) {
|
||||
const {
|
||||
baseTextStyle,
|
||||
blockStyles,
|
||||
metadata,
|
||||
onPermalinkPress,
|
||||
value,
|
||||
textStyles,
|
||||
} = props;
|
||||
|
||||
if (!value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={style.container}>
|
||||
<Markdown
|
||||
|
||||
// TODO: remove any when migrating Markdown to typescript
|
||||
baseTextStyle={baseTextStyle as any}
|
||||
textStyles={textStyles}
|
||||
blockStyles={blockStyles}
|
||||
disableGallery={true}
|
||||
imagesMetadata={metadata?.images}
|
||||
value={value}
|
||||
onPermalinkPress={onPermalinkPress}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const style = StyleSheet.create({
|
||||
container: {
|
||||
marginTop: 5,
|
||||
},
|
||||
});
|
||||
|
|
@ -2,30 +2,42 @@
|
|||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {PureComponent} from 'react';
|
||||
import {ScrollView, StyleSheet, View} from 'react-native';
|
||||
import PropTypes from 'prop-types';
|
||||
import {LayoutChangeEvent, ScrollView, StyleProp, StyleSheet, TextStyle, View, ViewStyle} from 'react-native';
|
||||
|
||||
import Markdown from '@components/markdown';
|
||||
import ShowMoreButton from '@components/show_more_button';
|
||||
|
||||
import {PostMetadata} from '@mm-redux/types/posts';
|
||||
import {Theme} from '@mm-redux/types/preferences';
|
||||
|
||||
const SHOW_MORE_HEIGHT = 60;
|
||||
|
||||
export default class AttachmentText extends PureComponent {
|
||||
static propTypes = {
|
||||
baseTextStyle: PropTypes.oneOfType([PropTypes.object, PropTypes.number, PropTypes.array]),
|
||||
blockStyles: PropTypes.object.isRequired,
|
||||
deviceHeight: PropTypes.number.isRequired,
|
||||
hasThumbnail: PropTypes.bool,
|
||||
metadata: PropTypes.object,
|
||||
onPermalinkPress: PropTypes.func,
|
||||
textStyles: PropTypes.object.isRequired,
|
||||
theme: PropTypes.object,
|
||||
value: PropTypes.string,
|
||||
};
|
||||
type Props = {
|
||||
baseTextStyle: StyleProp<TextStyle>,
|
||||
blockStyles?: StyleProp<ViewStyle>[],
|
||||
deviceHeight: number,
|
||||
hasThumbnail?: boolean,
|
||||
metadata?: PostMetadata,
|
||||
onPermalinkPress?: () => void,
|
||||
textStyles?: StyleProp<TextStyle>[],
|
||||
theme?: Theme,
|
||||
value?: string,
|
||||
}
|
||||
|
||||
static getDerivedStateFromProps(nextProps, prevState) {
|
||||
type State = {
|
||||
collapsed: boolean;
|
||||
isLongText: boolean;
|
||||
maxHeight: number;
|
||||
}
|
||||
|
||||
function getMaxHeight(deviceHeight: number) {
|
||||
return Math.round((deviceHeight * 0.4) + SHOW_MORE_HEIGHT);
|
||||
}
|
||||
|
||||
export default class AttachmentText extends PureComponent<Props, State> {
|
||||
static getDerivedStateFromProps(nextProps: Props, prevState: State) {
|
||||
const {deviceHeight} = nextProps;
|
||||
const maxHeight = Math.round((deviceHeight * 0.4) + SHOW_MORE_HEIGHT);
|
||||
const maxHeight = getMaxHeight(deviceHeight);
|
||||
|
||||
if (maxHeight !== prevState.maxHeight) {
|
||||
return {
|
||||
|
|
@ -36,16 +48,18 @@ export default class AttachmentText extends PureComponent {
|
|||
return null;
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
const maxHeight = getMaxHeight(props.deviceHeight);
|
||||
this.state = {
|
||||
collapsed: true,
|
||||
isLongText: false,
|
||||
maxHeight,
|
||||
};
|
||||
}
|
||||
|
||||
handleLayout = (event) => {
|
||||
handleLayout = (event: LayoutChangeEvent) => {
|
||||
const {height} = event.nativeEvent.layout;
|
||||
const {maxHeight} = this.state;
|
||||
|
||||
|
|
@ -81,7 +95,7 @@ export default class AttachmentText extends PureComponent {
|
|||
return (
|
||||
<View style={hasThumbnail && style.container}>
|
||||
<ScrollView
|
||||
style={{maxHeight: (collapsed ? maxHeight : null), overflow: 'hidden'}}
|
||||
style={{maxHeight: (collapsed ? maxHeight : undefined), overflow: 'hidden'}}
|
||||
scrollEnabled={false}
|
||||
showsVerticalScrollIndicator={false}
|
||||
showsHorizontalScrollIndicator={false}
|
||||
|
|
@ -91,7 +105,9 @@ export default class AttachmentText extends PureComponent {
|
|||
removeClippedSubviews={isLongText && collapsed}
|
||||
>
|
||||
<Markdown
|
||||
baseTextStyle={baseTextStyle}
|
||||
|
||||
// TODO: remove any when migrating Markdown to typescript
|
||||
baseTextStyle={baseTextStyle as any}
|
||||
textStyles={textStyles}
|
||||
blockStyles={blockStyles}
|
||||
disableGallery={true}
|
||||
|
|
@ -1,44 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {PureComponent} from 'react';
|
||||
import {StyleSheet, View} from 'react-native';
|
||||
import PropTypes from 'prop-types';
|
||||
import FastImage from 'react-native-fast-image';
|
||||
|
||||
export default class AttachmentThumbnail extends PureComponent {
|
||||
static propTypes = {
|
||||
url: PropTypes.string,
|
||||
};
|
||||
|
||||
render() {
|
||||
const {url: uri} = this.props;
|
||||
|
||||
if (!uri) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={style.container}>
|
||||
<FastImage
|
||||
source={{uri}}
|
||||
resizeMode='contain'
|
||||
resizeMethod='scale'
|
||||
style={style.image}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const style = StyleSheet.create({
|
||||
container: {
|
||||
position: 'absolute',
|
||||
right: 10,
|
||||
top: 10,
|
||||
},
|
||||
image: {
|
||||
height: 45,
|
||||
width: 45,
|
||||
},
|
||||
});
|
||||
41
app/components/message_attachments/attachment_thumbnail.tsx
Normal file
41
app/components/message_attachments/attachment_thumbnail.tsx
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {StyleSheet, View} from 'react-native';
|
||||
import FastImage from 'react-native-fast-image';
|
||||
|
||||
import {isValidUrl} from '@utils/url';
|
||||
|
||||
type Props = {
|
||||
url?: string;
|
||||
}
|
||||
export default function AttachmentThumbnail(props: Props) {
|
||||
const {url: uri} = props;
|
||||
|
||||
if (!isValidUrl(uri)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={style.container}>
|
||||
<FastImage
|
||||
source={{uri}}
|
||||
resizeMode='contain'
|
||||
style={style.image}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const style = StyleSheet.create({
|
||||
container: {
|
||||
position: 'absolute',
|
||||
right: 10,
|
||||
top: 10,
|
||||
},
|
||||
image: {
|
||||
height: 45,
|
||||
width: 45,
|
||||
},
|
||||
});
|
||||
|
|
@ -4,19 +4,19 @@
|
|||
import React, {PureComponent} from 'react';
|
||||
import {Alert, Text, View} from 'react-native';
|
||||
import {intlShape} from 'react-intl';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import Markdown from '@components/markdown';
|
||||
import {makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {tryOpenURL} from '@utils/url';
|
||||
|
||||
export default class AttachmentTitle extends PureComponent {
|
||||
static propTypes = {
|
||||
link: PropTypes.string,
|
||||
theme: PropTypes.object.isRequired,
|
||||
value: PropTypes.string,
|
||||
};
|
||||
import {Theme} from '@mm-redux/types/preferences';
|
||||
|
||||
type Props = {
|
||||
link?: string;
|
||||
theme: Theme;
|
||||
value?: string;
|
||||
}
|
||||
export default class AttachmentTitle extends PureComponent<Props> {
|
||||
static contextTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
|
@ -93,7 +93,7 @@ export default class AttachmentTitle extends PureComponent {
|
|||
}
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
return {
|
||||
container: {
|
||||
marginTop: 3,
|
||||
|
|
@ -1,66 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {PureComponent} from 'react';
|
||||
import {View} from 'react-native';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import MessageAttachment from './message_attachment';
|
||||
|
||||
export default class MessageAttachments extends PureComponent {
|
||||
static propTypes = {
|
||||
attachments: PropTypes.array.isRequired,
|
||||
baseTextStyle: PropTypes.oneOfType([PropTypes.object, PropTypes.number, PropTypes.array]),
|
||||
blockStyles: PropTypes.object,
|
||||
deviceHeight: PropTypes.number.isRequired,
|
||||
deviceWidth: PropTypes.number.isRequired,
|
||||
postId: PropTypes.string.isRequired,
|
||||
metadata: PropTypes.object,
|
||||
onHashtagPress: PropTypes.func,
|
||||
onPermalinkPress: PropTypes.func,
|
||||
theme: PropTypes.object,
|
||||
textStyles: PropTypes.object,
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
attachments,
|
||||
baseTextStyle,
|
||||
blockStyles,
|
||||
deviceHeight,
|
||||
deviceWidth,
|
||||
metadata,
|
||||
onHashtagPress,
|
||||
onPermalinkPress,
|
||||
postId,
|
||||
theme,
|
||||
textStyles,
|
||||
} = this.props;
|
||||
const content = [];
|
||||
|
||||
attachments.forEach((attachment, i) => {
|
||||
content.push(
|
||||
<MessageAttachment
|
||||
attachment={attachment}
|
||||
baseTextStyle={baseTextStyle}
|
||||
blockStyles={blockStyles}
|
||||
deviceHeight={deviceHeight}
|
||||
deviceWidth={deviceWidth}
|
||||
key={'att_' + i}
|
||||
metadata={metadata}
|
||||
onHashtagPress={onHashtagPress}
|
||||
onPermalinkPress={onPermalinkPress}
|
||||
postId={postId}
|
||||
theme={theme}
|
||||
textStyles={textStyles}
|
||||
/>,
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<View style={{flex: 1, flexDirection: 'column'}}>
|
||||
{content}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
64
app/components/message_attachments/index.tsx
Normal file
64
app/components/message_attachments/index.tsx
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {StyleProp, TextStyle, View, ViewStyle} from 'react-native';
|
||||
|
||||
import {MessageAttachment as MessageAttachmentType} from '@mm-redux/types/message_attachments';
|
||||
import {PostMetadata} from '@mm-redux/types/posts';
|
||||
import {Theme} from '@mm-redux/types/preferences';
|
||||
|
||||
import MessageAttachment from './message_attachment';
|
||||
|
||||
type Props = {
|
||||
attachments: MessageAttachmentType[],
|
||||
baseTextStyle?: StyleProp<TextStyle>,
|
||||
blockStyles?: StyleProp<ViewStyle>[],
|
||||
deviceHeight: number,
|
||||
deviceWidth: number,
|
||||
postId: string,
|
||||
metadata?: PostMetadata,
|
||||
onPermalinkPress?: () => void,
|
||||
theme: Theme,
|
||||
textStyles?: StyleProp<TextStyle>[],
|
||||
}
|
||||
|
||||
export default function MessageAttachments(props: Props) {
|
||||
const {
|
||||
attachments,
|
||||
baseTextStyle,
|
||||
blockStyles,
|
||||
deviceHeight,
|
||||
deviceWidth,
|
||||
metadata,
|
||||
onPermalinkPress,
|
||||
postId,
|
||||
theme,
|
||||
textStyles,
|
||||
} = props;
|
||||
const content = [] as React.ReactNode[];
|
||||
|
||||
attachments.forEach((attachment, i) => {
|
||||
content.push(
|
||||
<MessageAttachment
|
||||
attachment={attachment}
|
||||
baseTextStyle={baseTextStyle}
|
||||
blockStyles={blockStyles}
|
||||
deviceHeight={deviceHeight}
|
||||
deviceWidth={deviceWidth}
|
||||
key={'att_' + i}
|
||||
metadata={metadata}
|
||||
onPermalinkPress={onPermalinkPress}
|
||||
postId={postId}
|
||||
theme={theme}
|
||||
textStyles={textStyles}
|
||||
/>,
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<View style={{flex: 1, flexDirection: 'column'}}>
|
||||
{content}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,147 +0,0 @@
|
|||
// 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 {View} from 'react-native';
|
||||
|
||||
import {getStatusColors} from '@utils/message_attachment_colors';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
import AttachmentActions from './attachment_actions';
|
||||
import AttachmentAuthor from './attachment_author';
|
||||
import AttachmentFields from './attachment_fields';
|
||||
import AttachmentImage from './attachment_image';
|
||||
import AttachmentPreText from './attachment_pretext';
|
||||
import AttachmentText from './attachment_text';
|
||||
import AttachmentThumbnail from './attachment_thumbnail';
|
||||
import AttachmentTitle from './attachment_title';
|
||||
import AttachmentFooter from './attachment_footer';
|
||||
|
||||
export default class MessageAttachment extends PureComponent {
|
||||
static propTypes = {
|
||||
attachment: PropTypes.object.isRequired,
|
||||
baseTextStyle: PropTypes.oneOfType([PropTypes.object, PropTypes.number, PropTypes.array]),
|
||||
blockStyles: PropTypes.object,
|
||||
deviceHeight: PropTypes.number.isRequired,
|
||||
deviceWidth: PropTypes.number.isRequired,
|
||||
metadata: PropTypes.object,
|
||||
postId: PropTypes.string.isRequired,
|
||||
onPermalinkPress: PropTypes.func,
|
||||
theme: PropTypes.object,
|
||||
textStyles: PropTypes.object,
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
attachment,
|
||||
baseTextStyle,
|
||||
blockStyles,
|
||||
deviceHeight,
|
||||
deviceWidth,
|
||||
metadata,
|
||||
onPermalinkPress,
|
||||
postId,
|
||||
textStyles,
|
||||
theme,
|
||||
} = this.props;
|
||||
|
||||
const style = getStyleSheet(theme);
|
||||
const STATUS_COLORS = getStatusColors(theme);
|
||||
const hasImage = Boolean(metadata?.images?.[attachment.image_url]);
|
||||
|
||||
let borderStyle;
|
||||
if (attachment.color) {
|
||||
if (attachment.color[0] === '#') {
|
||||
borderStyle = {borderLeftColor: attachment.color};
|
||||
} else if (STATUS_COLORS.hasOwnProperty(attachment.color)) {
|
||||
borderStyle = {borderLeftColor: STATUS_COLORS[attachment.color]};
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<AttachmentPreText
|
||||
baseTextStyle={baseTextStyle}
|
||||
blockStyles={blockStyles}
|
||||
metadata={metadata}
|
||||
onPermalinkPress={onPermalinkPress}
|
||||
textStyles={textStyles}
|
||||
value={attachment.pretext}
|
||||
/>
|
||||
<View style={[style.container, style.border, borderStyle]}>
|
||||
<AttachmentAuthor
|
||||
icon={attachment.author_icon}
|
||||
link={attachment.author_link}
|
||||
name={attachment.author_name}
|
||||
theme={theme}
|
||||
/>
|
||||
<AttachmentTitle
|
||||
link={attachment.title_link}
|
||||
theme={theme}
|
||||
value={attachment.title}
|
||||
/>
|
||||
<AttachmentThumbnail url={attachment.thumb_url}/>
|
||||
<AttachmentText
|
||||
baseTextStyle={baseTextStyle}
|
||||
blockStyles={blockStyles}
|
||||
deviceHeight={deviceHeight}
|
||||
hasThumbnail={Boolean(attachment.thumb_url)}
|
||||
metadata={metadata}
|
||||
onPermalinkPress={onPermalinkPress}
|
||||
textStyles={textStyles}
|
||||
value={attachment.text}
|
||||
theme={theme}
|
||||
/>
|
||||
<AttachmentFields
|
||||
baseTextStyle={baseTextStyle}
|
||||
blockStyles={blockStyles}
|
||||
fields={attachment.fields}
|
||||
metadata={metadata}
|
||||
onPermalinkPress={onPermalinkPress}
|
||||
textStyles={textStyles}
|
||||
theme={theme}
|
||||
/>
|
||||
<AttachmentFooter
|
||||
icon={attachment.footer_icon}
|
||||
text={attachment.footer}
|
||||
theme={theme}
|
||||
/>
|
||||
<AttachmentActions
|
||||
actions={attachment.actions}
|
||||
postId={postId}
|
||||
/>
|
||||
{hasImage &&
|
||||
<AttachmentImage
|
||||
deviceHeight={deviceHeight}
|
||||
deviceWidth={deviceWidth}
|
||||
imageUrl={attachment.image_url}
|
||||
imageMetadata={metadata?.images?.[attachment.image_url]}
|
||||
postId={postId}
|
||||
theme={theme}
|
||||
/>
|
||||
}
|
||||
</View>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
return {
|
||||
container: {
|
||||
borderBottomColor: changeOpacity(theme.centerChannelColor, 0.15),
|
||||
borderRightColor: changeOpacity(theme.centerChannelColor, 0.15),
|
||||
borderTopColor: changeOpacity(theme.centerChannelColor, 0.15),
|
||||
borderBottomWidth: 1,
|
||||
borderRightWidth: 1,
|
||||
borderTopWidth: 1,
|
||||
marginTop: 5,
|
||||
padding: 12,
|
||||
},
|
||||
border: {
|
||||
borderLeftColor: changeOpacity(theme.linkColor, 0.6),
|
||||
borderLeftWidth: 3,
|
||||
},
|
||||
};
|
||||
});
|
||||
147
app/components/message_attachments/message_attachment.tsx
Normal file
147
app/components/message_attachments/message_attachment.tsx
Normal file
|
|
@ -0,0 +1,147 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {StyleProp, TextStyle, View, ViewStyle} from 'react-native';
|
||||
|
||||
import {getStatusColors} from '@utils/message_attachment_colors';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
import {MessageAttachment as MessageAttachmentType} from '@mm-redux/types/message_attachments';
|
||||
import {PostMetadata} from '@mm-redux/types/posts';
|
||||
import {Theme} from '@mm-redux/types/preferences';
|
||||
|
||||
import AttachmentActions from './attachment_actions';
|
||||
import AttachmentAuthor from './attachment_author';
|
||||
import AttachmentFields from './attachment_fields';
|
||||
import AttachmentImage from './attachment_image';
|
||||
import AttachmentPreText from './attachment_pretext';
|
||||
import AttachmentText from './attachment_text';
|
||||
import AttachmentThumbnail from './attachment_thumbnail';
|
||||
import AttachmentTitle from './attachment_title';
|
||||
import AttachmentFooter from './attachment_footer';
|
||||
|
||||
type Props = {
|
||||
attachment: MessageAttachmentType,
|
||||
baseTextStyle?: StyleProp<TextStyle>,
|
||||
blockStyles?: StyleProp<ViewStyle>[],
|
||||
deviceHeight: number,
|
||||
deviceWidth: number,
|
||||
postId: string,
|
||||
metadata?: PostMetadata,
|
||||
onPermalinkPress?: () => void,
|
||||
theme: Theme,
|
||||
textStyles?: StyleProp<TextStyle>[],
|
||||
}
|
||||
export default function MessageAttachment(props: Props) {
|
||||
const {
|
||||
attachment,
|
||||
baseTextStyle,
|
||||
blockStyles,
|
||||
deviceHeight,
|
||||
deviceWidth,
|
||||
metadata,
|
||||
onPermalinkPress,
|
||||
postId,
|
||||
textStyles,
|
||||
theme,
|
||||
} = props;
|
||||
|
||||
const style = getStyleSheet(theme);
|
||||
const STATUS_COLORS = getStatusColors(theme);
|
||||
const hasImage = Boolean(metadata?.images?.[attachment.image_url]);
|
||||
|
||||
let borderStyle;
|
||||
if (attachment.color) {
|
||||
if (attachment.color[0] === '#') {
|
||||
borderStyle = {borderLeftColor: attachment.color};
|
||||
} else if (STATUS_COLORS.hasOwnProperty(attachment.color)) {
|
||||
borderStyle = {borderLeftColor: STATUS_COLORS[attachment.color]};
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<AttachmentPreText
|
||||
baseTextStyle={baseTextStyle}
|
||||
blockStyles={blockStyles}
|
||||
metadata={metadata}
|
||||
onPermalinkPress={onPermalinkPress}
|
||||
textStyles={textStyles}
|
||||
value={attachment.pretext}
|
||||
/>
|
||||
<View style={[style.container, style.border, borderStyle]}>
|
||||
<AttachmentAuthor
|
||||
icon={attachment.author_icon}
|
||||
link={attachment.author_link}
|
||||
name={attachment.author_name}
|
||||
theme={theme}
|
||||
/>
|
||||
<AttachmentTitle
|
||||
link={attachment.title_link}
|
||||
theme={theme}
|
||||
value={attachment.title}
|
||||
/>
|
||||
<AttachmentThumbnail url={attachment.thumb_url}/>
|
||||
<AttachmentText
|
||||
baseTextStyle={baseTextStyle}
|
||||
blockStyles={blockStyles}
|
||||
deviceHeight={deviceHeight}
|
||||
hasThumbnail={Boolean(attachment.thumb_url)}
|
||||
metadata={metadata}
|
||||
onPermalinkPress={onPermalinkPress}
|
||||
textStyles={textStyles}
|
||||
value={attachment.text}
|
||||
theme={theme}
|
||||
/>
|
||||
<AttachmentFields
|
||||
baseTextStyle={baseTextStyle}
|
||||
blockStyles={blockStyles}
|
||||
fields={attachment.fields}
|
||||
metadata={metadata}
|
||||
onPermalinkPress={onPermalinkPress}
|
||||
textStyles={textStyles}
|
||||
theme={theme}
|
||||
/>
|
||||
<AttachmentFooter
|
||||
icon={attachment.footer_icon}
|
||||
text={attachment.footer}
|
||||
theme={theme}
|
||||
/>
|
||||
<AttachmentActions
|
||||
actions={attachment.actions}
|
||||
postId={postId}
|
||||
/>
|
||||
{hasImage &&
|
||||
<AttachmentImage
|
||||
deviceHeight={deviceHeight}
|
||||
deviceWidth={deviceWidth}
|
||||
imageUrl={attachment.image_url}
|
||||
imageMetadata={metadata?.images?.[attachment.image_url]}
|
||||
postId={postId}
|
||||
theme={theme}
|
||||
/>
|
||||
}
|
||||
</View>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme:Theme) => {
|
||||
return {
|
||||
container: {
|
||||
borderBottomColor: changeOpacity(theme.centerChannelColor, 0.15),
|
||||
borderRightColor: changeOpacity(theme.centerChannelColor, 0.15),
|
||||
borderTopColor: changeOpacity(theme.centerChannelColor, 0.15),
|
||||
borderBottomWidth: 1,
|
||||
borderRightWidth: 1,
|
||||
borderTopWidth: 1,
|
||||
marginTop: 5,
|
||||
padding: 12,
|
||||
},
|
||||
border: {
|
||||
borderLeftColor: changeOpacity(theme.linkColor, 0.6),
|
||||
borderLeftWidth: 3,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
|
@ -5,9 +5,9 @@ import PropTypes from 'prop-types';
|
|||
import React from 'react';
|
||||
import {StyleSheet, View} from 'react-native';
|
||||
|
||||
import ProgressiveImage from 'app/components/progressive_image';
|
||||
import TouchableWithFeedback from 'app/components/touchable_with_feedback';
|
||||
import {isGifTooLarge} from 'app/utils/images';
|
||||
import ProgressiveImage from '@components/progressive_image';
|
||||
import TouchableWithFeedback from '@components/touchable_with_feedback';
|
||||
import {isGifTooLarge} from '@utils/images';
|
||||
|
||||
export default class PostAttachmentImage extends React.PureComponent {
|
||||
static propTypes = {
|
||||
|
|
|
|||
|
|
@ -355,7 +355,7 @@ export default class PostBodyAdditionalContent extends ImageViewPort {
|
|||
|
||||
if (attachments && attachments.length) {
|
||||
if (!MessageAttachments) {
|
||||
MessageAttachments = require('app/components/message_attachments').default;
|
||||
MessageAttachments = require('@components/message_attachments').default;
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -8,13 +8,13 @@ import {
|
|||
TouchableWithoutFeedback,
|
||||
} from 'react-native';
|
||||
|
||||
import Emoji from 'app/components/emoji';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
import {hapticFeedback} from 'app/utils/general';
|
||||
import Emoji from '@components/emoji';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {hapticFeedback} from '@utils/general';
|
||||
import {
|
||||
LARGE_CONTAINER_SIZE,
|
||||
LARGE_ICON_SIZE,
|
||||
} from 'app/constants/reaction_picker';
|
||||
} from '@constants/reaction_picker';
|
||||
|
||||
export default class ReactionButton extends PureComponent {
|
||||
static propTypes = {
|
||||
|
|
|
|||
|
|
@ -8,9 +8,9 @@ import {
|
|||
Text,
|
||||
} from 'react-native';
|
||||
|
||||
import Emoji from 'app/components/emoji';
|
||||
import TouchableWithFeedback from 'app/components/touchable_with_feedback';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
import Emoji from '@components/emoji';
|
||||
import TouchableWithFeedback from '@components/touchable_with_feedback';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
export default class Reaction extends PureComponent {
|
||||
static propTypes = {
|
||||
|
|
|
|||
|
|
@ -2,4 +2,3 @@
|
|||
// See LICENSE.txt for license information.
|
||||
|
||||
export const ATTACHMENT_DOWNLOAD = 'attachment_download';
|
||||
export const MAX_ATTACHMENT_FOOTER_LENGTH = 300;
|
||||
|
|
|
|||
30
app/mm-redux/types/message_attachments.ts
Normal file
30
app/mm-redux/types/message_attachments.ts
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {PostAction} from './integration_actions';
|
||||
|
||||
export type MessageAttachment = {
|
||||
id: number;
|
||||
fallback: string;
|
||||
color: string;
|
||||
pretext: string;
|
||||
author_name: string;
|
||||
author_link: string;
|
||||
author_icon: string;
|
||||
title: string;
|
||||
title_link: string;
|
||||
text: string;
|
||||
fields: MessageAttachmentField[];
|
||||
image_url: string;
|
||||
thumb_url: string;
|
||||
footer: string;
|
||||
footer_icon: string;
|
||||
timestamp: number | string;
|
||||
actions?: PostAction[];
|
||||
};
|
||||
|
||||
export type MessageAttachmentField = {
|
||||
title: string;
|
||||
value: any;
|
||||
short: boolean;
|
||||
}
|
||||
|
|
@ -95,7 +95,7 @@ const GalleryViewer = (props: GalleryProps) => {
|
|||
const imgHeight = currentFile.height || height;
|
||||
const imgWidth = currentFile.width || width;
|
||||
const calculatedDimensions = calculateDimensions(imgHeight, imgWidth, width, height);
|
||||
const imgCanvas = vec.create(calculatedDimensions.width, calculatedDimensions.height);
|
||||
const imgCanvas = vec.create(calculatedDimensions.width || 0, calculatedDimensions.height || 0);
|
||||
const minImgVec = vec.min(vec.multiply(-0.5, imgCanvas, sub(scale, 1)), 0);
|
||||
const maxImgVec = vec.max(vec.minus(minImgVec), 0);
|
||||
|
||||
|
|
|
|||
|
|
@ -8,12 +8,12 @@ import {
|
|||
TouchableOpacity,
|
||||
} from 'react-native';
|
||||
|
||||
import Emoji from 'app/components/emoji';
|
||||
import {makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
import Emoji from '@components/emoji';
|
||||
import {makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
import FormattedText from 'app/components/formatted_text';
|
||||
import FormattedText from '@components/formatted_text';
|
||||
|
||||
import {ALL_EMOJIS} from 'app/constants/emoji';
|
||||
import {ALL_EMOJIS} from '@constants/emoji';
|
||||
|
||||
export default class ReactionHeaderItem extends PureComponent {
|
||||
static propTypes = {
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {calculateDimensions, isGifTooLarge} from 'app/utils/images';
|
||||
import {calculateDimensions, isGifTooLarge} from '@utils/images';
|
||||
import {
|
||||
IMAGE_MAX_HEIGHT,
|
||||
IMAGE_MIN_DIMENSION,
|
||||
} from 'app/constants/image';
|
||||
} from '@constants/image';
|
||||
|
||||
const PORTRAIT_VIEWPORT = 315;
|
||||
|
||||
|
|
@ -13,18 +13,18 @@ describe('Images calculateDimensions', () => {
|
|||
it('image with falsy height should return null height and width', () => {
|
||||
const falsyHeights = [0, null, undefined, NaN, '', false];
|
||||
falsyHeights.forEach((falsyHeight) => {
|
||||
const {height, width} = calculateDimensions(falsyHeight, 20, PORTRAIT_VIEWPORT);
|
||||
expect(height).toEqual(null);
|
||||
expect(width).toEqual(null);
|
||||
const {height, width} = calculateDimensions(falsyHeight as number, 20, PORTRAIT_VIEWPORT);
|
||||
expect(height).toEqual(undefined);
|
||||
expect(width).toEqual(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
it('image with falsy width should return null height and width', () => {
|
||||
const falsyWidths = [0, null, undefined, NaN, '', false];
|
||||
falsyWidths.forEach((falsyWidth) => {
|
||||
const {height, width} = calculateDimensions(20, falsyWidth, PORTRAIT_VIEWPORT);
|
||||
expect(height).toEqual(null);
|
||||
expect(width).toEqual(null);
|
||||
const {height, width} = calculateDimensions(20, falsyWidth as number, PORTRAIT_VIEWPORT);
|
||||
expect(height).toEqual(undefined);
|
||||
expect(width).toEqual(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -93,7 +93,6 @@ describe('isGifTooLarge', () => {
|
|||
const testCases = [
|
||||
{
|
||||
name: 'no image metadata',
|
||||
imageMetadata: null,
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
|
|
@ -12,14 +12,16 @@ import {
|
|||
VIEWPORT_IMAGE_OFFSET,
|
||||
VIEWPORT_IMAGE_REPLY_OFFSET,
|
||||
} from '@constants/image';
|
||||
import {isImage} from './file';
|
||||
|
||||
export const calculateDimensions = (height, width, viewPortWidth = 0, viewPortHeight = 0) => {
|
||||
import {PostImage} from '@mm-redux/types/posts';
|
||||
import {FileInfo} from '@mm-redux/types/files';
|
||||
|
||||
import {isImage} from './file';
|
||||
import {Options, SharedElementTransition, StackAnimationOptions, ViewAnimationOptions} from 'react-native-navigation';
|
||||
|
||||
export const calculateDimensions = (height?: number, width?: number, viewPortWidth = 0, viewPortHeight = 0) => {
|
||||
if (!height || !width) {
|
||||
return {
|
||||
height: null,
|
||||
width: null,
|
||||
};
|
||||
return {};
|
||||
}
|
||||
|
||||
const ratio = height / width;
|
||||
|
|
@ -61,7 +63,7 @@ export const calculateDimensions = (height, width, viewPortWidth = 0, viewPortHe
|
|||
};
|
||||
};
|
||||
|
||||
export function getViewPortWidth(isReplyPost, permanentSidebar = false) {
|
||||
export function getViewPortWidth(isReplyPost: boolean, permanentSidebar = false) {
|
||||
const {width, height} = Dimensions.get('window');
|
||||
let portraitPostWidth = Math.min(width, height) - VIEWPORT_IMAGE_OFFSET;
|
||||
|
||||
|
|
@ -76,7 +78,7 @@ export function getViewPortWidth(isReplyPost, permanentSidebar = false) {
|
|||
return portraitPostWidth;
|
||||
}
|
||||
|
||||
export function openGalleryAtIndex(index, files) {
|
||||
export function openGalleryAtIndex(index: number, files: FileInfo[]) {
|
||||
Keyboard.dismiss();
|
||||
requestAnimationFrame(() => {
|
||||
const screen = 'Gallery';
|
||||
|
|
@ -85,9 +87,10 @@ export function openGalleryAtIndex(index, files) {
|
|||
files,
|
||||
};
|
||||
const windowHeight = Dimensions.get('window').height;
|
||||
const sharedElementTransitions = [];
|
||||
const contentPush = {};
|
||||
const contentPop = {};
|
||||
const sharedElementTransitions: SharedElementTransition[] = [];
|
||||
|
||||
const contentPush = {} as ViewAnimationOptions;
|
||||
const contentPop = {} as ViewAnimationOptions;
|
||||
const file = files[index];
|
||||
|
||||
if (isImage(file)) {
|
||||
|
|
@ -95,14 +98,14 @@ export function openGalleryAtIndex(index, files) {
|
|||
fromId: `image-${file.id}`,
|
||||
toId: `gallery-${file.id}`,
|
||||
duration: 300,
|
||||
interpolation: {type: 'accelerateDecelerate', factor: 9},
|
||||
interpolation: {type: 'accelerateDecelerate'},
|
||||
});
|
||||
} else {
|
||||
contentPush.y = {
|
||||
from: windowHeight,
|
||||
to: 0,
|
||||
duration: 300,
|
||||
interpolation: {mode: 'decelerate'},
|
||||
interpolation: {type: 'decelerate'},
|
||||
};
|
||||
|
||||
if (Platform.OS === 'ios') {
|
||||
|
|
@ -125,7 +128,7 @@ export function openGalleryAtIndex(index, files) {
|
|||
}
|
||||
}
|
||||
|
||||
const options = {
|
||||
const options: Options = {
|
||||
layout: {
|
||||
backgroundColor: '#000',
|
||||
componentBackgroundColor: '#000',
|
||||
|
|
@ -146,9 +149,9 @@ export function openGalleryAtIndex(index, files) {
|
|||
};
|
||||
|
||||
if (Object.keys(contentPush).length) {
|
||||
options.animations.push = {
|
||||
...options.animations.push,
|
||||
...Platform.select({
|
||||
options.animations!.push = {
|
||||
...options.animations!.push,
|
||||
...Platform.select<ViewAnimationOptions | StackAnimationOptions>({
|
||||
android: contentPush,
|
||||
ios: {
|
||||
content: contentPush,
|
||||
|
|
@ -158,7 +161,7 @@ export function openGalleryAtIndex(index, files) {
|
|||
}
|
||||
|
||||
if (Object.keys(contentPop).length) {
|
||||
options.animations.pop = Platform.select({
|
||||
options.animations!.pop = Platform.select<ViewAnimationOptions | StackAnimationOptions>({
|
||||
android: contentPop,
|
||||
ios: {
|
||||
content: contentPop,
|
||||
|
|
@ -172,7 +175,7 @@ export function openGalleryAtIndex(index, files) {
|
|||
|
||||
// isGifTooLarge returns true if we think that the GIF may cause the device to run out of memory when rendered
|
||||
// based on the image's dimensions and frame count.
|
||||
export function isGifTooLarge(imageMetadata) {
|
||||
export function isGifTooLarge(imageMetadata?: PostImage) {
|
||||
if (imageMetadata?.format !== 'gif') {
|
||||
// Not a gif or from an older server that doesn't count frames
|
||||
return false;
|
||||
|
|
@ -181,5 +184,5 @@ export function isGifTooLarge(imageMetadata) {
|
|||
const {frame_count: frameCount, height, width} = imageMetadata;
|
||||
|
||||
// Try to estimate the in-memory size of the gif to prevent the device out of memory
|
||||
return width * height * frameCount > MAX_GIF_SIZE;
|
||||
return width * height * (frameCount || 1) > MAX_GIF_SIZE;
|
||||
}
|
||||
|
|
@ -1,7 +1,10 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
export function getStatusColors(theme) {
|
||||
import {Theme} from '@mm-redux/types/preferences';
|
||||
import {Dictionary} from '@mm-redux/types/utilities';
|
||||
|
||||
export function getStatusColors(theme: Theme) {
|
||||
return {
|
||||
good: '#00c100',
|
||||
warning: '#dede01',
|
||||
|
|
@ -9,5 +12,5 @@ export function getStatusColors(theme) {
|
|||
default: theme.centerChannelColor,
|
||||
primary: theme.buttonBg,
|
||||
success: theme.onlineIndicator,
|
||||
};
|
||||
} as Dictionary<string>;
|
||||
}
|
||||
Loading…
Reference in a new issue