iOS Native Share Extension (Swift) (#2575)

* iOS Native Share Extension (Swift)

* Re-arrange files

* Fix .gitignore
This commit is contained in:
Elias Nahum
2019-02-26 14:31:57 -03:00
committed by GitHub
parent faf4b1dd96
commit 3feaa8e6bb
71 changed files with 2590 additions and 3348 deletions

View File

@@ -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.

View File

@@ -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 {

View File

@@ -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,

View File

@@ -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(() => {

View File

@@ -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,

View File

@@ -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,

View File

@@ -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);
}
},
};

View File

@@ -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,

View File

@@ -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,

View File

@@ -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);
});

View File

@@ -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(() => {

View File

@@ -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,

View File

@@ -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;
}

View File

@@ -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": {}

View File

@@ -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

View File

@@ -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"

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@@ -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

View 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
//

View File

@@ -0,0 +1,4 @@
//
// Use this file to import your target's public headers that you would like to expose to Swift.
//

View File

@@ -0,0 +1,7 @@
#import <Foundation/Foundation.h>
#import "React/RCTBridgeModule.h"
#import "MattermostBucket.h"
@interface MattermostBucketModule : NSObject<RCTBridgeModule>
@property MattermostBucket *bucket;
@end

View 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

View File

@@ -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

View File

@@ -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

View File

@@ -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"/>

View 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
}
}

View 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]
}
}

View 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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

View File

@@ -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>

View File

@@ -0,0 +1,7 @@
import Foundation
final class Item {
var id: String?
var title: String?
var selected: Bool = false
}

View 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"

View File

@@ -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>

View File

@@ -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

View File

@@ -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

View 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
}
}

View File

@@ -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

View File

@@ -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

View File

@@ -1,5 +0,0 @@
#import <UIKit/UIKit.h>
#import "React/RCTBridgeModule.h"
@interface ShareViewController : UIViewController<RCTBridgeModule>
@end

View File

@@ -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

View 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()
}
}

View 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])
}
}

View File

@@ -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 */;
}

View File

@@ -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>

View 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 collections 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
}
}

View 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() {}
}

View 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

View 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

View 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

View 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

View 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

View 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

View File

@@ -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"

View 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()
}
}

View 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!()
}
}
}
}

View File

@@ -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
View File

@@ -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",

View File

@@ -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",

View File

@@ -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);

View File

@@ -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),
},
};
});

View File

@@ -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%',
},
});

View File

@@ -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,
},
};
});

View File

@@ -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',
},
};
});

View File

@@ -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,
},
};
});

View File

@@ -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,
},
};
});

View File

@@ -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,
},
};
});

View File

@@ -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>
);
}