diff --git a/NOTICE.txt b/NOTICE.txt
index f466e4913..8f55842b0 100644
--- a/NOTICE.txt
+++ b/NOTICE.txt
@@ -1247,3 +1247,35 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+---
+
+## react-native-fast-image
+
+FastImage, performant React Native image component.
+
+* HOMEPAGE:
+ * https://github.com/DylanVann/react-native-fast-image
+
+* LICENSE:
+
+MIT License
+
+Copyright (c) 2017 Dylan Vann
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/app/actions/views/root.js b/app/actions/views/root.js
index d22235afe..3bc34650a 100644
--- a/app/actions/views/root.js
+++ b/app/actions/views/root.js
@@ -11,20 +11,16 @@ import {
import {handleTeamChange, selectFirstAvailableTeam} from 'app/actions/views/select_team';
import {General} from 'mattermost-redux/constants';
-import {getClientConfig, getLicenseConfig, setServerVersion} from 'mattermost-redux/actions/general';
+import {getClientConfig, getLicenseConfig} from 'mattermost-redux/actions/general';
import {markChannelAsRead, viewChannel} from 'mattermost-redux/actions/channels';
-export function loadConfigAndLicense(serverVersion) {
+export function loadConfigAndLicense() {
return async (dispatch, getState) => {
const [config, license] = await Promise.all([
getClientConfig()(dispatch, getState),
getLicenseConfig()(dispatch, getState)
]);
- if (config && license) {
- setServerVersion(serverVersion)(dispatch, getState);
- }
-
return {config, license};
};
}
diff --git a/app/components/emoji/emoji.js b/app/components/emoji/emoji.js
index 1198ff5cd..4aa3c27aa 100644
--- a/app/components/emoji/emoji.js
+++ b/app/components/emoji/emoji.js
@@ -3,7 +3,8 @@
import React from 'react';
import PropTypes from 'prop-types';
-import {Image, Text} from 'react-native';
+import {Image, Platform, Text} from 'react-native';
+import FastImage from 'react-native-fast-image';
import CustomPropTypes from 'app/constants/custom_prop_types';
import {EmojiIndicesByAlias, Emojis} from 'app/utils/emojis';
@@ -17,7 +18,8 @@ export default class Emoji extends React.PureComponent {
literal: PropTypes.string,
padding: PropTypes.number,
size: PropTypes.number.isRequired,
- textStyle: CustomPropTypes.Style
+ textStyle: CustomPropTypes.Style,
+ token: PropTypes.string.isRequired
};
static defaultProps = {
@@ -33,7 +35,8 @@ export default class Emoji extends React.PureComponent {
literal,
padding,
size,
- textStyle
+ textStyle,
+ token
} = this.props;
let imageUrl;
@@ -49,10 +52,23 @@ export default class Emoji extends React.PureComponent {
return {literal};
}
+ let ImageComponent = FastImage;
+ const source = {
+ uri: imageUrl,
+ headers: {
+ Authorization: `Bearer ${token}`
+ }
+ };
+
+ if (Platform.OS === 'android') {
+ ImageComponent = Image;
+ }
+
return (
-
);
}
diff --git a/app/components/emoji/index.js b/app/components/emoji/index.js
index dc261b6a9..a20cf1058 100644
--- a/app/components/emoji/index.js
+++ b/app/components/emoji/index.js
@@ -9,8 +9,9 @@ import Emoji from './emoji';
function mapStateToProps(state, ownProps) {
return {
+ ...ownProps,
customEmojis: getCustomEmojisByName(state),
- ...ownProps
+ token: state.entities.general.credentials.token
};
}
diff --git a/app/components/emoji_picker/emoji_picker.js b/app/components/emoji_picker/emoji_picker.js
new file mode 100644
index 000000000..d23dd7986
--- /dev/null
+++ b/app/components/emoji_picker/emoji_picker.js
@@ -0,0 +1,278 @@
+// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import React, {PureComponent} from 'react';
+import PropTypes from 'prop-types';
+import {injectIntl, intlShape} from 'react-intl';
+import {
+ Dimensions,
+ SectionList,
+ TouchableOpacity,
+ View
+} from 'react-native';
+
+import Emoji from 'app/components/emoji';
+import FormattedText from 'app/components/formatted_text';
+import SearchBar from 'app/components/search_bar';
+import {emptyFunction} from 'app/utils/general';
+import {makeStyleSheetFromTheme, changeOpacity} from 'app/utils/theme';
+
+const {width: deviceWidth} = Dimensions.get('window');
+const EMOJI_SIZE = 30;
+const EMOJI_GUTTER = 7.5;
+const SECTION_MARGIN = 15;
+
+class EmojiPicker extends PureComponent {
+ static propTypes = {
+ emojis: PropTypes.array.isRequired,
+ intl: intlShape.isRequired,
+ onEmojiPress: PropTypes.func,
+ theme: PropTypes.object.isRequired
+ };
+
+ static defaultProps = {
+ onEmojiPress: emptyFunction
+ };
+
+ leftButton = {
+ id: 'close-edit-post'
+ };
+
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ emojis: props.emojis,
+ searchTerm: ''
+ };
+ }
+
+ changeSearchTerm = (text) => {
+ this.setState({
+ searchTerm: text
+ });
+
+ clearTimeout(this.searchTermTimeout);
+ const timeout = text ? 350 : 0;
+ this.searchTermTimeout = setTimeout(() => {
+ const emojis = this.searchEmojis(text);
+ this.setState({
+ emojis
+ });
+ }, timeout);
+ };
+
+ cancelSearch = () => {
+ this.setState({
+ emojis: this.props.emojis,
+ searchTerm: ''
+ });
+ }
+
+ filterEmojiAliases = (aliases, searchTerm) => {
+ return aliases.findIndex((alias) => alias.includes(searchTerm)) !== -1;
+ }
+
+ searchEmojis = (searchTerm) => {
+ const {emojis} = this.props;
+ const searchTermLowerCase = searchTerm.toLowerCase();
+
+ if (!searchTerm) {
+ return emojis;
+ }
+
+ const nextEmojis = [];
+ emojis.forEach((section) => {
+ const {data, ...otherProps} = section;
+ const {key, items} = data[0];
+
+ const nextData = {
+ key,
+ items: items.filter((item) => {
+ if (item.aliases) {
+ return this.filterEmojiAliases(item.aliases, searchTermLowerCase);
+ }
+
+ return item.name.includes(searchTermLowerCase);
+ })
+ };
+
+ if (nextData.items.length) {
+ nextEmojis.push({
+ ...otherProps,
+ data: [nextData]
+ });
+ }
+ });
+
+ return nextEmojis;
+ }
+
+ renderSectionHeader = ({section}) => {
+ const {theme} = this.props;
+ const styles = getStyleSheetFromTheme(theme);
+
+ return (
+
+
+
+ );
+ }
+
+ renderEmojis = (emojis, index) => {
+ const {theme} = this.props;
+ const styles = getStyleSheetFromTheme(theme);
+
+ return (
+
+ {emojis.map((emoji, emojiIndex) => {
+ const style = [styles.emoji];
+ if (emojiIndex === 0) {
+ style.push(styles.emojiLeft);
+ } else if (emojiIndex === emojis.length - 1) {
+ style.push(styles.emojiRight);
+ }
+
+ return (
+ {
+ this.props.onEmojiPress(emoji.name);
+ }}
+ >
+
+
+ );
+ })}
+
+ );
+ }
+
+ renderItem = ({item}) => {
+ const {theme} = this.props;
+ const styles = getStyleSheetFromTheme(theme);
+
+ const numColumns = Number(((deviceWidth - (SECTION_MARGIN * 2)) / (EMOJI_SIZE + (EMOJI_GUTTER * 2))).toFixed(0));
+
+ const slices = item.items.reduce((slice, emoji, emojiIndex) => {
+ if (emojiIndex % numColumns === 0 && emojiIndex !== 0) {
+ slice.push([]);
+ }
+
+ slice[slice.length - 1].push(emoji);
+
+ return slice;
+ }, [[]]);
+
+ return (
+
+ {slices.map(this.renderEmojis)}
+
+ );
+ }
+
+ render() {
+ const {intl, theme} = this.props;
+ const {emojis, searchTerm} = this.state;
+ const {formatMessage} = intl;
+ const styles = getStyleSheetFromTheme(theme);
+
+ return (
+
+
+
+
+
+
+
+
+ );
+ }
+}
+
+const getStyleSheetFromTheme = makeStyleSheetFromTheme((theme) => {
+ return {
+ columnStyle: {
+ alignSelf: 'stretch',
+ flexDirection: 'row',
+ marginVertical: EMOJI_GUTTER,
+ justifyContent: 'flex-start'
+ },
+ container: {
+ alignItems: 'center',
+ backgroundColor: theme.centerChannelBg,
+ flex: 1
+ },
+ emoji: {
+ width: EMOJI_SIZE,
+ height: EMOJI_SIZE,
+ marginHorizontal: EMOJI_GUTTER,
+ alignItems: 'center',
+ justifyContent: 'center'
+ },
+ emojiLeft: {
+ marginLeft: 0
+ },
+ emojiRight: {
+ marginRight: 0
+ },
+ listView: {
+ backgroundColor: theme.centerChannelBg,
+ width: deviceWidth - (SECTION_MARGIN * 2)
+ },
+ searchBar: {
+ backgroundColor: changeOpacity(theme.centerChannelColor, 0.2),
+ paddingVertical: 5
+ },
+ section: {
+ alignItems: 'center'
+ },
+ sectionTitle: {
+ color: changeOpacity(theme.centerChannelColor, 0.2),
+ fontSize: 15,
+ fontWeight: '700',
+ paddingVertical: 5
+ },
+ wrapper: {
+ flex: 1
+ }
+ };
+});
+
+export default injectIntl(EmojiPicker);
diff --git a/app/components/emoji_picker/index.js b/app/components/emoji_picker/index.js
new file mode 100644
index 000000000..38d05a9ef
--- /dev/null
+++ b/app/components/emoji_picker/index.js
@@ -0,0 +1,108 @@
+// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import {connect} from 'react-redux';
+import {createSelector} from 'reselect';
+
+import {getCustomEmojisByName} from 'mattermost-redux/selectors/entities/emojis';
+
+import {getTheme} from 'app/selectors/preferences';
+import {CategoryNames, Emojis, EmojiIndicesByCategory} from 'app/utils/emojis';
+
+import EmojiPicker from './emoji_picker';
+
+const categoryToI18n = {
+ activity: {
+ id: 'mobile.emoji_picker.activity',
+ defaultMessage: 'ACTIVITY'
+ },
+ custom: {
+ id: 'mobile.emoji_picker.custom',
+ defaultMessage: 'CUSTOM'
+ },
+ flags: {
+ id: 'mobile.emoji_picker.flags',
+ defaultMessage: 'FLAGS'
+ },
+ foods: {
+ id: 'mobile.emoji_picker.foods',
+ defaultMessage: 'FOODS'
+ },
+ nature: {
+ id: 'mobile.emoji_picker.nature',
+ defaultMessage: 'NATURE'
+ },
+ objects: {
+ id: 'mobile.emoji_picker.objects',
+ defaultMessage: 'OBJECTS'
+ },
+ people: {
+ id: 'mobile.emoji_picker.people',
+ defaultMessage: 'PEOPLE'
+ },
+ places: {
+ id: 'mobile.emoji_picker.places',
+ defaultMessage: 'PLACES'
+ },
+ symbols: {
+ id: 'mobile.emoji_picker.symbols',
+ defaultMessage: 'SYMBOLS'
+ }
+};
+
+function fillEmoji(indice) {
+ const emoji = Emojis[indice];
+ return {
+ name: emoji.aliases[0],
+ aliases: emoji.aliases
+ };
+}
+
+const getEmojisBySection = createSelector(
+ getCustomEmojisByName,
+ (customEmojis) => {
+ const emoticons = CategoryNames.filter((name) => name !== 'custom').map((category) => {
+ const section = {
+ ...categoryToI18n[category],
+ key: category,
+ data: [{
+ key: `${category}-emojis`,
+ items: EmojiIndicesByCategory.get(category).map(fillEmoji)
+ }]
+ };
+
+ return section;
+ });
+
+ const customEmojiData = {
+ key: 'custom-emojis',
+ title: 'CUSTOM',
+ items: []
+ };
+
+ for (const [key] of customEmojis) {
+ customEmojiData.items.push({
+ name: key
+ });
+ }
+
+ emoticons.push({
+ ...categoryToI18n.custom,
+ key: 'custom',
+ data: [customEmojiData]
+ });
+
+ return emoticons;
+ }
+);
+
+function mapStateToProps(state) {
+ const emojis = getEmojisBySection(state);
+
+ return {
+ emojis,
+ theme: getTheme(state)
+ };
+}
+
+export default connect(mapStateToProps)(EmojiPicker);
diff --git a/app/components/post/index.js b/app/components/post/index.js
index bdcea112b..221be708c 100644
--- a/app/components/post/index.js
+++ b/app/components/post/index.js
@@ -4,7 +4,7 @@
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
-import {createPost, deletePost, removePost} from 'mattermost-redux/actions/posts';
+import {addReaction, createPost, deletePost, removePost} from 'mattermost-redux/actions/posts';
import {getPost} from 'mattermost-redux/selectors/entities/posts';
import {getCurrentUserId, getCurrentUserRoles} from 'mattermost-redux/selectors/entities/users';
@@ -38,6 +38,7 @@ function makeMapStateToProps() {
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
+ addReaction,
createPost,
deletePost,
removePost,
diff --git a/app/components/post/post.js b/app/components/post/post.js
index d468b7ace..6c8907f44 100644
--- a/app/components/post/post.js
+++ b/app/components/post/post.js
@@ -28,6 +28,7 @@ import {isAdmin, isSystemAdmin} from 'mattermost-redux/utils/user_utils';
class Post extends PureComponent {
static propTypes = {
actions: PropTypes.shape({
+ addReaction: PropTypes.func.isRequired,
createPost: PropTypes.func.isRequired,
deletePost: PropTypes.func.isRequired,
removePost: PropTypes.func.isRequired,
@@ -146,6 +147,35 @@ class Post extends PureComponent {
});
};
+ handleAddReactionToPost = (emoji) => {
+ const {post} = this.props;
+ this.props.actions.addReaction(post.id, emoji);
+ }
+
+ handleAddReaction = () => {
+ const {intl, navigator, post, theme} = this.props;
+
+ MaterialIcon.getImageSource('close', 20, theme.sidebarHeaderTextColor).
+ then((source) => {
+ navigator.showModal({
+ screen: 'AddReaction',
+ title: intl.formatMessage({id: 'mobile.post_info.add_reaction', defaultMessage: 'Add Reaction'}),
+ animated: true,
+ navigatorStyle: {
+ navBarTextColor: theme.sidebarHeaderTextColor,
+ navBarBackgroundColor: theme.sidebarHeaderBg,
+ navBarButtonColor: theme.sidebarHeaderTextColor,
+ screenBackgroundColor: theme.centerChannelBg
+ },
+ passProps: {
+ post,
+ closeButton: source,
+ onEmojiPress: this.handleAddReactionToPost
+ }
+ });
+ });
+ }
+
handleFailedPostPress = () => {
const options = {
title: {
@@ -304,6 +334,7 @@ class Post extends PureComponent {
canEdit={this.state.canEdit}
isSearchResult={isSearchResult}
navigator={this.props.navigator}
+ onAddReaction={this.handleAddReaction}
onFailedPostPress={this.handleFailedPostPress}
onPostDelete={this.handlePostDelete}
onPostEdit={this.handlePostEdit}
diff --git a/app/components/post_body/post_body.js b/app/components/post_body/post_body.js
index 51545934b..0b38876dd 100644
--- a/app/components/post_body/post_body.js
+++ b/app/components/post_body/post_body.js
@@ -43,6 +43,7 @@ class PostBody extends PureComponent {
isSystemMessage: PropTypes.bool,
message: PropTypes.string,
navigator: PropTypes.object.isRequired,
+ onAddReaction: PropTypes.func,
onFailedPostPress: PropTypes.func,
onPostDelete: PropTypes.func,
onPostEdit: PropTypes.func,
@@ -55,6 +56,7 @@ class PostBody extends PureComponent {
static defaultProps = {
fileIds: [],
+ onAddReaction: emptyFunction,
onFailedPostPress: emptyFunction,
onPostDelete: emptyFunction,
onPostEdit: emptyFunction,
@@ -192,6 +194,11 @@ class PostBody extends PureComponent {
if (canDelete && !hasBeenDeleted) {
actions.push({text: formatMessage({id: 'post_info.del', defaultMessage: 'Delete'}), onPress: onPostDelete});
}
+
+ actions.push({
+ text: formatMessage({id: 'mobile.post_info.add_reaction', defaultMessage: 'Add Reaction'}),
+ onPress: this.props.onAddReaction
+ });
}
let messageComponent;
diff --git a/app/mattermost.js b/app/mattermost.js
index e7a65bf26..8c048b7b2 100644
--- a/app/mattermost.js
+++ b/app/mattermost.js
@@ -220,7 +220,7 @@ export default class Mattermost {
);
} else {
setServerVersion(serverVersion)(dispatch, getState);
- const data = await loadConfigAndLicense(serverVersion)(dispatch, getState);
+ const data = await loadConfigAndLicense()(dispatch, getState);
this.configureAnalytics(data.config);
}
}
@@ -418,8 +418,11 @@ export default class Mattermost {
setServerVersion('')(dispatch, getState);
};
- restartApp = () => {
+ restartApp = async () => {
Navigation.dismissModal({animationType: 'none'});
+
+ const {dispatch, getState} = store;
+ await loadConfigAndLicense()(dispatch, getState);
this.startApp('fade');
};
diff --git a/app/screens/add_reaction/add_reaction.js b/app/screens/add_reaction/add_reaction.js
new file mode 100644
index 000000000..efc4556e3
--- /dev/null
+++ b/app/screens/add_reaction/add_reaction.js
@@ -0,0 +1,72 @@
+// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import React, {PureComponent} from 'react';
+import PropTypes from 'prop-types';
+import {
+ StyleSheet,
+ View
+} from 'react-native';
+
+import EmojiPicker from 'app/components/emoji_picker';
+import {emptyFunction} from 'app/utils/general';
+
+export default class AddReaction extends PureComponent {
+ static propTypes = {
+ closeButton: PropTypes.object,
+ navigator: PropTypes.object.isRequired,
+ onEmojiPress: PropTypes.func
+ };
+
+ static defaultProps = {
+ onEmojiPress: emptyFunction
+ };
+
+ leftButton = {
+ id: 'close-edit-post'
+ };
+
+ constructor(props) {
+ super(props);
+
+ props.navigator.setOnNavigatorEvent(this.onNavigatorEvent);
+ props.navigator.setButtons({
+ leftButtons: [{...this.leftButton, icon: props.closeButton}]
+ });
+ }
+
+ close = () => {
+ this.props.navigator.dismissModal({
+ animationType: 'slide-down'
+ });
+ };
+
+ onNavigatorEvent = (event) => {
+ if (event.type === 'NavBarButtonPress') {
+ switch (event.id) {
+ case 'close-edit-post':
+ this.close();
+ break;
+ }
+ }
+ };
+
+ handleEmojiPress = (emoji) => {
+ this.props.onEmojiPress(emoji);
+ this.close();
+ }
+
+ render() {
+ return (
+
+
+
+ );
+ }
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1
+ }
+});
diff --git a/app/screens/add_reaction/index.js b/app/screens/add_reaction/index.js
new file mode 100644
index 000000000..4f7bc6df7
--- /dev/null
+++ b/app/screens/add_reaction/index.js
@@ -0,0 +1,6 @@
+// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import AddReaction from './add_reaction';
+
+export default AddReaction;
diff --git a/app/screens/index.js b/app/screens/index.js
index 0f486d86e..574c739a2 100644
--- a/app/screens/index.js
+++ b/app/screens/index.js
@@ -7,6 +7,7 @@ import {Navigation} from 'react-native-navigation';
import About from 'app/screens/about';
import AccountSettings from 'app/screens/account_settings';
import AccountNotifications from 'app/screens/account_notifications';
+import AddReaction from 'app/screens/add_reaction';
import AdvancedSettings from 'app/screens/advanced_settings';
import Channel from 'app/screens/channel';
import ChannelAddMembers from 'app/screens/channel_add_members';
@@ -53,6 +54,7 @@ export function registerScreens(store, Provider) {
Navigation.registerComponent('About', () => wrapWithContextProvider(About), store, Provider);
Navigation.registerComponent('AccountSettings', () => wrapWithContextProvider(AccountSettings), store, Provider);
Navigation.registerComponent('AccountNotifications', () => wrapWithContextProvider(AccountNotifications), store, Provider);
+ Navigation.registerComponent('AddReaction', () => wrapWithContextProvider(AddReaction), store, Provider);
Navigation.registerComponent('AdvancedSettings', () => wrapWithContextProvider(AdvancedSettings), store, Provider);
Navigation.registerComponent('Channel', () => wrapWithContextProvider(Channel), store, Provider);
Navigation.registerComponent('ChannelAddMembers', () => wrapWithContextProvider(ChannelAddMembers), store, Provider);
diff --git a/assets/base/i18n/en.json b/assets/base/i18n/en.json
index 76a79ce97..6ace3ca1d 100644
--- a/assets/base/i18n/en.json
+++ b/assets/base/i18n/en.json
@@ -1832,6 +1832,15 @@
"mobile.custom_list.no_results": "No Results",
"mobile.drawer.teamsTitle": "Teams",
"mobile.edit_post.title": "Editing Message",
+ "mobile.emoji_picker.activity": "ACTIVITY",
+ "mobile.emoji_picker.custom": "CUSTOM",
+ "mobile.emoji_picker.flags": "FLAGS",
+ "mobile.emoji_picker.foods": "FOODS",
+ "mobile.emoji_picker.nature": "NATURE",
+ "mobile.emoji_picker.objects": "OBJECTS",
+ "mobile.emoji_picker.people": "PEOPLE",
+ "mobile.emoji_picker.places": "PLACES",
+ "mobile.emoji_picker.symbols": "SYMBOLS",
"mobile.error_handler.button": "Relaunch",
"mobile.error_handler.description": "\nClick relaunch to open the app again. After restart, you can report the problem from the settings menu.\n",
"mobile.error_handler.title": "Unexpected error occurred",
@@ -1864,12 +1873,14 @@
"mobile.offlineIndicator.offline": "No internet connection",
"mobile.open_dm.error": "We couldn't open a direct message with {displayName}. Please check your connection and try again.",
"mobile.open_gm.error": "We couldn't open a group message with those users. Please check your connection and try again.",
+
"mobile.post.cancel": "Cancel",
"mobile.post.delete_question": "Are you sure you want to delete this post?",
"mobile.post.delete_title": "Delete Post",
"mobile.post.failed_delete": "Delete Message",
"mobile.post.failed_retry": "Try Again",
"mobile.post.failed_title": "Unable to send your message",
+ "mobile.post_info.add_reaction": "Add Reaction",
"mobile.post.retry": "Refresh",
"mobile.request.invalid_response": "Received invalid response from the server.",
"mobile.routes.channelInfo": "Info",
@@ -2010,6 +2021,7 @@
"post_info.edit": "Edit",
"post_info.message.visible": "(Only visible to you)",
"post_info.message.visible.compact": " (Only visible to you)",
+ "post_info.mobile.add_reaction": "Add Reaction",
"post_info.mobile.flag": "Flag",
"post_info.mobile.unflag": "Unflag",
"post_info.permalink": "Permalink",
diff --git a/ios/Mattermost.xcodeproj/project.pbxproj b/ios/Mattermost.xcodeproj/project.pbxproj
index b4c5245dd..d38c6ef5e 100644
--- a/ios/Mattermost.xcodeproj/project.pbxproj
+++ b/ios/Mattermost.xcodeproj/project.pbxproj
@@ -5,7 +5,6 @@
};
objectVersion = 46;
objects = {
-
/* Begin PBXBuildFile section */
00C302E51ABCBA2D00DB3ED1 /* libRCTActionSheet.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 00C302AC1ABCB8CE00DB3ED1 /* libRCTActionSheet.a */; };
00C302E71ABCBA2D00DB3ED1 /* libRCTGeolocation.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 00C302BA1ABCB90400DB3ED1 /* libRCTGeolocation.a */; };
@@ -62,6 +61,7 @@
F006936FC2884C24A1321FC0 /* MaterialIcons.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 356C9186FA374641A00EB2EA /* MaterialIcons.ttf */; };
F083DB472349411A8E6E7AAD /* OpenSans-LightItalic.ttf in Resources */ = {isa = PBXBuildFile; fileRef = BE17F630DB5D41FD93F32D22 /* OpenSans-LightItalic.ttf */; };
FDEF24E9D9AB47728E11033B /* libRNPasscodeStatus.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 17A75F5A3D0D4A4A995DCD76 /* libRNPasscodeStatus.a */; };
+ 1C1F002AC5DE465FAE9E0D74 /* libFastImage.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 0C0C75A424714EAC8E876A8F /* libFastImage.a */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@@ -445,6 +445,8 @@
EE671DF7637347CD8C069819 /* libRNDeviceInfo.a */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = archive.ar; path = libRNDeviceInfo.a; sourceTree = ""; };
F1F071EE85494E269A50AE88 /* SimpleLineIcons.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = SimpleLineIcons.ttf; path = "../node_modules/react-native-vector-icons/Fonts/SimpleLineIcons.ttf"; sourceTree = ""; };
FBBEC29EE2D3418D9AC33BD5 /* OpenSans-ExtraBoldItalic.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "OpenSans-ExtraBoldItalic.ttf"; path = "../assets/fonts/OpenSans-ExtraBoldItalic.ttf"; sourceTree = ""; };
+ 50ECB1D221E44F51B5690DF2 /* FastImage.xcodeproj */ = {isa = PBXFileReference; name = "FastImage.xcodeproj"; path = "../node_modules/react-native-fast-image/ios/FastImage.xcodeproj"; sourceTree = ""; fileEncoding = undefined; lastKnownFileType = wrapper.pb-project; explicitFileType = undefined; includeInIndex = 0; };
+ 0C0C75A424714EAC8E876A8F /* libFastImage.a */ = {isa = PBXFileReference; name = "libFastImage.a"; path = "libFastImage.a"; sourceTree = ""; fileEncoding = undefined; lastKnownFileType = archive.ar; explicitFileType = undefined; includeInIndex = 0; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@@ -484,6 +486,7 @@
615D2E7805814A3A9B9C69EC /* libRNLocalAuth.a in Frameworks */,
FDEF24E9D9AB47728E11033B /* libRNPasscodeStatus.a in Frameworks */,
38FB8B0A3AAD4F839EF6EF0B /* libJailMonkey.a in Frameworks */,
+ 1C1F002AC5DE465FAE9E0D74 /* libFastImage.a in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -790,6 +793,7 @@
D0281D64B98143668D6AD42B /* RNLocalAuth.xcodeproj */,
EBA6063A99C141098D40C67A /* RNPasscodeStatus.xcodeproj */,
27A6EA89298440439DA9F98D /* JailMonkey.xcodeproj */,
+ 50ECB1D221E44F51B5690DF2 /* FastImage.xcodeproj */,
);
name = Libraries;
sourceTree = "";
@@ -1405,8 +1409,6 @@
LIBRARY_SEARCH_PATHS = (
"$(inherited)",
"\"$(SRCROOT)/$(TARGET_NAME)\"",
- "\"$(SRCROOT)/$(TARGET_NAME)\"",
- "\"$(SRCROOT)/$(TARGET_NAME)\"",
);
PRODUCT_BUNDLE_IDENTIFIER = "org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier)";
PRODUCT_NAME = "$(TARGET_NAME)";
@@ -1425,8 +1427,6 @@
LIBRARY_SEARCH_PATHS = (
"$(inherited)",
"\"$(SRCROOT)/$(TARGET_NAME)\"",
- "\"$(SRCROOT)/$(TARGET_NAME)\"",
- "\"$(SRCROOT)/$(TARGET_NAME)\"",
);
PRODUCT_BUNDLE_IDENTIFIER = "org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier)";
PRODUCT_NAME = "$(TARGET_NAME)";
@@ -1456,6 +1456,7 @@
"$(SRCROOT)/../node_modules/react-native-passcode-status/ios",
"$(SRCROOT)/../node_modules/jail-monkey/JailMonkey",
"$(SRCROOT)/../node_modules/react-native/Libraries/**",
+ "$(SRCROOT)/../node_modules/react-native-fast-image/ios/FastImage/**",
);
INFOPLIST_FILE = Mattermost/Info.plist;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
@@ -1494,6 +1495,7 @@
"$(SRCROOT)/../node_modules/react-native-passcode-status/ios",
"$(SRCROOT)/../node_modules/jail-monkey/JailMonkey",
"$(SRCROOT)/../node_modules/react-native/Libraries/**",
+ "$(SRCROOT)/../node_modules/react-native-fast-image/ios/FastImage/**",
);
INFOPLIST_FILE = Mattermost/Info.plist;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
diff --git a/package.json b/package.json
index 540b92023..08d6ec208 100644
--- a/package.json
+++ b/package.json
@@ -26,6 +26,7 @@
"react-native-device-info": "0.11.0",
"react-native-drawer": "2.3.0",
"react-native-exception-handler": "1.1.0",
+ "react-native-fast-image": "1.0.0",
"react-native-image-picker": "jp928/react-native-image-picker#6ee35b69f3dbd6c7c66f580fd4d9eabf398703d4",
"react-native-keyboard-aware-scroll-view": "0.2.8",
"react-native-linear-gradient": "2.0.0",
diff --git a/yarn.lock b/yarn.lock
index 4350423c4..ebc7873cf 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -4288,7 +4288,7 @@ promise@^7.1.1:
dependencies:
asap "~2.0.3"
-prop-types@15.5.10, prop-types@^15.0.0, prop-types@^15.5.4, prop-types@^15.5.8, prop-types@~15.5.7:
+prop-types@15.5.10, prop-types@^15.0.0, prop-types@^15.5.10, prop-types@^15.5.4, prop-types@^15.5.8, prop-types@~15.5.7:
version "15.5.10"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.5.10.tgz#2797dfc3126182e3a95e3dfbb2e893ddd7456154"
dependencies:
@@ -4495,6 +4495,12 @@ react-native-exception-handler@1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/react-native-exception-handler/-/react-native-exception-handler-1.1.0.tgz#320b7fc9c104e11d66173a2b0daeac97831a1a37"
+react-native-fast-image@1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/react-native-fast-image/-/react-native-fast-image-1.0.0.tgz#a88337efa18fd34d530bab9ee05680374f3e2a2d"
+ dependencies:
+ prop-types "^15.5.10"
+
react-native-image-picker@jp928/react-native-image-picker#6ee35b69f3dbd6c7c66f580fd4d9eabf398703d4:
version "0.26.2"
resolved "https://codeload.github.com/jp928/react-native-image-picker/tar.gz/6ee35b69f3dbd6c7c66f580fd4d9eabf398703d4"