├── .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 |
--------------------------------------------------------------------------------