From 0ebf81832bfdce8ed9ba1f54abeb588acbf98d3c Mon Sep 17 00:00:00 2001 From: YuigaWada Date: Sun, 17 May 2020 19:49:42 +0900 Subject: [PATCH] =?UTF-8?q?v1.0.11=20=E2=86=92=20v1.1.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci.yml | 2 + .gitignore | 1 + ApiServer/README.md | 14 + ApiServer/api.js | 73 ++ ApiServer/key/edch_private_key.txt | 0 ApiServer/key/fcm_private_key.json | 0 ApiServer/key/keyGenerator.js | 10 + ApiServer/notification.js | 64 + ApiServer/test/fcm_token.txt | 0 ApiServer/test/mock.json | 1 + ApiServer/test/mock.text | 1 + ApiServer/test/test_decipher.js | 15 + ApiServer/test/test_join.js | 29 + ApiServer/test/test_notification.js | 15 + ApiServer/webPushDecipher.js | 132 ++ CHANGELOG.md | 20 + MissCat.xcodeproj/project.pbxproj | 48 +- MissCat/GoogleService-Info.plist | 36 + MissCat/MFMEngine/MFMImageView.swift | 2 +- MissCat/Model/Main/HomeModel.swift | 10 +- MissCat/Model/Main/NotificationModel.swift | 4 +- MissCat/Model/Main/TimelineModel.swift | 239 ++-- .../Model/Settings/ProfileSettingsModel.swift | 43 + MissCat/Others/App/AppDelegate.swift | 243 +--- MissCat/Others/App/Bridge.h | 3 +- MissCat/Others/App/MisscatApi.swift | 64 + MissCat/Others/Extension/UIView+MissCat.swift | 9 + .../UIViewController+PhotoEditor.swift | 39 + MissCat/Others/Utilities/Cache.swift | 6 + .../Others/Utilities/MissCatImageView.swift | 23 + .../Others/Utilities/MissCatTableView.swift | 38 +- MissCat/Others/Utilities/RxEureka.swift | 29 + MissCat/View/Base.lproj/Main.storyboard | 64 +- .../View/Details/ProfileViewController.swift | 66 +- MissCat/View/Login/StartViewController.swift | 28 +- MissCat/View/Main/HomeViewController.swift | 11 +- .../Main/NotificationsViewController.swift | 3 +- MissCat/View/Main/PostViewController.swift | 44 +- MissCat/View/Main/SearchViewController.swift | 4 +- .../View/Main/TimelineViewController.swift | 14 +- MissCat/View/Reusable/Emoji/EmojiView.swift | 21 +- .../Reusable/NoteCell/FileContainer.swift | 14 +- MissCat/View/Reusable/NoteCell/NoteCell.swift | 18 +- MissCat/View/Reusable/NoteCell/PollView.swift | 371 +++++- .../View/Reusable/NoteCell/ReactionCell.swift | 72 +- .../View/Reusable/NoteCell/nib/PollView.xib | 17 +- .../View/Reusable/Others/NoteDisplay.swift | 7 +- .../Reaction/ReactionGenViewController.swift | 12 +- MissCat/View/Reusable/User/UserCell.swift | 4 +- .../User/UserListViewController.swift | 5 +- MissCat/View/Reusable/User/nib/UserCell.xib | 4 +- .../View/Settings/AccountViewController.swift | 12 +- .../View/Settings/LicenseViewController.swift | 1164 +++++++++++------ .../ProfileSettingsViewController.swift | 486 +++++++ .../Settings/SettingsViewController.swift | 18 +- .../ViewModel/Details/ProfileViewModel.swift | 158 ++- MissCat/ViewModel/Main/HomeViewModel.swift | 2 +- MissCat/ViewModel/Main/PostViewModel.swift | 16 + .../ViewModel/Main/TimelineViewModel.swift | 41 +- .../Reusable/NoteCell/NoteCellViewModel.swift | 35 +- .../NotificationCellViewModel.swift | 2 +- .../Settings/ProfileSettingsViewModel.swift | 181 +++ Podfile | 3 +- Podfile.lock | 113 +- python_utls/COMBINED_LICENSE | 527 +++++--- python_utls/library_list.txt | 25 +- python_utls/licenser.py | 9 +- 67 files changed, 3548 insertions(+), 1236 deletions(-) create mode 100644 ApiServer/README.md create mode 100644 ApiServer/api.js create mode 100644 ApiServer/key/edch_private_key.txt create mode 100644 ApiServer/key/fcm_private_key.json create mode 100644 ApiServer/key/keyGenerator.js create mode 100644 ApiServer/notification.js create mode 100644 ApiServer/test/fcm_token.txt create mode 100644 ApiServer/test/mock.json create mode 100644 ApiServer/test/mock.text create mode 100644 ApiServer/test/test_decipher.js create mode 100644 ApiServer/test/test_join.js create mode 100644 ApiServer/test/test_notification.js create mode 100644 ApiServer/webPushDecipher.js create mode 100644 MissCat/GoogleService-Info.plist create mode 100644 MissCat/Model/Settings/ProfileSettingsModel.swift create mode 100644 MissCat/Others/App/MisscatApi.swift create mode 100644 MissCat/Others/Utilities/MissCatImageView.swift create mode 100644 MissCat/Others/Utilities/RxEureka.swift create mode 100644 MissCat/View/Settings/ProfileSettingsViewController.swift create mode 100644 MissCat/ViewModel/Settings/ProfileSettingsViewModel.swift diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a6b99bc..0325361 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,6 +15,8 @@ jobs: - uses: actions/checkout@v2 - name: Select Xcode version run: sudo xcode-select -s '/Applications/Xcode_11.3.1.app/Contents/Developer' + - name: Inject api url + run: echo ${{ secrets.API_KEY_MANAGER_CODE }} > ./MissCat/Others/App/ApiKeyManager.swift - name: Cache CocoaPods files uses: actions/cache@v1 with: diff --git a/.gitignore b/.gitignore index ebc40f7..c3dd962 100644 --- a/.gitignore +++ b/.gitignore @@ -96,3 +96,4 @@ Pods MissCat.xcworkspace/xcuserdata/yuigawada.xcuserdatad/xcdebugger/Expressions.xcexplist +MissCat/Others/App/ApiKeyManager.swift diff --git a/ApiServer/README.md b/ApiServer/README.md new file mode 100644 index 0000000..2d9cb1a --- /dev/null +++ b/ApiServer/README.md @@ -0,0 +1,14 @@ +# webPushDecipher.js +[```'sw/register'```](https://misskey.io/api-doc#operation/sw/register)で購読したPush通知をdecryptするヤツ + +### **参考にしたもの** + +- https://tools.ietf.org/html/rfc8188 + +- https://tools.ietf.org/html/rfc8291 + +- https://tools.ietf.org/html/rfc8291#appendix-A + +- https://gist.github.com/tateisu/685eab242549d9c9ffc85020f09a4b71 + +- https://mastodon.juggler.jp/@tateisu/104098620591598243 diff --git a/ApiServer/api.js b/ApiServer/api.js new file mode 100644 index 0000000..c349629 --- /dev/null +++ b/ApiServer/api.js @@ -0,0 +1,73 @@ +'use strict'; + +const fs = require('fs'); +const express = require('express'); +const bodyParser = require('body-parser'); + +const notification = require('./notification.js'); +const webPushDecipher = require('./webPushDecipher.js'); + +// For Distributed +const authSecret = "Q8Zgu-WDvN5EDT_emFGovQ" +const publicKey = "BJNAJpIOIJnXVVgCTAd4geduXEsNKre0XVvz0j-E_z-8CbGI6VaRPsVI7r-hF88MijMBZApurU2HmSNQ4e-cTmA" +const privateKey = fs.readFileSync('./key/edch_private_key.txt', 'utf8'); + + +// For test +// const authSecret = "43w_wOVYeF9XzyRyZL3O8g" +// const publicKey = "BJgVD2cj1pNKNR2Ss3U_8e7P9AyoL5kWaxVio5aO16Cvnx-P1r7HH8SRb-h5tuxaydZ1ky3oO0V40s6t_uN1SdA" +// const privateKey = "ciQ800G-6jyKWf6KKG94g5rCSU_l_rgbHbyHny_UsIM" + + + +function decrypt(raw) { + // const converted = raw.toString('utf-8') // for debug + const converted = raw.toString('base64') + + const reciverKey = webPushDecipher.reciverKeyBuilder(publicKey,privateKey,authSecret) + var decrypted = webPushDecipher.decrypt(converted,reciverKey,false) + return decrypted +} + +const app = express(); + +var concat = require('concat-stream'); +app.use(function(req, res, next){ + req.pipe(concat(function(data){ + req.body = data; + next(); + })); +}); + + +app.post("/api/:version/push/:lang/:userId/:deviceToken", function(req, res){ + if (req.params.version != "v1") { res.status(410).send('Invalid Version.').end(); } + + const rawBody = req.body; + if (!rawBody) { res.status(200).send('Invalid Body.').end(); } + + const rawJson = decrypt(rawBody); + const userId = req.params.userId; + const deviceToken = req.params.deviceToken; + const lang = req.params.lang; + if (!rawJson||!userId||!deviceToken||!lang) { res.status(410).send('Invalid Url.').end(); } + + console.log(rawJson) + const contents = notification.generateContents(rawJson,lang); + const title = contents[0]; + const body = contents[1]; + if (!title) { res.status(200).send('Invalid Json.').end(); } + + // console.log("deviceToken",deviceToken); + notification.send(deviceToken, title, body); // send! + res.status(200).send('Ok').end(); +}); + +// Start the server +const PORT = process.env.PORT || 8080; +app.listen(PORT, () => { + console.log(`App listening on port ${PORT}`); + console.log('Press Ctrl+C to quit.'); +}); + +module.exports = app; diff --git a/ApiServer/key/edch_private_key.txt b/ApiServer/key/edch_private_key.txt new file mode 100644 index 0000000..e69de29 diff --git a/ApiServer/key/fcm_private_key.json b/ApiServer/key/fcm_private_key.json new file mode 100644 index 0000000..e69de29 diff --git a/ApiServer/key/keyGenerator.js b/ApiServer/key/keyGenerator.js new file mode 100644 index 0000000..63bf015 --- /dev/null +++ b/ApiServer/key/keyGenerator.js @@ -0,0 +1,10 @@ +const crypto = require('crypto'); +const util = require('util'); +const urlsafeBase64 = require('urlsafe-base64'); + +const keyCurve = crypto.createECDH('prime256v1'); +keyCurve.generateKeys(); + +console.log("public:", urlsafeBase64.encode(keyCurve.getPublicKey())); +console.log("private:", urlsafeBase64.encode(keyCurve.getPrivateKey())); +console.log("auth:", urlsafeBase64.encode(crypto.randomBytes(16))); diff --git a/ApiServer/notification.js b/ApiServer/notification.js new file mode 100644 index 0000000..4e13bc7 --- /dev/null +++ b/ApiServer/notification.js @@ -0,0 +1,64 @@ +const fcmNode = require('fcm-node'); +const serverKey = require('./key/fcm_private_key.json'); +const fcm = new fcmNode(serverKey); + +exports.generateContents = function(rawJson, lang) { + const json = JSON.parse(rawJson); + const body = json.body; + if (json.type != "notification") { return null; } + + const type = body.type; + const fromUser = body.user.name != null ? body.user.name : body.user.username; + + // cf. https://github.com/YuigaWada/MissCat/blob/develop/MissCat/Model/Main/NotificationModel.swift + if (type == "reaction") { + const reaction = body.reaction; + const myNote = body.note.text; + + var title = fromUser + "さんがリアクション\"" + reaction+ "\"を送信しました"; + var message = myNote; + return [title,message]; + } + else if (type == "follow") { + const hostLabel = body.user.host != null ? "@" + body.user.host : ""; // 自インスタンスの場合 host == nullになる + var title = ""; + var message = "@" + body.user.username + hostLabel + "さんに" + "フォローされました"; + return [title,message]; + } + else if (type == "reply") { + var title = fromUser + "さんの返信:"; + var message = body.note.text; + return [title,message]; + } + else if (type == "renote" || type == "quote") { + const justRenote = body.note.text == null; // 引用RNでなければ body.note.text == null + var renoteKind = justRenote ? "" : "引用"; + + var title = fromUser + "さんが" + renoteKind + "Renoteしました"; + var message = justRenote ? body.note.renote.text : body.note.text; + return [title,message]; + } + + return [null,null] +} + + +exports.send = function (token, title, body) { + var message = { + to: token, + // collapse_key: key, + + notification: { + title: title, + body: body, + badge: "1" + } + }; + + fcm.send(message, function(error, response){ + const success = error == null + if (!success) { + console.log("FCM Error:",error); + } + }); +} diff --git a/ApiServer/test/fcm_token.txt b/ApiServer/test/fcm_token.txt new file mode 100644 index 0000000..e69de29 diff --git a/ApiServer/test/mock.json b/ApiServer/test/mock.json new file mode 100644 index 0000000..1072e02 --- /dev/null +++ b/ApiServer/test/mock.json @@ -0,0 +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:"}} diff --git a/ApiServer/test/mock.text b/ApiServer/test/mock.text new file mode 100644 index 0000000..cdc78de --- /dev/null +++ b/ApiServer/test/mock.text @@ -0,0 +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= \ No newline at end of file diff --git a/ApiServer/test/test_decipher.js b/ApiServer/test/test_decipher.js new file mode 100644 index 0000000..33678e8 --- /dev/null +++ b/ApiServer/test/test_decipher.js @@ -0,0 +1,15 @@ + +/**** Test: decrypt ****/ + +const webPushDecipher = require('../webPushDecipher.js'); + +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=" + +publicKey = "BJgVD2cj1pNKNR2Ss3U_8e7P9AyoL5kWaxVio5aO16Cvnx-P1r7HH8SRb-h5tuxaydZ1ky3oO0V40s6t_uN1SdA" +privateKey = "ciQ800G-6jyKWf6KKG94g5rCSU_l_rgbHbyHny_UsIM" +authSecret = "43w_wOVYeF9XzyRyZL3O8g" + +reciverKey = webPushDecipher.reciverKeyBuilder(publicKey,privateKey,authSecret) +decrypted = webPushDecipher.decrypt(body,reciverKey,true) + +console.log("\n",decrypted) diff --git a/ApiServer/test/test_join.js b/ApiServer/test/test_join.js new file mode 100644 index 0000000..d064cc4 --- /dev/null +++ b/ApiServer/test/test_join.js @@ -0,0 +1,29 @@ + +/** Test: Decrypt → convert json → generate contents → send the notification **/ + +const webPushDecipher = require('../webPushDecipher.js'); +const fs = require('fs'); +const notification = require('../notification.js'); + +publicKey = "BJgVD2cj1pNKNR2Ss3U_8e7P9AyoL5kWaxVio5aO16Cvnx-P1r7HH8SRb-h5tuxaydZ1ky3oO0V40s6t_uN1SdA" +privateKey = "ciQ800G-6jyKWf6KKG94g5rCSU_l_rgbHbyHny_UsIM" +authSecret = "43w_wOVYeF9XzyRyZL3O8g" + +function decrypt(raw) { + const reciverKey = webPushDecipher.reciverKeyBuilder(publicKey,privateKey,authSecret); + var decrypted = webPushDecipher.decrypt(raw,reciverKey,false); + return decrypted; +} + + +const rawBody = fs.readFileSync('./mock.text', 'utf8'); +const lang = "ja"; + +const rawJson = decrypt(rawBody); +const contents = notification.generateContents(rawJson,lang); + +console.log("title:",contents[0]) +console.log("body:",contents[1]) + +const token = fs.readFileSync('./fcm_token.txt', 'utf8'); +notification.send(token, contents[0], contents[1]) diff --git a/ApiServer/test/test_notification.js b/ApiServer/test/test_notification.js new file mode 100644 index 0000000..ddb8b35 --- /dev/null +++ b/ApiServer/test/test_notification.js @@ -0,0 +1,15 @@ + +/** Test: generate contents → send the notification **/ + +const fs = require('fs'); +const notification = require('../notification.js'); + +const rawJson = fs.readFileSync('./mock.json', 'utf8'); +const lang = "ja"; + +const contents = notification.generateContents(rawJson,lang); +console.log("title:",contents[0]) +console.log("body:",contents[1]) + +const token = fs.readFileSync('./fcm_token.txt', 'utf8'); +notification.send(token, contents[0], contents[1]) diff --git a/ApiServer/webPushDecipher.js b/ApiServer/webPushDecipher.js new file mode 100644 index 0000000..1edf047 --- /dev/null +++ b/ApiServer/webPushDecipher.js @@ -0,0 +1,132 @@ +// +// WebPushをdecryptするヤツ(on Node.js) +// +// **参考** +// https://tools.ietf.org/html/rfc8188 +// https://tools.ietf.org/html/rfc8291 +// https://tools.ietf.org/html/rfc8291#appendix-A +// https://gist.github.com/tateisu/685eab242549d9c9ffc85020f09a4b71 +// ↑一部 @tateisu氏のコードを参考にしています +// (アドバイスありがとうございました!→ https://mastodon.juggler.jp/@tateisu/104098620591598243) + +const util = require("util"); +const crypto = require("crypto"); + +function decodeBase64(src) { + return Buffer.from(src, "base64"); +} + +function sha256(key, data) { + return crypto.createHmac("sha256", key).update(data).digest(); +} + +function log(verbose, label, text) { + if (!verbose) { return; } + console.log(label, text); +} + +// 通知を受け取る側で生成したキーを渡す +exports.reciverKeyBuilder = function (public, private, authSecret) { + this.public = decodeBase64(public); + this.private = decodeBase64(private); + this.authSecret = decodeBase64(authSecret); + return this; +}; + +// WebPushで流れてきた通知をdecrypt +exports.decrypt = function (body64, receiverKey, verbose) { + body = decodeBase64(body64); + auth_secret = receiverKey.authSecret; + receiver_public = receiverKey.public; + receiver_private = receiverKey.private; + + // bodyを分解してsalt, keyid, 暗号化されたcontentsを取り出す + // bodyの構造は以下の通り↓ + /* + +-----------+--------+-----------+---------------+ + | salt (16) | rs (4) | idlen (1) | keyid (idlen) | + +-----------+--------+-----------+---------------+ + */ + + const salt = body.slice(0, 16); + const rs = body.slice(16, 16 + 4); + + const idlen_hex = body.slice(16 + 4, 16 + 4 + 1).toString("hex"); + const idlen = parseInt(idlen_hex, 16); // keyidの長さ + const keyid = body.slice(16 + 4 + 1, 16 + 4 + 1 + idlen); + + const content = body.slice(16 + 4 + 1 + idlen, body.length); + + const sender_public = decodeBase64(keyid.toString("base64")); + + // 共有秘密鍵を生成(ECDH) + receiver_curve = crypto.createECDH("prime256v1"); + receiver_curve.setPrivateKey(receiver_private); + const sharedSecret = receiver_curve.computeSecret(keyid); + + // For Verbose Mode + log(verbose, "salt", salt.toString("base64")); + log(verbose, "rs", rs.toString("hex")); + log(verbose, "idlen_hex", idlen_hex); + log(verbose, "idlen", idlen); + log(verbose, "keyid", keyid.toString("base64")); + log(verbose, "content", content.toString("base64")); + log(verbose, "sender_public", sender_public.toString("base64")); + log(verbose, "sharedSecret:", sharedSecret.toString("base64")); + + /* + # HKDF-Extract(salt=auth_secret, IKM=ecdh_secret) + PRK_key = HMAC-SHA-256(auth_secret, ecdh_secret) + # HKDF-Expand(PRK_key, key_info, L_key=32) + key_info = "WebPush: info" || 0x00 || ua_public || as_public + IKM = HMAC-SHA-256(PRK_key, key_info || 0x01) + + ## HKDF calculations from RFC 8188 + # HKDF-Extract(salt, IKM) + PRK = HMAC-SHA-256(salt, IKM) + # HKDF-Expand(PRK, cek_info, L_cek=16) + cek_info = "Content-Encoding: aes128gcm" || 0x00 + CEK = HMAC-SHA-256(PRK, cek_info || 0x01)[0..15] + # HKDF-Expand(PRK, nonce_info, L_nonce=12) + nonce_info = "Content-Encoding: nonce" || 0x00 + NONCE = HMAC-SHA-256(PRK, nonce_info || 0x01)[0..11] + */ + + // key + const prk_key = sha256(auth_secret, sharedSecret); + const keyInfo = Buffer.concat([ + Buffer.from("WebPush: info\0"), + receiver_public, + sender_public, + Buffer.from("\1") + ]); + const ikm = sha256(prk_key, keyInfo); + + // prk + const prk = sha256(salt, ikm); + log(verbose, "prk", prk.toString("base64")); + + // cek + const cekInfo = Buffer.from("Content-Encoding: aes128gcm\0\1"); + const cek = sha256(prk, cekInfo).slice(0, 16); + log(verbose, "cek", cek.toString("base64")); + + // initialization vector + const nonceInfo = Buffer.from("Content-Encoding: nonce\0\1"); + const nonce = sha256(prk, nonceInfo).slice(0, 12); + const iv = nonce; + log(verbose, "nonce:", nonce.toString("base64")); + + // aes-128-gcm + const decipher = crypto.createDecipheriv("aes-128-gcm", cek, iv); + result = decipher.update(content); + log(verbose, "decrypted: ", result.toString("UTF-8")); + + // remove padding and GCM auth tag + while (result.slice(result.length-1,result.length) != "}") { // jsonの末端が見えるまで一文字ずつ消していく + result = result.slice(0,result.length-1); + } + + log(verbose, "shaped:", result.toString("UTF-8")); + return result.toString("UTF-8"); +}; diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e9d7d5..a60484d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,25 @@ # Change Log +## [1.1.0] - 2020-05-17 +### Added +- 通知機能を実装 +- プロフィール編集機能を追加 + +### Changed +- 投票のデザインを刷新 +- 通知欄でのリアクションのマークを炎マークからハートマークへ変更 +- 複数の添付画像が存在する時スワイプでも閲覧できるように +- ライセンスを見やすい表示に変更 +- ダークモード時の投稿画面にて、リプライ先が見やすいように変更 + +### Fixed +- TL遡り時にスクロール位置がずれる問題を修正 +- Streamingで流れてきたRNが通知欄でうまく表示されない不具合を修正 +- 絵文字ピッカーでデフォルト絵文字のサイズがおかしくなる不具合を修正 +- ログアウト直後だとログインできなくなる不具合を修正 +- プロフィール欄でのスクロールの挙動がおかしい問題を修正 + + ## [1.0.11] - 2020-05-01 ### Added - ダークモードを追加 diff --git a/MissCat.xcodeproj/project.pbxproj b/MissCat.xcodeproj/project.pbxproj index a636a43..3e25513 100755 --- a/MissCat.xcodeproj/project.pbxproj +++ b/MissCat.xcodeproj/project.pbxproj @@ -62,6 +62,7 @@ 8D502D58238024B700525A96 /* HomeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D502D57238024B700525A96 /* HomeViewController.swift */; }; 8D502D5A2380270A00525A96 /* TabBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D502D592380270A00525A96 /* TabBar.swift */; }; 8D53741B23A32D03004523FC /* ViewModelType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D53741A23A32D03004523FC /* ViewModelType.swift */; }; + 8D560354246F9E8500DD41BE /* RxEureka.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D560353246F9E8500DD41BE /* RxEureka.swift */; }; 8D57CA43243FDB9C005DCEB2 /* PromotionCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 8D57CA42243FDB9C005DCEB2 /* PromotionCell.xib */; }; 8D57CA47243FDBC3005DCEB2 /* PromotionCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D57CA46243FDBC3005DCEB2 /* PromotionCell.swift */; }; 8D57CA4924412430005DCEB2 /* SearchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D57CA4824412430005DCEB2 /* SearchViewController.swift */; }; @@ -102,6 +103,7 @@ 8D8863B4238493FA00A7A5A8 /* RenoteeCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 8D8863B3238493FA00A7A5A8 /* RenoteeCell.xib */; }; 8D8863B62384951300A7A5A8 /* RenoteeCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D8863B52384951200A7A5A8 /* RenoteeCell.swift */; }; 8D8A50C1242355200065B1D2 /* AccountViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D8A50C0242355200065B1D2 /* AccountViewController.swift */; }; + 8D905644246A7120006D0F50 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 8D905643246A7120006D0F50 /* GoogleService-Info.plist */; }; 8D98C268242E0AD600BB8049 /* PostDetailModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D98C267242E0AD600BB8049 /* PostDetailModel.swift */; }; 8D9EF4C62381875F005449EC /* ReactionGenViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D9EF4C52381875F005449EC /* ReactionGenViewController.swift */; }; 8D9EF4C8238187A4005449EC /* ReactionGenCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 8D9EF4C7238187A4005449EC /* ReactionGenCell.xib */; }; @@ -116,6 +118,7 @@ 8DA36F862394B9D000512459 /* UIFont+MissCat.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8DA36F852394B9D000512459 /* UIFont+MissCat.swift */; }; 8DA536AC23AC8B70005EB1B5 /* AttachmentCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 8DA536AB23AC8B70005EB1B5 /* AttachmentCell.xib */; }; 8DA536AE23AC8C47005EB1B5 /* AttachmentCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8DA536AD23AC8C47005EB1B5 /* AttachmentCell.swift */; }; + 8DA83FA9246E6024006F3880 /* ProfileSettingsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8DA83FA8246E6024006F3880 /* ProfileSettingsModel.swift */; }; 8DABCD0A2414D52D00D9F054 /* AuthWebViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8DABCD092414D52D00D9F054 /* AuthWebViewController.swift */; }; 8DAD238A240CE76000AB1F36 /* StartViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8DAD2389240CE76000AB1F36 /* StartViewController.swift */; }; 8DAEA1322382E19A0083AA69 /* ReactionGenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8DAEA1312382E19A0083AA69 /* ReactionGenViewModel.swift */; }; @@ -135,6 +138,11 @@ 8DC27D9C243BF6DD0037E610 /* UrlPreviewerModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8DC27D9B243BF6DD0037E610 /* UrlPreviewerModel.swift */; }; 8DC95AF723B0BD3C0013CABC /* NotificationBanner.xib in Resources */ = {isa = PBXBuildFile; fileRef = 8DC95AF623B0BD3C0013CABC /* NotificationBanner.xib */; }; 8DC95AF923B0BD450013CABC /* NotificationBanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8DC95AF823B0BD450013CABC /* NotificationBanner.swift */; }; + 8DCBFBD624711B510028A5E6 /* MissCatImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8DCBFBD524711B510028A5E6 /* MissCatImageView.swift */; }; + 8DCCF8E5246A97C50001A21C /* MisscatApi.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8DCCF8E4246A97C50001A21C /* MisscatApi.swift */; }; + 8DCCF8E7246AB1090001A21C /* ApiKeyManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8DCCF8E6246AB1090001A21C /* ApiKeyManager.swift */; }; + 8DD2F9C5246D5DA100562E85 /* ProfileSettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8DD2F9C4246D5DA100562E85 /* ProfileSettingsViewController.swift */; }; + 8DD2F9C7246D8EA900562E85 /* ProfileSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8DD2F9C6246D8EA900562E85 /* ProfileSettingsViewModel.swift */; }; 8DD8898B23935CC400D22910 /* PostDetailViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8DD8898A23935CC400D22910 /* PostDetailViewModel.swift */; }; 8DDB52CD245032DD00C97130 /* ColorPickerCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8DDB52CC245032DD00C97130 /* ColorPickerCell.swift */; }; 8DDB52CF245032E500C97130 /* ColorPickerCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 8DDB52CE245032E500C97130 /* ColorPickerCell.xib */; }; @@ -223,6 +231,7 @@ 8D502D57238024B700525A96 /* HomeViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeViewController.swift; sourceTree = ""; }; 8D502D592380270A00525A96 /* TabBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabBar.swift; sourceTree = ""; }; 8D53741A23A32D03004523FC /* ViewModelType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewModelType.swift; sourceTree = ""; }; + 8D560353246F9E8500DD41BE /* RxEureka.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RxEureka.swift; sourceTree = ""; }; 8D5733E123F667DB002C17D2 /* Agrume.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Agrume.framework; path = Pods/_Prebuild/GeneratedFrameworks/Agrume/Agrume.framework; sourceTree = ""; }; 8D57CA42243FDB9C005DCEB2 /* PromotionCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = PromotionCell.xib; sourceTree = ""; }; 8D57CA46243FDBC3005DCEB2 /* PromotionCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PromotionCell.swift; sourceTree = ""; }; @@ -264,6 +273,7 @@ 8D8863B3238493FA00A7A5A8 /* RenoteeCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = RenoteeCell.xib; sourceTree = ""; }; 8D8863B52384951200A7A5A8 /* RenoteeCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RenoteeCell.swift; sourceTree = ""; }; 8D8A50C0242355200065B1D2 /* AccountViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountViewController.swift; sourceTree = ""; }; + 8D905643246A7120006D0F50 /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = ""; }; 8D98C267242E0AD600BB8049 /* PostDetailModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostDetailModel.swift; sourceTree = ""; }; 8D9EF4C52381875F005449EC /* ReactionGenViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReactionGenViewController.swift; sourceTree = ""; }; 8D9EF4C7238187A4005449EC /* ReactionGenCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ReactionGenCell.xib; sourceTree = ""; }; @@ -279,6 +289,7 @@ 8DA37093242F6F52004BCA5D /* AWSCognito.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AWSCognito.framework; path = Pods/_Prebuild/GeneratedFrameworks/AWSCognito/AWSCognito.framework; sourceTree = ""; }; 8DA536AB23AC8B70005EB1B5 /* AttachmentCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = AttachmentCell.xib; sourceTree = ""; }; 8DA536AD23AC8C47005EB1B5 /* AttachmentCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentCell.swift; sourceTree = ""; }; + 8DA83FA8246E6024006F3880 /* ProfileSettingsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileSettingsModel.swift; sourceTree = ""; }; 8DABCD092414D52D00D9F054 /* AuthWebViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthWebViewController.swift; sourceTree = ""; }; 8DAD2389240CE76000AB1F36 /* StartViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartViewController.swift; sourceTree = ""; }; 8DAEA1312382E19A0083AA69 /* ReactionGenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReactionGenViewModel.swift; sourceTree = ""; }; @@ -298,7 +309,12 @@ 8DC27D9B243BF6DD0037E610 /* UrlPreviewerModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UrlPreviewerModel.swift; sourceTree = ""; }; 8DC95AF623B0BD3C0013CABC /* NotificationBanner.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = NotificationBanner.xib; sourceTree = ""; }; 8DC95AF823B0BD450013CABC /* NotificationBanner.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NotificationBanner.swift; sourceTree = ""; }; + 8DCBFBD524711B510028A5E6 /* MissCatImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MissCatImageView.swift; sourceTree = ""; }; + 8DCCF8E4246A97C50001A21C /* MisscatApi.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MisscatApi.swift; sourceTree = ""; }; + 8DCCF8E6246AB1090001A21C /* ApiKeyManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApiKeyManager.swift; sourceTree = ""; }; 8DD0D80B23B47CA8003F02ED /* YanagiText.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = YanagiText.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 8DD2F9C4246D5DA100562E85 /* ProfileSettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileSettingsViewController.swift; sourceTree = ""; }; + 8DD2F9C6246D8EA900562E85 /* ProfileSettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileSettingsViewModel.swift; sourceTree = ""; }; 8DD8898A23935CC400D22910 /* PostDetailViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostDetailViewModel.swift; sourceTree = ""; }; 8DDB52CC245032DD00C97130 /* ColorPickerCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorPickerCell.swift; sourceTree = ""; }; 8DDB52CE245032E500C97130 /* ColorPickerCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ColorPickerCell.xib; sourceTree = ""; }; @@ -369,6 +385,7 @@ 8D065B15237AC387003DFB7C /* Model */ = { isa = PBXGroup; children = ( + 8DA83FA7246E601A006F3880 /* Settings */, 8D72E519244AEA1A00B30C3A /* Details */, 8D72E518244AEA1500B30C3A /* Main */, 8D5D2C462448149D00D15974 /* DirectMessage */, @@ -435,6 +452,8 @@ 8D2A7235242A49CC00F8827D /* MissCatTableView.swift */, 8D0D6890244A9A6800725ABB /* PlaceholderTableView.swift */, 8D03BFE9245C19FF00F66E04 /* CustomNavigationController.swift */, + 8D560353246F9E8500DD41BE /* RxEureka.swift */, + 8DCBFBD524711B510028A5E6 /* MissCatImageView.swift */, ); path = Utilities; sourceTree = ""; @@ -517,6 +536,7 @@ children = ( 8D4A2A23237437C000503685 /* Info.plist */, 8D1C150E2424E3B6004B1202 /* MissCat.entitlements */, + 8D905643246A7120006D0F50 /* GoogleService-Info.plist */, 8D065B13237AC376003DFB7C /* View */, 8D065B14237AC381003DFB7C /* ViewModel */, 8D065B15237AC387003DFB7C /* Model */, @@ -770,6 +790,7 @@ isa = PBXGroup; children = ( 8D31018E24397D5E004F8E55 /* ReactionSettingsViewModel.swift */, + 8DD2F9C6246D8EA900562E85 /* ProfileSettingsViewModel.swift */, ); path = Settings; sourceTree = ""; @@ -893,10 +914,20 @@ 8D4A2A15237437BE00503685 /* AppDelegate.swift */, 8D4A2A17237437BE00503685 /* SceneDelegate.swift */, 8D1C150D2424C649004B1202 /* Bridge.h */, + 8DCCF8E4246A97C50001A21C /* MisscatApi.swift */, + 8DCCF8E6246AB1090001A21C /* ApiKeyManager.swift */, ); path = App; sourceTree = ""; }; + 8DA83FA7246E601A006F3880 /* Settings */ = { + isa = PBXGroup; + children = ( + 8DA83FA8246E6024006F3880 /* ProfileSettingsModel.swift */, + ); + path = Settings; + sourceTree = ""; + }; 8DAEA13A2382E7FF0083AA69 /* Reusable */ = { isa = PBXGroup; children = ( @@ -931,6 +962,7 @@ 8D31018C24397B45004F8E55 /* ReactionSettingsViewController.swift */, 8D5889F4244FFAC200C6EBC2 /* DesignSettingsViewController.swift */, 8D75EC7E2452E49100EAC2A8 /* ColorPicker.swift */, + 8DD2F9C4246D5DA100562E85 /* ProfileSettingsViewController.swift */, ); path = Settings; sourceTree = ""; @@ -1031,6 +1063,7 @@ 8D57CA43243FDB9C005DCEB2 /* PromotionCell.xib in Resources */, 8D5D2C4224480E5700D15974 /* SenderCell.xib in Resources */, 8D4A2A22237437C000503685 /* LaunchScreen.storyboard in Resources */, + 8D905644246A7120006D0F50 /* GoogleService-Info.plist in Resources */, 8D2D52392442CA9700712F48 /* UserCell.xib in Resources */, 8D4754F9240E21F800BAD410 /* PollView.xib in Resources */, 8DA06A092389E9820093A718 /* NotificationCell.xib in Resources */, @@ -1145,6 +1178,7 @@ 8D3036F62385DA8900BA6D38 /* PostViewModel.swift in Sources */, 8D3037002388F71E00BA6D38 /* MisskeyTextView.swift in Sources */, 8DBDD58E244198C90014E77F /* SearchModel.swift in Sources */, + 8DCCF8E5246A97C50001A21C /* MisscatApi.swift in Sources */, 8D0F7D0F2398F21F009B45E1 /* ReactionGenPanel.swift in Sources */, 8D5D2C4024480E4E00D15974 /* SenderCell.swift in Sources */, 8DEFF31E239BD4F200142802 /* ProfileModel.swift in Sources */, @@ -1158,12 +1192,14 @@ 8D2A7236242A49CC00F8827D /* MissCatTableView.swift in Sources */, 8D79687F2404BE59004F87FA /* ReactionCollectionHeader.swift in Sources */, 8D31018D24397B45004F8E55 /* ReactionSettingsViewController.swift in Sources */, + 8DCBFBD624711B510028A5E6 /* MissCatImageView.swift in Sources */, 8D0AC5C1237B865B00530508 /* TimelineModel.swift in Sources */, 8D5889F5244FFAC200C6EBC2 /* DesignSettingsViewController.swift in Sources */, 8DF49FA224445B0500C4D2B2 /* TrendViewController.swift in Sources */, 8D3037022389018800BA6D38 /* Cache.swift in Sources */, 8D9EF4C62381875F005449EC /* ReactionGenViewController.swift in Sources */, 8D84061F2393547800EED595 /* PostDetailViewController.swift in Sources */, + 8D560354246F9E8500DD41BE /* RxEureka.swift in Sources */, 8D3036F82385DAED00BA6D38 /* PostModel.swift in Sources */, 8DAF30192419842D0041BB85 /* AboutMisskeyViewController.swift in Sources */, 8D2D52432443D2BD00712F48 /* UserListViewModel.swift in Sources */, @@ -1201,6 +1237,7 @@ 8D2D523D2443BDDB00712F48 /* UserCellModel.swift in Sources */, 8D31018F24397D5E004F8E55 /* ReactionSettingsViewModel.swift in Sources */, 8DE88825242764AE002CC9DE /* NoteCell.Model.swift in Sources */, + 8DCCF8E7246AB1090001A21C /* ApiKeyManager.swift in Sources */, 8DC95AF923B0BD450013CABC /* NotificationBanner.swift in Sources */, 8D2D52452443D4DD00712F48 /* UserListModel.swift in Sources */, 8D613555238B6D8B00AB50C2 /* NavBar.swift in Sources */, @@ -1214,6 +1251,7 @@ 8D03BFEA245C19FF00F66E04 /* CustomNavigationController.swift in Sources */, 8D61391C2427C1B3003B5BEF /* FileContainerViewModel.swift in Sources */, 8D4A2A18237437BE00503685 /* SceneDelegate.swift in Sources */, + 8DD2F9C7246D8EA900562E85 /* ProfileSettingsViewModel.swift in Sources */, 8DAF301B241B599B0041BB85 /* LicenseViewController.swift in Sources */, 8DA06A16238A86DC0093A718 /* NotificationCellModel.swift in Sources */, 8D613551238A978700AB50C2 /* EmojiViewCell.swift in Sources */, @@ -1225,6 +1263,7 @@ 8D8863A92383F8A900A7A5A8 /* NoteCellViewModel.swift in Sources */, 8D2D523B2442CC9100712F48 /* UserCell.swift in Sources */, 8D310191243AE9AB004F8E55 /* MFMImageView.swift in Sources */, + 8DD2F9C5246D5DA100562E85 /* ProfileSettingsViewController.swift in Sources */, 8D0D47AA23A74EC80095361D /* UIViewController+PhotoEditor.swift in Sources */, 8D84061D238FFC5C00EED595 /* UIImage+MissCat.swift in Sources */, 8DBC7A45241245A800D8FEA5 /* AVKit+MissCat.swift in Sources */, @@ -1236,6 +1275,7 @@ 8D6139202427CAED003B5BEF /* FileContainerCell.swift in Sources */, 8DEFF31C239BD45700142802 /* ProfileViewModel.swift in Sources */, 8DBDD58C244197840014E77F /* SearchViewModel.swift in Sources */, + 8DA83FA9246E6024006F3880 /* ProfileSettingsModel.swift in Sources */, 8DE8882724276512002CC9DE /* NoteCell.FileView.swift in Sources */, 8DA06A12238A58A20093A718 /* NSMutableAttributedString+MissCat.swift in Sources */, 8D502D58238024B700525A96 /* HomeViewController.swift in Sources */, @@ -1417,7 +1457,7 @@ CODE_SIGN_ENTITLEMENTS = MissCat/MissCat.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1.0.11; + CURRENT_PROJECT_VERSION = 1.1.0; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = ""; DEVELOPMENT_TEAM = 4LMTSU8RLS; @@ -1432,7 +1472,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.11; + MARKETING_VERSION = 1.1.0; OTHER_LDFLAGS = ( "$(inherited)", "-l\"xml2\"", @@ -1496,7 +1536,7 @@ CODE_SIGN_ENTITLEMENTS = MissCat/MissCat.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1.0.11; + CURRENT_PROJECT_VERSION = 1.1.0; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = ""; DEVELOPMENT_TEAM = 4LMTSU8RLS; @@ -1511,7 +1551,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.11; + MARKETING_VERSION = 1.1.0; OTHER_LDFLAGS = ( "$(inherited)", "-l\"xml2\"", diff --git a/MissCat/GoogleService-Info.plist b/MissCat/GoogleService-Info.plist new file mode 100644 index 0000000..9fe4b28 --- /dev/null +++ b/MissCat/GoogleService-Info.plist @@ -0,0 +1,36 @@ + + + + + CLIENT_ID + 829232913896-ml8sncp8uftda7jbgjl3p0atv4ffkt2f.apps.googleusercontent.com + REVERSED_CLIENT_ID + com.googleusercontent.apps.829232913896-ml8sncp8uftda7jbgjl3p0atv4ffkt2f + API_KEY + AIzaSyAvc4DNj1FuQivImVTfxW9zHikH_8yFf38 + GCM_SENDER_ID + 829232913896 + PLIST_VERSION + 1 + BUNDLE_ID + yuwd.MissCat + PROJECT_ID + misscat-9c053 + STORAGE_BUCKET + misscat-9c053.appspot.com + IS_ADS_ENABLED + + IS_ANALYTICS_ENABLED + + IS_APPINVITE_ENABLED + + IS_GCM_ENABLED + + IS_SIGNIN_ENABLED + + GOOGLE_APP_ID + 1:829232913896:ios:bb47b5babdbb686b1ed85d + DATABASE_URL + https://misscat-9c053.firebaseio.com + + \ No newline at end of file diff --git a/MissCat/MFMEngine/MFMImageView.swift b/MissCat/MFMEngine/MFMImageView.swift index a8540f6..73143f2 100644 --- a/MissCat/MFMEngine/MFMImageView.swift +++ b/MissCat/MFMEngine/MFMImageView.swift @@ -11,7 +11,7 @@ import Gifu import SVGKit import UIKit -class MFMImageView: UIImageView { +class MFMImageView: MissCatImageView { // MARK: Views private var apngView: APNGImageView? diff --git a/MissCat/Model/Main/HomeModel.swift b/MissCat/Model/Main/HomeModel.swift index 8fdeab0..65fff3c 100644 --- a/MissCat/Model/Main/HomeModel.swift +++ b/MissCat/Model/Main/HomeModel.swift @@ -9,10 +9,12 @@ import MisskeyKit class HomeModel { - func vote(choice: Int, to noteId: String) { - MisskeyKit.notes.vote(noteId: noteId, choice: choice, result: { _, _ in - // print(error) - }) + func vote(choice: [Int], to noteId: String) { + choice.forEach { + MisskeyKit.notes.vote(noteId: noteId, choice: $0, result: { _, _ in + // print(error) + }) + } } func renote(noteId: String) { diff --git a/MissCat/Model/Main/NotificationModel.swift b/MissCat/Model/Main/NotificationModel.swift index 0919838..e1d29ec 100644 --- a/MissCat/Model/Main/NotificationModel.swift +++ b/MissCat/Model/Main/NotificationModel.swift @@ -182,12 +182,14 @@ class NotificationsModel { guard let target = target as? StreamingModel, let fromUser = target.user else { return nil } var type: ActionType + var targetNote: NoteModel? = target.note if target.reaction != nil { type = .reaction } else if target.type == "follow" { type = .follow } else if target.type == "renote" { type = .renote + targetNote = target.note?.renote // renoteの場合は相手の投稿(=target.note)のrenote内に自分の投稿が含まれている } else { return nil } @@ -195,7 +197,7 @@ class NotificationsModel { let externalEmojis = getExternalEmojis(target) return NotificationCell.Model(notificationId: target.id ?? "", type: type, - myNote: target.note?.getNoteCellModel(), + myNote: targetNote?.getNoteCellModel(), replyNote: nil, fromUser: fromUser, reaction: target.reaction, diff --git a/MissCat/Model/Main/TimelineModel.swift b/MissCat/Model/Main/TimelineModel.swift index daaeee0..73f5d5d 100644 --- a/MissCat/Model/Main/TimelineModel.swift +++ b/MissCat/Model/Main/TimelineModel.swift @@ -85,110 +85,16 @@ class TimelineModel { private let handleTargetType: [String] = ["note", "CapturedNoteUpdated"] private lazy var streaming = MisskeyKit.Streaming() - // MARK: Shape NoteModel - - private func transformNote(with observer: AnyObserver, post: NoteModel, reverse: Bool) { - let noteType = checkNoteType(post) - if noteType == .Renote { - guard let renoteId = post.renoteId, - let user = post.user, - let renote = post.renote, - let renoteModel = renote.getNoteCellModel(withRN: checkNoteType(renote) == .CommentRenote) else { return } - - let renoteeModel = NoteCell.Model.fakeRenoteecell(renotee: user.name ?? user.username ?? "", - renoteeUserName: user.username ?? "", - baseNoteId: renoteId) - - var cellModels = [renoteeModel, renoteModel] - if reverse { cellModels.reverse() } - - for cellModel in cellModels { - MFMEngine.shapeModel(cellModel) - observer.onNext(cellModel) - } - } else if noteType == .Promotion { - guard let noteId = post.id, - let cellModel = post.getNoteCellModel(withRN: checkNoteType(post) == .CommentRenote) else { return } - - let prModel = NoteCell.Model.fakePromotioncell(baseNoteId: noteId) - var cellModels = [prModel, cellModel] - - if reverse { cellModels.reverse() } - - for cellModel in cellModels { - MFMEngine.shapeModel(cellModel) - observer.onNext(cellModel) - } - } else { // just a note or a note with commentRN - var newCellsModel = getCellsModel(post, withRN: noteType == .CommentRenote) - guard newCellsModel != nil else { return } - - if reverse { newCellsModel!.reverse() } // reverseしてからinsert (streamingの場合) - newCellsModel!.forEach { - MFMEngine.shapeModel($0) - observer.onNext($0) - } - } - } - // MARK: REST API + /// 投稿を読み込む + /// - Parameter option: LoadOption func loadNotes(with option: LoadOption) -> Observable { let dispose = Disposables.create() - let isInitalLoad = option.untilId == nil - let isReload = option.isReload && (option.lastNoteId != nil) return Observable.create { [unowned self] observer in - let handleResult = { (posts: [NoteModel]?, error: MisskeyKitError?) in - guard let posts = posts, error == nil else { - if let error = error { observer.onError(error) } - print(error ?? "error is nil") - return - } - - if posts.count == 0 { // 新規登録された場合はpostsが空集合 - observer.onError(NotesLoadingError.NotesEmpty) - } - - if isReload { - // timelineにすでに表示してある投稿を取得した場合、ロードを終了する - var newPosts: [NoteModel] = [] - for index in 0 ..< posts.count { - let post = posts[index] - if !post.isRecommended { // ハイライトの投稿は無視する - // 表示済みの投稿に当たったらbreak - guard option.lastNoteId != post.id, option.lastNoteId != post.renoteId else { break } - newPosts.append(post) - } - } - - newPosts.reverse() // 逆順に読み込む - newPosts.forEach { post in - self.transformNote(with: observer, post: post, reverse: true) - if let noteId = post.id { self.initialNoteIds.append(noteId) } - } - - observer.onCompleted() - return - } - - // if !isReload... - - posts.forEach { post in - // 初期ロード: prのみ表示する / 二回目からはprとハイライトを無視 - let ignore = isInitalLoad ? post.isFeatured : post.isRecommended - guard !ignore else { return } - - self.transformNote(with: observer, post: post, reverse: false) - if let noteId = post.id { - self.initialNoteIds.append(noteId) // ここでcaptureしようとしてもwebsocketとの接続が未確定なのでcapture不確実 - } - } - - observer.onCompleted() - } - + let handleResult = self.getNotesHandler(with: option, and: observer) switch option.type { case .Home: MisskeyKit.notes.getTimeline(limit: option.loadLimit, @@ -247,8 +153,126 @@ class TimelineModel { } } + // MARK: Handler (REST API) + + /// handleNotes()をパラメータを補ってNotesCallBackとして返す + /// - Parameters: + /// - option: LoadOption + /// - observer: AnyObserver + private func getNotesHandler(with option: LoadOption, and observer: AnyObserver) -> NotesCallBack { + return { (posts: [NoteModel]?, error: MisskeyKitError?) in + self.handleNotes(option: option, observer: observer, posts: posts, error: error) + } + } + + /// MisskeyKitから流れてきた投稿データを適切にobserverへと流していく + private func handleNotes(option: LoadOption, observer: AnyObserver, posts: [NoteModel]?, error: MisskeyKitError?) { + let isInitalLoad = option.untilId == nil + let isReload = option.isReload && (option.lastNoteId != nil) + + guard let posts = posts, error == nil else { + if let error = error { observer.onError(error) } + print(error ?? "error is nil") + return + } + + if posts.count == 0 { // 新規登録された場合はpostsが空集合 + observer.onError(NotesLoadingError.NotesEmpty) + } + + if isReload { + // timelineにすでに表示してある投稿を取得した場合、ロードを終了する + var newPosts: [NoteModel] = [] + for index in 0 ..< posts.count { + let post = posts[index] + if !post.isRecommended { // ハイライトの投稿は無視する + // 表示済みの投稿に当たったらbreak + guard option.lastNoteId != post.id, option.lastNoteId != post.renoteId else { break } + newPosts.append(post) + } + } + + newPosts.reverse() // 逆順に読み込む + newPosts.forEach { post in + self.transformNote(with: observer, post: post, reverse: true) + if let noteId = post.id { self.initialNoteIds.append(noteId) } + } + + observer.onCompleted() + return + } + + // if !isReload... + + posts.forEach { post in + // 初期ロード: prのみ表示する / 二回目からはprとハイライトを無視 + let ignore = isInitalLoad ? post.isFeatured : post.isRecommended + guard !ignore else { return } + + self.transformNote(with: observer, post: post, reverse: false) + if let noteId = post.id { + self.initialNoteIds.append(noteId) // ここでcaptureしようとしてもwebsocketとの接続が未確定なのでcapture不確実 + } + } + + observer.onCompleted() + } + + /// MisskeyKit.NoteModelをNoteCell.Modelへ変換してobserverへ送る + /// - Parameters: + /// - observer: AnyObserver + /// - post: NoteModel + /// - reverse: 逆順に送るかどうか + private func transformNote(with observer: AnyObserver, post: NoteModel, reverse: Bool) { + let noteType = checkNoteType(post) + if noteType == .Renote { // renoteの場合 ヘッダーとなるrenoteecellとnotecell、2つのモデルを送る + guard let renoteId = post.renoteId, + let user = post.user, + let renote = post.renote, + let renoteModel = renote.getNoteCellModel(withRN: checkNoteType(renote) == .CommentRenote) else { return } + + let renoteeModel = NoteCell.Model.fakeRenoteecell(renotee: user.name ?? user.username ?? "", + renoteeUserName: user.username ?? "", + baseNoteId: renoteId) + + var cellModels = [renoteeModel, renoteModel] + if reverse { cellModels.reverse() } + + for cellModel in cellModels { + MFMEngine.shapeModel(cellModel) + observer.onNext(cellModel) + } + } else if noteType == .Promotion { // PR投稿 + guard let noteId = post.id, + let cellModel = post.getNoteCellModel(withRN: checkNoteType(post) == .CommentRenote) else { return } + + let prModel = NoteCell.Model.fakePromotioncell(baseNoteId: noteId) + var cellModels = [prModel, cellModel] + + if reverse { cellModels.reverse() } + + for cellModel in cellModels { + MFMEngine.shapeModel(cellModel) + observer.onNext(cellModel) + } + } else { // just a note or a note with commentRN + var newCellsModel = getCellsModel(post, withRN: noteType == .CommentRenote) + guard newCellsModel != nil else { return } + + if reverse { newCellsModel!.reverse() } // reverseしてからinsert (streamingの場合) + newCellsModel!.forEach { + MFMEngine.shapeModel($0) + observer.onNext($0) + } + } + } + // MARK: Streaming API + /// Streamingへと接続する + /// - Parameters: + /// - type: TimelineType + /// - reconnect: 再接続かどうか func connectStream(type: TimelineType, isReconnection reconnect: Bool = false) -> Observable { // streamingのresponseを捌くのはhandleStreamで行う let dipose = Disposables.create() var isReconnection = reconnect @@ -270,19 +294,21 @@ class TimelineModel { } } + /// Streamingで流れてきたデータを適切にobserverへと流す private func handleStream(response: Any?, channel: SentStreamModel.Channel?, typeString: String?, error: MisskeyKitError?, observer: AnyObserver) { if let error = error { print(error) - if error == .CannotConnectStream || error == .NoStreamConnection { + if error == .CannotConnectStream || error == .NoStreamConnection { // streaming関連のエラーのみ流す observer.onError(error) } return } - guard let _ = channel, let typeString = typeString, self.handleTargetType.contains(typeString) else { - return - } + guard let _ = channel, + let typeString = typeString, + self.handleTargetType.contains(typeString) else { return } + // captureした投稿に対して更新が行われた場合 if typeString == "CapturedNoteUpdated" { guard let updateContents = response as? NoteUpdatedModel, let updateType = updateContents.type else { return } @@ -317,12 +343,13 @@ class TimelineModel { } } - guard let post = response as? NoteModel else { return } - - DispatchQueue.main.async { - self.transformNote(with: observer, post: post, reverse: true) + // 通常の投稿 + if let post = response as? NoteModel { + DispatchQueue.main.async { + self.transformNote(with: observer, post: post, reverse: true) + } + self.captureNote(noteId: post.id) } - self.captureNote(noteId: post.id) } // MARK: Capture diff --git a/MissCat/Model/Settings/ProfileSettingsModel.swift b/MissCat/Model/Settings/ProfileSettingsModel.swift new file mode 100644 index 0000000..67e5008 --- /dev/null +++ b/MissCat/Model/Settings/ProfileSettingsModel.swift @@ -0,0 +1,43 @@ +// +// ProfileSettingsModel.swift +// MissCat +// +// Created by Yuiga Wada on 2020/05/15. +// Copyright © 2020 Yuiga Wada. All rights reserved. +// + +import MisskeyKit +import RxSwift + +class ProfileSettingsModel { + /// プロフィールの差分をMisskeyへ伝達する + /// - Parameter diff: 差分 + func save(diff: ChangedProfile) { + guard diff.hasChanged else { return } + uploadImage(diff.banner) { bannerId in + self.uploadImage(diff.icon) { avatarId in + self.updateProfile(with: diff, avatarId: avatarId, bannerId: bannerId) + } + } + } + + /// "i/update"を叩く + private func updateProfile(with diff: ChangedProfile, avatarId: String?, bannerId: String?) { + MisskeyKit.users.updateMyAccount(name: diff.name ?? "", description: diff.description ?? "", avatarId: avatarId ?? "", bannerId: bannerId ?? "", isCat: diff.isCat ?? nil) { res, error in + guard let res = res, error == nil else { return } + print(res) + } + } + + /// 画像をdriveへとアップロードする + private func uploadImage(_ image: UIImage?, completion: @escaping (String?) -> Void) { + guard let image = image, + let resizedImage = image.resized(widthUnder: 1024), + let targetImage = resizedImage.jpegData(compressionQuality: 0.5) else { completion(nil); return } + + MisskeyKit.drive.createFile(fileData: targetImage, fileType: "image/jpeg", name: UUID().uuidString + ".jpeg", force: false) { result, error in + guard let result = result, error == nil else { return } + completion(result.id) + } + } +} diff --git a/MissCat/Others/App/AppDelegate.swift b/MissCat/Others/App/AppDelegate.swift index 87fd575..57cfe57 100644 --- a/MissCat/Others/App/AppDelegate.swift +++ b/MissCat/Others/App/AppDelegate.swift @@ -6,77 +6,74 @@ // Copyright © 2019 Yuiga Wada. All rights reserved. // -import AWSCognito -import AWSSNS import CoreData +import FirebaseCore +import FirebaseInstanceID +import FirebaseMessaging import MisskeyKit import UIKit @UIApplicationMain -class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate { +class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate, MessagingDelegate { // MARK: Main var window: UIWindow? + private let gcmMessageIDKey = "gcm.message_id" func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // Override point for customization after application launch. setupMissCat() - // setupCognito() + setupFirebase() setupNotifications(with: application) + registerSw() return true } - func application(_ application: UIApplication, performFetchWithCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { - // バックグラウンドで実行する処理 -// notification() - completionHandler(.newData) + func applicationDidBecomeActive(_ application: UIApplication) { + UIApplication.shared.applicationIconBadgeNumber = 0 // アプリ開いたらバッジを削除する } - func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable: Any], - fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { - let appState = application.applicationState + func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable: Any]) { + // If you are receiving a notification message while your app is in the background, + // this callback will not be fired till the user taps on the notification launching the application. + // TODO: Handle data of notification -// switch appState { -// case .active: -// break -// case .inactive: -// notification() -// case .background: -// notification() -// @unknown default: -// notification() -// } + // With swizzling disabled you must let Messaging know about the message, for Analytics + // Messaging.messaging().appDidReceiveMessage(userInfo) + + // Print message ID. + if let messageID = userInfo[gcmMessageIDKey] { + print("Message ID: \(messageID)") + } + + // Print full message. + print(userInfo) } - func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { - // Device Tokenの取得 - let deviceTokenString = deviceToken.map { String(format: "%.2hhx", $0) }.joined() - saveDeviceToken(deviceTokenString) + func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable: Any], + fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { + // If you are receiving a notification message while your app is in the background, + // this callback will not be fired till the user taps on the notification launching the application. + // TODO: Handle data of notification - let sns = AWSSNS.default() - guard let request = AWSSNSCreatePlatformEndpointInput() else { return } + // With swizzling disabled you must let Messaging know about the message, for Analytics + // Messaging.messaging().appDidReceiveMessage(userInfo) - request.token = deviceTokenString - request.platformApplicationArn = "arn:aws:sns:ap-northeast-1:098581475404:app/APNS_SANDBOX/MissCat" - request.customUserData = "{\"lang\":\"ja\"}" // 言語情報を渡す + // Print message ID. + if let messageID = userInfo[gcmMessageIDKey] { + print("Message ID: \(messageID)") + } - sns.createPlatformEndpoint(request).continueWith(executor: AWSExecutor.mainThread(), block: { task in - guard task.error == nil, - let result = task.result, - let subscribeInput = AWSSNSSubscribeInput() else { return nil } - - subscribeInput.topicArn = "arn:aws:sns:ap-northeast-1:098581475404:app/APNS_SANDBOX/MissCat" - subscribeInput.endpoint = result.endpointArn - subscribeInput.protocols = "Application" - sns.subscribe(subscribeInput) - - self.saveEndpointArn(result.endpointArn) - return nil - }) + // Print full message. + print(userInfo) + + completionHandler(UIBackgroundFetchResult.newData) } + func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {} + func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) { print(error) } @@ -84,28 +81,27 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD // MARK: Setup private func setupMissCat() { - if let currentInstance = Cache.UserDefaults.shared.getCurrentLoginedInstance(), - !currentInstance.isEmpty { - MisskeyKit.changeInstance(instance: currentInstance) - _ = EmojiHandler.handler - } - Theme.shared.set() -// if let currentUserId = Cache.UserDefaults.shared.getCurrentLoginedUserId() { -// Theme.shared.set(userId: currentUserId) -// } } - private func setupCognito() { - // Amazon Cognito - let credentialsProvider = AWSCognitoCredentialsProvider(regionType: .APNortheast1, - identityPoolId: "ap-northeast-1:70a071ae-23f9-46e8-b0f1-d5c9000e4f29") - AWSServiceManager.default().defaultServiceConfiguration = AWSServiceConfiguration(region: .APNortheast1, credentialsProvider: credentialsProvider) + private func setupFirebase() { + FirebaseApp.configure() + Messaging.messaging().delegate = self } private func setupNotifications(with application: UIApplication) { - application.setMinimumBackgroundFetchInterval(UIApplication.backgroundFetchIntervalMinimum) checkNotifyPermission(with: application) + application.registerForRemoteNotifications() + } + + private func registerSw() { + #if targetEnvironment(simulator) + let misscatApi = MisscatApi(apiKeyManager: MockApiKeyManager()) + misscatApi.registerSw() + #else + let misscatApi = MisscatApi(apiKeyManager: ApiKeyManager()) + misscatApi.registerSw() + #endif } // MARK: Notifications @@ -116,140 +112,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD center.requestAuthorization(options: [.badge, .sound, .alert], completionHandler: { granted, error in guard error == nil, granted else { return } print("通知許可") -// DispatchQueue.main.async { -// application.registerForRemoteNotifications() -// } }) } - private func notification() { - // UserDefaultsに保存してから通知を流すように - guard let current = Cache.UserDefaults.shared.getLatestNotificationId(), - !current.isEmpty else { return } - - MisskeyKit.notifications.get(limit: 20, following: false) { results, error in - guard let results = results, results.count > 0, error == nil else { return } - - if let notificationId = results[0].id { // 最新の通知をsave - Cache.UserDefaults.shared.setLatestNotificationId(notificationId) - } - - for index in 0 ..< results.count { - let model = results[index] - - guard model.id != current else { break } - self.sendNotification(of: model) // 順に通知を出していく - } - } - } - - private func sendNotification(of model: NotificationModel) { - guard let type = model.type else { return } - - var contents: NotificationContents? - switch type { - case .follow: - contents = getFollowNotification(of: model) - case .mention: - contents = getReplyNotification(of: model) - case .reply: - contents = getReplyNotification(of: model) - case .renote: - contents = getRenoteNotification(of: model) - case .quote: - contents = getCommentRenoteNotification(of: model) - case .reaction: - contents = getReactionNotification(of: model) - case .pollVote: - break - case .receiveFollowRequest: - contents = getFollowNotification(of: model) - default: - return - } - - if let contents = contents { - sendNotification(title: contents.title, body: contents.body) - } - } - - private func getReplyNotification(of model: NotificationModel) -> NotificationContents? { - guard let user = model.user, let note = model.note else { return nil } - let name = getDisplayName(user) - let text = note.text ?? "" - - let hasFile = (note.files?.count ?? 0) > 0 - let hasFileTitle = hasFile ? "画像を添付して" : "" - - return .init(title: "\(name)さんが\(hasFileTitle)返信しました", - body: text) - } - - private func getFollowNotification(of model: NotificationModel) -> NotificationContents? { - guard let user = model.user else { return nil } - let name = getDisplayName(user) - - return .init(title: "", - body: "\(name)さんがフォローしました") - } - - private func getRenoteNotification(of model: NotificationModel) -> NotificationContents? { - guard let user = model.user, let note = model.note else { return nil } - let name = getDisplayName(user) - let text = note.text ?? "" - - return .init(title: "\(name)さんがRenoteしました", - body: text) - } - - private func getCommentRenoteNotification(of model: NotificationModel) -> NotificationContents? { - guard let user = model.user, let note = model.note else { return nil } - let name = getDisplayName(user) - let text = note.text ?? "" - - let hasFile = (note.files?.count ?? 0) > 0 - let hasFileTitle = hasFile ? "画像を添付して" : "" - - return .init(title: "\(name)さんが\(hasFileTitle)引用Renoteしました", - body: text) - } - - private func getReactionNotification(of model: NotificationModel) -> NotificationContents? { - guard let user = model.user, let note = model.note, let reaction = model.reaction else { return nil } - let name = getDisplayName(user) - let text = note.text ?? "" - - return .init(title: "\(name)さんがリアクション\"\(reaction)\"を送信しました", - body: text) - } - - private func sendNotification(title: String, body: String) { - let content = UNMutableNotificationContent() - content.title = title - content.body = body - content.sound = UNNotificationSound.default - - let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 1, repeats: false) - let request = UNNotificationRequest(identifier: "misscat.\(UUID().uuidString)", content: content, trigger: trigger) - UNUserNotificationCenter.current().add(request) - } - - private func getDisplayName(_ user: UserModel) -> String { - guard let name = user.name else { return user.username ?? "" } - return name.count > 13 ? String(name.prefix(13)) : name - } - - // MARK: UserDefaults - - private func saveDeviceToken(_ token: String) { - UserDefaults.standard.set(token, forKey: "deviceToken") - } - - private func saveEndpointArn(_ endpointArn: String?) { - guard let endpointArn = endpointArn else { return } - UserDefaults.standard.set(endpointArn, forKey: "endpointArn") - } - // MARK: UISceneSession Lifecycle @available(iOS 13.0, *) diff --git a/MissCat/Others/App/Bridge.h b/MissCat/Others/App/Bridge.h index 712a25c..6c5da0b 100644 --- a/MissCat/Others/App/Bridge.h +++ b/MissCat/Others/App/Bridge.h @@ -6,5 +6,4 @@ // Copyright © 2020 Yuiga Wada. All rights reserved. // -#import -#import + diff --git a/MissCat/Others/App/MisscatApi.swift b/MissCat/Others/App/MisscatApi.swift new file mode 100644 index 0000000..6d4ca7d --- /dev/null +++ b/MissCat/Others/App/MisscatApi.swift @@ -0,0 +1,64 @@ +// +// MisscatApi.swift +// MissCat +// +// Created by Yuiga Wada on 2020/05/12. +// Copyright © 2020 Yuiga Wada. All rights reserved. +// + +import FirebaseInstanceID +import MisskeyKit + +protocol ApiKeyManagerProtocol { + var baseUrl: String { get } +} + +struct MockApiKeyManager: ApiKeyManagerProtocol { + var baseUrl = "" +} + +class MisscatApi { + private let apiKeyManager: ApiKeyManagerProtocol + private let authSecret = "Q8Zgu-WDvN5EDT_emFGovQ" + private let publicKey = "BJNAJpIOIJnXVVgCTAd4geduXEsNKre0XVvz0j-E_z-8CbGI6VaRPsVI7r-hF88MijMBZApurU2HmSNQ4e-cTmA" + private var baseApiUrl: String { + return apiKeyManager.baseUrl + } + + init(apiKeyManager: ApiKeyManagerProtocol) { + self.apiKeyManager = apiKeyManager + } + + /// 適切なendpointを生成し、sw/registerを叩く + func registerSw() { + guard let userId = Cache.UserDefaults.shared.getCurrentLoginedUserId(), + let apiKey = Cache.UserDefaults.shared.getCurrentLoginedApiKey(), + !baseApiUrl.isEmpty, + !apiKey.isEmpty, + !userId.isEmpty else { return } + + MisskeyKit.auth.setAPIKey(apiKey) + InstanceID.instanceID().instanceID { result, error in + guard error == nil else { print("Error fetching remote instance ID: \(error!)"); return } + if let token = result?.token { + self.registerSw(userId: userId, token: token) + } + } + } + + private func registerSw(userId: String, token: String) { + let endpoint = generateEndpoint(with: userId, and: token) + + MisskeyKit.serviceWorker.register(endpoint: endpoint, auth: authSecret, publicKey: publicKey, result: { state, error in + guard error == nil, let state = state else { return } + print(state) + }) + } + + private func generateEndpoint(with userId: String, and token: String) -> String { + let currentLang = Locale.current.languageCode?.description ?? "ja" + let endpoint = "\(baseApiUrl)/api/v1/push/\(currentLang)/\(userId)/\(token)" + + return endpoint + } +} diff --git a/MissCat/Others/Extension/UIView+MissCat.swift b/MissCat/Others/Extension/UIView+MissCat.swift index ff1b886..551c878 100644 --- a/MissCat/Others/Extension/UIView+MissCat.swift +++ b/MissCat/Others/Extension/UIView+MissCat.swift @@ -6,10 +6,19 @@ // Copyright © 2019 Yuiga Wada. All rights reserved. // +import RxCocoa import RxSwift import UIKit extension UIView { + var rxTap: ControlEvent { + let tapGesture = UITapGestureRecognizer() + + isUserInteractionEnabled = true + addGestureRecognizer(tapGesture) + return tapGesture.rx.event + } + func setTapGesture(_ disposeBag: DisposeBag, closure: @escaping () -> Void) { let tapGesture = UITapGestureRecognizer() diff --git a/MissCat/Others/Extension/UIViewController+PhotoEditor.swift b/MissCat/Others/Extension/UIViewController+PhotoEditor.swift index 007daec..914acdb 100644 --- a/MissCat/Others/Extension/UIViewController+PhotoEditor.swift +++ b/MissCat/Others/Extension/UIViewController+PhotoEditor.swift @@ -6,6 +6,7 @@ // Copyright © 2019 Yuiga Wada. All rights reserved. // +import AVKit import iOSPhotoEditor import RxSwift import UIKit @@ -52,3 +53,41 @@ extension UIViewController { } } } + +extension UIViewController: UIImagePickerControllerDelegate, UINavigationControllerDelegate { + func pickImage(type: UIImagePickerController.SourceType, delegate: (UIImagePickerControllerDelegate & UINavigationControllerDelegate)? = nil) { + if UIImagePickerController.isSourceTypeAvailable(.savedPhotosAlbum) { + let picker = UIImagePickerController() + picker.sourceType = type + picker.mediaTypes = UIImagePickerController.availableMediaTypes(for: type) ?? [] + picker.videoQuality = .typeHigh + picker.delegate = delegate ?? self + + presentOnFullScreen(picker, animated: true, completion: nil) + } + } + + func transformAttachment(disposeBag: DisposeBag, picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any], completion: @escaping (UIImage?, UIImage?, URL?) -> Void) { + picker.dismiss(animated: true, completion: nil) + + let isImage = info[UIImagePickerController.InfoKey.originalImage] is UIImage + + if isImage { + guard let originalImage = info[UIImagePickerController.InfoKey.originalImage] as? UIImage else { return } + + showPhotoEditor(with: originalImage).subscribe(onNext: { editedImage in // 画像エディタを表示 + guard let editedImage = editedImage else { return } + completion(originalImage, editedImage, nil) + }).disposed(by: disposeBag) + + return + } + // is Video + guard let url = info[UIImagePickerController.InfoKey.mediaURL] as? NSURL else { return } + AVAsset.convert2Mp4(videoUrl: url) { session in // 動画のデフォルトがmovなのでmp4に変換する + guard session.status == .completed, let filePath = session.outputURL else { return } + + completion(nil, nil, filePath) + } + } +} diff --git a/MissCat/Others/Utilities/Cache.swift b/MissCat/Others/Utilities/Cache.swift index 90d0b57..83b8272 100644 --- a/MissCat/Others/Utilities/Cache.swift +++ b/MissCat/Others/Utilities/Cache.swift @@ -28,6 +28,12 @@ class Cache { private lazy var applicationSupportDir = CreateApplicationSupportDir() + // MARK: Reset + + func resetMyCache() { + me = nil + } + // MARK: Save func saveIcon(username: String, image: UIImage) { diff --git a/MissCat/Others/Utilities/MissCatImageView.swift b/MissCat/Others/Utilities/MissCatImageView.swift new file mode 100644 index 0000000..9a361bc --- /dev/null +++ b/MissCat/Others/Utilities/MissCatImageView.swift @@ -0,0 +1,23 @@ +// +// MissCatImageView.swift +// MissCat +// +// Created by Yuiga Wada on 2020/05/17. +// Copyright © 2020 Yuiga Wada. All rights reserved. +// + +import UIKit + +class MissCatImageView: UIImageView { + private var isCircle: Bool = false + + /// 円を描くようにフラグを建てる + func maskCircle() { + isCircle = true + } + + override func layoutSubviews() { + super.layoutSubviews() + if isCircle { layer.cornerRadius = frame.width / 2 } + } +} diff --git a/MissCat/Others/Utilities/MissCatTableView.swift b/MissCat/Others/Utilities/MissCatTableView.swift index 2cae111..772ee86 100644 --- a/MissCat/Others/Utilities/MissCatTableView.swift +++ b/MissCat/Others/Utilities/MissCatTableView.swift @@ -13,19 +13,25 @@ import UIKit /// スクロール位置を固定するTableView /// Qiitaに記事書いた→ https://qiita.com/yuwd/items/bc152a0c9c4ce7754003 class MissCatTableView: PlaceholderTableView { - var _lockScroll: Bool = true - var lockScroll: PublishRelay? { + private var _lockScroll: Bool = true + private var hasReseverd: Bool = false + var lockScroll: Observable? { didSet { lockScroll?.subscribe(onNext: { self._lockScroll = $0 }).disposed(by: disposeBag) } } + var spinnerHeight: CGFloat = 0 private lazy var spinner = UIActivityIndicatorView(style: .gray) private var disposeBag = DisposeBag() private var onTop: Bool { return contentOffset.y <= 0 } + private var needLock: Bool { + return !self.onTop && self._lockScroll + } + override init(frame: CGRect, style: UITableView.Style) { super.init(frame: frame, style: style) setupSpinner() @@ -36,17 +42,27 @@ class MissCatTableView: PlaceholderTableView { setupSpinner() } + /// 次のupdate時にスクロールをロックするように予約する + func reserveLock() { + guard !_lockScroll else { return } + hasReseverd = true + } + /// このperformBatchUpdatesにラッピングされたメソッドはすべてスクロール位置を固定された状態で実行されます override func performBatchUpdates(_ updates: (() -> Void)?, completion: ((Bool) -> Void)? = nil) { #if !targetEnvironment(simulator) let bottomOffset = contentSize.height - contentOffset.y - if !onTop, _lockScroll { - CATransaction.begin() - CATransaction.setDisableActions(true) - } + if needLock { stopAnimation() } super.performBatchUpdates(updates, completion: { finished in - guard finished, !self.onTop, self._lockScroll else { completion?(finished); return } + guard finished, self.needLock else { // ロックが不要・updateに失敗した場合 + completion?(finished) + if self.hasReseverd { // ロック予約があればロックする + self._lockScroll = true + self.hasReseverd = false + } + return + } self.contentOffset = CGPoint(x: 0, y: self.contentSize.height - bottomOffset) completion?(finished) CATransaction.commit() @@ -56,6 +72,11 @@ class MissCatTableView: PlaceholderTableView { #endif } + private func stopAnimation() { + CATransaction.begin() + CATransaction.setDisableActions(true) + } + private func setupSpinner() { spinner.color = UIColor.darkGray spinner.hidesWhenStopped = true @@ -85,6 +106,9 @@ class MissCatTableView: PlaceholderTableView { multiplier: 1.0, constant: 0) ]) + + parentView.layoutIfNeeded() + spinnerHeight = parentView.frame.height } func stopSpinner() { diff --git a/MissCat/Others/Utilities/RxEureka.swift b/MissCat/Others/Utilities/RxEureka.swift new file mode 100644 index 0000000..75b65d6 --- /dev/null +++ b/MissCat/Others/Utilities/RxEureka.swift @@ -0,0 +1,29 @@ +// +// RxEureka.swift +// MissCat +// +// Created by Yuiga Wada on 2020/05/16. +// Copyright © 2020 Yuiga Wada. All rights reserved. +// + +import Eureka +import RxCocoa +import RxSwift + +extension RowOf: ReactiveCompatible {} + +extension Reactive where Base: RowType, Base: BaseRow { + var value: ControlProperty { + let source = Observable.create { observer in + self.base.onChange { row in + observer.onNext(row.value) +// row.updateCell() + } + return Disposables.create() + } + let bindingObserver = Binder(base) { row, value in + row.value = value + } + return ControlProperty(values: source, valueSink: bindingObserver) + } +} diff --git a/MissCat/View/Base.lproj/Main.storyboard b/MissCat/View/Base.lproj/Main.storyboard index 7a64321..ce68570 100755 --- a/MissCat/View/Base.lproj/Main.storyboard +++ b/MissCat/View/Base.lproj/Main.storyboard @@ -261,9 +261,9 @@ - + - + @@ -470,7 +470,7 @@ - + @@ -828,7 +828,7 @@ - + @@ -936,7 +936,7 @@ - + @@ -1287,9 +1287,6 @@ - - - @@ -1350,66 +1347,63 @@ - + - + - - - - - + + + + - - - - + - + - - - - - + + + + - + @@ -1500,13 +1494,13 @@ - + - + - + Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda. diff --git a/MissCat/View/Details/ProfileViewController.swift b/MissCat/View/Details/ProfileViewController.swift index 6a06e55..9aaf2e7 100644 --- a/MissCat/View/Details/ProfileViewController.swift +++ b/MissCat/View/Details/ProfileViewController.swift @@ -7,6 +7,7 @@ // import Agrume +import MisskeyKit import RxCocoa import RxSwift import SkeletonView @@ -60,7 +61,8 @@ class ProfileViewController: ButtonBarPagerTabStripViewController, UITextViewDel private var childVCs: [TimelineViewController] = [] - private var maxScroll: CGFloat { + // containerScrollView ← ContainerView(XLPagerTabStrip) ← tlScrollView の順で入れ子にScrollViewが載っている + private var containerMaxScroll: CGFloat { updateAnimateBlurHeight() // 自己紹介文の高さが変更されるので、Blurの高さも変更する pagerTab.layoutIfNeeded() return pagerTab.frame.origin.y - getSafeAreaSize().height - 10 // 10 = 微調整 @@ -121,7 +123,11 @@ class ProfileViewController: ButtonBarPagerTabStripViewController, UITextViewDel } private func getViewModel() -> ViewModel { - let input = ViewModel.Input(nameYanagi: nameTextView, introYanagi: introTextView) + let input = ViewModel.Input(nameYanagi: nameTextView, + introYanagi: introTextView, + followButtonTapped: followButton.rx.tap, + backButtonTapped: backButton.rx.tap, + settingsButtonTapped: settingsButton.rx.tap) return .init(with: input, and: disposeBag) } @@ -261,29 +267,29 @@ class ProfileViewController: ButtonBarPagerTabStripViewController, UITextViewDel self.followButton.setTitleColor(isFollowing ? self.mainColor : UIColor.white, for: .normal) }).disposed(by: disposeBag) - followButton.rx.tap.subscribe(onNext: { - guard let isFollowing = self.viewModel.state.isFollowing else { return } - - if isFollowing { // try フォロー解除 - self.showUnfollowAlert() - } else { - self.viewModel.follow() - } - - }).disposed(by: disposeBag) } else { // 自分のプロフィール画面の場合 followButton.setTitle("編集", for: .normal) followButton.setTitleColor(mainColor, for: .normal) } - backButton.rx.tap.subscribe(onNext: { - _ = self.navigationController?.popViewController(animated: true) - }).disposed(by: disposeBag) + // trigger - settingsButton.rx.tap.subscribe(onNext: { + output.openSettingsTrigger.subscribe(onNext: { self.homeViewController?.openSettings() }).disposed(by: disposeBag) + output.showUnfollowAlertTrigger.subscribe(onNext: { + self.showUnfollowAlert() + }).disposed(by: disposeBag) + + output.showProfileSettingsTrigger.subscribe(onNext: { profile in + self.showProfileSettings(of: profile) + }).disposed(by: disposeBag) + + output.popViewControllerTrigger.subscribe(onNext: { + _ = self.navigationController?.popViewController(animated: true) + }).disposed(by: disposeBag) + backButton.isHidden = output.isMe } @@ -351,6 +357,27 @@ class ProfileViewController: ButtonBarPagerTabStripViewController, UITextViewDel present(alert, animated: true, completion: nil) } + /// プロフィール編集画面へと遷移する + /// - Parameter profile: ProfileViewModel.Profile + private func showProfileSettings(of profile: ProfileViewModel.Profile) { + let settings = ProfileSettingsViewController() + settings.homeViewController = homeViewController + settings.setup(banner: bannerImageView.image, + bannerUrl: profile.bannerUrl, + icon: iconImageView.image, + iconUrl: profile.iconUrl, + name: profile.name, + description: profile.description, + isCat: profile.isCat) + + // プロフィールの書き換え命令が出たら書き換える + settings.overrideInfoTrigger.subscribe(onNext: { diff in + self.viewModel.overrideInfo(diff) + }).disposed(by: disposeBag) + + navigationController?.pushViewController(settings, animated: true) + } + // MARK: UITextViewDelegate func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool { @@ -455,11 +482,12 @@ class ProfileViewController: ButtonBarPagerTabStripViewController, UITextViewDel var needContainerScroll: Bool = true // tlScrollViewをスクロール if scroll > 0 { - if containerScrollView.contentOffset.y >= maxScroll { + if containerScrollView.contentOffset.y >= containerMaxScroll { tlScrollView.contentOffset.y += scroll needContainerScroll = false - if tlScrollView.contentOffset.y >= tlScrollView.contentSize.height - containerView.frame.height { // スクロールの上限 + let tlMaxScroll = tlScrollView.contentSize.height - containerView.frame.height + pagerTab.frame.height + tlScrollView.spinnerHeight + if tlScrollView.contentOffset.y >= tlMaxScroll { // スクロールの上限 tlScrollView.contentOffset.y -= scroll } } @@ -476,7 +504,7 @@ class ProfileViewController: ButtonBarPagerTabStripViewController, UITextViewDel scrollView.contentOffset.y = scrollBegining } else { // スクロールがmaxScrollの半分を超えたあたりから、fractionComplete: 0→1と動かしてanimateさせる - let blurProportion = containerScrollView.contentOffset.y * 2 / maxScroll - 1 + let blurProportion = containerScrollView.contentOffset.y * 2 / containerMaxScroll - 1 scrollBegining = scrollView.contentOffset.y // ブラーアニメーションをかける diff --git a/MissCat/View/Login/StartViewController.swift b/MissCat/View/Login/StartViewController.swift index 7a65c7d..d47a319 100644 --- a/MissCat/View/Login/StartViewController.swift +++ b/MissCat/View/Login/StartViewController.swift @@ -23,6 +23,7 @@ class StartViewController: UIViewController { @IBOutlet weak var changeInstanceButton: UIButton! private var appSecret: String? + private var afterLogout: Bool = false // ログアウト直後かどうか private var ioAppSecret: String = "0fRSNkKKl9hcZTGrUSyZOb19n8UUVkxw" // misskey.ioの場合はappSecret固定 private var misskeyInstance: String = "misskey.io" { didSet { @@ -62,6 +63,10 @@ class StartViewController: UIViewController { // MARK: LifeCycle + func setup(afterLogout: Bool = false) { + self.afterLogout = afterLogout + } + override func viewDidLoad() { super.viewDidLoad() setGradientLayer() @@ -97,12 +102,14 @@ class StartViewController: UIViewController { private func binding() { signupButton.rx.tap.subscribe(onNext: { _ in + guard !self.afterLogout else { self.signup(); return } // ログアウト直後なら普通にMisskeyのトップページを表示する guard let tos = self.getViewController(name: "tos") as? TosViewController else { return } tos.agreed = self.signup self.navigationController?.pushViewController(tos, animated: true) }).disposed(by: disposeBag) loginButton.rx.tap.subscribe(onNext: { _ in + guard !self.afterLogout else { self.login(); return } // ログアウト直後なら普通にログインする guard let tos = self.getViewController(name: "tos") as? TosViewController else { return } tos.agreed = self.login self.navigationController?.pushViewController(tos, animated: true) @@ -180,9 +187,16 @@ class StartViewController: UIViewController { Cache.UserDefaults.shared.setCurrentLoginedInstance(misskeyInstance) _ = EmojiHandler.handler // カスタム絵文字を読み込む + registerSw() // 通知を登録する DispatchQueue.main.async { - self.navigationController?.popViewController(animated: true) + if self.afterLogout { // 設定画面からのログイン + MisskeyKit.changeInstance(instance: self.misskeyInstance) + MisskeyKit.auth.setAPIKey(apiKey) + self.dismiss(animated: true) + } else { // 初期画面からのログイン + self.navigationController?.popViewController(animated: true) + } } } @@ -206,6 +220,18 @@ class StartViewController: UIViewController { return shaped } + // MARK: SW + + private func registerSw() { + #if targetEnvironment(simulator) + let misscatApi = MisscatApi(apiKeyManager: MockApiKeyManager()) + misscatApi.registerSw() + #else + let misscatApi = MisscatApi(apiKeyManager: ApiKeyManager()) + misscatApi.registerSw() + #endif + } + // MARK: Alert private func showInstanceTextFiled() { diff --git a/MissCat/View/Main/HomeViewController.swift b/MissCat/View/Main/HomeViewController.swift index e6eed02..903c988 100644 --- a/MissCat/View/Main/HomeViewController.swift +++ b/MissCat/View/Main/HomeViewController.swift @@ -6,6 +6,7 @@ // Copyright © 2019 Yuiga Wada. All rights reserved. // +import Agrume import AVKit import MisskeyKit import PolioPager @@ -18,7 +19,7 @@ import UIKit // 上タブ管理はBGで動いている親クラスのPolioPagerが行い、下タブ管理はHomeViewControllerが自前で行う。 // 一時的なキャッシュ管理についてはsingletonのCacheクラスを使用 -class HomeViewController: PolioPagerViewController, UIGestureRecognizerDelegate, UINavigationControllerDelegate { +class HomeViewController: PolioPagerViewController, UIGestureRecognizerDelegate { private var isXSeries = UIScreen.main.bounds.size.height > 811 private let footerTabHeight: CGFloat = 55 private var initialized: Bool = false @@ -551,11 +552,15 @@ extension HomeViewController: NoteCellDelegate { func updateMyReaction(targetNoteId: String, rawReaction: String, plus: Bool) {} - func vote(choice: Int, to noteId: String) { - // TODO: modelの変更 / api処理 + func vote(choice: [Int], to noteId: String) { viewModel.vote(choice: choice, to: noteId) } + func showImage(_ urls: [URL], start startIndex: Int) { + let agrume = Agrume(urls: urls, startIndex: startIndex) + agrume.show(from: self) // 画像を表示 + } + func playVideo(url: String) { guard let url = URL(string: url) else { return } let videoPlayer = AVPlayer(url: url) diff --git a/MissCat/View/Main/NotificationsViewController.swift b/MissCat/View/Main/NotificationsViewController.swift index 6755203..f71b620 100644 --- a/MissCat/View/Main/NotificationsViewController.swift +++ b/MissCat/View/Main/NotificationsViewController.swift @@ -14,7 +14,7 @@ import UIKit typealias NotificationDataSource = RxTableViewSectionedAnimatedDataSource class NotificationsViewController: NoteDisplay, UITableViewDelegate, FooterTabBarDelegate { - @IBOutlet weak var mainTableView: UITableView! + @IBOutlet weak var mainTableView: MissCatTableView! private var viewModel: NotificationsViewModel? private let disposeBag = DisposeBag() @@ -77,6 +77,7 @@ class NotificationsViewController: NoteDisplay, UITableViewDelegate, FooterTabBa mainTableView.register(UINib(nibName: "NoteCell", bundle: nil), forCellReuseIdentifier: "NoteCell") mainTableView.rx.setDelegate(self).disposed(by: disposeBag) + mainTableView.lockScroll = Observable.of(false) } private func setupDataSource() -> NotificationDataSource { diff --git a/MissCat/View/Main/PostViewController.swift b/MissCat/View/Main/PostViewController.swift index 40a8d06..36c7cf7 100644 --- a/MissCat/View/Main/PostViewController.swift +++ b/MissCat/View/Main/PostViewController.swift @@ -14,7 +14,7 @@ import RxSwift import UIKit typealias AttachmentsDataSource = RxCollectionViewSectionedReloadDataSource -class PostViewController: UIViewController, UITextViewDelegate, UIImagePickerControllerDelegate, UINavigationControllerDelegate, UICollectionViewDelegate { +class PostViewController: UIViewController, UITextViewDelegate, UICollectionViewDelegate { @IBOutlet weak var attachmentCollectionView: UICollectionView! @IBOutlet weak var cancelButton: UIButton! @@ -95,6 +95,8 @@ class PostViewController: UIViewController, UITextViewDelegate, UIImagePickerCon private func setTheme() { if let colorPattern = Theme.shared.currentModel?.colorPattern.ui { view.backgroundColor = colorPattern.base + markLabel.textColor = colorPattern.text + innerNoteLabel.textColor = colorPattern.sub0 } mainTextView.textColor = placeholderColor } @@ -168,6 +170,11 @@ class PostViewController: UIViewController, UITextViewDelegate, UIImagePickerCon .asDriver(onErrorDriveWith: Driver.empty()) .drive(innerNoteLabel.rx.text) .disposed(by: disposeBag) + + output.mark + .asDriver(onErrorDriveWith: Driver.empty()) + .drive(markLabel.rx.text) + .disposed(by: disposeBag) } private func setupCollectionView() { @@ -443,18 +450,6 @@ class PostViewController: UIViewController, UITextViewDelegate, UIImagePickerCon } } - private func pickImage(type: UIImagePickerController.SourceType) { - if UIImagePickerController.isSourceTypeAvailable(.savedPhotosAlbum) { - let picker = UIImagePickerController() - picker.sourceType = type - picker.mediaTypes = UIImagePickerController.availableMediaTypes(for: type) ?? [] - picker.videoQuality = .typeHigh - picker.delegate = self - - presentOnFullScreen(picker, animated: true, completion: nil) - } - } - // MARK: Delegate func textViewDidBeginEditing(_ textView: UITextView) { @@ -472,25 +467,12 @@ class PostViewController: UIViewController, UITextViewDelegate, UIImagePickerCon } func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) { - picker.dismiss(animated: true, completion: nil) - - let isImage = info[UIImagePickerController.InfoKey.originalImage] is UIImage - - if isImage { - guard let originalImage = info[UIImagePickerController.InfoKey.originalImage] as? UIImage else { return } - - showPhotoEditor(with: originalImage).subscribe(onNext: { editedImage in // 画像エディタを表示 - guard let editedImage = editedImage else { return } + transformAttachment(disposeBag: disposeBag, picker: picker, didFinishPickingMediaWithInfo: info) { originalImage, editedImage, videoUrl in + if let originalImage = originalImage, let editedImage = editedImage { self.viewModel?.stackFile(original: originalImage, edited: editedImage) - }).disposed(by: disposeBag) - - return - } - // is Video - guard let url = info[UIImagePickerController.InfoKey.mediaURL] as? NSURL else { return } - AVAsset.convert2Mp4(videoUrl: url) { session in // 動画のデフォルトがmovなのでmp4に変換する - guard session.status == .completed, let filePath = session.outputURL else { return } - self.viewModel?.stackFile(videoUrl: filePath) + } else if let videoUrl = videoUrl { + self.viewModel?.stackFile(videoUrl: videoUrl) + } } } diff --git a/MissCat/View/Main/SearchViewController.swift b/MissCat/View/Main/SearchViewController.swift index 1083027..d1b9c6b 100644 --- a/MissCat/View/Main/SearchViewController.swift +++ b/MissCat/View/Main/SearchViewController.swift @@ -271,7 +271,7 @@ class SearchViewController: UIViewController, PolioPagerSearchTabDelegate, UITex let viewController = getViewController(name: "timeline") as? TimelineViewController else { return nil } - viewController.setup(type: .NoteSearch, query: query, withTopShadow: true) + viewController.setup(type: .NoteSearch, query: query, lockScroll: false, withTopShadow: true) viewController.view.frame = timelineView.frame viewController.view.translatesAutoresizingMaskIntoConstraints = false timelineView.addSubview(viewController.view) @@ -286,7 +286,7 @@ class SearchViewController: UIViewController, PolioPagerSearchTabDelegate, UITex let viewController = getViewController(name: "user-list") as? UserListViewController else { return nil } - viewController.setup(type: .search, query: query, withTopShadow: true) + viewController.setup(type: .search, query: query, lockScroll: false, withTopShadow: true) viewController.view.frame = timelineView.frame viewController.view.translatesAutoresizingMaskIntoConstraints = false timelineView.addSubview(viewController.view) diff --git a/MissCat/View/Main/TimelineViewController.swift b/MissCat/View/Main/TimelineViewController.swift index 49706b2..fb7fb7f 100644 --- a/MissCat/View/Main/TimelineViewController.swift +++ b/MissCat/View/Main/TimelineViewController.swift @@ -70,6 +70,7 @@ class TimelineViewController: NoteDisplay, UITableViewDelegate, FooterTabBarDele /// - query: 検索クエリ /// - withNavBar: NavBarが必要か /// - scrollable: スクロール可能か + /// - lockScroll: スクロールを固定するかどうか /// - loadLimit: 一度に読み込むnoteの量 /// - xlTitle: タブに表示する名前 func setup(type: TimelineType, @@ -80,6 +81,7 @@ class TimelineViewController: NoteDisplay, UITableViewDelegate, FooterTabBarDele query: String? = nil, withNavBar: Bool = true, scrollable: Bool = true, + lockScroll: Bool = true, withTopShadow: Bool = false, loadLimit: Int = 40, xlTitle: IndicatorInfo? = nil) { @@ -90,6 +92,7 @@ class TimelineViewController: NoteDisplay, UITableViewDelegate, FooterTabBarDele userId: userId, listId: listId, query: query, + lockScroll: lockScroll, loadLimit: loadLimit) viewModel = ViewModel(with: input, and: disposeBag) @@ -192,7 +195,11 @@ class TimelineViewController: NoteDisplay, UITableViewDelegate, FooterTabBarDele homeViewController.changedStreamState(success: success) }).disposed(by: disposeBag) - mainTableView.lockScroll = output.lockTableScroll + output.reserveLockTrigger.subscribe(onNext: { _ in + self.mainTableView.reserveLock() // 次にセルがupdateされた時にスクロールを固定し直す + }).disposed(by: disposeBag) + + mainTableView.lockScroll = output.lockTableScroll.asObservable() } // MARK: Gesture @@ -222,11 +229,6 @@ class TimelineViewController: NoteDisplay, UITableViewDelegate, FooterTabBarDele let index = indexPath.row let item = viewModel.cellsModel[index] - if item.identity == viewModel.state.reloadTopModelId { // untilLoadが完了した場合 - viewModel.state.reloadTopModelId = nil - mainTableView.lockScroll?.accept(true) // スクロールを固定し直す - } - // View側で NoteCell / RenoteeCell / PromotionCell を区別する if item.isPromotionCell { let prCell = tableView.dequeueReusableCell(withIdentifier: "PromotionCell", for: indexPath) diff --git a/MissCat/View/Reusable/Emoji/EmojiView.swift b/MissCat/View/Reusable/Emoji/EmojiView.swift index 2cfb5ea..fd3b73f 100644 --- a/MissCat/View/Reusable/Emoji/EmojiView.swift +++ b/MissCat/View/Reusable/Emoji/EmojiView.swift @@ -49,6 +49,7 @@ class EmojiView: UIView { view.frame = bounds view.backgroundColor = .clear addSubview(view) + adjustFontSize() } } @@ -81,7 +82,6 @@ class EmojiView: UIView { if emoji.isDefault { emojiLabel.text = emoji.defaultEmoji emojiLabel.backgroundColor = .clear - adjustFontSize() } else { guard let customEmojiUrl = emoji.customEmojiUrl else { return } emojiImageView.setImage(url: customEmojiUrl, cachedToStorage: true) // イメージをset @@ -90,23 +90,10 @@ class EmojiView: UIView { // MARK: Others - // 最適なwidthとなるフォントサイズを探索する + // 最適なフォントサイズに変更する private func adjustFontSize() { - guard let emoji = emoji, emoji.isDefault, let defaultEmoji = emoji.defaultEmoji else { return } - - var labelWidth: CGFloat = 0 - var font: UIFont = emojiLabel.font ?? UIFont.systemFont(ofSize: 20.0) - - var previousLabelWidth: CGFloat = 0 - while font.pointSize < 50, frame.width - labelWidth >= 2 { // フォントサイズは高々50程度だろう - font = font.withSize(font.pointSize + 1) - labelWidth = getLabelWidth(text: defaultEmoji, font: font) - - if previousLabelWidth > 0, previousLabelWidth == labelWidth { break } // widthが更新されなくなったらbreak - previousLabelWidth = labelWidth - } - - emojiLabel.font = font + let font: UIFont = emojiLabel.font ?? UIFont.systemFont(ofSize: 50.0) + emojiLabel.font = font.withSize(50) // 多分shrinkが効いていい感じのサイズになる } } diff --git a/MissCat/View/Reusable/NoteCell/FileContainer.swift b/MissCat/View/Reusable/NoteCell/FileContainer.swift index ccd2e78..469ad57 100644 --- a/MissCat/View/Reusable/NoteCell/FileContainer.swift +++ b/MissCat/View/Reusable/NoteCell/FileContainer.swift @@ -6,7 +6,6 @@ // Copyright © 2020 Yuiga Wada. All rights reserved. // -import Agrume import MisskeyKit import RxCocoa import RxDataSources @@ -106,16 +105,13 @@ class FileContainer: UICollectionView, UICollectionViewDelegate, ComponentType { if item.isVideo { noteCellDelegate?.playVideo(url: item.originalUrl) } else { - showImage(url: item.originalUrl) + guard let url = URL(string: item.originalUrl) else { return } + let urls = fileModel.compactMap { $0.originalUrl }.compactMap { URL(string: $0) } + + let startIndex = urls.firstIndex(of: url) ?? 0 + noteCellDelegate?.showImage(urls, start: startIndex) } } - - private func showImage(url: String) { - guard let url = URL(string: url), let delegate = noteCellDelegate as? UIViewController else { return } - - let agrume = Agrume(url: url) - agrume.show(from: delegate) // 画像を表示 - } } extension FileContainer { diff --git a/MissCat/View/Reusable/NoteCell/NoteCell.swift b/MissCat/View/Reusable/NoteCell/NoteCell.swift index 1822840..ad6e54f 100644 --- a/MissCat/View/Reusable/NoteCell/NoteCell.swift +++ b/MissCat/View/Reusable/NoteCell/NoteCell.swift @@ -24,11 +24,12 @@ protocol NoteCellDelegate { func move2PostDetail(item: NoteCell.Model) func updateMyReaction(targetNoteId: String, rawReaction: String, plus: Bool) - func vote(choice: Int, to noteId: String) + func vote(choice: [Int], to noteId: String) func tappedLink(text: String) func move2Profile(userId: String) + func showImage(_ urls: [URL], start startIndex: Int) func playVideo(url: String) } @@ -169,6 +170,7 @@ class NoteCell: UITableViewCell, UITextViewDelegate, ReactionCellDelegate, UICol self.setupCollectionView() self.setupFileContainer() self.setupInnerRenoteDisplay() + self.setupPoll() self.selectedBackgroundView = UIView() return {} }() @@ -242,6 +244,14 @@ class NoteCell: UITableViewCell, UITextViewDelegate, ReactionCellDelegate, UICol } } + private func setupPoll() { + pollView.voteTriggar.asDriver(onErrorDriveWith: Driver.empty()).drive(onNext: { ids in + guard let noteId = self.noteId else { return } + self.viewModel?.updateVote(choices: ids) + self.delegate?.vote(choice: ids, to: noteId) + }).disposed(by: disposeBag) + } + private func binding(viewModel: ViewModel, noteId: String) { let output = viewModel.output @@ -325,10 +335,6 @@ class NoteCell: UITableViewCell, UITextViewDelegate, ReactionCellDelegate, UICol self.pollView.isHidden = false self.pollView.setPoll(with: poll) self.pollViewHeightConstraint.constant = self.pollView.height - - self.pollView.voteTriggar?.asDriver(onErrorDriveWith: Driver.empty()).drive(onNext: { id in - self.delegate?.vote(choice: id, to: noteId) - }).disposed(by: self.disposeBag) }).disposed(by: disposeBag) // general @@ -593,7 +599,7 @@ class NoteCell: UITableViewCell, UITextViewDelegate, ReactionCellDelegate, UICol if index < reactionsModel.count { let item = reactionsModel[index] - let shapedCell = viewModel.setReactionCell(with: item, to: cell) + let shapedCell = cell.transform(with: item) shapedCell.delegate = self return shapedCell diff --git a/MissCat/View/Reusable/NoteCell/PollView.swift b/MissCat/View/Reusable/NoteCell/PollView.swift index 3bb7163..096dcae 100644 --- a/MissCat/View/Reusable/NoteCell/PollView.swift +++ b/MissCat/View/Reusable/NoteCell/PollView.swift @@ -14,8 +14,9 @@ import UIKit class PollView: UIView { @IBOutlet weak var stackView: UIStackView! @IBOutlet weak var totalPollLabel: UILabel! + @IBOutlet weak var pollButton: UIButton! - var voteTriggar: Observable? // タップされるとvote対象のidを流す + var voteTriggar: PublishRelay<[Int]> = .init() // タップされるとvote対象のidを流す var height: CGFloat { guard pollBarCount > 0 else { return 0 } @@ -23,7 +24,7 @@ class PollView: UIView { return CGFloat(spaceCount * 10 + pollBarCount * pollBarHeight + 38) + totalPollLabel.frame.height } - private var pollBarHeight = 30 + private var pollBarHeight = 35 private var pollBarCount = 0 private var votesCountSum: Float = 0 { didSet { @@ -32,6 +33,9 @@ class PollView: UIView { } private var pollBars: [PollBar] = [] + private var selectedId: [Int] = [] + private var allowedMultiple: Bool = false // 複数選択可かどうか + private var finishVoting: Bool = false private let disposeBag = DisposeBag() // MARK: Life Cycle @@ -40,12 +44,14 @@ class PollView: UIView { super.init(frame: frame) loadNib() setTheme() + setupComponent() } required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder)! loadNib() setTheme() + setupComponent() } func loadNib() { @@ -63,6 +69,26 @@ class PollView: UIView { backgroundColor = colorPattern.base totalPollLabel.textColor = colorPattern.text } + if let mainColorHex = Theme.shared.currentModel?.mainColorHex { + let mainColor = UIColor(hex: mainColorHex) + pollButton.setTitleColor(mainColor, for: .normal) + pollButton.layer.borderColor = mainColor.cgColor + } + } + + private func setupComponent() { + pollButton.layer.borderWidth = 1 + pollButton.layer.cornerRadius = 5 + pollButton.setTitle("投票", for: .normal) + pollButton.contentEdgeInsets = .init(top: 5, left: 10, bottom: 5, right: 10) + + pollButton.rx.tap.subscribe(onNext: { _ in + guard self.selectedId.count > 0, !self.finishVoting else { return } + self.finishVoting = true + self.updatePoll(tapped: self.selectedId) + self.disablePollButton() + self.voteTriggar.accept(self.selectedId) + }).disposed(by: disposeBag) } // MARK: Publics @@ -74,7 +100,7 @@ class PollView: UIView { votesCountSum = choices.map { Float($0.votes ?? 0) }.reduce(0) { x, y in x + y } pollBarCount = choices.count - let canSeeRate: Bool = choices.map { $0.isVoted ?? false }.reduce(false) { x, y in x || y } // 一度でも自分は投票したか? + let finishVoting: Bool = choices.map { $0.isVoted ?? false }.reduce(false) { x, y in x || y } // 一度でも自分は投票したか? for id in 0 ..< choices.count { // 実際にvoteする際に、何番目の選択肢なのかサーバーに送信するのでforで回す let choice = choices[id] @@ -85,17 +111,25 @@ class PollView: UIView { name: choice.text ?? "", voteCount: count, rate: votesCountSum == 0 ? 0 : Float(count) / votesCountSum, - canSeeRate: canSeeRate, - isVoted: choice.isVoted ?? false) + finishVoting: finishVoting, + myVoted: choice.isVoted ?? false) - setupPollBarTapGesture(with: pollBar) + setupPollBarTapGesture(with: pollBar, finishVoting) pollBars.append(pollBar) stackView.addArrangedSubview(pollBar) } + + if finishVoting { + pollButton.setTitle("投票済", for: .normal) + } + + allowedMultiple = pollModel.multiple ?? false + self.finishVoting = finishVoting } func initialize() { pollBars.removeAll() + selectedId.removeAll() stackView.arrangedSubviews.forEach { $0.removeFromSuperview() } @@ -105,20 +139,42 @@ class PollView: UIView { // MARK: Privates - private func setupPollBarTapGesture(with pollBar: PollBar) { - guard let idOfTapped = pollBar.idOfTapped else { return } - - // PollViewのイベントをすべてmerge - voteTriggar = voteTriggar == nil ? idOfTapped : Observable.of(voteTriggar!, idOfTapped).merge() - voteTriggar!.subscribe(onNext: { id in - self.votesCountSum += 1 - self.pollBars.forEach { pollBar in // PollViewのタップイベントが発生したら、PollViewの状態をすべて変更する - let newVoteCount = pollBar.voteCount + (pollBar.id == id ? 1 : 0) - let newRate = Float(newVoteCount) / self.votesCountSum - - pollBar.changeState(voted: true, voteCount: newVoteCount, rate: newRate) + private func setupPollBarTapGesture(with pollBar: PollBar, _ cannotVote: Bool) { + guard !cannotVote else { return } + + pollBar.setTapGesture(disposeBag, closure: { + if self.selectedId.count > 0, !self.allowedMultiple { // 選択は一つまでの時 + self.pollBars + .filter { self.selectedId.contains($0.id) } + .forEach { $0.changeRadioState() } // 既存の選択を解除する + self.selectedId.removeAll() } - }).disposed(by: disposeBag) + + pollBar.changeRadioState() + + if pollBar.selected { + self.selectedId.append(pollBar.id) + } else if self.allowedMultiple { // 選択が解除され且つ複数選択可の場合 + guard let index = self.selectedId.firstIndex(of: pollBar.id) else { return } + self.selectedId.remove(at: index) + } + }) + } + + private func updatePoll(tapped ids: [Int]) { + votesCountSum += Float(ids.count) + pollBars.forEach { pollBar in // PollViewのタップイベントが発生したら、PollViewの状態をすべて変更する + let newVoteCount = pollBar.voteCount + (ids.contains(pollBar.id) ? 1 : 0) + let newRate = Float(newVoteCount) / self.votesCountSum + + pollBar.changeState(voted: true, voteCount: newVoteCount, rate: newRate) + } + } + + private func disablePollButton() { + UIView.animate(withDuration: 0.9, delay: 0.3, options: .curveEaseInOut, animations: { + self.pollButton.setTitle("投票済", for: .normal) + }, completion: nil) } } @@ -127,48 +183,78 @@ class PollView: UIView { extension PollView { class PollBar: UIView { struct Style { - var backgroundColor: UIColor = .init(hex: "ebebeb") + var backgroundColor: UIColor = .clear var textColor: UIColor = .black var progressColor: UIColor = .systemBlue + var borderColor: UIColor = .lightGray - var cornerRadius: CGFloat = 8 + var cornerRadius: CGFloat = 5 } var id: Int = -1 var voteCount: Int = 0 var idOfTapped: Observable? // PollBarのidを流す - private var style: Style = .init() - private var canSeeRate: Bool = false + private lazy var style: Style = getStyle() + private var finishVoting: Bool = false private var nameLabel: UILabel = .init() private var rateLabel: UILabel = .init() private var progressView: UIView = .init() + private var radioButton: RadioButton? private var progressConstraint: NSLayoutConstraint? + private var pollNameConstraint: NSLayoutConstraint? private let disposeBag = DisposeBag() + var selected: Bool { + guard let radioButton = radioButton else { return false } + return radioButton.currentState == .on + } + // MARK: LifeCycle - init(frame: CGRect, id: Int, name: String, voteCount: Int, rate: Float, canSeeRate: Bool, isVoted: Bool, style: Style = .init()) { + init(frame: CGRect, id: Int, name: String, voteCount: Int, rate: Float, finishVoting: Bool, myVoted: Bool, style: Style? = nil) { super.init(frame: frame) self.frame = frame - self.style = style + self.style = style ?? self.style self.id = id - self.canSeeRate = canSeeRate + self.finishVoting = finishVoting self.voteCount = voteCount - progressView = setupProgressView(rate: rate, canSeeRate: canSeeRate, style: style) - nameLabel = setupNameLabel(name: name, isVoted: isVoted, style: style) - rateLabel = setupRateLabel(rate: rate, canSeeRate: canSeeRate, style: style) + let progressView = setupProgressView(rate: rate, canSeeRate: finishVoting, style: self.style) + let radioButton = setupRadioButton(finishVoting: finishVoting) + let nameLabel = setupNameLabel(name: name, finishVoting: finishVoting, style: self.style, radioButton: radioButton) + let rateLabel = setupRateLabel(rate: rate, canSeeRate: finishVoting, style: self.style) + + changeStyle(with: self.style) + + self.progressView = progressView + self.radioButton = radioButton + self.nameLabel = nameLabel + self.rateLabel = rateLabel - changeStyle(with: style) + isUserInteractionEnabled = !finishVoting } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } + private func getStyle() -> Style { + let theme = Theme.shared.currentModel + return .init(backgroundColor: .clear, + textColor: theme?.colorPattern.ui.text ?? .black, + progressColor: getMainColor(), + borderColor: theme?.colorPattern.ui.sub3 ?? .lightGray, + cornerRadius: 5) + } + + private func getMainColor() -> UIColor { + guard let mainColorHex = Theme.shared.currentModel?.mainColorHex else { return .systemBlue } + return UIColor(hex: mainColorHex) + } + // MARK: Publics /// 投票率を変更し、アニメートさせる @@ -177,9 +263,12 @@ extension PollView { rateLabel.text = "\(Int(100 * newRate))%" // AutoLayoutを再設定 - guard let progressConstraint = progressConstraint else { return } + guard let progressConstraint = progressConstraint, + let pollNameConstraint = pollNameConstraint else { return } removeConstraint(progressConstraint) + removeConstraint(pollNameConstraint) + let newProgressConstraint = NSLayoutConstraint(item: progressView, attribute: .width, relatedBy: .equal, @@ -187,18 +276,30 @@ extension PollView { attribute: .width, multiplier: CGFloat(newRate), constant: 0) + + let newPollNameConstraint = NSLayoutConstraint(item: nameLabel, + attribute: .left, + relatedBy: .equal, + toItem: self, + attribute: .left, + multiplier: 1.0, + constant: 10) + addConstraint(newProgressConstraint) + addConstraint(newPollNameConstraint) + self.progressConstraint = newProgressConstraint UIView.animate(withDuration: 0.5, delay: 0, options: .curveEaseInOut, animations: { - if !self.canSeeRate { + if !self.finishVoting { self.progressView.alpha = 1 self.rateLabel.alpha = 1 } + self.radioButton?.alpha = 0 self.layoutIfNeeded() // AutoLayout更新 }, completion: { _ in - self.canSeeRate = true + self.finishVoting = true }) } @@ -265,7 +366,6 @@ extension PollView { constant: 0) ]) - setVoteGesture() progressView.alpha = canSeeRate ? 1 : 0 // 表示OKなら表示 self.progressConstraint = progressConstraint @@ -273,12 +373,12 @@ extension PollView { } // 選択肢のラベルを設定 - private func setupNameLabel(name: String, isVoted: Bool, style: Style) -> UILabel { + private func setupNameLabel(name: String, finishVoting: Bool, style: Style, radioButton: UIView) -> UILabel { let pollNameLabel = UILabel() pollNameLabel.numberOfLines = 0 pollNameLabel.lineBreakMode = NSLineBreakMode.byWordWrapping pollNameLabel.font = UIFont.systemFont(ofSize: 15.0) - pollNameLabel.text = (isVoted ? "✔ " : "") + name + pollNameLabel.text = name pollNameLabel.textColor = style.textColor pollNameLabel.sizeToFit() @@ -291,16 +391,28 @@ extension PollView { pollNameLabel.translatesAutoresizingMaskIntoConstraints = false addSubview(pollNameLabel) + var pollNameConstraint: NSLayoutConstraint + if finishVoting { + pollNameConstraint = NSLayoutConstraint(item: pollNameLabel, + attribute: .left, + relatedBy: .equal, + toItem: self, + attribute: .left, + multiplier: 1.0, + constant: 10) + } else { + pollNameConstraint = NSLayoutConstraint(item: pollNameLabel, + attribute: .left, + relatedBy: .equal, + toItem: radioButton, + attribute: .right, + multiplier: 1.0, + constant: 10) + } + // AutoLayout addConstraints([ - NSLayoutConstraint(item: pollNameLabel, - attribute: .left, - relatedBy: .equal, - toItem: self, - attribute: .left, - multiplier: 1.0, - constant: 10), - + pollNameConstraint, NSLayoutConstraint(item: pollNameLabel, attribute: .centerY, relatedBy: .equal, @@ -310,6 +422,7 @@ extension PollView { constant: 0) ]) + self.pollNameConstraint = pollNameConstraint return pollNameLabel } @@ -355,28 +468,64 @@ extension PollView { return pollRateLabel } + private func setupRadioButton(finishVoting: Bool) -> RadioButton { + // color + let theme = Theme.shared.currentModel + + let radio = RadioButton(frame: .zero, + normalColor: theme?.colorPattern.ui.sub2 ?? .black, + selectedColor: theme?.colorPattern.ui.sub2 ?? .black) // getMainColor()) + radio.layer.borderColor = theme?.colorPattern.ui.sub2.cgColor ?? UIColor.lightGray.cgColor + + // autolayout + radio.translatesAutoresizingMaskIntoConstraints = false + addSubview(radio) + addConstraints([ + NSLayoutConstraint(item: radio, + attribute: .left, + relatedBy: .equal, + toItem: self, + attribute: .left, + multiplier: 1.0, + constant: 10), + + NSLayoutConstraint(item: radio, + attribute: .centerY, + relatedBy: .equal, + toItem: self, + attribute: .centerY, + multiplier: 1.0, + constant: 0), + + NSLayoutConstraint(item: radio, + attribute: .width, + relatedBy: .equal, + toItem: self, + attribute: .height, + multiplier: 0.7, + constant: 0), + + NSLayoutConstraint(item: radio, + attribute: .height, + relatedBy: .equal, + toItem: self, + attribute: .height, + multiplier: 0.7, + constant: 0) + ]) + + radio.alpha = finishVoting ? 0 : 1 + return radio + } + // PollBarのデザインを変更 private func changeStyle(with style: Style) { backgroundColor = style.backgroundColor layer.cornerRadius = style.cornerRadius - } - - // Voteジェスチャー(タップジェスチャー)を設定 - private func setVoteGesture() { - let tapGesture = UITapGestureRecognizer() - idOfTapped = tapGesture.rx.event.map { _ in self.id } - - isUserInteractionEnabled = !canSeeRate - addGestureRecognizer(tapGesture) - setupVisualizePollTrigger(with: tapGesture.rx.event.asObservable()) - } - - // 使用者が投票したら、投票率と投票数を表示する - private func setupVisualizePollTrigger(with observable: Observable) { - observable.subscribe(onNext: { _ in - guard !self.canSeeRate else { return } - self.visualizePoll() - }).disposed(by: disposeBag) + layer.borderWidth = 1 + layer.borderColor = style.borderColor.cgColor + nameLabel.textColor = style.textColor + rateLabel.textColor = style.textColor } // 未投票時は見えなくなっているものを見えるようにする @@ -386,8 +535,104 @@ extension PollView { self.rateLabel.alpha = 1 }, completion: { _ in - self.canSeeRate = true + self.finishVoting = true }) } + + func changeRadioState() { + guard let radioButton = radioButton else { return } + let state: RadioButton.RadioState = radioButton.currentState == .on ? .off : .on + + radioButton.change(state: state) + } + } +} + +class RadioButton: UIView { + enum RadioState { + case on + case off + } + + var currentState: RadioState = .off + + private var normalColor: UIColor = .black + private var selectedColor: UIColor = .systemBlue + private var innerView: UIView = .init() + + init(frame: CGRect, normalColor: UIColor, selectedColor: UIColor) { + self.normalColor = normalColor + self.selectedColor = selectedColor + super.init(frame: frame) + setup() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + setup() + } + + override func layoutSubviews() { + super.layoutSubviews() + layer.cornerRadius = frame.width / 2 + innerView.layer.cornerRadius = innerView.frame.width / 2 + } + + private func setup() { + backgroundColor = .clear + layer.borderColor = normalColor.cgColor + layer.borderWidth = 1 + clipsToBounds = true + + // innerView + innerView.translatesAutoresizingMaskIntoConstraints = false + innerView.clipsToBounds = true + innerView.backgroundColor = selectedColor + innerView.alpha = 0 + addSubview(innerView) + addConstraints([ + NSLayoutConstraint(item: innerView, + attribute: .centerX, + relatedBy: .equal, + toItem: self, + attribute: .centerX, + multiplier: 1.0, + constant: 0), + + NSLayoutConstraint(item: innerView, + attribute: .centerY, + relatedBy: .equal, + toItem: self, + attribute: .centerY, + multiplier: 1.0, + constant: 0), + + NSLayoutConstraint(item: innerView, + attribute: .width, + relatedBy: .equal, + toItem: innerView, + attribute: .height, + multiplier: 1.0, + constant: 0), + + NSLayoutConstraint(item: innerView, + attribute: .height, + relatedBy: .equal, + toItem: self, + attribute: .height, + multiplier: 0.55, + constant: 0) + ]) + } + + func change(state: RadioState) { + guard state != currentState else { return } + + UIView.animate(withDuration: 0.2, delay: 0, options: .curveEaseInOut, animations: { + self.innerView.layer.cornerRadius = self.innerView.frame.width / 2 + self.innerView.alpha = state == .on ? 1 : 0 + }, completion: nil) + + currentState = state } } diff --git a/MissCat/View/Reusable/NoteCell/ReactionCell.swift b/MissCat/View/Reusable/NoteCell/ReactionCell.swift index 52b9875..66fdaf2 100644 --- a/MissCat/View/Reusable/NoteCell/ReactionCell.swift +++ b/MissCat/View/Reusable/NoteCell/ReactionCell.swift @@ -14,7 +14,10 @@ protocol ReactionCellDelegate { func tappedReaction(noteId: String, reaction: String, isRegister: Bool) // isRegister: リアクションを「登録」するのか「取り消す」のか } -class ReactionCell: UICollectionViewCell { +class ReactionCell: UICollectionViewCell, ComponentType { + typealias Arg = NoteCell.Reaction + typealias Transformed = ReactionCell + @IBOutlet weak var reactionCounterLabel: UILabel! @IBOutlet weak var defaultEmojiLabel: UILabel! @@ -51,33 +54,32 @@ class ReactionCell: UICollectionViewCell { changeColor(isMyReaction: isMyReaction) } - // MARK: Setup + // MARK: Publics - private func addTapGesture(to view: UIView) { - let tapGesture = UITapGestureRecognizer() + func transform(with arg: NoteCell.Reaction) -> ReactionCell { + let item = arg + guard let rawEmoji = item.rawEmoji else { return self } - // 各々のEmojiViewに対してtap gestureを付加する - tapGesture.rx.event.bind { _ in - self.isMyReaction = !self.isMyReaction - - UIView.animate(withDuration: 0.3, - animations: { - self.changeColor(isMyReaction: self.isMyReaction) - self.changeCounter(plus: self.isMyReaction) - }, completion: { _ in - guard let delegate = self.delegate, - let noteId = self.noteId, - let rawReaction = self.rawReaction else { return } - - delegate.tappedReaction(noteId: noteId, reaction: rawReaction, isRegister: self.isMyReaction) - - }) - }.disposed(by: disposeBag) + if let customEmojiUrl = item.url { + setup(noteId: item.noteId, + count: item.count, + customEmoji: customEmojiUrl, + isMyReaction: item.isMyReaction, + rawReaction: rawEmoji) + } else { + setup(noteId: item.noteId, + count: item.count, + rawDefaultEmoji: rawEmoji, + isMyReaction: item.isMyReaction, + rawReaction: rawEmoji) + } - view.addGestureRecognizer(tapGesture) + return self } - func setup(noteId: String?, count: String, defaultEmoji: String? = nil, customEmoji: String? = nil, rawDefaultEmoji: String? = nil, isMyReaction: Bool, rawReaction: String) { + // MARK: Setup + + private func setup(noteId: String?, count: String, defaultEmoji: String? = nil, customEmoji: String? = nil, rawDefaultEmoji: String? = nil, isMyReaction: Bool, rawReaction: String) { self.isMyReaction = isMyReaction self.rawReaction = rawReaction self.noteId = noteId ?? "" @@ -105,6 +107,30 @@ class ReactionCell: UICollectionViewCell { } } + private func addTapGesture(to view: UIView) { + let tapGesture = UITapGestureRecognizer() + + // 各々のEmojiViewに対してtap gestureを付加する + tapGesture.rx.event.bind { _ in + self.isMyReaction = !self.isMyReaction + + UIView.animate(withDuration: 0.3, + animations: { + self.changeColor(isMyReaction: self.isMyReaction) + self.changeCounter(plus: self.isMyReaction) + }, completion: { _ in + guard let delegate = self.delegate, + let noteId = self.noteId, + let rawReaction = self.rawReaction else { return } + + delegate.tappedReaction(noteId: noteId, reaction: rawReaction, isRegister: self.isMyReaction) + + }) + }.disposed(by: disposeBag) + + view.addGestureRecognizer(tapGesture) + } + func setGradation(view: UIView) { let gradientLayer = CAGradientLayer() gradientLayer.colors = [UIColor.darkGray.cgColor, UIColor.lightGray.cgColor] diff --git a/MissCat/View/Reusable/NoteCell/nib/PollView.xib b/MissCat/View/Reusable/NoteCell/nib/PollView.xib index a038408..cc25619 100644 --- a/MissCat/View/Reusable/NoteCell/nib/PollView.xib +++ b/MissCat/View/Reusable/NoteCell/nib/PollView.xib @@ -10,6 +10,7 @@ + @@ -22,21 +23,27 @@ - - + - - + + + + diff --git a/MissCat/View/Reusable/Others/NoteDisplay.swift b/MissCat/View/Reusable/Others/NoteDisplay.swift index ebe2834..07232e3 100644 --- a/MissCat/View/Reusable/Others/NoteDisplay.swift +++ b/MissCat/View/Reusable/Others/NoteDisplay.swift @@ -7,6 +7,7 @@ // import Foundation +import UIKit /// NoteCell上のタップ処理はすべてHomeViewControllerが行う。 /// そこで、NoteCellを表示するViewControllerはすべて、このNoteDisplayを継承することで、 @@ -56,10 +57,14 @@ class NoteDisplay: UIViewController, NoteCellDelegate, UserCellDelegate { func updateMyReaction(targetNoteId: String, rawReaction: String, plus: Bool) {} - func vote(choice: Int, to noteId: String) { + func vote(choice: [Int], to noteId: String) { homeViewController?.vote(choice: choice, to: noteId) } + func showImage(_ urls: [URL], start startIndex: Int) { + homeViewController?.showImage(urls, start: startIndex) + } + func playVideo(url: String) { homeViewController?.playVideo(url: url) } diff --git a/MissCat/View/Reusable/Reaction/ReactionGenViewController.swift b/MissCat/View/Reusable/Reaction/ReactionGenViewController.swift index 18acf83..57fab61 100644 --- a/MissCat/View/Reusable/Reaction/ReactionGenViewController.swift +++ b/MissCat/View/Reusable/Reaction/ReactionGenViewController.swift @@ -19,7 +19,7 @@ private typealias ViewModel = ReactionGenViewModel typealias EmojisDataSource = RxCollectionViewSectionedReloadDataSource class ReactionGenViewController: UIViewController, UISearchBarDelegate, UIScrollViewDelegate, UICollectionViewDelegate, UICollectionViewDelegateFlowLayout { @IBOutlet weak var searchBar: UISearchBar! - @IBOutlet weak var iconImageView: UIImageView! + @IBOutlet weak var iconImageView: MissCatImageView! @IBOutlet weak var displayNameLabel: UILabel! @IBOutlet weak var targetNoteTextView: UITextView! @IBOutlet weak var targetNoteDisplayView: UIView! @@ -61,7 +61,6 @@ class ReactionGenViewController: UIViewController, UISearchBarDelegate, UIScroll targetNoteDisplayView.isHidden = onPostViewController borderOriginXConstraint.isActive = !onPostViewController - iconImageView.layer.cornerRadius = iconImageView.frame.width / 2 } override func viewDidAppear(_ animated: Bool) { @@ -150,18 +149,25 @@ class ReactionGenViewController: UIViewController, UISearchBarDelegate, UIScroll } private func setupComponents() { + // icon + iconImageView.maskCircle() + + // collectionView emojiCollectionView.register(UINib(nibName: "ReactionCollectionHeader", bundle: nil), forCellWithReuseIdentifier: "ReactionCollectionHeader") emojiCollectionView.register(UINib(nibName: "EmojiViewCell", bundle: nil), forCellWithReuseIdentifier: "EmojiCell") emojiCollectionView.rx.setDelegate(self).disposed(by: disposeBag) + + // searchBar searchBar.delegate = self searchBar.autocorrectionType = .no searchBar.keyboardType = .emailAddress + // targetNote targetNoteTextView.textContainer.lineBreakMode = .byTruncatingTail targetNoteTextView.textContainer.maximumNumberOfLines = 2 + // button settingsButton.titleLabel?.font = .awesomeSolid(fontSize: 15.0) - settingsButton.rx.tap.subscribe(onNext: { _ in // ReactionGenが閉じてから設定を開く self.dismiss(animated: true, completion: { diff --git a/MissCat/View/Reusable/User/UserCell.swift b/MissCat/View/Reusable/User/UserCell.swift index 0868ab0..fc1fcee 100644 --- a/MissCat/View/Reusable/User/UserCell.swift +++ b/MissCat/View/Reusable/User/UserCell.swift @@ -28,7 +28,7 @@ class UserCell: UITableViewCell, ComponentType, UITextViewDelegate { // MARK: Views - @IBOutlet weak var iconView: UIImageView! + @IBOutlet weak var iconView: MissCatImageView! @IBOutlet weak var nameTextView: MisskeyTextView! @IBOutlet weak var descriptionTextView: MisskeyTextView! @IBOutlet weak var separatorView: UIView! @@ -113,7 +113,7 @@ class UserCell: UITableViewCell, ComponentType, UITextViewDelegate { } private func setupComponents() { - iconView.layer.cornerRadius = iconView.frame.height / 2 + iconView.maskCircle() descriptionTextView.delegate = self } diff --git a/MissCat/View/Reusable/User/UserListViewController.swift b/MissCat/View/Reusable/User/UserListViewController.swift index 56d083e..e3be7b7 100644 --- a/MissCat/View/Reusable/User/UserListViewController.swift +++ b/MissCat/View/Reusable/User/UserListViewController.swift @@ -19,12 +19,13 @@ class UserListViewController: NoteDisplay, UITableViewDelegate { private lazy var dataSource = self.setupDataSource() private let disposeBag: DisposeBag = .init() + private var lockScroll: Bool = true private var withTopShadow: Bool = false private var topShadow: CALayer? // MARK: I/O - func setup(type: UserListType, userId: String? = nil, query: String? = nil, listId: String? = nil, withTopShadow: Bool = false) { + func setup(type: UserListType, userId: String? = nil, query: String? = nil, lockScroll: Bool = true, listId: String? = nil, withTopShadow: Bool = false) { let input = UserListViewModel.Input(dataSource: dataSource, type: type, userId: userId, @@ -34,6 +35,7 @@ class UserListViewController: NoteDisplay, UITableViewDelegate { let viewModel: UserListViewModel = .init(with: input, and: disposeBag) self.viewModel = viewModel self.withTopShadow = withTopShadow + self.lockScroll = lockScroll } // MARK: LifeCycle @@ -65,6 +67,7 @@ class UserListViewController: NoteDisplay, UITableViewDelegate { let output = viewModel.output output.users.bind(to: mainTableView.rx.items(dataSource: dataSource)).disposed(by: disposeBag) + mainTableView.lockScroll = Observable.of(lockScroll) } private func bindTheme() { diff --git a/MissCat/View/Reusable/User/nib/UserCell.xib b/MissCat/View/Reusable/User/nib/UserCell.xib index 28d2b1e..5a9dab4 100644 --- a/MissCat/View/Reusable/User/nib/UserCell.xib +++ b/MissCat/View/Reusable/User/nib/UserCell.xib @@ -23,7 +23,7 @@ - + @@ -62,7 +62,7 @@ - + diff --git a/MissCat/View/Settings/AccountViewController.swift b/MissCat/View/Settings/AccountViewController.swift index 5e69073..9fcaa83 100644 --- a/MissCat/View/Settings/AccountViewController.swift +++ b/MissCat/View/Settings/AccountViewController.swift @@ -6,6 +6,7 @@ // Copyright © 2020 Yuiga Wada. All rights reserved. // +import MisskeyKit import RxCocoa import RxSwift import UIKit @@ -13,6 +14,7 @@ import UIKit class AccountViewController: UITableViewController { @IBOutlet weak var logoutButton: UIButton! + var homeViewController: HomeViewController? private var disposeBag = DisposeBag() override func viewDidLoad() { @@ -68,13 +70,19 @@ class AccountViewController: UITableViewController { } private func logout() { - let viewController = getViewController(name: "start") + guard let startViewController = getViewController(name: "start") as? StartViewController else { return } Cache.UserDefaults.shared.setCurrentLoginedApiKey("") Cache.UserDefaults.shared.setCurrentLoginedInstance("") Cache.UserDefaults.shared.setCurrentLoginedUserId("") + Cache.shared.resetMyCache() - navigationController?.pushViewController(viewController, animated: true) + MisskeyKit.auth.setAPIKey("") + + startViewController.setup(afterLogout: true) + presentOnFullScreen(startViewController, animated: true) { + self.homeViewController?.relaunchView(start: .main) // すべてのviewをrelaunchする + } } private func getViewController(name: String) -> UIViewController { diff --git a/MissCat/View/Settings/LicenseViewController.swift b/MissCat/View/Settings/LicenseViewController.swift index 8402948..75a4e24 100644 --- a/MissCat/View/Settings/LicenseViewController.swift +++ b/MissCat/View/Settings/LicenseViewController.swift @@ -6,454 +6,772 @@ // Copyright © 2020 Yuiga Wada. All rights reserved. // +import Eureka +import RxCocoa +import RxSwift import UIKit -class LicenseViewController: UIViewController { - @IBOutlet weak var textView: UITextView! +class LicenseTableViewController: FormViewController { + private var disposeBag: DisposeBag = .init() + private lazy var ossDesc: [String: String] = getOssDescription() + + // MARK: LifeCycle override func viewDidLoad() { super.viewDidLoad() - - let font = UIFont(name: "Helvetica", size: 11.0) - textView.attributedText = MFMEngine.generatePlaneString(string: raw, font: font) + setupComponent() + setTable() + setTheme() } - private var raw = """ - ・Agrume by JanGorman
・Down by AssistoLab
・FloatingPanel by SCENEE
・Gifu by kaishin
・MisskeyKit by YuigaWada
・PolioPager by YuigaWada
・YanagiText by YuigaWada
・photo-editor by eventtus
・RxCocoa by RxSwiftCommunity
・RxDataSources by RxSwiftCommunity
・RxSwift by ReactiveX
・SVGKit by gupuru
・SkeletonView by Juanpe
・Starscream by daltoniam
・XLPagerTabStrip by xmartlabs




Agrume


The MIT License (MIT) - - Copyright (c) 2015 Jan Gorman - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. -



Down


The MIT License (MIT) - - Copyright (c) 2014 kevin-hirsch - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE.



FloatingPanel


MIT License - - Copyright (c) 2018 Shin Yamamoto - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE.



Gifu


The MIT License (MIT) - - Copyright (c) 2014-2018 Reda Lemeden. - - Permission is hereby granted, free of charge, to any person obtaining a copy of - this software and associated documentation files (the "Software"), to deal in - the Software without restriction, including without limitation the rights to - use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of - the Software, and to permit persons to whom the Software is furnished to do so, - subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER - IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN - CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - - The name and characters used in the demo of this software are property of their - respective owners. - -



MisskeyKit


MIT License - - Copyright (c) 2019 YuigaWada - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. -



PolioPager


MIT License - - Copyright (c) 2019 YuigaWada - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. -



YanagiText


MIT License - - Copyright (c) 2019 YuigaWada - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. -



photo-editor


MIT License - - Copyright (c) 2017 Mohamed Hamed - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. -



RxCocoa


Copyright (c) 2017 - present RxSwiftCommunity - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in - all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - THE SOFTWARE. -



RxDataSources


MIT License - - Copyright (c) 2017 RxSwift Community - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. -



RxSwift


**The MIT License** - **Copyright © 2015 Krunoslav Zaher** - **All rights reserved.** - - Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.



SkeletonView


The MIT License (MIT) - - Copyright (c) 2017 Juanpe Catalán - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. + // MARK: Design -



Starscream


Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - Copyright (c) 2014-2016 Dalton Cherry. - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. + private func bindTheme() { + let theme = Theme.shared.theme + + theme.map { $0.colorPattern.ui }.subscribe(onNext: { colorPattern in + self.view.backgroundColor = colorPattern.base + }).disposed(by: disposeBag) + } - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." + private func setTheme() { + if let colorPattern = Theme.shared.currentModel?.colorPattern.ui { + view.backgroundColor = colorPattern.base + tableView.backgroundColor = colorPattern.base + } + } - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. + /// ステータスバーの文字色 + override var preferredStatusBarStyle: UIStatusBarStyle { + let currentColorMode = Theme.shared.currentModel?.colorMode ?? .light + return currentColorMode == .light ? UIStatusBarStyle.default : UIStatusBarStyle.lightContent + } - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - ly display, ly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. + private func getCellBackgroundColor() -> UIColor { + guard let theme = Theme.shared.currentModel else { return .white } + return theme.colorMode == .light ? theme.colorPattern.ui.base : theme.colorPattern.ui.sub2 + } - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. + private func getCellTextColor() -> UIColor { + guard let theme = Theme.shared.currentModel else { return .black } + return theme.colorPattern.ui.text + } - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: + private func changeSeparatorStyle() { + let currentColorMode = Theme.shared.currentModel?.colorMode ?? .light + tableView.separatorStyle = currentColorMode == .light ? .singleLine : .none + } - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and + // MARK: Setup - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and + private func setupComponent() { + title = "LICENSE" + changeSeparatorStyle() + } - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and + private func setTable() { + guard let theme = Theme.shared.currentModel else { return } + + let licenseSection = getLicenseSection(with: theme) + form +++ licenseSection + } - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. + private func getLicenseSection(with theme: Theme.Model) -> Section { + let section = Section(header: "License", footer: nil) + + for (name, desc) in ossDesc { + section <<< LabelRow { + $0.title = name + }.cellSetup { cell, _ in + cell.height = { 60 } + }.cellUpdate { cell, _ in + cell.backgroundColor = self.getCellBackgroundColor() + cell.textLabel?.textColor = self.getCellTextColor() + cell.detailTextLabel?.textColor = self.getCellTextColor() + + cell.textLabel?.font = .boldSystemFont(ofSize: 17) + cell.detailTextLabel?.font = .boldSystemFont(ofSize: 17) + }.onCellSelection { _, _ in + self.showDescription(desc) + } + } + + return section + } - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. + // MARK: ViewController - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. + private func showDescription(_ desc: String) { + guard let viewController = getViewController(name: "license-desc") as? LicenseViewController else { return } + viewController.setText(desc) + navigationController?.pushViewController(viewController, animated: true) + } - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. + private func getViewController(name: String) -> UIViewController { + let storyboard = UIStoryboard(name: "Main", bundle: nil) + let viewController = storyboard.instantiateViewController(withIdentifier: name) + + return viewController + } - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. + // MARK: Const - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. + private func getOssDescription() -> [String: String] { + return ["MisskeyKit": """ + MIT License + + Copyright (c) 2019 YuigaWada + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + + """, + "YanagiText": """ + MIT License + + Copyright (c) 2019 YuigaWada + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + + """, + "PolioPager": """ + MIT License + + Copyright (c) 2019 YuigaWada + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + + """, + "Starscream": """ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + Copyright (c) 2014-2016 Dalton Cherry. + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + """, + "RxSwift": """ + **The MIT License** + **Copyright © 2015 Krunoslav Zaher** + **All rights reserved.** + + Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + """, + "RxCocoa": """ + Copyright (c) 2017 - present RxSwiftCommunity + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + + """, + "RxDataSources": """ + MIT License + + Copyright (c) 2017 RxSwift Community + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + + """, + "Agrume": """ + The MIT License (MIT) + + Copyright (c) 2015 Jan Gorman + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + + """, + "SkeletonView": """ + The MIT License (MIT) + + Copyright (c) 2017 Juanpe Catalán + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + + + """, + "FloatingPanel": """ + MIT License + + Copyright (c) 2018 Shin Yamamoto + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + """, + "Gifu": """ + The MIT License (MIT) + + Copyright (c) 2014-2018 Reda Lemeden. + + Permission is hereby granted, free of charge, to any person obtaining a copy of + this software and associated documentation files (the "Software"), to deal in + the Software without restriction, including without limitation the rights to + use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + the Software, and to permit persons to whom the Software is furnished to do so, + subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + The name and characters used in the demo of this software are property of their + respective owners. + + + """, + "photo-editor": """ + MIT License + + Copyright (c) 2017 Mohamed Hamed + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + + """, + "XLPagerTabStrip": """ + The MIT License (MIT) + + Copyright (c) 2019 Xmartlabs SRL + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + + """, + "APNGKit": """ + The MIT License (MIT) + + Copyright (c) 2015 Wei Wang + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + + """, + "SwiftLinkPreview": """ + The MIT License (MIT) + + Copyright (c) 2016 Leonardo Cardoso + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + """, + "Cache": """ + Licensed under the **MIT** license + + > Copyright (c) 2015 Hyper Interaktiv AS + > + > Permission is hereby granted, free of charge, to any person obtaining + > a copy of this software and associated documentation files (the + > "Software"), to deal in the Software without restriction, including + > without limitation the rights to use, copy, modify, merge, publish, + > distribute, sublicense, and/or sell copies of the Software, and to + > permit persons to whom the Software is furnished to do so, subject to + > the following conditions: + > + > The above copyright notice and this permission notice shall be + > included in all copies or substantial portions of the Software. + > + > THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + > EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + > MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + > IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + > CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + > TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + > SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + """, + "MessageKit": """ + MIT License + + Copyright (c) 2017-2019 MessageKit + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + + """, + "Eureka": """ + The MIT License (MIT) + + Copyright (c) 2019 XMARTLABS + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + + """, + "ChromaColorPicker": """ + MIT License + + Copyright (c) 2016 Jonathan Cardasis + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + + """, + "XLActionController": """ + The MIT License (MIT) + + Copyright (c) 2019 XMARTLABS (http://xmartlabs.com) + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + + """] + } +} + +/// ライセンスの全文を表示するためだけのクラス +class LicenseViewController: UIViewController { + @IBOutlet weak var textView: UITextView! - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability.



XLPagerTabStrip


The MIT License (MIT) + private var rawText: String = "" + override func viewDidLoad() { + super.viewDidLoad() + setTheme() + + textView.font = UIFont(name: "Helvetica", size: 14.0) + textView.text = rawText + } - Copyright (c) 2019 Xmartlabs SRL + func setText(_ raw: String) { + rawText = raw + } - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: + // MARK: Design - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. + private func setTheme() { + if let colorPattern = Theme.shared.currentModel?.colorPattern.ui { + view.backgroundColor = colorPattern.base + textView.backgroundColor = colorPattern.base + textView.textColor = colorPattern.text + } + } - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. -



- """ + /// ステータスバーの文字色 + override var preferredStatusBarStyle: UIStatusBarStyle { + let currentColorMode = Theme.shared.currentModel?.colorMode ?? .light + return currentColorMode == .light ? UIStatusBarStyle.default : UIStatusBarStyle.lightContent + } } diff --git a/MissCat/View/Settings/ProfileSettingsViewController.swift b/MissCat/View/Settings/ProfileSettingsViewController.swift new file mode 100644 index 0000000..12e12c1 --- /dev/null +++ b/MissCat/View/Settings/ProfileSettingsViewController.swift @@ -0,0 +1,486 @@ +// +// ProfileSettingsViewController.swift +// MissCat +// +// Created by Yuiga Wada on 2020/05/14. +// Copyright © 2020 Yuiga Wada. All rights reserved. +// + +import Eureka +import RxCocoa +import RxSwift +import UIKit + +class ProfileSettingsViewController: FormViewController { + var homeViewController: HomeViewController? + var overrideInfoTrigger: PublishRelay = .init() + + private var disposeBag: DisposeBag = .init() + + private lazy var bannerCover: UIView = .init() + private lazy var bannerImage: MissCatImageView = .init() + private lazy var iconImage: MissCatImageView = .init() + + private let saveButtonItem = UIBarButtonItem(title: "保存", style: .plain, target: nil, action: nil) + private var headerHeight: CGFloat = 150 + + private let selectedImage: PublishRelay = .init() + private let resetImage: PublishRelay = .init() + private var viewModel: ProfileSettingsViewModel? + + // MARK: Row + + private lazy var catSwitch: SwitchRow = SwitchRow { row in + row.tag = "cat-switch" + row.title = "Catとして設定" + }.cellUpdate { cell, _ in + cell.backgroundColor = self.getCellBackgroundColor() + cell.textLabel?.textColor = Theme.shared.currentModel?.colorPattern.ui.text ?? .black + } + + private lazy var bioTextArea = TextAreaRow { row in + row.tag = "bio-text-area" + row.placeholder = "自分について..." + row.textAreaHeight = .dynamic(initialTextViewHeight: 220) + }.cellUpdate { cell, _ in + cell.backgroundColor = self.getCellBackgroundColor() + cell.textLabel?.textColor = Theme.shared.currentModel?.colorPattern.ui.text + cell.placeholderLabel?.textColor = .lightGray + cell.textView?.textColor = Theme.shared.currentModel?.colorPattern.ui.text + } + + private lazy var nameTextArea = TextRow { row in + row.tag = "name-text" + row.title = "名前" + }.cellUpdate { cell, _ in + cell.backgroundColor = self.getCellBackgroundColor() + cell.textLabel?.textColor = Theme.shared.currentModel?.colorPattern.ui.text + cell.textField?.textColor = Theme.shared.currentModel?.colorPattern.ui.text + } + + // MARK: LifeCycle + + func setup(banner: UIImage? = nil, bannerUrl: String, icon: UIImage? = nil, iconUrl: String, name: String, description: String, isCat: Bool) { + bannerImage.image = banner + iconImage.image = icon + + let loadIcon = icon == nil + let loadBanner = banner == nil + + nameTextArea.value = name + bioTextArea.value = description + catSwitch.value = isCat + let viewModel = getViewModel(loadIcon: loadIcon, + loadBanner: loadBanner, + bannerUrl: bannerUrl, + iconUrl: iconUrl, + name: name, + description: description, + isCat: isCat) + self.viewModel = viewModel + } + + private func getViewModel(loadIcon: Bool, loadBanner: Bool, bannerUrl: String?, iconUrl: String?, name: String, description: String, isCat: Bool) -> ProfileSettingsViewModel { + let input: ProfileSettingsViewModel.Input = .init(needLoadIcon: loadIcon, + needLoadBanner: loadBanner, + iconUrl: iconUrl, + bannerUrl: bannerUrl, + currentName: name, + currentDescription: description, + currentCatState: isCat, + rxName: nameTextArea.rx.value, + rxDesc: bioTextArea.rx.value, + rxCat: catSwitch.rx.value, + rightNavButtonTapped: saveButtonItem.rx.tap, + iconTapped: iconImage.rxTap, + bannerTapped: bannerImage.rxTap, + selectedImage: selectedImage.asObservable(), + resetImage: resetImage.asObservable(), + overrideInfoTrigger: overrideInfoTrigger) + let viewModel = ProfileSettingsViewModel(with: input, and: disposeBag) + binding(with: viewModel) + + return viewModel + } + + override func viewDidLoad() { + super.viewDidLoad() + // ui + setupComponent() + setTable() + setIconCover() + setupNavBar() + + // theme + setTheme() + bindTheme() + + // viewModel + viewModel?.transform() + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + navigationController?.setNavigationBarHidden(false, animated: animated) + } + + // MARK: Design + + private func bindTheme() { + let theme = Theme.shared.theme + + theme.map { $0.colorPattern.ui }.subscribe(onNext: { colorPattern in + self.view.backgroundColor = colorPattern.base + }).disposed(by: disposeBag) + } + + private func setTheme() { + if let colorPattern = Theme.shared.currentModel?.colorPattern.ui { + view.backgroundColor = colorPattern.base + tableView.backgroundColor = colorPattern.base + } + + if let mainColorHex = Theme.shared.currentModel?.mainColorHex { + catSwitch.cell.switchControl.onTintColor = UIColor(hex: mainColorHex) + } + } + + /// ステータスバーの文字色 + override var preferredStatusBarStyle: UIStatusBarStyle { + let currentColorMode = Theme.shared.currentModel?.colorMode ?? .light + return currentColorMode == .light ? UIStatusBarStyle.default : UIStatusBarStyle.lightContent + } + + private func getCellBackgroundColor() -> UIColor { + guard let theme = Theme.shared.currentModel else { return .white } + return theme.colorMode == .light ? theme.colorPattern.ui.base : theme.colorPattern.ui.sub2 + } + + private func changeSeparatorStyle() { + let currentColorMode = Theme.shared.currentModel?.colorMode ?? .light + tableView.separatorStyle = currentColorMode == .light ? .singleLine : .none + } + + // MARK: Binding + + private func binding(with viewModel: ProfileSettingsViewModel) { + viewModel.output + .banner + .asDriver(onErrorDriveWith: Driver.empty()) + .drive(bannerImage.rx.image) + .disposed(by: disposeBag) + + viewModel.output + .icon + .asDriver(onErrorDriveWith: Driver.empty()) + .drive(iconImage.rx.image) + .disposed(by: disposeBag) + + viewModel.output + .name + .asDriver(onErrorDriveWith: Driver.empty()) + .drive(onNext: { name in + self.nameTextArea.value = name + }).disposed(by: disposeBag) + + viewModel.output + .description + .asDriver(onErrorDriveWith: Driver.empty()) + .drive(onNext: { description in + self.bioTextArea.value = description + }).disposed(by: disposeBag) + + viewModel.output + .isCat + .asDriver(onErrorDriveWith: Driver.empty()) + .drive(onNext: { isCat in + self.catSwitch.value = isCat + }).disposed(by: disposeBag) + + // trigger + viewModel.output + .pickImageTrigger + .asDriver(onErrorDriveWith: Driver.empty()) + .drive(onNext: { hasChanged in + self.showImageMenu(hasChanged) + }).disposed(by: disposeBag) + + viewModel.output + .popViewControllerTrigger + .asDriver(onErrorDriveWith: Driver.empty()) + .drive(onNext: { _ in + self.navigationController?.popViewController(animated: true) + }).disposed(by: disposeBag) + } + + // MARK: Setup + + private func setupNavBar() { + navigationItem.rightBarButtonItem = saveButtonItem + } + + private func setupComponent() { + title = "プロフィールの編集" + changeSeparatorStyle() + bannerImage.clipsToBounds = true + iconImage.clipsToBounds = true + bannerImage.backgroundColor = .lightGray + iconImage.backgroundColor = .lightGray + + bannerImage.contentMode = .scaleAspectFill + } + + private func setTable() { + let headerSection = Section { section in // バナーimageをHeaderとしてEurekaに埋め込む + section.header = { + var header = HeaderFooterView(.callback { + self.getHeader() + }) + header.height = { self.headerHeight } + return header + }() + } + + let nameSection = getNameSection() + let descSection = getDescSection() + let miscSection = getMiscSection() + + form +++ headerSection +++ nameSection +++ descSection +++ miscSection + + bioTextArea.cell.textView.inputAccessoryView = getToolBar(for: bioTextArea.cell.textView) + nameTextArea.cell.textField.inputAccessoryView = getToolBar(for: nameTextArea.cell.textField) + } + + // MARK: Toolbar + + /// targetに対してToolBarを生成する + /// - Parameter target: Target + private func getToolBar(for target: UITextInput) -> UIToolbar { + let toolBar = UIToolbar() + let flexibleItem = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil) + + let emojiButton = UIBarButtonItem(title: "laugh-squint", style: .plain, target: self, action: nil) + let doneButton = UIBarButtonItem(title: "Done", style: .done, target: self, action: nil) + emojiButton.rx.tap.subscribe { _ in self.showReactionGen(target: target) }.disposed(by: disposeBag) + doneButton.rx.tap.subscribe { _ in self.view.endEditing(true) }.disposed(by: disposeBag) + toolBar.setItems([flexibleItem, flexibleItem, + flexibleItem, + flexibleItem, flexibleItem, + emojiButton, doneButton], animated: true) + toolBar.sizeToFit() + + emojiButton.setTitleTextAttributes([NSAttributedString.Key.font: UIFont.awesomeSolid(fontSize: 17.0)!], for: .normal) + emojiButton.setTitleTextAttributes([NSAttributedString.Key.font: UIFont.awesomeSolid(fontSize: 17.0)!], for: .selected) + return toolBar + } + + /// ReactionGen(絵文字ピッカー)を表示する + /// - Parameter viewWithText: UITextInput + private func showReactionGen(target viewWithText: UITextInput) { + guard let reactionGen = getViewController(name: "reaction-gen") as? ReactionGenViewController else { return } + + reactionGen.onPostViewController = true + reactionGen.selectedEmoji.subscribe(onNext: { emojiModel in // ReactionGenで絵文字が選択されたらに送られてくる + self.insertCustomEmoji(with: emojiModel, to: viewWithText) + reactionGen.dismiss(animated: true, completion: nil) + }).disposed(by: disposeBag) + + presentWithSemiModal(reactionGen, animated: true, completion: nil) + } + + /// StoryBoardからVCを生成する + /// - Parameter name: name + private func getViewController(name: String) -> UIViewController { + let storyboard = UIStoryboard(name: "Main", bundle: nil) + let viewController = storyboard.instantiateViewController(withIdentifier: name) + + return viewController + } + + /// TextView, TextFiledに対して、現時点でのカーソル位置に絵文字を挿入する + /// - Parameters: + /// - emojiModel: EmojiView.EmojiModel + /// - viewWithText: TextView, TextFiled...etc + private func insertCustomEmoji(with emojiModel: EmojiView.EmojiModel, to viewWithText: UITextInput) { + if let selectedTextRange = viewWithText.selectedTextRange { + guard let emoji = emojiModel.isDefault ? emojiModel.defaultEmoji : ":\(emojiModel.rawEmoji):" else { return } + viewWithText.replace(selectedTextRange, + withText: emoji) + } + } + + // MARK: Section + + private func getNameSection() -> Section { + return Section("Name") <<< nameTextArea + } + + private func getDescSection() -> Section { + return Section("Bio") <<< bioTextArea + } + + private func getMiscSection() -> Section { + return Section(header: "Cat", footer: "ONにすると自分の投稿がネコ語に翻訳されます") <<< catSwitch + } + + // MARK: Header + + private func getHeader() -> UIView { + iconImage.translatesAutoresizingMaskIntoConstraints = false + bannerImage.addSubview(iconImage) + + // AutoLayout + bannerImage.addConstraints([ + NSLayoutConstraint(item: iconImage, + attribute: .leading, + relatedBy: .equal, + toItem: bannerImage, + attribute: .leading, + multiplier: 1.0, + constant: 20), + + NSLayoutConstraint(item: iconImage, + attribute: .height, + relatedBy: .equal, + toItem: iconImage, + attribute: .width, + multiplier: 1.0, + constant: 0), + + NSLayoutConstraint(item: iconImage, + attribute: .width, + relatedBy: .equal, + toItem: bannerImage, + attribute: .height, + multiplier: 0.45, + constant: 0), + + NSLayoutConstraint(item: iconImage, + attribute: .centerY, + relatedBy: .equal, + toItem: bannerImage, + attribute: .centerY, + multiplier: 1.0, + constant: 0) + ]) + + iconImage.maskCircle() + return bannerImage + } + + private func setIconCover() { + setCover(on: iconImage, fontSize: 15) + setCover(on: bannerImage, fontSize: 22) + } + + private func setCover(on parentView: UIView, fontSize: CGFloat) { + let bannerCover = UIView() + let editableIcon = UILabel() + + editableIcon.font = .awesomeSolid(fontSize: fontSize) + editableIcon.text = "camera" + editableIcon.textColor = .white + + bannerCover.backgroundColor = .black + bannerCover.alpha = 0.35 + + bannerCover.translatesAutoresizingMaskIntoConstraints = false + editableIcon.translatesAutoresizingMaskIntoConstraints = false + + parentView.addSubview(bannerCover) + parentView.addSubview(editableIcon) + + parentView.addConstraints([ + NSLayoutConstraint(item: bannerCover, + attribute: .leading, + relatedBy: .equal, + toItem: parentView, + attribute: .leading, + multiplier: 1.0, + constant: 0), + + NSLayoutConstraint(item: bannerCover, + attribute: .top, + relatedBy: .equal, + toItem: parentView, + attribute: .top, + multiplier: 1.0, + constant: 0), + + NSLayoutConstraint(item: bannerCover, + attribute: .trailing, + relatedBy: .equal, + toItem: parentView, + attribute: .trailing, + multiplier: 1.0, + constant: 0), + + NSLayoutConstraint(item: bannerCover, + attribute: .bottom, + relatedBy: .equal, + toItem: parentView, + attribute: .bottom, + multiplier: 1.0, + constant: 0) + ]) + + parentView.addConstraints([ + NSLayoutConstraint(item: editableIcon, + attribute: .centerX, + relatedBy: .equal, + toItem: parentView, + attribute: .centerX, + multiplier: 1.0, + constant: 0), + + NSLayoutConstraint(item: editableIcon, + attribute: .centerY, + relatedBy: .equal, + toItem: parentView, + attribute: .centerY, + multiplier: 1.0, + constant: 0) + ]) + } + + // MARK: Alert + + private func showImageMenu(_ hasChanged: Bool) { + let panelMenu = PanelMenuViewController() + var menuItems: [PanelMenuViewController.MenuItem] = [.init(title: "カメラから", awesomeIcon: "", order: 0), + .init(title: "アルバムから", awesomeIcon: "", order: 1)] + + if hasChanged { // イメージが一度でも変更されたら、もとに戻すオプションを追加する + menuItems.append(.init(title: "元に戻す", awesomeIcon: "", order: 2)) + } + + panelMenu.setupMenu(items: menuItems) + panelMenu.tapTrigger.asDriver(onErrorDriveWith: Driver.empty()).drive(onNext: { order in // どのメニューがタップされたのか + guard order >= 0 else { return } + panelMenu.dismiss(animated: true, completion: nil) + + switch order { + case 0: // camera + self.pickImage(type: .camera) + case 1: // albam + self.pickImage(type: .photoLibrary) + case 2: + guard hasChanged else { return } + self.resetImage.accept(()) + default: + break + } + }).disposed(by: disposeBag) + + present(panelMenu, animated: true, completion: nil) + } + + // MARK: Delegate + + func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) { + transformAttachment(disposeBag: disposeBag, picker: picker, didFinishPickingMediaWithInfo: info) { _, editedImage, _ in + guard let editedImage = editedImage else { return } + self.selectedImage.accept(editedImage) + } + } +} diff --git a/MissCat/View/Settings/SettingsViewController.swift b/MissCat/View/Settings/SettingsViewController.swift index 9cf8d73..1e65125 100755 --- a/MissCat/View/Settings/SettingsViewController.swift +++ b/MissCat/View/Settings/SettingsViewController.swift @@ -97,12 +97,26 @@ class SettingsViewController: UITableViewController { override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { let index = indexPath.row - - if index == 1 { + if index == 0 { + guard let accountSettings = getViewController(name: "accounts-settings") as? AccountViewController else { return } + + accountSettings.homeViewController = homeViewController + navigationController?.pushViewController(accountSettings, animated: true) + } else if index == 1 { let designSettings = DesignSettingsViewController() designSettings.homeViewController = homeViewController navigationController?.pushViewController(designSettings, animated: true) + } else if index == 3 { + let licenseTable = LicenseTableViewController() + navigationController?.pushViewController(licenseTable, animated: true) } } + + private func getViewController(name: String) -> UIViewController { + let storyboard = UIStoryboard(name: "Main", bundle: nil) + let viewController = storyboard.instantiateViewController(withIdentifier: name) + + return viewController + } } diff --git a/MissCat/ViewModel/Details/ProfileViewModel.swift b/MissCat/ViewModel/Details/ProfileViewModel.swift index befca46..41bd2cf 100644 --- a/MissCat/ViewModel/Details/ProfileViewModel.swift +++ b/MissCat/ViewModel/Details/ProfileViewModel.swift @@ -12,9 +12,22 @@ import RxSwift import UIKit class ProfileViewModel: ViewModelType { + struct Profile { + var bannerUrl: String + var iconUrl: String + var name: String + var username: String + var description: String + var isCat: Bool + } + struct Input { let nameYanagi: YanagiText let introYanagi: YanagiText + + let followButtonTapped: ControlEvent + let backButtonTapped: ControlEvent + let settingsButtonTapped: ControlEvent } struct Output { @@ -29,6 +42,11 @@ class ProfileViewModel: ViewModelType { let followerCount: PublishRelay = .init() let relation: PublishRelay = .init() + let showUnfollowAlertTrigger: PublishRelay = .init() + let showProfileSettingsTrigger: PublishRelay = .init() + let openSettingsTrigger: PublishRelay = .init() + let popViewControllerTrigger: PublishRelay = .init() + var isMe: Bool = false } @@ -42,14 +60,19 @@ class ProfileViewModel: ViewModelType { private var userId: String? private var relation: UserRelationship? + private var emojis: [EmojiModel?] = [] + private var profile: Profile? + + private var disposeBag: DisposeBag private lazy var model = ProfileModel() init(with input: Input, and disposeBag: DisposeBag) { self.input = input + self.disposeBag = disposeBag } func setUserId(_ userId: String, isMe: Bool) { - model.getUser(userId: userId, completion: handleUserInfo) + model.getUser(userId: userId, completion: binding) output.isMe = isMe self.userId = userId @@ -79,67 +102,144 @@ class ProfileViewModel: ViewModelType { } } - private func handleUserInfo(_ user: UserModel?) { + /// プロフィール情報を書き換える + /// MissCat側で編集したプロフィールの差分を適応するときなどに使う + /// - Parameter diff: 差分 + func overrideInfo(_ diff: ChangedProfile) { + guard let profile = profile else { return } + if let icon = diff.icon { + output.iconImage.accept(icon) + } + + if let banner = diff.banner { + output.bannerImage.accept(banner) + } + + if let isCat = diff.isCat { + output.isCat.accept(isCat) + self.profile?.isCat = isCat + } + + if let description = diff.description { + input.introYanagi.resetViewString() + setDesc(description, externalEmojis: emojis) + self.profile?.description = description + } + + if let name = diff.name { + input.nameYanagi.resetViewString() + setName(name: name, username: profile.username, externalEmojis: emojis) + self.profile?.name = name + } + } + + /// binding + /// - Parameter user: UserModel + private func binding(_ user: UserModel?) { guard let user = user else { return } + profile = user.getProfile() + // Notes || FF output.notesCount.accept(user.notesCount?.description ?? "0") output.followCount.accept(user.followingCount?.description ?? "0") output.followerCount.accept(user.followersCount?.description ?? "0") setRelation(targetUserId: user.id) + setIcon(from: user) + setName(name: user.name, username: user.username, externalEmojis: user.emojis) + setDesc(user.description, externalEmojis: user.emojis) + setBanner(from: user) + + output.isCat.accept(user.isCat ?? false) + // tapped event + input.followButtonTapped.subscribe(onNext: { _ in + if !self.output.isMe { + if self.state.isFollowing ?? true { // try フォロー解除 + self.output.showUnfollowAlertTrigger.accept(()) + } else { + self.follow() + } + } else { // 自分のプロフィールの場合 + guard let profile = self.profile else { return } + self.output.showProfileSettingsTrigger.accept(profile) + } + }).disposed(by: disposeBag) + + input.backButtonTapped.subscribe(onNext: { _ in + self.output.popViewControllerTrigger.accept(()) + }).disposed(by: disposeBag) + + input.settingsButtonTapped.subscribe(onNext: { _ in + self.output.openSettingsTrigger.accept(()) + }).disposed(by: disposeBag) + } + + // MARK: Set + + private func setIcon(from user: UserModel) { // Icon Image let host = user.host ?? "" if let username = user.username, let cachediconImage = Cache.shared.getIcon(username: "\(username)@\(host)") { output.iconImage.accept(cachediconImage) } else if let iconImageUrl = user.avatarUrl { - iconImageUrl.toUIImage { image in + _ = iconImageUrl.toUIImage { image in guard let image = image else { return } self.output.iconImage.accept(image) } } + } + + private func setDesc(_ description: String?, externalEmojis: [EmojiModel?]?) { + if let externalEmojis = externalEmojis { emojis += externalEmojis } // overrideInfoに備えてemoji情報を保持 - // Description - if let description = user.description { - let textHex = Theme.shared.currentModel?.colorPattern.hex.text + let textHex = Theme.shared.currentModel?.colorPattern.hex.text + if let description = description { DispatchQueue.main.async { let shaped = description.mfmPreTransform().mfmTransform(font: UIFont(name: "Helvetica", size: 11.0) ?? .systemFont(ofSize: 11.0), - externalEmojis: user.emojis, + externalEmojis: externalEmojis, textHex: textHex) self.output.intro.accept(shaped.attributed ?? .init()) shaped.mfmEngine.renderCustomEmojis(on: self.input.introYanagi) + self.input.introYanagi.renderViewStrings() + self.input.introYanagi.setNeedsLayout() } } else { - output.intro.accept("自己紹介はありません".toAttributedString(family: "Helvetica", size: 11.0) ?? .init()) + output.intro.accept("自己紹介はありません".toAttributedString(family: "Helvetica", size: 11.0, textHex: textHex) ?? .init()) } - + } + + private func setBanner(from user: UserModel) { // Banner Image if let bannerUrl = user.bannerUrl { - bannerUrl.toUIImage { image in + _ = bannerUrl.toUIImage { image in guard let image = image else { return } self.output.bannerImage.accept(image) } } + } + + private func setName(name: String?, username: String?, externalEmojis: [EmojiModel?]?) { + if let externalEmojis = externalEmojis { emojis += externalEmojis } // overrideInfoに備えてemoji情報を保持 - // username / displayName - if let username = user.username { - let shaped = MFMEngine.shapeDisplayName(name: user.name ?? username, - username: username, - emojis: user.emojis, - nameFont: UIFont(name: "Helvetica", size: 13.0), - usernameFont: UIFont(name: "Helvetica", size: 12.0), - nameHex: "#ffffff", - usernameColor: .white) - - output.displayName.accept(shaped.attributed ?? .init()) + if let username = username { DispatchQueue.main.async { + let shaped = MFMEngine.shapeDisplayName(name: name ?? username, + username: username, + emojis: externalEmojis, + nameFont: UIFont(name: "Helvetica", size: 13.0), + usernameFont: UIFont(name: "Helvetica", size: 12.0), + nameHex: "#ffffff", + usernameColor: .white) + + self.output.displayName.accept(shaped.attributed ?? .init()) shaped.mfmEngine.renderCustomEmojis(on: self.input.nameYanagi) + self.input.nameYanagi.renderViewStrings() + self.input.nameYanagi.setNeedsLayout() } } - - output.isCat.accept(user.isCat ?? false) } private func setRelation(targetUserId: String) { @@ -151,3 +251,15 @@ class ProfileViewModel: ViewModelType { } } } + +extension UserModel { + /// UserModelをProfileViewModel.Profileに変更 + fileprivate func getProfile() -> ProfileViewModel.Profile { + return .init(bannerUrl: bannerUrl ?? "", + iconUrl: avatarUrl ?? "", + name: name ?? "", + username: username ?? "", + description: description ?? "", + isCat: isCat ?? false) + } +} diff --git a/MissCat/ViewModel/Main/HomeViewModel.swift b/MissCat/ViewModel/Main/HomeViewModel.swift index 7fcdbbc..9524b7b 100644 --- a/MissCat/ViewModel/Main/HomeViewModel.swift +++ b/MissCat/ViewModel/Main/HomeViewModel.swift @@ -15,7 +15,7 @@ class HomeViewModel: ViewModelType { private let model = HomeModel() - func vote(choice: Int, to noteId: String) { + func vote(choice: [Int], to noteId: String) { model.vote(choice: choice, to: noteId) } diff --git a/MissCat/ViewModel/Main/PostViewModel.swift b/MissCat/ViewModel/Main/PostViewModel.swift index 79493d2..24157d2 100644 --- a/MissCat/ViewModel/Main/PostViewModel.swift +++ b/MissCat/ViewModel/Main/PostViewModel.swift @@ -23,6 +23,7 @@ class PostViewModel: ViewModelType { let innerIcon: PublishRelay = .init() let innerNote: PublishRelay = .init() + let mark: PublishRelay = .init() let attachments: PublishSubject<[PostViewController.AttachmentsSection]> } @@ -165,7 +166,11 @@ class PostViewModel: ViewModelType { func setInnerNote() { guard let target = input.targetNote else { return } + // text + setMark(type: input.type) output.innerNote.accept(target.original?.text ?? "") + + // image if let image = Cache.shared.getIcon(username: "\(target.username)@\(target.hostInstance)") { output.innerIcon.accept(image) } else if let iconImageUrl = target.iconImageUrl, let imageUrl = URL(string: iconImageUrl) { @@ -177,6 +182,17 @@ class PostViewModel: ViewModelType { } } + private func setMark(type: PostViewController.PostType) { + switch type { + case .Reply: + output.mark.accept("chevron-right") + case .CommentRenote: + output.mark.accept("retweet") + default: + break + } + } + // MARK: Privates private func uploadImages(completion: @escaping ([String]) -> Void) { diff --git a/MissCat/ViewModel/Main/TimelineViewModel.swift b/MissCat/ViewModel/Main/TimelineViewModel.swift index 9a41291..a1836ce 100644 --- a/MissCat/ViewModel/Main/TimelineViewModel.swift +++ b/MissCat/ViewModel/Main/TimelineViewModel.swift @@ -23,6 +23,7 @@ class TimelineViewModel: ViewModelType { let userId: String? let listId: String? let query: String? + let lockScroll: Bool let loadLimit: Int } @@ -34,6 +35,8 @@ class TimelineViewModel: ViewModelType { let finishedLoading: PublishRelay = .init() let connectedStream: PublishRelay = .init() + + let reserveLockTrigger: PublishRelay = .init() } class State { @@ -41,32 +44,28 @@ class TimelineViewModel: ViewModelType { var cellCount: Int var renoteeCellCount: Int var isLoading: Bool - var reloadTopModelId: String? // untilLoadした分のセルのうち、最上端にある投稿のid var cellCompleted: Bool { // 準備した分のセルがすべて表示されたかどうか return (cellCount - renoteeCellCount) % loadLimit == 0 } - init(cellCount: Int, renoteeCellCount: Int, isLoading: Bool, loadLimit: Int, reloadTopModelId: String? = nil) { + init(cellCount: Int, renoteeCellCount: Int, isLoading: Bool, loadLimit: Int) { self.cellCount = cellCount self.renoteeCellCount = renoteeCellCount self.isLoading = isLoading self.loadLimit = loadLimit - self.reloadTopModelId = reloadTopModelId } } private let input: Input let output: Output = .init() - private var reloadTopModelId: String? private var _isLoading: Bool = false var state: State { return .init(cellCount: { cellsModel.count }(), renoteeCellCount: { cellsModel.filter { $0.isRenoteeCell }.count }(), isLoading: _isLoading, - loadLimit: input.loadLimit, - reloadTopModelId: reloadTopModelId) + loadLimit: input.loadLimit) } // MARK: PublishSubject @@ -109,11 +108,12 @@ class TimelineViewModel: ViewModelType { // タイムラインをロードする loadNotes().subscribe(onError: { error in if let error = error as? TimelineModel.NotesLoadingError, error == .NotesEmpty, self.input.type == .Home { - self.initialPost() + self.initialFollow() } print(error) }, onCompleted: { + self.output.lockTableScroll.accept(self.input.lockScroll) // ロックの初期状態を決める DispatchQueue.main.async { self.output.finishedLoading.accept(true) @@ -156,25 +156,10 @@ class TimelineViewModel: ViewModelType { updateNotes(new: cellsModel) } - private func initialPost() { - guard initialNoteCount < 2 else { return } - MisskeyKit.notes.createNote(text: "MissCatからアカウントを作成しました。") { note, error in - guard note != nil, error == nil else { // 失敗した場合は何回か再帰 - self.initialNoteCount += 1 - self.initialPost() - return - } - DispatchQueue.main.async { - self.initialNoteCount = 0 - self.initialFollow() // 次にユーザーをフォローしておく - } - } - } - private func initialFollow() { guard initialNoteCount < 2 else { return } - MisskeyKit.users.follow(userId: "7ze0f2goa7") { user, error in - guard user != nil, error == nil else { // 失敗した場合は何回か再帰 + MisskeyKit.users.follow(userId: "7ze0f2goa7") { _, error in + guard error == nil else { // 失敗した場合は何回か再帰 self.initialNoteCount += 1 self.initialFollow() return @@ -287,7 +272,10 @@ class TimelineViewModel: ViewModelType { } return loadNotes(untilId: untilId).do(onCompleted: { - self.output.lockTableScroll.accept(false) // スクロールのロックを解除 + if self.input.lockScroll { + self.output.lockTableScroll.accept(false) // スクロールのロックを解除 + self.output.reserveLockTrigger.accept(()) + } self.updateNotes(new: self.cellsModel) }) } @@ -308,9 +296,6 @@ class TimelineViewModel: ViewModelType { return model.loadNotes(with: option).do(onNext: { cellModel in self.cellsModel.append(cellModel) - if untilId != nil, self.reloadTopModelId == nil { // reloadTopModelIdを記憶 - self.reloadTopModelId = cellModel.identity - } }, onCompleted: { self.initialNoteIds = self.model.initialNoteIds self._isLoading = false diff --git a/MissCat/ViewModel/Reusable/NoteCell/NoteCellViewModel.swift b/MissCat/ViewModel/Reusable/NoteCell/NoteCellViewModel.swift index 25b6d7b..6ecda04 100644 --- a/MissCat/ViewModel/Reusable/NoteCell/NoteCellViewModel.swift +++ b/MissCat/ViewModel/Reusable/NoteCell/NoteCellViewModel.swift @@ -15,7 +15,7 @@ class NoteCellViewModel: ViewModelType { // MARK: I/O struct Input { - let cellModel: NoteCell.Model + var cellModel: NoteCell.Model let isDetailMode: Bool // Modelに渡さなければならないので看過 @@ -136,26 +136,6 @@ class NoteCellViewModel: ViewModelType { setFooter(from: item) } - func setReactionCell(with item: NoteCell.Reaction, to reactionCell: ReactionCell) -> ReactionCell { - guard let rawEmoji = item.rawEmoji else { return reactionCell } - - if let customEmojiUrl = item.url { - reactionCell.setup(noteId: item.noteId, - count: item.count, - customEmoji: customEmojiUrl, - isMyReaction: item.isMyReaction, - rawReaction: rawEmoji) - } else { - reactionCell.setup(noteId: item.noteId, - count: item.count, - rawDefaultEmoji: rawEmoji, - isMyReaction: item.isMyReaction, - rawReaction: rawEmoji) - } - - return reactionCell - } - private func setColor() { output.backgroundColor.accept(properBackgroundColor) output.separatorBackgroundColor.accept(Theme.shared.currentModel?.colorPattern.ui.sub2 ?? .lightGray) @@ -304,6 +284,19 @@ class NoteCellViewModel: ViewModelType { model.cancelReaction(noteId: noteId) } + func updateVote(choices: [Int]) { + guard let poll = input.cellModel.poll, + let currentChoices = poll.choices else { return } + + choices.forEach { choiceIndex in + guard choiceIndex >= 0, choiceIndex < currentChoices.count else { return } + let currentVotes = currentChoices[choiceIndex]?.votes ?? 0 + + input.cellModel.poll?.choices?[choiceIndex]?.votes = currentVotes + 1 + input.cellModel.poll?.choices?[choiceIndex]?.isVoted = true + } + } + private func updateReactions(new: [NoteCell.Reaction]) { updateReactions(new: [NoteCell.Reaction.Section(items: new)]) } diff --git a/MissCat/ViewModel/Reusable/Notification/NotificationCellViewModel.swift b/MissCat/ViewModel/Reusable/Notification/NotificationCellViewModel.swift index 49fff37..9bd472f 100644 --- a/MissCat/ViewModel/Reusable/Notification/NotificationCellViewModel.swift +++ b/MissCat/ViewModel/Reusable/Notification/NotificationCellViewModel.swift @@ -150,7 +150,7 @@ class NotificationCellViewModel: ViewModelType { // reaction else if let reaction = item.reaction { - output.typeIconString.accept("fire-alt") + output.typeIconString.accept("heart") output.typeString.accept("Reaction") output.needEmoji.accept(true) output.typeIconColor.accept(reactionIconColor) diff --git a/MissCat/ViewModel/Settings/ProfileSettingsViewModel.swift b/MissCat/ViewModel/Settings/ProfileSettingsViewModel.swift new file mode 100644 index 0000000..bb1e97a --- /dev/null +++ b/MissCat/ViewModel/Settings/ProfileSettingsViewModel.swift @@ -0,0 +1,181 @@ +// +// ProfileSettingsViewModel.swift +// MissCat +// +// Created by Yuiga Wada on 2020/05/14. +// Copyright © 2020 Yuiga Wada. All rights reserved. +// + +import Eureka +import Foundation +import RxCocoa +import RxSwift +import UIKit + +// プロフィールの差分を表す +class ChangedProfile { + var icon: UIImage? + var banner: UIImage? + + var name: String? + var description: String? + var isCat: Bool? + + var hasChanged: Bool { + let emptyIcon = icon == nil + let emptyBanner = banner == nil + let emptyName = name == nil + let emptyDesc = description == nil + let emptyCat = isCat == nil + return !(emptyIcon && emptyBanner && emptyName && emptyDesc && emptyCat) + } +} + +class ProfileSettingsViewModel: ViewModelType { + enum ImageTarget { + case icon + case banner + } + + struct Input { + let needLoadIcon: Bool + let needLoadBanner: Bool + + let iconUrl: String? + let bannerUrl: String? + + let currentName: String + let currentDescription: String + let currentCatState: Bool + + let rxName: ControlProperty + let rxDesc: ControlProperty + let rxCat: ControlProperty + + let rightNavButtonTapped: ControlEvent + let iconTapped: ControlEvent + let bannerTapped: ControlEvent + let selectedImage: Observable + let resetImage: Observable + let overrideInfoTrigger: PublishRelay + } + + struct Output { + let icon: PublishRelay = .init() + let banner: PublishRelay = .init() + + let name: PublishRelay = .init() + let description: PublishRelay = .init() + let isCat: PublishRelay = .init() + + let showSaveAlertTrigger: PublishRelay = .init() + let pickImageTrigger: PublishRelay = .init() // Bool: hasChanged + let popViewControllerTrigger: PublishRelay = .init() + } + + class State { + var hasEdited: Bool = false + var currentTarget: ImageTarget? + var changed: ChangedProfile = .init() + } + + private let model = ProfileSettingsModel() + private let input: Input + let output: Output = .init() + let state: State = .init() + + private let disposeBag: DisposeBag + + init(with input: Input, and disposeBag: DisposeBag) { + self.input = input + self.disposeBag = disposeBag + } + + func transform() { + // image + if input.needLoadIcon { setDefaultIcon() } + + if input.needLoadBanner { setDefaultBanner() } + + // input + input.rxName.subscribe(onNext: { name in + let hasChanged = name != self.input.currentName + self.state.changed.name = hasChanged ? name : nil + }).disposed(by: disposeBag) + + input.rxDesc.subscribe(onNext: { desc in + let hasChanged = desc != self.input.currentDescription + self.state.changed.description = hasChanged ? desc : nil + }).disposed(by: disposeBag) + + input.rxCat.subscribe(onNext: { isCat in + let hasChanged = isCat != self.input.currentCatState + self.state.changed.isCat = hasChanged ? isCat : nil + }).disposed(by: disposeBag) + + // tap event + input.rightNavButtonTapped.subscribe(onNext: { _ in + self.model.save(diff: self.state.changed) + self.output.popViewControllerTrigger.accept(()) + + if self.state.changed.hasChanged { + self.input.overrideInfoTrigger.accept(self.state.changed) + } + }).disposed(by: disposeBag) + + input.iconTapped.subscribe(onNext: { _ in + self.state.currentTarget = .icon + let hasChanged = self.state.changed.icon != nil + self.output.pickImageTrigger.accept(hasChanged) + }).disposed(by: disposeBag) + + input.bannerTapped.subscribe(onNext: { _ in + self.state.currentTarget = .banner + let hasChanged = self.state.changed.banner != nil + self.output.pickImageTrigger.accept(hasChanged) + }).disposed(by: disposeBag) + + // trigger + input.selectedImage.subscribe(onNext: { image in + guard let target = self.state.currentTarget else { return } + switch target { + case .icon: + self.output.icon.accept(image) + self.state.changed.icon = image + case .banner: + self.output.banner.accept(image) + self.state.changed.banner = image + } + + self.state.currentTarget = nil + }).disposed(by: disposeBag) + + input.resetImage.subscribe(onNext: { _ in + guard let target = self.state.currentTarget else { return } + switch target { + case .icon: + self.setDefaultIcon() + self.state.changed.icon = nil + case .banner: + self.setDefaultBanner() + self.state.changed.banner = nil + } + + self.state.currentTarget = nil + }).disposed(by: disposeBag) + } + + private func setDefaultBanner() { + _ = input.bannerUrl?.toUIImage { image in + guard let image = image else { return } + self.output.banner.accept(image) + } + } + + private func setDefaultIcon() { + _ = input.iconUrl?.toUIImage { image in + guard let image = image else { return } + self.output.icon.accept(image) + } + } +} diff --git a/Podfile b/Podfile index b20af3f..9bc3dc2 100755 --- a/Podfile +++ b/Podfile @@ -10,8 +10,7 @@ pod 'PolioPager' pod 'SwiftFormat/CLI' pod 'Starscream','3.1.1' -pod 'AWSSNS', :binary => true -pod 'AWSCognito', :binary => true +pod 'Firebase/Messaging', :binary => true pod 'RxSwift', '~> 5', :binary => true pod 'RxCocoa', '~> 5', :binary => true pod 'RxDataSources', :binary => true diff --git a/Podfile.lock b/Podfile.lock index 87060ca..dcac3b0 100755 --- a/Podfile.lock +++ b/Podfile.lock @@ -2,11 +2,6 @@ PODS: - Agrume (5.6.7): - SwiftyGif - APNGKit (1.1.3) - - AWSCognito (2.13.1): - - AWSCore (= 2.13.1) - - AWSCore (2.13.1) - - AWSSNS (2.13.1): - - AWSCore (= 2.13.1) - Cache (5.2.0) - ChromaColorPicker (2.0.2) - CocoaLumberjack (3.6.1): @@ -14,17 +9,82 @@ PODS: - CocoaLumberjack/Core (3.6.1) - Differentiator (4.0.1) - Eureka (5.2.1) + - Firebase/CoreOnly (6.23.0): + - FirebaseCore (= 6.6.7) + - Firebase/Messaging (6.23.0): + - Firebase/CoreOnly + - FirebaseMessaging (~> 4.3.1) + - FirebaseAnalyticsInterop (1.5.0) + - FirebaseCore (6.6.7): + - FirebaseCoreDiagnostics (~> 1.2) + - FirebaseCoreDiagnosticsInterop (~> 1.2) + - GoogleUtilities/Environment (~> 6.5) + - GoogleUtilities/Logger (~> 6.5) + - FirebaseCoreDiagnostics (1.2.4): + - FirebaseCoreDiagnosticsInterop (~> 1.2) + - GoogleDataTransportCCTSupport (~> 3.0) + - GoogleUtilities/Environment (~> 6.5) + - GoogleUtilities/Logger (~> 6.5) + - nanopb (~> 0.3.901) + - FirebaseCoreDiagnosticsInterop (1.2.0) + - FirebaseInstallations (1.2.0): + - FirebaseCore (~> 6.6) + - GoogleUtilities/Environment (~> 6.6) + - GoogleUtilities/UserDefaults (~> 6.6) + - PromisesObjC (~> 1.2) + - FirebaseInstanceID (4.3.4): + - FirebaseCore (~> 6.6) + - FirebaseInstallations (~> 1.0) + - GoogleUtilities/Environment (~> 6.5) + - GoogleUtilities/UserDefaults (~> 6.5) + - FirebaseMessaging (4.3.1): + - FirebaseAnalyticsInterop (~> 1.5) + - FirebaseCore (~> 6.6) + - FirebaseInstanceID (~> 4.3) + - GoogleUtilities/AppDelegateSwizzler (~> 6.5) + - GoogleUtilities/Environment (~> 6.5) + - GoogleUtilities/Reachability (~> 6.5) + - GoogleUtilities/UserDefaults (~> 6.5) + - Protobuf (>= 3.9.2, ~> 3.9) - FloatingPanel (1.7.4) - Gifu (3.2.0) + - GoogleDataTransport (6.0.0) + - GoogleDataTransportCCTSupport (3.0.0): + - GoogleDataTransport (~> 6.0) + - nanopb (~> 0.3.901) + - GoogleUtilities/AppDelegateSwizzler (6.6.0): + - GoogleUtilities/Environment + - GoogleUtilities/Logger + - GoogleUtilities/Network + - GoogleUtilities/Environment (6.6.0): + - PromisesObjC (~> 1.2) + - GoogleUtilities/Logger (6.6.0): + - GoogleUtilities/Environment + - GoogleUtilities/Network (6.6.0): + - GoogleUtilities/Logger + - "GoogleUtilities/NSData+zlib" + - GoogleUtilities/Reachability + - "GoogleUtilities/NSData+zlib (6.6.0)" + - GoogleUtilities/Reachability (6.6.0): + - GoogleUtilities/Logger + - GoogleUtilities/UserDefaults (6.6.0): + - GoogleUtilities/Logger - InputBarAccessoryView (4.3.2): - InputBarAccessoryView/Core (= 4.3.2) - InputBarAccessoryView/Core (4.3.2) - iOSPhotoEditor (1.0.0) - MessageKit (3.1.0): - InputBarAccessoryView (~> 4.3.0) - - MisskeyKit (2.2): + - MisskeyKit (2.6.1): - Starscream (= 3.1.1) + - nanopb (0.3.9011): + - nanopb/decode (= 0.3.9011) + - nanopb/encode (= 0.3.9011) + - nanopb/decode (0.3.9011) + - nanopb/encode (0.3.9011) - PolioPager (2.5) + - PromisesObjC (1.2.8) + - Protobuf (3.11.4) - RxCocoa (5.1.1): - RxRelay (~> 5) - RxSwift (~> 5) @@ -54,11 +114,10 @@ PODS: DEPENDENCIES: - Agrume - APNGKit (~> 1.0) - - AWSCognito - - AWSSNS - Cache - ChromaColorPicker - Eureka + - Firebase/Messaging - FloatingPanel - Gifu - iOSPhotoEditor @@ -82,21 +141,32 @@ SPEC REPOS: https://github.com/cocoapods/specs.git: - Agrume - APNGKit - - AWSCognito - - AWSCore - - AWSSNS - Cache - ChromaColorPicker - CocoaLumberjack - Differentiator - Eureka + - Firebase + - FirebaseAnalyticsInterop + - FirebaseCore + - FirebaseCoreDiagnostics + - FirebaseCoreDiagnosticsInterop + - FirebaseInstallations + - FirebaseInstanceID + - FirebaseMessaging - FloatingPanel - Gifu + - GoogleDataTransport + - GoogleDataTransportCCTSupport + - GoogleUtilities - InputBarAccessoryView - iOSPhotoEditor - MessageKit - MisskeyKit + - nanopb - PolioPager + - PromisesObjC + - Protobuf - RxCocoa - RxDataSources - RxRelay @@ -122,21 +192,32 @@ CHECKOUT OPTIONS: SPEC CHECKSUMS: Agrume: 1440351654f1d8669f240b5e7f1c76bafa15cd02 APNGKit: fbf5270804a2861f451ca27cf1cce0893b13fbf5 - AWSCognito: c6311e35fe74c9ee490fff3e03548afae629dc5c - AWSCore: ec394695d054fa4136b20ccd22d8901faea5de04 - AWSSNS: e6b9b684aec01af0f161670d3e1ebfe445b80426 Cache: 807c5d86d01a177f06ede9865add3aea269bbfd4 ChromaColorPicker: 307585f2dc23d4d72f24cad320c1e08825cb5c09 CocoaLumberjack: b17ae15142558d08bbacf69775fa10c4abbebcc9 Differentiator: 886080237d9f87f322641dedbc5be257061b0602 Eureka: c883105488e05bc65539f583246ecf9657cabbfe + Firebase: 585ae467b3edda6a5444e788fda6888f024d8d6f + FirebaseAnalyticsInterop: 3f86269c38ae41f47afeb43ebf32a001f58fcdae + FirebaseCore: a2788a0d5f6c1dff17b8f79b4a73654a8d4bfdbd + FirebaseCoreDiagnostics: b59c024493a409f8aecba02c99928d0d8431d159 + FirebaseCoreDiagnosticsInterop: 296e2c5f5314500a850ad0b83e9e7c10b011a850 + FirebaseInstallations: 2119fb3e46b0a88bfdbf12562f855ee3252462fa + FirebaseInstanceID: cef67c4967c7cecb56ea65d8acbb4834825c587b + FirebaseMessaging: 828e66eb357a893e3cebd9ee0f6bc1941447cc94 FloatingPanel: 3c9d0e30fe350e1613157557769d2ec97f76b96b Gifu: 7bcb6427457d85e0b4dff5a84ec5947ac19a93ea + GoogleDataTransport: 061fe7d9b476710e3cd8ea51e8e07d8b67c2b420 + GoogleDataTransportCCTSupport: 0f39025e8cf51f168711bd3fb773938d7e62ddb5 + GoogleUtilities: 39530bc0ad980530298e9c4af8549e991fd033b1 InputBarAccessoryView: 7985d418040a05fe894bd4b8328dd43ab35517c3 iOSPhotoEditor: 2951f35246a31dcb428b6292248a9c065582d03e MessageKit: 3beb578737a5aa2bba25cc27c7b6d6faa09af5a7 - MisskeyKit: 40c24228b79e21c5d681199697a6402d57eedef6 + MisskeyKit: ac0f495360000dde777e5e66c67b786abdcda71f + nanopb: 18003b5e52dab79db540fe93fe9579f399bd1ccd PolioPager: ccb4e382fef0c3c0ee52bdec3d9e9794eafa510a + PromisesObjC: c119f3cd559f50b7ae681fa59dc1acd19173b7e6 + Protobuf: 176220c526ad8bd09ab1fb40a978eac3fef665f7 RxCocoa: 32065309a38d29b5b0db858819b5bf9ef038b601 RxDataSources: efee07fa4de48477eca0a4611e6d11e2da9c1114 RxRelay: d77f7d771495f43c556cbc43eebd1bb54d01e8e9 @@ -150,6 +231,6 @@ SPEC CHECKSUMS: XLActionController: f1f2226d2c6629db197d4a41f2da907085a4cb9d XLPagerTabStrip: 61c57fd61f611ee5f01ff1495ad6fbee8bf496c5 -PODFILE CHECKSUM: 578652b0615d6f8b1f5caf47fb4079f5d54dec43 +PODFILE CHECKSUM: 6c149a53613e37e5f322a3c9a1655387440cb835 COCOAPODS: 1.7.1 diff --git a/python_utls/COMBINED_LICENSE b/python_utls/COMBINED_LICENSE index be2559e..f74e668 100644 --- a/python_utls/COMBINED_LICENSE +++ b/python_utls/COMBINED_LICENSE @@ -1,89 +1,5 @@ -・Agrume by JanGorman
・Down by AssistoLab
・FloatingPanel by SCENEE
・Gifu by kaishin
・MisskeyKit by YuigaWada
・PolioPager by YuigaWada
・photo-editor by eventtus
・RxCocoa by RxSwiftCommunity
・RxDataSources by RxSwiftCommunity
・RxSwift by ReactiveX
・SVGKit by gupuru
・SkeletonView by Juanpe
・Starscream by daltoniam
・XLPagerTabStrip by xmartlabs




Agrume


The MIT License (MIT) - -Copyright (c) 2015 Jan Gorman - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. -



Down


The MIT License (MIT) - -Copyright (c) 2014 kevin-hirsch - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE.



FloatingPanel


MIT License - -Copyright (c) 2018 Shin Yamamoto - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE.



Gifu


The MIT License (MIT) - -Copyright (c) 2014-2018 Reda Lemeden. - -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the "Software"), to deal in -the Software without restriction, including without limitation the rights to -use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software is furnished to do so, -subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -The name and characters used in the demo of this software are property of their -respective owners. - -



MisskeyKit


MIT License +["MisskeyKit": """ +MIT License Copyright (c) 2019 YuigaWada @@ -104,70 +20,12 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -



PolioPager


MIT License -Copyright (c) 2019 YuigaWada - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. -



photo-editor


MIT License +""", +"YanagiText": """ +MIT License -Copyright (c) 2017 Mohamed Hamed - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. -



RxCocoa


Copyright (c) 2017 - present RxSwiftCommunity - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. -



RxDataSources


MIT License - -Copyright (c) 2017 RxSwift Community +Copyright (c) 2019 YuigaWada Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -186,17 +44,12 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -



RxSwift


**The MIT License** -**Copyright © 2015 Krunoslav Zaher** -**All rights reserved.** - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.



SkeletonView


The MIT License (MIT) +""", +"PolioPager": """ +MIT License -Copyright (c) 2017 Juanpe Catalán +Copyright (c) 2019 YuigaWada Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -216,7 +69,9 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -



Starscream


Apache License +""", +"Starscream": """ + Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ @@ -391,7 +246,190 @@ SOFTWARE. of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability.



XLPagerTabStrip


The MIT License (MIT) + of your accepting any such warranty or additional liability. +""", +"RxSwift": """ +**The MIT License** +**Copyright © 2015 Krunoslav Zaher** +**All rights reserved.** + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +""", +"RxCocoa": """ +Copyright (c) 2017 - present RxSwiftCommunity + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +""", +"RxDataSources": """ +MIT License + +Copyright (c) 2017 RxSwift Community + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +""", +"Agrume": """ +The MIT License (MIT) + +Copyright (c) 2015 Jan Gorman + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +""", +"SkeletonView": """ +The MIT License (MIT) + +Copyright (c) 2017 Juanpe Catalán + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +""", +"FloatingPanel": """ +MIT License + +Copyright (c) 2018 Shin Yamamoto + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""", +"Gifu": """ +The MIT License (MIT) + +Copyright (c) 2014-2018 Reda Lemeden. + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +The name and characters used in the demo of this software are property of their +respective owners. + + +""", +"photo-editor": """ +MIT License + +Copyright (c) 2017 Mohamed Hamed + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +""", +"XLPagerTabStrip": """ +The MIT License (MIT) Copyright (c) 2019 Xmartlabs SRL @@ -412,4 +450,173 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -



\ No newline at end of file + +""", +"APNGKit": """ +The MIT License (MIT) + +Copyright (c) 2015 Wei Wang + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +""", +"SwiftLinkPreview": """ +The MIT License (MIT) + +Copyright (c) 2016 Leonardo Cardoso + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""", +"Cache": """ +Licensed under the **MIT** license + +> Copyright (c) 2015 Hyper Interaktiv AS +> +> Permission is hereby granted, free of charge, to any person obtaining +> a copy of this software and associated documentation files (the +> "Software"), to deal in the Software without restriction, including +> without limitation the rights to use, copy, modify, merge, publish, +> distribute, sublicense, and/or sell copies of the Software, and to +> permit persons to whom the Software is furnished to do so, subject to +> the following conditions: +> +> The above copyright notice and this permission notice shall be +> included in all copies or substantial portions of the Software. +> +> THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +> EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +> MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +> IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +> CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +> TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +> SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +""", +"MessageKit": """ +MIT License + +Copyright (c) 2017-2019 MessageKit + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +""", +"Eureka": """ +The MIT License (MIT) + +Copyright (c) 2019 XMARTLABS + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +""", +"ChromaColorPicker": """ +MIT License + +Copyright (c) 2016 Jonathan Cardasis + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +""", +"XLActionController": """ +The MIT License (MIT) + +Copyright (c) 2019 XMARTLABS (http://xmartlabs.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +"""] \ No newline at end of file diff --git a/python_utls/library_list.txt b/python_utls/library_list.txt index e959a8f..a0fb3c9 100644 --- a/python_utls/library_list.txt +++ b/python_utls/library_list.txt @@ -1,14 +1,23 @@ -Agrume -FloatingPanel -Gifu MisskeyKit -PolioPager YanagiText -photo-editor +PolioPager +AWSSNS +AWSCognito +Starscream +RxSwift RxCocoa RxDataSources -RxSwift +Agrume SVGKit SkeletonView -Starscream -XLPagerTabStrip \ No newline at end of file +FloatingPanel +Gifu +photo-editor +XLPagerTabStrip +APNGKit +SwiftLinkPreview +Cache +MessageKit +Eureka +ChromaColorPicker +XLActionController \ No newline at end of file diff --git a/python_utls/licenser.py b/python_utls/licenser.py index c6ef1ba..5f6b3d6 100644 --- a/python_utls/licenser.py +++ b/python_utls/licenser.py @@ -3,9 +3,8 @@ g = Github("username", "password") -output = "" +output = "[" libraries = [] -header = "" with open('library_list.txt', 'r') as f: line = f.readline() while line: @@ -18,7 +17,6 @@ repositories = g.search_repositories(query=target_library+' language:swift') org = repositories[0].full_name.split('/')[0] - header += "・{} by {}
".format(target_library,org) for tail in ["",".md"]: #LICENSE / LICENSE.mdどっちかのパターンがあるっぽい license_url = 'https://raw.githubusercontent.com/' + repositories[0].full_name +'/master/LICENSE' + tail print(repositories) @@ -27,10 +25,11 @@ req = requests.get(license_url) if req.status_code == 200: raw_license = req.text - output += "

{}


{}".format(target_library, raw_license) + '
'*4 + triple_quote = '"' * 3 + output += "\"{}\": {}\n{}\n{},\n".format(target_library, triple_quote, raw_license, triple_quote) # else: # print("**ERROR!**≠\nstatus code: not 200⇛",license_url) -output = header + "
"*4 + output +output = output[:-2] + "]" # 最後のコロンを無視 with open('COMBINED_LICENSE', mode='w') as f: f.write(output)