├── .gitattributes ├── .gitconfig ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ └── ci.yml ├── .gitignore ├── ApiServer ├── README.md ├── api.js ├── key │ ├── edch_private_key.txt │ ├── fcm_private_key.json │ └── keyGenerator.js ├── notification.js ├── test │ ├── fcm_token.txt │ ├── mock.json │ ├── mock.text │ ├── test_decipher.js │ ├── test_join.js │ └── test_notification.js └── webPushDecipher.js ├── Icon ├── Misscat-transparent.png ├── MisscatIcon.png ├── MisscatIcon_1024x1024.png ├── MisscatIcon_old.png ├── MisscatIcon_shaped.png └── Sad_Ai.png ├── LICENSE ├── MissCat.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ ├── xcshareddata │ │ └── IDEWorkspaceChecks.plist │ └── xcuserdata │ │ └── yuigawada.xcuserdatad │ │ └── UserInterfaceState.xcuserstate ├── xcshareddata │ └── xcschemes │ │ └── MissCat.xcscheme └── xcuserdata │ └── yuigawada.xcuserdatad │ └── xcschemes │ └── xcschememanagement.plist ├── MissCat.xcworkspace ├── contents.xcworkspacedata ├── xcshareddata │ ├── IDEWorkspaceChecks.plist │ └── WorkspaceSettings.xcsettings └── xcuserdata │ └── yuigawada.xcuserdatad │ ├── IDEFindNavigatorScopes.plist │ └── WorkspaceSettings.xcsettings ├── MissCat ├── Entity │ ├── NoteEntity.swift │ ├── ReactionEntity.swift │ ├── UrlSummalyEntity.swift │ └── UserEntity.swift ├── GoogleService-Info.plist ├── Info.plist ├── MFMEngine │ ├── EmojiHandler.swift │ ├── MFMEngine.swift │ ├── MFMImageView.swift │ └── MisskeyTextView.swift ├── MissCat.entitlements ├── Model │ ├── Details │ │ ├── PostDetailModel.swift │ │ └── ProfileModel.swift │ ├── DirectMessage │ │ ├── DirectMessageModel.swift │ │ └── MessageListModel.swift │ ├── Main │ │ ├── AccountsListModel.swift │ │ ├── HomeModel.swift │ │ ├── NotificationModel.swift │ │ ├── PostModel.swift │ │ ├── SearchModel.swift │ │ └── TimelineModel.swift │ ├── Reusable │ │ ├── Accounts │ │ │ └── AccountCellModel.swift │ │ ├── NoteCell │ │ │ ├── NoteCellModel.swift │ │ │ └── UrlPreviewerModel.swift │ │ ├── Notification │ │ │ ├── NotificationBannerModel.swift │ │ │ └── NotificationCellModel.swift │ │ ├── Reaction │ │ │ └── ReactionGenModel.swift │ │ ├── Tab │ │ │ └── NavBarModel.swift │ │ └── User │ │ │ ├── UserCellModel.swift │ │ │ └── UserListModel.swift │ └── Settings │ │ └── ProfileSettingsModel.swift ├── Others │ ├── App │ │ ├── ApiKeyManager.swift │ │ ├── AppDelegate.swift │ │ ├── Bridge.h │ │ ├── MisscatApi.swift │ │ ├── SceneDelegate.swift │ │ └── SecureUser.swift │ ├── Extension │ │ ├── AVKit+MissCat.swift │ │ ├── Double+MissCat.swift │ │ ├── Error+MissCat.swift │ │ ├── MisskeyKit+MissCat.swift │ │ ├── NSMutableAttributedString+MissCat.swift │ │ ├── NoteModel+MissCat.swift │ │ ├── String+MissCat.swift │ │ ├── UIColor+MissCat.swift │ │ ├── UIFont+MissCat.swift │ │ ├── UIImage+MissCat.swift │ │ ├── UIView+MissCat.swift │ │ ├── UIViewController+ImageViewer.swift │ │ ├── UIViewController+MissCat.swift │ │ ├── UIViewController+NavigationController.swift │ │ ├── UIViewController+PhotoEditor.swift │ │ ├── URL+MissCat.swift │ │ └── UserModel+MissCat.swift │ └── Utilities │ │ ├── Cache.swift │ │ ├── CellModel.swift │ │ ├── CustomNavigationController.swift │ │ ├── ImageManager.swift │ │ ├── MissCatImageView.swift │ │ ├── MissCatTableView.swift │ │ ├── PlaceholderTableView.swift │ │ ├── Requestor.swift │ │ ├── RxEureka.swift │ │ └── Theme.swift ├── Resources │ ├── 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-60x60@2x.png │ │ │ ├── Icon-App-60x60@3x.png │ │ │ ├── Icon-App-76x76@1x.png │ │ │ ├── Icon-App-76x76@2x.png │ │ │ ├── Icon-App-83.5x83.5@2x.png │ │ │ └── ItunesArtwork@2x.png │ │ ├── Cat.imageset │ │ │ ├── Contents.json │ │ │ ├── cat_-1.png │ │ │ ├── cat_-2.png │ │ │ └── cat_.png │ │ ├── Contents.json │ │ ├── Logo.imageset │ │ │ ├── Contents.json │ │ │ ├── Logo-1.png │ │ │ ├── Logo-2.png │ │ │ └── Logo.png │ │ ├── MissCat.imageset │ │ │ ├── Contents.json │ │ │ ├── MisscatIcon_shaped-1.png │ │ │ ├── MisscatIcon_shaped-2.png │ │ │ └── MisscatIcon_shaped-3.png │ │ ├── Sad_Ai.imageset │ │ │ ├── Contents.json │ │ │ ├── Sad_Ai-1.png │ │ │ ├── Sad_Ai-2.png │ │ │ └── Sad_Ai.png │ │ ├── SprashBack.imageset │ │ │ ├── Contents.json │ │ │ ├── SprashBack-1.png │ │ │ ├── SprashBack-2.png │ │ │ └── SprashBack.png │ │ ├── del.imageset │ │ │ ├── Contents.json │ │ │ ├── del-1.png │ │ │ ├── del-2.png │ │ │ └── del.png │ │ ├── error.imageset │ │ │ ├── Contents.json │ │ │ ├── error-1.png │ │ │ ├── error-2.png │ │ │ └── error.png │ │ ├── misskey.imageset │ │ │ ├── Contents.json │ │ │ ├── misskey-1.jpg │ │ │ ├── misskey-2.jpg │ │ │ └── misskey.jpg │ │ └── play.imageset │ │ │ ├── Contents.json │ │ │ ├── play-1.png │ │ │ ├── play-2.png │ │ │ └── play.png │ ├── Base.lproj │ │ └── LaunchScreen.storyboard │ ├── Font Awesome 5 Brands-Regular-400.otf │ ├── Font Awesome 5 Free-Regular-400.otf │ └── Font Awesome 5 Free-Solid-900.otf ├── View │ ├── Base.lproj │ │ └── Main.storyboard │ ├── Details │ │ ├── PostDetailViewController.swift │ │ └── ProfileViewController.swift │ ├── DirectMessage │ │ ├── ChatViewController.swift │ │ ├── DirectMessageViewController.swift │ │ ├── MessageListViewController.swift │ │ └── Reusable │ │ │ ├── SenderCell.swift │ │ │ └── SenderCell.xib │ ├── Login │ │ ├── AuthWebViewController.swift │ │ ├── StartViewController.swift │ │ └── TosViewController.swift │ ├── Main │ │ ├── AccountsListViewController.swift │ │ ├── HomeViewController.swift │ │ ├── NotificationsViewController.swift │ │ ├── PostViewController.swift │ │ ├── SearchViewController.swift │ │ └── TimelineViewController.swift │ ├── Reusable │ │ ├── Accounts │ │ │ ├── AccountCell.swift │ │ │ ├── AccountsDropdownMenu.swift │ │ │ └── nib │ │ │ │ └── AccountCell.xib │ │ ├── Emoji │ │ │ ├── EmojiView.swift │ │ │ ├── EmojiViewCell.swift │ │ │ └── nib │ │ │ │ ├── EmojiView.xib │ │ │ │ └── EmojiViewCell.xib │ │ ├── NoteCell │ │ │ ├── FileContainer.swift │ │ │ ├── FileContainerCell.swift │ │ │ ├── NoteCell.FileView.swift │ │ │ ├── NoteCell.Model.swift │ │ │ ├── NoteCell.swift │ │ │ ├── PollView.swift │ │ │ ├── PromotionCell.swift │ │ │ ├── ReactionCell.swift │ │ │ ├── RenoteeCell.swift │ │ │ ├── UrlPreviewer.swift │ │ │ └── nib │ │ │ │ ├── FileContainerCell.xib │ │ │ │ ├── NoteCell.xib │ │ │ │ ├── PollView.xib │ │ │ │ ├── PromotionCell.xib │ │ │ │ ├── ReactionCell.xib │ │ │ │ ├── RenoteeCell.xib │ │ │ │ └── UrlPreviewer.xib │ │ ├── Notification │ │ │ ├── NanoNotificationBanner.swift │ │ │ ├── NotificationBanner.swift │ │ │ ├── NotificationCell.swift │ │ │ └── nib │ │ │ │ ├── NanoNotificationBanner.xib │ │ │ │ ├── NotificationBanner.xib │ │ │ │ └── NotificationCell.xib │ │ ├── Others │ │ │ ├── ComponentType.swift │ │ │ ├── DropdownMenuViewController.swift │ │ │ ├── NoteDisplay.swift │ │ │ └── PanelMenuViewController.swift │ │ ├── Post │ │ │ ├── AttachmentCell.swift │ │ │ ├── EditablePollView.swift │ │ │ └── nib │ │ │ │ └── AttachmentCell.xib │ │ ├── Reaction │ │ │ ├── ReactionCollectionHeader.swift │ │ │ ├── ReactionGenPanel.swift │ │ │ ├── ReactionGenViewController.swift │ │ │ └── nib │ │ │ │ ├── ReactionCollectionHeader.xib │ │ │ │ └── ReactionGenCell.xib │ │ ├── Tab │ │ │ ├── NavBar.swift │ │ │ ├── TabBar.swift │ │ │ └── nib │ │ │ │ ├── NavBar.xib │ │ │ │ └── TabBar.xib │ │ ├── Trend │ │ │ └── TrendViewController.swift │ │ └── User │ │ │ ├── UserCell.swift │ │ │ ├── UserListViewController.swift │ │ │ └── nib │ │ │ └── UserCell.xib │ └── Settings │ │ ├── AboutMisskeyViewController.swift │ │ ├── AccountViewController.swift │ │ ├── Cell │ │ ├── ColorPickerCell.swift │ │ ├── ColorPickerCell.xib │ │ ├── TabSettingsCell.swift │ │ └── TabSettingsCell.xib │ │ ├── ColorPicker.swift │ │ ├── DesignSettingsViewController.swift │ │ ├── InstanceViewController.swift │ │ ├── LicenseViewController.swift │ │ ├── ProfileSettingsViewController.swift │ │ ├── ReactionSettingsViewController.swift │ │ ├── SettingsViewController.swift │ │ └── ThemeViewController.swift └── ViewModel │ ├── Details │ ├── PostDetailViewModel.swift │ └── ProfileViewModel.swift │ ├── DirectMessage │ ├── DirectMessageViewModel.swift │ └── MessageListViewModel.swift │ ├── Main │ ├── AccountsListViewModel.swift │ ├── HomeViewModel.swift │ ├── NotificationsViewModel.swift │ ├── PostViewModel.swift │ ├── SearchViewModel.swift │ └── TimelineViewModel.swift │ ├── Others │ └── ViewModelType.swift │ ├── Reusable │ ├── Accounts │ │ └── AccountCellViewModel.swift │ ├── NoteCell │ │ ├── FileContainerViewModel.swift │ │ ├── NoteCellViewModel.swift │ │ └── UrlPreviewerViewModel.swift │ ├── Notification │ │ ├── NotificationBannerViewModel.swift │ │ └── NotificationCellViewModel.swift │ ├── Reaction │ │ └── ReactionGenViewModel.swift │ ├── Tab │ │ └── NavBarViewModel.swift │ └── User │ │ ├── UserCellViewModel.swift │ │ └── UserListViewModel.swift │ └── Settings │ ├── ProfileSettingsViewModel.swift │ └── ReactionSettingsViewModel.swift ├── MissCatShare ├── AccountsViewController.swift ├── Base.lproj │ └── MainInterface.storyboard ├── Info.plist ├── MissCatShare.entitlements ├── PostModel.swift ├── ShareAssets.xcassets │ ├── Contents.json │ ├── MissCat.imageset │ │ ├── Contents.json │ │ ├── MisscatIcon_shaped-1.png │ │ ├── MisscatIcon_shaped-2.png │ │ └── MisscatIcon_shaped-3.png │ └── back.imageset │ │ ├── Contents.json │ │ ├── SprashBack-1.png │ │ ├── SprashBack-2.png │ │ └── SprashBack.png ├── ShareViewController.swift ├── UserModel.swift └── VisibilityViewController.swift ├── MissCatTests ├── Info.plist ├── MFMTests.swift └── MissCatTests.swift ├── Podfile ├── Podfile.lock ├── README.md ├── images ├── Badge.svg ├── Banner.png └── ScreenShot │ ├── iPhone Pro │ ├── Simulator Screen Shot - iPhone 8 Plus - 2020-03-29 at 00.27.35.png │ ├── Simulator Screen Shot - iPhone 8 Plus - 2020-03-29 at 00.28.09.png │ ├── Simulator Screen Shot - iPhone 8 Plus - 2020-03-29 at 00.28.14.png │ ├── Simulator Screen Shot - iPhone 8 Plus - 2020-03-29 at 00.28.28.png │ └── Simulator Screen Shot - iPhone 8 Plus - 2020-03-29 at 00.28.44.png │ └── iPhoneX │ ├── Simulator Screen Shot - iPhone 11 Pro Max - 2020-03-28 at 19.32.36.png │ ├── Simulator Screen Shot - iPhone 11 Pro Max - 2020-03-28 at 19.32.58.png │ ├── Simulator Screen Shot - iPhone 11 Pro Max - 2020-03-28 at 19.33.13.png │ ├── Simulator Screen Shot - iPhone 11 Pro Max - 2020-03-28 at 19.33.57.png │ ├── Simulator Screen Shot - iPhone 11 Pro Max - 2020-03-28 at 19.34.44.png │ └── Simulator Screen Shot - iPhone 11 Pro Max - 2020-03-28 at 19.35.01.png └── python_utls ├── COMBINED_LICENSE ├── library_list.txt └── licenser.py /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | *.pbxproj merge=mergepbx 4 | -------------------------------------------------------------------------------- /.gitconfig: -------------------------------------------------------------------------------- 1 | [merge "mergepbx"] 2 | name = Xcode project files merger 3 | driver = mergepbx %O %A %B 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve (バグ報告) 4 | title: '' 5 | labels: "\U0001F623Bug" 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | 12 | # **Describe the bug (概要)** 13 | A clear and concise description of what the bug is. 14 | 15 | # **Expected behavior (想定されている挙動)** 16 | A clear and concise description of what you expected to happen. 17 | 18 | # **To Reproduce (再現手順)** 19 | Steps to reproduce the behavior: 20 | 1. Go to '...' 21 | 2. See error 22 | 23 | # **Possible cause (考えられる原因)** 24 | If you think of anything that might have caused this bug, write down here. 25 | 26 | # **Todo** 27 | If you wanna tell us todos, add possible todo. 28 | 29 | # **Screenshots** 30 | If applicable, add screenshots to help explain your problem. 31 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project(要望) 4 | title: '' 5 | labels: "⭐Enhancement" 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | 12 | # **Describe the request (概要)** 13 | A clear and concise description of what your request is. 14 | 15 | # **Purpose (目的)** 16 | Add your clear idea of why we need this feature. 17 | 18 | # **Todo** 19 | If you think of anything about todo, write down here. 20 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Swift 2 | 3 | on: 4 | push: 5 | branches: 6 | - develop 7 | - feature/* 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: macos-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: Select Xcode version 17 | run: sudo xcode-select -s '/Applications/Xcode_11.3.1.app/Contents/Developer' 18 | - name: Inject api url 19 | run: echo ${{ secrets.API_KEY_MANAGER_CODE }} > ./MissCat/Others/App/ApiKeyManager.swift 20 | - name: Cache CocoaPods files 21 | uses: actions/cache@v1 22 | with: 23 | path: Pods 24 | key: ${{ runner.os }}-pods-${{ hashFiles('**/Podfile.lock') }} 25 | restore-keys: | 26 | ${{ runner.os }}-pods- 27 | - name: Cache gems 28 | uses: actions/cache@preview 29 | with: 30 | path: vendor/bundle 31 | key: ${{ runner.os }}-gem-${{ hashFiles('**/Gemfile.lock') }} 32 | restore-keys: | 33 | ${{ runner.os }}-gem- 34 | - name: Install cocoapods-binary 35 | run: gem install cocoapods-binary 36 | - name: Run pod install 37 | run: pod install 38 | - name: Build and Test # On Simulator 39 | run: xcodebuild test -workspace MissCat.xcworkspace -scheme MissCat -destination "platform=iOS Simulator,OS=13.3,name=iPhone 11 Pro" CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO ONLY_ACTIVE_ARCH=NO 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.swp 3 | # Xcode 4 | # 5 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 6 | 7 | ## User settings 8 | xcuserdata/ 9 | 10 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 11 | *.xcscmblueprint 12 | *.xccheckout 13 | 14 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 15 | build/ 16 | DerivedData/ 17 | *.moved-aside 18 | *.pbxuser 19 | !default.pbxuser 20 | *.mode1v3 21 | !default.mode1v3 22 | *.mode2v3 23 | !default.mode2v3 24 | *.perspectivev3 25 | !default.perspectivev3 26 | 27 | ## Obj-C/Swift specific 28 | *.hmap 29 | 30 | ## App packaging 31 | *.ipa 32 | *.dSYM.zip 33 | *.dSYM 34 | 35 | ## Playgrounds 36 | timeline.xctimeline 37 | playground.xcworkspace 38 | 39 | # Swift Package Manager 40 | # 41 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 42 | # Packages/ 43 | # Package.pins 44 | # Package.resolved 45 | # *.xcodeproj 46 | # 47 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 48 | # hence it is not needed unless you have added a package configuration file to your project 49 | # .swiftpm 50 | 51 | .build/ 52 | 53 | # CocoaPods 54 | # 55 | # We recommend against adding the Pods directory to your .gitignore. However 56 | # you should judge for yourself, the pros and cons are mentioned at: 57 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 58 | # 59 | # Pods/ 60 | # 61 | # Add this line if you want to avoid checking in source code from the Xcode workspace 62 | # *.xcworkspace 63 | 64 | # Carthage 65 | # 66 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 67 | # Carthage/Checkouts 68 | 69 | Carthage/Build/ 70 | 71 | # Accio dependency management 72 | Dependencies/ 73 | .accio/ 74 | 75 | # fastlane 76 | # 77 | # It is recommended to not store the screenshots in the git repo. 78 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 79 | # For more information about the recommended setup visit: 80 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 81 | 82 | fastlane/report.xml 83 | fastlane/Preview.html 84 | fastlane/screenshots/**/*.png 85 | fastlane/test_output 86 | 87 | # Code Injection 88 | # 89 | # After new code Injection tools there's a generated folder /iOSInjectionProject 90 | # https://github.com/johnno1962/injectionforxcode 91 | 92 | iOSInjectionProject/ 93 | 94 | 95 | Pods 96 | 97 | 98 | MissCat.xcworkspace/xcuserdata/yuigawada.xcuserdatad/xcdebugger/Expressions.xcexplist 99 | # MissCat/Others/App/ApiKeyManager.swift 100 | -------------------------------------------------------------------------------- /ApiServer/README.md: -------------------------------------------------------------------------------- 1 | # webPushDecipher.js 2 | [```'sw/register'```](https://misskey.io/api-doc#operation/sw/register)で購読したPush通知をdecryptするヤツ 3 | 4 | ### **参考にしたもの** 5 | 6 | - https://tools.ietf.org/html/rfc8188 7 | 8 | - https://tools.ietf.org/html/rfc8291 9 | 10 | - https://tools.ietf.org/html/rfc8291#appendix-A 11 | 12 | - https://gist.github.com/tateisu/685eab242549d9c9ffc85020f09a4b71 13 | 14 | - https://mastodon.juggler.jp/@tateisu/104098620591598243 15 | -------------------------------------------------------------------------------- /ApiServer/api.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const express = require('express'); 5 | const bodyParser = require('body-parser'); 6 | 7 | const notification = require('./notification.js'); 8 | const webPushDecipher = require('./webPushDecipher.js'); 9 | 10 | // For Distributed 11 | const authSecret = "Q8Zgu-WDvN5EDT_emFGovQ" 12 | const publicKey = "BJNAJpIOIJnXVVgCTAd4geduXEsNKre0XVvz0j-E_z-8CbGI6VaRPsVI7r-hF88MijMBZApurU2HmSNQ4e-cTmA" 13 | const privateKey = fs.readFileSync('./key/edch_private_key.txt', 'utf8'); 14 | 15 | 16 | // For test 17 | // const authSecret = "43w_wOVYeF9XzyRyZL3O8g" 18 | // const publicKey = "BJgVD2cj1pNKNR2Ss3U_8e7P9AyoL5kWaxVio5aO16Cvnx-P1r7HH8SRb-h5tuxaydZ1ky3oO0V40s6t_uN1SdA" 19 | // const privateKey = "ciQ800G-6jyKWf6KKG94g5rCSU_l_rgbHbyHny_UsIM" 20 | 21 | 22 | 23 | function decrypt(raw) { 24 | // const converted = raw.toString('utf-8') // for debug 25 | const converted = raw.toString('base64') 26 | 27 | const reciverKey = webPushDecipher.reciverKeyBuilder(publicKey,privateKey,authSecret) 28 | var decrypted = webPushDecipher.decrypt(converted,reciverKey,false) 29 | return decrypted 30 | } 31 | 32 | const app = express(); 33 | 34 | var concat = require('concat-stream'); 35 | app.use(function(req, res, next){ 36 | req.pipe(concat(function(data){ 37 | req.body = data; 38 | next(); 39 | })); 40 | }); 41 | 42 | 43 | app.post("/api/:version/push/:lang/:userId/:deviceToken", function(req, res){ 44 | if (req.params.version != "v1") { res.status(410).send('Invalid Version.').end(); return; } 45 | 46 | const rawBody = req.body; 47 | if (!rawBody) { res.status(200).send('Invalid Body.').end(); } 48 | 49 | const rawJson = decrypt(rawBody); 50 | const userId = req.params.userId; 51 | const deviceToken = req.params.deviceToken; 52 | const lang = req.params.lang; 53 | if (!rawJson||!userId||!deviceToken||!lang) { res.status(410).send('Invalid Url.').end(); return; } 54 | 55 | console.log(rawJson) 56 | const contents = notification.generateContents(rawJson,userId,lang); 57 | const title = contents[0]; 58 | const body = contents[1]; 59 | const extra = contents[2]; 60 | if (!title && !body) { res.status(200).send('Invalid Json.').end(); return; } 61 | 62 | // console.log("deviceToken",deviceToken); 63 | notification.send(deviceToken, title, body, extra); // send! 64 | res.status(200).send('Ok').end(); 65 | }); 66 | 67 | // Start the server 68 | const PORT = process.env.PORT || 8080; 69 | app.listen(PORT, () => { 70 | console.log(`App listening on port ${PORT}`); 71 | console.log('Press Ctrl+C to quit.'); 72 | }); 73 | 74 | module.exports = app; 75 | -------------------------------------------------------------------------------- /ApiServer/key/edch_private_key.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YuigaWada/MissCat/574355469ebbcbe46d03a24c66630aeb5239018f/ApiServer/key/edch_private_key.txt -------------------------------------------------------------------------------- /ApiServer/key/fcm_private_key.json: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YuigaWada/MissCat/574355469ebbcbe46d03a24c66630aeb5239018f/ApiServer/key/fcm_private_key.json -------------------------------------------------------------------------------- /ApiServer/key/keyGenerator.js: -------------------------------------------------------------------------------- 1 | const crypto = require('crypto'); 2 | const util = require('util'); 3 | const urlsafeBase64 = require('urlsafe-base64'); 4 | 5 | const keyCurve = crypto.createECDH('prime256v1'); 6 | keyCurve.generateKeys(); 7 | 8 | console.log("public:", urlsafeBase64.encode(keyCurve.getPublicKey())); 9 | console.log("private:", urlsafeBase64.encode(keyCurve.getPrivateKey())); 10 | console.log("auth:", urlsafeBase64.encode(crypto.randomBytes(16))); 11 | -------------------------------------------------------------------------------- /ApiServer/notification.js: -------------------------------------------------------------------------------- 1 | const fcmNode = require('fcm-node'); 2 | const serverKey = require('./key/fcm_private_key.json'); 3 | const fcm = new fcmNode(serverKey); 4 | 5 | exports.generateContents = function(rawJson, ownerId, lang) { 6 | const json = JSON.parse(rawJson); 7 | const body = json.body; 8 | if (json.type != "notification") { return [null,null]; } 9 | 10 | const type = body.type; 11 | const fromUser = body.user.name != null ? body.user.name : body.user.username; 12 | 13 | var title; 14 | var messages; 15 | var extra = generateExtraContents(body, ownerId); // アプリ内通知で利用するデータ 16 | 17 | // cf. https://github.com/YuigaWada/MissCat/blob/develop/MissCat/Model/Main/NotificationModel.swift 18 | 19 | if (type == "reaction") { 20 | const reaction = body.reaction; 21 | const myNote = body.note.text; 22 | 23 | title = fromUser + "さんがリアクション: \"" + reaction+ "\""; 24 | message = myNote; 25 | } 26 | else if (type == "follow") { 27 | const hostLabel = body.user.host != null ? "@" + body.user.host : ""; // 自インスタンスの場合 host == nullになる 28 | title = ""; 29 | message = "@" + body.user.username + hostLabel + "さんに" + "フォローされました"; 30 | } 31 | else if (type == "reply" || type == "mention") { 32 | title = fromUser + "さんの返信:"; 33 | message = body.note.text; 34 | } 35 | else if (type == "renote" || type == "quote") { 36 | const justRenote = body.note.text == null; // 引用RNでなければ body.note.text == null 37 | var renoteKind = justRenote ? "" : "引用"; 38 | 39 | title = fromUser + "さんが" + renoteKind + "Renoteしました"; 40 | message = justRenote ? body.note.renote.text : body.note.text; 41 | } 42 | else { return [null,null,null]; } 43 | 44 | return [title,message,extra]; 45 | } 46 | 47 | 48 | exports.send = function (token, title, body, extra) { 49 | // cf. https://www.npmjs.com/package/fcm-node 50 | 51 | var message = { 52 | to: token, 53 | 54 | notification: { 55 | title: title, 56 | body: body, 57 | badge: "1" 58 | } 59 | }; 60 | 61 | // アプリ内通知で利用するデータをペイロードに積む 62 | if(extra){ 63 | message.data = extra 64 | } 65 | 66 | fcm.send(message, function(error, response){ 67 | const success = error == null 68 | if (!success) { 69 | console.log("FCM Error:",error); 70 | } 71 | }); 72 | } 73 | 74 | 75 | // アプリ内通知で利用するデータを適切なフォーマットに変換しておく 76 | function generateExtraContents(body, ownerId) { 77 | const id = body.id; 78 | 79 | return { 80 | owner_id: ownerId, // userIdからどのユーザー宛の通知かを識別する 81 | notification_id: id 82 | } 83 | } -------------------------------------------------------------------------------- /ApiServer/test/fcm_token.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YuigaWada/MissCat/574355469ebbcbe46d03a24c66630aeb5239018f/ApiServer/test/fcm_token.txt -------------------------------------------------------------------------------- /ApiServer/test/mock.json: -------------------------------------------------------------------------------- 1 | {"type":"notification","body":{"id":"86x615sq97","createdAt":"2020-05-05T07:38:42.458Z","type":"reaction","userId":"84micuph6b","user":{"id":"84micuph6b","name":null,"username":"Wt","host":"misskey.dev","avatarUrl":"https://misskey.io/avatar/84micuph6b","avatarColor":null,"emojis":[{"name":"misscat","host":"misskey.dev","url":"https://s3.arkjp.net/dev/f311ae70-8f91-4154-be51-4199815bae94.png","aliases":[]}]},"note":{"id":"86wduzuy8w","createdAt":"2020-05-04T18:30:05.578Z","userId":"7ze0f2goa7","user":{"id":"7ze0f2goa7","name":"だわくん:miyano_yay:","username":"wada","host":null,"avatarUrl":"https://s3.arkjp.net/misskey/thumbnail-f340c8e4-8951-495d-b7d9-12fd3151f9b2.jpg","avatarColor":"rgb(165,151,172)","isCat":true,"emojis":[{"name":"miyano_yay","host":null,"url":"https://emoji.arkjp.net/misskey/miyano_yay.png","aliases":["miyano","yay"]}]},"text":"やっと仕様を理解したので明日実験してみよう...","cw":null,"visibility":"public","renoteCount":0,"repliesCount":0,"reactions":{":misscat@misskey.dev:":1},"emojis":[{"name":"misscat@misskey.dev","url":"https://s3.arkjp.net/dev/f311ae70-8f91-4154-be51-4199815bae94.png"}],"fileIds":[],"files":[],"replyId":null,"renoteId":null},"reaction":":misscat@misskey.dev:"}} 2 | -------------------------------------------------------------------------------- /ApiServer/test/mock.text: -------------------------------------------------------------------------------- 1 | B8TZpK9vmRlCbkFTLG6l5gAAEABBBE8bArSb8EH1d8PH0J4Wrf/p2CY2rLUx55TgRayuyH0B3ZjJ2JiMDJH+c2FsA526yd08GVf7QjwqGWnNo4+LwpI4dyaI36CvBMPCf1lOAF50FV7JkgvMGyuYnzgUOh5KSvjDygDpygjRdFYI7amKXXCeRSMcwqn8lXgP1G6CUI43z88+bg/piRVBEnnALn2d60vzUzQNSXUdHAGCQ6aElewpxe3xT74ua0Bxcd4tB3fFaUzpOzYApQRpIlG5ITDTd1haMCuarE5vUGO+oPMsIv1nJO5keRhcvKBWSynj3d0+pGkgajrNjdQentooEQt5GmntKus+mUzLT+UQN1KNWnR3FN9LjsXX8fTk3Vhl7NabiW+N/vDPxI0/lw0VdBNeI460XcWi8aJd6Yb4THB0DjJ5p2JwXHB3zZ1dGSOA6f2hQWTQbVM/9Kisji0SEYdmysFNJaiajax2IeP8eG5lmJ/Lsq6Fs+CCRHnEUZIENEo0Mw3H44Wx/zWG1mEJxffIeuWUFbCcyfD8JYZkvilUu6azhPkTbhidbZ/NQO8oJHU21K5qQbOxiFhPD//cUORE/aCeF8uMdS1PgCvD1I0wfGOxmj3tLOXyvjmTNVzR7jZ7jRMro69/9K2dZ0YrcVMHgZuXfr9OtBD25FoHkkZ6uOxU8rX+FBDt4Af5A3mseX72dUFuhSCYh2akdrhUldvqnjb7IQKQpLrBDh4t4YGmPgXvVs/uSa48MOWNfgWUgng2BReDD7RwT1MeF3KQObAdOaocRZFRWx51JgDv16o7qr5Q/vd5TPIXGKloMgMMwCA5sAsIhmBdBZLjpPrFEspPVJvfGgcu7Hdb/9/xRAOsXBjlcg4iDetdTjkBpr5Gysn1EJfEZZAJaOTD4kgKtKURmynxvtX/nDKLmaIUt1f8Dfo/UKhq+9HcSfoohH/9AcFQCiSPPJrfJFt1BstJi/PhwbmWAMvxwbrzRZXzmXk8iiTEqbnlOqml+dnDZ4aJUjTrCb5OxbfaGsr3kXATcKT8jNKTOK6/0r25NNyGHCXlaF7awdjzQXLamWrsa/ieFmRnrpWKGobW45uNXEwiKmXvDWcVBH5PDeKQpu3IQSdzBXJSpYtrwpwnyyMbI0h+Y8JufjLWFZqNqCpcHG7CiwnKAR3icC3MLbZOS8oxo2AHYfhWXxzuqU5apbuCAzKNtk4N86wEuFLb1XkpI2AJ9u9k8MDBUdRxhxWhgKbQ2EOh4KZeKQDD6olVWR7ECU0otyM1g3+Ej89WOSrqJGhk+S704v1A73+EED0MFaY+MjrIOhCsrgL/Tcu0bbHYjOiuoEFN3Uf75Nr/+qTS6SatkHtqyIi24Y/NWCIZ976946gtym8iWi9aJieCLNYE4IuKjhgIOVrWqo57nbg0/4OQvTubSzBbis3x1X+PrL3IrSKufEs5pzEPuRbyhQfLHJ2gL4v2S0od5v9mxT3XacJAq2rHEd3/GXIxfgwWm/s/aSAIOnxU5DIM71UbVuEKBFtpk0IGt6DzsZiC7lELGUfR/VSv7Hg0d4aMmAsHPmo5r9FDDsALxp2OfL1XMg8/rPnUvRoKjdkY61MHfH7rQoXfy3Yidtx7dHeZ/O21tT/V83HXC6xNXpcGew0wyTjWGBqhVXkRZUjN3EteFniv90gg0SIPW4zzKJ42/uf0tzg76gZiJboAqWwlXmt+FmjnMg4990vA1obltewm6NIRha6EJb4fpafsnA8Hqk/rM7zu57vBLCr22o0VC7gery4xbRMW/es= -------------------------------------------------------------------------------- /ApiServer/test/test_decipher.js: -------------------------------------------------------------------------------- 1 | 2 | /**** Test: decrypt ****/ 3 | 4 | const webPushDecipher = require('../webPushDecipher.js'); 5 | 6 | body = "B8TZpK9vmRlCbkFTLG6l5gAAEABBBE8bArSb8EH1d8PH0J4Wrf/p2CY2rLUx55TgRayuyH0B3ZjJ2JiMDJH+c2FsA526yd08GVf7QjwqGWnNo4+LwpI4dyaI36CvBMPCf1lOAF50FV7JkgvMGyuYnzgUOh5KSvjDygDpygjRdFYI7amKXXCeRSMcwqn8lXgP1G6CUI43z88+bg/piRVBEnnALn2d60vzUzQNSXUdHAGCQ6aElewpxe3xT74ua0Bxcd4tB3fFaUzpOzYApQRpIlG5ITDTd1haMCuarE5vUGO+oPMsIv1nJO5keRhcvKBWSynj3d0+pGkgajrNjdQentooEQt5GmntKus+mUzLT+UQN1KNWnR3FN9LjsXX8fTk3Vhl7NabiW+N/vDPxI0/lw0VdBNeI460XcWi8aJd6Yb4THB0DjJ5p2JwXHB3zZ1dGSOA6f2hQWTQbVM/9Kisji0SEYdmysFNJaiajax2IeP8eG5lmJ/Lsq6Fs+CCRHnEUZIENEo0Mw3H44Wx/zWG1mEJxffIeuWUFbCcyfD8JYZkvilUu6azhPkTbhidbZ/NQO8oJHU21K5qQbOxiFhPD//cUORE/aCeF8uMdS1PgCvD1I0wfGOxmj3tLOXyvjmTNVzR7jZ7jRMro69/9K2dZ0YrcVMHgZuXfr9OtBD25FoHkkZ6uOxU8rX+FBDt4Af5A3mseX72dUFuhSCYh2akdrhUldvqnjb7IQKQpLrBDh4t4YGmPgXvVs/uSa48MOWNfgWUgng2BReDD7RwT1MeF3KQObAdOaocRZFRWx51JgDv16o7qr5Q/vd5TPIXGKloMgMMwCA5sAsIhmBdBZLjpPrFEspPVJvfGgcu7Hdb/9/xRAOsXBjlcg4iDetdTjkBpr5Gysn1EJfEZZAJaOTD4kgKtKURmynxvtX/nDKLmaIUt1f8Dfo/UKhq+9HcSfoohH/9AcFQCiSPPJrfJFt1BstJi/PhwbmWAMvxwbrzRZXzmXk8iiTEqbnlOqml+dnDZ4aJUjTrCb5OxbfaGsr3kXATcKT8jNKTOK6/0r25NNyGHCXlaF7awdjzQXLamWrsa/ieFmRnrpWKGobW45uNXEwiKmXvDWcVBH5PDeKQpu3IQSdzBXJSpYtrwpwnyyMbI0h+Y8JufjLWFZqNqCpcHG7CiwnKAR3icC3MLbZOS8oxo2AHYfhWXxzuqU5apbuCAzKNtk4N86wEuFLb1XkpI2AJ9u9k8MDBUdRxhxWhgKbQ2EOh4KZeKQDD6olVWR7ECU0otyM1g3+Ej89WOSrqJGhk+S704v1A73+EED0MFaY+MjrIOhCsrgL/Tcu0bbHYjOiuoEFN3Uf75Nr/+qTS6SatkHtqyIi24Y/NWCIZ976946gtym8iWi9aJieCLNYE4IuKjhgIOVrWqo57nbg0/4OQvTubSzBbis3x1X+PrL3IrSKufEs5pzEPuRbyhQfLHJ2gL4v2S0od5v9mxT3XacJAq2rHEd3/GXIxfgwWm/s/aSAIOnxU5DIM71UbVuEKBFtpk0IGt6DzsZiC7lELGUfR/VSv7Hg0d4aMmAsHPmo5r9FDDsALxp2OfL1XMg8/rPnUvRoKjdkY61MHfH7rQoXfy3Yidtx7dHeZ/O21tT/V83HXC6xNXpcGew0wyTjWGBqhVXkRZUjN3EteFniv90gg0SIPW4zzKJ42/uf0tzg76gZiJboAqWwlXmt+FmjnMg4990vA1obltewm6NIRha6EJb4fpafsnA8Hqk/rM7zu57vBLCr22o0VC7gery4xbRMW/es=" 7 | 8 | publicKey = "BJgVD2cj1pNKNR2Ss3U_8e7P9AyoL5kWaxVio5aO16Cvnx-P1r7HH8SRb-h5tuxaydZ1ky3oO0V40s6t_uN1SdA" 9 | privateKey = "ciQ800G-6jyKWf6KKG94g5rCSU_l_rgbHbyHny_UsIM" 10 | authSecret = "43w_wOVYeF9XzyRyZL3O8g" 11 | 12 | reciverKey = webPushDecipher.reciverKeyBuilder(publicKey,privateKey,authSecret) 13 | decrypted = webPushDecipher.decrypt(body,reciverKey,true) 14 | 15 | console.log("\n",decrypted) 16 | -------------------------------------------------------------------------------- /ApiServer/test/test_join.js: -------------------------------------------------------------------------------- 1 | 2 | /** Test: Decrypt → convert json → generate contents → send the notification **/ 3 | 4 | const webPushDecipher = require('../webPushDecipher.js'); 5 | const fs = require('fs'); 6 | const notification = require('../notification.js'); 7 | 8 | publicKey = "BJgVD2cj1pNKNR2Ss3U_8e7P9AyoL5kWaxVio5aO16Cvnx-P1r7HH8SRb-h5tuxaydZ1ky3oO0V40s6t_uN1SdA" 9 | privateKey = "ciQ800G-6jyKWf6KKG94g5rCSU_l_rgbHbyHny_UsIM" 10 | authSecret = "43w_wOVYeF9XzyRyZL3O8g" 11 | 12 | function decrypt(raw) { 13 | const reciverKey = webPushDecipher.reciverKeyBuilder(publicKey,privateKey,authSecret); 14 | var decrypted = webPushDecipher.decrypt(raw,reciverKey,false); 15 | return decrypted; 16 | } 17 | 18 | 19 | const rawBody = fs.readFileSync('./mock.text', 'utf8'); 20 | const lang = "ja"; 21 | 22 | const rawJson = decrypt(rawBody); 23 | const contents = notification.generateContents(rawJson,lang); 24 | 25 | console.log("title:",contents[0]) 26 | console.log("body:",contents[1]) 27 | 28 | const token = fs.readFileSync('./fcm_token.txt', 'utf8'); 29 | notification.send(token, contents[0], contents[1]) 30 | -------------------------------------------------------------------------------- /ApiServer/test/test_notification.js: -------------------------------------------------------------------------------- 1 | 2 | /** Test: generate contents → send the notification **/ 3 | 4 | const fs = require('fs'); 5 | const notification = require('../notification.js'); 6 | 7 | const rawJson = fs.readFileSync('./mock.json', 'utf8'); 8 | const lang = "ja"; 9 | 10 | const contents = notification.generateContents(rawJson,lang); 11 | console.log("title:",contents[0]) 12 | console.log("body:",contents[1]) 13 | 14 | const token = fs.readFileSync('./fcm_token.txt', 'utf8'); 15 | notification.send(token, contents[0], contents[1]) 16 | -------------------------------------------------------------------------------- /Icon/Misscat-transparent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YuigaWada/MissCat/574355469ebbcbe46d03a24c66630aeb5239018f/Icon/Misscat-transparent.png -------------------------------------------------------------------------------- /Icon/MisscatIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YuigaWada/MissCat/574355469ebbcbe46d03a24c66630aeb5239018f/Icon/MisscatIcon.png -------------------------------------------------------------------------------- /Icon/MisscatIcon_1024x1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YuigaWada/MissCat/574355469ebbcbe46d03a24c66630aeb5239018f/Icon/MisscatIcon_1024x1024.png -------------------------------------------------------------------------------- /Icon/MisscatIcon_old.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YuigaWada/MissCat/574355469ebbcbe46d03a24c66630aeb5239018f/Icon/MisscatIcon_old.png -------------------------------------------------------------------------------- /Icon/MisscatIcon_shaped.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YuigaWada/MissCat/574355469ebbcbe46d03a24c66630aeb5239018f/Icon/MisscatIcon_shaped.png -------------------------------------------------------------------------------- /Icon/Sad_Ai.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YuigaWada/MissCat/574355469ebbcbe46d03a24c66630aeb5239018f/Icon/Sad_Ai.png -------------------------------------------------------------------------------- /MissCat.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /MissCat.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /MissCat.xcodeproj/project.xcworkspace/xcuserdata/yuigawada.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YuigaWada/MissCat/574355469ebbcbe46d03a24c66630aeb5239018f/MissCat.xcodeproj/project.xcworkspace/xcuserdata/yuigawada.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /MissCat.xcodeproj/xcshareddata/xcschemes/MissCat.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 42 | 43 | 53 | 55 | 61 | 62 | 63 | 64 | 70 | 72 | 78 | 79 | 80 | 81 | 83 | 84 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /MissCat.xcodeproj/xcuserdata/yuigawada.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | MissCat.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 0 11 | 12 | MissCatShare.xcscheme_^#shared#^_ 13 | 14 | orderHint 15 | 41 16 | 17 | 18 | SuppressBuildableAutocreation 19 | 20 | 8D4A2A11237437BE00503685 21 | 22 | primary 23 | 24 | 25 | 8D4A2A27237437C100503685 26 | 27 | primary 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /MissCat.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /MissCat.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /MissCat.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /MissCat.xcworkspace/xcuserdata/yuigawada.xcuserdatad/IDEFindNavigatorScopes.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /MissCat.xcworkspace/xcuserdata/yuigawada.xcuserdatad/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | BuildLocationStyle 6 | UseAppPreferences 7 | CustomBuildLocationType 8 | RelativeToDerivedData 9 | DerivedDataLocationStyle 10 | Default 11 | IssueFilterStyle 12 | ShowActiveSchemeOnly 13 | LiveSourceIssuesEnabled 14 | 15 | ShowSharedSchemesAutomaticallyEnabled 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /MissCat/Entity/NoteEntity.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NoteEntity.swift 3 | // MissCat 4 | // 5 | // Created by Yuiga Wada on 2020/06/30. 6 | // Copyright © 2020 Yuiga Wada. All rights reserved. 7 | // 8 | 9 | import MisskeyKit 10 | import UIKit 11 | 12 | class NoteEntity { 13 | // MARK: Id 14 | 15 | let noteId: String? 16 | let userId: String 17 | 18 | // MARK: Icon 19 | 20 | let iconImageUrl: String? 21 | var iconImage: UIImage? 22 | let isCat: Bool 23 | 24 | // MARK: Name 25 | 26 | let displayName: String 27 | let username: String 28 | let hostInstance: String 29 | 30 | // MARK: CW 31 | 32 | var hasCw: Bool { return cw != nil } 33 | let cw: String? 34 | 35 | // MARK: Note 36 | 37 | let note: String 38 | var original: NoteModel? 39 | 40 | // MARK: Reactions 41 | 42 | var reactions: [ReactionCount] 43 | var myReaction: String? 44 | 45 | // MARK: Files 46 | 47 | var files: [File] 48 | 49 | // MARK: Poll 50 | 51 | var poll: Poll? 52 | 53 | // MARK: Meta 54 | 55 | var emojis: [EmojiModel]? 56 | let ago: String 57 | let replyCount: Int 58 | let renoteCount: Int 59 | 60 | init(noteId: String? = nil, iconImageUrl: String? = nil, iconImage: UIImage? = nil, isCat: Bool = false, userId: String = "", displayName: String = "", username: String = "", hostInstance: String = "", note: String = "", ago: String = "", replyCount: Int = 0, renoteCount: Int = 0, reactions: [ReactionCount] = [], shapedReactions: [ReactionEntity] = [], myReaction: String? = nil, files: [File] = [], emojis: [EmojiModel]? = nil, commentRNTarget: NoteCell.Model? = nil, original: NoteModel? = nil, onOtherNote: Bool = false, poll: Poll? = nil, cw: String? = nil) { 61 | self.noteId = noteId 62 | self.iconImageUrl = iconImageUrl 63 | self.iconImage = iconImage 64 | self.isCat = isCat 65 | self.userId = userId 66 | self.displayName = displayName 67 | self.username = username 68 | self.hostInstance = hostInstance 69 | self.note = note 70 | self.ago = ago 71 | self.replyCount = replyCount 72 | self.renoteCount = renoteCount 73 | self.reactions = reactions 74 | self.myReaction = myReaction 75 | self.files = files 76 | self.emojis = emojis 77 | self.original = original 78 | self.poll = poll 79 | self.cw = cw 80 | } 81 | } 82 | 83 | extension NoteEntity { 84 | static var mock: NoteEntity { 85 | return NoteEntity(note: "") 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /MissCat/Entity/ReactionEntity.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ReactionEntity.swift 3 | // MissCat 4 | // 5 | // Created by Yuiga Wada on 2020/07/01. 6 | // Copyright © 2020 Yuiga Wada. All rights reserved. 7 | // 8 | 9 | import MisskeyKit 10 | 11 | class ReactionEntity { 12 | var noteId: String 13 | 14 | var url: String? 15 | 16 | var rawEmoji: String? 17 | var emoji: String? 18 | 19 | var isMyReaction: Bool 20 | 21 | var count: String 22 | 23 | init?(from reaction: ReactionCount, with externalEmojis: [EmojiModel?]?, myReaction: String?, noteId: String, owner: SecureUser) { 24 | guard let count = reaction.count, count != "0" else { return nil } 25 | 26 | let rawEmoji = reaction.name ?? "" 27 | let isMyReaction = rawEmoji == myReaction 28 | 29 | guard rawEmoji != "", 30 | let handler = EmojiHandler.getHandler(owner: owner), 31 | let convertedEmojiData = handler.convertEmoji(raw: rawEmoji, external: externalEmojis) 32 | else { 33 | // If being not converted 34 | 35 | self.noteId = noteId 36 | url = nil 37 | self.rawEmoji = rawEmoji 38 | self.isMyReaction = isMyReaction 39 | self.count = count 40 | return 41 | } 42 | 43 | switch convertedEmojiData.type { 44 | case .default: 45 | self.noteId = noteId 46 | url = nil 47 | self.rawEmoji = rawEmoji 48 | emoji = convertedEmojiData.emoji 49 | self.isMyReaction = isMyReaction 50 | self.count = count 51 | case .custom: 52 | self.noteId = noteId 53 | url = convertedEmojiData.emoji 54 | self.rawEmoji = rawEmoji 55 | emoji = convertedEmojiData.emoji 56 | self.isMyReaction = isMyReaction 57 | self.count = count 58 | case .nonColon: 59 | self.noteId = noteId 60 | url = nil 61 | self.rawEmoji = convertedEmojiData.emoji 62 | emoji = convertedEmojiData.emoji 63 | self.isMyReaction = isMyReaction 64 | self.count = count 65 | default: 66 | return nil 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /MissCat/Entity/UrlSummalyEntity.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UrlSummalyEntity.swift 3 | // MissCat 4 | // 5 | // Created by Yuiga Wada on 2020/08/01. 6 | // Copyright © 2020 Yuiga Wada. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class UrlSummalyEntity: Codable { 12 | init(title: String?, icon: String?, description: String?, thumbnail: String?, sitename: String?, sensitive: Bool?, url: String?) { 13 | self.title = title 14 | self.icon = icon 15 | self.description = description 16 | self.thumbnail = thumbnail 17 | self.sitename = sitename 18 | self.sensitive = sensitive 19 | self.url = url 20 | } 21 | 22 | let title: String? 23 | let icon: String? 24 | let description: String? 25 | let thumbnail: String? 26 | let sitename: String? 27 | let sensitive: Bool? 28 | let url: String? 29 | } 30 | -------------------------------------------------------------------------------- /MissCat/Entity/UserEntity.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserEntity.swift 3 | // MissCat 4 | // 5 | // Created by Yuiga Wada on 2020/06/30. 6 | // Copyright © 2020 Yuiga Wada. All rights reserved. 7 | // 8 | 9 | import MisskeyKit 10 | 11 | class UserEntity { 12 | public var userId: String 13 | public var name: String? 14 | public var username: String? 15 | public var description: String? 16 | public var host: String? 17 | public var avatarUrl: String? 18 | public var isCat: Bool? 19 | public var emojis: [String: String]? 20 | public var bannerUrl: String? 21 | public var followersCount, followingCount, notesCount: Int? 22 | 23 | init(id: String, name: String? = nil, username: String? = nil, description: String? = nil, host: String? = nil, avatarUrl: String? = nil, isCat: Bool? = nil, emojis: [String: String]? = nil, bannerUrl: String? = nil, followersCount: Int? = nil, followingCount: Int? = nil, notesCount: Int? = nil) { 24 | userId = id 25 | self.name = name 26 | self.username = username 27 | self.description = description 28 | self.host = host 29 | self.avatarUrl = avatarUrl 30 | self.isCat = isCat 31 | self.emojis = emojis 32 | self.bannerUrl = bannerUrl 33 | self.followersCount = followersCount 34 | self.followingCount = followingCount 35 | self.notesCount = notesCount 36 | } 37 | 38 | init(from user: UserModel) { 39 | userId = user.id 40 | name = user.name 41 | username = user.username 42 | description = user.description 43 | host = user.host 44 | avatarUrl = user.avatarUrl 45 | isCat = user.isCat 46 | // emojis = user.emojis 47 | bannerUrl = user.bannerUrl 48 | followersCount = user.followersCount 49 | followingCount = user.followingCount 50 | notesCount = user.notesCount 51 | } 52 | } 53 | 54 | extension UserEntity { 55 | static var mock: UserEntity { 56 | return UserEntity(id: "") 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /MissCat/GoogleService-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CLIENT_ID 6 | 829232913896-ml8sncp8uftda7jbgjl3p0atv4ffkt2f.apps.googleusercontent.com 7 | REVERSED_CLIENT_ID 8 | com.googleusercontent.apps.829232913896-ml8sncp8uftda7jbgjl3p0atv4ffkt2f 9 | API_KEY 10 | AIzaSyAvc4DNj1FuQivImVTfxW9zHikH_8yFf38 11 | GCM_SENDER_ID 12 | 829232913896 13 | PLIST_VERSION 14 | 1 15 | BUNDLE_ID 16 | yuwd.MissCat 17 | PROJECT_ID 18 | misscat-9c053 19 | STORAGE_BUCKET 20 | misscat-9c053.appspot.com 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 | GOOGLE_APP_ID 32 | 1:829232913896:ios:bb47b5babdbb686b1ed85d 33 | DATABASE_URL 34 | https://misscat-9c053.firebaseio.com 35 | 36 | -------------------------------------------------------------------------------- /MissCat/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSAppleMusicUsageDescription 6 | #nowplaying投稿に必要です 7 | BGTaskSchedulerPermittedIdentifiers 8 | 9 | yuwd.MissCat 10 | 11 | CFBundleDevelopmentRegion 12 | $(DEVELOPMENT_LANGUAGE) 13 | CFBundleExecutable 14 | $(EXECUTABLE_NAME) 15 | CFBundleIdentifier 16 | $(PRODUCT_BUNDLE_IDENTIFIER) 17 | CFBundleInfoDictionaryVersion 18 | 6.0 19 | CFBundleName 20 | $(PRODUCT_NAME) 21 | CFBundlePackageType 22 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 23 | CFBundleShortVersionString 24 | $(MARKETING_VERSION) 25 | CFBundleVersion 26 | $(CURRENT_PROJECT_VERSION) 27 | LSApplicationQueriesSchemes 28 | 29 | twitter 30 | 31 | LSRequiresIPhoneOS 32 | 33 | NSCameraUsageDescription 34 | アップロードする画像を撮影するために必要です。 35 | NSMicrophoneUsageDescription 36 | アップロードする動画を撮影するために必要です。 37 | NSPhotoLibraryAddUsageDescription 38 | アップロードする画像を管理するために必要です。 39 | NSPhotoLibraryUsageDescription 40 | アップロードする画像を選択するために必要です。 41 | UIAppFonts 42 | 43 | Font Awesome 5 Free-Solid-900.otf 44 | Font Awesome 5 Free-Regular-400.otf 45 | Font Awesome 5 Brands-Regular-400.otf 46 | 47 | UIApplicationSceneManifest 48 | 49 | UIApplicationSupportsMultipleScenes 50 | 51 | UISceneConfigurations 52 | 53 | UIWindowSceneSessionRoleApplication 54 | 55 | 56 | UISceneConfigurationName 57 | Default Configuration 58 | UISceneDelegateClassName 59 | $(PRODUCT_MODULE_NAME).SceneDelegate 60 | UISceneStoryboardFile 61 | Main 62 | 63 | 64 | 65 | 66 | UIBackgroundModes 67 | 68 | fetch 69 | processing 70 | remote-notification 71 | 72 | UILaunchStoryboardName 73 | LaunchScreen 74 | UIMainStoryboardFile 75 | Main 76 | UIRequiredDeviceCapabilities 77 | 78 | armv7 79 | 80 | UISupportedInterfaceOrientations 81 | 82 | UIInterfaceOrientationPortrait 83 | UIInterfaceOrientationPortraitUpsideDown 84 | 85 | UISupportedInterfaceOrientations~ipad 86 | 87 | UIInterfaceOrientationPortrait 88 | UIInterfaceOrientationPortraitUpsideDown 89 | UIInterfaceOrientationLandscapeLeft 90 | UIInterfaceOrientationLandscapeRight 91 | 92 | UIUserInterfaceStyle 93 | Light 94 | 95 | 96 | -------------------------------------------------------------------------------- /MissCat/MissCat.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | aps-environment 6 | development 7 | com.apple.security.application-groups 8 | 9 | group.yuwd.MissCat 10 | 11 | keychain-access-groups 12 | 13 | $(AppIdentifierPrefix)yuwd.MissCat 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /MissCat/Model/Details/PostDetailModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PostDetailModel.swift 3 | // MissCat 4 | // 5 | // Created by Yuiga Wada on 2020/03/27. 6 | // Copyright © 2020 Yuiga Wada. All rights reserved. 7 | // 8 | 9 | import MisskeyKit 10 | 11 | class PostDetailModel { 12 | private var backReplies: [NoteCell.Model] = [] 13 | private var replies: [NoteCell.Model] = [] 14 | 15 | private let misskey: MisskeyKit? 16 | private let owner: SecureUser? 17 | init(from misskey: MisskeyKit?, owner: SecureUser?) { 18 | self.misskey = misskey 19 | self.owner = owner 20 | } 21 | 22 | /// リプライを遡る 23 | /// - Parameter note: モデル 24 | func goBackReplies(id: String, completion: @escaping ([NoteCell.Model]) -> Void) { 25 | misskey?.notes.showNote(noteId: id) { note, error in 26 | guard error == nil, 27 | let note = note, 28 | let shaped = note.getNoteCellModel(owner: self.owner) else { completion(self.backReplies); return } 29 | 30 | shaped.isReplyTarget = true 31 | MFMEngine.shapeModel(shaped) 32 | self.backReplies.append(shaped) 33 | 34 | if let replyId = note.replyId { 35 | self.goBackReplies(id: replyId, completion: completion) 36 | } else { 37 | completion(self.backReplies) 38 | } 39 | } 40 | } 41 | 42 | /// リプライを探す 43 | /// - Parameter id: noteId 44 | func getReplies(id: String, completion: @escaping ([NoteCell.Model]) -> Void) { 45 | misskey?.notes.getChildren(noteId: id) { notes, error in 46 | guard let notes = notes, error == nil else { return } 47 | DispatchQueue.global().async { 48 | self.replies = self.convertReplies(notes) 49 | completion(self.replies) 50 | } 51 | } 52 | } 53 | 54 | /// NoteModelをNoteCell.Modelへ 55 | /// - Parameter notes: [NoteModel] 56 | private func convertReplies(_ notes: [NoteModel]) -> [NoteCell.Model] { 57 | return notes.map { 58 | guard let cellModel = $0.getNoteCellModel(owner: self.owner) else { return nil } 59 | MFMEngine.shapeModel(cellModel) 60 | return cellModel 61 | }.compactMap { $0 } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /MissCat/Model/Details/ProfileModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProfileModel.swift 3 | // MissCat 4 | // 5 | // Created by Yuiga Wada on 2019/12/07. 6 | // Copyright © 2019 Yuiga Wada. All rights reserved. 7 | // 8 | 9 | import MisskeyKit 10 | import UIKit 11 | 12 | class ProfileModel { 13 | private let misskey: MisskeyKit? 14 | init(from misskey: MisskeyKit?) { 15 | self.misskey = misskey 16 | } 17 | 18 | func getUser(userId: String, completion: @escaping (UserEntity?) -> Void) { 19 | misskey?.users.showUser(userId: userId) { user, error in 20 | guard error == nil, let user = user else { completion(nil); return } 21 | completion(UserEntity(from: user)) 22 | } 23 | } 24 | 25 | func follow(userId: String, completion: @escaping (Bool) -> Void) { 26 | misskey?.users.follow(userId: userId) { _, _ in 27 | completion(true) 28 | } 29 | } 30 | 31 | func unfollow(userId: String, completion: @escaping (Bool) -> Void) { 32 | misskey?.users.unfollow(userId: userId) { _, _ in 33 | completion(true) 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /MissCat/Model/DirectMessage/DirectMessageModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DirectMessageModel.swift 3 | // MissCat 4 | // 5 | // Created by Yuiga Wada on 2020/04/16. 6 | // Copyright © 2020 Yuiga Wada. All rights reserved. 7 | // 8 | 9 | import MisskeyKit 10 | import RxCocoa 11 | import RxSwift 12 | 13 | class DirectMessageModel { 14 | struct LoadOption { 15 | let userId: String 16 | let untilId: String? 17 | } 18 | 19 | private let misskey: MisskeyKit? 20 | init(from misskey: MisskeyKit?) { 21 | self.misskey = misskey 22 | } 23 | 24 | private func transformModel(with observer: AnyObserver, message: MessageModel) { 25 | let user: DirectMessage.User = .init(senderId: message.userId ?? "", 26 | displayName: message.user?.username ?? "", 27 | iconUrl: message.user?.avatarUrl ?? "") 28 | 29 | let transformed: DirectMessage = .init(text: message.text ?? "", 30 | user: user, 31 | messageId: message.id ?? UUID().uuidString, 32 | date: message.createdAt?.date ?? .init()) 33 | 34 | transformed.changeReadStatus(read: message.isRead ?? false) 35 | observer.onNext(transformed) 36 | } 37 | 38 | func load(with option: LoadOption) -> Observable { 39 | let dispose = Disposables.create() 40 | 41 | return Observable.create { [unowned self] observer in 42 | 43 | let handleResult = { (messages: [MessageModel]?, error: MisskeyKitError?) in 44 | guard let messages = messages, error == nil else { 45 | if let error = error { observer.onError(error) } 46 | print(error ?? "error is nil") 47 | return 48 | } 49 | 50 | DispatchQueue.global().async { 51 | messages.forEach { message in 52 | self.transformModel(with: observer, message: message) 53 | } 54 | observer.onCompleted() 55 | } 56 | } 57 | 58 | self.misskey?.messaging.getMessageWithUser(userId: option.userId, 59 | limit: 40, 60 | untilId: option.untilId ?? "", 61 | markAsRead: true, result: handleResult) 62 | return dispose 63 | } 64 | } 65 | 66 | func send(to userId: String, with text: String) -> Observable { 67 | let dispose = Disposables.create() 68 | 69 | return Observable.create { observer in 70 | self.misskey?.messaging.create(userId: userId, text: text) { sent, error in 71 | guard let sent = sent, error == nil else { 72 | if let error = error { observer.onError(error) } 73 | print(error ?? "error is nil") 74 | return 75 | } 76 | self.transformModel(with: observer, message: sent) 77 | observer.onCompleted() 78 | } 79 | return dispose 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /MissCat/Model/DirectMessage/MessageListModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MessageListModel.swift 3 | // MissCat 4 | // 5 | // Created by Yuiga Wada on 2020/04/16. 6 | // Copyright © 2020 Yuiga Wada. All rights reserved. 7 | // 8 | 9 | import MisskeyKit 10 | import RxCocoa 11 | import RxSwift 12 | 13 | class MessageListModel { 14 | private var misskey: MisskeyKit? 15 | private var owner: SecureUser? 16 | init(from misskey: MisskeyKit?, owner: SecureUser?) { 17 | self.misskey = misskey 18 | self.owner = owner 19 | } 20 | 21 | func change(from misskey: MisskeyKit?, owner: SecureUser?) { 22 | self.misskey = misskey 23 | self.owner = owner 24 | } 25 | 26 | private func transformModel(with observer: AnyObserver, history: MessageHistoryModel) { 27 | let myId = owner?.userId ?? "" 28 | let others = [history.recipient, history.user].compactMap { $0 }.filter { $0.id != myId } // チャット相手 29 | let other = others.count > 0 ? others[0] : history.recipient 30 | let otherEntity = other != nil ? UserEntity(from: other!) : nil 31 | 32 | let sender: SenderCell.Model = .init(isSkelton: false, 33 | userId: other?.id, 34 | icon: other?.avatarUrl, 35 | name: other?.name, 36 | username: other?.username, 37 | latestMessage: history.text, 38 | createdAt: history.createdAt) 39 | 40 | sender.shapedName = MFMEngine.shapeDisplayName(owner: owner, user: otherEntity) 41 | observer.onNext(sender) 42 | } 43 | 44 | func loadHistory() -> Observable { 45 | let dispose = Disposables.create() 46 | 47 | return Observable.create { [unowned self] observer in 48 | 49 | let handleResult = { (lists: [MessageHistoryModel]?, error: MisskeyKitError?) in 50 | guard let lists = lists, error == nil else { 51 | if let error = error { observer.onError(error) } 52 | print(error ?? "error is nil") 53 | return 54 | } 55 | 56 | DispatchQueue.global().async { 57 | lists.forEach { history in 58 | self.transformModel(with: observer, history: history) 59 | } 60 | observer.onCompleted() 61 | } 62 | } 63 | 64 | self.misskey?.messaging.getHistory(result: handleResult) 65 | return dispose 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /MissCat/Model/Main/AccountsListModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AccountsListModel.swift 3 | // MissCat 4 | // 5 | // Created by Yuiga Wada on 2020/06/08. 6 | // Copyright © 2020 Yuiga Wada. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class AccountsListModel { 12 | func getUsers() -> [SecureUser] { 13 | return Cache.UserDefaults.shared.getUsers() 14 | } 15 | 16 | func removeUser(user: SecureUser) { 17 | Cache.UserDefaults.shared.removeUser(userId: user.userId) 18 | } 19 | 20 | /// 削除しようとしているアカウントが紐付けられたタブを削除しておく 21 | /// - Parameter user: SecureUser 22 | func checkTabs(for user: SecureUser) -> Bool { 23 | return Theme.shared.removeUserTabs(for: user) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /MissCat/Model/Main/HomeModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HomeModel.swift 3 | // MissCat 4 | // 5 | // Created by Yuiga Wada on 2020/03/26. 6 | // Copyright © 2020 Yuiga Wada. All rights reserved. 7 | // 8 | 9 | import MisskeyKit 10 | 11 | class HomeModel { 12 | func vote(choice: [Int], to noteId: String, owner: SecureUser) { 13 | guard let misskey = MisskeyKit(from: owner) else { return } 14 | choice.forEach { 15 | misskey.notes.vote(noteId: noteId, choice: $0, result: { _, _ in 16 | // print(error) 17 | }) 18 | } 19 | } 20 | 21 | func renote(noteId: String, owner: SecureUser) { 22 | guard let misskey = MisskeyKit(from: owner) else { return } 23 | misskey.notes.renote(renoteId: noteId) { _, _ in 24 | // print(error) 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /MissCat/Model/Main/SearchModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SearchModel.swift 3 | // MissCat 4 | // 5 | // Created by Yuiga Wada on 2020/04/11. 6 | // Copyright © 2020 Yuiga Wada. All rights reserved. 7 | // 8 | 9 | import MisskeyKit 10 | import RxSwift 11 | 12 | class SearchModel { 13 | private let misskey: MisskeyKit? 14 | init(from misskey: MisskeyKit?) { 15 | self.misskey = misskey 16 | } 17 | 18 | func searchUser(with query: String, sinceId: String = "", untilId: String = "") -> Observable<[UserModel]> { 19 | return Observable.create { observer in 20 | let dispose = Disposables.create() 21 | 22 | self.misskey?.search.user(query: query, limit: 40, sinceId: sinceId, untilId: untilId, detail: true) { users, error in 23 | guard let users = users else { return } 24 | if let error = error { observer.onError(error); return } 25 | 26 | observer.onNext(users) 27 | observer.onCompleted() 28 | } 29 | return dispose 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /MissCat/Model/Reusable/Accounts/AccountCellModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AccountCellModel.swift 3 | // MissCat 4 | // 5 | // Created by Yuiga Wada on 2020/06/08. 6 | // Copyright © 2020 Yuiga Wada. All rights reserved. 7 | // 8 | 9 | import MisskeyKit 10 | import UIKit 11 | 12 | class AccountCellModel { 13 | private let misskey: MisskeyKit? 14 | init(from misskey: MisskeyKit?) { 15 | self.misskey = misskey 16 | } 17 | 18 | func getAccountInfo(completion: @escaping (UserModel?) -> Void) { 19 | misskey?.users.i { me, _ in 20 | completion(me) 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /MissCat/Model/Reusable/NoteCell/NoteCellModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NoteCellModel.swift 3 | // MissCat 4 | // 5 | // Created by Yuiga Wada on 2019/11/19. 6 | // Copyright © 2019 Yuiga Wada. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import MisskeyKit 11 | 12 | class NoteCellModel { 13 | private let misskey: MisskeyKit? 14 | init(from misskey: MisskeyKit?) { 15 | self.misskey = misskey 16 | } 17 | 18 | func registerReaction(noteId: String, reaction: String) { 19 | misskey?.notes.createReaction(noteId: noteId, reaction: reaction) { _, _ in 20 | // print("registerReaction: [result: \(result), error: \(error)]") 21 | } 22 | } 23 | 24 | func cancelReaction(noteId: String) { 25 | misskey?.notes.deleteReaction(noteId: noteId) { _, _ in 26 | // print("cancelReaction: [result: \(result), error: \(error)]") 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /MissCat/Model/Reusable/NoteCell/UrlPreviewerModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UrlPreviewerModel.swift 3 | // MissCat 4 | // 5 | // Created by Yuiga Wada on 2020/04/07. 6 | // Copyright © 2020 Yuiga Wada. All rights reserved. 7 | // 8 | 9 | import SwiftLinkPreview 10 | 11 | class UrlPreviewerModel { 12 | func getPreview(of url: String, instance: String, handler: @escaping (UrlSummalyEntity) -> Void) { 13 | if let cache = Cache.shared.getUrlPreview(on: url) { 14 | DispatchQueue.main.async { handler(cache) } 15 | return 16 | } 17 | 18 | getPreviewWithSummalyProxy(of: url, instance: instance, handler: { summaly in 19 | if let summaly = summaly { // プロキシに存在してたら 20 | handler(summaly) 21 | return 22 | } 23 | 24 | self.getPreviewWithSLP(of: url, instance: instance, handler: { summalyWithSLP in // 存在してない場合はSLPから取得 25 | handler(summalyWithSLP) 26 | }) 27 | 28 | }) 29 | } 30 | 31 | // cf. https://github.com/Kinoshita0623/MisskeyAndroidClient/blob/master/app/src/main/java/jp/panta/misskeyandroidclient/model/api/GetUrlPreview.kt#L32 32 | private func getPreviewWithSummalyProxy(of url: String, instance: String, handler: @escaping (UrlSummalyEntity?) -> Void) { 33 | let proxyUrl = "https://\(instance)/url?url=\(url)" 34 | 35 | Requestor.get(url: proxyUrl, completion: { _, dataString in 36 | guard let dataString = dataString else { handler(nil); return } 37 | let entity = self.decodeJSON(raw: dataString, type: UrlSummalyEntity.self) 38 | handler(entity) 39 | }) 40 | } 41 | 42 | private func getPreviewWithSLP(of url: String, instance: String, handler: @escaping (UrlSummalyEntity) -> Void) { 43 | let slp = SwiftLinkPreview(session: URLSession.shared, 44 | workQueue: DispatchQueue.global(), 45 | responseQueue: DispatchQueue.main) 46 | 47 | slp.preview(url, onSuccess: { res in 48 | // UrlSummalyEntityに詰め替える 49 | let entity = UrlSummalyEntity(title: res.title, 50 | icon: res.icon, 51 | description: res.description, 52 | thumbnail: self.searchImageUrlWithSLP(res), 53 | sitename: res.title, 54 | sensitive: false, 55 | url: url) 56 | 57 | Cache.shared.saveUrlPreview(entity, on: url) 58 | handler(entity) 59 | }, onError: handleError) 60 | } 61 | 62 | private func searchImageUrlWithSLP(_ response: Response) -> String? { 63 | guard let url = response.finalUrl?.absoluteURL.absoluteString else { return response.image } 64 | if url.contains("github.com") { 65 | return response.icon 66 | } 67 | 68 | return response.image 69 | } 70 | 71 | private func decodeJSON(raw rawJson: String, type: T.Type) -> T? where T: Decodable { 72 | guard rawJson.count > 0 else { return nil } 73 | 74 | do { 75 | return try JSONDecoder().decode(type, from: rawJson.data(using: .utf8)!) 76 | } catch { 77 | print(error) 78 | return nil 79 | } 80 | } 81 | 82 | private func handleError(_ error: PreviewError) {} 83 | } 84 | -------------------------------------------------------------------------------- /MissCat/Model/Reusable/Notification/NotificationBannerModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NotificationBannerModel.swift 3 | // MissCat 4 | // 5 | // Created by Yuiga Wada on 2020/07/04. 6 | // Copyright © 2020 Yuiga Wada. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class NotificationBannerModel: NotificationsModel { 12 | func getModel(with contents: NotificationCell.CustomModel) -> NotificationCell.Model { 13 | let id = UUID().uuidString 14 | return .init(notificationId: id, custom: contents) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /MissCat/Model/Reusable/Notification/NotificationCellModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NotificationCellModel.swift 3 | // MissCat 4 | // 5 | // Created by Yuiga Wada on 2019/11/24. 6 | // Copyright © 2019 Yuiga Wada. All rights reserved. 7 | // 8 | 9 | import MisskeyKit 10 | 11 | class NotificationCellModel { 12 | func shapeNote(note: String, isReply: Bool) -> NSAttributedString? { 13 | let replyHeader: NSMutableAttributedString = isReply ? .getReplyMark() : .init() // リプライの場合は先頭にreplyマークつける 14 | let body = MFMEngine.generatePlaneString(string: note, font: UIFont(name: "Helvetica", size: 11.0)) 15 | 16 | return replyHeader + body 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /MissCat/Model/Reusable/Tab/NavBarModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NavBarModel.swift 3 | // MissCat 4 | // 5 | // Created by Yuiga Wada on 2020/06/10. 6 | // Copyright © 2020 Yuiga Wada. All rights reserved. 7 | // 8 | 9 | import MisskeyKit 10 | 11 | class NavBarModel { 12 | func getIconImage(from misskey: MisskeyKit, completion: @escaping (UIImage) -> Void) { 13 | misskey.users.i { userInfo, _ in 14 | _ = userInfo?.avatarUrl?.toUIImage { 15 | guard let image = $0 else { return } 16 | completion(image) 17 | } 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /MissCat/Model/Reusable/User/UserCellModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserCellModel.swift 3 | // MissCat 4 | // 5 | // Created by Yuiga Wada on 2020/04/13. 6 | // Copyright © 2020 Yuiga Wada. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | -------------------------------------------------------------------------------- /MissCat/Model/Reusable/User/UserListModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserListModel.swift 3 | // MissCat 4 | // 5 | // Created by Yuiga Wada on 2020/04/13. 6 | // Copyright © 2020 Yuiga Wada. All rights reserved. 7 | // 8 | 9 | import MisskeyKit 10 | import RxCocoa 11 | import RxSwift 12 | 13 | enum UserListType { 14 | case follow 15 | case follower 16 | case list 17 | case search 18 | } 19 | 20 | class UserListModel { 21 | struct LoadOption { 22 | let type: UserListType 23 | let userId: String? 24 | let query: String? 25 | let listId: String? 26 | let untilId: String? 27 | let loadLimit: Int = 40 28 | } 29 | 30 | private let misskey: MisskeyKit? 31 | private let owner: SecureUser? 32 | init(from misskey: MisskeyKit?, owner: SecureUser?) { 33 | self.misskey = misskey 34 | self.owner = owner 35 | } 36 | 37 | private func transformUser(with observer: AnyObserver, user: UserEntity, reverse: Bool) { 38 | let userModel = UserCell.Model(user: user) 39 | 40 | userModel.shapedName = MFMEngine.shapeDisplayName(owner: owner, user: user) 41 | userModel.shapedDescritpion = MFMEngine.shapeString(owner: owner, 42 | needReplyMark: false, 43 | text: user.description?.mfmPreTransform() ?? "自己紹介文はありません", 44 | emojis: user.emojis?.compactMap { EmojiModel(id: "", aliases: [], name: $0.key, url: $0.value, uri: $0.value, category: "") }) 45 | 46 | observer.onNext(userModel) 47 | } 48 | 49 | func loadUsers(with option: LoadOption) -> Observable { 50 | let dispose = Disposables.create() 51 | 52 | return Observable.create { [unowned self] observer in 53 | 54 | let handleResult = { (posts: [UserModel]?, error: MisskeyKitError?) in 55 | guard let posts = posts, error == nil else { 56 | if let error = error { observer.onError(error) } 57 | print(error ?? "error is nil") 58 | return 59 | } 60 | 61 | DispatchQueue.global().async { 62 | posts.forEach { user in 63 | self.transformUser(with: observer, user: UserEntity(from: user), reverse: false) 64 | } 65 | observer.onCompleted() 66 | } 67 | } 68 | 69 | switch option.type { 70 | case .search: 71 | guard let query = option.query else { return dispose } 72 | self.misskey?.search.user(query: query, 73 | limit: option.loadLimit, 74 | untilId: option.untilId ?? "", // TODO: ここOffsetにすべき 75 | localOnly: false, 76 | result: handleResult) 77 | default: 78 | break 79 | } 80 | 81 | return dispose 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /MissCat/Model/Settings/ProfileSettingsModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProfileSettingsModel.swift 3 | // MissCat 4 | // 5 | // Created by Yuiga Wada on 2020/05/15. 6 | // Copyright © 2020 Yuiga Wada. All rights reserved. 7 | // 8 | 9 | import MisskeyKit 10 | import RxSwift 11 | 12 | class ProfileSettingsModel { 13 | private let misskey: MisskeyKit? 14 | init(from misskey: MisskeyKit?) { 15 | self.misskey = misskey 16 | } 17 | 18 | /// プロフィールの差分をMisskeyへ伝達する 19 | /// - Parameter diff: 差分 20 | func save(diff: ChangedProfile) { 21 | guard diff.hasChanged else { return } 22 | uploadImage(diff.banner) { bannerId in 23 | self.uploadImage(diff.icon) { avatarId in 24 | self.updateProfile(with: diff, avatarId: avatarId, bannerId: bannerId) 25 | } 26 | } 27 | } 28 | 29 | /// "i/update"を叩く 30 | private func updateProfile(with diff: ChangedProfile, avatarId: String?, bannerId: String?) { 31 | misskey?.users.updateMyAccount(name: diff.name ?? "", description: diff.description ?? "", avatarId: avatarId ?? "", bannerId: bannerId ?? "", isCat: diff.isCat ?? nil) { res, error in 32 | guard let res = res, error == nil else { return } 33 | print(res) 34 | } 35 | } 36 | 37 | /// 画像をdriveへとアップロードする 38 | private func uploadImage(_ image: UIImage?, completion: @escaping (String?) -> Void) { 39 | guard let image = image, 40 | let resizedImage = image.resized(widthUnder: 1024), 41 | let targetImage = resizedImage.jpegData(compressionQuality: 0.5) else { completion(nil); return } 42 | 43 | misskey?.drive.createFile(fileData: targetImage, fileType: "image/jpeg", name: UUID().uuidString + ".jpeg", force: false) { result, error in 44 | guard let result = result, error == nil else { return } 45 | completion(result.id) 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /MissCat/Others/App/ApiKeyManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ApiKeyManager.swift 3 | // MissCat 4 | // 5 | // Created by Yuiga Wada on 2020/05/12. 6 | // Copyright © 2020 Yuiga Wada. All rights reserved. 7 | // 8 | 9 | struct ApiKeyManager: ApiKeyManagerProtocol { var baseUrl = "" } 10 | -------------------------------------------------------------------------------- /MissCat/Others/App/Bridge.h: -------------------------------------------------------------------------------- 1 | // 2 | // Bridge.h 3 | // MissCat 4 | // 5 | // Created by Yuiga Wada on 2020/03/20. 6 | // Copyright © 2020 Yuiga Wada. All rights reserved. 7 | // 8 | 9 | 10 | -------------------------------------------------------------------------------- /MissCat/Others/App/MisscatApi.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MisscatApi.swift 3 | // MissCat 4 | // 5 | // Created by Yuiga Wada on 2020/05/12. 6 | // Copyright © 2020 Yuiga Wada. All rights reserved. 7 | // 8 | 9 | import FirebaseMessaging 10 | import MisskeyKit 11 | 12 | protocol ApiKeyManagerProtocol { 13 | var baseUrl: String { get } 14 | } 15 | 16 | struct MockApiKeyManager: ApiKeyManagerProtocol { 17 | var baseUrl = "" 18 | } 19 | 20 | class MisscatApi { 21 | private let misskey: MisskeyKit? 22 | private let apiKeyManager: ApiKeyManagerProtocol 23 | private let authSecret = "Q8Zgu-WDvN5EDT_emFGovQ" 24 | private let publicKey = "BJNAJpIOIJnXVVgCTAd4geduXEsNKre0XVvz0j-E_z-8CbGI6VaRPsVI7r-hF88MijMBZApurU2HmSNQ4e-cTmA" 25 | private var baseApiUrl: String { 26 | return apiKeyManager.baseUrl 27 | } 28 | 29 | static var name2emojis: [String: EmojiModel] = [:] 30 | static var emojis: [EmojiModel] = [] 31 | 32 | init(apiKeyManager: ApiKeyManagerProtocol, and user: SecureUser) { 33 | self.apiKeyManager = apiKeyManager 34 | misskey = MisskeyKit(from: user) 35 | fetchEmojis() 36 | } 37 | 38 | /// 適切なendpointを生成し、sw/registerを叩く 39 | func registerSw() { 40 | guard let currentUser = Cache.UserDefaults.shared.getCurrentUser(), 41 | let apiKey = currentUser.apiKey, 42 | !baseApiUrl.isEmpty, 43 | !apiKey.isEmpty, 44 | !currentUser.userId.isEmpty else { return } 45 | 46 | misskey?.auth.setAPIKey(apiKey) 47 | Messaging.messaging().token { token, error in 48 | guard error == nil else { print("Error fetching remote instance ID: \(error!)"); return } 49 | if let token = token { 50 | self.registerSw(userId: currentUser.userId, token: token) 51 | } 52 | } 53 | } 54 | 55 | private func fetchEmojis() { 56 | misskey?.notes.getEmojis { emojis, _ in 57 | MisscatApi.emojis = emojis ?? [] 58 | MisscatApi.emojis.forEach { emoji in 59 | if let name = emoji.name { 60 | MisscatApi.name2emojis[name] = emoji 61 | } 62 | } 63 | } 64 | } 65 | 66 | private func registerSw(userId: String, token: String) { 67 | let endpoint = generateEndpoint(with: userId, and: token) 68 | 69 | misskey?.serviceWorker.register(endpoint: endpoint, auth: authSecret, publicKey: publicKey, result: { state, error in 70 | guard error == nil, let state = state else { return } 71 | print(state) 72 | }) 73 | } 74 | 75 | private func generateEndpoint(with userId: String, and token: String) -> String { 76 | let currentLang = Locale.current.languageCode?.description ?? "ja" 77 | let endpoint = "\(baseApiUrl)/api/v1/push/\(currentLang)/\(userId)/\(token)" 78 | 79 | return endpoint 80 | } 81 | } 82 | 83 | public extension EmojiModel { 84 | static func convert(from dict: [String: String]?) -> [EmojiModel]? { 85 | let emojis = dict?.compactMap { EmojiModel(id: "", aliases: [], name: $0.key, url: $0.value, uri: $0.value, category: "other-instance") } ?? [] 86 | MisscatApi.emojis += emojis 87 | emojis.forEach { emoji in 88 | if let name = emoji.name { 89 | MisscatApi.name2emojis[name] = emoji 90 | } 91 | } 92 | return MisscatApi.emojis 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /MissCat/Others/App/SceneDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SceneDelegate.swift 3 | // MissCat 4 | // 5 | // Created by Yuiga Wada on 2019/11/07. 6 | // Copyright © 2019 Yuiga Wada. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class SceneDelegate: UIResponder, UIWindowSceneDelegate { 12 | var window: UIWindow? 13 | 14 | @available(iOS 13.0, *) 15 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { 16 | // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. 17 | // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. 18 | // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). 19 | guard let _ = (scene as? UIWindowScene) else { return } 20 | } 21 | 22 | @available(iOS 13.0, *) 23 | func sceneDidDisconnect(_ scene: UIScene) { 24 | // Called as the scene is being released by the system. 25 | // This occurs shortly after the scene enters the background, or when its session is discarded. 26 | // Release any resources associated with this scene that can be re-created the next time the scene connects. 27 | // The scene may re-connect later, as its session was not neccessarily discarded (see `application:didDiscardSceneSessions` instead). 28 | } 29 | 30 | @available(iOS 13.0, *) 31 | func sceneDidBecomeActive(_ scene: UIScene) { 32 | // Called when the scene has moved from an inactive state to an active state. 33 | // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. 34 | UIApplication.shared.applicationIconBadgeNumber = 0 // アプリ開いたらバッジを削除する 35 | } 36 | 37 | @available(iOS 13.0, *) 38 | func sceneWillResignActive(_ scene: UIScene) { 39 | // Called when the scene will move from an active state to an inactive state. 40 | // This may occur due to temporary interruptions (ex. an incoming phone call). 41 | } 42 | 43 | @available(iOS 13.0, *) 44 | func sceneWillEnterForeground(_ scene: UIScene) { 45 | // Called as the scene transitions from the background to the foreground. 46 | // Use this method to undo the changes made on entering the background. 47 | } 48 | 49 | @available(iOS 13.0, *) 50 | func sceneDidEnterBackground(_ scene: UIScene) { 51 | // Called as the scene transitions from the foreground to the background. 52 | // Use this method to save data, release shared resources, and store enough scene-specific state information 53 | // to restore the scene back to its current state. 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /MissCat/Others/App/SecureUser.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SecureUser.swift 3 | // MissCat 4 | // 5 | // Created by Yuiga Wada on 2020/06/11. 6 | // Copyright © 2020 Yuiga Wada. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class SecureUser: Codable { 12 | let userId: String 13 | let instance: String 14 | let username: String 15 | var apiKey: String? 16 | 17 | init(userId: String, username: String, instance: String, apiKey: String?) { 18 | self.userId = userId 19 | self.username = username 20 | self.instance = instance 21 | self.apiKey = apiKey 22 | } 23 | } 24 | 25 | extension SecureUser {} 26 | -------------------------------------------------------------------------------- /MissCat/Others/Extension/AVKit+MissCat.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AVKit+MissCat.swift 3 | // MissCat 4 | // 5 | // Created by Yuiga Wada on 2020/03/06. 6 | // Copyright © 2020 Yuiga Wada. All rights reserved. 7 | // 8 | 9 | import AVKit 10 | 11 | extension AVAsset { 12 | /// mov→mp4へと変換する 13 | /// - Parameters: 14 | /// - videoUrl: 動画のurl 15 | /// - completion: completion 16 | static func convert2Mp4(videoUrl: NSURL, completion: @escaping (_ session: AVAssetExportSession) -> Void) { 17 | let documentsPath = NSSearchPathForDirectoriesInDomains(.documentationDirectory, .userDomainMask, true)[0] as String 18 | let fileName = UUID().uuidString + ".mp4" 19 | let tempPath = documentsPath + fileName 20 | 21 | guard let tempUrl = (NSURL.fileURL(withPath: tempPath) as NSURL).absoluteURL else { return } // 一時的にデータを保存 22 | do { 23 | try FileManager.default.removeItem(at: tempUrl) // ファイルがすでに存在していれば削除 24 | } catch {} 25 | 26 | guard let exportSession = AVAssetExportSession(asset: AVURLAsset(url: videoUrl as URL, options: nil), 27 | presetName: AVAssetExportPresetPassthrough) else { return } 28 | 29 | exportSession.outputURL = tempUrl 30 | exportSession.outputFileType = AVFileType.mp4 31 | exportSession.exportAsynchronously(completionHandler: { () -> Void in 32 | completion(exportSession) 33 | }) 34 | } 35 | 36 | /// 動画からサムネイルを取得する 37 | /// - Parameter url: 動画のPath 38 | static func generateThumbnail(videoFrom url: URL) -> UIImage { 39 | let asset = AVAsset(url: url) 40 | let imageGenerator = AVAssetImageGenerator(asset: asset) 41 | imageGenerator.appliesPreferredTrackTransform = true 42 | var time = asset.duration 43 | time.value = min(time.value, 2) 44 | do { 45 | let imageRef = try imageGenerator.copyCGImage(at: time, actualTime: nil) 46 | return UIImage(cgImage: imageRef) 47 | } catch { 48 | return .init() 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /MissCat/Others/Extension/Double+MissCat.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Double+MissCat.swift 3 | // MissCat 4 | // 5 | // Created by Yuiga Wada on 2019/11/17. 6 | // Copyright © 2019 Yuiga Wada. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension Double { 12 | func toAgo() -> String? { 13 | let endingDate = Date() 14 | let startingDate = endingDate.addingTimeInterval(-self) 15 | let calendar = Calendar.current 16 | 17 | let componentsNow = calendar.dateComponents([.month, .weekday, .day, .hour, .minute, .second], from: startingDate, to: endingDate) 18 | if let month = componentsNow.month, let weekday = componentsNow.weekday, let day = componentsNow.day, let hour = componentsNow.hour, let minute = componentsNow.minute, let seconds = componentsNow.second { 19 | if month >= 6 { 20 | return nil 21 | } else if month != 0 { 22 | return "\(month)mo" 23 | } 24 | if weekday != 0 { 25 | return "\(weekday)w" 26 | } else if day != 0 { 27 | return "\(day)d" 28 | } else if hour != 0 { 29 | return "\(hour)h" 30 | } else if minute != 0 { 31 | return "\(minute)m" 32 | } else if seconds != 0 { 33 | return "\(seconds)s" 34 | } else { // if seconds == 0 35 | return "now" 36 | } 37 | } 38 | 39 | return nil 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /MissCat/Others/Extension/Error+MissCat.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Error+MissCat.swift 3 | // MissCat 4 | // 5 | // Created by Yuiga Wada on 2020/04/06. 6 | // Copyright © 2020 Yuiga Wada. All rights reserved. 7 | // 8 | 9 | import MisskeyKit 10 | 11 | extension Error { 12 | var description: String { 13 | let error = self as? MisskeyKitError 14 | return error?.description ?? localizedDescription 15 | } 16 | } 17 | 18 | extension MisskeyKitError { 19 | var errorMessage: String { 20 | switch self { 21 | case .ClientError: 22 | return "ClientError" 23 | case .AuthenticationError: 24 | return "認証AuthenticationError" 25 | case .ForbiddonError: 26 | return "ForbiddonError" 27 | case .ImAI: 28 | return "ImAI" 29 | case .TooManyError: 30 | return "TooManyError" 31 | case .InternalServerError: 32 | return "InternalServerError" 33 | case .CannotConnectStream: 34 | return "CannotConnectStream" 35 | case .NoStreamConnection: 36 | return "NoStreamConnection" 37 | case .FailedToDecodeJson: 38 | return "FailedToDecodeJson" 39 | case .FailedToCommunicateWithServer: 40 | return "FailedToCommunicateWithServer" 41 | case .UnknownTypeResponse: 42 | return "UnknownTypeResponse" 43 | case .ResponseIsNull: 44 | return "ResponseIsNull" 45 | } 46 | } 47 | 48 | var description: String { 49 | switch self { 50 | case .ClientError: 51 | return "サーバー側の問題が発生しました" 52 | case .AuthenticationError: 53 | return "認証エラーが発生しました" 54 | case .ForbiddonError: 55 | return "アカウント連携が解除されている可能性があります" 56 | case .ImAI: 57 | return "ImAI" 58 | case .TooManyError: 59 | return "TooManyError" 60 | case .InternalServerError: 61 | return "サーバー側の問題が発生しました" 62 | case .CannotConnectStream: 63 | return "Streaming接続に失敗しました" 64 | case .NoStreamConnection: 65 | return "NoStreamConnection" 66 | case .FailedToDecodeJson: 67 | return "データ処理に失敗しました" 68 | case .FailedToCommunicateWithServer: 69 | return "サーバーとの通信に失敗しました" 70 | case .UnknownTypeResponse: 71 | return "不明なレスポンス" 72 | case .ResponseIsNull: 73 | return "レスポンスが空です" 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /MissCat/Others/Extension/MisskeyKit+MissCat.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MisskeyKit+MissCat.swift 3 | // MissCat 4 | // 5 | // Created by Yuiga Wada on 2020/06/05. 6 | // Copyright © 2020 Yuiga Wada. All rights reserved. 7 | // 8 | 9 | import MisskeyKit 10 | 11 | extension MisskeyKit { 12 | convenience init(instance: String, apiKey: String) { 13 | self.init() 14 | changeInstance(instance: instance) 15 | auth.setAPIKey(apiKey) 16 | } 17 | 18 | convenience init?(from user: SecureUser) { 19 | guard let apiKey = user.apiKey else { return nil } 20 | self.init(instance: user.instance, apiKey: apiKey) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /MissCat/Others/Extension/NSMutableAttributedString+MissCat.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSMutableAttributedString+MissCat.swift 3 | // MissCat 4 | // 5 | // Created by Yuiga Wada on 2019/11/24. 6 | // Copyright © 2019 Yuiga Wada. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension NSMutableAttributedString { 12 | static func getReplyMark() -> NSMutableAttributedString { 13 | let attribute: [NSAttributedString.Key: Any] = [.font: UIFont.awesomeSolid(fontSize: 15.0) ?? UIFont.systemFont(ofSize: 15.0), 14 | .foregroundColor: UIColor.lightGray] 15 | let replyMark = NSMutableAttributedString(string: "reply ", attributes: attribute) 16 | 17 | return replyMark 18 | } 19 | } 20 | 21 | extension NSAttributedString { 22 | static func + (left: NSAttributedString, right: NSAttributedString) -> NSAttributedString { 23 | let result = NSMutableAttributedString() 24 | result.append(left) 25 | result.append(right) 26 | return result 27 | } 28 | 29 | func changeColor(to color: UIColor) -> NSAttributedString { 30 | let result = NSMutableAttributedString() 31 | result.append(self) 32 | 33 | let range = NSRange(location: 0, length: string.count) 34 | result.addAttribute(NSAttributedString.Key.foregroundColor, value: color, range: range) 35 | 36 | return result 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /MissCat/Others/Extension/NoteModel+MissCat.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NoteModel+MissCat.swift 3 | // MissCat 4 | // 5 | // Created by Yuiga Wada on 2019/11/25. 6 | // Copyright © 2019 Yuiga Wada. All rights reserved. 7 | // 8 | 9 | import MisskeyKit 10 | 11 | extension NoteModel { 12 | fileprivate var post: NoteModel { return self } 13 | 14 | var isFeatured: Bool { return post._featuredId_ != nil } 15 | var isPr: Bool { return post._prId_ != nil } 16 | /// おすすめノートかどうか 17 | var isRecommended: Bool { return post._featuredId_ != nil || post._prId_ != nil } 18 | 19 | /// NoteModelをNoteCell.Modelに変換する 20 | /// - Parameters: 21 | /// - withRN: 引用RNかどうか 22 | /// - onOtherNote: 何らかの形で、別の投稿の上に載ってる投稿か 23 | func getNoteCellModel(owner: SecureUser?, withRN: Bool = false, onOtherNote: Bool = false) -> NoteCell.Model? { 24 | guard let user = post.user else { return nil } 25 | 26 | let displayName = (user.name ?? "") == "" ? user.username : user.name // user.nameがnilか""ならusernameで代替 27 | let emojis = (EmojiModel.convert(from: post.emojis) ?? []) + (EmojiModel.convert(from: user.emojis) ?? []) // 絵文字情報を統合する 28 | 29 | let entity = NoteEntity(noteId: post.id, 30 | iconImageUrl: user.avatarUrl, 31 | isCat: user.isCat ?? false, 32 | userId: user.id, 33 | displayName: displayName ?? "", 34 | username: user.username ?? "", 35 | hostInstance: user.host ?? owner?.instance ?? "", // 同じインスタンスのユーザーはhostがnilになるので追加しておく 36 | note: post.text?.mfmPreTransform() ?? "", // MFMEngineを通して加工の前処理をしておく 37 | ago: post.createdAt!, 38 | replyCount: post.repliesCount ?? 0, 39 | renoteCount: post.renoteCount ?? 0, 40 | reactions: post.reactions?.compactMap { $0 } ?? [], 41 | shapedReactions: [], 42 | myReaction: post.myReaction, 43 | files: post.files?.compactMap { $0 } ?? [], 44 | emojis: emojis.compactMap { $0 }, 45 | original: self, 46 | onOtherNote: onOtherNote, 47 | poll: post.poll, 48 | cw: post.cw) 49 | 50 | let commentRNTarget = withRN ? post.renote?.getNoteCellModel(owner: owner, onOtherNote: true) ?? nil : nil 51 | let cellModel = NoteCell.Model(owner: owner, 52 | entity: entity, 53 | commentRNTarget: commentRNTarget) 54 | 55 | cellModel.shapedReactions = cellModel.getReactions(with: emojis) // ココ 56 | cellModel.isReply = post.reply != nil 57 | return cellModel 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /MissCat/Others/Extension/UIColor+MissCat.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIColor+MissCat.swift 3 | // MissCat 4 | // 5 | // Created by Yuiga Wada on 2019/11/19. 6 | // Copyright © 2019 Yuiga Wada. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension UIColor { 12 | convenience init(hex: String, alpha: CGFloat) { 13 | let v = hex.map { String($0) } + Array(repeating: "0", count: max(6 - hex.count, 0)) 14 | let r = CGFloat(Int(v[0] + v[1], radix: 16) ?? 0) / 255.0 15 | let g = CGFloat(Int(v[2] + v[3], radix: 16) ?? 0) / 255.0 16 | let b = CGFloat(Int(v[4] + v[5], radix: 16) ?? 0) / 255.0 17 | self.init(red: r, green: g, blue: b, alpha: min(max(alpha, 0), 1)) 18 | } 19 | 20 | convenience init(hex: String) { 21 | self.init(hex: hex, alpha: 1.0) 22 | } 23 | 24 | func toUInt32() -> UInt32 { 25 | let color = self 26 | // read colors to CGFloats and convert and position to proper bit positions in UInt32 27 | var red: CGFloat = 0, green: CGFloat = 0, blue: CGFloat = 0, alpha: CGFloat = 0 28 | if color.getRed(&red, green: &green, blue: &blue, alpha: &alpha) { 29 | var colorAsUInt: UInt32 = 0 30 | 31 | colorAsUInt += UInt32(red * 255.0) << 16 + 32 | UInt32(green * 255.0) << 8 + 33 | UInt32(blue * 255.0) 34 | 35 | return colorAsUInt 36 | } 37 | return 0 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /MissCat/Others/Extension/UIFont+MissCat.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIFont+MissCat.swift 3 | // MissCat 4 | // 5 | // Created by Yuiga Wada on 2019/12/02. 6 | // Copyright © 2019 Yuiga Wada. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension UIFont { 12 | static func awesomeSolid(fontSize: CGFloat) -> UIFont? { 13 | return UIFont(name: "FontAwesome5Free-Solid", size: fontSize) 14 | } 15 | 16 | static func awesomeRegular(fontSize: CGFloat) -> UIFont? { 17 | return UIFont(name: "FontAwesome5Free-Regular", size: fontSize) 18 | } 19 | 20 | static func awesomeBrand(fontSize: CGFloat) -> UIFont? { 21 | return UIFont(name: "FontAwesome5Brands-Regular", size: fontSize) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /MissCat/Others/Extension/UIImage+MissCat.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIImage+MissCat.swift 3 | // MissCat 4 | // 5 | // Created by Yuiga Wada on 2019/11/28. 6 | // Copyright © 2019 Yuiga Wada. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | // cf. https://stackoverflow.com/a/46181337 12 | // Thanks for Tung Fam 13 | extension UIImage { 14 | func resized(widthUnder: CGFloat) -> UIImage? { 15 | return resized(withPercentage: widthUnder / size.width) 16 | } 17 | 18 | func resized(withPercentage percentage: CGFloat) -> UIImage? { 19 | let canvasSize = CGSize(width: size.width * percentage, height: size.height * percentage) 20 | UIGraphicsBeginImageContextWithOptions(canvasSize, false, scale) 21 | defer { UIGraphicsEndImageContext() } 22 | draw(in: CGRect(origin: .zero, size: canvasSize)) 23 | return UIGraphicsGetImageFromCurrentImageContext() 24 | } 25 | 26 | func resizedTo5MB() -> UIImage? { 27 | guard let imageData = pngData() else { return nil } 28 | 29 | var resizingImage = self 30 | var imageSizeKB = Double(imageData.count) / 1000.0 // ! Or devide for 1024 if you need KB but not kB 31 | 32 | while imageSizeKB > 5000 { // ! Or use 1024 if you need KB but not kB 33 | guard let resizedImage = resizingImage.resized(withPercentage: 0.9), 34 | let imageData = resizedImage.pngData() 35 | else { return nil } 36 | 37 | resizingImage = resizedImage 38 | imageSizeKB = Double(imageData.count) / 1000.0 // ! Or devide for 1024 if you need KB but not kB 39 | } 40 | 41 | return resizingImage 42 | } 43 | } 44 | 45 | extension UIImage { 46 | // UIImageに対して適切な文字色を返す 47 | var opticalTextColor: UIColor { 48 | let ciColor = CIColor(color: averageColor) 49 | 50 | let red = ciColor.red * 255 51 | let green = ciColor.green * 255 52 | let blue = ciColor.blue * 255 53 | 54 | let target = red * 0.299 + green * 0.587 + blue * 0.114 55 | let threshold: CGFloat = 186 56 | 57 | if target < threshold / 2 { 58 | return .white 59 | } else if target < threshold { 60 | return .lightGray 61 | } else { 62 | return .black 63 | } 64 | } 65 | 66 | private var averageColor: UIColor { 67 | let rawImageRef: CGImage = cgImage! 68 | let data: CFData = rawImageRef.dataProvider!.data! 69 | let rawPixelData = CFDataGetBytePtr(data) 70 | 71 | let imageHeight = rawImageRef.height 72 | let imageWidth = rawImageRef.width 73 | let bytesPerRow = rawImageRef.bytesPerRow 74 | let stride = rawImageRef.bitsPerPixel / 6 75 | 76 | var red = 0 77 | var green = 0 78 | var blue = 0 79 | 80 | for row in 0 ... imageHeight { 81 | var rowPtr = rawPixelData! + bytesPerRow * row 82 | for _ in 0 ... imageWidth { 83 | red += Int(rowPtr[0]) 84 | green += Int(rowPtr[1]) 85 | blue += Int(rowPtr[2]) 86 | rowPtr += Int(stride) 87 | } 88 | } 89 | 90 | let f: CGFloat = 1.0 / (255.0 * CGFloat(imageWidth) * CGFloat(imageHeight)) 91 | return UIColor(red: f * CGFloat(red), green: f * CGFloat(green), blue: f * CGFloat(blue), alpha: 1.0) 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /MissCat/Others/Extension/UIViewController+ImageViewer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIViewController+ImageViewer.swift 3 | // MissCat 4 | // 5 | // Created by Yuiga Wada on 2020/08/05. 6 | // Copyright © 2020 Yuiga Wada. All rights reserved. 7 | // 8 | 9 | import Agrume 10 | import RxSwift 11 | import UIKit 12 | 13 | extension UIViewController { 14 | func viewImage(urls: [URL], startIndex: Int, disposeBag: DisposeBag) { 15 | let overlayView = AgrumeOverlay() 16 | let agrume = Agrume(urls: urls, startIndex: startIndex, background: .blurred(.dark), overlayView: overlayView) 17 | 18 | overlayView.shareTrigger?.subscribe(onNext: { _ in // 共有 19 | agrume.image(forIndex: agrume.currentIndex) { image in 20 | guard let image = image else { return } 21 | let activityVC = UIActivityViewController(activityItems: [image], applicationActivities: nil) 22 | agrume.present(activityVC, animated: true, completion: nil) 23 | } 24 | }).disposed(by: disposeBag) 25 | 26 | agrume.show(from: self) // 画像を表示 27 | } 28 | 29 | func viewImage(image: UIImage, disposeBag: DisposeBag) { 30 | let overlayView = AgrumeOverlay() 31 | let agrume = Agrume(image: image, background: .blurred(.dark), overlayView: overlayView) 32 | 33 | overlayView.shareTrigger?.subscribe(onNext: { _ in // 共有 34 | agrume.image(forIndex: agrume.currentIndex) { image in 35 | guard let image = image else { return } 36 | let activityVC = UIActivityViewController(activityItems: [image], applicationActivities: nil) 37 | agrume.present(activityVC, animated: true, completion: nil) 38 | } 39 | }).disposed(by: disposeBag) 40 | 41 | agrume.show(from: self) // 画像を表示 42 | } 43 | } 44 | 45 | class AgrumeOverlay: AgrumeOverlayView { 46 | var shareTrigger: Observable? 47 | 48 | lazy var toolbar: UIToolbar = { 49 | let toolbar = UIToolbar() 50 | let flexibleItem = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil) 51 | let shareItem = UIBarButtonItem(barButtonSystemItem: .action, target: self, action: nil) 52 | 53 | toolbar.translatesAutoresizingMaskIntoConstraints = false 54 | toolbar.barStyle = .blackTranslucent 55 | toolbar.setItems([ 56 | flexibleItem, 57 | shareItem, 58 | ], animated: false) 59 | 60 | self.shareTrigger = shareItem.rx.tap.asObservable() 61 | return toolbar 62 | }() 63 | 64 | var portableSafeLayoutGuide: UILayoutGuide { 65 | if #available(iOS 11.0, *) { 66 | return safeAreaLayoutGuide 67 | } 68 | return layoutMarginsGuide 69 | } 70 | 71 | override init(frame: CGRect) { 72 | super.init(frame: frame) 73 | commonInit() 74 | } 75 | 76 | required init?(coder: NSCoder) { 77 | super.init(coder: coder) 78 | commonInit() 79 | } 80 | 81 | private func commonInit() { 82 | addSubview(toolbar) 83 | 84 | NSLayoutConstraint.activate([ 85 | toolbar.bottomAnchor.constraint(equalTo: portableSafeLayoutGuide.bottomAnchor), 86 | toolbar.leadingAnchor.constraint(equalTo: leadingAnchor), 87 | toolbar.trailingAnchor.constraint(equalTo: trailingAnchor) 88 | ]) 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /MissCat/Others/Extension/UIViewController+NavigationController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIViewController+NavigationController.swift 3 | // MissCat 4 | // 5 | // Created by Yuiga Wada on 2020/09/06. 6 | // Copyright © 2020 Yuiga Wada. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension UIViewController: UIGestureRecognizerDelegate { 12 | // たまにnavigationControllerが機能しなくなってフリーズするため、フリーズしないように 13 | // 参考→ https://stackoverflow.com/a/36637556 14 | public func navigationController(_ navigationController: UINavigationController, didShow viewController: UIViewController, animated: Bool) { 15 | if navigationController.viewControllers.count > 1 { 16 | self.navigationController?.interactivePopGestureRecognizer?.delegate = self 17 | navigationController.interactivePopGestureRecognizer?.isEnabled = true 18 | } else { 19 | self.navigationController?.interactivePopGestureRecognizer?.delegate = nil 20 | navigationController.interactivePopGestureRecognizer?.isEnabled = false 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /MissCat/Others/Extension/URL+MissCat.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIImage+MissCat.swift 3 | // MissCat 4 | // 5 | // Created by Yuiga Wada on 2019/11/13. 6 | // Copyright © 2019 Yuiga Wada. All rights reserved. 7 | // 8 | 9 | import SVGKit 10 | import UIKit 11 | 12 | extension URL { 13 | func toUIImage(_ completion: @escaping (UIImage?) -> Void) -> URLSessionDataTask? { 14 | var request = URLRequest(url: self) 15 | request.timeoutInterval = 10 16 | 17 | let task = URLSession.shared.dataTask(with: request) { data, _, _ in 18 | guard let data = data else { return completion(nil) } 19 | 20 | if let uiImage = UIImage(data: data) { 21 | completion(uiImage) 22 | } else { // Type: SVG 23 | guard let svgImage = SVGKImage(data: data) else { return } 24 | completion(svgImage.uiImage) 25 | } 26 | } 27 | 28 | task.resume() 29 | return task 30 | } 31 | 32 | func getData(_ completion: @escaping (Data?) -> Void) { 33 | var request = URLRequest(url: self) 34 | request.timeoutInterval = 10 35 | 36 | let task = URLSession.shared.dataTask(with: request) { data, _, _ in 37 | guard let data = data else { return completion(nil) } 38 | completion(data) 39 | } 40 | 41 | task.resume() 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /MissCat/Others/Extension/UserModel+MissCat.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserModel+MissCat.swift 3 | // MissCat 4 | // 5 | // Created by Yuiga Wada on 2020/04/13. 6 | // Copyright © 2020 Yuiga Wada. All rights reserved. 7 | // 8 | 9 | import MisskeyKit 10 | 11 | extension UserModel {} 12 | -------------------------------------------------------------------------------- /MissCat/Others/Utilities/CellModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CellModel.swift 3 | // MissCat 4 | // 5 | // Created by Yuiga Wada on 2020/06/30. 6 | // Copyright © 2020 Yuiga Wada. All rights reserved. 7 | // 8 | 9 | import RxCocoa 10 | import RxDataSources 11 | import RxSwift 12 | 13 | // IdentifiableでEquatableなクラス 14 | // RxDataSources等でIdentifiableType && Equatableを求められるモデルクラスはこれを継承する 15 | class CellModel: IdentifiableType, Equatable { 16 | typealias Identity = String 17 | let identity = String(Float.random(in: 1 ..< 100)) 18 | 19 | static func == (lhs: CellModel, rhs: CellModel) -> Bool { 20 | return lhs.identity == rhs.identity 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /MissCat/Others/Utilities/CustomNavigationController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CustomNavigationController.swift 3 | // MissCat 4 | // 5 | // Created by Yuiga Wada on 2020/05/01. 6 | // Copyright © 2020 Yuiga Wada. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class CustomNavigationController: UINavigationController { 12 | override var childForStatusBarStyle: UIViewController? { // ステータスバーの色をHomeViewController側からイジれるように 13 | return visibleViewController 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /MissCat/Others/Utilities/MissCatImageView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MissCatImageView.swift 3 | // MissCat 4 | // 5 | // Created by Yuiga Wada on 2020/05/17. 6 | // Copyright © 2020 Yuiga Wada. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class MissCatImageView: UIImageView { 12 | private var isCircle: Bool = false 13 | 14 | /// 円を描くようにフラグを建てる 15 | func maskCircle() { 16 | isCircle = true 17 | } 18 | 19 | override func layoutSubviews() { 20 | super.layoutSubviews() 21 | if isCircle { layer.cornerRadius = frame.width / 2 } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /MissCat/Others/Utilities/PlaceholderTableView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PlaceholderTableView.swift 3 | // MissCat 4 | // 5 | // Created by Yuiga Wada on 2020/04/18. 6 | // Copyright © 2020 Yuiga Wada. All rights reserved. 7 | // 8 | 9 | import RxSwift 10 | import UIKit 11 | 12 | class PlaceholderTableView: UITableView { 13 | private let disposeBag: DisposeBag = .init() 14 | private lazy var placeholder = preparePlaceholder() 15 | 16 | override func draw(_ rect: CGRect) { 17 | super.draw(rect) 18 | showPlaceholder() 19 | } 20 | 21 | override func layoutSubviews() { 22 | super.layoutSubviews() 23 | showPlaceholder() 24 | } 25 | 26 | private func showPlaceholder() { 27 | guard numberOfSections > 0 else { setPlaceholder(); return } 28 | 29 | let num = numberOfRows(inSection: 0) 30 | if num == 0 { 31 | guard !contains(placeholder) else { return } 32 | 33 | addSubview(placeholder) 34 | setAutoLayout(to: placeholder) 35 | } else { 36 | placeholder.removeFromSuperview() 37 | } 38 | 39 | placeholder.frame = frame 40 | } 41 | 42 | private func setPlaceholder() { 43 | placeholder.frame = frame 44 | guard !contains(placeholder) else { return } 45 | 46 | addSubview(placeholder) 47 | setAutoLayout(to: placeholder) 48 | } 49 | 50 | private func preparePlaceholder() -> UIView { 51 | let storyboard = UIStoryboard(name: "Main", bundle: nil) 52 | let viewController = storyboard.instantiateViewController(withIdentifier: "placeholder") 53 | guard let view = viewController.view else { return .init() } 54 | 55 | view.frame = frame 56 | view.translatesAutoresizingMaskIntoConstraints = false 57 | 58 | return view 59 | } 60 | 61 | private func setAutoLayout(to view: UIView) { 62 | addConstraints([ 63 | NSLayoutConstraint(item: self, 64 | attribute: .width, 65 | relatedBy: .equal, 66 | toItem: view, 67 | attribute: .width, 68 | multiplier: 1.0, 69 | constant: 0), 70 | 71 | NSLayoutConstraint(item: self, 72 | attribute: .height, 73 | relatedBy: .equal, 74 | toItem: view, 75 | attribute: .height, 76 | multiplier: 1.0, 77 | constant: 0), 78 | 79 | NSLayoutConstraint(item: self, 80 | attribute: .centerX, 81 | relatedBy: .equal, 82 | toItem: view, 83 | attribute: .centerX, 84 | multiplier: 1.0, 85 | constant: 0), 86 | 87 | NSLayoutConstraint(item: self, 88 | attribute: .centerY, 89 | relatedBy: .equal, 90 | toItem: view, 91 | attribute: .centerY, 92 | multiplier: 1.0, 93 | constant: 0) 94 | ]) 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /MissCat/Others/Utilities/RxEureka.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RxEureka.swift 3 | // MissCat 4 | // 5 | // Created by Yuiga Wada on 2020/05/16. 6 | // Copyright © 2020 Yuiga Wada. All rights reserved. 7 | // 8 | 9 | import Eureka 10 | import RxCocoa 11 | import RxSwift 12 | 13 | extension RowOf: ReactiveCompatible {} 14 | 15 | extension Reactive where Base: RowType, Base: BaseRow { 16 | var value: ControlProperty { 17 | let source = Observable.create { observer in 18 | self.base.onChange { row in 19 | observer.onNext(row.value) 20 | // row.updateCell() 21 | } 22 | return Disposables.create() 23 | } 24 | let bindingObserver = Binder(base) { row, value in 25 | row.value = value 26 | } 27 | return ControlProperty(values: source, valueSink: bindingObserver) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /MissCat/Resources/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" : "60x60", 47 | "idiom" : "iphone", 48 | "filename" : "Icon-App-60x60@2x.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "60x60", 53 | "idiom" : "iphone", 54 | "filename" : "Icon-App-60x60@3x.png", 55 | "scale" : "3x" 56 | }, 57 | { 58 | "size" : "20x20", 59 | "idiom" : "ipad", 60 | "filename" : "Icon-App-20x20@1x.png", 61 | "scale" : "1x" 62 | }, 63 | { 64 | "size" : "20x20", 65 | "idiom" : "ipad", 66 | "filename" : "Icon-App-20x20@2x.png", 67 | "scale" : "2x" 68 | }, 69 | { 70 | "size" : "29x29", 71 | "idiom" : "ipad", 72 | "filename" : "Icon-App-29x29@1x.png", 73 | "scale" : "1x" 74 | }, 75 | { 76 | "size" : "29x29", 77 | "idiom" : "ipad", 78 | "filename" : "Icon-App-29x29@2x.png", 79 | "scale" : "2x" 80 | }, 81 | { 82 | "size" : "40x40", 83 | "idiom" : "ipad", 84 | "filename" : "Icon-App-40x40@1x.png", 85 | "scale" : "1x" 86 | }, 87 | { 88 | "size" : "40x40", 89 | "idiom" : "ipad", 90 | "filename" : "Icon-App-40x40@2x.png", 91 | "scale" : "2x" 92 | }, 93 | { 94 | "size" : "76x76", 95 | "idiom" : "ipad", 96 | "filename" : "Icon-App-76x76@1x.png", 97 | "scale" : "1x" 98 | }, 99 | { 100 | "size" : "76x76", 101 | "idiom" : "ipad", 102 | "filename" : "Icon-App-76x76@2x.png", 103 | "scale" : "2x" 104 | }, 105 | { 106 | "size" : "83.5x83.5", 107 | "idiom" : "ipad", 108 | "filename" : "Icon-App-83.5x83.5@2x.png", 109 | "scale" : "2x" 110 | }, 111 | { 112 | "size" : "1024x1024", 113 | "idiom" : "ios-marketing", 114 | "filename" : "ItunesArtwork@2x.png", 115 | "scale" : "1x" 116 | }, 117 | { 118 | "size" : "76x76", 119 | "idiom" : "iphone", 120 | "filename" : "Icon-App-76x76@2x.png", 121 | "scale" : "2x" 122 | } 123 | ], 124 | "info" : { 125 | "version" : 1, 126 | "author" : "xcode" 127 | } 128 | } -------------------------------------------------------------------------------- /MissCat/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YuigaWada/MissCat/574355469ebbcbe46d03a24c66630aeb5239018f/MissCat/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /MissCat/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YuigaWada/MissCat/574355469ebbcbe46d03a24c66630aeb5239018f/MissCat/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /MissCat/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YuigaWada/MissCat/574355469ebbcbe46d03a24c66630aeb5239018f/MissCat/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /MissCat/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YuigaWada/MissCat/574355469ebbcbe46d03a24c66630aeb5239018f/MissCat/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /MissCat/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YuigaWada/MissCat/574355469ebbcbe46d03a24c66630aeb5239018f/MissCat/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /MissCat/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YuigaWada/MissCat/574355469ebbcbe46d03a24c66630aeb5239018f/MissCat/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /MissCat/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YuigaWada/MissCat/574355469ebbcbe46d03a24c66630aeb5239018f/MissCat/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /MissCat/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YuigaWada/MissCat/574355469ebbcbe46d03a24c66630aeb5239018f/MissCat/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /MissCat/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YuigaWada/MissCat/574355469ebbcbe46d03a24c66630aeb5239018f/MissCat/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /MissCat/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YuigaWada/MissCat/574355469ebbcbe46d03a24c66630aeb5239018f/MissCat/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /MissCat/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YuigaWada/MissCat/574355469ebbcbe46d03a24c66630aeb5239018f/MissCat/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /MissCat/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YuigaWada/MissCat/574355469ebbcbe46d03a24c66630aeb5239018f/MissCat/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /MissCat/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YuigaWada/MissCat/574355469ebbcbe46d03a24c66630aeb5239018f/MissCat/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /MissCat/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YuigaWada/MissCat/574355469ebbcbe46d03a24c66630aeb5239018f/MissCat/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /MissCat/Resources/Assets.xcassets/AppIcon.appiconset/ItunesArtwork@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YuigaWada/MissCat/574355469ebbcbe46d03a24c66630aeb5239018f/MissCat/Resources/Assets.xcassets/AppIcon.appiconset/ItunesArtwork@2x.png -------------------------------------------------------------------------------- /MissCat/Resources/Assets.xcassets/Cat.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "cat_.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "cat_-1.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "cat_-2.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /MissCat/Resources/Assets.xcassets/Cat.imageset/cat_-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YuigaWada/MissCat/574355469ebbcbe46d03a24c66630aeb5239018f/MissCat/Resources/Assets.xcassets/Cat.imageset/cat_-1.png -------------------------------------------------------------------------------- /MissCat/Resources/Assets.xcassets/Cat.imageset/cat_-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YuigaWada/MissCat/574355469ebbcbe46d03a24c66630aeb5239018f/MissCat/Resources/Assets.xcassets/Cat.imageset/cat_-2.png -------------------------------------------------------------------------------- /MissCat/Resources/Assets.xcassets/Cat.imageset/cat_.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YuigaWada/MissCat/574355469ebbcbe46d03a24c66630aeb5239018f/MissCat/Resources/Assets.xcassets/Cat.imageset/cat_.png -------------------------------------------------------------------------------- /MissCat/Resources/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /MissCat/Resources/Assets.xcassets/Logo.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "Logo.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "Logo-1.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "Logo-2.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /MissCat/Resources/Assets.xcassets/Logo.imageset/Logo-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YuigaWada/MissCat/574355469ebbcbe46d03a24c66630aeb5239018f/MissCat/Resources/Assets.xcassets/Logo.imageset/Logo-1.png -------------------------------------------------------------------------------- /MissCat/Resources/Assets.xcassets/Logo.imageset/Logo-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YuigaWada/MissCat/574355469ebbcbe46d03a24c66630aeb5239018f/MissCat/Resources/Assets.xcassets/Logo.imageset/Logo-2.png -------------------------------------------------------------------------------- /MissCat/Resources/Assets.xcassets/Logo.imageset/Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YuigaWada/MissCat/574355469ebbcbe46d03a24c66630aeb5239018f/MissCat/Resources/Assets.xcassets/Logo.imageset/Logo.png -------------------------------------------------------------------------------- /MissCat/Resources/Assets.xcassets/MissCat.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "MisscatIcon_shaped-1.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "MisscatIcon_shaped-3.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "MisscatIcon_shaped-2.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /MissCat/Resources/Assets.xcassets/MissCat.imageset/MisscatIcon_shaped-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YuigaWada/MissCat/574355469ebbcbe46d03a24c66630aeb5239018f/MissCat/Resources/Assets.xcassets/MissCat.imageset/MisscatIcon_shaped-1.png -------------------------------------------------------------------------------- /MissCat/Resources/Assets.xcassets/MissCat.imageset/MisscatIcon_shaped-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YuigaWada/MissCat/574355469ebbcbe46d03a24c66630aeb5239018f/MissCat/Resources/Assets.xcassets/MissCat.imageset/MisscatIcon_shaped-2.png -------------------------------------------------------------------------------- /MissCat/Resources/Assets.xcassets/MissCat.imageset/MisscatIcon_shaped-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YuigaWada/MissCat/574355469ebbcbe46d03a24c66630aeb5239018f/MissCat/Resources/Assets.xcassets/MissCat.imageset/MisscatIcon_shaped-3.png -------------------------------------------------------------------------------- /MissCat/Resources/Assets.xcassets/Sad_Ai.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "Sad_Ai.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "Sad_Ai-1.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "Sad_Ai-2.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /MissCat/Resources/Assets.xcassets/Sad_Ai.imageset/Sad_Ai-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YuigaWada/MissCat/574355469ebbcbe46d03a24c66630aeb5239018f/MissCat/Resources/Assets.xcassets/Sad_Ai.imageset/Sad_Ai-1.png -------------------------------------------------------------------------------- /MissCat/Resources/Assets.xcassets/Sad_Ai.imageset/Sad_Ai-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YuigaWada/MissCat/574355469ebbcbe46d03a24c66630aeb5239018f/MissCat/Resources/Assets.xcassets/Sad_Ai.imageset/Sad_Ai-2.png -------------------------------------------------------------------------------- /MissCat/Resources/Assets.xcassets/Sad_Ai.imageset/Sad_Ai.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YuigaWada/MissCat/574355469ebbcbe46d03a24c66630aeb5239018f/MissCat/Resources/Assets.xcassets/Sad_Ai.imageset/Sad_Ai.png -------------------------------------------------------------------------------- /MissCat/Resources/Assets.xcassets/SprashBack.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "SprashBack.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "SprashBack-1.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "SprashBack-2.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /MissCat/Resources/Assets.xcassets/SprashBack.imageset/SprashBack-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YuigaWada/MissCat/574355469ebbcbe46d03a24c66630aeb5239018f/MissCat/Resources/Assets.xcassets/SprashBack.imageset/SprashBack-1.png -------------------------------------------------------------------------------- /MissCat/Resources/Assets.xcassets/SprashBack.imageset/SprashBack-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YuigaWada/MissCat/574355469ebbcbe46d03a24c66630aeb5239018f/MissCat/Resources/Assets.xcassets/SprashBack.imageset/SprashBack-2.png -------------------------------------------------------------------------------- /MissCat/Resources/Assets.xcassets/SprashBack.imageset/SprashBack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YuigaWada/MissCat/574355469ebbcbe46d03a24c66630aeb5239018f/MissCat/Resources/Assets.xcassets/SprashBack.imageset/SprashBack.png -------------------------------------------------------------------------------- /MissCat/Resources/Assets.xcassets/del.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "del.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "del-1.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "del-2.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /MissCat/Resources/Assets.xcassets/del.imageset/del-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YuigaWada/MissCat/574355469ebbcbe46d03a24c66630aeb5239018f/MissCat/Resources/Assets.xcassets/del.imageset/del-1.png -------------------------------------------------------------------------------- /MissCat/Resources/Assets.xcassets/del.imageset/del-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YuigaWada/MissCat/574355469ebbcbe46d03a24c66630aeb5239018f/MissCat/Resources/Assets.xcassets/del.imageset/del-2.png -------------------------------------------------------------------------------- /MissCat/Resources/Assets.xcassets/del.imageset/del.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YuigaWada/MissCat/574355469ebbcbe46d03a24c66630aeb5239018f/MissCat/Resources/Assets.xcassets/del.imageset/del.png -------------------------------------------------------------------------------- /MissCat/Resources/Assets.xcassets/error.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "error.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "error-1.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "error-2.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /MissCat/Resources/Assets.xcassets/error.imageset/error-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YuigaWada/MissCat/574355469ebbcbe46d03a24c66630aeb5239018f/MissCat/Resources/Assets.xcassets/error.imageset/error-1.png -------------------------------------------------------------------------------- /MissCat/Resources/Assets.xcassets/error.imageset/error-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YuigaWada/MissCat/574355469ebbcbe46d03a24c66630aeb5239018f/MissCat/Resources/Assets.xcassets/error.imageset/error-2.png -------------------------------------------------------------------------------- /MissCat/Resources/Assets.xcassets/error.imageset/error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YuigaWada/MissCat/574355469ebbcbe46d03a24c66630aeb5239018f/MissCat/Resources/Assets.xcassets/error.imageset/error.png -------------------------------------------------------------------------------- /MissCat/Resources/Assets.xcassets/misskey.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "misskey.jpg", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "misskey-1.jpg", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "misskey-2.jpg", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /MissCat/Resources/Assets.xcassets/misskey.imageset/misskey-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YuigaWada/MissCat/574355469ebbcbe46d03a24c66630aeb5239018f/MissCat/Resources/Assets.xcassets/misskey.imageset/misskey-1.jpg -------------------------------------------------------------------------------- /MissCat/Resources/Assets.xcassets/misskey.imageset/misskey-2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YuigaWada/MissCat/574355469ebbcbe46d03a24c66630aeb5239018f/MissCat/Resources/Assets.xcassets/misskey.imageset/misskey-2.jpg -------------------------------------------------------------------------------- /MissCat/Resources/Assets.xcassets/misskey.imageset/misskey.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YuigaWada/MissCat/574355469ebbcbe46d03a24c66630aeb5239018f/MissCat/Resources/Assets.xcassets/misskey.imageset/misskey.jpg -------------------------------------------------------------------------------- /MissCat/Resources/Assets.xcassets/play.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "play.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "play-1.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "play-2.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /MissCat/Resources/Assets.xcassets/play.imageset/play-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YuigaWada/MissCat/574355469ebbcbe46d03a24c66630aeb5239018f/MissCat/Resources/Assets.xcassets/play.imageset/play-1.png -------------------------------------------------------------------------------- /MissCat/Resources/Assets.xcassets/play.imageset/play-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YuigaWada/MissCat/574355469ebbcbe46d03a24c66630aeb5239018f/MissCat/Resources/Assets.xcassets/play.imageset/play-2.png -------------------------------------------------------------------------------- /MissCat/Resources/Assets.xcassets/play.imageset/play.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YuigaWada/MissCat/574355469ebbcbe46d03a24c66630aeb5239018f/MissCat/Resources/Assets.xcassets/play.imageset/play.png -------------------------------------------------------------------------------- /MissCat/Resources/Font Awesome 5 Brands-Regular-400.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YuigaWada/MissCat/574355469ebbcbe46d03a24c66630aeb5239018f/MissCat/Resources/Font Awesome 5 Brands-Regular-400.otf -------------------------------------------------------------------------------- /MissCat/Resources/Font Awesome 5 Free-Regular-400.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YuigaWada/MissCat/574355469ebbcbe46d03a24c66630aeb5239018f/MissCat/Resources/Font Awesome 5 Free-Regular-400.otf -------------------------------------------------------------------------------- /MissCat/Resources/Font Awesome 5 Free-Solid-900.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YuigaWada/MissCat/574355469ebbcbe46d03a24c66630aeb5239018f/MissCat/Resources/Font Awesome 5 Free-Solid-900.otf -------------------------------------------------------------------------------- /MissCat/View/DirectMessage/DirectMessageViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DirectMessageViewController.swift 3 | // MissCat 4 | // 5 | // Created by Yuiga Wada on 2020/04/16. 6 | // Copyright © 2020 Yuiga Wada. All rights reserved. 7 | // 8 | 9 | import RxCocoa 10 | import RxSwift 11 | import UIKit 12 | 13 | class DirectMessageViewController: ChatViewController { 14 | var homeViewController: HomeViewController? 15 | private var viewModel: DirectMessageViewModel? 16 | private let disposeBag = DisposeBag() 17 | 18 | func setup(userId: String? = nil, groupId: String? = nil, owner: SecureUser) { 19 | let viewModel: DirectMessageViewModel = .init(with: .init(owner: owner, 20 | userId: userId ?? "", 21 | sendTrigger: sendTrigger), 22 | and: disposeBag) 23 | 24 | binding(with: viewModel) 25 | viewModel.load() 26 | self.viewModel = viewModel 27 | } 28 | 29 | private func binding(with viewModel: DirectMessageViewModel) { 30 | let output = viewModel.output 31 | output.messages.bind(to: messages).disposed(by: disposeBag) 32 | output.sendCompleted.bind(to: sendCompleted).disposed(by: disposeBag) 33 | 34 | tapTrigger.subscribe(onNext: { link in 35 | switch link.type { 36 | case .url: 37 | self.homeViewController?.openLink(url: link.value) 38 | case .hashtag: 39 | self.navigationController?.popViewController(animated: true) 40 | self.homeViewController?.emulateFooterTabTap(tab: .home) 41 | self.homeViewController?.searchHashtag(tag: link.value) 42 | case .mention: 43 | self.homeViewController?.openUserPage(username: link.value, owner: viewModel.state.owner) 44 | } 45 | }).disposed(by: disposeBag) 46 | 47 | loadTrigger.subscribe(onNext: { 48 | viewModel.load { 49 | self.endRefreshing() 50 | } 51 | }).disposed(by: disposeBag) 52 | } 53 | 54 | override func viewDidLoad() { 55 | super.viewDidLoad() 56 | setTheme() 57 | } 58 | 59 | override func viewWillAppear(_ animated: Bool) { 60 | super.viewWillAppear(animated) 61 | navigationController?.setNavigationBarHidden(false, animated: animated) 62 | } 63 | 64 | private func setTheme() { 65 | if let colorPattern = Theme.shared.currentModel?.colorPattern.ui { 66 | view.backgroundColor = colorPattern.base 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /MissCat/View/Login/TosViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TosViewController.swift 3 | // MissCat 4 | // 5 | // Created by Yuiga Wada on 2020/03/31. 6 | // Copyright © 2020 Yuiga Wada. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class TosViewController: UIViewController { 12 | var agreed: (() -> Void)? 13 | 14 | private var hasTapped: Bool = false 15 | 16 | override func viewWillAppear(_ animated: Bool) { 17 | super.viewWillAppear(true) 18 | title = "利用規約" 19 | navigationController?.setNavigationBarHidden(false, animated: animated) 20 | } 21 | 22 | override func viewDidDisappear(_ animated: Bool) { 23 | super.viewDidDisappear(animated) 24 | 25 | guard hasTapped else { return } 26 | agreed?() 27 | } 28 | 29 | @IBAction func tapped(_ sender: Any) { 30 | hasTapped = true 31 | navigationController?.popViewController(animated: true) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /MissCat/View/Reusable/Emoji/EmojiViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EmojiViewCell.swift 3 | // MissCat 4 | // 5 | // Created by Yuiga Wada on 2019/11/24. 6 | // Copyright © 2019 Yuiga Wada. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class EmojiViewCell: UICollectionViewCell { 12 | @IBOutlet weak var mainView: EmojiView! 13 | } 14 | -------------------------------------------------------------------------------- /MissCat/View/Reusable/Emoji/nib/EmojiViewCell.xib: -------------------------------------------------------------------------------- 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 | -------------------------------------------------------------------------------- /MissCat/View/Reusable/NoteCell/FileContainerCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FileContainerCell.swift 3 | // MissCat 4 | // 5 | // Created by Yuiga Wada on 2020/03/23. 6 | // Copyright © 2020 Yuiga Wada. All rights reserved. 7 | // 8 | 9 | import Agrume 10 | import RxSwift 11 | import UIKit 12 | 13 | class FileContainerCell: UICollectionViewCell { 14 | @IBOutlet weak var imageView: FileView! 15 | 16 | private let disposeBag = DisposeBag() 17 | 18 | override init(frame: CGRect) { 19 | super.init(frame: frame) 20 | setComponent() 21 | } 22 | 23 | required init?(coder: NSCoder) { 24 | super.init(coder: coder) 25 | setComponent() 26 | } 27 | 28 | override func layoutSubviews() { 29 | imageView.contentMode = .scaleAspectFill 30 | super.layoutSubviews() 31 | } 32 | 33 | private func setComponent() { 34 | backgroundColor = .lightGray 35 | } 36 | 37 | private func initialize() { 38 | setComponent() 39 | imageView.image = nil 40 | } 41 | 42 | func transform(with fileModel: FileContainer.Model, and delegate: NoteCellDelegate?) { 43 | initialize() 44 | 45 | let cached = Cache.shared.getUiImage(url: fileModel.originalUrl) ?? Cache.shared.getUiImage(url: fileModel.thumbnailUrl) 46 | if let cached = cached { 47 | setImage(image: cached, 48 | originalUrl: fileModel.originalUrl, 49 | isVideo: fileModel.isVideo, 50 | isSensitive: fileModel.isSensitive) 51 | return 52 | } 53 | 54 | // キャッシュが存在しない場合 55 | 56 | fileModel.thumbnailUrl.toUIImage { image in 57 | guard let image = image else { return } 58 | 59 | Cache.shared.saveUiImage(image, url: fileModel.thumbnailUrl) 60 | 61 | self.setImage(image: image, 62 | originalUrl: fileModel.originalUrl, 63 | isVideo: fileModel.isVideo, 64 | isSensitive: fileModel.isSensitive) 65 | } 66 | 67 | fileModel.originalUrl.toUIImage { image in 68 | guard let image = image else { return } 69 | 70 | Cache.shared.saveUiImage(image, url: fileModel.originalUrl) 71 | 72 | self.setImage(image: image, 73 | originalUrl: fileModel.originalUrl, 74 | isVideo: fileModel.isVideo, 75 | isSensitive: fileModel.isSensitive) 76 | } 77 | } 78 | 79 | private func setImage(image: UIImage, originalUrl: String, isVideo: Bool, isSensitive: Bool) { 80 | DispatchQueue.main.async { 81 | self.imageView.backgroundColor = .clear 82 | self.imageView.image = image 83 | self.imageView.isHidden = false 84 | self.imageView.setPlayIconImage(hide: !isVideo) 85 | self.imageView.setNSFW(hide: !isSensitive) 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /MissCat/View/Reusable/NoteCell/PromotionCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PromotionCell.swift 3 | // MissCat 4 | // 5 | // Created by Yuiga Wada on 2020/04/10. 6 | // Copyright © 2020 Yuiga Wada. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class PromotionCell: UITableViewCell { 12 | @IBOutlet weak var iconLabel: UILabel! 13 | @IBOutlet weak var promotionLabel: UILabel! 14 | 15 | private lazy var mainColor: UIColor = .init(hex: "d28a3f") 16 | 17 | override func layoutSubviews() { 18 | setupComponent() 19 | } 20 | 21 | private func setupComponent() { 22 | iconLabel.font = .awesomeSolid(fontSize: 15.0) 23 | iconLabel.textColor = mainColor 24 | promotionLabel.textColor = mainColor 25 | backgroundColor = Theme.shared.currentModel?.colorPattern.ui.base ?? .white 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /MissCat/View/Reusable/NoteCell/RenoteeCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RenoteeCell.swift 3 | // MissCat 4 | // 5 | // Created by Yuiga Wada on 2019/11/20. 6 | // Copyright © 2019 Yuiga Wada. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class RenoteeCell: UITableViewCell { 12 | @IBOutlet weak var renoteMarkLabel: UILabel! 13 | @IBOutlet weak var renoteeLabel: UILabel! 14 | 15 | private var renotee: String? { 16 | didSet { 17 | guard let renotee = renotee else { renoteeLabel.text = nil; return } 18 | renoteeLabel.text = renotee + "さんがRenoteしました" 19 | } 20 | } 21 | 22 | override func layoutSubviews() { 23 | setupComponent() 24 | } 25 | 26 | private func setupComponent() { 27 | renoteMarkLabel.font = .awesomeSolid(fontSize: 13.0) 28 | backgroundColor = Theme.shared.currentModel?.colorPattern.ui.base ?? .white 29 | renoteMarkLabel.textColor = Theme.shared.currentModel?.colorPattern.ui.text ?? .black 30 | renoteeLabel.textColor = Theme.shared.currentModel?.colorPattern.ui.text ?? .black 31 | } 32 | 33 | func setRenotee(_ renotee: String?) { 34 | self.renotee = renotee 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /MissCat/View/Reusable/NoteCell/UrlPreviewer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UrlPreviewer.swift 3 | // MissCat 4 | // 5 | // Created by Yuiga Wada on 2020/04/07. 6 | // Copyright © 2020 Yuiga Wada. All rights reserved. 7 | // 8 | 9 | import RxCocoa 10 | import RxSwift 11 | import UIKit 12 | 13 | class UrlPreviewer: UIView, ComponentType { 14 | typealias Transformed = UrlPreviewer 15 | struct Arg { 16 | let url: String 17 | let owner: SecureUser? 18 | } 19 | 20 | @IBOutlet weak var imageView: UIImageView! 21 | @IBOutlet weak var titleLabel: UILabel! 22 | @IBOutlet weak var previewTextView: UITextView! 23 | 24 | private var viewModel: UrlPreviewerViewModel? 25 | private let disposeBag = DisposeBag() 26 | 27 | // MARK: Life Cycle 28 | 29 | override init(frame: CGRect) { 30 | super.init(frame: frame) 31 | loadNib() 32 | setComponent() 33 | setTheme() 34 | } 35 | 36 | required init?(coder aDecoder: NSCoder) { 37 | super.init(coder: aDecoder)! 38 | loadNib() 39 | setComponent() 40 | setTheme() 41 | } 42 | 43 | func loadNib() { 44 | if let view = UINib(nibName: "UrlPreviewer", bundle: Bundle(for: type(of: self))).instantiate(withOwner: self, options: nil)[0] as? UIView { 45 | view.frame = bounds 46 | view.backgroundColor = .clear 47 | addSubview(view) 48 | } 49 | } 50 | 51 | // MARK: Design 52 | 53 | private func setTheme() { 54 | guard let theme = Theme.shared.currentModel else { return } 55 | let colorPattern = theme.colorPattern.ui 56 | 57 | backgroundColor = colorPattern.sub2 58 | titleLabel.textColor = colorPattern.text 59 | previewTextView.textColor = colorPattern.sub0 60 | } 61 | 62 | // MARK: Setup 63 | 64 | private func binding(_ viewModel: UrlPreviewerViewModel) { 65 | let output = viewModel.output 66 | 67 | output.title 68 | .asDriver(onErrorDriveWith: Driver.empty()) 69 | .drive(titleLabel.rx.text) 70 | .disposed(by: disposeBag) 71 | 72 | output.description 73 | .asDriver(onErrorDriveWith: Driver.empty()) 74 | .drive(previewTextView.rx.text) 75 | .disposed(by: disposeBag) 76 | 77 | output.image 78 | .asDriver(onErrorDriveWith: Driver.empty()) 79 | .drive(onNext: { image in 80 | self.imageView.image = image 81 | self.backgroundColor = .clear 82 | }) 83 | .disposed(by: disposeBag) 84 | } 85 | 86 | private func setComponent() { 87 | layer.cornerRadius = 5 88 | layer.borderColor = UIColor.lightGray.cgColor 89 | layer.borderWidth = 0.3 90 | clipsToBounds = true 91 | 92 | imageView.backgroundColor = .lightGray 93 | imageView.contentMode = .scaleAspectFill 94 | previewTextView.textContainer.lineBreakMode = .byTruncatingTail 95 | } 96 | 97 | // MARK: Publics 98 | 99 | func transform(with arg: UrlPreviewer.Arg) -> UrlPreviewer { 100 | let viewModel = UrlPreviewerViewModel(with: .init(url: arg.url, owner: arg.owner), and: disposeBag) 101 | binding(viewModel) 102 | 103 | self.viewModel = viewModel 104 | return self 105 | } 106 | 107 | func initialize() { 108 | titleLabel.text = nil 109 | previewTextView.text = "Loading..." 110 | imageView.image = nil 111 | viewModel?.prepareForReuse() 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /MissCat/View/Reusable/NoteCell/nib/FileContainerCell.xib: -------------------------------------------------------------------------------- 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 | -------------------------------------------------------------------------------- /MissCat/View/Reusable/Others/ComponentType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ComponentType.swift 3 | // MissCat 4 | // 5 | // Created by Yuiga Wada on 2020/03/12. 6 | // Copyright © 2020 Yuiga Wada. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | protocol ComponentType { 12 | associatedtype Arg 13 | associatedtype Transformed 14 | 15 | func transform(with arg: Arg) -> Transformed 16 | } 17 | -------------------------------------------------------------------------------- /MissCat/View/Reusable/Others/NoteDisplay.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NoteDisplay.swift 3 | // MissCat 4 | // 5 | // Created by Yuiga Wada on 2020/03/26. 6 | // Copyright © 2020 Yuiga Wada. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | 12 | /// NoteCell上のタップ処理はすべてHomeViewControllerが行う。 13 | /// そこで、NoteCellを表示するViewControllerはすべて、このNoteDisplayを継承することで、 14 | /// それらのタップ処理は勝手にHomeViewControllerへと流れてくれる。 15 | class NoteDisplay: UIViewController, NoteCellDelegate, UserCellDelegate { 16 | var homeViewController: HomeViewController? 17 | 18 | func tappedReply(note: NoteCell.Model) { 19 | homeViewController?.tappedReply(note: note) 20 | } 21 | 22 | func tappedRenote(note: NoteCell.Model) { 23 | homeViewController?.tappedRenote(note: note) 24 | } 25 | 26 | func tappedReaction(owner: SecureUser, reactioned: Bool, noteId: String, iconUrl: String?, displayName: String, username: String, hostInstance: String, note: NSAttributedString, hasFile: Bool, hasMarked: Bool, myReaction: String?) { 27 | _ = presentReactionGen(owner: owner, 28 | noteId: noteId, 29 | iconUrl: iconUrl, 30 | displayName: displayName, 31 | username: username, 32 | hostInstance: hostInstance, 33 | note: note, 34 | hasFile: hasFile, 35 | hasMarked: hasMarked, 36 | navigationController: nil) 37 | } 38 | 39 | func tappedOthers(note: NoteCell.Model) { 40 | homeViewController?.tappedOthers(note: note) 41 | } 42 | 43 | func move2PostDetail(item: NoteCell.Model) { 44 | homeViewController?.tappedCell(item: item) 45 | } 46 | 47 | func tappedLink(text: String, owner: SecureUser) { 48 | homeViewController?.tappedLink(text: text, owner: owner) 49 | } 50 | 51 | func openUser(username: String, owner: SecureUser) { 52 | homeViewController?.openUserPage(username: username, owner: owner) 53 | } 54 | 55 | func move2Profile(userId: String, owner: SecureUser) { 56 | homeViewController?.move2Profile(userId: userId, owner: owner) 57 | } 58 | 59 | func updateMyReaction(targetNoteId: String, rawReaction: String, plus: Bool) {} 60 | 61 | func vote(choice: [Int], to noteId: String, owner: SecureUser) { 62 | homeViewController?.vote(choice: choice, to: noteId, owner: owner) 63 | } 64 | 65 | func showImage(_ urls: [URL], start startIndex: Int) { 66 | homeViewController?.showImage(urls, start: startIndex) 67 | } 68 | 69 | func playVideo(url: String) { 70 | homeViewController?.playVideo(url: url) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /MissCat/View/Reusable/Post/AttachmentCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AttachmentCell.swift 3 | // MissCat 4 | // 5 | // Created by Yuiga Wada on 2019/12/20. 6 | // Copyright © 2019 Yuiga Wada. All rights reserved. 7 | // 8 | 9 | import RxCocoa 10 | import RxSwift 11 | import UIKit 12 | 13 | class AttachmentCell: UICollectionViewCell { 14 | @IBOutlet weak var imageView: UIImageView! 15 | @IBOutlet weak var discardButton: UIButton! 16 | 17 | let tapGesture = UITapGestureRecognizer() 18 | lazy var tappedImage: Observable = { 19 | Observable.create { observer in 20 | 21 | self.imageView.setTapGesture(self.disposeBag) { 22 | observer.onNext(self.id) 23 | } 24 | 25 | return Disposables.create() 26 | } 27 | }() 28 | 29 | lazy var tappedDiscardButton: Observable = { 30 | Observable.create { observer in 31 | 32 | self.discardButton.rx.tap.subscribe { _ in 33 | observer.onNext(self.id) 34 | }.disposed(by: self.disposeBag) 35 | 36 | return Disposables.create() 37 | } 38 | }() 39 | 40 | private var disposeBag: DisposeBag = .init() 41 | private var id: String = "" 42 | 43 | override func layoutSubviews() { 44 | super.layoutSubviews() 45 | 46 | setupComponent() 47 | } 48 | 49 | func setupComponent() { 50 | contentMode = .left 51 | 52 | // imageView / discardButtonの上にcontentViewが掛かっているのでUserInteractionをfalseにする 53 | contentView.isUserInteractionEnabled = false 54 | 55 | // self.layer 56 | layer.cornerRadius = 5 57 | imageView.layer.cornerRadius = 5 58 | 59 | // discardButton 60 | discardButton.titleLabel?.font = .awesomeSolid(fontSize: 15.0) 61 | 62 | discardButton.layoutIfNeeded() 63 | discardButton.layer.cornerRadius = discardButton.frame.width / 2 64 | 65 | // imageView 66 | guard let image = imageView.image else { return } 67 | if frame.size.width > image.size.width || frame.size.height > image.size.height { 68 | imageView.contentMode = .scaleAspectFill 69 | } else { 70 | imageView.contentMode = .center 71 | } 72 | } 73 | 74 | func setupCell(_ attachment: PostViewController.Attachments) -> AttachmentCell { 75 | imageView.image = attachment.image 76 | id = attachment.id 77 | 78 | return self 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /MissCat/View/Reusable/Reaction/ReactionCollectionHeader.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ReactionCollectionHeader.swift 3 | // MissCat 4 | // 5 | // Created by Yuiga Wada on 2020/02/25. 6 | // Copyright © 2020 Yuiga Wada. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class ReactionCollectionHeader: UICollectionViewCell { 12 | @IBOutlet weak var headerTitleLabel: UILabel! 13 | 14 | func setTitle(_ name: String) { 15 | headerTitleLabel.text = name 16 | headerTitleLabel.textColor = Theme.shared.currentModel?.colorPattern.ui.text 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /MissCat/View/Reusable/Reaction/ReactionGenPanel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ReactionGenPanel.swift 3 | // MissCat 4 | // 5 | // Created by Yuiga Wada on 2019/12/05. 6 | // Copyright © 2019 Yuiga Wada. All rights reserved. 7 | // 8 | 9 | import FloatingPanel 10 | 11 | class ReactionGenPanel: FloatingPanelController, ReactionGenViewControllerDelegate { 12 | override func viewDidLoad() { 13 | super.viewDidLoad() 14 | 15 | surfaceView.cornerRadius = 12 16 | isRemovalInteractionEnabled = true // Optional: Let it removable by a swipe-down 17 | } 18 | 19 | // MARK: ReactionGenViewControllerDelegate 20 | 21 | func scrollUp() { 22 | move(to: .full, animated: true) 23 | } 24 | } 25 | 26 | // MARK: Layout 27 | 28 | class MissCatFloatingPanelLayout: FloatingPanelLayout { 29 | var initialPosition: FloatingPanelPosition { 30 | return .half 31 | } 32 | 33 | func insetFor(position: FloatingPanelPosition) -> CGFloat? { 34 | switch position { 35 | case .full: return 16.0 // A top inset from safe area 36 | case .half: return 330 // A bottom inset from the safe area 37 | case .tip: return nil 38 | default: return nil // Or `case .hidden: return nil` 39 | } 40 | } 41 | 42 | func backdropAlphaFor(position: FloatingPanelPosition) -> CGFloat { 43 | return 0.5 44 | } 45 | } 46 | 47 | class MissCatFloatingPanelStocksBehavior: FloatingPanelBehavior { 48 | func shouldProjectMomentum(_ fpc: FloatingPanelController, for proposedTargetPosition: FloatingPanelPosition) -> Bool { 49 | return true 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /MissCat/View/Reusable/Reaction/nib/ReactionCollectionHeader.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /MissCat/View/Settings/Cell/TabSettingsCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TabSettingsCell.swift 3 | // MissCat 4 | // 5 | // Created by Yuiga Wada on 2020/04/22. 6 | // Copyright © 2020 Yuiga Wada. All rights reserved. 7 | // 8 | 9 | import Eureka 10 | import RxCocoa 11 | import RxSwift 12 | import UIKit 13 | 14 | public class TabSettingsCell: Cell, CellType { 15 | @IBOutlet weak var nameLabel: UILabel! 16 | @IBOutlet weak var textFiled: UITextField! 17 | 18 | var tabKind: Theme.TabKind? 19 | var owner: SecureUser? 20 | var value: Theme.Tab { 21 | var name = textFiled.text ?? "" 22 | if name.isEmpty { 23 | name = textFiled.placeholder ?? "" 24 | } 25 | 26 | return .init(name: name, 27 | kind: tabKind ?? .home, 28 | userId: owner?.userId, 29 | listId: nil) 30 | } 31 | 32 | var showMenuTrigger: PublishRelay = .init() 33 | var beingRemoved: Bool = false // removeすることになったらsetする 34 | 35 | private var tabSelected: Bool { 36 | return tabKind != nil 37 | } 38 | 39 | override public func setup() { 40 | super.setup() 41 | setTheme() 42 | } 43 | 44 | override public func layoutSubviews() { 45 | super.layoutSubviews() 46 | 47 | if !tabSelected, !beingRemoved { 48 | showMenuTrigger.accept(()) 49 | } 50 | } 51 | 52 | // MARK: Design 53 | 54 | private func setTheme() { 55 | guard let theme = Theme.shared.currentModel else { return } 56 | let colorPattern = theme.colorPattern.ui 57 | 58 | nameLabel.textColor = colorPattern.text 59 | textFiled.textColor = colorPattern.text 60 | 61 | let backView = UIView() 62 | backView.backgroundColor = .clear 63 | selectedBackgroundView = backView 64 | } 65 | 66 | // MARK: Utiliteis 67 | 68 | func setKind(_ kind: Theme.TabKind) { 69 | tabKind = kind 70 | switch kind { 71 | case .home: 72 | nameLabel.text = "ホーム" 73 | textFiled.placeholder = "@\(owner?.username ?? "")" 74 | case .local: 75 | nameLabel.text = "ローカル" 76 | textFiled.placeholder = "Local" 77 | case .social: 78 | nameLabel.text = "ソーシャル" 79 | textFiled.placeholder = "Social" 80 | case .global: 81 | nameLabel.text = "グローバル" 82 | textFiled.placeholder = "Global" 83 | case .user: 84 | nameLabel.text = "ユーザー" 85 | textFiled.placeholder = "User" 86 | case .list: 87 | nameLabel.text = "リスト" 88 | textFiled.placeholder = "List" 89 | } 90 | } 91 | 92 | func setName(_ name: String) { 93 | textFiled.text = name 94 | } 95 | 96 | func setOwner(userId: String?) { 97 | guard let userId = userId else { return } 98 | owner = Cache.UserDefaults.shared.getUser(userId: userId) 99 | } 100 | } 101 | 102 | public final class TabSettingsRow: Row, RowType { 103 | public var tab: Theme.Tab { 104 | return cell.value 105 | } 106 | 107 | public required init(tag: String?) { 108 | super.init(tag: tag) 109 | cellProvider = CellProvider(nibName: "TabSettingsCell") 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /MissCat/View/Settings/ColorPicker.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ColorPicker.swift 3 | // MissCat 4 | // 5 | // Created by Yuiga Wada on 2020/04/24. 6 | // Copyright © 2020 Yuiga Wada. All rights reserved. 7 | // 8 | 9 | import ChromaColorPicker 10 | import RxCocoa 11 | import RxSwift 12 | 13 | class ColorPicker: UIView, ChromaColorPickerDelegate { 14 | var selectedColor: PublishRelay = .init() 15 | private var currentColor: UIColor? 16 | 17 | private lazy var colorPicker = ChromaColorPicker(frame: CGRect(x: 0, y: 0, width: 300, height: 300)) 18 | private lazy var brightnessSlider = ChromaBrightnessSlider(frame: CGRect(x: 0, y: 0, width: 280, height: 32)) 19 | 20 | init(frame: CGRect, initialColor: UIColor) { 21 | super.init(frame: frame) 22 | setup(with: initialColor) 23 | } 24 | 25 | override init(frame: CGRect) { 26 | super.init(frame: frame) 27 | setup() 28 | } 29 | 30 | required init?(coder: NSCoder) { 31 | super.init(coder: coder) 32 | setup() 33 | } 34 | 35 | override func point(inside point: CGPoint, with event: UIEvent?) -> Bool { 36 | if !colorPicker.frame.contains(point), !brightnessSlider.frame.contains(point) { 37 | dismiss() 38 | } 39 | 40 | return super.point(inside: point, with: event) 41 | } 42 | 43 | private func setup(with initialColor: UIColor = .systemBlue) { 44 | setupBlur() 45 | setupColorPicker(with: initialColor) 46 | setNeedsLayout() 47 | layoutIfNeeded() 48 | } 49 | 50 | private func setupBlur() { 51 | let blurView = getBlurView() 52 | blurView.frame = frame 53 | addSubview(blurView) 54 | } 55 | 56 | private func getBlurView() -> UIVisualEffectView { 57 | let colorMode = Theme.shared.currentModel?.colorMode ?? .light 58 | let style: UIBlurEffect.Style = colorMode == .light ? .light : .dark 59 | 60 | return UIVisualEffectView(effect: UIBlurEffect(style: style)) 61 | } 62 | 63 | private func setupColorPicker(with initialColor: UIColor) { 64 | colorPicker.delegate = self 65 | addSubview(colorPicker) 66 | addSubview(brightnessSlider) 67 | 68 | colorPicker.connect(brightnessSlider) 69 | _ = colorPicker.addHandle(at: initialColor) 70 | 71 | colorPicker.layoutIfNeeded() 72 | colorPicker.center = .init(x: center.x, y: center.y * 0.8) 73 | brightnessSlider.center = .init(x: center.x, y: center.y * 1.2) 74 | } 75 | 76 | private func dismiss() { 77 | if let currentColor = currentColor { 78 | selectedColor.accept(currentColor) 79 | } 80 | 81 | UIView.animate(withDuration: 0.15, delay: 0, options: .curveEaseInOut, animations: { 82 | self.alpha = 0 83 | }, completion: { fin in 84 | guard fin else { return } 85 | self.removeFromSuperview() 86 | }) 87 | } 88 | 89 | // MARK: ChromaColorPickerDelegate 90 | 91 | func colorPickerHandleDidChange(_ colorPicker: ChromaColorPicker, handle: ChromaColorHandle, to color: UIColor) { 92 | currentColor = color 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /MissCat/ViewModel/Details/PostDetailViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PostDetailViewModel.swift 3 | // MissCat 4 | // 5 | // Created by Yuiga Wada on 2019/12/01. 6 | // Copyright © 2019 Yuiga Wada. All rights reserved. 7 | // 8 | 9 | import MisskeyKit 10 | import RxSwift 11 | 12 | class PostDetailViewModel { 13 | let notes: PublishSubject<[NoteCell.Section]> = .init() 14 | let forceUpdateIndex: PublishSubject = .init() 15 | var dataSource: NotesDataSource? 16 | var cellCount: Int { return cellsModel.count } 17 | var owner: SecureUser? 18 | 19 | private var hasReactionGenCell: Bool = false 20 | var cellsModel: [NoteCell.Model] = [] // TODO: エラー再発しないか意識しておく 21 | 22 | private lazy var misskey: MisskeyKit? = { 23 | guard let owner = owner else { return nil } 24 | return MisskeyKit(from: owner) 25 | }() 26 | 27 | private lazy var model = PostDetailModel(from: misskey, owner: owner) 28 | 29 | // private lazy var model = PostDetailModel() 30 | 31 | // MARK: Life Cycle 32 | 33 | init(disposeBag: DisposeBag) {} 34 | 35 | func setItem(_ item: NoteCell.Model) { 36 | owner = item.owner 37 | cellsModel.append(item) 38 | updateNotes(new: cellsModel) 39 | 40 | DispatchQueue.global().async { 41 | self.goBackReplies(item) // リプライ先を遡る 42 | self.getReplies(item) 43 | } 44 | } 45 | 46 | // MARK: Setup 47 | 48 | // MARK: REST 49 | 50 | private func goBackReplies(_ item: NoteCell.Model) { 51 | guard let replyId = item.noteEntity.original?.replyId else { return } 52 | model.goBackReplies(id: replyId) { replies in 53 | self.cellsModel = replies.reversed() + self.cellsModel 54 | self.updateNotes(new: self.cellsModel) 55 | } 56 | } 57 | 58 | private func getReplies(_ item: NoteCell.Model) { 59 | guard let noteId = item.noteEntity.noteId else { return } 60 | model.getReplies(id: noteId) { replies in 61 | self.cellsModel += replies 62 | self.updateNotes(new: self.cellsModel) 63 | } 64 | } 65 | 66 | // MARK: Utilities 67 | 68 | private func updateNotes(new: [NoteCell.Model]) { 69 | updateNotes(new: [NoteCell.Section(items: new)]) 70 | } 71 | 72 | private func updateNotes(new: [NoteCell.Section]) { 73 | notes.onNext(new) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /MissCat/ViewModel/DirectMessage/DirectMessageViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DirectMessageViewModel.swift 3 | // MissCat 4 | // 5 | // Created by Yuiga Wada on 2020/04/16. 6 | // Copyright © 2020 Yuiga Wada. All rights reserved. 7 | // 8 | 9 | import MisskeyKit 10 | import RxCocoa 11 | import RxSwift 12 | import SwiftLinkPreview 13 | 14 | class DirectMessageViewModel: ViewModelType { 15 | // MARK: I/O 16 | 17 | struct Input { 18 | let owner: SecureUser 19 | var userId: String 20 | var sendTrigger: PublishRelay 21 | } 22 | 23 | struct Output { 24 | let messages: PublishRelay<[DirectMessage]> = .init() 25 | let sendCompleted: PublishRelay = .init() 26 | } 27 | 28 | struct State { 29 | let owner: SecureUser 30 | } 31 | 32 | private let input: Input 33 | let output: Output = .init() 34 | var state: State { return .init(owner: input.owner) } 35 | 36 | private var messages: [DirectMessage] = [] 37 | private lazy var misskey = MisskeyKit(from: input.owner) 38 | private lazy var model: DirectMessageModel = .init(from: misskey) 39 | 40 | private let disposeBag: DisposeBag 41 | 42 | // MARK: LifeCycle 43 | 44 | init(with input: Input, and disposeBag: DisposeBag) { 45 | self.input = input 46 | self.disposeBag = disposeBag 47 | binding(with: input) 48 | } 49 | 50 | private func binding(with input: Input) { 51 | input.sendTrigger.subscribe(onNext: { message in 52 | self.send(message) 53 | }).disposed(by: disposeBag) 54 | } 55 | 56 | func load(completion: (() -> Void)? = nil) { 57 | let untilId = messages.count > 0 ? messages[0].messageId : nil 58 | let option: DirectMessageModel.LoadOption = .init(userId: input.userId, untilId: untilId) 59 | 60 | model.load(with: option).subscribe(onNext: { loaded in 61 | self.messages.insert(loaded, at: 0) 62 | }, onCompleted: { 63 | self.output.messages.accept(self.messages) 64 | completion?() 65 | }).disposed(by: disposeBag) 66 | } 67 | 68 | func send(_ text: String) { 69 | model.send(to: input.userId, with: text).subscribe(onNext: { sent in 70 | self.messages.append(sent) 71 | }, onCompleted: { 72 | self.output.sendCompleted.accept(true) 73 | self.output.messages.accept(self.messages) 74 | }).disposed(by: disposeBag) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /MissCat/ViewModel/DirectMessage/MessageListViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MessageListViewModel.swift 3 | // MissCat 4 | // 5 | // Created by Yuiga Wada on 2020/04/16. 6 | // Copyright © 2020 Yuiga Wada. All rights reserved. 7 | // 8 | 9 | import MisskeyKit 10 | import RxCocoa 11 | import RxSwift 12 | 13 | class MessageListViewModel: ViewModelType { 14 | // MARK: I/O 15 | 16 | struct Input { 17 | let dataSource: SenderDataSource 18 | } 19 | 20 | struct Output { 21 | let users: PublishSubject<[SenderCell.Section]> = .init() 22 | } 23 | 24 | struct State { 25 | var isLoading: Bool = false 26 | var hasPrepared: Bool = false 27 | 28 | var hasAccounts: Bool { 29 | return Cache.UserDefaults.shared.getUsers().count > 0 30 | } 31 | } 32 | 33 | private let input: Input 34 | let output: Output = .init() 35 | var state: State = .init() 36 | 37 | var cellsModel: [SenderCell.Model] = [] 38 | private lazy var misskey: MisskeyKit? = { 39 | guard let owner = owner else { return nil } 40 | return MisskeyKit(from: owner) 41 | }() 42 | 43 | var owner: SecureUser? { 44 | didSet { 45 | guard let owner = owner else { return } 46 | model.change(from: MisskeyKit(from: owner), owner: owner) 47 | } 48 | } 49 | 50 | private lazy var model: MessageListModel = .init(from: misskey, owner: owner) 51 | 52 | private let disposeBag: DisposeBag 53 | 54 | // MARK: LifeCycle 55 | 56 | init(with input: Input, and disposeBag: DisposeBag) { 57 | self.input = input 58 | self.disposeBag = disposeBag 59 | owner = Cache.UserDefaults.shared.getCurrentUser() 60 | } 61 | 62 | func load() { 63 | state.hasPrepared = true 64 | loadHistory().subscribe(onError: { error in 65 | print(error) 66 | }, onCompleted: { 67 | DispatchQueue.main.async { 68 | self.updateUsers(new: self.cellsModel) 69 | } 70 | }, onDisposed: nil).disposed(by: disposeBag) 71 | } 72 | 73 | func removeAll() { 74 | cellsModel = [] 75 | updateUsers(new: cellsModel) 76 | } 77 | 78 | // MARK: Load 79 | 80 | // func loadUntilUsers() -> Observable { 81 | // guard let untilId = cellsModel[cellsModel.count - 1].userId else { 82 | // return Observable.create { _ in 83 | // Disposables.create() 84 | // } 85 | // } 86 | // 87 | // return loadUsers(untilId: untilId).do(onCompleted: { 88 | // self.updateUsers(new: self.cellsModel) 89 | // }) 90 | // } 91 | 92 | func loadHistory(untilId: String? = nil) -> Observable { 93 | state.isLoading = true 94 | return model.loadHistory().do(onNext: { cellModel in 95 | self.cellsModel.append(cellModel) 96 | }, onCompleted: { 97 | self.state.isLoading = false 98 | }) 99 | } 100 | 101 | // MARK: Rx 102 | 103 | private func updateUsers(new: [SenderCell.Model]) { 104 | updateUsers(new: [SenderCell.Section(items: new)]) 105 | } 106 | 107 | private func updateUsers(new: [SenderCell.Section]) { 108 | output.users.onNext(new) 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /MissCat/ViewModel/Main/AccountsListViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AccountsListViewModel.swift 3 | // MissCat 4 | // 5 | // Created by Yuiga Wada on 2020/06/08. 6 | // Copyright © 2020 Yuiga Wada. All rights reserved. 7 | // 8 | 9 | import MisskeyKit 10 | import RxCocoa 11 | import RxSwift 12 | import UIKit 13 | 14 | class AccountsListViewModel: ViewModelType { 15 | struct Input { 16 | let loginTrigger: Observable 17 | let editTrigger: Observable 18 | } 19 | 20 | struct Output { 21 | let accounts: PublishRelay<[AccountCell.Section]> = .init() 22 | let showLoginViewTrigger: PublishRelay = .init() 23 | let switchEditableTrigger: PublishRelay = .init() 24 | let switchNormalTrigger: PublishRelay = .init() 25 | let noAccountsTrigger: PublishRelay = .init() 26 | let relaunchTabsTrigger: PublishRelay = .init() 27 | } 28 | 29 | struct State { 30 | var isEditing: Bool = false 31 | var hasPrepared: Bool = false 32 | var hasAccounts: Bool { 33 | return Cache.UserDefaults.shared.getUsers().count > 0 34 | } 35 | } 36 | 37 | private let input: Input 38 | var output: Output = .init() 39 | var state: State = .init() 40 | var dataSource: AccountsListDataSource? 41 | 42 | var accounts: [AccountCell.Section] = [] 43 | private let disposeBag: DisposeBag 44 | private lazy var model = AccountsListModel() 45 | 46 | init(with input: Input, and disposeBag: DisposeBag) { 47 | self.input = input 48 | self.disposeBag = disposeBag 49 | transform() 50 | } 51 | 52 | func load() { 53 | let users = model.getUsers() 54 | 55 | accounts.removeAll() // 初期化しておく 56 | state.hasPrepared = true 57 | users.forEach { user in 58 | let account = AccountCell.Model(owner: user) 59 | let accountSection = AccountCell.Section(items: [account]) 60 | 61 | accounts.append(accountSection) 62 | } 63 | 64 | update(accounts) 65 | } 66 | 67 | func delete(index: Int) { 68 | let user = accounts[index].items[0].owner 69 | 70 | // アカウントを削除 71 | model.removeUser(user: user) 72 | accounts.remove(at: index) 73 | update(accounts) 74 | 75 | // タブをチェック 76 | let isChanged = model.checkTabs(for: user) 77 | 78 | if isChanged { 79 | output.relaunchTabsTrigger.accept(()) 80 | } 81 | 82 | // アカウントが残っているかチェック 83 | if accounts.count == 0 { 84 | output.noAccountsTrigger.accept(()) 85 | } 86 | } 87 | 88 | private func transform() { 89 | input.editTrigger.subscribe(onNext: { 90 | if self.state.isEditing { 91 | self.output.switchNormalTrigger.accept(()) 92 | } else { 93 | self.output.switchEditableTrigger.accept(()) 94 | } 95 | self.state.isEditing = !self.state.isEditing 96 | }).disposed(by: disposeBag) 97 | 98 | input.loginTrigger.subscribe(onNext: { 99 | self.output.showLoginViewTrigger.accept(()) 100 | }).disposed(by: disposeBag) 101 | } 102 | 103 | private func update(_ accounts: [AccountCell.Section]) { 104 | output.accounts.accept(accounts) 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /MissCat/ViewModel/Main/HomeViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HomeViewModel.swift 3 | // MissCat 4 | // 5 | // Created by Yuiga Wada on 2020/03/26. 6 | // Copyright © 2020 Yuiga Wada. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class HomeViewModel: ViewModelType { 12 | struct Input {} 13 | struct Output {} 14 | struct State {} 15 | 16 | private let model = HomeModel() 17 | 18 | func vote(choice: [Int], to noteId: String, owner: SecureUser) { 19 | model.vote(choice: choice, to: noteId, owner: owner) 20 | } 21 | 22 | func renote(noteId: String, owner: SecureUser) { 23 | model.renote(noteId: noteId, owner: owner) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /MissCat/ViewModel/Main/SearchViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SearchViewModel.swift 3 | // MissCat 4 | // 5 | // Created by Yuiga Wada on 2020/04/11. 6 | // Copyright © 2020 Yuiga Wada. All rights reserved. 7 | // 8 | 9 | import RxSwift 10 | 11 | class SearchViewModel: ViewModelType { 12 | struct Input {} 13 | struct Output {} 14 | struct State {} 15 | 16 | private let input: Input 17 | private let disposeBag: DisposeBag 18 | 19 | init(with input: Input, and disposeBag: DisposeBag) { 20 | self.input = input 21 | self.disposeBag = disposeBag 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /MissCat/ViewModel/Others/ViewModelType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewModelType.swift 3 | // MissCat 4 | // 5 | // Created by Yuiga Wada on 2019/12/13. 6 | // Copyright © 2019 Yuiga Wada. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | protocol ViewModelType { 12 | // Input: rx.tap等のイベントはtriggerとしてInputに打ち込むが、hogehoge.rx.textのようなBinderに対するbindingはView側で行う 13 | associatedtype Input 14 | associatedtype Output 15 | associatedtype State 16 | 17 | // func transform() -> Output 18 | } 19 | -------------------------------------------------------------------------------- /MissCat/ViewModel/Reusable/Accounts/AccountCellViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AccountCellViewModel.swift 3 | // MissCat 4 | // 5 | // Created by Yuiga Wada on 2020/06/08. 6 | // Copyright © 2020 Yuiga Wada. All rights reserved. 7 | // 8 | 9 | import MisskeyKit 10 | import RxCocoa 11 | import RxSwift 12 | import UIKit 13 | 14 | class AccountCellViewModel: ViewModelType { 15 | struct Input { 16 | let user: SecureUser 17 | } 18 | 19 | struct Output { 20 | let iconImage: PublishRelay = .init() 21 | 22 | let name: PublishRelay = .init() 23 | let username: PublishRelay = .init() 24 | let instance: PublishRelay = .init() 25 | } 26 | 27 | struct State {} 28 | 29 | private let input: Input 30 | var output: Output = .init() 31 | 32 | private let disposeBag: DisposeBag 33 | private lazy var misskey = MisskeyKit(from: input.user) 34 | private lazy var model = AccountCellModel(from: misskey) 35 | 36 | init(with input: Input, and disposeBag: DisposeBag) { 37 | self.input = input 38 | self.disposeBag = disposeBag 39 | } 40 | 41 | func transform() { 42 | let user = input.user 43 | 44 | // username 45 | let hasUsername = !user.username.isEmpty 46 | output.username.accept("@\(user.username)") 47 | 48 | // キャッシュから 49 | if let cache = Cache.shared.getUserInfo(user: user) { 50 | let name = cache.name 51 | let username = "@\(cache.username)" 52 | 53 | output.name.accept(name) 54 | output.username.accept(username) 55 | output.instance.accept(user.instance) 56 | output.iconImage.accept(cache.image) 57 | return 58 | } 59 | 60 | // APIから 61 | model.getAccountInfo { info in 62 | guard let info = info else { return } 63 | 64 | // icon 65 | _ = info.avatarUrl?.toUIImage { image in 66 | guard let image = image else { return } 67 | self.output.iconImage.accept(image) 68 | let userEntity = UserEntity(from: info) 69 | self.cache(user: user, userModel: userEntity, image: image) 70 | } 71 | 72 | // text 73 | 74 | let name = info.name 75 | let username = "@\(info.username ?? "")" 76 | 77 | self.output.name.accept(name ?? username) 78 | self.output.username.accept(username) 79 | self.output.instance.accept(user.instance) 80 | 81 | // username情報が保存されていない場合は保存しておく 82 | if !hasUsername { 83 | let secureUser = SecureUser(userId: user.userId, username: info.username ?? "", instance: user.instance, apiKey: user.apiKey) 84 | _ = Cache.UserDefaults.shared.saveUser(secureUser) 85 | } 86 | } 87 | } 88 | 89 | private func cache(user: SecureUser, userModel info: UserEntity, image: UIImage) { 90 | let username = info.username ?? "" 91 | let cache = Cache.UserInfo(user: user, 92 | name: info.name ?? username, 93 | username: username, 94 | host: user.instance, 95 | image: image) 96 | 97 | Cache.shared.saveUserInfo(info: cache) // キャッシュしておく 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /MissCat/ViewModel/Reusable/NoteCell/FileContainerViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FileContainerViewModel.swift 3 | // MissCat 4 | // 5 | // Created by Yuiga Wada on 2020/03/23. 6 | // Copyright © 2020 Yuiga Wada. All rights reserved. 7 | // 8 | 9 | import RxSwift 10 | 11 | class FileContainerViewModel: ViewModelType { 12 | // MARK: IO 13 | 14 | struct Input {} 15 | 16 | struct Output { 17 | let files: PublishSubject<[FileContainer.Section]> = .init() 18 | } 19 | 20 | struct State {} 21 | 22 | let output: Output = .init() 23 | 24 | var fileModel: [FileContainer.Model] = [] 25 | 26 | private let disposeBag: DisposeBag 27 | 28 | // MARK: Publics 29 | 30 | init(disposeBag: DisposeBag) { 31 | self.disposeBag = disposeBag 32 | } 33 | 34 | /// モデルをsetする 35 | func setFileModel(with arg: FileContainer.Arg) { 36 | guard arg.fileVisible else { return } 37 | 38 | let files = arg.files 39 | let fileCount = files.count 40 | 41 | for index in 0 ..< fileCount { 42 | let file = files[index] 43 | let fileType = checkFileType(file.type) 44 | 45 | guard fileType != .Unknown, 46 | let thumbnailUrl = file.thumbnailUrl, 47 | let original = file.url else { break } 48 | 49 | if fileType == .Audio { 50 | } else { 51 | fileModel.append(FileContainer.Model(thumbnailUrl: thumbnailUrl, 52 | originalUrl: original, 53 | isVideo: fileType == .Video, 54 | isSensitive: file.isSensitive ?? true)) 55 | } 56 | } 57 | updateFiles(new: fileModel) 58 | } 59 | 60 | func initialize() { 61 | fileModel = [] 62 | updateFiles(new: fileModel) 63 | } 64 | 65 | /// ファイルの種類を識別する 66 | /// - Parameter type: MIME Type 67 | func checkFileType(_ type: String?) -> FileType { 68 | guard let type = type else { return .Unknown } 69 | 70 | if type.contains("video") { 71 | return .Video 72 | } else if type.contains("audio") { 73 | return .Audio 74 | } 75 | 76 | let isImage: Bool = type.contains("image") 77 | let isGif: Bool = type.contains("gif") 78 | 79 | return isImage ? (isGif ? .GIFImage : .PlaneImage) : .Unknown 80 | } 81 | 82 | private func updateFiles(new: [FileContainer.Model]) { 83 | updateFiles(new: [FileContainer.Section(items: new)]) 84 | } 85 | 86 | private func updateFiles(new: [FileContainer.Section]) { 87 | output.files.onNext(new) 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /MissCat/ViewModel/Reusable/NoteCell/UrlPreviewerViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UrlPreviewerViewModel.swift 3 | // MissCat 4 | // 5 | // Created by Yuiga Wada on 2020/04/07. 6 | // Copyright © 2020 Yuiga Wada. All rights reserved. 7 | // 8 | 9 | import RxCocoa 10 | import RxSwift 11 | import SwiftLinkPreview 12 | 13 | class UrlPreviewerViewModel: ViewModelType { 14 | struct Input { 15 | let url: String 16 | let owner: SecureUser? 17 | } 18 | 19 | struct Output { 20 | let title: PublishRelay = .init() 21 | let description: PublishRelay = .init() 22 | let image: PublishRelay = .init() 23 | } 24 | 25 | struct State {} 26 | 27 | private let input: Input 28 | let output = Output() 29 | 30 | private let model = UrlPreviewerModel() 31 | private let disposeBag: DisposeBag 32 | private var imageSessionTasks: [URLSessionDataTask] = [] 33 | 34 | init(with input: Input, and disposeBag: DisposeBag) { 35 | self.input = input 36 | self.disposeBag = disposeBag 37 | getPreview() 38 | } 39 | 40 | func prepareForReuse() { 41 | imageSessionTasks.forEach { task in 42 | task.cancel() 43 | } 44 | imageSessionTasks.removeAll() 45 | } 46 | 47 | // MARK: Privates 48 | 49 | /// プレビューを取得 50 | private func getPreview() { 51 | model.getPreview(of: input.url, instance: input.owner?.instance ?? "misskey.io") { res in 52 | self.output.title.accept(res.title ?? "No Title") 53 | self.output.description.accept(res.description ?? "No Description") 54 | if let imageUrl = res.thumbnail { 55 | self.getImage(from: imageUrl) 56 | } 57 | } 58 | } 59 | 60 | /// urlからプレビューimageを取得 61 | /// - Parameter url: URL 62 | private func getImage(from url: String) { 63 | if let cache = Cache.shared.getUiImage(url: url) { 64 | output.image.accept(cache) 65 | return 66 | } 67 | 68 | let task = url.toUIImage { 69 | guard let image = $0 else { return } 70 | self.output.image.accept(image) 71 | Cache.shared.saveUiImage(image, url: url) 72 | } 73 | 74 | if let task = task { 75 | imageSessionTasks.append(task) 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /MissCat/ViewModel/Reusable/Notification/NotificationBannerViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NotificationBannerViewModel.swift 3 | // MissCat 4 | // 5 | // Created by Yuiga Wada on 2020/07/04. 6 | // Copyright © 2020 Yuiga Wada. All rights reserved. 7 | // 8 | 9 | import MisskeyKit 10 | import RxSwift 11 | 12 | // NotificationCellのViewModelをベースに。 13 | class NotificationBannerViewModel: NotificationCellViewModel { 14 | convenience init?(with contents: NotificationModel, and disposeBag: DisposeBag, owner: SecureUser) { 15 | let model: NotificationBannerModel = .init(from: nil, owner: nil) 16 | guard let item = model.getModel(notification: contents) else { return nil } 17 | 18 | // ownerを詰める 19 | item.owner = owner 20 | 21 | // Shapeしておく 22 | if item.type == .mention || item.type == .reply || item.type == .quote, let replyNote = item.replyNote { 23 | MFMEngine.shapeModel(replyNote) 24 | } else { 25 | MFMEngine.shapeModel(item) 26 | } 27 | 28 | let input = NotificationCellViewModel.Input(item: item) 29 | self.init(with: input, and: disposeBag) 30 | } 31 | 32 | convenience init?(with contents: NotificationCell.CustomModel, disposeBag: DisposeBag) { 33 | let model: NotificationBannerModel = .init(from: nil, owner: nil) 34 | let item = model.getModel(with: contents) 35 | 36 | let input = NotificationCellViewModel.Input(item: item) 37 | self.init(with: input, and: disposeBag) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /MissCat/ViewModel/Reusable/Tab/NavBarViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NavBarViewModel.swift 3 | // MissCat 4 | // 5 | // Created by Yuiga Wada on 2020/06/10. 6 | // Copyright © 2020 Yuiga Wada. All rights reserved. 7 | // 8 | 9 | import MisskeyKit 10 | import RxCocoa 11 | import RxSwift 12 | 13 | class NavBarViewModel: ViewModelType { 14 | struct Icon { 15 | let owner: SecureUser 16 | let image: UIImage 17 | } 18 | 19 | struct Input {} 20 | struct Output { 21 | let userIcon: PublishRelay = .init() 22 | } 23 | 24 | struct State {} 25 | 26 | private let input: Input? 27 | var output: Output = .init() 28 | private let disposeBag: DisposeBag 29 | 30 | private let model = NavBarModel() 31 | private var iconImages: [Icon] = [] 32 | 33 | init(with input: Input?, and disposeBag: DisposeBag) { 34 | self.input = input 35 | self.disposeBag = disposeBag 36 | } 37 | 38 | func transform(user: SecureUser) { 39 | let users = iconImages.filter { $0.owner.userId == user.userId } // アイコンのキャッシュを確認する 40 | 41 | if users.count > 0 { 42 | output.userIcon.accept(users[0].image) 43 | } else { // キャッシュがない場合 44 | guard let misskey = MisskeyKit(from: user) else { return } 45 | 46 | model.getIconImage(from: misskey) { image in 47 | let icon = Icon(owner: user, image: image) 48 | 49 | self.output.userIcon.accept(image) 50 | self.iconImages.append(icon) 51 | } 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /MissCat/ViewModel/Reusable/User/UserCellViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserCellViewModel.swift 3 | // MissCat 4 | // 5 | // Created by Yuiga Wada on 2020/04/13. 6 | // Copyright © 2020 Yuiga Wada. All rights reserved. 7 | // 8 | 9 | import RxCocoa 10 | import RxSwift 11 | import UIKit 12 | 13 | class UserCellViewModel: ViewModelType { 14 | // MARK: I/O 15 | 16 | struct Input { 17 | let owner: SecureUser 18 | let icon: String? 19 | let shapedName: MFMString? 20 | let shapedDescription: MFMString? 21 | let nameYanagi: YanagiText 22 | let descYanagi: YanagiText 23 | } 24 | 25 | struct Output { 26 | let icon: PublishRelay = .init() 27 | let name: PublishRelay = .init() 28 | let description: PublishRelay = .init() 29 | 30 | let backgroundColor: PublishRelay = .init() 31 | let separatorBackgroundColor: PublishRelay = .init() 32 | } 33 | 34 | struct State { 35 | var owner: SecureUser 36 | } 37 | 38 | private let input: Input 39 | let output: Output = .init() 40 | var state: State { return .init(owner: input.owner) } 41 | 42 | private let disposeBag: DisposeBag 43 | 44 | init(with input: Input, and disposeBag: DisposeBag) { 45 | self.input = input 46 | self.disposeBag = disposeBag 47 | } 48 | 49 | func transform() { 50 | // icon 51 | if let icon = input.icon { 52 | icon.toUIImage { image in 53 | guard let image = image else { return } 54 | self.output.icon.accept(image) 55 | } 56 | } 57 | 58 | // name 59 | output.name.accept(input.shapedName?.attributed) 60 | input.shapedName?.mfmEngine.renderCustomEmojis(on: input.nameYanagi) 61 | 62 | // description 63 | output.description.accept(input.shapedDescription?.attributed) 64 | input.shapedDescription?.mfmEngine.renderCustomEmojis(on: input.nameYanagi) 65 | 66 | // color 67 | output.backgroundColor.accept(Theme.shared.currentModel?.colorPattern.ui.base ?? .white) 68 | output.separatorBackgroundColor.accept(Theme.shared.currentModel?.colorPattern.ui.sub2 ?? .lightGray) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /MissCatShare/AccountsViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AccountsViewController.swift 3 | // MissCatShare 4 | // 5 | // Created by Yuiga Wada on 2020/08/08. 6 | // Copyright © 2020 Yuiga Wada. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | protocol AccountsProtocol { 12 | func switchAccount(userId: String) 13 | } 14 | 15 | class AccountsViewController: UITableViewController { 16 | private var accounts: [SecureUser] = [] 17 | var delegate: AccountsProtocol? 18 | 19 | convenience init(with accounts: [SecureUser]) { 20 | self.init() 21 | self.accounts = accounts 22 | } 23 | 24 | override func viewDidLoad() { 25 | super.viewDidLoad() 26 | clearsSelectionOnViewWillAppear = false 27 | view.backgroundColor = .clear 28 | } 29 | 30 | override func numberOfSections(in tableView: UITableView) -> Int { 31 | return 1 32 | } 33 | 34 | override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 35 | return accounts.count 36 | } 37 | 38 | override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 39 | let user = accounts[indexPath.row] 40 | let cell = UITableViewCell() 41 | 42 | cell.backgroundColor = .clear 43 | cell.textLabel?.text = "\(user.username)@\(user.instance)" 44 | 45 | return cell 46 | } 47 | 48 | override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 49 | let user = accounts[indexPath.row] 50 | delegate?.switchAccount(userId: user.userId) 51 | navigationController?.popViewController(animated: true) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /MissCatShare/Base.lproj/MainInterface.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 | -------------------------------------------------------------------------------- /MissCatShare/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleDisplayName 8 | MissCat 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleVersion 22 | 1 23 | NSExtension 24 | 25 | NSExtensionAttributes 26 | 27 | NSExtensionActivationRule 28 | 29 | NSExtensionActivationSupportsText 30 | 31 | NSExtensionActivationSupportsImageWithMaxCount 32 | 4 33 | NSExtensionActivationSupportsWebURLWithMaxCount 34 | 1 35 | 36 | 37 | NSExtensionMainStoryboard 38 | MainInterface 39 | NSExtensionPointIdentifier 40 | com.apple.share-services 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /MissCatShare/MissCatShare.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.application-groups 6 | 7 | group.yuwd.MissCat 8 | 9 | keychain-access-groups 10 | 11 | $(AppIdentifierPrefix)yuwd.MissCat.MissCatShare 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /MissCatShare/ShareAssets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /MissCatShare/ShareAssets.xcassets/MissCat.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "MisscatIcon_shaped-1.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "MisscatIcon_shaped-3.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "MisscatIcon_shaped-2.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /MissCatShare/ShareAssets.xcassets/MissCat.imageset/MisscatIcon_shaped-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YuigaWada/MissCat/574355469ebbcbe46d03a24c66630aeb5239018f/MissCatShare/ShareAssets.xcassets/MissCat.imageset/MisscatIcon_shaped-1.png -------------------------------------------------------------------------------- /MissCatShare/ShareAssets.xcassets/MissCat.imageset/MisscatIcon_shaped-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YuigaWada/MissCat/574355469ebbcbe46d03a24c66630aeb5239018f/MissCatShare/ShareAssets.xcassets/MissCat.imageset/MisscatIcon_shaped-2.png -------------------------------------------------------------------------------- /MissCatShare/ShareAssets.xcassets/MissCat.imageset/MisscatIcon_shaped-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YuigaWada/MissCat/574355469ebbcbe46d03a24c66630aeb5239018f/MissCatShare/ShareAssets.xcassets/MissCat.imageset/MisscatIcon_shaped-3.png -------------------------------------------------------------------------------- /MissCatShare/ShareAssets.xcassets/back.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "SprashBack.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "SprashBack-1.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "SprashBack-2.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /MissCatShare/ShareAssets.xcassets/back.imageset/SprashBack-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YuigaWada/MissCat/574355469ebbcbe46d03a24c66630aeb5239018f/MissCatShare/ShareAssets.xcassets/back.imageset/SprashBack-1.png -------------------------------------------------------------------------------- /MissCatShare/ShareAssets.xcassets/back.imageset/SprashBack-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YuigaWada/MissCat/574355469ebbcbe46d03a24c66630aeb5239018f/MissCatShare/ShareAssets.xcassets/back.imageset/SprashBack-2.png -------------------------------------------------------------------------------- /MissCatShare/ShareAssets.xcassets/back.imageset/SprashBack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YuigaWada/MissCat/574355469ebbcbe46d03a24c66630aeb5239018f/MissCatShare/ShareAssets.xcassets/back.imageset/SprashBack.png -------------------------------------------------------------------------------- /MissCatShare/UserModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserModel.swift 3 | // MissCatShare 4 | // 5 | // Created by Yuiga Wada on 2020/08/07. 6 | // Copyright © 2020 Yuiga Wada. All rights reserved. 7 | // 8 | 9 | import KeychainAccess 10 | import MisskeyKit 11 | 12 | class SecureUser: Codable { 13 | let userId: String 14 | let instance: String 15 | let username: String 16 | var apiKey: String? 17 | 18 | init(userId: String, username: String, instance: String, apiKey: String?) { 19 | self.userId = userId 20 | self.username = username 21 | self.instance = instance 22 | self.apiKey = apiKey 23 | } 24 | } 25 | 26 | class UserModel { 27 | // MARK: Stores 28 | 29 | private lazy var userDefaults = Foundation.UserDefaults(suiteName: "group.yuwd.MissCat")! 30 | private lazy var keychain = Keychain(service: "yuwd.MissCat", accessGroup: "group.yuwd.MissCat") 31 | 32 | // MARK: Keys 33 | 34 | private let savedUserKey = "saved-user" 35 | private let currentUserIdKey = "current-user-id" 36 | private let currentVisibilityKey = "current-visibility" 37 | 38 | // MARK: User Itself 39 | 40 | /// 保存されている全てのユーザー情報を取得する 41 | func getUsers() -> [SecureUser] { 42 | guard let data = userDefaults.data(forKey: savedUserKey), 43 | let users = try? JSONDecoder().decode([SecureUser].self, from: data), 44 | users.count > 0 else { return [] } 45 | 46 | var noApiKeyUserIds: [String] = [] 47 | 48 | // apikeyをキーチェーンから取り出して詰め替えていく 49 | let _users: [SecureUser] = users.compactMap { 50 | guard let apiKey = self.keychain[$0.userId] else { noApiKeyUserIds.append($0.userId); return nil } 51 | return SecureUser(userId: $0.userId, username: $0.username, instance: $0.instance, apiKey: apiKey) 52 | } 53 | 54 | if noApiKeyUserIds.count > 0 { // apiKeyを持っていないユーザーは削除する 55 | guard let usersData = try? JSONEncoder().encode(users.filter { !noApiKeyUserIds.contains($0.userId) }) else { return _users } 56 | userDefaults.set(usersData, forKey: savedUserKey) 57 | } 58 | 59 | return _users 60 | } 61 | 62 | /// 現在ログイン中のユーザーデータを取得する 63 | func getCurrentUser() -> SecureUser? { 64 | // currentUserIdを持つアカウントを探す 65 | guard let currentUserId = getCurrentUserId(), let currentUser = getUser(userId: currentUserId) else { 66 | let savedUser = getUsers() 67 | return savedUser.count > 0 ? savedUser[0] : nil 68 | } 69 | 70 | return currentUser 71 | } 72 | 73 | /// 指定されたuserIdのユーザーを取得する 74 | func getUser(userId: String) -> SecureUser? { 75 | var user: SecureUser? 76 | let savedUser = getUsers() 77 | savedUser.forEach { 78 | if userId == $0.userId { user = $0; return } 79 | } 80 | 81 | return user 82 | } 83 | 84 | /// 現在ログイン中のユーザーのuserIdを取得する 85 | func getCurrentUserId() -> String? { 86 | return userDefaults.string(forKey: currentUserIdKey) 87 | } 88 | 89 | // MARK: Visiblity 90 | 91 | func getCurrentVisibility() -> Visibility? { 92 | guard let raw = userDefaults.string(forKey: currentVisibilityKey) else { return nil } 93 | return Visibility(rawValue: raw) 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /MissCatShare/VisibilityViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VisibilityViewController.swift 3 | // MissCatShare 4 | // 5 | // Created by Yuiga Wada on 2020/08/08. 6 | // Copyright © 2020 Yuiga Wada. All rights reserved. 7 | // 8 | 9 | import MisskeyKit 10 | import UIKit 11 | 12 | protocol VisibilityProtocol { 13 | func switchVisibility(_ visibility: Visibility) 14 | } 15 | 16 | class VisibilityViewController: UITableViewController { 17 | private var visibilities: [Visibility] = [.public, .home, .followers] 18 | var delegate: VisibilityProtocol? 19 | 20 | override func viewDidLoad() { 21 | super.viewDidLoad() 22 | clearsSelectionOnViewWillAppear = false 23 | view.backgroundColor = .clear 24 | } 25 | 26 | override func numberOfSections(in tableView: UITableView) -> Int { 27 | return 1 28 | } 29 | 30 | override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 31 | return visibilities.count 32 | } 33 | 34 | override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 35 | let visibility = visibilities[indexPath.row] 36 | let cell = UITableViewCell() 37 | 38 | cell.backgroundColor = .clear 39 | cell.textLabel?.text = visibility.rawValue 40 | 41 | return cell 42 | } 43 | 44 | override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 45 | let visibility = visibilities[indexPath.row] 46 | delegate?.switchVisibility(visibility) 47 | navigationController?.popViewController(animated: true) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /MissCatTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /MissCatTests/MissCatTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MissCatTests.swift 3 | // MissCatTests 4 | // 5 | // Created by Yuiga Wada on 2019/11/07. 6 | // Copyright © 2019 Yuiga Wada. All rights reserved. 7 | // 8 | 9 | @testable import MissCat 10 | import UIKit 11 | import XCTest 12 | 13 | class MissCatTests: XCTestCase { 14 | override func setUp() { 15 | // Put setup code here. This method is called before the invocation of each test method in the class. 16 | } 17 | 18 | override func tearDown() { 19 | // Put teardown code here. This method is called after the invocation of each test method in the class. 20 | } 21 | 22 | func testExample() { 23 | let text = NSTextAttachment() 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Podfile: -------------------------------------------------------------------------------- 1 | use_frameworks! 2 | # plugin 'cocoapods-binary' 3 | # enable_bitcode_for_prebuilt_frameworks! 4 | 5 | target 'MissCatShare' do 6 | pod 'MisskeyKit' 7 | pod 'KeychainAccess', :binary => true 8 | end 9 | 10 | 11 | target 'MissCat' do 12 | 13 | pod 'MisskeyKit' 14 | pod 'PolioPager' 15 | pod 'SwiftFormat/CLI' 16 | pod 'Starscream','3.1.1' 17 | 18 | pod 'KeychainAccess', :binary => true 19 | pod 'Firebase/Messaging', :binary => true 20 | pod 'RxSwift', '~> 5', :binary => true 21 | pod 'RxCocoa', '~> 5', :binary => true 22 | pod 'RxDataSources', :binary => true 23 | pod 'Agrume', :binary => true 24 | pod 'SVGKit', :git => 'https://github.com/SVGKit/SVGKit.git', :branch => '3.x', :binary => true 25 | pod "SkeletonView",'1.8.2', :binary => true 26 | pod 'FloatingPanel', '1.7.4', :binary => true 27 | pod 'Gifu', :binary => true 28 | pod 'iOSPhotoEditor', :binary => true 29 | pod 'XLPagerTabStrip', :binary => true 30 | pod 'APNGKit', '~> 1.0', :binary => true 31 | pod 'SwiftLinkPreview', '~> 3.1.0', :binary => true 32 | pod 'Cache', :binary => true 33 | pod 'MessageKit', :binary => true 34 | pod 'Eureka', :binary => true 35 | pod 'ChromaColorPicker', :binary => true 36 | pod 'XLActionController', :binary => true 37 | pod 'XLActionController/Twitter' 38 | pod 'XLActionController/Tweetbot' 39 | end 40 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MissCat 2 | 3 |
4 | 5 | 6 | 7 |
8 | 9 | [公式ページ](https://yuiga.dev/misscat) 10 | 11 | 12 | MissCatはMisskeyに特化したiOS向けのネイティブアプリです。 13 | 14 |

15 | ## Missions & Goals 16 | 17 | MissCatのミッションは以下の5つです。 18 | 19 | - Misskeyをより身近なSNSにすること 20 | - スマホに適した直感的な操作性を提供すること 21 | - インスタンスのバージョン差分を意識しないMisskey環境を提供すること 22 | - iOSらしく分かりやすいデザインを提供すること 23 | - Misskeyを広めること 24 | 25 |

26 | これらに応じて、順に以下のようなゴールを設定しています。 27 |

28 | 29 | 30 | 31 | - iOSネイティブアプリの提供・通知機能や拡張機能の実装 32 | - スワイプやタップ、半モーダルによる直感的な操作と快適な画面遷移 33 | - めいすきー等のサポート 34 | - 奇抜なデザインを用いない、iOSらしいデザイン設計 35 | - 新規登録画面までの動線 36 | 37 |

38 | ## Technical Aspects 39 | 40 | ### カスタム絵文字 41 | 42 | Misskeyにはカスタム絵文字というモノが存在します。 43 | 44 | 一般に、複数行の文字列表示にはUITextViewを使いますが、純正のUITextViewが対応しているのは静止画(UIImage)の挿入のみで、GIFアニメやAPNGといったアニメーション画像を挿入することはできません。また、非同期で取得した画像を挿入することもできません。 45 | 46 | そこで、UITextViewをベースに、任意のUIViewを挿入することができる[YanagiText](https://github.com/YuigaWada/YanagiText)というライブラリを作りました。 47 | 48 | MissCatではこの[YanagiText](https://github.com/YuigaWada/YanagiText)がカスタム絵文字を支える大きな基盤となっています。 49 | 50 | また、APNGの表示は[APNGKit](https://github.com/onevcat/APNGKit), アニメGIFの表示は[Gifu](https://github.com/kaishin/Gifu)を利用しています。 51 | 52 | 53 | 54 |

55 | ### MFM(Misskey Flavored Markdown) 56 | 57 | Misskeyでは、独自の構文MFMを用いることで文章の修飾をすることができます。([参照](https://join.misskey.page/ja/wiki/usage/mfm)) 58 | 59 | MissCatでは、Foundationの[NSAttributedString](https://developer.apple.com/documentation/foundation/nsattributedstring)を利用することで一部のMFMに対応しています。 60 | 61 |

62 | 63 | ### 通知 64 | MissCatはWeb版のMisskeyと同じように、WebPushと呼ばれる技術を利用して通知システムを構築しています。 65 | 66 |
67 | 68 | Web版Misskeyは[`'sw/register'`](https://misskey.io/api-doc#operation/sw/register)というエンドポイントを叩き、イベントが発生した際に通知がブラウザにPushされるよう登録します。 69 | 70 | MissCatでは、この仕組みを利用して、Misskey側で発火した通知イベントを、そのままサーバーへ送るよう[`'sw/register'`](https://misskey.io/api-doc#operation/sw/register)へ登録します。サーバー側は暗号化された通知メッセージを受け取ると、メッセージを復号して、適切なフォーマットに変換し、Firebaseへ投げることで各端末に通知を届けます。 71 | 72 | ※WebPushはprime256v1と言う楕円曲線暗号を元にメッセージが暗号化されているため、サーバーで通知メッセージを受け取った後、適切なカタチで復号してあげる必要があります。そこで、通知の実装にあたり、サーバー側で通知メッセージを復号するモジュールを作りました。[webPushDecipher.js](https://github.com/YuigaWada/MissCat/blob/develop/ApiServer/webPushDecipher.js) 73 | 74 |

75 | 76 | ### タブ 77 | 78 | 上タブは私の作った[PolioPager](https://github.com/YuigaWada/PolioPager)というライブラリを使っています。 79 | 80 |

81 | ## Others 82 | 83 | - コード汚いです 84 | - 実装していない機能が山程あります 85 | - バグも山程あります 86 | 87 |

88 | 89 | MissCatは皆様のご意見をお待ちしております。 90 | 91 | [Twitter](https://twitter.com/yuigawada)か[Misskey](https://misskey.io/@wada)にて、ご気軽にリプライ/DMを頂けると幸いです。 92 | -------------------------------------------------------------------------------- /images/Banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YuigaWada/MissCat/574355469ebbcbe46d03a24c66630aeb5239018f/images/Banner.png -------------------------------------------------------------------------------- /images/ScreenShot/iPhone Pro/Simulator Screen Shot - iPhone 8 Plus - 2020-03-29 at 00.27.35.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YuigaWada/MissCat/574355469ebbcbe46d03a24c66630aeb5239018f/images/ScreenShot/iPhone Pro/Simulator Screen Shot - iPhone 8 Plus - 2020-03-29 at 00.27.35.png -------------------------------------------------------------------------------- /images/ScreenShot/iPhone Pro/Simulator Screen Shot - iPhone 8 Plus - 2020-03-29 at 00.28.09.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YuigaWada/MissCat/574355469ebbcbe46d03a24c66630aeb5239018f/images/ScreenShot/iPhone Pro/Simulator Screen Shot - iPhone 8 Plus - 2020-03-29 at 00.28.09.png -------------------------------------------------------------------------------- /images/ScreenShot/iPhone Pro/Simulator Screen Shot - iPhone 8 Plus - 2020-03-29 at 00.28.14.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YuigaWada/MissCat/574355469ebbcbe46d03a24c66630aeb5239018f/images/ScreenShot/iPhone Pro/Simulator Screen Shot - iPhone 8 Plus - 2020-03-29 at 00.28.14.png -------------------------------------------------------------------------------- /images/ScreenShot/iPhone Pro/Simulator Screen Shot - iPhone 8 Plus - 2020-03-29 at 00.28.28.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YuigaWada/MissCat/574355469ebbcbe46d03a24c66630aeb5239018f/images/ScreenShot/iPhone Pro/Simulator Screen Shot - iPhone 8 Plus - 2020-03-29 at 00.28.28.png -------------------------------------------------------------------------------- /images/ScreenShot/iPhone Pro/Simulator Screen Shot - iPhone 8 Plus - 2020-03-29 at 00.28.44.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YuigaWada/MissCat/574355469ebbcbe46d03a24c66630aeb5239018f/images/ScreenShot/iPhone Pro/Simulator Screen Shot - iPhone 8 Plus - 2020-03-29 at 00.28.44.png -------------------------------------------------------------------------------- /images/ScreenShot/iPhoneX/Simulator Screen Shot - iPhone 11 Pro Max - 2020-03-28 at 19.32.36.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YuigaWada/MissCat/574355469ebbcbe46d03a24c66630aeb5239018f/images/ScreenShot/iPhoneX/Simulator Screen Shot - iPhone 11 Pro Max - 2020-03-28 at 19.32.36.png -------------------------------------------------------------------------------- /images/ScreenShot/iPhoneX/Simulator Screen Shot - iPhone 11 Pro Max - 2020-03-28 at 19.32.58.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YuigaWada/MissCat/574355469ebbcbe46d03a24c66630aeb5239018f/images/ScreenShot/iPhoneX/Simulator Screen Shot - iPhone 11 Pro Max - 2020-03-28 at 19.32.58.png -------------------------------------------------------------------------------- /images/ScreenShot/iPhoneX/Simulator Screen Shot - iPhone 11 Pro Max - 2020-03-28 at 19.33.13.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YuigaWada/MissCat/574355469ebbcbe46d03a24c66630aeb5239018f/images/ScreenShot/iPhoneX/Simulator Screen Shot - iPhone 11 Pro Max - 2020-03-28 at 19.33.13.png -------------------------------------------------------------------------------- /images/ScreenShot/iPhoneX/Simulator Screen Shot - iPhone 11 Pro Max - 2020-03-28 at 19.33.57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YuigaWada/MissCat/574355469ebbcbe46d03a24c66630aeb5239018f/images/ScreenShot/iPhoneX/Simulator Screen Shot - iPhone 11 Pro Max - 2020-03-28 at 19.33.57.png -------------------------------------------------------------------------------- /images/ScreenShot/iPhoneX/Simulator Screen Shot - iPhone 11 Pro Max - 2020-03-28 at 19.34.44.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YuigaWada/MissCat/574355469ebbcbe46d03a24c66630aeb5239018f/images/ScreenShot/iPhoneX/Simulator Screen Shot - iPhone 11 Pro Max - 2020-03-28 at 19.34.44.png -------------------------------------------------------------------------------- /images/ScreenShot/iPhoneX/Simulator Screen Shot - iPhone 11 Pro Max - 2020-03-28 at 19.35.01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YuigaWada/MissCat/574355469ebbcbe46d03a24c66630aeb5239018f/images/ScreenShot/iPhoneX/Simulator Screen Shot - iPhone 11 Pro Max - 2020-03-28 at 19.35.01.png -------------------------------------------------------------------------------- /python_utls/library_list.txt: -------------------------------------------------------------------------------- 1 | MisskeyKit 2 | YanagiText 3 | PolioPager 4 | AWSSNS 5 | AWSCognito 6 | Starscream 7 | RxSwift 8 | RxCocoa 9 | RxDataSources 10 | Agrume 11 | SVGKit 12 | SkeletonView 13 | FloatingPanel 14 | Gifu 15 | photo-editor 16 | XLPagerTabStrip 17 | APNGKit 18 | SwiftLinkPreview 19 | Cache 20 | MessageKit 21 | Eureka 22 | ChromaColorPicker 23 | XLActionController -------------------------------------------------------------------------------- /python_utls/licenser.py: -------------------------------------------------------------------------------- 1 | import requests 2 | from github import Github 3 | 4 | g = Github("username", "password") 5 | 6 | output = "[" 7 | libraries = [] 8 | with open('library_list.txt', 'r') as f: 9 | line = f.readline() 10 | while line: 11 | target_library = line.strip() 12 | libraries.append(target_library) 13 | line = f.readline() 14 | f.close() 15 | 16 | for target_library in libraries: 17 | repositories = g.search_repositories(query=target_library+' language:swift') 18 | org = repositories[0].full_name.split('/')[0] 19 | 20 | for tail in ["",".md"]: #LICENSE / LICENSE.mdどっちかのパターンがあるっぽい 21 | license_url = 'https://raw.githubusercontent.com/' + repositories[0].full_name +'/master/LICENSE' + tail 22 | print(repositories) 23 | print(license_url) 24 | 25 | req = requests.get(license_url) 26 | if req.status_code == 200: 27 | raw_license = req.text 28 | triple_quote = '"' * 3 29 | output += "\"{}\": {}\n{}\n{},\n".format(target_library, triple_quote, raw_license, triple_quote) 30 | # else: 31 | # print("**ERROR!**≠\nstatus code: not 200⇛",license_url) 32 | 33 | output = output[:-2] + "]" # 最後のコロンを無視 34 | with open('COMBINED_LICENSE', mode='w') as f: 35 | f.write(output) 36 | --------------------------------------------------------------------------------