@@ -1,157 +1,172 @@
import UIKit
import UIKit
import UniformTypeIdentifiers
import UniformTypeIdentifiers
// S h a r e E x t e n s i o n — t h i s i s w h a t p u t s " B i z C o n n e c t " i n t h e i O S s h a r e s h e e t ( P h o t o s , F i l e s , S a f a r i , … ) .
// S h a r e E x t e n s i o n — p u t s " B i z C o n n e c t " i n t h e i O S s h a r e s h e e t A N D d o e s t h e w h o l e s e n d r i g h t h e r e , t h e w a y
// A n a p p C A N N O T a p p e a r t h e r e a n y o t h e r w a y : t h e s h a r e s h e e t o n l y l i s t s a p p e x t e n s i o n s , s o t h i s h a s t o b e a
// T e a m s / W h a t s A p p d o : p i c k a c h a t i n t h e s h e e t , i t u p l o a d s a n d s e n d s , n o a p p - o p e n n e e d e d .
// s e c o n d t a r g e t i n s i d e t h e a p p b u n d l e , w i t h i t s o w n b u n d l e i d a n d p r o v i s i o n i n g p r o f i l e .
//
//
// T h e p i c k e r i t s e l f l i v e s i n t h e A P P , n o t h e r e — t h e c h a t l i s t , s e a r c h a n d u p l o a d p r o g r e s s a l r e a d y e x i s t
// H o w i t c a n s e n d w i t h o u t t h e a p p : t h e a p p w r i t e s a b e a r e r t o k e n + A P I b a s e i n t o t h e A p p G r o u p e a c h l a u n c h
// t h e r e , a n d r e - i m p l e m e n t i n g t h e m n a t i v e l y w o u l d b e a s e c o n d , d i v e r g e n t c o p y ( a n d t h e e x t e n s i o n c a n ' t
// ( v i a / a p i / s h a r e / t o k e n → S h a r e I n b o x . s e t A u t h ) . T h i s e x t e n s i o n r e a d s t h a t t o k e n a n d c a l l s t h e s a m e s e r v e r
// r e a c h t h e w e b s e s s i o n w h e r e c o n t a c t s l i v e ) . S o t h i s e x t e n s i o n :
// A P I t h e n a t i v e c l i e n t u s e s — G E T / a p i / m e s s a g e s / c o n v e r s a t i o n s , P O S T / a p i / m e s s a g e s / u p l o a d , P O S T
// 1 . c o p i e s t h e s h a r e d i t e m s i n t o t h e A p p G r o u p c o n t a i n e r ( t h e o n l y s t o r a g e b o t h p r o c e s s e s c a n s e e ) ,
// / a p i / m e s s a g e s . I f t h e r e ' s n o t o k e n ( u s e r n e v e r o p e n e d / s i g n e d i n ) , w e s a y s o a n d s t i l l s t a g e t h e f i l e s s o
// 2 . t r i e s t o o p e n t h e a p p ( b i z c o n n e c t : / / s h a r e ) , a n d
// o p e n i n g t h e a p p l a t e r p i c k s t h e m u p .
// 3 . s h o w s a s m a l l c o n f i r m a t i o n c a r d — b e c a u s e a S h a r e E x t e n s i o n o p e n i n g i t s h o s t a p p i s u n s u p p o r t e d o n
//
// m o d e r n i O S a n d o f t e n s i l e n t l y b l o c k e d , s o t h e c a r d g i v e s t h e u s e r a c l e a r " O p e n B i z C o n n e c t " t a p
// T h e A p p G r o u p i s t h e o n l y s t o r a g e t h e a p p a n d t h i s s e p a r a t e p r o c e s s s h a r e .
// a n d s t a t e s t h e f i l e s a r e r e a d y . E i t h e r w a y t h e a p p c o l l e c t s t h e s t a g e d f i l e s w h e n n e x t o p e n e d , s o
// n o t h i n g i s e v e r l o s t .
struct ShareChat {
class ShareViewController : UIViewController {
let kind : String // " d m " | " g r o u p "
let id : String
let name : String
}
struct ShareItem {
let name : String
let url : URL // s t a g e d c o p y i n t h e A p p G r o u p c o n t a i n e r
let mime : String
let isText : Bool
let text : String
}
class ShareViewController : UIViewController , UITableViewDataSource , UITableViewDelegate {
private let appGroup = " group.com.bizgaze.connect "
private let appGroup = " group.com.bizgaze.connect "
private let urlScheme = " bizconnect "
private let maxItems = 20
private let maxItems = 20
// M i n i m a l c o n f i r m a t i o n c a r d ( b u i l t i n c o d e — n o s t o r y b o a r d ) .
private var token : String = " "
private let card = UIView ( )
private var apiBase : String = " https://remote.bizgaze.com "
private let titleLabel = UILabel ( )
private var items : [ ShareItem ] = [ ]
private let subtitleLabel = UILabel ( )
private var chats : [ ShareChat ] = [ ]
private let openButton = UIButton ( type : . system )
private var filtered : [ ShareChat ] = [ ]
private let doneButton = UIButton ( type : . system )
private var sending = false
private var staged = false
private let table = UITableView ( frame : . zero , style : . plain )
private let search = UISearchBar ( )
private let statusLabel = UILabel ( )
private let spinner = UIActivityIndicatorView ( style : . medium )
private let subtitle = UILabel ( )
// MARK: - L i f e c y c l e
override func viewDidLoad ( ) {
override func viewDidLoad ( ) {
super . viewDidLoad ( )
super . viewDidLoad ( )
view . backgroundColor = UIColor . black . withAlphaComponent ( 0.35 )
readAuth ( )
buildCard ( )
buildUI ( )
ingest ( ) // s t a g e t h e s h a r e d i t e m s , t h e n l o a d t h e c h a t l i s t
}
}
override func viewDidAppear ( _ animated : Bool ) {
private func readAuth ( ) {
super . viewDidAppear ( animated )
if let d = UserDefaults ( suiteName : appGroup ) {
handleShare ( )
token = d . string ( forKey : " bzc_token " ) ? ? " "
let b = d . string ( forKey : " bzc_base " ) ? ? " "
if ! b . isEmpty { apiBase = b }
}
}
}
// MARK: - I n g e s t
// MARK: - U I
private func handleShare ( ) {
private func buildUI ( ) {
view . backgroundColor = . systemBackground
let nav = UINavigationBar ( )
nav . translatesAutoresizingMaskIntoConstraints = false
let navItem = UINavigationItem ( title : " Send to… " )
navItem . leftBarButtonItem = UIBarButtonItem ( barButtonSystemItem : . cancel , target : self , action : #selector ( cancelTapped ) )
nav . setItems ( [ navItem ] , animated : false )
view . addSubview ( nav )
subtitle . font = . systemFont ( ofSize : 12 )
subtitle . textColor = . secondaryLabel
subtitle . textAlignment = . center
subtitle . translatesAutoresizingMaskIntoConstraints = false
view . addSubview ( subtitle )
search . placeholder = " Search chats… "
search . delegate = self
search . searchBarStyle = . minimal
search . translatesAutoresizingMaskIntoConstraints = false
view . addSubview ( search )
table . dataSource = self
table . delegate = self
table . register ( UITableViewCell . self , forCellReuseIdentifier : " chat " )
table . translatesAutoresizingMaskIntoConstraints = false
view . addSubview ( table )
statusLabel . font = . systemFont ( ofSize : 15 )
statusLabel . textColor = . secondaryLabel
statusLabel . textAlignment = . center
statusLabel . numberOfLines = 0
statusLabel . translatesAutoresizingMaskIntoConstraints = false
view . addSubview ( statusLabel )
spinner . translatesAutoresizingMaskIntoConstraints = false
spinner . hidesWhenStopped = true
view . addSubview ( spinner )
NSLayoutConstraint . activate ( [
nav . topAnchor . constraint ( equalTo : view . safeAreaLayoutGuide . topAnchor ) ,
nav . leadingAnchor . constraint ( equalTo : view . leadingAnchor ) ,
nav . trailingAnchor . constraint ( equalTo : view . trailingAnchor ) ,
subtitle . topAnchor . constraint ( equalTo : nav . bottomAnchor , constant : 2 ) ,
subtitle . leadingAnchor . constraint ( equalTo : view . leadingAnchor , constant : 16 ) ,
subtitle . trailingAnchor . constraint ( equalTo : view . trailingAnchor , constant : - 16 ) ,
search . topAnchor . constraint ( equalTo : subtitle . bottomAnchor , constant : 4 ) ,
search . leadingAnchor . constraint ( equalTo : view . leadingAnchor , constant : 6 ) ,
search . trailingAnchor . constraint ( equalTo : view . trailingAnchor , constant : - 6 ) ,
table . topAnchor . constraint ( equalTo : search . bottomAnchor , constant : 2 ) ,
table . leadingAnchor . constraint ( equalTo : view . leadingAnchor ) ,
table . trailingAnchor . constraint ( equalTo : view . trailingAnchor ) ,
table . bottomAnchor . constraint ( equalTo : view . bottomAnchor ) ,
statusLabel . centerXAnchor . constraint ( equalTo : view . centerXAnchor ) ,
statusLabel . centerYAnchor . constraint ( equalTo : view . centerYAnchor ) ,
statusLabel . leadingAnchor . constraint ( equalTo : view . leadingAnchor , constant : 32 ) ,
statusLabel . trailingAnchor . constraint ( equalTo : view . trailingAnchor , constant : - 32 ) ,
spinner . centerXAnchor . constraint ( equalTo : view . centerXAnchor ) ,
spinner . topAnchor . constraint ( equalTo : statusLabel . bottomAnchor , constant : 14 )
] )
}
private func setStatus ( _ text : String ? , busy : Bool = false ) {
DispatchQueue . main . async {
self . statusLabel . text = text
self . statusLabel . isHidden = ( text = = nil )
if busy { self . spinner . startAnimating ( ) } else { self . spinner . stopAnimating ( ) }
}
}
// MARK: - I n g e s t s h a r e d i t e m s
private func ingest ( ) {
setStatus ( " Preparing… " , busy : true )
let providers = ( extensionContext ? . inputItems as ? [ NSExtensionItem ] ? ? [ ] )
let providers = ( extensionContext ? . inputItems as ? [ NSExtensionItem ] ? ? [ ] )
. flatMap { $0 . attachments ? ? [ ] }
. flatMap { $0 . attachments ? ? [ ] }
. prefix ( maxItems )
. prefix ( maxItems )
guard ! providers . isEmpty else { return finish ( ) }
guard ! providers . isEmpty else { return finishFail ( " Nothing to share. " ) }
var collected : [ [ String : Any ] ] = [ ]
var collected : [ ShareItem ] = [ ]
let lock = NSLock ( )
let lock = NSLock ( )
let group = DispatchGroup ( )
let group = DispatchGroup ( )
for provider in providers {
for provider in providers {
group . enter ( )
group . enter ( )
load ( provider ) { record in
load ( provider ) { item in
if let record = record { lock . lock ( ) ; collected . append ( record ) ; lock . unlock ( ) }
if let item = item { lock . lock ( ) ; collected . append ( item ) ; lock . unlock ( ) }
group . leave ( )
group . leave ( )
}
}
}
}
group . notify ( queue : . main ) { [ weak self ] in
group . notify ( queue : . main ) { [ weak self ] in
guard let self = self else { return }
guard let self = self else { return }
if ! collected . isEmpty { self . appendToManifest ( collected ) }
self . items = collected
self . staged = ! collected . isEmpty
// A l w a y s A L S O l e a v e a m a n i f e s t , s o i f t h e s e n d h e r e f a i l s t h e a p p c a n s t i l l c o l l e c t t h e m l a t e r .
self . openHostApp ( ) // b e s t - e f f o r t ; u s u a l l y b l o c k e d , h e n c e t h e c a r d b e l o w
self . writeManifest ( collected )
self . showReady ( count : collected . count )
if collected . isEmpty { return self . finishFail ( " Couldn’ t read the shared file. " ) }
let fileCount = collected . filter { ! $0 . isText } . count
self . subtitle . text = fileCount > 0 ? " Sharing \( fileCount ) file \( fileCount = = 1 ? " " : " s " ) " : " Sharing a link "
self . loadChats ( )
}
}
}
}
// MARK: - C o n f i r m a t i o n U I
private func load ( _ provider : NSItemProvider , completion : @ escaping ( ShareItem ? ) -> Void ) {
private func buildCard ( ) {
card . backgroundColor = . systemBackground
card . layer . cornerRadius = 16
card . translatesAutoresizingMaskIntoConstraints = false
view . addSubview ( card )
titleLabel . text = " Preparing… "
titleLabel . font = . systemFont ( ofSize : 17 , weight : . semibold )
titleLabel . textColor = . label
titleLabel . textAlignment = . center
titleLabel . numberOfLines = 0
subtitleLabel . text = " "
subtitleLabel . font = . systemFont ( ofSize : 13 )
subtitleLabel . textColor = . secondaryLabel
subtitleLabel . textAlignment = . center
subtitleLabel . numberOfLines = 0
openButton . setTitle ( " Open Biz Connect " , for : . normal )
openButton . titleLabel ? . font = . systemFont ( ofSize : 16 , weight : . semibold )
openButton . setTitleColor ( . white , for : . normal )
openButton . backgroundColor = UIColor ( red : 0.12 , green : 0.23 , blue : 0.45 , alpha : 1 ) // b r a n d n a v y
openButton . layer . cornerRadius = 10
openButton . addTarget ( self , action : #selector ( openTapped ) , for : . touchUpInside )
openButton . isHidden = true
doneButton . setTitle ( " Done " , for : . normal )
doneButton . titleLabel ? . font = . systemFont ( ofSize : 15 )
doneButton . addTarget ( self , action : #selector ( doneTapped ) , for : . touchUpInside )
doneButton . isHidden = true
let stack = UIStackView ( arrangedSubviews : [ titleLabel , subtitleLabel , openButton , doneButton ] )
stack . axis = . vertical
stack . spacing = 12
stack . alignment = . fill
stack . setCustomSpacing ( 6 , after : titleLabel )
stack . setCustomSpacing ( 18 , after : subtitleLabel )
stack . translatesAutoresizingMaskIntoConstraints = false
card . addSubview ( stack )
NSLayoutConstraint . activate ( [
card . centerXAnchor . constraint ( equalTo : view . centerXAnchor ) ,
card . centerYAnchor . constraint ( equalTo : view . centerYAnchor ) ,
card . leadingAnchor . constraint ( greaterThanOrEqualTo : view . leadingAnchor , constant : 32 ) ,
card . trailingAnchor . constraint ( lessThanOrEqualTo : view . trailingAnchor , constant : - 32 ) ,
card . widthAnchor . constraint ( lessThanOrEqualToConstant : 340 ) ,
stack . topAnchor . constraint ( equalTo : card . topAnchor , constant : 22 ) ,
stack . bottomAnchor . constraint ( equalTo : card . bottomAnchor , constant : - 18 ) ,
stack . leadingAnchor . constraint ( equalTo : card . leadingAnchor , constant : 22 ) ,
stack . trailingAnchor . constraint ( equalTo : card . trailingAnchor , constant : - 22 ) ,
openButton . heightAnchor . constraint ( equalToConstant : 46 )
] )
}
private func showReady ( count : Int ) {
if ! staged {
titleLabel . text = " Couldn’ t prepare the file "
subtitleLabel . text = " Please try sharing again. "
} else {
titleLabel . text = " ✓ Ready to send "
let noun = count = = 1 ? " item " : " \( count ) items "
subtitleLabel . text = " Open Biz Connect to choose a chat and send your \( noun ) . "
openButton . isHidden = false
}
doneButton . isHidden = false
}
@objc private func openTapped ( ) {
openHostApp ( )
finish ( )
}
@objc private func doneTapped ( ) {
finish ( )
}
// / R e s o l v e o n e a t t a c h m e n t t o a s t a g e d f i l e ( o r a t e x t / l i n k r e c o r d ) .
private func load ( _ provider : NSItemProvider , completion : @ escaping ( [ String : Any ] ? ) -> Void ) {
// M o s t s p e c i f i c f i r s t ; . i t e m i s t h e c a t c h - a l l f o r a r b i t r a r y d o c u m e n t s .
let fileTypes : [ UTType ] = [ . movie , . image , . audio , . pdf , . item ]
let fileTypes : [ UTType ] = [ . movie , . image , . audio , . pdf , . item ]
if let type = fileTypes . first ( where : { provider . hasItemConformingToTypeIdentifier ( $0 . identifier ) } ) {
if let type = fileTypes . first ( where : { provider . hasItemConformingToTypeIdentifier ( $0 . identifier ) } ) {
// l o a d F i l e R e p r e s e n t a t i o n h a n d s b a c k a U R L t h a t i s d e l e t e d t h e m o m e n t t h i s c l o s u r e r e t u r n s ,
// s o t h e c o p y h a s t o h a p p e n s y n c h r o n o u s l y i n s i d e i t .
provider . loadFileRepresentation ( forTypeIdentifier : type . identifier ) { [ weak self ] url , _ in
provider . loadFileRepresentation ( forTypeIdentifier : type . identifier ) { [ weak self ] url , _ in
guard let self = self , let url = url else { return completion ( nil ) }
guard let self = self , let url = url else { return completion ( nil ) }
completion ( self . stage ( url ) )
completion ( self . stage ( url ) )
@@ -160,37 +175,177 @@ class ShareViewController: UIViewController {
}
}
if provider . hasItemConformingToTypeIdentifier ( UTType . url . identifier ) {
if provider . hasItemConformingToTypeIdentifier ( UTType . url . identifier ) {
provider . loadItem ( forTypeIdentifier : UTType . url . identifier ) { item , _ in
provider . loadItem ( forTypeIdentifier : UTType . url . identifier ) { item , _ in
completion ( ( item as ? URL ) . map { [ " kind " : " text " , " text " : $0 . absoluteString ] } )
if let u = item as ? URL { completion ( ShareItem ( name : " " , url : URL ( fileURLWithPath : " / " ) , mime : " " , isText : true , text : u . absoluteString ) ) }
else { completion ( nil ) }
}
}
return
return
}
}
if provider . hasItemConformingToTypeIdentifier ( UTType . plainText . identifier ) {
if provider . hasItemConformingToTypeIdentifier ( UTType . plainText . identifier ) {
provider . loadItem ( forTypeIdentifier : UTType . plainText . identifier ) { item , _ in
provider . loadItem ( forTypeIdentifier : UTType . plainText . identifier ) { item , _ in
completion ( ( item as ? String ) . map { [ " kind " : " text " , " text " : $0 ] } )
if let s = item as ? String { completion ( ShareItem ( name : " " , url : URL ( fileURLWithPath : " / " ) , mime : " " , isText : true , text : s ) ) }
else { completion ( nil ) }
}
}
return
return
}
}
completion ( nil )
completion ( nil )
}
}
private func stage ( _ src : URL ) -> [ String : Any ] ? {
private func stage ( _ src : URL ) -> ShareItem ? {
guard let dir = sharedDirectory ( ) else { return nil }
guard let dir = sharedDir ( ) else { return nil }
let name = src . lastPathComponent . isEmpty ? " shared-file " : src . lastPathComponent
let name = src . lastPathComponent . isEmpty ? " shared-file " : src . lastPathComponent
let dest = uniqueURL ( in : dir , preferred : name )
let dest = uniqueURL ( in : dir , preferred : name )
do { try FileManager . default . copyItem ( at : src , to : dest ) } catch { return nil }
do { try FileManager . default . copyItem ( at : src , to : dest ) } catch { return nil }
let size = ( try ? FileManager . default . attributesOfItem ( atPath : dest . path ) [ . size ] as ? Int64 ) ? ? 0
return ShareItem ( name : dest . lastPathComponent , url : dest , mime : Self . mimeType ( for : dest ) , isText : false , text : " " )
return [
}
" kind " : " file " ,
" name " : dest . lastPathComponent ,
// MARK: - C h a t l i s t
" path " : dest . path ,
" mime " : Self . mimeType ( for : dest ) ,
private func loadChats ( ) {
" size " : size ? ? 0
guard ! token . isEmpty else {
]
setStatus ( " Open Biz Connect and sign in first, then share again. \n \n Your file is saved and will be waiting in the app. " )
return
}
setStatus ( " Loading your chats… " , busy : true )
api ( " GET " , " /api/messages/conversations " , body : nil ) { [ weak self ] data , _ in
guard let self = self else { return }
guard let data = data ,
let arr = ( try ? JSONSerialization . jsonObject ( with : data ) ) as ? [ [ String : Any ] ] else {
self . setStatus ( " Couldn’ t load your chats. \n \n Open Biz Connect once, then try sharing again. " )
return
}
let parsed : [ ShareChat ] = arr . compactMap { row in
guard let kind = row [ " kind " ] as ? String , let id = idString ( row [ " id " ] ) , let name = row [ " name " ] as ? String else { return nil }
return ShareChat ( kind : kind , id : id , name : name )
}
DispatchQueue . main . async {
self . chats = parsed
self . filtered = parsed
self . setStatus ( parsed . isEmpty ? " No chats yet. " : nil )
self . table . reloadData ( )
}
}
}
// MARK: - T a b l e
func tableView ( _ tableView : UITableView , numberOfRowsInSection section : Int ) -> Int { filtered . count }
func tableView ( _ tableView : UITableView , cellForRowAt indexPath : IndexPath ) -> UITableViewCell {
let cell = tableView . dequeueReusableCell ( withIdentifier : " chat " , for : indexPath )
let c = filtered [ indexPath . row ]
cell . textLabel ? . text = c . name
cell . detailTextLabel ? . text = nil
cell . imageView ? . image = nil
cell . accessoryType = . disclosureIndicator
return cell
}
func tableView ( _ tableView : UITableView , didSelectRowAt indexPath : IndexPath ) {
tableView . deselectRow ( at : indexPath , animated : true )
guard ! sending else { return }
sendAll ( to : filtered [ indexPath . row ] )
}
// MARK: - S e n d
private func sendAll ( to chat : ShareChat ) {
sending = true
search . isHidden = true
table . isHidden = true
let files = items . filter { ! $0 . isText }
let texts = items . filter { $0 . isText } . map { $0 . text }
setStatus ( " Sending to \( chat . name ) … " , busy : true )
DispatchQueue . global ( qos : . userInitiated ) . async {
var ok = true
for ( i , f ) in files . enumerated ( ) {
if files . count > 1 { self . setStatus ( " Sending \( i + 1 ) of \( files . count ) to \( chat . name ) … " , busy : true ) }
guard let data = try ? Data ( contentsOf : f . url ) , let attId = self . uploadSync ( name : f . name , mime : f . mime , data : data ) else { ok = false ; break }
let body = self . messageBody ( chat : chat , attachmentId : attId , text : nil )
if ! self . sendMessageSync ( body ) { ok = false ; break }
}
if ok && ! texts . isEmpty {
let body = self . messageBody ( chat : chat , attachmentId : nil , text : texts . joined ( separator : " \n " ) )
if ! self . sendMessageSync ( body ) { ok = false }
}
DispatchQueue . main . async {
if ok {
self . clearManifest ( ) // s e n t f r o m h e r e → d o n ' t l e t t h e a p p r e - o f f e r t h e m
self . setStatus ( " ✓ Sent to \( chat . name ) " )
DispatchQueue . main . asyncAfter ( deadline : . now ( ) + 0.8 ) { self . finish ( ) }
} else {
self . sending = false
self . setStatus ( " Couldn’ t send. Your file is saved — open Biz Connect to send it. " , busy : false )
DispatchQueue . main . asyncAfter ( deadline : . now ( ) + 2.2 ) { self . finish ( ) }
}
}
}
}
private func messageBody ( chat : ShareChat , attachmentId : String ? , text : String ? ) -> [ String : Any ] {
var b : [ String : Any ] = [ : ]
if chat . kind = = " group " { b [ " group " ] = chat . id } else { b [ " to " ] = chat . id }
if let a = attachmentId { b [ " attachmentId " ] = a }
if let t = text { b [ " body " ] = t }
return b
}
// MARK: - N e t w o r k i n g ( s y n c h r o n o u s h e l p e r s , a l w a y s c a l l e d o f f t h e m a i n t h r e a d )
private func uploadSync ( name : String , mime : String , data : Data ) -> String ? {
guard let url = URL ( string : apiBase + " /api/messages/upload " ) else { return nil }
var req = URLRequest ( url : url )
req . httpMethod = " POST "
req . setValue ( " Bearer " + token , forHTTPHeaderField : " Authorization " )
req . setValue ( mime . isEmpty ? " application/octet-stream " : mime , forHTTPHeaderField : " Content-Type " )
req . setValue ( name . addingPercentEncoding ( withAllowedCharacters : . urlQueryAllowed ) ? ? name , forHTTPHeaderField : " X-Filename " )
req . httpBody = data
req . timeoutInterval = 300
var out : String ? = nil
let sem = DispatchSemaphore ( value : 0 )
URLSession . shared . dataTask ( with : req ) { d , resp , _ in
defer { sem . signal ( ) }
guard let d = d , let http = resp as ? HTTPURLResponse , ( 200. . < 300 ) . contains ( http . statusCode ) ,
let obj = ( try ? JSONSerialization . jsonObject ( with : d ) ) as ? [ String : Any ] else { return }
out = idString ( obj [ " id " ] )
} . resume ( )
sem . wait ( )
return out
}
private func sendMessageSync ( _ body : [ String : Any ] ) -> Bool {
guard let url = URL ( string : apiBase + " /api/messages " ) ,
let json = try ? JSONSerialization . data ( withJSONObject : body ) else { return false }
var req = URLRequest ( url : url )
req . httpMethod = " POST "
req . setValue ( " Bearer " + token , forHTTPHeaderField : " Authorization " )
req . setValue ( " application/json " , forHTTPHeaderField : " Content-Type " )
req . httpBody = json
req . timeoutInterval = 60
var ok = false
let sem = DispatchSemaphore ( value : 0 )
URLSession . shared . dataTask ( with : req ) { _ , resp , _ in
if let http = resp as ? HTTPURLResponse , ( 200. . < 300 ) . contains ( http . statusCode ) { ok = true }
sem . signal ( )
} . resume ( )
sem . wait ( )
return ok
}
// G E T / g e n e r i c a s y n c h e l p e r f o r l o a d i n g t h e c h a t l i s t .
private func api ( _ method : String , _ path : String , body : Data ? , completion : @ escaping ( Data ? , HTTPURLResponse ? ) -> Void ) {
guard let url = URL ( string : apiBase + path ) else { return completion ( nil , nil ) }
var req = URLRequest ( url : url )
req . httpMethod = method
req . setValue ( " Bearer " + token , forHTTPHeaderField : " Authorization " )
if let body = body { req . httpBody = body ; req . setValue ( " application/json " , forHTTPHeaderField : " Content-Type " ) }
req . timeoutInterval = 30
URLSession . shared . dataTask ( with : req ) { d , resp , _ in completion ( d , resp as ? HTTPURLResponse ) } . resume ( )
}
}
// MARK: - A p p G r o u p s t o r a g e
// MARK: - A p p G r o u p s t o r a g e
private func sharedDirectory ( ) -> URL ? {
private func sharedDir ( ) -> URL ? {
guard let base = FileManager . default . containerURL ( forSecurityApplicationGroupIdentifier : appGroup ) else { return nil }
guard let base = FileManager . default . containerURL ( forSecurityApplicationGroupIdentifier : appGroup ) else { return nil }
let dir = base . appendingPathComponent ( " Shared " , isDirectory : true )
let dir = base . appendingPathComponent ( " Shared " , isDirectory : true )
try ? FileManager . default . createDirectory ( at : dir , withIntermediateDirectories : true )
try ? FileManager . default . createDirectory ( at : dir , withIntermediateDirectories : true )
@@ -210,49 +365,58 @@ class ShareViewController: UIViewController {
return candidate
return candidate
}
}
// / A P P E N D , n e v e r o v e r w r i t e : t h e u s e r c a n s h a r e t w i c e b e f o r e o p e n i n g t h e a p p , a n d t h e s e c o n d s h a r e
// F a l l b a c k m a n i f e s t s o t h e a p p c a n s t i l l p i c k t h e s e u p i f s e n d i n g f r o m h e r e f a i l s .
// / m u s t n o t d i s c a r d t h e f i r s t .
private func writeManifest ( _ its : [ ShareItem ] ) {
private func appendToManifest ( _ records : [ [ String : Any ] ] ) {
guard let dir = sharedDir ( ) else { return }
guard let dir = sharedDirectory ( ) else { return }
let records : [ [ String : Any ] ] = its . map { it in
let manifest = dir . appendingPathComponent ( " manifest.json " )
it . isText ? [ " kind " : " text " , " text " : it . text ]
var all : [ [ String : Any ] ] = [ ]
: [ " kind " : " file " , " name " : it . name , " path " : it . url . path , " mime " : it . mime ]
if let data = try ? Data ( contentsOf : manifest ) ,
let existing = try ? JSONSerialization . jsonObject ( with : data ) as ? [ [ String : Any ] ] {
all = existing
}
}
all . append ( contentsOf : records )
if let data = try ? JSONSerialization . data ( withJSONObject : records ) {
if let data = try ? JSONSerialization . data ( withJSONObject : all ) {
try ? data . write ( to : dir . appendingPathComponent ( " manifest.json " ) , options : . atomic )
try ? data . write ( to : manifest , options : . atomic )
}
}
}
}
// MARK: - H a n d - o f f
private func clearManifest ( ) {
guard let dir = sharedDir ( ) else { return }
// / S h a r e e x t e n s i o n s h a v e n o p u b l i c A P I t o l a u n c h t h e i r c o n t a i n e r a p p , s o w a l k t h e r e s p o n d e r c h a i n t o
let fm = FileManager . default
// / U I A p p l i c a t i o n a n d u s e o p e n U R L : . I f i t f a i l s t h e f i l e s s t a y s t a g e d a n d t h e a p p c o l l e c t s t h e m o n n e x t
if let entries = try ? fm . contentsOfDirectory ( at : dir , includingPropertiesForKeys : nil ) {
// / l a u n c h , s o t h i s i s a n o p t i m i s a t i o n , n e v e r a r e q u i r e m e n t .
for e in entries { try ? fm . removeItem ( at : e ) }
private func openHostApp ( ) {
guard let url = URL ( string : " \( urlScheme ) ://share " ) else { return }
let selector = NSSelectorFromString ( " openURL: " )
var responder : UIResponder ? = self
while let current = responder {
if current . responds ( to : selector ) && current is UIApplication {
_ = current . perform ( selector , with : url )
return
}
responder = current . next
}
}
}
}
private static func mimeType ( for url : URL ) -> String {
if let t = UTType ( filenameExtension : url . pathExtension . lowercased ( ) ) , let m = t . preferredMIMEType { return m }
return " application/octet-stream "
}
// MARK: - F i n i s h
@objc private func cancelTapped ( ) {
extensionContext ? . cancelRequest ( withError : NSError ( domain : " share " , code : 0 ) )
}
private func finish ( ) {
private func finish ( ) {
extensionContext ? . completeRequest ( returningItems : nil , completionHandler : nil )
extensionContext ? . completeRequest ( returningItems : nil , completionHandler : nil )
}
}
private static func mimeType ( for url : URL ) -> String {
private func finishFail ( _ msg : String ) {
if let type = UTType ( filenameExtension : url . pathExtension . lowercased ( ) ) ,
setStatus ( msg )
let mime = type . preferredMIMEType {
DispatchQueue . main . asyncAfter ( deadline : . now ( ) + 1.8 ) { self . finish ( ) }
return mime
}
}
}
return " application/octet-stream "
// A J S O N i d m a y d e c o d e a s S t r i n g o r N S N u m b e r d e p e n d i n g o n t h e v a l u e — n o r m a l i s e t o S t r i n g .
private func idString ( _ v : Any ? ) -> String ? {
if let s = v as ? String { return s }
if let n = v as ? NSNumber { return n . stringValue }
return nil
}
extension ShareViewController : UISearchBarDelegate {
func searchBar ( _ searchBar : UISearchBar , textDidChange searchText : String ) {
let q = searchText . trimmingCharacters ( in : . whitespaces ) . lowercased ( )
filtered = q . isEmpty ? chats : chats . filter { $0 . name . lowercased ( ) . contains ( q ) }
table . reloadData ( )
}
}
}
}