From bffc75f8d1fd5612e46c1f4fef650276fcb4806a Mon Sep 17 00:00:00 2001 From: Chris Duarte Date: Wed, 23 Aug 2017 11:29:09 -0700 Subject: [PATCH] RN-223 Emoji picker for adding new emoji reactions (#854) * RN-223 Emoji picker for adding new emoji reactions * Remove redundant setServerVersion and fetch custom emojis on reset * Review feedback * Review feedback 2 * Review feedback 3 * Review feedback 4 * Change aliases to array and add header to image source * Remove aliases from custom emojis --- NOTICE.txt | 32 +++ app/actions/views/root.js | 8 +- app/components/emoji/emoji.js | 26 +- app/components/emoji/index.js | 3 +- app/components/emoji_picker/emoji_picker.js | 278 ++++++++++++++++++++ app/components/emoji_picker/index.js | 108 ++++++++ app/components/post/index.js | 3 +- app/components/post/post.js | 31 +++ app/components/post_body/post_body.js | 7 + app/mattermost.js | 7 +- app/screens/add_reaction/add_reaction.js | 72 +++++ app/screens/add_reaction/index.js | 6 + app/screens/index.js | 2 + assets/base/i18n/en.json | 12 + ios/Mattermost.xcodeproj/project.pbxproj | 12 +- package.json | 1 + yarn.lock | 8 +- 17 files changed, 595 insertions(+), 21 deletions(-) create mode 100644 app/components/emoji_picker/emoji_picker.js create mode 100644 app/components/emoji_picker/index.js create mode 100644 app/screens/add_reaction/add_reaction.js create mode 100644 app/screens/add_reaction/index.js 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"