├── .gitignore ├── AddaMeIOS.xcodeproj ├── project.pbxproj └── xcshareddata │ └── xcschemes │ ├── AddaMeIOS.xcscheme │ └── AddaMeIOSUITests.xcscheme ├── AddaMeIOS ├── API │ ├── ChatAPI │ │ ├── ConversationAPI.swift │ │ └── MessageAPI.swift │ ├── ContactAPI │ │ └── ContactAPI.swift │ ├── EventAPI │ │ ├── EventAPI.swift │ │ └── EventPlaceAPI.swift │ └── UserAPI │ │ ├── AttachmentAPI.swift │ │ ├── AuthAPI.swift │ │ ├── DeviceAPI.swift │ │ ├── RefreshTokenAPI.swift │ │ └── UserAPI.swift ├── AddaMeIOS.entitlements ├── App │ ├── AppDelegate+PKPushRegistryDelegate.swift │ ├── AppDelegate.swift │ ├── AppState.swift │ ├── AppTabView.swift │ ├── Persistence.swift │ └── SceneDelegate.swift ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ ├── Contents.json │ │ ├── Icon-App-20x20@1x.png │ │ ├── Icon-App-20x20@2x.png │ │ ├── Icon-App-20x20@3x.png │ │ ├── Icon-App-29x29@1x.png │ │ ├── Icon-App-29x29@2x.png │ │ ├── Icon-App-29x29@3x.png │ │ ├── Icon-App-40x40@1x.png │ │ ├── Icon-App-40x40@2x.png │ │ ├── Icon-App-40x40@3x.png │ │ ├── Icon-App-60x60@2x.png │ │ ├── Icon-App-60x60@3x.png │ │ ├── Icon-App-76x76@1x.png │ │ ├── Icon-App-76x76@2x.png │ │ ├── Icon-App-83.5x83.5@2x.png │ │ └── ItunesArtwork@2x.png │ ├── Avatar.imageset │ │ ├── Avatar.png │ │ ├── Avatar@2x.png │ │ ├── Avatar@3x.png │ │ └── Contents.json │ ├── Color.colorset │ │ └── Contents.json │ ├── Contents.json │ ├── bg.colorset │ │ └── Contents.json │ └── iTunesArtwork.imageset │ │ ├── Contents.json │ │ ├── iTunesArtwork@1x.png │ │ ├── iTunesArtwork@2x.png │ │ └── iTunesArtwork@3x.png ├── Auth │ ├── AuthProfileView.swift │ ├── AuthView.swift │ ├── AuthViewModel.swift │ ├── HUDProgressView.swift │ ├── LoginAndVerificationResponse.swift │ └── RootView.swift ├── Base.lproj │ └── LaunchScreen.storyboard ├── Chat │ ├── Authenticator.swift │ ├── ChatBottomView.swift │ ├── ChatOutGoingEvent.swift │ ├── ChatRoomView.swift │ ├── ChatRow.swift │ └── WebSocketViewModel.swift ├── Configs │ └── Development.xcconfig ├── Conversation │ ├── ConversationList.swift │ ├── ConversationRow.swift │ └── ConversationViewModel.swift ├── CoreDataEntities │ ├── ContactEntity+CoreDataClass.swift │ ├── ContactEntity+CoreDataProperties.swift │ └── ContactEntityExtension.swift ├── CoreDataModels │ ├── AddaModel.xcdatamodeld │ │ └── AddaModel.xcdatamodel │ │ │ └── contents │ └── Contacts.xcdatamodeld │ │ └── Contacts.xcdatamodel │ │ └── contents ├── Environment.swift ├── Event │ ├── ChatDataHandler.swift │ ├── EventDetail.swift │ ├── EventForm.swift │ ├── EventList.swift │ ├── EventRow.swift │ └── EventViewModel.swift ├── ExprimentViews │ ├── Navigation_WithList.swift │ ├── TabBarTopView.swift │ └── VarticalExample.swift ├── Extension │ ├── Array+RemoveDuplicate.swift │ ├── AssetExtractor.swift │ ├── Codable+Extension.swift │ ├── DataExtension.swift │ ├── Date │ │ ├── Date+Extension.swift │ │ └── DateFormatter+Extension.swift │ ├── Image+Compress.swift │ ├── Publishers+Keyboard.swift │ ├── RangeReplaceableCollection+AppendIfNotContains.swift │ ├── String+Extension.swift │ ├── String │ │ ├── String+CodingKey.swift │ │ ├── String+Data.swift │ │ ├── String+Date.swift │ │ └── String+ReplaceCharcters.swift │ ├── UIApplicationKeyboardOnDragGestureExtension.swift │ ├── UIImageView+Extension.swift │ └── View │ │ └── View+Extension+HideRowSeparatorModifier.swift ├── Helper │ ├── AppUserDefaults.swift │ ├── AwsS3Manager.swift │ ├── Bundle+Decode.swift │ ├── CoreDataHelpers │ │ ├── ManagedModel.swift │ │ └── NSManagedObjectContext+Extensions.swift │ ├── DateHelper.swift │ ├── Image │ │ ├── AsyncImage.swift │ │ ├── EnvironmentValues+ImageCache.swift │ │ ├── ImageCache.swift │ │ ├── ImageDraw.swift │ │ ├── ImageLoader.swift │ │ └── ImagePicker.swift │ ├── Keyboard │ │ ├── AdaptsToSoftwareKeyboard.swift │ │ ├── KeyboardAdaptive.swift │ │ └── UIResponder+Current.swift │ ├── KeychainService.swift │ ├── ModalView.swift │ ├── MongoDBObjectID.swift │ ├── SwiftUIFormHelper │ │ ├── KeyboardDismissModifier.swift │ │ └── KeyboardOffsetModifier.swift │ └── View │ │ └── LazyView.swift ├── Info.plist ├── Map │ ├── LocationManager.swift │ ├── LocationQuery.swift │ ├── LocationSearchService.swift │ ├── MapSearchView.swift │ ├── MapView.swift │ ├── MapViewModel.swift │ ├── MapViewUI.swift │ └── SearchBar.swift ├── Models │ ├── Attachment.swift │ ├── Chat │ │ └── ChatMessage.swift │ ├── Contact │ │ └── Contact.swift │ ├── Conversation │ │ ├── Conversation.swift │ │ └── LastMessage.swift │ ├── DemoData.swift │ ├── Event │ │ └── Event.swift │ ├── EventPlace │ │ └── EventPlace.swift │ ├── FetachDataFromJsonFile.swift │ └── User │ │ └── CurrentUser.swift ├── Preview Content │ └── Preview Assets.xcassets │ │ ├── Contents.json │ │ └── avatar.imageset │ │ ├── Contents.json │ │ └── avatar.jpg ├── Resources │ ├── chatResponseData.json │ ├── contacts.json │ ├── conversationResponseData.json │ └── eventResponseData.json ├── Services │ └── SystemServices.swift ├── Settings │ └── SettingsView.swift ├── Shape+Extension.swift └── User │ ├── ContactRow.swift │ ├── ContactViewModel.swift │ ├── ContactsView.swift │ ├── DeviceViewModel.swift │ ├── ProfileView.swift │ ├── TermsAndPrivacyWebView.swift │ ├── TermsWebView.swift │ └── UserViewModel.swift ├── AddaMeIOSUITests ├── AddaMeIOSUITests.swift └── Info.plist ├── Gemfile ├── Gemfile.lock ├── PhoneNumberMetadata.json ├── README.md └── fastlane ├── Appfile ├── Deliverfile ├── Fastfile ├── Precheckfile ├── README.md ├── Snapfile ├── SnapshotHelper.swift ├── metadata ├── copyright.txt ├── en-US │ ├── apple_tv_privacy_policy.txt │ ├── description.txt │ ├── keywords.txt │ ├── marketing_url.txt │ ├── name.txt │ ├── privacy_url.txt │ ├── promotional_text.txt │ ├── release_notes.txt │ ├── subtitle.txt │ └── support_url.txt ├── primary_category.txt ├── primary_first_sub_category.txt ├── primary_second_sub_category.txt ├── review_information │ ├── demo_password.txt │ ├── demo_user.txt │ ├── email_address.txt │ ├── first_name.txt │ ├── last_name.txt │ ├── notes.txt │ └── phone_number.txt ├── secondary_category.txt ├── secondary_first_sub_category.txt └── secondary_second_sub_category.txt └── screenshots ├── Framefile.json ├── README.txt ├── background.jpg ├── en-US └── title.strings ├── fonts └── Chalkduster.ttf └── screenshots.html /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.toptal.com/developers/gitignore/api/swift,xcode,cocoapods 2 | # Edit at https://www.toptal.com/developers/gitignore?templates=swift,xcode,cocoapods 3 | 4 | ### CocoaPods ### 5 | ## CocoaPods GitIgnore Template 6 | 7 | # CocoaPods - Only use to conserve bandwidth / Save time on Pushing 8 | # - Also handy if you have a large number of dependant pods 9 | # - AS PER https://guides.cocoapods.org/using/using-cocoapods.html NEVER IGNORE THE LOCK FILE 10 | Pods/ 11 | 12 | ### Swift ### 13 | # Xcode 14 | # 15 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 16 | 17 | ## User settings 18 | xcuserdata/ 19 | 20 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 21 | *.xcscmblueprint 22 | *.xccheckout 23 | 24 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 25 | build/ 26 | DerivedData/ 27 | *.moved-aside 28 | *.pbxuser 29 | !default.pbxuser 30 | *.mode1v3 31 | !default.mode1v3 32 | *.mode2v3 33 | !default.mode2v3 34 | *.perspectivev3 35 | !default.perspectivev3 36 | 37 | ## Obj-C/Swift specific 38 | *.hmap 39 | 40 | ## App packaging 41 | *.ipa 42 | *.dSYM.zip 43 | *.dSYM 44 | 45 | ## Playgrounds 46 | timeline.xctimeline 47 | playground.xcworkspace 48 | 49 | # Swift Package Manager 50 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 51 | # Packages/ 52 | # Package.pins 53 | # Package.resolved 54 | # *.xcodeproj 55 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 56 | # hence it is not needed unless you have added a package configuration file to your project 57 | # .swiftpm 58 | 59 | .build/ 60 | 61 | # CocoaPods 62 | # We recommend against adding the Pods directory to your .gitignore. However 63 | # you should judge for yourself, the pros and cons are mentioned at: 64 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 65 | # Pods/ 66 | # Add this line if you want to avoid checking in source code from the Xcode workspace 67 | # *.xcworkspace 68 | 69 | # Carthage 70 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 71 | # Carthage/Checkouts 72 | 73 | Carthage/Build/ 74 | 75 | # Accio dependency management 76 | Dependencies/ 77 | .accio/ 78 | 79 | # fastlane 80 | # It is recommended to not store the screenshots in the git repo. 81 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 82 | # For more information about the recommended setup visit: 83 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 84 | 85 | fastlane/report.xml 86 | fastlane/Preview.html 87 | fastlane/screenshots/**/*.png 88 | fastlane/test_output 89 | 90 | # Code Injection 91 | # After new code Injection tools there's a generated folder /iOSInjectionProject 92 | # https://github.com/johnno1962/injectionforxcode 93 | 94 | iOSInjectionProject/ 95 | 96 | ### Xcode ### 97 | # Xcode 98 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 99 | 100 | 101 | ## Gcc Patch 102 | /*.gcno 103 | 104 | ### Xcode Patch ### 105 | *.xcodeproj/* 106 | !*.xcodeproj/project.pbxproj 107 | !*.xcodeproj/xcshareddata/ 108 | !*.xcworkspace/contents.xcworkspacedata 109 | **/xcshareddata/WorkspaceSettings.xcsettings 110 | AddaMeIOS/Views/Navigation_WithList.swift 111 | AddaMeIOS/Views/VarticalExample.swift 112 | AddaMeIOS/Configs/Production.xcconfig 113 | vendor 114 | .bundle/config 115 | AddaMeIOS/ExprimentViews 116 | -------------------------------------------------------------------------------- /AddaMeIOS.xcodeproj/xcshareddata/xcschemes/AddaMeIOS.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 38 | 39 | 44 | 45 | 46 | 47 | 57 | 59 | 65 | 66 | 67 | 68 | 72 | 73 | 74 | 75 | 81 | 83 | 89 | 90 | 91 | 92 | 94 | 95 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /AddaMeIOS.xcodeproj/xcshareddata/xcschemes/AddaMeIOSUITests.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 42 | 43 | 53 | 54 | 60 | 61 | 67 | 68 | 69 | 70 | 72 | 73 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /AddaMeIOS/API/ChatAPI/ConversationAPI.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ConversationAPI.swift 3 | // AddaMeIOS 4 | // 5 | // Created by Saroar Khandoker on 14.10.2020. 6 | // 7 | 8 | import Foundation 9 | import Pyramid 10 | import Combine 11 | 12 | struct AddUser: Codable { 13 | let conversationsId: String 14 | let usersId: String 15 | } 16 | 17 | enum ConversationAPI { 18 | case list(_ query: QueryItem) 19 | case addUserToConversation(_ addUser: AddUser) 20 | case create(_ createConversation: CreateConversation) 21 | case find(conversationsId: String) 22 | } 23 | 24 | extension ConversationAPI: APIConfiguration { 25 | 26 | var pathPrefix: String { 27 | return "/conversations" 28 | } 29 | 30 | var path: String { 31 | return pathPrefix + { 32 | switch self { 33 | case .create: return String.empty 34 | case .addUserToConversation(let addUser): 35 | return "/\(addUser.conversationsId)/users/\(addUser.usersId)" 36 | case .list: return String.empty 37 | case .find(let conversationsId): return "/\(conversationsId)" 38 | } 39 | }() 40 | } 41 | 42 | var baseURL: URL { EnvironmentKeys.rootURL } // URL(string: "http://10.0.1.3:6060/v1")! 43 | 44 | var method: HTTPMethod { 45 | switch self { 46 | case .create, .addUserToConversation: return .post 47 | case .list, .find: return .get 48 | } 49 | } 50 | 51 | var dataType: DataType { 52 | switch self { 53 | case .create(let createConversation): 54 | return .requestWithEncodable(encodable: AnyEncodable(createConversation)) 55 | case .addUserToConversation(let addUser): 56 | return .requestWithEncodable(encodable: AnyEncodable(addUser)) 57 | case .list(let query): 58 | return .requestParameters(parameters: [ 59 | query.page: query.pageNumber, 60 | query.per: query.perSize 61 | ]) 62 | case .find: return .requestPlain 63 | } 64 | } 65 | 66 | var authType: AuthType { 67 | return .bearer(token: 68 | Authenticator.shared.currentToken?.accessToken ?? String.empty 69 | ) 70 | } 71 | 72 | var contentType: ContentType? { 73 | switch self { 74 | case .create, .addUserToConversation, .list, .find: 75 | return .applicationJson 76 | } 77 | } 78 | 79 | var headers: [String : String]? { 80 | return nil 81 | } 82 | 83 | } 84 | -------------------------------------------------------------------------------- /AddaMeIOS/API/ChatAPI/MessageAPI.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MessageAPI.swift 3 | // AddaMeIOS 4 | // 5 | // Created by Saroar Khandoker on 02.10.2020. 6 | // 7 | 8 | import Foundation 9 | import Pyramid 10 | import Combine 11 | 12 | enum MessageAPI { 13 | case list(_ query: QueryItem, _ conversationId: String) 14 | } 15 | 16 | extension MessageAPI: APIConfiguration { 17 | 18 | var pathPrefix: String { 19 | return "/messages" 20 | } 21 | 22 | var path: String { 23 | return pathPrefix + { 24 | switch self { 25 | case .list(_, let conversationsId): 26 | return "/by/conversations/\(conversationsId)" 27 | } 28 | }() 29 | } 30 | 31 | var baseURL: URL { EnvironmentKeys.rootURL } 32 | 33 | 34 | var method: HTTPMethod { 35 | switch self { 36 | case .list: return .get 37 | } 38 | } 39 | 40 | var dataType: DataType { 41 | switch self { 42 | case .list(let query, _): 43 | return .requestParameters(parameters: [ 44 | query.page: query.pageNumber, 45 | query.per: query.perSize 46 | ]) 47 | } 48 | } 49 | 50 | var authType: AuthType { 51 | return .bearer(token: 52 | Authenticator.shared.currentToken?.accessToken ?? String.empty 53 | ) 54 | } 55 | 56 | var contentType: ContentType? { 57 | switch self { 58 | case .list: 59 | return .applicationJson 60 | } 61 | } 62 | 63 | var headers: [String : String]? { 64 | return nil 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /AddaMeIOS/API/ContactAPI/ContactAPI.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContactAPI.swift 3 | // AddaMeIOS 4 | // 5 | // Created by Saroar Khandoker on 13.11.2020. 6 | // 7 | 8 | import Foundation 9 | import Pyramid 10 | import Combine 11 | 12 | enum ContactAPI { 13 | case create(contacts: [Contact]) 14 | } 15 | 16 | extension ContactAPI: APIConfiguration { 17 | var baseURL: URL { EnvironmentKeys.rootURL }// { URL(string: "http://10.0.1.3:3030/v1")! } //{ EnvironmentKeys.rootURL } 18 | 19 | var pathPrefix: String { 20 | return "/contacts" 21 | } 22 | 23 | var path: String { 24 | return pathPrefix + { 25 | switch self { 26 | case .create: return String.empty 27 | } 28 | }() 29 | } 30 | 31 | 32 | var method: HTTPMethod { 33 | switch self { 34 | case .create: return .post 35 | } 36 | } 37 | 38 | var dataType: DataType { 39 | switch self { 40 | case .create(let contacts): 41 | return .requestWithEncodable(encodable: AnyEncodable(contacts)) 42 | } 43 | } 44 | 45 | var authType: AuthType { 46 | return .bearer(token: 47 | Authenticator.shared.currentToken?.accessToken ?? String.empty 48 | ) 49 | } 50 | 51 | var contentType: ContentType? { 52 | switch self { 53 | case .create: 54 | return .applicationJson 55 | } 56 | } 57 | 58 | var headers: [String : String]? { 59 | return nil 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /AddaMeIOS/API/EventAPI/EventAPI.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EventAPI.swift 3 | // AddaMeIOS 4 | // 5 | // Created by Saroar Khandoker on 28.08.2020. 6 | // 7 | 8 | import Foundation 9 | import Pyramid 10 | import Combine 11 | 12 | enum EventAPI { 13 | case events(_ query: EventQueryItem) 14 | case create(_ event: Event) 15 | case myEvents(_ query: QueryItem ) 16 | } 17 | 18 | //extension RequiresAuth { 19 | //// var header: [String: String] { get } 20 | //// return .bearer(token: 21 | //// Authenticator.shared.currentToken?.accessToken ?? String.empty 22 | //// ) 23 | // var header: [String: String] { 24 | // ["Authorization": "Bearer \(Authenticator.shared.currentToken?.accessToken ?? String.empty)"] 25 | // } 26 | //} 27 | 28 | ///planets?page=2&per=5 29 | struct QueryItem: Codable { 30 | var page: String 31 | var pageNumber: String 32 | var per: String 33 | var perSize: String 34 | } 35 | 36 | struct EventQueryItem: Codable { 37 | var page: String 38 | var pageNumber: String 39 | var per: String 40 | var perSize: String 41 | var lat: String 42 | var long: String 43 | var distance: String 44 | var latValue: String 45 | var longValue: String 46 | var distanceValue: String 47 | 48 | } 49 | 50 | extension EventAPI: APIConfiguration { 51 | 52 | var baseURL: URL { EnvironmentKeys.rootURL } 53 | 54 | var pathPrefix: String { 55 | return "/events" 56 | } 57 | 58 | var path: String { 59 | return pathPrefix + { 60 | switch self { 61 | case .create: return String.empty 62 | case .events: return String.empty 63 | case .myEvents: return "/my" 64 | } 65 | }() 66 | } 67 | 68 | 69 | var method: HTTPMethod { 70 | switch self { 71 | case .create: return .post 72 | case .events, .myEvents: return .get 73 | } 74 | } 75 | 76 | var dataType: DataType { 77 | switch self { 78 | case .create(let event): 79 | return .requestWithEncodable(encodable: AnyEncodable(event)) 80 | case .events(let eventQuery): 81 | return .requestParameters(parameters: [ 82 | eventQuery.page: eventQuery.pageNumber, 83 | eventQuery.per: eventQuery.perSize, 84 | eventQuery.lat: eventQuery.latValue, 85 | eventQuery.long: eventQuery.longValue, 86 | eventQuery.distance: eventQuery.distanceValue, 87 | ]) 88 | case .myEvents(let query): 89 | return .requestParameters(parameters: [ 90 | query.page: query.pageNumber, 91 | query.per: query.perSize 92 | ]) 93 | } 94 | } 95 | 96 | var authType: AuthType { 97 | return .bearer(token: 98 | Authenticator.shared.currentToken?.accessToken ?? String.empty 99 | ) 100 | } 101 | 102 | var contentType: ContentType? { 103 | switch self { 104 | case .create, .events, .myEvents: 105 | return .applicationJson 106 | } 107 | } 108 | 109 | var headers: [String : String]? { 110 | return nil 111 | } 112 | 113 | } 114 | -------------------------------------------------------------------------------- /AddaMeIOS/API/EventAPI/EventPlaceAPI.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GeoLocationAPI.swift 3 | // AddaMeIOS 4 | // 5 | // Created by Saroar Khandoker on 20.09.2020. 6 | // 7 | -------------------------------------------------------------------------------- /AddaMeIOS/API/UserAPI/AttachmentAPI.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AttachmentAPI.swift 3 | // AddaMeIOS 4 | // 5 | // Created by Saroar Khandoker on 19.11.2020. 6 | // 7 | 8 | import Foundation 9 | import Pyramid 10 | 11 | enum AttachmentAPI { 12 | case create(_ attachment: Attachment) 13 | } 14 | 15 | extension AttachmentAPI: APIConfiguration { 16 | var pathPrefix: String { 17 | return "attachments" 18 | } 19 | 20 | var path: String { 21 | return pathPrefix + { 22 | switch self { 23 | case .create: return "" 24 | } 25 | }() 26 | } 27 | 28 | var baseURL: URL { EnvironmentKeys.rootURL } 29 | 30 | var method: HTTPMethod { 31 | switch self { 32 | case .create: return .post 33 | } 34 | } 35 | 36 | var dataType: DataType { 37 | switch self { 38 | case .create(let attachment): 39 | return .requestWithEncodable(encodable: AnyEncodable(attachment)) 40 | } 41 | } 42 | 43 | var authType: AuthType { 44 | return .bearer(token: 45 | Authenticator.shared.currentToken?.accessToken ?? String.empty 46 | ) 47 | } 48 | 49 | var contentType: ContentType? { 50 | switch self { 51 | case .create: 52 | return .applicationJson 53 | } 54 | } 55 | 56 | var headers: [String : String]? { 57 | return nil 58 | } 59 | 60 | } 61 | 62 | -------------------------------------------------------------------------------- /AddaMeIOS/API/UserAPI/AuthAPI.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UesrAPI.swift 3 | // AddaMeIOS 4 | // 5 | // Created by Saroar Khandoker on 20.08.2020. 6 | // 7 | 8 | import Foundation 9 | import Pyramid 10 | 11 | enum AuthAPI { 12 | case login(login: LoginAndVerificationResponse) 13 | case verification(verificationResponse: LoginAndVerificationResponse) 14 | } 15 | 16 | extension AuthAPI: APIConfiguration { 17 | var pathPrefix: String { 18 | return "auth/" 19 | } 20 | 21 | var path: String { 22 | return pathPrefix + { 23 | switch self { 24 | case .login: return "login" 25 | case .verification: return "verify_sms" 26 | } 27 | }() 28 | } 29 | 30 | var baseURL: URL { EnvironmentKeys.rootURL } 31 | 32 | var method: HTTPMethod { 33 | switch self { 34 | case .login, .verification: return .post 35 | } 36 | } 37 | 38 | var dataType: DataType { 39 | switch self { 40 | case .login(let loginAndVerificationResponse): 41 | return .requestWithEncodable(encodable: AnyEncodable(loginAndVerificationResponse)) 42 | case .verification(let verificationResponse): 43 | return .requestWithEncodable(encodable: AnyEncodable(verificationResponse)) 44 | } 45 | } 46 | 47 | var authType: AuthType { 48 | return .none 49 | } 50 | 51 | var contentType: ContentType? { 52 | switch self { 53 | case .login, .verification: 54 | return .applicationJson 55 | } 56 | } 57 | 58 | var headers: [String : String]? { 59 | return nil 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /AddaMeIOS/API/UserAPI/DeviceAPI.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DeviceAPI.swift 3 | // AddaMeIOS 4 | // 5 | // Created by Saroar Khandoker on 29.11.2020. 6 | // 7 | 8 | import Foundation 9 | import Pyramid 10 | import Combine 11 | 12 | enum DeviceAPI { 13 | case createOrUpdate(_ device: Device) 14 | } 15 | 16 | extension DeviceAPI: APIConfiguration { 17 | var pathPrefix: String { 18 | return "/devices" 19 | } 20 | 21 | var path: String { 22 | return pathPrefix + { 23 | switch self { 24 | case .createOrUpdate: 25 | return "" 26 | } 27 | }() 28 | } 29 | 30 | var baseURL: URL { EnvironmentKeys.rootURL } 31 | 32 | 33 | var method: HTTPMethod { 34 | switch self { 35 | case .createOrUpdate: return .post 36 | } 37 | } 38 | 39 | var dataType: DataType { 40 | switch self { 41 | case .createOrUpdate(let device): 42 | return .requestWithEncodable(encodable: AnyEncodable(device)) 43 | } 44 | } 45 | 46 | var authType: AuthType { 47 | return .bearer(token: 48 | Authenticator.shared.currentToken?.accessToken ?? String.empty 49 | ) 50 | } 51 | 52 | var contentType: ContentType? { 53 | switch self { 54 | case .createOrUpdate: 55 | return .applicationJson 56 | } 57 | } 58 | 59 | var headers: [String : String]? { 60 | return nil 61 | } 62 | } 63 | 64 | struct Device: Codable { 65 | var id: String? 66 | var ownerId: String 67 | var name: String 68 | var model: String? 69 | var osVersion: String? 70 | var token: String 71 | var voipToken: String 72 | var createAt, updatedAt: String? 73 | } 74 | -------------------------------------------------------------------------------- /AddaMeIOS/API/UserAPI/RefreshTokenAPI.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RefreshTokenAPI.swift 3 | // AddaMeIOS 4 | // 5 | // Created by Saroar Khandoker on 28.08.2020. 6 | // 7 | 8 | import Foundation 9 | import Pyramid 10 | 11 | struct RefreshTokenResponse: Codable { 12 | var accessToken: String 13 | var refreshToken: String 14 | } 15 | 16 | struct RefreshTokenInput: Codable { 17 | var refreshToken: String 18 | } 19 | 20 | enum RefreshTokenAPI { 21 | case refresh(token: RefreshTokenInput) 22 | } 23 | 24 | extension RefreshTokenAPI: APIConfiguration { 25 | var path: String { 26 | return pathPrefix + { 27 | switch self { 28 | case .refresh: 29 | return "/refreshToken" 30 | } 31 | }() 32 | } 33 | 34 | var baseURL: URL { EnvironmentKeys.rootURL } 35 | 36 | var method: HTTPMethod { 37 | switch self { 38 | case .refresh: return .post 39 | } 40 | } 41 | 42 | var dataType: DataType { 43 | switch self { 44 | case .refresh(let rToken): 45 | return .requestWithEncodable(encodable: AnyEncodable(rToken)) 46 | } 47 | } 48 | 49 | var authType: AuthType { 50 | return .bearer(token: 51 | Authenticator.shared.currentToken?.accessToken ?? String.empty 52 | ) 53 | } 54 | 55 | var pathPrefix: String { 56 | return "auth/" 57 | } 58 | 59 | var contentType: ContentType? { 60 | switch self { 61 | case .refresh: 62 | return .applicationJson 63 | } 64 | } 65 | 66 | var headers: [String : String]? { 67 | return nil 68 | } 69 | 70 | } 71 | -------------------------------------------------------------------------------- /AddaMeIOS/API/UserAPI/UserAPI.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserAPI.swift 3 | // AddaMeIOS 4 | // 5 | // Created by Saroar Khandoker on 17.10.2020. 6 | // 7 | 8 | import Foundation 9 | import Pyramid 10 | 11 | enum UserAPI { 12 | case me(_ usersId: String) 13 | case update(_ user: CurrentUser) 14 | } 15 | 16 | extension UserAPI: APIConfiguration { 17 | var pathPrefix: String { 18 | return "users" 19 | } 20 | 21 | var path: String { 22 | return pathPrefix + { 23 | switch self { 24 | case .me(let usersId): return "/\(usersId)" 25 | case .update: return "" 26 | } 27 | }() 28 | } 29 | 30 | var baseURL: URL { EnvironmentKeys.rootURL } 31 | 32 | var method: HTTPMethod { 33 | switch self { 34 | case .me: return .get 35 | case .update: return .put 36 | } 37 | } 38 | 39 | var dataType: DataType { 40 | switch self { 41 | case .me: 42 | return .requestPlain 43 | case .update(let user): 44 | return .requestWithEncodable(encodable: AnyEncodable(user)) 45 | } 46 | } 47 | 48 | var authType: AuthType { 49 | return .bearer(token: 50 | Authenticator.shared.currentToken?.accessToken ?? String.empty 51 | ) 52 | } 53 | 54 | var contentType: ContentType? { 55 | switch self { 56 | case .me, .update: 57 | return .applicationJson 58 | } 59 | } 60 | 61 | var headers: [String : String]? { 62 | return nil 63 | } 64 | 65 | } 66 | 67 | -------------------------------------------------------------------------------- /AddaMeIOS/AddaMeIOS.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | aps-environment 6 | development 7 | 8 | 9 | -------------------------------------------------------------------------------- /AddaMeIOS/App/AppDelegate+PKPushRegistryDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate+PKPushRegistryDelegate.swift 3 | // AddaMeIOS 4 | // 5 | // Created by Saroar Khandoker on 29.11.2020. 6 | // 7 | 8 | import PushKit 9 | import UIKit 10 | import CallKit 11 | 12 | // MARK: PKPushRegistryDelegate 13 | extension AppDelegate: PKPushRegistryDelegate { 14 | 15 | func pushRegistry(_ registry: PKPushRegistry, didUpdate credentials: PKPushCredentials, for type: PKPushType) { 16 | 17 | print(#line, "voip token saved") 18 | let voipToken = credentials.token.toHexString() 19 | KeychainService.save(string: voipToken, for: .voipToken) 20 | } 21 | 22 | func pushRegistry(_ registry: PKPushRegistry, didInvalidatePushTokenFor type: PKPushType) { 23 | print(#line, "invalid \(self)") 24 | } 25 | 26 | func pushRegistry( 27 | _ registry: PKPushRegistry, 28 | didReceiveIncomingPushWith payload: PKPushPayload, 29 | for type: PKPushType, completion: @escaping () -> Void) { 30 | 31 | 32 | if type == .voIP { 33 | print(#line, "lets fire PKPushPayload voIP") 34 | if let handle = payload.dictionaryPayload["aps"] { 35 | print(#line, handle) 36 | } 37 | } 38 | 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /AddaMeIOS/App/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // AddaMeIOS 4 | // 5 | // Created by Alif on 11/7/20. 6 | // 7 | 8 | import UIKit 9 | import UserNotifications 10 | import PushKit 11 | 12 | @UIApplicationMain 13 | class AppDelegate: UIResponder, UIApplicationDelegate { 14 | 15 | let voipRegistry = PKPushRegistry(queue: DispatchQueue.main) 16 | 17 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 18 | // Override point for customization after application launch. 19 | 20 | registerForPushNotifications() 21 | 22 | return true 23 | } 24 | 25 | // MARK: UISceneSession Lifecycle 26 | 27 | func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { 28 | // Called when a new scene session is being created. 29 | // Use this method to select a configuration to create the new scene with. 30 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) 31 | } 32 | 33 | func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { 34 | // Called when the user discards a scene session. 35 | // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. 36 | // Use this method to release any resources that were specific to the discarded scenes, as they will not return. 37 | } 38 | 39 | func application( 40 | _ application: UIApplication, 41 | didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data 42 | ) { 43 | let token = deviceToken.toHexString() 44 | KeychainService.save(string: token, for: .deviceToken) 45 | print("Device Token: \(token)") 46 | } 47 | 48 | func application( 49 | _ application: UIApplication, 50 | didFailToRegisterForRemoteNotificationsWithError error: Error 51 | ) { 52 | print("Failed to register: \(error)") 53 | } 54 | 55 | func registerForPushNotifications() { 56 | 57 | UNUserNotificationCenter.current() 58 | .requestAuthorization( 59 | options: [.alert, .sound, .badge]) { [weak self] granted, _ in 60 | print(#line, "Permission granted: \(granted)") 61 | guard granted else { return } 62 | self?.getNotificationSettings() 63 | } 64 | 65 | registerForPushNVoip() 66 | } 67 | 68 | func getNotificationSettings() { 69 | UNUserNotificationCenter.current().getNotificationSettings { settings in 70 | print(#line, "Notification settings: \(settings)") 71 | guard settings.authorizationStatus == .authorized else { return } 72 | DispatchQueue.main.async { 73 | UIApplication.shared.registerForRemoteNotifications() 74 | } 75 | } 76 | } 77 | 78 | private func registerForPushNVoip() { 79 | //register for voip notifications 80 | voipRegistry.delegate = self 81 | voipRegistry.desiredPushTypes = [.voIP] 82 | UIApplication.shared.registerForRemoteNotifications() 83 | } 84 | 85 | } 86 | 87 | -------------------------------------------------------------------------------- /AddaMeIOS/App/AppState.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppState.swift 3 | // AddaMeIOS 4 | // 5 | // Created by Saroar Khandoker on 06.11.2020. 6 | // 7 | 8 | import Combine 9 | 10 | class AppState: ObservableObject { 11 | @Published var currentTab = AppTabs.event 12 | @Published var tabBarIsHidden: Bool = false 13 | } 14 | 15 | enum AppTabs: Int { 16 | case event, chat, profile 17 | } 18 | -------------------------------------------------------------------------------- /AddaMeIOS/App/AppTabView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TabView.swift 3 | // AddaMeIOS 4 | // 5 | // Created by Saroar Khandoker on 26.08.2020. 6 | // 7 | 8 | import SwiftUI 9 | import Combine 10 | 11 | struct AppTabView: View { 12 | @State var index = 0 13 | @State var expand = true 14 | @State var searchExpand = true 15 | 16 | @EnvironmentObject var appState: AppState 17 | // @EnvironmentObject var conversationView: ConversationViewModel 18 | @Environment(\.colorScheme) var colorScheme 19 | 20 | // var newConversationCountText: String { 21 | // if conversationView.conversations.isEmpty { 22 | // return "Chat" 23 | // } else { 24 | // return "Chat - \(conversationView.conversations.count)" 25 | // } 26 | // } 27 | 28 | @ViewBuilder var body: some View { 29 | 30 | VStack(alignment: .center) { 31 | ZStack { 32 | if index == 0 { 33 | EventList() 34 | .environmentObject(appState) 35 | } else if index == 1 { 36 | ConversationList() 37 | .environmentObject(appState) 38 | } else if index == 2 { 39 | ProfileView() 40 | .environmentObject(appState) 41 | } 42 | } 43 | 44 | Spacer() 45 | if appState.tabBarIsHidden == false { 46 | CustomTabs(index: self.$index, expand: self.$expand) 47 | } 48 | } 49 | .background(Color(.systemBackground)) 50 | .edgesIgnoringSafeArea(.all) 51 | } 52 | } 53 | 54 | struct AppTabView_Previews: PreviewProvider { 55 | static var previews: some View { 56 | AppTabView() 57 | // .environmentObject(ConversationViewModel()) 58 | } 59 | } 60 | 61 | struct CustomTabs: View { 62 | @Binding var index: Int 63 | @Binding var expand: Bool 64 | 65 | @Environment(\.colorScheme) var colorScheme 66 | 67 | var body: some View { 68 | HStack { 69 | 70 | Button(action: { 71 | self.index = 0 72 | self.expand = true 73 | }) { 74 | VStack { 75 | Image(systemName: "captions.bubble") 76 | Text("Events") 77 | } 78 | } 79 | .foregroundColor(Color("bg").opacity(self.index == 0 ? 1 : 0.6)) 80 | Spacer() 81 | 82 | Button(action: { 83 | self.expand = false 84 | self.index = 1 85 | }) { 86 | VStack { 87 | Image(systemName: "bubble.left.and.bubble.right.fill") 88 | Text("Chat") 89 | } 90 | } 91 | .foregroundColor(Color("bg").opacity(self.index == 1 ? 1 : 0.6)) 92 | 93 | Spacer() 94 | 95 | Button(action: { 96 | self.expand = false 97 | self.index = 2 98 | }) { 99 | VStack { 100 | Image(systemName: "person.fill") 101 | Text("Profile") 102 | } 103 | } 104 | .foregroundColor(Color("bg").opacity(self.index == 2 ? 1 : 0.6)) 105 | 106 | } 107 | .padding([.bottom, .trailing, .leading], 40) 108 | .background(Color(.systemBackground)) 109 | } 110 | } 111 | 112 | struct CustomTabs_Previews: PreviewProvider { 113 | static var previews: some View { 114 | CustomTabs(index: .constant(1), expand: .constant(true)) 115 | // .environmentObject(ConversationViewModel()) 116 | } 117 | } 118 | 119 | -------------------------------------------------------------------------------- /AddaMeIOS/App/Persistence.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Persistence.swift 3 | // AddaMeIOS 4 | // 5 | // Created by Saroar Khandoker on 01.12.2020. 6 | // 7 | 8 | import CoreData 9 | 10 | struct PersistenceController { 11 | static let shared = PersistenceController() 12 | 13 | public var moc: NSManagedObjectContext { 14 | return container.viewContext 15 | } 16 | 17 | static public var preview: PersistenceController = { 18 | let result = PersistenceController(inMemory: false) 19 | let viewContext = result.container.viewContext 20 | 21 | if viewContext.hasChanges { 22 | do { 23 | try viewContext.save() 24 | } catch { 25 | // The context couldn't be saved. 26 | // You should add your own error handling here. 27 | let nserror = error as NSError 28 | fatalError("Unresolved error \(nserror), \(nserror.userInfo)") 29 | } 30 | } 31 | 32 | return result 33 | 34 | }() 35 | 36 | let container: NSPersistentContainer 37 | 38 | init(inMemory: Bool = false) { 39 | container = NSPersistentContainer(name: "AddaModel") 40 | 41 | if inMemory { 42 | container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null") 43 | } 44 | 45 | container.loadPersistentStores(completionHandler: { (storeDescription, error) in 46 | if let error = error as NSError? { 47 | fatalError("Unresolved error \(error), \(error.userInfo)") 48 | } 49 | }) 50 | 51 | container.viewContext.automaticallyMergesChangesFromParent = true 52 | container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy 53 | } 54 | 55 | // MARK: - Core Data Saving support 56 | public func saveContext () { 57 | let context = container.viewContext 58 | if context.hasChanges { 59 | do { 60 | try context.save() 61 | } catch { 62 | let nserror = error as NSError 63 | fatalError("Unresolved error \(nserror), \(nserror.userInfo)") 64 | } 65 | } 66 | } 67 | 68 | } 69 | -------------------------------------------------------------------------------- /AddaMeIOS/App/SceneDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SceneDelegate.swift 3 | // AddaMeIOS 4 | // 5 | // Created by Alif on 11/7/20. 6 | // 7 | 8 | import UIKit 9 | import SwiftUI 10 | import Combine 11 | 12 | class SceneDelegate: UIResponder, UIWindowSceneDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { 17 | 18 | let conentView = RootView() 19 | .modifier(SystemServices()) 20 | 21 | if let windowScene = scene as? UIWindowScene { 22 | let window = UIWindow(windowScene: windowScene) 23 | 24 | window.rootViewController = UIHostingController(rootView: conentView) 25 | self.window = window 26 | window.makeKeyAndVisible() 27 | } 28 | } 29 | 30 | func sceneDidDisconnect(_ scene: UIScene) { 31 | // Called as the scene is being released by the system. 32 | // This occurs shortly after the scene enters the background, or when its session is discarded. 33 | // Release any resources associated with this scene that can be re-created the next time the scene connects. 34 | // The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead). 35 | } 36 | 37 | func sceneDidBecomeActive(_ scene: UIScene) { 38 | // Called when the scene has moved from an inactive state to an active state. 39 | // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. 40 | 41 | guard let _ : CurrentUser = KeychainService.loadCodable(for: .currentUser) else { 42 | print(#line, "Missing current user from KeychainService") 43 | return 44 | } 45 | 46 | guard UserDefaults.standard.bool(forKey: "isAuthorized") == true else { return } 47 | 48 | _ = SocketViewModel.shared 49 | } 50 | 51 | func sceneWillResignActive(_ scene: UIScene) { 52 | // Called when the scene will move from an active state to an inactive state. 53 | // This may occur due to temporary interruptions (ex. an incoming phone call). 54 | } 55 | 56 | func sceneWillEnterForeground(_ scene: UIScene) { 57 | // Called as the scene transitions from the background to the foreground. 58 | // Use this method to undo the changes made on entering the background. 59 | } 60 | 61 | func sceneDidEnterBackground(_ scene: UIScene) { 62 | // Called as the scene transitions from the foreground to the background. 63 | // Use this method to save data, release shared resources, and store enough scene-specific state information 64 | // to restore the scene back to its current state. 65 | } 66 | 67 | 68 | } 69 | 70 | extension SceneDelegate: UIGestureRecognizerDelegate { 71 | func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { 72 | return true 73 | } 74 | } 75 | 76 | class AnyGestureRecognizer: UIGestureRecognizer { 77 | override func touchesBegan(_ touches: Set, with event: UIEvent) { 78 | if let touchedView = touches.first?.view, touchedView is UIControl { 79 | state = .cancelled 80 | 81 | } else if let touchedView = touches.first?.view as? UITextView, touchedView.isEditable { 82 | state = .cancelled 83 | 84 | } else { 85 | state = .began 86 | } 87 | } 88 | 89 | override func touchesEnded(_ touches: Set, with event: UIEvent?) { 90 | state = .ended 91 | } 92 | 93 | override func touchesCancelled(_ touches: Set, with event: UIEvent) { 94 | state = .cancelled 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /AddaMeIOS/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /AddaMeIOS/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Icon-App-20x20@2x.png", 5 | "idiom" : "iphone", 6 | "scale" : "2x", 7 | "size" : "20x20" 8 | }, 9 | { 10 | "filename" : "Icon-App-20x20@3x.png", 11 | "idiom" : "iphone", 12 | "scale" : "3x", 13 | "size" : "20x20" 14 | }, 15 | { 16 | "filename" : "Icon-App-29x29@1x.png", 17 | "idiom" : "iphone", 18 | "scale" : "1x", 19 | "size" : "29x29" 20 | }, 21 | { 22 | "filename" : "Icon-App-29x29@2x.png", 23 | "idiom" : "iphone", 24 | "scale" : "2x", 25 | "size" : "29x29" 26 | }, 27 | { 28 | "filename" : "Icon-App-29x29@3x.png", 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "29x29" 32 | }, 33 | { 34 | "filename" : "Icon-App-40x40@2x.png", 35 | "idiom" : "iphone", 36 | "scale" : "2x", 37 | "size" : "40x40" 38 | }, 39 | { 40 | "filename" : "Icon-App-40x40@3x.png", 41 | "idiom" : "iphone", 42 | "scale" : "3x", 43 | "size" : "40x40" 44 | }, 45 | { 46 | "filename" : "Icon-App-60x60@2x.png", 47 | "idiom" : "iphone", 48 | "scale" : "2x", 49 | "size" : "60x60" 50 | }, 51 | { 52 | "filename" : "Icon-App-60x60@3x.png", 53 | "idiom" : "iphone", 54 | "scale" : "3x", 55 | "size" : "60x60" 56 | }, 57 | { 58 | "filename" : "Icon-App-20x20@1x.png", 59 | "idiom" : "ipad", 60 | "scale" : "1x", 61 | "size" : "20x20" 62 | }, 63 | { 64 | "filename" : "Icon-App-20x20@2x.png", 65 | "idiom" : "ipad", 66 | "scale" : "2x", 67 | "size" : "20x20" 68 | }, 69 | { 70 | "filename" : "Icon-App-29x29@1x.png", 71 | "idiom" : "ipad", 72 | "scale" : "1x", 73 | "size" : "29x29" 74 | }, 75 | { 76 | "filename" : "Icon-App-29x29@2x.png", 77 | "idiom" : "ipad", 78 | "scale" : "2x", 79 | "size" : "29x29" 80 | }, 81 | { 82 | "filename" : "Icon-App-40x40@1x.png", 83 | "idiom" : "ipad", 84 | "scale" : "1x", 85 | "size" : "40x40" 86 | }, 87 | { 88 | "filename" : "Icon-App-40x40@2x.png", 89 | "idiom" : "ipad", 90 | "scale" : "2x", 91 | "size" : "40x40" 92 | }, 93 | { 94 | "filename" : "Icon-App-76x76@1x.png", 95 | "idiom" : "ipad", 96 | "scale" : "1x", 97 | "size" : "76x76" 98 | }, 99 | { 100 | "filename" : "Icon-App-76x76@2x.png", 101 | "idiom" : "ipad", 102 | "scale" : "2x", 103 | "size" : "76x76" 104 | }, 105 | { 106 | "filename" : "Icon-App-83.5x83.5@2x.png", 107 | "idiom" : "ipad", 108 | "scale" : "2x", 109 | "size" : "83.5x83.5" 110 | }, 111 | { 112 | "filename" : "ItunesArtwork@2x.png", 113 | "idiom" : "ios-marketing", 114 | "scale" : "1x", 115 | "size" : "1024x1024" 116 | } 117 | ], 118 | "info" : { 119 | "author" : "xcode", 120 | "version" : 1 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /AddaMeIOS/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AddaMeSPB/AddaMeIOSSwiftUI/ddb62dba6d4f2c0073f939646037c260faf9675a/AddaMeIOS/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /AddaMeIOS/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AddaMeSPB/AddaMeIOSSwiftUI/ddb62dba6d4f2c0073f939646037c260faf9675a/AddaMeIOS/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /AddaMeIOS/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AddaMeSPB/AddaMeIOSSwiftUI/ddb62dba6d4f2c0073f939646037c260faf9675a/AddaMeIOS/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /AddaMeIOS/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AddaMeSPB/AddaMeIOSSwiftUI/ddb62dba6d4f2c0073f939646037c260faf9675a/AddaMeIOS/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /AddaMeIOS/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AddaMeSPB/AddaMeIOSSwiftUI/ddb62dba6d4f2c0073f939646037c260faf9675a/AddaMeIOS/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /AddaMeIOS/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AddaMeSPB/AddaMeIOSSwiftUI/ddb62dba6d4f2c0073f939646037c260faf9675a/AddaMeIOS/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /AddaMeIOS/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AddaMeSPB/AddaMeIOSSwiftUI/ddb62dba6d4f2c0073f939646037c260faf9675a/AddaMeIOS/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /AddaMeIOS/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AddaMeSPB/AddaMeIOSSwiftUI/ddb62dba6d4f2c0073f939646037c260faf9675a/AddaMeIOS/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /AddaMeIOS/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AddaMeSPB/AddaMeIOSSwiftUI/ddb62dba6d4f2c0073f939646037c260faf9675a/AddaMeIOS/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /AddaMeIOS/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AddaMeSPB/AddaMeIOSSwiftUI/ddb62dba6d4f2c0073f939646037c260faf9675a/AddaMeIOS/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /AddaMeIOS/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AddaMeSPB/AddaMeIOSSwiftUI/ddb62dba6d4f2c0073f939646037c260faf9675a/AddaMeIOS/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /AddaMeIOS/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AddaMeSPB/AddaMeIOSSwiftUI/ddb62dba6d4f2c0073f939646037c260faf9675a/AddaMeIOS/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /AddaMeIOS/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AddaMeSPB/AddaMeIOSSwiftUI/ddb62dba6d4f2c0073f939646037c260faf9675a/AddaMeIOS/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /AddaMeIOS/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AddaMeSPB/AddaMeIOSSwiftUI/ddb62dba6d4f2c0073f939646037c260faf9675a/AddaMeIOS/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /AddaMeIOS/Assets.xcassets/AppIcon.appiconset/ItunesArtwork@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AddaMeSPB/AddaMeIOSSwiftUI/ddb62dba6d4f2c0073f939646037c260faf9675a/AddaMeIOS/Assets.xcassets/AppIcon.appiconset/ItunesArtwork@2x.png -------------------------------------------------------------------------------- /AddaMeIOS/Assets.xcassets/Avatar.imageset/Avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AddaMeSPB/AddaMeIOSSwiftUI/ddb62dba6d4f2c0073f939646037c260faf9675a/AddaMeIOS/Assets.xcassets/Avatar.imageset/Avatar.png -------------------------------------------------------------------------------- /AddaMeIOS/Assets.xcassets/Avatar.imageset/Avatar@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AddaMeSPB/AddaMeIOSSwiftUI/ddb62dba6d4f2c0073f939646037c260faf9675a/AddaMeIOS/Assets.xcassets/Avatar.imageset/Avatar@2x.png -------------------------------------------------------------------------------- /AddaMeIOS/Assets.xcassets/Avatar.imageset/Avatar@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AddaMeSPB/AddaMeIOSSwiftUI/ddb62dba6d4f2c0073f939646037c260faf9675a/AddaMeIOS/Assets.xcassets/Avatar.imageset/Avatar@3x.png -------------------------------------------------------------------------------- /AddaMeIOS/Assets.xcassets/Avatar.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Avatar.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "Avatar@2x.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "Avatar@3x.png", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /AddaMeIOS/Assets.xcassets/Color.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.938", 9 | "green" : "0.938", 10 | "red" : "0.938" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /AddaMeIOS/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /AddaMeIOS/Assets.xcassets/bg.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0xF9", 9 | "green" : "0x79", 10 | "red" : "0x5E" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /AddaMeIOS/Assets.xcassets/iTunesArtwork.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "iTunesArtwork@1x.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "iTunesArtwork@2x.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "iTunesArtwork@3x.png", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /AddaMeIOS/Assets.xcassets/iTunesArtwork.imageset/iTunesArtwork@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AddaMeSPB/AddaMeIOSSwiftUI/ddb62dba6d4f2c0073f939646037c260faf9675a/AddaMeIOS/Assets.xcassets/iTunesArtwork.imageset/iTunesArtwork@1x.png -------------------------------------------------------------------------------- /AddaMeIOS/Assets.xcassets/iTunesArtwork.imageset/iTunesArtwork@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AddaMeSPB/AddaMeIOSSwiftUI/ddb62dba6d4f2c0073f939646037c260faf9675a/AddaMeIOS/Assets.xcassets/iTunesArtwork.imageset/iTunesArtwork@2x.png -------------------------------------------------------------------------------- /AddaMeIOS/Assets.xcassets/iTunesArtwork.imageset/iTunesArtwork@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AddaMeSPB/AddaMeIOSSwiftUI/ddb62dba6d4f2c0073f939646037c260faf9675a/AddaMeIOS/Assets.xcassets/iTunesArtwork.imageset/iTunesArtwork@3x.png -------------------------------------------------------------------------------- /AddaMeIOS/Auth/AuthViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AuthViewModel.swift 3 | // AddaMeIOS 4 | // 5 | // Created by Alif on 4/8/20. 6 | // 7 | 8 | import Foundation 9 | import Combine 10 | import Pyramid 11 | import SwiftUI 12 | 13 | enum AddaError: Error { 14 | case guardFail(String) 15 | } 16 | 17 | extension AddaError { 18 | var reason: String { 19 | switch self { 20 | case .guardFail(let location): 21 | return "guard statment fail \(location)" 22 | } 23 | } 24 | } 25 | 26 | class AuthViewModel: ObservableObject { 27 | 28 | @AppStorage(AppUserDefaults.Key.isAuthorized.rawValue) var isAuthorized: Bool = false 29 | @AppStorage(AppUserDefaults.Key.isUserFristNameUpdated.rawValue) var isUserFristNameUpdated: Bool = false 30 | 31 | @Published var lAndVRes = LoginAndVerificationResponse(phoneNumber: String.empty) 32 | @Published var verificationCodeResponse = String.empty { 33 | didSet { 34 | lAndVRes.code = verificationCodeResponse 35 | if verificationCodeResponse.count == 6 { 36 | verification() 37 | } 38 | } 39 | } 40 | 41 | @Published var isLoadingPage = false 42 | @Published var inputWrongVarificationCode = false 43 | 44 | let provider = Pyramid() 45 | var cancellationToken: AnyCancellable? 46 | 47 | var anyCancellable: AnyCancellable? = nil 48 | 49 | init() {} 50 | 51 | } 52 | 53 | extension AuthViewModel { 54 | func login() { 55 | 56 | isLoadingPage = true 57 | 58 | guard !lAndVRes.phoneNumber.isEmpty else { 59 | return 60 | } 61 | 62 | cancellationToken = provider.request( 63 | with: AuthAPI.login(login: lAndVRes), 64 | scheduler: RunLoop.main, 65 | class: LoginAndVerificationResponse.self 66 | ).sink(receiveCompletion: { [weak self] completionResponse in 67 | switch completionResponse { 68 | case .failure(let error): 69 | self?.isLoadingPage = false 70 | print(#line, error.errorDescription) 71 | case .finished: 72 | self?.isLoadingPage = false 73 | break 74 | } 75 | }, receiveValue: { [weak self] res in 76 | print(res) 77 | self?.isLoadingPage = false 78 | self?.lAndVRes = res 79 | }) 80 | } 81 | 82 | func verification() { 83 | isLoadingPage = true 84 | 85 | guard !lAndVRes.phoneNumber.isEmpty else { 86 | return 87 | } 88 | 89 | cancellationToken = provider.request( 90 | with: AuthAPI.verification(verificationResponse: lAndVRes), 91 | scheduler: RunLoop.main, 92 | class: LoginRes.self 93 | ).sink(receiveCompletion: { [weak self] completionResponse in 94 | switch completionResponse { 95 | case .failure(let error): 96 | print(#line, error.errorDescription) 97 | self?.inputWrongVarificationCode = true 98 | self?.isLoadingPage = false 99 | case .finished: 100 | self?.isLoadingPage = false 101 | break 102 | } 103 | }, receiveValue: { [weak self] res in 104 | print(res) 105 | print("Your have login") 106 | guard let self = self else { return } 107 | 108 | AppUserDefaults.saveCurrentUserAndToken(res) 109 | self.isAuthorized = true 110 | self.isLoadingPage = false 111 | 112 | }) // add more logic loading 113 | } 114 | } 115 | 116 | -------------------------------------------------------------------------------- /AddaMeIOS/Auth/HUDProgressView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HUDProgressView.swift 3 | // AddaMeIOS 4 | // 5 | // Created by Saroar Khandoker on 27.10.2020. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct HUDProgressView: View { 11 | var placeHolder: String 12 | @Binding var show: Bool 13 | @State var animate = false 14 | 15 | var body: some View { 16 | VStack { 17 | Circle() 18 | .stroke(AngularGradient(gradient: .init(colors: [Color(.systemBlue), Color.primary.opacity(0)]), center: .center)) 19 | .frame(width: 80, height: 80) 20 | .rotationEffect(.init(degrees: animate ? 360 : 0)) 21 | 22 | Text(placeHolder) 23 | .fontWeight(.bold) 24 | .foregroundColor(.white) 25 | 26 | } 27 | .padding(.vertical, 25) 28 | .padding(.horizontal, 35) 29 | // .background(BlueView()) 30 | .cornerRadius(20) 31 | .frame(maxWidth: .infinity, maxHeight: .infinity) 32 | .background( 33 | Color.clear 34 | .onTapGesture { 35 | withAnimation { 36 | show.toggle() 37 | } 38 | } 39 | ) 40 | .onAppear { 41 | withAnimation( 42 | Animation.linear(duration: 1.5) 43 | .repeatForever(autoreverses: false)) { 44 | animate.toggle() 45 | } 46 | } 47 | } 48 | } 49 | 50 | struct BlurView: UIViewRepresentable { 51 | func makeUIView(context: Context) -> some UIView { 52 | let effect = UIBlurEffect(style: .extraLight) 53 | let view = UIVisualEffectView(effect: effect) 54 | return view 55 | } 56 | 57 | func updateUIView(_ uiView: UIViewType, context: Context) { 58 | 59 | } 60 | } 61 | 62 | -------------------------------------------------------------------------------- /AddaMeIOS/Auth/LoginAndVerificationResponse.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LoginAndVerificationResponse.swift 3 | // AddaMeIOS 4 | // 5 | // Created by Saroar Khandoker on 24.08.2020. 6 | // 7 | 8 | import Foundation 9 | 10 | // MARK: - Login and Verification request/response 11 | struct LoginAndVerificationResponse: Codable { 12 | internal init( 13 | phoneNumber: String, 14 | attemptId: String? = nil, 15 | code: String? = nil, 16 | isLoggedIn: Bool = false 17 | ) { 18 | self.phoneNumber = phoneNumber 19 | self.attemptId = attemptId 20 | self.code = code 21 | self.isLoggedIn = isLoggedIn 22 | } 23 | 24 | var phoneNumber: String 25 | var attemptId: String? 26 | var code: String? 27 | var isLoggedIn: Bool? = false 28 | } 29 | 30 | // MARK: - Login Response 31 | struct LoginRes: Codable { 32 | let status: String 33 | let user: CurrentUser 34 | let access: Access 35 | } 36 | 37 | // MARK: - Access 38 | struct Access: Codable { 39 | let accessToken, refreshToken: String 40 | } 41 | 42 | 43 | -------------------------------------------------------------------------------- /AddaMeIOS/Auth/RootView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RootView.swift 3 | // AddaMeIOS 4 | // 5 | // Created by Saroar Khandoker on 06.11.2020. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct RootView: View { 11 | 12 | @AppStorage(AppUserDefaults.Key.isUserFristNameUpdated.rawValue) var isUserFristNameUpdated: Bool = false 13 | 14 | @ViewBuilder var body: some View { 15 | if isUserFristNameUpdated { 16 | AppTabView() 17 | } else { 18 | AuthView() 19 | } 20 | } 21 | 22 | } 23 | 24 | struct RootView_Previews: PreviewProvider { 25 | static var previews: some View { 26 | RootView() 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /AddaMeIOS/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /AddaMeIOS/Chat/Authenticator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Authenticator.swift 3 | // AddaMeIOS 4 | // 5 | // Created by Saroar Khandoker on 26.08.2020. 6 | // 7 | 8 | import Foundation 9 | import Combine 10 | import Pyramid 11 | import SwiftUI 12 | 13 | class Authenticator: ObservableObject { 14 | static let shared = Authenticator() 15 | let provider = Pyramid() 16 | var cancellables = Set() 17 | @AppStorage(AppUserDefaults.Key.isAuthorized.rawValue) var isAuthorized: Bool = false 18 | 19 | 20 | var currentToken: Access? { 21 | get { 22 | guard 23 | let token: Access = KeychainService.loadCodable(for: .token), 24 | isAuthorized == true else { 25 | print(#line, "not Authorized Token are missing") 26 | return nil 27 | } 28 | 29 | return token 30 | 31 | } set { 32 | KeychainService.save(codable: newValue, for: .token) 33 | } 34 | } 35 | 36 | init() {} 37 | 38 | private var cancellation: AnyCancellable? 39 | 40 | func refreshToken(using subject: S) where S.Output == RefreshTokenResponse { 41 | //self.currentToken = Token(isValid: true) 42 | //let referehTokenInput = RefreshTokenInput(refreshToken: currentToken!.refreshToken) 43 | // provider.request( 44 | // with: RefreshTokenAPI.refresh(token: referehTokenInput), 45 | // scheduler: RunLoop.main, 46 | // class: RefreshTokenResponse.self 47 | // ) 48 | // .retry(3) 49 | // .sink(receiveCompletion: { completionResponse in 50 | // switch completionResponse { 51 | // case .failure(let error): 52 | // print(#line, error) 53 | //// print(#line, error.errorDescription.contains("401")) 54 | // case .finished: 55 | // break 56 | // } 57 | // }, receiveValue: { res in 58 | // print(res) 59 | // KeychainService.save(codable: res, for: .token) 60 | // subject.send(self.currentToken!) 61 | // }).store(in: &cancellables) 62 | 63 | // let headers = [ 64 | // "authorization": "Bearer \(currentToken?.accessToken)", 65 | // "Content-Type": "application/json" 66 | // ] 67 | // 68 | // let parameters: [String: Any] = [ 69 | // "refresh_token": currentToken?.refreshToken, 70 | // ] 71 | // 72 | // let jsonDecoder: JSONDecoder = .ISO8601JSONDecoder 73 | // let url: URL = URL(string: "http://10.0.1.3:8080/v1/auth/refreshToken")! 74 | // var request = URLRequest(url: url) 75 | // request.httpMethod = "POST" 76 | // request.allHTTPHeaderFields = headers 77 | // 78 | // do { 79 | // request.httpBody = try JSONSerialization.data(withJSONObject: parameters, options: .prettyPrinted) 80 | // } catch let error { 81 | // print(error.localizedDescription) 82 | // } 83 | // 84 | // URLSession.shared.dataTaskPublisher(for: request) 85 | // .sink { com in 86 | // print(com) 87 | // } receiveValue: { data in 88 | // print(#line, data) 89 | // 90 | // do { 91 | // let rtResponse = try JSONDecoder().decode(RefreshTokenResponse.self, from: data.data) 92 | // 93 | // 94 | // KeychainService.save(codable: rtResponse, for: .token) 95 | // 96 | // subject.send(self.currentToken!) 97 | // } catch { 98 | // print(#line, error) 99 | // } 100 | // 101 | // }.store(in: &cancellables) 102 | } 103 | 104 | func tokenSubject() -> CurrentValueSubject { 105 | return CurrentValueSubject(currentToken!) 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /AddaMeIOS/Chat/ChatBottomView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ChatBottomView.swift 3 | // AddaMeIOS 4 | // 5 | // Created by Saroar Khandoker on 05.09.2020. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct ChatBottomView: View { 11 | 12 | @State var composedMessage: String = String.empty 13 | @State var isMicButtonHide = false 14 | @State var preWordCount: Int = 0 15 | @State var newLineCount = 1 16 | @State var placeholderString: String = "Type..." 17 | @State var tEheight: CGFloat = 40 18 | 19 | @EnvironmentObject var chatData: ChatDataHandler 20 | 21 | private func onComment() { 22 | chatData.send() 23 | chatData.clearComposedMessage() 24 | tEheight = 40 25 | } 26 | 27 | var body: some View { 28 | ZStack { 29 | VStack { 30 | // Spacer() 31 | HStack { 32 | TextEditor(text: self.$chatData.composedMessage) 33 | .foregroundColor(self.chatData.composedMessage == placeholderString ? .gray : .primary) 34 | .onChange(of: self.chatData.composedMessage, perform: { value in 35 | preWordCount = value.split { $0.isNewline }.count 36 | if preWordCount == newLineCount && preWordCount < 9 { 37 | newLineCount += 1 38 | tEheight += 20 39 | } 40 | 41 | if chatData.composedMessage == String.empty { 42 | tEheight = 40 43 | } 44 | }) 45 | .lineLimit(9) // its does not work ios 14 and swiftui 2.0 46 | .font(Font.system(size: 20, weight: .thin, design: .rounded)) 47 | .frame(height: tEheight) 48 | .onTapGesture { 49 | if self.chatData.composedMessage == placeholderString { 50 | self.chatData.composedMessage = String.empty 51 | } 52 | } 53 | .padding([.trailing, .leading], 10) 54 | .background(RoundedRectangle(cornerRadius: 8).stroke()) 55 | .background(Color.clear) 56 | 57 | Button(action: onComment) { 58 | Image(systemName: "arrow.up") 59 | //.resizable() 60 | .imageScale(.large) 61 | .frame(width: 23, height: 23) 62 | .padding(11) 63 | .foregroundColor(.white) 64 | .background(self.chatData.newMessageTextIsEmpty ? Color.gray : Color("bg")) 65 | .clipShape(Circle()) 66 | } 67 | .disabled(self.chatData.newMessageTextIsEmpty) 68 | .foregroundColor(.gray) 69 | 70 | } 71 | .frame(height: 55) 72 | .padding(.horizontal, 15) 73 | .background(Color.clear) 74 | .modifier(AdaptsToSoftwareKeyboard()) 75 | 76 | } 77 | } 78 | 79 | } 80 | 81 | } 82 | 83 | struct ChatBottomView_Previews: PreviewProvider { 84 | static var previews: some View { 85 | ChatBottomView() 86 | .environmentObject(ChatDataHandler()) 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /AddaMeIOS/Chat/ChatOutGoingEvent.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ChatOutGoingEvent.swift 3 | // AddaMeIOS 4 | // 5 | // Created by Saroar Khandoker on 29.09.2020. 6 | // 7 | 8 | import Foundation 9 | 10 | private let jsonEncoder: JSONEncoder = { 11 | let encoder = JSONEncoder() 12 | encoder.dateEncodingStrategy = .iso8601 13 | return encoder 14 | }() 15 | 16 | enum ChatOutGoingEvent: Encodable, Decodable { 17 | 18 | case conversation(ChatMessageResponse.Item) 19 | case message(ChatMessageResponse.Item) 20 | case connect(CurrentUser) 21 | case disconnect(CurrentUser) 22 | case notice(String) 23 | case error(String) 24 | 25 | private enum CodingKeys: String, CodingKey { 26 | case type, user, message, conversation 27 | } 28 | 29 | enum CodingError: Error { 30 | case unknownValue 31 | } 32 | 33 | init(from decoder: Decoder) throws { 34 | let container = try decoder.container(keyedBy: CodingKeys.self) 35 | let type = try container.decode(String.self, forKey: .type) 36 | 37 | switch type { 38 | case "connect": 39 | let connect = try container.decode(CurrentUser.self, forKey: .user) 40 | self = .connect(connect) 41 | case "disconnect": 42 | let disconnect = try container.decode(CurrentUser.self, forKey: .user) 43 | self = .disconnect(disconnect) 44 | case "message": 45 | let message = try container.decode(ChatMessageResponse.Item.self, forKey: .message) 46 | self = .message(message) 47 | case "conversation": 48 | let lastMessage = try container.decode(ChatMessageResponse.Item.self, forKey: .conversation) 49 | self = .conversation(lastMessage) 50 | case "notice": 51 | let notice = try container.decode(String.self, forKey: .message) 52 | self = .notice(notice) 53 | case "error": 54 | let error = try container.decode(String.self, forKey: .message) 55 | self = .error(error) 56 | default: 57 | throw CodingError.unknownValue 58 | } 59 | } 60 | 61 | func encode(to encoder: Encoder) throws { 62 | var kvc = encoder.container(keyedBy: String.self) 63 | 64 | switch self { 65 | case .connect(let user): 66 | try kvc.encode("connect", forKey: "type") 67 | try kvc.encode(user, forKey: "user") 68 | case .disconnect(let user): 69 | try kvc.encode("disconnect", forKey: "type") 70 | try kvc.encode(user, forKey: "user") 71 | case .message(let message): 72 | try kvc.encode("message", forKey: "type") 73 | try kvc.encode(message, forKey: "message") 74 | case .conversation(let conversation): 75 | try kvc.encode("conversation", forKey: "type") 76 | try kvc.encode(conversation, forKey: "conversation") 77 | case .notice(let message): 78 | try kvc.encode("notice", forKey: "type") 79 | try kvc.encode(message, forKey: "message") 80 | case .error(let error): 81 | try kvc.encode("error", forKey: "type") 82 | try kvc.encode(error, forKey: "message") 83 | } 84 | } 85 | 86 | } 87 | -------------------------------------------------------------------------------- /AddaMeIOS/Chat/ChatRoomView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ChatDetails.swift 3 | // AddaMeIOS 4 | // 5 | // Created by Saroar Khandoker on 05.09.2020. 6 | // 7 | 8 | import Combine 9 | import SwiftUI 10 | 11 | struct ChatRoomView: View { 12 | 13 | @Environment(\.colorScheme) var colorScheme 14 | @Environment(\.presentationMode) var presentationMode 15 | 16 | @StateObject private var chatData = ChatDataHandler() 17 | 18 | @State var conversation: ConversationResponse.Item! 19 | @State var isMicButtonHide = false 20 | 21 | private func onApperAction() { 22 | self.chatData.conversationsId = conversation.id 23 | self.chatData.fetchMoreMessages() 24 | } 25 | 26 | @State var fromContactsOrEvents: Bool 27 | 28 | var body: some View { 29 | VStack { 30 | ZStack { 31 | List { 32 | // self.chatData.socket.messages[conversation.id] ?? [] // chatDemoData 33 | ForEach( self.chatData.socket.messages[conversation.id] ?? [] ) { message in 34 | LazyView(ChatRow(chatMessageResponse: message)) 35 | .padding([.top, .bottom], 5) 36 | .padding([.leading, .trailing], -10) 37 | .onAppear { 38 | self.chatData.fetchMoreMessagIfNeeded(currentItem: message) 39 | } 40 | .scaleEffect(x: 1, y: -1, anchor: .center) 41 | .hideRowSeparator() 42 | .background(Color(.systemBackground)) 43 | .foregroundColor(Color(.systemBackground)) 44 | } 45 | 46 | 47 | if self.chatData.isLoadingPage { 48 | ProgressView() 49 | } 50 | } 51 | .scaleEffect(x: 1, y: -1, anchor: .center) 52 | .offset(x: 0, y: 2) 53 | .padding(10) 54 | .onAppear(perform: { 55 | onApperAction() 56 | }) 57 | .navigationBarTitle("\(conversation.title)", displayMode: .inline) 58 | .background(Color(.systemBackground)) 59 | .resignKeyboardOnDragGesture() 60 | 61 | } 62 | Spacer() 63 | ChatBottomView() 64 | .environmentObject(chatData) 65 | } 66 | .overlay( 67 | DismissButton(fromContactsOrEvents), 68 | alignment: .topLeading 69 | ) 70 | .ignoresSafeArea(.keyboard, edges: .bottom) 71 | .animation(.default) 72 | } 73 | 74 | } 75 | 76 | struct ChatDetailsView_Previews: PreviewProvider { 77 | static var previews: some View { 78 | ChatRoomView(conversation: conversationData.last!, fromContactsOrEvents: true) 79 | .environmentObject(ChatDataHandler()) 80 | } 81 | } 82 | 83 | struct DismissButton: View { 84 | @Environment(\.presentationMode) var presentationMode 85 | 86 | var fromContactsOrEvents: Bool 87 | 88 | init(_ fromContactsOrEvents: Bool) { 89 | self.fromContactsOrEvents = fromContactsOrEvents 90 | } 91 | 92 | var body: some View { 93 | HStack { 94 | if fromContactsOrEvents { 95 | Button(action: { 96 | presentationMode.wrappedValue.dismiss() 97 | }, label: { 98 | Image(systemName: "xmark.circle").font(.title) 99 | }) 100 | .padding(.top, -10) 101 | .padding() 102 | } 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /AddaMeIOS/Chat/ChatRow.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ChatRow.swift 3 | // AddaMeIOS 4 | // 5 | // Created by Saroar Khandoker on 28.09.2020. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct ChatRow : View { 11 | let image = ImageDraw() 12 | var chatMessageResponse: ChatMessageResponse.Item 13 | 14 | @Environment(\.colorScheme) var colorScheme 15 | 16 | @ViewBuilder var body: some View { 17 | Group { 18 | 19 | if !currenuser(chatMessageResponse.sender.id) { 20 | HStack { 21 | Group { 22 | 23 | if chatMessageResponse.sender.avatarUrl != nil { 24 | AsyncImage( 25 | urlString: chatMessageResponse.sender.avatarUrl, 26 | placeholder: { Text("Loading...").frame(width: 40, height: 40, alignment: .center) }, 27 | image: { 28 | Image(uiImage: $0).resizable() 29 | } 30 | ) 31 | .aspectRatio(contentMode: .fit) 32 | .frame(width: 40, height: 40) 33 | .clipShape(Circle()) 34 | } else { 35 | Image(uiImage: image.random()) 36 | .aspectRatio(contentMode: .fit) 37 | .frame(width: 40, height: 40) 38 | .clipShape(Circle()) 39 | } 40 | 41 | Text(chatMessageResponse.messageBody) 42 | .bold() 43 | .padding(10) 44 | .foregroundColor(Color.white) 45 | .background(Color.blue) 46 | .cornerRadius(10) 47 | } 48 | .background(Color(.systemBackground)) 49 | Spacer() 50 | } 51 | .background(Color(.systemBackground)) 52 | } else { 53 | HStack { 54 | Group { 55 | Spacer() 56 | Text(chatMessageResponse.messageBody) 57 | .bold() 58 | .foregroundColor(Color.white) 59 | .padding(10) 60 | .background(Color.red) 61 | .cornerRadius(10) 62 | 63 | if chatMessageResponse.sender.avatarUrl != nil { 64 | AsyncImage( 65 | urlString: chatMessageResponse.sender.avatarUrl, 66 | placeholder: { Text("Loading...").frame(width: 40, height: 40, alignment: .center) }, 67 | image: { 68 | Image(uiImage: $0).resizable() 69 | } 70 | ) 71 | .aspectRatio(contentMode: .fit) 72 | .frame(width: 40, height: 40) 73 | .clipShape(Circle()) 74 | } else { 75 | Image(uiImage: image.random()) 76 | .aspectRatio(contentMode: .fit) 77 | .frame(width: 40, height: 40) 78 | .clipShape(Circle()) 79 | } 80 | 81 | } 82 | 83 | } 84 | .background(Color(.systemBackground)) 85 | } 86 | } 87 | .background(Color(.systemBackground)) 88 | } 89 | 90 | func currenuser(_ userId: String) -> Bool { 91 | guard let currentUSER: CurrentUser = KeychainService.loadCodable(for: .currentUser) else { 92 | return false 93 | } 94 | 95 | return currentUSER.id == userId ? true : false 96 | 97 | } 98 | } 99 | 100 | 101 | struct ChatRow_Previews: PreviewProvider { 102 | static var previews: some View { 103 | ChatRow(chatMessageResponse: chatDemoData[1]) 104 | .environmentObject(ChatDataHandler()) 105 | .environment(\.colorScheme, .dark) 106 | } 107 | } 108 | 109 | // fixed this image issue i mean night mode issue 110 | //#imageLiteral(resourceName: "IMG_9505 3.PNG") 111 | -------------------------------------------------------------------------------- /AddaMeIOS/Configs/Development.xcconfig: -------------------------------------------------------------------------------- 1 | // 2 | // Development.xcconfig 3 | // AddaMeIOS 4 | // 5 | // Created by Saroar Khandoker on 02.11.2020. 6 | // 7 | 8 | // Configuration settings file format documentation can be found at: 9 | // https://help.apple.com/xcode/#/dev745c5c974 10 | 11 | // Server URL 12 | ROOT_URL = http:/$()/10.0.1.3:8080/v1 13 | WEB_SOCKET_URL = ws:/$()/10.0.1.3:6060/v1/chat 14 | 15 | // Image Uploading to DigitalOcen 16 | ACCESS_KEY_ID = Z4R4QP36Q4JH3LZXU4BC 17 | SECRET_ACCESS_KEY = +RqibSk+I6D1/9Tdpm/afnpUuHaBP2lV4Rg+HKAYbRE 18 | 19 | -------------------------------------------------------------------------------- /AddaMeIOS/Conversation/ConversationList.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MessageListView.swift 3 | // AddaMeIOS 4 | // 5 | // Created by Saroar Khandoker on 05.09.2020. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct ConversationList: View { 11 | 12 | @StateObject var contactStore: ContactStore = ContactStore() 13 | @StateObject var conversationViewModel = ConversationViewModel() 14 | @EnvironmentObject var appState: AppState 15 | @Environment(\.colorScheme) var colorScheme 16 | 17 | @State var distanationTag = false 18 | @State var moveToContacts = false 19 | 20 | @ViewBuilder var body : some View { 21 | NavigationView { 22 | List { 23 | ForEach(conversationViewModel.socket.conversations.map { $1 }.sorted(), id: \.self) { conversation in 24 | NavigationLink( 25 | destination: ChatRoomView(conversation: conversation, fromContactsOrEvents: false) 26 | .environmentObject(appState) 27 | .onAppear(perform: { 28 | appState.tabBarIsHidden = true 29 | }) 30 | .onDisappear(perform: { 31 | appState.tabBarIsHidden = false 32 | }) 33 | ) { 34 | 35 | ConversationRow(conversation: conversation) 36 | .environmentObject(appState) 37 | .onAppear { 38 | conversationViewModel.fetchMoreEventIfNeeded( 39 | currentItem: conversation 40 | ) 41 | } 42 | 43 | } 44 | } 45 | 46 | if conversationViewModel.isLoadingPage { 47 | ProgressView() 48 | } 49 | 50 | } 51 | .onAppear { 52 | self.conversationViewModel.fetchMoreConversations() 53 | } 54 | .navigationTitle("Chats") 55 | .toolbar { 56 | ToolbarItem(placement: .navigationBarTrailing ) { contactList } 57 | } 58 | .background(Color(.systemBackground)) 59 | } 60 | .navigationViewStyle(StackNavigationViewStyle()) 61 | } 62 | 63 | private var contactList: some View { 64 | Button(action: { 65 | self.moveToContacts = true 66 | }) { 67 | Image(systemName: "plus.circle") 68 | .font(.title) 69 | .foregroundColor(Color("bg")) 70 | }.background( 71 | NavigationLink( 72 | destination: ContactsView() 73 | .environmentObject(contactStore) 74 | .edgesIgnoringSafeArea(.bottom) 75 | .onAppear(perform: { 76 | appState.tabBarIsHidden = true 77 | self.moveToContacts = false 78 | }) 79 | .onDisappear(perform: { 80 | appState.tabBarIsHidden = false 81 | }), 82 | isActive: $moveToContacts 83 | ) { 84 | EmptyView() 85 | } 86 | .navigationBarTitle("Contacts") 87 | ) 88 | } 89 | 90 | } 91 | 92 | 93 | struct ConversationList_Previews: PreviewProvider { 94 | static var previews: some View { 95 | ConversationList() 96 | .environmentObject(AppState()) 97 | .environmentObject(ConversationViewModel()) 98 | } 99 | } 100 | 101 | -------------------------------------------------------------------------------- /AddaMeIOS/Conversation/ConversationRow.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ConversationRow.swift 3 | // AddaMeIOS 4 | // 5 | // Created by Saroar Khandoker on 05.09.2020. 6 | // 7 | 8 | import SwiftUI 9 | 10 | 11 | struct ConversationRow: View { 12 | 13 | let image = ImageDraw() 14 | var conversation: ConversationResponse.Item 15 | 16 | @Environment(\.imageCache) var cache: ImageCache 17 | @Environment(\.presentationMode) var presentationMode 18 | @Environment(\.colorScheme) var colorScheme 19 | 20 | var body : some View { 21 | Group { 22 | HStack(spacing: 0) { 23 | if conversation.lastMessage?.sender.avatarUrl != nil { 24 | AsyncImage( 25 | urlString: conversation.lastMessage?.sender.avatarUrl, 26 | placeholder: { 27 | Text("Loading...").frame(width: 100, height: 100, alignment: .center) 28 | }, 29 | image: { 30 | Image(uiImage: $0).resizable() 31 | } 32 | ) 33 | .aspectRatio(contentMode: .fit) 34 | .frame(width: 50, height: 50) 35 | .clipShape(Circle()) 36 | .padding(.trailing, 5) 37 | } else { 38 | Image(uiImage: image.random()) 39 | .aspectRatio(contentMode: .fit) 40 | .frame(width: 50, height: 50) 41 | .clipShape(Circle()) 42 | } 43 | 44 | VStack(alignment: .leading, spacing: 5) { 45 | Text(conversation.title) 46 | .lineLimit(1) 47 | .font(.system(size: 18, weight: .semibold, design: .rounded)) 48 | .foregroundColor(Color(.systemBlue)) 49 | 50 | if conversation.lastMessage != nil { 51 | Text(conversation.lastMessage!.messageBody).lineLimit(1) 52 | } 53 | } 54 | .padding(5) 55 | 56 | Spacer(minLength: 3) 57 | 58 | VStack(alignment: .trailing, spacing: 5) { 59 | if conversation.lastMessage != nil { 60 | Text("\(conversation.lastMessage!.createdAt?.dateFormatter ?? String.empty)") 61 | } 62 | 63 | if conversation.lastMessage?.messageBody != String.empty { 64 | //Text("6").padding(8).background(Color("bg")) 65 | // .foregroundColor(.white).clipShape(Circle()) 66 | } else { 67 | Spacer() 68 | } 69 | } 70 | 71 | } 72 | .frame(minWidth: 0, maxWidth: .infinity, minHeight: 20, alignment: .leading) 73 | .padding(2) 74 | } 75 | } 76 | } 77 | 78 | //struct ConversationRow_Previews: PreviewProvider { 79 | // static var previews: some View { 80 | // ConversationRow(conversation: demoConversations[0]) 81 | // } 82 | //} 83 | -------------------------------------------------------------------------------- /AddaMeIOS/CoreDataEntities/ContactEntity+CoreDataClass.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContactEntity+CoreDataClass.swift 3 | // AddaMeIOS 4 | // 5 | // Created by Saroar Khandoker on 01.12.2020. 6 | // 7 | // 8 | 9 | import Foundation 10 | import CoreData 11 | 12 | @objc(ContactEntity) 13 | public class ContactEntity: NSManagedObject { 14 | 15 | } 16 | -------------------------------------------------------------------------------- /AddaMeIOS/CoreDataEntities/ContactEntity+CoreDataProperties.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContactEntity+CoreDataProperties.swift 3 | // AddaMeIOS 4 | // 5 | // Created by Saroar Khandoker on 01.12.2020. 6 | // 7 | // 8 | 9 | import Foundation 10 | import CoreData 11 | 12 | 13 | extension ContactEntity { 14 | 15 | @nonobjc public class func fetchRequest() -> NSFetchRequest { 16 | return NSFetchRequest(entityName: "ContactEntity") 17 | } 18 | 19 | @NSManaged public var avatar: String? 20 | @NSManaged public var fullName: String 21 | @NSManaged public var id: String? 22 | @NSManaged public var identifier: String 23 | @NSManaged public var isRegister: Bool 24 | @NSManaged public var phoneNumber: String 25 | @NSManaged public var userId: String 26 | 27 | } 28 | 29 | extension ContactEntity : Identifiable { 30 | 31 | } 32 | -------------------------------------------------------------------------------- /AddaMeIOS/CoreDataEntities/ContactEntityExtension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContactEntityExtension.swift 3 | // AddaMeIOS 4 | // 5 | // Created by Saroar Khandoker on 01.12.2020. 6 | // 7 | 8 | import Foundation 9 | import CoreData 10 | extension ContactEntity { 11 | static func allContactsFetchRequest() -> NSFetchRequest { 12 | let request: NSFetchRequest = ContactEntity.fetchRequest() 13 | request.sortDescriptors = [NSSortDescriptor(key: "phoneNumber", ascending: true)] 14 | print(#line, request) 15 | return request 16 | } 17 | } 18 | 19 | extension ContactEntity { 20 | static var registerContactsFetchRequest: NSFetchRequest { 21 | let request: NSFetchRequest = ContactEntity.fetchRequest() 22 | request.predicate = NSPredicate(format: "isRegister == true") 23 | request.sortDescriptors = [NSSortDescriptor(key: "fullName", ascending: true)] 24 | 25 | return request 26 | } 27 | } 28 | 29 | extension ContactEntity: ManagedModel { 30 | public static var defaultPredicate: NSPredicate { return NSPredicate(value: true) } 31 | 32 | static func findOrCreate(withData data: APIData, in context: NSManagedObjectContext) -> ContactEntity { 33 | 34 | guard let content = data as? Contact else { 35 | fatalError("Incorrent API response") 36 | } 37 | 38 | let predicate = NSPredicate(format: "%K == %@", #keyPath(phoneNumber), content.phoneNumber) 39 | let contact = ContactEntity.findOrCreate(in: context, matching: predicate) { contact in 40 | contact.id = content.id ?? String.empty 41 | contact.fullName = content.fullName ?? String.empty 42 | contact.avatar = content.avatar 43 | contact.identifier = content.identifier 44 | contact.isRegister = content.isRegister ?? false 45 | contact.userId = content.userId ?? String.empty 46 | contact.phoneNumber = content.phoneNumber 47 | } 48 | 49 | contact.id = content.id ?? String.empty 50 | contact.fullName = content.fullName ?? String.empty 51 | contact.avatar = content.avatar 52 | contact.identifier = content.identifier 53 | contact.isRegister = content.isRegister ?? false 54 | contact.userId = content.userId ?? String.empty 55 | contact.phoneNumber = content.phoneNumber 56 | 57 | return contact 58 | } 59 | 60 | public static var defaultSortDescriptors: [NSSortDescriptor] { 61 | return [NSSortDescriptor(key: #keyPath(isRegister), ascending: true)] 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /AddaMeIOS/CoreDataModels/AddaModel.xcdatamodeld/AddaModel.xcdatamodel/contents: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /AddaMeIOS/CoreDataModels/Contacts.xcdatamodeld/Contacts.xcdatamodel/contents: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /AddaMeIOS/Environment.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Environment.swift 3 | // AddaMeIOS 4 | // 5 | // Created by Saroar Khandoker on 02.11.2020. 6 | // 7 | 8 | import Foundation 9 | 10 | public enum EnvironmentKeys { 11 | // MARK: - Keys 12 | private enum Keys { 13 | enum Plist { 14 | static let rootURL = "ROOT_URL" 15 | static let webSocketURL = "WEB_SOCKET_URL" 16 | static let accessKeyId = "ACCESS_KEY_ID" 17 | static let secretAccessKey = "SECRET_ACCESS_KEY" 18 | } 19 | } 20 | 21 | // MARK: - Plist 22 | private static let infoDictionary: [String: Any] = { 23 | 24 | guard let dict = Bundle.main.infoDictionary else { 25 | fatalError("Plist file not found") 26 | } 27 | 28 | return dict 29 | }() 30 | 31 | // MARK: - Plist values 32 | static let rootURL: URL = { 33 | 34 | guard let rootURLstring = EnvironmentKeys.infoDictionary["ROOT_URL"] as? String else { 35 | fatalError("Root URL not set in plist for this environment") 36 | } 37 | 38 | guard let url = URL(string: rootURLstring) else { 39 | fatalError("Root URL is invalid") 40 | } 41 | 42 | return url 43 | }() 44 | 45 | static let webSocketURL: URL = { 46 | 47 | guard let webSocketString = EnvironmentKeys.infoDictionary[Keys.Plist.webSocketURL] as? String else { 48 | fatalError("WEB SOCKET URL Key not set in plist for this environment") 49 | } 50 | 51 | guard let url = URL(string: webSocketString) else { 52 | fatalError("WEB SOCKET URL is invalid") 53 | } 54 | 55 | return url 56 | }() 57 | 58 | static let accessKeyId: String = { 59 | 60 | guard let accessKeyId = EnvironmentKeys.infoDictionary["ACCESS_KEY_ID"] as? String else { 61 | fatalError("ACCESS_KEY_ID not set in plist for this environment") 62 | } 63 | 64 | return accessKeyId 65 | }() 66 | 67 | static let secretAccessKey: String = { 68 | 69 | guard let secretAccessKey = EnvironmentKeys.infoDictionary["SECRET_ACCESS_KEY"] as? String else { 70 | fatalError("SECRET_ACCESS_KEY not set in plist for this environment") 71 | } 72 | 73 | return secretAccessKey 74 | }() 75 | 76 | 77 | } 78 | -------------------------------------------------------------------------------- /AddaMeIOS/ExprimentViews/Navigation_WithList.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Navigation_WithList.swift 3 | // AddaMeIOS 4 | // 5 | // Created by Saroar Khandoker on 16.10.2020. 6 | // 7 | 8 | import SwiftUI 9 | import Combine 10 | import MapKit 11 | 12 | class TestViewModel: ObservableObject { 13 | @Published var questions = [Int: String]() 14 | 15 | var count: Int = 0 16 | 17 | init() { 18 | self.questions = [ 19 | 0: "Hello", 20 | 1: "Private", 21 | 2: "Asalamualikum" 22 | ] 23 | } 24 | 25 | func add() { 26 | count = self.questions.count 27 | count += 1 28 | self.questions.updateValue("Hello Alif \(count)", forKey: 0) 29 | self.questions[count] = "Hi \(count)" 30 | } 31 | } 32 | 33 | struct Navigation_WithList: View { 34 | 35 | @ObservedObject var data: TestViewModel = .init() 36 | 37 | var body: some View { 38 | NavigationView { 39 | ScrollView { 40 | LazyVStack { 41 | ForEach(data.questions.map { $1 }.sorted() , id: \.self) { datum in 42 | NavigationLink(destination: ShoppingDetail(shoppingItem: datum)) { 43 | Text(datum).font(Font.system(size: 24)).padding() 44 | } 45 | } 46 | .listStyle(GroupedListStyle()) 47 | .navigationTitle("Shopping") 48 | .toolbar { 49 | ToolbarItem { 50 | Button("Add", action: {self.data.add()}) 51 | } 52 | 53 | } 54 | } 55 | } 56 | } 57 | 58 | } 59 | 60 | } 61 | 62 | 63 | struct Navigation_WithList_Previews: PreviewProvider { 64 | static var previews: some View { 65 | Navigation_WithList() 66 | } 67 | } 68 | 69 | 70 | struct ShoppingDetail: View { 71 | var shoppingItem: String! 72 | var body: some View { 73 | VStack { 74 | Text("Shopping List Details") 75 | .font(.title) 76 | .frame(maxWidth: .infinity) 77 | .padding() 78 | .background(Color("Theme3ForegroundColor")) 79 | .foregroundColor(Color("Theme3BackgroundColor")) 80 | Spacer() 81 | Text(shoppingItem).font(.title) 82 | Spacer() 83 | 84 | }.navigationTitle(shoppingItem) 85 | 86 | } 87 | 88 | } 89 | -------------------------------------------------------------------------------- /AddaMeIOS/ExprimentViews/TabBarTopView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TabBarTopView.swift 3 | // AddaMeIOS 4 | // 5 | // Created by Saroar Khandoker on 03.09.2020. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct TabBarTopView: View { 11 | @State var search = String.empty 12 | @Binding var expand: Bool 13 | @Binding var searchExpand: Bool 14 | 15 | var body: some View { 16 | VStack(spacing: 20) { 17 | 18 | if self.expand { 19 | 20 | ScrollView(.horizontal, showsIndicators: false) { 21 | 22 | HStack(spacing: 18) { 23 | 24 | Button(action: { 25 | 26 | }) { 27 | Image(systemName: "plus") 28 | .resizable() 29 | .frame(width: 25, height: 25) 30 | .foregroundColor(Color.yellow) 31 | .padding(18) 32 | 33 | }.background(Color.red) 34 | .clipShape(Circle()) 35 | 36 | ForEach(1...7, id: \.self) { i in 37 | 38 | Button(action: { 39 | 40 | }) { 41 | Image(systemName: "ant.circle") 42 | .resizable() 43 | .renderingMode(.original) 44 | .frame(width: 60, height: 60) 45 | } 46 | } 47 | } 48 | } 49 | 50 | } 51 | 52 | if self.searchExpand { 53 | HStack(spacing: 15){ 54 | 55 | Image(systemName: "magnifyingglass") 56 | .resizable() 57 | .frame(width: 18, height: 18) 58 | .foregroundColor(Color.black.opacity(0.3)) 59 | 60 | TextField("Search", text: self.$search) 61 | 62 | }.padding() 63 | .background(Color.white) 64 | .cornerRadius(8) 65 | .padding(.bottom, 10) 66 | } 67 | }.padding() 68 | .padding(.top, UIApplication.shared.windows.first?.safeAreaInsets.top) 69 | .background(Color.gray.opacity(0.1)) 70 | .clipShape(shape()) 71 | .animation(.default) 72 | } 73 | } 74 | 75 | var globalBool: Bool = true { 76 | didSet { 77 | // This will get called 78 | NSLog("Did Set" + globalBool.description) 79 | } 80 | } 81 | 82 | struct TabBarTopView_Previews: PreviewProvider { 83 | 84 | static var previews: some View { 85 | TabBarTopView(expand: 86 | Binding(get: { globalBool }, set: { globalBool = $0 }), 87 | searchExpand: Binding(get: { globalBool }, set: { globalBool = $0 }) 88 | ) 89 | } 90 | } 91 | 92 | struct shape: Shape { 93 | func path(in rect: CGRect) -> Path { 94 | let path = UIBezierPath(roundedRect: rect, byRoundingCorners: [.bottomLeft, .bottomRight], cornerRadii: CGSize(width: 22, height: 22) ) 95 | 96 | return Path(path.cgPath) 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /AddaMeIOS/Extension/Array+RemoveDuplicate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Array+RemoveDuplicate.swift 3 | // AddaMeIOS 4 | // 5 | // Created by Saroar Khandoker on 17.10.2020. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Array where Element: Hashable { 11 | func uniqElemets() -> [Element] { 12 | let set = Set(self) 13 | return Array(set) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /AddaMeIOS/Extension/AssetExtractor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AssetExtractor.swift 3 | // AddaMeIOS 4 | // 5 | // Created by Saroar Khandoker on 18.10.2020. 6 | // 7 | 8 | import UIKit 9 | 10 | class AssetExtractor { 11 | 12 | static func createLocalUrl(forImageNamed name: String) -> URL? { 13 | 14 | let fileManager = FileManager.default 15 | let cacheDirectory = fileManager.urls(for: .cachesDirectory, in: .userDomainMask)[0] 16 | let url = cacheDirectory.appendingPathComponent("\(name).png") 17 | 18 | guard fileManager.fileExists(atPath: url.path) else { 19 | guard 20 | let image = UIImage(named: name), 21 | let data = image.pngData() 22 | else { 23 | print(#line, self, "cant find image by name: \(name)") 24 | return nil 25 | 26 | } 27 | 28 | fileManager.createFile(atPath: url.path, contents: data, attributes: nil) 29 | return url 30 | } 31 | 32 | return url 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /AddaMeIOS/Extension/Codable+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Codable+Extension.swift 3 | // AddaMeIOS 4 | // 5 | // Created by Saroar Khandoker on 28.09.2020. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Encodable { 11 | var jsonData: Data? { 12 | let encoder = JSONEncoder() 13 | encoder.dateEncodingStrategy = .iso8601 14 | do { 15 | return try encoder.encode(self) 16 | } catch { 17 | print(error.localizedDescription) 18 | return nil 19 | } 20 | } 21 | 22 | var jsonString: String? { 23 | guard let data = self.jsonData else { return nil } 24 | return String(data: data, encoding: .utf8) 25 | } 26 | } 27 | 28 | extension Decodable { 29 | static func from(json: String, using encoding: String.Encoding = .utf8) -> Self? { 30 | guard let data = json.data(using: encoding) else { return nil } 31 | return Self.from(data: data) 32 | } 33 | 34 | static func from(data: Data) -> Self? { 35 | do { 36 | let jsonDecoder = JSONDecoder() 37 | jsonDecoder.dateDecodingStrategy = .iso8601 38 | return try JSONDecoder().decode(Self.self, from: data) 39 | } catch { 40 | print(error.localizedDescription) 41 | return nil 42 | } 43 | } 44 | 45 | static func decode(json: String, using usingForWebRtcingEncoding: String.Encoding = .utf8) -> Self? { 46 | guard let data = json.data(using: usingForWebRtcingEncoding) else { return nil } 47 | return Self.decode(data: data) 48 | } 49 | 50 | static func decode(data usingForWebRtcingData: Data) -> Self? { 51 | let decoder = JSONDecoder() 52 | decoder.dateDecodingStrategy = .iso8601 53 | 54 | do { 55 | return try decoder.decode(Self.self, from: usingForWebRtcingData) 56 | } catch { 57 | print(#line, error) 58 | return nil 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /AddaMeIOS/Extension/DataExtension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DataExtension.swift 3 | // AddaMeIOS 4 | // 5 | // Created by Saroar Khandoker on 29.11.2020. 6 | // 7 | 8 | import Foundation 9 | 10 | public extension Data { 11 | func toHexString() -> String { 12 | return map { String(format: "%02.2hhx", $0) }.joined() 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /AddaMeIOS/Extension/Date/Date+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Date+Extension.swift 3 | // AddaMeIOS 4 | // 5 | // Created by Saroar Khandoker on 03.09.2020. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Date { 11 | var toISO8601String: String? { 12 | ISO8601DateFormatter().string(from: self) 13 | } 14 | 15 | func getFormattedDate(format: String) -> String { 16 | let dateformat = DateFormatter() 17 | dateformat.locale = Locale.current 18 | dateformat.dateFormat = format 19 | return dateformat.string(from: self) 20 | } 21 | 22 | var hour : Int { 23 | let components = Calendar.current.dateComponents([.hour ], from: self) 24 | return components.hour ?? 0 25 | } 26 | 27 | var minute : Int { 28 | let components = Calendar.current.dateComponents([.minute ], from: self) 29 | return components.minute ?? 0 30 | } 31 | 32 | var hourMinuteString : String { 33 | let formatter = DateFormatter() 34 | formatter.dateFormat = "HH:mm" 35 | return formatter.string(from: self) 36 | } 37 | 38 | var dayMonthYear : String { 39 | let formatter = DateFormatter() 40 | formatter.dateFormat = "dd.MM.yyyy" 41 | return formatter.string(from: self) 42 | } 43 | 44 | var dateFormatter: String { 45 | 46 | let dateFormatter = DateFormatter() 47 | dateFormatter.locale = Locale.autoupdatingCurrent 48 | 49 | let timeSinceDateInSconds = Date().timeIntervalSince(self) 50 | let secondInDay: TimeInterval = 24*60*60 51 | 52 | if timeSinceDateInSconds > 7 * secondInDay { 53 | dateFormatter.dateFormat = "MM/dd/yy" 54 | } else if timeSinceDateInSconds > secondInDay { 55 | dateFormatter.dateFormat = "EEEE" 56 | } else { 57 | dateFormatter.timeStyle = .short 58 | dateFormatter.string(from: self) 59 | } 60 | 61 | return dateFormatter.string(from: self) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /AddaMeIOS/Extension/Date/DateFormatter+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DateFormatter+Extension.swift 3 | // AddaMeIOS 4 | // 5 | // Created by Saroar Khandoker on 03.09.2020. 6 | // 7 | 8 | import Foundation 9 | 10 | extension DateFormatter { 11 | static var iso8601DateFormatter: DateFormatter = { 12 | let dateFormatter = DateFormatter() 13 | dateFormatter.locale = Locale.current 14 | dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZZZZZ" 15 | return dateFormatter 16 | }() 17 | } 18 | -------------------------------------------------------------------------------- /AddaMeIOS/Extension/Image+Compress.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Image+Compress.swift 3 | // AddaMeIOS 4 | // 5 | // Created by Saroar Khandoker on 19.11.2020. 6 | // 7 | 8 | import UIKit 9 | import AVFoundation 10 | 11 | extension UIImage { 12 | enum JPEGQuality: CGFloat { 13 | case lowest = 0 14 | case low = 0.25 15 | case medium = 0.5 16 | case high = 0.75 17 | case highest = 1 18 | } 19 | 20 | func compressImage(_ compressionQuality: JPEGQuality? = .medium) -> (Data?, String) { 21 | var unsuported = false 22 | var imageData = Data() 23 | var imageFormat = "jpeg" 24 | 25 | do { 26 | let data = try self.heicData(compressionQuality: compressionQuality!) 27 | imageData = data 28 | imageFormat = "heic" 29 | } catch { 30 | print("Error creating HEIC data: \(error.localizedDescription)") 31 | unsuported = true 32 | } 33 | 34 | if unsuported == true { 35 | 36 | guard let data = self.jpegData(compressionQuality: compressionQuality!.rawValue) else { 37 | return (nil, imageFormat) 38 | } 39 | 40 | imageData = data 41 | 42 | } 43 | 44 | return (imageData, imageFormat) 45 | 46 | } 47 | 48 | } 49 | 50 | extension UIImage { 51 | enum HEICError: Error { 52 | case heicNotSupported 53 | case cgImageMissing 54 | case couldNotFinalize 55 | } 56 | 57 | func heicData(compressionQuality: JPEGQuality) throws -> Data { 58 | let data = NSMutableData() 59 | guard let imageDestination = 60 | CGImageDestinationCreateWithData( 61 | data, AVFileType.heic as CFString, 1, nil 62 | ) 63 | else { 64 | throw HEICError.heicNotSupported 65 | } 66 | 67 | guard let cgImage = self.cgImage else { 68 | throw HEICError.cgImageMissing 69 | } 70 | 71 | let options: NSDictionary = [ 72 | kCGImageDestinationLossyCompressionQuality: compressionQuality.rawValue 73 | ] 74 | 75 | CGImageDestinationAddImage(imageDestination, cgImage, options) 76 | guard CGImageDestinationFinalize(imageDestination) else { 77 | throw HEICError.couldNotFinalize 78 | } 79 | 80 | return data as Data 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /AddaMeIOS/Extension/Publishers+Keyboard.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Publishers+Keyboard.swift 3 | // AddaMeIOS 4 | // 5 | // Created by Saroar Khandoker on 13.09.2020. 6 | // 7 | 8 | import Combine 9 | import UIKit 10 | 11 | extension Publishers { 12 | static var keyboardHeight: AnyPublisher { 13 | let willShow = NotificationCenter.default.publisher(for: UIApplication.keyboardWillShowNotification) 14 | .map { $0.keyboardHeight } 15 | 16 | let willHide = NotificationCenter.default.publisher(for: UIApplication.keyboardWillHideNotification) 17 | .map { _ in CGFloat(0) } 18 | 19 | return MergeMany(willShow, willHide) 20 | .eraseToAnyPublisher() 21 | } 22 | } 23 | 24 | extension Notification { 25 | var keyboardHeight: CGFloat { 26 | return (userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect)?.height ?? 0 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /AddaMeIOS/Extension/RangeReplaceableCollection+AppendIfNotContains.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RangeReplaceableCollection+AppendIfNotContains.swift 3 | // AddaMeIOS 4 | // 5 | // Created by Saroar Khandoker on 22.10.2020. 6 | // 7 | 8 | import Foundation 9 | 10 | extension RangeReplaceableCollection where Element: Equatable { 11 | @discardableResult 12 | mutating func appendIfNotContains(_ element: Element) -> (appended: Bool, memberAfterAppend: Element) { 13 | if let index = firstIndex(of: element) { 14 | return (false, self[index]) 15 | } else { 16 | append(element) 17 | return (true, element) 18 | } 19 | } 20 | } 21 | 22 | extension RangeReplaceableCollection where Element: Equatable { 23 | mutating func prependUnique(_ element: Element) { 24 | if let index = firstIndex(of: element) { 25 | remove(at: index) 26 | } 27 | insert(element, at: startIndex) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /AddaMeIOS/Extension/String+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // String+Extension.swift 3 | // AddaMeIOS 4 | // 5 | // Created by Saroar Khandoker on 18.11.2020. 6 | // 7 | 8 | import Foundation 9 | 10 | extension String { 11 | static var empty: String { return "" } 12 | } 13 | -------------------------------------------------------------------------------- /AddaMeIOS/Extension/String/String+CodingKey.swift: -------------------------------------------------------------------------------- 1 | // 2 | // String+CodingKey.swift 3 | // AddaMeIOS 4 | // 5 | // Created by Saroar Khandoker on 28.09.2020. 6 | // 7 | 8 | import Foundation 9 | 10 | extension String: CodingKey { 11 | public var stringValue: String { 12 | return self 13 | } 14 | 15 | public var intValue: Int? { 16 | return Int(self) 17 | } 18 | 19 | public init?(intValue: Int) { 20 | self.init(intValue.description) 21 | } 22 | 23 | public init?(stringValue: String) { 24 | self.init(stringValue) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /AddaMeIOS/Extension/String/String+Data.swift: -------------------------------------------------------------------------------- 1 | // 2 | // String+Data.swift 3 | // AddaMeIOS 4 | // 5 | // Created by Saroar Khandoker on 02.09.2020. 6 | // 7 | 8 | import Foundation 9 | 10 | extension String { 11 | var stringToData: Data? { 12 | return Data( 13 | base64Encoded: self, 14 | options: Data.Base64DecodingOptions.ignoreUnknownCharacters 15 | ) ?? nil 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /AddaMeIOS/Extension/String/String+Date.swift: -------------------------------------------------------------------------------- 1 | // 2 | // String+Date.swift 3 | // AddaMeIOS 4 | // 5 | // Created by Saroar Khandoker on 29.09.2020. 6 | // 7 | 8 | import Foundation 9 | 10 | extension String { 11 | var toISO8601Date: Date? { 12 | ISO8601DateFormatter().date(from: self) 13 | } 14 | } 15 | 16 | -------------------------------------------------------------------------------- /AddaMeIOS/Extension/String/String+ReplaceCharcters.swift: -------------------------------------------------------------------------------- 1 | // 2 | // String+Replace.swift 3 | // AddaMeIOS 4 | // 5 | // Created by Saroar Khandoker on 24.10.2020. 6 | // 7 | 8 | import Foundation 9 | 10 | extension String { 11 | func replace(target: String, withString: String) -> String { 12 | return self.replacingOccurrences(of: target, with: withString, options: NSString.CompareOptions.literal, range: nil) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /AddaMeIOS/Extension/UIApplicationKeyboardOnDragGestureExtension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIApplicationKeyboardOnDragGestureExtension.swift 3 | // AddaMeIOS 4 | // 5 | // Created by Saroar Khandoker on 30.11.2020. 6 | // 7 | 8 | import SwiftUI 9 | import Foundation 10 | 11 | extension UIApplication { 12 | func endEditing(_ force: Bool) { 13 | self.windows 14 | .filter{$0.isKeyWindow} 15 | .first? 16 | .endEditing(force) 17 | } 18 | } 19 | 20 | struct ResignKeyboardOnDragGesture: ViewModifier { 21 | var gesture = DragGesture().onChanged{_ in 22 | UIApplication.shared.endEditing(true) 23 | } 24 | func body(content: Content) -> some View { 25 | content.gesture(gesture) 26 | } 27 | } 28 | 29 | extension View { 30 | func resignKeyboardOnDragGesture() -> some View { 31 | return modifier(ResignKeyboardOnDragGesture()) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /AddaMeIOS/Extension/UIImageView+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIImageView+Extension.swift 3 | // AddaMeIOS 4 | // 5 | // Created by Saroar Khandoker on 30.08.2020. 6 | // 7 | 8 | import Foundation 9 | import UIKit 10 | 11 | //extension UIImageView { 12 | // func loadImage(at url: URL) { 13 | // UIImageLoader.loader.load(url, for: self) 14 | // } 15 | // 16 | // func cancelImageLoad() { 17 | // UIImageLoader.loader.cancel(for: self) 18 | // } 19 | //} 20 | -------------------------------------------------------------------------------- /AddaMeIOS/Extension/View/View+Extension+HideRowSeparatorModifier.swift: -------------------------------------------------------------------------------- 1 | // 2 | // View+Extension+HideRowSeparatorModifier.swift 3 | // AddaMeIOS 4 | // 5 | // Created by Saroar Khandoker on 27.10.2020. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct HideRowSeparatorModifier: ViewModifier { 11 | 12 | static let defaultListRowHeight: CGFloat = 44 13 | var insets: EdgeInsets 14 | var background: Color 15 | 16 | init(insets: EdgeInsets, background: Color) { 17 | self.insets = insets 18 | var alpha: CGFloat = 0 19 | UIColor(background).getWhite(nil, alpha: &alpha) 20 | assert(alpha == 1, "Setting background to a non-opaque color will result in separators remaining visible.") 21 | self.background = background 22 | } 23 | 24 | func body(content: Content) -> some View { 25 | content 26 | .padding(insets) 27 | .frame( 28 | minWidth: 0, maxWidth: .infinity, 29 | minHeight: Self.defaultListRowHeight, 30 | alignment: .leading 31 | ) 32 | .listRowInsets(EdgeInsets()) 33 | .background(background) 34 | } 35 | } 36 | 37 | extension EdgeInsets { 38 | static let defaultListRowInsets = Self(top: 0, leading: 16, bottom: 0, trailing: 16) 39 | } 40 | 41 | extension View { 42 | func hideRowSeparator( 43 | insets: EdgeInsets = .defaultListRowInsets, 44 | background: Color = Color(.systemBackground) 45 | ) -> some View { 46 | modifier(HideRowSeparatorModifier( 47 | insets: insets, 48 | background: background 49 | )) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /AddaMeIOS/Helper/AppUserDefaults.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppUserDefaults.swift 3 | // AddaMeIOS 4 | // 5 | // Created by Saroar Khandoker on 08.11.2020. 6 | // 7 | 8 | import Foundation 9 | 10 | enum AppUserDefaults {} 11 | 12 | extension AppUserDefaults { 13 | enum Key: String { 14 | case isAuthorized 15 | case currentUser 16 | case token 17 | case cllocation 18 | case distance 19 | case isUserFristNameUpdated 20 | } 21 | } 22 | 23 | extension AppUserDefaults { 24 | 25 | static func save(_ value: Any?, forKey key: Key) { 26 | if value is [Any] { 27 | UserDefaults.standard.setValue(value, forKey: key.rawValue) 28 | } else { 29 | UserDefaults.standard.set(value, forKey: key.rawValue) 30 | } 31 | UserDefaults.standard.synchronize() 32 | } 33 | 34 | static func saveCodable(_ value: T?, forKey key: Key) { 35 | let data = try? JSONEncoder().encode(value) 36 | UserDefaults.standard.set(data, forKey: key.rawValue) 37 | UserDefaults.standard.synchronize() 38 | } 39 | 40 | static func saveCodable(_ value: [T], forKey key: Key) { 41 | let data = try? JSONEncoder().encode(value) 42 | UserDefaults.standard.set(data, forKey: key.rawValue) 43 | UserDefaults.standard.synchronize() 44 | } 45 | 46 | static func value(forKey key: Key) -> T? where T: Decodable { 47 | guard let encodedData = UserDefaults.standard.data(forKey: key.rawValue) else { 48 | return nil 49 | } 50 | return try? JSONDecoder().decode(T.self, from: encodedData) 51 | } 52 | 53 | static func value(forKey key: Key) -> [T] where T: Decodable { 54 | guard let encodedData = UserDefaults.standard.data(forKey: key.rawValue) else { 55 | return [] 56 | } 57 | return (try? JSONDecoder().decode(Array.self, from: encodedData)) ?? [] 58 | } 59 | 60 | static func removeValue(forKey key: Key) { 61 | UserDefaults.standard.removeObject(forKey: key.rawValue) 62 | UserDefaults.standard.synchronize() 63 | } 64 | 65 | static func saveCurrentUserAndToken(_ res: LoginRes) { 66 | KeychainService.save(codable: res.user, for: .currentUser) 67 | KeychainService.save(codable: res.access, for: .token) 68 | } 69 | 70 | static func resetAuthData() { 71 | save(false, forKey: .isAuthorized) 72 | KeychainService.save(codable: CurrentUser?.none, for: .currentUser) 73 | KeychainService.save(codable: Access?.none, for: .token) 74 | KeychainService.logout() 75 | erase() 76 | } 77 | 78 | static func erase() { 79 | let appDomain = Bundle.main.bundleIdentifier! 80 | UserDefaults.standard.removePersistentDomain(forName: appDomain) 81 | UserDefaults.standard.synchronize() 82 | } 83 | 84 | } 85 | -------------------------------------------------------------------------------- /AddaMeIOS/Helper/AwsS3Manager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AwsS3Manager.swift 3 | // AddaMeIOS 4 | // 5 | // Created by Saroar Khandoker on 18.11.2020. 6 | // 7 | 8 | import UIKit 9 | import S3 10 | import NIO 11 | import AVFoundation 12 | 13 | class AWSS3Helper { 14 | 15 | static var bucketWithEndpoint = "https://adda.nyc3.digitaloceanspaces.com/" 16 | static private let compressionQueue = OperationQueue() 17 | 18 | static var getCurrentMillis: Int64 { 19 | return Int64(Date().timeIntervalSince1970 * 1000) 20 | } 21 | 22 | static func uploadImage( 23 | _ image: UIImage, 24 | conversationId: String? = nil, 25 | userId: String? = nil, 26 | completion: @escaping (String?) -> ()) { 27 | 28 | guard let currentUSER : CurrentUser = KeychainService.loadCodable(for: .currentUser) else { 29 | print(#line, "Missing current user from KeychainService") 30 | return 31 | } 32 | 33 | let s3 = S3.init( 34 | accessKeyId: EnvironmentKeys.accessKeyId, 35 | secretAccessKey: EnvironmentKeys.secretAccessKey, 36 | region: .useast1, endpoint: "https://nyc3.digitaloceanspaces.com" 37 | ) 38 | 39 | 40 | let data = image.compressImage(conversationId == nil ? .highest : .medium) 41 | let imageFormat = data.1 42 | guard let imageData = data.0 else { 43 | completion(nil) 44 | return 45 | } 46 | 47 | let currentTime = AWSS3Helper.getCurrentMillis 48 | var imageKey = String(format: "%ld", currentTime) 49 | if conversationId != nil { 50 | imageKey = "uploads/images/\(conversationId!)/\(imageKey).\(imageFormat)" 51 | } else if userId != nil { 52 | imageKey = "uploads/images/\(userId!)/\(imageKey).\(imageFormat)" 53 | } else { 54 | imageKey = "uploads/images/\(currentUSER.id)_\(imageKey).\(imageFormat) " 55 | } 56 | 57 | // Put an Object 58 | let putObjectRequest = S3.PutObjectRequest( 59 | acl: .publicRead, 60 | body: imageData, 61 | bucket: "adda", 62 | contentLength: Int64(imageData.count), 63 | key: imageKey 64 | ) 65 | 66 | let futureOutput = s3.putObject(putObjectRequest) 67 | 68 | futureOutput.whenSuccess({ (response) in 69 | print(#line, self, response, imageKey) 70 | let finalURL = bucketWithEndpoint + imageKey 71 | completion(finalURL) 72 | }) 73 | 74 | futureOutput.whenFailure({ (error) in 75 | print(#line, self, error) 76 | completion(nil) 77 | }) 78 | 79 | } 80 | 81 | } 82 | -------------------------------------------------------------------------------- /AddaMeIOS/Helper/Bundle+Decode.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Bundle+Decode.swift 3 | // AddaMeIOS 4 | // 5 | // Created by Saroar Khandoker on 06.11.2020. 6 | // 7 | 8 | import UIKit 9 | 10 | extension Bundle { 11 | func decode(_ type: T.Type, from file: String) -> T { 12 | guard let url = self.url(forResource: file, withExtension: nil) else { 13 | fatalError("Failed to locate \(file) in bundle.") 14 | } 15 | 16 | guard let data = try? Data(contentsOf: url) else { 17 | fatalError("Failed to load \(file) from bundle.") 18 | } 19 | 20 | let decoder = JSONDecoder() 21 | 22 | guard let loaded = try? decoder.decode(T.self, from: data) else { 23 | fatalError("Failed to decode \(file) from bundle.") 24 | } 25 | 26 | return loaded 27 | } 28 | } 29 | 30 | // will use featch demo data 31 | //func load() { 32 | // DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) { 33 | // let sections = Bundle.main.decode([MenuSection].self, from: "menu.json") 34 | // self.sections = sections 35 | // self.representativeSample = [sections[0].items[0], sections[1].items[2], sections[2].items[2]] 36 | // } 37 | //} 38 | 39 | -------------------------------------------------------------------------------- /AddaMeIOS/Helper/DateHelper.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DateHelper.swift 3 | // AddaMeIOS 4 | // 5 | // Created by Saroar Khandoker on 03.09.2020. 6 | // 7 | 8 | import Foundation 9 | 10 | public class DateHelper { 11 | private static let formatter = DateFormatter() 12 | private static let timeZone = TimeZone.current 13 | private static let timeLocale = Locale.current 14 | 15 | public static func fromISO8601String(_ dateString: String) -> Date? { 16 | formatter.timeZone = TimeZone.current 17 | formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ" 18 | return formatter.date(from: dateString) 19 | } 20 | 21 | public static func toISO8601String(_ date: Date) -> String { 22 | formatter.timeZone = TimeZone.current 23 | formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ" 24 | return formatter.string(from: date) 25 | } 26 | 27 | static func dateFromString(_ dateAsString: String?) -> Date? { 28 | guard let string = dateAsString else { return nil } 29 | formatter.timeZone = TimeZone.current 30 | formatter.dateFormat = "yyyy-MM-dd HH:mm:ss.SSSS" 31 | let val = formatter.date(from: string) 32 | return val 33 | } 34 | 35 | static func dateToString(_ dateIn: Date?) -> String? { 36 | guard let date = dateIn else { return nil } 37 | formatter.timeZone = TimeZone.current 38 | formatter.dateFormat = "yyyy-MM-dd HH:mm:ss.SSSS" 39 | let val = formatter.string(from: date) 40 | return val 41 | } 42 | 43 | static func getDateFromTimeStamp(timeStamp : Double) -> String { 44 | 45 | let date = NSDate(timeIntervalSince1970: timeStamp) 46 | let dateFormatter = DateFormatter() 47 | dateFormatter.timeStyle = DateFormatter.Style.none //Set time style 48 | dateFormatter.dateStyle = DateFormatter.Style.short 49 | 50 | // UnComment below to get only time 51 | // dayTimePeriodFormatter.dateFormat = "hh:mm a" 52 | 53 | let dateString = dateFormatter.string(from: date as Date) 54 | return dateString 55 | } 56 | 57 | static func dateFormatter(timestamp: Double?) -> String? { 58 | if let timestamp = timestamp { 59 | let date = Date(timeIntervalSinceReferenceDate: timestamp) 60 | let dateFormatter = DateFormatter() 61 | dateFormatter.locale = Locale.autoupdatingCurrent 62 | let timeSinceDateInSconds = Date().timeIntervalSince(date) 63 | let secondInDays: TimeInterval = 24*60*60 64 | 65 | if timeSinceDateInSconds > 7 * secondInDays { 66 | dateFormatter.dateFormat = "MM/dd/yy" 67 | } else if timeSinceDateInSconds > secondInDays { 68 | dateFormatter.dateFormat = "EEEE" 69 | } else { 70 | dateFormatter.dateFormat = "HH:mm" 71 | } 72 | return dateFormatter.string(from: date) 73 | } else { 74 | return nil 75 | } 76 | } 77 | } 78 | 79 | extension Date { 80 | var millisecondsSince1970: Int64 { 81 | return Int64((self.timeIntervalSince1970 * 1000.0).rounded()) 82 | } 83 | 84 | init(milliseconds: Int64) { 85 | self = Date(timeIntervalSince1970: TimeInterval(milliseconds / 1000)) 86 | } 87 | 88 | func adding(minutes: Int) -> Date { 89 | return Calendar.current.date(byAdding: .minute, value: minutes, to: self)! 90 | } 91 | 92 | } 93 | 94 | let mediaDurationFormatter: DateComponentsFormatter = { 95 | let formatter = DateComponentsFormatter() 96 | formatter.allowedUnits = [.minute, .second] 97 | formatter.zeroFormattingBehavior = [.pad] 98 | formatter.unitsStyle = .positional 99 | return formatter 100 | }() 101 | 102 | let millisecondsPerSecond: Double = 1000 103 | //let nanosecondsPerSecond: CMTimeScale = 1000000000 104 | -------------------------------------------------------------------------------- /AddaMeIOS/Helper/Image/AsyncImage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AsyncImage.swift 3 | // AddaMeIOS 4 | // 5 | // Created by Saroar Khandoker on 31.08.2020. 6 | // 7 | 8 | import SwiftUI 9 | import Combine 10 | 11 | struct AsyncImage: View { 12 | 13 | @StateObject private var loader: ImageLoader 14 | private var placeholder: Placeholder 15 | private var image: (UIImage) -> Image 16 | 17 | init( 18 | url: URL, 19 | @ViewBuilder placeholder: () -> Placeholder, 20 | @ViewBuilder image: @escaping (UIImage) -> Image = Image.init(uiImage:) 21 | ) { 22 | self.placeholder = placeholder() 23 | self.image = image 24 | _loader = StateObject(wrappedValue: ImageLoader(url: url, cache: Environment(\.imageCache).wrappedValue)) 25 | } 26 | 27 | init( 28 | urlString: String?, 29 | @ViewBuilder placeholder: () -> Placeholder, 30 | @ViewBuilder image: @escaping (UIImage) -> Image = Image.init(uiImage:) 31 | ) { 32 | 33 | var url = URL(string: String.empty) 34 | if urlString == nil { 35 | if let fileURL = AssetExtractor.createLocalUrl(forImageNamed: "Avatar") { 36 | url = fileURL 37 | } 38 | } else { 39 | url = URL(string: urlString!)! 40 | } 41 | 42 | self.placeholder = placeholder() 43 | self.image = image 44 | _loader = StateObject( 45 | wrappedValue: ImageLoader( 46 | url: url!, 47 | cache: Environment(\.imageCache).wrappedValue 48 | ) 49 | ) 50 | } 51 | 52 | var body: some View { 53 | content 54 | .onAppear(perform: loader.load) 55 | } 56 | 57 | private var content: some View { 58 | Group { 59 | if loader.image != nil { 60 | image(loader.image!) 61 | } else { 62 | placeholder 63 | } 64 | } 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /AddaMeIOS/Helper/Image/EnvironmentValues+ImageCache.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EnvironmentValues+ImageCache.swift 3 | // AddaMeIOS 4 | // 5 | // Created by Saroar Khandoker on 31.08.2020. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct ImageCacheKey: EnvironmentKey { 11 | static let defaultValue: ImageCache = TemporaryImageCache() 12 | } 13 | 14 | extension EnvironmentValues { 15 | var imageCache: ImageCache { 16 | get { self[ImageCacheKey.self] } 17 | set { self[ImageCacheKey.self] = newValue } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /AddaMeIOS/Helper/Image/ImageCache.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImageCache.swift 3 | // AddaMeIOS 4 | // 5 | // Created by Saroar Khandoker on 31.08.2020. 6 | // 7 | 8 | import SwiftUI 9 | 10 | protocol ImageCache { 11 | subscript(_ url: URL) -> UIImage? { get set } 12 | } 13 | 14 | struct TemporaryImageCache: ImageCache { 15 | 16 | private let cache = NSCache() 17 | 18 | subscript(key: URL) -> UIImage? { 19 | get { cache.object(forKey: key as NSURL) } 20 | set { 21 | newValue == nil ? cache.removeObject(forKey: key as NSURL) : cache.setObject(newValue!, forKey: key as NSURL) 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /AddaMeIOS/Helper/Image/ImageDraw.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImageDraw.swift 3 | // AddaMeIOS 4 | // 5 | // Created by Saroar Khandoker on 22.11.2020. 6 | // 7 | 8 | import Foundation 9 | import UIKit 10 | 11 | class ImageDraw { 12 | private let renderer = UIGraphicsImageRenderer(size: CGSize(width: 400, height: 400)) 13 | private var colors = [UIColor.red, UIColor.brown, UIColor.yellow, UIColor.green, UIColor.black, UIColor.blue] 14 | 15 | func random() -> UIImage { 16 | 17 | colors.shuffle() 18 | 19 | let image = renderer.image { (context) in 20 | UIColor.darkGray.setStroke() 21 | context.stroke(renderer.format.bounds) 22 | 23 | let count = 400 / colors.count 24 | 25 | colors.enumerated().forEach { (idx, color) in 26 | color.setFill() 27 | context.fill(CGRect(x: idx * count, y: 0, width: idx * count, height: 400)) 28 | } 29 | } 30 | 31 | return image 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /AddaMeIOS/Helper/Image/ImageLoader.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImageLoader.swift 3 | // AddaMeIOS 4 | // 5 | // Created by Saroar Khandoker on 30.08.2020. 6 | // 7 | 8 | import SwiftUI 9 | import Combine 10 | import Foundation 11 | 12 | class ImageLoader: ObservableObject { 13 | 14 | @Published var image: UIImage? 15 | private let url: URL 16 | private var cancellable: AnyCancellable? 17 | private var cache: ImageCache? 18 | private(set) var isLodaing = false 19 | private static let imageProcessingQueue = DispatchQueue(label: "image-processing") 20 | 21 | init(url: URL, cache: ImageCache? = nil) { 22 | self.url = url 23 | self.cache = cache 24 | } 25 | 26 | deinit { 27 | cancel() 28 | } 29 | 30 | func load() { 31 | 32 | guard !isLodaing else { return } 33 | 34 | if let image = cache?[url] { 35 | self.image = image 36 | return 37 | } 38 | 39 | cancellable = URLSession.shared.dataTaskPublisher(for: url) 40 | .map { UIImage(data: $0.data) } 41 | .replaceError(with: nil) 42 | .handleEvents( 43 | receiveSubscription: { _ in self.onStart() }, 44 | receiveOutput: { self.cache($0) }, 45 | receiveCompletion: { _ in self.onFinished() }, 46 | receiveCancel: { self.onFinished() } 47 | ) 48 | .subscribe(on: Self.imageProcessingQueue) 49 | .receive(on: DispatchQueue.main) 50 | .sink(receiveValue: { [weak self] result in 51 | self?.image = result 52 | }) 53 | 54 | } 55 | 56 | func cancel() { 57 | cancellable?.cancel() 58 | } 59 | 60 | private func onStart() { 61 | isLodaing = true 62 | } 63 | 64 | private func onFinished() { 65 | isLodaing = false 66 | } 67 | 68 | private func cache(_ image: UIImage?) { 69 | image.map { cache?[url] = $0 } 70 | } 71 | 72 | } 73 | -------------------------------------------------------------------------------- /AddaMeIOS/Helper/Image/ImagePicker.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImagePicker.swift 3 | // AddaMeIOS 4 | // 5 | // Created by Saroar Khandoker on 18.11.2020. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | 11 | struct ImagePicker: UIViewControllerRepresentable { 12 | @Environment(\.presentationMode) var presentationMode 13 | @Binding var image: UIImage? 14 | 15 | func makeUIViewController(context: UIViewControllerRepresentableContext) -> UIImagePickerController { 16 | let picker = UIImagePickerController() 17 | picker.delegate = context.coordinator 18 | return picker 19 | } 20 | 21 | func updateUIViewController(_ uiViewController: UIImagePickerController, context: UIViewControllerRepresentableContext) { 22 | 23 | } 24 | 25 | func makeCoordinator() -> Coordinator { 26 | Coordinator(self) 27 | } 28 | 29 | class Coordinator: NSObject, UINavigationControllerDelegate, UIImagePickerControllerDelegate { 30 | let parent: ImagePicker 31 | 32 | init(_ parent: ImagePicker) { 33 | self.parent = parent 34 | } 35 | 36 | func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) { 37 | 38 | if let uiImage = info[.originalImage] as? UIImage { 39 | parent.image = uiImage 40 | } 41 | 42 | parent.presentationMode.wrappedValue.dismiss() 43 | 44 | } 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /AddaMeIOS/Helper/Keyboard/AdaptsToSoftwareKeyboard.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AdaptsToSoftwareKeyboard.swift 3 | // AddaMeIOS 4 | // 5 | // Created by Saroar Khandoker on 18.11.2020. 6 | // 7 | 8 | import SwiftUI 9 | import Combine 10 | 11 | struct AdaptsToSoftwareKeyboard: ViewModifier { 12 | @State var currentHeight: CGFloat = 0 13 | 14 | func body(content: Content) -> some View { 15 | content 16 | .padding(.bottom, currentHeight) 17 | .animation(.default) 18 | .edgesIgnoringSafeArea(currentHeight == 0 ? [] : .bottom) 19 | .onAppear(perform: subscribeToKeyboardEvents) 20 | } 21 | 22 | private func subscribeToKeyboardEvents() { 23 | NotificationCenter.Publisher( 24 | center: NotificationCenter.default, 25 | name: UIResponder.keyboardWillShowNotification 26 | ).compactMap { notification in 27 | notification.userInfo?["UIKeyboardFrameEndUserInfoKey"] as? CGRect 28 | }.map { rect in 29 | rect.height 30 | }.subscribe(Subscribers.Assign(object: self, keyPath: \.currentHeight)) 31 | 32 | NotificationCenter.Publisher( 33 | center: NotificationCenter.default, 34 | name: UIResponder.keyboardWillHideNotification 35 | ).compactMap { notification in 36 | CGFloat.zero 37 | }.subscribe(Subscribers.Assign(object: self, keyPath: \.currentHeight)) 38 | } 39 | } 40 | 41 | -------------------------------------------------------------------------------- /AddaMeIOS/Helper/Keyboard/KeyboardAdaptive.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KeyboardAdaptive.swift 3 | // AddaMeIOS 4 | // 5 | // Created by Saroar Khandoker on 13.09.2020. 6 | // 7 | 8 | import Combine 9 | import SwiftUI 10 | 11 | struct KeyboardAdaptive: ViewModifier { 12 | @State private var bottomPadding: CGFloat = 0 13 | 14 | func body(content: Content) -> some View { 15 | GeometryReader { geometry in 16 | content 17 | .padding(.bottom, self.bottomPadding) 18 | .onReceive(Publishers.keyboardHeight) { keyboardHeight in 19 | let keyboardTop = geometry.frame(in: .global).height - keyboardHeight 20 | let focusedTextInputBottom = UIResponder.currentFirstResponder?.globalFrame?.maxY ?? 0 21 | 22 | self.bottomPadding = max(0, focusedTextInputBottom - keyboardTop - geometry.safeAreaInsets.bottom) 23 | } 24 | .animation(.easeOut(duration: 0.16)) 25 | } 26 | } 27 | } 28 | 29 | extension View { 30 | func keyboardAdaptive() -> some View { 31 | ModifiedContent(content: self, modifier: KeyboardAdaptive()) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /AddaMeIOS/Helper/Keyboard/UIResponder+Current.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIResponder+Current.swift 3 | // AddaMeIOS 4 | // 5 | // Created by Saroar Khandoker on 13.09.2020. 6 | // 7 | 8 | import Foundation 9 | import UIKit 10 | 11 | extension UIResponder { 12 | static var currentFirstResponder: UIResponder? { 13 | _currentFirstResponder = nil 14 | UIApplication.shared.sendAction(#selector(UIResponder.findFirstResponder(_:)), to: nil, from: nil, for: nil) 15 | return _currentFirstResponder 16 | } 17 | 18 | private static weak var _currentFirstResponder: UIResponder? 19 | 20 | @objc private func findFirstResponder(_ sendr: Any) { 21 | UIResponder._currentFirstResponder = self 22 | } 23 | 24 | var globalFrame: CGRect? { 25 | guard let view = self as? UIView else { return nil } 26 | return view.superview?.convert(view.frame, to: nil) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /AddaMeIOS/Helper/ModalView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ModalView.swift 3 | // AddaMeIOS 4 | // 5 | // Created by Saroar Khandoker on 14.09.2020. 6 | // 7 | 8 | import SwiftUI 9 | 10 | private final class Pipe : ObservableObject { 11 | struct Content: Identifiable { 12 | fileprivate typealias ID = String 13 | fileprivate let id = UUID().uuidString 14 | var view: AnyView 15 | } 16 | @Published var content: Content? = nil 17 | } 18 | 19 | public struct ModalPresenter : View where Content : View { 20 | @ObservedObject private var modalView = Pipe() 21 | 22 | private var content: Content 23 | 24 | public init(@ViewBuilder content: () -> Content) { 25 | self.content = content() 26 | } 27 | 28 | public var body: some View { 29 | content 30 | .environmentObject(modalView) 31 | .sheet(item: $modalView.content, content: { $0.view }) 32 | } 33 | } 34 | 35 | public struct ModalLink : View where Label : View, Destination : View { 36 | public typealias DestinationBuilder = (_ dismiss: @escaping() -> ()) -> Destination 37 | @EnvironmentObject private var modalView: Pipe 38 | 39 | private enum DestinationProvider { 40 | case view(AnyView) 41 | case builder(DestinationBuilder) 42 | } 43 | 44 | private var destinationProvider: DestinationProvider 45 | private var label: Label 46 | 47 | // Default initializer 48 | public init(destination: Destination, @ViewBuilder label: () -> Label) { 49 | self.destinationProvider = .view(AnyView(destination)) 50 | self.label = label() 51 | } 52 | 53 | // Use this initializer when `dismiss` method is needed in the modal view 54 | public init(@ViewBuilder destination: @escaping DestinationBuilder, @ViewBuilder label: () -> Label) { 55 | self.destinationProvider = .builder(destination) 56 | self.label = label() 57 | } 58 | 59 | public var body: some View { 60 | Button(action: presentModalView){ label } 61 | } 62 | 63 | private func presentModalView() { 64 | modalView.content = Pipe.Content(view: { 65 | switch destinationProvider { 66 | case let .view(view): 67 | return view 68 | case let .builder(build): 69 | return AnyView(build { self.dismissModalView() }) 70 | } 71 | }()) 72 | } 73 | 74 | private func dismissModalView() { 75 | modalView.content = nil 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /AddaMeIOS/Helper/MongoDBObjectID.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MongoDBObjectID.swift 3 | // AddaMeIOS 4 | // 5 | // Created by Saroar Khandoker on 21.10.2020. 6 | // 7 | 8 | import Foundation 9 | 10 | class ObjectId { 11 | private init() {} 12 | static let shared = ObjectId() 13 | 14 | private var counter = Int.random(in: 0...0xffffff) 15 | 16 | private func incrementCounter() { 17 | if (counter >= 0xffffff) { 18 | counter = 0 19 | } else { 20 | counter += 1 21 | } 22 | } 23 | 24 | func generate() -> String { 25 | let time = ~(~Int(NSDate().timeIntervalSince1970)) 26 | let random = Int.random(in: 0...0xffffffffff) 27 | let i = counter 28 | incrementCounter() 29 | 30 | var byteArray = Array.init(repeating: 0, count: 12) 31 | 32 | byteArray[0] = UInt8((time >> 24) & 0xff) 33 | byteArray[1] = UInt8((time >> 16) & 0xff) 34 | byteArray[2] = UInt8((time >> 8) & 0xff) 35 | byteArray[3] = UInt8(time & 0xff) 36 | byteArray[4] = UInt8((random >> 32) & 0xff) 37 | byteArray[5] = UInt8((random >> 24) & 0xff) 38 | byteArray[6] = UInt8((random >> 16) & 0xff) 39 | byteArray[7] = UInt8((random >> 8) & 0xff) 40 | byteArray[8] = UInt8(random & 0xff) 41 | byteArray[9] = UInt8((i >> 16) & 0xff) 42 | byteArray[10] = UInt8((i >> 8) & 0xff) 43 | byteArray[11] = UInt8(i & 0xff) 44 | 45 | let id = byteArray 46 | .map({ String($0, radix: 16, uppercase: false) 47 | .padding(toLength: 2, withPad: "0", startingAt: 0) }) 48 | .joined() 49 | 50 | return id 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /AddaMeIOS/Helper/SwiftUIFormHelper/KeyboardDismissModifier.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KeyboardDismissModifier.swift 3 | // AddaMeIOS 4 | // 5 | // Created by Saroar Khandoker on 07.09.2020. 6 | // 7 | 8 | import SwiftUI 9 | 10 | @available(iOS 13, *) 11 | public struct KeyboardDismissModifier: ViewModifier { 12 | 13 | public func body(content: Content) -> some View { 14 | content.onTapGesture { 15 | UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil) 16 | } 17 | } 18 | } 19 | 20 | @available(iOS 13, *) 21 | extension TextField { 22 | /// Dismiss the keyboard when pressing on something different then a form field 23 | /// - Returns: KeyboardDismissModifier 24 | public func hideKeyboardOnTap() -> ModifiedContent { 25 | return modifier(KeyboardDismissModifier()) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /AddaMeIOS/Helper/SwiftUIFormHelper/KeyboardOffsetModifier.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KeyboardOffsetModifier.swift 3 | // AddaMeIOS 4 | // 5 | // Created by Saroar Khandoker on 07.09.2020. 6 | // 7 | 8 | import SwiftUI 9 | 10 | @available(iOS 13, *) 11 | public struct KeyboardOffsetModifier: ViewModifier { 12 | @State private var keyboardOffset: CGFloat = 0 13 | 14 | public func body(content: Content) -> some View { 15 | content 16 | .padding(.bottom, keyboardOffset) 17 | .onAppear { 18 | NotificationCenter.default.addObserver(forName: UIResponder.keyboardWillShowNotification, object: nil, queue: .main) { notif in 19 | guard let value = notif.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect else { return } 20 | let bottomSafeAreaInset: CGFloat = UIApplication.shared.windows.first?.safeAreaInsets.bottom ?? 0 21 | self.keyboardOffset = value.height - bottomSafeAreaInset 22 | } 23 | 24 | NotificationCenter.default.addObserver(forName: UIResponder.keyboardWillHideNotification, object: nil, queue: .main) { notif in 25 | self.keyboardOffset = 0 26 | } 27 | } 28 | } 29 | } 30 | 31 | @available(iOS 13, *) 32 | extension View { 33 | /// Create some offset for the keyboard when visible 34 | /// - Returns: KeyboardOffsetModifier 35 | public func enableKeyboardOffset() -> ModifiedContent { 36 | return modifier(KeyboardOffsetModifier()) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /AddaMeIOS/Helper/View/LazyView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LazyView.swift 3 | // AddaMeIOS 4 | // 5 | // Created by Saroar Khandoker on 24.10.2020. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct LazyView: View { 11 | let build: () -> Content 12 | init(_ build: @autoclosure @escaping () -> Content) { 13 | self.build = build 14 | } 15 | var body: Content { 16 | build() 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /AddaMeIOS/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ACCESS_KEY_ID 6 | $(ACCESS_KEY_ID) 7 | CFBundleDevelopmentRegion 8 | $(DEVELOPMENT_LANGUAGE) 9 | CFBundleDisplayName 10 | Adda2 11 | CFBundleExecutable 12 | $(EXECUTABLE_NAME) 13 | CFBundleIdentifier 14 | $(PRODUCT_BUNDLE_IDENTIFIER) 15 | CFBundleInfoDictionaryVersion 16 | 6.0 17 | CFBundleName 18 | $(PRODUCT_NAME) 19 | CFBundlePackageType 20 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 21 | CFBundleShortVersionString 22 | $(MARKETING_VERSION) 23 | CFBundleVersion 24 | $(CURRENT_PROJECT_VERSION) 25 | LSRequiresIPhoneOS 26 | 27 | NSAppTransportSecurity 28 | 29 | NSAllowsArbitraryLoads 30 | 31 | 32 | NSContactsUsageDescription 33 | Adda2 uses your contacts to find users you konw. We do not store your contacts on the server. 34 | NSLocationAlwaysAndWhenInUseUsageDescription 35 | We dont share your location with anyone, with share your locatuon you can create and see other user events/hangouts nearby you and other user can see your. 36 | NSLocationWhenInUseUsageDescription 37 | We dont share your location with anyone, with share your locatuon you can create and see other user events/hangouts nearby you and other user can see your. 38 | ROOT_URL 39 | $(ROOT_URL) 40 | SECRET_ACCESS_KEY 41 | $(SECRET_ACCESS_KEY) 42 | UIApplicationSceneManifest 43 | 44 | UIApplicationSupportsMultipleScenes 45 | 46 | UISceneConfigurations 47 | 48 | UIWindowSceneSessionRoleApplication 49 | 50 | 51 | UISceneConfigurationName 52 | Default Configuration 53 | UISceneDelegateClassName 54 | $(PRODUCT_MODULE_NAME).SceneDelegate 55 | 56 | 57 | 58 | 59 | UIApplicationSupportsIndirectInputEvents 60 | 61 | UIBackgroundModes 62 | 63 | audio 64 | fetch 65 | remote-notification 66 | voip 67 | 68 | UILaunchStoryboardName 69 | LaunchScreen 70 | UIRequiredDeviceCapabilities 71 | 72 | armv7 73 | 74 | UISupportedInterfaceOrientations 75 | 76 | UIInterfaceOrientationPortrait 77 | UIInterfaceOrientationLandscapeLeft 78 | UIInterfaceOrientationLandscapeRight 79 | 80 | UISupportedInterfaceOrientations~ipad 81 | 82 | UIInterfaceOrientationPortrait 83 | UIInterfaceOrientationPortraitUpsideDown 84 | UIInterfaceOrientationLandscapeLeft 85 | UIInterfaceOrientationLandscapeRight 86 | 87 | WEB_SOCKET_URL 88 | $(WEB_SOCKET_URL) 89 | 90 | 91 | -------------------------------------------------------------------------------- /AddaMeIOS/Map/LocationQuery.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LocationQuery.swift 3 | // AddaMeIOS 4 | // 5 | // Created by Saroar Khandoker on 07.11.2020. 6 | // 7 | 8 | import Foundation 9 | import MapKit 10 | import Combine 11 | 12 | final class LocationQuery: ObservableObject { 13 | 14 | @Published var searchQuery = String.empty 15 | @Published private(set) var searchResults: [MKMapItem] = [] 16 | 17 | private var subscriptions: Set = [] 18 | private let region: MKCoordinateRegion 19 | 20 | init(region: MKCoordinateRegion) { 21 | self.region = region 22 | $searchQuery 23 | .debounce(for: .milliseconds(150), scheduler: RunLoop.main) 24 | .removeDuplicates() 25 | .sink { [weak self] value in 26 | let searchRequest = MKLocalSearch.Request() 27 | searchRequest.naturalLanguageQuery = value 28 | searchRequest.region = region 29 | let search = MKLocalSearch(request: searchRequest) 30 | search.start { response, error in 31 | guard let response = response else { 32 | if let error = error { 33 | print(error.localizedDescription) 34 | } 35 | return 36 | } 37 | self?.searchResults = response.mapItems 38 | } 39 | } 40 | .store(in: &subscriptions) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /AddaMeIOS/Map/LocationSearchService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LocationSearchService.swift 3 | // AddaMeIOS 4 | // 5 | // Created by Saroar Khandoker on 18.09.2020. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | import MapKit 11 | import Combine 12 | 13 | class LocationSearchService: NSObject, ObservableObject, MKLocalSearchCompleterDelegate { 14 | 15 | enum LocationStatus: Equatable { 16 | case idle 17 | case noResults 18 | case isSearching 19 | case error(String) 20 | case result 21 | } 22 | 23 | @Published var searchQuery = String.empty 24 | @Published var completions: [MKLocalSearchCompletion] = [] 25 | @Published private(set) var status: LocationStatus = .idle 26 | @Published var mapItems: [MKMapItem] = [] 27 | @Published var coordinate: CLLocationCoordinate2D? 28 | // @State var eventPlaec: EventPlace = EventPlace.defualtInit 29 | 30 | var completer: MKLocalSearchCompleter 31 | var cancellable: AnyCancellable? 32 | 33 | private let locationManager = CLLocationManager() 34 | @EnvironmentObject var eventViewModel: EventViewModel 35 | 36 | func askLocationPermission() { 37 | self.locationManager.delegate = self 38 | self.locationManager.desiredAccuracy = kCLLocationAccuracyBest 39 | self.locationManager.requestWhenInUseAuthorization() 40 | // self.locationManager.startUpdatingLocation() 41 | } 42 | 43 | override init() { 44 | completer = MKLocalSearchCompleter() 45 | completer.resultTypes = .address 46 | 47 | super.init() 48 | completer.delegate = self 49 | 50 | cancellable = $searchQuery 51 | .receive(on: DispatchQueue.main) 52 | .debounce(for: .milliseconds(150), scheduler: RunLoop.main, options: nil) 53 | .sink(receiveValue: { fragment in 54 | self.status = .isSearching 55 | if !fragment.isEmpty { 56 | self.completer.queryFragment = fragment 57 | } else { 58 | self.status = .idle 59 | self.mapItems = [] 60 | } 61 | }) 62 | } 63 | 64 | func completerDidUpdateResults(_ completer: MKLocalSearchCompleter) { 65 | //self.completions = completer.results 66 | 67 | self.status = completer.results.isEmpty ? .noResults : .result 68 | self.mapItems = [] 69 | completer.results.forEach { mKLocalSearchCompletion in 70 | 71 | let request = MKLocalSearch.Request() 72 | request.naturalLanguageQuery = mKLocalSearchCompletion.title 73 | //let isEventDetailsPage: Binding = .constant(true) 74 | //request.region = MapViewModel(checkPoint: $checkPoint, isEventDetailsPage: isEventDetailsPage).mapView.region 75 | let search = MKLocalSearch(request: request) 76 | search.start { (response, error) in 77 | guard let response = response else {return} 78 | self.mapItems.append(contentsOf: response.mapItems) 79 | } 80 | } 81 | } 82 | 83 | func completer(_ completer: MKLocalSearchCompleter, didFailWithError error: Error) { 84 | self.status = .error(error.localizedDescription) 85 | } 86 | } 87 | 88 | extension MKLocalSearchCompletion: Identifiable {} 89 | 90 | extension LocationSearchService: CLLocationManagerDelegate { 91 | 92 | func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { 93 | guard let location = locations.last else { return } 94 | self.coordinate = location.coordinate 95 | print(#line, location) 96 | 97 | } 98 | } 99 | 100 | import Contacts 101 | 102 | extension MKPlacemark { 103 | var formattedAddress: String? { 104 | guard let postalAddress = postalAddress else { return nil } 105 | return CNPostalAddressFormatter.string( 106 | from: postalAddress, style: .mailingAddress) 107 | .replacingOccurrences(of: "\n", with: " " 108 | ) 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /AddaMeIOS/Map/MapSearchView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MapSearchView.swift 3 | // AddaMeIOS 4 | // 5 | // Created by Saroar Khandoker on 12.09.2020. 6 | // 7 | 8 | //import SwiftUI 9 | //import MapKit 10 | // 11 | //struct MapSearchView: View { 12 | // 13 | // @StateObject private var locationQuery: LocationQuery 14 | // 15 | // init(region: MKCoordinateRegion) { 16 | // _locationQuery = StateObject(wrappedValue: LocationQuery(region: region)) 17 | // } 18 | // 19 | // var body: some View { 20 | // NavigationView { 21 | // VStack { 22 | // TextField("Search", text: $locationQuery.searchQuery) 23 | // .textFieldStyle(RoundedBorderTextFieldStyle()) 24 | // .padding() 25 | // //List(locationQuery.searchResults, id: \.self, rowContent: Text.init) 26 | // List(locationQuery.searchResults, id: \.self) { data in 27 | // Text(data.placemark.formattedAddress ?? String.empty) 28 | // } 29 | // } 30 | // .navigationTitle("Search") 31 | // } 32 | // } 33 | //} 34 | // 35 | //struct MapSearchView_Previews: PreviewProvider { 36 | // static var previews: some View { 37 | // let coordinate = CLLocationCoordinate2D(latitude: 33.6525, longitude: -85.8296) 38 | // let region = MKCoordinateRegion(center: coordinate, latitudinalMeters: 100, longitudinalMeters: 100) 39 | // MapSearchView(region: region) 40 | // } 41 | //} 42 | 43 | 44 | 45 | 46 | //struct MapSearchView: View { 47 | // 48 | // @EnvironmentObject var locationSearchService: LocationSearchService 49 | // @EnvironmentObject var eventModel: EventViewModel 50 | // 51 | // var body: some View { 52 | // VStack(alignment: .leading) { 53 | // Text("Search near me") 54 | // .font(.title) 55 | // .bold() 56 | // .padding() 57 | // .padding(.top, 0) 58 | // .padding(.bottom, -15) 59 | // ZStack(alignment: .trailing) { 60 | // SearchBar(text: $locationSearchService.searchQuery) 61 | // .padding() 62 | // .padding(.top, -25) 63 | // if locationSearchService.status == .isSearching { 64 | // Image(systemName: "clock") 65 | // .foregroundColor(Color.red) 66 | // .padding() 67 | // .padding(.trailing, 25) 68 | // .padding(.top, -25) 69 | // } 70 | // } 71 | // 72 | // List(locationSearchService.mapItems) { completion in 73 | // Button(action: { 74 | // 75 | // }) { 76 | // VStack(alignment: .leading) { 77 | // Group { () -> AnyView in 78 | // switch self.locationSearchService.status { 79 | // case .noResults: return AnyView(Text("No Results")) 80 | // case .error(let description): return AnyView(Text("Error: \(description)")) 81 | // default: return AnyView(EmptyView()) 82 | // } 83 | // }.foregroundColor(Color.gray) 84 | // Text(completion.placemark.name ?? String.empty) 85 | // Text(completion.placemark.formattedAddress ?? String.empty) 86 | // .font(.subheadline) 87 | // .foregroundColor(.gray) 88 | // } 89 | // } 90 | // } 91 | // .padding(.top, -25) 92 | // } 93 | // } 94 | //} 95 | // 96 | //struct MapSearchView_Previews: PreviewProvider { 97 | // static var previews: some View { 98 | // MapSearchView() 99 | // .environmentObject(LocationSearchService()) 100 | // } 101 | //} 102 | // 103 | //extension MKMapItem: Identifiable {} 104 | -------------------------------------------------------------------------------- /AddaMeIOS/Map/SearchBar.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SearchBar.swift 3 | // AddaMeIOS 4 | // 5 | // Created by Saroar Khandoker on 12.09.2020. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | import MapKit 11 | import Combine 12 | 13 | struct SearchBar: View { 14 | @Binding var text: String 15 | @State private var isEditing = false 16 | 17 | var body: some View { 18 | HStack { 19 | TextField("Search ...", text: $text) 20 | .padding(3) 21 | .padding(.horizontal, 25) 22 | .cornerRadius(8) 23 | .overlay( 24 | HStack { 25 | Image(systemName: "magnifyingglass") 26 | .foregroundColor(.gray) 27 | .frame(minWidth: 0, maxWidth: .infinity, alignment: .leading) 28 | .padding(.leading, 0) 29 | 30 | if isEditing { 31 | Button(action: { 32 | self.text = String.empty 33 | }) { 34 | Image(systemName: "multiply.circle.fill") 35 | .foregroundColor(.gray) 36 | .padding(.trailing, 5) 37 | } 38 | } 39 | } 40 | ) 41 | .padding(.horizontal, 10) 42 | .onTapGesture { 43 | self.isEditing = true 44 | } 45 | 46 | } 47 | .padding(10) 48 | .background(Color(.systemGray6)) 49 | .clipShape(Capsule()) 50 | 51 | } 52 | } 53 | 54 | struct SearchBar_Previews: PreviewProvider { 55 | static var previews: some View { 56 | SearchBar(text: .constant(String.empty)) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /AddaMeIOS/Models/Attachment.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Attachment.swift 3 | // AddaMeIOS 4 | // 5 | // Created by Saroar Khandoker on 19.11.2020. 6 | // 7 | 8 | import Foundation 9 | 10 | struct Attachment: Codable { 11 | enum AttachmentType: String, Codable { 12 | case file, image, audio, video 13 | } 14 | 15 | internal init(id: String? = nil, type: AttachmentType, userId: String, imageUrlString: String? = nil, audioUrlString: String? = nil, videoUrlString: String? = nil, fileUrlString: String? = nil, createdAt: Date? = nil, updatedAt: Date? = nil) { 16 | self.id = id 17 | self.type = type 18 | self.userId = userId 19 | self.imageUrlString = imageUrlString 20 | self.audioUrlString = audioUrlString 21 | self.videoUrlString = videoUrlString 22 | self.fileUrlString = fileUrlString 23 | self.createdAt = createdAt 24 | self.updatedAt = updatedAt 25 | } 26 | 27 | var id: String? 28 | var type: AttachmentType 29 | var userId: String 30 | var imageUrlString: String? 31 | var audioUrlString: String? 32 | var videoUrlString: String? 33 | var fileUrlString: String? 34 | var createdAt, updatedAt: Date? 35 | 36 | static func < (lhs: Attachment, rhs: Attachment) -> Bool { 37 | guard let lhsDate = lhs.createdAt, let rhsDate = rhs.createdAt else { return false } 38 | return lhsDate > rhsDate 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /AddaMeIOS/Models/Contact/Contact.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Contact.swift 3 | // AddaMeIOS 4 | // 5 | // Created by Saroar Khandoker on 02.09.2020. 6 | // 7 | 8 | import Foundation 9 | import CoreData 10 | 11 | struct Contact: Codable, Identifiable { 12 | 13 | var id: String? 14 | var identifier: String 15 | var phoneNumber: String 16 | var fullName: String? 17 | var avatar: String? 18 | var isRegister: Bool? 19 | var userId: String? 20 | 21 | var response: Res { 22 | .init(self) 23 | } 24 | 25 | init(id: String? = nil, 26 | identifier: String, 27 | userId: String? = nil, 28 | phoneNumber: String, 29 | fullName: String? = nil, 30 | avatar: String? = nil, 31 | isRegister: Bool = false 32 | ) { 33 | self.id = id 34 | self.identifier = identifier 35 | self.userId = userId 36 | self.phoneNumber = phoneNumber 37 | self.fullName = fullName 38 | self.avatar = avatar 39 | self.isRegister = isRegister 40 | } 41 | 42 | init(_ contactEntity: ContactEntity) { 43 | self.id = contactEntity.id 44 | self.userId = contactEntity.userId 45 | self.identifier = contactEntity.identifier 46 | self.fullName = contactEntity.fullName 47 | self.avatar = contactEntity.avatar 48 | self.phoneNumber = contactEntity.phoneNumber 49 | self.isRegister = contactEntity.isRegister 50 | } 51 | 52 | 53 | struct Res: Codable { 54 | var id: String? 55 | var identifier: String 56 | var phoneNumber: String 57 | var fullName: String? 58 | var avatar: String? 59 | var isRegister: Bool? 60 | var userId: String 61 | 62 | init(_ contact: Contact) { 63 | self.id = contact.id 64 | self.identifier = contact.identifier 65 | self.userId = contact.userId ?? String.empty 66 | self.phoneNumber = contact.phoneNumber 67 | self.fullName = contact.fullName 68 | self.avatar = contact.avatar 69 | self.isRegister = contact.isRegister! 70 | self.userId = contact.userId! 71 | } 72 | 73 | } 74 | 75 | } 76 | 77 | extension Contact: Hashable { 78 | func hash(into hasher: inout Hasher) { 79 | hasher.combine(phoneNumber) 80 | hasher.combine(avatar) 81 | } 82 | 83 | static func == (lhs: Contact, rhs: Contact) -> Bool { 84 | return lhs.phoneNumber == rhs.phoneNumber && lhs.avatar == rhs.avatar 85 | } 86 | } 87 | 88 | extension Contact.Res: Hashable { 89 | func hash(into hasher: inout Hasher) { 90 | hasher.combine(phoneNumber) 91 | hasher.combine(avatar) 92 | } 93 | 94 | static func == (lhs: Contact.Res, rhs: Contact.Res) -> Bool { 95 | return lhs.phoneNumber == rhs.phoneNumber && lhs.avatar == rhs.avatar 96 | } 97 | } 98 | 99 | 100 | struct CreateContact: Codable { 101 | var items: [Contact] 102 | } 103 | -------------------------------------------------------------------------------- /AddaMeIOS/Models/Conversation/Conversation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Conversation.swift 3 | // AddaMeIOS 4 | // 5 | // Created by Saroar Khandoker on 05.09.2020. 6 | // 7 | 8 | import Foundation 9 | 10 | 11 | struct CreateConversation: Codable { 12 | let title: String 13 | let type: ConversationType 14 | let opponentPhoneNumber: String 15 | } 16 | 17 | enum ConversationType: String, Codable { 18 | case oneToOne, group 19 | } 20 | 21 | struct Conversation: Codable, Hashable, Identifiable { 22 | 23 | let id, title: String 24 | var type: ConversationType 25 | let members: [CurrentUser]? 26 | let admins: [CurrentUser]? 27 | var lastMessage: ChatMessageResponse.Item? 28 | 29 | let createdAt: Date 30 | let updatedAt: Date 31 | 32 | func hash(into hasher: inout Hasher) { 33 | hasher.combine(id) 34 | } 35 | 36 | static func == (lhs: Conversation, rhs: Conversation) -> Bool { 37 | lhs.id == rhs.id 38 | } 39 | } 40 | 41 | struct ConversationResponse: Codable { 42 | 43 | let items: [Item] 44 | let metadata: Metadata 45 | 46 | struct Item: Codable, Hashable, Identifiable, Comparable { 47 | init(id: String, title: String, type: ConversationType, members: [CurrentUser], admins: [CurrentUser], lastMessage: ChatMessageResponse.Item?, createdAt: Date, updatedAt: Date) { 48 | self.id = id 49 | self.title = title 50 | self.type = type 51 | self.members = members 52 | self.admins = admins 53 | self.lastMessage = lastMessage 54 | self.createdAt = createdAt 55 | self.updatedAt = updatedAt 56 | } 57 | 58 | static var defint: Self { 59 | .init(id: ObjectId.shared.generate(), title: "defualt", type: .group, members: [], admins: [], lastMessage: nil, createdAt: Date(), updatedAt: Date()) 60 | } 61 | 62 | init(_ conversation: Conversation) { 63 | self.id = conversation.id 64 | self.title = conversation.title 65 | self.type = conversation.type 66 | self.members = conversation.members 67 | self.admins = conversation.admins 68 | 69 | self.lastMessage = conversation.lastMessage 70 | self.createdAt = conversation.createdAt 71 | self.updatedAt = conversation.updatedAt 72 | } 73 | 74 | var wSconversation: Conversation { 75 | .init(id: id, title: title, type: type, members: nil, admins: nil, lastMessage: nil, createdAt: Date(), updatedAt: Date()) 76 | } 77 | 78 | let id, title: String 79 | var type: ConversationType 80 | let members: [CurrentUser]? 81 | let admins: [CurrentUser]? 82 | var lastMessage: ChatMessageResponse.Item? 83 | 84 | let createdAt, updatedAt: Date 85 | 86 | func hash(into hasher: inout Hasher) { 87 | hasher.combine(id) 88 | } 89 | 90 | static func == (lhs: Item, rhs: Item) -> Bool { 91 | lhs.id == rhs.id 92 | } 93 | 94 | static func < (lhs: Item, rhs: Item) -> Bool { 95 | guard let lhsLstMsg = lhs.lastMessage, let rhsLstMsg = rhs.lastMessage, 96 | let lhsDate = lhsLstMsg.updatedAt, let rhsDate = rhsLstMsg.updatedAt 97 | else { return false } 98 | return lhsDate > rhsDate 99 | } 100 | 101 | } 102 | 103 | } 104 | 105 | 106 | extension ConversationResponse.Item { 107 | 108 | func canJoinConversation() -> Bool { 109 | guard let currentUSER: CurrentUser = KeychainService.loadCodable(for: .currentUser) else { 110 | return false 111 | } 112 | 113 | return self.admins!.contains(where: { $0.id == currentUSER.id }) || 114 | self.members!.contains(where: { $0.id == currentUSER.id }) 115 | 116 | } 117 | 118 | } 119 | -------------------------------------------------------------------------------- /AddaMeIOS/Models/Conversation/LastMessage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LastMessage.swift 3 | // AddaMeIOS 4 | // 5 | // Created by Saroar Khandoker on 28.09.2020. 6 | // 7 | 8 | import Foundation 9 | 10 | struct LastMessage: Codable, Identifiable, Hashable { 11 | var id, senderID: String 12 | var phoneNumber: String 13 | var firstName, lastName: String? 14 | var avatar, messageBody: String 15 | var totalUnreadMessages: Int 16 | var timestamp: Int 17 | 18 | enum CodingKeys: String, CodingKey { 19 | case senderID = "sender_id" 20 | case phoneNumber = "phone_number" 21 | case firstName = "first_name" 22 | case lastName = "last_name" 23 | case messageBody = "message_body" 24 | case totalUnreadMessages = "total_unread_messages" 25 | case id, avatar, timestamp 26 | } 27 | 28 | func hash(into hasher: inout Hasher) { 29 | hasher.combine(id) 30 | hasher.combine(phoneNumber) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /AddaMeIOS/Models/EventPlace/EventPlace.swift: -------------------------------------------------------------------------------- 1 | //// 2 | //// GeoLocation.swift 3 | //// AddaMeIOS 4 | //// 5 | //// Created by Saroar Khandoker on 20.09.2020. 6 | //// 7 | // 8 | //import Foundation 9 | //import MapKit 10 | // 11 | //final class EventPlace: NSObject, Encodable , Decodable, Identifiable { 12 | // 13 | // var id: String? 14 | // var eventId: String 15 | // var addressName: String 16 | // var image: String? 17 | // var details: String? 18 | // var type: GeoType = .Point 19 | // var sponsored: Bool? = false 20 | // var overlay: Bool? = false 21 | // var coordinates: [Double] 22 | // var regionRadius: CLLocationDistance? = 1000 23 | // var createdAt, updatedAt: Date? 24 | // 25 | // init(id: String? = nil, eventId: String, addressName: String, coordinates: [Double], image: String? = "person.fill", sponsored: Bool = false, overlay: Bool = false, details: String? = nil, type: GeoType = .Point) { 26 | // self.id = id 27 | // self.eventId = eventId 28 | // self.addressName = addressName 29 | // self.coordinates = coordinates 30 | // self.image = image 31 | // self.sponsored = sponsored 32 | // self.overlay = overlay 33 | // self.details = details 34 | // self.type = type 35 | // } 36 | // 37 | // static var defualtInit: Self { 38 | // .init(id: ObjectId.shared.generate(), eventId: ObjectId.shared.generate(), addressName: String.empty, coordinates: [+60.02055149, +30.38782751], image: "person.fill", sponsored: true, overlay: true, details: String.empty) 39 | // } 40 | // 41 | // static func == (lhs: EventPlace, rhs: EventPlace) -> Bool { 42 | // lhs.id == rhs.id 43 | // } 44 | // 45 | // static func < (lhs: EventPlace, rhs: EventPlace) -> Bool { 46 | // guard let lhsDate = lhs.createdAt, let rhsDate = rhs.createdAt else { return false } 47 | // return lhsDate < rhsDate 48 | // } 49 | // 50 | //} 51 | // 52 | // 53 | // 54 | //struct EventPlaceResponse: Hashable, Codable, Identifiable { 55 | // let id, eventId, addressName, type: String 56 | // let image, details: String? 57 | // let sponsored, overlay: Bool? 58 | // let coordinates: [Double] 59 | //} 60 | // 61 | -------------------------------------------------------------------------------- /AddaMeIOS/Models/FetachDataFromJsonFile.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Data.swift 3 | // AddaMeIOS 4 | // 5 | // Created by Saroar Khandoker on 26.08.2020. 6 | // 7 | 8 | import Foundation 9 | import UIKit 10 | import SwiftUI 11 | import CoreLocation 12 | 13 | let demoContacts: [Contact] = load("contacts.json") 14 | let eventData: [EventResponse.Item] = load("eventResponseData.json") 15 | let chatDemoData: [ChatMessageResponse.Item] = load("chatResponseData.json") 16 | let conversationData: [ConversationResponse.Item] = load("conversationResponseData.json") 17 | 18 | // will use featch demo data 19 | //func load() -> T { 20 | // let sections = Bundle.main.decode([T].self, from: "menu.json") 21 | // self.sections = sections 22 | // self.representativeSample = [sections[0].items[0], sections[1].items[2], sections[2].items[2]] 23 | //} 24 | 25 | func load(_ filename: String) -> T { 26 | let data: Data 27 | 28 | guard let file = Bundle.main.url(forResource: filename, withExtension: nil) 29 | else { 30 | fatalError("\(#line) Couldn't find \(filename) in main bundle.") 31 | } 32 | 33 | do { 34 | data = try Data(contentsOf: file) 35 | } catch { 36 | fatalError("\(#line) Couldn't load \(filename) from main bundle:\n\(error)") 37 | } 38 | 39 | do { 40 | let decoder = JSONDecoder() 41 | decoder.dateDecodingStrategy = .iso8601 42 | return try decoder.decode(T.self, from: data) 43 | } catch { 44 | fatalError("\(#line) Couldn't parse \(filename) as \(T.self):\n\(error)") 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /AddaMeIOS/Models/User/CurrentUser.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CurrentUser.swift 3 | // AddaMeIOS 4 | // 5 | // Created by Saroar Khandoker on 28.09.2020. 6 | // 7 | 8 | import Foundation 9 | 10 | // MARK: - CurrentUser 11 | struct CurrentUser: Codable, Equatable, Hashable, Identifiable { 12 | 13 | internal init(id: String, phoneNumber: String, avatarUrl: String? = nil, firstName: String? = nil, lastName: String? = nil, email: String? = nil, contactIDs: [String]? = nil, deviceIDs: [String]? = nil, attachments: [Attachment]? = nil, createdAt: Date, updatedAt: Date) { 14 | self.id = id 15 | self.phoneNumber = phoneNumber 16 | self.avatarUrl = avatarUrl 17 | self.firstName = firstName 18 | self.lastName = lastName 19 | self.email = email 20 | self.contactIDs = contactIDs 21 | self.deviceIDs = deviceIDs 22 | self.attachments = attachments 23 | self.createdAt = createdAt 24 | self.updatedAt = updatedAt 25 | } 26 | 27 | static var dInit: Self { 28 | .init(id: UUID().uuidString, phoneNumber: "+79212121211", avatarUrl: nil, firstName: nil, lastName: nil, email: nil, contactIDs: nil, deviceIDs: nil, attachments: nil, createdAt: Date(), updatedAt: Date()) 29 | } 30 | 31 | var id, phoneNumber: String 32 | var avatarUrl, firstName, lastName, email: String? 33 | var contactIDs, deviceIDs: [String]? 34 | var attachments: [Attachment]? 35 | var createdAt, updatedAt: Date 36 | 37 | var fullName: String { 38 | var fullName = String.empty 39 | if let firstN = firstName { 40 | fullName += "\(firstN) " 41 | } 42 | 43 | if let lastN = lastName { 44 | fullName += "\(lastN)" 45 | } 46 | 47 | if fullName.isEmpty { 48 | return hideLast4DigitFromPhoneNumber() 49 | } 50 | 51 | return fullName 52 | } 53 | 54 | func hideLast4DigitFromPhoneNumber() -> String { 55 | guard let currentUSER: CurrentUser = KeychainService.loadCodable(for: .currentUser) else { 56 | return "SwiftUI preview missing CurrentUser" 57 | } 58 | 59 | let lastFourCharacters = String(self.phoneNumber.suffix(4)) 60 | let phoneNumberWithLastFourHiddenCharcters = self.phoneNumber.replace(target: lastFourCharacters, withString:"****") 61 | 62 | return currentUSER.id == self.id ? self.phoneNumber : phoneNumberWithLastFourHiddenCharcters 63 | } 64 | 65 | func hash(into hasher: inout Hasher) { 66 | hasher.combine(id) 67 | } 68 | 69 | static func == (lhs: CurrentUser, rhs: CurrentUser) -> Bool { 70 | return 71 | lhs.id == rhs.id && 72 | lhs.avatarUrl == rhs.avatarUrl && 73 | lhs.phoneNumber == rhs.phoneNumber && 74 | lhs.firstName == rhs.firstName && 75 | lhs.lastName == rhs.lastName && 76 | lhs.email == rhs.email 77 | } 78 | 79 | func lastAvatarURLString() -> String? { 80 | guard let atchmts = self.attachments else { 81 | return nil 82 | } 83 | print(#line, atchmts) 84 | return atchmts.filter { $0.type == .image }.last?.imageUrlString 85 | } 86 | 87 | var imageURL: URL? { 88 | guard lastAvatarURLString() != nil else { 89 | return nil 90 | } 91 | return URL(string: lastAvatarURLString()!)! 92 | 93 | } 94 | 95 | } 96 | -------------------------------------------------------------------------------- /AddaMeIOS/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /AddaMeIOS/Preview Content/Preview Assets.xcassets/avatar.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "avatar.jpg", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /AddaMeIOS/Preview Content/Preview Assets.xcassets/avatar.imageset/avatar.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AddaMeSPB/AddaMeIOSSwiftUI/ddb62dba6d4f2c0073f939646037c260faf9675a/AddaMeIOS/Preview Content/Preview Assets.xcassets/avatar.imageset/avatar.jpg -------------------------------------------------------------------------------- /AddaMeIOS/Resources/contacts.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "avatar": null, 4 | "phoneNumber": "+79218821217", 5 | "id": "5faea05b717a5064845accb5", 6 | "isRegister": true, 7 | "fullName": "Saroar Khandoker", 8 | "identifier": "BBBA81AD-2D3F-4786-AA1D-7D2E654126B1", 9 | "updatedAt": "2020-11-13T15:03:55Z", 10 | "createdAt": "2020-11-13T15:03:55Z", 11 | "userId": "5fabb1ebaa5f5774ccfe48c3" 12 | }, 13 | { 14 | "avatar": null, 15 | "userId": "5fabb1ebaa5f5774ccfe48c3" 16 | "id": "5faea05cbdcea2c92c19b1a0", 17 | "isRegister": true, 18 | "fullName": "Alla Fake Number Update", 19 | "identifier": "134C1859-D324-4425-BAB6-98DABB7FFCE3:ABPerson", 20 | "updatedAt": "2020-11-13T15:03:56Z", 21 | "createdAt": "2020-11-13T15:03:56Z", 22 | "phoneNumber": "+79218821219" 23 | } 24 | ] 25 | -------------------------------------------------------------------------------- /AddaMeIOS/Resources/eventResponseData.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "isActive": true, 4 | "categories": "General", 5 | "updatedAt": "2020-11-11T11:38:48Z", 6 | "sponsored": false, 7 | "addressName": "", 8 | "details": "", 9 | "type": "Point", 10 | "imageUrl": "https://avatars.mds.yandex.net/get-pdb/2776508/af73774d-7409-4e73-81c8-c8ab127c2f8b/s1200?webp=false", 11 | "coordinates": [ 12 | 29.873706166262373, 13 | 60.261340452875721 14 | ], 15 | "duration": 7200, 16 | "createdAt": "2020-11-11T11:38:48Z", 17 | "overlay": false, 18 | "name": "Walk Around 🚶🏽🚶🏼‍♀️" 19 | } 20 | ] 21 | -------------------------------------------------------------------------------- /AddaMeIOS/Services/SystemServices.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SystemServices.swift 3 | // AddaMeIOS 4 | // 5 | // Created by Saroar Khandoker on 05.11.2020. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct SystemServices: ViewModifier { 11 | static var appState: AppState = AppState() 12 | static var authViewModel: AuthViewModel = AuthViewModel() 13 | static var eventViewModel: EventViewModel = EventViewModel() 14 | static var deviceViewModel: DeviceViewModel = DeviceViewModel() 15 | let persistenceController = PersistenceController.shared 16 | 17 | func body(content: Content) -> some View { 18 | content 19 | // services 20 | .environmentObject(Self.appState) 21 | .environmentObject(Self.authViewModel) 22 | .environmentObject(Self.eventViewModel) 23 | .environmentObject(Self.deviceViewModel) 24 | .environment(\.managedObjectContext, persistenceController.container.viewContext) 25 | } 26 | } 27 | 28 | //struct SystemServices: ViewModifier { 29 | // 30 | // 31 | // let persistenceController: PersistenceController 32 | // @StateObject var contactStorage: ContactStore 33 | // 34 | // init() { 35 | // let persistenceManager = PersistenceController() 36 | // self.persistenceController = persistenceManager 37 | // let managedObjectContext = persistenceManager.container.viewContext 38 | // let storage = ContactStore(managedObjectContext: managedObjectContext) 39 | // self._contactStorage = StateObject(wrappedValue: storage) 40 | // } 41 | // 42 | // static var appState: AppState = AppState() 43 | // static var authViewModel: AuthViewModel = AuthViewModel() 44 | // static var eventViewModel: EventViewModel = EventViewModel() 45 | // static var deviceViewModel: DeviceViewModel = DeviceViewModel() 46 | //// let persistenceController = PersistenceController.shared 47 | // 48 | // func body(content: Content) -> some View { 49 | // content 50 | // // services 51 | // .environmentObject(Self.appState) 52 | // .environmentObject(Self.authViewModel) 53 | // .environmentObject(Self.eventViewModel) 54 | // .environmentObject(Self.deviceViewModel) 55 | //// .environment(\.managedObjectContext, persistenceController.container.viewContext) 56 | // } 57 | //} 58 | -------------------------------------------------------------------------------- /AddaMeIOS/Settings/SettingsView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SettingsView.swift 3 | // AddaMeIOS 4 | // 5 | // Created by Saroar Khandoker on 26.11.2020. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct SettingsView: View { 11 | @AppStorage(AppUserDefaults.Key.distance.rawValue) var distance: Double = 250.0 12 | 13 | @State private var showingTermsSheet = false 14 | @State private var showingPrivacySheet = false 15 | 16 | var body: some View { 17 | VStack(alignment: .leading, spacing: 20) { 18 | 19 | Text("Settings") 20 | .font(.title) 21 | .bold() 22 | .padding() 23 | 24 | DistanceFilterView(distance: self.$distance) 25 | .padding([.top, .bottom], 20) 26 | .transition(.opacity) 27 | 28 | HStack { 29 | Spacer() 30 | Button(action: { 31 | showingTermsSheet = true 32 | }, label: { 33 | Text("Terms") 34 | .font(.title) 35 | .bold() 36 | .foregroundColor(.blue) 37 | }) 38 | .sheet(isPresented: $showingTermsSheet) { 39 | TermsAndPrivacyWebView(urlString: EnvironmentKeys.rootURL.absoluteString + "/terms") 40 | } 41 | 42 | Text("&") 43 | .font(.title3) 44 | .bold() 45 | .padding([.leading, .trailing], 10) 46 | 47 | Button(action: { 48 | showingPrivacySheet = true 49 | }, label: { 50 | Text("Privacy") 51 | .font(.title) 52 | .bold() 53 | .foregroundColor(.blue) 54 | }) 55 | .sheet(isPresented: $showingPrivacySheet) { 56 | TermsAndPrivacyWebView(urlString: EnvironmentKeys.rootURL.absoluteString + "/privacy") 57 | } 58 | 59 | Spacer() 60 | } 61 | //.frame(width: .infinity, height: 100, alignment: .center) 62 | .background(Color.yellow) 63 | .clipShape(Capsule.init()) 64 | .padding() 65 | 66 | Spacer() 67 | 68 | } 69 | } 70 | } 71 | 72 | struct SettingsView_Previews: PreviewProvider { 73 | static var previews: some View { 74 | SettingsView() 75 | } 76 | } 77 | 78 | struct DistanceFilterView: View { 79 | 80 | @Binding var distance: Double 81 | @AppStorage(AppUserDefaults.Key.distance.rawValue) var distanceValue: Double = 250.0 82 | 83 | var minDistance = 5.0 84 | var maxDistance = 250.0 85 | 86 | var body: some View { 87 | VStack(alignment: .leading) { 88 | 89 | Text("Near by distance \(Int(distance)) km") 90 | .font(.title3) 91 | .bold() 92 | .onChange(of: /*@START_MENU_TOKEN@*/"Value"/*@END_MENU_TOKEN@*/, perform: { value in 93 | distanceValue = distance 94 | }) 95 | .font(.system(.headline, design: .rounded)) 96 | 97 | HStack { 98 | Slider(value: $distance, in: minDistance...maxDistance, step: 1, onEditingChanged: {changing in self.update(changing) }) 99 | .accentColor(.green) 100 | } 101 | 102 | HStack { 103 | Text("\(Int(minDistance))") 104 | .font(.system(.footnote, design: .rounded)) 105 | 106 | Spacer() 107 | 108 | Text("\(Int(maxDistance))") 109 | .font(.system(.footnote, design: .rounded)) 110 | } 111 | 112 | } 113 | .onAppear { 114 | update(true) 115 | } 116 | .padding(.horizontal) 117 | .padding(.bottom, 10) 118 | } 119 | 120 | func update(_ changing: Bool) -> Void { 121 | distanceValue = distance == 0 ? 249 : distance 122 | } 123 | 124 | } 125 | 126 | struct DistanceFilterView_Previews: PreviewProvider { 127 | static var previews: some View { 128 | DistanceFilterView(distance: .constant(250)) 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /AddaMeIOS/Shape+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Shape+Extension.swift 3 | // AddaMeIOS 4 | // 5 | // Created by Saroar Khandoker on 05.09.2020. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct RoundLeft: Shape { 11 | func path(in rect: CGRect) -> Path { 12 | let path = UIBezierPath(roundedRect: rect, byRoundingCorners: .topLeft, cornerRadii: CGSize(width: 55, height: 55) ) 13 | 14 | return Path(path.cgPath) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /AddaMeIOS/User/ContactRow.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContactRow.swift 3 | // AddaMeIOS 4 | // 5 | // Created by Saroar Khandoker on 04.12.2020. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct ContactRow: View { 11 | @ObservedObject var contact: ContactEntity 12 | @StateObject var conversationView = ConversationViewModel() 13 | @Environment(\.colorScheme) var colorScheme 14 | 15 | var body: some View { 16 | HStack { 17 | if contact.avatar == nil { 18 | Image(systemName: "person.crop.circle.fill") 19 | .resizable() 20 | .frame(width: 55, height: 55) 21 | .clipShape(Circle()) 22 | .foregroundColor(colorScheme == .dark ? Color.white : Color.black) 23 | } else { 24 | AsyncImage( 25 | urlString: contact.avatar, 26 | placeholder: { Text("Loading...").frame(width: 35, height: 35, alignment: .center) }, 27 | image: { 28 | Image(uiImage: $0).resizable() 29 | } 30 | ) 31 | .aspectRatio(contentMode: .fill) 32 | .frame(width: 45, height: 45, alignment: .center) 33 | .clipShape(Circle()) 34 | 35 | } 36 | 37 | 38 | VStack(alignment: .leading) { 39 | Text(contact.fullName) 40 | Text(contact.phoneNumber) 41 | } 42 | 43 | Spacer(minLength: 0) 44 | 45 | Button(action: { 46 | startChat(contact) 47 | }, label: { 48 | Image(systemName: "bubble.left.and.bubble.right.fill") 49 | .imageScale(.large) 50 | .frame(width: 60, height: 60, alignment: .center) 51 | }) 52 | .sheet(isPresented: $conversationView.startChat) { 53 | LazyView( 54 | ChatRoomView(conversation: conversationView.conversation, fromContactsOrEvents: true) 55 | .edgesIgnoringSafeArea(.bottom) 56 | ) 57 | } 58 | 59 | } 60 | } 61 | 62 | func startChat(_ contact: ContactEntity) { 63 | guard let currentUSER: CurrentUser = KeychainService.loadCodable(for: .currentUser) else { 64 | return 65 | } 66 | 67 | let conversation = CreateConversation( 68 | title: "\(currentUSER.fullName), \(contact.fullName)", 69 | type: .oneToOne, 70 | opponentPhoneNumber: contact.phoneNumber 71 | ) 72 | 73 | conversationView.startOneToOneChat(conversation) 74 | } 75 | 76 | func invite() { 77 | let url = URL(string: "https://testflight.apple.com/join/gXWnCqLB") 78 | let av = UIActivityViewController(activityItems: [url!], applicationActivities: nil) 79 | UIApplication.shared.windows.first?.rootViewController?.present(av, animated: true, completion: nil) 80 | } 81 | } 82 | 83 | //struct ContactRow_Previews: PreviewProvider { 84 | // static var previews: some View { 85 | // ContactRow(contact: <#ContactEntity#>) 86 | // } 87 | //} 88 | -------------------------------------------------------------------------------- /AddaMeIOS/User/ContactsView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContactsView.swift 3 | // AddaMeIOS 4 | // 5 | // Created by Saroar Khandoker on 02.09.2020. 6 | // 7 | 8 | import SwiftUI 9 | import Contacts 10 | import Foundation 11 | import CoreData 12 | 13 | struct ContactsView: View { 14 | // @State var expand = false 15 | // @State var searchExpand = true 16 | 17 | @State private var refreshing = false 18 | private var didSave = NotificationCenter.default.publisher(for: .NSManagedObjectContextDidSave) 19 | 20 | @EnvironmentObject var store: ContactStore 21 | @Environment(\.colorScheme) var colorScheme 22 | @State var startChat = false 23 | @State var conversation: ConversationResponse.Item? 24 | 25 | @FetchRequest(entity: ContactEntity.entity(), 26 | sortDescriptors: [ NSSortDescriptor(key: "fullName", ascending: true)], 27 | predicate: NSPredicate(format: "isRegister == true") 28 | ) private var contacts: FetchedResults 29 | 30 | var body: some View { 31 | List { 32 | ForEach(contacts, id: \.identifier) { contact in 33 | ContactRow(contact: contact) 34 | } 35 | } 36 | .navigationBarTitle("Contacts", displayMode: .automatic) 37 | } 38 | 39 | } 40 | 41 | struct ContactsView_Previews: PreviewProvider { 42 | static var previews: some View { 43 | ContactsView() 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /AddaMeIOS/User/DeviceViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DeviceViewModel.swift 3 | // AddaMeIOS 4 | // 5 | // Created by Saroar Khandoker on 29.11.2020. 6 | // 7 | 8 | import Foundation 9 | import Combine 10 | import Pyramid 11 | import UIKit 12 | 13 | class DeviceViewModel: ObservableObject { 14 | 15 | @Published var device: Device? 16 | 17 | let provider = Pyramid() 18 | var cancellationToken: AnyCancellable? 19 | 20 | init() { 21 | createOrUpdate() 22 | } 23 | 24 | } 25 | 26 | extension DeviceViewModel { 27 | func createOrUpdate() { 28 | guard let my: CurrentUser = KeychainService.loadCodable(for: .currentUser), UserDefaults.standard.bool(forKey: "isAuthorized") else { 29 | return 30 | } 31 | 32 | guard let token: String = KeychainService.loadString(for: .deviceToken), 33 | let voipToken: String = KeychainService.loadString(for: .voipToken) else { 34 | print(#line, "token or voipToken are missing") 35 | return 36 | } 37 | 38 | let device = Device(id: nil, ownerId: my.id, name: UIDevice.current.name, model: UIDevice.current.model, osVersion: UIDevice.current.systemVersion, token: token, voipToken: voipToken, createAt: nil, updatedAt: nil) 39 | 40 | cancellationToken = provider.request( 41 | with: DeviceAPI.createOrUpdate(device), 42 | scheduler: RunLoop.main, 43 | class: Device.self 44 | ) 45 | .receive(on: RunLoop.main) 46 | .sink(receiveCompletion: { completionResponse in 47 | switch completionResponse { 48 | case .failure(let error): 49 | print(error) 50 | case .finished: 51 | break 52 | } 53 | }, receiveValue: { [weak self] res in 54 | guard let self = self else { return } 55 | self.device = res 56 | }) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /AddaMeIOS/User/TermsAndPrivacyWebView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TermsAndPrivacyWebView.swift 3 | // AddaMeIOS 4 | // 5 | // Created by Saroar Khandoker on 07.12.2020. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct TermsAndPrivacyWebView: View { 11 | @Environment(\.presentationMode) var presentationMode 12 | let urlString: String 13 | 14 | var body: some View { 15 | TermsAndPrivacyWebRepresentableView(urlString: urlString) 16 | .overlay( 17 | Button(action: { 18 | presentationMode.wrappedValue.dismiss() 19 | }, label: { 20 | Image(systemName: "xmark.circle").font(.title) 21 | }) 22 | .padding(.bottom, 10) 23 | .padding(), 24 | 25 | alignment: .bottomTrailing 26 | ) 27 | 28 | } 29 | } 30 | 31 | struct TermsAndPrivacyWebView_Previews: PreviewProvider { 32 | static var previews: some View { 33 | TermsAndPrivacyWebView(urlString: "http://10.0.1.3:3030/privacy") 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /AddaMeIOS/User/TermsWebView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TermsAndPrivacyWebView.swift 3 | // AddaMeIOS 4 | // 5 | // Created by Saroar Khandoker on 07.12.2020. 6 | // 7 | 8 | import SwiftUI 9 | import WebKit 10 | 11 | struct TermsAndPrivacyWebRepresentableView: UIViewRepresentable { 12 | let urlString: String? 13 | 14 | func makeUIView(context: Context) -> WKWebView { 15 | return WKWebView() 16 | } 17 | 18 | func updateUIView(_ uiView: WKWebView, context: Context) { 19 | if let safeString = urlString, let url = URL(string: safeString) { 20 | let request = URLRequest(url: url) 21 | uiView.load(request) 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /AddaMeIOS/User/UserViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserViewModel.swift 3 | // AddaMeIOS 4 | // 5 | // Created by Saroar Khandoker on 17.10.2020. 6 | // 7 | 8 | import Foundation 9 | import Combine 10 | import Pyramid 11 | import UIKit 12 | import SwiftUI 13 | 14 | class UserViewModel: ObservableObject { 15 | 16 | @Published var user: CurrentUser = CurrentUser.dInit 17 | @AppStorage(AppUserDefaults.Key.isAuthorized.rawValue) var isAuthorized: Bool = false 18 | @AppStorage(AppUserDefaults.Key.isUserFristNameUpdated.rawValue) var isUserFristNameUpdated: Bool = false 19 | 20 | @Published var uploading = false 21 | 22 | let provider = Pyramid() 23 | var cancellationToken: AnyCancellable? 24 | 25 | init() { 26 | self.fetchMySelf() 27 | } 28 | 29 | } 30 | 31 | extension UserViewModel { 32 | 33 | func updateUserName(_ firstName: String, _ lastName: String?) { 34 | guard let my: CurrentUser = KeychainService.loadCodable(for: .currentUser) else { 35 | return 36 | } 37 | 38 | var cuser = my 39 | cuser.firstName = firstName 40 | cuser.lastName = lastName 41 | 42 | cancellationToken = provider.request( 43 | with: UserAPI.update(cuser), 44 | scheduler: RunLoop.main, 45 | class: CurrentUser.self 46 | ) 47 | .receive(on: RunLoop.main) 48 | .sink(receiveCompletion: { completionResponse in 49 | switch completionResponse { 50 | case .failure(let error): 51 | print(error) 52 | case .finished: 53 | break 54 | } 55 | }, receiveValue: { [weak self] res in 56 | guard let self = self else { return } 57 | self.isUserFristNameUpdated = true 58 | self.user = res 59 | KeychainService.save(codable: res, for: .currentUser) 60 | self.isUserFristNameUpdated = self.user.firstName == nil ? false : true 61 | }) 62 | 63 | } 64 | 65 | func fetchMySelf() { 66 | guard let my: CurrentUser = KeychainService.loadCodable(for: .currentUser) else { 67 | return 68 | } 69 | 70 | cancellationToken = provider.request( 71 | with: UserAPI.me(my.id), 72 | scheduler: RunLoop.main, 73 | class: CurrentUser.self 74 | ) 75 | .receive(on: RunLoop.main) 76 | .sink(receiveCompletion: { completionResponse in 77 | switch completionResponse { 78 | case .failure(let error): 79 | print(error) 80 | case .finished: 81 | break 82 | } 83 | }, receiveValue: { [weak self] res in 84 | guard let self = self else { return } 85 | self.user = res 86 | KeychainService.save(codable: res, for: .currentUser) 87 | self.isUserFristNameUpdated = self.user.firstName == nil ? false : true 88 | }) 89 | } 90 | 91 | func uploadAvatar(_ image: UIImage) { 92 | uploading = true 93 | 94 | guard let me: CurrentUser = KeychainService.loadCodable(for: .currentUser) else { 95 | uploading = false 96 | return 97 | } 98 | 99 | AWSS3Helper.uploadImage(image, conversationId: nil, userId: me.id) { [weak self] imageURLString in 100 | guard imageURLString != nil else { 101 | DispatchQueue.main.async { self?.uploading = false } 102 | return 103 | } 104 | 105 | DispatchQueue.main.async { 106 | self?.uploading = false 107 | let attachment = Attachment(type: .image, userId: me.id, imageUrlString: imageURLString) 108 | self?.createAttachment(attachment) 109 | } 110 | } 111 | } 112 | 113 | func createAttachment(_ attachment: Attachment) { 114 | 115 | cancellationToken = provider.request( 116 | with: AttachmentAPI.create(attachment), 117 | scheduler: RunLoop.main, 118 | class: Attachment.self 119 | ).sink(receiveCompletion: { completionResponse in 120 | switch completionResponse { 121 | case .failure(let error): 122 | print(error) 123 | case .finished: 124 | break 125 | } 126 | }, receiveValue: { [weak self] res in 127 | print(#line, res) 128 | self?.fetchMySelf() 129 | }) 130 | } 131 | 132 | } 133 | 134 | -------------------------------------------------------------------------------- /AddaMeIOSUITests/AddaMeIOSUITests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AddaMeIOSUITests.swift 3 | // AddaMeIOSUITests 4 | // 5 | // Created by Saroar Khandoker on 08.12.2020. 6 | // 7 | import XCTest 8 | 9 | class AddaMeIOSUITests: XCTestCase { 10 | override func setUp() { 11 | let app = XCUIApplication() 12 | app.resetAuthorizationStatus(for: .contacts) 13 | setupSnapshot(app) 14 | app.launch() 15 | } 16 | 17 | override func setUpWithError() throws { 18 | continueAfterFailure = false 19 | 20 | addUIInterruptionMonitor(withDescription: "Allow Notification") { element -> Bool in 21 | 22 | let notificationAlertButton = element.buttons["Allow"].firstMatch 23 | 24 | if element.elementType == .alert && notificationAlertButton.exists { 25 | notificationAlertButton.tap() 26 | return true 27 | } 28 | 29 | return false 30 | } 31 | 32 | addUIInterruptionMonitor(withDescription: "Allow While Using App") { element -> Bool in 33 | 34 | let okButton = element.buttons.element(boundBy: 1) 35 | if okButton.exists { 36 | okButton.tap() 37 | return true 38 | } 39 | 40 | // let allowButtonForLocation = element.buttons["Allow While Using App"].firstMatch 41 | // 42 | // if element.elementType == .alert && allowButtonForLocation.exists { 43 | // allowButtonForLocation.tap() 44 | // return true 45 | // } 46 | 47 | return false 48 | 49 | } 50 | 51 | addUIInterruptionMonitor(withDescription: "Contacts Access") { element -> Bool in 52 | 53 | let okButton = element.buttons["OK"].firstMatch 54 | 55 | if element.elementType == .alert && okButton.exists { 56 | okButton.tap() 57 | return true 58 | } 59 | 60 | return false 61 | } 62 | 63 | } 64 | 65 | override func tearDownWithError() throws {} 66 | 67 | func testTakeScreenshots() { 68 | let secondDigit = Int.random(in: 1..<9) 69 | let firstDigit = Int.random(in: 1..<9) 70 | let app = XCUIApplication() 71 | 72 | app.typeText("60975795\(firstDigit)\(secondDigit)") 73 | sleep(1) 74 | snapshot("inputMobileNumber") 75 | app.buttons["GO"].tap() 76 | sleep(4) 77 | 78 | let textField = app.textFields.element 79 | snapshot("inputVerificationCode") 80 | textField.tap() 81 | sleep(1) 82 | textField.typeText("336699") 83 | 84 | sleep(2) 85 | snapshot("userform") 86 | let firstNameTextField = app.textFields["First Name"] 87 | firstNameTextField.tap() 88 | firstNameTextField.typeText("Alex") 89 | 90 | let lastNameOptionalTextField = app.textFields["Last Name - Optional"] 91 | lastNameOptionalTextField.tap() 92 | lastNameOptionalTextField.typeText("Semina") 93 | 94 | app/*@START_MENU_TOKEN@*/.buttons["Return"]/*[[".keyboards",".buttons[\"return\"]",".buttons[\"Return\"]"],[[[-1,2],[-1,1],[-1,0,1]],[[-1,2],[-1,1]]],[0]]@END_MENU_TOKEN@*/.tap() 95 | app.buttons["Save"].tap() 96 | sleep(4) 97 | 98 | app.navigationBars["Hangouts"].buttons["plus.circle"].tap() 99 | sleep(2) 100 | snapshot("createEvent") 101 | sleep(1) 102 | app.navigationBars["Create Event"].buttons["Hangouts"].tap() 103 | snapshot("events") 104 | sleep(2) 105 | 106 | app.buttons["Chat"].tap() 107 | app.navigationBars["Chats"].buttons["plus.circle"].tap() 108 | app.navigationBars["Contacts"].buttons["Chats"].tap() 109 | sleep(2) 110 | snapshot("chat") 111 | 112 | app.buttons["Profile"].tap() 113 | snapshot("profile") 114 | 115 | } 116 | 117 | } 118 | -------------------------------------------------------------------------------- /AddaMeIOSUITests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 34 21 | 22 | 23 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem "fastlane" 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Adda2 iOS app 2 | Adda2 is a free, open source, messaging app for simple private communication with friends. 3 | 4 | [![Available on the App Store](http://cl.ly/WouG/Download_on_the_App_Store_Badge_US-UK_135x40.svg)](https://apps.apple.com/app/id1538487173) 5 | 6 | ### Use Adda to: 7 | * Discover users who live nearby 8 | * You can see nearby stories anonymously 9 | * You can see any place on the map to explore new friends and events 10 | * Locate content anywhere on the map 11 | * Build your own local social network 12 | * Be social, Be Friendly 13 | 14 | Adda2, a new way location-based network to meet new people and know more about them. With real-time events, hangouts, and communication. You can post what you want to do, nearby so people can see: Grabbing a taxi, selling, buying, events around you, hangouts or helping out those near you are some examples.​ We believe with this app you can meet people in real life much easier. Our goal is to let people connect in real life as opposed to just connecting on your screen. 15 | 16 | Meeting new neighbors​ is easier than ever. 17 | 18 | ## Requirements 19 | 20 | ### Build 21 | - Xcode 12 22 | - SwiftUI 100% 23 | - Swift 5 24 | 25 | ### Deployment target 26 | - iOS 14.0 27 | 28 | ### Privacy - Contacts Usage Description 29 | Importent Note: Adda2 uses your contacts to find users you konw. We do not store your contacts on the server. 30 | code here: https://github.com/AddaMeSPB/AddaMeAuth/blob/master/Sources/App/Controllers/ContactController.swift#L21 31 | 32 | 33 | ### SwiftUI Test for create screenshot with fastlane 34 | [YouTube Link](https://youtu.be/A_Xvjs6frCQ) 35 | 36 | ![SwiftUI Test](https://user-images.githubusercontent.com/8770772/102008996-91051800-3d45-11eb-8bd0-1fd05d7acfbc.gif) 37 | -------------------------------------------------------------------------------- /fastlane/Appfile: -------------------------------------------------------------------------------- 1 | app_identifier("com.addame.AddaMeIOS") # The bundle identifier of your app 2 | apple_id("saroarkhandoker@yahoo.com") # Your Apple email address 3 | 4 | itc_team_id("119123163") # App Store Connect Team ID 5 | team_id("6989658CU5") # Developer Portal Team ID 6 | 7 | # For more information about the Appfile, see: 8 | # https://docs.fastlane.tools/advanced/#appfile 9 | -------------------------------------------------------------------------------- /fastlane/Deliverfile: -------------------------------------------------------------------------------- 1 | # The Deliverfile allows you to store various App Store Connect metadata 2 | # For more information, check out the docs 3 | # https://docs.fastlane.tools/actions/deliver/ 4 | 5 | languages(['en-US']) 6 | team_name("Saroar Kkhandoker") 7 | app_identifier "com.addame.AddaMeIOS" 8 | screenshots_path "./fastlane/screenshots" -------------------------------------------------------------------------------- /fastlane/Fastfile: -------------------------------------------------------------------------------- 1 | # This file contains the fastlane.tools configuration 2 | # You can find the documentation at https://docs.fastlane.tools 3 | # 4 | # For a list of all available actions, check out 5 | # 6 | # https://docs.fastlane.tools/actions 7 | # 8 | # For a list of all available plugins, check out 9 | # 10 | # https://docs.fastlane.tools/plugins/available-plugins 11 | # 12 | 13 | # Uncomment the line if you want fastlane to automatically update itself 14 | # update_fastlane 15 | 16 | default_platform(:ios) 17 | 18 | platform :ios do 19 | desc "Push a new beta build to TestFlight" 20 | lane :tf do 21 | increment_build_number(xcodeproj: "AddaMeIOS.xcodeproj") 22 | build_app(scheme: "AddaMeIOS") 23 | upload_to_testflight 24 | end 25 | 26 | desc "Push a new release build" 27 | lane :release do 28 | precheck 29 | increment_build_number(xcodeproj: "AddaMeIOS.xcodeproj") 30 | snapshot 31 | frameit 32 | deliver( 33 | submit_for_review: true, 34 | automatic_release: true, 35 | force: true, # Skip HTMl report verification 36 | skip_metadata: true, 37 | skip_screenshots: false, 38 | skip_binary_upload: true 39 | ) 40 | end 41 | 42 | desc "Generate new localized screenshots" 43 | lane :screenshots do 44 | capture_screenshots(workspace: "AddaMeIOS.xcodeproj", scheme: "AddaMeIOSUITests") 45 | end 46 | 47 | lane :sandbox do 48 | capture_screenshots 49 | frame_screenshots(white: true) 50 | frameit(path: "./fastlane/screenshots") 51 | end 52 | 53 | lane :submit_review do 54 | frame_screenshots(white: true) 55 | frameit(path: "./fastlane/screenshots") 56 | deliver( 57 | submit_for_review: true, 58 | automatic_release: true, 59 | force: true, # Skip HTMl report verification 60 | skip_metadata: true, 61 | skip_screenshots: false, 62 | skip_binary_upload: true 63 | ) 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /fastlane/Precheckfile: -------------------------------------------------------------------------------- 1 | app_identifier "com.addame.AddaMeIOS" 2 | username "saroarkhandoker@yahoo.com" 3 | team_name "Saroar Khandoker" 4 | 5 | # For more information about this configuration visit 6 | # https://docs.fastlane.tools/actions/precheck/ 7 | 8 | # In general, you can use the options available 9 | # fastlane precheck --help 10 | 11 | # Remove the # in front of the line to enable the option 12 | 13 | # You have three possible values for each rule options 14 | # :skip 15 | # indicates that your metadata will not be checked by this rule 16 | 17 | # :warn 18 | # when triggered, this rule will warn you of a potential problem 19 | 20 | # :error 21 | # when triggered, this rule will cause an error to be displayed and it will prevent any further fastlane commands from running after precheck finishes 22 | 23 | # Examples: 24 | # negative_apple_sentiment(level: :skip) 25 | # curse_words(level: :warn) 26 | # future_functionality(level: :error) 27 | # other_platforms(level: :error) 28 | # placeholder_text(level: :error) 29 | # test_words(level: :error) 30 | # unreachable_urls(level: :error) 31 | custom_text(data: ["thunderbolt", "lightning", "frightening"], level: :warn) 32 | -------------------------------------------------------------------------------- /fastlane/README.md: -------------------------------------------------------------------------------- 1 | fastlane documentation 2 | ================ 3 | # Installation 4 | 5 | Make sure you have the latest version of the Xcode command line tools installed: 6 | 7 | ``` 8 | xcode-select --install 9 | ``` 10 | 11 | Install _fastlane_ using 12 | ``` 13 | [sudo] gem install fastlane -NV 14 | ``` 15 | or alternatively using `brew install fastlane` 16 | 17 | # Available Actions 18 | ## iOS 19 | ### ios tf 20 | ``` 21 | fastlane ios tf 22 | ``` 23 | Push a new beta build to TestFlight 24 | ### ios build_appstore 25 | ``` 26 | fastlane ios build_appstore 27 | ``` 28 | Build for App Store submission 29 | ### ios release 30 | ``` 31 | fastlane ios release 32 | ``` 33 | Push a new release build 34 | ### ios screenshots 35 | ``` 36 | fastlane ios screenshots 37 | ``` 38 | Generate new localized screenshots 39 | ### ios sandbox 40 | ``` 41 | fastlane ios sandbox 42 | ``` 43 | 44 | ### ios submit_review 45 | ``` 46 | fastlane ios submit_review 47 | ``` 48 | 49 | 50 | ---- 51 | 52 | This README.md is auto-generated and will be re-generated every time [fastlane](https://fastlane.tools) is run. 53 | More information about fastlane can be found on [fastlane.tools](https://fastlane.tools). 54 | The documentation of fastlane can be found on [docs.fastlane.tools](https://docs.fastlane.tools). 55 | -------------------------------------------------------------------------------- /fastlane/Snapfile: -------------------------------------------------------------------------------- 1 | # Uncomment the lines below you want to change by removing the # in the beginning 2 | 3 | # A list of devices you want to take the screenshots from 4 | devices([ 5 | "iPhone 8 Plus", 6 | "iPhone Xs Max", 7 | "iPad Pro (12.9-inch) (2nd generation)", 8 | "iPad Pro (12.9-inch) (3rd generation)" 9 | ]) 10 | 11 | languages([ 12 | "en-US", 13 | # "de-DE", 14 | # "it-IT", 15 | # ["pt", "pt_BR"] # Portuguese with Brazilian locale 16 | ]) 17 | 18 | # The name of the scheme which contains the UI Tests 19 | scheme("AddaMeIOSUITests") 20 | 21 | # set-simulator-location -q Lyft HQ Apple 22 | stop_after_first_error true 23 | erase_simulator true 24 | clear_previous_screenshots true 25 | reinstall_app true 26 | 27 | # Where should the resulting screenshots be stored? 28 | # output_directory("./screenshots") 29 | 30 | # remove the '#' to clear all previously generated screenshots before creating new ones 31 | # clear_previous_screenshots(true) 32 | 33 | # Remove the '#' to set the status bar to 9:41 AM, and show full battery and reception. 34 | # override_status_bar(true) 35 | 36 | # Arguments to pass to the app on launch. See https://docs.fastlane.tools/actions/snapshot/#launch-arguments 37 | # launch_arguments(["-favColor red"]) 38 | 39 | # For more information about all available options run 40 | # fastlane action snapshot 41 | -------------------------------------------------------------------------------- /fastlane/metadata/copyright.txt: -------------------------------------------------------------------------------- 1 | 2020 Saroar Khandoker 2 | -------------------------------------------------------------------------------- /fastlane/metadata/en-US/apple_tv_privacy_policy.txt: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /fastlane/metadata/en-US/description.txt: -------------------------------------------------------------------------------- 1 | Use Adda to: 2 | -> Discover users who live nearby 3 | -> You can see nearby stories anonymously 4 | -> You can see any place on the map to explore new friends and events 5 | -> Locate content anywhere on the map 6 | -> Build your own local social network 7 | -> Be social, Be Friendly 8 | 9 | ADDA, a new way location-based network to meet new people and know more about them. With real-time events, hangouts, and communication. You can post what you want to do, nearby so people can see: Grabbing a taxi, selling, buying, events around you, hangouts, or helping out those near you are some examples.​ We believe with this app you can meet people in real life much easier. Our goal is to let people connect in real life as opposed to just connecting on your screen. 10 | 11 | Meeting new neighbors​ is easier than ever. 12 | -------------------------------------------------------------------------------- /fastlane/metadata/en-US/keywords.txt: -------------------------------------------------------------------------------- 1 | walk,nearby now, Area news,neighbours,Новости района,друг,соседи,隣人,гулять,আড্ডা,会う,私の周り 2 | -------------------------------------------------------------------------------- /fastlane/metadata/en-US/marketing_url.txt: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /fastlane/metadata/en-US/name.txt: -------------------------------------------------------------------------------- 1 | Adda2 2 | -------------------------------------------------------------------------------- /fastlane/metadata/en-US/privacy_url.txt: -------------------------------------------------------------------------------- 1 | https://addame.com/v1/privacy 2 | -------------------------------------------------------------------------------- /fastlane/metadata/en-US/promotional_text.txt: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /fastlane/metadata/en-US/release_notes.txt: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /fastlane/metadata/en-US/subtitle.txt: -------------------------------------------------------------------------------- 1 | Adda 2 | -------------------------------------------------------------------------------- /fastlane/metadata/en-US/support_url.txt: -------------------------------------------------------------------------------- 1 | https://addame.com/v1/terms 2 | -------------------------------------------------------------------------------- /fastlane/metadata/primary_category.txt: -------------------------------------------------------------------------------- 1 | SOCIAL_NETWORKING 2 | -------------------------------------------------------------------------------- /fastlane/metadata/primary_first_sub_category.txt: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /fastlane/metadata/primary_second_sub_category.txt: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /fastlane/metadata/review_information/demo_password.txt: -------------------------------------------------------------------------------- 1 | we dont use mobile login system and we use one time login system which you have to input your real mobile number then get back one time password via SMS 2 | -------------------------------------------------------------------------------- /fastlane/metadata/review_information/demo_user.txt: -------------------------------------------------------------------------------- 1 | we dont use mobile login system and we use one time login system which you have to input your real mobile number then get back one time password via SMS 2 | -------------------------------------------------------------------------------- /fastlane/metadata/review_information/email_address.txt: -------------------------------------------------------------------------------- 1 | saroar9@gmail.com 2 | -------------------------------------------------------------------------------- /fastlane/metadata/review_information/first_name.txt: -------------------------------------------------------------------------------- 1 | Saroar 2 | -------------------------------------------------------------------------------- /fastlane/metadata/review_information/last_name.txt: -------------------------------------------------------------------------------- 1 | Khandoker 2 | -------------------------------------------------------------------------------- /fastlane/metadata/review_information/notes.txt: -------------------------------------------------------------------------------- 1 | Mobile auth login 2 | -------------------------------------------------------------------------------- /fastlane/metadata/review_information/phone_number.txt: -------------------------------------------------------------------------------- 1 | +79218821217 2 | -------------------------------------------------------------------------------- /fastlane/metadata/secondary_category.txt: -------------------------------------------------------------------------------- 1 | TRAVEL 2 | -------------------------------------------------------------------------------- /fastlane/metadata/secondary_first_sub_category.txt: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /fastlane/metadata/secondary_second_sub_category.txt: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /fastlane/screenshots/Framefile.json: -------------------------------------------------------------------------------- 1 | { 2 | "device_frame_version": "latest", 3 | "default": { 4 | "keyword": { 5 | "font": "./fonts/Chalkduster.ttf" 6 | }, 7 | "title": { 8 | "color": "#FFFFFF" 9 | }, 10 | "padding": 50, 11 | "title_below_image": false, 12 | "background": "./background.jpg", 13 | "show_complete_frame": true 14 | }, 15 | "data": [] 16 | } 17 | -------------------------------------------------------------------------------- /fastlane/screenshots/README.txt: -------------------------------------------------------------------------------- 1 | Put all screenshots you want to use inside the folder of its language (e.g. en-US). 2 | The device type will automatically be recognized using the image resolution. Apple TV screenshots 3 | should be stored in a subdirectory named appleTV with language folders inside of it. iMessage 4 | screenshots, like Apple TV screenshots, should also be stored in a subdirectory named iMessage 5 | with language folders inside of it. 6 | 7 | The screenshots can be named whatever you want, but keep in mind they are sorted alphabetically. 8 | -------------------------------------------------------------------------------- /fastlane/screenshots/background.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AddaMeSPB/AddaMeIOSSwiftUI/ddb62dba6d4f2c0073f939646037c260faf9675a/fastlane/screenshots/background.jpg -------------------------------------------------------------------------------- /fastlane/screenshots/en-US/title.strings: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AddaMeSPB/AddaMeIOSSwiftUI/ddb62dba6d4f2c0073f939646037c260faf9675a/fastlane/screenshots/en-US/title.strings -------------------------------------------------------------------------------- /fastlane/screenshots/fonts/Chalkduster.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AddaMeSPB/AddaMeIOSSwiftUI/ddb62dba6d4f2c0073f939646037c260faf9675a/fastlane/screenshots/fonts/Chalkduster.ttf --------------------------------------------------------------------------------