forked from Ivasoft/mattermost-mobile
iOS Native Share Extension (Swift) (#2575)
* iOS Native Share Extension (Swift) * Re-arrange files * Fix .gitignore
This commit is contained in:
37
NOTICE.txt
37
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.
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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": {}
|
||||
|
||||
@@ -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
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -6,6 +6,20 @@
|
||||
parallelizeBuildables = "NO"
|
||||
buildImplicitDependencies = "YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "7FABE03522137F2900D0F595"
|
||||
BuildableName = "libUploadAttachments.a"
|
||||
BlueprintName = "UploadAttachments"
|
||||
ReferencedContainer = "container:UploadAttachments/UploadAttachments.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1010"
|
||||
wasCreatedForAppExtension = "YES"
|
||||
version = "2.0">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "7FABE03522137F2900D0F595"
|
||||
BuildableName = "libUploadAttachments.a"
|
||||
BlueprintName = "UploadAttachments"
|
||||
ReferencedContainer = "container:UploadAttachments/UploadAttachments.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "7F240A18220D3A2300637665"
|
||||
BuildableName = "MattermostShare.appex"
|
||||
BlueprintName = "MattermostShare"
|
||||
ReferencedContainer = "container:Mattermost.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<Testables>
|
||||
</Testables>
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "7F240A18220D3A2300637665"
|
||||
BuildableName = "MattermostShare.appex"
|
||||
BlueprintName = "MattermostShare"
|
||||
ReferencedContainer = "container:Mattermost.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
<AdditionalOptions>
|
||||
</AdditionalOptions>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = ""
|
||||
selectedLauncherIdentifier = "Xcode.IDEFoundation.Launcher.PosixSpawn"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES"
|
||||
launchAutomaticallySubstyle = "2">
|
||||
<RemoteRunnable
|
||||
runnableDebuggingMode = "1"
|
||||
BundleIdentifier = "com.apple.mobileslideshow"
|
||||
RemotePath = "/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/Library/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/Applications/MobileSlideShow.app">
|
||||
</RemoteRunnable>
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "7F240A18220D3A2300637665"
|
||||
BuildableName = "MattermostShare.appex"
|
||||
BlueprintName = "MattermostShare"
|
||||
ReferencedContainer = "container:Mattermost.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
<AdditionalOptions>
|
||||
</AdditionalOptions>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
launchAutomaticallySubstyle = "2">
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "7F240A18220D3A2300637665"
|
||||
BuildableName = "MattermostShare.appex"
|
||||
BlueprintName = "MattermostShare"
|
||||
ReferencedContainer = "container:Mattermost.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>IDEDidComputeMac32BitWarning</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict/>
|
||||
</plist>
|
||||
|
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 2.5 KiB |
@@ -19,13 +19,17 @@
|
||||
#endif
|
||||
#import "RCCManager.h"
|
||||
#import "RNNotifications.h"
|
||||
#import "SessionManager.h"
|
||||
#import <UploadAttachments/UploadAttachments-Swift.h>
|
||||
#import <UserNotifications/UserNotifications.h>
|
||||
|
||||
@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
|
||||
|
||||
6
ios/Mattermost/Dummy.swift
Normal file
6
ios/Mattermost/Dummy.swift
Normal file
@@ -0,0 +1,6 @@
|
||||
//
|
||||
// Dummy.swift
|
||||
// Mattermost
|
||||
//
|
||||
// DO NOT delete this file as is needed by the project to add support for swift compilation
|
||||
//
|
||||
4
ios/Mattermost/Mattermost-Bridging-Header.h
Normal file
4
ios/Mattermost/Mattermost-Bridging-Header.h
Normal file
@@ -0,0 +1,4 @@
|
||||
//
|
||||
// Use this file to import your target's public headers that you would like to expose to Swift.
|
||||
//
|
||||
|
||||
7
ios/Mattermost/MattermostBucketModule.h
Normal file
7
ios/Mattermost/MattermostBucketModule.h
Normal file
@@ -0,0 +1,7 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
#import "React/RCTBridgeModule.h"
|
||||
#import "MattermostBucket.h"
|
||||
|
||||
@interface MattermostBucketModule : NSObject<RCTBridgeModule>
|
||||
@property MattermostBucket *bucket;
|
||||
@end
|
||||
67
ios/Mattermost/MattermostBucketModule.m
Normal file
67
ios/Mattermost/MattermostBucketModule.m
Normal file
@@ -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
|
||||
@@ -1,7 +0,0 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
#import "React/RCTBridgeModule.h"
|
||||
|
||||
@interface MattermostBucket : NSObject <RCTBridgeModule>
|
||||
- (NSUserDefaults *)bucketByName:(NSString*)name;
|
||||
-(NSString *)readFromFile:(NSString *)fileName appGroupId:(NSString *)appGroupId;
|
||||
@end
|
||||
@@ -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
|
||||
@@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="13122.16" systemVersion="17A278a" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="j1y-V4-xli">
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="13122.16" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="j1y-V4-xli">
|
||||
<dependencies>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="13104.12"/>
|
||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||
@@ -9,7 +9,7 @@
|
||||
<!--Share View Controller-->
|
||||
<scene sceneID="ceB-am-kn3">
|
||||
<objects>
|
||||
<viewController id="j1y-V4-xli" customClass="ShareViewController" customModuleProvider="" sceneMemberID="viewController">
|
||||
<viewController id="j1y-V4-xli" customClass="ShareViewController" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<view key="view" opaque="NO" contentMode="scaleToFill" id="wbc-yd-nQP">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
|
||||
134
ios/MattermostShare/ChannelsViewController.swift
Normal file
134
ios/MattermostShare/ChannelsViewController.swift
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
25
ios/MattermostShare/GenericPreview.swift
Normal file
25
ios/MattermostShare/GenericPreview.swift
Normal file
@@ -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]
|
||||
}
|
||||
}
|
||||
57
ios/MattermostShare/GenericPreview.xib
Normal file
57
ios/MattermostShare/GenericPreview.xib
Normal file
@@ -0,0 +1,57 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="14460.31" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES">
|
||||
<device id="retina4_7" orientation="portrait">
|
||||
<adaptation id="fullscreen"/>
|
||||
</device>
|
||||
<dependencies>
|
||||
<deployment version="4144" identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14460.20"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<objects>
|
||||
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner" customClass="GenericPreview" customModule="MattermostShare">
|
||||
<connections>
|
||||
<outlet property="contentView" destination="iN0-l3-epB" id="zsU-gT-pKk"/>
|
||||
<outlet property="mainLabel" destination="VCO-Fr-5i7" id="kIN-uo-sj1"/>
|
||||
</connections>
|
||||
</placeholder>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
|
||||
<view userInteractionEnabled="NO" contentMode="scaleAspectFit" id="iN0-l3-epB">
|
||||
<rect key="frame" x="0.0" y="0.0" width="70" height="80"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<imageView userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="generic.png" translatesAutoresizingMaskIntoConstraints="NO" id="TSh-2S-BCu">
|
||||
<rect key="frame" x="0.0" y="0.0" width="70" height="60"/>
|
||||
<accessibility key="accessibilityConfiguration">
|
||||
<accessibilityTraits key="traits" image="YES" notEnabled="YES"/>
|
||||
</accessibility>
|
||||
</imageView>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="VCO-Fr-5i7">
|
||||
<rect key="frame" x="0.0" y="80" width="70" height="0.0"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="11"/>
|
||||
<color key="textColor" white="0.33333333333333331" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
<accessibility key="accessibilityConfiguration">
|
||||
<accessibilityTraits key="traits" notEnabled="YES"/>
|
||||
</accessibility>
|
||||
<constraints>
|
||||
<constraint firstItem="TSh-2S-BCu" firstAttribute="top" secondItem="iN0-l3-epB" secondAttribute="top" id="5B6-O1-NR2"/>
|
||||
<constraint firstItem="VCO-Fr-5i7" firstAttribute="centerX" secondItem="iN0-l3-epB" secondAttribute="centerX" id="aFc-0b-hsh"/>
|
||||
<constraint firstItem="TSh-2S-BCu" firstAttribute="leading" secondItem="iN0-l3-epB" secondAttribute="leading" id="cce-KU-XTm"/>
|
||||
<constraint firstAttribute="bottom" secondItem="TSh-2S-BCu" secondAttribute="bottom" constant="20" id="lHP-mg-Ogd"/>
|
||||
<constraint firstAttribute="trailing" secondItem="TSh-2S-BCu" secondAttribute="trailing" id="q2A-Bm-uko"/>
|
||||
<constraint firstAttribute="bottom" secondItem="VCO-Fr-5i7" secondAttribute="bottom" id="rpf-k7-OZN"/>
|
||||
<constraint firstItem="VCO-Fr-5i7" firstAttribute="trailing" secondItem="iN0-l3-epB" secondAttribute="trailingMargin" constant="16" id="thO-W2-G2h"/>
|
||||
</constraints>
|
||||
<nil key="simulatedTopBarMetrics"/>
|
||||
<nil key="simulatedBottomBarMetrics"/>
|
||||
<freeformSimulatedSizeMetrics key="simulatedDestinationMetrics"/>
|
||||
<point key="canvasLocation" x="-387.19999999999999" y="-305.84707646176912"/>
|
||||
</view>
|
||||
</objects>
|
||||
<resources>
|
||||
<image name="generic.png" width="512" height="512"/>
|
||||
</resources>
|
||||
</document>
|
||||
BIN
ios/MattermostShare/Images/generic.png
Normal file
BIN
ios/MattermostShare/Images/generic.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 44 KiB |
@@ -2,10 +2,6 @@
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>BundleEntryFilename</key>
|
||||
<string>share.ios.js</string>
|
||||
<key>BundleForced</key>
|
||||
<true/>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
@@ -54,12 +50,5 @@
|
||||
<key>NSExtensionPointIdentifier</key>
|
||||
<string>com.apple.share-services</string>
|
||||
</dict>
|
||||
<key>UIAppFonts</key>
|
||||
<array>
|
||||
<string>Ionicons.ttf</string>
|
||||
<string>FontAwesome.ttf</string>
|
||||
<string>EvilIcons.ttf</string>
|
||||
<string>OpenSans-Bold.ttf</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
7
ios/MattermostShare/Item.swift
Normal file
7
ios/MattermostShare/Item.swift
Normal file
@@ -0,0 +1,7 @@
|
||||
import Foundation
|
||||
|
||||
final class Item {
|
||||
var id: String?
|
||||
var title: String?
|
||||
var selected: Bool = false
|
||||
}
|
||||
6
ios/MattermostShare/MattermostShare-Bridging-Header.h
Normal file
6
ios/MattermostShare/MattermostShare-Bridging-Header.h
Normal file
@@ -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"
|
||||
@@ -2,27 +2,9 @@
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.developer.icloud-container-identifiers</key>
|
||||
<array>
|
||||
<string>iCloud.$(CFBundleIdentifier)</string>
|
||||
</array>
|
||||
<key>com.apple.developer.icloud-services</key>
|
||||
<array>
|
||||
<string>CloudDocuments</string>
|
||||
</array>
|
||||
<key>com.apple.developer.ubiquity-container-identifiers</key>
|
||||
<array>
|
||||
<string>iCloud.$(CFBundleIdentifier)</string>
|
||||
</array>
|
||||
<key>com.apple.developer.ubiquity-kvstore-identifier</key>
|
||||
<string>$(TeamIdentifierPrefix)$(CFBundleIdentifier)</string>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.com.mattermost.rnbeta</string>
|
||||
</array>
|
||||
<key>keychain-access-groups</key>
|
||||
<array>
|
||||
<string>$(AppIdentifierPrefix)com.mattermost.rnbeta</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
#import "MattermostBucket.h"
|
||||
|
||||
@interface PerformRequests : NSObject<NSURLSessionDelegate, NSURLSessionTaskDelegate>
|
||||
@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
|
||||
@@ -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
|
||||
13
ios/MattermostShare/Section.swift
Normal file
13
ios/MattermostShare/Section.swift
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
#import "MattermostBucket.h"
|
||||
#import "KeyChainDataSource.h"
|
||||
|
||||
@interface SessionManager : NSObject<NSURLSessionDelegate, NSURLSessionTaskDelegate>
|
||||
@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
|
||||
@@ -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
|
||||
@@ -1,5 +0,0 @@
|
||||
#import <UIKit/UIKit.h>
|
||||
#import "React/RCTBridgeModule.h"
|
||||
|
||||
@interface ShareViewController : UIViewController<RCTBridgeModule>
|
||||
@end
|
||||
@@ -1,285 +0,0 @@
|
||||
#import "ShareViewController.h"
|
||||
#import <React/RCTBundleURLProvider.h>
|
||||
#import <React/RCTRootView.h>
|
||||
#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<NSSecureCoding, NSObject> 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<NSSecureCoding, NSObject> 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<NSSecureCoding, NSObject> 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<NSSecureCoding, NSObject> 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<NSSecureCoding, NSObject> 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
|
||||
405
ios/MattermostShare/ShareViewController.swift
Normal file
405
ios/MattermostShare/ShareViewController.swift
Normal file
@@ -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<AttachmentItem>()
|
||||
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()
|
||||
}
|
||||
}
|
||||
57
ios/MattermostShare/TeamsViewController.swift
Normal file
57
ios/MattermostShare/TeamsViewController.swift
Normal file
@@ -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])
|
||||
}
|
||||
}
|
||||
@@ -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 = "<group>"; };
|
||||
7FABE04C2213818900D0F595 /* MattermostBucket.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MattermostBucket.m; sourceTree = "<group>"; };
|
||||
7FABE04D2213818A00D0F595 /* MattermostBucket.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MattermostBucket.h; sourceTree = "<group>"; };
|
||||
7FABE053221387B400D0F595 /* Constants.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = Constants.m; sourceTree = "<group>"; };
|
||||
7FABE054221387B500D0F595 /* Constants.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Constants.h; sourceTree = "<group>"; };
|
||||
7FABE057221388D600D0F595 /* UploadSessionManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UploadSessionManager.swift; sourceTree = "<group>"; };
|
||||
7FABE0592213892100D0F595 /* AttachmentItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AttachmentItem.swift; sourceTree = "<group>"; };
|
||||
7FABE05A2213892100D0F595 /* AttachmentArray.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AttachmentArray.swift; sourceTree = "<group>"; };
|
||||
7FABE0F6221466F900D0F595 /* UploadManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UploadManager.swift; sourceTree = "<group>"; };
|
||||
7FABE0F82214674200D0F595 /* StoreManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = StoreManager.m; sourceTree = "<group>"; };
|
||||
7FABE0F92214674200D0F595 /* StoreManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = StoreManager.h; sourceTree = "<group>"; };
|
||||
7FABE0FB2214800F00D0F595 /* UploadSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UploadSession.swift; sourceTree = "<group>"; };
|
||||
/* 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 = "<group>";
|
||||
};
|
||||
7FABE03722137F2900D0F595 /* Products */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
7FABE03622137F2900D0F595 /* libUploadAttachments.a */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
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 = "<group>";
|
||||
};
|
||||
/* 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 */;
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1010"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "7FABE03522137F2900D0F595"
|
||||
BuildableName = "libUploadAttachments.a"
|
||||
BlueprintName = "UploadAttachments"
|
||||
ReferencedContainer = "container:UploadAttachments.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<Testables>
|
||||
</Testables>
|
||||
<AdditionalOptions>
|
||||
</AdditionalOptions>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES">
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "7FABE03522137F2900D0F595"
|
||||
BuildableName = "libUploadAttachments.a"
|
||||
BlueprintName = "UploadAttachments"
|
||||
ReferencedContainer = "container:UploadAttachments.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
<AdditionalOptions>
|
||||
</AdditionalOptions>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "7FABE03522137F2900D0F595"
|
||||
BuildableName = "libUploadAttachments.a"
|
||||
BlueprintName = "UploadAttachments"
|
||||
ReferencedContainer = "container:UploadAttachments.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
||||
269
ios/UploadAttachments/UploadAttachments/AttachmentArray.swift
Normal file
269
ios/UploadAttachments/UploadAttachments/AttachmentArray.swift
Normal file
@@ -0,0 +1,269 @@
|
||||
/// A thread-safe array.
|
||||
public class AttachmentArray<Element>: 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<ElementOfResult>(_ 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..<self.array.endIndex ~= index else { return }
|
||||
result = self.array[index]
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
set {
|
||||
guard let newValue = newValue else { return }
|
||||
|
||||
queue.async(flags: .barrier) {
|
||||
self.array[index] = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Equatable
|
||||
public extension AttachmentArray where Element: Equatable {
|
||||
|
||||
/// Returns a Boolean value indicating whether the sequence contains the given element.
|
||||
///
|
||||
/// - Parameter element: The element to find in the sequence.
|
||||
/// - Returns: true if the element was found in the sequence; otherwise, false.
|
||||
func contains(_ element: Element) -> 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
|
||||
}
|
||||
}
|
||||
11
ios/UploadAttachments/UploadAttachments/AttachmentItem.swift
Normal file
11
ios/UploadAttachments/UploadAttachments/AttachmentItem.swift
Normal file
@@ -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() {}
|
||||
}
|
||||
|
||||
7
ios/UploadAttachments/UploadAttachments/Constants.h
Normal file
7
ios/UploadAttachments/UploadAttachments/Constants.h
Normal file
@@ -0,0 +1,7 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
|
||||
@interface Constants : NSObject
|
||||
extern NSString *APP_GROUP_ID;
|
||||
extern UInt64 DEFAULT_SERVER_MAX_FILE_SIZE;
|
||||
@end
|
||||
6
ios/UploadAttachments/UploadAttachments/Constants.m
Normal file
6
ios/UploadAttachments/UploadAttachments/Constants.m
Normal file
@@ -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
|
||||
12
ios/UploadAttachments/UploadAttachments/MattermostBucket.h
Normal file
12
ios/UploadAttachments/UploadAttachments/MattermostBucket.h
Normal file
@@ -0,0 +1,12 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
@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
|
||||
69
ios/UploadAttachments/UploadAttachments/MattermostBucket.m
Normal file
69
ios/UploadAttachments/UploadAttachments/MattermostBucket.m
Normal file
@@ -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
|
||||
22
ios/UploadAttachments/UploadAttachments/StoreManager.h
Normal file
22
ios/UploadAttachments/UploadAttachments/StoreManager.h
Normal file
@@ -0,0 +1,22 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
#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
|
||||
303
ios/UploadAttachments/UploadAttachments/StoreManager.m
Normal file
303
ios/UploadAttachments/UploadAttachments/StoreManager.m
Normal file
@@ -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
|
||||
@@ -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"
|
||||
41
ios/UploadAttachments/UploadAttachments/UploadManager.swift
Normal file
41
ios/UploadAttachments/UploadAttachments/UploadManager.swift
Normal file
@@ -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<AttachmentItem>, 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..<attachments.count {
|
||||
guard let item = attachments[index] else {return}
|
||||
let url = URL(string: "\(baseURL)/api/v4/files?channel_id=\(channelId)&filename=\(item.fileName!)")
|
||||
var uploadRequest = URLRequest(url: url!)
|
||||
uploadRequest.httpMethod = "POST"
|
||||
uploadRequest.addValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
|
||||
|
||||
let task = uploadSession.uploadTask(with: uploadRequest, fromFile: item.fileURL!)
|
||||
task.resume()
|
||||
}
|
||||
} else if message != nil {
|
||||
// if the share action only has a message we post it
|
||||
UploadSession.shared.createPost(identifier: identifier)
|
||||
}
|
||||
callback()
|
||||
}
|
||||
}
|
||||
102
ios/UploadAttachments/UploadAttachments/UploadSession.swift
Normal file
102
ios/UploadAttachments/UploadAttachments/UploadSession.swift
Normal file
@@ -0,0 +1,102 @@
|
||||
import UIKit
|
||||
|
||||
@objc @objcMembers public class UploadSession: NSObject, URLSessionDataDelegate {
|
||||
public class var shared :UploadSession {
|
||||
struct Singleton {
|
||||
static let instance = UploadSession()
|
||||
}
|
||||
return Singleton.instance
|
||||
}
|
||||
var completionHandler: (() -> 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!()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
8
package-lock.json
generated
8
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
@@ -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 (
|
||||
<View style={styles.errorWrapper}>
|
||||
<View style={styles.errorContainer}>
|
||||
<View style={styles.errorContent}>
|
||||
<View style={styles.errorMessage}>
|
||||
<FormattedText
|
||||
style={styles.errorMessageText}
|
||||
id={'mobile.share_extension.error_message'}
|
||||
defaultMessage={'An error has occurred while using the share extension.'}
|
||||
/>
|
||||
</View>
|
||||
<TouchableOpacity
|
||||
style={styles.errorButton}
|
||||
onPress={() => close()}
|
||||
>
|
||||
<FormattedText
|
||||
style={styles.errorButtonText}
|
||||
id={'mobile.share_extension.error_close'}
|
||||
defaultMessage={'Close'}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
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),
|
||||
},
|
||||
};
|
||||
});
|
||||
@@ -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 (
|
||||
<View
|
||||
style={styles.flex}
|
||||
onLayout={this.onLayout}
|
||||
>
|
||||
<AnimatedView style={[styles.backdrop, {opacity: this.state.backdropOpacity}]}/>
|
||||
<View style={styles.wrapper}>
|
||||
<AnimatedView
|
||||
style={[
|
||||
styles.container,
|
||||
{
|
||||
opacity: this.state.containerOpacity,
|
||||
height: this.userIsLoggedIn() ? 250 : 130,
|
||||
top: isLandscape ? 20 : 65,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<NavigatorIOS
|
||||
initialRoute={initialRoute}
|
||||
style={styles.flex}
|
||||
navigationBarHidden={true}
|
||||
/>
|
||||
</AnimatedView>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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%',
|
||||
},
|
||||
});
|
||||
@@ -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 (
|
||||
<View style={styles.errorContainer}>
|
||||
<Text style={styles.error}>
|
||||
{error.message}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
if (!sections) {
|
||||
return (
|
||||
<View style={styles.loadingContainer}>
|
||||
<ActivityIndicator/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<TableView
|
||||
tableViewStyle={TableView.Consts.Style.Plain}
|
||||
tableViewCellStyle={TableView.Consts.CellStyle.Default}
|
||||
separatorColor={changeOpacity(theme.centerChannelColor, 0.5)}
|
||||
tintColor={theme.linkColor}
|
||||
detailFontSize={16}
|
||||
detailTextColor={theme.centerChannelColor}
|
||||
headerFontSize={15}
|
||||
headerTextColor={changeOpacity(theme.centerChannelColor, 0.6)}
|
||||
style={styles.flex}
|
||||
>
|
||||
<TableView.Header>
|
||||
<TableView.Cell>{this.renderSearchBar(styles)}</TableView.Cell>
|
||||
</TableView.Header>
|
||||
{this.renderSections()}
|
||||
</TableView>
|
||||
);
|
||||
};
|
||||
|
||||
renderSearchBar = (styles) => {
|
||||
const {formatMessage} = this.context.intl;
|
||||
const {theme} = this.props;
|
||||
|
||||
return (
|
||||
<View style={styles.searchContainer}>
|
||||
<SearchBar
|
||||
ref='search_bar'
|
||||
placeholder={formatMessage({id: 'search_bar.search', defaultMessage: 'Search'})}
|
||||
cancelTitle={formatMessage({id: 'mobile.post.cancel', defaultMessage: 'Cancel'})}
|
||||
backgroundColor='transparent'
|
||||
inputHeight={33}
|
||||
inputStyle={styles.searchBarInput}
|
||||
placeholderTextColor={changeOpacity(theme.centerChannelColor, 0.5)}
|
||||
tintColorSearch={changeOpacity(theme.centerChannelColor, 0.5)}
|
||||
tintColorDelete={changeOpacity(theme.centerChannelColor, 0.3)}
|
||||
titleCancelColor={theme.linkColor}
|
||||
onChangeText={this.handleSearch}
|
||||
onCancelButtonPress={this.cancelSearch}
|
||||
autoCapitalize='none'
|
||||
value={this.state.term}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
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 (
|
||||
<TableView.Item
|
||||
key={c.id}
|
||||
value={c.id}
|
||||
detail={s.id}
|
||||
selected={c.id === currentChannelId}
|
||||
onPress={this.handleSelectChannel}
|
||||
image={icon}
|
||||
>
|
||||
{c.display_name}
|
||||
</TableView.Item>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<TableView.Section
|
||||
key={s.id}
|
||||
label={formatMessage({id: s.id, defaultMessage: s.defaultMessage})}
|
||||
headerHeight={30}
|
||||
>
|
||||
{items}
|
||||
</TableView.Section>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
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 (
|
||||
<View style={styles.flex}>
|
||||
<ExtensionNavBar
|
||||
backButton={true}
|
||||
onLeftButtonPress={this.goBack}
|
||||
title={title}
|
||||
theme={theme}
|
||||
/>
|
||||
{this.renderBody(styles)}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
},
|
||||
};
|
||||
});
|
||||
@@ -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 = (
|
||||
<IonIcon
|
||||
name='ios-arrow-back'
|
||||
style={styles.backButton}
|
||||
/>
|
||||
);
|
||||
} else if (leftButtonTitle) {
|
||||
backComponent = (
|
||||
<Text
|
||||
ellipsisMode='tail'
|
||||
numberOfLines={1}
|
||||
style={styles.leftButton}
|
||||
>
|
||||
{leftButtonTitle}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
if (backComponent) {
|
||||
return (
|
||||
<TouchableOpacity
|
||||
onPress={onLeftButtonPress}
|
||||
style={styles.backButtonContainer}
|
||||
>
|
||||
{backComponent}
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
|
||||
return <View style={styles.backButtonContainer}/>;
|
||||
};
|
||||
|
||||
renderRightButton = (styles) => {
|
||||
const {authenticated, onRightButtonPress, rightButtonTitle} = this.props;
|
||||
|
||||
if (rightButtonTitle && authenticated) {
|
||||
return (
|
||||
<TouchableOpacity
|
||||
onPress={onRightButtonPress}
|
||||
style={styles.rightButtonContainer}
|
||||
>
|
||||
<Text
|
||||
ellipsisMode='tail'
|
||||
numberOfLines={1}
|
||||
style={styles.rightButton}
|
||||
>
|
||||
{rightButtonTitle}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
|
||||
return <View style={styles.rightButtonContainer}/>;
|
||||
};
|
||||
|
||||
render() {
|
||||
const {theme, title} = this.props;
|
||||
const styles = getStyleSheet(theme);
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
{this.renderLeftButton(styles)}
|
||||
<View style={styles.titleContainer}>
|
||||
<Text style={styles.title}>
|
||||
{title}
|
||||
</Text>
|
||||
</View>
|
||||
{this.renderRightButton(styles)}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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',
|
||||
},
|
||||
};
|
||||
});
|
||||
@@ -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 (
|
||||
<View style={styles.unauthenticatedContainer}>
|
||||
<Text style={styles.unauthenticated}>
|
||||
{error.message}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
if (sending) {
|
||||
return (
|
||||
<View style={styles.sendingContainer}>
|
||||
<ActivityIndicator/>
|
||||
<Text style={styles.sendingText}>
|
||||
{formatMessage({
|
||||
id: 'mobile.extension.posting',
|
||||
defaultMessage: 'Posting...',
|
||||
})}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
if (exceededSize) {
|
||||
return (
|
||||
<View style={styles.unauthenticatedContainer}>
|
||||
<Text style={styles.unauthenticated}>
|
||||
{formatMessage({
|
||||
id: 'mobile.extension.max_file_size',
|
||||
defaultMessage: 'File attachments shared in Mattermost must be less than {size}.',
|
||||
}, {size: getFormattedFileSize({size: maxSize})})}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
if (authenticated && !error) {
|
||||
return (
|
||||
<ScrollView
|
||||
ref={this.getScrollViewRef}
|
||||
contentContainerStyle={styles.scrollView}
|
||||
style={styles.flex}
|
||||
>
|
||||
<TextInput
|
||||
ref={this.getInputRef}
|
||||
maxLength={MAX_MESSAGE_LENGTH}
|
||||
multiline={true}
|
||||
onChangeText={this.handleTextChange}
|
||||
placeholder={formatMessage({id: 'create_post.write', defaultMessage: 'Write a message...'})}
|
||||
placeholderTextColor={changeOpacity(theme.centerChannelColor, 0.5)}
|
||||
style={[styles.input, {maxHeight: MAX_INPUT_HEIGHT}]}
|
||||
value={value}
|
||||
/>
|
||||
{this.renderFiles(styles)}
|
||||
</ScrollView>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={styles.unauthenticatedContainer}>
|
||||
<Text style={styles.unauthenticated}>
|
||||
{formatMessage({
|
||||
id: 'mobile.extension.authentication_required',
|
||||
defaultMessage: 'Authentication required: Please first login using the app.',
|
||||
})}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
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 (
|
||||
<TouchableHighlight
|
||||
onPress={this.goToChannels}
|
||||
style={styles.buttonContainer}
|
||||
underlayColor={changeOpacity(theme.centerChannelColor, 0.2)}
|
||||
>
|
||||
<View style={styles.buttonWrapper}>
|
||||
<View style={styles.buttonLabelContainer}>
|
||||
<Text style={styles.buttonLabel}>
|
||||
{formatMessage({id: 'mobile.share_extension.channel', defaultMessage: 'Channel'})}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={styles.buttonValueContainer}>
|
||||
<Text
|
||||
ellipsizeMode='tail'
|
||||
numberOfLines={1}
|
||||
style={styles.buttonValue}
|
||||
>
|
||||
{channelName}
|
||||
</Text>
|
||||
<View style={styles.arrowContainer}>
|
||||
<IonIcon
|
||||
color={changeOpacity(theme.centerChannelColor, 0.4)}
|
||||
name='ios-arrow-forward'
|
||||
size={25}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</TouchableHighlight>
|
||||
);
|
||||
};
|
||||
|
||||
renderFiles = (styles) => {
|
||||
const {files} = this.state;
|
||||
return files.map((file, index) => {
|
||||
let component;
|
||||
|
||||
switch (file.type) {
|
||||
case 'public.image':
|
||||
component = (
|
||||
<View
|
||||
key={`item-${index}`}
|
||||
style={styles.imageContainer}
|
||||
>
|
||||
<Image
|
||||
source={{uri: file.fullPath}}
|
||||
resizeMode='cover'
|
||||
style={styles.image}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
break;
|
||||
case 'public.movie':
|
||||
component = (
|
||||
<View
|
||||
key={`item-${index}`}
|
||||
style={styles.imageContainer}
|
||||
>
|
||||
<Video
|
||||
style={styles.video}
|
||||
resizeMode='cover'
|
||||
source={{uri: file.fullPath}}
|
||||
volume={0}
|
||||
paused={true}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
break;
|
||||
case 'public.file-url': {
|
||||
let SvgIcon = extensionSvg[file.extension];
|
||||
if (!SvgIcon) {
|
||||
SvgIcon = GenericSvg;
|
||||
}
|
||||
|
||||
component = (
|
||||
<View
|
||||
key={`item-${index}`}
|
||||
style={styles.otherContainer}
|
||||
>
|
||||
<View style={styles.otherWrapper}>
|
||||
<View style={styles.fileIcon}>
|
||||
<SvgIcon
|
||||
width={19}
|
||||
height={48}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<View
|
||||
style={styles.fileContainer}
|
||||
key={`item-${index}`}
|
||||
>
|
||||
{component}
|
||||
<Text
|
||||
ellipsisMode='tail'
|
||||
numberOfLines={1}
|
||||
style={styles.filename}
|
||||
>
|
||||
{`${file.size} - ${file.filename}`}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
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 (
|
||||
<TouchableHighlight
|
||||
onPress={this.goToTeams}
|
||||
style={styles.buttonContainer}
|
||||
underlayColor={changeOpacity(theme.centerChannelColor, 0.2)}
|
||||
>
|
||||
<View style={styles.buttonWrapper}>
|
||||
<View style={styles.flex}>
|
||||
<Text style={styles.buttonLabel}>
|
||||
{formatMessage({id: 'mobile.share_extension.team', defaultMessage: 'Team'})}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={styles.buttonValueContainer}>
|
||||
<Text style={styles.buttonValue}>
|
||||
{teamName}
|
||||
</Text>
|
||||
<View style={styles.arrowContainer}>
|
||||
<IonIcon
|
||||
color={changeOpacity(theme.centerChannelColor, 0.4)}
|
||||
name='ios-arrow-forward'
|
||||
size={25}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</TouchableHighlight>
|
||||
);
|
||||
};
|
||||
|
||||
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 (
|
||||
<View
|
||||
onLayout={this.onLayout}
|
||||
style={styles.container}
|
||||
>
|
||||
<ExtensionNavBar
|
||||
authenticated={authenticated}
|
||||
leftButtonTitle={cancelButton}
|
||||
onLeftButtonPress={this.handleCancel}
|
||||
onRightButtonPress={this.sendMessage}
|
||||
rightButtonTitle={postButtonText}
|
||||
theme={theme}
|
||||
title={title}
|
||||
/>
|
||||
{this.renderBody(styles)}
|
||||
{!error && this.renderTeamButton(styles)}
|
||||
{!error && this.renderChannelButton(styles)}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
},
|
||||
};
|
||||
});
|
||||
@@ -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 (
|
||||
<TouchableHighlight
|
||||
underlayColor={changeOpacity(theme.sidebarTextHoverBg, 0.5)}
|
||||
onPress={this.onPress}
|
||||
style={styles.wrapper}
|
||||
>
|
||||
<View style={styles.container}>
|
||||
<View style={styles.item}>
|
||||
<TeamIcon
|
||||
styleContainer={styles.teamIconContainer}
|
||||
styleText={styles.teamIconText}
|
||||
styleImage={styles.imageContainer}
|
||||
teamId={team.id}
|
||||
team={team}
|
||||
theme={theme}
|
||||
/>
|
||||
<Text
|
||||
style={[styles.text]}
|
||||
ellipsizeMode='tail'
|
||||
numberOfLines={1}
|
||||
>
|
||||
{team.display_name}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</TouchableHighlight>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
},
|
||||
};
|
||||
});
|
||||
@@ -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 (
|
||||
<View style={styles.errorContainer}>
|
||||
<Text style={styles.error}>
|
||||
{error.message}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
if (!myTeams) {
|
||||
return (
|
||||
<View style={styles.loadingContainer}>
|
||||
<ActivityIndicator/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<TableView
|
||||
tableViewStyle={TableView.Consts.Style.Plain}
|
||||
tableViewCellStyle={TableView.Consts.CellStyle.Default}
|
||||
separatorColor={changeOpacity(theme.centerChannelColor, 0.5)}
|
||||
tintColor={theme.linkColor}
|
||||
detailFontSize={16}
|
||||
detailTextColor={theme.centerChannelColor}
|
||||
headerFontSize={15}
|
||||
headerTextColor={changeOpacity(theme.centerChannelColor, 0.6)}
|
||||
style={styles.flex}
|
||||
>
|
||||
<TableView.Section>
|
||||
{this.renderItems(myTeams)}
|
||||
</TableView.Section>
|
||||
</TableView>
|
||||
);
|
||||
};
|
||||
|
||||
renderItems = (myTeams) => {
|
||||
const {currentTeamId, theme} = this.props;
|
||||
|
||||
return myTeams.map((team) => {
|
||||
return (
|
||||
<TableView.Cell
|
||||
key={team.id}
|
||||
selected={team.id === currentTeamId}
|
||||
>
|
||||
<View>
|
||||
<ExtensionTeamItem
|
||||
currentTeamId={currentTeamId}
|
||||
onSelectTeam={this.handleSelectTeam}
|
||||
team={team}
|
||||
theme={theme}
|
||||
/>
|
||||
</View>
|
||||
</TableView.Cell>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
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 (
|
||||
<View style={styles.flex}>
|
||||
<ExtensionNavBar
|
||||
backButton={true}
|
||||
onLeftButtonPress={this.goBack}
|
||||
title={formatMessage({id: 'mobile.drawer.teamsTitle', defaultMessage: 'Teams'})}
|
||||
theme={theme}
|
||||
/>
|
||||
{this.renderBody(styles)}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
},
|
||||
};
|
||||
});
|
||||
@@ -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 (
|
||||
<ErrorMessage close={this.close}/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Extension
|
||||
appGroupId={Config.AppGroupId}
|
||||
onClose={this.close}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default function ShareExtensionProvider() {
|
||||
const locale = DeviceInfo.getDeviceLocale().split('-')[0];
|
||||
|
||||
return (
|
||||
<IntlProvider
|
||||
locale={locale}
|
||||
messages={getTranslations(locale)}
|
||||
>
|
||||
<SharedApp/>
|
||||
</IntlProvider>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user