From 3feaa8e6bb95d6c258bd09cbbde8b2a9dc514b39 Mon Sep 17 00:00:00 2001 From: Elias Nahum Date: Tue, 26 Feb 2019 14:31:57 -0300 Subject: [PATCH] iOS Native Share Extension (Swift) (#2575) * iOS Native Share Extension (Swift) * Re-arrange files * Fix .gitignore --- NOTICE.txt | 37 - .../file_attachment_document.js | 4 +- .../file_upload_item/file_upload_item.js | 3 +- .../network_indicator/network_indicator.js | 3 +- app/fetch_preconfig.js | 2 +- app/mattermost.js | 2 +- app/mattermost_bucket/index.js | 26 +- app/screens/edit_profile/edit_profile.js | 4 +- app/screens/image_preview/downloader.ios.js | 4 +- app/screens/select_server/select_server.js | 4 +- app/store/index.js | 9 +- app/utils/image_cache_manager.js | 4 +- app/utils/network.js | 3 +- assets/base/config.json | 2 - fastlane/Fastfile | 8 +- ios/Mattermost.xcodeproj/project.pbxproj | 542 +++++------ .../xcschemes/Mattermost.xcscheme | 14 + .../xcschemes/MattermostShare.xcscheme | 111 +++ .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../xcshareddata/WorkspaceSettings.xcsettings | 5 + ios/{ => Mattermost}/0155-keys.png | Bin ios/Mattermost/AppDelegate.m | 11 +- ios/Mattermost/Dummy.swift | 6 + ios/Mattermost/Mattermost-Bridging-Header.h | 4 + ios/Mattermost/MattermostBucketModule.h | 7 + ios/Mattermost/MattermostBucketModule.m | 67 ++ ios/MattermostBucket.h | 7 - ios/MattermostBucket.m | 126 --- .../Base.lproj/MainInterface.storyboard | 4 +- .../ChannelsViewController.swift | 134 +++ ios/MattermostShare/GenericPreview.swift | 25 + ios/MattermostShare/GenericPreview.xib | 57 ++ ios/MattermostShare/Images/generic.png | Bin 0 -> 45380 bytes ios/MattermostShare/Info.plist | 11 - ios/MattermostShare/Item.swift | 7 + .../MattermostShare-Bridging-Header.h | 6 + .../MattermostShare.entitlements | 18 - ios/MattermostShare/PerformRequests.h | 23 - ios/MattermostShare/PerformRequests.m | 154 ---- ios/MattermostShare/Section.swift | 13 + ios/MattermostShare/SessionManager.h | 23 - ios/MattermostShare/SessionManager.m | 300 ------- ios/MattermostShare/ShareViewController.h | 5 - ios/MattermostShare/ShareViewController.m | 285 ------ ios/MattermostShare/ShareViewController.swift | 405 +++++++++ ios/MattermostShare/TeamsViewController.swift | 57 ++ .../project.pbxproj | 371 ++++++++ .../xcschemes/UploadAttachments.xcscheme | 80 ++ .../UploadAttachments/AttachmentArray.swift | 269 ++++++ .../UploadAttachments/AttachmentItem.swift | 11 + .../UploadAttachments/Constants.h | 7 + .../UploadAttachments/Constants.m | 6 + .../UploadAttachments/MattermostBucket.h | 12 + .../UploadAttachments/MattermostBucket.m | 69 ++ .../UploadAttachments/StoreManager.h | 22 + .../UploadAttachments/StoreManager.m | 303 +++++++ .../UploadAttachments-Bridging-Header.h | 7 + .../UploadAttachments/UploadManager.swift | 41 + .../UploadAttachments/UploadSession.swift | 102 +++ .../UploadSessionManager.swift | 79 ++ package-lock.json | 8 - package.json | 1 - share.ios.js | 8 - share_extension/ios/error_message.js | 86 -- share_extension/ios/extension.js | 164 ---- share_extension/ios/extension_channels.js | 347 -------- share_extension/ios/extension_nav_bar.js | 150 ---- share_extension/ios/extension_post.js | 839 ------------------ share_extension/ios/extension_team_item.js | 119 --- share_extension/ios/extension_teams.js | 219 ----- share_extension/ios/index.js | 68 -- 71 files changed, 2590 insertions(+), 3348 deletions(-) create mode 100644 ios/Mattermost.xcodeproj/xcshareddata/xcschemes/MattermostShare.xcscheme create mode 100644 ios/Mattermost.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 ios/Mattermost.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings rename ios/{ => Mattermost}/0155-keys.png (100%) create mode 100644 ios/Mattermost/Dummy.swift create mode 100644 ios/Mattermost/Mattermost-Bridging-Header.h create mode 100644 ios/Mattermost/MattermostBucketModule.h create mode 100644 ios/Mattermost/MattermostBucketModule.m delete mode 100644 ios/MattermostBucket.h delete mode 100644 ios/MattermostBucket.m create mode 100644 ios/MattermostShare/ChannelsViewController.swift create mode 100644 ios/MattermostShare/GenericPreview.swift create mode 100644 ios/MattermostShare/GenericPreview.xib create mode 100644 ios/MattermostShare/Images/generic.png create mode 100644 ios/MattermostShare/Item.swift create mode 100644 ios/MattermostShare/MattermostShare-Bridging-Header.h delete mode 100644 ios/MattermostShare/PerformRequests.h delete mode 100644 ios/MattermostShare/PerformRequests.m create mode 100644 ios/MattermostShare/Section.swift delete mode 100644 ios/MattermostShare/SessionManager.h delete mode 100644 ios/MattermostShare/SessionManager.m delete mode 100644 ios/MattermostShare/ShareViewController.h delete mode 100644 ios/MattermostShare/ShareViewController.m create mode 100644 ios/MattermostShare/ShareViewController.swift create mode 100644 ios/MattermostShare/TeamsViewController.swift create mode 100644 ios/UploadAttachments/UploadAttachments.xcodeproj/project.pbxproj create mode 100644 ios/UploadAttachments/UploadAttachments.xcodeproj/xcshareddata/xcschemes/UploadAttachments.xcscheme create mode 100644 ios/UploadAttachments/UploadAttachments/AttachmentArray.swift create mode 100644 ios/UploadAttachments/UploadAttachments/AttachmentItem.swift create mode 100644 ios/UploadAttachments/UploadAttachments/Constants.h create mode 100644 ios/UploadAttachments/UploadAttachments/Constants.m create mode 100644 ios/UploadAttachments/UploadAttachments/MattermostBucket.h create mode 100644 ios/UploadAttachments/UploadAttachments/MattermostBucket.m create mode 100644 ios/UploadAttachments/UploadAttachments/StoreManager.h create mode 100644 ios/UploadAttachments/UploadAttachments/StoreManager.m create mode 100644 ios/UploadAttachments/UploadAttachments/UploadAttachments-Bridging-Header.h create mode 100644 ios/UploadAttachments/UploadAttachments/UploadManager.swift create mode 100644 ios/UploadAttachments/UploadAttachments/UploadSession.swift create mode 100644 ios/UploadAttachments/UploadAttachments/UploadSessionManager.swift delete mode 100644 share.ios.js delete mode 100644 share_extension/ios/error_message.js delete mode 100644 share_extension/ios/extension.js delete mode 100644 share_extension/ios/extension_channels.js delete mode 100644 share_extension/ios/extension_nav_bar.js delete mode 100644 share_extension/ios/extension_post.js delete mode 100644 share_extension/ios/extension_team_item.js delete mode 100644 share_extension/ios/extension_teams.js delete mode 100644 share_extension/ios/index.js diff --git a/NOTICE.txt b/NOTICE.txt index 54e9f2dce1..ed8df0a812 100644 --- a/NOTICE.txt +++ b/NOTICE.txt @@ -2086,43 +2086,6 @@ SOFTWARE. --- -## react-native-tableview - -This product contains 'react-native-tableview' by Pavlo Aksonov. - -Native iOS TableView wrapper for React Native - -* HOMEPAGE: - * https://github.com/aksonov/react-native-tableview#readme - -* LICENSE: BSD-2-Clause - -Copyright (c) 2015, aksonov -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - -* Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -* Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - ---- - ## react-native-vector-icons This product contains 'react-native-vector-icons' by Joel Arvidsson. diff --git a/app/components/file_attachment_list/file_attachment_document.js b/app/components/file_attachment_list/file_attachment_document.js index af7ac727aa..473463227a 100644 --- a/app/components/file_attachment_list/file_attachment_document.js +++ b/app/components/file_attachment_list/file_attachment_document.js @@ -25,8 +25,6 @@ import {DeviceTypes} from 'app/constants/'; import mattermostBucket from 'app/mattermost_bucket'; import {changeOpacity} from 'app/utils/theme'; -import LocalConfig from 'assets/config'; - import FileAttachmentIcon from './file_attachment_icon'; const {DOCUMENTS_PATH} = DeviceTypes; @@ -114,7 +112,7 @@ export default class FileAttachmentDocument extends PureComponent { this.setState({didCancel: false}); try { - const certificate = await mattermostBucket.getPreference('cert', LocalConfig.AppGroupId); + const certificate = await mattermostBucket.getPreference('cert'); const isDir = await RNFetchBlob.fs.isDir(DOCUMENTS_PATH); if (!isDir) { try { diff --git a/app/components/file_upload_preview/file_upload_item/file_upload_item.js b/app/components/file_upload_preview/file_upload_item/file_upload_item.js index 698f375834..fab33a2e3b 100644 --- a/app/components/file_upload_preview/file_upload_item/file_upload_item.js +++ b/app/components/file_upload_preview/file_upload_item/file_upload_item.js @@ -15,7 +15,6 @@ import FileUploadRetry from 'app/components/file_upload_preview/file_upload_retr import FileUploadRemove from 'app/components/file_upload_preview/file_upload_remove'; import mattermostBucket from 'app/mattermost_bucket'; import {buildFileUploadData, encodeHeaderURIStringToUTF8} from 'app/utils/file'; -import LocalConfig from 'assets/config'; export default class FileUploadItem extends PureComponent { static propTypes = { @@ -137,7 +136,7 @@ export default class FileUploadItem extends PureComponent { Client4.trackEvent('api', 'api_files_upload'); - const certificate = await mattermostBucket.getPreference('cert', LocalConfig.AppGroupId); + const certificate = await mattermostBucket.getPreference('cert'); const options = { timeout: 10000, certificate, diff --git a/app/components/network_indicator/network_indicator.js b/app/components/network_indicator/network_indicator.js index 9ab511f6e8..1c89987f71 100644 --- a/app/components/network_indicator/network_indicator.js +++ b/app/components/network_indicator/network_indicator.js @@ -22,7 +22,6 @@ import mattermostBucket from 'app/mattermost_bucket'; import PushNotifications from 'app/push_notifications'; import networkConnectionListener, {checkConnection} from 'app/utils/network'; import {t} from 'app/utils/i18n'; -import LocalConfig from 'assets/config'; import {RequestStatus} from 'mattermost-redux/constants'; @@ -293,7 +292,7 @@ export default class NetworkIndicator extends PureComponent { const platform = Platform.OS; let certificate = null; if (platform === 'ios') { - certificate = await mattermostBucket.getPreference('cert', LocalConfig.AppGroupId); + certificate = await mattermostBucket.getPreference('cert'); } initWebSocket(platform, null, null, null, {certificate, forceConnection: true}).catch(() => { diff --git a/app/fetch_preconfig.js b/app/fetch_preconfig.js index 7e4860c249..9e3336ca14 100644 --- a/app/fetch_preconfig.js +++ b/app/fetch_preconfig.js @@ -130,7 +130,7 @@ Client4.doFetchWithResponse = async (url, options) => { const initFetchConfig = async () => { let fetchConfig = {}; if (Platform.OS === 'ios') { - const certificate = await mattermostBucket.getPreference('cert', LocalConfig.AppGroupId); + const certificate = await mattermostBucket.getPreference('cert'); fetchConfig = { auto: true, certificate, diff --git a/app/mattermost.js b/app/mattermost.js index acc650a905..7b282770ef 100644 --- a/app/mattermost.js +++ b/app/mattermost.js @@ -322,7 +322,7 @@ const handleAuthentication = async (vendor) => { const translations = app.getTranslations(); if (isSecured) { try { - mattermostBucket.setPreference('emm', vendor, LocalConfig.AppGroupId); + mattermostBucket.setPreference('emm', vendor); await mattermostManaged.authenticate({ reason: translations[t('mobile.managed.secured_by')].replace('{vendor}', vendor), fallbackToPasscode: true, diff --git a/app/mattermost_bucket/index.js b/app/mattermost_bucket/index.js index a1a39eabe8..c1458bc130 100644 --- a/app/mattermost_bucket/index.js +++ b/app/mattermost_bucket/index.js @@ -4,17 +4,17 @@ import {NativeModules, Platform} from 'react-native'; // TODO: Remove platform specific once android is implemented -const MattermostBucket = Platform.OS === 'ios' ? NativeModules.MattermostBucket : null; +const MattermostBucket = Platform.OS === 'ios' ? NativeModules.MattermostBucketModule : null; export default { - setPreference: (key, value, groupName) => { + setPreference: (key, value) => { if (MattermostBucket) { - MattermostBucket.setPreference(key, value, groupName); + MattermostBucket.setPreference(key, value); } }, - getPreference: async (key, groupName) => { + getPreference: async (key) => { if (MattermostBucket) { - const value = await MattermostBucket.getPreference(key, groupName); + const value = await MattermostBucket.getPreference(key); if (value) { try { return JSON.parse(value); @@ -26,19 +26,19 @@ export default { return null; }, - removePreference: (key, groupName) => { + removePreference: (key) => { if (MattermostBucket) { - MattermostBucket.removePreference(key, groupName); + MattermostBucket.removePreference(key); } }, - writeToFile: (fileName, content, groupName) => { + writeToFile: (fileName, content) => { if (MattermostBucket) { - MattermostBucket.writeToFile(fileName, content, groupName); + MattermostBucket.writeToFile(fileName, content); } }, - readFromFile: async (fileName, groupName) => { + readFromFile: async (fileName) => { if (MattermostBucket) { - const value = await MattermostBucket.readFromFile(fileName, groupName); + const value = await MattermostBucket.readFromFile(fileName); if (value) { try { return JSON.parse(value); @@ -50,9 +50,9 @@ export default { return null; }, - removeFile: (fileName, groupName) => { + removeFile: (fileName) => { if (MattermostBucket) { - MattermostBucket.removeFile(fileName, groupName); + MattermostBucket.removeFile(fileName); } }, }; diff --git a/app/screens/edit_profile/edit_profile.js b/app/screens/edit_profile/edit_profile.js index 8c8edf6d9f..b3d1ddbc0b 100644 --- a/app/screens/edit_profile/edit_profile.js +++ b/app/screens/edit_profile/edit_profile.js @@ -24,7 +24,7 @@ import StatusBar from 'app/components/status_bar/index'; import ProfilePictureButton from 'app/components/profile_picture_button'; import ProfilePicture from 'app/components/profile_picture'; import mattermostBucket from 'app/mattermost_bucket'; -import LocalConfig from 'assets/config'; + import {getFormattedFileSize} from 'mattermost-redux/utils/file_utils'; const MAX_SIZE = 20 * 1024 * 1024; @@ -235,7 +235,7 @@ export default class EditProfile extends PureComponent { type: fileData.type, }; - const certificate = await mattermostBucket.getPreference('cert', LocalConfig.AppGroupId); + const certificate = await mattermostBucket.getPreference('cert'); const options = { timeout: 10000, certificate, diff --git a/app/screens/image_preview/downloader.ios.js b/app/screens/image_preview/downloader.ios.js index ee28e62c4e..1c0d631ffc 100644 --- a/app/screens/image_preview/downloader.ios.js +++ b/app/screens/image_preview/downloader.ios.js @@ -16,8 +16,6 @@ import mattermostBucket from 'app/mattermost_bucket'; import {getLocalFilePathFromFile} from 'app/utils/file'; import {emptyFunction} from 'app/utils/general'; -import LocalConfig from 'assets/config'; - import DownloaderBottomContent from './downloader_bottom_content.js'; const {View: AnimatedView} = Animated; @@ -236,7 +234,7 @@ export default class Downloader extends PureComponent { this.setState({didCancel: false}); } - const certificate = await mattermostBucket.getPreference('cert', LocalConfig.AppGroupId); + const certificate = await mattermostBucket.getPreference('cert'); const imageUrl = Client4.getFileUrl(data.id); const options = { session: data.id, diff --git a/app/screens/select_server/select_server.js b/app/screens/select_server/select_server.js index 785e0e925b..b9285967eb 100644 --- a/app/screens/select_server/select_server.js +++ b/app/screens/select_server/select_server.js @@ -187,7 +187,7 @@ export default class SelectServer extends PureComponent { if (LocalConfig.ExperimentalClientSideCertEnable && Platform.OS === 'ios') { RNFetchBlob.cba.selectCertificate((certificate) => { if (certificate) { - mattermostBucket.setPreference('cert', certificate, LocalConfig.AppGroupId); + mattermostBucket.setPreference('cert', certificate); window.fetch = new RNFetchBlob.polyfill.Fetch({ auto: true, certificate, @@ -377,7 +377,7 @@ export default class SelectServer extends PureComponent { const url = this.getUrl(); RNFetchBlob.cba.selectCertificate((certificate) => { if (certificate) { - mattermostBucket.setPreference('cert', certificate, LocalConfig.AppGroupId); + mattermostBucket.setPreference('cert', certificate); fetchConfig().then(() => { this.pingServer(url, true); }); diff --git a/app/store/index.js b/app/store/index.js index bde927cd4f..3cb203f3e1 100644 --- a/app/store/index.js +++ b/app/store/index.js @@ -19,7 +19,6 @@ import {getSiteUrl, setSiteUrl} from 'app/utils/image_cache_manager'; import {createSentryMiddleware} from 'app/utils/sentry/middleware'; import mattermostBucket from 'app/mattermost_bucket'; -import Config from 'assets/config'; import {messageRetention} from './middleware'; import {createThunkMiddleware} from './thunk'; @@ -181,7 +180,7 @@ export default function configureAppStore(initialState) { profilesNotInChannel: [], }, }; - mattermostBucket.writeToFile('entities', JSON.stringify(entities), Config.AppGroupId); + mattermostBucket.writeToFile('entities', JSON.stringify(entities)); } }, 1000)); } @@ -216,9 +215,9 @@ export default function configureAppStore(initialState) { ])); // When logging out remove the data stored in the bucket - mattermostBucket.removePreference('cert', Config.AppGroupId); - mattermostBucket.removePreference('emm', Config.AppGroupId); - mattermostBucket.removeFile('entities', Config.AppGroupId); + mattermostBucket.removePreference('cert'); + mattermostBucket.removePreference('emm'); + mattermostBucket.removeFile('entities'); setSiteUrl(null); setTimeout(() => { diff --git a/app/utils/image_cache_manager.js b/app/utils/image_cache_manager.js index e911d3ba64..c990c62776 100644 --- a/app/utils/image_cache_manager.js +++ b/app/utils/image_cache_manager.js @@ -11,8 +11,6 @@ import {Client4} from 'mattermost-redux/client'; import {DeviceTypes} from 'app/constants'; import mattermostBucket from 'app/mattermost_bucket'; -import LocalConfig from 'assets/config'; - const {IMAGES_PATH} = DeviceTypes; let siteUrl; @@ -34,7 +32,7 @@ export default class ImageCacheManager { addListener(uri, listener); if (uri.startsWith('http')) { try { - const certificate = await mattermostBucket.getPreference('cert', LocalConfig.AppGroupId); + const certificate = await mattermostBucket.getPreference('cert'); const options = { session: uri, timeout: 10000, diff --git a/app/utils/network.js b/app/utils/network.js index 78c02fedb3..767eab7da5 100644 --- a/app/utils/network.js +++ b/app/utils/network.js @@ -8,7 +8,6 @@ import RNFetchBlob from 'rn-fetch-blob'; import {Client4} from 'mattermost-redux/client'; import mattermostBucket from 'app/mattermost_bucket'; -import LocalConfig from 'assets/config'; let certificate = ''; let previousState; @@ -27,7 +26,7 @@ export async function checkConnection(isConnected) { }; if (Platform.OS === 'ios' && certificate === '') { - certificate = await mattermostBucket.getPreference('cert', LocalConfig.AppGroupId); + certificate = await mattermostBucket.getPreference('cert'); config.certificate = certificate; } diff --git a/assets/base/config.json b/assets/base/config.json index 0a34c13232..f85cd1e508 100644 --- a/assets/base/config.json +++ b/assets/base/config.json @@ -39,8 +39,6 @@ "MobileClientUpgradeAndroidApkLink": "https://about.mattermost.com/mattermost-android-app/", "MobileClientUpgradeIosIpaLink": "https://about.mattermost.com/mattermost-ios-app/", - "AppGroupId": "group.com.mattermost.rnbeta", - "ShowSentryDebugOptions": false, "CustomRequestHeaders": {} diff --git a/fastlane/Fastfile b/fastlane/Fastfile index fb962d6eeb..7846626774 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -283,6 +283,13 @@ platform :ios do app_group_identifiers: [app_group_id] ) + find_replace_string( + path_to_file: './ios/UploadAttachments/UploadAttachments/Constants.m', + old_string: 'group.com.mattermost.rnbeta', + new_string: app_group_id + ) + + update_app_group_identifiers( entitlements_file: './ios/MattermostShare/MattermostShare.entitlements', app_group_identifiers: [app_group_id] @@ -290,7 +297,6 @@ platform :ios do # Save changes to the config.json file config_json = load_config_json - config_json['AppGroupId'] = app_group_id save_config_json(config_json) # Sync the provisioning profiles using match diff --git a/ios/Mattermost.xcodeproj/project.pbxproj b/ios/Mattermost.xcodeproj/project.pbxproj index 8dfddba56a..80e5c683f3 100644 --- a/ios/Mattermost.xcodeproj/project.pbxproj +++ b/ios/Mattermost.xcodeproj/project.pbxproj @@ -50,12 +50,20 @@ 74D116AD208A8D5600CF8A79 /* libRNKeychain.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 74D116AA208A8D3100CF8A79 /* libRNKeychain.a */; }; 7BD159C40A68467FB5A17141 /* FontAwesome5_Solid.ttf in Resources */ = {isa = PBXBuildFile; fileRef = DC1D660B55BE462A9C3B8028 /* FontAwesome5_Solid.ttf */; }; 7F0AA8C121A613A000E4370E /* libRNSentry.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7F0AA8BB21A6138200E4370E /* libRNSentry.a */; }; + 7F151D3E221B062700FAD8F3 /* Dummy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F151D3D221B062700FAD8F3 /* Dummy.swift */; }; + 7F151D41221B069200FAD8F3 /* 0155-keys.png in Resources */ = {isa = PBXBuildFile; fileRef = 7F151D40221B069200FAD8F3 /* 0155-keys.png */; }; + 7F240A1C220D3A2300637665 /* ShareViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F240A1B220D3A2300637665 /* ShareViewController.swift */; }; + 7F240A1F220D3A2300637665 /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 7F240A1D220D3A2300637665 /* MainInterface.storyboard */; }; + 7F240A23220D3A2300637665 /* MattermostShare.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 7F240A19220D3A2300637665 /* MattermostShare.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + 7F240ACD220D460300637665 /* MattermostBucketModule.m in Sources */ = {isa = PBXBuildFile; fileRef = 7F240ACC220D460300637665 /* MattermostBucketModule.m */; }; + 7F240AD0220D4A6100637665 /* KeyChainDataSource.mm in Sources */ = {isa = PBXBuildFile; fileRef = 7F240ACE220D4A6100637665 /* KeyChainDataSource.mm */; }; + 7F240ADB220E089300637665 /* Item.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F240ADA220E089300637665 /* Item.swift */; }; + 7F240ADD220E094A00637665 /* TeamsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F240ADC220E094A00637665 /* TeamsViewController.swift */; }; 7F2691A11EE1DC6A007574FE /* libRNNotifications.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7F2691A01EE1DC51007574FE /* libRNNotifications.a */; }; + 7F26C1CC219C463D00FEB42D /* libRNCWebView.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7F26C1CB219C463300FEB42D /* libRNCWebView.a */; }; 7F292A711E8AB73400A450A3 /* SplashScreenResource in Resources */ = {isa = PBXBuildFile; fileRef = 7F292A701E8AB73400A450A3 /* SplashScreenResource */; }; 7F292AA61E8ABB1100A450A3 /* LaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = 7F292AA41E8ABB1100A450A3 /* LaunchScreen.xib */; }; 7F292AA71E8ABB1100A450A3 /* splash.png in Resources */ = {isa = PBXBuildFile; fileRef = 7F292AA51E8ABB1100A450A3 /* splash.png */; }; - 7F380D0B1FDB3CFC0061AAD2 /* libRCTVideo.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7F93F9D91FBB726B0088E416 /* libRCTVideo.a */; }; - 7F3F66021FE426EE0085CA0E /* libRNSVG.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7FDF28E11E1F4B1F00DBBE56 /* libRNSVG.a */; }; 7F43D5D61F6BF8C2001FC614 /* libRNLocalAuth.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7F8C49871F3DFC30003A22BA /* libRNLocalAuth.a */; }; 7F43D5D71F6BF8D0001FC614 /* libRNPasscodeStatus.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7F8C49F81F3E0710003A22BA /* libRNPasscodeStatus.a */; }; 7F43D5D81F6BF8E9001FC614 /* libJailMonkey.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7F8C4A621F3E21FB003A22BA /* libJailMonkey.a */; }; @@ -66,47 +74,24 @@ 7F43D6061F6BF9EB001FC614 /* libPods-Mattermost.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7F43D6051F6BF9EB001FC614 /* libPods-Mattermost.a */; }; 7F43D63F1F6BFA19001FC614 /* libBVLinearGradient.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 37D8FEC21E80B5230091F3BD /* libBVLinearGradient.a */; }; 7F43D6401F6BFA82001FC614 /* libRCTPushNotification.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7F63D2811E6C957C001FAE12 /* libRCTPushNotification.a */; }; - 7F50C96A203C6E80007CA374 /* SessionManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 7F50C969203C6E80007CA374 /* SessionManager.m */; }; - 7F50C96B203C6E80007CA374 /* SessionManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 7F50C969203C6E80007CA374 /* SessionManager.m */; }; 7F5CA9A0208FE3B9004F91CE /* libRNDocumentPicker.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7F5CA991208FE38F004F91CE /* libRNDocumentPicker.a */; }; 7F642DF02093533300F3165E /* libRNDeviceInfo.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7F642DED2093530B00F3165E /* libRNDeviceInfo.a */; }; - 7F642DF1209353A400F3165E /* libRNDeviceInfo.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7F642DED2093530B00F3165E /* libRNDeviceInfo.a */; }; - 7F6C47A51FE87E8C00F5A912 /* PerformRequests.m in Sources */ = {isa = PBXBuildFile; fileRef = 7F6C47A41FE87E8C00F5A912 /* PerformRequests.m */; }; + 7F72F2EE2211220500F98FFF /* GenericPreview.xib in Resources */ = {isa = PBXBuildFile; fileRef = 7F72F2ED2211220500F98FFF /* GenericPreview.xib */; }; + 7F72F2F0221123E200F98FFF /* GenericPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F72F2EF221123E200F98FFF /* GenericPreview.swift */; }; + 7F72F2F322112EC700F98FFF /* generic.png in Resources */ = {isa = PBXBuildFile; fileRef = 7F72F2F222112EC700F98FFF /* generic.png */; }; 7F7D7F98201645E100D31155 /* libReactNativePermissions.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7F7D7F87201645D300D31155 /* libReactNativePermissions.a */; }; - 7F95132F208646CE00616E3E /* 0155-keys.png in Resources */ = {isa = PBXBuildFile; fileRef = 7F9512EA208646CD00616E3E /* 0155-keys.png */; }; - 7F97F2312217989F00F5BCCC /* libRNCWebView.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7F26C1CB219C463300FEB42D /* libRNCWebView.a */; }; + 7FABDFC22211A39000D0F595 /* Section.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FABDFC12211A39000D0F595 /* Section.swift */; }; + 7FABE00A2212650600D0F595 /* ChannelsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FABE0092212650600D0F595 /* ChannelsViewController.swift */; }; + 7FABE04622137F5C00D0F595 /* libUploadAttachments.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7FABE04522137F2A00D0F595 /* libUploadAttachments.a */; }; + 7FABE0562213884700D0F595 /* libUploadAttachments.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7FABE04522137F2A00D0F595 /* libUploadAttachments.a */; }; 7FBB5E9B1E1F5A4B000DE18A /* libRNVectorIcons.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7FDF290C1E1F4B4E00DBBE56 /* libRNVectorIcons.a */; }; 7FC200E81EBB65370099331B /* libReactNativeNavigation.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7FC200DF1EBB65100099331B /* libReactNativeNavigation.a */; }; - 7FC649EE1FE983660074E4C7 /* EvilIcons.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 349FBA7338E74D9BBD709528 /* EvilIcons.ttf */; }; - 7FC649F01FE9B5D90074E4C7 /* OpenSans-Bold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = D4B1B363C2414DA19C1AC521 /* OpenSans-Bold.ttf */; }; - 7FCC63CA221C5291002EC8A5 /* libReact.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 146834041AC3E56700842450 /* libReact.a */; }; - 7FCC63CC221C53BC002EC8A5 /* JavaScriptCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7FCC63CB221C53BB002EC8A5 /* JavaScriptCore.framework */; }; - 7FD8DEDE2029EDB7001AAC5E /* libRNTableView.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7FD8DEDA2029ECDF001AAC5E /* libRNTableView.a */; }; 7FDB92B11F706F58006CDFD1 /* libRNImagePicker.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7FDB92A71F706F45006CDFD1 /* libRNImagePicker.a */; }; 7FEB10981F6101710039A015 /* BlurAppScreen.m in Sources */ = {isa = PBXBuildFile; fileRef = 7FEB10971F6101710039A015 /* BlurAppScreen.m */; }; 7FEB109D1F61019C0039A015 /* MattermostManaged.m in Sources */ = {isa = PBXBuildFile; fileRef = 7FEB109A1F61019C0039A015 /* MattermostManaged.m */; }; 7FEB109E1F61019C0039A015 /* UIImage+ImageEffects.m in Sources */ = {isa = PBXBuildFile; fileRef = 7FEB109C1F61019C0039A015 /* UIImage+ImageEffects.m */; }; 7FF2AF8B2086483E00FFBDF4 /* KeyShareConsumer.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 7FF2AF8A2086483E00FFBDF4 /* KeyShareConsumer.storyboard */; }; 7FF31C1421330B7900680B75 /* libRNFetchBlob.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7FF31C1321330B4200680B75 /* libRNFetchBlob.a */; }; - 7FF31C1521330BAD00680B75 /* libRNFetchBlob.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7FF31C1321330B4200680B75 /* libRNFetchBlob.a */; }; - 7FF7BE2C1FDEE4AE005E55FE /* MattermostManaged.m in Sources */ = {isa = PBXBuildFile; fileRef = 7FEB109A1F61019C0039A015 /* MattermostManaged.m */; }; - 7FF7BE6D1FDEE5E8005E55FE /* MattermostBucket.m in Sources */ = {isa = PBXBuildFile; fileRef = 7FF7BE6C1FDEE5E8005E55FE /* MattermostBucket.m */; }; - 7FF7BE6E1FDEE5E8005E55FE /* MattermostBucket.m in Sources */ = {isa = PBXBuildFile; fileRef = 7FF7BE6C1FDEE5E8005E55FE /* MattermostBucket.m */; }; - 7FF7BE6F1FDF3CE4005E55FE /* libRNVectorIcons.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7FDF290C1E1F4B4E00DBBE56 /* libRNVectorIcons.a */; }; - 7FF7BE701FDF3EE7005E55FE /* Ionicons.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 2DCFD31D3F4A4154822AB532 /* Ionicons.ttf */; }; - 7FFDB3191FE3566C009E3BCF /* FontAwesome.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 005346E5C0E542BFABAE1411 /* FontAwesome.ttf */; }; - 7FFE329E1FD9CB650038C7A0 /* ShareViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 7FFE329D1FD9CB650038C7A0 /* ShareViewController.m */; }; - 7FFE32A11FD9CB650038C7A0 /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 7FFE329F1FD9CB650038C7A0 /* MainInterface.storyboard */; }; - 7FFE32A51FD9CB650038C7A0 /* MattermostShare.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 7FFE329A1FD9CB640038C7A0 /* MattermostShare.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; - 7FFE32E71FD9CCD00038C7A0 /* libART.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 37ABD39C1F4CE13B001FDE6B /* libART.a */; }; - 7FFE32E81FD9CCDE0038C7A0 /* libRCTCameraRoll.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 3752184F1F4B9E980035444B /* libRCTCameraRoll.a */; }; - 7FFE32E91FD9CCF40038C7A0 /* libRCTAnimation.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5E9157331DD0AC6500FF2AA8 /* libRCTAnimation.a */; }; - 7FFE32EB1FD9CD170038C7A0 /* libRCTImage.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 00C302C01ABCB91800DB3ED1 /* libRCTImage.a */; }; - 7FFE32EC1FD9CD360038C7A0 /* libRCTText.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 832341B51AAA6A8300B99B32 /* libRCTText.a */; }; - 7FFE32ED1FD9CD450038C7A0 /* libRCTNetwork.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 00C302DC1ABCB9D200DB3ED1 /* libRCTNetwork.a */; }; - 7FFE32EE1FD9CD800038C7A0 /* libRNLocalAuth.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7F8C49871F3DFC30003A22BA /* libRNLocalAuth.a */; }; - 7FFE32EF1FD9CD800038C7A0 /* libRNPasscodeStatus.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7F8C49F81F3E0710003A22BA /* libRNPasscodeStatus.a */; }; - 7FFE32F11FD9D64E0038C7A0 /* libRCTWebSocket.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 139FDEF41B06529B00C62182 /* libRCTWebSocket.a */; }; 832341BD1AAA6AB300B99B32 /* libRCTText.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 832341B51AAA6A8300B99B32 /* libRCTText.a */; }; 895C9A56B94A45C1BAF568FE /* Entypo.ttf in Resources */ = {isa = PBXBuildFile; fileRef = AC6EB561E1F64C17A69D2FAD /* Entypo.ttf */; }; 8D26455C994F46C39B1392F2 /* libRNSafeArea.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 9263CF9B16054263B13EA23B /* libRNSafeArea.a */; }; @@ -384,6 +369,13 @@ remoteGlobalIDString = 134814201AA4EA6300B7C361; remoteInfo = RNSentry; }; + 7F240A21220D3A2300637665 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 83CBB9F71A601CBA00E9B192 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 7F240A18220D3A2300637665; + remoteInfo = MattermostShare; + }; 7F26919F1EE1DC51007574FE /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 7F26919B1EE1DC51007574FE /* RNNotifications.xcodeproj */; @@ -433,6 +425,34 @@ remoteGlobalIDString = 3DBE0D0D1F3B181C0099AA32; remoteInfo = "fishhook-tvOS"; }; + 7F581C71221DF8DE0099E66B /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 146833FF1AC3E56700842450 /* React.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = EDEBC6D6214B3E7000DD5AC8; + remoteInfo = jsi; + }; + 7F581C73221DF8DE0099E66B /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 146833FF1AC3E56700842450 /* React.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = EDEBC73B214B45A300DD5AC8; + remoteInfo = jsiexecutor; + }; + 7F581C75221DF8DE0099E66B /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 146833FF1AC3E56700842450 /* React.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = ED296FB6214C9A0900B7C4FE; + remoteInfo = "jsi-tvOS"; + }; + 7F581C77221DF8DE0099E66B /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 146833FF1AC3E56700842450 /* React.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = ED296FEE214C9CF800B7C4FE; + remoteInfo = "jsiexecutor-tvOS"; + }; 7F5CA990208FE38F004F91CE /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 7F5CA956208FE38F004F91CE /* RNDocumentPicker.xcodeproj */; @@ -531,6 +551,13 @@ remoteGlobalIDString = 134814201AA4EA6300B7C361; remoteInfo = ReactNativeExceptionHandler; }; + 7FABE04422137F2A00D0F595 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 7FABE04022137F2900D0F595 /* UploadAttachments.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 7FABE03622137F2900D0F595; + remoteInfo = UploadAttachments; + }; 7FC200DE1EBB65100099331B /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 7FC200BC1EBB65100099331B /* ReactNativeNavigation.xcodeproj */; @@ -538,41 +565,6 @@ remoteGlobalIDString = D8AFADBD1BEE6F3F00A4592D; remoteInfo = ReactNativeNavigation; }; - 7FCC63AA221C5270002EC8A5 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 146833FF1AC3E56700842450 /* React.xcodeproj */; - proxyType = 2; - remoteGlobalIDString = EDEBC6D6214B3E7000DD5AC8; - remoteInfo = jsi; - }; - 7FCC63AC221C5270002EC8A5 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 146833FF1AC3E56700842450 /* React.xcodeproj */; - proxyType = 2; - remoteGlobalIDString = EDEBC73B214B45A300DD5AC8; - remoteInfo = jsiexecutor; - }; - 7FCC63AE221C5270002EC8A5 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 146833FF1AC3E56700842450 /* React.xcodeproj */; - proxyType = 2; - remoteGlobalIDString = ED296FB6214C9A0900B7C4FE; - remoteInfo = "jsi-tvOS"; - }; - 7FCC63B0221C5270002EC8A5 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 146833FF1AC3E56700842450 /* React.xcodeproj */; - proxyType = 2; - remoteGlobalIDString = ED296FEE214C9CF800B7C4FE; - remoteInfo = "jsiexecutor-tvOS"; - }; - 7FD8DED92029ECDF001AAC5E /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 7FD8DE972029ECDE001AAC5E /* RNTableView.xcodeproj */; - proxyType = 2; - remoteGlobalIDString = 873769951B7CD6E900F7C3C2; - remoteInfo = RNTableView; - }; 7FDB92A61F706F45006CDFD1 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 7FDB92751F706F45006CDFD1 /* RNImagePicker.xcodeproj */; @@ -622,13 +614,6 @@ remoteGlobalIDString = 6DA7B8031F692C4C00FD1D50; remoteInfo = RNSafeArea; }; - 7FFE32A31FD9CB650038C7A0 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 83CBB9F71A601CBA00E9B192 /* Project object */; - proxyType = 1; - remoteGlobalIDString = 7FFE32991FD9CB640038C7A0; - remoteInfo = MattermostShare; - }; 832341B41AAA6A8300B99B32 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 832341B01AAA6A8300B99B32 /* RCTText.xcodeproj */; @@ -655,7 +640,7 @@ dstPath = ""; dstSubfolderSpec = 13; files = ( - 7FFE32A51FD9CB650038C7A0 /* MattermostShare.appex in Embed App Extensions */, + 7F240A23220D3A2300637665 /* MattermostShare.appex in Embed App Extensions */, ); name = "Embed App Extensions"; runOnlyForDeploymentPostprocessing = 0; @@ -728,28 +713,41 @@ 79CB6EBA24FE4ABFB0C155F0 /* YTPlayerView-iframe-player.html */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "YTPlayerView-iframe-player.html"; path = "../node_modules/react-native-youtube/assets/YTPlayerView-iframe-player.html"; sourceTree = ""; }; 7DCC3D826CE640AF8F491692 /* BVLinearGradient.xcodeproj */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = "wrapper.pb-project"; name = BVLinearGradient.xcodeproj; path = "../node_modules/react-native-linear-gradient/BVLinearGradient.xcodeproj"; sourceTree = ""; }; 7F0AA87621A6138200E4370E /* RNSentry.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RNSentry.xcodeproj; path = "../node_modules/react-native-sentry/ios/RNSentry.xcodeproj"; sourceTree = ""; }; + 7F151D3D221B062700FAD8F3 /* Dummy.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Dummy.swift; path = Mattermost/Dummy.swift; sourceTree = ""; }; + 7F151D40221B069200FAD8F3 /* 0155-keys.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "0155-keys.png"; path = "Mattermost/0155-keys.png"; sourceTree = ""; }; + 7F151D42221B07F700FAD8F3 /* MattermostShare-Bridging-Header.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "MattermostShare-Bridging-Header.h"; sourceTree = ""; }; + 7F151D43221B082A00FAD8F3 /* Mattermost-Bridging-Header.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "Mattermost-Bridging-Header.h"; path = "Mattermost/Mattermost-Bridging-Header.h"; sourceTree = ""; }; + 7F240A19220D3A2300637665 /* MattermostShare.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = MattermostShare.appex; sourceTree = BUILT_PRODUCTS_DIR; }; + 7F240A1B220D3A2300637665 /* ShareViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareViewController.swift; sourceTree = ""; }; + 7F240A1E220D3A2300637665 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/MainInterface.storyboard; sourceTree = ""; }; + 7F240A20220D3A2300637665 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 7F240A27220D3A7E00637665 /* MattermostShare.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = MattermostShare.entitlements; sourceTree = ""; }; + 7F240ACB220D460300637665 /* MattermostBucketModule.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = MattermostBucketModule.h; path = Mattermost/MattermostBucketModule.h; sourceTree = ""; }; + 7F240ACC220D460300637665 /* MattermostBucketModule.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = MattermostBucketModule.m; path = Mattermost/MattermostBucketModule.m; sourceTree = ""; }; + 7F240ACE220D4A6100637665 /* KeyChainDataSource.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = KeyChainDataSource.mm; path = "../../node_modules/rn-fetch-blob/ios/KeyShareConsumer/KeyChainDataSource.mm"; sourceTree = ""; }; + 7F240ACF220D4A6100637665 /* KeyChainDataSource.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = KeyChainDataSource.h; path = "../../node_modules/rn-fetch-blob/ios/KeyShareConsumer/KeyChainDataSource.h"; sourceTree = ""; }; + 7F240ADA220E089300637665 /* Item.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Item.swift; sourceTree = ""; }; + 7F240ADC220E094A00637665 /* TeamsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeamsViewController.swift; sourceTree = ""; }; 7F26919B1EE1DC51007574FE /* RNNotifications.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RNNotifications.xcodeproj; path = "../node_modules/react-native-notifications/RNNotifications/RNNotifications.xcodeproj"; sourceTree = ""; }; 7F26C1C6219C463300FEB42D /* RNCWebView.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RNCWebView.xcodeproj; path = "../node_modules/react-native-webview/ios/RNCWebView.xcodeproj"; sourceTree = ""; }; 7F292A701E8AB73400A450A3 /* SplashScreenResource */ = {isa = PBXFileReference; lastKnownFileType = folder; path = SplashScreenResource; sourceTree = ""; }; 7F292AA41E8ABB1100A450A3 /* LaunchScreen.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; name = LaunchScreen.xib; path = SplashScreenResource/LaunchScreen.xib; sourceTree = ""; }; 7F292AA51E8ABB1100A450A3 /* splash.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = splash.png; path = SplashScreenResource/splash.png; sourceTree = ""; }; - 7F380D0A1FDB28160061AAD2 /* MattermostShare.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = MattermostShare.entitlements; sourceTree = ""; }; 7F43D5DF1F6BF994001FC614 /* libRNSVG.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libRNSVG.a; path = "../../../../../../../Library/Developer/Xcode/DerivedData/Mattermost-czlinsdviifujheezzjvmisotjrm/Build/Products/Debug-iphonesimulator/libRNSVG.a"; sourceTree = ""; }; 7F43D6051F6BF9EB001FC614 /* libPods-Mattermost.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = "libPods-Mattermost.a"; path = "../../../../../../../Library/Developer/Xcode/DerivedData/Mattermost-czlinsdviifujheezzjvmisotjrm/Build/Products/Debug-iphonesimulator/libPods-Mattermost.a"; sourceTree = ""; }; - 7F50C968203C6E80007CA374 /* SessionManager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SessionManager.h; sourceTree = ""; }; - 7F50C969203C6E80007CA374 /* SessionManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SessionManager.m; sourceTree = ""; }; 7F54ABFAE6CE4A6DB11D1ED7 /* Roboto-BlackItalic.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "Roboto-BlackItalic.ttf"; path = "../assets/fonts/Roboto-BlackItalic.ttf"; sourceTree = ""; }; 7F5CA956208FE38F004F91CE /* RNDocumentPicker.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RNDocumentPicker.xcodeproj; path = "../node_modules/react-native-document-picker/ios/RNDocumentPicker.xcodeproj"; sourceTree = ""; }; 7F63D27B1E6C957C001FAE12 /* RCTPushNotification.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTPushNotification.xcodeproj; path = "../node_modules/react-native/Libraries/PushNotificationIOS/RCTPushNotification.xcodeproj"; sourceTree = ""; }; 7F63D2C21E6DD98A001FAE12 /* Mattermost.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; name = Mattermost.entitlements; path = Mattermost/Mattermost.entitlements; sourceTree = ""; }; 7F642DE72093530B00F3165E /* RNDeviceInfo.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RNDeviceInfo.xcodeproj; path = "../node_modules/react-native-device-info/ios/RNDeviceInfo.xcodeproj"; sourceTree = ""; }; - 7F6C47A31FE87E8C00F5A912 /* PerformRequests.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PerformRequests.h; sourceTree = ""; }; - 7F6C47A41FE87E8C00F5A912 /* PerformRequests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = PerformRequests.m; sourceTree = ""; }; + 7F72F2ED2211220500F98FFF /* GenericPreview.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = GenericPreview.xib; sourceTree = ""; }; + 7F72F2EF221123E200F98FFF /* GenericPreview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenericPreview.swift; sourceTree = ""; }; + 7F72F2F222112EC700F98FFF /* generic.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = generic.png; sourceTree = ""; }; 7F7D7F52201645D300D31155 /* ReactNativePermissions.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = ReactNativePermissions.xcodeproj; path = "../node_modules/react-native-permissions/ios/ReactNativePermissions.xcodeproj"; sourceTree = ""; }; - 7F9512EA208646CD00616E3E /* 0155-keys.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "0155-keys.png"; sourceTree = ""; }; + 7FABDFC12211A39000D0F595 /* Section.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Section.swift; sourceTree = ""; }; + 7FABE0092212650600D0F595 /* ChannelsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChannelsViewController.swift; sourceTree = ""; }; + 7FABE04022137F2900D0F595 /* UploadAttachments.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = UploadAttachments.xcodeproj; path = UploadAttachments/UploadAttachments.xcodeproj; sourceTree = ""; }; 7FC200BC1EBB65100099331B /* ReactNativeNavigation.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = ReactNativeNavigation.xcodeproj; path = "../node_modules/react-native-navigation/ios/ReactNativeNavigation.xcodeproj"; sourceTree = ""; }; - 7FCC63CB221C53BB002EC8A5 /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; }; - 7FD8DE972029ECDE001AAC5E /* RNTableView.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RNTableView.xcodeproj; path = "../node_modules/react-native-tableview/RNTableView.xcodeproj"; sourceTree = ""; }; 7FDB92751F706F45006CDFD1 /* RNImagePicker.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RNImagePicker.xcodeproj; path = "../node_modules/react-native-image-picker/ios/RNImagePicker.xcodeproj"; sourceTree = ""; }; 7FEB10961F6101710039A015 /* BlurAppScreen.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = BlurAppScreen.h; path = Mattermost/BlurAppScreen.h; sourceTree = ""; }; 7FEB10971F6101710039A015 /* BlurAppScreen.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = BlurAppScreen.m; path = Mattermost/BlurAppScreen.m; sourceTree = ""; }; @@ -759,13 +757,6 @@ 7FEB109C1F61019C0039A015 /* UIImage+ImageEffects.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = "UIImage+ImageEffects.m"; path = "Mattermost/UIImage+ImageEffects.m"; sourceTree = ""; }; 7FF2AF8A2086483E00FFBDF4 /* KeyShareConsumer.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = KeyShareConsumer.storyboard; sourceTree = ""; }; 7FF31C0E21330B4200680B75 /* RNFetchBlob.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RNFetchBlob.xcodeproj; path = "../node_modules/rn-fetch-blob/ios/RNFetchBlob.xcodeproj"; sourceTree = ""; }; - 7FF7BE6B1FDEE5E8005E55FE /* MattermostBucket.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MattermostBucket.h; sourceTree = ""; }; - 7FF7BE6C1FDEE5E8005E55FE /* MattermostBucket.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MattermostBucket.m; sourceTree = ""; }; - 7FFE329A1FD9CB640038C7A0 /* MattermostShare.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = MattermostShare.appex; sourceTree = BUILT_PRODUCTS_DIR; }; - 7FFE329C1FD9CB650038C7A0 /* ShareViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ShareViewController.h; sourceTree = ""; }; - 7FFE329D1FD9CB650038C7A0 /* ShareViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ShareViewController.m; sourceTree = ""; }; - 7FFE32A01FD9CB650038C7A0 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/MainInterface.storyboard; sourceTree = ""; }; - 7FFE32A21FD9CB650038C7A0 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 7FFE32B51FD9CCAA0038C7A0 /* FLAnimatedImage.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = FLAnimatedImage.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 7FFE32B61FD9CCAA0038C7A0 /* KSCrash.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = KSCrash.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 7FFE32B71FD9CCAA0038C7A0 /* KSCrash.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = KSCrash.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -822,6 +813,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 7FABE04622137F5C00D0F595 /* libUploadAttachments.a in Frameworks */, 74D116AD208A8D5600CF8A79 /* libRNKeychain.a in Frameworks */, 37ABD3C81F4CE142001FDE6B /* libART.a in Frameworks */, 375218501F4B9EE70035444B /* libRCTCameraRoll.a in Frameworks */, @@ -854,38 +846,22 @@ 7FDB92B11F706F58006CDFD1 /* libRNImagePicker.a in Frameworks */, 7F5CA9A0208FE3B9004F91CE /* libRNDocumentPicker.a in Frameworks */, 7F43D5DE1F6BF96A001FC614 /* libRCTYouTube.a in Frameworks */, - 7F97F2312217989F00F5BCCC /* libRNCWebView.a in Frameworks */, + 7F26C1CC219C463D00FEB42D /* libRNCWebView.a in Frameworks */, + 7F43D6061F6BF9EB001FC614 /* libPods-Mattermost.a in Frameworks */, + A9B746D2CFA9CEBFB8AE2B5B /* libPods-Mattermost.a in Frameworks */, 5A0920184BD344979BCFCD5C /* libRCTVideo.a in Frameworks */, 3F876A0DC6314E27B16C8A96 /* libRNReactNativeDocViewer.a in Frameworks */, 8D26455C994F46C39B1392F2 /* libRNSafeArea.a in Frameworks */, C3BD62ED5B5940AB8C0AC678 /* libz.tbd in Frameworks */, 3F55075810254369AB39F032 /* libRNGestureHandler.a in Frameworks */, - 7F43D6061F6BF9EB001FC614 /* libPods-Mattermost.a in Frameworks */, - A9B746D2CFA9CEBFB8AE2B5B /* libPods-Mattermost.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; - 7FFE32971FD9CB640038C7A0 /* Frameworks */ = { + 7F240A16220D3A2300637665 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 7FCC63CC221C53BC002EC8A5 /* JavaScriptCore.framework in Frameworks */, - 7FCC63CA221C5291002EC8A5 /* libReact.a in Frameworks */, - 7FFE32E71FD9CCD00038C7A0 /* libART.a in Frameworks */, - 7F642DF1209353A400F3165E /* libRNDeviceInfo.a in Frameworks */, - 7FF7BE6F1FDF3CE4005E55FE /* libRNVectorIcons.a in Frameworks */, - 7F380D0B1FDB3CFC0061AAD2 /* libRCTVideo.a in Frameworks */, - 7FFE32F11FD9D64E0038C7A0 /* libRCTWebSocket.a in Frameworks */, - 7FFE32EE1FD9CD800038C7A0 /* libRNLocalAuth.a in Frameworks */, - 7FFE32EF1FD9CD800038C7A0 /* libRNPasscodeStatus.a in Frameworks */, - 7F3F66021FE426EE0085CA0E /* libRNSVG.a in Frameworks */, - 7FFE32ED1FD9CD450038C7A0 /* libRCTNetwork.a in Frameworks */, - 7FF31C1521330BAD00680B75 /* libRNFetchBlob.a in Frameworks */, - 7FFE32EC1FD9CD360038C7A0 /* libRCTText.a in Frameworks */, - 7FFE32EB1FD9CD170038C7A0 /* libRCTImage.a in Frameworks */, - 7FFE32E91FD9CCF40038C7A0 /* libRCTAnimation.a in Frameworks */, - 7FFE32E81FD9CCDE0038C7A0 /* libRCTCameraRoll.a in Frameworks */, - 7FD8DEDE2029EDB7001AAC5E /* libRNTableView.a in Frameworks */, + 7FABE0562213884700D0F595 /* libUploadAttachments.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1019,23 +995,25 @@ 13B07FAE1A68108700A75B9A /* Mattermost */ = { isa = PBXGroup; children = ( + 7F151D43221B082A00FAD8F3 /* Mattermost-Bridging-Header.h */, 13B07FAF1A68108700A75B9A /* AppDelegate.h */, 13B07FB01A68108700A75B9A /* AppDelegate.m */, 7FEB10961F6101710039A015 /* BlurAppScreen.h */, 7FEB10971F6101710039A015 /* BlurAppScreen.m */, + 7F151D3D221B062700FAD8F3 /* Dummy.swift */, 13B07FB51A68108700A75B9A /* Images.xcassets */, 13B07FB61A68108700A75B9A /* Info.plist */, 7F292AA41E8ABB1100A450A3 /* LaunchScreen.xib */, 7FF2AF8A2086483E00FFBDF4 /* KeyShareConsumer.storyboard */, 13B07FB71A68108700A75B9A /* main.m */, 7F63D2C21E6DD98A001FAE12 /* Mattermost.entitlements */, - 7FF7BE6B1FDEE5E8005E55FE /* MattermostBucket.h */, - 7FF7BE6C1FDEE5E8005E55FE /* MattermostBucket.m */, + 7F240ACB220D460300637665 /* MattermostBucketModule.h */, + 7F240ACC220D460300637665 /* MattermostBucketModule.m */, 7FEB10991F61019C0039A015 /* MattermostManaged.h */, 7FEB109A1F61019C0039A015 /* MattermostManaged.m */, 7FEB109B1F61019C0039A015 /* UIImage+ImageEffects.h */, 7FEB109C1F61019C0039A015 /* UIImage+ImageEffects.m */, - 7F9512EA208646CD00616E3E /* 0155-keys.png */, + 7F151D40221B069200FAD8F3 /* 0155-keys.png */, 7F292AA51E8ABB1100A450A3 /* splash.png */, ); name = Mattermost; @@ -1056,10 +1034,10 @@ 37DF8AF41F5F0D430079BF89 /* libthird-party.a */, 37DF8AF61F5F0D430079BF89 /* libdouble-conversion.a */, 37DF8AF81F5F0D430079BF89 /* libdouble-conversion.a */, - 7FCC63AB221C5270002EC8A5 /* libjsi.a */, - 7FCC63AD221C5270002EC8A5 /* libjsiexecutor.a */, - 7FCC63AF221C5270002EC8A5 /* libjsi-tvOS.a */, - 7FCC63B1221C5270002EC8A5 /* libjsiexecutor-tvOS.a */, + 7F581C72221DF8DE0099E66B /* libjsi.a */, + 7F581C74221DF8DE0099E66B /* libjsiexecutor.a */, + 7F581C76221DF8DE0099E66B /* libjsi-tvOS.a */, + 7F581C78221DF8DE0099E66B /* libjsiexecutor-tvOS.a */, ); name = Products; sourceTree = ""; @@ -1120,7 +1098,6 @@ 4B992D7BAAEDF8759DB525B5 /* Frameworks */ = { isa = PBXGroup; children = ( - 7FCC63CB221C53BB002EC8A5 /* JavaScriptCore.framework */, 74D116A3208A8C8400CF8A79 /* libRNKeychain.a */, 7FFE32B51FD9CCAA0038C7A0 /* FLAnimatedImage.framework */, 7FFE32B61FD9CCAA0038C7A0 /* KSCrash.framework */, @@ -1188,6 +1165,27 @@ name = Products; sourceTree = ""; }; + 7F240A1A220D3A2300637665 /* MattermostShare */ = { + isa = PBXGroup; + children = ( + 7F72F2E4221113DF00F98FFF /* Images */, + 7F240A20220D3A2300637665 /* Info.plist */, + 7F240ACF220D4A6100637665 /* KeyChainDataSource.h */, + 7F240ACE220D4A6100637665 /* KeyChainDataSource.mm */, + 7F240A27220D3A7E00637665 /* MattermostShare.entitlements */, + 7F240A1D220D3A2300637665 /* MainInterface.storyboard */, + 7F151D42221B07F700FAD8F3 /* MattermostShare-Bridging-Header.h */, + 7F240ADA220E089300637665 /* Item.swift */, + 7FABE0092212650600D0F595 /* ChannelsViewController.swift */, + 7F240A1B220D3A2300637665 /* ShareViewController.swift */, + 7F240ADC220E094A00637665 /* TeamsViewController.swift */, + 7F72F2ED2211220500F98FFF /* GenericPreview.xib */, + 7F72F2EF221123E200F98FFF /* GenericPreview.swift */, + 7FABDFC12211A39000D0F595 /* Section.swift */, + ); + path = MattermostShare; + sourceTree = ""; + }; 7F26919C1EE1DC51007574FE /* Products */ = { isa = PBXGroup; children = ( @@ -1246,6 +1244,14 @@ name = Products; sourceTree = ""; }; + 7F72F2E4221113DF00F98FFF /* Images */ = { + isa = PBXGroup; + children = ( + 7F72F2F222112EC700F98FFF /* generic.png */, + ); + path = Images; + sourceTree = ""; + }; 7F7D7F53201645D300D31155 /* Products */ = { isa = PBXGroup; children = ( @@ -1295,18 +1301,18 @@ name = Products; sourceTree = ""; }; - 7FC200BD1EBB65100099331B /* Products */ = { + 7FABE04122137F2900D0F595 /* Products */ = { isa = PBXGroup; children = ( - 7FC200DF1EBB65100099331B /* libReactNativeNavigation.a */, + 7FABE04522137F2A00D0F595 /* libUploadAttachments.a */, ); name = Products; sourceTree = ""; }; - 7FD8DE982029ECDE001AAC5E /* Products */ = { + 7FC200BD1EBB65100099331B /* Products */ = { isa = PBXGroup; children = ( - 7FD8DEDA2029ECDF001AAC5E /* libRNTableView.a */, + 7FC200DF1EBB65100099331B /* libReactNativeNavigation.a */, ); name = Products; sourceTree = ""; @@ -1361,32 +1367,16 @@ name = Products; sourceTree = ""; }; - 7FFE329B1FD9CB650038C7A0 /* MattermostShare */ = { - isa = PBXGroup; - children = ( - 7F380D0A1FDB28160061AAD2 /* MattermostShare.entitlements */, - 7FFE329C1FD9CB650038C7A0 /* ShareViewController.h */, - 7FFE329D1FD9CB650038C7A0 /* ShareViewController.m */, - 7FFE329F1FD9CB650038C7A0 /* MainInterface.storyboard */, - 7FFE32A21FD9CB650038C7A0 /* Info.plist */, - 7F6C47A31FE87E8C00F5A912 /* PerformRequests.h */, - 7F6C47A41FE87E8C00F5A912 /* PerformRequests.m */, - 7F50C968203C6E80007CA374 /* SessionManager.h */, - 7F50C969203C6E80007CA374 /* SessionManager.m */, - ); - path = MattermostShare; - sourceTree = ""; - }; 832341AE1AAA6A7D00B99B32 /* Libraries */ = { isa = PBXGroup; children = ( + 7FABE04022137F2900D0F595 /* UploadAttachments.xcodeproj */, 7F0AA87621A6138200E4370E /* RNSentry.xcodeproj */, 7F26C1C6219C463300FEB42D /* RNCWebView.xcodeproj */, 7FF31C0E21330B4200680B75 /* RNFetchBlob.xcodeproj */, 7F5CA956208FE38F004F91CE /* RNDocumentPicker.xcodeproj */, 7F642DE72093530B00F3165E /* RNDeviceInfo.xcodeproj */, 74D116A4208A8D3000CF8A79 /* RNKeychain.xcodeproj */, - 7FD8DE972029ECDE001AAC5E /* RNTableView.xcodeproj */, 7F7D7F52201645D300D31155 /* ReactNativePermissions.xcodeproj */, 7FDB92751F706F45006CDFD1 /* RNImagePicker.xcodeproj */, 37ABD3971F4CE13B001FDE6B /* ART.xcodeproj */, @@ -1434,11 +1424,11 @@ 83CBB9F61A601CBA00E9B192 = { isa = PBXGroup; children = ( - 7F292A701E8AB73400A450A3 /* SplashScreenResource */, 13B07FAE1A68108700A75B9A /* Mattermost */, + 7F240A1A220D3A2300637665 /* MattermostShare */, + 7F292A701E8AB73400A450A3 /* SplashScreenResource */, 832341AE1AAA6A7D00B99B32 /* Libraries */, 00E356EF1AD99517003FC87E /* MattermostTests */, - 7FFE329B1FD9CB650038C7A0 /* MattermostShare */, 83CBBA001A601CBA00E9B192 /* Products */, 0156F464626F49C2977D7982 /* Resources */, 37DF8AC51F5F0D410079BF89 /* Recovered References */, @@ -1454,7 +1444,7 @@ children = ( 13B07F961A680F5B00A75B9A /* Mattermost.app */, 00E356EE1AD99517003FC87E /* MattermostTests.xctest */, - 7FFE329A1FD9CB640038C7A0 /* MattermostShare.appex */, + 7F240A19220D3A2300637665 /* MattermostShare.appex */, ); name = Products; sourceTree = ""; @@ -1497,21 +1487,20 @@ buildRules = ( ); dependencies = ( - 7FFE32A41FD9CB650038C7A0 /* PBXTargetDependency */, + 7F240A22220D3A2300637665 /* PBXTargetDependency */, ); name = Mattermost; productName = "Hello World"; productReference = 13B07F961A680F5B00A75B9A /* Mattermost.app */; productType = "com.apple.product-type.application"; }; - 7FFE32991FD9CB640038C7A0 /* MattermostShare */ = { + 7F240A18220D3A2300637665 /* MattermostShare */ = { isa = PBXNativeTarget; - buildConfigurationList = 7FFE32A61FD9CB650038C7A0 /* Build configuration list for PBXNativeTarget "MattermostShare" */; + buildConfigurationList = 7F240A24220D3A2300637665 /* Build configuration list for PBXNativeTarget "MattermostShare" */; buildPhases = ( - 7FFE32961FD9CB640038C7A0 /* Sources */, - 7FFE32971FD9CB640038C7A0 /* Frameworks */, - 7FFE32981FD9CB640038C7A0 /* Resources */, - 7FFE32F01FD9D1F00038C7A0 /* Bundle React Native code and images */, + 7F240A15220D3A2300637665 /* Sources */, + 7F240A16220D3A2300637665 /* Frameworks */, + 7F240A17220D3A2300637665 /* Resources */, ); buildRules = ( ); @@ -1519,7 +1508,7 @@ ); name = MattermostShare; productName = MattermostShare; - productReference = 7FFE329A1FD9CB640038C7A0 /* MattermostShare.appex */; + productReference = 7F240A19220D3A2300637665 /* MattermostShare.appex */; productType = "com.apple.product-type.app-extension"; }; /* End PBXNativeTarget section */ @@ -1528,6 +1517,7 @@ 83CBB9F71A601CBA00E9B192 /* Project object */ = { isa = PBXProject; attributes = { + LastSwiftUpdateCheck = 1010; LastUpgradeCheck = 820; ORGANIZATIONNAME = Facebook; TargetAttributes = { @@ -1537,6 +1527,7 @@ }; 13B07F861A680F5B00A75B9A = { DevelopmentTeam = UQ8HT4Q2XM; + LastSwiftMigration = 1010; ProvisioningStyle = Automatic; SystemCapabilities = { com.apple.ApplicationGroups.iOS = { @@ -1556,21 +1547,14 @@ }; }; }; - 7FFE32991FD9CB640038C7A0 = { - CreatedOnToolsVersion = 9.1; + 7F240A18220D3A2300637665 = { + CreatedOnToolsVersion = 10.1; DevelopmentTeam = UQ8HT4Q2XM; - LastSwiftMigration = 920; ProvisioningStyle = Automatic; SystemCapabilities = { com.apple.ApplicationGroups.iOS = { enabled = 1; }; - com.apple.Keychain = { - enabled = 1; - }; - com.apple.iCloud = { - enabled = 1; - }; }; }; }; @@ -1731,20 +1715,20 @@ ProductGroup = 7FDF28C41E1F4B1F00DBBE56 /* Products */; ProjectRef = 2CBE9C0FB56E4FDA96C30792 /* RNSVG.xcodeproj */; }, - { - ProductGroup = 7FD8DE982029ECDE001AAC5E /* Products */; - ProjectRef = 7FD8DE972029ECDE001AAC5E /* RNTableView.xcodeproj */; - }, { ProductGroup = 7FDF28EE1E1F4B4E00DBBE56 /* Products */; ProjectRef = 2B25899FDAC149EB96ED3305 /* RNVectorIcons.xcodeproj */; }, + { + ProductGroup = 7FABE04122137F2900D0F595 /* Products */; + ProjectRef = 7FABE04022137F2900D0F595 /* UploadAttachments.xcodeproj */; + }, ); projectRoot = ""; targets = ( 13B07F861A680F5B00A75B9A /* Mattermost */, 00E356ED1AD99517003FC87E /* MattermostTests */, - 7FFE32991FD9CB640038C7A0 /* MattermostShare */, + 7F240A18220D3A2300637665 /* MattermostShare */, ); }; /* End PBXProject section */ @@ -2044,6 +2028,34 @@ remoteRef = 7F4C1F851FBE572B0029D1DF /* PBXContainerItemProxy */; sourceTree = BUILT_PRODUCTS_DIR; }; + 7F581C72221DF8DE0099E66B /* libjsi.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = libjsi.a; + remoteRef = 7F581C71221DF8DE0099E66B /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + 7F581C74221DF8DE0099E66B /* libjsiexecutor.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = libjsiexecutor.a; + remoteRef = 7F581C73221DF8DE0099E66B /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + 7F581C76221DF8DE0099E66B /* libjsi-tvOS.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = "libjsi-tvOS.a"; + remoteRef = 7F581C75221DF8DE0099E66B /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + 7F581C78221DF8DE0099E66B /* libjsiexecutor-tvOS.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = "libjsiexecutor-tvOS.a"; + remoteRef = 7F581C77221DF8DE0099E66B /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; 7F5CA991208FE38F004F91CE /* libRNDocumentPicker.a */ = { isa = PBXReferenceProxy; fileType = archive.ar; @@ -2142,6 +2154,13 @@ remoteRef = 7FA7950A1F61A1A500C02924 /* PBXContainerItemProxy */; sourceTree = BUILT_PRODUCTS_DIR; }; + 7FABE04522137F2A00D0F595 /* libUploadAttachments.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = libUploadAttachments.a; + remoteRef = 7FABE04422137F2A00D0F595 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; 7FC200DF1EBB65100099331B /* libReactNativeNavigation.a */ = { isa = PBXReferenceProxy; fileType = archive.ar; @@ -2149,41 +2168,6 @@ remoteRef = 7FC200DE1EBB65100099331B /* PBXContainerItemProxy */; sourceTree = BUILT_PRODUCTS_DIR; }; - 7FCC63AB221C5270002EC8A5 /* libjsi.a */ = { - isa = PBXReferenceProxy; - fileType = archive.ar; - path = libjsi.a; - remoteRef = 7FCC63AA221C5270002EC8A5 /* PBXContainerItemProxy */; - sourceTree = BUILT_PRODUCTS_DIR; - }; - 7FCC63AD221C5270002EC8A5 /* libjsiexecutor.a */ = { - isa = PBXReferenceProxy; - fileType = archive.ar; - path = libjsiexecutor.a; - remoteRef = 7FCC63AC221C5270002EC8A5 /* PBXContainerItemProxy */; - sourceTree = BUILT_PRODUCTS_DIR; - }; - 7FCC63AF221C5270002EC8A5 /* libjsi-tvOS.a */ = { - isa = PBXReferenceProxy; - fileType = archive.ar; - path = "libjsi-tvOS.a"; - remoteRef = 7FCC63AE221C5270002EC8A5 /* PBXContainerItemProxy */; - sourceTree = BUILT_PRODUCTS_DIR; - }; - 7FCC63B1221C5270002EC8A5 /* libjsiexecutor-tvOS.a */ = { - isa = PBXReferenceProxy; - fileType = archive.ar; - path = "libjsiexecutor-tvOS.a"; - remoteRef = 7FCC63B0221C5270002EC8A5 /* PBXContainerItemProxy */; - sourceTree = BUILT_PRODUCTS_DIR; - }; - 7FD8DEDA2029ECDF001AAC5E /* libRNTableView.a */ = { - isa = PBXReferenceProxy; - fileType = archive.ar; - path = libRNTableView.a; - remoteRef = 7FD8DED92029ECDF001AAC5E /* PBXContainerItemProxy */; - sourceTree = BUILT_PRODUCTS_DIR; - }; 7FDB92A71F706F45006CDFD1 /* libRNImagePicker.a */ = { isa = PBXReferenceProxy; fileType = archive.ar; @@ -2254,6 +2238,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + 7F151D41221B069200FAD8F3 /* 0155-keys.png in Resources */, 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */, 895C9A56B94A45C1BAF568FE /* Entypo.ttf in Resources */, 3D38ABA732A34A9BB3294F90 /* EvilIcons.ttf in Resources */, @@ -2273,7 +2258,6 @@ 55C6561DDBBA45929D88B6D1 /* OpenSans-BoldItalic.ttf in Resources */, D719A67137964F08BE47A5FC /* OpenSans-ExtraBold.ttf in Resources */, 0111A42B7F264BCF8CBDE3ED /* OpenSans-ExtraBoldItalic.ttf in Resources */, - 7F95132F208646CE00616E3E /* 0155-keys.png in Resources */, 0C0D24F53F254F75869E5951 /* OpenSans-Italic.ttf in Resources */, 2D5296A8926B4D7FBAF2D6E2 /* OpenSans-Light.ttf in Resources */, F083DB472349411A8E6E7AAD /* OpenSans-LightItalic.ttf in Resources */, @@ -2301,15 +2285,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - 7FFE32981FD9CB640038C7A0 /* Resources */ = { + 7F240A17220D3A2300637665 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - 7FC649F01FE9B5D90074E4C7 /* OpenSans-Bold.ttf in Resources */, - 7FC649EE1FE983660074E4C7 /* EvilIcons.ttf in Resources */, - 7FF7BE701FDF3EE7005E55FE /* Ionicons.ttf in Resources */, - 7FFDB3191FE3566C009E3BCF /* FontAwesome.ttf in Resources */, - 7FFE32A11FD9CB650038C7A0 /* MainInterface.storyboard in Resources */, + 7F240A1F220D3A2300637665 /* MainInterface.storyboard in Resources */, + 7F72F2EE2211220500F98FFF /* GenericPreview.xib in Resources */, + 7F72F2F322112EC700F98FFF /* generic.png in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -2348,20 +2330,6 @@ shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; - 7FFE32F01FD9D1F00038C7A0 /* Bundle React Native code and images */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - ); - name = "Bundle React Native code and images"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = ./bundleReactNative.sh; - }; AE4769B235D14E6C9C64EA78 /* Upload Debug Symbols to Sentry */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -2410,24 +2378,26 @@ buildActionMask = 2147483647; files = ( 13B07FBC1A68108700A75B9A /* AppDelegate.m in Sources */, + 7F151D3E221B062700FAD8F3 /* Dummy.swift in Sources */, 7FEB109E1F61019C0039A015 /* UIImage+ImageEffects.m in Sources */, 13B07FC11A68108700A75B9A /* main.m in Sources */, - 7FF7BE6D1FDEE5E8005E55FE /* MattermostBucket.m in Sources */, 7FEB10981F6101710039A015 /* BlurAppScreen.m in Sources */, - 7F50C96A203C6E80007CA374 /* SessionManager.m in Sources */, 7FEB109D1F61019C0039A015 /* MattermostManaged.m in Sources */, + 7F240ACD220D460300637665 /* MattermostBucketModule.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; - 7FFE32961FD9CB640038C7A0 /* Sources */ = { + 7F240A15220D3A2300637665 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 7F50C96B203C6E80007CA374 /* SessionManager.m in Sources */, - 7FF7BE6E1FDEE5E8005E55FE /* MattermostBucket.m in Sources */, - 7FF7BE2C1FDEE4AE005E55FE /* MattermostManaged.m in Sources */, - 7FFE329E1FD9CB650038C7A0 /* ShareViewController.m in Sources */, - 7F6C47A51FE87E8C00F5A912 /* PerformRequests.m in Sources */, + 7F240AD0220D4A6100637665 /* KeyChainDataSource.mm in Sources */, + 7F72F2F0221123E200F98FFF /* GenericPreview.swift in Sources */, + 7F240ADB220E089300637665 /* Item.swift in Sources */, + 7F240A1C220D3A2300637665 /* ShareViewController.swift in Sources */, + 7FABDFC22211A39000D0F595 /* Section.swift in Sources */, + 7FABE00A2212650600D0F595 /* ChannelsViewController.swift in Sources */, + 7F240ADD220E094A00637665 /* TeamsViewController.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -2439,18 +2409,18 @@ target = 13B07F861A680F5B00A75B9A /* Mattermost */; targetProxy = 00E356F41AD99517003FC87E /* PBXContainerItemProxy */; }; - 7FFE32A41FD9CB650038C7A0 /* PBXTargetDependency */ = { + 7F240A22220D3A2300637665 /* PBXTargetDependency */ = { isa = PBXTargetDependency; - target = 7FFE32991FD9CB640038C7A0 /* MattermostShare */; - targetProxy = 7FFE32A31FD9CB650038C7A0 /* PBXContainerItemProxy */; + target = 7F240A18220D3A2300637665 /* MattermostShare */; + targetProxy = 7F240A21220D3A2300637665 /* PBXContainerItemProxy */; }; /* End PBXTargetDependency section */ /* Begin PBXVariantGroup section */ - 7FFE329F1FD9CB650038C7A0 /* MainInterface.storyboard */ = { + 7F240A1D220D3A2300637665 /* MainInterface.storyboard */ = { isa = PBXVariantGroup; children = ( - 7FFE32A01FD9CB650038C7A0 /* Base */, + 7F240A1E220D3A2300637665 /* Base */, ); name = MainInterface.storyboard; sourceTree = ""; @@ -2462,6 +2432,7 @@ isa = XCBuildConfiguration; baseConfigurationReference = 3820CC3BBC4B0129958555A9 /* Pods-MattermostTests.debug.xcconfig */; buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; BUNDLE_LOADER = "$(TEST_HOST)"; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", @@ -2496,6 +2467,7 @@ isa = XCBuildConfiguration; baseConfigurationReference = 41465DE822E9429EB8B90CB4 /* Pods-MattermostTests.release.xcconfig */; buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; BUNDLE_LOADER = "$(TEST_HOST)"; COPY_PHASE_STRIP = NO; HEADER_SEARCH_PATHS = ( @@ -2527,11 +2499,13 @@ isa = XCBuildConfiguration; baseConfigurationReference = 634A8F047C73D24A87850EC0 /* Pods-Mattermost.debug.xcconfig */; buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Mattermost/Mattermost.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 176; + CURRENT_PROJECT_VERSION = 175; DEAD_CODE_STRIPPING = NO; DEVELOPMENT_TEAM = UQ8HT4Q2XM; ENABLE_BITCODE = NO; @@ -2557,6 +2531,7 @@ "$(SRCROOT)../node_modules/react-native-svg/ios/**", "$(SRCROOT)/../node_modules/react-native-sentry/ios/**", "$(SRCROOT)/../node_modules/react-native-gesture-handler/ios/**", + "$(SRCROOT)/UploadAttachments/UploadAttachments/**", ); INFOPLIST_FILE = Mattermost/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 10.3; @@ -2570,6 +2545,9 @@ PRODUCT_NAME = Mattermost; PROVISIONING_PROFILE = ""; PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OBJC_BRIDGING_HEADER = "Mattermost/Mattermost-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 4.2; TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; }; @@ -2579,11 +2557,13 @@ isa = XCBuildConfiguration; baseConfigurationReference = BFB7025EA936C1B5DC9725C2 /* Pods-Mattermost.release.xcconfig */; buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Mattermost/Mattermost.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 176; + CURRENT_PROJECT_VERSION = 175; DEAD_CODE_STRIPPING = NO; DEVELOPMENT_TEAM = UQ8HT4Q2XM; ENABLE_BITCODE = NO; @@ -2609,6 +2589,7 @@ "$(SRCROOT)../node_modules/react-native-svg/ios/**", "$(SRCROOT)/../node_modules/react-native-sentry/ios/**", "$(SRCROOT)/../node_modules/react-native-gesture-handler/ios/**", + "$(SRCROOT)/UploadAttachments/UploadAttachments/**", ); INFOPLIST_FILE = Mattermost/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 10.3; @@ -2622,129 +2603,90 @@ PRODUCT_NAME = Mattermost; PROVISIONING_PROFILE = ""; PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OBJC_BRIDGING_HEADER = "Mattermost/Mattermost-Bridging-Header.h"; + SWIFT_VERSION = 4.2; TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; }; name = Release; }; - 7FFE32A71FD9CB650038C7A0 /* Debug */ = { + 7F240A25220D3A2300637665 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_WEAK = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_COMMA = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CODE_SIGN_ENTITLEMENTS = MattermostShare/MattermostShare.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = UQ8HT4Q2XM; GCC_C_LANGUAGE_STANDARD = gnu11; - HEADER_SEARCH_PATHS = ( - "$(SRCROOT)/../node_modules/react-native/Libraries/PushNotificationIOS/**", - "$(SRCROOT)/../node_modules/react-native-navigation/ios/**", - "$(SRCROOT)/../node_modules/react-native-notifications/RNNotifications/**", - "$(SRCROOT)/../node_modules/react-native-local-auth", - "$(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-navigation/ios/**", - "$(SRCROOT)/../node_modules/react-native-local-auth/**", - "$(SRCROOT)/../node_modules/react-native-passcode-status/ios/**", - "$(SRCROOT)/../node_modules/jail-monkey/JailMonkey/**", - "$(SRCROOT)/../node_modules/rn-fetch-blob/ios/**", - "$(SRCROOT)/../node_modules/react-native-cookies/ios/RNCookieManagerIOS/**", - "$(SRCROOT)/../node_modules/react-native-video/ios/**", - "$(SRCROOT)/../node_modules/react-native-tableview/**", - "$(SRCROOT)/../node_modules/react-native-device-info/ios/**", - "$(SRCROOT)/../node_modules/react-native-gesture-handler/ios/**", - ); + HEADER_SEARCH_PATHS = "$(SRCROOT)/UploadAttachments/UploadAttachments"; INFOPLIST_FILE = MattermostShare/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 10.3; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; - LIBRARY_SEARCH_PATHS = ( - "$(inherited)", - "\"$(SRCROOT)/$(TARGET_NAME)\"", - ); - OTHER_LDFLAGS = ( - "-ObjC", - "-lc++", - ); + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = com.mattermost.rnbeta.MattermostShare; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OBJC_BRIDGING_HEADER = "MattermostShare/MattermostShare-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 3.0; + SWIFT_VERSION = 4.2; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; }; - 7FFE32A81FD9CB650038C7A0 /* Release */ = { + 7F240A26220D3A2300637665 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_WEAK = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_COMMA = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CODE_SIGN_ENTITLEMENTS = MattermostShare/MattermostShare.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = UQ8HT4Q2XM; GCC_C_LANGUAGE_STANDARD = gnu11; - HEADER_SEARCH_PATHS = ( - "$(SRCROOT)/../node_modules/react-native/Libraries/PushNotificationIOS/**", - "$(SRCROOT)/../node_modules/react-native-navigation/ios/**", - "$(SRCROOT)/../node_modules/react-native-notifications/RNNotifications/**", - "$(SRCROOT)/../node_modules/react-native-local-auth", - "$(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-navigation/ios/**", - "$(SRCROOT)/../node_modules/react-native-local-auth/**", - "$(SRCROOT)/../node_modules/react-native-passcode-status/ios/**", - "$(SRCROOT)/../node_modules/jail-monkey/JailMonkey/**", - "$(SRCROOT)/../node_modules/rn-fetch-blob/ios/**", - "$(SRCROOT)/../node_modules/react-native-cookies/ios/RNCookieManagerIOS/**", - "$(SRCROOT)/../node_modules/react-native-video/ios/**", - "$(SRCROOT)/../node_modules/react-native-tableview/**", - "$(SRCROOT)/../node_modules/react-native-device-info/ios/**", - "$(SRCROOT)/../node_modules/react-native-gesture-handler/ios/**", - ); + HEADER_SEARCH_PATHS = "$(SRCROOT)/UploadAttachments/UploadAttachments"; INFOPLIST_FILE = MattermostShare/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 10.3; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; - LIBRARY_SEARCH_PATHS = ( - "$(inherited)", - "\"$(SRCROOT)/$(TARGET_NAME)\"", - ); - OTHER_LDFLAGS = ( - "-ObjC", - "-lc++", - ); + MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = com.mattermost.rnbeta.MattermostShare; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; SWIFT_OBJC_BRIDGING_HEADER = "MattermostShare/MattermostShare-Bridging-Header.h"; - SWIFT_VERSION = 3.0; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + SWIFT_VERSION = 4.2; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Release; @@ -2854,11 +2796,11 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - 7FFE32A61FD9CB650038C7A0 /* Build configuration list for PBXNativeTarget "MattermostShare" */ = { + 7F240A24220D3A2300637665 /* Build configuration list for PBXNativeTarget "MattermostShare" */ = { isa = XCConfigurationList; buildConfigurations = ( - 7FFE32A71FD9CB650038C7A0 /* Debug */, - 7FFE32A81FD9CB650038C7A0 /* Release */, + 7F240A25220D3A2300637665 /* Debug */, + 7F240A26220D3A2300637665 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; diff --git a/ios/Mattermost.xcodeproj/xcshareddata/xcschemes/Mattermost.xcscheme b/ios/Mattermost.xcodeproj/xcshareddata/xcschemes/Mattermost.xcscheme index bc78e51032..1ad8ca513e 100644 --- a/ios/Mattermost.xcodeproj/xcshareddata/xcschemes/Mattermost.xcscheme +++ b/ios/Mattermost.xcodeproj/xcshareddata/xcschemes/Mattermost.xcscheme @@ -6,6 +6,20 @@ parallelizeBuildables = "NO" buildImplicitDependencies = "YES"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/Mattermost.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/ios/Mattermost.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000000..18d981003d --- /dev/null +++ b/ios/Mattermost.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/ios/Mattermost.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/ios/Mattermost.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000000..0c67376eba --- /dev/null +++ b/ios/Mattermost.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,5 @@ + + + + + diff --git a/ios/0155-keys.png b/ios/Mattermost/0155-keys.png similarity index 100% rename from ios/0155-keys.png rename to ios/Mattermost/0155-keys.png diff --git a/ios/Mattermost/AppDelegate.m b/ios/Mattermost/AppDelegate.m index 312b4b0f99..fb9954efcf 100644 --- a/ios/Mattermost/AppDelegate.m +++ b/ios/Mattermost/AppDelegate.m @@ -19,13 +19,17 @@ #endif #import "RCCManager.h" #import "RNNotifications.h" -#import "SessionManager.h" +#import #import @implementation AppDelegate NSString* const NotificationClearAction = @"clear"; +-(void)application:(UIApplication *)application handleEventsForBackgroundURLSession:(NSString *)identifier completionHandler:(void (^)(void))completionHandler { + [[UploadSession shared] attachSessionWithIdentifier:identifier completionHandler:completionHandler]; +} + - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { // Clear keychain on first run in case of reinstallation @@ -139,11 +143,6 @@ NSString* const NotificationClearAction = @"clear"; [RNNotifications handleActionWithIdentifier:identifier forRemoteNotification:userInfo withResponseInfo:responseInfo completionHandler:completionHandler]; } --(void)application:(UIApplication *)application handleEventsForBackgroundURLSession:(nonnull NSString *)identifier completionHandler:(nonnull void (^)(void))completionHandler { - [SessionManager sharedSession].savedCompletionHandler = completionHandler; - [[SessionManager sharedSession] createSessionForRequestRequest:identifier]; -} - // Required for deeplinking - (BOOL)application:(UIApplication *)application openURL:(NSURL *)url diff --git a/ios/Mattermost/Dummy.swift b/ios/Mattermost/Dummy.swift new file mode 100644 index 0000000000..c4369bc8da --- /dev/null +++ b/ios/Mattermost/Dummy.swift @@ -0,0 +1,6 @@ +// +// Dummy.swift +// Mattermost +// +// DO NOT delete this file as is needed by the project to add support for swift compilation +// diff --git a/ios/Mattermost/Mattermost-Bridging-Header.h b/ios/Mattermost/Mattermost-Bridging-Header.h new file mode 100644 index 0000000000..1b2cb5d6d0 --- /dev/null +++ b/ios/Mattermost/Mattermost-Bridging-Header.h @@ -0,0 +1,4 @@ +// +// Use this file to import your target's public headers that you would like to expose to Swift. +// + diff --git a/ios/Mattermost/MattermostBucketModule.h b/ios/Mattermost/MattermostBucketModule.h new file mode 100644 index 0000000000..ff69dc22ec --- /dev/null +++ b/ios/Mattermost/MattermostBucketModule.h @@ -0,0 +1,7 @@ +#import +#import "React/RCTBridgeModule.h" +#import "MattermostBucket.h" + +@interface MattermostBucketModule : NSObject +@property MattermostBucket *bucket; +@end diff --git a/ios/Mattermost/MattermostBucketModule.m b/ios/Mattermost/MattermostBucketModule.m new file mode 100644 index 0000000000..f17e4b170c --- /dev/null +++ b/ios/Mattermost/MattermostBucketModule.m @@ -0,0 +1,67 @@ +#import "MattermostBucketModule.h" +#import "MattermostBucket.h" + +@implementation MattermostBucketModule + +RCT_EXPORT_MODULE(); + ++(BOOL)requiresMainQueueSetup +{ + return YES; +} + +- (instancetype)init { + self = [super init]; + if (self) { + self.bucket = [[MattermostBucket alloc] init]; + } + return self; +} + +RCT_EXPORT_METHOD(writeToFile:(NSString *)fileName + content:(NSString *)content) { + [self.bucket writeToFile:fileName content:content]; +} + +RCT_EXPORT_METHOD(readFromFile:(NSString *)fileName + getWithResolver:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) +{ + id value = [self.bucket readFromFile:fileName]; + + if (value == nil) { + value = [NSNull null]; + } + + resolve(value); +} + +RCT_EXPORT_METHOD(removeFile:(NSString *)fileName) +{ + [self.bucket removeFile:fileName]; +} + +RCT_EXPORT_METHOD(setPreference:(NSString *) key + value:(NSString *) value) +{ + [self.bucket setPreference:key value:value]; +} + +RCT_EXPORT_METHOD(getPreference:(NSString *) key + getWithResolver:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) +{ + id value = [self.bucket getPreference:key]; + + if (value == nil) { + value = [NSNull null]; + } + + resolve(value); +} + +RCT_EXPORT_METHOD(removePreference:(NSString *) key) +{ + [self.bucket removePreference:key]; +} +@end diff --git a/ios/MattermostBucket.h b/ios/MattermostBucket.h deleted file mode 100644 index 79bda4b8a1..0000000000 --- a/ios/MattermostBucket.h +++ /dev/null @@ -1,7 +0,0 @@ -#import -#import "React/RCTBridgeModule.h" - -@interface MattermostBucket : NSObject -- (NSUserDefaults *)bucketByName:(NSString*)name; --(NSString *)readFromFile:(NSString *)fileName appGroupId:(NSString *)appGroupId; -@end diff --git a/ios/MattermostBucket.m b/ios/MattermostBucket.m deleted file mode 100644 index 3816a94289..0000000000 --- a/ios/MattermostBucket.m +++ /dev/null @@ -1,126 +0,0 @@ -// -// MattermostBucket.m -// Mattermost -// -// Created by Elias Nahum on 12/11/17. -// Copyright © 2017 Facebook. All rights reserved. -// - -#import "MattermostBucket.h" - -@implementation MattermostBucket - -+(BOOL)requiresMainQueueSetup -{ - return YES; -} - -RCT_EXPORT_MODULE(); - -RCT_EXPORT_METHOD(writeToFile:(NSString *)fileName - content:(NSString *)content - bucketName:(NSString *)bucketName) { - [self writeToFile:fileName content:content appGroupId:bucketName]; -} - -RCT_EXPORT_METHOD(readFromFile:(NSString *)fileName - bucketName:(NSString*)bucketName - getWithResolver:(RCTPromiseResolveBlock)resolve - rejecter:(RCTPromiseRejectBlock)reject) -{ - id value = [self readFromFile:fileName appGroupId:bucketName]; - - if (value == nil) { - value = [NSNull null]; - } - - resolve(value); -} - -RCT_EXPORT_METHOD(removeFile:(NSString *)fileName - bucketName:(NSString*)bucketName) -{ - [self removeFile:fileName appGroupId:bucketName]; -} - -RCT_EXPORT_METHOD(setPreference:(NSString *) key - value:(NSString *) value - bucketName:(NSString*) bucketName) -{ - [self setPreference:key value:value appGroupId:bucketName]; -} - -RCT_EXPORT_METHOD(getPreference:(NSString *) key - bucketName:(NSString*) bucketName - getWithResolver:(RCTPromiseResolveBlock)resolve - rejecter:(RCTPromiseRejectBlock)reject) -{ - id value = [self getPreference:key appGroupId:bucketName]; - - if (value == nil) { - value = [NSNull null]; - } - - resolve(value); -} - -RCT_EXPORT_METHOD(removePreference:(NSString *) key - bucketName:(NSString*) bucketName) -{ - [self removePreference:key appGroupId:bucketName]; -} - --(NSString *)fileUrl:(NSString *)fileName appGroupdId:(NSString *)appGroupId { - NSURL *fileManagerURL = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:appGroupId]; - return [NSString stringWithFormat:@"%@/%@", fileManagerURL.path, fileName]; -} - --(void)writeToFile:(NSString *)fileName content:(NSString *)content appGroupId:(NSString *)appGroupId { - NSString *filePath = [self fileUrl:fileName appGroupdId:appGroupId]; - NSFileManager *fileManager= [NSFileManager defaultManager]; - if(![fileManager fileExistsAtPath:filePath]) { - [fileManager createFileAtPath:filePath contents:nil attributes:nil]; - } - if ([content length] > 0) { - [content writeToFile:filePath atomically:YES encoding:NSUTF8StringEncoding error:nil]; - } -} - --(NSString *)readFromFile:(NSString *)fileName appGroupId:(NSString *)appGroupId { - NSString *filePath = [self fileUrl:fileName appGroupdId:appGroupId]; - NSFileManager *fileManager= [NSFileManager defaultManager]; - if(![fileManager fileExistsAtPath:filePath]) { - return nil; - } - return [NSString stringWithContentsOfFile:filePath encoding:NSUTF8StringEncoding error:nil]; -} - --(void)removeFile:(NSString *)fileName appGroupId:(NSString *)appGroupId { - NSString *filePath = [self fileUrl:fileName appGroupdId:appGroupId]; - NSFileManager *fileManager= [NSFileManager defaultManager]; - if([fileManager isDeletableFileAtPath:filePath]) { - [fileManager removeItemAtPath:filePath error:nil]; - } -} - --(NSUserDefaults *)bucketByName:(NSString*)name { - return [[NSUserDefaults alloc] initWithSuiteName: name]; -} - --(void) setPreference:(NSString *)key value:(NSString *) value appGroupId:(NSString*)appGroupId { - NSUserDefaults* bucket = [self bucketByName: appGroupId]; - if (bucket && [key length] > 0 && [value length] > 0) { - [bucket setObject:value forKey:key]; - } -} - --(id) getPreference:(NSString *)key appGroupId:(NSString*)appGroupId { - NSUserDefaults* bucket = [self bucketByName: appGroupId]; - return [bucket objectForKey:key]; -} - --(void) removePreference:(NSString *)key appGroupId:(NSString*)appGroupId { - NSUserDefaults* bucket = [self bucketByName: appGroupId]; - [bucket removeObjectForKey: key]; -} -@end diff --git a/ios/MattermostShare/Base.lproj/MainInterface.storyboard b/ios/MattermostShare/Base.lproj/MainInterface.storyboard index a141885030..286a50894d 100644 --- a/ios/MattermostShare/Base.lproj/MainInterface.storyboard +++ b/ios/MattermostShare/Base.lproj/MainInterface.storyboard @@ -1,5 +1,5 @@ - + @@ -9,7 +9,7 @@ - + diff --git a/ios/MattermostShare/ChannelsViewController.swift b/ios/MattermostShare/ChannelsViewController.swift new file mode 100644 index 0000000000..a68eb13f09 --- /dev/null +++ b/ios/MattermostShare/ChannelsViewController.swift @@ -0,0 +1,134 @@ +import UIKit + +class ChannelsViewController: UIViewController { + + let searchController = UISearchController(searchResultsController: nil) + + lazy var tableView: UITableView = { + let tableView = UITableView(frame: self.view.frame) + tableView.autoresizingMask = [.flexibleWidth, .flexibleHeight] + tableView.dataSource = self + tableView.delegate = self + tableView.backgroundColor = .clear + tableView.register(UITableViewCell.self, forCellReuseIdentifier: Identifiers.ChannelCell) + + return tableView + }() + + var channelDecks = [Section]() + var filteredDecks: [Section]? + weak var delegate: ChannelsViewControllerDelegate? + + override func viewDidLoad() { + super.viewDidLoad() + + filteredDecks = channelDecks + title = "Channels" + configureSearchBar() + view.addSubview(tableView) + } + + func configureSearchBar() { + searchController.searchResultsUpdater = self + searchController.hidesNavigationBarDuringPresentation = false + searchController.dimsBackgroundDuringPresentation = false + searchController.searchBar.searchBarStyle = .minimal + searchController.searchBar.autocapitalizationType = .none + + if #available(iOS 11.0, *) { + // For iOS 11 and later, place the search bar in the navigation bar. + self.definesPresentationContext = true + + // Make the search bar always visible. + navigationItem.hidesSearchBarWhenScrolling = true + + navigationItem.searchController = searchController + } else { + // For iOS 10 and earlier, place the search controller's search bar in the table view's header. + tableView.tableHeaderView = searchController.searchBar + } + } + +} + +private extension ChannelsViewController { + struct Identifiers { + static let ChannelCell = "channelCell" + } +} + +extension ChannelsViewController: UITableViewDataSource { + func numberOfSections(in tableView: UITableView) -> Int { + return filteredDecks?.count ?? 0 + } + + func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { + let sec = filteredDecks?[section] + if (sec?.items.count)! > 0 { + return sec?.title + } + + return nil + } + + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return filteredDecks?[section].items.count ?? 0 + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let section = filteredDecks?[indexPath.section] + let cell = tableView.dequeueReusableCell(withIdentifier: Identifiers.ChannelCell, for: indexPath) + let item = section?.items[indexPath.row] + cell.textLabel?.text = item?.title + if item?.selected ?? false { + cell.accessoryType = .checkmark + } else { + cell.accessoryType = .none + } + cell.backgroundColor = .clear + return cell + } +} + +protocol ChannelsViewControllerDelegate: class { + func selectedChannel(deck: Item) +} + +extension ChannelsViewController: UITableViewDelegate { + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + let section = filteredDecks?[indexPath.section] + if (section?.items != nil) { + delegate?.selectedChannel(deck: (section?.items[indexPath.row])!) + } + } +} + +extension ChannelsViewController: UISearchResultsUpdating { + func updateSearchResults(for searchController: UISearchController) { + if let searchText = searchController.searchBar.text, !searchText.isEmpty { + filteredDecks = channelDecks.map {section in + let s = section.copy() as! Section + let items = section.items.filter{($0.title?.lowercased().contains(searchText.lowercased()))!} + s.items = items + return s + } + } else { + filteredDecks = channelDecks + } + + tableView.reloadData() + } +} + +extension ChannelsViewController: UISearchBarDelegate { + func searchBarCancelButtonClicked(_ searchBar: UISearchBar) { + searchBar.showsCancelButton = false + searchBar.text = "" + searchBar.resignFirstResponder() + tableView.reloadData() + } + + func searchBarTextDidBeginEditing(_ searchBar: UISearchBar) { + searchBar.showsCancelButton = true + } +} diff --git a/ios/MattermostShare/GenericPreview.swift b/ios/MattermostShare/GenericPreview.swift new file mode 100644 index 0000000000..1647451f05 --- /dev/null +++ b/ios/MattermostShare/GenericPreview.swift @@ -0,0 +1,25 @@ +import UIKit + +class GenericPreview: UIView { + + @IBOutlet var contentView: UIView! + @IBOutlet weak var mainLabel: UILabel! + + override init(frame: CGRect) { + super.init(frame: frame) + commonInit() + } + + required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + commonInit() + } + + + private func commonInit() { + Bundle.main.loadNibNamed("GenericPreview", owner: self, options: nil) + addSubview(contentView) + contentView.frame = self.bounds + contentView.autoresizingMask = [.flexibleHeight, .flexibleWidth] + } +} diff --git a/ios/MattermostShare/GenericPreview.xib b/ios/MattermostShare/GenericPreview.xib new file mode 100644 index 0000000000..80b4b05b08 --- /dev/null +++ b/ios/MattermostShare/GenericPreview.xib @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/MattermostShare/Images/generic.png b/ios/MattermostShare/Images/generic.png new file mode 100644 index 0000000000000000000000000000000000000000..34a17f91f4224f4ec304607afc72f98f8359f635 GIT binary patch literal 45380 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�x3rBW!?d4*Z&oM=%BY`5Dq#WA)oiTd3pC2 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 literal 0 HcmV?d00001 diff --git a/ios/MattermostShare/Info.plist b/ios/MattermostShare/Info.plist index 58c291d26e..ac287c4262 100644 --- a/ios/MattermostShare/Info.plist +++ b/ios/MattermostShare/Info.plist @@ -2,10 +2,6 @@ - BundleEntryFilename - share.ios.js - BundleForced - CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleDisplayName @@ -54,12 +50,5 @@ NSExtensionPointIdentifier com.apple.share-services - UIAppFonts - - Ionicons.ttf - FontAwesome.ttf - EvilIcons.ttf - OpenSans-Bold.ttf - diff --git a/ios/MattermostShare/Item.swift b/ios/MattermostShare/Item.swift new file mode 100644 index 0000000000..ecf99299ae --- /dev/null +++ b/ios/MattermostShare/Item.swift @@ -0,0 +1,7 @@ +import Foundation + +final class Item { + var id: String? + var title: String? + var selected: Bool = false +} diff --git a/ios/MattermostShare/MattermostShare-Bridging-Header.h b/ios/MattermostShare/MattermostShare-Bridging-Header.h new file mode 100644 index 0000000000..75ead341d9 --- /dev/null +++ b/ios/MattermostShare/MattermostShare-Bridging-Header.h @@ -0,0 +1,6 @@ +// +// Use this file to import your target's public headers that you would like to expose to Swift. +// + +#import "StoreManager.h" +#import "Constants.h" diff --git a/ios/MattermostShare/MattermostShare.entitlements b/ios/MattermostShare/MattermostShare.entitlements index c3808dbda8..55fdcdea61 100644 --- a/ios/MattermostShare/MattermostShare.entitlements +++ b/ios/MattermostShare/MattermostShare.entitlements @@ -2,27 +2,9 @@ - com.apple.developer.icloud-container-identifiers - - iCloud.$(CFBundleIdentifier) - - com.apple.developer.icloud-services - - CloudDocuments - - com.apple.developer.ubiquity-container-identifiers - - iCloud.$(CFBundleIdentifier) - - com.apple.developer.ubiquity-kvstore-identifier - $(TeamIdentifierPrefix)$(CFBundleIdentifier) com.apple.security.application-groups group.com.mattermost.rnbeta - keychain-access-groups - - $(AppIdentifierPrefix)com.mattermost.rnbeta - diff --git a/ios/MattermostShare/PerformRequests.h b/ios/MattermostShare/PerformRequests.h deleted file mode 100644 index 748f887aee..0000000000 --- a/ios/MattermostShare/PerformRequests.h +++ /dev/null @@ -1,23 +0,0 @@ -#import -#import "MattermostBucket.h" - -@interface PerformRequests : NSObject -@property (nonatomic, strong) NSString *appGroupId; -@property (nonatomic, strong) NSString *requestId; -@property (nonatomic, strong) NSMutableArray *fileIds; -@property (nonatomic, strong) NSArray *files; -@property (nonatomic, strong) NSDictionary *post; - -@property (nonatomic, strong) NSString *serverUrl; -@property (nonatomic, strong) NSString *token; -@property (nonatomic, strong) NSExtensionContext *extensionContext; -@property MattermostBucket *bucket; - -- (id) initWithPost:(NSDictionary *) post - withFiles:(NSArray *) files - forRequestId:(NSString *)requestId - inAppGroupId:(NSString *) appGroupId - inContext:(NSExtensionContext *) extensionContext; - --(void)createPost; -@end diff --git a/ios/MattermostShare/PerformRequests.m b/ios/MattermostShare/PerformRequests.m deleted file mode 100644 index 3d582406d5..0000000000 --- a/ios/MattermostShare/PerformRequests.m +++ /dev/null @@ -1,154 +0,0 @@ -#import "PerformRequests.h" -#import "MattermostBucket.h" -#import "SessionManager.h" - -@implementation PerformRequests - -- (id) initWithPost:(NSDictionary *) post - withFiles:(NSArray *) files - forRequestId:(NSString *)requestId - inAppGroupId:(NSString *) appGroupId - inContext:(NSExtensionContext *) context { - self = [super init]; - if (self) { - self.post = post; - self.files = files; - self.appGroupId = appGroupId; - self.requestId = requestId; - self.extensionContext = context; - - self.bucket = [[MattermostBucket alloc] init]; - [self setCredentials]; - } - return self; -} - --(void)setCredentials { - NSString *entitiesString = [self.bucket readFromFile:@"entities" appGroupId:self.appGroupId]; - NSData *entitiesData = [entitiesString dataUsingEncoding:NSUTF8StringEncoding]; - NSDictionary *entities = [NSJSONSerialization JSONObjectWithData:entitiesData options:NSJSONReadingMutableContainers error:nil]; - NSDictionary *credentials = [[entities objectForKey:@"general"] objectForKey:@"credentials"]; - self.serverUrl = [credentials objectForKey:@"url"]; - self.token = [credentials objectForKey:@"token"]; -} - --(void)URLSession:(NSURLSession *)session task:(NSURLSessionDataTask *)task didCompleteWithError:(nullable NSError *)error { - if(error != nil) { - NSLog(@"ERROR %@", [error userInfo]); - [self.extensionContext completeRequestReturningItems:nil - completionHandler:nil]; - NSLog(@"invalidating session %@", self.requestId); - [session finishTasksAndInvalidate]; - } -} - --(void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data { - NSString *sessionRequestId = [[session configuration] identifier]; - - if ([sessionRequestId containsString:@"files"]) { - NSLog(@"Got file response"); - NSDictionary *json = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil]; - if (json != nil) { - NSArray *fileInfos = [json objectForKey:@"file_infos"]; - self.fileIds = [[NSMutableArray alloc] init]; - for (id file in fileInfos) { - [self.fileIds addObject:[file objectForKey:@"id"]]; - } - NSLog(@"Calling sendPostRequest"); - [self sendPostRequest]; - } - - NSLog(@"Cleaning temp files"); - [self cleanUpTempFiles]; - } -} - --(void)createPost { - NSString *channelId = [self.post objectForKey:@"channel_id"]; - - NSURL *filesUrl = [NSURL URLWithString:[self.serverUrl stringByAppendingString:@"/api/v4/files"]]; - - if (self.files != nil && [self.files count] > 0) { - NSString *POST_BODY_BOUNDARY = @"mobile.client.file.upload"; - NSURLSessionConfiguration* config = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:[self.requestId stringByAppendingString:@"-files"]]; - config.sharedContainerIdentifier = self.appGroupId; - - NSMutableURLRequest *uploadRequest = [NSMutableURLRequest requestWithURL:filesUrl cachePolicy:NSURLRequestUseProtocolCachePolicy timeoutInterval:120.0]; - [uploadRequest setHTTPMethod:@"POST"]; - [uploadRequest setValue:[@"Bearer " stringByAppendingString:self.token] forHTTPHeaderField:@"Authorization"]; - [uploadRequest setValue:@"application/json" forHTTPHeaderField:@"Accept"]; - - NSString *contentTypeValue = [NSString stringWithFormat:@"multipart/form-data;boundary=%@", POST_BODY_BOUNDARY]; - [uploadRequest addValue:contentTypeValue forHTTPHeaderField:@"Content-Type"]; - - NSMutableData *dataForm = [NSMutableData alloc]; - [dataForm appendData:[[NSString stringWithFormat:@"\r\n--%@\r\n", POST_BODY_BOUNDARY] dataUsingEncoding:NSUTF8StringEncoding]]; - [dataForm appendData:[[NSString stringWithFormat:@"Content-Disposition: form-data; name=\"channel_id\";\r\n\r\n%@", channelId] dataUsingEncoding:NSUTF8StringEncoding]]; - - for (id file in self.files) { - NSData *fileData = [NSData dataWithContentsOfFile:[file objectForKey:@"filePath"]]; - NSString *mimeType = [file objectForKey:@"mimeType"]; - NSLog(@"MimeType %@", mimeType); - [dataForm appendData:[[NSString stringWithFormat:@"\r\n--%@\r\n", POST_BODY_BOUNDARY] dataUsingEncoding:NSUTF8StringEncoding]]; - [dataForm appendData:[[NSString stringWithFormat:@"Content-Disposition: form-data; name=\"files\"; filename=\"%@\"\r\n", - [file objectForKey:@"filename"]] dataUsingEncoding:NSUTF8StringEncoding]]; - [dataForm appendData:[[NSString stringWithFormat:@"Content-Type: %@\r\n\r\n", mimeType] dataUsingEncoding:NSUTF8StringEncoding]]; - [dataForm appendData:[NSData dataWithData:fileData]]; - } - - [dataForm appendData:[[NSString stringWithFormat:@"\r\n--%@--\r\n", POST_BODY_BOUNDARY] dataUsingEncoding:NSUTF8StringEncoding]]; - [uploadRequest setHTTPBody:dataForm]; - NSURLSession *uploadSession = [NSURLSession sessionWithConfiguration:config delegate:self delegateQueue:[NSOperationQueue mainQueue]]; - NSURLSessionDataTask *uploadTask = [uploadSession dataTaskWithRequest:uploadRequest]; - NSLog(@"Executing file request"); - [uploadTask resume]; - } else { - [self sendPostRequest]; - } -} - --(void)sendPostRequest { - NSMutableDictionary *post = [self.post mutableCopy]; - [post setValue:self.fileIds forKey:@"file_ids"]; - NSData *postData = [NSJSONSerialization dataWithJSONObject:post options:NSJSONWritingPrettyPrinted error:nil]; - NSString* postAsString = [[NSString alloc] initWithData:postData encoding:NSUTF8StringEncoding]; - - NSURL *createUrl = [NSURL URLWithString:[self.serverUrl stringByAppendingString:@"/api/v4/posts"]]; - - NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:createUrl cachePolicy:NSURLRequestUseProtocolCachePolicy timeoutInterval:5.0]; - [request setHTTPMethod:@"POST"]; - [request setValue:[@"Bearer " stringByAppendingString:self.token] forHTTPHeaderField:@"Authorization"]; - [request setValue:@"application/json" forHTTPHeaderField:@"Accept"]; - [request setValue:@"application/json; charset=utf-8" forHTTPHeaderField:@"Content-Type"]; - [request setHTTPBody:[postAsString dataUsingEncoding:NSUTF8StringEncoding]]; - - NSURLSessionConfiguration* config = [NSURLSessionConfiguration ephemeralSessionConfiguration]; - NSURLSession *createSession = [NSURLSession sessionWithConfiguration:config delegate:self delegateQueue:nil]; - NSURLSessionDataTask *createTask = [createSession dataTaskWithRequest:request]; - NSLog(@"Executing post request"); - [createTask resume]; - [self.extensionContext completeRequestReturningItems:nil - completionHandler:nil]; - NSLog(@"Extension closed"); -} - -- (void) cleanUpTempFiles { - NSURL *tmpDirectoryURL = [[SessionManager sharedSession] tempContainerURL:self.appGroupId]; - if (tmpDirectoryURL == nil) { - return; - } - - NSFileManager *fileManager = [NSFileManager defaultManager]; - NSError *error; - NSArray *tmpFiles = [fileManager contentsOfDirectoryAtPath:[tmpDirectoryURL path] error:&error]; - if (error) { - return; - } - - for (NSString *file in tmpFiles) - { - error = nil; - [fileManager removeItemAtPath:[[tmpDirectoryURL URLByAppendingPathComponent:file] path] error:&error]; - } -} -@end diff --git a/ios/MattermostShare/Section.swift b/ios/MattermostShare/Section.swift new file mode 100644 index 0000000000..9a5eed3b8d --- /dev/null +++ b/ios/MattermostShare/Section.swift @@ -0,0 +1,13 @@ +import Foundation + +class Section: NSObject, NSCopying { + var title: String? + var items: [Item] = [] + + func copy(with zone: NSZone? = nil) -> Any { + let copy = Section() + copy.title = title + copy.items = items + return copy + } +} diff --git a/ios/MattermostShare/SessionManager.h b/ios/MattermostShare/SessionManager.h deleted file mode 100644 index 5a23cd3ee7..0000000000 --- a/ios/MattermostShare/SessionManager.h +++ /dev/null @@ -1,23 +0,0 @@ -#import -#import "MattermostBucket.h" -#import "KeyChainDataSource.h" - -@interface SessionManager : NSObject -@property (nonatomic, copy) void (^savedCompletionHandler)(void); -@property (nonatomic, copy) void (^sendShareEvent)(NSString *); -@property (nonatomic, copy) void (^closeExtension)(void); -@property MattermostBucket *bucket; -@property (nonatomic, retain) KeyChainDataSource *keyChain; -@property (nonatomic, strong) NSString *requestWithGroup; -@property (nonatomic, strong) NSString *certificateName; -@property (nonatomic) BOOL isBackground; - - -+(instancetype)sharedSession; --(NSString *)getAppGroupIdFromRequestIdentifier:(NSString *) requestWithGroup; --(NSURLSession *)createSessionForRequestRequest:(NSString *)requestId; --(void)setRequestWithGroup:(NSString *)requestWithGroup certificateName:(NSString *)certificateName; --(void)setDataForRequest:(NSDictionary *)data forRequestWithGroup:(NSString *)requestId; --(void)createPostForRequest:(NSString *)requestId; --(NSURL*)tempContainerURL:(NSString*)appGroupId; -@end diff --git a/ios/MattermostShare/SessionManager.m b/ios/MattermostShare/SessionManager.m deleted file mode 100644 index 7259774b9a..0000000000 --- a/ios/MattermostShare/SessionManager.m +++ /dev/null @@ -1,300 +0,0 @@ -#import "SessionManager.h" -#import "MattermostBucket.h" - -@implementation SessionManager - -@synthesize keyChain; - -+ (instancetype)sharedSession { - static id sharedMyManager = nil; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - sharedMyManager = [[self alloc] init]; - }); - return sharedMyManager; -} - -- (instancetype)init { - self = [super init]; - if (self) { - self.bucket = [[MattermostBucket alloc] init]; - self.keyChain = [[KeyChainDataSource alloc] initWithMode:KSM_Identities]; - } - return self; -} - --(void)setRequestWithGroup:(NSString *)requestWithGroup certificateName:(NSString *)certificateName { - self.requestWithGroup = requestWithGroup; - self.certificateName = certificateName; - self.isBackground = [certificateName length] == 0; -} - --(void)setDataForRequest:(NSDictionary *)data forRequestWithGroup:(NSString *)requestWithGroup { - NSString *appGroupId = [self getAppGroupIdFromRequestIdentifier:requestWithGroup]; - [[self.bucket bucketByName:appGroupId] setObject:data forKey:requestWithGroup]; -} - - --(NSString *)getAppGroupIdFromRequestIdentifier:(NSString *) requestWithGroup { - return [requestWithGroup componentsSeparatedByString:@"|"][1]; -} - --(NSDictionary *)getCredentialsForRequest:(NSString *)requestWithGroup { - NSString * appGroupId = [self getAppGroupIdFromRequestIdentifier:requestWithGroup]; - NSString *entitiesString = [self.bucket readFromFile:@"entities" appGroupId:appGroupId]; - NSData *entitiesData = [entitiesString dataUsingEncoding:NSUTF8StringEncoding]; - NSError *error; - NSDictionary *entities = [NSJSONSerialization JSONObjectWithData:entitiesData options:NSJSONReadingMutableContainers error:&error]; - if (error != nil) { - return nil; - } - return [[entities objectForKey:@"general"] objectForKey:@"credentials"]; -} - --(NSDictionary *)getDataForRequest:(NSString *)requestWithGroup { - NSString *appGroupId = [self getAppGroupIdFromRequestIdentifier:requestWithGroup]; - return [[self.bucket bucketByName:appGroupId] objectForKey:requestWithGroup]; -} - --(NSURLSession *)createSessionForRequestRequest:(NSString *)requestWithGroup { - NSString *appGroupId = [self getAppGroupIdFromRequestIdentifier:requestWithGroup]; - NSURLSessionConfiguration* config; - if (self.isBackground) { - config = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:requestWithGroup]; - } else { - config = [NSURLSessionConfiguration defaultSessionConfiguration]; - } - - config.sharedContainerIdentifier = appGroupId; - return [NSURLSession sessionWithConfiguration:config delegate:self delegateQueue:nil]; -} - --(void) createPost:(NSDictionary *) post - withFiles:(NSArray *)files - credentials:(NSDictionary *) credentials - requestWithGroup:(NSString *)requestWithGroup { - NSString *serverUrl = [credentials objectForKey:@"url"]; - NSString *token = [credentials objectForKey:@"token"]; - NSString *channelId = [post objectForKey:@"channel_id"]; - NSURLSession *session = [self createSessionForRequestRequest:requestWithGroup]; - NSString *appGroupId = [self getAppGroupIdFromRequestIdentifier:requestWithGroup]; - - for (id file in files) { - NSURL *filePath = [NSURL fileURLWithPath:[file objectForKey:@"filePath"]]; - NSString *fileName = [file objectForKey:@"filename"]; - - NSError *err; - NSURL *tempContainerURL = [self tempContainerURL:appGroupId]; - NSURL *destinationURL = [tempContainerURL URLByAppendingPathComponent: fileName]; - BOOL bVal = [[NSFileManager defaultManager] copyItemAtURL:filePath toURL:destinationURL error:&err]; - - NSCharacterSet *allowedCharacters = [NSCharacterSet URLQueryAllowedCharacterSet]; - NSString *encodedFilename = [[file objectForKey:@"filename"] stringByAddingPercentEncodingWithAllowedCharacters:allowedCharacters]; - NSString *url = [serverUrl stringByAppendingString:@"/api/v4/files"]; - NSString *queryString = [NSString stringWithFormat:@"?channel_id=%@&filename=%@", channelId, encodedFilename]; - NSURL *filesUrl = [NSURL URLWithString:[url stringByAppendingString:queryString]]; - NSMutableURLRequest *uploadRequest = [NSMutableURLRequest requestWithURL:filesUrl cachePolicy:NSURLRequestUseProtocolCachePolicy timeoutInterval:120.0]; - [uploadRequest setHTTPMethod:@"POST"]; - [uploadRequest setValue:[@"Bearer " stringByAppendingString:token] forHTTPHeaderField:@"Authorization"]; - [uploadRequest setValue:@"application/json" forHTTPHeaderField:@"Accept"]; - - NSURLSessionUploadTask *task = [session uploadTaskWithRequest:uploadRequest fromFile:destinationURL]; - NSLog(@"Executing file request %@", fileName); - [task resume]; - } -} - --(void) createPost:(NSMutableDictionary *)post - withFileIds:(NSArray *)fileIds - credentials:(NSDictionary *) credentials { - NSString *serverUrl = [credentials objectForKey:@"url"]; - NSString *token = [credentials objectForKey:@"token"]; - - if (fileIds != nil && [fileIds count] > 0) { - [post setObject:fileIds forKey:@"file_ids"]; - } - - NSError *error; - NSData *postData = [NSJSONSerialization dataWithJSONObject:post options:NSJSONWritingPrettyPrinted error:&error]; - - if (error == nil) { - NSString* postAsString = [[NSString alloc] initWithData:postData encoding:NSUTF8StringEncoding]; - - NSURL *createUrl = [NSURL URLWithString:[serverUrl stringByAppendingString:@"/api/v4/posts"]]; - - NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:createUrl cachePolicy:NSURLRequestUseProtocolCachePolicy timeoutInterval:5.0]; - [request setHTTPMethod:@"POST"]; - [request setValue:[@"Bearer " stringByAppendingString:token] forHTTPHeaderField:@"Authorization"]; - [request setValue:@"application/json" forHTTPHeaderField:@"Accept"]; - [request setValue:@"application/json; charset=utf-8" forHTTPHeaderField:@"Content-Type"]; - [request setHTTPBody:[postAsString dataUsingEncoding:NSUTF8StringEncoding]]; - - NSURLSessionConfiguration* config = [NSURLSessionConfiguration ephemeralSessionConfiguration]; - NSURLSession *createSession = [NSURLSession sessionWithConfiguration:config delegate:self delegateQueue:nil]; - NSURLSessionDataTask *createTask = [createSession dataTaskWithRequest:request]; - NSLog(@"Executing post request"); - [createTask resume]; - self.closeExtension(); - } else { - self.sendShareEvent(@"extensionPostFailed"); - } -} - --(void)createPostForRequest:(NSString *)requestWithGroup { - NSDictionary *data = [self getDataForRequest:requestWithGroup]; - NSDictionary *post = [data objectForKey:@"post"]; - NSArray *files = [data objectForKey:@"files"]; - NSDictionary *credentials = [self getCredentialsForRequest:requestWithGroup]; - - if (credentials == nil) { - self.sendShareEvent(@"extensionPostFailed"); - return; - } - - if (files != nil && [files count] > 0) { - [self createPost:post withFiles:files credentials:credentials requestWithGroup:requestWithGroup]; - } - else { - [self createPost:[post mutableCopy] withFileIds:nil credentials:credentials]; - } -} - --(void)sendPostRequestForId:(NSString *)requestWithGroup { - NSDictionary *data = [self getDataForRequest:requestWithGroup]; - NSDictionary *credentials = [self getCredentialsForRequest:requestWithGroup]; - - NSMutableDictionary *post = [[data objectForKey:@"post"] mutableCopy]; - NSArray *fileIds = [data objectForKey:@"file_ids"]; - - [self createPost:post withFileIds:fileIds credentials:credentials]; -} - --(NSURL*)tempContainerURL:(NSString*)appGroupId { - NSFileManager *manager = [NSFileManager defaultManager]; - NSURL *containerURL = [manager containerURLForSecurityApplicationGroupIdentifier:appGroupId]; - NSURL *tempDirectoryURL = [containerURL URLByAppendingPathComponent:@"shareTempItems"]; - if (![manager fileExistsAtPath:[tempDirectoryURL path]]) { - NSError *err; - [manager createDirectoryAtURL:tempDirectoryURL withIntermediateDirectories:YES attributes:nil error:&err]; - if (err) { - return nil; - } - } - - return tempDirectoryURL; -} - - --(void)URLSession:(NSURLSession *)session didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition, NSURLCredential * _Nullable))completionHandler { - NSLog(@"completition handler from normal challenge"); - if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodClientCertificate]) { - if (self.certificateName) { - SecIdentityRef identity = [keyChain GetIdentityByName:self.certificateName]; - if (identity != nil) { - SecCertificateRef certificate = NULL; - OSStatus status = SecIdentityCopyCertificate(identity, &certificate); - if (!status) { - CFArrayRef emailAddresses = NULL; - SecCertificateCopyEmailAddresses(certificate, &emailAddresses); - NSArray *emails = (NSArray *)CFBridgingRelease(emailAddresses); - CFStringRef summaryRef = SecCertificateCopySubjectSummary(certificate); - NSString *tagstr = (NSString*)CFBridgingRelease(summaryRef); - NSString *email = @""; - if ([emails count] > 0){ - email = [emails objectAtIndex:0]; - } - NSLog(@"completion %@ %@", tagstr, email); - const void *certs[] = {certificate}; - CFArrayRef certArray = CFArrayCreate(kCFAllocatorDefault, certs, 1, NULL); - NSURLCredential *credential = [NSURLCredential credentialWithIdentity:identity certificates:(__bridge NSArray*)certArray persistence:NSURLCredentialPersistencePermanent]; - [challenge.sender useCredential:credential forAuthenticationChallenge:challenge]; - completionHandler(NSURLSessionAuthChallengeUseCredential,credential); - NSLog(@"completion handler for certificate"); - return; - } - } - } - completionHandler(NSURLSessionAuthChallengeUseCredential, [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust]); - } else { - NSLog(@"completion handler regular stuff"); - completionHandler(NSURLSessionAuthChallengeUseCredential, [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust]); - } -} - --(void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data { - NSString *requestWithGroup; - if (self.isBackground) { - requestWithGroup = [[session configuration] identifier]; - } else { - requestWithGroup = self.requestWithGroup; - } - NSURL *requestUrl = [[dataTask originalRequest] URL]; - - if ([[requestUrl absoluteString] containsString:@"files"]) { - NSString *appGroupId = [self getAppGroupIdFromRequestIdentifier:requestWithGroup]; - NSError *error; - NSDictionary *json = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error]; - if (error == nil && json != nil) { - NSArray *fileInfos = [json objectForKey:@"file_infos"]; - NSDictionary *dataFromBucket = [self getDataForRequest:requestWithGroup]; - NSMutableDictionary *data = [dataFromBucket mutableCopy]; - NSMutableArray *fileIds = [[data objectForKey:@"file_ids"] mutableCopy]; - if (fileIds == nil && data != nil) { - fileIds = [[NSMutableArray alloc] init]; - } - - for (id file in fileInfos) { - [fileIds addObject:[file objectForKey:@"id"]]; - NSString * filename = [file objectForKey:@"name"]; - NSLog(@"got file id %@ %@", [file objectForKey:@"id"], filename); - NSURL *tempContainerURL = [self tempContainerURL:appGroupId]; - NSURL *destinationURL = [tempContainerURL URLByAppendingPathComponent: filename]; - [[NSFileManager defaultManager] removeItemAtURL:destinationURL error:nil]; - } - [data setObject:fileIds forKey:@"file_ids"]; - [self setDataForRequest:data forRequestWithGroup:requestWithGroup]; - } else { - self.sendShareEvent(@"extensionPostFailed"); - } - } -} - --(void)URLSession:(NSURLSession *)session task:(NSURLSessionDataTask *)task didCompleteWithError:(nullable NSError *)error { - NSString *requestWithGroup; - if (self.isBackground) { - requestWithGroup = [[session configuration] identifier]; - } else { - requestWithGroup = self.requestWithGroup; - } - - if(error != nil) { - NSLog(@"completition ERROR %@", [error userInfo]); - NSLog(@"invalidating session %@", requestWithGroup); - [session invalidateAndCancel]; - self.sendShareEvent(@"extensionPostFailed"); - } else if (requestWithGroup != nil) { - NSString *appGroupId = [self getAppGroupIdFromRequestIdentifier:requestWithGroup]; - NSURL *requestUrl = [[task originalRequest] URL]; - - NSDictionary *data = [self getDataForRequest:requestWithGroup]; - NSArray *files = [data objectForKey:@"files"]; - NSMutableArray *fileIds = [data objectForKey:@"file_ids"]; - - if ([[requestUrl absoluteString] containsString:@"files"] && - [files count] == [fileIds count]) { - [self sendPostRequestForId:requestWithGroup]; - [[self.bucket bucketByName:appGroupId] removeObjectForKey:requestWithGroup]; - } - } else { - NSLog(@"SOMETHING ELSE"); - } -} - --(void)URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session { - if (self.savedCompletionHandler) { - self.savedCompletionHandler(); - self.savedCompletionHandler = nil; - } -} - -@end diff --git a/ios/MattermostShare/ShareViewController.h b/ios/MattermostShare/ShareViewController.h deleted file mode 100644 index 390c05d416..0000000000 --- a/ios/MattermostShare/ShareViewController.h +++ /dev/null @@ -1,5 +0,0 @@ -#import -#import "React/RCTBridgeModule.h" - -@interface ShareViewController : UIViewController -@end diff --git a/ios/MattermostShare/ShareViewController.m b/ios/MattermostShare/ShareViewController.m deleted file mode 100644 index 70ac27fce8..0000000000 --- a/ios/MattermostShare/ShareViewController.m +++ /dev/null @@ -1,285 +0,0 @@ -#import "ShareViewController.h" -#import -#import -#import "PerformRequests.h" -#import "SessionManager.h" - -NSExtensionContext* extensionContext; - -@implementation ShareViewController -+ (BOOL)requiresMainQueueSetup -{ - return YES; -} - -@synthesize bridge = _bridge; - -- (UIView*) shareView { - NSURL *jsCodeLocation; - - jsCodeLocation = [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"share.ios" fallbackResource:nil]; - - RCTRootView *rootView = [[RCTRootView alloc] initWithBundleURL:jsCodeLocation - moduleName:@"MattermostShare" - initialProperties:nil - launchOptions:nil]; - rootView.backgroundColor = nil; - return rootView; -} - -RCT_EXPORT_MODULE(MattermostShare); - -- (void)viewDidLoad { - [super viewDidLoad]; - extensionContext = self.extensionContext; - UIView *rootView = [self shareView]; - if (rootView.backgroundColor == nil) { - rootView.backgroundColor = [[UIColor alloc] initWithRed:1 green:1 blue:1 alpha:0.1]; - } - - self.view = rootView; -} - -RCT_REMAP_METHOD(getOrientation, - resolver:(RCTPromiseResolveBlock)resolve - rejecter:(RCTPromiseRejectBlock)reject) { - if([UIScreen mainScreen].bounds.size.width < [UIScreen mainScreen].bounds.size.height) { - resolve(@"PORTRAIT"); - } else { - resolve(@"LANDSCAPE"); - } -} - -RCT_EXPORT_METHOD(close:(NSDictionary *)data appGroupId:(NSString *)appGroupId) { - if (data != nil) { - NSString *requestId = [data objectForKey:@"requestId"]; - NSString *useBackgroundUpload = [data objectForKey:@"useBackgroundUpload"]; - NSString *certificateName = [data objectForKey:@"certificate"]; - BOOL tryToUploadInTheBackgound = useBackgroundUpload ? [useBackgroundUpload boolValue] : NO; - - if (tryToUploadInTheBackgound) { - NSString *requestWithGroup = [NSString stringWithFormat:@"%@|%@", requestId, appGroupId]; - [SessionManager sharedSession].closeExtension = ^{ - [extensionContext completeRequestReturningItems:nil - completionHandler:nil]; - NSLog(@"Extension closed"); - }; - - [SessionManager sharedSession].sendShareEvent = ^(NSString* eventName) { - NSLog(@"Send Share Extension Event to JS"); - [_bridge enqueueJSCall:@"RCTDeviceEventEmitter" - method:@"emit" - args:@[eventName] - completion:nil]; - }; - - [[SessionManager sharedSession] setRequestWithGroup:requestWithGroup certificateName:certificateName]; - [[SessionManager sharedSession] setDataForRequest:data forRequestWithGroup:requestWithGroup]; - [[SessionManager sharedSession] createPostForRequest:requestWithGroup]; - } else { - NSDictionary *post = [data objectForKey:@"post"]; - NSArray *files = [data objectForKey:@"files"]; - PerformRequests *request = [[PerformRequests alloc] initWithPost:post withFiles:files forRequestId:requestId inAppGroupId:appGroupId inContext:extensionContext]; - [request createPost]; - } - } else { - [extensionContext completeRequestReturningItems:nil - completionHandler:nil]; - NSLog(@"Extension closed"); - } -} - -RCT_REMAP_METHOD(data, - appGroupId: (NSString *)appGroupId - resolver:(RCTPromiseResolveBlock)resolve - rejecter:(RCTPromiseRejectBlock)reject) -{ - [self extractDataFromContext: extensionContext withAppGroup: appGroupId andCallback:^(NSArray* items ,NSError* err) { - if (err) { - reject(@"data", @"Failed to extract attachment content", err); - return; - } - resolve(items); - }]; -} - -typedef void (^ProviderCallback)(NSString *content, NSString *contentType, BOOL owner, NSError *err); - -- (void)extractDataFromContext:(NSExtensionContext *)context withAppGroup:(NSString *) appGroupId andCallback:(void(^)(NSArray *items ,NSError *err))callback { - @try { - NSExtensionItem *item = [context.inputItems firstObject]; - NSArray *attachments = item.attachments; - NSMutableArray *items = [[NSMutableArray alloc] init]; - - __block int attachmentIdx = 0; - __block ProviderCallback providerCb = nil; - __block __weak ProviderCallback weakProviderCb = nil; - providerCb = ^ void (NSString *content, NSString *contentType, BOOL owner, NSError *err) { - if (err) { - callback(nil, err); - return; - } - - if (content != nil) { - [items addObject:@{ - @"type": contentType, - @"value": content, - @"owner": [NSNumber numberWithBool:owner], - }]; - } - - ++attachmentIdx; - if (attachmentIdx == [attachments count]) { - callback(items, nil); - } else { - [self extractDataFromProvider:attachments[attachmentIdx] withAppGroup:appGroupId andCallback: weakProviderCb]; - } - }; - weakProviderCb = providerCb; - [self extractDataFromProvider:attachments[0] withAppGroup:appGroupId andCallback: providerCb]; - } - @catch (NSException *exc) { - NSError *error = [NSError errorWithDomain:@"fiftythree.paste" code:1 userInfo:@{ - @"reason": [exc description] - }]; - callback(nil, error); - } -} - -- (void)extractDataFromProvider:(NSItemProvider *)provider withAppGroup:(NSString *) appGroupId andCallback:(void(^)(NSString* content, NSString* contentType, BOOL owner, NSError *err))callback { - if([provider hasItemConformingToTypeIdentifier:@"public.movie"]) { - [provider loadItemForTypeIdentifier:@"public.movie" options:nil completionHandler:^(id item, NSError *error) { - @try { - if ([item isKindOfClass: NSURL.class]) { - NSURL *url = (NSURL *)item; - return callback([url absoluteString], @"public.movie", NO, nil); - } - return callback(nil, nil, NO, nil); - } - @catch(NSException *exc) { - NSError *error = [NSError errorWithDomain:@"fiftythree.paste" code:2 userInfo:@{ - @"reason": [exc description] - }]; - callback(nil, nil, NO, error); - } - }]; - return; - } - if([provider hasItemConformingToTypeIdentifier:@"public.image"]) { - [provider loadItemForTypeIdentifier:@"public.image" options:nil completionHandler:^(id item, NSError *error) { - if (error) { - callback(nil, nil, NO, error); - return; - } - - @try { - if ([item isKindOfClass: NSURL.class]) { - NSURL *url = (NSURL *)item; - return callback([url absoluteString], @"public.image", NO, nil); - } else if ([item isKindOfClass: UIImage.class]) { - UIImage *image = (UIImage *)item; - NSString *fileName = [NSString stringWithFormat:@"%@.jpg", [[NSUUID UUID] UUIDString]]; - NSURL *tempContainerURL = [[SessionManager sharedSession] tempContainerURL:appGroupId]; - if (tempContainerURL == nil){ - return callback(nil, nil, NO, nil); - } - - NSURL *tempFileURL = [tempContainerURL URLByAppendingPathComponent: fileName]; - BOOL created = [UIImageJPEGRepresentation(image, 0.8) writeToFile:[tempFileURL path] atomically:YES]; - if (created) { - return callback([tempFileURL absoluteString], @"public.image", YES, nil); - } else { - return callback(nil, nil, NO, nil); - } - } else if ([item isKindOfClass: NSData.class]) { - NSString *fileName = [NSString stringWithFormat:@"%@.jpg", [[NSUUID UUID] UUIDString]]; - NSData *data = (NSData *)item; - UIImage *image = [UIImage imageWithData:data]; - NSURL *tempContainerURL = [[SessionManager sharedSession] tempContainerURL:appGroupId]; - if (tempContainerURL == nil){ - return callback(nil, nil, NO, nil); - } - NSURL *tempFileURL = [tempContainerURL URLByAppendingPathComponent: fileName]; - BOOL created = [UIImageJPEGRepresentation(image, 0.8) writeToFile:[tempFileURL path] atomically:YES]; - if (created) { - return callback([tempFileURL absoluteString], @"public.image", YES, nil); - } else { - return callback(nil, nil, NO, nil); - } - } else { - // Do nothing, some type we don't support. - return callback(nil, nil, NO, nil); - } - } - @catch(NSException *exc) { - NSError *error = [NSError errorWithDomain:@"fiftythree.paste" code:2 userInfo:@{ - @"reason": [exc description] - }]; - callback(nil, nil, NO, error); - } - }]; - return; - } - - if([provider hasItemConformingToTypeIdentifier:@"public.file-url"]) { - [provider loadItemForTypeIdentifier:@"public.file-url" options:nil completionHandler:^(id item, NSError *error) { - if (error) { - callback(nil, nil, NO, error); - return; - } - - if ([item isKindOfClass:NSURL.class]) { - return callback([(NSURL *)item absoluteString], @"public.file-url", NO, nil); - } else if ([item isKindOfClass:NSString.class]) { - return callback((NSString *)item, @"public.file-url", NO, nil); - } - callback(nil, nil, NO, nil); - }]; - return; - } - - if([provider hasItemConformingToTypeIdentifier:@"public.url"]) { - [provider loadItemForTypeIdentifier:@"public.url" options:nil completionHandler:^(id item, NSError *error) { - if (error) { - callback(nil, nil, NO, error); - return; - } - - if ([item isKindOfClass:NSURL.class]) { - return callback([(NSURL *)item absoluteString], @"public.url", NO, nil); - } else if ([item isKindOfClass:NSString.class]) { - return callback((NSString *)item, @"public.url", NO, nil); - } - }]; - return; - } - - if([provider hasItemConformingToTypeIdentifier:@"public.plain-text"]) { - [provider loadItemForTypeIdentifier:@"public.plain-text" options:nil completionHandler:^(id item, NSError *error) { - if (error) { - callback(nil, nil, NO, error); - return; - } - - if ([item isKindOfClass:NSString.class]) { - return callback((NSString *)item, @"public.plain-text", NO, nil); - } else if ([item isKindOfClass:NSAttributedString.class]) { - NSAttributedString *str = (NSAttributedString *)item; - return callback([str string], @"public.plain-text", NO, nil); - } else if ([item isKindOfClass:NSData.class]) { - NSString *str = [[NSString alloc] initWithData:(NSData *)item encoding:NSUTF8StringEncoding]; - if (str) { - return callback(str, @"public.plain-text", NO, nil); - } else { - return callback(nil, nil, NO, nil); - } - } else { - return callback(nil, nil, NO, nil); - } - }]; - return; - } - - callback(nil, nil, NO, nil); -} -@end diff --git a/ios/MattermostShare/ShareViewController.swift b/ios/MattermostShare/ShareViewController.swift new file mode 100644 index 0000000000..67da8d192a --- /dev/null +++ b/ios/MattermostShare/ShareViewController.swift @@ -0,0 +1,405 @@ +import UIKit +import Social +import MobileCoreServices +import UploadAttachments + +extension Bundle { + var displayName: String? { + return object(forInfoDictionaryKey: "CFBundleDisplayName") as? String + } +} + +class ShareViewController: SLComposeServiceViewController { + + private var dispatchGroup = DispatchGroup() + private var attachments = AttachmentArray() + private var store = StoreManager.shared() as StoreManager + private var entities: [AnyHashable:Any]? = nil + private var sessionToken: String? + private var serverURL: String? + private var message: String? + private var publicURL: String? + + fileprivate var selectedChannel: Item? + fileprivate var selectedTeam: Item? + + // MARK: - Lifecycle methods + override func viewDidLoad() { + super.viewDidLoad() + + title = Bundle.main.displayName + placeholder = "Write a message..." + entities = store.getEntities(true) as [AnyHashable:Any]? + sessionToken = store.getToken() + serverURL = store.getServerUrl() + + extractDataFromContext() + + if sessionToken == nil { + showErrorMessage(title: "", message: "Authentication required: Please first login using the app.", VC: self) + } + } + + override func isContentValid() -> Bool { + // Do validation of contentText and/or NSExtensionContext attachments here + if (attachments.count > 0) { + let maxFileSize = store.getMaxFileSize() + if attachments.hasAttachementLargerThan(fileSize: maxFileSize) { + let readableMaxSize = formatFileSize(bytes: Double(maxFileSize)) + showErrorMessage(title: "", message: "File attachments shared in Mattermost must be less than \(readableMaxSize).", VC: self) + } + } + + return serverURL != nil && + sessionToken != nil && + attachmentsCount() == attachments.count && + selectedTeam != nil && + selectedChannel != nil + } + + override func didSelectCancel() { + UploadSessionManager.shared.clearTempDirectory() + super.didSelectCancel() + } + + override func didSelectPost() { + // This is called after the user selects Post. Do the upload of contentText and/or NSExtensionContext attachments. + if publicURL != nil { + self.message = "\(contentText!)\n\n\(publicURL!)" + } else { + self.message = contentText + } + + UploadManager.shared.uploadFiles(baseURL: serverURL!, token: sessionToken!, channelId: selectedChannel!.id!, message: message, attachments: attachments, callback: { + // Inform the host that we're done, so it un-blocks its UI. Note: Alternatively you could call super's -didSelectPost, which will similarly complete the extension context. + self.extensionContext!.completeRequest(returningItems: [], completionHandler: nil) + }) + } + + override func loadPreviewView() -> UIView! { + if attachments.findBy(type: kUTTypeFileURL as String) { + let genericPreview = GenericPreview() + genericPreview.contentMode = .scaleAspectFit + genericPreview.clipsToBounds = true + genericPreview.isUserInteractionEnabled = false + genericPreview.addConstraints([ + NSLayoutConstraint(item: genericPreview, attribute: .width, relatedBy: .equal, toItem: nil, attribute: .width, multiplier: 1.0, constant: 70), + NSLayoutConstraint(item: genericPreview, attribute: .height, relatedBy: .equal, toItem: nil, attribute: .height, multiplier: 1.0, constant: 70) + ]) + + if attachments.count > 1 { + genericPreview.mainLabel.text = "\(attachments.count) Items" + } + + return genericPreview + } + return super.loadPreviewView(); + } + + override func configurationItems() -> [Any]! { + var items: [SLComposeSheetConfigurationItem] = [] + + // To add configuration options via table cells at the bottom of the sheet, return an array of SLComposeSheetConfigurationItem here. + let teamDecks = getTeamItems() + if let teams = SLComposeSheetConfigurationItem() { + teams.title = "Team" + teams.value = selectedTeam?.title + teams.tapHandler = { + let vc = TeamsViewController() + vc.teamDecks = teamDecks + vc.delegate = self + self.pushConfigurationViewController(vc) + } + items.append(teams) + } + + let channelDecks = getChannelItems(forTeamId: selectedTeam?.id) + if let channels = SLComposeSheetConfigurationItem() { + channels.title = "Channels" + channels.value = selectedChannel?.title + channels.valuePending = channelDecks == nil + channels.tapHandler = { + let vc = ChannelsViewController() + vc.channelDecks = channelDecks! + vc.delegate = self + self.pushConfigurationViewController(vc) + } + + items.append(channels) + } + + validateContent() + return items + } + + // MARK: - Extension Builder + + func attachmentsCount() -> Int { + var count = 0 + for item in extensionContext?.inputItems as! [NSExtensionItem] { + guard let attachments = item.attachments else {return 0} + for itemProvider in attachments { + if itemProvider.hasItemConformingToTypeIdentifier(kUTTypeMovie as String) || + itemProvider.hasItemConformingToTypeIdentifier(kUTTypeImage as String) || + itemProvider.hasItemConformingToTypeIdentifier(kUTTypeFileURL as String) { + count = count + 1 + } + } + } + + return count + } + + func buildChannelSection(channels: NSArray, currentChannelId: String, key: String, title:String) -> Section { + let section = Section() + section.title = title + for channel in channels as! [NSDictionary] { + let item = Item() + let id = channel.object(forKey: "id") as? String + item.id = id + item.title = channel.object(forKey: "display_name") as? String + if id == currentChannelId { + item.selected = true + selectedChannel = item + } + section.items.append(item) + } + return section + } + + func extractDataFromContext() { + for item in extensionContext?.inputItems as! [NSExtensionItem] { + guard let attachments = item.attachments else {continue} + for itemProvider in attachments { + if itemProvider.hasItemConformingToTypeIdentifier(kUTTypeMovie as String) { + dispatchGroup.enter() + itemProvider.loadItem(forTypeIdentifier: kUTTypeMovie as String, options: nil, completionHandler: ({item, error in + if error == nil { + let attachment = self.saveAttachment(url: item as! URL) + if (attachment != nil) { + attachment?.type = kUTTypeMovie as String + self.attachments.append(attachment!) + } + } + self.dispatchGroup.leave() + })) + } else if itemProvider.hasItemConformingToTypeIdentifier(kUTTypeImage as String) { + dispatchGroup.enter() + itemProvider.loadItem(forTypeIdentifier: kUTTypeImage as String, options: nil, completionHandler: ({item, error in + if error == nil { + let attachment = self.saveAttachment(url: item as! URL) + if (attachment != nil) { + attachment?.type = kUTTypeImage as String + self.attachments.append(attachment!) + } + } + self.dispatchGroup.leave() + })) + } else if itemProvider.hasItemConformingToTypeIdentifier(kUTTypeFileURL as String) { + dispatchGroup.enter() + itemProvider.loadItem(forTypeIdentifier: kUTTypeFileURL as String, options: nil, completionHandler: ({item, error in + if error == nil { + let attachment = self.saveAttachment(url: item as! URL) + if (attachment != nil) { + attachment?.type = kUTTypeFileURL as String + self.attachments.append(attachment!) + } + } + self.dispatchGroup.leave() + })) + } else if itemProvider.hasItemConformingToTypeIdentifier(kUTTypeURL as String) { + itemProvider.loadItem(forTypeIdentifier: kUTTypeURL as String, options: nil, completionHandler: ({item, error in + if let url = item as? URL { + self.publicURL = url.absoluteString + } + })) + } + } + } + dispatchGroup.notify(queue: DispatchQueue.main) { + self.validateContent() + } + } + + func getChannelsFromServerAndReload(forTeamId: String) { + var currentChannel = store.getCurrentChannel() as NSDictionary? + if currentChannel?.object(forKey: "team_id") as! String != forTeamId { + currentChannel = store.getDefaultChannel(forTeamId) as NSDictionary? + } + + // If currentChannel is nil it means we don't have the channels for this team + if (currentChannel == nil) { + let urlString = "\(serverURL!)/api/v4/users/me/teams/\(forTeamId)/channels" + let url = URL(string: urlString) + var request = URLRequest(url: url!) + let auth = "Bearer \(sessionToken!)" as String + request.setValue(auth, forHTTPHeaderField: "Authorization") + + let task = URLSession.shared.dataTask(with: request) { (data, response, error) in + guard let dataResponse = data, + error == nil else { + print(error?.localizedDescription ?? "Response Error") + return + + } + + do{ + //here dataResponse received from a network request + let jsonArray = try JSONSerialization.jsonObject(with: dataResponse, options: []) as! NSArray + let channels = jsonArray.filter {element in + let channel = element as! NSDictionary + let type = channel.object(forKey: "type") as! String + return type == "O" || type == "P" + } + let ent = self.store.getEntities(false)! as NSDictionary + let mutableEntities = ent.mutableCopy() as! NSMutableDictionary + let entitiesChannels = NSDictionary(dictionary: mutableEntities.object(forKey: "channels") as! NSMutableDictionary) + .object(forKey: "channels") as! NSMutableDictionary + + for item in channels { + let channel = item as! NSDictionary + entitiesChannels.setValue(channel, forKey: channel.object(forKey: "id") as! String) + } + + if let entitiesData: NSData = try? JSONSerialization.data(withJSONObject: ent, options: JSONSerialization.WritingOptions.prettyPrinted) as NSData { + let jsonString = String(data: entitiesData as Data, encoding: String.Encoding.utf8)! as String + self.store.updateEntities(jsonString) + self.store.getEntities(true) + self.reloadConfigurationItems() + self.view.setNeedsDisplay() + } + } catch let parsingError { + print("Error", parsingError) + } + } + task.resume() + } + } + + func getChannelItems(forTeamId: String?) -> [Section]? { + var channelDecks = [Section]() + var currentChannel = store.getCurrentChannel() as NSDictionary? + if currentChannel?.object(forKey: "team_id") as? String != forTeamId { + currentChannel = store.getDefaultChannel(forTeamId) as NSDictionary? + } + + if currentChannel == nil { + return nil + } + + let channelsInTeamBySections = store.getChannelsBySections(forTeamId) as NSDictionary + channelDecks.append(buildChannelSection( + channels: channelsInTeamBySections.object(forKey: "public") as! NSArray, + currentChannelId: selectedChannel?.id ?? currentChannel?.object(forKey: "id") as! String, + key: "public", + title: "Public Channels" + )) + + channelDecks.append(buildChannelSection( + channels: channelsInTeamBySections.object(forKey: "private") as! NSArray, + currentChannelId: selectedChannel?.id ?? currentChannel?.object(forKey: "id") as! String, + key: "private", + title: "Private Channels" + )) + + channelDecks.append(buildChannelSection( + channels: channelsInTeamBySections.object(forKey: "direct") as! NSArray, + currentChannelId: selectedChannel?.id ?? currentChannel?.object(forKey: "id") as! String, + key: "direct", + title: "Direct Channels" + )) + + return channelDecks + } + + func getTeamItems() -> [Item] { + var teamDecks = [Item]() + let currentTeamId = store.getCurrentTeamId() + let teams = store.getMyTeams() as NSArray? + + for case let team as NSDictionary in teams! { + let item = Item() + item.title = team.object(forKey: "display_name") as! String? + item.id = team.object(forKey: "id") as! String? + item.selected = false + if (item.id == (selectedTeam?.id ?? currentTeamId)) { + item.selected = true + selectedTeam = item + } + teamDecks.append(item) + } + + return teamDecks + } + + func saveAttachment(url: URL) -> AttachmentItem? { + let tempURL: URL? = UploadSessionManager.shared.tempContainerURL() as URL? + let fileMgr = FileManager.default + let fileName = url.lastPathComponent + let tempFileURL = tempURL?.appendingPathComponent(fileName) + + do { + try? FileManager.default.removeItem(at: tempFileURL!) + try fileMgr.copyItem(at: url, to: tempFileURL!) + let attr = try fileMgr.attributesOfItem(atPath: (tempFileURL?.path)!) as NSDictionary + + let attachment = AttachmentItem() + attachment.fileName = fileName + attachment.fileURL = tempFileURL + attachment.fileSize = attr.fileSize() + return attachment + } catch { + return nil + } + } + + // MARK: - Utiilities + + func showErrorMessage(title: String, message: String, VC: UIViewController) { + let alert: UIAlertController = UIAlertController(title: title, message: message, preferredStyle: UIAlertController.Style.alert) + let okAction = UIAlertAction(title: "OK", style: UIAlertAction.Style.default) { + UIAlertAction in + self.cancel() + } + alert.addAction(okAction) + VC.present(alert, animated: true, completion: nil) + } + + func formatFileSize(bytes: Double) -> String { + guard bytes > 0 else { + return "0 bytes" + } + + // Adapted from http://stackoverflow.com/a/18650828 + let suffixes = ["bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"] + let k: Double = 1024 + let i = floor(log(bytes) / log(k)) + + // Format number with thousands separator and everything below 1 GB with no decimal places. + let numberFormatter = NumberFormatter() + numberFormatter.maximumFractionDigits = i < 3 ? 0 : 1 + numberFormatter.numberStyle = .decimal + + let numberString = numberFormatter.string(from: NSNumber(value: bytes / pow(k, i))) ?? "Unknown" + let suffix = suffixes[Int(i)] + return "\(numberString) \(suffix)" + } +} + +extension ShareViewController: TeamsViewControllerDelegate { + func selectedTeam(deck: Item) { + selectedTeam = deck + selectedChannel = nil + self.getChannelsFromServerAndReload(forTeamId: deck.id!) + reloadConfigurationItems() + popConfigurationViewController() + } +} + +extension ShareViewController: ChannelsViewControllerDelegate { + func selectedChannel(deck: Item) { + selectedChannel = deck + reloadConfigurationItems() + popConfigurationViewController() + } +} diff --git a/ios/MattermostShare/TeamsViewController.swift b/ios/MattermostShare/TeamsViewController.swift new file mode 100644 index 0000000000..14768f0cc6 --- /dev/null +++ b/ios/MattermostShare/TeamsViewController.swift @@ -0,0 +1,57 @@ +import UIKit + +class TeamsViewController: UIViewController { + + lazy var tableView: UITableView = { + let tableView = UITableView(frame: self.view.frame) + tableView.autoresizingMask = [.flexibleWidth, .flexibleHeight] + tableView.dataSource = self + tableView.delegate = self + tableView.backgroundColor = .clear + tableView.register(UITableViewCell.self, forCellReuseIdentifier: Identifiers.TeamCell) + return tableView + }() + var teamDecks = [Item]() + weak var delegate: TeamsViewControllerDelegate? + + override func viewDidLoad() { + super.viewDidLoad() + + title = "Teams" + view.addSubview(tableView) + } + +} + +private extension TeamsViewController { + struct Identifiers { + static let TeamCell = "teamCell" + } +} + +extension TeamsViewController: UITableViewDataSource { + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return teamDecks.count + } + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell = tableView.dequeueReusableCell(withIdentifier: Identifiers.TeamCell, for: indexPath) + cell.textLabel?.text = teamDecks[indexPath.row].title + if teamDecks[indexPath.row].selected { + cell.accessoryType = .checkmark + } else { + cell.accessoryType = .none + } + cell.backgroundColor = .clear + return cell + } +} + +protocol TeamsViewControllerDelegate: class { + func selectedTeam(deck: Item) +} + +extension TeamsViewController: UITableViewDelegate { + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + delegate?.selectedTeam(deck: teamDecks[indexPath.row]) + } +} diff --git a/ios/UploadAttachments/UploadAttachments.xcodeproj/project.pbxproj b/ios/UploadAttachments/UploadAttachments.xcodeproj/project.pbxproj new file mode 100644 index 0000000000..0e60d8d3b8 --- /dev/null +++ b/ios/UploadAttachments/UploadAttachments.xcodeproj/project.pbxproj @@ -0,0 +1,371 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 50; + objects = { + +/* Begin PBXBuildFile section */ + 7FABE04E2213818A00D0F595 /* MattermostBucket.m in Sources */ = {isa = PBXBuildFile; fileRef = 7FABE04C2213818900D0F595 /* MattermostBucket.m */; }; + 7FABE055221387B500D0F595 /* Constants.m in Sources */ = {isa = PBXBuildFile; fileRef = 7FABE053221387B400D0F595 /* Constants.m */; }; + 7FABE058221388D700D0F595 /* UploadSessionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FABE057221388D600D0F595 /* UploadSessionManager.swift */; }; + 7FABE05B2213892200D0F595 /* AttachmentItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FABE0592213892100D0F595 /* AttachmentItem.swift */; }; + 7FABE05C2213892200D0F595 /* AttachmentArray.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FABE05A2213892100D0F595 /* AttachmentArray.swift */; }; + 7FABE0F7221466F900D0F595 /* UploadManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FABE0F6221466F900D0F595 /* UploadManager.swift */; }; + 7FABE0FA2214674200D0F595 /* StoreManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 7FABE0F82214674200D0F595 /* StoreManager.m */; }; + 7FABE0FC2214800F00D0F595 /* UploadSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FABE0FB2214800F00D0F595 /* UploadSession.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 7FABE03422137F2900D0F595 /* CopyFiles */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = "include/$(PRODUCT_NAME)"; + dstSubfolderSpec = 16; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 7FABE03622137F2900D0F595 /* libUploadAttachments.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libUploadAttachments.a; sourceTree = BUILT_PRODUCTS_DIR; }; + 7FABE04B2213818900D0F595 /* UploadAttachments-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "UploadAttachments-Bridging-Header.h"; sourceTree = ""; }; + 7FABE04C2213818900D0F595 /* MattermostBucket.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MattermostBucket.m; sourceTree = ""; }; + 7FABE04D2213818A00D0F595 /* MattermostBucket.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MattermostBucket.h; sourceTree = ""; }; + 7FABE053221387B400D0F595 /* Constants.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = Constants.m; sourceTree = ""; }; + 7FABE054221387B500D0F595 /* Constants.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Constants.h; sourceTree = ""; }; + 7FABE057221388D600D0F595 /* UploadSessionManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UploadSessionManager.swift; sourceTree = ""; }; + 7FABE0592213892100D0F595 /* AttachmentItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AttachmentItem.swift; sourceTree = ""; }; + 7FABE05A2213892100D0F595 /* AttachmentArray.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AttachmentArray.swift; sourceTree = ""; }; + 7FABE0F6221466F900D0F595 /* UploadManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UploadManager.swift; sourceTree = ""; }; + 7FABE0F82214674200D0F595 /* StoreManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = StoreManager.m; sourceTree = ""; }; + 7FABE0F92214674200D0F595 /* StoreManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = StoreManager.h; sourceTree = ""; }; + 7FABE0FB2214800F00D0F595 /* UploadSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UploadSession.swift; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 7FABE03322137F2900D0F595 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 7FABE02D22137F2900D0F595 = { + isa = PBXGroup; + children = ( + 7FABE03822137F2900D0F595 /* UploadAttachments */, + 7FABE03722137F2900D0F595 /* Products */, + ); + sourceTree = ""; + }; + 7FABE03722137F2900D0F595 /* Products */ = { + isa = PBXGroup; + children = ( + 7FABE03622137F2900D0F595 /* libUploadAttachments.a */, + ); + name = Products; + sourceTree = ""; + }; + 7FABE03822137F2900D0F595 /* UploadAttachments */ = { + isa = PBXGroup; + children = ( + 7FABE05A2213892100D0F595 /* AttachmentArray.swift */, + 7FABE0592213892100D0F595 /* AttachmentItem.swift */, + 7FABE054221387B500D0F595 /* Constants.h */, + 7FABE053221387B400D0F595 /* Constants.m */, + 7FABE04D2213818A00D0F595 /* MattermostBucket.h */, + 7FABE04C2213818900D0F595 /* MattermostBucket.m */, + 7FABE0F92214674200D0F595 /* StoreManager.h */, + 7FABE0F82214674200D0F595 /* StoreManager.m */, + 7FABE04B2213818900D0F595 /* UploadAttachments-Bridging-Header.h */, + 7FABE0F6221466F900D0F595 /* UploadManager.swift */, + 7FABE057221388D600D0F595 /* UploadSessionManager.swift */, + 7FABE0FB2214800F00D0F595 /* UploadSession.swift */, + ); + path = UploadAttachments; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 7FABE03522137F2900D0F595 /* UploadAttachments */ = { + isa = PBXNativeTarget; + buildConfigurationList = 7FABE03D22137F2900D0F595 /* Build configuration list for PBXNativeTarget "UploadAttachments" */; + buildPhases = ( + 7FABE03222137F2900D0F595 /* Sources */, + 7FABE03322137F2900D0F595 /* Frameworks */, + 7FABE0472213800800D0F595 /* Generate header files */, + 7FABE03422137F2900D0F595 /* CopyFiles */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = UploadAttachments; + productName = UploadAttachments; + productReference = 7FABE03622137F2900D0F595 /* libUploadAttachments.a */; + productType = "com.apple.product-type.library.static"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 7FABE02E22137F2900D0F595 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 1010; + LastUpgradeCheck = 1010; + ORGANIZATIONNAME = Mattermost; + TargetAttributes = { + 7FABE03522137F2900D0F595 = { + CreatedOnToolsVersion = 10.1; + LastSwiftMigration = 1010; + }; + }; + }; + buildConfigurationList = 7FABE03122137F2900D0F595 /* Build configuration list for PBXProject "UploadAttachments" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + ); + mainGroup = 7FABE02D22137F2900D0F595; + productRefGroup = 7FABE03722137F2900D0F595 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 7FABE03522137F2900D0F595 /* UploadAttachments */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXShellScriptBuildPhase section */ + 7FABE0472213800800D0F595 /* Generate header files */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = "Generate header files"; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "# Type a script or drag a script file from your workspace to insert its path.\n\ntarget_dir=${BUILT_PRODUCTS_DIR}/include/${PRODUCT_MODULE_NAME}/\n\n# Ensure the target include path exists\nmkdir -p ${target_dir}\n\n# Copy any file that looks like a Swift generated header to the include path\ncp ${DERIVED_SOURCES_DIR}/*-Swift.h ${target_dir}\n"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 7FABE03222137F2900D0F595 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 7FABE05C2213892200D0F595 /* AttachmentArray.swift in Sources */, + 7FABE0FA2214674200D0F595 /* StoreManager.m in Sources */, + 7FABE058221388D700D0F595 /* UploadSessionManager.swift in Sources */, + 7FABE0F7221466F900D0F595 /* UploadManager.swift in Sources */, + 7FABE04E2213818A00D0F595 /* MattermostBucket.m in Sources */, + 7FABE055221387B500D0F595 /* Constants.m in Sources */, + 7FABE05B2213892200D0F595 /* AttachmentItem.swift in Sources */, + 7FABE0FC2214800F00D0F595 /* UploadSession.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 7FABE03B22137F2900D0F595 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 10.3; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 7FABE03C22137F2900D0F595 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 10.3; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 7FABE03E22137F2900D0F595 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = UQ8HT4Q2XM; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MODULEMAP_FILE = "$(SRCROOT)/UploadAttachments/module.modulemap"; + OTHER_LDFLAGS = "-ObjC"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_OBJC_BRIDGING_HEADER = "UploadAttachments/UploadAttachments-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 4.2; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 7FABE03F22137F2900D0F595 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = UQ8HT4Q2XM; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MODULEMAP_FILE = "$(SRCROOT)/UploadAttachments/module.modulemap"; + OTHER_LDFLAGS = "-ObjC"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_OBJC_BRIDGING_HEADER = "UploadAttachments/UploadAttachments-Bridging-Header.h"; + SWIFT_VERSION = 4.2; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 7FABE03122137F2900D0F595 /* Build configuration list for PBXProject "UploadAttachments" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 7FABE03B22137F2900D0F595 /* Debug */, + 7FABE03C22137F2900D0F595 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 7FABE03D22137F2900D0F595 /* Build configuration list for PBXNativeTarget "UploadAttachments" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 7FABE03E22137F2900D0F595 /* Debug */, + 7FABE03F22137F2900D0F595 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 7FABE02E22137F2900D0F595 /* Project object */; +} diff --git a/ios/UploadAttachments/UploadAttachments.xcodeproj/xcshareddata/xcschemes/UploadAttachments.xcscheme b/ios/UploadAttachments/UploadAttachments.xcodeproj/xcshareddata/xcschemes/UploadAttachments.xcscheme new file mode 100644 index 0000000000..8dd4cbe613 --- /dev/null +++ b/ios/UploadAttachments/UploadAttachments.xcodeproj/xcshareddata/xcschemes/UploadAttachments.xcscheme @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/UploadAttachments/UploadAttachments/AttachmentArray.swift b/ios/UploadAttachments/UploadAttachments/AttachmentArray.swift new file mode 100644 index 0000000000..0377a237db --- /dev/null +++ b/ios/UploadAttachments/UploadAttachments/AttachmentArray.swift @@ -0,0 +1,269 @@ +/// A thread-safe array. +public class AttachmentArray: NSObject { + fileprivate let queue = DispatchQueue(label: "com.mattermost.SynchronizedArray", attributes: .concurrent) + fileprivate var array = [Element]() + + public override init() {} +} + +// MARK: - Properties +public extension AttachmentArray { + + /// The first element of the collection. + var first: Element? { + var result: Element? + queue.sync { result = self.array.first } + return result + } + + /// The last element of the collection. + var last: Element? { + var result: Element? + queue.sync { result = self.array.last } + return result + } + + /// The number of elements in the array. + var count: Int { + var result = 0 + queue.sync { result = self.array.count } + return result + } + + /// A Boolean value indicating whether the collection is empty. + var isEmpty: Bool { + var result = false + queue.sync { result = self.array.isEmpty } + return result + } +} + +// MARK: - Immutable +public extension AttachmentArray { + /// Returns the first element of the sequence that satisfies the given predicate or nil if no such element is found. + /// + /// - Parameter predicate: A closure that takes an element of the sequence as its argument and returns a Boolean value indicating whether the element is a match. + /// - Returns: The first match or nil if there was no match. + func first(where predicate: (Element) -> Bool) -> Element? { + var result: Element? + queue.sync { result = self.array.first(where: predicate) } + return result + } + + /// Returns an array containing, in order, the elements of the sequence that satisfy the given predicate. + /// + /// - Parameter isIncluded: A closure that takes an element of the sequence as its argument and returns a Boolean value indicating whether the element should be included in the returned array. + /// - Returns: An array of the elements that includeElement allowed. + func filter(_ isIncluded: (Element) -> Bool) -> [Element] { + var result = [Element]() + queue.sync { result = self.array.filter(isIncluded) } + return result + } + + /// Returns the first index in which an element of the collection satisfies the given predicate. + /// + /// - Parameter predicate: A closure that takes an element as its argument and returns a Boolean value that indicates whether the passed element represents a match. + /// - Returns: The index of the first element for which predicate returns true. If no elements in the collection satisfy the given predicate, returns nil. + func index(where predicate: (Element) -> Bool) -> Int? { + var result: Int? + queue.sync { result = self.array.index(where: predicate) } + return result + } + + /// Returns the elements of the collection, sorted using the given predicate as the comparison between elements. + /// + /// - Parameter areInIncreasingOrder: A predicate that returns true if its first argument should be ordered before its second argument; otherwise, false. + /// - Returns: A sorted array of the collection’s elements. + func sorted(by areInIncreasingOrder: (Element, Element) -> Bool) -> [Element] { + var result = [Element]() + queue.sync { result = self.array.sorted(by: areInIncreasingOrder) } + return result + } + + /// Returns an array containing the non-nil results of calling the given transformation with each element of this sequence. + /// + /// - Parameter transform: A closure that accepts an element of this sequence as its argument and returns an optional value. + /// - Returns: An array of the non-nil results of calling transform with each element of the sequence. + func flatMap(_ transform: (Element) -> ElementOfResult?) -> [ElementOfResult] { + var result = [ElementOfResult]() + queue.sync { result = self.array.flatMap(transform) } + return result + } + + /// Calls the given closure on each element in the sequence in the same order as a for-in loop. + /// + /// - Parameter body: A closure that takes an element of the sequence as a parameter. + func forEach(_ body: (Element) -> Void) { + queue.sync { self.array.forEach(body) } + } + + /// Returns a Boolean value indicating whether the sequence contains an element that satisfies the given predicate. + /// + /// - Parameter predicate: A closure that takes an element of the sequence as its argument and returns a Boolean value that indicates whether the passed element represents a match. + /// - Returns: true if the sequence contains an element that satisfies predicate; otherwise, false. + func contains(where predicate: (Element) -> Bool) -> Bool { + var result = false + queue.sync { result = self.array.contains(where: predicate) } + return result + } +} + +// MARK: - Mutable +public extension AttachmentArray { + + /// Adds a new element at the end of the array. + /// + /// - Parameter element: The element to append to the array. + func append( _ element: Element) { + queue.async(flags: .barrier) { + self.array.append(element) + } + } + + /// Adds a new element at the end of the array. + /// + /// - Parameter element: The element to append to the array. + func append( _ elements: [Element]) { + queue.async(flags: .barrier) { + self.array += elements + } + } + + /// Inserts a new element at the specified position. + /// + /// - Parameters: + /// - element: The new element to insert into the array. + /// - index: The position at which to insert the new element. + func insert( _ element: Element, at index: Int) { + queue.async(flags: .barrier) { + self.array.insert(element, at: index) + } + } + + /// Removes and returns the element at the specified position. + /// + /// - Parameters: + /// - index: The position of the element to remove. + /// - completion: The handler with the removed element. + func remove(at index: Int, completion: ((Element) -> Void)? = nil) { + queue.async(flags: .barrier) { + let element = self.array.remove(at: index) + + DispatchQueue.main.async { + completion?(element) + } + } + } + + /// Removes and returns the element at the specified position. + /// + /// - Parameters: + /// - predicate: A closure that takes an element of the sequence as its argument and returns a Boolean value indicating whether the element is a match. + /// - completion: The handler with the removed element. + func remove(where predicate: @escaping (Element) -> Bool, completion: ((Element) -> Void)? = nil) { + queue.async(flags: .barrier) { + guard let index = self.array.index(where: predicate) else { return } + let element = self.array.remove(at: index) + + DispatchQueue.main.async { + completion?(element) + } + } + } + + /// Removes all elements from the array. + /// + /// - Parameter completion: The handler with the removed elements. + func removeAll(completion: (([Element]) -> Void)? = nil) { + queue.async(flags: .barrier) { + let elements = self.array + self.array.removeAll() + + DispatchQueue.main.async { + completion?(elements) + } + } + } +} + +public extension AttachmentArray { + + /// Accesses the element at the specified position if it exists. + /// + /// - Parameter index: The position of the element to access. + /// - Returns: optional element if it exists. + subscript(index: Int) -> Element? { + get { + var result: Element? + + queue.sync { + guard self.array.startIndex.. Bool { + var result = false + queue.sync { result = self.array.contains(element) } + return result + } +} + +// MARK: - Infix operators +public extension AttachmentArray { + + static func +=(left: inout AttachmentArray, right: Element) { + left.append(right) + } + + static func +=(left: inout AttachmentArray, right: [Element]) { + left.append(right) + } +} + +extension AttachmentArray { + public func findBy(type: String) -> Bool { + var found = false + + self.queue.sync { + found = self.array.contains {element in + let attachment = element as! AttachmentItem + return attachment.type == type + } + } + + return found + } + + public func hasAttachementLargerThan(fileSize: UInt64) -> Bool { + var exceed = false + + self.queue.sync { + exceed = self.array.contains { element in + let attachment = element as! AttachmentItem + return attachment.fileSize > fileSize + } + } + + return exceed + } +} diff --git a/ios/UploadAttachments/UploadAttachments/AttachmentItem.swift b/ios/UploadAttachments/UploadAttachments/AttachmentItem.swift new file mode 100644 index 0000000000..7baeb946dc --- /dev/null +++ b/ios/UploadAttachments/UploadAttachments/AttachmentItem.swift @@ -0,0 +1,11 @@ +import Foundation + +public class AttachmentItem: NSObject { + public var fileName: String? + public var fileURL: URL? + public var fileSize: UInt64 = 0 + public var type: String? + + public override init() {} +} + diff --git a/ios/UploadAttachments/UploadAttachments/Constants.h b/ios/UploadAttachments/UploadAttachments/Constants.h new file mode 100644 index 0000000000..05a791f68e --- /dev/null +++ b/ios/UploadAttachments/UploadAttachments/Constants.h @@ -0,0 +1,7 @@ +#import + + +@interface Constants : NSObject +extern NSString *APP_GROUP_ID; +extern UInt64 DEFAULT_SERVER_MAX_FILE_SIZE; +@end diff --git a/ios/UploadAttachments/UploadAttachments/Constants.m b/ios/UploadAttachments/UploadAttachments/Constants.m new file mode 100644 index 0000000000..4c3f899a48 --- /dev/null +++ b/ios/UploadAttachments/UploadAttachments/Constants.m @@ -0,0 +1,6 @@ +#import "Constants.h" + +@implementation Constants +NSString *APP_GROUP_ID = @"group.com.mattermost.rnbeta"; +UInt64 DEFAULT_SERVER_MAX_FILE_SIZE = 50 * 1024 * 1024; +@end diff --git a/ios/UploadAttachments/UploadAttachments/MattermostBucket.h b/ios/UploadAttachments/UploadAttachments/MattermostBucket.h new file mode 100644 index 0000000000..7ffd7aef4e --- /dev/null +++ b/ios/UploadAttachments/UploadAttachments/MattermostBucket.h @@ -0,0 +1,12 @@ +#import + +@interface MattermostBucket : NSObject +- (NSUserDefaults *)bucketByName:(NSString*)name; +-(id) getPreference:(NSString *)key; +-(NSString *)readFromFile:(NSString *)fileName; +-(NSDictionary *)readFromFileAsJSON:(NSString *)fileName; +-(void)removeFile:(NSString *)fileName; +-(void)removePreference:(NSString *)key; +-(void)setPreference:(NSString *)key value:(NSString *) value; +-(void)writeToFile:(NSString *)fileName content:(NSString *)content; +@end diff --git a/ios/UploadAttachments/UploadAttachments/MattermostBucket.m b/ios/UploadAttachments/UploadAttachments/MattermostBucket.m new file mode 100644 index 0000000000..1aa8c7e3e5 --- /dev/null +++ b/ios/UploadAttachments/UploadAttachments/MattermostBucket.m @@ -0,0 +1,69 @@ +#import "Constants.h" +#import "MattermostBucket.h" + +@implementation MattermostBucket + +-(NSUserDefaults *)bucketByName:(NSString*)name { + return [[NSUserDefaults alloc] initWithSuiteName: name]; +} + +-(NSString *)fileUrl:(NSString *)fileName { + NSURL *fileManagerURL = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:APP_GROUP_ID]; + return [NSString stringWithFormat:@"%@/%@", fileManagerURL.path, fileName]; +} + +-(id) getPreference:(NSString *)key { + NSUserDefaults* bucket = [self bucketByName: APP_GROUP_ID]; + return [bucket objectForKey:key]; +} + +-(NSString *)readFromFile:(NSString *)fileName { + NSString *filePath = [self fileUrl:fileName]; + NSFileManager *fileManager= [NSFileManager defaultManager]; + if(![fileManager fileExistsAtPath:filePath]) { + return nil; + } + return [NSString stringWithContentsOfFile:filePath encoding:NSUTF8StringEncoding error:nil]; +} + +-(NSDictionary *)readFromFileAsJSON:(NSString *)fileName { + NSString *filePath = [self fileUrl:fileName]; + NSFileManager *fileManager= [NSFileManager defaultManager]; + if(![fileManager fileExistsAtPath:filePath]) { + return nil; + } + NSData *data = [NSData dataWithContentsOfFile:filePath]; + return [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableContainers error:nil]; +} + +-(void)removeFile:(NSString *)fileName { + NSString *filePath = [self fileUrl:fileName]; + NSFileManager *fileManager= [NSFileManager defaultManager]; + if([fileManager isDeletableFileAtPath:filePath]) { + [fileManager removeItemAtPath:filePath error:nil]; + } +} + +-(void) removePreference:(NSString *)key { + NSUserDefaults* bucket = [self bucketByName: APP_GROUP_ID]; + [bucket removeObjectForKey: key]; +} + +-(void) setPreference:(NSString *)key value:(NSString *) value { + NSUserDefaults* bucket = [self bucketByName: APP_GROUP_ID]; + if (bucket && [key length] > 0 && [value length] > 0) { + [bucket setObject:value forKey:key]; + } +} + +-(void)writeToFile:(NSString *)fileName content:(NSString *)content { + NSString *filePath = [self fileUrl:fileName]; + NSFileManager *fileManager= [NSFileManager defaultManager]; + if(![fileManager fileExistsAtPath:filePath]) { + [fileManager createFileAtPath:filePath contents:nil attributes:nil]; + } + if ([content length] > 0) { + [content writeToFile:filePath atomically:YES encoding:NSUTF8StringEncoding error:nil]; + } +} +@end diff --git a/ios/UploadAttachments/UploadAttachments/StoreManager.h b/ios/UploadAttachments/UploadAttachments/StoreManager.h new file mode 100644 index 0000000000..8a0d282c5d --- /dev/null +++ b/ios/UploadAttachments/UploadAttachments/StoreManager.h @@ -0,0 +1,22 @@ +#import +#import "MattermostBucket.h" + +@interface StoreManager : NSObject +@property MattermostBucket *bucket; +@property (nonatomic, strong) NSDictionary *entities; + ++(instancetype)shared; +-(NSDictionary *)getChannelById:(NSString *)channelId; +-(NSDictionary *)getChannelsBySections:(NSString *)forTeamId; +-(NSDictionary *)getCurrentChannel; +-(NSString *)getCurrentChannelId; +-(NSString *)getCurrentTeamId; +-(NSString *)getCurrentUserId; +-(NSDictionary *)getDefaultChannel:(NSString *)forTeamId; +-(NSDictionary *)getEntities:(BOOL)loadFromFile; +-(UInt64)getMaxFileSize; +-(NSArray *)getMyTeams; +-(NSString *)getServerUrl; +-(NSString *)getToken; +-(void)updateEntities:(NSString *)content; +@end diff --git a/ios/UploadAttachments/UploadAttachments/StoreManager.m b/ios/UploadAttachments/UploadAttachments/StoreManager.m new file mode 100644 index 0000000000..7aebbcd4fc --- /dev/null +++ b/ios/UploadAttachments/UploadAttachments/StoreManager.m @@ -0,0 +1,303 @@ +#import "StoreManager.h" +#import "Constants.h" + +@implementation StoreManager ++(instancetype)shared { + static id manager = nil; + static dispatch_once_t once; + dispatch_once(&once, ^{ + manager = [[self alloc] init]; + }); + + return manager; +} + +-(instancetype)init { + self = [super init]; + if (self) { + self.bucket = [[MattermostBucket alloc] init]; + [self getEntities:true]; + } + return self; +} + +#pragma public methods + +-(NSDictionary *)getChannelById:(NSString *)channelId { + NSDictionary *channelsStore = [self.entities objectForKey:@"channels"]; + NSDictionary *channels = [channelsStore objectForKey:@"channels"]; + + for (NSString * key in channels) { + NSDictionary *channel = channels[key]; + NSString *channel_id = [channel objectForKey:@"id"]; + if ([channel_id isEqualToString:channelId]) { + return channel; + } + } + + return nil; +} + +-(NSDictionary *)getChannelsBySections:(NSString *)forTeamId { + NSDictionary *channelsStore = [self.entities objectForKey:@"channels"]; + NSString *currentUserId = [self getCurrentUserId]; + NSDictionary *channels = [channelsStore objectForKey:@"channels"]; + NSMutableDictionary *channelsInTeam = [[NSMutableDictionary alloc] init]; + NSMutableArray *publicChannels = [[NSMutableArray alloc] init]; + NSMutableArray *privateChannels = [[NSMutableArray alloc] init]; + NSMutableArray *directChannels = [[NSMutableArray alloc] init]; + + for (NSString * key in channels) { + NSMutableDictionary *channel = [[channels objectForKey:key] mutableCopy]; + NSString *team_id = [channel objectForKey:@"team_id"]; + NSString *channelType = [channel objectForKey:@"type"]; + BOOL isDM = [channelType isEqualToString:@"D"]; + BOOL isGM = [channelType isEqualToString:@"G"]; + BOOL isPublic = [channelType isEqualToString:@"O"]; + BOOL isPrivate = [channelType isEqualToString:@"P"]; + if ([team_id isEqualToString:forTeamId] || isDM || isGM) { + if (isPublic) { + // public channel + [publicChannels addObject:channel]; + } else if (isPrivate) { + // private channel + [privateChannels addObject:channel]; + } else if (isDM) { + // direct message + NSString *otherUserId = [self getOtherUserIdFromChannel:currentUserId withChannelName:[channel objectForKey:@"name"]]; + NSDictionary *otherUser = [self getUserById:otherUserId]; + if (otherUser) { + [channel setObject:[self displayUserName:otherUser] forKey:@"display_name"]; + [directChannels addObject:channel]; + } + } else { + [channel setObject:[self completeDirectGroupInfo:key] forKey:@"display_name"]; + [directChannels addObject:channel]; + } + } + } + + [channelsInTeam setObject:[self sortDictArrayByDisplayName:publicChannels] forKey:@"public"]; + [channelsInTeam setObject:[self sortDictArrayByDisplayName:privateChannels] forKey:@"private"]; + [channelsInTeam setObject:[self sortDictArrayByDisplayName:directChannels] forKey:@"direct"]; + + return channelsInTeam; +} + +-(NSDictionary *)getCurrentChannel { + NSString *currentChannelId = [self getCurrentChannelId]; + return [self getChannelById:currentChannelId]; +} + + + +-(NSString *)getCurrentChannelId { + return [[self.entities objectForKey:@"channels"] objectForKey:@"currentChannelId"]; +} + +-(NSString *)getCurrentTeamId { + return [[self.entities objectForKey:@"teams"] objectForKey:@"currentTeamId"]; +} + +-(NSString *)getCurrentUserId { + return [[self.entities objectForKey:@"users"] objectForKey:@"currentUserId"]; +} + +-(NSDictionary *)getDefaultChannel:(NSString *)forTeamId { + NSArray *channelsInTeam = [self getChannelsInTeam:forTeamId]; + NSPredicate *filter = [NSPredicate predicateWithFormat:@"name = %@", @"town-square"]; + NSArray *townSquare = [channelsInTeam filteredArrayUsingPredicate:filter]; + if (townSquare != nil && townSquare.count > 0) { + return townSquare.firstObject; + } + + return nil; +} + +-(NSDictionary *)getEntities:(BOOL)loadFromFile { + if (loadFromFile) { + self.entities = [self.bucket readFromFileAsJSON:@"entities"]; + } + return self.entities; +} + +-(NSArray *)getMyTeams { + NSDictionary *teamsStore = [self.entities objectForKey:@"teams"]; + NSDictionary *teams = [teamsStore objectForKey:@"teams"]; + NSDictionary *membership = [teamsStore objectForKey:@"myMembers"]; + NSMutableArray *myTeams = [[NSMutableArray alloc] init]; + + for (NSString* key in teams) { + NSDictionary *member = [membership objectForKey:key]; + NSDictionary *team = [teams objectForKey:key]; + if (member) { + [myTeams addObject:team]; + } + } + + + return [self sortDictArrayByDisplayName:myTeams]; +} + +-(NSString *)getServerUrl { + NSDictionary *general = [self.entities objectForKey:@"general"]; + NSDictionary *credentials = [general objectForKey:@"credentials"]; + + return [credentials objectForKey:@"url"]; +} + +-(NSString *)getToken { + NSDictionary *general = [self.entities objectForKey:@"general"]; + NSDictionary *credentials = [general objectForKey:@"credentials"]; + + return [credentials objectForKey:@"token"]; +} + +-(UInt64)getMaxFileSize { + NSDictionary *config = [self getConfig]; + if (config != nil && [config objectForKey:@"MaxFileSize"]) { + NSString *maxFileSize = [config objectForKey:@"MaxFileSize"]; + NSScanner *scanner = [NSScanner scannerWithString:maxFileSize]; + unsigned long long convertedValue = 0; + [scanner scanUnsignedLongLong:&convertedValue]; + return convertedValue; + } + + return DEFAULT_SERVER_MAX_FILE_SIZE; +} + +-(void)updateEntities:(NSString *)content { + [self.bucket writeToFile:@"entities" content:content]; +} + +#pragma utilities + +-(NSString *)completeDirectGroupInfo:(NSString *)channelId { + NSDictionary *usersStore = [self.entities objectForKey:@"users"]; + NSDictionary *profilesInChannels = [usersStore objectForKey:@"profilesInChannel"]; + NSDictionary *profileIds = [profilesInChannels objectForKey:channelId]; + NSDictionary *channel = [self getChannelById:channelId]; + NSString *currentUserId = [self getCurrentUserId]; + NSMutableArray *result = [[NSMutableArray alloc] init]; + + if (profileIds) { + for (NSString *key in profileIds) { + NSDictionary *user = [self getUserById:key]; + NSString *userId = [user objectForKey:@"id"]; + if (![userId isEqualToString:currentUserId]) { + NSString *fullName = [self getUserFullName:user]; + [result addObject:fullName]; + } + } + } + + if ([result count] != ([profileIds count] - 1)) { + return [channel objectForKey:@"display_name"]; + } + + NSArray *sorted = [result sortedArrayUsingSelector:@selector(localizedCaseInsensitiveCompare:)]; + return [sorted componentsJoinedByString:@", "]; +} + +-(NSString *)displayUserName:(NSDictionary *)user { + NSString *teammateNameDisplay = [self getTeammateNameDisplaySetting]; + NSString *username = [user objectForKey:@"username"]; + NSString *name; + + if ([teammateNameDisplay isEqualToString:@"nickname_full_name"]) { + name = [user objectForKey:@"nickname"] ?: [self getUserFullName:user]; + } else if ([teammateNameDisplay isEqualToString:@"full_name"]) { + name = [self getUserFullName:user]; + } else { + name = username; + } + + if (!name || [[name stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]] length] == 0) { + name = username; + } + + return name; +} + +-(NSArray *)getChannelsInTeam:(NSString *) teamId { + NSDictionary *channels = [[self.entities objectForKey:@"channels"] objectForKey:@"channels"]; + NSArray *arr = [channels allValues]; + NSPredicate *filter = [NSPredicate predicateWithFormat:@"team_id = %@", teamId]; + + return [arr filteredArrayUsingPredicate:filter]; +} + +-(NSDictionary *)getConfig { + return [[self.entities objectForKey:@"general"] objectForKey:@"config"]; +} + +-(NSDictionary *)getMyPreferences { + return [[self.entities objectForKey:@"preferences"] objectForKey:@"myPreferences"]; +} + +-(NSString *)getOtherUserIdFromChannel:(NSString *)currentUserId withChannelName:(NSString *)channelName { + NSArray *ids = [channelName componentsSeparatedByString:@"_"]; + if ([ids[0] isEqualToString:currentUserId]) { + return ids[2]; + } + + return ids[0]; +} + +-(NSDictionary *)getUserById:(NSString *)userId { + NSDictionary *usersStore = [self.entities objectForKey:@"users"]; + NSDictionary *users = [usersStore objectForKey:@"profiles"]; + + for (NSString* key in users) { + NSDictionary *user = [users objectForKey:key]; + NSString *user_id = [user objectForKey:@"id"]; + if ([user_id isEqualToString:userId]) { + return user; + } + } + + return nil; +} + +-(NSString *)getUserFullName:(NSDictionary*) user { + NSString *fistName = [user objectForKey:@"first_name"]; + NSString *lastName = [user objectForKey:@"last_name"]; + + if (fistName && lastName) { + return [fistName stringByAppendingFormat:@" %@", lastName]; + } else if (fistName) { + return fistName; + } else if (lastName) { + return lastName; + } + + return @""; +} + +-(NSString *)getTeammateNameDisplaySetting { + NSDictionary *config = [self getConfig]; + NSString *teammateNameDisplay = [config objectForKey:@"TeammateNameDisplay"]; + + NSDictionary *preferences = [self getMyPreferences]; + NSString *key = @"display_settings--name_format"; + NSDictionary *displayFormat = [preferences objectForKey:key]; + + if (displayFormat) { + return [displayFormat objectForKey:@"value"]; + } else if (teammateNameDisplay) { + return teammateNameDisplay; + } + + return @"username"; +} + +-(NSArray *)sortDictArrayByDisplayName:(NSArray *)array { + NSSortDescriptor *sd = [[NSSortDescriptor alloc] + initWithKey:@"display_name" + ascending:YES + selector: @selector(localizedCaseInsensitiveCompare:)]; + NSArray *sortDescriptor = [NSArray arrayWithObjects:sd, nil]; + return [array sortedArrayUsingDescriptors:sortDescriptor]; +} + +@end diff --git a/ios/UploadAttachments/UploadAttachments/UploadAttachments-Bridging-Header.h b/ios/UploadAttachments/UploadAttachments/UploadAttachments-Bridging-Header.h new file mode 100644 index 0000000000..dae2745e48 --- /dev/null +++ b/ios/UploadAttachments/UploadAttachments/UploadAttachments-Bridging-Header.h @@ -0,0 +1,7 @@ +// +// Use this file to import your target's public headers that you would like to expose to Swift. +// + +#import "Constants.h" +#import "MattermostBucket.h" +#import "StoreManager.h" diff --git a/ios/UploadAttachments/UploadAttachments/UploadManager.swift b/ios/UploadAttachments/UploadAttachments/UploadManager.swift new file mode 100644 index 0000000000..d86d5c14de --- /dev/null +++ b/ios/UploadAttachments/UploadAttachments/UploadManager.swift @@ -0,0 +1,41 @@ +import Foundation +import UIKit + +@objc @objcMembers public class UploadManager: NSObject { + public class var shared :UploadManager { + struct Singleton { + static let instance = UploadManager() + } + return Singleton.instance + } + + public func uploadFiles(baseURL: String, token: String, channelId: String, message: String?, attachments: AttachmentArray, callback: () -> Void) { + let identifier = "mattermost-share-upload-\(UUID().uuidString)" + UploadSessionManager.shared.createUploadSessionData( + identifier: identifier, + channelId: channelId, + message: message ?? "", + totalFiles: attachments.count + ) + + if attachments.count > 0 { + // if the share action has attachments we upload the files first + let uploadSession = UploadSession.shared.createURLSession(identifier: identifier) + + for index in 0.. Void)? + public var session: URLSession? + + public func createPost(identifier: String) { + let store = StoreManager.shared() as StoreManager + let _ = store.getEntities(true) + let serverURL = store.getServerUrl() + let sessionToken = store.getToken() + let urlString = "\(serverURL!)/api/v4/posts" + + guard let uploadSessionData = UploadSessionManager.shared.getUploadSessionData(identifier: identifier) else {return} + guard let url = URL(string: urlString) else {return} + + if uploadSessionData.message != "" || uploadSessionData.fileIds.count > 0 { + let jsonObject: [String: Any] = [ + "channel_id": uploadSessionData.channelId as Any, + "message": uploadSessionData.message as Any, + "file_ids": uploadSessionData.fileIds + ] + if !JSONSerialization.isValidJSONObject(jsonObject) {return} + + var request = URLRequest(url: url) + request.httpMethod = "POST" + request.setValue("Bearer \(sessionToken!)", forHTTPHeaderField: "Authorization") + request.setValue("application/json; charset=utf-8", forHTTPHeaderField: "Content-Type") + request.httpBody = try? JSONSerialization.data(withJSONObject: jsonObject, options: .prettyPrinted) + URLSession(configuration: .ephemeral).dataTask(with: request).resume() + + UploadSessionManager.shared.removeUploadSessionData(identifier: identifier) + UploadSessionManager.shared.clearTempDirectory() + } + } + + public func attachSession(identifier: String, completionHandler: @escaping () -> Void) { + self.completionHandler = completionHandler + let sessionConfig = URLSessionConfiguration.background(withIdentifier: identifier) + sessionConfig.sharedContainerIdentifier = APP_GROUP_ID + if #available(iOS 11.0, *) { + sessionConfig.waitsForConnectivity = true + } + + session = URLSession(configuration: sessionConfig, delegate: self, delegateQueue: OperationQueue.main) + } + + public func createURLSession(identifier: String) -> URLSession { + let sessionConfig = URLSessionConfiguration.background(withIdentifier: identifier) + sessionConfig.sharedContainerIdentifier = APP_GROUP_ID + if #available(iOS 11.0, *) { + sessionConfig.waitsForConnectivity = true + } + + self.session = URLSession(configuration: sessionConfig, delegate: self, delegateQueue: nil) + return self.session! + } + + public func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) { + // here we should get the file Id and update it in the session + guard let identifier = session.configuration.identifier else {return} + do { + let jsonObject = try JSONSerialization.jsonObject(with: data, options: []) as! NSDictionary + let fileInfos = jsonObject.object(forKey: "file_infos") as! NSArray + if fileInfos.count > 0 { + let fileInfoData = fileInfos[0] as! NSDictionary + let fileId = fileInfoData.object(forKey: "id") as! String + UploadSessionManager.shared.appendCompletedUploadToSession(identifier: identifier, fileId: fileId) + } + } catch { + print("MMLOG: Failed to get the file upload response %@", error.localizedDescription) + } + + } + + public func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { + if error == nil { + let identifier = session.configuration.identifier! + guard let sessionData = UploadSessionManager.shared.getUploadSessionData(identifier: identifier) else {return} + if sessionData.fileIds.count == sessionData.totalFiles { + ProcessInfo().performExpiringActivity(withReason: "Need to post the message") { (expires) in + self.createPost(identifier: identifier) + } + } + } + } + + public func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) { + DispatchQueue.main.async { + + if self.completionHandler != nil { + self.completionHandler!() + } + } + } +} diff --git a/ios/UploadAttachments/UploadAttachments/UploadSessionManager.swift b/ios/UploadAttachments/UploadAttachments/UploadSessionManager.swift new file mode 100644 index 0000000000..5406862a1a --- /dev/null +++ b/ios/UploadAttachments/UploadAttachments/UploadSessionManager.swift @@ -0,0 +1,79 @@ +import Foundation + +@objc public class UploadSessionData: NSObject { + public var channelId: String? + public var fileIds: [String] = [] + public var message: String = "" + public var totalFiles: Int = 0 +} + +@objc @objcMembers public class UploadSessionManager: NSObject { + private let bucket = MattermostBucket().bucket(byName: APP_GROUP_ID) + + public class var shared :UploadSessionManager { + struct Singleton { + static let instance = UploadSessionManager() + } + return Singleton.instance + } + + public func createUploadSessionData(identifier: String, channelId: String, message: String, totalFiles: Int) { + let fileIds: [String] = [] + let uploadSessionData: NSDictionary = [ + "channelId": channelId, + "fileIds": fileIds, + "message": message, + "totalFiles": totalFiles + ] + bucket?.set(uploadSessionData, forKey: identifier) + bucket?.synchronize() + } + + public func getUploadSessionData(identifier: String) -> UploadSessionData? { + let dictionary = bucket?.object(forKey: identifier) as? NSDictionary + let sessionData = UploadSessionData() + + sessionData.channelId = dictionary?.object(forKey: "channelId") as? String + sessionData.fileIds = dictionary?.object(forKey: "fileIds") as? [String] ?? [] + sessionData.message = dictionary?.object(forKey: "message") as! String + sessionData.totalFiles = dictionary?.object(forKey: "totalFiles") as! Int + + return sessionData + } + + public func removeUploadSessionData(identifier: String) { + bucket?.removeObject(forKey: identifier) + bucket?.synchronize() + } + + public func appendCompletedUploadToSession(identifier: String, fileId: String) { + let uploadSessionData = bucket?.object(forKey: identifier) as? NSDictionary + if (uploadSessionData != nil) { + let newData = uploadSessionData?.mutableCopy() as! NSMutableDictionary + var fileIds = newData.object(forKey: "fileIds") as! [String] + fileIds.append(fileId) + newData.setValue(fileIds, forKey: "fileIds") + bucket?.set(newData, forKey: identifier) + bucket?.synchronize() + } + } + + public func tempContainerURL() -> URL? { + let filemgr = FileManager.default + let containerURL = filemgr.containerURL(forSecurityApplicationGroupIdentifier: APP_GROUP_ID) + guard let tempDirectoryURL = containerURL?.appendingPathComponent("shareTempItems") else {return nil} + var isDirectory = ObjCBool(false) + let exists = filemgr.fileExists(atPath: tempDirectoryURL.path, isDirectory: &isDirectory) + if !exists && !isDirectory.boolValue { + try? filemgr.createDirectory(atPath: tempDirectoryURL.path, withIntermediateDirectories: true, attributes: nil) + } + + return tempDirectoryURL + } + + public func clearTempDirectory() { + guard let tempURL = tempContainerURL() else {return} + let fileMgr = FileManager.default + try? fileMgr.removeItem(atPath: tempURL.path) + } +} diff --git a/package-lock.json b/package-lock.json index 8a3239d2a1..801d3a543a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12098,14 +12098,6 @@ "prop-types": "^15.6.1" } }, - "react-native-tableview": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/react-native-tableview/-/react-native-tableview-2.4.1.tgz", - "integrity": "sha512-TrCUPYbWFBYGg0ZBZ6TDEwBA1+xksSk0ffpwMJNav9q6GQOD0t6eMXCkHMxA5vkBo3+i+fwWQd0sSVHYdZuPbA==", - "requires": { - "prop-types": "^15.6.2" - } - }, "react-native-vector-icons": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/react-native-vector-icons/-/react-native-vector-icons-6.3.0.tgz", diff --git a/package.json b/package.json index b24be8c014..deb967cacd 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,6 @@ "react-native-slider": "0.11.0", "react-native-status-bar-size": "0.3.3", "react-native-svg": "9.2.4", - "react-native-tableview": "2.4.1", "react-native-vector-icons": "6.3.0", "react-native-video": "4.4.0", "react-native-webview": "5.2.1", diff --git a/share.ios.js b/share.ios.js deleted file mode 100644 index 6b18654bd1..0000000000 --- a/share.ios.js +++ /dev/null @@ -1,8 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import {AppRegistry} from 'react-native'; - -import ShareExtension from 'share_extension/ios'; - -AppRegistry.registerComponent('MattermostShare', () => ShareExtension); diff --git a/share_extension/ios/error_message.js b/share_extension/ios/error_message.js deleted file mode 100644 index bdadbb9965..0000000000 --- a/share_extension/ios/error_message.js +++ /dev/null @@ -1,86 +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 {TouchableOpacity, View} from 'react-native'; - -import {Preferences} from 'mattermost-redux/constants'; - -import FormattedText from 'app/components/formatted_text'; -import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme'; - -export default function errorMessage(props) { - const {close} = props; - const theme = Preferences.THEMES.default; - const styles = getStyleSheet(theme); - - return ( - - - - - - - close()} - > - - - - - - ); -} - -errorMessage.propTypes = { - close: PropTypes.func.isRequired, -}; - -const getStyleSheet = makeStyleSheetFromTheme((theme) => { - return { - errorButton: { - alignItems: 'center', - justifyContent: 'center', - borderTopWidth: 2, - borderTopColor: changeOpacity(theme.linkColor, 0.3), - paddingVertical: 15, - }, - errorButtonText: { - color: changeOpacity(theme.linkColor, 0.7), - fontSize: 18, - }, - errorContainer: { - borderRadius: 5, - backgroundColor: 'white', - marginHorizontal: 35, - }, - errorContent: { - backgroundColor: changeOpacity(theme.centerChannelColor, 0.05), - }, - errorMessage: { - alignItems: 'center', - justifyContent: 'center', - padding: 25, - }, - errorMessageText: { - color: theme.centerChannelColor, - fontSize: 16, - textAlign: 'center', - }, - errorWrapper: { - flex: 1, - alignItems: 'center', - justifyContent: 'center', - backgroundColor: changeOpacity(theme.centerChannelColor, 0.5), - }, - }; -}); diff --git a/share_extension/ios/extension.js b/share_extension/ios/extension.js deleted file mode 100644 index ae60ac76e5..0000000000 --- a/share_extension/ios/extension.js +++ /dev/null @@ -1,164 +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 {Animated, Dimensions, NavigatorIOS, StyleSheet, View} from 'react-native'; -import {intlShape} from 'react-intl'; - -import {Preferences} from 'mattermost-redux/constants'; - -import initialState from 'app/initial_state'; -import mattermostBucket from 'app/mattermost_bucket'; - -import ExtensionPost from './extension_post'; - -const {View: AnimatedView} = Animated; - -export default class SharedApp extends PureComponent { - static propTypes = { - appGroupId: PropTypes.string.isRequired, - onClose: PropTypes.func.isRequired, - }; - - static contextTypes = { - intl: intlShape, - }; - - constructor(props) { - super(props); - - const {height, width} = Dimensions.get('window'); - const isLandscape = width > height; - - this.state = { - backdropOpacity: new Animated.Value(0), - containerOpacity: new Animated.Value(0), - isLandscape, - }; - - mattermostBucket.readFromFile('entities', props.appGroupId).then((value) => { - this.entities = value || initialState.entities; - this.setState({init: true}); - }); - } - - componentDidMount() { - Animated.parallel([ - Animated.timing(this.state.backdropOpacity, { - toValue: 0.5, - duration: 100, - }), - Animated.timing(this.state.containerOpacity, { - toValue: 1, - duration: 250, - }), - ]).start(); - } - - onLayout = (e) => { - const {height, width} = e.nativeEvent.layout; - const isLandscape = width > height; - if (this.state.isLandscape !== isLandscape) { - this.setState({isLandscape}); - } - }; - - userIsLoggedIn = () => { - if ( - this.entities && - this.entities.general && - this.entities.general.credentials && - this.entities.general.credentials.token && - this.entities.general.credentials.url - ) { - return true; - } - - return false; - }; - - render() { - const {init, isLandscape} = this.state; - const {intl} = this.context; - const {formatMessage} = intl; - - if (!init) { - return null; - } - - const title = formatMessage({ - id: 'mobile.extension.title', - defaultMessage: 'Share in Mattermost', - }); - - const theme = Preferences.THEMES.default; - - const initialRoute = { - component: ExtensionPost, - title, - passProps: { - authenticated: this.userIsLoggedIn(), - entities: this.entities, - onClose: this.props.onClose, - isLandscape, - theme, - title, - }, - wrapperStyle: { - borderRadius: 10, - backgroundColor: theme.centerChannelBg, - }, - }; - - return ( - - - - - - - - - ); - } -} - -const styles = StyleSheet.create({ - flex: { - flex: 1, - }, - backdrop: { - position: 'absolute', - flex: 1, - backgroundColor: '#000', - height: '100%', - width: '100%', - }, - wrapper: { - flex: 1, - marginHorizontal: 20, - }, - container: { - backgroundColor: 'white', - borderRadius: 10, - position: 'relative', - width: '100%', - }, -}); diff --git a/share_extension/ios/extension_channels.js b/share_extension/ios/extension_channels.js deleted file mode 100644 index 46a5e5f077..0000000000 --- a/share_extension/ios/extension_channels.js +++ /dev/null @@ -1,347 +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 { - ActivityIndicator, - Text, - View, -} from 'react-native'; -import DeviceInfo from 'react-native-device-info'; -import {intlShape} from 'react-intl'; -import TableView from 'react-native-tableview'; -import Icon from 'react-native-vector-icons/FontAwesome'; - -import {General} from 'mattermost-redux/constants'; - -import SearchBar from 'app/components/search_bar'; -import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme'; - -import { - getExtensionSortedDirectChannels, - getExtensionSortedPrivateChannels, - getExtensionSortedPublicChannels, -} from 'share_extension/common/selectors'; - -import ExtensionNavBar from './extension_nav_bar'; - -export default class ExtensionChannels extends PureComponent { - static propTypes = { - entities: PropTypes.object, - currentChannelId: PropTypes.string.isRequired, - navigator: PropTypes.object.isRequired, - onSelectChannel: PropTypes.func.isRequired, - teamId: PropTypes.string.isRequired, - theme: PropTypes.object.isRequired, - title: PropTypes.string.isRequired, - }; - - static contextTypes = { - intl: intlShape, - }; - - state = { - sections: null, - }; - - componentWillMount() { - this.loadChannels(); - } - - buildSections = (term = '') => { - const {publicChannels: pub, privateChannels: priv, directChannels: dms} = this.state; - const sections = []; - const publicChannels = this.filterChannels(pub, term); - const privateChannels = this.filterChannels(priv, term); - const directChannels = this.filterChannels(dms, term); - - if (publicChannels.length) { - sections.push({ - id: 'sidebar.channels', - defaultMessage: 'PUBLIC CHANNELS', - data: publicChannels.sort(this.sortDisplayName), - }); - } - - if (privateChannels.length) { - sections.push({ - id: 'sidebar.pg', - defaultMessage: 'PRIVATE CHANNELS', - data: privateChannels.sort(this.sortDisplayName), - }); - } - - if (directChannels.length) { - sections.push({ - id: 'sidebar.direct', - defaultMessage: 'DIRECT MESSAGES', - data: directChannels.sort(this.sortDisplayName), - }); - } - - this.setState({sections}); - }; - - cancelSearch = () => { - this.setState({term: ''}); - this.buildSections(); - }; - - filterChannels = (channels, term) => { - return channels.filter((c) => c.display_name.toLowerCase().includes(term.toLocaleLowerCase()) && c.delete_at === 0); - }; - - goBack = () => { - this.props.navigator.pop(); - }; - - loadChannels = async () => { - try { - const {entities, teamId} = this.props; - const views = { - extension: { - selectedTeamId: teamId, - }, - }; - const state = {entities, views}; - const publicChannels = getExtensionSortedPublicChannels(state); - const privateChannels = getExtensionSortedPrivateChannels(state); - const directChannels = getExtensionSortedDirectChannels(state); - - const icons = await Promise.all([ - Icon.getImageSource('globe', 16, this.props.theme.centerChannelColor), - Icon.getImageSource('lock', 16, this.props.theme.centerChannelColor), - Icon.getImageSource('user', 16, this.props.theme.centerChannelColor), - Icon.getImageSource('users', 16, this.props.theme.centerChannelColor), - ]); - - this.publicChannelIcon = icons[0]; - this.privateChannelIcon = icons[1]; - this.dmChannelIcon = icons[2]; - this.gmChannelIcon = icons[3]; - - this.setState({ - publicChannels, - privateChannels, - directChannels, - }, () => { - this.buildSections(); - }); - } catch (error) { - this.setState({error}); - } - }; - - handleSearch = (term) => { - this.setState({term}, () => { - if (this.throttleTimeout) { - clearTimeout(this.throttleTimeout); - } - - this.throttleTimeout = setTimeout(() => { - this.buildSections(term); - }, 300); - }); - }; - - handleSelectChannel = (selected) => { - const {sections} = this.state; - const section = sections.find((s) => s.id === selected.detail); - if (section) { - const channel = section.data.find((c) => c.id === selected.value); - if (channel) { - this.props.onSelectChannel(channel); - this.goBack(); - } - } - }; - - renderBody = (styles) => { - const {theme} = this.props; - const {error, sections} = this.state; - - if (error) { - return ( - - - {error.message} - - - ); - } - - if (!sections) { - return ( - - - - ); - } - - return ( - - - {this.renderSearchBar(styles)} - - {this.renderSections()} - - ); - }; - - renderSearchBar = (styles) => { - const {formatMessage} = this.context.intl; - const {theme} = this.props; - - return ( - - - - ); - }; - - renderSections = () => { - const {formatMessage} = this.context.intl; - const {currentChannelId} = this.props; - const {sections} = this.state; - - return sections.map((s) => { - const items = s.data.map((c) => { - let icon; - switch (c.type) { - case General.OPEN_CHANNEL: - icon = this.publicChannelIcon.uri; - break; - case General.PRIVATE_CHANNEL: - icon = this.privateChannelIcon.uri; - break; - case General.DM_CHANNEL: - icon = this.dmChannelIcon.uri; - break; - case General.GM_CHANNEL: - icon = this.gmChannelIcon.uri; - break; - } - - return ( - - {c.display_name} - - ); - }); - - return ( - - {items} - - ); - }); - }; - - sortDisplayName = (a, b) => { - const locale = DeviceInfo.getDeviceLocale().split('-')[0]; - return a.display_name.localeCompare(b.display_name, locale, {numeric: true}); - }; - - render() { - const {theme, title} = this.props; - const styles = getStyleSheet(theme); - - return ( - - - {this.renderBody(styles)} - - ); - } -} - -const getStyleSheet = makeStyleSheetFromTheme((theme) => { - return { - flex: { - flex: 1, - }, - separatorContainer: { - paddingLeft: 35, - }, - separator: { - backgroundColor: changeOpacity(theme.centerChannelColor, 0.2), - height: 1, - }, - loadingContainer: { - alignItems: 'center', - flex: 1, - justifyContent: 'center', - }, - searchContainer: { - backgroundColor: changeOpacity(theme.centerChannelColor, 0.2), - paddingBottom: 2, - paddingHorizontal: 5, - }, - searchBarInput: { - backgroundColor: '#fff', - color: theme.centerChannelColor, - fontSize: 15, - }, - titleContainer: { - height: 30, - }, - title: { - color: changeOpacity(theme.centerChannelColor, 0.6), - fontSize: 15, - lineHeight: 30, - paddingHorizontal: 15, - }, - errorContainer: { - alignItems: 'center', - flex: 1, - justifyContent: 'center', - paddingHorizontal: 15, - }, - error: { - color: theme.errorTextColor, - fontSize: 14, - }, - }; -}); diff --git a/share_extension/ios/extension_nav_bar.js b/share_extension/ios/extension_nav_bar.js deleted file mode 100644 index 9546979303..0000000000 --- a/share_extension/ios/extension_nav_bar.js +++ /dev/null @@ -1,150 +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 {Text, TouchableOpacity, View} from 'react-native'; -import IonIcon from 'react-native-vector-icons/Ionicons'; - -import {emptyFunction} from 'app/utils/general'; -import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme'; - -export default class ExtensionNavBar extends PureComponent { - static propTypes = { - authenticated: PropTypes.bool, - backButton: PropTypes.bool, - leftButtonTitle: PropTypes.string, - onLeftButtonPress: PropTypes.func, - onRightButtonPress: PropTypes.func, - rightButtonTitle: PropTypes.string, - theme: PropTypes.object.isRequired, - title: PropTypes.string, - }; - - static defaultProps = { - backButton: false, - onLeftButtonPress: emptyFunction, - }; - - renderLeftButton = (styles) => { - const {backButton, leftButtonTitle, onLeftButtonPress} = this.props; - let backComponent; - if (backButton) { - backComponent = ( - - ); - } else if (leftButtonTitle) { - backComponent = ( - - {leftButtonTitle} - - ); - } - - if (backComponent) { - return ( - - {backComponent} - - ); - } - - return ; - }; - - renderRightButton = (styles) => { - const {authenticated, onRightButtonPress, rightButtonTitle} = this.props; - - if (rightButtonTitle && authenticated) { - return ( - - - {rightButtonTitle} - - - ); - } - - return ; - }; - - render() { - const {theme, title} = this.props; - const styles = getStyleSheet(theme); - - return ( - - {this.renderLeftButton(styles)} - - - {title} - - - {this.renderRightButton(styles)} - - ); - } -} - -const getStyleSheet = makeStyleSheetFromTheme((theme) => { - return { - container: { - borderBottomColor: changeOpacity(theme.centerChannelColor, 0.2), - borderBottomWidth: 1, - flexDirection: 'row', - height: 45, - }, - backButtonContainer: { - justifyContent: 'center', - flex: 1, - paddingLeft: 15, - }, - titleContainer: { - alignItems: 'center', - flex: 3, - justifyContent: 'center', - }, - backButton: { - color: theme.linkColor, - fontSize: 34, - }, - leftButton: { - color: theme.linkColor, - fontSize: 16, - fontWeight: '600', - }, - title: { - fontSize: 17, - fontWeight: '600', - textAlign: 'center', - }, - rightButtonContainer: { - justifyContent: 'center', - flex: 1, - paddingRight: 15, - }, - rightButton: { - color: theme.linkColor, - fontSize: 16, - fontWeight: '600', - textAlign: 'right', - }, - }; -}); diff --git a/share_extension/ios/extension_post.js b/share_extension/ios/extension_post.js deleted file mode 100644 index 20c856c04c..0000000000 --- a/share_extension/ios/extension_post.js +++ /dev/null @@ -1,839 +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 {intlShape} from 'react-intl'; -import { - ActivityIndicator, - DeviceEventEmitter, - Dimensions, - Image, - NativeModules, - ScrollView, - Text, - TextInput, - TouchableHighlight, - View, -} from 'react-native'; -import IonIcon from 'react-native-vector-icons/Ionicons'; -import Video from 'react-native-video'; -import LocalAuth from 'react-native-local-auth'; -import RNFetchBlob from 'rn-fetch-blob'; - -import {Client4} from 'mattermost-redux/client'; -import {General} from 'mattermost-redux/constants'; -import {getChannel, getDefaultChannel} from 'mattermost-redux/selectors/entities/channels'; -import {getFormattedFileSize, lookupMimeType} from 'mattermost-redux/utils/file_utils'; -import {isMinimumServerVersion} from 'mattermost-redux/utils/helpers'; - -import mattermostBucket from 'app/mattermost_bucket'; -import {generateId, getAllowedServerMaxFileSize} from 'app/utils/file'; -import {preventDoubleTap} from 'app/utils/tap'; -import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme'; -import Config from 'assets/config'; - -import { - ExcelSvg, - GenericSvg, - PdfSvg, - PptSvg, - ZipSvg, -} from 'share_extension/common/icons'; - -import ExtensionChannels from './extension_channels'; -import ExtensionNavBar from './extension_nav_bar'; -import ExtensionTeams from './extension_teams'; - -const ShareExtension = NativeModules.MattermostShare; -const MAX_INPUT_HEIGHT = 95; -const MAX_MESSAGE_LENGTH = 4000; -const MAX_FILE_SIZE = 20 * 1024 * 1024; - -const extensionSvg = { - csv: ExcelSvg, - pdf: PdfSvg, - ppt: PptSvg, - pptx: PptSvg, - xls: ExcelSvg, - xlsx: ExcelSvg, - zip: ZipSvg, -}; - -export default class ExtensionPost extends PureComponent { - static propTypes = { - authenticated: PropTypes.bool.isRequired, - entities: PropTypes.object, - navigator: PropTypes.object.isRequired, - onClose: PropTypes.func.isRequired, - theme: PropTypes.object.isRequired, - title: PropTypes.string, - }; - - static contextTypes = { - intl: intlShape, - }; - - constructor(props, context) { - super(props, context); - - const {height, width} = Dimensions.get('window'); - const isLandscape = width > height; - const entities = props.entities; - - this.useBackgroundUpload = props.authenticated ? isMinimumServerVersion(entities.general.serverVersion, 4, 8) : false; - - this.state = { - entities, - error: null, - files: [], - isLandscape, - exceededSize: 0, - value: '', - sending: false, - }; - } - - componentWillMount() { - this.event = DeviceEventEmitter.addListener('extensionPostFailed', this.handlePostFailed); - this.loadData(); - } - - componentDidMount() { - this.focusInput(); - } - - componentWillUnmount() { - this.event.remove(); - } - - componentDidUpdate() { - this.focusInput(); - } - - emmAuthenticationIfNeeded = async () => { - try { - const emmSecured = await mattermostBucket.getPreference('emm', Config.AppGroupId); - if (emmSecured) { - const {intl} = this.context; - await LocalAuth.authenticate({ - reason: intl.formatMessage({ - id: 'mobile.managed.secured_by', - defaultMessage: 'Secured by {vendor}', - }, {emmSecured}), - fallbackToPasscode: true, - suppressEnterPassword: true, - }); - } - } catch (error) { - this.props.onClose(); - } - }; - - focusInput = () => { - if (this.input && !this.input.isFocused()) { - this.input.focus(); - } - }; - - getInputRef = (ref) => { - this.input = ref; - }; - - getScrollViewRef = (ref) => { - this.scrollView = ref; - }; - - goToChannels = preventDoubleTap(() => { - const {navigator, theme} = this.props; - const {channel, entities, team} = this.state; - - if (!entities || !team || !channel) { - return; - } - - navigator.push({ - component: ExtensionChannels, - wrapperStyle: { - borderRadius: 10, - backgroundColor: theme.centerChannelBg, - }, - passProps: { - currentChannelId: channel.id, - entities, - onSelectChannel: this.selectChannel, - teamId: team.id, - theme, - title: team.display_name, - }, - }); - }); - - goToTeams = preventDoubleTap(() => { - const {navigator, theme} = this.props; - const {formatMessage} = this.context.intl; - const {entities, team} = this.state; - - if (!entities || !team) { - return; - } - - navigator.push({ - component: ExtensionTeams, - title: formatMessage({id: 'quick_switch_modal.teams', defaultMessage: 'Teams'}), - wrapperStyle: { - borderRadius: 10, - backgroundColor: theme.centerChannelBg, - }, - passProps: { - entities, - currentTeamId: team.id, - onSelectTeam: this.selectTeam, - theme, - }, - }); - }); - - handleCancel = preventDoubleTap(() => { - this.props.onClose(); - }); - - handleTextChange = (value) => { - this.setState({value}); - }; - - handlePostFailed = () => { - const {formatMessage} = this.context.intl; - this.setState({ - error: { - message: formatMessage({ - id: 'mobile.share_extension.post_error', - defaultMessage: 'An error has occurred while posting the message. Please try again.', - }), - }, - sending: false, - }); - }; - - loadData = async () => { - const {entities} = this.state; - - if (this.props.authenticated) { - try { - const {config, credentials} = entities.general; - const {currentUserId} = entities.users; - const team = entities.teams.teams[entities.teams.currentTeamId]; - let channel = entities.channels.channels[entities.channels.currentChannelId]; - const items = await ShareExtension.data(Config.AppGroupId); - const serverMaxFileSize = getAllowedServerMaxFileSize(config); - const maxSize = Math.min(MAX_FILE_SIZE, serverMaxFileSize); - const text = []; - const urls = []; - const files = []; - let totalSize = 0; - let exceededSize = false; - - if (channel && (channel.type === General.GM_CHANNEL || channel.type === General.DM_CHANNEL)) { - channel = getChannel({entities}, channel.id); - } - - if (channel.delete_at !== 0) { - channel = getDefaultChannel({entities}); - } - - for (let i = 0; i < items.length; i++) { - const item = items[i]; - switch (item.type) { - case 'public.plain-text': - text.push(item.value); - break; - case 'public.url': - urls.push(item.value); - break; - default: { - const fullPath = item.value; - const filePath = decodeURIComponent(fullPath.replace('file://', '')); - const fileSize = await RNFetchBlob.fs.stat(filePath); // eslint-disable-line no-await-in-loop - const filename = decodeURIComponent(fullPath.replace(/^.*[\\/]/, '')); - const extension = filename.split('.').pop(); - - if (this.useBackgroundUpload) { - if (!exceededSize) { - exceededSize = fileSize.size >= maxSize; - } - } else { - totalSize += fileSize.size; - } - files.push({ - extension, - filename, - filePath, - fullPath, - mimeType: lookupMimeType(filename.toLowerCase()), - size: getFormattedFileSize(fileSize), - type: item.type, - }); - break; - } - } - } - - let value = text.join('\n'); - if (urls.length) { - value += text.length ? `\n${urls.join('\n')}` : urls.join('\n'); - } - - Client4.setUrl(credentials.url); - Client4.setToken(credentials.token); - Client4.setUserId(currentUserId); - - if (!this.useBackgroundUpload) { - exceededSize = totalSize >= maxSize; - } - - this.setState({channel, files, team, value, exceededSize}); - } catch (error) { - this.setState({error}); - } - } - }; - - onLayout = async () => { - const isLandscape = await ShareExtension.getOrientation() === 'LANDSCAPE'; - - if (this.state.isLandscape !== isLandscape) { - if (this.scrollView) { - setTimeout(() => { - this.scrollView.scrollTo({y: 0, animated: false}); - }, 250); - } - this.setState({isLandscape}); - } - }; - - renderBody = (styles) => { - const {formatMessage} = this.context.intl; - const {authenticated, theme} = this.props; - const {entities, error, sending, exceededSize, value} = this.state; - const {config} = entities.general; - const serverMaxFileSize = getAllowedServerMaxFileSize(config); - const maxSize = Math.min(MAX_FILE_SIZE, serverMaxFileSize); - - if (error) { - return ( - - - {error.message} - - - ); - } - - if (sending) { - return ( - - - - {formatMessage({ - id: 'mobile.extension.posting', - defaultMessage: 'Posting...', - })} - - - ); - } - - if (exceededSize) { - return ( - - - {formatMessage({ - id: 'mobile.extension.max_file_size', - defaultMessage: 'File attachments shared in Mattermost must be less than {size}.', - }, {size: getFormattedFileSize({size: maxSize})})} - - - ); - } - - if (authenticated && !error) { - return ( - - - {this.renderFiles(styles)} - - ); - } - - return ( - - - {formatMessage({ - id: 'mobile.extension.authentication_required', - defaultMessage: 'Authentication required: Please first login using the app.', - })} - - - ); - }; - - renderChannelButton = (styles) => { - const {formatMessage} = this.context.intl; - const {authenticated, theme} = this.props; - const {channel, sending} = this.state; - const channelName = channel ? channel.display_name : ''; - - if (sending) { - return null; - } - - if (!authenticated) { - return null; - } - - return ( - - - - - {formatMessage({id: 'mobile.share_extension.channel', defaultMessage: 'Channel'})} - - - - - {channelName} - - - - - - - - ); - }; - - renderFiles = (styles) => { - const {files} = this.state; - return files.map((file, index) => { - let component; - - switch (file.type) { - case 'public.image': - component = ( - - - - ); - break; - case 'public.movie': - component = ( - - - ); - break; - case 'public.file-url': { - let SvgIcon = extensionSvg[file.extension]; - if (!SvgIcon) { - SvgIcon = GenericSvg; - } - - component = ( - - - - - - - - ); - break; - } - } - - return ( - - {component} - - {`${file.size} - ${file.filename}`} - - - ); - }); - }; - - renderTeamButton = (styles) => { - const {formatMessage} = this.context.intl; - const {authenticated, theme} = this.props; - const {sending, team} = this.state; - const teamName = team ? team.display_name : ''; - - if (sending) { - return null; - } - - if (!authenticated) { - return null; - } - - return ( - - - - - {formatMessage({id: 'mobile.share_extension.team', defaultMessage: 'Team'})} - - - - - {teamName} - - - - - - - - ); - }; - - selectChannel = (channel) => { - this.setState({channel}); - }; - - selectTeam = (team, channel) => { - this.setState({channel, team, error: null}); - - // Update the channels for the team - Client4.getMyChannels(team.id).then((channels) => { - const defaultChannel = channels.find((c) => c.name === General.DEFAULT_CHANNEL && c.team_id === team.id); - this.updateChannelsInEntities(channels); - if (!channel) { - this.setState({channel: defaultChannel}); - } - }).catch((error) => { - const {entities} = this.props; - if (entities.channels.channelsInTeam[team.id]) { - const townSquare = Object.values(entities.channels.channels).find((c) => { - return c.name === General.DEFAULT_CHANNEL && c.team_id === team.id; - }); - - if (!channel) { - this.setState({channel: townSquare}); - } - } else { - this.setState({error, channel: null}); - } - }); - }; - - sendMessage = preventDoubleTap(async () => { - const {authenticated, onClose} = this.props; - const {channel, entities, files, value} = this.state; - const {currentUserId} = entities.users; - - // If no text and no files do nothing - if ((!value && !files.length) || !channel) { - return; - } - - if (currentUserId && authenticated) { - await this.emmAuthenticationIfNeeded(); - const certificate = await mattermostBucket.getPreference('cert', Config.AppGroupId); - - try { - // Check to see if the use still belongs to the channel - await Client4.getMyChannelMember(channel.id); - const post = { - user_id: currentUserId, - channel_id: channel.id, - message: value, - }; - - const data = { - files, - post, - requestId: generateId().replace(/-/g, ''), - useBackgroundUpload: this.useBackgroundUpload, - certificate: certificate || '', - }; - - this.setState({sending: true}); - onClose(data); - } catch (error) { - this.setState({error}); - setTimeout(() => { - onClose(); - }, 5000); - } - } - }); - - updateChannelsInEntities = (newChannels) => { - const {entities} = this.state; - const newEntities = { - ...entities, - channels: { - ...entities.channels, - channels: {...entities.channels.channels}, - channelsInTeam: {...entities.channels.channelsInTeam}, - }, - }; - const {channels, channelsInTeam} = newEntities.channels; - - newChannels.forEach((c) => { - channels[c.id] = c; - const channelIdsInTeam = channelsInTeam[c.team_id]; - if (channelIdsInTeam) { - if (!channelIdsInTeam.includes(c.id)) { - channelsInTeam[c.team_id].push(c.id); - } - } else { - channelsInTeam[c.team_id] = [c.id]; - } - }); - - this.setState({entities: newEntities}); - mattermostBucket.writeToFile('entities', JSON.stringify(newEntities), Config.AppGroupId); - }; - - render() { - const {authenticated, theme, title} = this.props; - const {channel, error, totalSize, sending} = this.state; - const {formatMessage} = this.context.intl; - const styles = getStyleSheet(theme); - - let postButtonText = formatMessage({id: 'mobile.share_extension.send', defaultMessage: 'Send'}); - if (totalSize >= MAX_FILE_SIZE || sending || error || !channel) { - postButtonText = null; - } - - let cancelButton = formatMessage({id: 'mobile.share_extension.cancel', defaultMessage: 'Cancel'}); - if (sending) { - cancelButton = null; - } else if (error) { - cancelButton = formatMessage({id: 'mobile.share_extension.error_close', defaultMessage: 'Close'}); - } - - return ( - - - {this.renderBody(styles)} - {!error && this.renderTeamButton(styles)} - {!error && this.renderChannelButton(styles)} - - ); - } -} - -const getStyleSheet = makeStyleSheetFromTheme((theme) => { - return { - flex: { - flex: 1, - }, - container: { - flex: 1, - backgroundColor: changeOpacity(theme.centerChannelColor, 0.05), - }, - input: { - color: theme.centerChannelColor, - fontSize: 17, - marginBottom: 5, - width: '100%', - }, - divider: { - backgroundColor: changeOpacity(theme.centerChannelColor, 0.1), - height: 1, - marginVertical: 5, - width: '100%', - }, - scrollView: { - paddingHorizontal: 15, - }, - buttonContainer: { - borderTopColor: changeOpacity(theme.centerChannelColor, 0.2), - borderTopWidth: 1, - height: 45, - paddingHorizontal: 15, - }, - buttonWrapper: { - alignItems: 'center', - flex: 1, - flexDirection: 'row', - }, - buttonLabelContainer: { - flex: 1, - }, - buttonLabel: { - fontSize: 17, - lineHeight: 45, - }, - buttonValueContainer: { - justifyContent: 'flex-end', - flex: 1, - flexDirection: 'row', - }, - buttonValue: { - color: changeOpacity(theme.centerChannelColor, 0.4), - alignSelf: 'flex-end', - fontSize: 17, - lineHeight: 45, - }, - arrowContainer: { - height: 45, - justifyContent: 'center', - marginLeft: 15, - top: 2, - }, - unauthenticatedContainer: { - alignItems: 'center', - flex: 1, - justifyContent: 'center', - paddingHorizontal: 15, - }, - unauthenticated: { - color: theme.errorTextColor, - fontSize: 14, - }, - fileContainer: { - alignItems: 'center', - backgroundColor: theme.centerChannelBg, - borderColor: changeOpacity(theme.centerChannelColor, 0.2), - borderRadius: 4, - borderWidth: 1, - flexDirection: 'row', - height: 48, - marginBottom: 10, - width: '100%', - }, - filename: { - color: changeOpacity(theme.centerChannelColor, 0.5), - fontSize: 13, - flex: 1, - }, - otherContainer: { - borderBottomLeftRadius: 4, - borderTopLeftRadius: 4, - height: 48, - marginRight: 10, - paddingVertical: 10, - width: 38, - }, - otherWrapper: { - borderRightWidth: 1, - borderRightColor: changeOpacity(theme.centerChannelColor, 0.2), - flex: 1, - }, - fileIcon: { - alignItems: 'center', - justifyContent: 'center', - flex: 1, - }, - imageContainer: { - borderBottomLeftRadius: 4, - borderTopLeftRadius: 4, - height: 48, - marginRight: 10, - width: 38, - }, - image: { - alignItems: 'center', - height: 48, - justifyContent: 'center', - overflow: 'hidden', - width: 38, - }, - video: { - backgroundColor: theme.centerChannelBg, - alignItems: 'center', - height: 48, - justifyContent: 'center', - overflow: 'hidden', - width: 38, - }, - sendingContainer: { - alignItems: 'center', - flex: 1, - justifyContent: 'center', - paddingHorizontal: 15, - }, - sendingText: { - color: theme.centerChannelColor, - fontSize: 16, - paddingTop: 10, - }, - }; -}); diff --git a/share_extension/ios/extension_team_item.js b/share_extension/ios/extension_team_item.js deleted file mode 100644 index 1b7141ce01..0000000000 --- a/share_extension/ios/extension_team_item.js +++ /dev/null @@ -1,119 +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 { - Text, - TouchableHighlight, - View, -} from 'react-native'; - -import {preventDoubleTap} from 'app/utils/tap'; -import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme'; - -import TeamIcon from 'app/components/team_icon/team_icon'; - -export default class TeamsListItem extends React.PureComponent { - static propTypes = { - currentTeamId: PropTypes.string.isRequired, - onSelectTeam: PropTypes.func.isRequired, - team: PropTypes.object.isRequired, - theme: PropTypes.object.isRequired, - }; - - onPress = preventDoubleTap(() => { - const {onSelectTeam, team} = this.props; - onSelectTeam(team); - }); - - render() { - const { - currentTeamId, - team, - theme, - } = this.props; - const styles = getStyleSheet(theme); - - const wrapperStyle = [styles.wrapper]; - if (team.id === currentTeamId) { - wrapperStyle.push({ - width: '90%', - }); - } - - return ( - - - - - - {team.display_name} - - - - - ); - } -} - -const getStyleSheet = makeStyleSheetFromTheme((theme) => { - return { - wrapper: { - height: 45, - width: '100%', - }, - container: { - flex: 1, - flexDirection: 'row', - height: 45, - paddingHorizontal: 15, - }, - item: { - alignItems: 'center', - height: 45, - flex: 1, - flexDirection: 'row', - }, - text: { - color: theme.centerChannelColor, - flex: 1, - fontSize: 16, - fontWeight: '600', - lineHeight: 16, - paddingRight: 5, - }, - teamIconContainer: { - backgroundColor: theme.sidebarBg, - marginRight: 10, - }, - teamIconText: { - color: theme.sidebarText, - }, - imageContainer: { - backgroundColor: '#ffffff', - }, - checkmarkContainer: { - alignItems: 'flex-end', - }, - checkmark: { - color: theme.linkColor, - fontSize: 16, - }, - }; -}); diff --git a/share_extension/ios/extension_teams.js b/share_extension/ios/extension_teams.js deleted file mode 100644 index 946a1b8428..0000000000 --- a/share_extension/ios/extension_teams.js +++ /dev/null @@ -1,219 +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 {ActivityIndicator, Text, View} from 'react-native'; -import DeviceInfo from 'react-native-device-info'; -import {intlShape} from 'react-intl'; -import TableView from 'react-native-tableview'; - -import {General} from 'mattermost-redux/constants'; -import {getChannelsInTeam} from 'mattermost-redux/selectors/entities/channels'; - -import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme'; - -import ExtensionNavBar from './extension_nav_bar'; -import ExtensionTeamItem from './extension_team_item'; - -export default class ExtensionTeams extends PureComponent { - static propTypes = { - currentTeamId: PropTypes.string.isRequired, - entities: PropTypes.object, - navigator: PropTypes.object.isRequired, - onSelectTeam: PropTypes.func.isRequired, - theme: PropTypes.object.isRequired, - }; - - static contextTypes = { - intl: intlShape, - }; - - state = { - defaultChannels: null, - error: null, - myTeams: null, - }; - - componentWillMount() { - this.loadTeams(); - } - - goBack = () => { - this.props.navigator.pop(); - }; - - handleSelectTeam = (team) => { - const {defaultChannels} = this.state; - const townSquare = defaultChannels[team.id]; - this.props.onSelectTeam(team, townSquare); - this.goBack(); - }; - - loadTeams = async () => { - try { - const defaultChannels = {}; - const {teams, myMembers} = this.props.entities.teams; - const myTeams = []; - const channelsInTeam = getChannelsInTeam({entities: this.props.entities}); - - for (const key in teams) { - if (teams.hasOwnProperty(key)) { - const team = teams[key]; - const belong = myMembers[key]; - if (belong) { - const channelIds = channelsInTeam[key]; - let channels; - if (channelIds) { - channels = channelIds.map((id) => this.props.entities.channels.channels[id]); - defaultChannels[team.id] = channels.find((channel) => channel.name === General.DEFAULT_CHANNEL); - } - - myTeams.push(team); - } - } - } - - this.setState({ - defaultChannels, - myTeams: myTeams.sort(this.sortDisplayName), - }); - } catch (error) { - this.setState({error}); - } - }; - - renderBody = (styles) => { - const {theme} = this.props; - const {error, myTeams} = this.state; - - if (error) { - return ( - - - {error.message} - - - ); - } - - if (!myTeams) { - return ( - - - - ); - } - - return ( - - - {this.renderItems(myTeams)} - - - ); - }; - - renderItems = (myTeams) => { - const {currentTeamId, theme} = this.props; - - return myTeams.map((team) => { - return ( - - - - - - ); - }); - }; - - sortDisplayName = (a, b) => { - const locale = DeviceInfo.getDeviceLocale().split('-')[0]; - return a.display_name.localeCompare(b.display_name, locale, {numeric: true}); - }; - - render() { - const {formatMessage} = this.context.intl; - const {theme} = this.props; - const styles = getStyleSheet(theme); - - return ( - - - {this.renderBody(styles)} - - ); - } -} - -const getStyleSheet = makeStyleSheetFromTheme((theme) => { - return { - flex: { - flex: 1, - }, - separatorContainer: { - paddingLeft: 60, - }, - separator: { - backgroundColor: changeOpacity(theme.centerChannelColor, 0.2), - height: 1, - }, - loadingContainer: { - alignItems: 'center', - flex: 1, - justifyContent: 'center', - }, - searchContainer: { - backgroundColor: changeOpacity(theme.centerChannelColor, 0.2), - paddingBottom: 2, - }, - searchBarInput: { - backgroundColor: '#fff', - color: theme.centerChannelColor, - fontSize: 15, - }, - titleContainer: { - height: 30, - }, - title: { - color: changeOpacity(theme.centerChannelColor, 0.6), - fontSize: 15, - lineHeight: 30, - paddingHorizontal: 15, - }, - errorContainer: { - alignItems: 'center', - flex: 1, - justifyContent: 'center', - paddingHorizontal: 15, - }, - error: { - color: theme.errorTextColor, - fontSize: 14, - }, - }; -}); diff --git a/share_extension/ios/index.js b/share_extension/ios/index.js deleted file mode 100644 index 1d653eb4e7..0000000000 --- a/share_extension/ios/index.js +++ /dev/null @@ -1,68 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import React, {PureComponent} from 'react'; -import {NativeModules} from 'react-native'; -import {IntlProvider} from 'react-intl'; -import DeviceInfo from 'react-native-device-info'; - -import Config from 'assets/config'; -import {getTranslations} from 'app/i18n'; -import 'app/fetch_preconfig'; - -import {captureExceptionWithoutState, initializeSentry, LOGGER_EXTENSION} from 'app/utils/sentry'; - -import ErrorMessage from './error_message'; -import Extension from './extension'; - -const ShareExtension = NativeModules.MattermostShare; - -export class SharedApp extends PureComponent { - constructor(props) { - super(props); - - initializeSentry(); - - this.state = { - hasError: false, - }; - } - - componentDidCatch(error) { - this.setState({hasError: true}); - - captureExceptionWithoutState(error, LOGGER_EXTENSION); - } - - close = (data) => { - ShareExtension.close(data, Config.AppGroupId); - }; - - render() { - if (this.state.hasError) { - return ( - - ); - } - - return ( - - ); - } -} - -export default function ShareExtensionProvider() { - const locale = DeviceInfo.getDeviceLocale().split('-')[0]; - - return ( - - - - ); -}