├── .github └── workflows │ └── main.yml ├── .gitignore ├── .swiftlint.yml ├── CONTRIBUTING.md ├── FriendlyPix.xcodeproj ├── project.pbxproj └── xcshareddata │ └── xcschemes │ └── FriendlyPix.xcscheme ├── FriendlyPix ├── Amaranth-Bold.otf ├── AppDelegate.swift ├── Assets.xcassets │ ├── 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-57x57@1x.png │ │ ├── Icon-App-57x57@2x.png │ │ ├── Icon-App-60x60@2x.png │ │ ├── Icon-App-60x60@3x.png │ │ ├── Icon-App-72x72@1x.png │ │ ├── Icon-App-72x72@2x.png │ │ ├── Icon-App-76x76@1x.png │ │ ├── Icon-App-76x76@2x.png │ │ ├── Icon-App-83.5x83.5@2x.png │ │ ├── Icon-Small-50x50@1x.png │ │ ├── Icon-Small-50x50@2x.png │ │ └── ItunesArtwork@2x.png │ ├── Contents.json │ ├── Logo.imageset │ │ ├── Contents.json │ │ ├── logo1024-universal-341@1x.png │ │ ├── logo1024-universal-341@2x.png │ │ └── logo1024-universal-341@3x.png │ ├── friendlypix_launch_logo.imageset │ │ ├── 144_1x.png │ │ ├── 144_2x.png │ │ ├── 144_3x.png │ │ ├── 192_1x.png │ │ ├── 192_2x.png │ │ └── Contents.json │ ├── google_launch_logo.imageset │ │ ├── Contents.json │ │ ├── googlelogo_dark20_color_132x44pt_1x.png │ │ ├── googlelogo_dark20_color_132x44pt_2x.png │ │ ├── googlelogo_dark20_color_132x44pt_3x.png │ │ ├── googlelogo_dark20_color_184x60pt_1x.png │ │ └── googlelogo_dark20_color_184x60pt_2x.png │ ├── ic_account_circle_36pt.imageset │ │ ├── Contents.json │ │ ├── ic_account_circle_36pt.png │ │ ├── ic_account_circle_36pt_2x.png │ │ └── ic_account_circle_36pt_3x.png │ ├── ic_arrow_back.imageset │ │ ├── Contents.json │ │ ├── ic_arrow_back.png │ │ ├── ic_arrow_back_2x.png │ │ └── ic_arrow_back_3x.png │ ├── ic_close.imageset │ │ ├── Contents.json │ │ ├── ic_close.png │ │ ├── ic_close_2x.png │ │ └── ic_close_3x.png │ ├── ic_comment.imageset │ │ ├── Contents.json │ │ ├── ic_comment_36pt.png │ │ ├── ic_comment_36pt_2x.png │ │ └── ic_comment_36pt_3x.png │ ├── ic_comment_white.imageset │ │ ├── Contents.json │ │ ├── ic_comment_white_36pt.png │ │ ├── ic_comment_white_36pt_2x.png │ │ └── ic_comment_white_36pt_3x.png │ ├── ic_delete_forever_white.imageset │ │ ├── Contents.json │ │ ├── ic_delete_forever_white.png │ │ ├── ic_delete_forever_white_2x.png │ │ └── ic_delete_forever_white_3x.png │ ├── ic_favorite.imageset │ │ ├── Contents.json │ │ ├── ic_favorite_36pt.png │ │ ├── ic_favorite_36pt_2x.png │ │ └── ic_favorite_36pt_3x.png │ ├── ic_favorite_border.imageset │ │ ├── Contents.json │ │ ├── ic_favorite_border_36pt.png │ │ ├── ic_favorite_border_36pt_2x.png │ │ └── ic_favorite_border_36pt_3x.png │ ├── ic_home.imageset │ │ ├── Contents.json │ │ ├── ic_home.png │ │ ├── ic_home_2x.png │ │ └── ic_home_3x.png │ ├── ic_logout.imageset │ │ ├── Contents.json │ │ ├── ic_logout_black_1x_ios_24dp.png │ │ ├── ic_logout_black_2x_ios_24dp.png │ │ └── ic_logout_black_3x_ios_24dp.png │ ├── ic_more_vert.imageset │ │ ├── Contents.json │ │ ├── ic_more_vert.png │ │ ├── ic_more_vert_2x.png │ │ └── ic_more_vert_3x.png │ ├── ic_more_vert_white.imageset │ │ ├── Contents.json │ │ ├── ic_more_vert_white.png │ │ ├── ic_more_vert_white_2x.png │ │ └── ic_more_vert_white_3x.png │ ├── ic_photo_camera.imageset │ │ ├── Contents.json │ │ ├── ic_photo_camera.png │ │ ├── ic_photo_camera_2x.png │ │ └── ic_photo_camera_3x.png │ ├── ic_photo_camera_white.imageset │ │ ├── Contents.json │ │ ├── ic_photo_camera_white.png │ │ ├── ic_photo_camera_white_2x.png │ │ └── ic_photo_camera_white_3x.png │ ├── ic_search.imageset │ │ ├── Contents.json │ │ ├── ic_search.png │ │ ├── ic_search_2x.png │ │ └── ic_search_3x.png │ ├── ic_send.imageset │ │ ├── Contents.json │ │ ├── ic_send.png │ │ ├── ic_send_2x.png │ │ └── ic_send_3x.png │ ├── ic_trending_up.imageset │ │ ├── Contents.json │ │ ├── ic_trending_up.png │ │ ├── ic_trending_up_2x.png │ │ └── ic_trending_up_3x.png │ └── image_logo.imageset │ │ ├── Contents.json │ │ ├── Icon-App-29x29@1x.png │ │ ├── Icon-App-29x29@2x.png │ │ └── Icon-App-29x29@3x.png ├── Base.lproj │ └── Main.storyboard ├── DateExtension.swift ├── FPAccountHeader.swift ├── FPAccountViewController.swift ├── FPAuthPickerViewController.swift ├── FPAuthPickerViewController.xib ├── FPCardCollectionViewCell.swift ├── FPCardCollectionViewCell.xib ├── FPCollectionViewTextCell.swift ├── FPComment.swift ├── FPCommentCell.swift ├── FPCommentViewController.swift ├── FPFeedViewController.swift ├── FPHashTagViewController.swift ├── FPPost.swift ├── FPPostDetailViewController.swift ├── FPSearchViewController.swift ├── FPUploadViewController.swift ├── FPUser.swift ├── FriendlyPix.entitlements ├── Launch.storyboard ├── UIImage+Circle.swift └── UITextViewPlaceholder.swift ├── FriendlyPixTests ├── FriendlyPixTests.swift └── Info.plist ├── FriendlyPixUITests ├── FriendlyPixUITests.swift └── Info.plist ├── Gemfile ├── Gemfile.lock ├── Info.plist ├── LICENSE ├── Podfile ├── Podfile.lock ├── README.md ├── friendly-pix.png ├── info_script.rb ├── mock-GoogleService-Info.plist └── test.sh /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | workflow_dispatch: 10 | 11 | jobs: 12 | build: 13 | runs-on: macos-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v2 17 | 18 | # Runs a set of commands using the runners shell 19 | - name: Run a multi-line script 20 | run: | 21 | bundle install 22 | gem install xcpretty 23 | gem install xcodeproj 24 | bundle exec pod install --repo-update 25 | cp ./mock-GoogleService-Info.plist GoogleService-Info.plist 26 | sed -i '' 's/YOUR_REVERSED_CLIENT_ID/com.googleusercontent.apps.123456789000-hjugbg6ud799v4c49dim8ce2usclthar/' Info.plist 27 | ruby ./info_script.rb 28 | ./test.sh 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | Pods/ 3 | xcuserdata/ 4 | *.xcworkspace/ 5 | GoogleService-Info.plist 6 | -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | opt_in_rules: 2 | - empty_count 3 | - file_header 4 | - explicit_init 5 | - closure_spacing 6 | - overridden_super_call 7 | - redundant_nil_coalescing 8 | - private_outlet 9 | - nimble_operator 10 | - attributes 11 | - operator_usage_whitespace 12 | - closure_end_indentation 13 | - first_where 14 | - sorted_imports 15 | - object_literal 16 | - number_separator 17 | - prohibited_super_call 18 | - fatal_error_message 19 | - vertical_parameter_alignment_on_call 20 | - let_var_whitespace 21 | - unneeded_parentheses_in_closure_argument 22 | - extension_access_modifier 23 | - pattern_matching_keywords 24 | - array_init 25 | - literal_expression_end_indentation 26 | 27 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Friendly Pix 2 | 3 | We'd love for you to contribute to our source code and to make the Friendly Pix even better than it is today! Here are the guidelines we'd like you to follow: 4 | 5 | - [Code of Conduct](#coc) 6 | - [Question or Problem?](#question) 7 | - [Issues and Bugs](#issue) 8 | - [Feature Requests](#feature) 9 | - [Submission Guidelines](#submit) 10 | - [Coding Rules](#rules) 11 | - [Signing the CLA](#cla) 12 | 13 | ## Code of Conduct 14 | 15 | As contributors and maintainers of the Friendly Pix project, we pledge to respect everyone who contributes by posting issues, updating documentation, submitting pull requests, providing feedback in comments, and any other activities. 16 | 17 | Communication through any of Friendly Pix's channels (GitHub, StackOverflow, Google+, Twitter, etc.) must be constructive and never resort to personal attacks, trolling, public or private harassment, insults, or other unprofessional conduct. 18 | 19 | We promise to extend courtesy and respect to everyone involved in this project regardless of gender, gender identity, sexual orientation, disability, age, race, ethnicity, religion, or level of experience. We expect anyone contributing to the project to do the same. 20 | 21 | If any member of the community violates this code of conduct, the maintainers of the Friendly Pix project may take action, removing issues, comments, and PRs or blocking accounts as deemed appropriate. 22 | 23 | If you are subject to or witness unacceptable behavior, or have any other concerns, please drop us a line at nivco@google.com. 24 | 25 | ## Got a Question or Problem? 26 | 27 | If you have technical questions about Friendly Pix, please direct these to [StackOverflow][stackoverflow] and use the `friendly-pix` tag. We are also available on GitHub issues. 28 | 29 | ## Found an Issue? 30 | If you find a bug in the source code or a mistake in the documentation, you can help us by 31 | submitting an issue to our [GitHub Repository][github]. Even better you can submit a Pull Request 32 | with a fix. 33 | 34 | See [below](#submit) for some guidelines. 35 | 36 | ## Want a Feature? 37 | You can request a new feature by submitting an issue to our [GitHub Repository][github]. 38 | 39 | If you would like to implement a new feature then consider what kind of change it is: 40 | 41 | * **Major Changes** that you wish to contribute to the project should be discussed first on our 42 | [issue tracker][github] so that we can better coordinate our efforts, prevent 43 | duplication of work, and help you to craft the change so that it is successfully accepted into the 44 | project. 45 | * **Small Changes** can be crafted and submitted to the [GitHub Repository][github] as a Pull Request directly. 46 | 47 | ## Submission Guidelines 48 | 49 | ### Submitting an Issue 50 | Before you submit your issue search the archive, maybe your question was already answered. 51 | 52 | If your issue appears to be a bug, and hasn't been reported, open a new issue. 53 | Help us to maximize the effort we can spend fixing issues and adding new 54 | features, by not reporting duplicate issues. Providing the following information will increase the 55 | chances of your issue being dealt with quickly: 56 | 57 | * **Overview of the Issue** - if an error is being thrown a non-minified stack trace helps 58 | * **Motivation for or Use Case** - explain why this is a bug for you 59 | * **Friendly Pix Version(s)** - is it a regression? 60 | * **Browsers and Operating System** - is this a problem with all browsers or only some browsers? 61 | * **Reproduce the Error** - provide an unambiguous set of steps. 62 | * **Related Issues** - has a similar issue been reported before? 63 | * **Suggest a Fix** - if you can't fix the bug yourself, perhaps you can point to what might be 64 | causing the problem (line of code or commit) 65 | 66 | **If you get help, help others. Good karma rulez!** 67 | 68 | Here's a template to get you started: 69 | 70 | ``` 71 | Browser: 72 | Browser version: 73 | Operating system: 74 | Operating system version: 75 | URL, if applicable: 76 | 77 | What steps will reproduce the problem: 78 | 1. 79 | 2. 80 | 3. 81 | 82 | What is the expected result? 83 | 84 | What happens instead of that? 85 | 86 | Please provide any other information below, and attach a screenshot if possible. 87 | ``` 88 | 89 | ### Submitting a Pull Request 90 | Before you submit your pull request consider the following guidelines: 91 | 92 | * Search [GitHub](https://github.com/firebase/friendlypix/pulls) for an open or closed Pull Request 93 | that relates to your submission. You don't want to duplicate effort. 94 | * Please sign our [Contributor License Agreement (CLA)](#cla) before sending pull 95 | requests. We cannot accept code without this. 96 | * Make your changes in a new git branch: 97 | 98 | ```shell 99 | git checkout -b my-fix-branch master 100 | ``` 101 | 102 | * Create your patch, **including appropriate test cases**. 103 | * Follow our [Coding Rules](#rules). 104 | * Avoid checking in files that shouldn't be tracked (e.g `node_modules`, `gulp-cache`, `.tmp`, `.idea`). We recommend using a [global](#global-gitignore) gitignore for this. 105 | * Make sure **not** to include a recompiled version of the files as part of your PR. We will generate these automatically. 106 | * Commit your changes using a descriptive commit message. 107 | 108 | ```shell 109 | git commit -a 110 | ``` 111 | Note: the optional commit `-a` command line option will automatically "add" and "rm" edited files. 112 | 113 | * Build your changes locally to ensure all the tests pass: 114 | 115 | ```shell 116 | gulp 117 | ``` 118 | 119 | * Push your branch to GitHub: 120 | 121 | ```shell 122 | git push origin my-fix-branch 123 | ``` 124 | 125 | * In GitHub, send a pull request to `friendlypix:master`. 126 | * If we suggest changes then: 127 | * Make the required updates. 128 | * Re-run the Friendly Pix test suite to ensure tests are still passing. 129 | * Rebase your branch and force push to your GitHub repository (this will update your Pull Request): 130 | 131 | ```shell 132 | git rebase master -i 133 | git push origin my-fix-branch -f 134 | ``` 135 | 136 | That's it! Thank you for your contribution! 137 | 138 | #### After your pull request is merged 139 | 140 | After your pull request is merged, you can safely delete your branch and pull the changes 141 | from the main (upstream) repository: 142 | 143 | * Delete the remote branch on GitHub either through the GitHub web UI or your local shell as follows: 144 | 145 | ```shell 146 | git push origin --delete my-fix-branch 147 | ``` 148 | 149 | * Check out the master branch: 150 | 151 | ```shell 152 | git checkout master -f 153 | ``` 154 | 155 | * Delete the local branch: 156 | 157 | ```shell 158 | git branch -D my-fix-branch 159 | ``` 160 | 161 | * Update your master with the latest upstream version: 162 | 163 | ```shell 164 | git pull --ff upstream master 165 | ``` 166 | 167 | ## Coding Rules 168 | 169 | We generally follow the [Google JavaScript style guide][js-style-guide] for the Web version. 170 | 171 | ## Signing the CLA 172 | 173 | Please sign our [Contributor License Agreement][google-cla] (CLA) before sending pull requests. For any code 174 | changes to be accepted, the CLA must be signed. It's a quick process, we promise! 175 | 176 | *This guide was inspired by the [AngularJS contribution guidelines](https://github.com/angular/angular.js/blob/master/CONTRIBUTING.md).* 177 | 178 | [github]: https://github.com/firebase/friendlypix 179 | [google-cla]: https://cla.developers.google.com 180 | [js-style-guide]: http://google.github.io/styleguide/javascriptguide.xml 181 | [jsbin]: http://jsbin.com/ 182 | [stackoverflow]: http://stackoverflow.com/questions/tagged/friendly-pix 183 | [global-gitignore]: https://help.github.com/articles/ignoring-files/#create-a-global-gitignore 184 | -------------------------------------------------------------------------------- /FriendlyPix.xcodeproj/xcshareddata/xcschemes/FriendlyPix.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 42 | 48 | 49 | 50 | 52 | 58 | 59 | 60 | 61 | 62 | 72 | 74 | 80 | 81 | 82 | 83 | 87 | 88 | 89 | 92 | 93 | 94 | 100 | 102 | 108 | 109 | 110 | 111 | 113 | 114 | 117 | 118 | 119 | -------------------------------------------------------------------------------- /FriendlyPix/Amaranth-Bold.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Amaranth-Bold.otf -------------------------------------------------------------------------------- /FriendlyPix/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2017 Google Inc. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | import Firebase 18 | import FirebaseUI 19 | import MaterialComponents 20 | import UserNotifications 21 | 22 | private let kFirebaseTermsOfService = URL(string: "https://firebase.google.com/terms/")! 23 | 24 | @UIApplicationMain 25 | class AppDelegate: UIResponder, UIApplicationDelegate { 26 | 27 | let mdcMessage = MDCSnackbarMessage() 28 | let mdcSnackBarManager = MDCSnackbarManager() 29 | let mdcAction = MDCSnackbarMessageAction() 30 | var window: UIWindow? 31 | lazy var database = Database.database() 32 | var blockedRef: DatabaseReference! 33 | var blockingRef: DatabaseReference! 34 | let gcmMessageIDKey = "gcm.message_id" 35 | var notificationGranted = false 36 | private var blocked = Set() 37 | private var blocking = Set() 38 | static var euroZone: Bool = { 39 | switch Locale.current.regionCode { 40 | case "CH", "AT", "IT", "BE", "LV", "BG", "LT", "HR", "LX", "CY", "MT", "CZ", "NL", "DK", 41 | "PL", "EE", "PT", "FI", "RO", "FR", "SK", "DE", "SI", "GR", "ES", "HU", "SE", "IE", "GB": 42 | return true 43 | default: 44 | return false 45 | } 46 | }() 47 | 48 | func application(_ application: UIApplication, didFinishLaunchingWithOptions 49 | launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 50 | FirebaseApp.configure() 51 | Messaging.messaging().delegate = self 52 | if let uid = Auth.auth().currentUser?.uid { 53 | blockedRef = database.reference(withPath: "blocked/\(uid)") 54 | blockingRef = database.reference(withPath: "blocking/\(uid)") 55 | observeBlocks() 56 | } 57 | 58 | if #available(iOS 10.0, *) { 59 | // For iOS 10 display notification (sent via APNS) 60 | UNUserNotificationCenter.current().delegate = self 61 | 62 | let authOptions: UNAuthorizationOptions = [.alert, .badge, .sound] 63 | UNUserNotificationCenter.current().requestAuthorization( 64 | options: authOptions, 65 | completionHandler: { granted, _ in 66 | if granted { 67 | if let uid = Auth.auth().currentUser?.uid { 68 | self.database.reference(withPath: "people/\(uid)/notificationEnabled").setValue(true) 69 | } else { 70 | self.notificationGranted = true 71 | } 72 | } 73 | }) 74 | } else { 75 | let settings: UIUserNotificationSettings = 76 | UIUserNotificationSettings(types: [.alert, .badge, .sound], categories: nil) 77 | application.registerUserNotificationSettings(settings) 78 | } 79 | 80 | application.registerForRemoteNotifications() 81 | 82 | let authUI = FUIAuth.defaultAuthUI() 83 | authUI?.delegate = self 84 | authUI?.tosurl = kFirebaseTermsOfService 85 | authUI?.shouldAutoUpgradeAnonymousUsers = true 86 | 87 | let providers: [FUIAuthProvider] = AppDelegate.euroZone ? [FUIAnonymousAuth()] : [FUIGoogleAuth(), FUIAnonymousAuth()] 88 | // ((Auth.auth().currentUser != nil) ? [FUIGoogleAuth()] as [FUIAuthProvider] : [FUIGoogleAuth(), FUIAnonymousAuth()//, FUIFacebookAuth 89 | //] as [FUIAuthProvider]) 90 | authUI?.providers = providers 91 | return true 92 | } 93 | 94 | func showAlert(_ userInfo: [AnyHashable: Any]) { 95 | let apsKey = "aps" 96 | let gcmMessage = "alert" 97 | let gcmLabel = "google.c.a.c_l" 98 | if let aps = userInfo[apsKey] as? [String: String], !aps.isEmpty, let message = aps[gcmMessage], 99 | let label = userInfo[gcmLabel] as? String { 100 | mdcMessage.text = "\(label): \(message)" 101 | 102 | mdcSnackBarManager.show(mdcMessage) 103 | } 104 | } 105 | 106 | func showContent(_ content: UNNotificationContent) { 107 | mdcMessage.text = content.body 108 | mdcAction.title = content.title 109 | mdcMessage.duration = 10_000 110 | mdcAction.handler = { 111 | guard let feed = self.window?.rootViewController?.children[0] as? FPFeedViewController else { return } 112 | let userId = content.categoryIdentifier.components(separatedBy: "/user/")[1] 113 | feed.showProfile(FPUser(dictionary: ["uid": userId])) 114 | } 115 | mdcMessage.action = mdcAction 116 | mdcSnackBarManager.show(mdcMessage) 117 | } 118 | 119 | @available(iOS 9.0, *) 120 | func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any]) -> Bool { 121 | guard let sourceApplication = options[UIApplication.OpenURLOptionsKey.sourceApplication] as? String else { 122 | return false 123 | } 124 | return self.handleOpenUrl(url, sourceApplication: sourceApplication) 125 | } 126 | 127 | @available(iOS 8.0, *) 128 | func application(_ application: UIApplication, open url: URL, sourceApplication: String?, annotation: Any) -> Bool { 129 | return self.handleOpenUrl(url, sourceApplication: sourceApplication) 130 | } 131 | 132 | func handleOpenUrl(_ url: URL, sourceApplication: String?) -> Bool { 133 | return FUIAuth.defaultAuthUI()?.handleOpen(url, sourceApplication: sourceApplication) ?? false 134 | } 135 | 136 | func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable: Any]) { 137 | // If you are receiving a notification message while your app is in the background, 138 | // this callback will not be fired till the user taps on the notification launching the application. 139 | showAlert(userInfo) 140 | } 141 | 142 | func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable: Any], 143 | fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { 144 | // If you are receiving a notification message while your app is in the background, 145 | // this callback will not be fired till the user taps on the notification launching the application. 146 | showAlert(userInfo) 147 | completionHandler(.newData) 148 | } 149 | 150 | func observeBlocks() { 151 | blockedRef.observe(.childAdded) { self.blocked.insert($0.key) } 152 | blockingRef.observe(.childAdded) { self.blocking.insert($0.key) } 153 | blockedRef.observe(.childRemoved) { self.blocked.remove($0.key) } 154 | blockingRef.observe(.childRemoved) { self.blocking.remove($0.key) } 155 | } 156 | 157 | func isBlocked(_ snapshot: DataSnapshot) -> Bool { 158 | let author = snapshot.childSnapshot(forPath: "author/uid").value as! String 159 | if blocked.contains(author) || blocking.contains(author) { 160 | return true 161 | } 162 | return false 163 | } 164 | 165 | func isBlocked(by person: String) -> Bool { 166 | return blocked.contains(person) 167 | } 168 | 169 | func isBlocking(_ person: String) -> Bool { 170 | return blocking.contains(person) 171 | } 172 | } 173 | 174 | extension AppDelegate: FUIAuthDelegate { 175 | func authUI(_ authUI: FUIAuth, didSignInWith authDataResult: AuthDataResult?, error: Error?) { 176 | switch error { 177 | case .some(let error as NSError) where UInt(error.code) == FUIAuthErrorCode.userCancelledSignIn.rawValue: 178 | print("User cancelled sign-in") 179 | case .some(let error as NSError) where UInt(error.code) == FUIAuthErrorCode.mergeConflict.rawValue: 180 | mdcSnackBarManager.show(MDCSnackbarMessage(text: "This identity is already associated with a different user account.")) 181 | case .some(let error as NSError) where UInt(error.code) == FUIAuthErrorCode.providerError.rawValue: 182 | mdcSnackBarManager.show(MDCSnackbarMessage(text: "There is an error with Google Sign in.")) 183 | case .some(let error as NSError) where error.userInfo[NSUnderlyingErrorKey] != nil: 184 | mdcSnackBarManager.show(MDCSnackbarMessage(text: "\(error.userInfo[NSUnderlyingErrorKey]!)")) 185 | case .some(let error): 186 | mdcSnackBarManager.show(MDCSnackbarMessage(text: error.localizedDescription)) 187 | case .none: 188 | if let user = authDataResult?.user { 189 | signed(in: user) 190 | } 191 | } 192 | } 193 | 194 | func authPickerViewController(forAuthUI authUI: FUIAuth) -> FUIAuthPickerViewController { 195 | return FPAuthPickerViewController(nibName: "FPAuthPickerViewController", bundle: Bundle.main, authUI: authUI) 196 | } 197 | 198 | func signOut() { 199 | blockedRef.removeAllObservers() 200 | blockingRef.removeAllObservers() 201 | blocked.removeAll() 202 | blocking.removeAll() 203 | } 204 | 205 | func signed(in user: User) { 206 | blockedRef = database.reference(withPath: "blocked/\(user.uid)") 207 | blockingRef = database.reference(withPath: "blocking/\(user.uid)") 208 | observeBlocks() 209 | let imageUrl = user.isAnonymous ? "" : user.providerData[0].photoURL?.absoluteString 210 | 211 | // If the main profile Pic is an expiring facebook profile pic URL we'll update it automatically to use the permanent graph API URL. 212 | // if let url = imageUrl, url.contains("lookaside.facebook.com") || url.contains("fbcdn.net") { 213 | // let facebookUID = user.providerData.first { (userinfo) -> Bool in 214 | // return userinfo.providerID == "facebook.com" 215 | // }?.providerID 216 | // if let facebook = facebookUID { 217 | // imageUrl = "https://graph.facebook.com/\(facebook)/picture?type=large" 218 | // } 219 | // } 220 | let displayName = user.isAnonymous ? "Anonymous" : user.providerData[0].displayName ?? "" 221 | 222 | 223 | var values: [String: Any] = ["profile_picture": imageUrl ?? "", 224 | "full_name": displayName] 225 | 226 | if !user.isAnonymous, let name = user.providerData[0].displayName, !name.isEmpty { 227 | values["_search_index"] = ["full_name": name.lowercased(), 228 | "reversed_full_name": name.components(separatedBy: " ") 229 | .reversed().joined(separator: "")] 230 | } 231 | 232 | if notificationGranted { 233 | values["notificationEnabled"] = true 234 | notificationGranted = false 235 | } 236 | database.reference(withPath: "people/\(user.uid)") 237 | .updateChildValues(values) 238 | } 239 | } 240 | 241 | @available(iOS 10, *) 242 | extension AppDelegate: UNUserNotificationCenterDelegate { 243 | 244 | // Receive displayed notifications for iOS 10 devices. 245 | func userNotificationCenter(_ center: UNUserNotificationCenter, 246 | willPresent notification: UNNotification, 247 | withCompletionHandler completionHandler: 248 | @escaping (UNNotificationPresentationOptions) -> Void) { 249 | showContent(notification.request.content) 250 | completionHandler([]) 251 | } 252 | 253 | func userNotificationCenter(_ center: UNUserNotificationCenter, 254 | didReceive response: UNNotificationResponse, 255 | withCompletionHandler completionHandler: @escaping () -> Void) { 256 | showContent(response.notification.request.content) 257 | completionHandler() 258 | } 259 | } 260 | 261 | extension AppDelegate: MessagingDelegate { 262 | func messaging(_ messaging: Messaging, didReceiveRegistrationToken fcmToken: String) { 263 | guard let uid = Auth.auth().currentUser?.uid else { return } 264 | Database.database().reference(withPath: "/people/\(uid)/notificationTokens/\(fcmToken)").setValue(true) 265 | } 266 | 267 | // Receive data messages on iOS 10+ directly from FCM (bypassing APNs) when the app is in the foreground. 268 | // To enable direct data messages, you can set Messaging.messaging().shouldEstablishDirectChannel to true. 269 | func messaging(_ messaging: Messaging, didReceive remoteMessage: MessagingRemoteMessage) { 270 | let data = remoteMessage.appData 271 | showAlert(data) 272 | } 273 | } 274 | -------------------------------------------------------------------------------- /FriendlyPix/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "Icon-App-20x20@2x.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "Icon-App-20x20@3x.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "Icon-App-29x29@1x.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "Icon-App-29x29@2x.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "29x29", 29 | "idiom" : "iphone", 30 | "filename" : "Icon-App-29x29@3x.png", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "Icon-App-40x40@2x.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "40x40", 41 | "idiom" : "iphone", 42 | "filename" : "Icon-App-40x40@3x.png", 43 | "scale" : "3x" 44 | }, 45 | { 46 | "size" : "57x57", 47 | "idiom" : "iphone", 48 | "filename" : "Icon-App-57x57@1x.png", 49 | "scale" : "1x" 50 | }, 51 | { 52 | "size" : "57x57", 53 | "idiom" : "iphone", 54 | "filename" : "Icon-App-57x57@2x.png", 55 | "scale" : "2x" 56 | }, 57 | { 58 | "size" : "60x60", 59 | "idiom" : "iphone", 60 | "filename" : "Icon-App-60x60@2x.png", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "size" : "60x60", 65 | "idiom" : "iphone", 66 | "filename" : "Icon-App-60x60@3x.png", 67 | "scale" : "3x" 68 | }, 69 | { 70 | "size" : "20x20", 71 | "idiom" : "ipad", 72 | "filename" : "Icon-App-20x20@1x.png", 73 | "scale" : "1x" 74 | }, 75 | { 76 | "size" : "20x20", 77 | "idiom" : "ipad", 78 | "filename" : "Icon-App-20x20@2x.png", 79 | "scale" : "2x" 80 | }, 81 | { 82 | "size" : "29x29", 83 | "idiom" : "ipad", 84 | "filename" : "Icon-App-29x29@1x.png", 85 | "scale" : "1x" 86 | }, 87 | { 88 | "size" : "29x29", 89 | "idiom" : "ipad", 90 | "filename" : "Icon-App-29x29@2x.png", 91 | "scale" : "2x" 92 | }, 93 | { 94 | "size" : "40x40", 95 | "idiom" : "ipad", 96 | "filename" : "Icon-App-40x40@1x.png", 97 | "scale" : "1x" 98 | }, 99 | { 100 | "size" : "40x40", 101 | "idiom" : "ipad", 102 | "filename" : "Icon-App-40x40@2x.png", 103 | "scale" : "2x" 104 | }, 105 | { 106 | "size" : "50x50", 107 | "idiom" : "ipad", 108 | "filename" : "Icon-Small-50x50@1x.png", 109 | "scale" : "1x" 110 | }, 111 | { 112 | "size" : "50x50", 113 | "idiom" : "ipad", 114 | "filename" : "Icon-Small-50x50@2x.png", 115 | "scale" : "2x" 116 | }, 117 | { 118 | "size" : "72x72", 119 | "idiom" : "ipad", 120 | "filename" : "Icon-App-72x72@1x.png", 121 | "scale" : "1x" 122 | }, 123 | { 124 | "size" : "72x72", 125 | "idiom" : "ipad", 126 | "filename" : "Icon-App-72x72@2x.png", 127 | "scale" : "2x" 128 | }, 129 | { 130 | "size" : "76x76", 131 | "idiom" : "ipad", 132 | "filename" : "Icon-App-76x76@1x.png", 133 | "scale" : "1x" 134 | }, 135 | { 136 | "size" : "76x76", 137 | "idiom" : "ipad", 138 | "filename" : "Icon-App-76x76@2x.png", 139 | "scale" : "2x" 140 | }, 141 | { 142 | "size" : "83.5x83.5", 143 | "idiom" : "ipad", 144 | "filename" : "Icon-App-83.5x83.5@2x.png", 145 | "scale" : "2x" 146 | }, 147 | { 148 | "size" : "1024x1024", 149 | "idiom" : "ios-marketing", 150 | "filename" : "ItunesArtwork@2x.png", 151 | "scale" : "1x" 152 | } 153 | ], 154 | "info" : { 155 | "version" : 1, 156 | "author" : "xcode" 157 | } 158 | } -------------------------------------------------------------------------------- /FriendlyPix/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /FriendlyPix/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /FriendlyPix/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /FriendlyPix/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /FriendlyPix/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /FriendlyPix/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /FriendlyPix/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /FriendlyPix/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /FriendlyPix/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /FriendlyPix/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png -------------------------------------------------------------------------------- /FriendlyPix/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png -------------------------------------------------------------------------------- /FriendlyPix/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /FriendlyPix/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /FriendlyPix/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png -------------------------------------------------------------------------------- /FriendlyPix/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png -------------------------------------------------------------------------------- /FriendlyPix/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /FriendlyPix/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /FriendlyPix/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /FriendlyPix/Assets.xcassets/AppIcon.appiconset/Icon-Small-50x50@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/AppIcon.appiconset/Icon-Small-50x50@1x.png -------------------------------------------------------------------------------- /FriendlyPix/Assets.xcassets/AppIcon.appiconset/Icon-Small-50x50@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/AppIcon.appiconset/Icon-Small-50x50@2x.png -------------------------------------------------------------------------------- /FriendlyPix/Assets.xcassets/AppIcon.appiconset/ItunesArtwork@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/AppIcon.appiconset/ItunesArtwork@2x.png -------------------------------------------------------------------------------- /FriendlyPix/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /FriendlyPix/Assets.xcassets/Logo.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "logo1024-universal-341@1x.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "logo1024-universal-341@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "logo1024-universal-341@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /FriendlyPix/Assets.xcassets/Logo.imageset/logo1024-universal-341@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/Logo.imageset/logo1024-universal-341@1x.png -------------------------------------------------------------------------------- /FriendlyPix/Assets.xcassets/Logo.imageset/logo1024-universal-341@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/Logo.imageset/logo1024-universal-341@2x.png -------------------------------------------------------------------------------- /FriendlyPix/Assets.xcassets/Logo.imageset/logo1024-universal-341@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/Logo.imageset/logo1024-universal-341@3x.png -------------------------------------------------------------------------------- /FriendlyPix/Assets.xcassets/friendlypix_launch_logo.imageset/144_1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/friendlypix_launch_logo.imageset/144_1x.png -------------------------------------------------------------------------------- /FriendlyPix/Assets.xcassets/friendlypix_launch_logo.imageset/144_2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/friendlypix_launch_logo.imageset/144_2x.png -------------------------------------------------------------------------------- /FriendlyPix/Assets.xcassets/friendlypix_launch_logo.imageset/144_3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/friendlypix_launch_logo.imageset/144_3x.png -------------------------------------------------------------------------------- /FriendlyPix/Assets.xcassets/friendlypix_launch_logo.imageset/192_1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/friendlypix_launch_logo.imageset/192_1x.png -------------------------------------------------------------------------------- /FriendlyPix/Assets.xcassets/friendlypix_launch_logo.imageset/192_2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/friendlypix_launch_logo.imageset/192_2x.png -------------------------------------------------------------------------------- /FriendlyPix/Assets.xcassets/friendlypix_launch_logo.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "filename" : "144_1x.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "filename" : "144_2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "filename" : "144_3x.png", 16 | "scale" : "3x" 17 | }, 18 | { 19 | "idiom" : "ipad", 20 | "filename" : "192_1x.png", 21 | "scale" : "1x" 22 | }, 23 | { 24 | "idiom" : "ipad", 25 | "filename" : "192_2x.png", 26 | "scale" : "2x" 27 | } 28 | ], 29 | "info" : { 30 | "version" : 1, 31 | "author" : "xcode" 32 | } 33 | } -------------------------------------------------------------------------------- /FriendlyPix/Assets.xcassets/google_launch_logo.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "filename" : "googlelogo_dark20_color_132x44pt_1x.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "filename" : "googlelogo_dark20_color_132x44pt_2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "filename" : "googlelogo_dark20_color_132x44pt_3x.png", 16 | "scale" : "3x" 17 | }, 18 | { 19 | "idiom" : "ipad", 20 | "filename" : "googlelogo_dark20_color_184x60pt_1x.png", 21 | "scale" : "1x" 22 | }, 23 | { 24 | "idiom" : "ipad", 25 | "filename" : "googlelogo_dark20_color_184x60pt_2x.png", 26 | "scale" : "2x" 27 | } 28 | ], 29 | "info" : { 30 | "version" : 1, 31 | "author" : "xcode" 32 | } 33 | } -------------------------------------------------------------------------------- /FriendlyPix/Assets.xcassets/google_launch_logo.imageset/googlelogo_dark20_color_132x44pt_1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/google_launch_logo.imageset/googlelogo_dark20_color_132x44pt_1x.png -------------------------------------------------------------------------------- /FriendlyPix/Assets.xcassets/google_launch_logo.imageset/googlelogo_dark20_color_132x44pt_2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/google_launch_logo.imageset/googlelogo_dark20_color_132x44pt_2x.png -------------------------------------------------------------------------------- /FriendlyPix/Assets.xcassets/google_launch_logo.imageset/googlelogo_dark20_color_132x44pt_3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/google_launch_logo.imageset/googlelogo_dark20_color_132x44pt_3x.png -------------------------------------------------------------------------------- /FriendlyPix/Assets.xcassets/google_launch_logo.imageset/googlelogo_dark20_color_184x60pt_1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/google_launch_logo.imageset/googlelogo_dark20_color_184x60pt_1x.png -------------------------------------------------------------------------------- /FriendlyPix/Assets.xcassets/google_launch_logo.imageset/googlelogo_dark20_color_184x60pt_2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/google_launch_logo.imageset/googlelogo_dark20_color_184x60pt_2x.png -------------------------------------------------------------------------------- /FriendlyPix/Assets.xcassets/ic_account_circle_36pt.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "ic_account_circle_36pt.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "ic_account_circle_36pt_2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "ic_account_circle_36pt_3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /FriendlyPix/Assets.xcassets/ic_account_circle_36pt.imageset/ic_account_circle_36pt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/ic_account_circle_36pt.imageset/ic_account_circle_36pt.png -------------------------------------------------------------------------------- /FriendlyPix/Assets.xcassets/ic_account_circle_36pt.imageset/ic_account_circle_36pt_2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/ic_account_circle_36pt.imageset/ic_account_circle_36pt_2x.png -------------------------------------------------------------------------------- /FriendlyPix/Assets.xcassets/ic_account_circle_36pt.imageset/ic_account_circle_36pt_3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/ic_account_circle_36pt.imageset/ic_account_circle_36pt_3x.png -------------------------------------------------------------------------------- /FriendlyPix/Assets.xcassets/ic_arrow_back.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "ic_arrow_back.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "ic_arrow_back_2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "ic_arrow_back_3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | }, 23 | "properties" : { 24 | "template-rendering-intent" : "template" 25 | } 26 | } -------------------------------------------------------------------------------- /FriendlyPix/Assets.xcassets/ic_arrow_back.imageset/ic_arrow_back.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/ic_arrow_back.imageset/ic_arrow_back.png -------------------------------------------------------------------------------- /FriendlyPix/Assets.xcassets/ic_arrow_back.imageset/ic_arrow_back_2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/ic_arrow_back.imageset/ic_arrow_back_2x.png -------------------------------------------------------------------------------- /FriendlyPix/Assets.xcassets/ic_arrow_back.imageset/ic_arrow_back_3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/ic_arrow_back.imageset/ic_arrow_back_3x.png -------------------------------------------------------------------------------- /FriendlyPix/Assets.xcassets/ic_close.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "ic_close.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "ic_close_2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "ic_close_3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /FriendlyPix/Assets.xcassets/ic_close.imageset/ic_close.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/ic_close.imageset/ic_close.png -------------------------------------------------------------------------------- /FriendlyPix/Assets.xcassets/ic_close.imageset/ic_close_2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/ic_close.imageset/ic_close_2x.png -------------------------------------------------------------------------------- /FriendlyPix/Assets.xcassets/ic_close.imageset/ic_close_3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/ic_close.imageset/ic_close_3x.png -------------------------------------------------------------------------------- /FriendlyPix/Assets.xcassets/ic_comment.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "ic_comment_36pt.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "ic_comment_36pt_2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "ic_comment_36pt_3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | }, 23 | "properties" : { 24 | "template-rendering-intent" : "template" 25 | } 26 | } -------------------------------------------------------------------------------- /FriendlyPix/Assets.xcassets/ic_comment.imageset/ic_comment_36pt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/ic_comment.imageset/ic_comment_36pt.png -------------------------------------------------------------------------------- /FriendlyPix/Assets.xcassets/ic_comment.imageset/ic_comment_36pt_2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/ic_comment.imageset/ic_comment_36pt_2x.png -------------------------------------------------------------------------------- /FriendlyPix/Assets.xcassets/ic_comment.imageset/ic_comment_36pt_3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/ic_comment.imageset/ic_comment_36pt_3x.png -------------------------------------------------------------------------------- /FriendlyPix/Assets.xcassets/ic_comment_white.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "ic_comment_white_36pt.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "ic_comment_white_36pt_2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "ic_comment_white_36pt_3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | }, 23 | "properties" : { 24 | "template-rendering-intent" : "template" 25 | } 26 | } -------------------------------------------------------------------------------- /FriendlyPix/Assets.xcassets/ic_comment_white.imageset/ic_comment_white_36pt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/ic_comment_white.imageset/ic_comment_white_36pt.png -------------------------------------------------------------------------------- /FriendlyPix/Assets.xcassets/ic_comment_white.imageset/ic_comment_white_36pt_2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/ic_comment_white.imageset/ic_comment_white_36pt_2x.png -------------------------------------------------------------------------------- /FriendlyPix/Assets.xcassets/ic_comment_white.imageset/ic_comment_white_36pt_3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/ic_comment_white.imageset/ic_comment_white_36pt_3x.png -------------------------------------------------------------------------------- /FriendlyPix/Assets.xcassets/ic_delete_forever_white.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "ic_delete_forever_white.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "ic_delete_forever_white_2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "ic_delete_forever_white_3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /FriendlyPix/Assets.xcassets/ic_delete_forever_white.imageset/ic_delete_forever_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/ic_delete_forever_white.imageset/ic_delete_forever_white.png -------------------------------------------------------------------------------- /FriendlyPix/Assets.xcassets/ic_delete_forever_white.imageset/ic_delete_forever_white_2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/ic_delete_forever_white.imageset/ic_delete_forever_white_2x.png -------------------------------------------------------------------------------- /FriendlyPix/Assets.xcassets/ic_delete_forever_white.imageset/ic_delete_forever_white_3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/ic_delete_forever_white.imageset/ic_delete_forever_white_3x.png -------------------------------------------------------------------------------- /FriendlyPix/Assets.xcassets/ic_favorite.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "ic_favorite_36pt.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "ic_favorite_36pt_2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "ic_favorite_36pt_3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | }, 23 | "properties" : { 24 | "template-rendering-intent" : "template" 25 | } 26 | } -------------------------------------------------------------------------------- /FriendlyPix/Assets.xcassets/ic_favorite.imageset/ic_favorite_36pt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/ic_favorite.imageset/ic_favorite_36pt.png -------------------------------------------------------------------------------- /FriendlyPix/Assets.xcassets/ic_favorite.imageset/ic_favorite_36pt_2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/ic_favorite.imageset/ic_favorite_36pt_2x.png -------------------------------------------------------------------------------- /FriendlyPix/Assets.xcassets/ic_favorite.imageset/ic_favorite_36pt_3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/ic_favorite.imageset/ic_favorite_36pt_3x.png -------------------------------------------------------------------------------- /FriendlyPix/Assets.xcassets/ic_favorite_border.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "ic_favorite_border_36pt.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "ic_favorite_border_36pt_2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "ic_favorite_border_36pt_3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | }, 23 | "properties" : { 24 | "template-rendering-intent" : "template" 25 | } 26 | } -------------------------------------------------------------------------------- /FriendlyPix/Assets.xcassets/ic_favorite_border.imageset/ic_favorite_border_36pt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/ic_favorite_border.imageset/ic_favorite_border_36pt.png -------------------------------------------------------------------------------- /FriendlyPix/Assets.xcassets/ic_favorite_border.imageset/ic_favorite_border_36pt_2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/ic_favorite_border.imageset/ic_favorite_border_36pt_2x.png -------------------------------------------------------------------------------- /FriendlyPix/Assets.xcassets/ic_favorite_border.imageset/ic_favorite_border_36pt_3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/ic_favorite_border.imageset/ic_favorite_border_36pt_3x.png -------------------------------------------------------------------------------- /FriendlyPix/Assets.xcassets/ic_home.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "ic_home.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "ic_home_2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "ic_home_3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | }, 23 | "properties" : { 24 | "template-rendering-intent" : "template" 25 | } 26 | } -------------------------------------------------------------------------------- /FriendlyPix/Assets.xcassets/ic_home.imageset/ic_home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/ic_home.imageset/ic_home.png -------------------------------------------------------------------------------- /FriendlyPix/Assets.xcassets/ic_home.imageset/ic_home_2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/ic_home.imageset/ic_home_2x.png -------------------------------------------------------------------------------- /FriendlyPix/Assets.xcassets/ic_home.imageset/ic_home_3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/ic_home.imageset/ic_home_3x.png -------------------------------------------------------------------------------- /FriendlyPix/Assets.xcassets/ic_logout.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "ic_logout_black_1x_ios_24dp.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "ic_logout_black_2x_ios_24dp.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "ic_logout_black_3x_ios_24dp.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | }, 23 | "properties" : { 24 | "template-rendering-intent" : "template" 25 | } 26 | } -------------------------------------------------------------------------------- /FriendlyPix/Assets.xcassets/ic_logout.imageset/ic_logout_black_1x_ios_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/ic_logout.imageset/ic_logout_black_1x_ios_24dp.png -------------------------------------------------------------------------------- /FriendlyPix/Assets.xcassets/ic_logout.imageset/ic_logout_black_2x_ios_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/ic_logout.imageset/ic_logout_black_2x_ios_24dp.png -------------------------------------------------------------------------------- /FriendlyPix/Assets.xcassets/ic_logout.imageset/ic_logout_black_3x_ios_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/ic_logout.imageset/ic_logout_black_3x_ios_24dp.png -------------------------------------------------------------------------------- /FriendlyPix/Assets.xcassets/ic_more_vert.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "ic_more_vert.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "ic_more_vert_2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "ic_more_vert_3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /FriendlyPix/Assets.xcassets/ic_more_vert.imageset/ic_more_vert.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/ic_more_vert.imageset/ic_more_vert.png -------------------------------------------------------------------------------- /FriendlyPix/Assets.xcassets/ic_more_vert.imageset/ic_more_vert_2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/ic_more_vert.imageset/ic_more_vert_2x.png -------------------------------------------------------------------------------- /FriendlyPix/Assets.xcassets/ic_more_vert.imageset/ic_more_vert_3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/ic_more_vert.imageset/ic_more_vert_3x.png -------------------------------------------------------------------------------- /FriendlyPix/Assets.xcassets/ic_more_vert_white.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "ic_more_vert_white.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "ic_more_vert_white_2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "ic_more_vert_white_3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | }, 23 | "properties" : { 24 | "template-rendering-intent" : "template" 25 | } 26 | } -------------------------------------------------------------------------------- /FriendlyPix/Assets.xcassets/ic_more_vert_white.imageset/ic_more_vert_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/ic_more_vert_white.imageset/ic_more_vert_white.png -------------------------------------------------------------------------------- /FriendlyPix/Assets.xcassets/ic_more_vert_white.imageset/ic_more_vert_white_2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/ic_more_vert_white.imageset/ic_more_vert_white_2x.png -------------------------------------------------------------------------------- /FriendlyPix/Assets.xcassets/ic_more_vert_white.imageset/ic_more_vert_white_3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/ic_more_vert_white.imageset/ic_more_vert_white_3x.png -------------------------------------------------------------------------------- /FriendlyPix/Assets.xcassets/ic_photo_camera.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "ic_photo_camera.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "ic_photo_camera_2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "ic_photo_camera_3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /FriendlyPix/Assets.xcassets/ic_photo_camera.imageset/ic_photo_camera.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/ic_photo_camera.imageset/ic_photo_camera.png -------------------------------------------------------------------------------- /FriendlyPix/Assets.xcassets/ic_photo_camera.imageset/ic_photo_camera_2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/ic_photo_camera.imageset/ic_photo_camera_2x.png -------------------------------------------------------------------------------- /FriendlyPix/Assets.xcassets/ic_photo_camera.imageset/ic_photo_camera_3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/ic_photo_camera.imageset/ic_photo_camera_3x.png -------------------------------------------------------------------------------- /FriendlyPix/Assets.xcassets/ic_photo_camera_white.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "ic_photo_camera_white.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "ic_photo_camera_white_2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "ic_photo_camera_white_3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /FriendlyPix/Assets.xcassets/ic_photo_camera_white.imageset/ic_photo_camera_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/ic_photo_camera_white.imageset/ic_photo_camera_white.png -------------------------------------------------------------------------------- /FriendlyPix/Assets.xcassets/ic_photo_camera_white.imageset/ic_photo_camera_white_2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/ic_photo_camera_white.imageset/ic_photo_camera_white_2x.png -------------------------------------------------------------------------------- /FriendlyPix/Assets.xcassets/ic_photo_camera_white.imageset/ic_photo_camera_white_3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/ic_photo_camera_white.imageset/ic_photo_camera_white_3x.png -------------------------------------------------------------------------------- /FriendlyPix/Assets.xcassets/ic_search.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "ic_search.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "ic_search_2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "ic_search_3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | }, 23 | "properties" : { 24 | "template-rendering-intent" : "template" 25 | } 26 | } -------------------------------------------------------------------------------- /FriendlyPix/Assets.xcassets/ic_search.imageset/ic_search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/ic_search.imageset/ic_search.png -------------------------------------------------------------------------------- /FriendlyPix/Assets.xcassets/ic_search.imageset/ic_search_2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/ic_search.imageset/ic_search_2x.png -------------------------------------------------------------------------------- /FriendlyPix/Assets.xcassets/ic_search.imageset/ic_search_3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/ic_search.imageset/ic_search_3x.png -------------------------------------------------------------------------------- /FriendlyPix/Assets.xcassets/ic_send.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "ic_send.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "ic_send_2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "ic_send_3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | }, 23 | "properties" : { 24 | "template-rendering-intent" : "template" 25 | } 26 | } -------------------------------------------------------------------------------- /FriendlyPix/Assets.xcassets/ic_send.imageset/ic_send.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/ic_send.imageset/ic_send.png -------------------------------------------------------------------------------- /FriendlyPix/Assets.xcassets/ic_send.imageset/ic_send_2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/ic_send.imageset/ic_send_2x.png -------------------------------------------------------------------------------- /FriendlyPix/Assets.xcassets/ic_send.imageset/ic_send_3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/ic_send.imageset/ic_send_3x.png -------------------------------------------------------------------------------- /FriendlyPix/Assets.xcassets/ic_trending_up.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "ic_trending_up.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "ic_trending_up_2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "ic_trending_up_3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | }, 23 | "properties" : { 24 | "template-rendering-intent" : "template" 25 | } 26 | } -------------------------------------------------------------------------------- /FriendlyPix/Assets.xcassets/ic_trending_up.imageset/ic_trending_up.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/ic_trending_up.imageset/ic_trending_up.png -------------------------------------------------------------------------------- /FriendlyPix/Assets.xcassets/ic_trending_up.imageset/ic_trending_up_2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/ic_trending_up.imageset/ic_trending_up_2x.png -------------------------------------------------------------------------------- /FriendlyPix/Assets.xcassets/ic_trending_up.imageset/ic_trending_up_3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/ic_trending_up.imageset/ic_trending_up_3x.png -------------------------------------------------------------------------------- /FriendlyPix/Assets.xcassets/image_logo.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "Icon-App-29x29@1x.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "Icon-App-29x29@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "Icon-App-29x29@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /FriendlyPix/Assets.xcassets/image_logo.imageset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/image_logo.imageset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /FriendlyPix/Assets.xcassets/image_logo.imageset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/image_logo.imageset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /FriendlyPix/Assets.xcassets/image_logo.imageset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/FriendlyPix/Assets.xcassets/image_logo.imageset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /FriendlyPix/DateExtension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2017 Google Inc. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | import Foundation 18 | 19 | extension Date { 20 | 21 | func timeAgo() -> String { 22 | 23 | let interval = Calendar.current.dateComponents([.year, .day, .hour, .minute, .second], from: self, to: Date()) 24 | 25 | if let year = interval.year, year > 0 { 26 | return DateFormatter.localizedString(from: self, dateStyle: .long, timeStyle: .none) 27 | } else if let day = interval.day, day > 6 { 28 | let format = DateFormatter.dateFormat(fromTemplate: "MMMMd", options: 0, locale: NSLocale.current) 29 | let formatter = DateFormatter() 30 | formatter.dateFormat = format 31 | return formatter.string(from: self) 32 | } else if let day = interval.day, day > 0 { 33 | return day == 1 ? "\(day)" + " " + "day ago" : 34 | "\(day)" + " " + "days ago" 35 | } else if let hour = interval.hour, hour > 0 { 36 | return hour == 1 ? "\(hour)" + " " + "hour ago" : 37 | "\(hour)" + " " + "hours ago" 38 | } else if let minute = interval.minute, minute > 0 { 39 | return minute == 1 ? "\(minute)" + " " + "minute ago" : 40 | "\(minute)" + " " + "minutes ago" 41 | } else if let second = interval.second, second > 0 { 42 | return second == 1 ? "\(second)" + " " + "second ago" : 43 | "\(second)" + " " + "seconds ago" 44 | } else { 45 | return "just now" 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /FriendlyPix/FPAccountHeader.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2017 Google Inc. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | import MaterialComponents 18 | 19 | class FPAccountHeader: MDCBaseCell { 20 | @IBOutlet weak var postsLabel: UILabel! 21 | @IBOutlet weak var followingLabel: UILabel! 22 | @IBOutlet weak var followersLabel: UILabel! 23 | @IBOutlet weak var profilePictureImageView: UIImageView! 24 | @IBOutlet weak var followLabel: UILabel! 25 | @IBOutlet weak var followSwitch: UISwitch! 26 | } 27 | -------------------------------------------------------------------------------- /FriendlyPix/FPAccountViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2017 Google Inc. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | import Firebase 18 | import Lightbox 19 | import MaterialComponents 20 | 21 | class FPAccountViewController: UICollectionViewController, UICollectionViewDelegateFlowLayout { 22 | var headerView: FPAccountHeader! 23 | var profile: FPUser! 24 | let uid = Auth.auth().currentUser!.uid 25 | let database = Database.database() 26 | let ref = Database.database().reference() 27 | var postIds: [String: Any]? 28 | var postSnapshots = [DataSnapshot]() 29 | var loadingPostCount = 0 30 | var firebaseRefs = [DatabaseReference]() 31 | var insets: UIEdgeInsets! 32 | lazy var appDelegate = UIApplication.shared.delegate as! AppDelegate 33 | 34 | override func viewDidLoad() { 35 | super.viewDidLoad() 36 | 37 | 38 | navigationItem.title = profile.fullname.localizedCapitalized 39 | } 40 | 41 | override func viewDidAppear(_ animated: Bool) { 42 | super.viewDidAppear(animated) 43 | loadData() 44 | } 45 | 46 | override func viewWillDisappear(_ animated: Bool) { 47 | super.viewWillDisappear(animated) 48 | for firebaseRef in firebaseRefs { 49 | firebaseRef.removeAllObservers() 50 | } 51 | firebaseRefs = [DatabaseReference]() 52 | } 53 | 54 | @IBAction func valueChanged(_ sender: Any) { 55 | if profile.uid == uid { 56 | let notificationEnabled = database.reference(withPath: "people/\(uid)/notificationEnabled") 57 | if headerView.followSwitch.isOn { 58 | notificationEnabled.setValue(true) 59 | } else { 60 | notificationEnabled.removeValue() 61 | } 62 | return 63 | } 64 | 65 | toggleFollow(headerView.followSwitch.isOn) 66 | } 67 | 68 | 69 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { 70 | if indexPath.section == 0 { 71 | return CGSize(width: collectionView.bounds.size.width, height: 112) 72 | } 73 | let height = ceil(((collectionView.bounds.width) - 14) * 0.325) 74 | return CGSize(width: height, height: height) 75 | } 76 | 77 | func registerToFollowStatusUpdate() { 78 | let followStatusRef = database.reference(withPath: "people/\(uid)/following/\(profile.uid)") 79 | followStatusRef.observe(.value) { 80 | self.headerView.followSwitch.isOn = $0.exists() 81 | } 82 | firebaseRefs.append(followStatusRef) 83 | } 84 | 85 | func registerToNotificationEnabledStatusUpdate() { 86 | let notificationEnabledRef = database.reference(withPath: "people/\(uid)/notificationEnabled") 87 | notificationEnabledRef.observe(.value) { 88 | self.headerView.followSwitch.isOn = $0.exists() 89 | } 90 | firebaseRefs.append(notificationEnabledRef) 91 | } 92 | 93 | func registerForFollowersCount() { 94 | let followersRef = database.reference(withPath: "followers/\(profile.uid)") 95 | followersRef.observe(.value, with: { 96 | self.headerView.followersLabel.text = "\($0.childrenCount) follower\($0.childrenCount != 1 ? "s" : "")" 97 | }) 98 | firebaseRefs.append(followersRef) 99 | } 100 | 101 | func registerForFollowingCount() { 102 | let followingRef = database.reference(withPath: "people/\(profile.uid)/following") 103 | followingRef.observe(.value, with: { 104 | self.headerView.followingLabel.text = "\($0.childrenCount) following" 105 | }) 106 | firebaseRefs.append(followingRef) 107 | } 108 | 109 | func registerForPostsCount() { 110 | let userPostsRef = database.reference(withPath: "people/\(profile.uid)/posts") 111 | userPostsRef.observe(.value, with: { 112 | self.headerView.postsLabel.text = "\($0.childrenCount) post\($0.childrenCount != 1 ? "s" : "")" 113 | }) 114 | } 115 | 116 | func registerForPostsDeletion() { 117 | let userPostsRef = database.reference(withPath: "people/\(profile.uid)/posts") 118 | userPostsRef.observe(.childRemoved, with: { postSnapshot in 119 | var index = 0 120 | for post in self.postSnapshots { 121 | if post.key == postSnapshot.key { 122 | self.postSnapshots.remove(at: index) 123 | self.loadingPostCount -= 1 124 | self.collectionView?.deleteItems(at: [IndexPath(item: index, section: 1)]) 125 | return 126 | } 127 | index += 1 128 | } 129 | self.postIds?.removeValue(forKey: postSnapshot.key) 130 | }) 131 | } 132 | 133 | 134 | func loadUserPosts() { 135 | database.reference(withPath: "people/\(profile.uid)/posts").observeSingleEvent(of: .value, with: { 136 | if var posts = $0.value as? [String: Any] { 137 | if !self.postSnapshots.isEmpty { 138 | var index = self.postSnapshots.count - 1 139 | self.collectionView?.performBatchUpdates({ 140 | for post in self.postSnapshots.reversed() { 141 | if posts.removeValue(forKey: post.key) == nil { 142 | self.postSnapshots.remove(at: index) 143 | self.collectionView?.deleteItems(at: [IndexPath(item: index, section: 1)]) 144 | return 145 | } 146 | index -= 1 147 | } 148 | }, completion: nil) 149 | self.postIds = posts 150 | self.loadingPostCount = posts.count 151 | } else { 152 | self.postIds = posts 153 | self.loadFeed() 154 | } 155 | self.registerForPostsDeletion() 156 | } 157 | }) 158 | } 159 | 160 | func loadData() { 161 | if profile.uid == uid { 162 | registerToNotificationEnabledStatusUpdate() 163 | } else { 164 | registerToFollowStatusUpdate() 165 | } 166 | registerForFollowersCount() 167 | registerForFollowingCount() 168 | registerForPostsCount() 169 | loadUserPosts() 170 | } 171 | 172 | override func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, 173 | forItemAt indexPath: IndexPath) { 174 | if indexPath.section == 1 && indexPath.item == (loadingPostCount - 3) { 175 | loadFeed() 176 | } 177 | } 178 | 179 | func loadFeed() { 180 | loadingPostCount = postSnapshots.count + 10 181 | self.collectionView?.performBatchUpdates({ 182 | for _ in 1...10 { 183 | if let postId = self.postIds?.popFirst()?.key { 184 | database.reference(withPath: "posts/\(postId)").observeSingleEvent(of: .value, with: { postSnapshot in 185 | self.postSnapshots.append(postSnapshot) 186 | self.collectionView?.insertItems(at: [IndexPath(item: self.postSnapshots.count - 1, section: 1)]) 187 | }) 188 | } else { 189 | break 190 | } 191 | } 192 | }, completion: nil) 193 | } 194 | 195 | override func numberOfSections(in collectionView: UICollectionView) -> Int { 196 | return 2 197 | } 198 | 199 | override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { 200 | if section == 0 { 201 | return 1 202 | } 203 | return postSnapshots.count 204 | } 205 | 206 | override func collectionView(_ collectionView: UICollectionView, 207 | cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { 208 | if indexPath.section == 0 { 209 | let header = collectionView.dequeueReusableCell(withReuseIdentifier: "header", for: indexPath) as! FPAccountHeader 210 | header.inkColor = .clear 211 | headerView = header 212 | if profile.uid == uid { 213 | header.followLabel.text = "Notifications" 214 | header.followSwitch.accessibilityLabel = header.followSwitch.isOn ? "Notifications are on" : "Notifications are off" 215 | header.followSwitch.accessibilityHint = "Double-tap to \(header.followSwitch.isOn ? "disable" : "enable") notifications" 216 | } else { 217 | header.followSwitch.accessibilityHint = "Double-tap to \(header.followSwitch.isOn ? "un" : "")follow" 218 | header.followSwitch.accessibilityLabel = "\(header.followSwitch.isOn ? "" : "not ")following \(profile.fullname)" 219 | } 220 | header.profilePictureImageView.sd_setImage(with: profile.profilePictureURL, completed: nil) 221 | return header 222 | } else { 223 | let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) 224 | let postSnapshot = postSnapshots[indexPath.item] 225 | if let value = postSnapshot.value as? [String: Any], let photoUrl = value["thumb_url"] as? String { 226 | let imageView = UIImageView() 227 | cell.backgroundView = imageView 228 | imageView.sd_setImage(with: URL(string: photoUrl), completed: nil) 229 | imageView.contentMode = .scaleAspectFill 230 | imageView.isAccessibilityElement = true 231 | imageView.accessibilityLabel = "Photo by \(profile.fullname)" 232 | } 233 | return cell 234 | } 235 | } 236 | 237 | lazy var moreAlert: UIAlertController = { 238 | let alert = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet) 239 | if profile.uid == uid { 240 | alert.addAction(UIAlertAction(title: "Sign out", style: .default , handler:{ (UIAlertAction)in 241 | self.present(self.feedViewController.signOutAlert, animated:true, completion:nil) 242 | })) 243 | alert.addAction(UIAlertAction(title: "Delete account", style: .destructive , handler:{ _ in 244 | self.present(self.deleteAlert, animated:true, completion:nil) 245 | })) 246 | } else { 247 | if !appDelegate.isBlocking(profile.uid) { 248 | alert.addAction(UIAlertAction(title: "Block", style: .destructive , handler:{ _ in 249 | self.present(self.blockAlert, animated:true, completion:nil) 250 | })) 251 | } else { 252 | alert.addAction(UIAlertAction(title: "Unblock", style: .destructive , handler:{ _ in 253 | self.present(self.unblockAlert, animated:true, completion:nil) 254 | })) 255 | } 256 | } 257 | alert.addAction(UIAlertAction(title: "Cancel", style: .cancel , handler: nil)) 258 | return alert 259 | }() 260 | 261 | @IBAction func didTapMore(_ sender: UIBarButtonItem) { 262 | moreAlert.popoverPresentationController?.barButtonItem = sender 263 | present(moreAlert, animated: true, completion: nil) 264 | } 265 | 266 | func toggleFollow(_ follow: Bool) { 267 | feedViewController.followChanged = true 268 | let myFeed = "feed/\(uid)/" 269 | database.reference(withPath: "people/\(profile.uid)/posts").observeSingleEvent(of: .value, with: { snapshot in 270 | var lastPostID: Any = true 271 | var updateData = [String: Any]() 272 | if let posts = snapshot.value as? [String: Any] { 273 | // Add/remove followed user's posts to the home feed. 274 | for postId in posts.keys { 275 | updateData[myFeed + postId] = follow ? true : NSNull() 276 | lastPostID = postId 277 | } 278 | 279 | // Add/remove followed user to the 'following' list. 280 | updateData["people/\(self.uid)/following/\(self.profile.uid)"] = follow ? lastPostID : NSNull() 281 | 282 | // Add/remove signed-in user to the list of followers. 283 | updateData["followers/\(self.profile.uid)/\(self.uid)"] = follow ? true : NSNull() 284 | self.ref.updateChildValues(updateData) { error, _ in 285 | if let error = error { 286 | print(error.localizedDescription) 287 | } 288 | } 289 | } 290 | }) 291 | } 292 | 293 | func collectionView(_ collectionView: UICollectionView, cellHeightAt indexPath: IndexPath) -> CGFloat { 294 | if indexPath.section == 0 { 295 | return 112 296 | } 297 | return ceil(((self.collectionView?.bounds.width)! - 14) * 0.325) 298 | } 299 | 300 | override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { 301 | if indexPath.section != 0 { 302 | performSegue(withIdentifier: "detail", sender: postSnapshots[indexPath.item]) 303 | } 304 | } 305 | 306 | func backButtonAction(_ sender: Any) { 307 | navigationController?.popViewController(animated: true) 308 | } 309 | 310 | lazy var errorAlert: MDCAlertController = { 311 | let alertController = MDCAlertController(title: "Deletion requires recent authentication", 312 | message: "Log in again before retrying.") 313 | let okAction = MDCAlertAction(title:"OK") { _ in self.feedViewController.signOut() } 314 | alertController.addAction(okAction) 315 | return alertController 316 | }() 317 | 318 | lazy var deleteAlert: MDCAlertController = { 319 | let alertController = MDCAlertController.init(title: "Delete Account?", message: nil) 320 | let cancelAction = MDCAlertAction(title:"Cancel", handler: nil) 321 | let deleteAction = MDCAlertAction(title:"Delete") { _ in 322 | Auth.auth().currentUser?.delete(completion: { error in 323 | if error != nil { 324 | self.present(self.errorAlert, animated:true, completion:nil) 325 | return 326 | } 327 | self.feedViewController.signOut() 328 | }) 329 | } 330 | alertController.addAction(deleteAction) 331 | alertController.addAction(cancelAction) 332 | return alertController 333 | }() 334 | 335 | lazy var blockAlert: MDCAlertController = { 336 | let alertController = MDCAlertController.init(title: "Block Account?", message: nil) 337 | let cancelAction = MDCAlertAction(title:"Cancel", handler: nil) 338 | let blockAction = MDCAlertAction(title:"Block") { _ in 339 | if self.headerView.followSwitch.isOn { 340 | self.toggleFollow(false) 341 | } 342 | let updateData = ["blocked/\(self.profile.uid)/\(self.uid)": true, 343 | "blocking/\(self.uid)/\(self.profile.uid)" : true] 344 | self.ref.updateChildValues(updateData) { error, _ in 345 | if let error = error { 346 | print(error.localizedDescription) 347 | } 348 | } 349 | } 350 | alertController.addAction(blockAction) 351 | alertController.addAction(cancelAction) 352 | return alertController 353 | }() 354 | 355 | lazy var unblockAlert: MDCAlertController = { 356 | let alertController = MDCAlertController.init(title: "Unblock Account?", message: nil) 357 | let cancelAction = MDCAlertAction(title:"Cancel", handler: nil) 358 | let unblockAction = MDCAlertAction(title:"Unblock") { _ in 359 | 360 | let updateData = ["blocked/\(self.profile.uid)/\(self.uid)": NSNull(), 361 | "blocking/\(self.uid)/\(self.profile.uid)" : NSNull()] 362 | self.ref.updateChildValues(updateData) { error, _ in 363 | if let error = error { 364 | print(error.localizedDescription) 365 | } 366 | } 367 | } 368 | alertController.addAction(unblockAction) 369 | alertController.addAction(cancelAction) 370 | return alertController 371 | }() 372 | 373 | override func prepare(for segue: UIStoryboardSegue, sender: Any?) { 374 | if segue.identifier == "detail" { 375 | if let detailViewController = segue.destination as? FPPostDetailViewController, 376 | let sender = sender as? DataSnapshot { 377 | detailViewController.postSnapshot = sender 378 | } 379 | } 380 | } 381 | } 382 | -------------------------------------------------------------------------------- /FriendlyPix/FPAuthPickerViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2017 Google Inc. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | import FirebaseUI 18 | import MaterialComponents.MDCTypography 19 | 20 | class FPAuthPickerViewController: FUIAuthPickerViewController { 21 | @IBOutlet var readonlyWarningLabel: UILabel! 22 | let attributes = [NSAttributedString.Key.font: UIFont.preferredFont(forTextStyle: .caption1)] 23 | let attributes2 = [NSAttributedString.Key.font: UIFont.preferredFont(forTextStyle: .caption2)] 24 | var agreed = false 25 | 26 | lazy var disclaimer: MDCAlertController = { 27 | let alertController = MDCAlertController(title: nil, message: "I understand FriendlyPix is an application aimed at showcasing the Firebase platform capabilities, and should not be used with private or sensitive information. All FriendlyPix data and inactive accounts are regularly removed. I agree to the Terms of Service and Privacy Policy.") 28 | 29 | let acceptAction = MDCAlertAction(title: "I agree", emphasis: .high) { action in 30 | self.agreed = true 31 | } 32 | alertController.addAction(acceptAction) 33 | let termsAction = MDCAlertAction(title: "Terms") { action in 34 | UIApplication.shared.open(URL(string: "https://friendly-pix.com/terms")!, 35 | options: [:], completionHandler: { completion in 36 | self.present(alertController, animated: true, completion: nil) 37 | }) 38 | } 39 | alertController.addAction(termsAction) 40 | let policyAction = MDCAlertAction(title: "Privacy") { action in 41 | UIApplication.shared.open(URL(string: "https://www.google.com/policies/privacy")!, 42 | options: [:], completionHandler: { completion in 43 | self.present(alertController, animated: true, completion: nil) 44 | }) 45 | } 46 | alertController.addAction(policyAction) 47 | let colorScheme = MDCSemanticColorScheme() 48 | MDCAlertColorThemer.applySemanticColorScheme(colorScheme, to: alertController) 49 | return alertController 50 | }() 51 | 52 | override func viewDidAppear(_ animated: Bool) { 53 | super.viewDidAppear(animated) 54 | if (AppDelegate.euroZone) { 55 | readonlyWarningLabel.isHidden = false 56 | } 57 | if !agreed { 58 | self.present(disclaimer, animated: true, completion: nil) 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /FriendlyPix/FPAuthPickerViewController.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | Amaranth-Bold 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 34 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | -------------------------------------------------------------------------------- /FriendlyPix/FPCardCollectionViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2017 Google Inc. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | import MaterialComponents 18 | import SDWebImage 19 | import Firebase 20 | 21 | protocol FPCardCollectionViewCellDelegate: class { 22 | func showProfile(_ author: FPUser) 23 | func showTaggedPhotos(_ hashtag: String) 24 | func showLightbox(_ index: Int) 25 | func viewComments(_ post: FPPost) 26 | func toogleLike(_ post: FPPost, label: UILabel) 27 | func optionPost(_ post: FPPost, _ button: UIButton, completion: (() -> Swift.Void)? ) 28 | } 29 | 30 | class FPCardCollectionViewCell: MDCCardCollectionCell { 31 | @IBOutlet weak private var authorImageView: UIImageView! 32 | @IBOutlet weak private var authorLabel: UILabel! 33 | @IBOutlet weak private var dateLabel: UILabel! 34 | @IBOutlet weak private var postImageView: UIImageView! 35 | @IBOutlet weak private var titleLabel: UILabel! 36 | @IBOutlet weak private var likesLabel: UILabel! 37 | @IBOutlet weak private var likeButton: UIButton! 38 | @IBOutlet weak private var comment1Label: UILabel! 39 | @IBOutlet weak private var comment2Label: UILabel! 40 | @IBOutlet weak private var viewAllCommentsLabel: UIButton! 41 | var commentLabels: [UILabel]? 42 | let attributes = [NSAttributedString.Key.font: UIFont.mdc_preferredFont(forMaterialTextStyle: .body2)] 43 | 44 | var post: FPPost! 45 | weak var delegate: FPCardCollectionViewCellDelegate? 46 | var labelConstraints: [NSLayoutConstraint]! 47 | public var imageConstraint: NSLayoutConstraint? 48 | 49 | override func awakeFromNib() { 50 | super.awakeFromNib() 51 | 52 | authorImageView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(profileTapped))) 53 | authorLabel.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(profileTapped))) 54 | postImageView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(imageTapped))) 55 | authorImageView.isAccessibilityElement = true 56 | authorImageView.accessibilityHint = "Double-tap to open profile." 57 | 58 | commentLabels = [comment1Label, comment2Label] 59 | 60 | comment1Label.addGestureRecognizer(UITapGestureRecognizer(target: self, 61 | action: #selector(handleTapOnComment(recognizer:)))) 62 | comment2Label.addGestureRecognizer(UITapGestureRecognizer(target: self, 63 | action: #selector(handleTapOnComment(recognizer:)))) 64 | titleLabel.preferredMaxLayoutWidth = self.bounds.width - 16 65 | comment1Label.preferredMaxLayoutWidth = titleLabel.preferredMaxLayoutWidth 66 | comment2Label.preferredMaxLayoutWidth = titleLabel.preferredMaxLayoutWidth 67 | } 68 | 69 | private func convertCacheTypeToString(_ cacheType: SDImageCacheType) -> String { 70 | switch cacheType { 71 | case .none: 72 | return "none" 73 | case .disk: 74 | return "disk" 75 | case .memory: 76 | return "memory" 77 | case .all: 78 | return "all" 79 | } 80 | } 81 | 82 | func populateContent(post: FPPost, index: Int, isDryRun: Bool) { 83 | if Auth.auth().currentUser!.isAnonymous { 84 | likeButton.isEnabled = false 85 | } 86 | self.post = post 87 | let postAuthor = post.author 88 | if !isDryRun, let profilePictureURL = postAuthor.profilePictureURL { 89 | UIImage.circleImage(with: profilePictureURL, to: authorImageView) 90 | authorImageView.accessibilityLabel = postAuthor.fullname 91 | } 92 | authorLabel.text = postAuthor.fullname 93 | dateLabel.text = post.postDate.timeAgo() 94 | postImageView.tag = index 95 | if !isDryRun { 96 | let trace = Performance.startTrace(name: "post_load") 97 | postImageView?.sd_setImage(with: post.thumbURL, completed: { image, error, cacheType, url in 98 | trace?.incrementMetric(self.convertCacheTypeToString(cacheType), by: 1) 99 | trace?.stop() 100 | }) 101 | postImageView.accessibilityLabel = "Photo by \(postAuthor.fullname)" 102 | } 103 | 104 | let title = NSMutableAttributedString(string: postAuthor.fullname + " ", attributes: attributes) 105 | let attrString = NSMutableAttributedString(string: post.text) 106 | let regex = try? NSRegularExpression(pattern: "(#[a-zA-Z0-9_\\p{Arabic}\\p{N}]*)", options: []) 107 | if let matches = regex?.matches(in: post.text, options:[], range:NSMakeRange(0, post.text.count)) { 108 | for match in matches { 109 | attrString.addAttribute(NSAttributedString.Key.link, value: (post.text as NSString).substring(with: match.range), range: match.range) 110 | attrString.addAttribute(NSAttributedString.Key.foregroundColor, value: UIColor.blue , range: match.range) 111 | } 112 | } 113 | title.append(attrString) 114 | title.addAttribute(.paragraphStyle, value: MDCSelfSizingStereoCell.paragraphStyle, range: NSMakeRange(0, title.length)) 115 | titleLabel.attributedText = title 116 | titleLabel.accessibilityLabel = "\(post.text), posted by \(postAuthor.fullname)" 117 | 118 | titleLabel.addGestureRecognizer(UITapGestureRecognizer(target: self, 119 | action: #selector(handleTapOnProfileLabel(recognizer:)))) 120 | likesLabel.text = post.likeCount == 1 ? "1 like" : "\(post.likeCount) likes" 121 | likesLabel.font = UIFont.mdc_preferredFont(forMaterialTextStyle: .body2) 122 | if post.isLiked { 123 | likeButton.setImage(#imageLiteral(resourceName: "ic_favorite"), for: .normal) 124 | likeButton.accessibilityLabel = "you liked this post" 125 | likeButton.accessibilityHint = "double-tap to unlike" 126 | } else { 127 | likeButton.setImage(#imageLiteral(resourceName: "ic_favorite_border"), for: .normal) 128 | likeButton.accessibilityLabel = "you haven't liked this post" 129 | likeButton.accessibilityHint = "double-tap to like" 130 | } 131 | 132 | if labelConstraints != nil { 133 | NSLayoutConstraint.deactivate(labelConstraints) 134 | labelConstraints = nil 135 | } 136 | 137 | let betweenConstant: CGFloat = 2 138 | let bottomConstant: CGFloat = 12 139 | let commentCount = post.comments.count 140 | switch commentCount { 141 | case 0: 142 | labelConstraints = [contentView.bottomAnchor.constraint(equalTo: titleLabel.bottomAnchor, 143 | constant: bottomConstant)] 144 | viewAllCommentsLabel.isHidden = true 145 | comment1Label.isHidden = true 146 | comment1Label.text = nil 147 | comment2Label.isHidden = true 148 | comment2Label.text = nil 149 | case 1: 150 | labelConstraints = [comment1Label.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, 151 | constant: betweenConstant), 152 | contentView.bottomAnchor.constraint(equalTo: comment1Label.bottomAnchor, 153 | constant: bottomConstant)] 154 | viewAllCommentsLabel.isHidden = true 155 | attributeComment(index: 0) 156 | comment2Label.isHidden = true 157 | comment2Label.text = nil 158 | case 2: 159 | labelConstraints = [comment1Label.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, 160 | constant: betweenConstant), 161 | comment2Label.topAnchor.constraint(equalTo: comment1Label.bottomAnchor, 162 | constant: betweenConstant), 163 | contentView.bottomAnchor.constraint(equalTo: comment2Label.bottomAnchor, 164 | constant: bottomConstant)] 165 | viewAllCommentsLabel.isHidden = true 166 | attributeComment(index: 0) 167 | attributeComment(index: 1) 168 | default: 169 | labelConstraints = [viewAllCommentsLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, 170 | constant: betweenConstant), 171 | comment1Label.topAnchor.constraint(equalTo: viewAllCommentsLabel.bottomAnchor, 172 | constant: betweenConstant), 173 | comment2Label.topAnchor.constraint(equalTo: comment1Label.bottomAnchor, 174 | constant: betweenConstant), 175 | contentView.bottomAnchor.constraint(equalTo: comment2Label.bottomAnchor, 176 | constant: bottomConstant)] 177 | viewAllCommentsLabel.isHidden = false 178 | viewAllCommentsLabel.setTitle("View all \(commentCount) comments", for: .normal) 179 | attributeComment(index: 0) 180 | attributeComment(index: 1) 181 | } 182 | NSLayoutConstraint.activate(labelConstraints) 183 | } 184 | 185 | private func attributeComment(index: Int) { 186 | if let commentLabel = commentLabels?[index] { 187 | let comment = post.comments[index] 188 | commentLabel.isHidden = false 189 | commentLabel.accessibilityLabel = "\(comment.from.fullname) said, \(comment.text)" 190 | let text = NSMutableAttributedString(string: comment.from.fullname, attributes: attributes) 191 | text.append(NSAttributedString(string: " " + comment.text)) 192 | text.addAttribute(.paragraphStyle, value: MDCSelfSizingStereoCell.paragraphStyle, range: NSMakeRange(0, text.length)) 193 | commentLabel.attributedText = text 194 | } 195 | } 196 | 197 | override func updateConstraints() { 198 | super.updateConstraints() 199 | 200 | let constant = ceil((self.bounds.width - 2) * 0.65) 201 | if imageConstraint == nil { 202 | imageConstraint = postImageView.heightAnchor.constraint(equalToConstant: constant) 203 | 204 | imageConstraint?.isActive = true 205 | } 206 | imageConstraint?.constant = constant 207 | } 208 | 209 | @IBAction func toggledLike() { 210 | delegate?.toogleLike(post, label: likesLabel) 211 | } 212 | 213 | @IBAction func tappedOption(_ sender: UIButton) { 214 | delegate?.optionPost(post, sender, completion: nil) 215 | } 216 | 217 | override func prepareForReuse() { 218 | super.prepareForReuse() 219 | NSLayoutConstraint.deactivate(labelConstraints) 220 | labelConstraints = nil 221 | } 222 | 223 | @objc func profileTapped() { 224 | delegate?.showProfile(post.author) 225 | } 226 | 227 | @objc func imageTapped() { 228 | delegate?.showLightbox(postImageView.tag) 229 | } 230 | 231 | @objc func handleTapOnProfileLabel(recognizer: UITapGestureRecognizer) { 232 | let touchIndex = recognizer.touchIndexInLabel(label: titleLabel) 233 | if touchIndex < post.author.fullname.count { 234 | profileTapped() 235 | } else if let tag = titleLabel.attributedText?.attribute(NSAttributedString.Key.link, at: touchIndex, effectiveRange: nil) as? String { 236 | delegate?.showTaggedPhotos(String(tag.dropFirst())) 237 | } 238 | } 239 | 240 | @objc func handleTapOnComment(recognizer: UITapGestureRecognizer) { 241 | if let index = recognizer.view?.tag { 242 | let from = post.comments[index].from 243 | if recognizer.didTapAttributedTextInLabel(label: commentLabels![index], 244 | inRange: NSRange(location: 0, 245 | length: from.fullname.count)) { 246 | delegate?.showProfile(from) 247 | } 248 | } 249 | } 250 | 251 | @IBAction func viewAllComments(_ sender: Any) { 252 | delegate?.viewComments(post) 253 | } 254 | 255 | var layerClass: AnyClass { 256 | return MDCShadowLayer.self 257 | } 258 | } 259 | 260 | extension UITapGestureRecognizer { 261 | 262 | func didTapAttributedTextInLabel(label: UILabel, inRange targetRange: NSRange) -> Bool { 263 | return NSLocationInRange(touchIndexInLabel(label: label), targetRange) 264 | } 265 | 266 | func touchIndexInLabel(label: UILabel) -> Int { 267 | // Create instances of NSLayoutManager, NSTextContainer and NSTextStorage 268 | let layoutManager = NSLayoutManager() 269 | let textContainer = NSTextContainer(size: CGSize.zero) 270 | let textStorage = NSTextStorage(attributedString: label.attributedText!) 271 | 272 | // Configure layoutManager and textStorage 273 | layoutManager.addTextContainer(textContainer) 274 | textStorage.addLayoutManager(layoutManager) 275 | 276 | // Configure textContainer 277 | textContainer.lineFragmentPadding = 0.0 278 | textContainer.lineBreakMode = label.lineBreakMode 279 | textContainer.maximumNumberOfLines = label.numberOfLines 280 | let labelSize = label.bounds.size 281 | textContainer.size = labelSize 282 | 283 | // Find the tapped character location and compare it to the specified range 284 | let locationOfTouchInLabel = self.location(in: label) 285 | let textBoundingBox = layoutManager.usedRect(for: textContainer) 286 | let textContainerOffset = 287 | CGPoint(x: (labelSize.width - textBoundingBox.size.width) * 0.5 - textBoundingBox.origin.x, 288 | y: (labelSize.height - textBoundingBox.size.height) * 0.5 - textBoundingBox.origin.y) 289 | let locationOfTouchInTextContainer = 290 | CGPoint(x: locationOfTouchInLabel.x - textContainerOffset.x, 291 | y: locationOfTouchInLabel.y - textContainerOffset.y) 292 | let indexOfCharacter = layoutManager.characterIndex(for: locationOfTouchInTextContainer, 293 | in: textContainer, 294 | fractionOfDistanceBetweenInsertionPoints: nil) 295 | return indexOfCharacter 296 | } 297 | } 298 | -------------------------------------------------------------------------------- /FriendlyPix/FPCollectionViewTextCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2017 Google Inc. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | import MaterialComponents 18 | 19 | class FPCollectionViewTextCell: MDCCollectionViewTextCell { 20 | override func prepareForReuse() { 21 | super.prepareForReuse() 22 | textLabel?.numberOfLines = 0 23 | detailTextLabel?.numberOfLines = 0 24 | setNeedsLayout() 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /FriendlyPix/FPComment.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2017 Google Inc. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | import Firebase 18 | 19 | class FPComment { 20 | var commentID: String 21 | var text: String 22 | var postDate: Date 23 | var from: FPUser 24 | 25 | init(snapshot: DataSnapshot) { 26 | self.commentID = snapshot.key 27 | let value = snapshot.value as! [String: Any] 28 | self.text = value["text"] as? String ?? "" 29 | let timestamp = value["timestamp"] as! Double 30 | self.postDate = Date(timeIntervalSince1970: timestamp / 1_000.0) 31 | let author = value["author"] as! [String: String] 32 | self.from = FPUser(dictionary: author) 33 | } 34 | } 35 | 36 | extension FPComment: Equatable { 37 | static func ==(lhs: FPComment, rhs: FPComment) -> Bool { 38 | return lhs.commentID == rhs.commentID && lhs.postDate == rhs.postDate 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /FriendlyPix/FPCommentCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2017 Google Inc. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | import Firebase 18 | import MaterialComponents 19 | 20 | extension MDCSelfSizingStereoCell { 21 | 22 | static let attributes = [NSAttributedString.Key.font: UIFont.mdc_preferredFont(forMaterialTextStyle: .body2)] 23 | static let attributes2 = [NSAttributedString.Key.font: UIFont.mdc_preferredFont(forMaterialTextStyle: .body1)] 24 | 25 | func populateContent(from: FPUser, text: String, date: Date, index: Int) { 26 | let attrText = NSMutableAttributedString(string: from.fullname , attributes: MDCSelfSizingStereoCell.attributes) 27 | attrText.append(NSAttributedString(string: " " + text, attributes: MDCSelfSizingStereoCell.attributes2)) 28 | attrText.addAttribute(.paragraphStyle, value: MDCSelfSizingStereoCell.paragraphStyle, range: NSMakeRange(0, attrText.length)) 29 | titleLabel.attributedText = attrText 30 | titleLabel.accessibilityLabel = "\(from.fullname) said, \(text)" 31 | if let profilePictureURL = from.profilePictureURL { 32 | UIImage.circleImage(with: profilePictureURL, to: leadingImageView) 33 | leadingImageView.accessibilityLabel = from.fullname 34 | leadingImageView.accessibilityHint = "Double-tap to open profile." 35 | } 36 | leadingImageView.tag = index 37 | titleLabel.tag = index 38 | detailLabel.text = date.timeAgo() 39 | } 40 | 41 | 42 | static let paragraphStyle = { () -> NSMutableParagraphStyle in 43 | let style = NSMutableParagraphStyle() 44 | style.lineSpacing = 2 45 | return style 46 | }() 47 | } 48 | -------------------------------------------------------------------------------- /FriendlyPix/FPHashTagViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2018 Google Inc. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | import Firebase 18 | import Lightbox 19 | import MaterialComponents 20 | 21 | class FPHashTagViewController: UICollectionViewController, UICollectionViewDelegateFlowLayout { 22 | var hashtag = "" 23 | let uid = Auth.auth().currentUser!.uid 24 | let database = Database.database() 25 | let ref = Database.database().reference() 26 | var postIds: [String: Any]? 27 | var postSnapshots = [DataSnapshot]() 28 | var loadingPostCount = 0 29 | var firebaseRefs = [DatabaseReference]() 30 | lazy var appDelegate = UIApplication.shared.delegate as! AppDelegate 31 | 32 | override func viewDidLoad() { 33 | super.viewDidLoad() 34 | navigationItem.title = "#\(hashtag)" 35 | } 36 | 37 | override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { 38 | return postSnapshots.count 39 | } 40 | 41 | override func viewDidAppear(_ animated: Bool) { 42 | super.viewDidAppear(animated) 43 | loadData() 44 | } 45 | 46 | override func viewWillDisappear(_ animated: Bool) { 47 | super.viewWillDisappear(animated) 48 | for firebaseRef in firebaseRefs { 49 | firebaseRef.removeAllObservers() 50 | } 51 | firebaseRefs = [DatabaseReference]() 52 | } 53 | 54 | func registerForPostsDeletion() { 55 | let userPostsRef = database.reference(withPath: "hashtags/\(hashtag)") 56 | userPostsRef.observe(.childRemoved, with: { postSnapshot in 57 | var index = 0 58 | for post in self.postSnapshots { 59 | if post.key == postSnapshot.key { 60 | self.postSnapshots.remove(at: index) 61 | self.loadingPostCount -= 1 62 | self.collectionView?.deleteItems(at: [IndexPath(item: index, section: 0)]) 63 | return 64 | } 65 | index += 1 66 | } 67 | self.postIds?.removeValue(forKey: postSnapshot.key) 68 | }) 69 | } 70 | 71 | 72 | func loadUserPosts() { 73 | database.reference(withPath: "hashtags/\(hashtag.lowercased())").observeSingleEvent(of: .value, with: { 74 | if var posts = $0.value as? [String: Any] { 75 | if !self.postSnapshots.isEmpty { 76 | var index = self.postSnapshots.count - 1 77 | self.collectionView?.performBatchUpdates({ 78 | for post in self.postSnapshots.reversed() { 79 | if posts.removeValue(forKey: post.key) == nil { 80 | self.postSnapshots.remove(at: index) 81 | self.collectionView?.deleteItems(at: [IndexPath(item: index, section: 0)]) 82 | return 83 | } 84 | index -= 1 85 | } 86 | }, completion: nil) 87 | self.postIds = posts 88 | self.loadingPostCount = posts.count 89 | } else { 90 | self.postIds = posts 91 | self.loadFeed() 92 | } 93 | self.registerForPostsDeletion() 94 | } 95 | }) 96 | } 97 | 98 | func loadData() { 99 | loadUserPosts() 100 | } 101 | 102 | override func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, 103 | forItemAt indexPath: IndexPath) { 104 | if indexPath.item == (loadingPostCount - 3) { 105 | loadFeed() 106 | } 107 | } 108 | 109 | func loadFeed() { 110 | loadingPostCount = postSnapshots.count + 12 111 | self.collectionView?.performBatchUpdates({ 112 | for _ in 1...12 { 113 | if let postId = self.postIds?.popFirst()?.key { 114 | database.reference(withPath: "posts/\(postId)").observeSingleEvent(of: .value, with: { postSnapshot in 115 | self.postSnapshots.append(postSnapshot) 116 | self.collectionView?.insertItems(at: [IndexPath(item: self.postSnapshots.count - 1, section: 0)]) 117 | }) 118 | } else { 119 | break 120 | } 121 | } 122 | }, completion: nil) 123 | } 124 | 125 | override func collectionView(_ collectionView: UICollectionView, 126 | cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { 127 | let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) 128 | let postSnapshot = postSnapshots[indexPath.item] 129 | if let value = postSnapshot.value as? [String: Any], let photoUrl = value["thumb_url"] as? String { 130 | let imageView = UIImageView() 131 | cell.backgroundView = imageView 132 | imageView.sd_setImage(with: URL(string: photoUrl), completed: nil) 133 | imageView.contentMode = .scaleAspectFill 134 | imageView.isAccessibilityElement = true 135 | imageView.accessibilityLabel = "Photo with hashtag \(hashtag)" 136 | } 137 | return cell 138 | } 139 | 140 | public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { 141 | let height = ceil(((self.collectionView.bounds.width) - 14) * 0.325) 142 | return CGSize(width: height, height: height) 143 | } 144 | 145 | override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { 146 | performSegue(withIdentifier: "detail", sender: postSnapshots[indexPath.item]) 147 | } 148 | 149 | func backButtonAction(_ sender: Any) { 150 | navigationController?.popViewController(animated: true) 151 | } 152 | 153 | override func prepare(for segue: UIStoryboardSegue, sender: Any?) { 154 | if segue.identifier == "detail" { 155 | if let detailViewController = segue.destination as? FPPostDetailViewController, 156 | let sender = sender as? DataSnapshot { 157 | detailViewController.postSnapshot = sender 158 | } 159 | } 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /FriendlyPix/FPPost.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2018 Google Inc. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | import Firebase 18 | 19 | class FPPost { 20 | var postID: String 21 | var postDate: Date 22 | var thumbURL: URL 23 | var fullURL: URL 24 | var author: FPUser 25 | var text: String 26 | var comments: [FPComment] 27 | var isLiked = false 28 | var mine = false 29 | var likeCount = 0 30 | 31 | convenience init(snapshot: DataSnapshot, andComments comments: [FPComment], andLikes likes: [String: Any]?) { 32 | self.init(id: snapshot.key, value: snapshot.value as! [String : Any], andComments: comments, andLikes: likes) 33 | } 34 | 35 | init(id: String, value: [String: Any], andComments comments: [FPComment], andLikes likes: [String: Any]?) { 36 | self.postID = id 37 | self.text = value["text"] as! String 38 | let timestamp = value["timestamp"] as! Double 39 | self.postDate = Date(timeIntervalSince1970: (timestamp / 1_000.0)) 40 | let author = value["author"] as! [String: String] 41 | self.author = FPUser(dictionary: author) 42 | self.thumbURL = URL(string: value["thumb_url"] as! String)! 43 | self.fullURL = URL(string: value["full_url"] as! String)! 44 | self.comments = comments 45 | if let likes = likes { 46 | likeCount = likes.count 47 | if let uid = Auth.auth().currentUser?.uid { 48 | isLiked = (likes.index(forKey: uid) != nil) 49 | } 50 | } 51 | self.mine = self.author == Auth.auth().currentUser! 52 | } 53 | } 54 | 55 | extension FPPost: Equatable { 56 | static func ==(lhs: FPPost, rhs: FPPost) -> Bool { 57 | return lhs.postID == rhs.postID 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /FriendlyPix/FPPostDetailViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2017 Google Inc. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | import Firebase 18 | 19 | class FPPostDetailViewController: FPFeedViewController { 20 | var postSnapshot: DataSnapshot! 21 | 22 | override func loadData() { 23 | if let post = posts.first { 24 | postsRef.child(post.postID).observeSingleEvent(of: .value, with: { 25 | if $0.exists() && !self.appDelegate.isBlocked($0) { 26 | self.updatePost(post, postSnapshot: $0) 27 | self.listenPost(post) 28 | } else { 29 | self.navigationController?.popViewController(animated: true) 30 | } 31 | }) 32 | } else { 33 | loadPost(postSnapshot) 34 | } 35 | } 36 | 37 | override func optionPost(_ post: FPPost, _ button: UIButton, completion: (() -> Swift.Void)? = nil) { 38 | super.optionPost(post, button, completion: { self.navigationController?.popViewController(animated: true) }) 39 | } 40 | 41 | override func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, 42 | forItemAt indexPath: IndexPath) { 43 | } 44 | 45 | override func awakeFromNib() { 46 | } 47 | 48 | override func showProfile(_ profile: FPUser) { 49 | feedViewController.showProfile(profile) 50 | } 51 | 52 | override func viewComments(_ post: FPPost) { 53 | feedViewController.viewComments(post) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /FriendlyPix/FPSearchViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2017 Google Inc. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | import Firebase 18 | import MaterialComponents 19 | 20 | class FPSearchViewController: UICollectionViewController, UISearchBarDelegate, UISearchControllerDelegate { 21 | 22 | let searchController = UISearchController(searchResultsController: nil) 23 | let peopleRef = Database.database().reference(withPath: "people") 24 | let hashtagsRef = Database.database().reference(withPath: "hashtags") 25 | lazy var appDelegate = UIApplication.shared.delegate as! AppDelegate 26 | lazy var uid = Auth.auth().currentUser!.uid 27 | var people = [FPUser]() 28 | var hashtags = [String]() 29 | let emptyLabel: UILabel = { 30 | let messageLabel = UILabel() 31 | messageLabel.text = "No people or hashtag found." 32 | messageLabel.textColor = UIColor.black 33 | messageLabel.numberOfLines = 0 34 | messageLabel.textAlignment = .center 35 | messageLabel.font = UIFont.preferredFont(forTextStyle: .title3) 36 | messageLabel.sizeToFit() 37 | return messageLabel 38 | }() 39 | // We keep track of the pending work item as a property 40 | private var pendingRequestWorkItem: DispatchWorkItem? 41 | 42 | override func viewDidLoad() { 43 | super.viewDidLoad() 44 | collectionView.register(MDCSelfSizingStereoCell.self, forCellWithReuseIdentifier: "cell") 45 | // Setup the Search Controller 46 | searchController.searchResultsUpdater = self 47 | searchController.searchBar.delegate = self 48 | searchController.delegate = self 49 | searchController.obscuresBackgroundDuringPresentation = false 50 | searchController.hidesNavigationBarDuringPresentation = false 51 | searchController.searchBar.placeholder = "Search" 52 | 53 | UITextField.appearance(whenContainedInInstancesOf: [UISearchBar.self]).leftViewMode = .never 54 | searchController.searchBar.setImage(#imageLiteral(resourceName: "ic_close"), for: .clear, state: .normal) 55 | //searchController.searchBar.setImage(#imageLiteral(resourceName: "ic_arrow_back"), for: .search, state: .normal) 56 | UIImageView.appearance(whenContainedInInstancesOf: [UISearchBar.self]).bounds = CGRect(x: 0, y: 0, width: 24, height: 24) 57 | 58 | guard let collectionViewLayout = self.collectionViewLayout as? UICollectionViewFlowLayout else { return } 59 | 60 | collectionViewLayout.estimatedItemSize = CGSize(width: collectionView.bounds.size.width, 61 | height: 75) 62 | 63 | let x = UIButton.init() 64 | x.setImage(#imageLiteral(resourceName: "ic_arrow_back"), for: .normal) 65 | x.addTarget(self, action: #selector(back), for: .touchUpInside) 66 | 67 | UIButton.appearance(whenContainedInInstancesOf: [UINavigationBar.self]).translatesAutoresizingMaskIntoConstraints = false 68 | UIButton.appearance(whenContainedInInstancesOf: [UINavigationBar.self]).contentEdgeInsets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: -4) 69 | 70 | navigationItem.leftBarButtonItem = UIBarButtonItem(customView: x) 71 | 72 | navigationItem.titleView = searchController.searchBar 73 | navigationItem.hidesBackButton = true 74 | definesPresentationContext = true 75 | searchController.searchBar.showsCancelButton = false 76 | 77 | UITextField.appearance(whenContainedInInstancesOf: [UISearchBar.self]).tintColor = UIColor.init(red: 0, green: 137/255, blue: 249/255, alpha: 1) 78 | } 79 | 80 | @objc func back() { 81 | navigationController?.popViewController(animated: true) 82 | } 83 | 84 | func searchBarTextDidBeginEditing(_ searchBar: UISearchBar) { 85 | searchController.searchBar.showsCancelButton = false 86 | self.searchController.searchBar.becomeFirstResponder() 87 | } 88 | 89 | func didPresentSearchController(_ searchController: UISearchController) { 90 | searchController.searchBar.showsCancelButton = false 91 | self.searchController.searchBar.becomeFirstResponder() 92 | } 93 | 94 | override func viewDidAppear(_ animated: Bool) { 95 | super.viewDidAppear(animated) 96 | DispatchQueue.global(qos: .default).async(execute: {() -> Void in 97 | DispatchQueue.main.async(execute: {() -> Void in 98 | self.searchController.searchBar.becomeFirstResponder() 99 | }) 100 | }) 101 | } 102 | 103 | override func viewWillAppear(_ animated: Bool) { 104 | super.viewWillAppear(animated) 105 | navigationController?.navigationBar.barTintColor = .white 106 | navigationController?.navigationBar.tintColor = .gray 107 | } 108 | 109 | override func viewWillDisappear(_ animated: Bool) { 110 | super.viewWillDisappear(animated) 111 | navigationController?.navigationBar.barTintColor = UIColor(red: 12, green: 34, blue: 56, alpha: 1) 112 | navigationController?.navigationBar.tintColor = .white 113 | } 114 | 115 | func filterContentForSearchText(_ searchText: String, scope: String = "All") { 116 | if searchText.isEmpty { 117 | return 118 | } 119 | let searchString = searchText.lowercased() 120 | // Cancel the currently pending item 121 | pendingRequestWorkItem?.cancel() 122 | 123 | // Wrap our request in a work item 124 | let requestWorkItem = DispatchWorkItem { [weak self] in 125 | self?.people = [FPUser]() 126 | self?.hashtags = [String]() 127 | self?.collectionView?.reloadData() 128 | self?.collectionView?.performBatchUpdates({ 129 | self?.search(searchString, at: "full_name") 130 | self?.search(searchString, at: "reversed_full_name") 131 | self?.searchHashtags(searchString) 132 | }, completion: nil) 133 | } 134 | 135 | // Save the new work item and execute it after 250 ms 136 | pendingRequestWorkItem = requestWorkItem 137 | DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(250), 138 | execute: requestWorkItem) 139 | } 140 | 141 | private func search(_ searchString: String, at index: String) { 142 | peopleRef.queryOrdered(byChild: "_search_index/\(index)").queryStarting(atValue: searchString) 143 | .queryLimited(toFirst: 10).observeSingleEvent(of: .value, with: { snapshot in 144 | let enumerator = snapshot.children 145 | while let person = enumerator.nextObject() as? DataSnapshot { 146 | if !self.appDelegate.isBlocked(by: person.key), let value = person.value as? [String: Any], let searchIndex = value["_search_index"] as? [String: Any], 147 | let fullName = searchIndex[index] as? String, fullName.hasPrefix(searchString) { 148 | self.people.append(FPUser(snapshot: person)) 149 | self.collectionView?.insertItems(at: [IndexPath(item: self.people.count - 1, section: 0)]) 150 | } 151 | } 152 | }) 153 | } 154 | 155 | private func searchHashtags(_ searchString: String) { 156 | hashtagsRef.queryOrderedByKey().queryStarting(atValue: searchString) 157 | .queryLimited(toFirst: 10).observeSingleEvent(of: .value, with: { snapshot in 158 | let enumerator = snapshot.children 159 | while let hashtag = enumerator.nextObject() as? DataSnapshot { 160 | let tag = hashtag.key 161 | if tag.hasPrefix(searchString) { 162 | self.hashtags.append(tag) 163 | self.collectionView?.insertItems(at: [IndexPath(item: self.hashtags.count - 1, section: 1)]) 164 | } 165 | } 166 | }) 167 | } 168 | 169 | override func numberOfSections(in collectionView: UICollectionView) -> Int { 170 | return 2 171 | } 172 | 173 | override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { 174 | collectionView.backgroundView = people.isEmpty && hashtags.isEmpty ? emptyLabel : nil 175 | if section == 0 { 176 | return people.count 177 | } else { 178 | return hashtags.count 179 | } 180 | } 181 | 182 | override func collectionView(_ collectionView: UICollectionView, 183 | cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { 184 | let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) 185 | if let cell = cell as? MDCSelfSizingStereoCell { 186 | cell.trailingImageView.isHidden = true 187 | if indexPath.section == 0 { 188 | let user = people[indexPath.item] 189 | if let profilePictureURL = user.profilePictureURL { 190 | UIImage.circleImage(with: profilePictureURL, to: cell.leadingImageView) 191 | } else { 192 | cell.leadingImageView.image = #imageLiteral(resourceName: "ic_account_circle_36pt") 193 | } 194 | cell.titleLabel.text = user.fullname 195 | } else { 196 | cell.leadingImageView.image = #imageLiteral(resourceName: "ic_trending_up") 197 | cell.titleLabel.text = hashtags[indexPath.item] 198 | } 199 | cell.titleLabel.numberOfLines = 1 200 | cell.detailLabel.numberOfLines = 0 201 | } 202 | return cell 203 | } 204 | 205 | override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { 206 | if indexPath.section == 0 { 207 | feedViewController.showProfile(people[indexPath.item]) 208 | } else { 209 | feedViewController.showTaggedPhotos(hashtags[indexPath.item]) 210 | } 211 | } 212 | } 213 | 214 | extension FPSearchViewController: UISearchResultsUpdating { 215 | func updateSearchResults(for searchController: UISearchController) { 216 | filterContentForSearchText(searchController.searchBar.text!) 217 | } 218 | } 219 | -------------------------------------------------------------------------------- /FriendlyPix/FPUploadViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2017 Google Inc. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | import Firebase 18 | import FirebaseMLVision 19 | import MaterialComponents 20 | 21 | class FPUploadViewController: UIViewController, UITextViewDelegate { 22 | @IBOutlet weak private var imageView: UIImageView! 23 | private var bottomConstraint: NSLayoutConstraint! 24 | private var heightConstraint: NSLayoutConstraint! 25 | private var inputBottomConstraint: NSLayoutConstraint! 26 | private var sendBottomConstraint: NSLayoutConstraint! 27 | private var isKeyboardShown = false 28 | private var mdcSnackBarManager = MDCSnackbarManager() 29 | private let messageInputContainerView: UIView = { 30 | let view = UIView() 31 | view.backgroundColor = .white 32 | return view 33 | }() 34 | 35 | var bottomAreaInset: CGFloat = 0 36 | 37 | let inputTextView: UITextView = { 38 | let textView = UITextView(placeholder: "Write a caption...") 39 | textView.font = UIFont.preferredFont(forTextStyle: UIFont.TextStyle.callout) 40 | textView.isScrollEnabled = false 41 | textView.returnKeyType = .done 42 | return textView 43 | }() 44 | 45 | let smartTagsView: UIStackView = { 46 | let view = UIStackView() 47 | view.distribution = .equalSpacing 48 | return view 49 | }() 50 | 51 | let sendButton: MDCFloatingButton = { 52 | let button = MDCFloatingButton(shape: .mini) 53 | button.setImage(#imageLiteral(resourceName: "ic_send"), for: .normal) 54 | button.tintColor = .blue 55 | button.backgroundColor = .white 56 | button.accessibilityLabel = "Upload" 57 | button.isEnabled = false 58 | button.addTarget(self, action: #selector(uploadPressed(_:)), for: .touchUpInside) 59 | return button 60 | }() 61 | 62 | var image: UIImage! 63 | var vision: Vision! 64 | @IBOutlet weak private var button: MDCButton! 65 | lazy var database = Database.database() 66 | lazy var storage = Storage.storage() 67 | 68 | let uid = Auth.auth().currentUser!.uid 69 | var fullURL = "" 70 | var thumbURL = "" 71 | var spinner: UIView? 72 | 73 | override func viewDidLoad() { 74 | super.viewDidLoad() 75 | imageView.image = image 76 | detectLabelsInImage() 77 | 78 | if #available(iOS 11.0, *) { 79 | bottomAreaInset = UIApplication.shared.keyWindow!.safeAreaInsets.bottom 80 | } 81 | inputTextView.delegate = self 82 | 83 | NotificationCenter.default.addObserver(self, selector: #selector(handleKeyboardNotification), 84 | name: UIResponder.keyboardWillShowNotification, object: nil) 85 | NotificationCenter.default.addObserver(self, selector: #selector(handleKeyboardNotification), 86 | name: UIResponder.keyboardWillHideNotification, object: nil) 87 | view.addSubview(messageInputContainerView) 88 | 89 | view.addConstraintsWithFormat(format: "H:|[v0]|", views: messageInputContainerView) 90 | 91 | heightConstraint = messageInputContainerView.heightAnchor.constraint(equalToConstant: 88 + bottomAreaInset) 92 | 93 | bottomConstraint = NSLayoutConstraint(item: messageInputContainerView, attribute: .bottom, relatedBy: .equal, 94 | toItem: view, attribute: .bottom, multiplier: 1, constant: 0) 95 | view.addConstraint(bottomConstraint) 96 | view.addConstraint(heightConstraint) 97 | setupInputComponents() 98 | } 99 | 100 | private func setupInputComponents() { 101 | let topBorderView = UIView() 102 | topBorderView.backgroundColor = UIColor(white: 0.5, alpha: 0.5) 103 | messageInputContainerView.addSubview(inputTextView) 104 | messageInputContainerView.addSubview(sendButton) 105 | messageInputContainerView.addSubview(topBorderView) 106 | messageInputContainerView.addSubview(smartTagsView) 107 | 108 | messageInputContainerView.addConstraintsWithFormat(format: "H:|-8-[v0][v1(40)]-16-|", 109 | views: inputTextView, sendButton) 110 | messageInputContainerView.addConstraintsWithFormat(format: "H:|[v0]|", views: topBorderView) 111 | messageInputContainerView.addConstraintsWithFormat(format: "H:|-16-[v0]-16-|", views: smartTagsView) 112 | 113 | smartTagsView.topAnchor.constraint(equalTo: messageInputContainerView.topAnchor, constant: 6).isActive = true 114 | smartTagsView.bottomAnchor.constraint(equalTo: inputTextView.topAnchor, constant: -6).isActive = true 115 | 116 | inputBottomConstraint = messageInputContainerView.bottomAnchor.constraint(equalTo: inputTextView.bottomAnchor, 117 | constant: bottomAreaInset) 118 | inputBottomConstraint.isActive = true 119 | 120 | sendBottomConstraint = messageInputContainerView.bottomAnchor.constraint(equalTo: sendButton.bottomAnchor, 121 | constant: bottomAreaInset + 6) 122 | sendBottomConstraint.isActive = true 123 | 124 | messageInputContainerView.addConstraintsWithFormat(format: "V:|[v0(0.5)]|", views: topBorderView) 125 | } 126 | 127 | override func viewWillAppear(_ animated: Bool) { 128 | super.viewWillAppear(animated) 129 | inputTextView.becomeFirstResponder() 130 | } 131 | 132 | override func viewWillDisappear(_ animated: Bool) { 133 | super.viewWillDisappear(animated) 134 | if let spinner = spinner { 135 | removeSpinner(spinner) 136 | } 137 | inputTextView.endEditing(true) 138 | } 139 | 140 | func textViewDidEndEditing(_ textView: UITextView) { 141 | sendButton.isEnabled = false 142 | heightConstraint.constant = 88 + bottomAreaInset 143 | } 144 | 145 | func textViewDidChange(_ textView: UITextView) { 146 | sendButton.isEnabled = !textView.text.isEmpty 147 | let size = CGSize(width: view.frame.width - 60, height: .infinity) 148 | let estimatedSize = textView.sizeThatFits(size) 149 | heightConstraint.constant = estimatedSize.height + 54 + (self.isKeyboardShown ? 0 : bottomAreaInset) 150 | } 151 | 152 | func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool { 153 | if(text == "\n") { 154 | textView.resignFirstResponder() 155 | return true 156 | } 157 | return true 158 | } 159 | 160 | @objc func tagSelected(_ tag: MDCChipView) { 161 | guard let title = tag.titleLabel.text else { return } 162 | inputTextView.insertText(title) 163 | } 164 | 165 | @objc func handleKeyboardNotification(notification: NSNotification) { 166 | guard let keyboardSize = (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] 167 | as? NSValue)?.cgRectValue else { return } 168 | let isKeyboardShowing = notification.name == UIResponder.keyboardWillShowNotification 169 | guard self.isKeyboardShown != isKeyboardShowing else { 170 | bottomConstraint?.constant = isKeyboardShowing ? -keyboardSize.height : 0 171 | return 172 | } 173 | self.isKeyboardShown = isKeyboardShowing 174 | bottomConstraint?.constant = isKeyboardShowing ? -keyboardSize.height : 0 175 | let inset = isKeyboardShowing ? -bottomAreaInset : bottomAreaInset 176 | heightConstraint?.constant += inset 177 | inputBottomConstraint?.constant = isKeyboardShowing ? 0 : bottomAreaInset 178 | sendBottomConstraint?.constant = isKeyboardShowing ? 6 : (6 + bottomAreaInset) 179 | if let animationDuration = notification.userInfo![UIResponder.keyboardAnimationDurationUserInfoKey] as? Double { 180 | UIView.animate(withDuration: animationDuration, delay: 0, options: .curveEaseOut, animations: { 181 | self.view.layoutIfNeeded() 182 | }, completion: nil) 183 | } 184 | } 185 | 186 | func textViewDidBeginEditing(_ textView: UITextView) { 187 | textViewDidChange(textView) 188 | } 189 | 190 | 191 | func detectLabelsInImage() { 192 | self.vision = Vision.vision() 193 | let options = VisionCloudImageLabelerOptions() 194 | options.confidenceThreshold = 0.7 195 | let imageLabeler = vision.cloudImageLabeler(options: options) 196 | let imageMetadata = VisionImageMetadata() 197 | imageMetadata.orientation = FPUploadViewController.visionImageOrientation(from: image.imageOrientation) 198 | let visionImage = VisionImage(image: image) 199 | visionImage.metadata = imageMetadata 200 | 201 | imageLabeler.process(visionImage) { labels, error in 202 | guard error == nil, let labels = labels, !labels.isEmpty else { 203 | return 204 | } 205 | 206 | labels.prefix(3).forEach { 207 | let chip = MDCChipView() 208 | chip.titleLabel.text = "#" + $0.text.components(separatedBy: .whitespaces).joined(separator: "_") 209 | chip.sizeToFit() 210 | chip.addTarget(self, action: #selector(self.tagSelected(_:)), for: .touchUpInside) 211 | self.smartTagsView.addArrangedSubview(chip) 212 | } 213 | } 214 | } 215 | 216 | public static func visionImageOrientation( 217 | from imageOrientation: UIImage.Orientation 218 | ) -> VisionDetectorImageOrientation { 219 | switch imageOrientation { 220 | case .up: 221 | return .topLeft 222 | case .down: 223 | return .bottomRight 224 | case .left: 225 | return .leftBottom 226 | case .right: 227 | return .rightTop 228 | case .upMirrored: 229 | return .topRight 230 | case .downMirrored: 231 | return .bottomLeft 232 | case .leftMirrored: 233 | return .leftTop 234 | case .rightMirrored: 235 | return .rightBottom 236 | } 237 | } 238 | 239 | @IBAction func uploadPressed(_ sender: Any) { 240 | spinner = displaySpinner() 241 | button.isEnabled = false 242 | inputTextView.endEditing(true) 243 | let postRef = database.reference(withPath: "posts").childByAutoId() 244 | guard let postId = postRef.key else { return } 245 | guard let resizedImageData = image.resizeImage(1280, with: 0.9) else { return } 246 | guard let thumbnailImageData = image.resizeImage(640, with: 0.7) else { return } 247 | let fullRef = storage.reference(withPath: "\(self.uid)/full/\(postId)/jpeg") 248 | let thumbRef = storage.reference(withPath: "\(self.uid)/thumb/\(postId)/jpeg") 249 | let metadata = StorageMetadata() 250 | metadata.contentType = "image/jpeg" 251 | 252 | let message = MDCSnackbarMessage() 253 | let myGroup = DispatchGroup() 254 | myGroup.enter() 255 | fullRef.putData(resizedImageData, metadata: metadata) { fullmetadata, error in 256 | if let error = error { 257 | message.text = "Error uploading image" 258 | self.mdcSnackBarManager.show(message) 259 | self.button.isEnabled = true 260 | print("Error uploading image: \(error.localizedDescription)") 261 | return 262 | } 263 | 264 | fullRef.downloadURL(completion: { (url, error) in 265 | if let error = error { 266 | print(error.localizedDescription) 267 | return 268 | } 269 | if let url = url?.absoluteString { 270 | self.fullURL = url 271 | } 272 | myGroup.leave() 273 | }) 274 | } 275 | myGroup.enter() 276 | thumbRef.putData(thumbnailImageData, metadata: metadata) { thumbmetadata, error in 277 | if let error = error { 278 | message.text = "Error uploading thumbnail" 279 | self.mdcSnackBarManager.show(message) 280 | self.button.isEnabled = true 281 | print("Error uploading thumbnail: \(error.localizedDescription)") 282 | return 283 | } 284 | thumbRef.downloadURL(completion: { (url, error) in 285 | if let error = error { 286 | print(error.localizedDescription) 287 | return 288 | } 289 | if let url = url?.absoluteString { 290 | self.thumbURL = url 291 | } 292 | myGroup.leave() 293 | }) 294 | } 295 | myGroup.notify(queue: .main) { 296 | if let spinner = self.spinner { 297 | self.removeSpinner(spinner) 298 | } 299 | 300 | let trimmedComment = self.inputTextView.text?.trimmingCharacters(in: CharacterSet.whitespaces) 301 | let data = ["full_url": self.fullURL, "full_storage_uri": fullRef.fullPath, 302 | "thumb_url": self.thumbURL, "thumb_storage_uri": thumbRef.fullPath, 303 | "text": trimmedComment ?? "", "client": "ios", 304 | "author": FPUser.currentUser().author(), "timestamp": ServerValue.timestamp()] as [String: Any] 305 | postRef.setValue(data) 306 | postRef.root.updateChildValues(["people/\(self.uid)/posts/\(postId)": true, "feed/\(self.uid)/\(postId)": true]) 307 | self.navigationController?.popViewController(animated: true) 308 | } 309 | } 310 | } 311 | -------------------------------------------------------------------------------- /FriendlyPix/FPUser.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2017 Google Inc. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | import Firebase 18 | 19 | class FPUser { 20 | var uid: String 21 | var fullname: String 22 | var profilePictureURL: URL? 23 | 24 | init(snapshot: DataSnapshot) { 25 | self.uid = snapshot.key 26 | let value = snapshot.value as! [String: Any] 27 | self.fullname = value["full_name"] as? String ?? "" 28 | guard let profile_picture = value["profile_picture"] as? String, 29 | let profilePictureURL = URL(string: profile_picture) else { return } 30 | self.profilePictureURL = profilePictureURL 31 | } 32 | 33 | init(dictionary: [String: String]) { 34 | self.uid = dictionary["uid"]! 35 | self.fullname = dictionary["full_name"] ?? "" 36 | guard let profile_picture = dictionary["profile_picture"], 37 | let profilePictureURL = URL(string: profile_picture) else { return } 38 | self.profilePictureURL = profilePictureURL 39 | } 40 | 41 | private init(user: User) { 42 | self.uid = user.uid 43 | self.fullname = user.displayName ?? "" 44 | self.profilePictureURL = user.photoURL 45 | } 46 | 47 | static func currentUser() -> FPUser { 48 | return FPUser(user: Auth.auth().currentUser!) 49 | } 50 | 51 | func author() -> [String: String] { 52 | return ["uid": uid, "full_name": fullname, "profile_picture": profilePictureURL?.absoluteString ?? ""] 53 | } 54 | } 55 | 56 | extension FPUser: Equatable { 57 | static func ==(lhs: FPUser, rhs: FPUser) -> Bool { 58 | return lhs.uid == rhs.uid 59 | } 60 | static func ==(lhs: FPUser, rhs: User) -> Bool { 61 | return lhs.uid == rhs.uid 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /FriendlyPix/FriendlyPix.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | aps-environment 6 | development 7 | com.apple.developer.associated-domains 8 | 9 | applinks:z66x3.app.goo.gl 10 | 11 | com.apple.security.application-groups 12 | 13 | group.com.google.common 14 | 15 | keychain-access-groups 16 | 17 | $(AppIdentifierPrefix)com.google.friendlypix.dev 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /FriendlyPix/Launch.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 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /FriendlyPix/UIImage+Circle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2017 Google Inc. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | import SDWebImage 18 | import Firebase 19 | 20 | extension UIImage { 21 | var circle: UIImage? { 22 | let square = CGSize(width: min(size.width, size.height), height: min(size.width, size.height)) 23 | //let square = CGSize(width: 36, height: 36) 24 | let imageView = UIImageView(frame: CGRect(origin: CGPoint(x: 0, y: 0), size: square)) 25 | imageView.contentMode = .scaleAspectFill 26 | imageView.image = self 27 | imageView.layer.cornerRadius = square.width / 2 28 | imageView.layer.masksToBounds = true 29 | UIGraphicsBeginImageContext(imageView.bounds.size) 30 | guard let context = UIGraphicsGetCurrentContext() else { return nil } 31 | imageView.layer.render(in: context) 32 | let result = UIGraphicsGetImageFromCurrentImageContext() 33 | UIGraphicsEndImageContext() 34 | return result 35 | } 36 | 37 | func resizeImage(_ dimension: CGFloat) -> UIImage { 38 | var width: CGFloat 39 | var height: CGFloat 40 | var newImage: UIImage 41 | 42 | let size = self.size 43 | let aspectRatio = size.width / size.height 44 | 45 | if aspectRatio > 1 { // Landscape image 46 | width = dimension 47 | height = dimension / aspectRatio 48 | } else { // Portrait image 49 | height = dimension 50 | width = dimension * aspectRatio 51 | } 52 | 53 | if #available(iOS 10.0, *) { 54 | let renderFormat = UIGraphicsImageRendererFormat.default() 55 | let renderer = UIGraphicsImageRenderer(size: CGSize(width: width, height: height), format: renderFormat) 56 | newImage = renderer.image { _ in 57 | self.draw(in: CGRect(x: 0, y: 0, width: width, height: height)) 58 | } 59 | } else { 60 | UIGraphicsBeginImageContext(CGSize(width: width, height: height)) 61 | self.draw(in: CGRect(x: 0, y: 0, width: width, height: height)) 62 | newImage = UIGraphicsGetImageFromCurrentImageContext()! 63 | UIGraphicsEndImageContext() 64 | } 65 | return newImage 66 | } 67 | 68 | func resizeImage(_ dimension: CGFloat, with quality: CGFloat) -> Data? { 69 | return resizeImage(dimension).jpegData(compressionQuality: quality) 70 | } 71 | 72 | static func circleImage(with url: URL, to imageView: UIImageView) { 73 | let urlString = url.absoluteString 74 | let trace = Performance.startTrace(name: "load_profile_pic") 75 | if let image = SDImageCache.shared.imageFromCache(forKey: urlString) { 76 | trace?.incrementMetric("cache", by: 1) 77 | trace?.stop() 78 | imageView.image = image 79 | return 80 | } 81 | SDWebImageDownloader.shared.downloadImage(with: url, 82 | options: .highPriority, progress: nil) { image, _, error, _ in 83 | trace?.incrementMetric("download", by: 1) 84 | trace?.stop() 85 | 86 | if let error = error { 87 | print(error) 88 | return 89 | } 90 | if let image = image { 91 | let circleImage = image.circle 92 | SDImageCache.shared.store(circleImage, forKey: urlString, completion: nil) 93 | imageView.image = circleImage 94 | } 95 | } 96 | } 97 | 98 | static func circleButton(with url: URL, to button: UIBarButtonItem) { 99 | let urlString = url.absoluteString 100 | let trace = Performance.startTrace(name: "load_profile_pic") 101 | if let image = SDImageCache.shared.imageFromCache(forKey: urlString) { 102 | trace?.incrementMetric("cache", by: 1) 103 | trace?.stop() 104 | button.image = image.resizeImage(36).withRenderingMode(.alwaysOriginal) 105 | return 106 | } 107 | SDWebImageDownloader.shared.downloadImage(with: url, options: .highPriority, progress: nil) { image, _, _, _ in 108 | trace?.incrementMetric("download", by: 1) 109 | trace?.stop() 110 | if let image = image { 111 | let circleImage = image.circle 112 | button.tintColor = .red 113 | SDImageCache.shared.store(circleImage, forKey: urlString, completion: nil) 114 | button.image = circleImage?.resizeImage(36).withRenderingMode(.alwaysOriginal) 115 | } 116 | } 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /FriendlyPix/UITextViewPlaceholder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2017 Google Inc. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | import UIKit 18 | 19 | /// Extend UITextView and implemented UITextViewDelegate to listen for changes 20 | extension UITextView { 21 | 22 | public convenience init(placeholder: String) { 23 | self.init() 24 | self.placeholder = placeholder 25 | NotificationCenter.default.addObserver(self, 26 | selector: #selector(textViewDidChange), 27 | name: UITextView.textDidChangeNotification, 28 | object: nil) 29 | } 30 | 31 | /// Resize the placeholder when the UITextView bounds change 32 | override open var bounds: CGRect { 33 | didSet { 34 | self.resizePlaceholder() 35 | } 36 | } 37 | 38 | /// The UITextView placeholder text 39 | public var placeholder: String? { 40 | get { 41 | var placeholderText: String? 42 | 43 | if let placeholderLabel = self.viewWithTag(100) as? UILabel { 44 | placeholderText = placeholderLabel.text 45 | } 46 | 47 | return placeholderText 48 | } 49 | set { 50 | if let placeholderLabel = self.viewWithTag(100) as! UILabel? { 51 | placeholderLabel.text = newValue 52 | placeholderLabel.sizeToFit() 53 | } else { 54 | self.addPlaceholder(newValue!) 55 | } 56 | } 57 | } 58 | 59 | /// When the UITextView did change, show or hide the label based on if the UITextView is empty or not 60 | /// 61 | /// - Parameter textView: The UITextView that got updated 62 | @objc public func textViewDidChange(_ textView: UITextView) { 63 | if let placeholderLabel = self.viewWithTag(100) as? UILabel { 64 | placeholderLabel.isHidden = !self.text.isEmpty 65 | } 66 | } 67 | 68 | /// Resize the placeholder UILabel to make sure it's in the same position as the UITextView text 69 | private func resizePlaceholder() { 70 | if let placeholderLabel = self.viewWithTag(100) as! UILabel? { 71 | let labelX = self.textContainer.lineFragmentPadding 72 | let labelY = self.textContainerInset.top - 2 73 | let labelWidth = self.frame.width - (labelX * 2) 74 | let labelHeight = placeholderLabel.frame.height 75 | 76 | placeholderLabel.frame = CGRect(x: labelX, y: labelY, width: labelWidth, height: labelHeight) 77 | } 78 | } 79 | 80 | /// Adds a placeholder UILabel to this UITextView 81 | private func addPlaceholder(_ placeholderText: String) { 82 | let placeholderLabel = UILabel() 83 | 84 | placeholderLabel.text = placeholderText 85 | placeholderLabel.sizeToFit() 86 | 87 | placeholderLabel.font = UIFont.preferredFont(forTextStyle: .footnote) 88 | placeholderLabel.textColor = UIColor.lightGray 89 | placeholderLabel.tag = 100 90 | 91 | placeholderLabel.isHidden = !self.text.isEmpty 92 | 93 | self.addSubview(placeholderLabel) 94 | self.resizePlaceholder() 95 | } 96 | 97 | } 98 | -------------------------------------------------------------------------------- /FriendlyPixTests/FriendlyPixTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2018 Google Inc. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | import XCTest 18 | 19 | class FriendlyPixTests: XCTestCase { 20 | 21 | override func setUp() { 22 | super.setUp() 23 | // Put setup code here. This method is called before the invocation of each test method in the class. 24 | } 25 | 26 | override func tearDown() { 27 | // Put teardown code here. This method is called after the invocation of each test method in the class. 28 | super.tearDown() 29 | } 30 | 31 | func testExample() { 32 | // This is an example of a functional test case. 33 | // Use XCTAssert and related functions to verify your tests produce the correct results. 34 | } 35 | 36 | func testPerformanceExample() { 37 | // This is an example of a performance test case. 38 | self.measure { 39 | // Put the code you want to measure the time of here. 40 | } 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /FriendlyPixTests/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 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /FriendlyPixUITests/FriendlyPixUITests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2018 Google Inc. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | import XCTest 18 | 19 | // A simple test of FriendlyPix. 20 | class FriendlyPixUITest: XCTestCase { 21 | 22 | var win: XCUIElement! 23 | 24 | open override func setUp() { 25 | super.setUp() 26 | continueAfterFailure = false 27 | let app = XCUIApplication(bundleIdentifier: "com.google.friendlypix.dev") 28 | app.launch(); 29 | win = app.windows.element(boundBy: 0) 30 | } 31 | 32 | func testGuestSignIn() { 33 | acceptAllPermissions() 34 | acceptPrivacyandTerms() 35 | signInAsGuest() 36 | readComment() 37 | } 38 | 39 | func testFriendlyPix() { 40 | acceptAllPermissions() 41 | acceptPrivacyandTerms() 42 | signInWithGoogle() 43 | //takeNewPhoto() 44 | addComment() 45 | } 46 | 47 | func acceptAllPermissions() { 48 | addUIInterruptionMonitor(withDescription: "acceptAllPermissions", handler: { 49 | alert -> Bool in 50 | for id in ["OK", "Allow", "Continue", "Not now"] { 51 | if self.tryToTap(alert.buttons[id]) { 52 | return true 53 | } 54 | } 55 | return false 56 | }) 57 | } 58 | 59 | func acceptPrivacyandTerms() { 60 | tryToTap(win.buttons["I agree"], forTimeInterval: 3) 61 | } 62 | 63 | func signInAsGuest() { 64 | let signInButton = win.buttons["Sign in as guest"] 65 | if !signInButton.exists { 66 | signOut() 67 | } 68 | signInButton.tap() 69 | } 70 | 71 | func signOut() { 72 | let profileButton = win.buttons["Profile"] 73 | profileButton.tap() 74 | let logoutButton = win.buttons["Log out"] 75 | if !tryToTap(logoutButton) { 76 | win.buttons["more"].tap() 77 | win.buttons["Sign out"].tap() 78 | } 79 | win.buttons["Logout"].tap() 80 | win.tap() 81 | acceptAllPermissions() 82 | acceptPrivacyandTerms() 83 | } 84 | 85 | func signInWithGoogle() { 86 | let signInButton = win.buttons["Sign in with Google"] 87 | if !signInButton.exists { 88 | signOut() 89 | } 90 | signInButton.tap() 91 | 92 | // Enter email or choose account from account chooser. 93 | let email = ProcessInfo.processInfo.environment["GOOGLE_EMAIL"]! 94 | let emailElement = win 95 | .descendants(matching: .any) 96 | .matching(NSPredicate(format: "(label == 'Email or phone') || (label CONTAINS[c] %@)", email)) 97 | .element(boundBy: 0) 98 | if !tryToTap(emailElement, forTimeInterval: 3) { 99 | win.tap() // Sometimes need to trigger the permissions dialog first. 100 | } 101 | if emailElement.exists { 102 | emailElement.tap() 103 | } 104 | if emailElement.exists, emailElement.label == "Email or phone" { 105 | emailElement.tap() 106 | emailElement.typeText(email) 107 | win.tap() 108 | tryToTap(win.buttons["Next"], forTimeInterval: 3) 109 | } 110 | 111 | // Enter the password field if the user is signed out. 112 | let password = ProcessInfo.processInfo.environment["GOOGLE_PASSWORD"]! 113 | let passwordField = win.secureTextFields["Enter your password"] 114 | if tryToTap(passwordField, forTimeInterval: 3) { 115 | passwordField.typeText(password) 116 | win.tap() 117 | tryToTap(win.buttons["Next"], forTimeInterval: 3) 118 | } 119 | 120 | if tryToTap(win.buttons["Confirm your recovery email"]) { 121 | win.tap() 122 | } 123 | } 124 | 125 | func takeNewPhoto() { 126 | win.buttons["Open camera"].forceTapElement() 127 | let takePhotoButton = win.buttons["Take photo"] 128 | if !tryToTap(takePhotoButton, forTimeInterval: 3) { 129 | win.tap() 130 | takePhotoButton.tap() 131 | } 132 | let doneButton = win.buttons["Done"] 133 | if !tryToTap(doneButton, forTimeInterval: 3) { 134 | // The multiple permissions dialogs can prevent the take photo from happening. 135 | takePhotoButton.tap() 136 | if !tryToTap(doneButton, forTimeInterval: 3) { 137 | // No camera 138 | win.buttons["Cancel"].tap() 139 | return 140 | } 141 | } 142 | let captionField = win.textFields.element 143 | XCTAssert(tryToTap(captionField, forTimeInterval: 3)) 144 | captionField.typeText("my friendly pic") 145 | win.buttons["UPLOAD THIS PIC"].tap() 146 | } 147 | 148 | func readComment() { 149 | XCTAssert(tryToTap(win.buttons.matching(NSPredicate(format: "label == 'comment'")).element(boundBy: 0), forTimeInterval: 3)) 150 | let commentField = win.textViews.element 151 | commentField.tap() 152 | XCTAssert(!XCUIApplication().keyboards.element.exists) 153 | } 154 | 155 | func addComment() { 156 | XCTAssert(tryToTap(win.buttons.matching(NSPredicate(format: "label == 'comment'")).element(boundBy: 0), forTimeInterval: 3)) 157 | let commentField = win.textViews.element 158 | commentField.tap() 159 | commentField.typeText("Nice, pic!") 160 | win.buttons["Send comment"].tap() 161 | win.buttons["Back"].tap() 162 | } 163 | 164 | func tryToTap(_ element: XCUIElement, forTimeInterval ti: Double = 0) -> Bool { 165 | let startTime = NSDate.timeIntervalSinceReferenceDate 166 | repeat { 167 | if element.exists && element.isHittable { 168 | element.tap() 169 | return true 170 | } 171 | } while NSDate.timeIntervalSinceReferenceDate - startTime < ti 172 | return false 173 | } 174 | 175 | func wait(forElement element: XCUIElement, timeout: TimeInterval) { 176 | let predicate = NSPredicate(format: "exists == 1") 177 | 178 | // This will make the test runner continously evalulate the 179 | // predicate, and wait until it matches. 180 | expectation(for: predicate, evaluatedWith: element) 181 | waitForExpectations(timeout: timeout) 182 | } 183 | } 184 | 185 | extension XCUIElement { 186 | func forceTapElement() { 187 | if self.isHittable { 188 | self.tap() 189 | } 190 | else { 191 | let coordinate: XCUICoordinate = self.coordinate(withNormalizedOffset: CGVector(dx:0.0, dy:0.0)) 192 | coordinate.tap() 193 | } 194 | } 195 | } 196 | 197 | -------------------------------------------------------------------------------- /FriendlyPixUITests/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 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'cocoapods', :git => 'https://github.com/CocoaPods/CocoaPods.git' 4 | gem 'cocoapods-core', :git => 'https://github.com/CocoaPods/Core.git' 5 | gem 'xcodeproj', :git => 'https://github.com/CocoaPods/Xcodeproj.git' 6 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GIT 2 | remote: https://github.com/CocoaPods/CocoaPods.git 3 | revision: f5ea2cf05f562428830d7d9288020c01f46628bc 4 | specs: 5 | cocoapods (1.11.3) 6 | addressable (~> 2.8) 7 | claide (>= 1.0.2, < 2.0) 8 | cocoapods-core (= 1.11.3) 9 | cocoapods-deintegrate (>= 1.0.3, < 2.0) 10 | cocoapods-downloader (>= 1.6.0, < 2.0) 11 | cocoapods-plugins (>= 1.0.0, < 2.0) 12 | cocoapods-search (>= 1.0.0, < 2.0) 13 | cocoapods-trunk (>= 1.6.0, < 2.0) 14 | cocoapods-try (>= 1.1.0, < 2.0) 15 | colored2 (~> 3.1) 16 | escape (~> 0.0.4) 17 | fourflusher (>= 2.3.0, < 3.0) 18 | gh_inspector (~> 1.0) 19 | molinillo (~> 0.8.0) 20 | nap (~> 1.0) 21 | ruby-macho (>= 2.3.0, < 3.0) 22 | xcodeproj (>= 1.21.0, < 2.0) 23 | 24 | GIT 25 | remote: https://github.com/CocoaPods/Core.git 26 | revision: c6ac388ee43f0782fdb8e32386f41f96dbe29082 27 | specs: 28 | cocoapods-core (1.11.3) 29 | activesupport (>= 5.0, < 7) 30 | addressable (~> 2.8) 31 | algoliasearch (~> 1.0) 32 | concurrent-ruby (~> 1.1) 33 | fuzzy_match (~> 2.0.4) 34 | nap (~> 1.0) 35 | netrc (~> 0.11) 36 | public_suffix (~> 4.0) 37 | typhoeus (~> 1.0) 38 | 39 | GIT 40 | remote: https://github.com/CocoaPods/Xcodeproj.git 41 | revision: 29cd0821d47f864abbd1ca80f23ff2aded0adfed 42 | specs: 43 | xcodeproj (1.22.0) 44 | CFPropertyList (>= 2.3.3, < 4.0) 45 | atomos (~> 0.1.3) 46 | claide (>= 1.0.2, < 2.0) 47 | colored2 (~> 3.1) 48 | nanaimo (~> 0.3.0) 49 | rexml (~> 3.2.4) 50 | 51 | GEM 52 | remote: https://rubygems.org/ 53 | specs: 54 | CFPropertyList (3.0.5) 55 | rexml 56 | activesupport (6.1.7) 57 | concurrent-ruby (~> 1.0, >= 1.0.2) 58 | i18n (>= 1.6, < 2) 59 | minitest (>= 5.1) 60 | tzinfo (~> 2.0) 61 | zeitwerk (~> 2.3) 62 | addressable (2.8.1) 63 | public_suffix (>= 2.0.2, < 6.0) 64 | algoliasearch (1.27.5) 65 | httpclient (~> 2.8, >= 2.8.3) 66 | json (>= 1.5.1) 67 | atomos (0.1.3) 68 | claide (1.1.0) 69 | cocoapods-deintegrate (1.0.5) 70 | cocoapods-downloader (1.6.3) 71 | cocoapods-plugins (1.0.0) 72 | nap 73 | cocoapods-search (1.0.1) 74 | cocoapods-trunk (1.6.0) 75 | nap (>= 0.8, < 2.0) 76 | netrc (~> 0.11) 77 | cocoapods-try (1.2.0) 78 | colored2 (3.1.2) 79 | concurrent-ruby (1.1.10) 80 | escape (0.0.4) 81 | ethon (0.16.0) 82 | ffi (>= 1.15.0) 83 | ffi (1.15.5) 84 | fourflusher (2.3.1) 85 | fuzzy_match (2.0.4) 86 | gh_inspector (1.1.3) 87 | httpclient (2.8.3) 88 | i18n (1.12.0) 89 | concurrent-ruby (~> 1.0) 90 | json (2.6.2) 91 | minitest (5.16.3) 92 | molinillo (0.8.0) 93 | nanaimo (0.3.0) 94 | nap (1.1.0) 95 | netrc (0.11.0) 96 | public_suffix (4.0.7) 97 | rexml (3.2.5) 98 | ruby-macho (2.5.1) 99 | typhoeus (1.4.0) 100 | ethon (>= 0.9.0) 101 | tzinfo (2.0.5) 102 | concurrent-ruby (~> 1.0) 103 | zeitwerk (2.6.6) 104 | 105 | PLATFORMS 106 | ruby 107 | 108 | DEPENDENCIES 109 | cocoapods! 110 | cocoapods-core! 111 | xcodeproj! 112 | 113 | BUNDLED WITH 114 | 2.3.25 115 | -------------------------------------------------------------------------------- /Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIcons 10 | 11 | CFBundleIcons~ipad 12 | 13 | CFBundleIdentifier 14 | $(PRODUCT_BUNDLE_IDENTIFIER) 15 | CFBundleInfoDictionaryVersion 16 | 6.0 17 | CFBundleName 18 | $(PRODUCT_NAME) 19 | CFBundlePackageType 20 | APPL 21 | CFBundleShortVersionString 22 | 0.2.4 23 | CFBundleURLTypes 24 | 25 | 26 | CFBundleTypeRole 27 | Editor 28 | CFBundleURLName 29 | google 30 | CFBundleURLSchemes 31 | 32 | REVERSED_CLIENT_ID 33 | 34 | 35 | 36 | CFBundleVersion 37 | 0 38 | ITSAppUsesNonExemptEncryption 39 | 40 | LSApplicationCategoryType 41 | 42 | LSRequiresIPhoneOS 43 | 44 | NSAppTransportSecurity 45 | 46 | NSAllowsArbitraryLoads 47 | 48 | 49 | NSCameraUsageDescription 50 | to take photos to upload. 51 | NSPhotoLibraryUsageDescription 52 | to present photos to upload. 53 | UIBackgroundModes 54 | 55 | remote-notification 56 | 57 | UILaunchStoryboardName 58 | Launch 59 | UIMainStoryboardFile 60 | Main 61 | UIRequiredDeviceCapabilities 62 | 63 | armv7 64 | 65 | UIStatusBarTintParameters 66 | 67 | UINavigationBar 68 | 69 | Style 70 | UIBarStyleDefault 71 | Translucent 72 | 73 | 74 | 75 | UISupportedInterfaceOrientations 76 | 77 | UIInterfaceOrientationPortrait 78 | UIInterfaceOrientationLandscapeLeft 79 | UIInterfaceOrientationLandscapeRight 80 | 81 | UISupportedInterfaceOrientations~ipad 82 | 83 | UIInterfaceOrientationPortrait 84 | UIInterfaceOrientationPortraitUpsideDown 85 | UIInterfaceOrientationLandscapeLeft 86 | UIInterfaceOrientationLandscapeRight 87 | 88 | 89 | 90 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright 2015 Google Inc 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | 204 | All code in any directories or sub-directories that end with *.html or 205 | *.css is licensed under the Creative Commons Attribution International 206 | 4.0 License, which full text can be found here: 207 | https://creativecommons.org/licenses/by/4.0/legalcode. 208 | 209 | As an exception to this license, all html or css that is generated by 210 | the software at the direction of the user is copyright the user. The 211 | user has full ownership and control over such content, including 212 | whether and how they wish to license it. 213 | -------------------------------------------------------------------------------- /Podfile: -------------------------------------------------------------------------------- 1 | platform :ios, '13.0' 2 | target 'FriendlyPix' do 3 | use_frameworks! 4 | 5 | pod 'Firebase/Analytics' 6 | pod 'Firebase/Auth' 7 | pod 'Firebase/Crashlytics' 8 | pod 'Firebase/Database' 9 | pod 'Firebase/Messaging' 10 | pod 'Firebase/MLVision' 11 | pod 'Firebase/Performance' 12 | pod 'Firebase/Storage' 13 | pod 'FirebaseUI/Anonymous' 14 | pod 'FirebaseUI/Auth' 15 | pod 'FirebaseUI/Google' 16 | pod 'ImagePicker' 17 | pod 'Lightbox' 18 | pod 'MaterialComponents/BottomAppBar' 19 | pod 'MaterialComponents/Buttons+ColorThemer' 20 | pod 'MaterialComponents/Cards' 21 | pod 'MaterialComponents/Chips' 22 | pod 'MaterialComponents/Dialogs' 23 | pod 'MaterialComponents/Dialogs+ColorThemer' 24 | pod 'MaterialComponents/Snackbar' 25 | pod 'MaterialComponents/TextFields' 26 | pod 'MaterialComponents/List' 27 | pod 'SDWebImage' 28 | end 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Friendly Pix iOS 2 | 3 | FriendlyPix is a simple app to capture and share your favorite moments. It demonstrates the best practises of building an iOS app on the Firebase Platform. Follow interesting accounts of your choice. Interact with them through the comments. Stay up-to-date with the latest photos posted in the community. 4 | 5 | Use FriendlyPix to: 6 | 7 | * Post photos you want to keep on your profile grid. 8 | * Search profiles for friends and family. 9 | * Follow accounts to add photos from them in your Home feed. 10 | * Explore the latest photos from the Trending feed. 11 | * Interact with community through the comments under each photo. 12 | 13 | 14 | 15 | ## Initial setup, build tools and dependencies 16 | 17 | Friendly Pix iOS is built using Swift and [Firebase](https://firebase.google.com/docs/ios/setup). The Auth flow is built using [Firebase-UI](https://github.com/firebase/firebaseui-ios). Dependencies are managed using [CocoaPods](https://cocoapods.org/). Additionally server-side micro-services are built on [Cloud Functions for Firebase](https://firebase.google.com/docs/functions). 18 | 19 | Simply install the pods and open the .xcworkspace file to see the project in Xcode. 20 | 21 | ``` 22 | $ pod install 23 | $ open your-project.xcworkspace 24 | ``` 25 | 26 | ## Create Firebase Project 27 | 28 | 1. Create a Firebase project using the [Firebase Console](https://firebase.google.com/console). 29 | 1. To add the FriendlyPix app to a Firebase project, use the bundleID `com.google.firebase.friendlypix`. 30 | 1. Download the generated `GoogleService-Info.plist` file, and copy it to the root directory of this app. 31 | 32 | ### Google Sign In Setup 33 | - Go to the [Firebase Console](https://console.firebase.google.com) and navigate to your project: 34 | - Select the **Auth** panel and then click the **Sign In Method** tab. 35 | - Click **Google** and turn on the **Enable** switch, then click **Save**. 36 | - Open your regular `Info.plist`, navigate to `URL types > Item 0 > URL schemes`, and replace the value 37 | of `YOUR_REVERSED_CLIENT_ID` with the value of `REVERSED_CLIENT_ID` from the GoogleService-Info.plist`. 38 | - Run the app on your device or simulator. 39 | - Select **Sign In** and select Google to begin. 40 | 41 | ### Facebook Login Setup 42 | - Go to the [Facebook Developers Site](https://developers.facebook.com) and follow all 43 | instructions to set up a new iOS app. When asked for a bundle ID, use 44 | `com.google.firebase.quickstart.AuthenticationExample`. 45 | - Go to the [Firebase Console](https://console.firebase.google.com) and navigate to your project: 46 | - Select the **Auth** panel and then click the **Sign In Method** tab. 47 | - Click **Facebook** and turn on the **Enable** switch, then click **Save**. 48 | - Enter your Facebook **App Id** and **App Secret** and click **Save**. 49 | - Open your regular `Info.plist` and replace the value of the `FacebookAppID` with the ID of the 50 | Facebook app you just created, e.g 124567. Save that file. 51 | - In the *Info* tab of your target settings add a *URL Type* with a *URL Scheme* of 'fb' + the ID 52 | of your Facebook app, e.g. fb1234567. 53 | - Run the app on your device or simulator. 54 | - Select **Sign In** and select Facebook to begin. 55 | 56 | 57 | ## Requirements 58 | 59 | The mobile FriendlyPix app need the Cloud Functions, the Realtime Database rules and the Cloud Storage rules to be deployed to work properly. You can find instructions at [FriendlyPix Web Repository](https://github.com/firebase/friendlypix-web/blob/master/README.md#mobile-apps). 60 | 61 | 62 | ## Contributing 63 | 64 | We'd love that you contribute to the project. Before doing so please read our [Contributor guide](../CONTRIBUTING.md). 65 | 66 | 67 | ## License 68 | 69 | © Google, 2011. Licensed under an [Apache-2](../LICENSE) license. 70 | -------------------------------------------------------------------------------- /friendly-pix.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/firebase/friendlypix-ios/bbdae8bb5fea5ae0e01ac49da6738a3ab4d0c7c5/friendly-pix.png -------------------------------------------------------------------------------- /info_script.rb: -------------------------------------------------------------------------------- 1 | require 'xcodeproj' 2 | project_path = "FriendlyPix.xcodeproj" 3 | project = Xcodeproj::Project.open(project_path) 4 | 5 | # Add a file to the project in the main group 6 | file_name = 'GoogleService-Info.plist' 7 | file = project.new_file(file_name) 8 | 9 | # Add the file to the all targets 10 | project.targets.each do |target| 11 | target.add_file_references([file]) 12 | end 13 | 14 | #save project 15 | project.save() 16 | -------------------------------------------------------------------------------- /mock-GoogleService-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | AD_UNIT_ID_FOR_BANNER_TEST 6 | ca-app-pub-3940256099942544/2934735716 7 | AD_UNIT_ID_FOR_INTERSTITIAL_TEST 8 | ca-app-pub-3940256099942544/4411468910 9 | API_KEY 10 | AIzaSyAzlj4APqi5S58nFtE52Da-fYBOHA2MhaY 11 | com.google.friendlypix.dev 12 | id 13 | CLIENT_ID 14 | 123456789000-hjugbg6ud799v4c49dim8ce2usclthar.apps.googleusercontent.com 15 | DATABASE_URL 16 | https://mockproject-1234.firebaseio.com 17 | GCM_SENDER_ID 18 | 123456789000 19 | GOOGLE_APP_ID 20 | 1:123456789000:ios:f1bf012572b04063 21 | IS_ADS_ENABLED 22 | 23 | IS_ANALYTICS_ENABLED 24 | 25 | IS_APPINVITE_ENABLED 26 | 27 | IS_GCM_ENABLED 28 | 29 | IS_SIGNIN_ENABLED 30 | 31 | PLIST_VERSION 32 | 1 33 | PROJECT_ID 34 | mockproject-1234 35 | REVERSED_CLIENT_ID 36 | com.googleusercontent.apps.123456789000-hjugbg6ud799v4c49dim8ce2usclthar 37 | STORAGE_BUCKET 38 | mockproject-1234.appspot.com 39 | 40 | 41 | -------------------------------------------------------------------------------- /test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -eo pipefail 4 | 5 | EXIT_STATUS=0 6 | 7 | (xcodebuild \ 8 | -workspace FriendlyPix.xcworkspace \ 9 | -scheme FriendlyPix \ 10 | -sdk iphonesimulator \ 11 | -destination 'platform=iOS Simulator,name=iPhone XR' \ 12 | build \ 13 | #test \ 14 | ONLY_ACTIVE_ARCH=YES \ 15 | CODE_SIGNING_REQUIRED=NO \ 16 | | xcpretty) || EXIT_STATUS=$? 17 | 18 | exit $EXIT_STATUS 19 | --------------------------------------------------------------------------------